Skip to content

Commit

Permalink
Merge branch 'main' into mis
Browse files Browse the repository at this point in the history
  • Loading branch information
DLWoodruff authored Apr 26, 2024
2 parents 0f26a0a + 6393961 commit 1b638e6
Show file tree
Hide file tree
Showing 159 changed files with 2,076 additions and 1,915 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/test_branches.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ jobs:
python-version: '3.10'
- name: Black Formatting Check
run: |
pip install black
# Note v24.4.1 fails due to a bug in the parser
pip install 'black!=24.4.1'
black . -S -C --check --diff --exclude examples/pyomobook/python-ch/BadIndent.py
- name: Spell Check
uses: crate-ci/typos@master
Expand Down
12 changes: 10 additions & 2 deletions .github/workflows/test_pr_and_main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ on:
pull_request:
branches:
- main
types:
- opened
- reopened
- synchronize
- ready_for_review
workflow_dispatch:
inputs:
git-ref:
Expand Down Expand Up @@ -34,6 +39,8 @@ jobs:
lint:
name: lint/style-and-typos
runs-on: ubuntu-latest
if: |
contains(github.event.pull_request.title, '[WIP]') != true && !github.event.pull_request.draft
steps:
- name: Checkout Pyomo source
uses: actions/checkout@v4
Expand All @@ -43,7 +50,8 @@ jobs:
python-version: '3.10'
- name: Black Formatting Check
run: |
pip install black
# Note v24.4.1 fails due to a bug in the parser
pip install 'black!=24.4.1'
black . -S -C --check --diff --exclude examples/pyomobook/python-ch/BadIndent.py
- name: Spell Check
uses: crate-ci/typos@master
Expand Down Expand Up @@ -733,7 +741,7 @@ jobs:
cover:
name: process-coverage-${{ matrix.TARGET }}
needs: build
if: always() # run even if a build job fails
if: success() || failure() # run even if a build job fails, but not if cancelled
runs-on: ${{ matrix.os }}
timeout-minutes: 10
strategy:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Backward Compatibility
======================

While PyNumero is a third-party contribution to Pyomo, we intend to maintain
the stability of its core functionality. The core functionality of PyNumero
consists of:

1. The ``NLP`` API and ``PyomoNLP`` implementation of this API
2. HSL and MUMPS linear solver interfaces
3. ``BlockVector`` and ``BlockMatrix`` classes
4. CyIpopt and SciPy solver interfaces

Other parts of PyNumero, such as ``ExternalGreyBoxBlock`` and
``ImplicitFunctionSolver``, are experimental and subject to change without notice.
1 change: 1 addition & 0 deletions doc/OnlineDocs/contributed_packages/pynumero/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ PyNumero. For more details, see the API documentation (:ref:`pynumero_api`).
installation.rst
tutorial.rst
api.rst
backward_compatibility.rst


Developers
Expand Down
13 changes: 10 additions & 3 deletions doc/OnlineDocs/contribution_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ at least 70% coverage of the lines modified in the PR and prefer coverage
closer to 90%. We also require that all tests pass before a PR will be
merged.

.. note::
If you are having issues getting tests to pass on your Pull Request,
please tag any of the core developers to ask for help.

The Pyomo main branch provides a Github Actions workflow (configured
in the ``.github/`` directory) that will test any changes pushed to
a branch with a subset of the complete test harness that includes
Expand All @@ -82,13 +86,16 @@ This will enable the tests to run automatically with each push to your fork.

At any point in the development cycle, a "work in progress" pull request
may be opened by including '[WIP]' at the beginning of the PR
title. This allows your code changes to be tested by the full suite of
Pyomo's automatic
testing infrastructure. Any pull requests marked '[WIP]' will not be
title. Any pull requests marked '[WIP]' or draft will not be
reviewed or merged by the core development team. However, any
'[WIP]' pull request left open for an extended period of time without
active development may be marked 'stale' and closed.

.. note::
Draft and WIP Pull Requests will **NOT** trigger tests. This is an effort to
reduce our CI backlog. Please make use of the provided
branch test suite for evaluating / testing draft functionality.

Python Version Support
++++++++++++++++++++++

Expand Down
7 changes: 7 additions & 0 deletions doc/OnlineDocs/library_reference/common/enums.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

pyomo.common.enums
==================

