-
Notifications
You must be signed in to change notification settings - Fork 561
Observer gurobi refactor #3698
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Observer gurobi refactor #3698
Changes from all commits
070811d
27a3a14
d70dbb5
5b1d3f9
4818130
7998fda
909be88
862c387
8f7a61e
92fa4f5
8a9fc46
7249b19
25c48e7
df56887
275d848
cfa8633
1788ff3
873f176
a43a38b
e76baae
c2a0177
2c7208f
19fecf7
31e7e97
576a217
ce99fb2
066e4fd
6ccbaef
cf000a1
9abb4bf
7b20095
43a864f
5073ba0
5e280dc
8bff218
d9dc14d
1c26ae3
a4858ca
122511b
71963f1
8712884
8a729c0
7069f89
93bb118
7724678
9bfad14
2ab061a
f3d7f3a
d08d993
3ee4e99
b9ca201
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
| 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): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like that suggestion. Will do.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do.
There was a problem hiding this comment.
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__.pyfile.