diff --git a/doc/OnlineDocs/library_reference/common/enums.rst b/doc/OnlineDocs/library_reference/common/enums.rst new file mode 100644 index 00000000000..5ed2dbb1e80 --- /dev/null +++ b/doc/OnlineDocs/library_reference/common/enums.rst @@ -0,0 +1,7 @@ + +pyomo.common.enums +================== + +.. automodule:: pyomo.common.enums + :members: + :member-order: bysource diff --git a/doc/OnlineDocs/library_reference/common/index.rst b/doc/OnlineDocs/library_reference/common/index.rst index c9c99008250..c03436600f2 100644 --- a/doc/OnlineDocs/library_reference/common/index.rst +++ b/doc/OnlineDocs/library_reference/common/index.rst @@ -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 diff --git a/examples/pyomobook/pyomo-components-ch/obj_declaration.txt b/examples/pyomobook/pyomo-components-ch/obj_declaration.txt index 607586a1fb3..e4d4b02a252 100644 --- a/examples/pyomobook/pyomo-components-ch/obj_declaration.txt +++ b/examples/pyomobook/pyomo-components-ch/obj_declaration.txt @@ -55,7 +55,7 @@ Model unknown None value x[Q] + 2*x[R] -1 +minimize 6.5 Model unknown diff --git a/pyomo/common/enums.py b/pyomo/common/enums.py new file mode 100644 index 00000000000..4d969bf7a9e --- /dev/null +++ b/pyomo/common/enums.py @@ -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 + + >>> ProblemSense.maximize + + >>> ProblemSense(0) + + >>> ProblemSense(1) + + >>> ProblemSense('unknown') + + >>> ProblemSense('maximize') + + >>> 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 diff --git a/pyomo/common/tests/test_enums.py b/pyomo/common/tests/test_enums.py new file mode 100644 index 00000000000..80d081505e9 --- /dev/null +++ b/pyomo/common/tests/test_enums.py @@ -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') diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index 785a89d8982..8c703f8d842 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -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, @@ -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. diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index 1543497838f..7345af8a3e2 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -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 diff --git a/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py b/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py index 53616298415..0999550711c 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py @@ -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__) @@ -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 diff --git a/pyomo/core/__init__.py b/pyomo/core/__init__.py index bce79faacc5..f0d168d98f9 100644 --- a/pyomo/core/__init__.py +++ b/pyomo/core/__init__.py @@ -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 diff --git a/pyomo/core/base/__init__.py b/pyomo/core/base/__init__.py index 4bbd0c9dc44..df5ce743888 100644 --- a/pyomo/core/base/__init__.py +++ b/pyomo/core/base/__init__.py @@ -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 ( @@ -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 diff --git a/pyomo/core/base/objective.py b/pyomo/core/base/objective.py index fcc63755f2b..10cc853dafb 100644 --- a/pyomo/core/base/objective.py +++ b/pyomo/core/base/objective.py @@ -15,6 +15,7 @@ from pyomo.common.pyomo_typing import overload from pyomo.common.deprecation import RenamedClass +from pyomo.common.enums import ObjectiveSense, minimize, maximize from pyomo.common.log import is_debug_set from pyomo.common.modeling import NOTSET from pyomo.common.formatting import tabular_writer @@ -35,7 +36,6 @@ IndexedCallInitializer, CountedCallInitializer, ) -from pyomo.core.base import minimize, maximize logger = logging.getLogger('pyomo.core') @@ -152,14 +152,7 @@ def __init__(self, expr=None, sense=minimize, component=None): self._component = weakref_ref(component) if (component is not None) else None self._index = NOTSET self._active = True - self._sense = sense - - if (self._sense != minimize) and (self._sense != maximize): - raise ValueError( - "Objective sense must be set to one of " - "'minimize' (%s) or 'maximize' (%s). Invalid " - "value: %s'" % (minimize, maximize, sense) - ) + self._sense = ObjectiveSense(sense) def set_value(self, expr): if expr is None: @@ -182,14 +175,7 @@ def sense(self, sense): def set_sense(self, sense): """Set the sense (direction) of this objective.""" - if sense in {minimize, maximize}: - self._sense = sense - else: - raise ValueError( - "Objective sense must be set to one of " - "'minimize' (%s) or 'maximize' (%s). Invalid " - "value: %s'" % (minimize, maximize, sense) - ) + self._sense = ObjectiveSense(sense) @ModelComponentFactory.register("Expressions that are minimized or maximized.") @@ -353,11 +339,7 @@ def _pprint(self): ], self._data.items(), ("Active", "Sense", "Expression"), - lambda k, v: [ - v.active, - ("minimize" if (v.sense == minimize) else "maximize"), - v.expr, - ], + lambda k, v: [v.active, v.sense, v.expr], ) def display(self, prefix="", ostream=None): diff --git a/pyomo/core/kernel/objective.py b/pyomo/core/kernel/objective.py index 9aa8e3315ef..ac6f22d07d3 100644 --- a/pyomo/core/kernel/objective.py +++ b/pyomo/core/kernel/objective.py @@ -9,15 +9,12 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +from pyomo.common.enums import ObjectiveSense, minimize, maximize from pyomo.core.expr.numvalue import as_numeric from pyomo.core.kernel.base import _abstract_readwrite_property from pyomo.core.kernel.container_utils import define_simple_containers from pyomo.core.kernel.expression import IExpression -# Constants used to define the optimization sense -minimize = 1 -maximize = -1 - class IObjective(IExpression): """ @@ -84,14 +81,7 @@ def sense(self): @sense.setter def sense(self, sense): """Set the sense (direction) of this objective.""" - if (sense == minimize) or (sense == maximize): - self._sense = sense - else: - raise ValueError( - "Objective sense must be set to one of: " - "[minimize (%s), maximize (%s)]. Invalid " - "value: %s'" % (minimize, maximize, sense) - ) + self._sense = ObjectiveSense(sense) # inserts class definitions for simple _tuple, _list, and diff --git a/pyomo/opt/results/problem.py b/pyomo/opt/results/problem.py index 98f749f3aeb..a8eca1e3b41 100644 --- a/pyomo/opt/results/problem.py +++ b/pyomo/opt/results/problem.py @@ -12,19 +12,16 @@ import enum from pyomo.opt.results.container import MapContainer +from pyomo.common.enums import ExtendedEnumType, ObjectiveSense -class ProblemSense(str, enum.Enum): - unknown = 'unknown' - minimize = 'minimize' - maximize = 'maximize' - # 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. +class ProblemSense(enum.IntEnum, metaclass=ExtendedEnumType): + __base_enum__ = ObjectiveSense + + unknown = 0 + def __str__(self): - return self.value + return self.name class ProblemInformation(MapContainer): diff --git a/pyomo/solvers/plugins/solvers/CBCplugin.py b/pyomo/solvers/plugins/solvers/CBCplugin.py index eb6c2c2e1bd..f22fb117c8b 100644 --- a/pyomo/solvers/plugins/solvers/CBCplugin.py +++ b/pyomo/solvers/plugins/solvers/CBCplugin.py @@ -16,6 +16,7 @@ import subprocess from pyomo.common import Executable +from pyomo.common.enums import maximize, minimize from pyomo.common.errors import ApplicationError from pyomo.common.collections import Bunch from pyomo.common.tempfiles import TempfileManager @@ -29,7 +30,6 @@ SolverStatus, TerminationCondition, SolutionStatus, - ProblemSense, Solution, ) from pyomo.opt.solver import SystemCallSolver @@ -443,7 +443,7 @@ def process_logfile(self): # # Parse logfile lines # - results.problem.sense = ProblemSense.minimize + results.problem.sense = minimize results.problem.name = None optim_value = float('inf') lower_bound = None @@ -578,7 +578,7 @@ def process_logfile(self): 'CoinLpIO::readLp(): Maximization problem reformulated as minimization' in ' '.join(tokens) ): - results.problem.sense = ProblemSense.maximize + results.problem.sense = maximize # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L3047 elif n_tokens > 3 and tokens[:2] == ('Result', '-'): if tokens[2:4] in [('Run', 'abandoned'), ('User', 'ctrl-c')]: @@ -752,9 +752,9 @@ def process_logfile(self): "maxIterations parameter." ) soln.gap = gap - if results.problem.sense == ProblemSense.minimize: + if results.problem.sense == minimize: upper_bound = optim_value - elif results.problem.sense == ProblemSense.maximize: + elif results.problem.sense == maximize: _ver = self.version() if _ver and _ver[:3] < (2, 10, 2): optim_value *= -1 @@ -824,7 +824,7 @@ def process_soln_file(self, results): INPUT = [] _ver = self.version() - invert_objective_sense = results.problem.sense == ProblemSense.maximize and ( + invert_objective_sense = results.problem.sense == maximize and ( _ver and _ver[:3] < (2, 10, 2) ) diff --git a/pyomo/solvers/plugins/solvers/CPLEX.py b/pyomo/solvers/plugins/solvers/CPLEX.py index 9f876b2d0f8..3a08257c87c 100644 --- a/pyomo/solvers/plugins/solvers/CPLEX.py +++ b/pyomo/solvers/plugins/solvers/CPLEX.py @@ -17,6 +17,7 @@ import subprocess from pyomo.common import Executable +from pyomo.common.enums import maximize, minimize from pyomo.common.errors import ApplicationError from pyomo.common.tempfiles import TempfileManager @@ -28,7 +29,6 @@ SolverStatus, TerminationCondition, SolutionStatus, - ProblemSense, Solution, ) from pyomo.opt.solver import ILMLicensedSystemCallSolver @@ -547,9 +547,9 @@ def process_logfile(self): ): # CPLEX 11.2 and subsequent has two Nonzeros sections. results.problem.number_of_nonzeros = int(tokens[2]) elif len(tokens) >= 5 and tokens[4] == "MINIMIZE": - results.problem.sense = ProblemSense.minimize + results.problem.sense = minimize elif len(tokens) >= 5 and tokens[4] == "MAXIMIZE": - results.problem.sense = ProblemSense.maximize + results.problem.sense = maximize elif ( len(tokens) >= 4 and tokens[0] == "Solution" @@ -859,9 +859,9 @@ def process_soln_file(self, results): else: sense = tokens[0].lower() if sense in ['max', 'maximize']: - results.problem.sense = ProblemSense.maximize + results.problem.sense = maximize if sense in ['min', 'minimize']: - results.problem.sense = ProblemSense.minimize + results.problem.sense = minimize break tINPUT.close() @@ -952,7 +952,7 @@ def process_soln_file(self, results): ) if primal_feasible == 1: soln.status = SolutionStatus.feasible - if results.problem.sense == ProblemSense.minimize: + if results.problem.sense == minimize: results.problem.upper_bound = soln.objective[ '__default_objective__' ]['Value'] @@ -964,7 +964,7 @@ def process_soln_file(self, results): soln.status = SolutionStatus.infeasible if self._best_bound is not None: - if results.problem.sense == ProblemSense.minimize: + if results.problem.sense == minimize: results.problem.lower_bound = self._best_bound else: results.problem.upper_bound = self._best_bound diff --git a/pyomo/solvers/plugins/solvers/GAMS.py b/pyomo/solvers/plugins/solvers/GAMS.py index c0bab4dc23e..035bd0b7603 100644 --- a/pyomo/solvers/plugins/solvers/GAMS.py +++ b/pyomo/solvers/plugins/solvers/GAMS.py @@ -36,7 +36,6 @@ Solution, SolutionStatus, TerminationCondition, - ProblemSense, ) from pyomo.common.dependencies import attempt_import @@ -422,11 +421,10 @@ def solve(self, *args, **kwds): assert len(obj) == 1, 'Only one objective is allowed.' obj = obj[0] objctvval = t1.out_db["OBJVAL"].find_record().value + results.problem.sense = obj.sense if obj.is_minimizing(): - results.problem.sense = ProblemSense.minimize results.problem.upper_bound = objctvval else: - results.problem.sense = ProblemSense.maximize results.problem.lower_bound = objctvval results.solver.name = "GAMS " + str(self.version()) @@ -984,11 +982,10 @@ def solve(self, *args, **kwds): assert len(obj) == 1, 'Only one objective is allowed.' obj = obj[0] objctvval = stat_vars["OBJVAL"] + results.problem.sense = obj.sense if obj.is_minimizing(): - results.problem.sense = ProblemSense.minimize results.problem.upper_bound = objctvval else: - results.problem.sense = ProblemSense.maximize results.problem.lower_bound = objctvval results.solver.name = "GAMS " + str(self.version()) diff --git a/pyomo/solvers/plugins/solvers/GLPK.py b/pyomo/solvers/plugins/solvers/GLPK.py index e6d8576489d..c8d5bc14237 100644 --- a/pyomo/solvers/plugins/solvers/GLPK.py +++ b/pyomo/solvers/plugins/solvers/GLPK.py @@ -19,6 +19,7 @@ from pyomo.common import Executable from pyomo.common.collections import Bunch +from pyomo.common.enums import maximize, minimize from pyomo.common.errors import ApplicationError from pyomo.opt import ( SolverFactory, @@ -28,7 +29,6 @@ SolverResults, TerminationCondition, SolutionStatus, - ProblemSense, ) from pyomo.opt.base.solvers import _extract_version from pyomo.opt.solver import SystemCallSolver @@ -308,10 +308,8 @@ def process_soln_file(self, results): ): raise ValueError - self.is_integer = 'mip' == ptype and True or False - prob.sense = ( - 'min' == psense and ProblemSense.minimize or ProblemSense.maximize - ) + self.is_integer = 'mip' == ptype + prob.sense = minimize if 'min' == psense else maximize prob.number_of_constraints = prows prob.number_of_nonzeros = pnonz prob.number_of_variables = pcols diff --git a/pyomo/solvers/plugins/solvers/GUROBI.py b/pyomo/solvers/plugins/solvers/GUROBI.py index c8b0912970e..3a3a4d52322 100644 --- a/pyomo/solvers/plugins/solvers/GUROBI.py +++ b/pyomo/solvers/plugins/solvers/GUROBI.py @@ -18,6 +18,7 @@ from pyomo.common import Executable from pyomo.common.collections import Bunch +from pyomo.common.enums import maximize, minimize from pyomo.common.fileutils import this_file_dir from pyomo.common.tee import capture_output from pyomo.common.tempfiles import TempfileManager @@ -28,7 +29,6 @@ SolverStatus, TerminationCondition, SolutionStatus, - ProblemSense, Solution, ) from pyomo.opt.solver import ILMLicensedSystemCallSolver @@ -472,7 +472,7 @@ def process_soln_file(self, results): soln.objective['__default_objective__'] = { 'Value': float(tokens[1]) } - if results.problem.sense == ProblemSense.minimize: + if results.problem.sense == minimize: results.problem.upper_bound = float(tokens[1]) else: results.problem.lower_bound = float(tokens[1]) @@ -514,9 +514,9 @@ def process_soln_file(self, results): elif section == 1: if tokens[0] == 'sense': if tokens[1] == 'minimize': - results.problem.sense = ProblemSense.minimize + results.problem.sense = minimize elif tokens[1] == 'maximize': - results.problem.sense = ProblemSense.maximize + results.problem.sense = maximize else: try: val = eval(tokens[1]) diff --git a/pyomo/solvers/plugins/solvers/SCIPAMPL.py b/pyomo/solvers/plugins/solvers/SCIPAMPL.py index fd69954b428..c309ad29d96 100644 --- a/pyomo/solvers/plugins/solvers/SCIPAMPL.py +++ b/pyomo/solvers/plugins/solvers/SCIPAMPL.py @@ -20,12 +20,7 @@ from pyomo.opt.base import ProblemFormat, ResultsFormat from pyomo.opt.base.solvers import _extract_version, SolverFactory -from pyomo.opt.results import ( - SolverStatus, - TerminationCondition, - SolutionStatus, - ProblemSense, -) +from pyomo.opt.results import SolverStatus, TerminationCondition, SolutionStatus from pyomo.opt.solver import SystemCallSolver import logging @@ -374,9 +369,11 @@ def _postsolve(self): if len(results.solution) > 0: results.solution(0).status = SolutionStatus.optimal try: - if results.problem.sense == ProblemSense.minimize: + if results.solver.primal_bound < results.solver.dual_bound: results.problem.lower_bound = results.solver.primal_bound + results.problem.upper_bound = results.solver.dual_bound else: + results.problem.lower_bound = results.solver.dual_bound results.problem.upper_bound = results.solver.primal_bound except AttributeError: """ diff --git a/pyomo/solvers/tests/checks/test_CBCplugin.py b/pyomo/solvers/tests/checks/test_CBCplugin.py index 2ea0e55c5f4..ad8846509ea 100644 --- a/pyomo/solvers/tests/checks/test_CBCplugin.py +++ b/pyomo/solvers/tests/checks/test_CBCplugin.py @@ -29,7 +29,7 @@ maximize, minimize, ) -from pyomo.opt import SolverFactory, ProblemSense, TerminationCondition, SolverStatus +from pyomo.opt import SolverFactory, TerminationCondition, SolverStatus from pyomo.solvers.plugins.solvers.CBCplugin import CBCSHELL cbc_available = SolverFactory('cbc', solver_io='lp').available(exception_flag=False) @@ -62,7 +62,7 @@ def test_infeasible_lp(self): results = self.opt.solve(self.model) - self.assertEqual(ProblemSense.minimize, results.problem.sense) + self.assertEqual(minimize, results.problem.sense) self.assertEqual( TerminationCondition.infeasible, results.solver.termination_condition ) @@ -81,7 +81,7 @@ def test_unbounded_lp(self): results = self.opt.solve(self.model) - self.assertEqual(ProblemSense.maximize, results.problem.sense) + self.assertEqual(maximize, results.problem.sense) self.assertEqual( TerminationCondition.unbounded, results.solver.termination_condition ) @@ -99,7 +99,7 @@ def test_optimal_lp(self): self.assertEqual(0.0, results.problem.lower_bound) self.assertEqual(0.0, results.problem.upper_bound) - self.assertEqual(ProblemSense.minimize, results.problem.sense) + self.assertEqual(minimize, results.problem.sense) self.assertEqual( TerminationCondition.optimal, results.solver.termination_condition ) @@ -118,7 +118,7 @@ def test_infeasible_mip(self): results = self.opt.solve(self.model) - self.assertEqual(ProblemSense.minimize, results.problem.sense) + self.assertEqual(minimize, results.problem.sense) self.assertEqual( TerminationCondition.infeasible, results.solver.termination_condition ) @@ -134,7 +134,7 @@ def test_unbounded_mip(self): results = self.opt.solve(self.model) - self.assertEqual(ProblemSense.minimize, results.problem.sense) + self.assertEqual(minimize, results.problem.sense) self.assertEqual( TerminationCondition.unbounded, results.solver.termination_condition ) @@ -159,7 +159,7 @@ def test_optimal_mip(self): self.assertEqual(1.0, results.problem.upper_bound) self.assertEqual(results.problem.number_of_binary_variables, 2) self.assertEqual(results.problem.number_of_integer_variables, 4) - self.assertEqual(ProblemSense.maximize, results.problem.sense) + self.assertEqual(maximize, results.problem.sense) self.assertEqual( TerminationCondition.optimal, results.solver.termination_condition ) diff --git a/pyomo/solvers/tests/mip/test_scip_solve_from_instance.baseline b/pyomo/solvers/tests/mip/test_scip_solve_from_instance.baseline index a3eb9ffacec..976e4a1b82e 100644 --- a/pyomo/solvers/tests/mip/test_scip_solve_from_instance.baseline +++ b/pyomo/solvers/tests/mip/test_scip_solve_from_instance.baseline @@ -1,7 +1,7 @@ { "Problem": [ { - "Lower bound": -Infinity, + "Lower bound": 1.0, "Number of constraints": 0, "Number of objectives": 1, "Number of variables": 1,