Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
b57ab07
updating solution loader
michaelbynum Aug 9, 2025
070811d
refactoring gurobi interfaces
michaelbynum Aug 9, 2025
27a3a14
Merge branch 'main' into observer_gurobi_refactor
michaelbynum Aug 10, 2025
d70dbb5
revert_gurobi_persistent
michaelbynum Aug 10, 2025
5b1d3f9
refactoring gurobi interfaces
michaelbynum Aug 11, 2025
4818130
refactoring gurobi interfaces
michaelbynum Aug 11, 2025
7998fda
refactoring gurobi interfaces
michaelbynum Aug 12, 2025
909be88
refactoring gurobi interfaces
michaelbynum Aug 12, 2025
862c387
bugs
michaelbynum Aug 12, 2025
8f7a61e
refactoring gurobi interfaces
michaelbynum Aug 12, 2025
92fa4f5
remove unused imports
michaelbynum Aug 12, 2025
8a9fc46
run black
michaelbynum Aug 12, 2025
7249b19
update solution loader
michaelbynum Aug 12, 2025
710807b
Merge remote-tracking branch 'origin/main' into solver_api
michaelbynum Aug 12, 2025
25c48e7
Merge branch 'main' into observer_gurobi_refactor
michaelbynum Aug 12, 2025
438b9b5
Merge remote-tracking branch 'michaelbynum/observer_gurobi_refactor' …
michaelbynum Aug 12, 2025
ac42345
updating solution loader
michaelbynum Aug 12, 2025
df56887
Merge remote-tracking branch 'origin/main' into observer_gurobi_refactor
michaelbynum Aug 12, 2025
275d848
run black
michaelbynum Aug 12, 2025
cfa8633
Merge remote-tracking branch 'michaelbynum/observer_gurobi_refactor' …
michaelbynum Aug 12, 2025
70ca6e7
updating solution loader
michaelbynum Aug 13, 2025
1788ff3
dont free gurobi models twice
michaelbynum Aug 13, 2025
5ec0421
Merge branch 'observer_gurobi_refactor' into solver_api
michaelbynum Aug 13, 2025
2885f42
update solution loader
michaelbynum Aug 13, 2025
d16bee5
adding tests for trivial constraints and fixing bugs
michaelbynum Aug 14, 2025
873f176
merge observer
michaelbynum Aug 14, 2025
c62a7b3
Merge branch 'observer_gurobi_refactor' into solver_api
michaelbynum Aug 14, 2025
a4e2b81
run black
michaelbynum Aug 14, 2025
2001d15
Merge branch 'solver_api' into trivial_constraints
michaelbynum Aug 14, 2025
d25e721
run black
michaelbynum Aug 14, 2025
a43a38b
forgot to inherit from PersistentSolverBase
michaelbynum Aug 16, 2025
1750fc5
merge in observer_gurobi_refactor
michaelbynum Aug 16, 2025
02f383d
Merge branch 'solver_api' into trivial_constraints
michaelbynum Aug 16, 2025
e76baae
bug
michaelbynum Aug 16, 2025
413d63d
Merge branch 'observer_gurobi_refactor' into solver_api
michaelbynum Aug 16, 2025
3d302a1
Merge branch 'solver_api' into trivial_constraints
michaelbynum Aug 16, 2025
c2a0177
bug
michaelbynum Aug 18, 2025
0bbdd70
Merge branch 'observer_gurobi_refactor' into solver_api
michaelbynum Aug 18, 2025
ecd602d
Merge branch 'solver_api' into trivial_constraints
michaelbynum Aug 18, 2025
2c7208f
Merge branch 'main' into observer_gurobi_refactor
mrmundt Aug 26, 2025
19fecf7
merge in main and observer
michaelbynum Oct 2, 2025
31e7e97
Merge remote-tracking branch 'michaelbynum/observer_gurobi_refactor' …
michaelbynum Oct 2, 2025
576a217
run black
michaelbynum Oct 2, 2025
ce99fb2
observer improvements
michaelbynum Oct 4, 2025
066e4fd
run black
michaelbynum Oct 4, 2025
a96b518
merge observer_gurobi_refactor into solver_api
michaelbynum Oct 5, 2025
92e77ba
merge solver_api into trivial_constraints
michaelbynum Oct 5, 2025
6ccbaef
merge observer into observer_gurobi_refactor
michaelbynum Oct 28, 2025
cf000a1
directory for all gurobi interfaces
michaelbynum Nov 1, 2025
9abb4bf
merge main into observer_gurobi_refactor
michaelbynum Nov 1, 2025
7b20095
clean up gurobi interfaces
michaelbynum Nov 1, 2025
43a864f
Merge branch 'observer' into observer_gurobi_refactor
michaelbynum Nov 1, 2025
5073ba0
update gurobi persistent to use observer
michaelbynum Nov 2, 2025
5e280dc
gurobi refactor: bugs
michaelbynum Nov 2, 2025
8bff218
run black
michaelbynum Nov 2, 2025
d9dc14d
typo
michaelbynum Nov 5, 2025
1c26ae3
contrib.solvers: bug in gurobi refactor
michaelbynum Nov 5, 2025
a4858ca
contrib.solver: update tests
michaelbynum Nov 6, 2025
122511b
run black
michaelbynum Nov 7, 2025
71963f1
Changing the config option name for 'use_mipstart' to be 'warmstart_d…
emma58 Nov 7, 2025
8712884
Adding tests for Gurobi warmstarts in all the interfaces
emma58 Nov 7, 2025
8a729c0
Merge branch 'main' into observer_gurobi_refactor
michaelbynum Nov 11, 2025
7069f89
revert modification to ipopt interface
michaelbynum Nov 11, 2025
93bb118
Merge branch 'main' into observer_gurobi_refactor
michaelbynum Nov 12, 2025
7724678
Merge branch 'main' into observer_gurobi_refactor
michaelbynum Nov 14, 2025
9bfad14
contrib.solver.gurobi: better handling of temporary config options
michaelbynum Nov 16, 2025
2ab061a
fix error
michaelbynum Nov 16, 2025
f3d7f3a
contrib.solvers.gurobi: reworking the solution loader
michaelbynum Nov 18, 2025
d08d993
contrib.solvers.gurobi: reworking the solution loader
michaelbynum Nov 18, 2025
3ee4e99
run black
michaelbynum Nov 18, 2025
b9ca201
remove some timing statements
michaelbynum Nov 18, 2025
52d91f5
use absolute paths for imports
michaelbynum Dec 12, 2025
3660227
Merge remote-tracking branch 'origin/main' into observer_gurobi_refactor
michaelbynum Dec 12, 2025
ce4e77c
merge main
michaelbynum Dec 12, 2025
c3f2d48
solution loader updates
michaelbynum Dec 12, 2025
ba4b29c
run black
michaelbynum Dec 12, 2025
045f537
merge main
michaelbynum Dec 12, 2025
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: 5 additions & 2 deletions pyomo/contrib/observer/component_collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from pyomo.core.base.param import ParamData, ScalarParam
from pyomo.core.base.expression import ExpressionData, ScalarExpression
from pyomo.repn.util import ExitNodeDispatcher
from pyomo.common.numeric_types import native_numeric_types
from pyomo.common.collections import ComponentSet


