Skip to content
Open
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
63 changes: 57 additions & 6 deletions pyomo/contrib/solver/common/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
# This software is distributed under the 3-clause BSD License.
# ___________________________________________________________________________

from typing import Sequence, Dict, Optional, Mapping, List, Tuple
import os
from contextlib import contextmanager
from typing import Sequence, Dict, Optional, Mapping, List, Tuple

from pyomo.core.base.constraint import ConstraintData
from pyomo.core.base.var import VarData
Expand Down Expand Up @@ -45,12 +46,27 @@ class Availability(IntEnum):
order to record its availability for use.
"""

NoLicenseRequired = 3
"""The solver was found and no license is required to run."""

FullLicense = 2
"""The solver was found and a full license is accessible to use."""

LimitedLicense = 1
"""The solver was found and a limited license (e.g., demo license) is
accessible to use."""

NotFound = 0
BadVersion = -1
BadLicense = -2
NeedsCompiledExtension = -3
"""The solver was not found, either because the executable was not
on the path or the solver package is not importable."""

UnsupportedVersion = -1
"""The solver was found but is an unsupported version in Pyomo."""

LicenseError = -2
"""The solver was found but no usable license is available. This could
indicate either that no license was found, an expired or malformed
license was found, or the license is incorrect for the problem type."""

def __bool__(self):
return self._value_ > 0
Expand All @@ -62,6 +78,40 @@ def __str__(self):
return self.name


class _LicenseManager:
def acquire(self, timeout: Optional[float] = None) -> None:
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's put in a docstring (from the issue in which I took notes) explaining how users are expected to interact with licenses.

"""Acquire and lock a license. Default behavior is to simply return
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We had a discussion about timeout here - there is a difference between "time allowing the server to try before we give up / cancel" vs. "we are retrying for X amount of time, only if we keep getting errors"

Copy link
Owner Author

@mrmundt mrmundt Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timeout (flaky network / talking to license server) and retry (programming convenience, keep trying until a license becomes available) or whatever. If one/both are irrelevant, great, ignore it.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • timeout - infinity
  • retry - left at solver default (None / not set in some way)

because we assume, unless otherwise noted, that a solver does NOT
require a license."""
return

def release(self) -> None:
"""Release the lock on a license."""
return

def __enter__(self):
self.acquire()
return self

def __exit__(self, exc_type, exc, tb):
self.release()
return False

def __call__(self, timeout=None):
"""This logic is necessary in order to support this type of
context manager: ``with solver.license(timeout=5):``"""

@contextmanager
def _cm():
self.acquire(timeout)
try:
yield self
finally:
self.release()

return _cm()


class SolverBase:
"""The base class for "new-style" Pyomo solver interfaces.

Expand Down Expand Up @@ -98,6 +148,7 @@ def __init__(self, **kwds) -> None:

#: Instance configuration; see CONFIG documentation on derived class
self.config = self.CONFIG(value=kwds)
self.license = _LicenseManager()

def __enter__(self):
return self
Expand Down Expand Up @@ -138,7 +189,7 @@ def solve(self, model: BlockData, **kwargs) -> Results:
f"Derived class {self.__class__.__name__} failed to implement required method 'solve'."
)

def available(self) -> Availability:
def available(self, recheck: bool = False) -> Availability:
"""Test if the solver is available on this system.

Nominally, this will return `True` if the solver interface is
Expand All @@ -165,7 +216,7 @@ def available(self) -> Availability:
f"Derived class {self.__class__.__name__} failed to implement required method 'available'."
)

def version(self) -> Tuple:
def version(self, recheck: bool = False) -> Tuple:
"""Return the solver version found on the system.

Returns
Expand Down
Loading
Loading