.. automodule:: pyomo.common.enums
:members:
:member-order: bysource
1 change: 1 addition & 0 deletions doc/OnlineDocs/library_reference/common/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ or rely on any other parts of Pyomo.
config.rst
dependencies.rst
deprecation.rst
enums.rst
errors.rst
fileutils.rst
formatting.rst
Expand Down
2 changes: 1 addition & 1 deletion examples/pyomobook/pyomo-components-ch/obj_declaration.txt
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ Model unknown
None
value
x[Q] + 2*x[R]
1
minimize
6.5
Model unknown

Expand Down
162 changes: 162 additions & 0 deletions pyomo/common/enums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# ___________________________________________________________________________
#
# Pyomo: Python Optimization Modeling Objects
# Copyright (c) 2008-2024
# National Technology and Engineering Solutions of Sandia, LLC
# Under the terms of Contract DE-NA0003525 with National Technology and
# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
# rights in this software.
# This software is distributed under the 3-clause BSD License.
# ___________________________________________________________________________

"""This module provides standard :py:class:`enum.Enum` definitions used in
Pyomo, along with additional utilities for working with custom Enums
Utilities:
.. autosummary::
ExtendedEnumType
Standard Enums:
.. autosummary::
ObjectiveSense
"""

import enum
import itertools
import sys

if sys.version_info[:2] < (3, 11):
_EnumType = enum.EnumMeta
else:
_EnumType = enum.EnumType


class ExtendedEnumType(_EnumType):
"""Metaclass for creating an :py:class:`enum.Enum` that extends another Enum
In general, :py:class:`enum.Enum` classes are not extensible: that is,
they are frozen when defined and cannot be the base class of another
Enum. This Metaclass provides a workaround for creating a new Enum
that extends an existing enum. Members in the base Enum are all
present as members on the extended enum.
Example
-------
.. testcode::
:hide:
import enum
from pyomo.common.enums import ExtendedEnumType
.. testcode::
class ObjectiveSense(enum.IntEnum):
minimize = 1
maximize = -1
class ProblemSense(enum.IntEnum, metaclass=ExtendedEnumType):
__base_enum__ = ObjectiveSense
unknown = 0
.. doctest::
>>> list(ProblemSense)
[<ProblemSense.unknown: 0>, <ObjectiveSense.minimize: 1>, <ObjectiveSense.maximize: -1>]
>>> ProblemSense.unknown
<ProblemSense.unknown: 0>
>>> ProblemSense.maximize
<ObjectiveSense.maximize: -1>
>>> ProblemSense(0)
<ProblemSense.unknown: 0>
>>> ProblemSense(1)
<ObjectiveSense.minimize: 1>
>>> ProblemSense('unknown')
<ProblemSense.unknown: 0>
>>> ProblemSense('maximize')
<ObjectiveSense.maximize: -1>
>>> hasattr(ProblemSense, 'minimize')
True
>>> ProblemSense.minimize is ObjectiveSense.minimize
True
>>> ProblemSense.minimize in ProblemSense
True
"""

def __getattr__(cls, attr):
try:
return getattr(cls.__base_enum__, attr)
except:
return super().__getattr__(attr)

def __iter__(cls):
# The members of this Enum are the base enum members joined with
# the local members
return itertools.chain(super().__iter__(), cls.__base_enum__.__iter__())

def __contains__(cls, member):
# This enum "contains" both its local members and the members in
# the __base_enum__ (necessary for good auto-enum[sphinx] docs)
return super().__contains__(member) or member in cls.__base_enum__

def __instancecheck__(cls, instance):
if cls.__subclasscheck__(type(instance)):
return True
# Also pretend that members of the extended enum are subclasses
# of the __base_enum__. This is needed to circumvent error
# checking in enum.__new__ (e.g., for `ProblemSense('minimize')`)
return cls.__base_enum__.__subclasscheck__(type(instance))

def _missing_(cls, value):
# Support attribute lookup by value or name
for attr in ('value', 'name'):
for member in cls:
if getattr(member, attr) == value:
return member
return None

def __new__(metacls, cls, bases, classdict, **kwds):
# Support lookup by name - but only if the new Enum doesn't
# specify its own implementation of _missing_
if '_missing_' not in classdict:
classdict['_missing_'] = classmethod(ExtendedEnumType._missing_)
return super().__new__(metacls, cls, bases, classdict, **kwds)


class ObjectiveSense(enum.IntEnum):
"""Flag indicating if an objective is minimizing (1) or maximizing (-1).
While the numeric values are arbitrary, there are parts of Pyomo
that rely on this particular choice of value. These values are also
consistent with some solvers (notably Gurobi).
"""

minimize = 1
maximize = -1

