From 262567ef838760ed56c9f8eb0592c0d670d34266 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 12 Aug 2025 15:53:14 -0600 Subject: [PATCH 1/5] Create API version attribute for different solver generations --- pyomo/common/enums.py | 17 ++++++++++++++ pyomo/common/tests/test_enums.py | 24 +++++++++++++++++++- pyomo/contrib/appsi/base.py | 7 +++++- pyomo/contrib/solver/common/base.py | 7 +++++- pyomo/contrib/solver/tests/unit/test_base.py | 13 ++++++++++- pyomo/opt/base/solvers.py | 11 +++++++++ 6 files changed, 75 insertions(+), 4 deletions(-) diff --git a/pyomo/common/enums.py b/pyomo/common/enums.py index 0c90d358d65..96f34e73dfa 100644 --- a/pyomo/common/enums.py +++ b/pyomo/common/enums.py @@ -24,6 +24,7 @@ .. autosummary:: ObjectiveSense + SolverAPIVersion """ @@ -213,5 +214,21 @@ def __str__(self): return self.name +class SolverAPIVersion(NamedIntEnum): + """ + Helper enum that defines a common "generation" property + for solver interfaces since there are now multiple APIs across different + parts of Pyomo. + + 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. + """ + + V1 = 10 + APPSI = 15 + 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..5371e24beba 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..57d40dfcac2 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,11 @@ def __str__(self): # preserve the previous behavior return self.name + # There are now multiple API versions of solvers + @classmethod + def api_version(self): + 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..994c41869fe 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,11 @@ def __enter__(self): def __exit__(self, exc_type, exc_value, exc_traceback): """Exit statement - enables `with` statements.""" + # There are now multiple API versions of solvers + @classmethod + def api_version(self): + return SolverAPIVersion.V2 + def solve(self, model: BlockData, **kwargs) -> Results: """Solve a Pyomo model. 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..b41bd114b46 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,11 @@ def __enter__(self): def __exit__(self, t, v, traceback): pass + # There are now multiple API versions of solvers + @classmethod + def api_version(self): + return SolverAPIVersion.V1 + def available(self, exception_flag=True): """Determine if this optimizer is available.""" if exception_flag: @@ -249,6 +255,11 @@ def __enter__(self): def __exit__(self, t, v, traceback): pass + # There are now multiple API versions of solvers + @classmethod + def api_version(self): + return SolverAPIVersion.V1 + # # Adding to help track down invalid code after making # the following attributes private From 3530df2bbefe1abf841fca8aa22a807a55ff51bb Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 12 Aug 2025 16:09:33 -0600 Subject: [PATCH 2/5] Address comments --- pyomo/common/enums.py | 4 +--- pyomo/contrib/appsi/base.py | 9 ++++++++- pyomo/contrib/solver/common/base.py | 9 ++++++++- pyomo/opt/base/solvers.py | 18 ++++++++++++++++-- 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/pyomo/common/enums.py b/pyomo/common/enums.py index 96f34e73dfa..160a5ef8e66 100644 --- a/pyomo/common/enums.py +++ b/pyomo/common/enums.py @@ -216,9 +216,7 @@ def __str__(self): class SolverAPIVersion(NamedIntEnum): """ - Helper enum that defines a common "generation" property - for solver interfaces since there are now multiple APIs across different - parts of Pyomo. + 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 diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index 57d40dfcac2..1671e90897a 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -635,9 +635,16 @@ def __str__(self): # preserve the previous behavior return self.name - # There are now multiple API versions of solvers @classmethod def api_version(self): + """ + Defines the API version for all APPSI solvers. + + Returns + ------- + ~pyomo.common.enums.SolverAPIVersion + An solver API enum object + """ return SolverAPIVersion.APPSI @abc.abstractmethod diff --git a/pyomo/contrib/solver/common/base.py b/pyomo/contrib/solver/common/base.py index 994c41869fe..31dc4721125 100644 --- a/pyomo/contrib/solver/common/base.py +++ b/pyomo/contrib/solver/common/base.py @@ -105,9 +105,16 @@ def __enter__(self): def __exit__(self, exc_type, exc_value, exc_traceback): """Exit statement - enables `with` statements.""" - # There are now multiple API versions of solvers @classmethod def api_version(self): + """ + Defines the API version for all V2 solvers. + + Returns + ------- + ~pyomo.common.enums.SolverAPIVersion + An solver API enum object + """ return SolverAPIVersion.V2 def solve(self, model: BlockData, **kwargs) -> Results: diff --git a/pyomo/opt/base/solvers.py b/pyomo/opt/base/solvers.py index b41bd114b46..ff48498b7dc 100644 --- a/pyomo/opt/base/solvers.py +++ b/pyomo/opt/base/solvers.py @@ -81,9 +81,16 @@ def __enter__(self): def __exit__(self, t, v, traceback): pass - # There are now multiple API versions of solvers @classmethod def api_version(self): + """ + Defines the API version for all V1 solvers. + + Returns + ------- + ~pyomo.common.enums.SolverAPIVersion + An solver API enum object + """ return SolverAPIVersion.V1 def available(self, exception_flag=True): @@ -255,9 +262,16 @@ def __enter__(self): def __exit__(self, t, v, traceback): pass - # There are now multiple API versions of solvers @classmethod def api_version(self): + """ + Defines the API version for all V1 solvers. + + Returns + ------- + ~pyomo.common.enums.SolverAPIVersion + An solver API enum object + """ return SolverAPIVersion.V1 # From 51930dfae653563f5581869dacb92f6cf398cc5f Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 12 Aug 2025 16:13:43 -0600 Subject: [PATCH 3/5] Update docstrings --- pyomo/common/enums.py | 3 +++ pyomo/contrib/appsi/base.py | 2 +- pyomo/contrib/solver/common/base.py | 2 +- pyomo/opt/base/solvers.py | 4 ++-- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/pyomo/common/enums.py b/pyomo/common/enums.py index 160a5ef8e66..1d31900baf5 100644 --- a/pyomo/common/enums.py +++ b/pyomo/common/enums.py @@ -223,8 +223,11 @@ class SolverAPIVersion(NamedIntEnum): 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 diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index 1671e90897a..a61ddfee810 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -643,7 +643,7 @@ def api_version(self): Returns ------- ~pyomo.common.enums.SolverAPIVersion - An solver API enum object + A solver API enum object """ return SolverAPIVersion.APPSI diff --git a/pyomo/contrib/solver/common/base.py b/pyomo/contrib/solver/common/base.py index 31dc4721125..29fc51b9002 100644 --- a/pyomo/contrib/solver/common/base.py +++ b/pyomo/contrib/solver/common/base.py @@ -113,7 +113,7 @@ def api_version(self): Returns ------- ~pyomo.common.enums.SolverAPIVersion - An solver API enum object + A solver API enum object """ return SolverAPIVersion.V2 diff --git a/pyomo/opt/base/solvers.py b/pyomo/opt/base/solvers.py index ff48498b7dc..2ea13bd5b2e 100644 --- a/pyomo/opt/base/solvers.py +++ b/pyomo/opt/base/solvers.py @@ -89,7 +89,7 @@ def api_version(self): Returns ------- ~pyomo.common.enums.SolverAPIVersion - An solver API enum object + A solver API enum object """ return SolverAPIVersion.V1 @@ -270,7 +270,7 @@ def api_version(self): Returns ------- ~pyomo.common.enums.SolverAPIVersion - An solver API enum object + A solver API enum object """ return SolverAPIVersion.V1 From 03d4062416d1be5b47dae2cb1e8f88c1ce1e22c6 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 12 Aug 2025 16:27:46 -0600 Subject: [PATCH 4/5] One more time - docstrings --- pyomo/contrib/appsi/base.py | 2 +- pyomo/contrib/solver/common/base.py | 2 +- pyomo/opt/base/solvers.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index a61ddfee810..9d11297c60b 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -638,7 +638,7 @@ def __str__(self): @classmethod def api_version(self): """ - Defines the API version for all APPSI solvers. + Return the public API supported by this interface. Returns ------- diff --git a/pyomo/contrib/solver/common/base.py b/pyomo/contrib/solver/common/base.py index 29fc51b9002..90915b534f8 100644 --- a/pyomo/contrib/solver/common/base.py +++ b/pyomo/contrib/solver/common/base.py @@ -108,7 +108,7 @@ def __exit__(self, exc_type, exc_value, exc_traceback): @classmethod def api_version(self): """ - Defines the API version for all V2 solvers. + Return the public API supported by this interface. Returns ------- diff --git a/pyomo/opt/base/solvers.py b/pyomo/opt/base/solvers.py index 2ea13bd5b2e..4aeee0c1987 100644 --- a/pyomo/opt/base/solvers.py +++ b/pyomo/opt/base/solvers.py @@ -84,7 +84,7 @@ def __exit__(self, t, v, traceback): @classmethod def api_version(self): """ - Defines the API version for all V1 solvers. + Return the public API supported by this interface. Returns ------- @@ -265,7 +265,7 @@ def __exit__(self, t, v, traceback): @classmethod def api_version(self): """ - Defines the API version for all V1 solvers. + Return the public API supported by this interface. Returns ------- From 7c830a4df1ac380f83cde5a9cb60e016832524c8 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 13 Aug 2025 07:39:14 -0600 Subject: [PATCH 5/5] Missed list and missed test --- pyomo/common/tests/test_enums.py | 2 +- pyomo/contrib/solver/tests/solvers/test_ipopt.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pyomo/common/tests/test_enums.py b/pyomo/common/tests/test_enums.py index 5371e24beba..844e7539685 100644 --- a/pyomo/common/tests/test_enums.py +++ b/pyomo/common/tests/test_enums.py @@ -101,7 +101,7 @@ class TestSolverAPIVersion(unittest.TestCase): def test_members(self): self.assertEqual( list(SolverAPIVersion), - (SolverAPIVersion.V1, SolverAPIVersion.APPSI, SolverAPIVersion.V2), + [SolverAPIVersion.V1, SolverAPIVersion.APPSI, SolverAPIVersion.V2], ) def test_call(self): 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',