pyoframe 1.0.1__py3-none-any.whl → 1.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyoframe/__init__.py +2 -0
- pyoframe/_arithmetic.py +6 -5
- pyoframe/_constants.py +9 -10
- pyoframe/_core.py +104 -57
- pyoframe/_model.py +180 -18
- pyoframe/_model_element.py +2 -0
- pyoframe/_monkey_patch.py +5 -49
- pyoframe/_objective.py +2 -2
- pyoframe/_param.py +99 -0
- pyoframe/_utils.py +2 -2
- pyoframe/_version.py +2 -2
- {pyoframe-1.0.1.dist-info → pyoframe-1.2.0.dist-info}/METADATA +5 -5
- pyoframe-1.2.0.dist-info/RECORD +16 -0
- {pyoframe-1.0.1.dist-info → pyoframe-1.2.0.dist-info}/WHEEL +1 -1
- pyoframe-1.0.1.dist-info/RECORD +0 -15
- {pyoframe-1.0.1.dist-info → pyoframe-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {pyoframe-1.0.1.dist-info → pyoframe-1.2.0.dist-info}/top_level.txt +0 -0
pyoframe/__init__.py
CHANGED
|
@@ -11,6 +11,7 @@ from pyoframe._core import Constraint, Expression, Set, Variable, sum, sum_by
|
|
|
11
11
|
from pyoframe._model import Model
|
|
12
12
|
from pyoframe._monkey_patch import patch_dataframe_libraries
|
|
13
13
|
from pyoframe._objective import Objective
|
|
14
|
+
from pyoframe._param import Param
|
|
14
15
|
|
|
15
16
|
try:
|
|
16
17
|
from pyoframe._version import __version__, __version_tuple__ # noqa: F401
|
|
@@ -25,6 +26,7 @@ __all__ = [
|
|
|
25
26
|
"Expression",
|
|
26
27
|
"Constraint",
|
|
27
28
|
"Objective",
|
|
29
|
+
"Param",
|
|
28
30
|
"Set",
|
|
29
31
|
"Config",
|
|
30
32
|
"sum",
|
pyoframe/_arithmetic.py
CHANGED
|
@@ -42,7 +42,7 @@ def multiply(self: Expression, other: Expression) -> Expression:
|
|
|
42
42
|
>>> m.x3 = pf.Variable()
|
|
43
43
|
>>> result = 5 * m.x1 * m.x2
|
|
44
44
|
>>> result
|
|
45
|
-
<Expression terms=1
|
|
45
|
+
<Expression (quadratic) terms=1>
|
|
46
46
|
5 x2 * x1
|
|
47
47
|
>>> result * m.x3
|
|
48
48
|
Traceback (most recent call last):
|
|
@@ -112,7 +112,7 @@ def _quadratic_multiplication(self: Expression, other: Expression) -> Expression
|
|
|
112
112
|
>>> expr1 = df * m.x1
|
|
113
113
|
>>> expr2 = df * m.x2 * 2 + 4
|
|
114
114
|
>>> expr1 * expr2
|
|
115
|
-
<Expression height=3 terms=6
|
|
115
|
+
<Expression (quadratic) height=3 terms=6>
|
|
116
116
|
┌─────┬───────────────────┐
|
|
117
117
|
│ dim ┆ expression │
|
|
118
118
|
│ (3) ┆ │
|
|
@@ -122,7 +122,7 @@ def _quadratic_multiplication(self: Expression, other: Expression) -> Expression
|
|
|
122
122
|
│ 3 ┆ 12 x1 +18 x2 * x1 │
|
|
123
123
|
└─────┴───────────────────┘
|
|
124
124
|
>>> (expr1 * expr2) - df * m.x1 * df * m.x2 * 2
|
|
125
|
-
<Expression height=3 terms=3
|
|
125
|
+
<Expression (linear) height=3 terms=3>
|
|
126
126
|
┌─────┬────────────┐
|
|
127
127
|
│ dim ┆ expression │
|
|
128
128
|
│ (3) ┆ │
|
|
@@ -431,14 +431,15 @@ def _broadcast(
|
|
|
431
431
|
maintain_order="left" if Config.maintain_order else None,
|
|
432
432
|
)
|
|
433
433
|
if result.get_column(missing_dims[0]).null_count() > 0:
|
|
434
|
-
|
|
434
|
+
self_labels = self.data.select(self._dimensions_unsafe).unique(
|
|
435
435
|
maintain_order=Config.maintain_order
|
|
436
436
|
)
|
|
437
437
|
_raise_extras_error(
|
|
438
438
|
self,
|
|
439
439
|
target,
|
|
440
|
-
|
|
440
|
+
self_labels.join(target.data, how="anti", on=common_dims),
|
|
441
441
|
swapped,
|
|
442
|
+
extras_on_right=False,
|
|
442
443
|
)
|
|
443
444
|
res = self._new(result, self.name)
|
|
444
445
|
res._copy_flags(self)
|
pyoframe/_constants.py
CHANGED
|
@@ -141,19 +141,18 @@ class _Config:
|
|
|
141
141
|
Only consider enabling after you have thoroughly tested your code.
|
|
142
142
|
|
|
143
143
|
Examples:
|
|
144
|
-
>>>
|
|
145
|
-
>>> population = pl.DataFrame(
|
|
144
|
+
>>> population = pf.Param(
|
|
146
145
|
... {
|
|
147
146
|
... "city": ["Toronto", "Vancouver", "Montreal"],
|
|
148
147
|
... "pop": [2_731_571, 631_486, 1_704_694],
|
|
149
148
|
... }
|
|
150
|
-
... )
|
|
151
|
-
>>> population_influx =
|
|
149
|
+
... )
|
|
150
|
+
>>> population_influx = pf.Param(
|
|
152
151
|
... {
|
|
153
152
|
... "city": ["Toronto", "Vancouver", "Montreal"],
|
|
154
153
|
... "influx": [100_000, 50_000, None],
|
|
155
154
|
... }
|
|
156
|
-
... )
|
|
155
|
+
... )
|
|
157
156
|
|
|
158
157
|
Normally, an error warns users that the two expressions have conflicting labels:
|
|
159
158
|
>>> population + population_influx
|
|
@@ -174,7 +173,7 @@ class _Config:
|
|
|
174
173
|
But if `Config.disable_extras_checks = True`, the error is suppressed and the sum is considered to be `population.keep_extras() + population_influx.keep_extras()`:
|
|
175
174
|
>>> pf.Config.disable_extras_checks = True
|
|
176
175
|
>>> population + population_influx
|
|
177
|
-
<Expression height=3 terms=3
|
|
176
|
+
<Expression (parameter) height=3 terms=3>
|
|
178
177
|
┌───────────┬────────────┐
|
|
179
178
|
│ city ┆ expression │
|
|
180
179
|
│ (3) ┆ │
|
|
@@ -228,11 +227,11 @@ class _Config:
|
|
|
228
227
|
>>> m.X = pf.Variable()
|
|
229
228
|
>>> expr = 100.752038759 * m.X
|
|
230
229
|
>>> expr
|
|
231
|
-
<Expression terms=1
|
|
230
|
+
<Expression (linear) terms=1>
|
|
232
231
|
100.752 X
|
|
233
232
|
>>> pf.Config.float_to_str_precision = None
|
|
234
233
|
>>> expr
|
|
235
|
-
<Expression terms=1
|
|
234
|
+
<Expression (linear) terms=1>
|
|
236
235
|
100.752038759 X
|
|
237
236
|
"""
|
|
238
237
|
return self._settings.float_to_str_precision
|
|
@@ -280,7 +279,7 @@ class _Config:
|
|
|
280
279
|
>>> m = pf.Model()
|
|
281
280
|
>>> m.X = pf.Variable(pf.Set(x=range(100)), pf.Set(y=range(100)))
|
|
282
281
|
>>> m.X.sum("y")
|
|
283
|
-
<Expression height=100 terms=10000
|
|
282
|
+
<Expression (linear) height=100 terms=10000>
|
|
284
283
|
┌───────┬───────────────────────────────┐
|
|
285
284
|
│ x ┆ expression │
|
|
286
285
|
│ (100) ┆ │
|
|
@@ -298,7 +297,7 @@ class _Config:
|
|
|
298
297
|
│ 99 ┆ X[99,0] + X[99,1] + X[99,2] … │
|
|
299
298
|
└───────┴───────────────────────────────┘
|
|
300
299
|
>>> m.X.sum()
|
|
301
|
-
<Expression terms=10000
|
|
300
|
+
<Expression (linear) terms=10000>
|
|
302
301
|
X[0,0] + X[0,1] + X[0,2] …
|
|
303
302
|
"""
|
|
304
303
|
return self._settings.print_max_terms
|
pyoframe/_core.py
CHANGED
|
@@ -79,6 +79,8 @@ class BaseOperableBlock(BaseBlock):
|
|
|
79
79
|
See Also:
|
|
80
80
|
[`drop_extras`][pyoframe.Expression.drop_extras].
|
|
81
81
|
"""
|
|
82
|
+
if self._extras_strategy == ExtrasStrategy.KEEP:
|
|
83
|
+
return self
|
|
82
84
|
new = self._new(self.data, name=f"{self.name}.keep_extras()")
|
|
83
85
|
new._copy_flags(self)
|
|
84
86
|
new._extras_strategy = ExtrasStrategy.KEEP
|
|
@@ -92,6 +94,8 @@ class BaseOperableBlock(BaseBlock):
|
|
|
92
94
|
See Also:
|
|
93
95
|
[`keep_extras`][pyoframe.Expression.keep_extras].
|
|
94
96
|
"""
|
|
97
|
+
if self._extras_strategy == ExtrasStrategy.DROP:
|
|
98
|
+
return self
|
|
95
99
|
new = self._new(self.data, name=f"{self.name}.drop_extras()")
|
|
96
100
|
new._copy_flags(self)
|
|
97
101
|
new._extras_strategy = ExtrasStrategy.DROP
|
|
@@ -123,6 +127,8 @@ class BaseOperableBlock(BaseBlock):
|
|
|
123
127
|
See Also:
|
|
124
128
|
[`keep_extras`][pyoframe.Expression.keep_extras] and [`drop_extras`][pyoframe.Expression.drop_extras].
|
|
125
129
|
"""
|
|
130
|
+
if self._extras_strategy == ExtrasStrategy.UNSET:
|
|
131
|
+
return self
|
|
126
132
|
new = self._new(self.data, name=f"{self.name}.raise_extras()")
|
|
127
133
|
new._copy_flags(self)
|
|
128
134
|
new._extras_strategy = ExtrasStrategy.UNSET
|
|
@@ -169,7 +175,7 @@ class BaseOperableBlock(BaseBlock):
|
|
|
169
175
|
└───────┴─────────┴──────────────────┘
|
|
170
176
|
|
|
171
177
|
>>> m.v.rename({"city": "location"})
|
|
172
|
-
<Expression height=12 terms=12
|
|
178
|
+
<Expression (linear) height=12 terms=12>
|
|
173
179
|
┌───────┬──────────┬──────────────────┐
|
|
174
180
|
│ hour ┆ location ┆ expression │
|
|
175
181
|
│ (4) ┆ (3) ┆ │
|
|
@@ -228,7 +234,7 @@ class BaseOperableBlock(BaseBlock):
|
|
|
228
234
|
... ]
|
|
229
235
|
... )
|
|
230
236
|
>>> m.v.pick(hour="06:00")
|
|
231
|
-
<Expression height=3 terms=3
|
|
237
|
+
<Expression (linear) height=3 terms=3>
|
|
232
238
|
┌─────────┬──────────────────┐
|
|
233
239
|
│ city ┆ expression │
|
|
234
240
|
│ (3) ┆ │
|
|
@@ -238,7 +244,7 @@ class BaseOperableBlock(BaseBlock):
|
|
|
238
244
|
│ Paris ┆ v[06:00,Paris] │
|
|
239
245
|
└─────────┴──────────────────┘
|
|
240
246
|
>>> m.v.pick(hour="06:00", city="Toronto")
|
|
241
|
-
<Expression terms=1
|
|
247
|
+
<Expression (linear) terms=1>
|
|
242
248
|
v[06:00,Toronto]
|
|
243
249
|
|
|
244
250
|
See Also:
|
|
@@ -290,7 +296,7 @@ class BaseOperableBlock(BaseBlock):
|
|
|
290
296
|
>>> m = pf.Model()
|
|
291
297
|
>>> m.v = pf.Variable()
|
|
292
298
|
>>> m.v**2
|
|
293
|
-
<Expression terms=1
|
|
299
|
+
<Expression (quadratic) terms=1>
|
|
294
300
|
v * v
|
|
295
301
|
>>> m.v**3
|
|
296
302
|
Traceback (most recent call last):
|
|
@@ -320,7 +326,7 @@ class BaseOperableBlock(BaseBlock):
|
|
|
320
326
|
>>> df = pl.DataFrame({"dim1": [1, 2, 3], "value": [1, 2, 3]})
|
|
321
327
|
>>> m.v = pf.Variable(df["dim1"])
|
|
322
328
|
>>> m.v - df
|
|
323
|
-
<Expression height=3 terms=6
|
|
329
|
+
<Expression (linear) height=3 terms=6>
|
|
324
330
|
┌──────┬────────────┐
|
|
325
331
|
│ dim1 ┆ expression │
|
|
326
332
|
│ (3) ┆ │
|
|
@@ -331,7 +337,7 @@ class BaseOperableBlock(BaseBlock):
|
|
|
331
337
|
└──────┴────────────┘
|
|
332
338
|
"""
|
|
333
339
|
if not isinstance(other, (int, float)):
|
|
334
|
-
other = other.to_expr()
|
|
340
|
+
other = other.to_expr() # TODO don't rely on monkey patch
|
|
335
341
|
return self.to_expr() + (-other)
|
|
336
342
|
|
|
337
343
|
def __rmul__(self, other):
|
|
@@ -348,7 +354,7 @@ class BaseOperableBlock(BaseBlock):
|
|
|
348
354
|
>>> m = pf.Model()
|
|
349
355
|
>>> m.v = Variable({"dim1": [1, 2, 3]})
|
|
350
356
|
>>> m.v / 2
|
|
351
|
-
<Expression height=3 terms=3
|
|
357
|
+
<Expression (linear) height=3 terms=3>
|
|
352
358
|
┌──────┬────────────┐
|
|
353
359
|
│ dim1 ┆ expression │
|
|
354
360
|
│ (3) ┆ │
|
|
@@ -360,6 +366,13 @@ class BaseOperableBlock(BaseBlock):
|
|
|
360
366
|
"""
|
|
361
367
|
return self.to_expr() * (1 / other)
|
|
362
368
|
|
|
369
|
+
def __rtruediv__(self, other):
|
|
370
|
+
# This just improves error messages when trying to divide by a Set or Variable.
|
|
371
|
+
# When dividing by an Expression, see the Expression.__rtruediv__ method.
|
|
372
|
+
raise PyoframeError(
|
|
373
|
+
f"Cannot divide by '{self.name}' because it is not a number or parameter."
|
|
374
|
+
)
|
|
375
|
+
|
|
363
376
|
def __rsub__(self, other):
|
|
364
377
|
"""Supports right subtraction.
|
|
365
378
|
|
|
@@ -367,7 +380,7 @@ class BaseOperableBlock(BaseBlock):
|
|
|
367
380
|
>>> m = pf.Model()
|
|
368
381
|
>>> m.v = Variable({"dim1": [1, 2, 3]})
|
|
369
382
|
>>> 1 - m.v
|
|
370
|
-
<Expression height=3 terms=6
|
|
383
|
+
<Expression (linear) height=3 terms=6>
|
|
371
384
|
┌──────┬────────────┐
|
|
372
385
|
│ dim1 ┆ expression │
|
|
373
386
|
│ (3) ┆ │
|
|
@@ -379,6 +392,20 @@ class BaseOperableBlock(BaseBlock):
|
|
|
379
392
|
"""
|
|
380
393
|
return other + (-self.to_expr())
|
|
381
394
|
|
|
395
|
+
def __or__(self, other: Operable) -> Expression:
|
|
396
|
+
if isinstance(other, (int, float)):
|
|
397
|
+
raise PyoframeError(
|
|
398
|
+
"Cannot use '|' operator with scalars. Did you mean to use '+' instead?"
|
|
399
|
+
)
|
|
400
|
+
return self.to_expr().keep_extras() + other.to_expr().keep_extras() # type: ignore
|
|
401
|
+
|
|
402
|
+
def __ror__(self, other: Operable) -> Expression:
|
|
403
|
+
if isinstance(other, (int, float)):
|
|
404
|
+
raise PyoframeError(
|
|
405
|
+
"Cannot use '|' operator with scalars. Did you mean to use '+' instead?"
|
|
406
|
+
)
|
|
407
|
+
return self.to_expr().keep_extras() + other.to_expr().keep_extras() # type: ignore
|
|
408
|
+
|
|
382
409
|
def __le__(self, other):
|
|
383
410
|
return Constraint(self - other, ConstraintSense.LE)
|
|
384
411
|
|
|
@@ -571,7 +598,7 @@ class Set(BaseOperableBlock):
|
|
|
571
598
|
def __repr__(self):
|
|
572
599
|
header = get_obj_repr(
|
|
573
600
|
self,
|
|
574
|
-
"unnamed" if self.name == "unnamed_set" else self.name,
|
|
601
|
+
"'unnamed'" if self.name == "unnamed_set" else f"'{self.name}'",
|
|
575
602
|
height=self.data.height,
|
|
576
603
|
)
|
|
577
604
|
data = self._add_shape_to_columns(self.data)
|
|
@@ -645,7 +672,7 @@ class Expression(BaseOperableBlock):
|
|
|
645
672
|
>>> m.Size = pf.Variable(df.index)
|
|
646
673
|
>>> expr = df["cost"] * m.Time + df["cost"] * m.Size
|
|
647
674
|
>>> expr
|
|
648
|
-
<Expression height=5 terms=10
|
|
675
|
+
<Expression (linear) height=5 terms=10>
|
|
649
676
|
┌──────┬──────┬──────────────────────────────┐
|
|
650
677
|
│ item ┆ time ┆ expression │
|
|
651
678
|
│ (2) ┆ (3) ┆ │
|
|
@@ -691,7 +718,7 @@ class Expression(BaseOperableBlock):
|
|
|
691
718
|
|
|
692
719
|
Examples:
|
|
693
720
|
>>> pf.Expression.constant(5)
|
|
694
|
-
<Expression terms=1
|
|
721
|
+
<Expression (parameter) terms=1>
|
|
695
722
|
5
|
|
696
723
|
"""
|
|
697
724
|
return cls(
|
|
@@ -712,7 +739,7 @@ class Expression(BaseOperableBlock):
|
|
|
712
739
|
If no dimensions are specified, the sum is taken over all of the expression's dimensions.
|
|
713
740
|
|
|
714
741
|
Examples:
|
|
715
|
-
>>> expr =
|
|
742
|
+
>>> expr = pf.Param(
|
|
716
743
|
... {
|
|
717
744
|
... "time": ["mon", "tue", "wed", "mon", "tue"],
|
|
718
745
|
... "place": [
|
|
@@ -724,9 +751,9 @@ class Expression(BaseOperableBlock):
|
|
|
724
751
|
... ],
|
|
725
752
|
... "tiktok_posts": [1e6, 3e6, 2e6, 1e6, 2e6],
|
|
726
753
|
... }
|
|
727
|
-
... )
|
|
754
|
+
... )
|
|
728
755
|
>>> expr
|
|
729
|
-
<Expression height=5 terms=5
|
|
756
|
+
<Expression (parameter) height=5 terms=5>
|
|
730
757
|
┌──────┬───────────┬────────────┐
|
|
731
758
|
│ time ┆ place ┆ expression │
|
|
732
759
|
│ (3) ┆ (2) ┆ │
|
|
@@ -738,7 +765,7 @@ class Expression(BaseOperableBlock):
|
|
|
738
765
|
│ tue ┆ Vancouver ┆ 2000000 │
|
|
739
766
|
└──────┴───────────┴────────────┘
|
|
740
767
|
>>> expr.sum("time")
|
|
741
|
-
<Expression height=2 terms=2
|
|
768
|
+
<Expression (parameter) height=2 terms=2>
|
|
742
769
|
┌───────────┬────────────┐
|
|
743
770
|
│ place ┆ expression │
|
|
744
771
|
│ (2) ┆ │
|
|
@@ -747,7 +774,7 @@ class Expression(BaseOperableBlock):
|
|
|
747
774
|
│ Vancouver ┆ 3000000 │
|
|
748
775
|
└───────────┴────────────┘
|
|
749
776
|
>>> expr.sum()
|
|
750
|
-
<Expression terms=1
|
|
777
|
+
<Expression (parameter) terms=1>
|
|
751
778
|
9000000
|
|
752
779
|
|
|
753
780
|
If the given dimensions don't exist, an error will be raised:
|
|
@@ -783,7 +810,7 @@ class Expression(BaseOperableBlock):
|
|
|
783
810
|
"""Like [`Expression.sum`][pyoframe.Expression.sum], but the sum is taken over all dimensions *except* those specified in `by` (just like a `group_by().sum()` operation).
|
|
784
811
|
|
|
785
812
|
Examples:
|
|
786
|
-
>>> expr =
|
|
813
|
+
>>> expr = pf.Param(
|
|
787
814
|
... {
|
|
788
815
|
... "time": ["mon", "tue", "wed", "mon", "tue"],
|
|
789
816
|
... "place": [
|
|
@@ -795,9 +822,9 @@ class Expression(BaseOperableBlock):
|
|
|
795
822
|
... ],
|
|
796
823
|
... "tiktok_posts": [1e6, 3e6, 2e6, 1e6, 2e6],
|
|
797
824
|
... }
|
|
798
|
-
... )
|
|
825
|
+
... )
|
|
799
826
|
>>> expr
|
|
800
|
-
<Expression height=5 terms=5
|
|
827
|
+
<Expression (parameter) height=5 terms=5>
|
|
801
828
|
┌──────┬───────────┬────────────┐
|
|
802
829
|
│ time ┆ place ┆ expression │
|
|
803
830
|
│ (3) ┆ (2) ┆ │
|
|
@@ -810,7 +837,7 @@ class Expression(BaseOperableBlock):
|
|
|
810
837
|
└──────┴───────────┴────────────┘
|
|
811
838
|
|
|
812
839
|
>>> expr.sum_by("place")
|
|
813
|
-
<Expression height=2 terms=2
|
|
840
|
+
<Expression (parameter) height=2 terms=2>
|
|
814
841
|
┌───────────┬────────────┐
|
|
815
842
|
│ place ┆ expression │
|
|
816
843
|
│ (2) ┆ │
|
|
@@ -873,13 +900,13 @@ class Expression(BaseOperableBlock):
|
|
|
873
900
|
|
|
874
901
|
Examples:
|
|
875
902
|
>>> import polars as pl
|
|
876
|
-
>>> pop_data =
|
|
903
|
+
>>> pop_data = pf.Param(
|
|
877
904
|
... {
|
|
878
905
|
... "city": ["Toronto", "Vancouver", "Boston"],
|
|
879
906
|
... "year": [2024, 2024, 2024],
|
|
880
907
|
... "population": [10, 2, 8],
|
|
881
908
|
... }
|
|
882
|
-
... )
|
|
909
|
+
... )
|
|
883
910
|
>>> cities_and_countries = pl.DataFrame(
|
|
884
911
|
... {
|
|
885
912
|
... "city": ["Toronto", "Vancouver", "Boston"],
|
|
@@ -887,7 +914,7 @@ class Expression(BaseOperableBlock):
|
|
|
887
914
|
... }
|
|
888
915
|
... )
|
|
889
916
|
>>> pop_data.map(cities_and_countries)
|
|
890
|
-
<Expression height=2 terms=2
|
|
917
|
+
<Expression (parameter) height=2 terms=2>
|
|
891
918
|
┌──────┬─────────┬────────────┐
|
|
892
919
|
│ year ┆ country ┆ expression │
|
|
893
920
|
│ (1) ┆ (2) ┆ │
|
|
@@ -897,7 +924,7 @@ class Expression(BaseOperableBlock):
|
|
|
897
924
|
└──────┴─────────┴────────────┘
|
|
898
925
|
|
|
899
926
|
>>> pop_data.map(cities_and_countries, drop_shared_dims=False)
|
|
900
|
-
<Expression height=3 terms=3
|
|
927
|
+
<Expression (parameter) height=3 terms=3>
|
|
901
928
|
┌───────────┬──────┬─────────┬────────────┐
|
|
902
929
|
│ city ┆ year ┆ country ┆ expression │
|
|
903
930
|
│ (3) ┆ (1) ┆ (2) ┆ │
|
|
@@ -967,7 +994,7 @@ class Expression(BaseOperableBlock):
|
|
|
967
994
|
>>> m = pf.Model()
|
|
968
995
|
>>> m.quantity = pf.Variable(cost[["item", "time"]])
|
|
969
996
|
>>> (m.quantity * cost).rolling_sum(over="time", window_size=2)
|
|
970
|
-
<Expression height=5 terms=8
|
|
997
|
+
<Expression (linear) height=5 terms=8>
|
|
971
998
|
┌──────┬──────┬──────────────────────────────────┐
|
|
972
999
|
│ item ┆ time ┆ expression │
|
|
973
1000
|
│ (2) ┆ (3) ┆ │
|
|
@@ -1003,11 +1030,8 @@ class Expression(BaseOperableBlock):
|
|
|
1003
1030
|
"""Filters this expression to only include the dimensions within the provided set.
|
|
1004
1031
|
|
|
1005
1032
|
Examples:
|
|
1006
|
-
>>>
|
|
1007
|
-
>>>
|
|
1008
|
-
... {"dim1": [1, 2, 3], "value": [1, 2, 3]}
|
|
1009
|
-
... ).to_expr()
|
|
1010
|
-
>>> filter_expr = pd.DataFrame({"dim1": [1, 3], "value": [5, 6]}).to_expr()
|
|
1033
|
+
>>> general_expr = pf.Param({"dim1": [1, 2, 3], "value": [1, 2, 3]})
|
|
1034
|
+
>>> filter_expr = pf.Param({"dim1": [1, 3], "value": [5, 6]})
|
|
1011
1035
|
>>> general_expr.within(filter_expr).data
|
|
1012
1036
|
shape: (2, 3)
|
|
1013
1037
|
┌──────┬─────────┬───────────────┐
|
|
@@ -1068,11 +1092,10 @@ class Expression(BaseOperableBlock):
|
|
|
1068
1092
|
If `False`, returns the degree as an integer (0, 1, or 2).
|
|
1069
1093
|
|
|
1070
1094
|
Examples:
|
|
1071
|
-
>>> import pandas as pd
|
|
1072
1095
|
>>> m = pf.Model()
|
|
1073
1096
|
>>> m.v1 = pf.Variable()
|
|
1074
1097
|
>>> m.v2 = pf.Variable()
|
|
1075
|
-
>>> expr =
|
|
1098
|
+
>>> expr = pf.Param({"dim1": [1, 2, 3], "value": [1, 2, 3]})
|
|
1076
1099
|
>>> expr.degree()
|
|
1077
1100
|
0
|
|
1078
1101
|
>>> expr *= m.v1
|
|
@@ -1090,18 +1113,17 @@ class Expression(BaseOperableBlock):
|
|
|
1090
1113
|
elif (self.data.get_column(VAR_KEY) != CONST_TERM).any():
|
|
1091
1114
|
return "linear" if return_str else 1
|
|
1092
1115
|
else:
|
|
1093
|
-
return "
|
|
1116
|
+
return "parameter" if return_str else 0
|
|
1094
1117
|
|
|
1095
1118
|
def __add__(self, other):
|
|
1096
1119
|
"""Adds another expression or a constant to this expression.
|
|
1097
1120
|
|
|
1098
1121
|
Examples:
|
|
1099
|
-
>>> import pandas as pd
|
|
1100
1122
|
>>> m = pf.Model()
|
|
1101
|
-
>>> add =
|
|
1123
|
+
>>> add = pf.Param({"dim1": [1, 2, 3], "add": [10, 20, 30]})
|
|
1102
1124
|
>>> m.v = Variable(add)
|
|
1103
1125
|
>>> m.v + add
|
|
1104
|
-
<Expression height=3 terms=6
|
|
1126
|
+
<Expression (linear) height=3 terms=6>
|
|
1105
1127
|
┌──────┬────────────┐
|
|
1106
1128
|
│ dim1 ┆ expression │
|
|
1107
1129
|
│ (3) ┆ │
|
|
@@ -1112,7 +1134,7 @@ class Expression(BaseOperableBlock):
|
|
|
1112
1134
|
└──────┴────────────┘
|
|
1113
1135
|
|
|
1114
1136
|
>>> m.v + add + 2
|
|
1115
|
-
<Expression height=3 terms=6
|
|
1137
|
+
<Expression (linear) height=3 terms=6>
|
|
1116
1138
|
┌──────┬────────────┐
|
|
1117
1139
|
│ dim1 ┆ expression │
|
|
1118
1140
|
│ (3) ┆ │
|
|
@@ -1138,12 +1160,12 @@ class Expression(BaseOperableBlock):
|
|
|
1138
1160
|
https://bravos-power.github.io/pyoframe/latest/learn/concepts/addition
|
|
1139
1161
|
>>> m.v2 = Variable()
|
|
1140
1162
|
>>> 5 + 2 * m.v2
|
|
1141
|
-
<Expression terms=2
|
|
1163
|
+
<Expression (linear) terms=2>
|
|
1142
1164
|
2 v2 +5
|
|
1143
1165
|
"""
|
|
1144
1166
|
if isinstance(other, (int, float)):
|
|
1145
1167
|
return self._add_const(other)
|
|
1146
|
-
other = other.to_expr()
|
|
1168
|
+
other = other.to_expr() # TODO don't rely on monkey patch
|
|
1147
1169
|
self._learn_from_other(other)
|
|
1148
1170
|
return add(self, other)
|
|
1149
1171
|
|
|
@@ -1156,10 +1178,25 @@ class Expression(BaseOperableBlock):
|
|
|
1156
1178
|
name=f"({other} * {self.name})",
|
|
1157
1179
|
)
|
|
1158
1180
|
|
|
1159
|
-
other = other.to_expr()
|
|
1181
|
+
other: Expression = other.to_expr() # TODO don't rely on monkey patch
|
|
1160
1182
|
self._learn_from_other(other)
|
|
1161
1183
|
return multiply(self, other)
|
|
1162
1184
|
|
|
1185
|
+
def __rtruediv__(self, other):
|
|
1186
|
+
"""Support dividing by an expression when that expression is a constant."""
|
|
1187
|
+
assert isinstance(other, (int, float)), (
|
|
1188
|
+
f"Expected a number not a {type(other)} when dividing by an expression."
|
|
1189
|
+
)
|
|
1190
|
+
if self.degree() != 0:
|
|
1191
|
+
raise PyoframeError(
|
|
1192
|
+
f"Cannot divide by '{self.name}' because denominators cannot contain variables."
|
|
1193
|
+
)
|
|
1194
|
+
|
|
1195
|
+
return self._new(
|
|
1196
|
+
self.data.with_columns((pl.lit(other) / pl.col(COEF_KEY)).alias(COEF_KEY)),
|
|
1197
|
+
name=f"({other} / {self.name})",
|
|
1198
|
+
)
|
|
1199
|
+
|
|
1163
1200
|
def to_expr(self) -> Expression:
|
|
1164
1201
|
"""Returns the expression itself."""
|
|
1165
1202
|
return self
|
|
@@ -1181,13 +1218,13 @@ class Expression(BaseOperableBlock):
|
|
|
1181
1218
|
>>> m.x1 = Variable()
|
|
1182
1219
|
>>> m.x2 = Variable()
|
|
1183
1220
|
>>> m.x1 + 5
|
|
1184
|
-
<Expression terms=2
|
|
1221
|
+
<Expression (linear) terms=2>
|
|
1185
1222
|
x1 +5
|
|
1186
1223
|
>>> m.x1**2 + 5
|
|
1187
|
-
<Expression terms=2
|
|
1224
|
+
<Expression (quadratic) terms=2>
|
|
1188
1225
|
x1 * x1 +5
|
|
1189
1226
|
>>> m.x1**2 + m.x2 + 5
|
|
1190
|
-
<Expression terms=3
|
|
1227
|
+
<Expression (quadratic) terms=3>
|
|
1191
1228
|
x1 * x1 + x2 +5
|
|
1192
1229
|
|
|
1193
1230
|
It also works with dimensions
|
|
@@ -1195,7 +1232,7 @@ class Expression(BaseOperableBlock):
|
|
|
1195
1232
|
>>> m = pf.Model()
|
|
1196
1233
|
>>> m.v = Variable({"dim1": [1, 2, 3]})
|
|
1197
1234
|
>>> m.v * m.v + 5
|
|
1198
|
-
<Expression height=3 terms=6
|
|
1235
|
+
<Expression (quadratic) height=3 terms=6>
|
|
1199
1236
|
┌──────┬─────────────────┐
|
|
1200
1237
|
│ dim1 ┆ expression │
|
|
1201
1238
|
│ (3) ┆ │
|
|
@@ -1558,9 +1595,9 @@ class Expression(BaseOperableBlock):
|
|
|
1558
1595
|
"""Returns a string representation of the expression's header."""
|
|
1559
1596
|
return get_obj_repr(
|
|
1560
1597
|
self,
|
|
1598
|
+
f"({self.degree(return_str=True)})",
|
|
1561
1599
|
height=len(self) if self.dimensions else None,
|
|
1562
1600
|
terms=self.terms,
|
|
1563
|
-
type=self.degree(return_str=True),
|
|
1564
1601
|
)
|
|
1565
1602
|
|
|
1566
1603
|
def __repr__(self) -> str:
|
|
@@ -1581,7 +1618,7 @@ class Expression(BaseOperableBlock):
|
|
|
1581
1618
|
>>> m.v = pf.Variable({"t": [1, 2]})
|
|
1582
1619
|
>>> coef = pl.DataFrame({"t": [1, 2], "coef": [0, 1]})
|
|
1583
1620
|
>>> coef * (m.v + 4)
|
|
1584
|
-
<Expression height=2 terms=3
|
|
1621
|
+
<Expression (linear) height=2 terms=3>
|
|
1585
1622
|
┌─────┬────────────┐
|
|
1586
1623
|
│ t ┆ expression │
|
|
1587
1624
|
│ (2) ┆ │
|
|
@@ -2189,10 +2226,10 @@ class Constraint(BaseBlock):
|
|
|
2189
2226
|
return (
|
|
2190
2227
|
get_obj_repr(
|
|
2191
2228
|
self,
|
|
2192
|
-
self.name,
|
|
2229
|
+
f"'{self.name}'",
|
|
2230
|
+
f"({self.lhs.degree(return_str=True)})",
|
|
2193
2231
|
height=len(self) if self.dimensions else None,
|
|
2194
2232
|
terms=len(self.lhs.data),
|
|
2195
|
-
type=self.lhs.degree(return_str=True),
|
|
2196
2233
|
)
|
|
2197
2234
|
+ "\n"
|
|
2198
2235
|
+ self.to_str()
|
|
@@ -2202,18 +2239,22 @@ class Constraint(BaseBlock):
|
|
|
2202
2239
|
class Variable(BaseOperableBlock):
|
|
2203
2240
|
"""A decision variable for an optimization model.
|
|
2204
2241
|
|
|
2242
|
+
!!! tip
|
|
2243
|
+
If `lb` or `ub` are a dimensioned object (e.g. an [Expression][pyoframe.Expression]), they will automatically be [broadcasted](../../learn/concepts/addition.md#over) to match the variable's dimensions.
|
|
2244
|
+
|
|
2205
2245
|
Parameters:
|
|
2206
2246
|
*indexing_sets:
|
|
2207
2247
|
If no indexing_sets are provided, a single variable with no dimensions is created.
|
|
2208
2248
|
Otherwise, a variable is created for each element in the Cartesian product of the indexing_sets (see Set for details on behaviour).
|
|
2209
|
-
lb:
|
|
2210
|
-
The lower bound for all variables.
|
|
2211
|
-
ub:
|
|
2212
|
-
The upper bound for all variables.
|
|
2213
2249
|
vtype:
|
|
2214
2250
|
The type of the variable. Can be either a VType enum or a string. Default is VType.CONTINUOUS.
|
|
2251
|
+
lb:
|
|
2252
|
+
The lower bound for the variables.
|
|
2253
|
+
ub:
|
|
2254
|
+
The upper bound for the variables.
|
|
2215
2255
|
equals:
|
|
2216
|
-
When specified, a variable is created and a constraint is added to make the variable equal to the provided expression.
|
|
2256
|
+
When specified, a variable is created for every label in `equals` and a constraint is added to make the variable equal to the provided expression.
|
|
2257
|
+
`indexing_sets` cannot be provided when using `equals`.
|
|
2217
2258
|
|
|
2218
2259
|
Examples:
|
|
2219
2260
|
>>> import pandas as pd
|
|
@@ -2298,7 +2339,7 @@ class Variable(BaseOperableBlock):
|
|
|
2298
2339
|
assert len(indexing_sets) == 0, (
|
|
2299
2340
|
"Cannot specify both 'equals' and 'indexing_sets'"
|
|
2300
2341
|
)
|
|
2301
|
-
equals = equals.to_expr()
|
|
2342
|
+
equals = equals.to_expr() # TODO don't rely on monkey patch
|
|
2302
2343
|
indexing_sets = (equals,)
|
|
2303
2344
|
|
|
2304
2345
|
data = Set(*indexing_sets).data if len(indexing_sets) > 0 else pl.DataFrame()
|
|
@@ -2309,10 +2350,16 @@ class Variable(BaseOperableBlock):
|
|
|
2309
2350
|
self._equals: Expression | None = equals
|
|
2310
2351
|
|
|
2311
2352
|
if lb is not None and not isinstance(lb, (float, int)):
|
|
2353
|
+
lb: Expression = lb.to_expr() # TODO don't rely on monkey patch
|
|
2354
|
+
if not self.dimensionless:
|
|
2355
|
+
lb = lb.over(*self.dimensions)
|
|
2312
2356
|
self._lb_expr, self.lb = lb, None
|
|
2313
2357
|
else:
|
|
2314
2358
|
self._lb_expr, self.lb = None, lb
|
|
2315
2359
|
if ub is not None and not isinstance(ub, (float, int)):
|
|
2360
|
+
ub = ub.to_expr() # TODO don't rely on monkey patch
|
|
2361
|
+
if not self.dimensionless:
|
|
2362
|
+
ub = ub.over(*self.dimensions) # pyright: ignore[reportOptionalIterable]
|
|
2316
2363
|
self._ub_expr, self.ub = ub, None
|
|
2317
2364
|
else:
|
|
2318
2365
|
self._ub_expr, self.ub = None, ub
|
|
@@ -2518,7 +2565,7 @@ class Variable(BaseOperableBlock):
|
|
|
2518
2565
|
result = (
|
|
2519
2566
|
get_obj_repr(
|
|
2520
2567
|
self,
|
|
2521
|
-
self.name,
|
|
2568
|
+
f"'{self.name}'",
|
|
2522
2569
|
lb=self.lb,
|
|
2523
2570
|
ub=self.ub,
|
|
2524
2571
|
height=self.data.height if self.dimensions else None,
|
|
@@ -2581,7 +2628,7 @@ class Variable(BaseOperableBlock):
|
|
|
2581
2628
|
https://bravos-power.github.io/pyoframe/latest/learn/concepts/addition
|
|
2582
2629
|
|
|
2583
2630
|
>>> (m.bat_charge + m.bat_flow).drop_extras() == m.bat_charge.next("time")
|
|
2584
|
-
<Constraint 'unnamed' height=6 terms=18
|
|
2631
|
+
<Constraint 'unnamed' (linear) height=6 terms=18>
|
|
2585
2632
|
┌───────┬─────────┬────────────────────────────────────────────────────────────────────────────────┐
|
|
2586
2633
|
│ time ┆ city ┆ constraint │
|
|
2587
2634
|
│ (3) ┆ (2) ┆ │
|
|
@@ -2603,7 +2650,7 @@ class Variable(BaseOperableBlock):
|
|
|
2603
2650
|
>>> (m.bat_charge + m.bat_flow) == m.bat_charge.next(
|
|
2604
2651
|
... "time", wrap_around=True
|
|
2605
2652
|
... )
|
|
2606
|
-
<Constraint 'unnamed' height=8 terms=24
|
|
2653
|
+
<Constraint 'unnamed' (linear) height=8 terms=24>
|
|
2607
2654
|
┌───────┬─────────┬────────────────────────────────────────────────────────────────────────────────┐
|
|
2608
2655
|
│ time ┆ city ┆ constraint │
|
|
2609
2656
|
│ (4) ┆ (2) ┆ │
|
pyoframe/_model.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import time
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import TYPE_CHECKING, Any
|
|
7
8
|
|
|
@@ -50,6 +51,10 @@ class Model:
|
|
|
50
51
|
Either "min" or "max". Indicates whether it's a minimization or maximization problem.
|
|
51
52
|
Typically, this parameter can be omitted (`None`) as it will automatically be
|
|
52
53
|
set when the objective is set using `.minimize` or `.maximize`.
|
|
54
|
+
verbose:
|
|
55
|
+
If `True`, logging messages will be printed every time a Variable or Constraint is added to the model.
|
|
56
|
+
This is useful to discover performance bottlenecks.
|
|
57
|
+
Logging can be further configured via the [`logging` module](https://docs.python.org/3/howto/logging.html) by modifying the `pyoframe` logger.
|
|
53
58
|
|
|
54
59
|
Examples:
|
|
55
60
|
>>> m = pf.Model()
|
|
@@ -87,6 +92,8 @@ class Model:
|
|
|
87
92
|
"solver_name",
|
|
88
93
|
"minimize",
|
|
89
94
|
"maximize",
|
|
95
|
+
"_logger",
|
|
96
|
+
"_last_log",
|
|
90
97
|
]
|
|
91
98
|
|
|
92
99
|
def __init__(
|
|
@@ -98,6 +105,7 @@ class Model:
|
|
|
98
105
|
solver_uses_variable_names: bool = False,
|
|
99
106
|
print_uses_variable_names: bool = True,
|
|
100
107
|
sense: ObjSense | ObjSenseValue | None = None,
|
|
108
|
+
verbose: bool = False,
|
|
101
109
|
):
|
|
102
110
|
self._poi, self.solver = Model._create_poi_model(solver, solver_env)
|
|
103
111
|
self.solver_name: str = self.solver.name
|
|
@@ -112,6 +120,17 @@ class Model:
|
|
|
112
120
|
self._attr = Container(self._set_attr, self._get_attr)
|
|
113
121
|
self._solver_uses_variable_names = solver_uses_variable_names
|
|
114
122
|
|
|
123
|
+
self._logger = None
|
|
124
|
+
self._last_log = None
|
|
125
|
+
if verbose:
|
|
126
|
+
import logging
|
|
127
|
+
|
|
128
|
+
self._logger = logging.getLogger("pyoframe")
|
|
129
|
+
self._logger.addHandler(logging.NullHandler())
|
|
130
|
+
self._logger.setLevel(logging.DEBUG)
|
|
131
|
+
|
|
132
|
+
self._last_log = time.time()
|
|
133
|
+
|
|
115
134
|
@property
|
|
116
135
|
def poi(self):
|
|
117
136
|
"""The underlying PyOptInterface model used to interact with the solver.
|
|
@@ -234,7 +253,7 @@ class Model:
|
|
|
234
253
|
from pyoptinterface import ipopt
|
|
235
254
|
except ModuleNotFoundError as e: # pragma: no cover
|
|
236
255
|
raise ModuleNotFoundError(
|
|
237
|
-
"Failed to import the Ipopt solver. Did you run `pip install pyoptinterface[
|
|
256
|
+
"Failed to import the Ipopt solver. Did you run `pip install pyoptinterface[nlp]`?"
|
|
238
257
|
) from e
|
|
239
258
|
|
|
240
259
|
try:
|
|
@@ -326,18 +345,6 @@ class Model:
|
|
|
326
345
|
Raises:
|
|
327
346
|
ValueError: If the objective has not been defined.
|
|
328
347
|
|
|
329
|
-
Examples:
|
|
330
|
-
>>> m = pf.Model()
|
|
331
|
-
>>> m.X = pf.Variable()
|
|
332
|
-
>>> m.objective
|
|
333
|
-
Traceback (most recent call last):
|
|
334
|
-
...
|
|
335
|
-
ValueError: Objective is not defined.
|
|
336
|
-
>>> m.maximize = m.X
|
|
337
|
-
>>> m.objective
|
|
338
|
-
<Objective terms=1 type=linear>
|
|
339
|
-
X
|
|
340
|
-
|
|
341
348
|
See Also:
|
|
342
349
|
[`Model.has_objective`][pyoframe.Model.has_objective]
|
|
343
350
|
"""
|
|
@@ -357,9 +364,11 @@ class Model:
|
|
|
357
364
|
value._on_add_to_model(self, "objective")
|
|
358
365
|
|
|
359
366
|
@property
|
|
360
|
-
def minimize(self) -> Objective
|
|
367
|
+
def minimize(self) -> Objective:
|
|
361
368
|
"""Sets or gets the model's objective for minimization problems."""
|
|
362
|
-
if self.
|
|
369
|
+
if self._objective is None:
|
|
370
|
+
raise ValueError("Objective is not defined.")
|
|
371
|
+
if self.sense == ObjSense.MAX:
|
|
363
372
|
raise ValueError("Can't get .minimize in a maximization problem.")
|
|
364
373
|
return self._objective
|
|
365
374
|
|
|
@@ -372,9 +381,11 @@ class Model:
|
|
|
372
381
|
self.objective = value
|
|
373
382
|
|
|
374
383
|
@property
|
|
375
|
-
def maximize(self) -> Objective
|
|
384
|
+
def maximize(self) -> Objective:
|
|
376
385
|
"""Sets or gets the model's objective for maximization problems."""
|
|
377
|
-
if self.
|
|
386
|
+
if self._objective is None:
|
|
387
|
+
raise ValueError("Objective is not defined.")
|
|
388
|
+
if self.sense == ObjSense.MIN:
|
|
378
389
|
raise ValueError("Can't get .maximize in a minimization problem.")
|
|
379
390
|
return self._objective
|
|
380
391
|
|
|
@@ -400,14 +411,37 @@ class Model:
|
|
|
400
411
|
f"Cannot create {__name} since it was already created."
|
|
401
412
|
)
|
|
402
413
|
|
|
414
|
+
log = self._logger is not None and isinstance(
|
|
415
|
+
__value, (Constraint, Variable)
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
if log:
|
|
419
|
+
start_time = time.time()
|
|
420
|
+
|
|
403
421
|
__value._on_add_to_model(self, __name)
|
|
404
422
|
|
|
423
|
+
if log:
|
|
424
|
+
solver_time = time.time() - start_time # type: ignore
|
|
425
|
+
|
|
405
426
|
if isinstance(__value, Variable):
|
|
406
427
|
self._variables.append(__value)
|
|
407
428
|
if self._var_map is not None:
|
|
408
429
|
self._var_map.add(__value)
|
|
430
|
+
if log:
|
|
431
|
+
type_name = "variable"
|
|
409
432
|
elif isinstance(__value, Constraint):
|
|
410
433
|
self._constraints.append(__value)
|
|
434
|
+
if log:
|
|
435
|
+
type_name = "constraint"
|
|
436
|
+
|
|
437
|
+
if log:
|
|
438
|
+
elapsed_time = time.time() - self._last_log # type: ignore
|
|
439
|
+
self._last_log += elapsed_time # type: ignore
|
|
440
|
+
|
|
441
|
+
self._logger.debug( # type: ignore
|
|
442
|
+
f"Added {type_name} '{__name}' to model ({elapsed_time:.1f}s elapsed, {solver_time:.1f}s for solver, n={len(__value)})" # type: ignore
|
|
443
|
+
)
|
|
444
|
+
|
|
411
445
|
return super().__setattr__(__name, __value)
|
|
412
446
|
|
|
413
447
|
# Defining a custom __getattribute__ prevents type checkers from complaining about attribute access
|
|
@@ -417,7 +451,7 @@ class Model:
|
|
|
417
451
|
def __repr__(self) -> str:
|
|
418
452
|
return get_obj_repr(
|
|
419
453
|
self,
|
|
420
|
-
self.name,
|
|
454
|
+
f"'{self.name}'" if self.name is not None else None,
|
|
421
455
|
vars=len(self.variables),
|
|
422
456
|
constrs=len(self.constraints),
|
|
423
457
|
has_objective=self.has_objective,
|
|
@@ -532,6 +566,134 @@ class Model:
|
|
|
532
566
|
"""
|
|
533
567
|
self.poi.computeIIS()
|
|
534
568
|
|
|
569
|
+
def variables_size_info(self, memory_unit: pl.SizeUnit = "b") -> pl.DataFrame:
|
|
570
|
+
"""Returns a DataFrame with information about the memory usage of each variable in the model.
|
|
571
|
+
|
|
572
|
+
!!! warning "Experimental"
|
|
573
|
+
This method is experimental and may change or be removed in future versions. We're interested in your [feedback]([feedback](https://github.com/Bravos-Power/pyoframe/issues).
|
|
574
|
+
|
|
575
|
+
Parameters:
|
|
576
|
+
memory_unit:
|
|
577
|
+
The size of the memory unit to use for the memory usage information.
|
|
578
|
+
See [`polars.DataFrame.estimated_size`](https://docs.pola.rs/api/python/stable/reference/dataframe/api/polars.DataFrame.estimated_size.html).
|
|
579
|
+
|
|
580
|
+
Examples:
|
|
581
|
+
>>> m = pf.Model()
|
|
582
|
+
>>> m.X = pf.Variable()
|
|
583
|
+
>>> m.Y = pf.Variable(pf.Set(dim_x=range(100)))
|
|
584
|
+
>>> m.variables_size_info()
|
|
585
|
+
shape: (3, 5)
|
|
586
|
+
┌───────┬───────────────┬────────────────────┬───────────────────────┬────────────────────────────┐
|
|
587
|
+
│ name ┆ num_variables ┆ num_variables_perc ┆ pyoframe_memory_usage ┆ pyoframe_memory_usage_perc │
|
|
588
|
+
│ --- ┆ --- ┆ --- ┆ --- ┆ --- │
|
|
589
|
+
│ str ┆ i64 ┆ str ┆ i64 ┆ str │
|
|
590
|
+
╞═══════╪═══════════════╪════════════════════╪═══════════════════════╪════════════════════════════╡
|
|
591
|
+
│ Y ┆ 100 ┆ 99.0% ┆ 1200 ┆ 99.7% │
|
|
592
|
+
│ X ┆ 1 ┆ 1.0% ┆ 4 ┆ 0.3% │
|
|
593
|
+
│ TOTAL ┆ 101 ┆ 100.0% ┆ 1204 ┆ 100.0% │
|
|
594
|
+
└───────┴───────────────┴────────────────────┴───────────────────────┴────────────────────────────┘
|
|
595
|
+
"""
|
|
596
|
+
data = pl.DataFrame(
|
|
597
|
+
[
|
|
598
|
+
dict(name=v.name, n=len(v), mem=v.estimated_size(memory_unit))
|
|
599
|
+
for v in self.variables
|
|
600
|
+
]
|
|
601
|
+
).sort("n", descending=True)
|
|
602
|
+
|
|
603
|
+
total = data.sum().with_columns(name=pl.lit("TOTAL"))
|
|
604
|
+
data = pl.concat([data, total])
|
|
605
|
+
|
|
606
|
+
def format(expr: pl.Expr) -> pl.Expr:
|
|
607
|
+
return (100 * expr).round(1).cast(pl.String) + pl.lit("%")
|
|
608
|
+
|
|
609
|
+
data = data.with_columns(
|
|
610
|
+
n_per=format(pl.col("n") / total["n"].item()),
|
|
611
|
+
mem_per=format(pl.col("mem") / total["mem"].item()),
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
data = data.select("name", "n", "n_per", "mem", "mem_per")
|
|
615
|
+
data = data.rename(
|
|
616
|
+
{
|
|
617
|
+
"n": "num_variables",
|
|
618
|
+
"n_per": "num_variables_perc",
|
|
619
|
+
"mem": "pyoframe_memory_usage",
|
|
620
|
+
"mem_per": "pyoframe_memory_usage_perc",
|
|
621
|
+
}
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
return pl.DataFrame(data)
|
|
625
|
+
|
|
626
|
+
def constraints_size_info(self, memory_unit: pl.SizeUnit = "b") -> pl.DataFrame:
|
|
627
|
+
"""Returns a DataFrame with information about the memory usage of each constraint in the model.
|
|
628
|
+
|
|
629
|
+
!!! warning "Experimental"
|
|
630
|
+
This method is experimental and may change or be removed in future versions. We're interested in your [feedback](https://github.com/Bravos-Power/pyoframe/issues).
|
|
631
|
+
|
|
632
|
+
Parameters:
|
|
633
|
+
memory_unit:
|
|
634
|
+
The size of the memory unit to use for the memory usage information.
|
|
635
|
+
See [`polars.DataFrame.estimated_size`](https://docs.pola.rs/api/python/stable/reference/dataframe/api/polars.DataFrame.estimated_size.html).
|
|
636
|
+
|
|
637
|
+
Examples:
|
|
638
|
+
>>> m = pf.Model()
|
|
639
|
+
>>> m.X = pf.Variable()
|
|
640
|
+
>>> m.Y = pf.Variable(pf.Set(dim_x=range(100)))
|
|
641
|
+
>>> m.c1 = m.X.over("dim_x") + m.Y <= 10
|
|
642
|
+
>>> m.c2 = m.X + m.Y.sum() <= 20
|
|
643
|
+
>>> m.constraints_size_info()
|
|
644
|
+
shape: (3, 7)
|
|
645
|
+
┌───────┬───────────────┬──────────────┬──────────────┬──────────────┬──────────────┬──────────────┐
|
|
646
|
+
│ name ┆ num_constrain ┆ num_constrai ┆ num_non_zero ┆ num_non_zero ┆ pyoframe_mem ┆ pyoframe_mem │
|
|
647
|
+
│ --- ┆ ts ┆ nts_perc ┆ s ┆ s_perc ┆ ory_usage ┆ ory_usage_pe │
|
|
648
|
+
│ str ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ rc │
|
|
649
|
+
│ ┆ i64 ┆ str ┆ i64 ┆ str ┆ i64 ┆ --- │
|
|
650
|
+
│ ┆ ┆ ┆ ┆ ┆ ┆ str │
|
|
651
|
+
╞═══════╪═══════════════╪══════════════╪══════════════╪══════════════╪══════════════╪══════════════╡
|
|
652
|
+
│ c1 ┆ 100 ┆ 99.0% ┆ 300 ┆ 74.6% ┆ 7314 ┆ 85.6% │
|
|
653
|
+
│ c2 ┆ 1 ┆ 1.0% ┆ 102 ┆ 25.4% ┆ 1228 ┆ 14.4% │
|
|
654
|
+
│ TOTAL ┆ 101 ┆ 100.0% ┆ 402 ┆ 100.0% ┆ 8542 ┆ 100.0% │
|
|
655
|
+
└───────┴───────────────┴──────────────┴──────────────┴──────────────┴──────────────┴──────────────┘
|
|
656
|
+
"""
|
|
657
|
+
data = pl.DataFrame(
|
|
658
|
+
[
|
|
659
|
+
dict(
|
|
660
|
+
name=c.name,
|
|
661
|
+
n=len(c),
|
|
662
|
+
non_zeros=c.lhs.data.height,
|
|
663
|
+
mem=c.estimated_size(memory_unit),
|
|
664
|
+
)
|
|
665
|
+
for c in self.constraints
|
|
666
|
+
]
|
|
667
|
+
).sort("n", descending=True)
|
|
668
|
+
|
|
669
|
+
total = data.sum().with_columns(name=pl.lit("TOTAL"))
|
|
670
|
+
data = pl.concat([data, total])
|
|
671
|
+
|
|
672
|
+
def format(col: pl.Expr) -> pl.Expr:
|
|
673
|
+
return (100 * col).round(1).cast(pl.String) + pl.lit("%")
|
|
674
|
+
|
|
675
|
+
data = data.with_columns(
|
|
676
|
+
n_per=format(pl.col("n") / total["n"].item()),
|
|
677
|
+
non_zeros_per=format(pl.col("non_zeros") / total["non_zeros"].item()),
|
|
678
|
+
mem_per=format(pl.col("mem") / total["mem"].item()),
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
data = data.select(
|
|
682
|
+
"name", "n", "n_per", "non_zeros", "non_zeros_per", "mem", "mem_per"
|
|
683
|
+
)
|
|
684
|
+
data = data.rename(
|
|
685
|
+
{
|
|
686
|
+
"n": "num_constraints",
|
|
687
|
+
"n_per": "num_constraints_perc",
|
|
688
|
+
"non_zeros": "num_non_zeros",
|
|
689
|
+
"non_zeros_per": "num_non_zeros_perc",
|
|
690
|
+
"mem": "pyoframe_memory_usage",
|
|
691
|
+
"mem_per": "pyoframe_memory_usage_perc",
|
|
692
|
+
}
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
return pl.DataFrame(data)
|
|
696
|
+
|
|
535
697
|
def dispose(self):
|
|
536
698
|
"""Disposes of the model and cleans up the solver environment.
|
|
537
699
|
|
pyoframe/_model_element.py
CHANGED
|
@@ -48,6 +48,8 @@ class BaseBlock(ABC):
|
|
|
48
48
|
self._data = data
|
|
49
49
|
self._model: Model | None = None
|
|
50
50
|
self.name: str = name # gets overwritten if object is added to model
|
|
51
|
+
"""A user-friendly name that is displayed when printing the object or in error messages.
|
|
52
|
+
When an object is added to a model, this name is updated to the name used in the model."""
|
|
51
53
|
|
|
52
54
|
def _on_add_to_model(self, model: Model, name: str):
|
|
53
55
|
self.name = name
|
pyoframe/_monkey_patch.py
CHANGED
|
@@ -5,8 +5,8 @@ from functools import wraps
|
|
|
5
5
|
import pandas as pd
|
|
6
6
|
import polars as pl
|
|
7
7
|
|
|
8
|
-
from pyoframe.
|
|
9
|
-
from pyoframe.
|
|
8
|
+
from pyoframe._core import BaseOperableBlock
|
|
9
|
+
from pyoframe._param import Param
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def _patch_class(cls):
|
|
@@ -29,54 +29,10 @@ def _patch_class(cls):
|
|
|
29
29
|
cls.__contains__ = _patch_method(cls.__contains__)
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
def polars_df_to_expr(self: pl.DataFrame) -> Expression:
|
|
33
|
-
"""Converts a [polars](https://pola.rs/) `DataFrame` to a Pyoframe [Expression][pyoframe.Expression] by using the last column for values and the previous columns as dimensions.
|
|
34
|
-
|
|
35
|
-
See [Special Functions](../../learn/concepts/special-functions.md#dataframeto_expr) for more details.
|
|
36
|
-
|
|
37
|
-
Examples:
|
|
38
|
-
>>> import polars as pl
|
|
39
|
-
>>> df = pl.DataFrame({"x": [1, 2, 3], "y": [4, 5, 6], "z": [7, 8, 9]})
|
|
40
|
-
>>> df.to_expr()
|
|
41
|
-
<Expression height=3 terms=3 type=constant>
|
|
42
|
-
┌─────┬─────┬────────────┐
|
|
43
|
-
│ x ┆ y ┆ expression │
|
|
44
|
-
│ (3) ┆ (3) ┆ │
|
|
45
|
-
╞═════╪═════╪════════════╡
|
|
46
|
-
│ 1 ┆ 4 ┆ 7 │
|
|
47
|
-
│ 2 ┆ 5 ┆ 8 │
|
|
48
|
-
│ 3 ┆ 6 ┆ 9 │
|
|
49
|
-
└─────┴─────┴────────────┘
|
|
50
|
-
"""
|
|
51
|
-
name = self.columns[-1]
|
|
52
|
-
return Expression(
|
|
53
|
-
self.rename({name: COEF_KEY})
|
|
54
|
-
.drop_nulls(COEF_KEY)
|
|
55
|
-
.with_columns(pl.lit(CONST_TERM).alias(VAR_KEY)),
|
|
56
|
-
name=name,
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def pandas_df_to_expr(self: pd.DataFrame) -> Expression:
|
|
61
|
-
"""Same as [`polars.DataFrame.to_expr`](./polars.DataFrame.to_expr.md), but for [pandas](https://pandas.pydata.org/) DataFrames."""
|
|
62
|
-
return polars_df_to_expr(pl.from_pandas(self))
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def pandas_series_to_expr(self: pd.Series) -> Expression:
|
|
66
|
-
"""Converts a [pandas](https://pandas.pydata.org/) `Series` to a Pyoframe [Expression][pyoframe.Expression], using the index for labels.
|
|
67
|
-
|
|
68
|
-
See [Special Functions](../../learn/concepts/special-functions.md#dataframeto_expr) for more details.
|
|
69
|
-
|
|
70
|
-
Note that no equivalent method exists for Polars Series, as Polars does not support indexes.
|
|
71
|
-
"""
|
|
72
|
-
return pandas_df_to_expr(self.to_frame().reset_index())
|
|
73
|
-
|
|
74
|
-
|
|
75
32
|
def patch_dataframe_libraries():
|
|
76
33
|
_patch_class(pd.DataFrame)
|
|
77
34
|
_patch_class(pd.Series)
|
|
78
35
|
_patch_class(pl.DataFrame)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
pd.
|
|
82
|
-
pd.Series.to_expr = pandas_series_to_expr
|
|
36
|
+
pl.DataFrame.to_expr = lambda self: Param(self) # type: ignore
|
|
37
|
+
pd.DataFrame.to_expr = lambda self: Param(self) # type: ignore
|
|
38
|
+
pd.Series.to_expr = lambda self: Param(self) # type: ignore
|
pyoframe/_objective.py
CHANGED
|
@@ -20,7 +20,7 @@ class Objective(Expression):
|
|
|
20
20
|
>>> m.con = m.A + m.B <= 10
|
|
21
21
|
>>> m.maximize = 2 * m.B + 4
|
|
22
22
|
>>> m.maximize
|
|
23
|
-
<Objective terms=2
|
|
23
|
+
<Objective (linear) terms=2>
|
|
24
24
|
2 B +4
|
|
25
25
|
|
|
26
26
|
The objective value can be retrieved with from the solver once the model is solved using `.value`.
|
|
@@ -67,7 +67,7 @@ class Objective(Expression):
|
|
|
67
67
|
if isinstance(expr, (int, float)):
|
|
68
68
|
expr = Expression.constant(expr)
|
|
69
69
|
else:
|
|
70
|
-
expr = expr.to_expr()
|
|
70
|
+
expr = expr.to_expr() # TODO don't rely on monkey patch
|
|
71
71
|
super().__init__(expr.data, name="objective")
|
|
72
72
|
self._model = expr._model
|
|
73
73
|
if self.dimensions is not None:
|
pyoframe/_param.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Defines the function for creating model parameters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import polars as pl
|
|
9
|
+
|
|
10
|
+
from pyoframe._constants import COEF_KEY, CONST_TERM, VAR_KEY
|
|
11
|
+
from pyoframe._core import Expression
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def Param(
|
|
15
|
+
data: pl.DataFrame | pd.DataFrame | pd.Series | dict | str | Path,
|
|
16
|
+
) -> Expression:
|
|
17
|
+
"""Creates a model parameter, i.e. an [Expression][pyoframe.Expression] that doesn't involve any variables.
|
|
18
|
+
|
|
19
|
+
A Parameter can be created from a DataFrame, CSV file, Parquet file, data dictionary, or a Pandas Series.
|
|
20
|
+
|
|
21
|
+
!!! info "`Param` is a function, not a class"
|
|
22
|
+
Technically, `Param(data)` is a function that returns an [Expression][pyoframe.Expression], not a class.
|
|
23
|
+
However, for consistency with other modeling frameworks, we provide it as a class-like function (i.e. an uppercase function).
|
|
24
|
+
|
|
25
|
+
!!! tip "Smart naming"
|
|
26
|
+
If a Param is not given a name (i.e. if it is not assigned to a model: `m.my_name = Param(...)`),
|
|
27
|
+
then its [name][pyoframe._model_element.BaseBlock.name] is inferred from the name of the column in `data` that contains the parameter values.
|
|
28
|
+
This makes debugging models with inline parameters easier.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
data: The data to use for the parameter.
|
|
32
|
+
|
|
33
|
+
If `data` is a polars or pandas `DataFrame`, the last column will be treated as the values of the parameter, and all other columns as labels.
|
|
34
|
+
|
|
35
|
+
If `data` is a string or `Path`, it will be interpreted as a path to a CSV or Parquet file that will be read and used as a `DataFrame`. The file extension must be `.csv` or `.parquet`.
|
|
36
|
+
|
|
37
|
+
If `data` is a `pandas.Series`, the index(es) will be treated as columns for labels and the series values as the parameter values.
|
|
38
|
+
|
|
39
|
+
If `data` is of any other type (e.g. a dictionary), it will be used as if you had called `Param(pl.DataFrame(data))`.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
An Expression representing the parameter.
|
|
43
|
+
|
|
44
|
+
Examples:
|
|
45
|
+
>>> m = pf.Model()
|
|
46
|
+
>>> m.fixed_cost = pf.Param({"plant": ["A", "B"], "cost": [1000, 1500]})
|
|
47
|
+
>>> m.fixed_cost
|
|
48
|
+
<Expression (parameter) height=2 terms=2>
|
|
49
|
+
┌───────┬────────────┐
|
|
50
|
+
│ plant ┆ expression │
|
|
51
|
+
│ (2) ┆ │
|
|
52
|
+
╞═══════╪════════════╡
|
|
53
|
+
│ A ┆ 1000 │
|
|
54
|
+
│ B ┆ 1500 │
|
|
55
|
+
└───────┴────────────┘
|
|
56
|
+
|
|
57
|
+
Since `Param` simply returns an Expression, you can use it in building larger expressions as usual:
|
|
58
|
+
|
|
59
|
+
>>> m.variable_cost = pf.Param(
|
|
60
|
+
... pl.DataFrame({"plant": ["A", "B"], "cost": [50, 60]})
|
|
61
|
+
... )
|
|
62
|
+
>>> m.total_cost = m.fixed_cost + m.variable_cost
|
|
63
|
+
>>> m.total_cost
|
|
64
|
+
<Expression (parameter) height=2 terms=2>
|
|
65
|
+
┌───────┬────────────┐
|
|
66
|
+
│ plant ┆ expression │
|
|
67
|
+
│ (2) ┆ │
|
|
68
|
+
╞═══════╪════════════╡
|
|
69
|
+
│ A ┆ 1050 │
|
|
70
|
+
│ B ┆ 1560 │
|
|
71
|
+
└───────┴────────────┘
|
|
72
|
+
"""
|
|
73
|
+
if isinstance(data, pd.Series):
|
|
74
|
+
data = data.to_frame().reset_index()
|
|
75
|
+
if isinstance(data, pd.DataFrame):
|
|
76
|
+
data = pl.from_pandas(data)
|
|
77
|
+
|
|
78
|
+
if isinstance(data, (str, Path)):
|
|
79
|
+
data = Path(data)
|
|
80
|
+
if data.suffix.lower() == ".csv":
|
|
81
|
+
data = pl.read_csv(data)
|
|
82
|
+
elif data.suffix.lower() in {".parquet"}:
|
|
83
|
+
data = pl.read_parquet(data)
|
|
84
|
+
else:
|
|
85
|
+
raise NotImplementedError(
|
|
86
|
+
f"Could not create parameter. Unsupported file format: {data.suffix}"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if not isinstance(data, pl.DataFrame):
|
|
90
|
+
data = pl.DataFrame(data)
|
|
91
|
+
|
|
92
|
+
value_col = data.columns[-1]
|
|
93
|
+
|
|
94
|
+
return Expression(
|
|
95
|
+
data.rename({value_col: COEF_KEY})
|
|
96
|
+
.drop_nulls(COEF_KEY)
|
|
97
|
+
.with_columns(pl.lit(CONST_TERM).alias(VAR_KEY)),
|
|
98
|
+
name=value_col,
|
|
99
|
+
)
|
pyoframe/_utils.py
CHANGED
|
@@ -42,7 +42,7 @@ def get_obj_repr(obj: object, *props: str | None, **kwargs):
|
|
|
42
42
|
|
|
43
43
|
See usage for examples.
|
|
44
44
|
"""
|
|
45
|
-
props_str = " ".join(
|
|
45
|
+
props_str = " ".join(v for v in props if v is not None)
|
|
46
46
|
if props_str:
|
|
47
47
|
props_str += " "
|
|
48
48
|
kwargs_str = " ".join(f"{k}={v}" for k, v in kwargs.items() if v is not None)
|
|
@@ -302,7 +302,7 @@ class NamedVariableMapper:
|
|
|
302
302
|
>>> m = pf.Model()
|
|
303
303
|
>>> m.foo = pf.Variable(pl.DataFrame({"t": range(4)}))
|
|
304
304
|
>>> m.foo.sum()
|
|
305
|
-
<Expression terms=4
|
|
305
|
+
<Expression (linear) terms=4>
|
|
306
306
|
foo[0] + foo[1] + foo[2] + foo[3]
|
|
307
307
|
"""
|
|
308
308
|
|
pyoframe/_version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '1.0
|
|
32
|
-
__version_tuple__ = version_tuple = (1,
|
|
31
|
+
__version__ = version = '1.2.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 2, 0)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyoframe
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Blazing fast linear program interface
|
|
5
5
|
Author-email: Bravos Power <dev@bravospower.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -20,21 +20,21 @@ Requires-Dist: pyarrow
|
|
|
20
20
|
Requires-Dist: pandas<3
|
|
21
21
|
Requires-Dist: pyoptinterface==0.5.1
|
|
22
22
|
Provides-Extra: highs
|
|
23
|
-
Requires-Dist: highsbox<=1.
|
|
23
|
+
Requires-Dist: highsbox<=1.12.0; extra == "highs"
|
|
24
24
|
Provides-Extra: ipopt
|
|
25
25
|
Requires-Dist: pyoptinterface[nlp]; extra == "ipopt"
|
|
26
|
-
Requires-Dist: llvmlite<=0.
|
|
26
|
+
Requires-Dist: llvmlite<=0.46.0; extra == "ipopt"
|
|
27
27
|
Provides-Extra: dev
|
|
28
28
|
Requires-Dist: ruff==0.12.11; extra == "dev"
|
|
29
29
|
Requires-Dist: polars>=1.32.3; extra == "dev"
|
|
30
30
|
Requires-Dist: pytest==8.4.1; extra == "dev"
|
|
31
31
|
Requires-Dist: pytest-cov==6.2.1; extra == "dev"
|
|
32
|
-
Requires-Dist: sybil[pytest]==9.
|
|
32
|
+
Requires-Dist: sybil[pytest]==9.3.0; extra == "dev"
|
|
33
33
|
Requires-Dist: pre-commit==4.3.0; extra == "dev"
|
|
34
34
|
Requires-Dist: gurobipy==12.0.3; extra == "dev"
|
|
35
35
|
Requires-Dist: coverage==7.10.6; extra == "dev"
|
|
36
36
|
Requires-Dist: ipykernel==6.30.1; extra == "dev"
|
|
37
|
-
Requires-Dist: highsbox<=1.
|
|
37
|
+
Requires-Dist: highsbox<=1.12.0; extra == "dev"
|
|
38
38
|
Requires-Dist: pyoptinterface[nlp]; extra == "dev"
|
|
39
39
|
Requires-Dist: numpy; extra == "dev"
|
|
40
40
|
Provides-Extra: docs
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
pyoframe/__init__.py,sha256=Ij-9priyKTHaGzVhMhtZlDKWz0ggAwGAS9DqB4O6zWU,886
|
|
2
|
+
pyoframe/_arithmetic.py,sha256=n41QG9qlAn5rZkYUmF9EUR3B3abOUJWlJEDZ3P5jgj4,20578
|
|
3
|
+
pyoframe/_constants.py,sha256=LWlry4K5w-3vVyq7CpEQ28UfM3LulbKxkO-nBlWWJzE,17847
|
|
4
|
+
pyoframe/_core.py,sha256=M2WbOGrCAxPdl0W2AxC9GSCD8MnRSwBHMn9hytWiQcI,119353
|
|
5
|
+
pyoframe/_model.py,sha256=T9FxSeF8b3xw9P_es0LwMGnDhI4LSAuUpELCgg0RSXA,31754
|
|
6
|
+
pyoframe/_model_element.py,sha256=VVRqh2uM8HFvRFvqQmgM93jqofa-8mPwyB-qYA0YjRU,6345
|
|
7
|
+
pyoframe/_monkey_patch.py,sha256=7FWMRXZIHK7mRkZfOKQ8Y724q1sPbq_EiPjlJCTfYoA,1168
|
|
8
|
+
pyoframe/_objective.py,sha256=HeiP4KjlXn-IplqV-MALF26yMmh41JyHXjZhhtKJIsQ,4367
|
|
9
|
+
pyoframe/_param.py,sha256=FUSfITPb-WZA-xwVcF9dCCHO2K_pky5GBWooImsSy6I,4147
|
|
10
|
+
pyoframe/_utils.py,sha256=XaPZ8j9YQ-HnAuT2NLAvDCJGVzKSjUmRxARNuGWykIM,12508
|
|
11
|
+
pyoframe/_version.py,sha256=-uLONazCO1SzFfcY-K6A1keL--LIVfTYccGX6ciADac,704
|
|
12
|
+
pyoframe-1.2.0.dist-info/licenses/LICENSE,sha256=u_Spw4ynlwTMRZeCX-uacv_hBU547pBygiA6d2ONNV4,1074
|
|
13
|
+
pyoframe-1.2.0.dist-info/METADATA,sha256=C2JCa-gO8TeVP-wOAS0MJDfusaH4TAwtRJ7d7xunzx4,4060
|
|
14
|
+
pyoframe-1.2.0.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
15
|
+
pyoframe-1.2.0.dist-info/top_level.txt,sha256=10z3OOJSVLriQ0IrFLMH8CH9zByugPWolqhlHlkNjV4,9
|
|
16
|
+
pyoframe-1.2.0.dist-info/RECORD,,
|
pyoframe-1.0.1.dist-info/RECORD
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
pyoframe/__init__.py,sha256=Nlql3FYed7bXWumvUeMd3rjnoL4l8XC5orO4uxWrDAc,839
|
|
2
|
-
pyoframe/_arithmetic.py,sha256=9V3N2Yq7Ib13UfTQnINxa9oS7786f5DnRsrAPcFlYYE,20558
|
|
3
|
-
pyoframe/_constants.py,sha256=n80so80usutTJpeDlDXkHRE-_dpPD1LxF2plscXhbwQ,17925
|
|
4
|
-
pyoframe/_core.py,sha256=NXH_ze1iN61vk38Fj6Ire0uqKUJRSDUlD6PVc8mTHrQ,116975
|
|
5
|
-
pyoframe/_model.py,sha256=MtA9gleQoUAqO2dxhCFZ8GOZvyyJB_PIYBzvQ0m8enc,22466
|
|
6
|
-
pyoframe/_model_element.py,sha256=oQ7nykJ5XEzJ6Klq3lT6ZwQvDrxY_wgZYVaN7pgyZOs,6149
|
|
7
|
-
pyoframe/_monkey_patch.py,sha256=Y2zXN5MpqDeAWELddyaFQNam57fehSXHiza1PFaZ-QY,3128
|
|
8
|
-
pyoframe/_objective.py,sha256=yIHoaBLsjGCKzIB6RQErV3vzE2U5DGORlhifRStB_Mc,4335
|
|
9
|
-
pyoframe/_utils.py,sha256=5yy-5DOWCW7q3QzPv9tKquLUQJtNdpJJilEqxAq7TN8,12518
|
|
10
|
-
pyoframe/_version.py,sha256=JvmBpae6cHui8lSCsCcZQAxzawN2NERHGsr-rIUeJMo,704
|
|
11
|
-
pyoframe-1.0.1.dist-info/licenses/LICENSE,sha256=u_Spw4ynlwTMRZeCX-uacv_hBU547pBygiA6d2ONNV4,1074
|
|
12
|
-
pyoframe-1.0.1.dist-info/METADATA,sha256=UtnvQdR7vJQvLm8p0RvgwdGfR7yCISswH_V4KuihBr4,4060
|
|
13
|
-
pyoframe-1.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
14
|
-
pyoframe-1.0.1.dist-info/top_level.txt,sha256=10z3OOJSVLriQ0IrFLMH8CH9zByugPWolqhlHlkNjV4,9
|
|
15
|
-
pyoframe-1.0.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|