Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Redefine objective sense as a proper IntEnum #3224

Merged
merged 15 commits into from
Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"):
jsiirola marked this conversation as resolved.
Show resolved Hide resolved
ObjectiveSense('foo')

def test_str(self):
self.assertEqual(str(ObjectiveSense.minimize), 'minimize')
self.assertEqual(str(ObjectiveSense.maximize), 'maximize')
12 changes: 2 additions & 10 deletions pyomo/contrib/mindtpy/algorithm_base_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,7 @@
from operator import itemgetter
from pyomo.common.errors import DeveloperError
from pyomo.solvers.plugins.solvers.gurobi_direct import gurobipy
from pyomo.opt import (
SolverFactory,
SolverResults,
ProblemSense,
SolutionStatus,
SolverStatus,
)
from pyomo.opt import SolverFactory, SolverResults, SolutionStatus, SolverStatus
from pyomo.core import (
minimize,
maximize,
Expand Down Expand Up @@ -633,9 +627,7 @@ def process_objective(self, update_var_con_list=True):
raise ValueError('Model has multiple active objectives.')
else:
main_obj = active_objectives[0]
self.results.problem.sense = (
ProblemSense.minimize if main_obj.sense == 1 else ProblemSense.maximize
)
self.results.problem.sense = main_obj.sense
self.objective_sense = main_obj.sense

# Move the objective to the constraints if it is nonlinear or move_objective is True.
Expand Down
1 change: 0 additions & 1 deletion pyomo/contrib/mindtpy/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
from pyomo.contrib.mcpp.pyomo_mcpp import mcpp_available, McCormick
from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr
import pyomo.core.expr as EXPR
from pyomo.opt import ProblemSense
from pyomo.contrib.gdpopt.util import get_main_elapsed_time, time_code
from pyomo.util.model_size import build_model_size_report
from pyomo.common.dependencies import attempt_import
Expand Down
5 changes: 2 additions & 3 deletions pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
from pyomo.common.config import ConfigBlock, ConfigValue
from pyomo.common.timing import TicTocTimer
from pyomo.core.base import Block, Objective, minimize
from pyomo.opt import SolverStatus, SolverResults, TerminationCondition, ProblemSense
from pyomo.opt import SolverStatus, SolverResults, TerminationCondition
from pyomo.opt.results.solution import Solution

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

results.problem.name = model.name
obj = next(model.component_data_objects(Objective, active=True))
results.problem.sense = obj.sense
if obj.sense == minimize:
results.problem.sense = ProblemSense.minimize
results.problem.upper_bound = info["obj_val"]
else:
results.problem.sense = ProblemSense.maximize
results.problem.lower_bound = info["obj_val"]
results.problem.number_of_objectives = 1
results.problem.number_of_constraints = ng
Expand Down
2 changes: 1 addition & 1 deletion pyomo/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@
BooleanValue,
native_logical_values,
)
from pyomo.core.kernel.objective import minimize, maximize
from pyomo.core.base import minimize, maximize
from pyomo.core.base.config import PyomoOptions

from pyomo.core.base.expression import Expression
Expand Down
2 changes: 1 addition & 1 deletion pyomo/core/base/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
# TODO: this import is for historical backwards compatibility and should
# probably be removed
from pyomo.common.collections import ComponentMap
from pyomo.common.enums import minimize, maximize

from pyomo.core.expr.symbol_map import SymbolMap
from pyomo.core.expr.numvalue import (
Expand All @@ -33,7 +34,6 @@
BooleanValue,
native_logical_values,
)
from pyomo.core.kernel.objective import minimize, maximize
from pyomo.core.base.config import PyomoOptions

from pyomo.core.base.expression import Expression, _ExpressionData
Expand Down
Loading