pyoframe 0.2.0__py3-none-any.whl → 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyoframe/__init__.py +21 -14
- pyoframe/_arithmetic.py +346 -238
- pyoframe/_constants.py +463 -0
- pyoframe/_core.py +2652 -0
- pyoframe/_model.py +598 -0
- pyoframe/_model_element.py +189 -0
- pyoframe/_monkey_patch.py +82 -0
- pyoframe/{objective.py → _objective.py} +50 -17
- pyoframe/{util.py → _utils.py} +108 -129
- pyoframe/_version.py +16 -3
- {pyoframe-0.2.0.dist-info → pyoframe-1.0.0.dist-info}/METADATA +37 -31
- pyoframe-1.0.0.dist-info/RECORD +15 -0
- pyoframe/constants.py +0 -140
- pyoframe/core.py +0 -1794
- pyoframe/model.py +0 -408
- pyoframe/model_element.py +0 -184
- pyoframe/monkey_patch.py +0 -54
- pyoframe-0.2.0.dist-info/RECORD +0 -15
- {pyoframe-0.2.0.dist-info → pyoframe-1.0.0.dist-info}/WHEEL +0 -0
- {pyoframe-0.2.0.dist-info → pyoframe-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {pyoframe-0.2.0.dist-info → pyoframe-1.0.0.dist-info}/top_level.txt +0 -0
pyoframe/_model.py
ADDED
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
"""Defines the `Model` class for Pyoframe."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import polars as pl
|
|
10
|
+
import pyoptinterface as poi
|
|
11
|
+
|
|
12
|
+
from pyoframe._constants import (
|
|
13
|
+
CONST_TERM,
|
|
14
|
+
SUPPORTED_SOLVER_TYPES,
|
|
15
|
+
SUPPORTED_SOLVERS,
|
|
16
|
+
Config,
|
|
17
|
+
ObjSense,
|
|
18
|
+
ObjSenseValue,
|
|
19
|
+
PyoframeError,
|
|
20
|
+
VType,
|
|
21
|
+
_Solver,
|
|
22
|
+
)
|
|
23
|
+
from pyoframe._core import Constraint, Operable, Variable
|
|
24
|
+
from pyoframe._model_element import BaseBlock
|
|
25
|
+
from pyoframe._objective import Objective
|
|
26
|
+
from pyoframe._utils import Container, NamedVariableMapper, for_solvers, get_obj_repr
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
29
|
+
from collections.abc import Generator
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Model:
|
|
33
|
+
"""The founding block of any Pyoframe optimization model onto which variables, constraints, and an objective can be added.
|
|
34
|
+
|
|
35
|
+
Parameters:
|
|
36
|
+
solver:
|
|
37
|
+
The solver to use. If `None`, Pyoframe will try to use whichever solver is installed
|
|
38
|
+
(unless [Config.default_solver][pyoframe._Config.default_solver] was changed from its default value of `auto`).
|
|
39
|
+
solver_env:
|
|
40
|
+
Gurobi only: a dictionary of parameters to set when creating the Gurobi environment.
|
|
41
|
+
name:
|
|
42
|
+
The name of the model. Currently it is not used for much.
|
|
43
|
+
solver_uses_variable_names:
|
|
44
|
+
If `True`, the solver will use your custom variable names in its outputs (e.g. during [`Model.write()`][pyoframe.Model.write]).
|
|
45
|
+
This can be useful for debugging `.lp`, `.sol`, and `.ilp` files, but may worsen performance.
|
|
46
|
+
print_uses_variable_names:
|
|
47
|
+
If `True`, pyoframe will use your custom variables names when printing elements of the model to the console.
|
|
48
|
+
This is useful for debugging, but may slightly worsen performance.
|
|
49
|
+
sense:
|
|
50
|
+
Either "min" or "max". Indicates whether it's a minimization or maximization problem.
|
|
51
|
+
Typically, this parameter can be omitted (`None`) as it will automatically be
|
|
52
|
+
set when the objective is set using `.minimize` or `.maximize`.
|
|
53
|
+
|
|
54
|
+
Examples:
|
|
55
|
+
>>> m = pf.Model()
|
|
56
|
+
>>> m.X = pf.Variable()
|
|
57
|
+
>>> m.my_constraint = m.X <= 10
|
|
58
|
+
>>> m
|
|
59
|
+
<Model vars=1 constrs=1 has_objective=False solver=gurobi>
|
|
60
|
+
|
|
61
|
+
Use `solver_env` to, for example, connect to a Gurobi Compute Server:
|
|
62
|
+
>>> m = pf.Model(
|
|
63
|
+
... "gurobi",
|
|
64
|
+
... solver_env=dict(ComputeServer="myserver", ServerPassword="mypassword"),
|
|
65
|
+
... )
|
|
66
|
+
Traceback (most recent call last):
|
|
67
|
+
...
|
|
68
|
+
RuntimeError: Could not resolve host: myserver (code 6, command POST http://myserver/api/v1/cluster/jobs)
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
_reserved_attributes = [
|
|
72
|
+
"_variables",
|
|
73
|
+
"_constraints",
|
|
74
|
+
"_objective",
|
|
75
|
+
"objective",
|
|
76
|
+
"_var_map",
|
|
77
|
+
"name",
|
|
78
|
+
"solver",
|
|
79
|
+
"_poi",
|
|
80
|
+
"_params",
|
|
81
|
+
"params",
|
|
82
|
+
"_attr",
|
|
83
|
+
"attr",
|
|
84
|
+
"sense",
|
|
85
|
+
"_solver_uses_variable_names",
|
|
86
|
+
"ONE",
|
|
87
|
+
"solver_name",
|
|
88
|
+
"minimize",
|
|
89
|
+
"maximize",
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
def __init__(
|
|
93
|
+
self,
|
|
94
|
+
solver: SUPPORTED_SOLVER_TYPES | _Solver | None = None,
|
|
95
|
+
solver_env: dict[str, str] | None = None,
|
|
96
|
+
*,
|
|
97
|
+
name: str | None = None,
|
|
98
|
+
solver_uses_variable_names: bool = False,
|
|
99
|
+
print_uses_variable_names: bool = True,
|
|
100
|
+
sense: ObjSense | ObjSenseValue | None = None,
|
|
101
|
+
):
|
|
102
|
+
self._poi, self.solver = Model._create_poi_model(solver, solver_env)
|
|
103
|
+
self.solver_name: str = self.solver.name
|
|
104
|
+
self._variables: list[Variable] = []
|
|
105
|
+
self._constraints: list[Constraint] = []
|
|
106
|
+
self.sense: ObjSense | None = ObjSense(sense) if sense is not None else None
|
|
107
|
+
self._objective: Objective | None = None
|
|
108
|
+
self._var_map = NamedVariableMapper() if print_uses_variable_names else None
|
|
109
|
+
self.name: str | None = name
|
|
110
|
+
|
|
111
|
+
self._params = Container(self._set_param, self._get_param)
|
|
112
|
+
self._attr = Container(self._set_attr, self._get_attr)
|
|
113
|
+
self._solver_uses_variable_names = solver_uses_variable_names
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def poi(self):
|
|
117
|
+
"""The underlying PyOptInterface model used to interact with the solver.
|
|
118
|
+
|
|
119
|
+
Modifying the underlying model directly is not recommended and may lead to unexpected behaviors.
|
|
120
|
+
"""
|
|
121
|
+
return self._poi
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def solver_uses_variable_names(self):
|
|
125
|
+
"""Whether to pass human-readable variable names to the solver."""
|
|
126
|
+
return self._solver_uses_variable_names
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def attr(self) -> Container:
|
|
130
|
+
"""An object that allows reading and writing model attributes.
|
|
131
|
+
|
|
132
|
+
Several model attributes are common across all solvers making it easy to switch between solvers (see supported attributes for
|
|
133
|
+
[Gurobi](https://metab0t.github.io/PyOptInterface/gurobi.html#supported-model-attribute),
|
|
134
|
+
[HiGHS](https://metab0t.github.io/PyOptInterface/highs.html),
|
|
135
|
+
[Ipopt](https://metab0t.github.io/PyOptInterface/ipopt.html)), and
|
|
136
|
+
[COPT](https://metab0t.github.io/PyOptInterface/copt.html).
|
|
137
|
+
|
|
138
|
+
We additionally support all of [Gurobi's attributes](https://docs.gurobi.com/projects/optimizer/en/current/reference/attributes.html#sec:Attributes) when using Gurobi.
|
|
139
|
+
|
|
140
|
+
Examples:
|
|
141
|
+
>>> m = pf.Model()
|
|
142
|
+
>>> m.v = pf.Variable(lb=1, ub=1, vtype="integer")
|
|
143
|
+
>>> m.attr.Silent = True # Prevent solver output from being printed
|
|
144
|
+
>>> m.optimize()
|
|
145
|
+
>>> m.attr.TerminationStatus
|
|
146
|
+
<TerminationStatusCode.OPTIMAL: 2>
|
|
147
|
+
|
|
148
|
+
Some attributes, like `NumVars`, are solver-specific.
|
|
149
|
+
>>> m = pf.Model("gurobi")
|
|
150
|
+
>>> m.attr.NumConstrs
|
|
151
|
+
0
|
|
152
|
+
>>> m = pf.Model("highs")
|
|
153
|
+
>>> m.attr.NumConstrs
|
|
154
|
+
Traceback (most recent call last):
|
|
155
|
+
...
|
|
156
|
+
KeyError: 'NumConstrs'
|
|
157
|
+
|
|
158
|
+
See Also:
|
|
159
|
+
[Variable.attr][pyoframe.Variable.attr] for setting variable attributes and
|
|
160
|
+
[Constraint.attr][pyoframe.Constraint.attr] for setting constraint attributes.
|
|
161
|
+
"""
|
|
162
|
+
return self._attr
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def params(self) -> Container:
|
|
166
|
+
"""An object that allows reading and writing solver-specific parameters.
|
|
167
|
+
|
|
168
|
+
See the list of available parameters for
|
|
169
|
+
[Gurobi](https://docs.gurobi.com/projects/optimizer/en/current/reference/parameters.html#sec:Parameters),
|
|
170
|
+
[HiGHS](https://ergo-code.github.io/HiGHS/stable/options/definitions/),
|
|
171
|
+
[Ipopt](https://coin-or.github.io/Ipopt/OPTIONS.html),
|
|
172
|
+
and [COPT](https://guide.coap.online/copt/en-doc/parameter.html).
|
|
173
|
+
|
|
174
|
+
Examples:
|
|
175
|
+
For example, if you'd like to use Gurobi's barrier method, you can set the `Method` parameter:
|
|
176
|
+
>>> m = pf.Model("gurobi")
|
|
177
|
+
>>> m.params.Method = 2
|
|
178
|
+
"""
|
|
179
|
+
return self._params
|
|
180
|
+
|
|
181
|
+
@classmethod
|
|
182
|
+
def _create_poi_model(
|
|
183
|
+
cls, solver: str | _Solver | None, solver_env: dict[str, str] | None
|
|
184
|
+
):
|
|
185
|
+
if solver is None:
|
|
186
|
+
if Config.default_solver == "raise":
|
|
187
|
+
raise ValueError(
|
|
188
|
+
"No solver specified during model construction and automatic solver detection is disabled."
|
|
189
|
+
)
|
|
190
|
+
elif Config.default_solver == "auto":
|
|
191
|
+
for solver_option in SUPPORTED_SOLVERS:
|
|
192
|
+
try:
|
|
193
|
+
return cls._create_poi_model(solver_option, solver_env)
|
|
194
|
+
except RuntimeError:
|
|
195
|
+
pass
|
|
196
|
+
raise RuntimeError(
|
|
197
|
+
'Could not automatically find a solver. Is one installed? If so, specify which one: e.g. Model("gurobi")'
|
|
198
|
+
)
|
|
199
|
+
elif isinstance(Config.default_solver, (_Solver, str)):
|
|
200
|
+
solver = Config.default_solver
|
|
201
|
+
else:
|
|
202
|
+
raise ValueError(
|
|
203
|
+
f"Config.default_solver has an invalid value: {Config.default_solver}."
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
if isinstance(solver, str):
|
|
207
|
+
solver = solver.lower()
|
|
208
|
+
for s in SUPPORTED_SOLVERS:
|
|
209
|
+
if s.name == solver:
|
|
210
|
+
solver = s
|
|
211
|
+
break
|
|
212
|
+
else:
|
|
213
|
+
raise ValueError(
|
|
214
|
+
f"Unsupported solver: '{solver}'. Supported solvers are: {', '.join(s.name for s in SUPPORTED_SOLVERS)}."
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
if solver.name == "gurobi":
|
|
218
|
+
from pyoptinterface import gurobi
|
|
219
|
+
|
|
220
|
+
if solver_env is None:
|
|
221
|
+
env = gurobi.Env()
|
|
222
|
+
else:
|
|
223
|
+
env = gurobi.Env(empty=True)
|
|
224
|
+
for key, value in solver_env.items():
|
|
225
|
+
env.set_raw_parameter(key, value)
|
|
226
|
+
env.start()
|
|
227
|
+
model = gurobi.Model(env)
|
|
228
|
+
elif solver.name == "highs":
|
|
229
|
+
from pyoptinterface import highs
|
|
230
|
+
|
|
231
|
+
model = highs.Model()
|
|
232
|
+
elif solver.name == "ipopt":
|
|
233
|
+
try:
|
|
234
|
+
from pyoptinterface import ipopt
|
|
235
|
+
except ModuleNotFoundError as e: # pragma: no cover
|
|
236
|
+
raise ModuleNotFoundError(
|
|
237
|
+
"Failed to import the Ipopt solver. Did you run `pip install pyoptinterface[ipopt]`?"
|
|
238
|
+
) from e
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
model = ipopt.Model()
|
|
242
|
+
except RuntimeError as e: # pragma: no cover
|
|
243
|
+
if "IPOPT library is not loaded" in str(e):
|
|
244
|
+
raise RuntimeError(
|
|
245
|
+
"Could not find the Ipopt solver. Are you sure you've properly installed it and added it to your PATH?"
|
|
246
|
+
) from e
|
|
247
|
+
raise e
|
|
248
|
+
elif solver.name == "copt":
|
|
249
|
+
from pyoptinterface import copt
|
|
250
|
+
|
|
251
|
+
if solver_env is None:
|
|
252
|
+
env = copt.Env()
|
|
253
|
+
else:
|
|
254
|
+
# COPT uses EnvConfig for configuration
|
|
255
|
+
env_config = copt.EnvConfig()
|
|
256
|
+
for key, value in solver_env.items():
|
|
257
|
+
env_config.set(key, value)
|
|
258
|
+
env = copt.Env(env_config)
|
|
259
|
+
model = copt.Model(env)
|
|
260
|
+
else:
|
|
261
|
+
raise ValueError(
|
|
262
|
+
f"Solver {solver} not recognized or supported."
|
|
263
|
+
) # pragma: no cover
|
|
264
|
+
|
|
265
|
+
constant_var = model.add_variable(lb=1, ub=1, name="ONE")
|
|
266
|
+
assert constant_var.index == CONST_TERM, (
|
|
267
|
+
"The first variable should have index 0."
|
|
268
|
+
)
|
|
269
|
+
return model, solver
|
|
270
|
+
|
|
271
|
+
@property
|
|
272
|
+
def variables(self) -> list[Variable]:
|
|
273
|
+
"""Returns a list of the model's variables."""
|
|
274
|
+
return self._variables
|
|
275
|
+
|
|
276
|
+
@property
|
|
277
|
+
def binary_variables(self) -> Generator[Variable]:
|
|
278
|
+
"""Returns the model's binary variables.
|
|
279
|
+
|
|
280
|
+
Examples:
|
|
281
|
+
>>> m = pf.Model()
|
|
282
|
+
>>> m.X = pf.Variable(vtype=pf.VType.BINARY)
|
|
283
|
+
>>> m.Y = pf.Variable()
|
|
284
|
+
>>> len(list(m.binary_variables))
|
|
285
|
+
1
|
|
286
|
+
"""
|
|
287
|
+
return (v for v in self.variables if v.vtype == VType.BINARY)
|
|
288
|
+
|
|
289
|
+
@property
|
|
290
|
+
def integer_variables(self) -> Generator[Variable]:
|
|
291
|
+
"""Returns the model's integer variables.
|
|
292
|
+
|
|
293
|
+
Examples:
|
|
294
|
+
>>> m = pf.Model()
|
|
295
|
+
>>> m.X = pf.Variable(vtype=pf.VType.INTEGER)
|
|
296
|
+
>>> m.Y = pf.Variable()
|
|
297
|
+
>>> len(list(m.integer_variables))
|
|
298
|
+
1
|
|
299
|
+
"""
|
|
300
|
+
return (v for v in self.variables if v.vtype == VType.INTEGER)
|
|
301
|
+
|
|
302
|
+
@property
|
|
303
|
+
def constraints(self) -> list[Constraint]:
|
|
304
|
+
"""Returns the model's constraints."""
|
|
305
|
+
return self._constraints
|
|
306
|
+
|
|
307
|
+
@property
|
|
308
|
+
def has_objective(self) -> bool:
|
|
309
|
+
"""Returns whether the model's objective has been defined.
|
|
310
|
+
|
|
311
|
+
Examples:
|
|
312
|
+
>>> m = pf.Model()
|
|
313
|
+
>>> m.has_objective
|
|
314
|
+
False
|
|
315
|
+
>>> m.X = pf.Variable()
|
|
316
|
+
>>> m.maximize = m.X
|
|
317
|
+
>>> m.has_objective
|
|
318
|
+
True
|
|
319
|
+
"""
|
|
320
|
+
return self._objective is not None
|
|
321
|
+
|
|
322
|
+
@property
|
|
323
|
+
def objective(self) -> Objective:
|
|
324
|
+
"""Returns the model's objective.
|
|
325
|
+
|
|
326
|
+
Raises:
|
|
327
|
+
ValueError: If the objective has not been defined.
|
|
328
|
+
|
|
329
|
+
Examples:
|
|
330
|
+
>>> m = pf.Model()
|
|
331
|
+
>>> m.X = pf.Variable()
|
|
332
|
+
>>> m.objective
|
|
333
|
+
Traceback (most recent call last):
|
|
334
|
+
...
|
|
335
|
+
ValueError: Objective is not defined.
|
|
336
|
+
>>> m.maximize = m.X
|
|
337
|
+
>>> m.objective
|
|
338
|
+
<Objective terms=1 type=linear>
|
|
339
|
+
X
|
|
340
|
+
|
|
341
|
+
See Also:
|
|
342
|
+
[`Model.has_objective`][pyoframe.Model.has_objective]
|
|
343
|
+
"""
|
|
344
|
+
if self._objective is None:
|
|
345
|
+
raise ValueError("Objective is not defined.")
|
|
346
|
+
return self._objective
|
|
347
|
+
|
|
348
|
+
@objective.setter
|
|
349
|
+
def objective(self, value: Operable):
|
|
350
|
+
if self.has_objective and (
|
|
351
|
+
not isinstance(value, Objective) or not value._constructive
|
|
352
|
+
):
|
|
353
|
+
raise ValueError("An objective already exists. Use += or -= to modify it.")
|
|
354
|
+
if not isinstance(value, Objective):
|
|
355
|
+
value = Objective(value)
|
|
356
|
+
self._objective = value
|
|
357
|
+
value._on_add_to_model(self, "objective")
|
|
358
|
+
|
|
359
|
+
@property
|
|
360
|
+
def minimize(self) -> Objective | None:
|
|
361
|
+
"""Sets or gets the model's objective for minimization problems."""
|
|
362
|
+
if self.sense != ObjSense.MIN:
|
|
363
|
+
raise ValueError("Can't get .minimize in a maximization problem.")
|
|
364
|
+
return self._objective
|
|
365
|
+
|
|
366
|
+
@minimize.setter
|
|
367
|
+
def minimize(self, value: Operable):
|
|
368
|
+
if self.sense is None:
|
|
369
|
+
self.sense = ObjSense.MIN
|
|
370
|
+
if self.sense != ObjSense.MIN:
|
|
371
|
+
raise ValueError("Can't set .minimize in a maximization problem.")
|
|
372
|
+
self.objective = value
|
|
373
|
+
|
|
374
|
+
@property
|
|
375
|
+
def maximize(self) -> Objective | None:
|
|
376
|
+
"""Sets or gets the model's objective for maximization problems."""
|
|
377
|
+
if self.sense != ObjSense.MAX:
|
|
378
|
+
raise ValueError("Can't get .maximize in a minimization problem.")
|
|
379
|
+
return self._objective
|
|
380
|
+
|
|
381
|
+
@maximize.setter
|
|
382
|
+
def maximize(self, value: Operable):
|
|
383
|
+
if self.sense is None:
|
|
384
|
+
self.sense = ObjSense.MAX
|
|
385
|
+
if self.sense != ObjSense.MAX:
|
|
386
|
+
raise ValueError("Can't set .maximize in a minimization problem.")
|
|
387
|
+
self.objective = value
|
|
388
|
+
|
|
389
|
+
def __setattr__(self, __name: str, __value: Any) -> None:
|
|
390
|
+
if __name not in Model._reserved_attributes and not isinstance(
|
|
391
|
+
__value, (BaseBlock, pl.DataFrame, pd.DataFrame)
|
|
392
|
+
):
|
|
393
|
+
raise PyoframeError(
|
|
394
|
+
f"Cannot set attribute '{__name}' on the model because it isn't a subtype of BaseBlock (e.g. Variable, Constraint, ...)"
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
if isinstance(__value, BaseBlock) and __name not in Model._reserved_attributes:
|
|
398
|
+
if __value._get_id_column_name() is not None:
|
|
399
|
+
assert not hasattr(self, __name), (
|
|
400
|
+
f"Cannot create {__name} since it was already created."
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
__value._on_add_to_model(self, __name)
|
|
404
|
+
|
|
405
|
+
if isinstance(__value, Variable):
|
|
406
|
+
self._variables.append(__value)
|
|
407
|
+
if self._var_map is not None:
|
|
408
|
+
self._var_map.add(__value)
|
|
409
|
+
elif isinstance(__value, Constraint):
|
|
410
|
+
self._constraints.append(__value)
|
|
411
|
+
return super().__setattr__(__name, __value)
|
|
412
|
+
|
|
413
|
+
# Defining a custom __getattribute__ prevents type checkers from complaining about attribute access
|
|
414
|
+
def __getattribute__(self, name: str) -> Any:
|
|
415
|
+
return super().__getattribute__(name)
|
|
416
|
+
|
|
417
|
+
def __repr__(self) -> str:
|
|
418
|
+
return get_obj_repr(
|
|
419
|
+
self,
|
|
420
|
+
self.name,
|
|
421
|
+
vars=len(self.variables),
|
|
422
|
+
constrs=len(self.constraints),
|
|
423
|
+
has_objective=self.has_objective,
|
|
424
|
+
solver=self.solver_name,
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
def write(self, file_path: Path | str, pretty: bool = False):
|
|
428
|
+
"""Outputs the model or the solution to a file (e.g. a `.lp`, `.sol`, `.mps`, or `.ilp` file).
|
|
429
|
+
|
|
430
|
+
These files can be useful for manually debugging a model.
|
|
431
|
+
Consult your solver documentation to learn more.
|
|
432
|
+
|
|
433
|
+
When creating your model, set [`solver_uses_variable_names`][pyoframe.Model]
|
|
434
|
+
to make the outputed file human-readable.
|
|
435
|
+
|
|
436
|
+
```python
|
|
437
|
+
m = pf.Model(solver_uses_variable_names=True)
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
For Gurobi, `solver_uses_variable_names=True` is mandatory when using
|
|
441
|
+
.write(). This may become mandatory for other solvers too without notice.
|
|
442
|
+
|
|
443
|
+
Parameters:
|
|
444
|
+
file_path:
|
|
445
|
+
The path to the file to write to.
|
|
446
|
+
pretty:
|
|
447
|
+
Only used when writing .sol files in HiGHS. If `True`, will use HiGH's pretty print columnar style which contains more information.
|
|
448
|
+
"""
|
|
449
|
+
if not self.solver.supports_write:
|
|
450
|
+
raise NotImplementedError(f"{self.solver.name} does not support .write()")
|
|
451
|
+
if (
|
|
452
|
+
not self.solver_uses_variable_names
|
|
453
|
+
and self.solver.accelerate_with_repeat_names
|
|
454
|
+
):
|
|
455
|
+
raise ValueError(
|
|
456
|
+
f"{self.solver.name} requires solver_uses_variable_names=True to use .write()"
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
file_path = Path(file_path)
|
|
460
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
461
|
+
|
|
462
|
+
kwargs = {}
|
|
463
|
+
if self.solver.name == "highs":
|
|
464
|
+
if self.solver_uses_variable_names:
|
|
465
|
+
self.params.write_solution_style = 1
|
|
466
|
+
kwargs["pretty"] = pretty
|
|
467
|
+
self.poi.write(str(file_path), **kwargs)
|
|
468
|
+
|
|
469
|
+
def optimize(self):
|
|
470
|
+
"""Optimizes the model using your selected solver (e.g. Gurobi, HiGHS)."""
|
|
471
|
+
self.poi.optimize()
|
|
472
|
+
|
|
473
|
+
@for_solvers("gurobi")
|
|
474
|
+
def convert_to_fixed(self) -> None:
|
|
475
|
+
"""Gurobi only: Converts a mixed integer program into a continuous one by fixing all the non-continuous variables to their solution values.
|
|
476
|
+
|
|
477
|
+
!!! warning "Gurobi only"
|
|
478
|
+
This method only works with the Gurobi solver. Open an issue if you'd like to see support for other solvers.
|
|
479
|
+
|
|
480
|
+
Examples:
|
|
481
|
+
>>> m = pf.Model("gurobi")
|
|
482
|
+
>>> m.X = pf.Variable(vtype=pf.VType.BINARY, lb=0)
|
|
483
|
+
>>> m.Y = pf.Variable(vtype=pf.VType.INTEGER, lb=0)
|
|
484
|
+
>>> m.Z = pf.Variable(lb=0)
|
|
485
|
+
>>> m.my_constraint = m.X + m.Y + m.Z <= 10
|
|
486
|
+
>>> m.maximize = 3 * m.X + 2 * m.Y + m.Z
|
|
487
|
+
>>> m.optimize()
|
|
488
|
+
>>> m.X.solution, m.Y.solution, m.Z.solution
|
|
489
|
+
(1, 9, 0.0)
|
|
490
|
+
>>> m.my_constraint.dual
|
|
491
|
+
Traceback (most recent call last):
|
|
492
|
+
...
|
|
493
|
+
RuntimeError: Unable to retrieve attribute 'Pi'
|
|
494
|
+
>>> m.convert_to_fixed()
|
|
495
|
+
>>> m.optimize()
|
|
496
|
+
>>> m.my_constraint.dual
|
|
497
|
+
1.0
|
|
498
|
+
|
|
499
|
+
Only works for Gurobi:
|
|
500
|
+
|
|
501
|
+
>>> m = pf.Model("highs")
|
|
502
|
+
>>> m.convert_to_fixed()
|
|
503
|
+
Traceback (most recent call last):
|
|
504
|
+
...
|
|
505
|
+
NotImplementedError: Method 'convert_to_fixed' is not implemented for solver 'highs'.
|
|
506
|
+
"""
|
|
507
|
+
self.poi._converttofixed()
|
|
508
|
+
|
|
509
|
+
@for_solvers("gurobi", "copt")
|
|
510
|
+
def compute_IIS(self):
|
|
511
|
+
"""Gurobi and COPT only: Computes the Irreducible Infeasible Set (IIS) of the model.
|
|
512
|
+
|
|
513
|
+
!!! warning "Gurobi and COPT only"
|
|
514
|
+
This method only works with the Gurobi and COPT solver. Open an issue if you'd like to see support for other solvers.
|
|
515
|
+
|
|
516
|
+
Examples:
|
|
517
|
+
>>> m = pf.Model("gurobi")
|
|
518
|
+
>>> m.X = pf.Variable(lb=0, ub=2)
|
|
519
|
+
>>> m.Y = pf.Variable(lb=0, ub=2)
|
|
520
|
+
>>> m.bad_constraint = m.X >= 3
|
|
521
|
+
>>> m.minimize = m.X + m.Y
|
|
522
|
+
>>> m.optimize()
|
|
523
|
+
>>> m.attr.TerminationStatus
|
|
524
|
+
<TerminationStatusCode.INFEASIBLE: 3>
|
|
525
|
+
>>> m.bad_constraint.attr.IIS
|
|
526
|
+
Traceback (most recent call last):
|
|
527
|
+
...
|
|
528
|
+
RuntimeError: Unable to retrieve attribute 'IISConstr'
|
|
529
|
+
>>> m.compute_IIS()
|
|
530
|
+
>>> m.bad_constraint.attr.IIS
|
|
531
|
+
True
|
|
532
|
+
"""
|
|
533
|
+
self.poi.computeIIS()
|
|
534
|
+
|
|
535
|
+
def dispose(self):
|
|
536
|
+
"""Disposes of the model and cleans up the solver environment.
|
|
537
|
+
|
|
538
|
+
When using Gurobi compute server, this cleanup will
|
|
539
|
+
ensure your run is not marked as 'ABORTED'.
|
|
540
|
+
|
|
541
|
+
Note that once the model is disposed, it cannot be used anymore.
|
|
542
|
+
|
|
543
|
+
Examples:
|
|
544
|
+
>>> m = pf.Model()
|
|
545
|
+
>>> m.X = pf.Variable(ub=1)
|
|
546
|
+
>>> m.maximize = m.X
|
|
547
|
+
>>> m.optimize()
|
|
548
|
+
>>> m.X.solution
|
|
549
|
+
1.0
|
|
550
|
+
>>> m.dispose()
|
|
551
|
+
"""
|
|
552
|
+
env = None
|
|
553
|
+
if hasattr(self.poi, "_env"):
|
|
554
|
+
env = self.poi._env
|
|
555
|
+
self.poi.close()
|
|
556
|
+
if env is not None:
|
|
557
|
+
env.close()
|
|
558
|
+
|
|
559
|
+
def __del__(self):
|
|
560
|
+
# This ensures that the model is closed *before* the environment is. This avoids the Gurobi warning:
|
|
561
|
+
# Warning: environment still referenced so free is deferred (Continue to use WLS)
|
|
562
|
+
# I include the hasattr check to avoid errors in case __init__ failed and poi was never set.
|
|
563
|
+
if hasattr(self, "poi"):
|
|
564
|
+
self.poi.close()
|
|
565
|
+
|
|
566
|
+
def _set_param(self, name, value):
|
|
567
|
+
try:
|
|
568
|
+
self.poi.set_raw_parameter(name, value)
|
|
569
|
+
except KeyError as e:
|
|
570
|
+
raise KeyError(
|
|
571
|
+
f"Unknown parameter: '{name}'. See https://bravos-power.github.io/pyoframe/latest/learn/getting-started/solver-access/ for a list of valid parameters."
|
|
572
|
+
) from e
|
|
573
|
+
|
|
574
|
+
def _get_param(self, name):
|
|
575
|
+
try:
|
|
576
|
+
return self.poi.get_raw_parameter(name)
|
|
577
|
+
except KeyError as e:
|
|
578
|
+
raise KeyError(
|
|
579
|
+
f"Unknown parameter: '{name}'. See https://bravos-power.github.io/pyoframe/latest/learn/getting-started/solver-access/ for a list of valid parameters."
|
|
580
|
+
) from e
|
|
581
|
+
|
|
582
|
+
def _set_attr(self, name, value):
|
|
583
|
+
try:
|
|
584
|
+
self.poi.set_model_attribute(poi.ModelAttribute[name], value)
|
|
585
|
+
except KeyError as e:
|
|
586
|
+
if self.solver.name == "gurobi":
|
|
587
|
+
self.poi.set_model_raw_attribute(name, value)
|
|
588
|
+
else:
|
|
589
|
+
raise e
|
|
590
|
+
|
|
591
|
+
def _get_attr(self, name):
|
|
592
|
+
try:
|
|
593
|
+
return self.poi.get_model_attribute(poi.ModelAttribute[name])
|
|
594
|
+
except KeyError as e:
|
|
595
|
+
if self.solver.name == "gurobi":
|
|
596
|
+
return self.poi.get_model_raw_attribute(name)
|
|
597
|
+
else:
|
|
598
|
+
raise e
|