Expand Down Expand Up @@ -80,8 +81,6 @@ def handle_skip(node, collector):
collector_handlers[RangedExpression] = handle_skip
collector_handlers[InequalityExpression] = handle_skip
collector_handlers[EqualityExpression] = handle_skip
collector_handlers[int] = handle_skip
collector_handlers[float] = handle_skip


class _ComponentFromExprCollector(StreamBasedExpressionVisitor):
Expand All @@ -93,6 +92,10 @@ def __init__(self, **kwds):
super().__init__(**kwds)

def exitNode(self, node, data):
if type(node) in native_numeric_types:
# we need this here to handle numpy
# (we can't put numpy in the dispatcher?)
return None
return collector_handlers[node.__class__](node, self)

def beforeChild(self, node, child, child_idx):
Expand Down
2 changes: 1 addition & 1 deletion pyomo/contrib/observer/model_observer.py
Original file line number Diff line number Diff line change
Expand Up @@ -910,7 +910,7 @@ def _update_variables(self, variables: Optional[Collection[VarData]] = None):
reason = Reason.no_change
if _fixed != fixed:
reason |= Reason.fixed
elif _fixed and (value != _value):
elif (_fixed or fixed) and (value != _value):
reason |= Reason.value
if lb is not _lb or ub is not _ub:
reason |= Reason.bounds
Expand Down
41 changes: 2 additions & 39 deletions pyomo/contrib/solver/common/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,22 +321,6 @@ def set_objective(self, obj: ObjectiveData):
f"Derived class {self.__class__.__name__} failed to implement required method 'set_objective'."
)

