pyoframe 0.0.11__py3-none-any.whl → 0.1.1__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 +3 -4
- pyoframe/_arithmetic.py +170 -4
- pyoframe/constants.py +43 -199
- pyoframe/core.py +746 -421
- pyoframe/model.py +260 -28
- pyoframe/model_element.py +43 -84
- pyoframe/monkey_patch.py +3 -3
- pyoframe/objective.py +81 -31
- pyoframe/util.py +157 -19
- {pyoframe-0.0.11.dist-info → pyoframe-0.1.1.dist-info}/LICENSE +0 -2
- {pyoframe-0.0.11.dist-info → pyoframe-0.1.1.dist-info}/METADATA +29 -27
- pyoframe-0.1.1.dist-info/RECORD +14 -0
- {pyoframe-0.0.11.dist-info → pyoframe-0.1.1.dist-info}/WHEEL +1 -1
- pyoframe/io.py +0 -252
- pyoframe/io_mappers.py +0 -238
- pyoframe/solvers.py +0 -377
- pyoframe/user_defined.py +0 -60
- pyoframe-0.0.11.dist-info/RECORD +0 -18
- {pyoframe-0.0.11.dist-info → pyoframe-0.1.1.dist-info}/top_level.txt +0 -0
pyoframe/io_mappers.py
DELETED
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Defines various methods for mapping a variable or constraint to its string representation.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from dataclasses import dataclass
|
|
6
|
-
import math
|
|
7
|
-
import string
|
|
8
|
-
from abc import ABC, abstractmethod
|
|
9
|
-
|
|
10
|
-
from typing import TYPE_CHECKING, Optional, Type, Union
|
|
11
|
-
import polars as pl
|
|
12
|
-
from pyoframe.util import concat_dimensions
|
|
13
|
-
from pyoframe.constants import CONST_TERM
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if TYPE_CHECKING: # pragma: no cover
|
|
17
|
-
from pyoframe.model import Variable
|
|
18
|
-
from pyoframe.core import Constraint
|
|
19
|
-
from pyoframe.model_element import ModelElementWithId
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
@dataclass
|
|
23
|
-
class IOMappers:
|
|
24
|
-
var_map: "Mapper"
|
|
25
|
-
const_map: "Mapper"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class Mapper(ABC):
|
|
29
|
-
|
|
30
|
-
NAME_COL = "__name"
|
|
31
|
-
|
|
32
|
-
def __init__(self, cls: Type["ModelElementWithId"]) -> None:
|
|
33
|
-
self._ID_COL = cls.get_id_column_name()
|
|
34
|
-
self.mapping_registry = pl.DataFrame(
|
|
35
|
-
{self._ID_COL: [], Mapper.NAME_COL: []},
|
|
36
|
-
schema={self._ID_COL: pl.UInt32, Mapper.NAME_COL: pl.String},
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
def add(self, element: Union["Variable", "Constraint"]) -> None:
|
|
40
|
-
self._extend_registry(self._element_to_map(element))
|
|
41
|
-
|
|
42
|
-
def _extend_registry(self, df: pl.DataFrame) -> None:
|
|
43
|
-
self.mapping_registry = pl.concat([self.mapping_registry, df])
|
|
44
|
-
|
|
45
|
-
@abstractmethod
|
|
46
|
-
def _element_to_map(self, element: "ModelElementWithId") -> pl.DataFrame: ...
|
|
47
|
-
|
|
48
|
-
def apply(
|
|
49
|
-
self,
|
|
50
|
-
df: pl.DataFrame,
|
|
51
|
-
to_col: Optional[str] = None,
|
|
52
|
-
) -> pl.DataFrame:
|
|
53
|
-
if df.height == 0:
|
|
54
|
-
return df
|
|
55
|
-
result = df.join(
|
|
56
|
-
self.mapping_registry, on=self._ID_COL, how="left", validate="m:1"
|
|
57
|
-
)
|
|
58
|
-
if to_col is None:
|
|
59
|
-
result = result.drop(self._ID_COL)
|
|
60
|
-
to_col = self._ID_COL
|
|
61
|
-
return result.rename({Mapper.NAME_COL: to_col})
|
|
62
|
-
|
|
63
|
-
def undo(self, df: pl.DataFrame) -> pl.DataFrame:
|
|
64
|
-
if df.height == 0:
|
|
65
|
-
return df
|
|
66
|
-
df = df.rename({self._ID_COL: Mapper.NAME_COL})
|
|
67
|
-
return df.join(
|
|
68
|
-
self.mapping_registry, on=Mapper.NAME_COL, how="left", validate="m:1"
|
|
69
|
-
).drop(Mapper.NAME_COL)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
class NamedMapper(Mapper):
|
|
73
|
-
"""
|
|
74
|
-
Maps constraints or variables to a string representation using the object's name and dimensions.
|
|
75
|
-
|
|
76
|
-
Examples:
|
|
77
|
-
|
|
78
|
-
>>> import polars as pl
|
|
79
|
-
>>> import pyoframe as pf
|
|
80
|
-
>>> m = pf.Model("min")
|
|
81
|
-
>>> m.foo = pf.Variable(pl.DataFrame({"t": range(4)}))
|
|
82
|
-
>>> pf.sum(m.foo)
|
|
83
|
-
<Expression size=1 dimensions={} terms=4>
|
|
84
|
-
foo[0] + foo[1] + foo[2] + foo[3]
|
|
85
|
-
"""
|
|
86
|
-
|
|
87
|
-
def _element_to_map(self, element) -> pl.DataFrame:
|
|
88
|
-
element_name = element.name # type: ignore
|
|
89
|
-
assert (
|
|
90
|
-
element_name is not None
|
|
91
|
-
), "Element must have a name to be used in a named mapping."
|
|
92
|
-
return concat_dimensions(
|
|
93
|
-
element.ids, keep_dims=False, prefix=element_name, to_col=Mapper.NAME_COL
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
class NamedVariableMapper(NamedMapper):
|
|
98
|
-
CONST_TERM_NAME = "_ONE"
|
|
99
|
-
|
|
100
|
-
def __init__(self, *args, **kwargs) -> None:
|
|
101
|
-
super().__init__(*args, **kwargs)
|
|
102
|
-
self._extend_registry(
|
|
103
|
-
pl.DataFrame(
|
|
104
|
-
{self._ID_COL: [CONST_TERM], self.NAME_COL: [self.CONST_TERM_NAME]},
|
|
105
|
-
schema={self._ID_COL: pl.UInt32, self.NAME_COL: pl.String},
|
|
106
|
-
)
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
|
|
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)
|
|
113
|
-
_CHAR_TABLE = pl.DataFrame(
|
|
114
|
-
{"char": list(string.digits + string.ascii_lowercase)},
|
|
115
|
-
).with_columns(pl.int_range(pl.len()).cast(pl.UInt32).alias("code"))
|
|
116
|
-
|
|
117
|
-
_BASE = _CHAR_TABLE.height # _BASE = 36
|
|
118
|
-
_ZERO = _CHAR_TABLE.filter(pl.col("code") == 0).select("char").item() # _ZERO = "0"
|
|
119
|
-
|
|
120
|
-
@property
|
|
121
|
-
@abstractmethod
|
|
122
|
-
def _prefix(self) -> "str": ...
|
|
123
|
-
|
|
124
|
-
def apply(
|
|
125
|
-
self,
|
|
126
|
-
df: pl.DataFrame,
|
|
127
|
-
to_col: Optional[str] = None,
|
|
128
|
-
) -> pl.DataFrame:
|
|
129
|
-
if df.height == 0:
|
|
130
|
-
return df
|
|
131
|
-
|
|
132
|
-
query = pl.concat_str(
|
|
133
|
-
pl.lit(self._prefix),
|
|
134
|
-
pl.col(self._ID_COL).map_batches(
|
|
135
|
-
Base36Mapper._to_base36,
|
|
136
|
-
return_dtype=pl.String,
|
|
137
|
-
is_elementwise=True,
|
|
138
|
-
),
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
if to_col is None:
|
|
142
|
-
to_col = self._ID_COL
|
|
143
|
-
|
|
144
|
-
return df.with_columns(query.alias(to_col))
|
|
145
|
-
|
|
146
|
-
@classmethod
|
|
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.
|
|
150
|
-
|
|
151
|
-
Examples:
|
|
152
|
-
|
|
153
|
-
>>> import polars as pl
|
|
154
|
-
>>> s = pl.Series([0,10,20,60,53,66], dtype=pl.UInt32)
|
|
155
|
-
>>> Base36Mapper._to_base36(s).to_list()
|
|
156
|
-
['0', 'a', 'k', '1o', '1h', '1u']
|
|
157
|
-
|
|
158
|
-
>>> s = pl.Series([0], dtype=pl.UInt32)
|
|
159
|
-
>>> Base36Mapper._to_base36(s).to_list()
|
|
160
|
-
['0']
|
|
161
|
-
"""
|
|
162
|
-
assert isinstance(
|
|
163
|
-
int_col.dtype, pl.UInt32
|
|
164
|
-
), "_to_base36() only works for UInt32 id columns"
|
|
165
|
-
|
|
166
|
-
largest_id = int_col.max()
|
|
167
|
-
if largest_id == 0:
|
|
168
|
-
max_digits = 1
|
|
169
|
-
else:
|
|
170
|
-
max_digits = math.floor(math.log(largest_id, cls._BASE)) + 1 # type: ignore
|
|
171
|
-
|
|
172
|
-
digits = []
|
|
173
|
-
|
|
174
|
-
for i in range(max_digits):
|
|
175
|
-
remainder = int_col % cls._BASE
|
|
176
|
-
|
|
177
|
-
digits.append(
|
|
178
|
-
remainder.to_frame(name="code")
|
|
179
|
-
.join(cls._CHAR_TABLE, on="code", how="left")
|
|
180
|
-
.select("char")
|
|
181
|
-
.rename({"char": f"digit{i}"})
|
|
182
|
-
)
|
|
183
|
-
int_col //= cls._BASE
|
|
184
|
-
|
|
185
|
-
return (
|
|
186
|
-
pl.concat(reversed(digits), how="horizontal")
|
|
187
|
-
.select(pl.concat_str(pl.all()))
|
|
188
|
-
.to_series()
|
|
189
|
-
.str.strip_chars_start(cls._ZERO)
|
|
190
|
-
.replace("", cls._ZERO)
|
|
191
|
-
)
|
|
192
|
-
|
|
193
|
-
def _element_to_map(self, element) -> pl.DataFrame:
|
|
194
|
-
return self.apply(element.ids.select(self._ID_COL), to_col=Mapper.NAME_COL)
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
class Base36VarMapper(Base36Mapper):
|
|
198
|
-
"""
|
|
199
|
-
Examples:
|
|
200
|
-
>>> import polars as pl
|
|
201
|
-
>>> from pyoframe import Model, Variable
|
|
202
|
-
>>> from pyoframe.constants import VAR_KEY
|
|
203
|
-
>>> m = Model("min")
|
|
204
|
-
>>> m.x = Variable(pl.DataFrame({"t": range(1,63)}))
|
|
205
|
-
>>> (m.x.filter(t=11)+1).to_str()
|
|
206
|
-
'[11]: 1 + x[11]'
|
|
207
|
-
>>> (m.x.filter(t=11)+1).to_str(var_map=Base36VarMapper(Variable))
|
|
208
|
-
'[11]: 1 + xb'
|
|
209
|
-
|
|
210
|
-
>>> Base36VarMapper(Variable).apply(pl.DataFrame({VAR_KEY: []}))
|
|
211
|
-
shape: (0, 1)
|
|
212
|
-
┌───────────────┐
|
|
213
|
-
│ __variable_id │
|
|
214
|
-
│ --- │
|
|
215
|
-
│ null │
|
|
216
|
-
╞═══════════════╡
|
|
217
|
-
└───────────────┘
|
|
218
|
-
"""
|
|
219
|
-
|
|
220
|
-
def __init__(self, *args, **kwargs) -> None:
|
|
221
|
-
super().__init__(*args, **kwargs)
|
|
222
|
-
df = pl.DataFrame(
|
|
223
|
-
{self._ID_COL: [CONST_TERM]},
|
|
224
|
-
schema={self._ID_COL: pl.UInt32},
|
|
225
|
-
)
|
|
226
|
-
df = self.apply(df, to_col=Mapper.NAME_COL)
|
|
227
|
-
self._extend_registry(df)
|
|
228
|
-
|
|
229
|
-
@property
|
|
230
|
-
def _prefix(self) -> "str":
|
|
231
|
-
return "x"
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
class Base36ConstMapper(Base36Mapper):
|
|
235
|
-
|
|
236
|
-
@property
|
|
237
|
-
def _prefix(self) -> "str":
|
|
238
|
-
return "c"
|
pyoframe/solvers.py
DELETED
|
@@ -1,377 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Code to interface with various solvers
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from abc import abstractmethod, ABC
|
|
6
|
-
from functools import lru_cache
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
from typing import Any, Dict, Optional, Type, Union, TYPE_CHECKING
|
|
9
|
-
|
|
10
|
-
import polars as pl
|
|
11
|
-
|
|
12
|
-
from pyoframe.constants import (
|
|
13
|
-
DUAL_KEY,
|
|
14
|
-
SOLUTION_KEY,
|
|
15
|
-
SLACK_COL,
|
|
16
|
-
RC_COL,
|
|
17
|
-
VAR_KEY,
|
|
18
|
-
CONSTRAINT_KEY,
|
|
19
|
-
Result,
|
|
20
|
-
Solution,
|
|
21
|
-
Status,
|
|
22
|
-
)
|
|
23
|
-
import contextlib
|
|
24
|
-
import pyoframe as pf
|
|
25
|
-
|
|
26
|
-
from pathlib import Path
|
|
27
|
-
|
|
28
|
-
if TYPE_CHECKING: # pragma: no cover
|
|
29
|
-
from pyoframe.model import Model
|
|
30
|
-
|
|
31
|
-
available_solvers = []
|
|
32
|
-
solver_registry: Dict[str, Type["Solver"]] = {}
|
|
33
|
-
|
|
34
|
-
with contextlib.suppress(ImportError):
|
|
35
|
-
import gurobipy
|
|
36
|
-
|
|
37
|
-
available_solvers.append("gurobi")
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def _register_solver(solver_name):
|
|
41
|
-
def decorator(cls):
|
|
42
|
-
solver_registry[solver_name] = cls
|
|
43
|
-
return cls
|
|
44
|
-
|
|
45
|
-
return decorator
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def solve(
|
|
49
|
-
m: "Model",
|
|
50
|
-
solver=None,
|
|
51
|
-
directory: Optional[Union[Path, str]] = None,
|
|
52
|
-
use_var_names=False,
|
|
53
|
-
log_fn=None,
|
|
54
|
-
warmstart_fn=None,
|
|
55
|
-
basis_fn=None,
|
|
56
|
-
solution_file=None,
|
|
57
|
-
log_to_console=True,
|
|
58
|
-
):
|
|
59
|
-
if solver is None:
|
|
60
|
-
if len(available_solvers) == 0:
|
|
61
|
-
raise ValueError(
|
|
62
|
-
"No solvers available. Please install a solving library like gurobipy."
|
|
63
|
-
)
|
|
64
|
-
solver = available_solvers[0]
|
|
65
|
-
|
|
66
|
-
if solver not in solver_registry:
|
|
67
|
-
raise ValueError(f"Solver {solver} not recognized or supported.")
|
|
68
|
-
|
|
69
|
-
solver_cls = solver_registry[solver]
|
|
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)
|
|
77
|
-
m.solver.solver_model = m.solver_model
|
|
78
|
-
|
|
79
|
-
for attr_container in [m.variables, m.constraints, [m]]:
|
|
80
|
-
for container in attr_container:
|
|
81
|
-
for param_name, param_value in container.attr:
|
|
82
|
-
m.solver.set_attr(container, param_name, param_value)
|
|
83
|
-
|
|
84
|
-
result = m.solver.solve(log_fn, warmstart_fn, basis_fn, solution_file)
|
|
85
|
-
result = m.solver.process_result(result)
|
|
86
|
-
m.result = result
|
|
87
|
-
|
|
88
|
-
if result.solution is not None:
|
|
89
|
-
if m.objective is not None:
|
|
90
|
-
m.objective.value = result.solution.objective
|
|
91
|
-
|
|
92
|
-
for variable in m.variables:
|
|
93
|
-
variable.solution = result.solution.primal
|
|
94
|
-
|
|
95
|
-
if result.solution.dual is not None:
|
|
96
|
-
for constraint in m.constraints:
|
|
97
|
-
constraint.dual = result.solution.dual
|
|
98
|
-
|
|
99
|
-
return result
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
class Solver(ABC):
|
|
103
|
-
def __init__(self, model: "Model", log_to_console, params, directory):
|
|
104
|
-
self._model = model
|
|
105
|
-
self.solver_model: Optional[Any] = None
|
|
106
|
-
self.log_to_console: bool = log_to_console
|
|
107
|
-
self.params = params
|
|
108
|
-
self.directory = directory
|
|
109
|
-
|
|
110
|
-
@abstractmethod
|
|
111
|
-
def create_solver_model(self, use_var_names) -> Any: ...
|
|
112
|
-
|
|
113
|
-
@abstractmethod
|
|
114
|
-
def set_attr(self, element, param_name, param_value): ...
|
|
115
|
-
|
|
116
|
-
@abstractmethod
|
|
117
|
-
def solve(self, log_fn, warmstart_fn, basis_fn, solution_file) -> Result: ...
|
|
118
|
-
|
|
119
|
-
@abstractmethod
|
|
120
|
-
def process_result(self, results: Result) -> Result: ...
|
|
121
|
-
|
|
122
|
-
def load_rc(self):
|
|
123
|
-
rc = self._get_all_rc()
|
|
124
|
-
for variable in self._model.variables:
|
|
125
|
-
variable.RC = rc
|
|
126
|
-
|
|
127
|
-
def load_slack(self):
|
|
128
|
-
slack = self._get_all_slack()
|
|
129
|
-
for constraint in self._model.constraints:
|
|
130
|
-
constraint.slack = slack
|
|
131
|
-
|
|
132
|
-
@abstractmethod
|
|
133
|
-
def _get_all_rc(self): ...
|
|
134
|
-
|
|
135
|
-
@abstractmethod
|
|
136
|
-
def _get_all_slack(self): ...
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
class FileBasedSolver(Solver):
|
|
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:
|
|
155
|
-
problem_file = None
|
|
156
|
-
directory = self.directory
|
|
157
|
-
if directory is not None:
|
|
158
|
-
if isinstance(directory, str):
|
|
159
|
-
directory = Path(directory)
|
|
160
|
-
if not directory.exists():
|
|
161
|
-
directory.mkdir(parents=True)
|
|
162
|
-
filename = (
|
|
163
|
-
self._model.name if self._model.name is not None else "pyoframe-problem"
|
|
164
|
-
)
|
|
165
|
-
problem_file = directory / f"{filename}.lp"
|
|
166
|
-
self.problem_file = self._model.to_file(
|
|
167
|
-
problem_file, use_var_names=use_var_names
|
|
168
|
-
)
|
|
169
|
-
assert self._model.io_mappers is not None
|
|
170
|
-
return self.create_solver_model_from_lp()
|
|
171
|
-
|
|
172
|
-
@abstractmethod
|
|
173
|
-
def create_solver_model_from_lp(self) -> Any: ...
|
|
174
|
-
|
|
175
|
-
def set_attr(self, element, param_name, param_value):
|
|
176
|
-
if isinstance(param_value, pl.DataFrame):
|
|
177
|
-
if isinstance(element, pf.Variable):
|
|
178
|
-
param_value = self._model.io_mappers.var_map.apply(param_value)
|
|
179
|
-
elif isinstance(element, pf.Constraint):
|
|
180
|
-
param_value = self._model.io_mappers.const_map.apply(param_value)
|
|
181
|
-
return self.set_attr_unmapped(element, param_name, param_value)
|
|
182
|
-
|
|
183
|
-
@abstractmethod
|
|
184
|
-
def set_attr_unmapped(self, element, param_name, param_value): ...
|
|
185
|
-
|
|
186
|
-
def process_result(self, results: Result) -> Result:
|
|
187
|
-
if results.solution is not None:
|
|
188
|
-
results.solution.primal = self._model.io_mappers.var_map.undo(
|
|
189
|
-
results.solution.primal
|
|
190
|
-
)
|
|
191
|
-
if results.solution.dual is not None:
|
|
192
|
-
results.solution.dual = self._model.io_mappers.const_map.undo(
|
|
193
|
-
results.solution.dual
|
|
194
|
-
)
|
|
195
|
-
|
|
196
|
-
return results
|
|
197
|
-
|
|
198
|
-
def _get_all_rc(self):
|
|
199
|
-
return self._model.io_mappers.var_map.undo(self._get_all_rc_unmapped())
|
|
200
|
-
|
|
201
|
-
def _get_all_slack(self):
|
|
202
|
-
return self._model.io_mappers.const_map.undo(self._get_all_slack_unmapped())
|
|
203
|
-
|
|
204
|
-
@abstractmethod
|
|
205
|
-
def _get_all_rc_unmapped(self): ...
|
|
206
|
-
|
|
207
|
-
@abstractmethod
|
|
208
|
-
def _get_all_slack_unmapped(self): ...
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
@_register_solver("gurobi")
|
|
212
|
-
class GurobiSolver(FileBasedSolver):
|
|
213
|
-
# see https://www.gurobi.com/documentation/10.0/refman/optimization_status_codes.html
|
|
214
|
-
CONDITION_MAP = {
|
|
215
|
-
1: "unknown",
|
|
216
|
-
2: "optimal",
|
|
217
|
-
3: "infeasible",
|
|
218
|
-
4: "infeasible_or_unbounded",
|
|
219
|
-
5: "unbounded",
|
|
220
|
-
6: "other",
|
|
221
|
-
7: "iteration_limit",
|
|
222
|
-
8: "terminated_by_limit",
|
|
223
|
-
9: "time_limit",
|
|
224
|
-
10: "optimal",
|
|
225
|
-
11: "user_interrupt",
|
|
226
|
-
12: "other",
|
|
227
|
-
13: "suboptimal",
|
|
228
|
-
14: "unknown",
|
|
229
|
-
15: "terminated_by_limit",
|
|
230
|
-
16: "internal_solver_error",
|
|
231
|
-
17: "internal_solver_error",
|
|
232
|
-
}
|
|
233
|
-
|
|
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:
|
|
241
|
-
"""
|
|
242
|
-
Solve a linear problem using the gurobi solver.
|
|
243
|
-
|
|
244
|
-
This function communicates with gurobi using the gurubipy package.
|
|
245
|
-
"""
|
|
246
|
-
assert self.problem_file is not None
|
|
247
|
-
self.env = gurobipy.Env(params=self.params)
|
|
248
|
-
|
|
249
|
-
m = gurobipy.read(_path_to_str(self.problem_file), env=self.env)
|
|
250
|
-
if not self.keep_files:
|
|
251
|
-
self.problem_file.unlink()
|
|
252
|
-
|
|
253
|
-
return m
|
|
254
|
-
|
|
255
|
-
@lru_cache
|
|
256
|
-
def _get_var_mapping(self):
|
|
257
|
-
assert self.solver_model is not None
|
|
258
|
-
vars = self.solver_model.getVars()
|
|
259
|
-
return vars, pl.DataFrame(
|
|
260
|
-
{VAR_KEY: self.solver_model.getAttr("VarName", vars)}
|
|
261
|
-
).with_columns(i=pl.int_range(pl.len()))
|
|
262
|
-
|
|
263
|
-
@lru_cache
|
|
264
|
-
def _get_constraint_mapping(self):
|
|
265
|
-
assert self.solver_model is not None
|
|
266
|
-
constraints = self.solver_model.getConstrs()
|
|
267
|
-
return constraints, pl.DataFrame(
|
|
268
|
-
{CONSTRAINT_KEY: self.solver_model.getAttr("ConstrName", constraints)}
|
|
269
|
-
).with_columns(i=pl.int_range(pl.len()))
|
|
270
|
-
|
|
271
|
-
def set_attr_unmapped(self, element, param_name, param_value):
|
|
272
|
-
assert self.solver_model is not None
|
|
273
|
-
if isinstance(element, pf.Model):
|
|
274
|
-
self.solver_model.setAttr(param_name, param_value)
|
|
275
|
-
elif isinstance(element, pf.Variable):
|
|
276
|
-
v, v_map = self._get_var_mapping()
|
|
277
|
-
param_value = param_value.join(v_map, on=VAR_KEY, how="left").drop(VAR_KEY)
|
|
278
|
-
self.solver_model.setAttr(
|
|
279
|
-
param_name,
|
|
280
|
-
[v[i] for i in param_value["i"]],
|
|
281
|
-
param_value[param_name],
|
|
282
|
-
)
|
|
283
|
-
elif isinstance(element, pf.Constraint):
|
|
284
|
-
c, c_map = self._get_constraint_mapping()
|
|
285
|
-
param_value = param_value.join(c_map, on=CONSTRAINT_KEY, how="left").drop(
|
|
286
|
-
CONSTRAINT_KEY
|
|
287
|
-
)
|
|
288
|
-
self.solver_model.setAttr(
|
|
289
|
-
param_name,
|
|
290
|
-
[c[i] for i in param_value["i"]],
|
|
291
|
-
param_value[param_name],
|
|
292
|
-
)
|
|
293
|
-
else:
|
|
294
|
-
raise ValueError(f"Element type {type(element)} not recognized.")
|
|
295
|
-
|
|
296
|
-
def solve(self, log_fn, warmstart_fn, basis_fn, solution_file) -> Result:
|
|
297
|
-
assert self.solver_model is not None
|
|
298
|
-
m = self.solver_model
|
|
299
|
-
if log_fn is not None:
|
|
300
|
-
m.setParam("logfile", _path_to_str(log_fn))
|
|
301
|
-
if warmstart_fn:
|
|
302
|
-
m.read(_path_to_str(warmstart_fn))
|
|
303
|
-
|
|
304
|
-
m.optimize()
|
|
305
|
-
|
|
306
|
-
if basis_fn:
|
|
307
|
-
try:
|
|
308
|
-
m.write(_path_to_str(basis_fn))
|
|
309
|
-
except gurobipy.GurobiError as err:
|
|
310
|
-
print("No model basis stored. Raised error: %s", err)
|
|
311
|
-
|
|
312
|
-
condition = m.status
|
|
313
|
-
termination_condition = GurobiSolver.CONDITION_MAP.get(condition, condition)
|
|
314
|
-
status = Status.from_termination_condition(termination_condition)
|
|
315
|
-
|
|
316
|
-
if status.is_ok and (termination_condition == "optimal"):
|
|
317
|
-
if solution_file:
|
|
318
|
-
m.write(_path_to_str(solution_file))
|
|
319
|
-
|
|
320
|
-
objective = m.ObjVal
|
|
321
|
-
vars = m.getVars()
|
|
322
|
-
sol = pl.DataFrame(
|
|
323
|
-
{
|
|
324
|
-
VAR_KEY: m.getAttr("VarName", vars),
|
|
325
|
-
SOLUTION_KEY: m.getAttr("X", vars),
|
|
326
|
-
}
|
|
327
|
-
)
|
|
328
|
-
|
|
329
|
-
constraints = m.getConstrs()
|
|
330
|
-
try:
|
|
331
|
-
dual = pl.DataFrame(
|
|
332
|
-
{
|
|
333
|
-
DUAL_KEY: m.getAttr("Pi", constraints),
|
|
334
|
-
CONSTRAINT_KEY: m.getAttr("ConstrName", constraints),
|
|
335
|
-
}
|
|
336
|
-
)
|
|
337
|
-
except gurobipy.GurobiError:
|
|
338
|
-
dual = None
|
|
339
|
-
|
|
340
|
-
solution = Solution(sol, dual, objective)
|
|
341
|
-
else:
|
|
342
|
-
solution = None
|
|
343
|
-
|
|
344
|
-
return Result(status, solution)
|
|
345
|
-
|
|
346
|
-
def _get_all_rc_unmapped(self):
|
|
347
|
-
m = self._model.solver_model
|
|
348
|
-
vars = m.getVars()
|
|
349
|
-
return pl.DataFrame(
|
|
350
|
-
{
|
|
351
|
-
RC_COL: m.getAttr("RC", vars),
|
|
352
|
-
VAR_KEY: m.getAttr("VarName", vars),
|
|
353
|
-
}
|
|
354
|
-
)
|
|
355
|
-
|
|
356
|
-
def _get_all_slack_unmapped(self):
|
|
357
|
-
m = self._model.solver_model
|
|
358
|
-
constraints = m.getConstrs()
|
|
359
|
-
return pl.DataFrame(
|
|
360
|
-
{
|
|
361
|
-
SLACK_COL: m.getAttr("Slack", constraints),
|
|
362
|
-
CONSTRAINT_KEY: m.getAttr("ConstrName", constraints),
|
|
363
|
-
}
|
|
364
|
-
)
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
def _path_to_str(path: Union[Path, str]) -> str:
|
|
374
|
-
"""
|
|
375
|
-
Convert a pathlib.Path to a string.
|
|
376
|
-
"""
|
|
377
|
-
return str(path.resolve()) if isinstance(path, Path) else path
|
pyoframe/user_defined.py
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Contains the base classes to support .params and .attr containers for user-defined parameters and attributes.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from typing import Any
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class Container:
|
|
9
|
-
"""
|
|
10
|
-
A container for user-defined attributes or parameters.
|
|
11
|
-
|
|
12
|
-
Parameters:
|
|
13
|
-
preprocess : Callable[str, Any], optional
|
|
14
|
-
A function to preprocess user-defined values before adding them to the container.
|
|
15
|
-
|
|
16
|
-
Examples:
|
|
17
|
-
>>> params = Container()
|
|
18
|
-
>>> params.a = 1
|
|
19
|
-
>>> params.b = 2
|
|
20
|
-
>>> params.a
|
|
21
|
-
1
|
|
22
|
-
>>> params.b
|
|
23
|
-
2
|
|
24
|
-
>>> for k, v in params:
|
|
25
|
-
... print(k, v)
|
|
26
|
-
a 1
|
|
27
|
-
b 2
|
|
28
|
-
"""
|
|
29
|
-
|
|
30
|
-
def __init__(self, preprocess=None):
|
|
31
|
-
self._preprocess = preprocess
|
|
32
|
-
self._attributes = {}
|
|
33
|
-
|
|
34
|
-
def __setattr__(self, name: str, value: Any) -> None:
|
|
35
|
-
if name.startswith("_"):
|
|
36
|
-
return super().__setattr__(name, value)
|
|
37
|
-
if self._preprocess is not None:
|
|
38
|
-
value = self._preprocess(name, value)
|
|
39
|
-
self._attributes[name] = value
|
|
40
|
-
|
|
41
|
-
def __getattr__(self, name: str) -> Any:
|
|
42
|
-
if name.startswith("_"):
|
|
43
|
-
return super().__getattribute__(name)
|
|
44
|
-
return self._attributes[name]
|
|
45
|
-
|
|
46
|
-
def __iter__(self):
|
|
47
|
-
return iter(self._attributes.items())
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
class AttrContainerMixin:
|
|
51
|
-
def __init__(self, *args, **kwargs) -> None:
|
|
52
|
-
super().__init__(*args, **kwargs)
|
|
53
|
-
self.attr = Container(preprocess=self._preprocess_attr)
|
|
54
|
-
|
|
55
|
-
def _preprocess_attr(self, name: str, value: Any) -> Any:
|
|
56
|
-
"""
|
|
57
|
-
Preprocesses user-defined values before adding them to the Params container.
|
|
58
|
-
By default this function does nothing but subclasses can override it.
|
|
59
|
-
"""
|
|
60
|
-
return value
|
pyoframe-0.0.11.dist-info/RECORD
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
pyoframe/__init__.py,sha256=D7HHQPy2Me-LLyfPCcSE74dn83PeMK3aOby7i7oiLTs,507
|
|
2
|
-
pyoframe/_arithmetic.py,sha256=riyN2JC-BnOgTIxGfXKIS-X_p7zm8JaKrLk_KDwKAAw,9046
|
|
3
|
-
pyoframe/constants.py,sha256=WoWNVsTeAqOHdUPkaZqFR3X3swrsf7OQnZ9AOaUF3uE,7358
|
|
4
|
-
pyoframe/core.py,sha256=co4Z-i2AZ4jpvqfTKG67AKcBSFputZ0QrTtrglfkOzQ,51915
|
|
5
|
-
pyoframe/io.py,sha256=DJUQ-WlxYe-Ya49pn_T3z6HhJxSD2LSdxm43AGqLuXw,7106
|
|
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.11.dist-info/LICENSE,sha256=L1pXz6p_1OW5XGWb2UCR6PNu6k3JAT0XWhi8jV0cuRg,1137
|
|
15
|
-
pyoframe-0.0.11.dist-info/METADATA,sha256=qtlWiEBTxnFXguG57ZfmJ0rG4AcjOad5HM3a2CFJym8,3461
|
|
16
|
-
pyoframe-0.0.11.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
|
|
17
|
-
pyoframe-0.0.11.dist-info/top_level.txt,sha256=10z3OOJSVLriQ0IrFLMH8CH9zByugPWolqhlHlkNjV4,9
|
|
18
|
-
pyoframe-0.0.11.dist-info/RECORD,,
|
|
File without changes
|