Skip to content

Commit 9608e0e

Browse files
authored
Merge pull request #3224 from jsiirola/objective-sense
Redefine objective sense as a proper `IntEnum`
2 parents fb1341c + 1383095 commit 9608e0e

File tree

21 files changed

+321
-103
lines changed

21 files changed

+321
-103
lines changed

Diff for: doc/OnlineDocs/library_reference/common/enums.rst

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
2+
pyomo.common.enums
3+
==================
4+
5+
.. automodule:: pyomo.common.enums
6+
:members:
7+
:member-order: bysource

Diff for: doc/OnlineDocs/library_reference/common/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ or rely on any other parts of Pyomo.
1111
config.rst
1212
dependencies.rst
1313
deprecation.rst
14+
enums.rst
1415
errors.rst
1516
fileutils.rst
1617
formatting.rst

Diff for: examples/pyomobook/pyomo-components-ch/obj_declaration.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ Model unknown
5555
None
5656
value
5757
x[Q] + 2*x[R]
58-
1
58+
minimize
5959
6.5
6060
Model unknown
6161

Diff for: pyomo/common/enums.py

+162
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
# ___________________________________________________________________________
2+
#
3+
# Pyomo: Python Optimization Modeling Objects
4+
# Copyright (c) 2008-2024
5+
# National Technology and Engineering Solutions of Sandia, LLC
6+
# Under the terms of Contract DE-NA0003525 with National Technology and
7+
# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
8+
# rights in this software.
9+
# This software is distributed under the 3-clause BSD License.
10+
# ___________________________________________________________________________
11+
12+
"""This module provides standard :py:class:`enum.Enum` definitions used in
13+
Pyomo, along with additional utilities for working with custom Enums
14+
15+
Utilities:
16+
17+
.. autosummary::
18+
19+
ExtendedEnumType
20+
21+
Standard Enums:
22+
23+
.. autosummary::
24+
25+
ObjectiveSense
26+
27+
"""
28+
29+
import enum
30+
import itertools
31+
import sys
32+
33+
if sys.version_info[:2] < (3, 11):
34+
_EnumType = enum.EnumMeta
35+
else:
36+
_EnumType = enum.EnumType
37+
38+
39+
class ExtendedEnumType(_EnumType):
40+
"""Metaclass for creating an :py:class:`enum.Enum` that extends another Enum
41+
42+
In general, :py:class:`enum.Enum` classes are not extensible: that is,
43+
they are frozen when defined and cannot be the base class of another
44+
Enum. This Metaclass provides a workaround for creating a new Enum
45+
that extends an existing enum. Members in the base Enum are all
46+
present as members on the extended enum.
47+
48+
Example
49+
-------
50+
51+
.. testcode::
52+
:hide:
53+
54+
import enum
55+
from pyomo.common.enums import ExtendedEnumType
56+
57+
.. testcode::
58+
59+
class ObjectiveSense(enum.IntEnum):
60+
minimize = 1
61+
maximize = -1
62+
63+
class ProblemSense(enum.IntEnum, metaclass=ExtendedEnumType):
64+
__base_enum__ = ObjectiveSense
65+
66+
unknown = 0
67+
68+
.. doctest::
69+
70+
>>> list(ProblemSense)
71+
[<ProblemSense.unknown: 0>, <ObjectiveSense.minimize: 1>, <ObjectiveSense.maximize: -1>]
72+
>>> ProblemSense.unknown
73+
<ProblemSense.unknown: 0>
74+
>>> ProblemSense.maximize
75+
<ObjectiveSense.maximize: -1>
76+
>>> ProblemSense(0)
77+
<ProblemSense.unknown: 0>
78+
>>> ProblemSense(1)
79+
<ObjectiveSense.minimize: 1>
80+
>>> ProblemSense('unknown')
81+
<ProblemSense.unknown: 0>
82+
>>> ProblemSense('maximize')
83+
<ObjectiveSense.maximize: -1>
84+
>>> hasattr(ProblemSense, 'minimize')
85+
True
86+
>>> ProblemSense.minimize is ObjectiveSense.minimize
87+
True
88+
>>> ProblemSense.minimize in ProblemSense
89+
True
90+
91+
"""
92+
93+
def __getattr__(cls, attr):
94+
try:
95+
return getattr(cls.__base_enum__, attr)
96+
except:
97+
return super().__getattr__(attr)
98+
99+
def __iter__(cls):
100+
# The members of this Enum are the base enum members joined with
101+
# the local members
102+
return itertools.chain(super().__iter__(), cls.__base_enum__.__iter__())
103+
104+
def __contains__(cls, member):
105+
# This enum "contains" both its local members and the members in
106+
# the __base_enum__ (necessary for good auto-enum[sphinx] docs)
107+
return super().__contains__(member) or member in cls.__base_enum__
108+
109+
def __instancecheck__(cls, instance):
110+
if cls.__subclasscheck__(type(instance)):
111+
return True
112+
# Also pretend that members of the extended enum are subclasses
113+
# of the __base_enum__. This is needed to circumvent error
114+
# checking in enum.__new__ (e.g., for `ProblemSense('minimize')`)
115+
return cls.__base_enum__.__subclasscheck__(type(instance))
116+
117+
def _missing_(cls, value):
118+
# Support attribute lookup by value or name
119+
for attr in ('value', 'name'):
120+
for member in cls:
121+
if getattr(member, attr) == value:
122+
return member
123+
return None
124+
125+
def __new__(metacls, cls, bases, classdict, **kwds):
126+
# Support lookup by name - but only if the new Enum doesn't
127+
# specify its own implementation of _missing_
128+
if '_missing_' not in classdict:
129+
classdict['_missing_'] = classmethod(ExtendedEnumType._missing_)
130+
return super().__new__(metacls, cls, bases, classdict, **kwds)
131+
132+
133+
class ObjectiveSense(enum.IntEnum):
134+
"""Flag indicating if an objective is minimizing (1) or maximizing (-1).
135+
136+
While the numeric values are arbitrary, there are parts of Pyomo
137+
that rely on this particular choice of value. These values are also
138+
consistent with some solvers (notably Gurobi).
139+
140+
"""
141+
142+
minimize = 1
143+
maximize = -1
144+
145+
# Overloading __str__ is needed to match the behavior of the old
146+
# pyutilib.enum class (removed June 2020). There are spots in the
147+
# code base that expect the string representation for items in the
148+
# enum to not include the class name. New uses of enum shouldn't
149+
# need to do this.
150+
def __str__(self):
151+
return self.name
152+
153+
@classmethod
154+
def _missing_(cls, value):
155+
for member in cls:
156+
if member.name == value:
157+
return member
158+
return None
159+
160+
161+
minimize = ObjectiveSense.minimize
162+
maximize = ObjectiveSense.maximize