def add_variables(self, variables: List[VarData]):
"""
Add variables to the model.
"""
raise NotImplementedError(
f"Derived class {self.__class__.__name__} failed to implement required method 'add_variables'."
)

def add_parameters(self, params: List[ParamData]):
"""
Add parameters to the model.
"""
raise NotImplementedError(
f"Derived class {self.__class__.__name__} failed to implement required method 'add_parameters'."
)

def add_constraints(self, cons: List[ConstraintData]):
"""
Add constraints to the model.
Expand All @@ -353,22 +337,6 @@ def add_block(self, block: BlockData):
f"Derived class {self.__class__.__name__} failed to implement required method 'add_block'."
)

def remove_variables(self, variables: List[VarData]):
"""
Remove variables from the model.
"""
raise NotImplementedError(
f"Derived class {self.__class__.__name__} failed to implement required method 'remove_variables'."
)

def remove_parameters(self, params: List[ParamData]):
"""
Remove parameters from the model.
"""
raise NotImplementedError(
f"Derived class {self.__class__.__name__} failed to implement required method 'remove_parameters'."
)

def remove_constraints(self, cons: List[ConstraintData]):
"""
Remove constraints from the model.
Expand Down Expand Up @@ -604,15 +572,10 @@ def _solution_handler(
legacy_results._smap_id = id(symbol_map)
delete_legacy_soln = True
if load_solutions:
if hasattr(model, 'dual') and model.dual.import_enabled():
for con, val in results.solution_loader.get_duals().items():
model.dual[con] = val
if hasattr(model, 'rc') and model.rc.import_enabled():
for var, val in results.solution_loader.get_reduced_costs().items():
model.rc[var] = val
results.solution_loader.load_import_suffixes()
elif results.incumbent_objective is not None:
delete_legacy_soln = False
for var, val in results.solution_loader.get_primals().items():
for var, val in results.solution_loader.get_vars().items():
legacy_soln.variable[symbol_map.getSymbol(var)] = {'Value': val}
if hasattr(model, 'dual') and model.dual.import_enabled():
for con, val in results.solution_loader.get_duals().items():
Expand Down
182 changes: 160 additions & 22 deletions pyomo/contrib/solver/common/solution_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,35 @@
# This software is distributed under the 3-clause BSD License.
# ___________________________________________________________________________

from typing import Sequence, Dict, Optional, Mapping, NoReturn
from __future__ import annotations

from typing import Sequence, Dict, Optional, Mapping, List, Any

from pyomo.core.base.constraint import ConstraintData
from pyomo.core.base.var import VarData
from pyomo.core.staleflag import StaleFlagManager
from pyomo.core.base.suffix import Suffix
from .util import NoSolutionError


def load_import_suffixes(
pyomo_model, solution_loader: SolutionLoaderBase, solution_id=None
):
dual_suffix = None
rc_suffix = None
for suffix in pyomo_model.component_objects(Suffix, descend_into=True, active=True):
if not suffix.import_enabled():
continue
if suffix.local_name == 'dual':
dual_suffix = suffix
elif suffix.local_name == 'rc':
rc_suffix = suffix
if dual_suffix is not None:
for k, v in solution_loader.get_duals(solution_id=solution_id).items():
dual_suffix[k] = v
if rc_suffix is not None:
for k, v in solution_loader.get_reduced_costs(solution_id=solution_id).items():
rc_suffix[k] = v


class SolutionLoaderBase:
Expand All @@ -23,24 +47,70 @@ class SolutionLoaderBase:
Intent of this class and its children is to load the solution back into the model.
"""

def load_vars(self, vars_to_load: Optional[Sequence[VarData]] = None) -> NoReturn:
def get_solution_ids(self) -> List[Any]:
"""
If there are multiple solutions available, this will return a
list of the solution ids which can then be used with other
methods like `load_soltuion`. If only one solution is
available, this will return [None]. If no solutions
are available, this will return None

Returns
-------
solutions_ids: List[Any]
The identifiers for multiple solutions
"""
return NotImplemented

def get_number_of_solutions(self) -> int:
"""
Returns
-------
num_solutions: int
Indicates the number of solutions found
"""
return NotImplemented

def load_solution(self, solution_id=None):
"""
Load the solution (everything that can be) back into the model

Parameters
----------
solution_id: Optional[Any]
If there are multiple solutions, this specifies which solution
should be loaded. If None, the default solution will be used.
"""
# this should load everything it can
self.load_vars(solution_id=solution_id)
self.load_import_suffixes(solution_id=solution_id)

def load_vars(
self, vars_to_load: Optional[Sequence[VarData]] = None, solution_id=None
) -> None:
"""
Load the solution of the primal variables into the value attribute of the variables.
Load the solution of the primal variables into the value attribute
of the variables.

Parameters
----------
vars_to_load: list
The minimum set of variables whose solution should be loaded. If vars_to_load
is None, then the solution to all primal variables will be loaded. Even if
vars_to_load is specified, the values of other variables may also be
loaded depending on the interface.
The minimum set of variables whose solution should be loaded. If
vars_to_load is None, then the solution to all primal variables
will be loaded. Even if vars_to_load is specified, the values of
other variables may also be loaded depending on the interface.
solution_id: Optional[Any]
If there are multiple solutions, this specifies which solution
should be loaded. If None, the default solution will be used.
"""
for var, val in self.get_primals(vars_to_load=vars_to_load).items():
for var, val in self.get_vars(
vars_to_load=vars_to_load, solution_id=solution_id
).items():
var.set_value(val, skip_validation=True)
StaleFlagManager.mark_all_as_stale(delayed=True)

def get_primals(
self, vars_to_load: Optional[Sequence[VarData]] = None
def get_vars(
self, vars_to_load: Optional[Sequence[VarData]] = None, solution_id=None
) -> Mapping[VarData, float]:
"""
Returns a ComponentMap mapping variable to var value.
Expand All @@ -50,18 +120,21 @@ def get_primals(
vars_to_load: list
A list of the variables whose solution value should be retrieved. If vars_to_load
is None, then the values for all variables will be retrieved.
solution_id: Optional[Any]
If there are multiple solutions, this specifies which solution
should be retrieved. If None, the default solution will be used.

Returns
-------
primals: ComponentMap
Maps variables to solution values
"""
raise NotImplementedError(
f"Derived class {self.__class__.__name__} failed to implement required method 'get_primals'."
f"Derived class {self.__class__.__name__} failed to implement required method 'get_vars'."
)

def get_duals(
self, cons_to_load: Optional[Sequence[ConstraintData]] = None
self, cons_to_load: Optional[Sequence[ConstraintData]] = None, solution_id=None
) -> Dict[ConstraintData, float]:
"""
Returns a dictionary mapping constraint to dual value.
Expand All @@ -71,16 +144,19 @@ def get_duals(
cons_to_load: list
A list of the constraints whose duals should be retrieved. If cons_to_load
is None, then the duals for all constraints will be retrieved.
solution_id: Optional[Any]
If there are multiple solutions, this specifies which solution
should be retrieved. If None, the default solution will be used.

Returns
-------
duals: dict
Maps constraints to dual values
"""
raise NotImplementedError(f'{type(self)} does not support the get_duals method')
return NotImplemented

def get_reduced_costs(
self, vars_to_load: Optional[Sequence[VarData]] = None
self, vars_to_load: Optional[Sequence[VarData]] = None, solution_id=None
) -> Mapping[VarData, float]:
"""
Returns a ComponentMap mapping variable to reduced cost.
Expand All @@ -90,45 +166,107 @@ def get_reduced_costs(
vars_to_load: list
A list of the variables whose reduced cost should be retrieved. If vars_to_load
is None, then the reduced costs for all variables will be loaded.
solution_id: Optional[Any]
If there are multiple solutions, this specifies which solution
should be retrieved. If None, the default solution will be used.

Returns
-------
reduced_costs: ComponentMap
Maps variables to reduced costs
"""
raise NotImplementedError(
f'{type(self)} does not support the get_reduced_costs method'
)
return NotImplemented

def load_import_suffixes(self, solution_id=None):
"""
Parameters
----------
solution_id: Optional[Any]
If there are multiple solutions, this specifies which solution
should be loaded. If None, the default solution will be used.
"""
return NotImplemented


class NoSolutionSolutionLoader(SolutionLoaderBase):
def __init__(self) -> None:
pass

def get_solution_ids(self) -> List[Any]:
return []

def get_number_of_solutions(self) -> int:
return 0

def load_solution(self, solution_id=None):
raise NoSolutionError()

def load_vars(
self, vars_to_load: Sequence[VarData] | None = None, solution_id=None
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't we use Optional here?

) -> None:
raise NoSolutionError()

def get_vars(
self, vars_to_load: Sequence[VarData] | None = None, solution_id=None
) -> Mapping[VarData, float]:
raise NoSolutionError()

def get_duals(
self, cons_to_load: Sequence[ConstraintData] | None = None, solution_id=None
) -> Dict[ConstraintData, float]:
raise NoSolutionError()

def get_reduced_costs(
self, vars_to_load: Sequence[VarData] | None = None, solution_id=None
) -> Mapping[VarData, float]:
raise NoSolutionError()

def load_import_suffixes(self, solution_id=None):
raise NoSolutionError()


class PersistentSolutionLoader(SolutionLoaderBase):
"""
Loader for persistent solvers
"""

def __init__(self, solver):
def __init__(self, solver, pyomo_model):
self._solver = solver
self._valid = True
self._pyomo_model = pyomo_model

def _assert_solution_still_valid(self):
if not self._valid:
raise RuntimeError('The results in the solver are no longer valid.')

def get_primals(self, vars_to_load=None):
def get_solution_ids(self) -> List[Any]:
self._assert_solution_still_valid()
return self._solver._get_primals(vars_to_load=vars_to_load)
return super().get_solution_ids()

def get_number_of_solutions(self) -> int:
self._assert_solution_still_valid()
return super().get_number_of_solutions()

def get_vars(self, vars_to_load=None, solution_id=None):
self._assert_solution_still_valid()
return self._solver._get_primals(
vars_to_load=vars_to_load, solution_id=solution_id
)

def get_duals(
self, cons_to_load: Optional[Sequence[ConstraintData]] = None
self, cons_to_load: Optional[Sequence[ConstraintData]] = None, solution_id=None
) -> Dict[ConstraintData, float]:
self._assert_solution_still_valid()
return self._solver._get_duals(cons_to_load=cons_to_load)

def get_reduced_costs(
self, vars_to_load: Optional[Sequence[VarData]] = None
self, vars_to_load: Optional[Sequence[VarData]] = None, solution_id=None
) -> Mapping[VarData, float]:
self._assert_solution_still_valid()
return self._solver._get_reduced_costs(vars_to_load=vars_to_load)

def load_import_suffixes(self, solution_id=None):
load_import_suffixes(self._pyomo_model, self, solution_id=solution_id)

def invalidate(self):
self._valid = False
6 changes: 3 additions & 3 deletions pyomo/contrib/solver/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@

from .common.factory import SolverFactory
from .solvers.ipopt import Ipopt, LegacyIpoptSolver
from .solvers.gurobi_persistent import GurobiPersistent
from .solvers.gurobi_direct import GurobiDirect
from .solvers.gurobi_direct_minlp import GurobiDirectMINLP
from .solvers.gurobi.gurobi_direct import GurobiDirect
from .solvers.gurobi.gurobi_persistent import GurobiPersistent
from .solvers.gurobi.gurobi_direct_minlp import GurobiDirectMINLP
from .solvers.highs import Highs
from .solvers.knitro.direct import KnitroDirectSolver

Expand Down
3 changes: 3 additions & 0 deletions pyomo/contrib/solver/solvers/gurobi/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from pyomo.contrib.solver.solvers.gurobi.gurobi_direct import GurobiDirect
from pyomo.contrib.solver.solvers.gurobi.gurobi_persistent import GurobiPersistent
from pyomo.contrib.solver.solvers.gurobi.gurobi_direct_minlp import GurobiDirectMINLP
Loading
Loading