pyoframe 0.0.5__py3-none-any.whl → 0.0.7__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/_arithmetic.py +1 -1
- pyoframe/constants.py +5 -2
- pyoframe/core.py +30 -12
- pyoframe/io.py +15 -5
- pyoframe/io_mappers.py +20 -19
- pyoframe/model.py +7 -6
- pyoframe/model_element.py +6 -2
- pyoframe/solvers.py +57 -33
- {pyoframe-0.0.5.dist-info → pyoframe-0.0.7.dist-info}/METADATA +1 -1
- pyoframe-0.0.7.dist-info/RECORD +18 -0
- {pyoframe-0.0.5.dist-info → pyoframe-0.0.7.dist-info}/WHEEL +1 -1
- pyoframe-0.0.5.dist-info/RECORD +0 -18
- {pyoframe-0.0.5.dist-info → pyoframe-0.0.7.dist-info}/LICENSE +0 -0
- {pyoframe-0.0.5.dist-info → pyoframe-0.0.7.dist-info}/top_level.txt +0 -0
pyoframe/_arithmetic.py
CHANGED
|
@@ -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)
|
pyoframe/constants.py
CHANGED
|
@@ -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
|
|
pyoframe/core.py
CHANGED
|
@@ -271,7 +271,11 @@ class Set(ModelElement, SupportsMath, SupportPolarsMethodMixin):
|
|
|
271
271
|
elif isinstance(set, Constraint):
|
|
272
272
|
df = set.data.select(set.dimensions_unsafe)
|
|
273
273
|
elif isinstance(set, SupportsMath):
|
|
274
|
-
df =
|
|
274
|
+
df = (
|
|
275
|
+
set.to_expr()
|
|
276
|
+
.data.drop(RESERVED_COL_KEYS, strict=False)
|
|
277
|
+
.unique(maintain_order=True)
|
|
278
|
+
)
|
|
275
279
|
elif isinstance(set, pd.Index):
|
|
276
280
|
df = pl.from_pandas(pd.DataFrame(index=set).reset_index())
|
|
277
281
|
elif isinstance(set, pd.DataFrame):
|
|
@@ -689,9 +693,9 @@ class Expression(ModelElement, SupportsMath, SupportPolarsMethodMixin):
|
|
|
689
693
|
>>> import pyoframe as pf
|
|
690
694
|
>>> m = pf.Model("max")
|
|
691
695
|
>>> m.X = pf.Variable({"dim1": [1, 2, 3]}, ub=10)
|
|
692
|
-
>>> m.expr_1 = 2 * m.X
|
|
696
|
+
>>> m.expr_1 = 2 * m.X + 1
|
|
693
697
|
>>> m.expr_2 = pf.sum(m.expr_1)
|
|
694
|
-
>>> m.objective = m.expr_2
|
|
698
|
+
>>> m.objective = m.expr_2 - 3
|
|
695
699
|
>>> result = m.solve(log_to_console=False)
|
|
696
700
|
>>> m.expr_1.value
|
|
697
701
|
shape: (3, 2)
|
|
@@ -700,15 +704,16 @@ class Expression(ModelElement, SupportsMath, SupportPolarsMethodMixin):
|
|
|
700
704
|
│ --- ┆ --- │
|
|
701
705
|
│ i64 ┆ f64 │
|
|
702
706
|
╞══════╪══════════╡
|
|
703
|
-
│ 1 ┆
|
|
704
|
-
│ 2 ┆
|
|
705
|
-
│ 3 ┆
|
|
707
|
+
│ 1 ┆ 21.0 │
|
|
708
|
+
│ 2 ┆ 21.0 │
|
|
709
|
+
│ 3 ┆ 21.0 │
|
|
706
710
|
└──────┴──────────┘
|
|
707
711
|
>>> m.expr_2.value
|
|
708
|
-
60.0
|
|
709
|
-
>>> m.objective.value
|
|
710
712
|
63.0
|
|
711
713
|
"""
|
|
714
|
+
assert (
|
|
715
|
+
self._model is not None
|
|
716
|
+
), "Expression must be added to the model to use .value"
|
|
712
717
|
if self._model.result is None or self._model.result.solution is None:
|
|
713
718
|
raise ValueError(
|
|
714
719
|
"Can't obtain value of expression since the model has not been solved."
|
|
@@ -716,8 +721,15 @@ class Expression(ModelElement, SupportsMath, SupportPolarsMethodMixin):
|
|
|
716
721
|
|
|
717
722
|
df = (
|
|
718
723
|
self.data.join(self._model.result.solution.primal, on=VAR_KEY, how="left")
|
|
724
|
+
.with_columns(
|
|
725
|
+
(
|
|
726
|
+
pl.when(pl.col(VAR_KEY) == CONST_TERM)
|
|
727
|
+
.then(1)
|
|
728
|
+
.otherwise(pl.col(SOLUTION_KEY))
|
|
729
|
+
* pl.col(COEF_KEY)
|
|
730
|
+
).alias(SOLUTION_KEY)
|
|
731
|
+
)
|
|
719
732
|
.drop(VAR_KEY)
|
|
720
|
-
.with_columns((pl.col(SOLUTION_KEY) * pl.col(COEF_KEY)))
|
|
721
733
|
.drop(COEF_KEY)
|
|
722
734
|
)
|
|
723
735
|
|
|
@@ -924,6 +936,9 @@ class Constraint(ModelElementWithId):
|
|
|
924
936
|
The first call to this property will load the slack values from the solver (lazy loading).
|
|
925
937
|
"""
|
|
926
938
|
if SLACK_COL not in self.data.columns:
|
|
939
|
+
assert (
|
|
940
|
+
self._model is not None
|
|
941
|
+
), "Constraint must be added to a model to get the slack."
|
|
927
942
|
if self._model.solver is None:
|
|
928
943
|
raise ValueError("The model has not been solved yet.")
|
|
929
944
|
self._model.solver.load_slack()
|
|
@@ -1160,7 +1175,7 @@ class Variable(ModelElementWithId, SupportsMath, SupportPolarsMethodMixin):
|
|
|
1160
1175
|
lb: float | int | SupportsToExpr | None = None,
|
|
1161
1176
|
ub: float | int | SupportsToExpr | None = None,
|
|
1162
1177
|
vtype: VType | VTypeValue = VType.CONTINUOUS,
|
|
1163
|
-
equals:
|
|
1178
|
+
equals: Optional[SupportsMath] = None,
|
|
1164
1179
|
):
|
|
1165
1180
|
if lb is None:
|
|
1166
1181
|
lb = float("-inf")
|
|
@@ -1222,6 +1237,9 @@ class Variable(ModelElementWithId, SupportsMath, SupportPolarsMethodMixin):
|
|
|
1222
1237
|
The first call to this property will load the reduced costs from the solver (lazy loading).
|
|
1223
1238
|
"""
|
|
1224
1239
|
if RC_COL not in self.data.columns:
|
|
1240
|
+
assert (
|
|
1241
|
+
self._model is not None
|
|
1242
|
+
), "Variable must be added to a model to get the reduced cost."
|
|
1225
1243
|
if self._model.solver is None:
|
|
1226
1244
|
raise ValueError("The model has not been solved yet.")
|
|
1227
1245
|
self._model.solver.load_rc()
|
|
@@ -1245,7 +1263,7 @@ class Variable(ModelElementWithId, SupportsMath, SupportPolarsMethodMixin):
|
|
|
1245
1263
|
)
|
|
1246
1264
|
|
|
1247
1265
|
def to_expr(self) -> Expression:
|
|
1248
|
-
return self._new(self.data.drop(SOLUTION_KEY))
|
|
1266
|
+
return self._new(self.data.drop(SOLUTION_KEY, strict=False))
|
|
1249
1267
|
|
|
1250
1268
|
def _new(self, data: pl.DataFrame):
|
|
1251
1269
|
e = Expression(data.with_columns(pl.lit(1.0).alias(COEF_KEY)))
|
|
@@ -1323,5 +1341,5 @@ class Variable(ModelElementWithId, SupportsMath, SupportPolarsMethodMixin):
|
|
|
1323
1341
|
data = expr.data.rename({dim: "__prev"})
|
|
1324
1342
|
data = data.join(
|
|
1325
1343
|
wrapped, left_on="__prev", right_on="__next", how="inner"
|
|
1326
|
-
).drop(["__prev", "__next"])
|
|
1344
|
+
).drop(["__prev", "__next"], strict=False)
|
|
1327
1345
|
return expr._new(data)
|
pyoframe/io.py
CHANGED
|
@@ -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)
|
pyoframe/io_mappers.py
CHANGED
|
@@ -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":
|
pyoframe/model.py
CHANGED
|
@@ -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:
|
pyoframe/model_element.py
CHANGED
|
@@ -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
|
"""
|
pyoframe/solvers.py
CHANGED
|
@@ -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))
|
|
@@ -295,7 +313,7 @@ class GurobiSolver(FileBasedSolver):
|
|
|
295
313
|
termination_condition = GurobiSolver.CONDITION_MAP.get(condition, condition)
|
|
296
314
|
status = Status.from_termination_condition(termination_condition)
|
|
297
315
|
|
|
298
|
-
if status.is_ok:
|
|
316
|
+
if status.is_ok and (termination_condition == "optimal"):
|
|
299
317
|
if solution_file:
|
|
300
318
|
m.write(_path_to_str(solution_file))
|
|
301
319
|
|
|
@@ -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
|
"""
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
pyoframe/__init__.py,sha256=D7HHQPy2Me-LLyfPCcSE74dn83PeMK3aOby7i7oiLTs,507
|
|
2
|
+
pyoframe/_arithmetic.py,sha256=riyN2JC-BnOgTIxGfXKIS-X_p7zm8JaKrLk_KDwKAAw,9046
|
|
3
|
+
pyoframe/constants.py,sha256=olao24mkzQfwTlc5o4rQ-AOOTytmtVqNCFwJIc8zbds,7169
|
|
4
|
+
pyoframe/core.py,sha256=TwpWPYl1K5qDSFAX9yfabFKpahxgqoa2eL0zmkP91dI,51247
|
|
5
|
+
pyoframe/io.py,sha256=7bd_oKD8EZsuM0ONUAA9BDFSFvUoXLWGXDhnUqMde8o,5235
|
|
6
|
+
pyoframe/io_mappers.py,sha256=Op5451Yo4gNa-2BiPPCAjPYdFLo8jIeKHCYXUcGPRug,7385
|
|
7
|
+
pyoframe/model.py,sha256=xod3hSf__WWDy0V9pao9wPlQTc7-7x56FJoKKidsMbw,3768
|
|
8
|
+
pyoframe/model_element.py,sha256=H2gZxksb3UQ25vIdNlb07bCx3ZcWh7YD6-ViPVJV-JI,7691
|
|
9
|
+
pyoframe/monkey_patch.py,sha256=S_DU7cieU5C3t3kAyKQrGyLTwno0WANpDBV3xn7AyG8,2068
|
|
10
|
+
pyoframe/objective.py,sha256=JzuyMAQZ2OxEoAaK-splWwZei2hHPbCLdG-X2-yRkD0,1338
|
|
11
|
+
pyoframe/solvers.py,sha256=yf-hzUHDvKgmIHk2FobmygzE9-LnOzTBL5ps-nqGo8I,11875
|
|
12
|
+
pyoframe/user_defined.py,sha256=UWZSTpFj0a8n1_RHwC8Ubwqr4FO-gRPBqqfNUut1IZg,1717
|
|
13
|
+
pyoframe/util.py,sha256=KJubFV66E7WPI5UhcuUNsVwCm7WOcQBiLN1af1MAAgA,9647
|
|
14
|
+
pyoframe-0.0.7.dist-info/LICENSE,sha256=L1pXz6p_1OW5XGWb2UCR6PNu6k3JAT0XWhi8jV0cuRg,1137
|
|
15
|
+
pyoframe-0.0.7.dist-info/METADATA,sha256=zKjbfnByq8u6GRauys4BbRuWjL6SGiCmd9SBhwTDFCE,3445
|
|
16
|
+
pyoframe-0.0.7.dist-info/WHEEL,sha256=y4mX-SOX4fYIkonsAGA5N0Oy-8_gI4FXw5HNI1xqvWg,91
|
|
17
|
+
pyoframe-0.0.7.dist-info/top_level.txt,sha256=10z3OOJSVLriQ0IrFLMH8CH9zByugPWolqhlHlkNjV4,9
|
|
18
|
+
pyoframe-0.0.7.dist-info/RECORD,,
|
pyoframe-0.0.5.dist-info/RECORD
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
pyoframe/__init__.py,sha256=D7HHQPy2Me-LLyfPCcSE74dn83PeMK3aOby7i7oiLTs,507
|
|
2
|
-
pyoframe/_arithmetic.py,sha256=MIUSuvhGjBixzQCX-B7D8pMXwDE-d0Q4_f9vPNHAzys,9030
|
|
3
|
-
pyoframe/constants.py,sha256=KZJaHRlvGvhIoZbjqNEc5UdXmjK8RgsXG-nVFAJ7sws,7121
|
|
4
|
-
pyoframe/core.py,sha256=uBRfy4gpG9B08agjCo4nX6gCEUw1rz33X68Q6UQy-QM,50583
|
|
5
|
-
pyoframe/io.py,sha256=2YNo6SJkbTwAIseCZU10wCrbXNexXYGU6sq6ET895Ds,4853
|
|
6
|
-
pyoframe/io_mappers.py,sha256=xWvWarwzudMvJ1L70l-xSQgFCwHonmC0th5AqilTpIE,7249
|
|
7
|
-
pyoframe/model.py,sha256=5ATWB-04UurPHiEuiSjSxB7CVWsdvj4el76dcQmHAyo,3776
|
|
8
|
-
pyoframe/model_element.py,sha256=N9P56YbD_iscrgRk4yT-oW7pSuwMFDdjxxKVPvMwJ8U,7620
|
|
9
|
-
pyoframe/monkey_patch.py,sha256=S_DU7cieU5C3t3kAyKQrGyLTwno0WANpDBV3xn7AyG8,2068
|
|
10
|
-
pyoframe/objective.py,sha256=JzuyMAQZ2OxEoAaK-splWwZei2hHPbCLdG-X2-yRkD0,1338
|
|
11
|
-
pyoframe/solvers.py,sha256=b9nSw5k-ko4eiA5Ad3z4vlcgp-pTyNeIWHQ7N4zV_AE,10994
|
|
12
|
-
pyoframe/user_defined.py,sha256=UWZSTpFj0a8n1_RHwC8Ubwqr4FO-gRPBqqfNUut1IZg,1717
|
|
13
|
-
pyoframe/util.py,sha256=KJubFV66E7WPI5UhcuUNsVwCm7WOcQBiLN1af1MAAgA,9647
|
|
14
|
-
pyoframe-0.0.5.dist-info/LICENSE,sha256=L1pXz6p_1OW5XGWb2UCR6PNu6k3JAT0XWhi8jV0cuRg,1137
|
|
15
|
-
pyoframe-0.0.5.dist-info/METADATA,sha256=FV-aSYBS5nMnegxZgtdzEDRFZZth4DJDdo_KYfjwbOQ,3445
|
|
16
|
-
pyoframe-0.0.5.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
17
|
-
pyoframe-0.0.5.dist-info/top_level.txt,sha256=10z3OOJSVLriQ0IrFLMH8CH9zByugPWolqhlHlkNjV4,9
|
|
18
|
-
pyoframe-0.0.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|