Diff for: pyomo/common/tests/test_enums.py

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# ___________________________________________________________________________
2+
#
3+
# Pyomo: Python Optimization Modeling Objects
4+
# Copyright (c) 2008-2024
5+
# National Technology and Engineering Solutions of Sandia, LLC
6+
# Under the terms of Contract DE-NA0003525 with National Technology and
7+
# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
8+
# rights in this software.
9+
# This software is distributed under the 3-clause BSD License.
10+
# ___________________________________________________________________________
11+
12+
import enum
13+
14+
import pyomo.common.unittest as unittest
15+
16+
from pyomo.common.enums import ExtendedEnumType, ObjectiveSense
17+
18+
19+
class ProblemSense(enum.IntEnum, metaclass=ExtendedEnumType):
20+
__base_enum__ = ObjectiveSense
21+
22+
unknown = 0
23+
24+
25+
class TestExtendedEnumType(unittest.TestCase):
26+
def test_members(self):
27+
self.assertEqual(
28+
list(ProblemSense),
29+
[ProblemSense.unknown, ObjectiveSense.minimize, ObjectiveSense.maximize],
30+
)
31+
32+
def test_isinstance(self):
33+
self.assertIsInstance(ProblemSense.unknown, ProblemSense)
34+
self.assertIsInstance(ProblemSense.minimize, ProblemSense)
35+
self.assertIsInstance(ProblemSense.maximize, ProblemSense)
36+
37+
self.assertTrue(ProblemSense.__instancecheck__(ProblemSense.unknown))
38+
self.assertTrue(ProblemSense.__instancecheck__(ProblemSense.minimize))
39+
self.assertTrue(ProblemSense.__instancecheck__(ProblemSense.maximize))
40+
41+
def test_getattr(self):
42+
self.assertIs(ProblemSense.unknown, ProblemSense.unknown)
43+
self.assertIs(ProblemSense.minimize, ObjectiveSense.minimize)
44+
self.assertIs(ProblemSense.maximize, ObjectiveSense.maximize)
45+
46+
def test_hasattr(self):
47+
self.assertTrue(hasattr(ProblemSense, 'unknown'))
48+
self.assertTrue(hasattr(ProblemSense, 'minimize'))
49+
self.assertTrue(hasattr(ProblemSense, 'maximize'))
50+
51+
def test_call(self):
52+
self.assertIs(ProblemSense(0), ProblemSense.unknown)
53+
self.assertIs(ProblemSense(1), ObjectiveSense.minimize)
54+
self.assertIs(ProblemSense(-1), ObjectiveSense.maximize)
55+
56+
self.assertIs(ProblemSense('unknown'), ProblemSense.unknown)
57+
self.assertIs(ProblemSense('minimize'), ObjectiveSense.minimize)
58+
self.assertIs(ProblemSense('maximize'), ObjectiveSense.maximize)
59+
60+
with self.assertRaisesRegex(ValueError, "'foo' is not a valid ProblemSense"):
61+
ProblemSense('foo')
62+
with self.assertRaisesRegex(ValueError, "2 is not a valid ProblemSense"):
63+
ProblemSense(2)
64+
65+
def test_contains(self):
66+
self.assertIn(ProblemSense.unknown, ProblemSense)
67+
self.assertIn(ProblemSense.minimize, ProblemSense)
68+
self.assertIn(ProblemSense.maximize, ProblemSense)
69+
70+
self.assertNotIn(ProblemSense.unknown, ObjectiveSense)
71+
self.assertIn(ProblemSense.minimize, ObjectiveSense)
72+
self.assertIn(ProblemSense.maximize, ObjectiveSense)
73+
74+
75+
class TestObjectiveSense(unittest.TestCase):
76+
def test_members(self):
77+
self.assertEqual(
78+
list(ObjectiveSense), [ObjectiveSense.minimize, ObjectiveSense.maximize]
79+
)
80+
81+
def test_hasattr(self):
82+
self.assertTrue(hasattr(ProblemSense, 'minimize'))
83+
self.assertTrue(hasattr(ProblemSense, 'maximize'))
84+
85+
def test_call(self):
86+
self.assertIs(ObjectiveSense(1), ObjectiveSense.minimize)
87+
self.assertIs(ObjectiveSense(-1), ObjectiveSense.maximize)
88+
89+
self.assertIs(ObjectiveSense('minimize'), ObjectiveSense.minimize)
90+
self.assertIs(ObjectiveSense('maximize'), ObjectiveSense.maximize)
91+
92+
with self.assertRaisesRegex(ValueError, "'foo' is not a valid ObjectiveSense"):
93+
ObjectiveSense('foo')
94+
95+
def test_str(self):
96+
self.assertEqual(str(ObjectiveSense.minimize), 'minimize')
97+
self.assertEqual(str(ObjectiveSense.maximize), 'maximize')

