diff --git a/pyomo/common/enums.py b/pyomo/common/enums.py index 0c90d358d65..1d31900baf5 100644 --- a/pyomo/common/enums.py +++ b/pyomo/common/enums.py @@ -24,6 +24,7 @@ .. autosummary:: ObjectiveSense + SolverAPIVersion """ @@ -213,5 +214,22 @@ def __str__(self): return self.name +class SolverAPIVersion(NamedIntEnum): + """ + Enum identifying Pyomo solver API version + + The numeric values are intentionally a bit odd because APPSI came + between the official V1 and V2. We still want it to be chronologically + in order without sacrificing the human-logic of v1 vs. v2. + """ + + #: Original Coopr/Pyomo solver interface + V1 = 10 + #: Automatic Persistent Pyomo Solver Interface (experimental) + APPSI = 15 + #: Redesigned solver interface (circa 2024) + V2 = 20 + + minimize = ObjectiveSense.minimize maximize = ObjectiveSense.maximize diff --git a/pyomo/common/tests/test_enums.py b/pyomo/common/tests/test_enums.py index b5264a50825..844e7539685 100644 --- a/pyomo/common/tests/test_enums.py +++ b/pyomo/common/tests/test_enums.py @@ -13,7 +13,7 @@ import pyomo.common.unittest as unittest -from pyomo.common.enums import ExtendedEnumType, ObjectiveSense +from pyomo.common.enums import ExtendedEnumType, ObjectiveSense, SolverAPIVersion class ProblemSense(enum.IntEnum, metaclass=ExtendedEnumType): @@ -95,3 +95,25 @@ def test_call(self): def test_str(self): self.assertEqual(str(ObjectiveSense.minimize), 'minimize') self.assertEqual(str(ObjectiveSense.maximize), 'maximize') + + +class TestSolverAPIVersion(unittest.TestCase): + def test_members(self): + self.assertEqual( + list(SolverAPIVersion), + [SolverAPIVersion.V1, SolverAPIVersion.APPSI, SolverAPIVersion.V2], + ) + + def test_call(self): + self.assertIs(SolverAPIVersion(10), SolverAPIVersion.V1) + self.assertIs(SolverAPIVersion(15), SolverAPIVersion.APPSI) + self.assertIs(SolverAPIVersion(20), SolverAPIVersion.V2) + + self.assertIs(SolverAPIVersion('V1'), SolverAPIVersion.V1) + self.assertIs(SolverAPIVersion('APPSI'), SolverAPIVersion.APPSI) + self.assertIs(SolverAPIVersion('V2'), SolverAPIVersion.V2) + + with self.assertRaisesRegex( + ValueError, "'foo' is not a valid SolverAPIVersion" + ): + SolverAPIVersion('foo') diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index 0f7cfda4437..9d11297c60b 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -28,7 +28,7 @@ from pyomo.common.config import ConfigDict, ConfigValue, NonNegativeFloat from pyomo.common.errors import ApplicationError -from pyomo.common.enums import IntEnum +from pyomo.common.enums import IntEnum, SolverAPIVersion from pyomo.common.factory import Factory from pyomo.common.timing import HierarchicalTimer from pyomo.core.base.constraint import ConstraintData, Constraint @@ -635,6 +635,18 @@ def __str__(self): # preserve the previous behavior return self.name + @classmethod + def api_version(self): + """ + Return the public API supported by this interface. + + Returns + ------- + ~pyomo.common.enums.SolverAPIVersion + A solver API enum object + """ + return SolverAPIVersion.APPSI + @abc.abstractmethod def solve(self, model: BlockData, timer: HierarchicalTimer = None) -> Results: """ diff --git a/pyomo/contrib/solver/common/base.py b/pyomo/contrib/solver/common/base.py index 280b80629a3..90915b534f8 100644 --- a/pyomo/contrib/solver/common/base.py +++ b/pyomo/contrib/solver/common/base.py @@ -18,7 +18,7 @@ from pyomo.core.base.block import BlockData from pyomo.core.base.objective import Objective, ObjectiveData from pyomo.common.config import ConfigValue -from pyomo.common.enums import IntEnum +from pyomo.common.enums import IntEnum, SolverAPIVersion from pyomo.common.errors import ApplicationError from pyomo.common.deprecation import deprecation_warning from pyomo.common.modeling import NOTSET @@ -105,6 +105,18 @@ def __enter__(self): def __exit__(self, exc_type, exc_value, exc_traceback): """Exit statement - enables `with` statements.""" + @classmethod + def api_version(self): + """ + Return the public API supported by this interface. + + Returns + ------- + ~pyomo.common.enums.SolverAPIVersion + A solver API enum object + """ + return SolverAPIVersion.V2 + def solve(self, model: BlockData, **kwargs) -> Results: """Solve a Pyomo model. diff --git a/pyomo/contrib/solver/tests/solvers/test_ipopt.py b/pyomo/contrib/solver/tests/solvers/test_ipopt.py index d788b66982a..c79c70ec1ca 100644 --- a/pyomo/contrib/solver/tests/solvers/test_ipopt.py +++ b/pyomo/contrib/solver/tests/solvers/test_ipopt.py @@ -102,6 +102,7 @@ def test_class_member_list(self): expected_list = [ 'CONFIG', 'config', + 'api_version', 'available', 'has_linear_solver', 'is_persistent', diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py index a93ac645f0f..ae7c1a0598d 100644 --- a/pyomo/contrib/solver/tests/unit/test_base.py +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -13,6 +13,7 @@ from pyomo.common import unittest from pyomo.common.config import ConfigDict +from pyomo.common.enums import SolverAPIVersion from pyomo.contrib.solver.common import base @@ -22,7 +23,14 @@ class _LegacyWrappedSolverBase(base.LegacySolverWrapper, base.SolverBase): class TestSolverBase(unittest.TestCase): def test_class_method_list(self): - expected_list = ['CONFIG', 'available', 'is_persistent', 'solve', 'version'] + expected_list = [ + 'CONFIG', + 'api_version', + 'available', + 'is_persistent', + 'solve', + 'version', + ] method_list = [ method for method in dir(base.SolverBase) if method.startswith('_') is False ] @@ -32,6 +40,7 @@ def test_init(self): instance = base.SolverBase() self.assertFalse(instance.is_persistent()) self.assertEqual(instance.name, 'solverbase') + self.assertEqual(instance.api_version().name, 'V2') self.assertEqual(instance.CONFIG, instance.config) with self.assertRaises(NotImplementedError): self.assertEqual(instance.version(), None) @@ -67,6 +76,7 @@ def test_class_method_list(self): 'add_constraints', 'add_parameters', 'add_variables', + 'api_version', 'available', 'is_persistent', 'remove_block', @@ -90,6 +100,7 @@ def test_class_method_list(self): def test_init(self): instance = base.PersistentSolverBase() self.assertTrue(instance.is_persistent()) + self.assertEqual(instance.api_version(), SolverAPIVersion.V2) with self.assertRaises(NotImplementedError): self.assertEqual(instance.set_instance(None), None) with self.assertRaises(NotImplementedError): diff --git a/pyomo/opt/base/solvers.py b/pyomo/opt/base/solvers.py index 767f62d07c2..4aeee0c1987 100644 --- a/pyomo/opt/base/solvers.py +++ b/pyomo/opt/base/solvers.py @@ -16,6 +16,7 @@ import shlex from pyomo.common import Factory +from pyomo.common.enums import SolverAPIVersion from pyomo.common.errors import ApplicationError from pyomo.common.collections import Bunch @@ -80,6 +81,18 @@ def __enter__(self): def __exit__(self, t, v, traceback): pass + @classmethod + def api_version(self): + """ + Return the public API supported by this interface. + + Returns + ------- + ~pyomo.common.enums.SolverAPIVersion + A solver API enum object + """ + return SolverAPIVersion.V1 + def available(self, exception_flag=True): """Determine if this optimizer is available.""" if exception_flag: @@ -249,6 +262,18 @@ def __enter__(self): def __exit__(self, t, v, traceback): pass + @classmethod + def api_version(self): + """ + Return the public API supported by this interface. + + Returns + ------- + ~pyomo.common.enums.SolverAPIVersion + A solver API enum object + """ + return SolverAPIVersion.V1 + # # Adding to help track down invalid code after making # the following attributes private