# Overloading __str__ is needed to match the behavior of the old
# pyutilib.enum class (removed June 2020). There are spots in the
# code base that expect the string representation for items in the
# enum to not include the class name. New uses of enum shouldn't
# need to do this.
def __str__(self):
return self.name

@classmethod
def _missing_(cls, value):
for member in cls:
if member.name == value:
return member
return None


minimize = ObjectiveSense.minimize
maximize = ObjectiveSense.maximize
97 changes: 97 additions & 0 deletions pyomo/common/tests/test_enums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# ___________________________________________________________________________
#
# Pyomo: Python Optimization Modeling Objects
# Copyright (c) 2008-2024
# National Technology and Engineering Solutions of Sandia, LLC
# Under the terms of Contract DE-NA0003525 with National Technology and
# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
# rights in this software.
# This software is distributed under the 3-clause BSD License.
# ___________________________________________________________________________

import enum

import pyomo.common.unittest as unittest

from pyomo.common.enums import ExtendedEnumType, ObjectiveSense


class ProblemSense(enum.IntEnum, metaclass=ExtendedEnumType):
__base_enum__ = ObjectiveSense

unknown = 0


class TestExtendedEnumType(unittest.TestCase):
def test_members(self):
self.assertEqual(
list(ProblemSense),
[ProblemSense.unknown, ObjectiveSense.minimize, ObjectiveSense.maximize],
)

def test_isinstance(self):
self.assertIsInstance(ProblemSense.unknown, ProblemSense)
self.assertIsInstance(ProblemSense.minimize, ProblemSense)
self.assertIsInstance(ProblemSense.maximize, ProblemSense)

self.assertTrue(ProblemSense.__instancecheck__(ProblemSense.unknown))
self.assertTrue(ProblemSense.__instancecheck__(ProblemSense.minimize))
self.assertTrue(ProblemSense.__instancecheck__(ProblemSense.maximize))

def test_getattr(self):
self.assertIs(ProblemSense.unknown, ProblemSense.unknown)
self.assertIs(ProblemSense.minimize, ObjectiveSense.minimize)
self.assertIs(ProblemSense.maximize, ObjectiveSense.maximize)

def test_hasattr(self):
self.assertTrue(hasattr(ProblemSense, 'unknown'))
self.assertTrue(hasattr(ProblemSense, 'minimize'))
self.assertTrue(hasattr(ProblemSense, 'maximize'))

def test_call(self):
self.assertIs(ProblemSense(0), ProblemSense.unknown)
self.assertIs(ProblemSense(1), ObjectiveSense.minimize)
self.assertIs(ProblemSense(-1), ObjectiveSense.maximize)

self.assertIs(ProblemSense('unknown'), ProblemSense.unknown)
self.assertIs(ProblemSense('minimize'), ObjectiveSense.minimize)
self.assertIs(ProblemSense('maximize'), ObjectiveSense.maximize)

with self.assertRaisesRegex(ValueError, "'foo' is not a valid ProblemSense"):
ProblemSense('foo')
with self.assertRaisesRegex(ValueError, "2 is not a valid ProblemSense"):
ProblemSense(2)

def test_contains(self):
self.assertIn(ProblemSense.unknown, ProblemSense)
self.assertIn(ProblemSense.minimize, ProblemSense)
self.assertIn(ProblemSense.maximize, ProblemSense)

self.assertNotIn(ProblemSense.unknown, ObjectiveSense)
self.assertIn(ProblemSense.minimize, ObjectiveSense)
self.assertIn(ProblemSense.maximize, ObjectiveSense)


class TestObjectiveSense(unittest.TestCase):
def test_members(self):
self.assertEqual(
list(ObjectiveSense), [ObjectiveSense.minimize, ObjectiveSense.maximize]
)

def test_hasattr(self):
self.assertTrue(hasattr(ProblemSense, 'minimize'))
self.assertTrue(hasattr(ProblemSense, 'maximize'))

def test_call(self):
self.assertIs(ObjectiveSense(1), ObjectiveSense.minimize)
self.assertIs(ObjectiveSense(-1), ObjectiveSense.maximize)

self.assertIs(ObjectiveSense('minimize'), ObjectiveSense.minimize)
self.assertIs(ObjectiveSense('maximize'), ObjectiveSense.maximize)

with self.assertRaisesRegex(ValueError, "'foo' is not a valid ObjectiveSense"):
ObjectiveSense('foo')

def test_str(self):
self.assertEqual(str(ObjectiveSense.minimize), 'minimize')
self.assertEqual(str(ObjectiveSense.maximize), 'maximize')
Loading

0 comments on commit 1b638e6

Please sign in to comment.