Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
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
25c48e7
Merge branch 'main' into observer_gurobi_refactor
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
1788ff3
dont free gurobi models twice
michaelbynum Aug 13, 2025
873f176
merge observer
michaelbynum Aug 14, 2025
a43a38b
forgot to inherit from PersistentSolverBase
michaelbynum Aug 16, 2025
e76baae
bug
michaelbynum Aug 16, 2025
c2a0177
bug
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
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
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
32 changes: 0 additions & 32 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
4 changes: 2 additions & 2 deletions pyomo/contrib/solver/common/solution_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
# This software is distributed under the 3-clause BSD License.
# ___________________________________________________________________________

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

from pyomo.core.base.constraint import ConstraintData
from pyomo.core.base.var import VarData
Expand All @@ -23,7 +23,7 @@ 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 load_vars(self, vars_to_load: Optional[Sequence[VarData]] = None) -> None:
"""
Load the solution of the primal variables into the value attribute of the variables.

Expand Down
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 .gurobi_direct import GurobiDirect
from .gurobi_persistent import GurobiPersistent
from .gurobi_direct_minlp import GurobiDirectMINLP
Comment on lines +1 to +3
Copy link
Member

Choose a reason for hiding this comment

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

We have had issues with relative imports; it would probably be best to convert these to absolute imports...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will do.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, I think this has to be a relative import. This is in the __init__.py file.

157 changes: 157 additions & 0 deletions pyomo/contrib/solver/solvers/gurobi/gurobi_direct.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# ___________________________________________________________________________
#
# Pyomo: Python Optimization Modeling Objects
# Copyright (c) 2008-2025
# 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 operator

from pyomo.common.collections import ComponentMap, ComponentSet
from pyomo.common.shutdown import python_is_shutting_down
from pyomo.core.staleflag import StaleFlagManager
from pyomo.repn.plugins.standard_form import LinearStandardFormCompiler

from pyomo.contrib.solver.common.util import (
NoDualsError,
NoReducedCostsError,
NoSolutionError,
IncompatibleModelError,
)
from pyomo.contrib.solver.common.solution_loader import SolutionLoaderBase
from .gurobi_direct_base import (
GurobiDirectBase,
gurobipy,
GurobiDirectSolutionLoaderBase,
)
import logging


logger = logging.getLogger(__name__)


class GurobiDirectSolutionLoader(GurobiDirectSolutionLoaderBase):
Copy link
Member

Choose a reason for hiding this comment

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

Why is this derived class needed? Can't it be merged into the base class (and the base class renamed GurobiDirectSolutionLoader)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In the case of the persistent solution loader, we do not want to dispose of the gurobipy model when the solution loader is garbage collected. The solver interface needs to hang on to it for future solves.

def __init__(self, solver_model, pyomo_vars, gurobi_vars, con_map) -> None:
super().__init__(solver_model)
self._pyomo_vars = pyomo_vars
self._gurobi_vars = gurobi_vars
self._con_map = con_map

def _var_pair_iter(self):
return zip(self._pyomo_vars, self._gurobi_vars)

def _get_var_map(self):
return ComponentMap(self._var_pair_iter())

def _get_con_map(self):
return self._con_map

def __del__(self):
super().__del__()
if python_is_shutting_down():
return
# Free the associated model
if self._solver_model is not None:
self._var_map = None
self._con_map = None
# explicitly release the model
self._solver_model.dispose()
self._solver_model = None


class GurobiDirect(GurobiDirectBase):
_minimum_version = (9, 0, 0)

def __init__(self, **kwds):
super().__init__(**kwds)
self._gurobi_vars = None
self._pyomo_vars = None
Comment on lines +71 to +72
Copy link
Member

Choose a reason for hiding this comment

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

Why are these being stored on the solver interface? Direct is supposed to be non-persistent, and adding this seems to violate that principle.

This seems to only be needed for _pyomo_gurobi_var_iter(), which is in turn only needed by _mipstart. I propose that both the attributes and _pyomo_gurobi)var_iter() be removed, and instead _mipstart be updated to accept the SolutionLoader as an argument. As the SolutionLoader is responsible for remembering the mapping between Pyomo and Gurobi, that feels like the natural place to query to get the mapping within _mipstart.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I like that suggestion. Will do.

Copy link
Contributor

@emma58 emma58 Nov 14, 2025

Choose a reason for hiding this comment

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

Is this going to work for callbacks too? They have the same issue, that they need this state (and actually more) to work... So I think there may be a bigger design question lurking here: For example, they need the gurobipy model to be somewhere they can get to: In persistent, that is currently also sitting on the solver interface.

Copy link
Contributor

Choose a reason for hiding this comment

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

(I was originally thinking that this kind of state could be added in a contextmanager kind of style, and removed before solve is over, regardless of what happens. But that certainly might not be the right answer.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Excellent points, @emma58. I think this is worth further discussion.


def _pyomo_gurobi_var_iter(self):
return zip(self._pyomo_vars, self._gurobi_vars)

def _create_solver_model(self, pyomo_model, config):
timer = config.timer

timer.start('compile_model')
repn = LinearStandardFormCompiler().write(
pyomo_model, mixed_form=True, set_sense=None
)
timer.stop('compile_model')

if len(repn.objectives) > 1:
raise IncompatibleModelError(
f"The {self.__class__.__name__} solver only supports models "
f"with zero or one objectives (received {len(repn.objectives)})."
)

timer.start('prepare_matrices')
inf = float('inf')
ninf = -inf
bounds = list(map(operator.attrgetter('bounds'), repn.columns))
lb = [ninf if _b is None else _b for _b in map(operator.itemgetter(0), bounds)]
ub = [inf if _b is None else _b for _b in map(operator.itemgetter(1), bounds)]
CON = gurobipy.GRB.CONTINUOUS
BIN = gurobipy.GRB.BINARY
INT = gurobipy.GRB.INTEGER
vtype = [
(
CON
if v.is_continuous()
else BIN if v.is_binary() else INT if v.is_integer() else '?'
)
for v in repn.columns
]
sense_type = list('=<>') # Note: ordering matches 0, 1, -1
sense = [sense_type[r[1]] for r in repn.rows]
timer.stop('prepare_matrices')

gurobi_model = gurobipy.Model(env=self.env())

timer.start('transfer_model')
x = gurobi_model.addMVar(
len(repn.columns),
lb=lb,
ub=ub,
obj=repn.c.todense()[0] if repn.c.shape[0] else 0,
vtype=vtype,
)
A = gurobi_model.addMConstr(repn.A, x, sense, repn.rhs)
if repn.c.shape[0]:
gurobi_model.setAttr('ObjCon', repn.c_offset[0])
gurobi_model.setAttr('ModelSense', int(repn.objectives[0].sense))
# Note: calling gurobi_model.update() here is not
# necessary (it will happen as part of optimize()):
# gurobi_model.update()
timer.stop('transfer_model')

self._pyomo_vars = repn.columns
timer.start('tolist')
self._gurobi_vars = x.tolist()
timer.stop('tolist')

timer.start('create maps')
timer.start('con map')
con_map = {}
for row, gc in zip(repn.rows, A.tolist()):
pc = row.constraint
if pc in con_map:
# range constraint
con_map[pc] = (con_map[pc], gc)
else:
con_map[pc] = gc
timer.stop('con map')
timer.stop('create maps')
solution_loader = GurobiDirectSolutionLoader(
solver_model=gurobi_model,
pyomo_vars=self._pyomo_vars,
gurobi_vars=self._gurobi_vars,
con_map=con_map,
)
has_obj = len(repn.objectives) > 0

return gurobi_model, solution_loader, has_obj
Loading
Loading