pyoframe 1.0.0a0__py3-none-any.whl → 1.1.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 +179 -177
- pyoframe/_constants.py +103 -57
- pyoframe/_core.py +308 -204
- pyoframe/_model.py +49 -29
- pyoframe/_model_element.py +34 -18
- pyoframe/_monkey_patch.py +8 -50
- pyoframe/_objective.py +4 -6
- pyoframe/_param.py +99 -0
- pyoframe/_utils.py +10 -11
- pyoframe/_version.py +2 -2
- {pyoframe-1.0.0a0.dist-info → pyoframe-1.1.0.dist-info}/METADATA +13 -14
- pyoframe-1.1.0.dist-info/RECORD +16 -0
- pyoframe-1.0.0a0.dist-info/RECORD +0 -15
- {pyoframe-1.0.0a0.dist-info → pyoframe-1.1.0.dist-info}/WHEEL +0 -0
- {pyoframe-1.0.0a0.dist-info → pyoframe-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {pyoframe-1.0.0a0.dist-info → pyoframe-1.1.0.dist-info}/top_level.txt +0 -0
pyoframe/_core.py
CHANGED
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import warnings
|
|
6
6
|
from abc import abstractmethod
|
|
7
7
|
from collections.abc import Iterable, Mapping, Sequence
|
|
8
|
-
from typing import TYPE_CHECKING, Literal,
|
|
8
|
+
from typing import TYPE_CHECKING, Literal, Union, overload
|
|
9
9
|
|
|
10
10
|
import pandas as pd
|
|
11
11
|
import polars as pl
|
|
@@ -22,20 +22,19 @@ from pyoframe._constants import (
|
|
|
22
22
|
CONST_TERM,
|
|
23
23
|
CONSTRAINT_KEY,
|
|
24
24
|
DUAL_KEY,
|
|
25
|
-
KEY_TYPE,
|
|
26
25
|
QUAD_VAR_KEY,
|
|
27
26
|
RESERVED_COL_KEYS,
|
|
28
27
|
SOLUTION_KEY,
|
|
29
28
|
VAR_KEY,
|
|
30
29
|
Config,
|
|
31
30
|
ConstraintSense,
|
|
31
|
+
ExtrasStrategy,
|
|
32
32
|
ObjSense,
|
|
33
33
|
PyoframeError,
|
|
34
|
-
UnmatchedStrategy,
|
|
35
34
|
VType,
|
|
36
35
|
VTypeValue,
|
|
37
36
|
)
|
|
38
|
-
from pyoframe._model_element import
|
|
37
|
+
from pyoframe._model_element import BaseBlock
|
|
39
38
|
from pyoframe._utils import (
|
|
40
39
|
Container,
|
|
41
40
|
FuncArgs,
|
|
@@ -51,45 +50,82 @@ from pyoframe._utils import (
|
|
|
51
50
|
if TYPE_CHECKING: # pragma: no cover
|
|
52
51
|
from pyoframe._model import Model
|
|
53
52
|
|
|
53
|
+
Operable = Union["BaseOperableBlock", pl.DataFrame, pd.DataFrame, pd.Series, int, float]
|
|
54
|
+
"""Any of the following objects: `int`, `float`, [Variable][pyoframe.Variable], [Expression][pyoframe.Expression], [Set][pyoframe.Set], polars or pandas DataFrame, or pandas Series."""
|
|
54
55
|
|
|
55
|
-
# TODO consider changing this simply to a type and having a helper "Expression.from(object)"
|
|
56
|
-
class SupportsToExpr(Protocol):
|
|
57
|
-
"""Protocol for any object that can be converted to a Pyoframe [Expression][pyoframe.Expression]."""
|
|
58
56
|
|
|
59
|
-
|
|
60
|
-
"""Converts the object to a Pyoframe [Expression][pyoframe.Expression]."""
|
|
61
|
-
...
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
class SupportsMath(ModelElement, SupportsToExpr):
|
|
57
|
+
class BaseOperableBlock(BaseBlock):
|
|
65
58
|
"""Any object that can be converted into an expression."""
|
|
66
59
|
|
|
67
60
|
def __init__(self, *args, **kwargs):
|
|
68
|
-
self.
|
|
61
|
+
self._extras_strategy = ExtrasStrategy.UNSET
|
|
69
62
|
self._allowed_new_dims: list[str] = []
|
|
70
63
|
super().__init__(*args, **kwargs)
|
|
71
64
|
|
|
72
65
|
@abstractmethod
|
|
73
|
-
def _new(self, data: pl.DataFrame, name: str) ->
|
|
66
|
+
def _new(self, data: pl.DataFrame, name: str) -> BaseOperableBlock:
|
|
74
67
|
"""Helper method to create a new instance of the same (or for Variable derivative) class."""
|
|
75
68
|
|
|
76
|
-
def _copy_flags(self, other:
|
|
77
|
-
"""Copies the flags from another
|
|
78
|
-
self.
|
|
69
|
+
def _copy_flags(self, other: BaseOperableBlock):
|
|
70
|
+
"""Copies the flags from another BaseOperableBlock object."""
|
|
71
|
+
self._extras_strategy = other._extras_strategy
|
|
79
72
|
self._allowed_new_dims = other._allowed_new_dims.copy()
|
|
80
73
|
|
|
81
|
-
def
|
|
82
|
-
"""Indicates that
|
|
83
|
-
|
|
74
|
+
def keep_extras(self):
|
|
75
|
+
"""Indicates that labels not present in the other expression should be kept during addition, subtraction, or constraint creation.
|
|
76
|
+
|
|
77
|
+
[Learn more](../../learn/concepts/addition.md) about addition modifiers.
|
|
78
|
+
|
|
79
|
+
See Also:
|
|
80
|
+
[`drop_extras`][pyoframe.Expression.drop_extras].
|
|
81
|
+
"""
|
|
82
|
+
new = self._new(self.data, name=f"{self.name}.keep_extras()")
|
|
84
83
|
new._copy_flags(self)
|
|
85
|
-
new.
|
|
84
|
+
new._extras_strategy = ExtrasStrategy.KEEP
|
|
86
85
|
return new
|
|
87
86
|
|
|
88
|
-
def
|
|
89
|
-
"""Indicates that
|
|
90
|
-
|
|
87
|
+
def drop_extras(self):
|
|
88
|
+
"""Indicates that labels not present in the other expression should be discarded during addition, subtraction, or constraint creation.
|
|
89
|
+
|
|
90
|
+
[Learn more](../../learn/concepts/addition.md) about addition modifiers.
|
|
91
|
+
|
|
92
|
+
See Also:
|
|
93
|
+
[`keep_extras`][pyoframe.Expression.keep_extras].
|
|
94
|
+
"""
|
|
95
|
+
new = self._new(self.data, name=f"{self.name}.drop_extras()")
|
|
91
96
|
new._copy_flags(self)
|
|
92
|
-
new.
|
|
97
|
+
new._extras_strategy = ExtrasStrategy.DROP
|
|
98
|
+
return new
|
|
99
|
+
|
|
100
|
+
def keep_unmatched(self): # pragma: no cover
|
|
101
|
+
"""Deprecated, use [`keep_extras`][pyoframe.Expression.keep_extras] instead."""
|
|
102
|
+
warnings.warn(
|
|
103
|
+
"'keep_unmatched' has been renamed to 'keep_extras'. Please use 'keep_extras' instead.",
|
|
104
|
+
DeprecationWarning,
|
|
105
|
+
)
|
|
106
|
+
return self.keep_extras()
|
|
107
|
+
|
|
108
|
+
def drop_unmatched(self): # pragma: no cover
|
|
109
|
+
"""Deprecated, use [`drop_extras`][pyoframe.Expression.drop_extras] instead."""
|
|
110
|
+
warnings.warn(
|
|
111
|
+
"'drop_unmatched' has been renamed to 'drop_extras'. Please use 'drop_extras' instead.",
|
|
112
|
+
DeprecationWarning,
|
|
113
|
+
)
|
|
114
|
+
return self.drop_extras()
|
|
115
|
+
|
|
116
|
+
def raise_extras(self):
|
|
117
|
+
"""Indicates that labels not present in the other expression should raise an error during addition, subtraction, or constraint creation.
|
|
118
|
+
|
|
119
|
+
This is the default behavior and, as such, this addition modifier should only be used in the rare cases where you want to override a previous use of `keep_extras()` or `drop_extras()`.
|
|
120
|
+
|
|
121
|
+
[Learn more](../../learn/concepts/addition.md) about addition modifiers.
|
|
122
|
+
|
|
123
|
+
See Also:
|
|
124
|
+
[`keep_extras`][pyoframe.Expression.keep_extras] and [`drop_extras`][pyoframe.Expression.drop_extras].
|
|
125
|
+
"""
|
|
126
|
+
new = self._new(self.data, name=f"{self.name}.raise_extras()")
|
|
127
|
+
new._copy_flags(self)
|
|
128
|
+
new._extras_strategy = ExtrasStrategy.UNSET
|
|
93
129
|
return new
|
|
94
130
|
|
|
95
131
|
def over(self, *dims: str):
|
|
@@ -105,7 +141,7 @@ class SupportsMath(ModelElement, SupportsToExpr):
|
|
|
105
141
|
|
|
106
142
|
Takes the same arguments as [`polars.DataFrame.rename`](https://docs.pola.rs/api/python/stable/reference/dataframe/api/polars.DataFrame.rename.html).
|
|
107
143
|
|
|
108
|
-
See the [portfolio optimization example](
|
|
144
|
+
See the [portfolio optimization example](../../examples/portfolio_optimization.md) for a usage example.
|
|
109
145
|
|
|
110
146
|
Examples:
|
|
111
147
|
>>> m = pf.Model()
|
|
@@ -133,7 +169,7 @@ class SupportsMath(ModelElement, SupportsToExpr):
|
|
|
133
169
|
└───────┴─────────┴──────────────────┘
|
|
134
170
|
|
|
135
171
|
>>> m.v.rename({"city": "location"})
|
|
136
|
-
<Expression height=12 terms=12
|
|
172
|
+
<Expression (linear) height=12 terms=12>
|
|
137
173
|
┌───────┬──────────┬──────────────────┐
|
|
138
174
|
│ hour ┆ location ┆ expression │
|
|
139
175
|
│ (4) ┆ (3) ┆ │
|
|
@@ -192,7 +228,7 @@ class SupportsMath(ModelElement, SupportsToExpr):
|
|
|
192
228
|
... ]
|
|
193
229
|
... )
|
|
194
230
|
>>> m.v.pick(hour="06:00")
|
|
195
|
-
<Expression height=3 terms=3
|
|
231
|
+
<Expression (linear) height=3 terms=3>
|
|
196
232
|
┌─────────┬──────────────────┐
|
|
197
233
|
│ city ┆ expression │
|
|
198
234
|
│ (3) ┆ │
|
|
@@ -202,7 +238,7 @@ class SupportsMath(ModelElement, SupportsToExpr):
|
|
|
202
238
|
│ Paris ┆ v[06:00,Paris] │
|
|
203
239
|
└─────────┴──────────────────┘
|
|
204
240
|
>>> m.v.pick(hour="06:00", city="Toronto")
|
|
205
|
-
<Expression terms=1
|
|
241
|
+
<Expression (linear) terms=1>
|
|
206
242
|
v[06:00,Toronto]
|
|
207
243
|
|
|
208
244
|
See Also:
|
|
@@ -254,7 +290,7 @@ class SupportsMath(ModelElement, SupportsToExpr):
|
|
|
254
290
|
>>> m = pf.Model()
|
|
255
291
|
>>> m.v = pf.Variable()
|
|
256
292
|
>>> m.v**2
|
|
257
|
-
<Expression terms=1
|
|
293
|
+
<Expression (quadratic) terms=1>
|
|
258
294
|
v * v
|
|
259
295
|
>>> m.v**3
|
|
260
296
|
Traceback (most recent call last):
|
|
@@ -284,7 +320,7 @@ class SupportsMath(ModelElement, SupportsToExpr):
|
|
|
284
320
|
>>> df = pl.DataFrame({"dim1": [1, 2, 3], "value": [1, 2, 3]})
|
|
285
321
|
>>> m.v = pf.Variable(df["dim1"])
|
|
286
322
|
>>> m.v - df
|
|
287
|
-
<Expression height=3 terms=6
|
|
323
|
+
<Expression (linear) height=3 terms=6>
|
|
288
324
|
┌──────┬────────────┐
|
|
289
325
|
│ dim1 ┆ expression │
|
|
290
326
|
│ (3) ┆ │
|
|
@@ -295,7 +331,7 @@ class SupportsMath(ModelElement, SupportsToExpr):
|
|
|
295
331
|
└──────┴────────────┘
|
|
296
332
|
"""
|
|
297
333
|
if not isinstance(other, (int, float)):
|
|
298
|
-
other = other.to_expr()
|
|
334
|
+
other = other.to_expr() # TODO don't rely on monkey patch
|
|
299
335
|
return self.to_expr() + (-other)
|
|
300
336
|
|
|
301
337
|
def __rmul__(self, other):
|
|
@@ -312,7 +348,7 @@ class SupportsMath(ModelElement, SupportsToExpr):
|
|
|
312
348
|
>>> m = pf.Model()
|
|
313
349
|
>>> m.v = Variable({"dim1": [1, 2, 3]})
|
|
314
350
|
>>> m.v / 2
|
|
315
|
-
<Expression height=3 terms=3
|
|
351
|
+
<Expression (linear) height=3 terms=3>
|
|
316
352
|
┌──────┬────────────┐
|
|
317
353
|
│ dim1 ┆ expression │
|
|
318
354
|
│ (3) ┆ │
|
|
@@ -324,6 +360,13 @@ class SupportsMath(ModelElement, SupportsToExpr):
|
|
|
324
360
|
"""
|
|
325
361
|
return self.to_expr() * (1 / other)
|
|
326
362
|
|
|
363
|
+
def __rtruediv__(self, other):
|
|
364
|
+
# This just improves error messages when trying to divide by a Set or Variable.
|
|
365
|
+
# When dividing by an Expression, see the Expression.__rtruediv__ method.
|
|
366
|
+
raise PyoframeError(
|
|
367
|
+
f"Cannot divide by '{self.name}' because it is not a number or parameter."
|
|
368
|
+
)
|
|
369
|
+
|
|
327
370
|
def __rsub__(self, other):
|
|
328
371
|
"""Supports right subtraction.
|
|
329
372
|
|
|
@@ -331,7 +374,7 @@ class SupportsMath(ModelElement, SupportsToExpr):
|
|
|
331
374
|
>>> m = pf.Model()
|
|
332
375
|
>>> m.v = Variable({"dim1": [1, 2, 3]})
|
|
333
376
|
>>> 1 - m.v
|
|
334
|
-
<Expression height=3 terms=6
|
|
377
|
+
<Expression (linear) height=3 terms=6>
|
|
335
378
|
┌──────┬────────────┐
|
|
336
379
|
│ dim1 ┆ expression │
|
|
337
380
|
│ (3) ┆ │
|
|
@@ -344,39 +387,22 @@ class SupportsMath(ModelElement, SupportsToExpr):
|
|
|
344
387
|
return other + (-self.to_expr())
|
|
345
388
|
|
|
346
389
|
def __le__(self, other):
|
|
347
|
-
"""Equality constraint.
|
|
348
|
-
|
|
349
|
-
Examples:
|
|
350
|
-
>>> m = pf.Model()
|
|
351
|
-
>>> m.v = pf.Variable()
|
|
352
|
-
>>> m.v <= 1
|
|
353
|
-
<Constraint 'unnamed' terms=2 type=linear>
|
|
354
|
-
v <= 1
|
|
355
|
-
"""
|
|
356
390
|
return Constraint(self - other, ConstraintSense.LE)
|
|
357
391
|
|
|
358
|
-
def
|
|
359
|
-
|
|
392
|
+
def __lt__(self, _):
|
|
393
|
+
raise PyoframeError(
|
|
394
|
+
"Constraints cannot be created with the '<' or '>' operators. Did you mean to use '<=' or '>=' instead?"
|
|
395
|
+
)
|
|
360
396
|
|
|
361
|
-
|
|
362
|
-
>>> m = pf.Model()
|
|
363
|
-
>>> m.v = pf.Variable()
|
|
364
|
-
>>> m.v >= 1
|
|
365
|
-
<Constraint 'unnamed' terms=2 type=linear>
|
|
366
|
-
v >= 1
|
|
367
|
-
"""
|
|
397
|
+
def __ge__(self, other):
|
|
368
398
|
return Constraint(self - other, ConstraintSense.GE)
|
|
369
399
|
|
|
370
|
-
def
|
|
371
|
-
|
|
400
|
+
def __gt__(self, _):
|
|
401
|
+
raise PyoframeError(
|
|
402
|
+
"Constraints cannot be created with the '<' or '>' operator. Did you mean to use '<=' or '>=' instead?"
|
|
403
|
+
)
|
|
372
404
|
|
|
373
|
-
|
|
374
|
-
>>> m = pf.Model()
|
|
375
|
-
>>> m.v = pf.Variable()
|
|
376
|
-
>>> m.v == 1
|
|
377
|
-
<Constraint 'unnamed' terms=2 type=linear>
|
|
378
|
-
v = 1
|
|
379
|
-
"""
|
|
405
|
+
def __eq__(self, value: object): # type: ignore
|
|
380
406
|
return Constraint(self - value, ConstraintSense.EQ)
|
|
381
407
|
|
|
382
408
|
|
|
@@ -384,14 +410,14 @@ SetTypes = Union[
|
|
|
384
410
|
pl.DataFrame,
|
|
385
411
|
pd.Index,
|
|
386
412
|
pd.DataFrame,
|
|
387
|
-
|
|
413
|
+
BaseOperableBlock,
|
|
388
414
|
Mapping[str, Sequence[object]],
|
|
389
415
|
"Set",
|
|
390
416
|
"Constraint",
|
|
391
417
|
]
|
|
392
418
|
|
|
393
419
|
|
|
394
|
-
class Set(
|
|
420
|
+
class Set(BaseOperableBlock):
|
|
395
421
|
"""A set which can then be used to index variables.
|
|
396
422
|
|
|
397
423
|
Examples:
|
|
@@ -482,6 +508,45 @@ class Set(SupportsMath):
|
|
|
482
508
|
name=self.name,
|
|
483
509
|
)
|
|
484
510
|
|
|
511
|
+
def drop(self, *dims: str) -> Set:
|
|
512
|
+
"""Returns a new Set with the given dimensions dropped.
|
|
513
|
+
|
|
514
|
+
Only unique rows are kept in the resulting Set.
|
|
515
|
+
|
|
516
|
+
Examples:
|
|
517
|
+
>>> xy = pf.Set(x=range(3), y=range(2))
|
|
518
|
+
>>> xy
|
|
519
|
+
<Set 'unnamed' height=6>
|
|
520
|
+
┌─────┬─────┐
|
|
521
|
+
│ x ┆ y │
|
|
522
|
+
│ (3) ┆ (2) │
|
|
523
|
+
╞═════╪═════╡
|
|
524
|
+
│ 0 ┆ 0 │
|
|
525
|
+
│ 0 ┆ 1 │
|
|
526
|
+
│ 1 ┆ 0 │
|
|
527
|
+
│ 1 ┆ 1 │
|
|
528
|
+
│ 2 ┆ 0 │
|
|
529
|
+
│ 2 ┆ 1 │
|
|
530
|
+
└─────┴─────┘
|
|
531
|
+
>>> x = xy.drop("y")
|
|
532
|
+
>>> x
|
|
533
|
+
<Set 'unnamed_set.drop(…)' height=3>
|
|
534
|
+
┌─────┐
|
|
535
|
+
│ x │
|
|
536
|
+
│ (3) │
|
|
537
|
+
╞═════╡
|
|
538
|
+
│ 0 │
|
|
539
|
+
│ 1 │
|
|
540
|
+
│ 2 │
|
|
541
|
+
└─────┘
|
|
542
|
+
"""
|
|
543
|
+
if not dims:
|
|
544
|
+
raise ValueError("At least one dimension must be provided to drop.")
|
|
545
|
+
return self._new(
|
|
546
|
+
self.data.drop(dims).unique(maintain_order=Config.maintain_order),
|
|
547
|
+
name=f"{self.name}.drop(…)",
|
|
548
|
+
)
|
|
549
|
+
|
|
485
550
|
def __mul__(self, other):
|
|
486
551
|
if isinstance(other, Set):
|
|
487
552
|
overlap_dims = set(self.data.columns) & set(other.data.columns)
|
|
@@ -513,7 +578,7 @@ class Set(SupportsMath):
|
|
|
513
578
|
def __repr__(self):
|
|
514
579
|
header = get_obj_repr(
|
|
515
580
|
self,
|
|
516
|
-
"unnamed" if self.name == "unnamed_set" else self.name,
|
|
581
|
+
"'unnamed'" if self.name == "unnamed_set" else f"'{self.name}'",
|
|
517
582
|
height=self.data.height,
|
|
518
583
|
)
|
|
519
584
|
data = self._add_shape_to_columns(self.data)
|
|
@@ -529,7 +594,7 @@ class Set(SupportsMath):
|
|
|
529
594
|
df = pl.DataFrame(set)
|
|
530
595
|
elif isinstance(set, Constraint):
|
|
531
596
|
df = set.data.select(set._dimensions_unsafe)
|
|
532
|
-
elif isinstance(set,
|
|
597
|
+
elif isinstance(set, BaseOperableBlock):
|
|
533
598
|
df = (
|
|
534
599
|
set.to_expr()
|
|
535
600
|
.data.drop(RESERVED_COL_KEYS, strict=False)
|
|
@@ -570,7 +635,7 @@ class Set(SupportsMath):
|
|
|
570
635
|
return df
|
|
571
636
|
|
|
572
637
|
|
|
573
|
-
class Expression(
|
|
638
|
+
class Expression(BaseOperableBlock):
|
|
574
639
|
"""Represents a linear or quadratic mathematical expression.
|
|
575
640
|
|
|
576
641
|
Examples:
|
|
@@ -587,7 +652,7 @@ class Expression(SupportsMath):
|
|
|
587
652
|
>>> m.Size = pf.Variable(df.index)
|
|
588
653
|
>>> expr = df["cost"] * m.Time + df["cost"] * m.Size
|
|
589
654
|
>>> expr
|
|
590
|
-
<Expression height=5 terms=10
|
|
655
|
+
<Expression (linear) height=5 terms=10>
|
|
591
656
|
┌──────┬──────┬──────────────────────────────┐
|
|
592
657
|
│ item ┆ time ┆ expression │
|
|
593
658
|
│ (2) ┆ (3) ┆ │
|
|
@@ -605,14 +670,14 @@ class Expression(SupportsMath):
|
|
|
605
670
|
assert VAR_KEY in data.columns, "Missing variable column."
|
|
606
671
|
assert COEF_KEY in data.columns, "Missing coefficient column."
|
|
607
672
|
|
|
608
|
-
# Sanity check no duplicates
|
|
673
|
+
# Sanity check no duplicates labels
|
|
609
674
|
if Config.enable_is_duplicated_expression_safety_check:
|
|
610
675
|
duplicated_mask = data.drop(COEF_KEY).is_duplicated()
|
|
611
676
|
# In theory this should never happen unless there's a bug in the library
|
|
612
677
|
if duplicated_mask.any():
|
|
613
678
|
duplicated_data = data.filter(duplicated_mask)
|
|
614
679
|
raise ValueError(
|
|
615
|
-
f"Cannot create an expression with duplicate
|
|
680
|
+
f"Cannot create an expression with duplicate labels:\n{duplicated_data}."
|
|
616
681
|
)
|
|
617
682
|
|
|
618
683
|
data = _simplify_expr_df(data)
|
|
@@ -633,7 +698,7 @@ class Expression(SupportsMath):
|
|
|
633
698
|
|
|
634
699
|
Examples:
|
|
635
700
|
>>> pf.Expression.constant(5)
|
|
636
|
-
<Expression terms=1
|
|
701
|
+
<Expression (parameter) terms=1>
|
|
637
702
|
5
|
|
638
703
|
"""
|
|
639
704
|
return cls(
|
|
@@ -642,7 +707,7 @@ class Expression(SupportsMath):
|
|
|
642
707
|
COEF_KEY: [constant],
|
|
643
708
|
VAR_KEY: [CONST_TERM],
|
|
644
709
|
},
|
|
645
|
-
schema={COEF_KEY: pl.Float64, VAR_KEY:
|
|
710
|
+
schema={COEF_KEY: pl.Float64, VAR_KEY: Config.id_dtype},
|
|
646
711
|
),
|
|
647
712
|
name=str(constant),
|
|
648
713
|
)
|
|
@@ -654,7 +719,7 @@ class Expression(SupportsMath):
|
|
|
654
719
|
If no dimensions are specified, the sum is taken over all of the expression's dimensions.
|
|
655
720
|
|
|
656
721
|
Examples:
|
|
657
|
-
>>> expr =
|
|
722
|
+
>>> expr = pf.Param(
|
|
658
723
|
... {
|
|
659
724
|
... "time": ["mon", "tue", "wed", "mon", "tue"],
|
|
660
725
|
... "place": [
|
|
@@ -666,9 +731,9 @@ class Expression(SupportsMath):
|
|
|
666
731
|
... ],
|
|
667
732
|
... "tiktok_posts": [1e6, 3e6, 2e6, 1e6, 2e6],
|
|
668
733
|
... }
|
|
669
|
-
... )
|
|
734
|
+
... )
|
|
670
735
|
>>> expr
|
|
671
|
-
<Expression height=5 terms=5
|
|
736
|
+
<Expression (parameter) height=5 terms=5>
|
|
672
737
|
┌──────┬───────────┬────────────┐
|
|
673
738
|
│ time ┆ place ┆ expression │
|
|
674
739
|
│ (3) ┆ (2) ┆ │
|
|
@@ -680,7 +745,7 @@ class Expression(SupportsMath):
|
|
|
680
745
|
│ tue ┆ Vancouver ┆ 2000000 │
|
|
681
746
|
└──────┴───────────┴────────────┘
|
|
682
747
|
>>> expr.sum("time")
|
|
683
|
-
<Expression height=2 terms=2
|
|
748
|
+
<Expression (parameter) height=2 terms=2>
|
|
684
749
|
┌───────────┬────────────┐
|
|
685
750
|
│ place ┆ expression │
|
|
686
751
|
│ (2) ┆ │
|
|
@@ -689,7 +754,7 @@ class Expression(SupportsMath):
|
|
|
689
754
|
│ Vancouver ┆ 3000000 │
|
|
690
755
|
└───────────┴────────────┘
|
|
691
756
|
>>> expr.sum()
|
|
692
|
-
<Expression terms=1
|
|
757
|
+
<Expression (parameter) terms=1>
|
|
693
758
|
9000000
|
|
694
759
|
|
|
695
760
|
If the given dimensions don't exist, an error will be raised:
|
|
@@ -725,7 +790,7 @@ class Expression(SupportsMath):
|
|
|
725
790
|
"""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).
|
|
726
791
|
|
|
727
792
|
Examples:
|
|
728
|
-
>>> expr =
|
|
793
|
+
>>> expr = pf.Param(
|
|
729
794
|
... {
|
|
730
795
|
... "time": ["mon", "tue", "wed", "mon", "tue"],
|
|
731
796
|
... "place": [
|
|
@@ -737,9 +802,9 @@ class Expression(SupportsMath):
|
|
|
737
802
|
... ],
|
|
738
803
|
... "tiktok_posts": [1e6, 3e6, 2e6, 1e6, 2e6],
|
|
739
804
|
... }
|
|
740
|
-
... )
|
|
805
|
+
... )
|
|
741
806
|
>>> expr
|
|
742
|
-
<Expression height=5 terms=5
|
|
807
|
+
<Expression (parameter) height=5 terms=5>
|
|
743
808
|
┌──────┬───────────┬────────────┐
|
|
744
809
|
│ time ┆ place ┆ expression │
|
|
745
810
|
│ (3) ┆ (2) ┆ │
|
|
@@ -752,7 +817,7 @@ class Expression(SupportsMath):
|
|
|
752
817
|
└──────┴───────────┴────────────┘
|
|
753
818
|
|
|
754
819
|
>>> expr.sum_by("place")
|
|
755
|
-
<Expression height=2 terms=2
|
|
820
|
+
<Expression (parameter) height=2 terms=2>
|
|
756
821
|
┌───────────┬────────────┐
|
|
757
822
|
│ place ┆ expression │
|
|
758
823
|
│ (2) ┆ │
|
|
@@ -815,13 +880,13 @@ class Expression(SupportsMath):
|
|
|
815
880
|
|
|
816
881
|
Examples:
|
|
817
882
|
>>> import polars as pl
|
|
818
|
-
>>> pop_data =
|
|
883
|
+
>>> pop_data = pf.Param(
|
|
819
884
|
... {
|
|
820
885
|
... "city": ["Toronto", "Vancouver", "Boston"],
|
|
821
886
|
... "year": [2024, 2024, 2024],
|
|
822
887
|
... "population": [10, 2, 8],
|
|
823
888
|
... }
|
|
824
|
-
... )
|
|
889
|
+
... )
|
|
825
890
|
>>> cities_and_countries = pl.DataFrame(
|
|
826
891
|
... {
|
|
827
892
|
... "city": ["Toronto", "Vancouver", "Boston"],
|
|
@@ -829,7 +894,7 @@ class Expression(SupportsMath):
|
|
|
829
894
|
... }
|
|
830
895
|
... )
|
|
831
896
|
>>> pop_data.map(cities_and_countries)
|
|
832
|
-
<Expression height=2 terms=2
|
|
897
|
+
<Expression (parameter) height=2 terms=2>
|
|
833
898
|
┌──────┬─────────┬────────────┐
|
|
834
899
|
│ year ┆ country ┆ expression │
|
|
835
900
|
│ (1) ┆ (2) ┆ │
|
|
@@ -839,7 +904,7 @@ class Expression(SupportsMath):
|
|
|
839
904
|
└──────┴─────────┴────────────┘
|
|
840
905
|
|
|
841
906
|
>>> pop_data.map(cities_and_countries, drop_shared_dims=False)
|
|
842
|
-
<Expression height=3 terms=3
|
|
907
|
+
<Expression (parameter) height=3 terms=3>
|
|
843
908
|
┌───────────┬──────┬─────────┬────────────┐
|
|
844
909
|
│ city ┆ year ┆ country ┆ expression │
|
|
845
910
|
│ (3) ┆ (1) ┆ (2) ┆ │
|
|
@@ -909,7 +974,7 @@ class Expression(SupportsMath):
|
|
|
909
974
|
>>> m = pf.Model()
|
|
910
975
|
>>> m.quantity = pf.Variable(cost[["item", "time"]])
|
|
911
976
|
>>> (m.quantity * cost).rolling_sum(over="time", window_size=2)
|
|
912
|
-
<Expression height=5 terms=8
|
|
977
|
+
<Expression (linear) height=5 terms=8>
|
|
913
978
|
┌──────┬──────┬──────────────────────────────────┐
|
|
914
979
|
│ item ┆ time ┆ expression │
|
|
915
980
|
│ (2) ┆ (3) ┆ │
|
|
@@ -945,11 +1010,8 @@ class Expression(SupportsMath):
|
|
|
945
1010
|
"""Filters this expression to only include the dimensions within the provided set.
|
|
946
1011
|
|
|
947
1012
|
Examples:
|
|
948
|
-
>>>
|
|
949
|
-
>>>
|
|
950
|
-
... {"dim1": [1, 2, 3], "value": [1, 2, 3]}
|
|
951
|
-
... ).to_expr()
|
|
952
|
-
>>> filter_expr = pd.DataFrame({"dim1": [1, 3], "value": [5, 6]}).to_expr()
|
|
1013
|
+
>>> general_expr = pf.Param({"dim1": [1, 2, 3], "value": [1, 2, 3]})
|
|
1014
|
+
>>> filter_expr = pf.Param({"dim1": [1, 3], "value": [5, 6]})
|
|
953
1015
|
>>> general_expr.within(filter_expr).data
|
|
954
1016
|
shape: (2, 3)
|
|
955
1017
|
┌──────┬─────────┬───────────────┐
|
|
@@ -1010,11 +1072,10 @@ class Expression(SupportsMath):
|
|
|
1010
1072
|
If `False`, returns the degree as an integer (0, 1, or 2).
|
|
1011
1073
|
|
|
1012
1074
|
Examples:
|
|
1013
|
-
>>> import pandas as pd
|
|
1014
1075
|
>>> m = pf.Model()
|
|
1015
1076
|
>>> m.v1 = pf.Variable()
|
|
1016
1077
|
>>> m.v2 = pf.Variable()
|
|
1017
|
-
>>> expr =
|
|
1078
|
+
>>> expr = pf.Param({"dim1": [1, 2, 3], "value": [1, 2, 3]})
|
|
1018
1079
|
>>> expr.degree()
|
|
1019
1080
|
0
|
|
1020
1081
|
>>> expr *= m.v1
|
|
@@ -1032,18 +1093,17 @@ class Expression(SupportsMath):
|
|
|
1032
1093
|
elif (self.data.get_column(VAR_KEY) != CONST_TERM).any():
|
|
1033
1094
|
return "linear" if return_str else 1
|
|
1034
1095
|
else:
|
|
1035
|
-
return "
|
|
1096
|
+
return "parameter" if return_str else 0
|
|
1036
1097
|
|
|
1037
1098
|
def __add__(self, other):
|
|
1038
1099
|
"""Adds another expression or a constant to this expression.
|
|
1039
1100
|
|
|
1040
1101
|
Examples:
|
|
1041
|
-
>>> import pandas as pd
|
|
1042
1102
|
>>> m = pf.Model()
|
|
1043
|
-
>>> add =
|
|
1103
|
+
>>> add = pf.Param({"dim1": [1, 2, 3], "add": [10, 20, 30]})
|
|
1044
1104
|
>>> m.v = Variable(add)
|
|
1045
1105
|
>>> m.v + add
|
|
1046
|
-
<Expression height=3 terms=6
|
|
1106
|
+
<Expression (linear) height=3 terms=6>
|
|
1047
1107
|
┌──────┬────────────┐
|
|
1048
1108
|
│ dim1 ┆ expression │
|
|
1049
1109
|
│ (3) ┆ │
|
|
@@ -1054,7 +1114,7 @@ class Expression(SupportsMath):
|
|
|
1054
1114
|
└──────┴────────────┘
|
|
1055
1115
|
|
|
1056
1116
|
>>> m.v + add + 2
|
|
1057
|
-
<Expression height=3 terms=6
|
|
1117
|
+
<Expression (linear) height=3 terms=6>
|
|
1058
1118
|
┌──────┬────────────┐
|
|
1059
1119
|
│ dim1 ┆ expression │
|
|
1060
1120
|
│ (3) ┆ │
|
|
@@ -1067,31 +1127,29 @@ class Expression(SupportsMath):
|
|
|
1067
1127
|
>>> m.v + pd.DataFrame({"dim1": [1, 2], "add": [10, 20]})
|
|
1068
1128
|
Traceback (most recent call last):
|
|
1069
1129
|
...
|
|
1070
|
-
pyoframe._constants.PyoframeError: Cannot add the two expressions below because
|
|
1071
|
-
Expression 1:
|
|
1072
|
-
Expression 2:
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
│
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
└──────┴────────────┘
|
|
1082
|
-
If this is intentional, use .drop_unmatched() or .keep_unmatched().
|
|
1130
|
+
pyoframe._constants.PyoframeError: Cannot add the two expressions below because expression 1 has extra labels.
|
|
1131
|
+
Expression 1: v
|
|
1132
|
+
Expression 2: add
|
|
1133
|
+
Extra labels in expression 1:
|
|
1134
|
+
┌──────┐
|
|
1135
|
+
│ dim1 │
|
|
1136
|
+
╞══════╡
|
|
1137
|
+
│ 3 │
|
|
1138
|
+
└──────┘
|
|
1139
|
+
Use .drop_extras() or .keep_extras() to indicate how the extra labels should be handled. Learn more at
|
|
1140
|
+
https://bravos-power.github.io/pyoframe/latest/learn/concepts/addition
|
|
1083
1141
|
>>> m.v2 = Variable()
|
|
1084
1142
|
>>> 5 + 2 * m.v2
|
|
1085
|
-
<Expression terms=2
|
|
1143
|
+
<Expression (linear) terms=2>
|
|
1086
1144
|
2 v2 +5
|
|
1087
1145
|
"""
|
|
1088
1146
|
if isinstance(other, (int, float)):
|
|
1089
1147
|
return self._add_const(other)
|
|
1090
|
-
other = other.to_expr()
|
|
1148
|
+
other = other.to_expr() # TODO don't rely on monkey patch
|
|
1091
1149
|
self._learn_from_other(other)
|
|
1092
1150
|
return add(self, other)
|
|
1093
1151
|
|
|
1094
|
-
def __mul__(self: Expression, other:
|
|
1152
|
+
def __mul__(self: Expression, other: Operable) -> Expression:
|
|
1095
1153
|
if isinstance(other, (int, float)):
|
|
1096
1154
|
if other == 1:
|
|
1097
1155
|
return self
|
|
@@ -1100,10 +1158,25 @@ class Expression(SupportsMath):
|
|
|
1100
1158
|
name=f"({other} * {self.name})",
|
|
1101
1159
|
)
|
|
1102
1160
|
|
|
1103
|
-
other = other.to_expr()
|
|
1161
|
+
other: Expression = other.to_expr() # TODO don't rely on monkey patch
|
|
1104
1162
|
self._learn_from_other(other)
|
|
1105
1163
|
return multiply(self, other)
|
|
1106
1164
|
|
|
1165
|
+
def __rtruediv__(self, other):
|
|
1166
|
+
"""Support dividing by an expression when that expression is a constant."""
|
|
1167
|
+
assert isinstance(other, (int, float)), (
|
|
1168
|
+
f"Expected a number not a {type(other)} when dividing by an expression."
|
|
1169
|
+
)
|
|
1170
|
+
if self.degree() != 0:
|
|
1171
|
+
raise PyoframeError(
|
|
1172
|
+
f"Cannot divide by '{self.name}' because denominators cannot contain variables."
|
|
1173
|
+
)
|
|
1174
|
+
|
|
1175
|
+
return self._new(
|
|
1176
|
+
self.data.with_columns((pl.lit(other) / pl.col(COEF_KEY)).alias(COEF_KEY)),
|
|
1177
|
+
name=f"({other} / {self.name})",
|
|
1178
|
+
)
|
|
1179
|
+
|
|
1107
1180
|
def to_expr(self) -> Expression:
|
|
1108
1181
|
"""Returns the expression itself."""
|
|
1109
1182
|
return self
|
|
@@ -1125,13 +1198,13 @@ class Expression(SupportsMath):
|
|
|
1125
1198
|
>>> m.x1 = Variable()
|
|
1126
1199
|
>>> m.x2 = Variable()
|
|
1127
1200
|
>>> m.x1 + 5
|
|
1128
|
-
<Expression terms=2
|
|
1201
|
+
<Expression (linear) terms=2>
|
|
1129
1202
|
x1 +5
|
|
1130
1203
|
>>> m.x1**2 + 5
|
|
1131
|
-
<Expression terms=2
|
|
1204
|
+
<Expression (quadratic) terms=2>
|
|
1132
1205
|
x1 * x1 +5
|
|
1133
1206
|
>>> m.x1**2 + m.x2 + 5
|
|
1134
|
-
<Expression terms=3
|
|
1207
|
+
<Expression (quadratic) terms=3>
|
|
1135
1208
|
x1 * x1 + x2 +5
|
|
1136
1209
|
|
|
1137
1210
|
It also works with dimensions
|
|
@@ -1139,7 +1212,7 @@ class Expression(SupportsMath):
|
|
|
1139
1212
|
>>> m = pf.Model()
|
|
1140
1213
|
>>> m.v = Variable({"dim1": [1, 2, 3]})
|
|
1141
1214
|
>>> m.v * m.v + 5
|
|
1142
|
-
<Expression height=3 terms=6
|
|
1215
|
+
<Expression (quadratic) height=3 terms=6>
|
|
1143
1216
|
┌──────┬─────────────────┐
|
|
1144
1217
|
│ dim1 ┆ expression │
|
|
1145
1218
|
│ (3) ┆ │
|
|
@@ -1158,11 +1231,11 @@ class Expression(SupportsMath):
|
|
|
1158
1231
|
if CONST_TERM not in data[VAR_KEY]:
|
|
1159
1232
|
const_df = pl.DataFrame(
|
|
1160
1233
|
{COEF_KEY: [0.0], VAR_KEY: [CONST_TERM]},
|
|
1161
|
-
schema={COEF_KEY: pl.Float64, VAR_KEY:
|
|
1234
|
+
schema={COEF_KEY: pl.Float64, VAR_KEY: Config.id_dtype},
|
|
1162
1235
|
)
|
|
1163
1236
|
if self.is_quadratic:
|
|
1164
1237
|
const_df = const_df.with_columns(
|
|
1165
|
-
pl.lit(CONST_TERM).alias(QUAD_VAR_KEY).cast(
|
|
1238
|
+
pl.lit(CONST_TERM).alias(QUAD_VAR_KEY).cast(Config.id_dtype)
|
|
1166
1239
|
)
|
|
1167
1240
|
data = pl.concat(
|
|
1168
1241
|
[data, const_df],
|
|
@@ -1172,11 +1245,11 @@ class Expression(SupportsMath):
|
|
|
1172
1245
|
keys = (
|
|
1173
1246
|
data.select(dim)
|
|
1174
1247
|
.unique(maintain_order=Config.maintain_order)
|
|
1175
|
-
.with_columns(pl.lit(CONST_TERM).alias(VAR_KEY).cast(
|
|
1248
|
+
.with_columns(pl.lit(CONST_TERM).alias(VAR_KEY).cast(Config.id_dtype))
|
|
1176
1249
|
)
|
|
1177
1250
|
if self.is_quadratic:
|
|
1178
1251
|
keys = keys.with_columns(
|
|
1179
|
-
pl.lit(CONST_TERM).alias(QUAD_VAR_KEY).cast(
|
|
1252
|
+
pl.lit(CONST_TERM).alias(QUAD_VAR_KEY).cast(Config.id_dtype)
|
|
1180
1253
|
)
|
|
1181
1254
|
data = data.join(
|
|
1182
1255
|
keys,
|
|
@@ -1219,7 +1292,7 @@ class Expression(SupportsMath):
|
|
|
1219
1292
|
if len(constant_terms) == 0:
|
|
1220
1293
|
return pl.DataFrame(
|
|
1221
1294
|
{COEF_KEY: [0.0], VAR_KEY: [CONST_TERM]},
|
|
1222
|
-
schema={COEF_KEY: pl.Float64, VAR_KEY:
|
|
1295
|
+
schema={COEF_KEY: pl.Float64, VAR_KEY: Config.id_dtype},
|
|
1223
1296
|
)
|
|
1224
1297
|
return constant_terms
|
|
1225
1298
|
|
|
@@ -1314,16 +1387,22 @@ class Expression(SupportsMath):
|
|
|
1314
1387
|
"._to_poi() only works for non-dimensioned expressions."
|
|
1315
1388
|
)
|
|
1316
1389
|
|
|
1390
|
+
data = self.data
|
|
1391
|
+
|
|
1317
1392
|
if self.is_quadratic:
|
|
1393
|
+
# Workaround for bug https://github.com/metab0t/PyOptInterface/issues/59
|
|
1394
|
+
if self._model is None or self._model.solver.name == "highs":
|
|
1395
|
+
data = data.sort(VAR_KEY, QUAD_VAR_KEY, descending=False)
|
|
1396
|
+
|
|
1318
1397
|
return poi.ScalarQuadraticFunction(
|
|
1319
|
-
coefficients=
|
|
1320
|
-
var1s=
|
|
1321
|
-
var2s=
|
|
1398
|
+
coefficients=data.get_column(COEF_KEY).to_numpy(),
|
|
1399
|
+
var1s=data.get_column(VAR_KEY).to_numpy(),
|
|
1400
|
+
var2s=data.get_column(QUAD_VAR_KEY).to_numpy(),
|
|
1322
1401
|
)
|
|
1323
1402
|
else:
|
|
1324
1403
|
return poi.ScalarAffineFunction(
|
|
1325
|
-
coefficients=
|
|
1326
|
-
variables=
|
|
1404
|
+
coefficients=data.get_column(COEF_KEY).to_numpy(),
|
|
1405
|
+
variables=data.get_column(VAR_KEY).to_numpy(),
|
|
1327
1406
|
)
|
|
1328
1407
|
|
|
1329
1408
|
@overload
|
|
@@ -1496,9 +1575,9 @@ class Expression(SupportsMath):
|
|
|
1496
1575
|
"""Returns a string representation of the expression's header."""
|
|
1497
1576
|
return get_obj_repr(
|
|
1498
1577
|
self,
|
|
1578
|
+
f"({self.degree(return_str=True)})",
|
|
1499
1579
|
height=len(self) if self.dimensions else None,
|
|
1500
1580
|
terms=self.terms,
|
|
1501
|
-
type=self.degree(return_str=True),
|
|
1502
1581
|
)
|
|
1503
1582
|
|
|
1504
1583
|
def __repr__(self) -> str:
|
|
@@ -1519,7 +1598,7 @@ class Expression(SupportsMath):
|
|
|
1519
1598
|
>>> m.v = pf.Variable({"t": [1, 2]})
|
|
1520
1599
|
>>> coef = pl.DataFrame({"t": [1, 2], "coef": [0, 1]})
|
|
1521
1600
|
>>> coef * (m.v + 4)
|
|
1522
|
-
<Expression height=2 terms=3
|
|
1601
|
+
<Expression (linear) height=2 terms=3>
|
|
1523
1602
|
┌─────┬────────────┐
|
|
1524
1603
|
│ t ┆ expression │
|
|
1525
1604
|
│ (2) ┆ │
|
|
@@ -1534,16 +1613,16 @@ class Expression(SupportsMath):
|
|
|
1534
1613
|
|
|
1535
1614
|
|
|
1536
1615
|
@overload
|
|
1537
|
-
def sum(over: str | Sequence[str], expr:
|
|
1616
|
+
def sum(over: str | Sequence[str], expr: Operable) -> Expression: ...
|
|
1538
1617
|
|
|
1539
1618
|
|
|
1540
1619
|
@overload
|
|
1541
|
-
def sum(over:
|
|
1620
|
+
def sum(over: Operable) -> Expression: ...
|
|
1542
1621
|
|
|
1543
1622
|
|
|
1544
1623
|
def sum(
|
|
1545
|
-
over: str | Sequence[str] |
|
|
1546
|
-
expr:
|
|
1624
|
+
over: str | Sequence[str] | Operable,
|
|
1625
|
+
expr: Operable | None = None,
|
|
1547
1626
|
) -> Expression: # pragma: no cover
|
|
1548
1627
|
"""Deprecated: Use Expression.sum() or Variable.sum() instead.
|
|
1549
1628
|
|
|
@@ -1560,7 +1639,7 @@ def sum(
|
|
|
1560
1639
|
)
|
|
1561
1640
|
|
|
1562
1641
|
if expr is None:
|
|
1563
|
-
assert isinstance(over,
|
|
1642
|
+
assert isinstance(over, BaseOperableBlock)
|
|
1564
1643
|
return over.to_expr().sum()
|
|
1565
1644
|
else:
|
|
1566
1645
|
assert isinstance(over, (str, Sequence))
|
|
@@ -1569,9 +1648,7 @@ def sum(
|
|
|
1569
1648
|
return expr.to_expr().sum(*over)
|
|
1570
1649
|
|
|
1571
1650
|
|
|
1572
|
-
def sum_by(
|
|
1573
|
-
by: str | Sequence[str], expr: SupportsToExpr
|
|
1574
|
-
) -> Expression: # pragma: no cover
|
|
1651
|
+
def sum_by(by: str | Sequence[str], expr: Operable) -> Expression: # pragma: no cover
|
|
1575
1652
|
"""Deprecated: Use Expression.sum() or Variable.sum() instead."""
|
|
1576
1653
|
warnings.warn(
|
|
1577
1654
|
"pf.sum_by() is deprecated. Use Expression.sum_by() or Variable.sum_by() instead.",
|
|
@@ -1583,7 +1660,7 @@ def sum_by(
|
|
|
1583
1660
|
return expr.to_expr().sum_by(*by)
|
|
1584
1661
|
|
|
1585
1662
|
|
|
1586
|
-
class Constraint(
|
|
1663
|
+
class Constraint(BaseBlock):
|
|
1587
1664
|
"""An optimization constraint that can be added to a [Model][pyoframe.Model].
|
|
1588
1665
|
|
|
1589
1666
|
Tip: Implementation Note
|
|
@@ -1699,7 +1776,7 @@ class Constraint(ModelElementWithId):
|
|
|
1699
1776
|
# GRBaddconstr uses sprintf when no name or "" is given. sprintf is slow. As such, we specify "C" as the name.
|
|
1700
1777
|
# Specifying "" is the same as not specifying anything, see pyoptinterface:
|
|
1701
1778
|
# https://github.com/metab0t/PyOptInterface/blob/6d61f3738ad86379cff71fee77077d4ea919f2d5/lib/gurobi_model.cpp#L338
|
|
1702
|
-
name = "C" if self._model.solver.
|
|
1779
|
+
name = "C" if self._model.solver.accelerate_with_repeat_names else ""
|
|
1703
1780
|
|
|
1704
1781
|
if dims is None:
|
|
1705
1782
|
if self._model.solver_uses_variable_names:
|
|
@@ -1709,23 +1786,25 @@ class Constraint(ModelElementWithId):
|
|
|
1709
1786
|
if is_quadratic
|
|
1710
1787
|
else poi.ScalarAffineFunction.from_numpy # when called only once from_numpy is faster
|
|
1711
1788
|
)
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1789
|
+
constr_id = add_constraint(
|
|
1790
|
+
create_expression(
|
|
1791
|
+
*(
|
|
1792
|
+
df.get_column(c).to_numpy()
|
|
1793
|
+
for c in ([COEF_KEY] + self.lhs._variable_columns)
|
|
1794
|
+
)
|
|
1795
|
+
),
|
|
1796
|
+
sense,
|
|
1797
|
+
0,
|
|
1798
|
+
name,
|
|
1799
|
+
).index
|
|
1800
|
+
try:
|
|
1801
|
+
df = self.data.with_columns(
|
|
1802
|
+
pl.lit(constr_id).alias(CONSTRAINT_KEY).cast(Config.id_dtype)
|
|
1725
1803
|
)
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1804
|
+
except TypeError as e:
|
|
1805
|
+
raise TypeError(
|
|
1806
|
+
f"Number of constraints exceeds the current data type ({Config.id_dtype}). Consider increasing the data type by changing Config.id_dtype."
|
|
1807
|
+
) from e
|
|
1729
1808
|
else:
|
|
1730
1809
|
create_expression = (
|
|
1731
1810
|
poi.ScalarQuadraticFunction
|
|
@@ -1814,9 +1893,14 @@ class Constraint(ModelElementWithId):
|
|
|
1814
1893
|
).index
|
|
1815
1894
|
for s0, s1 in pairwise(split)
|
|
1816
1895
|
]
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1896
|
+
try:
|
|
1897
|
+
df = df_unique.with_columns(
|
|
1898
|
+
pl.Series(ids, dtype=Config.id_dtype).alias(CONSTRAINT_KEY)
|
|
1899
|
+
)
|
|
1900
|
+
except TypeError as e:
|
|
1901
|
+
raise TypeError(
|
|
1902
|
+
f"Number of constraints exceeds the current data type ({Config.id_dtype}). Consider increasing the data type by changing Config.id_dtype."
|
|
1903
|
+
) from e
|
|
1820
1904
|
|
|
1821
1905
|
self._data = df
|
|
1822
1906
|
|
|
@@ -1866,9 +1950,7 @@ class Constraint(ModelElementWithId):
|
|
|
1866
1950
|
"""Syntactic sugar on `Constraint.lhs.data.filter()`, to help debugging."""
|
|
1867
1951
|
return self.lhs.data.filter(*args, **kwargs)
|
|
1868
1952
|
|
|
1869
|
-
def relax(
|
|
1870
|
-
self, cost: SupportsToExpr, max: SupportsToExpr | None = None
|
|
1871
|
-
) -> Constraint:
|
|
1953
|
+
def relax(self, cost: Operable, max: Operable | None = None) -> Constraint:
|
|
1872
1954
|
"""Allows the constraint to be violated at a `cost` and, optionally, up to a maximum.
|
|
1873
1955
|
|
|
1874
1956
|
Warning:
|
|
@@ -2124,31 +2206,35 @@ class Constraint(ModelElementWithId):
|
|
|
2124
2206
|
return (
|
|
2125
2207
|
get_obj_repr(
|
|
2126
2208
|
self,
|
|
2127
|
-
self.name,
|
|
2209
|
+
f"'{self.name}'",
|
|
2210
|
+
f"({self.lhs.degree(return_str=True)})",
|
|
2128
2211
|
height=len(self) if self.dimensions else None,
|
|
2129
2212
|
terms=len(self.lhs.data),
|
|
2130
|
-
type=self.lhs.degree(return_str=True),
|
|
2131
2213
|
)
|
|
2132
2214
|
+ "\n"
|
|
2133
2215
|
+ self.to_str()
|
|
2134
2216
|
)
|
|
2135
2217
|
|
|
2136
2218
|
|
|
2137
|
-
class Variable(
|
|
2219
|
+
class Variable(BaseOperableBlock):
|
|
2138
2220
|
"""A decision variable for an optimization model.
|
|
2139
2221
|
|
|
2222
|
+
!!! tip
|
|
2223
|
+
If `lb` or `ub` are a dimensioned object (e.g. an [Expression][pyoframe.Expression]), they will automatically be [broadcasted](../../learn/concepts/addition.md#adding-expressions-with-differing-dimensions-using-over) to match the variable's dimensions.
|
|
2224
|
+
|
|
2140
2225
|
Parameters:
|
|
2141
2226
|
*indexing_sets:
|
|
2142
2227
|
If no indexing_sets are provided, a single variable with no dimensions is created.
|
|
2143
2228
|
Otherwise, a variable is created for each element in the Cartesian product of the indexing_sets (see Set for details on behaviour).
|
|
2144
|
-
lb:
|
|
2145
|
-
The lower bound for all variables.
|
|
2146
|
-
ub:
|
|
2147
|
-
The upper bound for all variables.
|
|
2148
2229
|
vtype:
|
|
2149
2230
|
The type of the variable. Can be either a VType enum or a string. Default is VType.CONTINUOUS.
|
|
2231
|
+
lb:
|
|
2232
|
+
The lower bound for the variables.
|
|
2233
|
+
ub:
|
|
2234
|
+
The upper bound for the variables.
|
|
2150
2235
|
equals:
|
|
2151
|
-
When specified, a variable is created and a constraint is added to make the variable equal to the provided expression.
|
|
2236
|
+
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.
|
|
2237
|
+
`indexing_sets` cannot be provided when using `equals`.
|
|
2152
2238
|
|
|
2153
2239
|
Examples:
|
|
2154
2240
|
>>> import pandas as pd
|
|
@@ -2216,29 +2302,44 @@ class Variable(ModelElementWithId, SupportsMath):
|
|
|
2216
2302
|
def __init__(
|
|
2217
2303
|
self,
|
|
2218
2304
|
*indexing_sets: SetTypes | Iterable[SetTypes],
|
|
2219
|
-
lb:
|
|
2220
|
-
ub:
|
|
2305
|
+
lb: Operable | None = None,
|
|
2306
|
+
ub: Operable | None = None,
|
|
2221
2307
|
vtype: VType | VTypeValue = VType.CONTINUOUS,
|
|
2222
|
-
equals:
|
|
2308
|
+
equals: Operable | None = None,
|
|
2223
2309
|
):
|
|
2224
2310
|
if equals is not None:
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2311
|
+
if isinstance(equals, (float, int)):
|
|
2312
|
+
if lb is not None:
|
|
2313
|
+
raise ValueError("Cannot specify 'lb' when 'equals' is a constant.")
|
|
2314
|
+
if ub is not None:
|
|
2315
|
+
raise ValueError("Cannot specify 'ub' when 'equals' is a constant.")
|
|
2316
|
+
lb = ub = equals
|
|
2317
|
+
equals = None
|
|
2318
|
+
else:
|
|
2319
|
+
assert len(indexing_sets) == 0, (
|
|
2320
|
+
"Cannot specify both 'equals' and 'indexing_sets'"
|
|
2321
|
+
)
|
|
2322
|
+
equals = equals.to_expr() # TODO don't rely on monkey patch
|
|
2323
|
+
indexing_sets = (equals,)
|
|
2229
2324
|
|
|
2230
2325
|
data = Set(*indexing_sets).data if len(indexing_sets) > 0 else pl.DataFrame()
|
|
2231
2326
|
super().__init__(data)
|
|
2232
2327
|
|
|
2233
2328
|
self.vtype: VType = VType(vtype)
|
|
2234
2329
|
self._attr = Container(self._set_attribute, self._get_attribute)
|
|
2235
|
-
self._equals = equals
|
|
2330
|
+
self._equals: Expression | None = equals
|
|
2236
2331
|
|
|
2237
2332
|
if lb is not None and not isinstance(lb, (float, int)):
|
|
2333
|
+
lb: Expression = lb.to_expr() # TODO don't rely on monkey patch
|
|
2334
|
+
if not self.dimensionless:
|
|
2335
|
+
lb = lb.over(*self.dimensions)
|
|
2238
2336
|
self._lb_expr, self.lb = lb, None
|
|
2239
2337
|
else:
|
|
2240
2338
|
self._lb_expr, self.lb = None, lb
|
|
2241
2339
|
if ub is not None and not isinstance(ub, (float, int)):
|
|
2340
|
+
ub = ub.to_expr() # TODO don't rely on monkey patch
|
|
2341
|
+
if not self.dimensionless:
|
|
2342
|
+
ub = ub.over(*self.dimensions) # pyright: ignore[reportOptionalIterable]
|
|
2242
2343
|
self._ub_expr, self.ub = ub, None
|
|
2243
2344
|
else:
|
|
2244
2345
|
self._ub_expr, self.ub = None, ub
|
|
@@ -2319,7 +2420,7 @@ class Variable(ModelElementWithId, SupportsMath):
|
|
|
2319
2420
|
else:
|
|
2320
2421
|
if self._model.solver_uses_variable_names:
|
|
2321
2422
|
name = self.name
|
|
2322
|
-
elif solver.
|
|
2423
|
+
elif solver.accelerate_with_repeat_names:
|
|
2323
2424
|
name = "V"
|
|
2324
2425
|
else:
|
|
2325
2426
|
name = ""
|
|
@@ -2331,7 +2432,14 @@ class Variable(ModelElementWithId, SupportsMath):
|
|
|
2331
2432
|
else:
|
|
2332
2433
|
ids = [poi_add_var(lb, ub, name=name).index for _ in range(n)]
|
|
2333
2434
|
|
|
2334
|
-
|
|
2435
|
+
try:
|
|
2436
|
+
df = self.data.with_columns(
|
|
2437
|
+
pl.Series(ids, dtype=Config.id_dtype).alias(VAR_KEY)
|
|
2438
|
+
)
|
|
2439
|
+
except TypeError as e:
|
|
2440
|
+
raise TypeError(
|
|
2441
|
+
f"Number of variables exceeds the current data type ({Config.id_dtype}). Consider increasing the data type by changing Config.id_dtype."
|
|
2442
|
+
) from e
|
|
2335
2443
|
|
|
2336
2444
|
self._data = df
|
|
2337
2445
|
|
|
@@ -2437,7 +2545,7 @@ class Variable(ModelElementWithId, SupportsMath):
|
|
|
2437
2545
|
result = (
|
|
2438
2546
|
get_obj_repr(
|
|
2439
2547
|
self,
|
|
2440
|
-
self.name,
|
|
2548
|
+
f"'{self.name}'",
|
|
2441
2549
|
lb=self.lb,
|
|
2442
2550
|
ub=self.ub,
|
|
2443
2551
|
height=self.data.height if self.dimensions else None,
|
|
@@ -2467,13 +2575,13 @@ class Variable(ModelElementWithId, SupportsMath):
|
|
|
2467
2575
|
|
|
2468
2576
|
@return_new
|
|
2469
2577
|
def next(self, dim: str, wrap_around: bool = False):
|
|
2470
|
-
"""Creates an expression where the variable at each
|
|
2578
|
+
"""Creates an expression where the variable at each label is the next variable in the specified dimension.
|
|
2471
2579
|
|
|
2472
2580
|
Parameters:
|
|
2473
2581
|
dim:
|
|
2474
2582
|
The dimension over which to shift the variable.
|
|
2475
2583
|
wrap_around:
|
|
2476
|
-
If `True`, the last
|
|
2584
|
+
If `True`, the last label in the dimension is connected to the first label.
|
|
2477
2585
|
|
|
2478
2586
|
Examples:
|
|
2479
2587
|
>>> import pandas as pd
|
|
@@ -2486,25 +2594,21 @@ class Variable(ModelElementWithId, SupportsMath):
|
|
|
2486
2594
|
>>> m.bat_charge + m.bat_flow == m.bat_charge.next("time")
|
|
2487
2595
|
Traceback (most recent call last):
|
|
2488
2596
|
...
|
|
2489
|
-
pyoframe._constants.PyoframeError: Cannot subtract the two expressions below because
|
|
2490
|
-
Expression 1:
|
|
2491
|
-
Expression 2:
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
│
|
|
2497
|
-
│
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
>>> (m.bat_charge + m.bat_flow).drop_unmatched() == m.bat_charge.next(
|
|
2505
|
-
... "time"
|
|
2506
|
-
... )
|
|
2507
|
-
<Constraint 'unnamed' height=6 terms=18 type=linear>
|
|
2597
|
+
pyoframe._constants.PyoframeError: Cannot subtract the two expressions below because expression 1 has extra labels.
|
|
2598
|
+
Expression 1: (bat_charge + bat_flow)
|
|
2599
|
+
Expression 2: bat_charge.next(…)
|
|
2600
|
+
Extra labels in expression 1:
|
|
2601
|
+
┌───────┬─────────┐
|
|
2602
|
+
│ time ┆ city │
|
|
2603
|
+
╞═══════╪═════════╡
|
|
2604
|
+
│ 18:00 ┆ Toronto │
|
|
2605
|
+
│ 18:00 ┆ Berlin │
|
|
2606
|
+
└───────┴─────────┘
|
|
2607
|
+
Use .drop_extras() or .keep_extras() to indicate how the extra labels should be handled. Learn more at
|
|
2608
|
+
https://bravos-power.github.io/pyoframe/latest/learn/concepts/addition
|
|
2609
|
+
|
|
2610
|
+
>>> (m.bat_charge + m.bat_flow).drop_extras() == m.bat_charge.next("time")
|
|
2611
|
+
<Constraint 'unnamed' (linear) height=6 terms=18>
|
|
2508
2612
|
┌───────┬─────────┬────────────────────────────────────────────────────────────────────────────────┐
|
|
2509
2613
|
│ time ┆ city ┆ constraint │
|
|
2510
2614
|
│ (3) ┆ (2) ┆ │
|
|
@@ -2526,7 +2630,7 @@ class Variable(ModelElementWithId, SupportsMath):
|
|
|
2526
2630
|
>>> (m.bat_charge + m.bat_flow) == m.bat_charge.next(
|
|
2527
2631
|
... "time", wrap_around=True
|
|
2528
2632
|
... )
|
|
2529
|
-
<Constraint 'unnamed' height=8 terms=24
|
|
2633
|
+
<Constraint 'unnamed' (linear) height=8 terms=24>
|
|
2530
2634
|
┌───────┬─────────┬────────────────────────────────────────────────────────────────────────────────┐
|
|
2531
2635
|
│ time ┆ city ┆ constraint │
|
|
2532
2636
|
│ (4) ┆ (2) ┆ │
|