pyoframe 1.0.0a0__py3-none-any.whl → 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyoframe/_arithmetic.py +176 -174
- pyoframe/_constants.py +94 -47
- pyoframe/_core.py +225 -148
- pyoframe/_model.py +46 -26
- pyoframe/_model_element.py +32 -18
- pyoframe/_monkey_patch.py +15 -13
- pyoframe/_objective.py +2 -4
- pyoframe/_utils.py +8 -9
- pyoframe/_version.py +2 -2
- {pyoframe-1.0.0a0.dist-info → pyoframe-1.0.1.dist-info}/METADATA +11 -12
- pyoframe-1.0.1.dist-info/RECORD +15 -0
- pyoframe-1.0.0a0.dist-info/RECORD +0 -15
- {pyoframe-1.0.0a0.dist-info → pyoframe-1.0.1.dist-info}/WHEEL +0 -0
- {pyoframe-1.0.0a0.dist-info → pyoframe-1.0.1.dist-info}/licenses/LICENSE +0 -0
- {pyoframe-1.0.0a0.dist-info → pyoframe-1.0.1.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()")
|
|
96
|
+
new._copy_flags(self)
|
|
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()")
|
|
91
127
|
new._copy_flags(self)
|
|
92
|
-
new.
|
|
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()
|
|
@@ -344,39 +380,22 @@ class SupportsMath(ModelElement, SupportsToExpr):
|
|
|
344
380
|
return other + (-self.to_expr())
|
|
345
381
|
|
|
346
382
|
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
383
|
return Constraint(self - other, ConstraintSense.LE)
|
|
357
384
|
|
|
358
|
-
def
|
|
359
|
-
|
|
385
|
+
def __lt__(self, _):
|
|
386
|
+
raise PyoframeError(
|
|
387
|
+
"Constraints cannot be created with the '<' or '>' operators. Did you mean to use '<=' or '>=' instead?"
|
|
388
|
+
)
|
|
360
389
|
|
|
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
|
-
"""
|
|
390
|
+
def __ge__(self, other):
|
|
368
391
|
return Constraint(self - other, ConstraintSense.GE)
|
|
369
392
|
|
|
370
|
-
def
|
|
371
|
-
|
|
393
|
+
def __gt__(self, _):
|
|
394
|
+
raise PyoframeError(
|
|
395
|
+
"Constraints cannot be created with the '<' or '>' operator. Did you mean to use '<=' or '>=' instead?"
|
|
396
|
+
)
|
|
372
397
|
|
|
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
|
-
"""
|
|
398
|
+
def __eq__(self, value: object): # type: ignore
|
|
380
399
|
return Constraint(self - value, ConstraintSense.EQ)
|
|
381
400
|
|
|
382
401
|
|
|
@@ -384,14 +403,14 @@ SetTypes = Union[
|
|
|
384
403
|
pl.DataFrame,
|
|
385
404
|
pd.Index,
|
|
386
405
|
pd.DataFrame,
|
|
387
|
-
|
|
406
|
+
BaseOperableBlock,
|
|
388
407
|
Mapping[str, Sequence[object]],
|
|
389
408
|
"Set",
|
|
390
409
|
"Constraint",
|
|
391
410
|
]
|
|
392
411
|
|
|
393
412
|
|
|
394
|
-
class Set(
|
|
413
|
+
class Set(BaseOperableBlock):
|
|
395
414
|
"""A set which can then be used to index variables.
|
|
396
415
|
|
|
397
416
|
Examples:
|
|
@@ -482,6 +501,45 @@ class Set(SupportsMath):
|
|
|
482
501
|
name=self.name,
|
|
483
502
|
)
|
|
484
503
|
|
|
504
|
+
def drop(self, *dims: str) -> Set:
|
|
505
|
+
"""Returns a new Set with the given dimensions dropped.
|
|
506
|
+
|
|
507
|
+
Only unique rows are kept in the resulting Set.
|
|
508
|
+
|
|
509
|
+
Examples:
|
|
510
|
+
>>> xy = pf.Set(x=range(3), y=range(2))
|
|
511
|
+
>>> xy
|
|
512
|
+
<Set 'unnamed' height=6>
|
|
513
|
+
┌─────┬─────┐
|
|
514
|
+
│ x ┆ y │
|
|
515
|
+
│ (3) ┆ (2) │
|
|
516
|
+
╞═════╪═════╡
|
|
517
|
+
│ 0 ┆ 0 │
|
|
518
|
+
│ 0 ┆ 1 │
|
|
519
|
+
│ 1 ┆ 0 │
|
|
520
|
+
│ 1 ┆ 1 │
|
|
521
|
+
│ 2 ┆ 0 │
|
|
522
|
+
│ 2 ┆ 1 │
|
|
523
|
+
└─────┴─────┘
|
|
524
|
+
>>> x = xy.drop("y")
|
|
525
|
+
>>> x
|
|
526
|
+
<Set 'unnamed_set.drop(…)' height=3>
|
|
527
|
+
┌─────┐
|
|
528
|
+
│ x │
|
|
529
|
+
│ (3) │
|
|
530
|
+
╞═════╡
|
|
531
|
+
│ 0 │
|
|
532
|
+
│ 1 │
|
|
533
|
+
│ 2 │
|
|
534
|
+
└─────┘
|
|
535
|
+
"""
|
|
536
|
+
if not dims:
|
|
537
|
+
raise ValueError("At least one dimension must be provided to drop.")
|
|
538
|
+
return self._new(
|
|
539
|
+
self.data.drop(dims).unique(maintain_order=Config.maintain_order),
|
|
540
|
+
name=f"{self.name}.drop(…)",
|
|
541
|
+
)
|
|
542
|
+
|
|
485
543
|
def __mul__(self, other):
|
|
486
544
|
if isinstance(other, Set):
|
|
487
545
|
overlap_dims = set(self.data.columns) & set(other.data.columns)
|
|
@@ -529,7 +587,7 @@ class Set(SupportsMath):
|
|
|
529
587
|
df = pl.DataFrame(set)
|
|
530
588
|
elif isinstance(set, Constraint):
|
|
531
589
|
df = set.data.select(set._dimensions_unsafe)
|
|
532
|
-
elif isinstance(set,
|
|
590
|
+
elif isinstance(set, BaseOperableBlock):
|
|
533
591
|
df = (
|
|
534
592
|
set.to_expr()
|
|
535
593
|
.data.drop(RESERVED_COL_KEYS, strict=False)
|
|
@@ -570,7 +628,7 @@ class Set(SupportsMath):
|
|
|
570
628
|
return df
|
|
571
629
|
|
|
572
630
|
|
|
573
|
-
class Expression(
|
|
631
|
+
class Expression(BaseOperableBlock):
|
|
574
632
|
"""Represents a linear or quadratic mathematical expression.
|
|
575
633
|
|
|
576
634
|
Examples:
|
|
@@ -605,14 +663,14 @@ class Expression(SupportsMath):
|
|
|
605
663
|
assert VAR_KEY in data.columns, "Missing variable column."
|
|
606
664
|
assert COEF_KEY in data.columns, "Missing coefficient column."
|
|
607
665
|
|
|
608
|
-
# Sanity check no duplicates
|
|
666
|
+
# Sanity check no duplicates labels
|
|
609
667
|
if Config.enable_is_duplicated_expression_safety_check:
|
|
610
668
|
duplicated_mask = data.drop(COEF_KEY).is_duplicated()
|
|
611
669
|
# In theory this should never happen unless there's a bug in the library
|
|
612
670
|
if duplicated_mask.any():
|
|
613
671
|
duplicated_data = data.filter(duplicated_mask)
|
|
614
672
|
raise ValueError(
|
|
615
|
-
f"Cannot create an expression with duplicate
|
|
673
|
+
f"Cannot create an expression with duplicate labels:\n{duplicated_data}."
|
|
616
674
|
)
|
|
617
675
|
|
|
618
676
|
data = _simplify_expr_df(data)
|
|
@@ -642,7 +700,7 @@ class Expression(SupportsMath):
|
|
|
642
700
|
COEF_KEY: [constant],
|
|
643
701
|
VAR_KEY: [CONST_TERM],
|
|
644
702
|
},
|
|
645
|
-
schema={COEF_KEY: pl.Float64, VAR_KEY:
|
|
703
|
+
schema={COEF_KEY: pl.Float64, VAR_KEY: Config.id_dtype},
|
|
646
704
|
),
|
|
647
705
|
name=str(constant),
|
|
648
706
|
)
|
|
@@ -1067,19 +1125,17 @@ class Expression(SupportsMath):
|
|
|
1067
1125
|
>>> m.v + pd.DataFrame({"dim1": [1, 2], "add": [10, 20]})
|
|
1068
1126
|
Traceback (most recent call last):
|
|
1069
1127
|
...
|
|
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().
|
|
1128
|
+
pyoframe._constants.PyoframeError: Cannot add the two expressions below because expression 1 has extra labels.
|
|
1129
|
+
Expression 1: v
|
|
1130
|
+
Expression 2: add
|
|
1131
|
+
Extra labels in expression 1:
|
|
1132
|
+
┌──────┐
|
|
1133
|
+
│ dim1 │
|
|
1134
|
+
╞══════╡
|
|
1135
|
+
│ 3 │
|
|
1136
|
+
└──────┘
|
|
1137
|
+
Use .drop_extras() or .keep_extras() to indicate how the extra labels should be handled. Learn more at
|
|
1138
|
+
https://bravos-power.github.io/pyoframe/latest/learn/concepts/addition
|
|
1083
1139
|
>>> m.v2 = Variable()
|
|
1084
1140
|
>>> 5 + 2 * m.v2
|
|
1085
1141
|
<Expression terms=2 type=linear>
|
|
@@ -1091,7 +1147,7 @@ class Expression(SupportsMath):
|
|
|
1091
1147
|
self._learn_from_other(other)
|
|
1092
1148
|
return add(self, other)
|
|
1093
1149
|
|
|
1094
|
-
def __mul__(self: Expression, other:
|
|
1150
|
+
def __mul__(self: Expression, other: Operable) -> Expression:
|
|
1095
1151
|
if isinstance(other, (int, float)):
|
|
1096
1152
|
if other == 1:
|
|
1097
1153
|
return self
|
|
@@ -1158,11 +1214,11 @@ class Expression(SupportsMath):
|
|
|
1158
1214
|
if CONST_TERM not in data[VAR_KEY]:
|
|
1159
1215
|
const_df = pl.DataFrame(
|
|
1160
1216
|
{COEF_KEY: [0.0], VAR_KEY: [CONST_TERM]},
|
|
1161
|
-
schema={COEF_KEY: pl.Float64, VAR_KEY:
|
|
1217
|
+
schema={COEF_KEY: pl.Float64, VAR_KEY: Config.id_dtype},
|
|
1162
1218
|
)
|
|
1163
1219
|
if self.is_quadratic:
|
|
1164
1220
|
const_df = const_df.with_columns(
|
|
1165
|
-
pl.lit(CONST_TERM).alias(QUAD_VAR_KEY).cast(
|
|
1221
|
+
pl.lit(CONST_TERM).alias(QUAD_VAR_KEY).cast(Config.id_dtype)
|
|
1166
1222
|
)
|
|
1167
1223
|
data = pl.concat(
|
|
1168
1224
|
[data, const_df],
|
|
@@ -1172,11 +1228,11 @@ class Expression(SupportsMath):
|
|
|
1172
1228
|
keys = (
|
|
1173
1229
|
data.select(dim)
|
|
1174
1230
|
.unique(maintain_order=Config.maintain_order)
|
|
1175
|
-
.with_columns(pl.lit(CONST_TERM).alias(VAR_KEY).cast(
|
|
1231
|
+
.with_columns(pl.lit(CONST_TERM).alias(VAR_KEY).cast(Config.id_dtype))
|
|
1176
1232
|
)
|
|
1177
1233
|
if self.is_quadratic:
|
|
1178
1234
|
keys = keys.with_columns(
|
|
1179
|
-
pl.lit(CONST_TERM).alias(QUAD_VAR_KEY).cast(
|
|
1235
|
+
pl.lit(CONST_TERM).alias(QUAD_VAR_KEY).cast(Config.id_dtype)
|
|
1180
1236
|
)
|
|
1181
1237
|
data = data.join(
|
|
1182
1238
|
keys,
|
|
@@ -1219,7 +1275,7 @@ class Expression(SupportsMath):
|
|
|
1219
1275
|
if len(constant_terms) == 0:
|
|
1220
1276
|
return pl.DataFrame(
|
|
1221
1277
|
{COEF_KEY: [0.0], VAR_KEY: [CONST_TERM]},
|
|
1222
|
-
schema={COEF_KEY: pl.Float64, VAR_KEY:
|
|
1278
|
+
schema={COEF_KEY: pl.Float64, VAR_KEY: Config.id_dtype},
|
|
1223
1279
|
)
|
|
1224
1280
|
return constant_terms
|
|
1225
1281
|
|
|
@@ -1314,16 +1370,22 @@ class Expression(SupportsMath):
|
|
|
1314
1370
|
"._to_poi() only works for non-dimensioned expressions."
|
|
1315
1371
|
)
|
|
1316
1372
|
|
|
1373
|
+
data = self.data
|
|
1374
|
+
|
|
1317
1375
|
if self.is_quadratic:
|
|
1376
|
+
# Workaround for bug https://github.com/metab0t/PyOptInterface/issues/59
|
|
1377
|
+
if self._model is None or self._model.solver.name == "highs":
|
|
1378
|
+
data = data.sort(VAR_KEY, QUAD_VAR_KEY, descending=False)
|
|
1379
|
+
|
|
1318
1380
|
return poi.ScalarQuadraticFunction(
|
|
1319
|
-
coefficients=
|
|
1320
|
-
var1s=
|
|
1321
|
-
var2s=
|
|
1381
|
+
coefficients=data.get_column(COEF_KEY).to_numpy(),
|
|
1382
|
+
var1s=data.get_column(VAR_KEY).to_numpy(),
|
|
1383
|
+
var2s=data.get_column(QUAD_VAR_KEY).to_numpy(),
|
|
1322
1384
|
)
|
|
1323
1385
|
else:
|
|
1324
1386
|
return poi.ScalarAffineFunction(
|
|
1325
|
-
coefficients=
|
|
1326
|
-
variables=
|
|
1387
|
+
coefficients=data.get_column(COEF_KEY).to_numpy(),
|
|
1388
|
+
variables=data.get_column(VAR_KEY).to_numpy(),
|
|
1327
1389
|
)
|
|
1328
1390
|
|
|
1329
1391
|
@overload
|
|
@@ -1534,16 +1596,16 @@ class Expression(SupportsMath):
|
|
|
1534
1596
|
|
|
1535
1597
|
|
|
1536
1598
|
@overload
|
|
1537
|
-
def sum(over: str | Sequence[str], expr:
|
|
1599
|
+
def sum(over: str | Sequence[str], expr: Operable) -> Expression: ...
|
|
1538
1600
|
|
|
1539
1601
|
|
|
1540
1602
|
@overload
|
|
1541
|
-
def sum(over:
|
|
1603
|
+
def sum(over: Operable) -> Expression: ...
|
|
1542
1604
|
|
|
1543
1605
|
|
|
1544
1606
|
def sum(
|
|
1545
|
-
over: str | Sequence[str] |
|
|
1546
|
-
expr:
|
|
1607
|
+
over: str | Sequence[str] | Operable,
|
|
1608
|
+
expr: Operable | None = None,
|
|
1547
1609
|
) -> Expression: # pragma: no cover
|
|
1548
1610
|
"""Deprecated: Use Expression.sum() or Variable.sum() instead.
|
|
1549
1611
|
|
|
@@ -1560,7 +1622,7 @@ def sum(
|
|
|
1560
1622
|
)
|
|
1561
1623
|
|
|
1562
1624
|
if expr is None:
|
|
1563
|
-
assert isinstance(over,
|
|
1625
|
+
assert isinstance(over, BaseOperableBlock)
|
|
1564
1626
|
return over.to_expr().sum()
|
|
1565
1627
|
else:
|
|
1566
1628
|
assert isinstance(over, (str, Sequence))
|
|
@@ -1569,9 +1631,7 @@ def sum(
|
|
|
1569
1631
|
return expr.to_expr().sum(*over)
|
|
1570
1632
|
|
|
1571
1633
|
|
|
1572
|
-
def sum_by(
|
|
1573
|
-
by: str | Sequence[str], expr: SupportsToExpr
|
|
1574
|
-
) -> Expression: # pragma: no cover
|
|
1634
|
+
def sum_by(by: str | Sequence[str], expr: Operable) -> Expression: # pragma: no cover
|
|
1575
1635
|
"""Deprecated: Use Expression.sum() or Variable.sum() instead."""
|
|
1576
1636
|
warnings.warn(
|
|
1577
1637
|
"pf.sum_by() is deprecated. Use Expression.sum_by() or Variable.sum_by() instead.",
|
|
@@ -1583,7 +1643,7 @@ def sum_by(
|
|
|
1583
1643
|
return expr.to_expr().sum_by(*by)
|
|
1584
1644
|
|
|
1585
1645
|
|
|
1586
|
-
class Constraint(
|
|
1646
|
+
class Constraint(BaseBlock):
|
|
1587
1647
|
"""An optimization constraint that can be added to a [Model][pyoframe.Model].
|
|
1588
1648
|
|
|
1589
1649
|
Tip: Implementation Note
|
|
@@ -1699,7 +1759,7 @@ class Constraint(ModelElementWithId):
|
|
|
1699
1759
|
# GRBaddconstr uses sprintf when no name or "" is given. sprintf is slow. As such, we specify "C" as the name.
|
|
1700
1760
|
# Specifying "" is the same as not specifying anything, see pyoptinterface:
|
|
1701
1761
|
# https://github.com/metab0t/PyOptInterface/blob/6d61f3738ad86379cff71fee77077d4ea919f2d5/lib/gurobi_model.cpp#L338
|
|
1702
|
-
name = "C" if self._model.solver.
|
|
1762
|
+
name = "C" if self._model.solver.accelerate_with_repeat_names else ""
|
|
1703
1763
|
|
|
1704
1764
|
if dims is None:
|
|
1705
1765
|
if self._model.solver_uses_variable_names:
|
|
@@ -1709,23 +1769,25 @@ class Constraint(ModelElementWithId):
|
|
|
1709
1769
|
if is_quadratic
|
|
1710
1770
|
else poi.ScalarAffineFunction.from_numpy # when called only once from_numpy is faster
|
|
1711
1771
|
)
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1772
|
+
constr_id = add_constraint(
|
|
1773
|
+
create_expression(
|
|
1774
|
+
*(
|
|
1775
|
+
df.get_column(c).to_numpy()
|
|
1776
|
+
for c in ([COEF_KEY] + self.lhs._variable_columns)
|
|
1777
|
+
)
|
|
1778
|
+
),
|
|
1779
|
+
sense,
|
|
1780
|
+
0,
|
|
1781
|
+
name,
|
|
1782
|
+
).index
|
|
1783
|
+
try:
|
|
1784
|
+
df = self.data.with_columns(
|
|
1785
|
+
pl.lit(constr_id).alias(CONSTRAINT_KEY).cast(Config.id_dtype)
|
|
1725
1786
|
)
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1787
|
+
except TypeError as e:
|
|
1788
|
+
raise TypeError(
|
|
1789
|
+
f"Number of constraints exceeds the current data type ({Config.id_dtype}). Consider increasing the data type by changing Config.id_dtype."
|
|
1790
|
+
) from e
|
|
1729
1791
|
else:
|
|
1730
1792
|
create_expression = (
|
|
1731
1793
|
poi.ScalarQuadraticFunction
|
|
@@ -1814,9 +1876,14 @@ class Constraint(ModelElementWithId):
|
|
|
1814
1876
|
).index
|
|
1815
1877
|
for s0, s1 in pairwise(split)
|
|
1816
1878
|
]
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1879
|
+
try:
|
|
1880
|
+
df = df_unique.with_columns(
|
|
1881
|
+
pl.Series(ids, dtype=Config.id_dtype).alias(CONSTRAINT_KEY)
|
|
1882
|
+
)
|
|
1883
|
+
except TypeError as e:
|
|
1884
|
+
raise TypeError(
|
|
1885
|
+
f"Number of constraints exceeds the current data type ({Config.id_dtype}). Consider increasing the data type by changing Config.id_dtype."
|
|
1886
|
+
) from e
|
|
1820
1887
|
|
|
1821
1888
|
self._data = df
|
|
1822
1889
|
|
|
@@ -1866,9 +1933,7 @@ class Constraint(ModelElementWithId):
|
|
|
1866
1933
|
"""Syntactic sugar on `Constraint.lhs.data.filter()`, to help debugging."""
|
|
1867
1934
|
return self.lhs.data.filter(*args, **kwargs)
|
|
1868
1935
|
|
|
1869
|
-
def relax(
|
|
1870
|
-
self, cost: SupportsToExpr, max: SupportsToExpr | None = None
|
|
1871
|
-
) -> Constraint:
|
|
1936
|
+
def relax(self, cost: Operable, max: Operable | None = None) -> Constraint:
|
|
1872
1937
|
"""Allows the constraint to be violated at a `cost` and, optionally, up to a maximum.
|
|
1873
1938
|
|
|
1874
1939
|
Warning:
|
|
@@ -2134,7 +2199,7 @@ class Constraint(ModelElementWithId):
|
|
|
2134
2199
|
)
|
|
2135
2200
|
|
|
2136
2201
|
|
|
2137
|
-
class Variable(
|
|
2202
|
+
class Variable(BaseOperableBlock):
|
|
2138
2203
|
"""A decision variable for an optimization model.
|
|
2139
2204
|
|
|
2140
2205
|
Parameters:
|
|
@@ -2216,23 +2281,32 @@ class Variable(ModelElementWithId, SupportsMath):
|
|
|
2216
2281
|
def __init__(
|
|
2217
2282
|
self,
|
|
2218
2283
|
*indexing_sets: SetTypes | Iterable[SetTypes],
|
|
2219
|
-
lb:
|
|
2220
|
-
ub:
|
|
2284
|
+
lb: Operable | None = None,
|
|
2285
|
+
ub: Operable | None = None,
|
|
2221
2286
|
vtype: VType | VTypeValue = VType.CONTINUOUS,
|
|
2222
|
-
equals:
|
|
2287
|
+
equals: Operable | None = None,
|
|
2223
2288
|
):
|
|
2224
2289
|
if equals is not None:
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2290
|
+
if isinstance(equals, (float, int)):
|
|
2291
|
+
if lb is not None:
|
|
2292
|
+
raise ValueError("Cannot specify 'lb' when 'equals' is a constant.")
|
|
2293
|
+
if ub is not None:
|
|
2294
|
+
raise ValueError("Cannot specify 'ub' when 'equals' is a constant.")
|
|
2295
|
+
lb = ub = equals
|
|
2296
|
+
equals = None
|
|
2297
|
+
else:
|
|
2298
|
+
assert len(indexing_sets) == 0, (
|
|
2299
|
+
"Cannot specify both 'equals' and 'indexing_sets'"
|
|
2300
|
+
)
|
|
2301
|
+
equals = equals.to_expr()
|
|
2302
|
+
indexing_sets = (equals,)
|
|
2229
2303
|
|
|
2230
2304
|
data = Set(*indexing_sets).data if len(indexing_sets) > 0 else pl.DataFrame()
|
|
2231
2305
|
super().__init__(data)
|
|
2232
2306
|
|
|
2233
2307
|
self.vtype: VType = VType(vtype)
|
|
2234
2308
|
self._attr = Container(self._set_attribute, self._get_attribute)
|
|
2235
|
-
self._equals = equals
|
|
2309
|
+
self._equals: Expression | None = equals
|
|
2236
2310
|
|
|
2237
2311
|
if lb is not None and not isinstance(lb, (float, int)):
|
|
2238
2312
|
self._lb_expr, self.lb = lb, None
|
|
@@ -2319,7 +2393,7 @@ class Variable(ModelElementWithId, SupportsMath):
|
|
|
2319
2393
|
else:
|
|
2320
2394
|
if self._model.solver_uses_variable_names:
|
|
2321
2395
|
name = self.name
|
|
2322
|
-
elif solver.
|
|
2396
|
+
elif solver.accelerate_with_repeat_names:
|
|
2323
2397
|
name = "V"
|
|
2324
2398
|
else:
|
|
2325
2399
|
name = ""
|
|
@@ -2331,7 +2405,14 @@ class Variable(ModelElementWithId, SupportsMath):
|
|
|
2331
2405
|
else:
|
|
2332
2406
|
ids = [poi_add_var(lb, ub, name=name).index for _ in range(n)]
|
|
2333
2407
|
|
|
2334
|
-
|
|
2408
|
+
try:
|
|
2409
|
+
df = self.data.with_columns(
|
|
2410
|
+
pl.Series(ids, dtype=Config.id_dtype).alias(VAR_KEY)
|
|
2411
|
+
)
|
|
2412
|
+
except TypeError as e:
|
|
2413
|
+
raise TypeError(
|
|
2414
|
+
f"Number of variables exceeds the current data type ({Config.id_dtype}). Consider increasing the data type by changing Config.id_dtype."
|
|
2415
|
+
) from e
|
|
2335
2416
|
|
|
2336
2417
|
self._data = df
|
|
2337
2418
|
|
|
@@ -2467,13 +2548,13 @@ class Variable(ModelElementWithId, SupportsMath):
|
|
|
2467
2548
|
|
|
2468
2549
|
@return_new
|
|
2469
2550
|
def next(self, dim: str, wrap_around: bool = False):
|
|
2470
|
-
"""Creates an expression where the variable at each
|
|
2551
|
+
"""Creates an expression where the variable at each label is the next variable in the specified dimension.
|
|
2471
2552
|
|
|
2472
2553
|
Parameters:
|
|
2473
2554
|
dim:
|
|
2474
2555
|
The dimension over which to shift the variable.
|
|
2475
2556
|
wrap_around:
|
|
2476
|
-
If `True`, the last
|
|
2557
|
+
If `True`, the last label in the dimension is connected to the first label.
|
|
2477
2558
|
|
|
2478
2559
|
Examples:
|
|
2479
2560
|
>>> import pandas as pd
|
|
@@ -2486,24 +2567,20 @@ class Variable(ModelElementWithId, SupportsMath):
|
|
|
2486
2567
|
>>> m.bat_charge + m.bat_flow == m.bat_charge.next("time")
|
|
2487
2568
|
Traceback (most recent call last):
|
|
2488
2569
|
...
|
|
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
|
-
... )
|
|
2570
|
+
pyoframe._constants.PyoframeError: Cannot subtract the two expressions below because expression 1 has extra labels.
|
|
2571
|
+
Expression 1: (bat_charge + bat_flow)
|
|
2572
|
+
Expression 2: bat_charge.next(…)
|
|
2573
|
+
Extra labels in expression 1:
|
|
2574
|
+
┌───────┬─────────┐
|
|
2575
|
+
│ time ┆ city │
|
|
2576
|
+
╞═══════╪═════════╡
|
|
2577
|
+
│ 18:00 ┆ Toronto │
|
|
2578
|
+
│ 18:00 ┆ Berlin │
|
|
2579
|
+
└───────┴─────────┘
|
|
2580
|
+
Use .drop_extras() or .keep_extras() to indicate how the extra labels should be handled. Learn more at
|
|
2581
|
+
https://bravos-power.github.io/pyoframe/latest/learn/concepts/addition
|
|
2582
|
+
|
|
2583
|
+
>>> (m.bat_charge + m.bat_flow).drop_extras() == m.bat_charge.next("time")
|
|
2507
2584
|
<Constraint 'unnamed' height=6 terms=18 type=linear>
|
|
2508
2585
|
┌───────┬─────────┬────────────────────────────────────────────────────────────────────────────────┐
|
|
2509
2586
|
│ time ┆ city ┆ constraint │
|