pyoframe 0.0.5__tar.gz → 0.0.6__tar.gz
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-0.0.5/src/pyoframe.egg-info → pyoframe-0.0.6}/PKG-INFO +1 -1
- {pyoframe-0.0.5 → pyoframe-0.0.6}/pyproject.toml +1 -1
- {pyoframe-0.0.5 → pyoframe-0.0.6}/src/pyoframe/_arithmetic.py +1 -1
- {pyoframe-0.0.5 → pyoframe-0.0.6}/src/pyoframe/constants.py +5 -2
- {pyoframe-0.0.5 → pyoframe-0.0.6}/src/pyoframe/core.py +23 -9
- {pyoframe-0.0.5 → pyoframe-0.0.6}/src/pyoframe/io.py +15 -5
- {pyoframe-0.0.5 → pyoframe-0.0.6}/src/pyoframe/io_mappers.py +20 -19
- {pyoframe-0.0.5 → pyoframe-0.0.6}/src/pyoframe/model.py +7 -6
- {pyoframe-0.0.5 → pyoframe-0.0.6}/src/pyoframe/model_element.py +6 -2
- {pyoframe-0.0.5 → pyoframe-0.0.6}/src/pyoframe/solvers.py +56 -32
- {pyoframe-0.0.5 → pyoframe-0.0.6/src/pyoframe.egg-info}/PKG-INFO +1 -1
- {pyoframe-0.0.5 → pyoframe-0.0.6}/LICENSE +0 -0
- {pyoframe-0.0.5 → pyoframe-0.0.6}/README.md +0 -0
- {pyoframe-0.0.5 → pyoframe-0.0.6}/setup.cfg +0 -0
- {pyoframe-0.0.5 → pyoframe-0.0.6}/src/pyoframe/__init__.py +0 -0
- {pyoframe-0.0.5 → pyoframe-0.0.6}/src/pyoframe/monkey_patch.py +0 -0
- {pyoframe-0.0.5 → pyoframe-0.0.6}/src/pyoframe/objective.py +0 -0
- {pyoframe-0.0.5 → pyoframe-0.0.6}/src/pyoframe/user_defined.py +0 -0
- {pyoframe-0.0.5 → pyoframe-0.0.6}/src/pyoframe/util.py +0 -0
- {pyoframe-0.0.5 → pyoframe-0.0.6}/src/pyoframe.egg-info/SOURCES.txt +0 -0
- {pyoframe-0.0.5 → pyoframe-0.0.6}/src/pyoframe.egg-info/dependency_links.txt +0 -0
- {pyoframe-0.0.5 → pyoframe-0.0.6}/src/pyoframe.egg-info/requires.txt +0 -0
- {pyoframe-0.0.5 → pyoframe-0.0.6}/src/pyoframe.egg-info/top_level.txt +0 -0
- {pyoframe-0.0.5 → pyoframe-0.0.6}/tests/test_arithmetic.py +0 -0
- {pyoframe-0.0.5 → pyoframe-0.0.6}/tests/test_examples.py +0 -0
- {pyoframe-0.0.5 → pyoframe-0.0.6}/tests/test_io.py +0 -0
- {pyoframe-0.0.5 → pyoframe-0.0.6}/tests/test_operations.py +0 -0
- {pyoframe-0.0.5 → pyoframe-0.0.6}/tests/test_solver.py +0 -0
|
@@ -106,7 +106,7 @@ def _add_expressions_core(*expressions: "Expression") -> "Expression":
|
|
|
106
106
|
|
|
107
107
|
strat = (left.unmatched_strategy, right.unmatched_strategy)
|
|
108
108
|
|
|
109
|
-
propogate_strat = propogatation_strategies[strat]
|
|
109
|
+
propogate_strat = propogatation_strategies[strat] # type: ignore
|
|
110
110
|
|
|
111
111
|
if strat == (UnmatchedStrategy.DROP, UnmatchedStrategy.DROP):
|
|
112
112
|
left_data = left.data.join(get_indices(right), how="inner", on=dims)
|
|
@@ -132,8 +132,11 @@ class SolverStatus(Enum):
|
|
|
132
132
|
def from_termination_condition(
|
|
133
133
|
cls, termination_condition: "TerminationCondition"
|
|
134
134
|
) -> "SolverStatus":
|
|
135
|
-
for
|
|
136
|
-
|
|
135
|
+
for (
|
|
136
|
+
status,
|
|
137
|
+
termination_conditions,
|
|
138
|
+
) in STATUS_TO_TERMINATION_CONDITION_MAP.items():
|
|
139
|
+
if termination_condition in termination_conditions:
|
|
137
140
|
return status
|
|
138
141
|
return cls("unknown")
|
|
139
142
|
|
|
@@ -689,9 +689,9 @@ class Expression(ModelElement, SupportsMath, SupportPolarsMethodMixin):
|
|
|
689
689
|
>>> import pyoframe as pf
|
|
690
690
|
>>> m = pf.Model("max")
|
|
691
691
|
>>> m.X = pf.Variable({"dim1": [1, 2, 3]}, ub=10)
|
|
692
|
-
>>> m.expr_1 = 2 * m.X
|
|
692
|
+
>>> m.expr_1 = 2 * m.X + 1
|
|
693
693
|
>>> m.expr_2 = pf.sum(m.expr_1)
|
|
694
|
-
>>> m.objective = m.expr_2
|
|
694
|
+
>>> m.objective = m.expr_2 - 3
|
|
695
695
|
>>> result = m.solve(log_to_console=False)
|
|
696
696
|
>>> m.expr_1.value
|
|
697
697
|
shape: (3, 2)
|
|
@@ -700,15 +700,16 @@ class Expression(ModelElement, SupportsMath, SupportPolarsMethodMixin):
|
|
|
700
700
|
│ --- ┆ --- │
|
|
701
701
|
│ i64 ┆ f64 │
|
|
702
702
|
╞══════╪══════════╡
|
|
703
|
-
│ 1 ┆
|
|
704
|
-
│ 2 ┆
|
|
705
|
-
│ 3 ┆
|
|
703
|
+
│ 1 ┆ 21.0 │
|
|
704
|
+
│ 2 ┆ 21.0 │
|
|
705
|
+
│ 3 ┆ 21.0 │
|
|
706
706
|
└──────┴──────────┘
|
|
707
707
|
>>> m.expr_2.value
|
|
708
|
-
60.0
|
|
709
|
-
>>> m.objective.value
|
|
710
708
|
63.0
|
|
711
709
|
"""
|
|
710
|
+
assert (
|
|
711
|
+
self._model is not None
|
|
712
|
+
), "Expression must be added to the model to use .value"
|
|
712
713
|
if self._model.result is None or self._model.result.solution is None:
|
|
713
714
|
raise ValueError(
|
|
714
715
|
"Can't obtain value of expression since the model has not been solved."
|
|
@@ -716,8 +717,15 @@ class Expression(ModelElement, SupportsMath, SupportPolarsMethodMixin):
|
|
|
716
717
|
|
|
717
718
|
df = (
|
|
718
719
|
self.data.join(self._model.result.solution.primal, on=VAR_KEY, how="left")
|
|
720
|
+
.with_columns(
|
|
721
|
+
(
|
|
722
|
+
pl.when(pl.col(VAR_KEY) == CONST_TERM)
|
|
723
|
+
.then(1)
|
|
724
|
+
.otherwise(pl.col(SOLUTION_KEY))
|
|
725
|
+
* pl.col(COEF_KEY)
|
|
726
|
+
).alias(SOLUTION_KEY)
|
|
727
|
+
)
|
|
719
728
|
.drop(VAR_KEY)
|
|
720
|
-
.with_columns((pl.col(SOLUTION_KEY) * pl.col(COEF_KEY)))
|
|
721
729
|
.drop(COEF_KEY)
|
|
722
730
|
)
|
|
723
731
|
|
|
@@ -924,6 +932,9 @@ class Constraint(ModelElementWithId):
|
|
|
924
932
|
The first call to this property will load the slack values from the solver (lazy loading).
|
|
925
933
|
"""
|
|
926
934
|
if SLACK_COL not in self.data.columns:
|
|
935
|
+
assert (
|
|
936
|
+
self._model is not None
|
|
937
|
+
), "Constraint must be added to a model to get the slack."
|
|
927
938
|
if self._model.solver is None:
|
|
928
939
|
raise ValueError("The model has not been solved yet.")
|
|
929
940
|
self._model.solver.load_slack()
|
|
@@ -1160,7 +1171,7 @@ class Variable(ModelElementWithId, SupportsMath, SupportPolarsMethodMixin):
|
|
|
1160
1171
|
lb: float | int | SupportsToExpr | None = None,
|
|
1161
1172
|
ub: float | int | SupportsToExpr | None = None,
|
|
1162
1173
|
vtype: VType | VTypeValue = VType.CONTINUOUS,
|
|
1163
|
-
equals:
|
|
1174
|
+
equals: Optional[SupportsMath] = None,
|
|
1164
1175
|
):
|
|
1165
1176
|
if lb is None:
|
|
1166
1177
|
lb = float("-inf")
|
|
@@ -1222,6 +1233,9 @@ class Variable(ModelElementWithId, SupportsMath, SupportPolarsMethodMixin):
|
|
|
1222
1233
|
The first call to this property will load the reduced costs from the solver (lazy loading).
|
|
1223
1234
|
"""
|
|
1224
1235
|
if RC_COL not in self.data.columns:
|
|
1236
|
+
assert (
|
|
1237
|
+
self._model is not None
|
|
1238
|
+
), "Variable must be added to a model to get the reduced cost."
|
|
1225
1239
|
if self._model.solver is None:
|
|
1226
1240
|
raise ValueError("The model has not been solved yet.")
|
|
1227
1241
|
self._model.solver.load_rc()
|
|
@@ -11,8 +11,8 @@ from tqdm import tqdm
|
|
|
11
11
|
from pyoframe.constants import CONST_TERM, VAR_KEY, ObjSense
|
|
12
12
|
from pyoframe.core import Constraint, Variable
|
|
13
13
|
from pyoframe.io_mappers import (
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
Base36ConstMapper,
|
|
15
|
+
Base36VarMapper,
|
|
16
16
|
IOMappers,
|
|
17
17
|
Mapper,
|
|
18
18
|
NamedMapper,
|
|
@@ -131,7 +131,7 @@ def get_var_map(m: "Model", use_var_names):
|
|
|
131
131
|
return m.var_map
|
|
132
132
|
var_map = NamedVariableMapper(Variable)
|
|
133
133
|
else:
|
|
134
|
-
var_map =
|
|
134
|
+
var_map = Base36VarMapper(Variable)
|
|
135
135
|
|
|
136
136
|
for v in m.variables:
|
|
137
137
|
var_map.add(v)
|
|
@@ -139,10 +139,20 @@ def get_var_map(m: "Model", use_var_names):
|
|
|
139
139
|
|
|
140
140
|
|
|
141
141
|
def to_file(
|
|
142
|
-
m: "Model", file_path: Optional[Union[str, Path]], use_var_names=False
|
|
142
|
+
m: "Model", file_path: Optional[Union[str, Path]] = None, use_var_names=False
|
|
143
143
|
) -> Path:
|
|
144
144
|
"""
|
|
145
145
|
Write out a model to a lp file.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
m: The model to write out.
|
|
149
|
+
file_path: The path to write the model to. If None, a temporary file is created. The caller is responsible for
|
|
150
|
+
deleting the file after use.
|
|
151
|
+
use_var_names: If True, variable names are used in the lp file. Otherwise, variable
|
|
152
|
+
indices are used.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
The path to the lp file.
|
|
146
156
|
"""
|
|
147
157
|
if file_path is None:
|
|
148
158
|
with NamedTemporaryFile(
|
|
@@ -157,7 +167,7 @@ def to_file(
|
|
|
157
167
|
file_path.unlink()
|
|
158
168
|
|
|
159
169
|
const_map = (
|
|
160
|
-
NamedMapper(Constraint) if use_var_names else
|
|
170
|
+
NamedMapper(Constraint) if use_var_names else Base36ConstMapper(Constraint)
|
|
161
171
|
)
|
|
162
172
|
for c in m.constraints:
|
|
163
173
|
const_map.add(c)
|
|
@@ -16,7 +16,7 @@ from pyoframe.constants import CONST_TERM
|
|
|
16
16
|
if TYPE_CHECKING: # pragma: no cover
|
|
17
17
|
from pyoframe.model import Variable
|
|
18
18
|
from pyoframe.core import Constraint
|
|
19
|
-
from pyoframe.
|
|
19
|
+
from pyoframe.model_element import ModelElementWithId
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
@dataclass
|
|
@@ -29,7 +29,7 @@ class Mapper(ABC):
|
|
|
29
29
|
|
|
30
30
|
NAME_COL = "__name"
|
|
31
31
|
|
|
32
|
-
def __init__(self, cls: Type["
|
|
32
|
+
def __init__(self, cls: Type["ModelElementWithId"]) -> None:
|
|
33
33
|
self._ID_COL = cls.get_id_column_name()
|
|
34
34
|
self.mapping_registry = pl.DataFrame(
|
|
35
35
|
{self._ID_COL: [], Mapper.NAME_COL: []},
|
|
@@ -43,7 +43,7 @@ class Mapper(ABC):
|
|
|
43
43
|
self.mapping_registry = pl.concat([self.mapping_registry, df])
|
|
44
44
|
|
|
45
45
|
@abstractmethod
|
|
46
|
-
def _element_to_map(self, element: "
|
|
46
|
+
def _element_to_map(self, element: "ModelElementWithId") -> pl.DataFrame: ...
|
|
47
47
|
|
|
48
48
|
def apply(
|
|
49
49
|
self,
|
|
@@ -107,13 +107,14 @@ class NamedVariableMapper(NamedMapper):
|
|
|
107
107
|
)
|
|
108
108
|
|
|
109
109
|
|
|
110
|
-
class
|
|
111
|
-
# Mapping between a base
|
|
110
|
+
class Base36Mapper(Mapper, ABC):
|
|
111
|
+
# Mapping between a base 36 character and its integer value
|
|
112
|
+
# Note: we must use only lowercase since Gurobi auto-converts variables that aren't in constraints to lowercase (kind of annoying)
|
|
112
113
|
_CHAR_TABLE = pl.DataFrame(
|
|
113
|
-
{"char": list(string.digits + string.
|
|
114
|
+
{"char": list(string.digits + string.ascii_lowercase)},
|
|
114
115
|
).with_columns(pl.int_range(pl.len()).cast(pl.UInt32).alias("code"))
|
|
115
116
|
|
|
116
|
-
_BASE = _CHAR_TABLE.height # _BASE =
|
|
117
|
+
_BASE = _CHAR_TABLE.height # _BASE = 36
|
|
117
118
|
_ZERO = _CHAR_TABLE.filter(pl.col("code") == 0).select("char").item() # _ZERO = "0"
|
|
118
119
|
|
|
119
120
|
@property
|
|
@@ -131,7 +132,7 @@ class Base62Mapper(Mapper, ABC):
|
|
|
131
132
|
query = pl.concat_str(
|
|
132
133
|
pl.lit(self._prefix),
|
|
133
134
|
pl.col(self._ID_COL).map_batches(
|
|
134
|
-
|
|
135
|
+
Base36Mapper._to_base36,
|
|
135
136
|
return_dtype=pl.String,
|
|
136
137
|
is_elementwise=True,
|
|
137
138
|
),
|
|
@@ -143,24 +144,24 @@ class Base62Mapper(Mapper, ABC):
|
|
|
143
144
|
return df.with_columns(query.alias(to_col))
|
|
144
145
|
|
|
145
146
|
@classmethod
|
|
146
|
-
def
|
|
147
|
-
"""Returns a series of dtype str with a base
|
|
148
|
-
The letters 0-
|
|
147
|
+
def _to_base36(cls, int_col: pl.Series) -> pl.Series:
|
|
148
|
+
"""Returns a series of dtype str with a base 36 representation of the integers in int_col.
|
|
149
|
+
The letters 0-9A-Z are used as symbols for the representation.
|
|
149
150
|
|
|
150
151
|
Examples:
|
|
151
152
|
|
|
152
153
|
>>> import polars as pl
|
|
153
154
|
>>> s = pl.Series([0,10,20,60,53,66], dtype=pl.UInt32)
|
|
154
|
-
>>>
|
|
155
|
-
['0', 'a', 'k', '
|
|
155
|
+
>>> Base36Mapper._to_base36(s).to_list()
|
|
156
|
+
['0', 'a', 'k', '1o', '1h', '1u']
|
|
156
157
|
|
|
157
158
|
>>> s = pl.Series([0], dtype=pl.UInt32)
|
|
158
|
-
>>>
|
|
159
|
+
>>> Base36Mapper._to_base36(s).to_list()
|
|
159
160
|
['0']
|
|
160
161
|
"""
|
|
161
162
|
assert isinstance(
|
|
162
163
|
int_col.dtype, pl.UInt32
|
|
163
|
-
), "
|
|
164
|
+
), "_to_base36() only works for UInt32 id columns"
|
|
164
165
|
|
|
165
166
|
largest_id = int_col.max()
|
|
166
167
|
if largest_id == 0:
|
|
@@ -193,7 +194,7 @@ class Base62Mapper(Mapper, ABC):
|
|
|
193
194
|
return self.apply(element.ids.select(self._ID_COL), to_col=Mapper.NAME_COL)
|
|
194
195
|
|
|
195
196
|
|
|
196
|
-
class
|
|
197
|
+
class Base36VarMapper(Base36Mapper):
|
|
197
198
|
"""
|
|
198
199
|
Examples:
|
|
199
200
|
>>> import polars as pl
|
|
@@ -203,10 +204,10 @@ class Base62VarMapper(Base62Mapper):
|
|
|
203
204
|
>>> m.x = Variable(pl.DataFrame({"t": range(1,63)}))
|
|
204
205
|
>>> (m.x.filter(t=11)+1).to_str()
|
|
205
206
|
'[11]: 1 + x[11]'
|
|
206
|
-
>>> (m.x.filter(t=11)+1).to_str(var_map=
|
|
207
|
+
>>> (m.x.filter(t=11)+1).to_str(var_map=Base36VarMapper(Variable))
|
|
207
208
|
'[11]: 1 + xb'
|
|
208
209
|
|
|
209
|
-
>>>
|
|
210
|
+
>>> Base36VarMapper(Variable).apply(pl.DataFrame({VAR_KEY: []}))
|
|
210
211
|
shape: (0, 1)
|
|
211
212
|
┌───────────────┐
|
|
212
213
|
│ __variable_id │
|
|
@@ -230,7 +231,7 @@ class Base62VarMapper(Base62Mapper):
|
|
|
230
231
|
return "x"
|
|
231
232
|
|
|
232
233
|
|
|
233
|
-
class
|
|
234
|
+
class Base36ConstMapper(Base36Mapper):
|
|
234
235
|
|
|
235
236
|
@property
|
|
236
237
|
def _prefix(self) -> "str":
|
|
@@ -37,6 +37,7 @@ class Model(AttrContainerMixin):
|
|
|
37
37
|
"result",
|
|
38
38
|
"attr",
|
|
39
39
|
"sense",
|
|
40
|
+
"objective",
|
|
40
41
|
]
|
|
41
42
|
|
|
42
43
|
def __init__(self, min_or_max: Union[ObjSense, ObjSenseValue], name=None, **kwargs):
|
|
@@ -75,6 +76,12 @@ class Model(AttrContainerMixin):
|
|
|
75
76
|
def objective(self):
|
|
76
77
|
return self._objective
|
|
77
78
|
|
|
79
|
+
@objective.setter
|
|
80
|
+
def objective(self, value):
|
|
81
|
+
value = Objective(value)
|
|
82
|
+
self._objective = value
|
|
83
|
+
value.on_add_to_model(self, "objective")
|
|
84
|
+
|
|
78
85
|
def __setattr__(self, __name: str, __value: Any) -> None:
|
|
79
86
|
if __name not in Model._reserved_attributes and not isinstance(
|
|
80
87
|
__value, (ModelElement, pl.DataFrame, pd.DataFrame)
|
|
@@ -87,9 +94,6 @@ class Model(AttrContainerMixin):
|
|
|
87
94
|
isinstance(__value, ModelElement)
|
|
88
95
|
and __name not in Model._reserved_attributes
|
|
89
96
|
):
|
|
90
|
-
if __name == "objective":
|
|
91
|
-
__value = Objective(__value)
|
|
92
|
-
|
|
93
97
|
if isinstance(__value, ModelElementWithId):
|
|
94
98
|
assert not hasattr(
|
|
95
99
|
self, __name
|
|
@@ -103,9 +107,6 @@ class Model(AttrContainerMixin):
|
|
|
103
107
|
self.var_map.add(__value)
|
|
104
108
|
elif isinstance(__value, Constraint):
|
|
105
109
|
self._constraints.append(__value)
|
|
106
|
-
elif isinstance(__value, Objective):
|
|
107
|
-
self._objective = __value
|
|
108
|
-
return
|
|
109
110
|
return super().__setattr__(__name, __value)
|
|
110
111
|
|
|
111
112
|
def __repr__(self) -> str:
|
|
@@ -109,7 +109,7 @@ def _support_polars_method(method_name: str):
|
|
|
109
109
|
Wrapper to add a method to ModelElement that simply calls the underlying Polars method on the data attribute.
|
|
110
110
|
"""
|
|
111
111
|
|
|
112
|
-
def method(self: "SupportPolarsMethodMixin", *args, **kwargs):
|
|
112
|
+
def method(self: "SupportPolarsMethodMixin", *args, **kwargs) -> Any:
|
|
113
113
|
result_from_polars = getattr(self.data, method_name)(*args, **kwargs)
|
|
114
114
|
if isinstance(result_from_polars, pl.DataFrame):
|
|
115
115
|
return self._new(result_from_polars)
|
|
@@ -119,7 +119,7 @@ def _support_polars_method(method_name: str):
|
|
|
119
119
|
return method
|
|
120
120
|
|
|
121
121
|
|
|
122
|
-
class SupportPolarsMethodMixin:
|
|
122
|
+
class SupportPolarsMethodMixin(ABC):
|
|
123
123
|
rename = _support_polars_method("rename")
|
|
124
124
|
with_columns = _support_polars_method("with_columns")
|
|
125
125
|
filter = _support_polars_method("filter")
|
|
@@ -131,6 +131,10 @@ class SupportPolarsMethodMixin:
|
|
|
131
131
|
Used to create a new instance of the same class with the given data (for e.g. on .rename(), .with_columns(), etc.).
|
|
132
132
|
"""
|
|
133
133
|
|
|
134
|
+
@property
|
|
135
|
+
@abstractmethod
|
|
136
|
+
def data(self): ...
|
|
137
|
+
|
|
134
138
|
|
|
135
139
|
class ModelElementWithId(ModelElement, AttrContainerMixin):
|
|
136
140
|
"""
|
|
@@ -5,7 +5,7 @@ Code to interface with various solvers
|
|
|
5
5
|
from abc import abstractmethod, ABC
|
|
6
6
|
from functools import lru_cache
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import Any, Dict,
|
|
8
|
+
from typing import Any, Dict, Optional, Type, Union, TYPE_CHECKING
|
|
9
9
|
|
|
10
10
|
import polars as pl
|
|
11
11
|
|
|
@@ -50,7 +50,6 @@ def solve(
|
|
|
50
50
|
solver=None,
|
|
51
51
|
directory: Optional[Union[Path, str]] = None,
|
|
52
52
|
use_var_names=False,
|
|
53
|
-
env=None,
|
|
54
53
|
log_fn=None,
|
|
55
54
|
warmstart_fn=None,
|
|
56
55
|
basis_fn=None,
|
|
@@ -68,8 +67,13 @@ def solve(
|
|
|
68
67
|
raise ValueError(f"Solver {solver} not recognized or supported.")
|
|
69
68
|
|
|
70
69
|
solver_cls = solver_registry[solver]
|
|
71
|
-
m.solver = solver_cls(
|
|
72
|
-
|
|
70
|
+
m.solver = solver_cls(
|
|
71
|
+
m,
|
|
72
|
+
log_to_console,
|
|
73
|
+
params={param: value for param, value in m.params},
|
|
74
|
+
directory=directory,
|
|
75
|
+
)
|
|
76
|
+
m.solver_model = m.solver.create_solver_model(use_var_names)
|
|
73
77
|
m.solver.solver_model = m.solver_model
|
|
74
78
|
|
|
75
79
|
for attr_container in [m.variables, m.constraints, [m]]:
|
|
@@ -77,9 +81,6 @@ def solve(
|
|
|
77
81
|
for param_name, param_value in container.attr:
|
|
78
82
|
m.solver.set_attr(container, param_name, param_value)
|
|
79
83
|
|
|
80
|
-
for param, value in m.params:
|
|
81
|
-
m.solver.set_param(param, value)
|
|
82
|
-
|
|
83
84
|
result = m.solver.solve(log_fn, warmstart_fn, basis_fn, solution_file)
|
|
84
85
|
result = m.solver.process_result(result)
|
|
85
86
|
m.result = result
|
|
@@ -99,20 +100,19 @@ def solve(
|
|
|
99
100
|
|
|
100
101
|
|
|
101
102
|
class Solver(ABC):
|
|
102
|
-
def __init__(self, model, log_to_console):
|
|
103
|
+
def __init__(self, model: "Model", log_to_console, params, directory):
|
|
103
104
|
self._model = model
|
|
104
105
|
self.solver_model: Optional[Any] = None
|
|
105
|
-
self.log_to_console = log_to_console
|
|
106
|
+
self.log_to_console: bool = log_to_console
|
|
107
|
+
self.params = params
|
|
108
|
+
self.directory = directory
|
|
106
109
|
|
|
107
110
|
@abstractmethod
|
|
108
|
-
def create_solver_model(self) -> Any: ...
|
|
111
|
+
def create_solver_model(self, use_var_names) -> Any: ...
|
|
109
112
|
|
|
110
113
|
@abstractmethod
|
|
111
114
|
def set_attr(self, element, param_name, param_value): ...
|
|
112
115
|
|
|
113
|
-
@abstractmethod
|
|
114
|
-
def set_param(self, param_name, param_value): ...
|
|
115
|
-
|
|
116
116
|
@abstractmethod
|
|
117
117
|
def solve(self, log_fn, warmstart_fn, basis_fn, solution_file) -> Result: ...
|
|
118
118
|
|
|
@@ -135,12 +135,25 @@ class Solver(ABC):
|
|
|
135
135
|
@abstractmethod
|
|
136
136
|
def _get_all_slack(self): ...
|
|
137
137
|
|
|
138
|
+
def dispose(self):
|
|
139
|
+
"""
|
|
140
|
+
Clean up any resources that wouldn't be cleaned up by the garbage collector.
|
|
141
|
+
|
|
142
|
+
For now, this is only used by the Gurobi solver to call .dispose() on the solver model and Gurobi environment
|
|
143
|
+
which helps close a connection to the Gurobi Computer Server. Note that this effectively disables commands that
|
|
144
|
+
need access to the solver model (like .slack and .RC)
|
|
145
|
+
"""
|
|
146
|
+
|
|
138
147
|
|
|
139
148
|
class FileBasedSolver(Solver):
|
|
140
|
-
def
|
|
141
|
-
|
|
142
|
-
|
|
149
|
+
def __init__(self, *args, **kwargs):
|
|
150
|
+
super().__init__(*args, **kwargs)
|
|
151
|
+
self.problem_file: Optional[Path] = None
|
|
152
|
+
self.keep_files = self.directory is not None
|
|
153
|
+
|
|
154
|
+
def create_solver_model(self, use_var_names) -> Any:
|
|
143
155
|
problem_file = None
|
|
156
|
+
directory = self.directory
|
|
144
157
|
if directory is not None:
|
|
145
158
|
if isinstance(directory, str):
|
|
146
159
|
directory = Path(directory)
|
|
@@ -150,12 +163,14 @@ class FileBasedSolver(Solver):
|
|
|
150
163
|
self._model.name if self._model.name is not None else "pyoframe-problem"
|
|
151
164
|
)
|
|
152
165
|
problem_file = directory / f"{filename}.lp"
|
|
153
|
-
problem_file = self._model.to_file(
|
|
166
|
+
self.problem_file = self._model.to_file(
|
|
167
|
+
problem_file, use_var_names=use_var_names
|
|
168
|
+
)
|
|
154
169
|
assert self._model.io_mappers is not None
|
|
155
|
-
return self.create_solver_model_from_lp(
|
|
170
|
+
return self.create_solver_model_from_lp()
|
|
156
171
|
|
|
157
172
|
@abstractmethod
|
|
158
|
-
def create_solver_model_from_lp(self
|
|
173
|
+
def create_solver_model_from_lp(self) -> Any: ...
|
|
159
174
|
|
|
160
175
|
def set_attr(self, element, param_name, param_value):
|
|
161
176
|
if isinstance(param_value, pl.DataFrame):
|
|
@@ -216,30 +231,30 @@ class GurobiSolver(FileBasedSolver):
|
|
|
216
231
|
17: "internal_solver_error",
|
|
217
232
|
}
|
|
218
233
|
|
|
219
|
-
def
|
|
234
|
+
def __init__(self, *args, **kwargs):
|
|
235
|
+
super().__init__(*args, **kwargs)
|
|
236
|
+
if not self.log_to_console:
|
|
237
|
+
self.params["LogToConsole"] = 0
|
|
238
|
+
self.env = None
|
|
239
|
+
|
|
240
|
+
def create_solver_model_from_lp(self) -> Any:
|
|
220
241
|
"""
|
|
221
242
|
Solve a linear problem using the gurobi solver.
|
|
222
243
|
|
|
223
244
|
This function communicates with gurobi using the gurubipy package.
|
|
224
245
|
"""
|
|
246
|
+
assert self.problem_file is not None
|
|
247
|
+
self.env = gurobipy.Env(params=self.params)
|
|
225
248
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
else:
|
|
230
|
-
# See https://support.gurobi.com/hc/en-us/articles/360044784552-How-do-I-suppress-all-console-output-from-Gurobi
|
|
231
|
-
env = gurobipy.Env(empty=True)
|
|
232
|
-
env.setParam("LogToConsole", 0)
|
|
233
|
-
env.start()
|
|
249
|
+
m = gurobipy.read(_path_to_str(self.problem_file), env=self.env)
|
|
250
|
+
if not self.keep_files:
|
|
251
|
+
self.problem_file.unlink()
|
|
234
252
|
|
|
235
|
-
m = gurobipy.read(_path_to_str(problem_fn), env=env)
|
|
236
253
|
return m
|
|
237
254
|
|
|
238
|
-
def set_param(self, param_name, param_value):
|
|
239
|
-
self.solver_model.setParam(param_name, param_value)
|
|
240
|
-
|
|
241
255
|
@lru_cache
|
|
242
256
|
def _get_var_mapping(self):
|
|
257
|
+
assert self.solver_model is not None
|
|
243
258
|
vars = self.solver_model.getVars()
|
|
244
259
|
return vars, pl.DataFrame(
|
|
245
260
|
{VAR_KEY: self.solver_model.getAttr("VarName", vars)}
|
|
@@ -247,12 +262,14 @@ class GurobiSolver(FileBasedSolver):
|
|
|
247
262
|
|
|
248
263
|
@lru_cache
|
|
249
264
|
def _get_constraint_mapping(self):
|
|
265
|
+
assert self.solver_model is not None
|
|
250
266
|
constraints = self.solver_model.getConstrs()
|
|
251
267
|
return constraints, pl.DataFrame(
|
|
252
268
|
{CONSTRAINT_KEY: self.solver_model.getAttr("ConstrName", constraints)}
|
|
253
269
|
).with_columns(i=pl.int_range(pl.len()))
|
|
254
270
|
|
|
255
271
|
def set_attr_unmapped(self, element, param_name, param_value):
|
|
272
|
+
assert self.solver_model is not None
|
|
256
273
|
if isinstance(element, pf.Model):
|
|
257
274
|
self.solver_model.setAttr(param_name, param_value)
|
|
258
275
|
elif isinstance(element, pf.Variable):
|
|
@@ -277,6 +294,7 @@ class GurobiSolver(FileBasedSolver):
|
|
|
277
294
|
raise ValueError(f"Element type {type(element)} not recognized.")
|
|
278
295
|
|
|
279
296
|
def solve(self, log_fn, warmstart_fn, basis_fn, solution_file) -> Result:
|
|
297
|
+
assert self.solver_model is not None
|
|
280
298
|
m = self.solver_model
|
|
281
299
|
if log_fn is not None:
|
|
282
300
|
m.setParam("logfile", _path_to_str(log_fn))
|
|
@@ -345,6 +363,12 @@ class GurobiSolver(FileBasedSolver):
|
|
|
345
363
|
}
|
|
346
364
|
)
|
|
347
365
|
|
|
366
|
+
def dispose(self):
|
|
367
|
+
if self.solver_model is not None:
|
|
368
|
+
self.solver_model.dispose()
|
|
369
|
+
if self.env is not None:
|
|
370
|
+
self.env.dispose()
|
|
371
|
+
|
|
348
372
|
|
|
349
373
|
def _path_to_str(path: Union[Path, str]) -> str:
|
|
350
374
|
"""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|