Diff for: pyomo/contrib/mindtpy/algorithm_base_class.py

+2-10
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,7 @@
2727
from operator import itemgetter
2828
from pyomo.common.errors import DeveloperError
2929
from pyomo.solvers.plugins.solvers.gurobi_direct import gurobipy
30-
from pyomo.opt import (
31-
SolverFactory,
32-
SolverResults,
33-
ProblemSense,
34-
SolutionStatus,
35-
SolverStatus,
36-
)
30+
from pyomo.opt import SolverFactory, SolverResults, SolutionStatus, SolverStatus
3731
from pyomo.core import (
3832
minimize,
3933
maximize,
@@ -633,9 +627,7 @@ def process_objective(self, update_var_con_list=True):
633627
raise ValueError('Model has multiple active objectives.')
634628
else:
635629
main_obj = active_objectives[0]
636-
self.results.problem.sense = (
637-
ProblemSense.minimize if main_obj.sense == 1 else ProblemSense.maximize
638-
)
630+
self.results.problem.sense = main_obj.sense
639631
self.objective_sense = main_obj.sense
640632

641633
# Move the objective to the constraints if it is nonlinear or move_objective is True.

Diff for: pyomo/contrib/mindtpy/util.py

-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
from pyomo.contrib.mcpp.pyomo_mcpp import mcpp_available, McCormick
3030
from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr
3131
import pyomo.core.expr as EXPR
32-
from pyomo.opt import ProblemSense
3332
from pyomo.contrib.gdpopt.util import get_main_elapsed_time, time_code
3433
from pyomo.util.model_size import build_model_size_report
3534
from pyomo.common.dependencies import attempt_import

Diff for: pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
from pyomo.common.config import ConfigBlock, ConfigValue
6666
from pyomo.common.timing import TicTocTimer
6767
from pyomo.core.base import Block, Objective, minimize
68-
from pyomo.opt import SolverStatus, SolverResults, TerminationCondition, ProblemSense
68+
from pyomo.opt import SolverStatus, SolverResults, TerminationCondition
6969
from pyomo.opt.results.solution import Solution
7070

7171
logger = logging.getLogger(__name__)
@@ -447,11 +447,10 @@ def solve(self, model, **kwds):
447447

448448
results.problem.name = model.name
449449
obj = next(model.component_data_objects(Objective, active=True))
450+
results.problem.sense = obj.sense
450451
if obj.sense == minimize:
451-
results.problem.sense = ProblemSense.minimize
452452
results.problem.upper_bound = info["obj_val"]
453453
else:
454-
results.problem.sense = ProblemSense.maximize
455454
results.problem.lower_bound = info["obj_val"]
456455
results.problem.number_of_objectives = 1
457456
results.problem.number_of_constraints = ng

Diff for: pyomo/core/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@
101101
BooleanValue,
102102
native_logical_values,
103103
)
104-
from pyomo.core.kernel.objective import minimize, maximize
104+
from pyomo.core.base import minimize, maximize
105105
from pyomo.core.base.config import PyomoOptions
106106

107107
from pyomo.core.base.expression import Expression

Diff for: pyomo/core/base/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# TODO: this import is for historical backwards compatibility and should
1313
# probably be removed
1414
from pyomo.common.collections import ComponentMap
15+
from pyomo.common.enums import minimize, maximize
1516

1617
from pyomo.core.expr.symbol_map import SymbolMap
1718
from pyomo.core.expr.numvalue import (
@@ -33,7 +34,6 @@
3334
BooleanValue,
3435
native_logical_values,
3536
)
36-
from pyomo.core.kernel.objective import minimize, maximize
3737
from pyomo.core.base.config import PyomoOptions
3838

3939
from pyomo.core.base.expression import Expression, _ExpressionData

0 commit comments

Comments
 (0)