pyoframe 0.2.1__py3-none-any.whl → 1.0.0a0__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 +265 -159
- pyoframe/_constants.py +416 -0
- pyoframe/_core.py +2575 -0
- pyoframe/_model.py +578 -0
- pyoframe/_model_element.py +175 -0
- pyoframe/_monkey_patch.py +80 -0
- pyoframe/{objective.py → _objective.py} +49 -14
- pyoframe/{util.py → _utils.py} +106 -126
- pyoframe/_version.py +2 -2
- {pyoframe-0.2.1.dist-info → pyoframe-1.0.0a0.dist-info}/METADATA +32 -25
- pyoframe-1.0.0a0.dist-info/RECORD +15 -0
- pyoframe/constants.py +0 -140
- pyoframe/core.py +0 -1787
- pyoframe/model.py +0 -408
- pyoframe/model_element.py +0 -184
- pyoframe/monkey_patch.py +0 -54
- pyoframe-0.2.1.dist-info/RECORD +0 -15
- {pyoframe-0.2.1.dist-info → pyoframe-1.0.0a0.dist-info}/WHEEL +0 -0
- {pyoframe-0.2.1.dist-info → pyoframe-1.0.0a0.dist-info}/licenses/LICENSE +0 -0
- {pyoframe-0.2.1.dist-info → pyoframe-1.0.0a0.dist-info}/top_level.txt +0 -0
pyoframe/model.py
DELETED
|
@@ -1,408 +0,0 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
from typing import Any, Dict, Iterable, List, Optional, Union
|
|
3
|
-
|
|
4
|
-
import pandas as pd
|
|
5
|
-
import polars as pl
|
|
6
|
-
import pyoptinterface as poi
|
|
7
|
-
|
|
8
|
-
from pyoframe.constants import (
|
|
9
|
-
CONST_TERM,
|
|
10
|
-
SUPPORTED_SOLVER_TYPES,
|
|
11
|
-
SUPPORTED_SOLVERS,
|
|
12
|
-
Config,
|
|
13
|
-
ObjSense,
|
|
14
|
-
ObjSenseValue,
|
|
15
|
-
PyoframeError,
|
|
16
|
-
VType,
|
|
17
|
-
)
|
|
18
|
-
from pyoframe.core import Constraint, Variable
|
|
19
|
-
from pyoframe.model_element import ModelElement, ModelElementWithId
|
|
20
|
-
from pyoframe.objective import Objective
|
|
21
|
-
from pyoframe.util import Container, NamedVariableMapper, for_solvers, get_obj_repr
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class Model:
|
|
25
|
-
"""
|
|
26
|
-
The object that holds all the variables, constraints, and the objective.
|
|
27
|
-
|
|
28
|
-
Parameters:
|
|
29
|
-
name:
|
|
30
|
-
The name of the model. Currently it is not used for much.
|
|
31
|
-
solver:
|
|
32
|
-
The solver to use. If `None`, `Config.default_solver` will be used.
|
|
33
|
-
If `Config.default_solver` has not been set (`None`), Pyoframe will try to detect whichever solver is already installed.
|
|
34
|
-
solver_env:
|
|
35
|
-
Gurobi only: a dictionary of parameters to set when creating the Gurobi environment.
|
|
36
|
-
use_var_names:
|
|
37
|
-
Whether to pass variable names to the solver. Set to `True` if you'd like outputs from e.g. `Model.write()` to be legible.
|
|
38
|
-
Does not work with HiGHS (see [here](https://github.com/Bravos-Power/pyoframe/issues/102#issuecomment-2727521430)).
|
|
39
|
-
sense:
|
|
40
|
-
Either "min" or "max". Indicates whether it's a minmization or maximization problem.
|
|
41
|
-
Typically, this parameter can be omitted (`None`) as it will automatically be
|
|
42
|
-
set when the objective is set using `.minimize` or `.maximize`.
|
|
43
|
-
|
|
44
|
-
Examples:
|
|
45
|
-
>>> m = pf.Model()
|
|
46
|
-
>>> m.X = pf.Variable()
|
|
47
|
-
>>> m.my_constraint = m.X <= 10
|
|
48
|
-
>>> m
|
|
49
|
-
<Model vars=1 constrs=1 objective=False>
|
|
50
|
-
|
|
51
|
-
Try setting the Gurobi license:
|
|
52
|
-
>>> m = pf.Model(solver="gurobi", solver_env=dict(ComputeServer="myserver", ServerPassword="mypassword"))
|
|
53
|
-
Traceback (most recent call last):
|
|
54
|
-
...
|
|
55
|
-
RuntimeError: Could not resolve host: myserver (code 6, command POST http://myserver/api/v1/cluster/jobs)
|
|
56
|
-
"""
|
|
57
|
-
|
|
58
|
-
_reserved_attributes = [
|
|
59
|
-
"_variables",
|
|
60
|
-
"_constraints",
|
|
61
|
-
"_objective",
|
|
62
|
-
"var_map",
|
|
63
|
-
"io_mappers",
|
|
64
|
-
"name",
|
|
65
|
-
"solver",
|
|
66
|
-
"poi",
|
|
67
|
-
"params",
|
|
68
|
-
"result",
|
|
69
|
-
"attr",
|
|
70
|
-
"sense",
|
|
71
|
-
"objective",
|
|
72
|
-
"_use_var_names",
|
|
73
|
-
"ONE",
|
|
74
|
-
"solver_name",
|
|
75
|
-
"minimize",
|
|
76
|
-
"maximize",
|
|
77
|
-
]
|
|
78
|
-
|
|
79
|
-
def __init__(
|
|
80
|
-
self,
|
|
81
|
-
name: Optional[str] = None,
|
|
82
|
-
solver: Optional[SUPPORTED_SOLVER_TYPES] = None,
|
|
83
|
-
solver_env: Optional[Dict[str, str]] = None,
|
|
84
|
-
use_var_names: bool = False,
|
|
85
|
-
sense: Union[ObjSense, ObjSenseValue, None] = None,
|
|
86
|
-
):
|
|
87
|
-
self.poi, self.solver_name = Model.create_poi_model(solver, solver_env)
|
|
88
|
-
self._variables: List[Variable] = []
|
|
89
|
-
self._constraints: List[Constraint] = []
|
|
90
|
-
self.sense = ObjSense(sense) if sense is not None else None
|
|
91
|
-
self._objective: Optional[Objective] = None
|
|
92
|
-
self.var_map = (
|
|
93
|
-
NamedVariableMapper(Variable) if Config.print_uses_variable_names else None
|
|
94
|
-
)
|
|
95
|
-
self.name = name
|
|
96
|
-
|
|
97
|
-
self.params = Container(self._set_param, self._get_param)
|
|
98
|
-
self.attr = Container(self._set_attr, self._get_attr)
|
|
99
|
-
self._use_var_names = use_var_names
|
|
100
|
-
|
|
101
|
-
@property
|
|
102
|
-
def use_var_names(self):
|
|
103
|
-
return self._use_var_names
|
|
104
|
-
|
|
105
|
-
@classmethod
|
|
106
|
-
def create_poi_model(
|
|
107
|
-
cls, solver: Optional[str], solver_env: Optional[Dict[str, str]]
|
|
108
|
-
):
|
|
109
|
-
if solver is None:
|
|
110
|
-
if Config.default_solver is None:
|
|
111
|
-
for solver_option in SUPPORTED_SOLVERS:
|
|
112
|
-
try:
|
|
113
|
-
return cls.create_poi_model(solver_option, solver_env)
|
|
114
|
-
except RuntimeError:
|
|
115
|
-
pass
|
|
116
|
-
raise ValueError(
|
|
117
|
-
'Could not automatically find a solver. Is one installed? If so, specify which one: e.g. Model(solver="gurobi")'
|
|
118
|
-
)
|
|
119
|
-
else:
|
|
120
|
-
solver = Config.default_solver
|
|
121
|
-
|
|
122
|
-
solver = solver.lower()
|
|
123
|
-
if solver == "gurobi":
|
|
124
|
-
from pyoptinterface import gurobi
|
|
125
|
-
|
|
126
|
-
if solver_env is None:
|
|
127
|
-
env = gurobi.Env()
|
|
128
|
-
else:
|
|
129
|
-
env = gurobi.Env(empty=True)
|
|
130
|
-
for key, value in solver_env.items():
|
|
131
|
-
env.set_raw_parameter(key, value)
|
|
132
|
-
env.start()
|
|
133
|
-
model = gurobi.Model(env)
|
|
134
|
-
elif solver == "highs":
|
|
135
|
-
from pyoptinterface import highs
|
|
136
|
-
|
|
137
|
-
model = highs.Model()
|
|
138
|
-
else:
|
|
139
|
-
raise ValueError(
|
|
140
|
-
f"Solver {solver} not recognized or supported."
|
|
141
|
-
) # pragma: no cover
|
|
142
|
-
|
|
143
|
-
constant_var = model.add_variable(lb=1, ub=1, name="ONE")
|
|
144
|
-
if constant_var.index != CONST_TERM:
|
|
145
|
-
raise ValueError(
|
|
146
|
-
"The first variable should have index 0."
|
|
147
|
-
) # pragma: no cover
|
|
148
|
-
return model, solver
|
|
149
|
-
|
|
150
|
-
@property
|
|
151
|
-
def variables(self) -> List[Variable]:
|
|
152
|
-
return self._variables
|
|
153
|
-
|
|
154
|
-
@property
|
|
155
|
-
def binary_variables(self) -> Iterable[Variable]:
|
|
156
|
-
"""
|
|
157
|
-
Examples:
|
|
158
|
-
>>> m = pf.Model()
|
|
159
|
-
>>> m.X = pf.Variable(vtype=pf.VType.BINARY)
|
|
160
|
-
>>> m.Y = pf.Variable()
|
|
161
|
-
>>> len(list(m.binary_variables))
|
|
162
|
-
1
|
|
163
|
-
"""
|
|
164
|
-
return (v for v in self.variables if v.vtype == VType.BINARY)
|
|
165
|
-
|
|
166
|
-
@property
|
|
167
|
-
def integer_variables(self) -> Iterable[Variable]:
|
|
168
|
-
"""
|
|
169
|
-
Examples:
|
|
170
|
-
>>> m = pf.Model()
|
|
171
|
-
>>> m.X = pf.Variable(vtype=pf.VType.INTEGER)
|
|
172
|
-
>>> m.Y = pf.Variable()
|
|
173
|
-
>>> len(list(m.integer_variables))
|
|
174
|
-
1
|
|
175
|
-
"""
|
|
176
|
-
return (v for v in self.variables if v.vtype == VType.INTEGER)
|
|
177
|
-
|
|
178
|
-
@property
|
|
179
|
-
def constraints(self):
|
|
180
|
-
return self._constraints
|
|
181
|
-
|
|
182
|
-
@property
|
|
183
|
-
def objective(self):
|
|
184
|
-
return self._objective
|
|
185
|
-
|
|
186
|
-
@objective.setter
|
|
187
|
-
def objective(self, value):
|
|
188
|
-
if self._objective is not None and (
|
|
189
|
-
not isinstance(value, Objective) or not value._constructive
|
|
190
|
-
):
|
|
191
|
-
raise ValueError("An objective already exists. Use += or -= to modify it.")
|
|
192
|
-
if not isinstance(value, Objective):
|
|
193
|
-
value = Objective(value)
|
|
194
|
-
self._objective = value
|
|
195
|
-
value.on_add_to_model(self, "objective")
|
|
196
|
-
|
|
197
|
-
@property
|
|
198
|
-
def minimize(self):
|
|
199
|
-
if self.sense != ObjSense.MIN:
|
|
200
|
-
raise ValueError("Can't get .minimize in a maximization problem.")
|
|
201
|
-
return self._objective
|
|
202
|
-
|
|
203
|
-
@minimize.setter
|
|
204
|
-
def minimize(self, value):
|
|
205
|
-
if self.sense is None:
|
|
206
|
-
self.sense = ObjSense.MIN
|
|
207
|
-
if self.sense != ObjSense.MIN:
|
|
208
|
-
raise ValueError("Can't set .minimize in a maximization problem.")
|
|
209
|
-
self.objective = value
|
|
210
|
-
|
|
211
|
-
@property
|
|
212
|
-
def maximize(self):
|
|
213
|
-
if self.sense != ObjSense.MAX:
|
|
214
|
-
raise ValueError("Can't get .maximize in a minimization problem.")
|
|
215
|
-
return self._objective
|
|
216
|
-
|
|
217
|
-
@maximize.setter
|
|
218
|
-
def maximize(self, value):
|
|
219
|
-
if self.sense is None:
|
|
220
|
-
self.sense = ObjSense.MAX
|
|
221
|
-
if self.sense != ObjSense.MAX:
|
|
222
|
-
raise ValueError("Can't set .maximize in a minimization problem.")
|
|
223
|
-
self.objective = value
|
|
224
|
-
|
|
225
|
-
def __setattr__(self, __name: str, __value: Any) -> None:
|
|
226
|
-
if __name not in Model._reserved_attributes and not isinstance(
|
|
227
|
-
__value, (ModelElement, pl.DataFrame, pd.DataFrame)
|
|
228
|
-
):
|
|
229
|
-
raise PyoframeError(
|
|
230
|
-
f"Cannot set attribute '{__name}' on the model because it isn't of type ModelElement (e.g. Variable, Constraint, ...)"
|
|
231
|
-
)
|
|
232
|
-
|
|
233
|
-
if (
|
|
234
|
-
isinstance(__value, ModelElement)
|
|
235
|
-
and __name not in Model._reserved_attributes
|
|
236
|
-
):
|
|
237
|
-
if isinstance(__value, ModelElementWithId):
|
|
238
|
-
assert not hasattr(self, __name), (
|
|
239
|
-
f"Cannot create {__name} since it was already created."
|
|
240
|
-
)
|
|
241
|
-
|
|
242
|
-
__value.on_add_to_model(self, __name)
|
|
243
|
-
|
|
244
|
-
if isinstance(__value, Variable):
|
|
245
|
-
self._variables.append(__value)
|
|
246
|
-
if self.var_map is not None:
|
|
247
|
-
self.var_map.add(__value)
|
|
248
|
-
elif isinstance(__value, Constraint):
|
|
249
|
-
self._constraints.append(__value)
|
|
250
|
-
return super().__setattr__(__name, __value)
|
|
251
|
-
|
|
252
|
-
def __repr__(self) -> str:
|
|
253
|
-
return get_obj_repr(
|
|
254
|
-
self,
|
|
255
|
-
name=self.name,
|
|
256
|
-
vars=len(self.variables),
|
|
257
|
-
constrs=len(self.constraints),
|
|
258
|
-
objective=bool(self.objective),
|
|
259
|
-
)
|
|
260
|
-
|
|
261
|
-
def write(self, file_path: Union[Path, str], pretty: bool = False):
|
|
262
|
-
"""
|
|
263
|
-
Output the model to a file.
|
|
264
|
-
|
|
265
|
-
Typical usage includes writing the solution to a `.sol` file as well as writing the problem to a `.lp` or `.mps` file.
|
|
266
|
-
Set `use_var_names` in your model constructor to `True` if you'd like the output to contain human-readable names (useful for debugging).
|
|
267
|
-
|
|
268
|
-
Parameters:
|
|
269
|
-
file_path:
|
|
270
|
-
The path to the file to write to.
|
|
271
|
-
pretty:
|
|
272
|
-
Only used when writing .sol files in HiGHS. If `True`, will use HiGH's pretty print columnar style which contains more information.
|
|
273
|
-
"""
|
|
274
|
-
file_path = Path(file_path)
|
|
275
|
-
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
276
|
-
kwargs = {}
|
|
277
|
-
if self.solver_name == "highs":
|
|
278
|
-
if self.use_var_names:
|
|
279
|
-
self.params.write_solution_style = 1
|
|
280
|
-
kwargs["pretty"] = pretty
|
|
281
|
-
self.poi.write(str(file_path), **kwargs)
|
|
282
|
-
|
|
283
|
-
def optimize(self):
|
|
284
|
-
"""
|
|
285
|
-
Optimize the model using your selected solver (e.g. Gurobi, HiGHS).
|
|
286
|
-
"""
|
|
287
|
-
self.poi.optimize()
|
|
288
|
-
|
|
289
|
-
@for_solvers("gurobi")
|
|
290
|
-
def convert_to_fixed(self) -> None:
|
|
291
|
-
"""
|
|
292
|
-
Turns a mixed integer program into a continuous one by fixing
|
|
293
|
-
all the integer and binary variables to their solution values.
|
|
294
|
-
|
|
295
|
-
!!! warning "Gurobi only"
|
|
296
|
-
This method only works with the Gurobi solver. Open an issue if you'd like to see support for other solvers.
|
|
297
|
-
|
|
298
|
-
Examples:
|
|
299
|
-
>>> m = pf.Model(solver="gurobi")
|
|
300
|
-
>>> m.X = pf.Variable(vtype=pf.VType.BINARY, lb=0)
|
|
301
|
-
>>> m.Y = pf.Variable(vtype=pf.VType.INTEGER, lb=0)
|
|
302
|
-
>>> m.Z = pf.Variable(lb=0)
|
|
303
|
-
>>> m.my_constraint = m.X + m.Y + m.Z <= 10
|
|
304
|
-
>>> m.maximize = 3 * m.X + 2 * m.Y + m.Z
|
|
305
|
-
>>> m.optimize()
|
|
306
|
-
>>> m.X.solution, m.Y.solution, m.Z.solution
|
|
307
|
-
(1, 9, 0.0)
|
|
308
|
-
>>> m.my_constraint.dual
|
|
309
|
-
Traceback (most recent call last):
|
|
310
|
-
...
|
|
311
|
-
RuntimeError: Unable to retrieve attribute 'Pi'
|
|
312
|
-
>>> m.convert_to_fixed()
|
|
313
|
-
>>> m.optimize()
|
|
314
|
-
>>> m.my_constraint.dual
|
|
315
|
-
1.0
|
|
316
|
-
|
|
317
|
-
Only works for Gurobi:
|
|
318
|
-
|
|
319
|
-
>>> m = pf.Model("max", solver="highs")
|
|
320
|
-
>>> m.convert_to_fixed()
|
|
321
|
-
Traceback (most recent call last):
|
|
322
|
-
...
|
|
323
|
-
NotImplementedError: Method 'convert_to_fixed' is not implemented for solver 'highs'.
|
|
324
|
-
"""
|
|
325
|
-
self.poi._converttofixed()
|
|
326
|
-
|
|
327
|
-
@for_solvers("gurobi", "copt")
|
|
328
|
-
def compute_IIS(self):
|
|
329
|
-
"""
|
|
330
|
-
Computes the Irreducible Infeasible Set (IIS) of the model.
|
|
331
|
-
|
|
332
|
-
!!! warning "Gurobi only"
|
|
333
|
-
This method only works with the Gurobi solver. Open an issue if you'd like to see support for other solvers.
|
|
334
|
-
|
|
335
|
-
Examples:
|
|
336
|
-
>>> m = pf.Model(solver="gurobi")
|
|
337
|
-
>>> m.X = pf.Variable(lb=0, ub=2)
|
|
338
|
-
>>> m.Y = pf.Variable(lb=0, ub=2)
|
|
339
|
-
>>> m.bad_constraint = m.X >= 3
|
|
340
|
-
>>> m.minimize = m.X + m.Y
|
|
341
|
-
>>> m.optimize()
|
|
342
|
-
>>> m.attr.TerminationStatus
|
|
343
|
-
<TerminationStatusCode.INFEASIBLE: 3>
|
|
344
|
-
>>> m.bad_constraint.attr.IIS
|
|
345
|
-
Traceback (most recent call last):
|
|
346
|
-
...
|
|
347
|
-
RuntimeError: Unable to retrieve attribute 'IISConstr'
|
|
348
|
-
>>> m.compute_IIS()
|
|
349
|
-
>>> m.bad_constraint.attr.IIS
|
|
350
|
-
True
|
|
351
|
-
"""
|
|
352
|
-
self.poi.computeIIS()
|
|
353
|
-
|
|
354
|
-
def dispose(self):
|
|
355
|
-
"""
|
|
356
|
-
Disposes of the model and cleans up the solver environment.
|
|
357
|
-
|
|
358
|
-
When using Gurobi compute server, this cleanup will
|
|
359
|
-
ensure your run is not marked as 'ABORTED'.
|
|
360
|
-
|
|
361
|
-
Note that once the model is disposed, it cannot be used anymore.
|
|
362
|
-
|
|
363
|
-
Examples:
|
|
364
|
-
>>> m = pf.Model()
|
|
365
|
-
>>> m.X = pf.Variable(ub=1)
|
|
366
|
-
>>> m.maximize = m.X
|
|
367
|
-
>>> m.optimize()
|
|
368
|
-
>>> m.X.solution
|
|
369
|
-
1.0
|
|
370
|
-
>>> m.dispose()
|
|
371
|
-
"""
|
|
372
|
-
env = None
|
|
373
|
-
if hasattr(self.poi, "_env"):
|
|
374
|
-
env = self.poi._env
|
|
375
|
-
self.poi.close()
|
|
376
|
-
if env is not None:
|
|
377
|
-
env.close()
|
|
378
|
-
|
|
379
|
-
def __del__(self):
|
|
380
|
-
# This ensures that the model is closed *before* the environment is. This avoids the Gurobi warning:
|
|
381
|
-
# Warning: environment still referenced so free is deferred (Continue to use WLS)
|
|
382
|
-
# I include the hasattr check to avoid errors in case __init__ failed and poi was never set.
|
|
383
|
-
if hasattr(self, "poi"):
|
|
384
|
-
self.poi.close()
|
|
385
|
-
|
|
386
|
-
def _set_param(self, name, value):
|
|
387
|
-
self.poi.set_raw_parameter(name, value)
|
|
388
|
-
|
|
389
|
-
def _get_param(self, name):
|
|
390
|
-
return self.poi.get_raw_parameter(name)
|
|
391
|
-
|
|
392
|
-
def _set_attr(self, name, value):
|
|
393
|
-
try:
|
|
394
|
-
self.poi.set_model_attribute(poi.ModelAttribute[name], value)
|
|
395
|
-
except KeyError as e:
|
|
396
|
-
if self.solver_name == "gurobi":
|
|
397
|
-
self.poi.set_model_raw_attribute(name, value)
|
|
398
|
-
else:
|
|
399
|
-
raise e
|
|
400
|
-
|
|
401
|
-
def _get_attr(self, name):
|
|
402
|
-
try:
|
|
403
|
-
return self.poi.get_model_attribute(poi.ModelAttribute[name])
|
|
404
|
-
except KeyError as e:
|
|
405
|
-
if self.solver_name == "gurobi":
|
|
406
|
-
return self.poi.get_model_raw_attribute(name)
|
|
407
|
-
else:
|
|
408
|
-
raise e
|
pyoframe/model_element.py
DELETED
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from abc import ABC, abstractmethod
|
|
4
|
-
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
|
5
|
-
|
|
6
|
-
import polars as pl
|
|
7
|
-
|
|
8
|
-
from pyoframe._arithmetic import _get_dimensions
|
|
9
|
-
from pyoframe.constants import (
|
|
10
|
-
COEF_KEY,
|
|
11
|
-
KEY_TYPE,
|
|
12
|
-
QUAD_VAR_KEY,
|
|
13
|
-
RESERVED_COL_KEYS,
|
|
14
|
-
VAR_KEY,
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
if TYPE_CHECKING: # pragma: no cover
|
|
18
|
-
from pyoframe.model import Model
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class ModelElement(ABC):
|
|
22
|
-
def __init__(self, data: pl.DataFrame, **kwargs) -> None:
|
|
23
|
-
# Sanity checks, no duplicate column names
|
|
24
|
-
assert len(data.columns) == len(set(data.columns)), (
|
|
25
|
-
"Duplicate column names found."
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
cols = _get_dimensions(data)
|
|
29
|
-
if cols is None:
|
|
30
|
-
cols = []
|
|
31
|
-
cols += [col for col in RESERVED_COL_KEYS if col in data.columns]
|
|
32
|
-
|
|
33
|
-
# Reorder columns to keep things consistent
|
|
34
|
-
data = data.select(cols)
|
|
35
|
-
|
|
36
|
-
# Cast to proper dtype
|
|
37
|
-
if COEF_KEY in data.columns:
|
|
38
|
-
data = data.cast({COEF_KEY: pl.Float64})
|
|
39
|
-
if VAR_KEY in data.columns:
|
|
40
|
-
data = data.cast({VAR_KEY: KEY_TYPE})
|
|
41
|
-
if QUAD_VAR_KEY in data.columns:
|
|
42
|
-
data = data.cast({QUAD_VAR_KEY: KEY_TYPE})
|
|
43
|
-
|
|
44
|
-
self._data = data
|
|
45
|
-
self._model: Optional[Model] = None
|
|
46
|
-
self.name = None
|
|
47
|
-
super().__init__(**kwargs)
|
|
48
|
-
|
|
49
|
-
def on_add_to_model(self, model: "Model", name: str):
|
|
50
|
-
self.name = name
|
|
51
|
-
self._model = model
|
|
52
|
-
|
|
53
|
-
@property
|
|
54
|
-
def data(self) -> pl.DataFrame:
|
|
55
|
-
return self._data
|
|
56
|
-
|
|
57
|
-
@property
|
|
58
|
-
def friendly_name(self) -> str:
|
|
59
|
-
return self.name if self.name is not None else "unnamed"
|
|
60
|
-
|
|
61
|
-
@property
|
|
62
|
-
def dimensions(self) -> Optional[List[str]]:
|
|
63
|
-
"""
|
|
64
|
-
The names of the data's dimensions.
|
|
65
|
-
|
|
66
|
-
Examples:
|
|
67
|
-
>>> # A variable with no dimensions
|
|
68
|
-
>>> pf.Variable().dimensions
|
|
69
|
-
|
|
70
|
-
>>> # A variable with dimensions of "hour" and "city"
|
|
71
|
-
>>> pf.Variable([{"hour": ["00:00", "06:00", "12:00", "18:00"]}, {"city": ["Toronto", "Berlin", "Paris"]}]).dimensions
|
|
72
|
-
['hour', 'city']
|
|
73
|
-
"""
|
|
74
|
-
return _get_dimensions(self.data)
|
|
75
|
-
|
|
76
|
-
@property
|
|
77
|
-
def dimensions_unsafe(self) -> List[str]:
|
|
78
|
-
"""
|
|
79
|
-
Same as `dimensions` but returns an empty list if there are no dimensions instead of None.
|
|
80
|
-
When unsure, use `dimensions` instead since the type checker forces users to handle the None case (no dimensions).
|
|
81
|
-
"""
|
|
82
|
-
dims = self.dimensions
|
|
83
|
-
if dims is None:
|
|
84
|
-
return []
|
|
85
|
-
return dims
|
|
86
|
-
|
|
87
|
-
@property
|
|
88
|
-
def shape(self) -> Dict[str, int]:
|
|
89
|
-
"""
|
|
90
|
-
The number of indices in each dimension.
|
|
91
|
-
|
|
92
|
-
Examples:
|
|
93
|
-
>>> # A variable with no dimensions
|
|
94
|
-
>>> pf.Variable().shape
|
|
95
|
-
{}
|
|
96
|
-
>>> # A variable with dimensions of "hour" and "city"
|
|
97
|
-
>>> pf.Variable([{"hour": ["00:00", "06:00", "12:00", "18:00"]}, {"city": ["Toronto", "Berlin", "Paris"]}]).shape
|
|
98
|
-
{'hour': 4, 'city': 3}
|
|
99
|
-
"""
|
|
100
|
-
dims = self.dimensions
|
|
101
|
-
if dims is None:
|
|
102
|
-
return {}
|
|
103
|
-
return {dim: self.data[dim].n_unique() for dim in dims}
|
|
104
|
-
|
|
105
|
-
def __len__(self) -> int:
|
|
106
|
-
dims = self.dimensions
|
|
107
|
-
if dims is None:
|
|
108
|
-
return 1
|
|
109
|
-
return self.data.select(dims).n_unique()
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
def _support_polars_method(method_name: str):
|
|
113
|
-
"""
|
|
114
|
-
Wrapper to add a method to ModelElement that simply calls the underlying Polars method on the data attribute.
|
|
115
|
-
"""
|
|
116
|
-
|
|
117
|
-
def method(self: "SupportPolarsMethodMixin", *args, **kwargs) -> Any:
|
|
118
|
-
result_from_polars = getattr(self.data, method_name)(*args, **kwargs)
|
|
119
|
-
if isinstance(result_from_polars, pl.DataFrame):
|
|
120
|
-
return self._new(result_from_polars)
|
|
121
|
-
else:
|
|
122
|
-
return result_from_polars
|
|
123
|
-
|
|
124
|
-
return method
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
class SupportPolarsMethodMixin(ABC):
|
|
128
|
-
rename = _support_polars_method("rename")
|
|
129
|
-
with_columns = _support_polars_method("with_columns")
|
|
130
|
-
filter = _support_polars_method("filter")
|
|
131
|
-
estimated_size = _support_polars_method("estimated_size")
|
|
132
|
-
|
|
133
|
-
@abstractmethod
|
|
134
|
-
def _new(self, data: pl.DataFrame):
|
|
135
|
-
"""
|
|
136
|
-
Used to create a new instance of the same class with the given data (for e.g. on .rename(), .with_columns(), etc.).
|
|
137
|
-
"""
|
|
138
|
-
|
|
139
|
-
@property
|
|
140
|
-
@abstractmethod
|
|
141
|
-
def data(self): ...
|
|
142
|
-
|
|
143
|
-
def pick(self, **kwargs):
|
|
144
|
-
"""
|
|
145
|
-
Filters elements by the given criteria and then drops the filtered dimensions.
|
|
146
|
-
|
|
147
|
-
Examples:
|
|
148
|
-
>>> m = pf.Model()
|
|
149
|
-
>>> m.v = pf.Variable([{"hour": ["00:00", "06:00", "12:00", "18:00"]}, {"city": ["Toronto", "Berlin", "Paris"]}])
|
|
150
|
-
>>> m.v.pick(hour="06:00")
|
|
151
|
-
<Expression size=3 dimensions={'city': 3} terms=3>
|
|
152
|
-
[Toronto]: v[06:00,Toronto]
|
|
153
|
-
[Berlin]: v[06:00,Berlin]
|
|
154
|
-
[Paris]: v[06:00,Paris]
|
|
155
|
-
>>> m.v.pick(hour="06:00", city="Toronto")
|
|
156
|
-
<Expression size=1 dimensions={} terms=1>
|
|
157
|
-
v[06:00,Toronto]
|
|
158
|
-
"""
|
|
159
|
-
return self._new(self.data.filter(**kwargs).drop(kwargs.keys()))
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
class ModelElementWithId(ModelElement):
|
|
163
|
-
"""
|
|
164
|
-
Provides a method that assigns a unique ID to each row in a DataFrame.
|
|
165
|
-
IDs start at 1 and go up consecutively. No zero ID is assigned since it is reserved for the constant variable term.
|
|
166
|
-
IDs are only unique for the subclass since different subclasses have different counters.
|
|
167
|
-
"""
|
|
168
|
-
|
|
169
|
-
@property
|
|
170
|
-
def _has_ids(self) -> bool:
|
|
171
|
-
return self.get_id_column_name() in self.data.columns
|
|
172
|
-
|
|
173
|
-
def _assert_has_ids(self):
|
|
174
|
-
if not self._has_ids:
|
|
175
|
-
raise ValueError(
|
|
176
|
-
f"Cannot use '{self.__class__.__name__}' before it has beed added to a model."
|
|
177
|
-
)
|
|
178
|
-
|
|
179
|
-
@classmethod
|
|
180
|
-
@abstractmethod
|
|
181
|
-
def get_id_column_name(cls) -> str:
|
|
182
|
-
"""
|
|
183
|
-
Returns the name of the column containing the IDs.
|
|
184
|
-
"""
|
pyoframe/monkey_patch.py
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
from functools import wraps
|
|
2
|
-
|
|
3
|
-
import pandas as pd
|
|
4
|
-
import polars as pl
|
|
5
|
-
|
|
6
|
-
from pyoframe.constants import COEF_KEY, CONST_TERM, VAR_KEY
|
|
7
|
-
from pyoframe.core import Expression, SupportsMath
|
|
8
|
-
|
|
9
|
-
# pyright: reportAttributeAccessIssue=false
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def _patch_class(cls):
|
|
13
|
-
def _patch_method(func):
|
|
14
|
-
@wraps(func)
|
|
15
|
-
def wrapper(self, other):
|
|
16
|
-
if isinstance(other, SupportsMath):
|
|
17
|
-
return NotImplemented
|
|
18
|
-
return func(self, other)
|
|
19
|
-
|
|
20
|
-
return wrapper
|
|
21
|
-
|
|
22
|
-
cls.__add__ = _patch_method(cls.__add__)
|
|
23
|
-
cls.__mul__ = _patch_method(cls.__mul__)
|
|
24
|
-
cls.__sub__ = _patch_method(cls.__sub__)
|
|
25
|
-
cls.__le__ = _patch_method(cls.__le__)
|
|
26
|
-
cls.__ge__ = _patch_method(cls.__ge__)
|
|
27
|
-
cls.__contains__ = _patch_method(cls.__contains__)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def _dataframe_to_expr(self: pl.DataFrame) -> Expression:
|
|
31
|
-
return Expression(
|
|
32
|
-
self.rename({self.columns[-1]: COEF_KEY})
|
|
33
|
-
.drop_nulls(COEF_KEY)
|
|
34
|
-
.with_columns(pl.lit(CONST_TERM).alias(VAR_KEY))
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def patch_dataframe_libraries():
|
|
39
|
-
"""
|
|
40
|
-
Applies two patches to the DataFrame and Series classes of both pandas and polars.
|
|
41
|
-
1) Patches arithmetic operators (e.g. `__add__`) such that operations between DataFrames/Series and `Expressionable`s
|
|
42
|
-
are not supported (i.e. `return NotImplemented`). This leads Python to try the reverse operation (e.g. `__radd__`)
|
|
43
|
-
which is supported by the `Expressionable` class.
|
|
44
|
-
2) Adds a `to_expr` method to DataFrame/Series that allows them to be converted to an `Expression` object.
|
|
45
|
-
Series become dataframes and dataframes become expressions where everything but the last column are treated as dimensions.
|
|
46
|
-
"""
|
|
47
|
-
_patch_class(pd.DataFrame)
|
|
48
|
-
_patch_class(pd.Series)
|
|
49
|
-
_patch_class(pl.DataFrame)
|
|
50
|
-
_patch_class(pl.Series)
|
|
51
|
-
pl.DataFrame.to_expr = _dataframe_to_expr
|
|
52
|
-
pl.Series.to_expr = lambda self: self.to_frame().to_expr()
|
|
53
|
-
pd.DataFrame.to_expr = lambda self: pl.from_pandas(self).to_expr()
|
|
54
|
-
pd.Series.to_expr = lambda self: self.to_frame().reset_index().to_expr()
|
pyoframe-0.2.1.dist-info/RECORD
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
pyoframe/__init__.py,sha256=YswFUwm6GX98dXeT99hqxWqYWLELS71JZf1OpT1kvCg,619
|
|
2
|
-
pyoframe/_arithmetic.py,sha256=agJm2Sl4EjEG7q4n2YHka4mGfCQI3LjOXLaW6oCfGiQ,17222
|
|
3
|
-
pyoframe/_version.py,sha256=vYqoJTG51NOUmYyL0xt8asRK8vUT4lGAdal_EZ59mvw,704
|
|
4
|
-
pyoframe/constants.py,sha256=WBCmhunavNVwJcmg9ojnA6TVJCLSrgWVE4YKZnhZNz4,4192
|
|
5
|
-
pyoframe/core.py,sha256=fjCu4eY7QJSFvVfCNtMq-o_spoo76FWO4AviCssHGoo,66925
|
|
6
|
-
pyoframe/model.py,sha256=a7pEwagVxHC1ZUMr8ifO4n0ca5Ways3wip-Wps0rlcg,14257
|
|
7
|
-
pyoframe/model_element.py,sha256=YmAdx4yM5irGTiZ5uQmDa-u05QdFKngIFy8qNnogvzo,5911
|
|
8
|
-
pyoframe/monkey_patch.py,sha256=9IfS14G6IPabmM9z80jzi_D4Rq0Mdx5aUCA39Yi2tgE,2044
|
|
9
|
-
pyoframe/objective.py,sha256=PBWxj30QkFlsvY6ijZ6KjyKdrJARD4to0ieF6GUqaQU,3238
|
|
10
|
-
pyoframe/util.py,sha256=dHIwAyyD9wn36yM8IOlrboTGUGA7STq3IBTxfYSOPjU,13480
|
|
11
|
-
pyoframe-0.2.1.dist-info/licenses/LICENSE,sha256=u_Spw4ynlwTMRZeCX-uacv_hBU547pBygiA6d2ONNV4,1074
|
|
12
|
-
pyoframe-0.2.1.dist-info/METADATA,sha256=_rmMdRjfEkv-1xn9UkjVubVxNUqSKgluVmXz7nSaRnA,3607
|
|
13
|
-
pyoframe-0.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
14
|
-
pyoframe-0.2.1.dist-info/top_level.txt,sha256=10z3OOJSVLriQ0IrFLMH8CH9zByugPWolqhlHlkNjV4,9
|
|
15
|
-
pyoframe-0.2.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|