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/_constants.py
CHANGED
|
@@ -17,41 +17,54 @@ CONSTRAINT_KEY = "__constraint_id"
|
|
|
17
17
|
SOLUTION_KEY = "solution"
|
|
18
18
|
DUAL_KEY = "dual"
|
|
19
19
|
|
|
20
|
-
# TODO: move as configuration since this could be too small... also add a test to make sure errors occur on overflow.
|
|
21
|
-
KEY_TYPE = pl.UInt32
|
|
22
|
-
|
|
23
20
|
|
|
24
21
|
@dataclass
|
|
25
22
|
class _Solver:
|
|
26
23
|
name: SUPPORTED_SOLVER_TYPES
|
|
27
24
|
supports_integer_variables: bool = True
|
|
28
|
-
|
|
25
|
+
supports_quadratic_constraints: bool = True
|
|
26
|
+
supports_non_convex: bool = True
|
|
29
27
|
supports_duals: bool = True
|
|
30
28
|
supports_objective_sense: bool = True
|
|
31
29
|
supports_write: bool = True
|
|
32
|
-
|
|
30
|
+
accelerate_with_repeat_names: bool = False
|
|
33
31
|
"""
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
that
|
|
38
|
-
|
|
39
|
-
|
|
32
|
+
If True, Pyoframe sets all the variable and constraint names to 'V'
|
|
33
|
+
and 'C', respectively, which, for some solvers, was found to improve
|
|
34
|
+
performance. This setting should only be enabled for a given solver after
|
|
35
|
+
testing that a) it actually improves performance, and b) the solver can
|
|
36
|
+
handle conflicting variable and constraint names.
|
|
37
|
+
So far, only Gurobi has been tested.
|
|
38
|
+
Note, that when enabled, Model.write() is not supported
|
|
39
|
+
(unless solver_uses_variable_names=True) because the outputted files would
|
|
40
|
+
be meaningless as all variables/constraints would have identical names.
|
|
40
41
|
"""
|
|
41
42
|
|
|
43
|
+
def __post_init__(self):
|
|
44
|
+
if self.supports_non_convex:
|
|
45
|
+
assert self.supports_quadratic_constraints, (
|
|
46
|
+
"Non-convex solvers typically support quadratic constraints. Are you sure this is correct?"
|
|
47
|
+
)
|
|
48
|
+
|
|
42
49
|
def __repr__(self):
|
|
43
50
|
return self.name
|
|
44
51
|
|
|
45
52
|
|
|
46
53
|
SUPPORTED_SOLVERS = [
|
|
47
|
-
_Solver("gurobi",
|
|
48
|
-
_Solver(
|
|
54
|
+
_Solver("gurobi", accelerate_with_repeat_names=True),
|
|
55
|
+
_Solver(
|
|
56
|
+
"highs",
|
|
57
|
+
supports_quadratic_constraints=False,
|
|
58
|
+
supports_non_convex=False,
|
|
59
|
+
supports_duals=False,
|
|
60
|
+
),
|
|
49
61
|
_Solver(
|
|
50
62
|
"ipopt",
|
|
51
63
|
supports_integer_variables=False,
|
|
52
64
|
supports_objective_sense=False,
|
|
53
65
|
supports_write=False,
|
|
54
66
|
),
|
|
67
|
+
_Solver("copt", supports_non_convex=False),
|
|
55
68
|
]
|
|
56
69
|
|
|
57
70
|
|
|
@@ -71,7 +84,7 @@ RESERVED_COL_KEYS = (
|
|
|
71
84
|
@dataclass
|
|
72
85
|
class ConfigDefaults:
|
|
73
86
|
default_solver: SUPPORTED_SOLVER_TYPES | _Solver | Literal["raise", "auto"] = "auto"
|
|
74
|
-
|
|
87
|
+
disable_extras_checks: bool = False
|
|
75
88
|
enable_is_duplicated_expression_safety_check: bool = False
|
|
76
89
|
integer_tolerance: float = 1e-8
|
|
77
90
|
float_to_str_precision: int | None = 5
|
|
@@ -85,6 +98,7 @@ class ConfigDefaults:
|
|
|
85
98
|
)
|
|
86
99
|
print_max_terms: int = 5
|
|
87
100
|
maintain_order: bool = True
|
|
101
|
+
id_dtype = pl.UInt32
|
|
88
102
|
|
|
89
103
|
|
|
90
104
|
class _Config:
|
|
@@ -114,55 +128,52 @@ class _Config:
|
|
|
114
128
|
self._settings.default_solver = value
|
|
115
129
|
|
|
116
130
|
@property
|
|
117
|
-
def
|
|
118
|
-
"""When `True`, improves performance by skipping
|
|
131
|
+
def disable_extras_checks(self) -> bool:
|
|
132
|
+
"""When `True`, improves performance by skipping checks for extra values (not recommended).
|
|
119
133
|
|
|
120
|
-
When `True`,
|
|
121
|
-
are treated as if they contained [`.
|
|
122
|
-
(unless [`.
|
|
134
|
+
When `True`, checks for extra values are disabled which effectively means that all expressions
|
|
135
|
+
are treated as if they contained [`.keep_extras()`][pyoframe.Expression.keep_extras]
|
|
136
|
+
(unless [`.drop_extras()`][pyoframe.Expression.drop_extras] was applied).
|
|
123
137
|
|
|
124
138
|
!!! warning
|
|
125
|
-
This might improve performance, but it will suppress the
|
|
126
|
-
behaviors (
|
|
139
|
+
This might improve performance, but it will suppress the errors that alert you of unexpected
|
|
140
|
+
behaviors ([learn more](../../learn/concepts/addition.md)).
|
|
127
141
|
Only consider enabling after you have thoroughly tested your code.
|
|
128
142
|
|
|
129
143
|
Examples:
|
|
130
|
-
>>>
|
|
131
|
-
>>> population = pl.DataFrame(
|
|
144
|
+
>>> population = pf.Param(
|
|
132
145
|
... {
|
|
133
146
|
... "city": ["Toronto", "Vancouver", "Montreal"],
|
|
134
147
|
... "pop": [2_731_571, 631_486, 1_704_694],
|
|
135
148
|
... }
|
|
136
|
-
... )
|
|
137
|
-
>>> population_influx =
|
|
149
|
+
... )
|
|
150
|
+
>>> population_influx = pf.Param(
|
|
138
151
|
... {
|
|
139
152
|
... "city": ["Toronto", "Vancouver", "Montreal"],
|
|
140
153
|
... "influx": [100_000, 50_000, None],
|
|
141
154
|
... }
|
|
142
|
-
... )
|
|
155
|
+
... )
|
|
143
156
|
|
|
144
|
-
Normally, an error warns users that the two expressions have conflicting
|
|
157
|
+
Normally, an error warns users that the two expressions have conflicting labels:
|
|
145
158
|
>>> population + population_influx
|
|
146
159
|
Traceback (most recent call last):
|
|
147
160
|
...
|
|
148
|
-
pyoframe._constants.PyoframeError: Cannot add the two expressions below because
|
|
161
|
+
pyoframe._constants.PyoframeError: Cannot add the two expressions below because expression 1 has extra labels.
|
|
149
162
|
Expression 1: pop
|
|
150
163
|
Expression 2: influx
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
│
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
But if `Config.disable_unmatched_checks = True`, the error is suppressed and the sum is considered to be `population.keep_unmatched() + population_influx.keep_unmatched()`:
|
|
163
|
-
>>> pf.Config.disable_unmatched_checks = True
|
|
164
|
+
Extra labels in expression 1:
|
|
165
|
+
┌──────────┐
|
|
166
|
+
│ city │
|
|
167
|
+
╞══════════╡
|
|
168
|
+
│ Montreal │
|
|
169
|
+
└──────────┘
|
|
170
|
+
Use .drop_extras() or .keep_extras() to indicate how the extra labels should be handled. Learn more at
|
|
171
|
+
https://bravos-power.github.io/pyoframe/latest/learn/concepts/addition
|
|
172
|
+
|
|
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()`:
|
|
174
|
+
>>> pf.Config.disable_extras_checks = True
|
|
164
175
|
>>> population + population_influx
|
|
165
|
-
<Expression height=3 terms=3
|
|
176
|
+
<Expression (parameter) height=3 terms=3>
|
|
166
177
|
┌───────────┬────────────┐
|
|
167
178
|
│ city ┆ expression │
|
|
168
179
|
│ (3) ┆ │
|
|
@@ -172,11 +183,11 @@ class _Config:
|
|
|
172
183
|
│ Montreal ┆ 1704694 │
|
|
173
184
|
└───────────┴────────────┘
|
|
174
185
|
"""
|
|
175
|
-
return self._settings.
|
|
186
|
+
return self._settings.disable_extras_checks
|
|
176
187
|
|
|
177
|
-
@
|
|
178
|
-
def
|
|
179
|
-
self._settings.
|
|
188
|
+
@disable_extras_checks.setter
|
|
189
|
+
def disable_extras_checks(self, value: bool):
|
|
190
|
+
self._settings.disable_extras_checks = value
|
|
180
191
|
|
|
181
192
|
@property
|
|
182
193
|
def enable_is_duplicated_expression_safety_check(self) -> bool:
|
|
@@ -216,11 +227,11 @@ class _Config:
|
|
|
216
227
|
>>> m.X = pf.Variable()
|
|
217
228
|
>>> expr = 100.752038759 * m.X
|
|
218
229
|
>>> expr
|
|
219
|
-
<Expression terms=1
|
|
230
|
+
<Expression (linear) terms=1>
|
|
220
231
|
100.752 X
|
|
221
232
|
>>> pf.Config.float_to_str_precision = None
|
|
222
233
|
>>> expr
|
|
223
|
-
<Expression terms=1
|
|
234
|
+
<Expression (linear) terms=1>
|
|
224
235
|
100.752038759 X
|
|
225
236
|
"""
|
|
226
237
|
return self._settings.float_to_str_precision
|
|
@@ -268,7 +279,7 @@ class _Config:
|
|
|
268
279
|
>>> m = pf.Model()
|
|
269
280
|
>>> m.X = pf.Variable(pf.Set(x=range(100)), pf.Set(y=range(100)))
|
|
270
281
|
>>> m.X.sum("y")
|
|
271
|
-
<Expression height=100 terms=10000
|
|
282
|
+
<Expression (linear) height=100 terms=10000>
|
|
272
283
|
┌───────┬───────────────────────────────┐
|
|
273
284
|
│ x ┆ expression │
|
|
274
285
|
│ (100) ┆ │
|
|
@@ -286,7 +297,7 @@ class _Config:
|
|
|
286
297
|
│ 99 ┆ X[99,0] + X[99,1] + X[99,2] … │
|
|
287
298
|
└───────┴───────────────────────────────┘
|
|
288
299
|
>>> m.X.sum()
|
|
289
|
-
<Expression terms=10000
|
|
300
|
+
<Expression (linear) terms=10000>
|
|
290
301
|
X[0,0] + X[0,1] + X[0,2] …
|
|
291
302
|
"""
|
|
292
303
|
return self._settings.print_max_terms
|
|
@@ -308,17 +319,52 @@ class _Config:
|
|
|
308
319
|
def maintain_order(self, value: bool):
|
|
309
320
|
self._settings.maintain_order = value
|
|
310
321
|
|
|
322
|
+
@property
|
|
323
|
+
def id_dtype(self):
|
|
324
|
+
"""The Polars data type to use for variable and constraint IDs.
|
|
325
|
+
|
|
326
|
+
Defaults to `pl.UInt32` which should be ideal for most users.
|
|
327
|
+
|
|
328
|
+
Users with more than 4 billion variables or constraints can change this to `pl.UInt64`.
|
|
329
|
+
|
|
330
|
+
Users concerned with memory usage and with fewer than 65k variables or constraints can change this to `pl.UInt16`.
|
|
331
|
+
|
|
332
|
+
!!! warning
|
|
333
|
+
Changing this setting after creating a model will lead to errors.
|
|
334
|
+
You should only change this setting before creating any models.
|
|
335
|
+
|
|
336
|
+
Examples:
|
|
337
|
+
An error is automatically raised if the number of variables or constraints exceeds the chosen data type:
|
|
338
|
+
>>> pf.Config.id_dtype = pl.UInt8
|
|
339
|
+
>>> m = pf.Model()
|
|
340
|
+
>>> big_set = pf.Set(x=range(2**8 + 1))
|
|
341
|
+
>>> m.X = pf.Variable()
|
|
342
|
+
>>> m.constraint = m.X.over("x") <= big_set
|
|
343
|
+
Traceback (most recent call last):
|
|
344
|
+
...
|
|
345
|
+
TypeError: Number of constraints exceeds the current data type (UInt8). Consider increasing the data type by changing Config.id_dtype.
|
|
346
|
+
>>> m.X_large = pf.Variable(big_set)
|
|
347
|
+
Traceback (most recent call last):
|
|
348
|
+
...
|
|
349
|
+
TypeError: Number of variables exceeds the current data type (UInt8). Consider increasing the data type by changing Config.id_dtype.
|
|
350
|
+
"""
|
|
351
|
+
return self._settings.id_dtype
|
|
352
|
+
|
|
353
|
+
@id_dtype.setter
|
|
354
|
+
def id_dtype(self, value):
|
|
355
|
+
self._settings.id_dtype = value
|
|
356
|
+
|
|
311
357
|
def reset_defaults(self):
|
|
312
358
|
"""Resets all configuration options to their default values.
|
|
313
359
|
|
|
314
360
|
Examples:
|
|
315
|
-
>>> pf.Config.
|
|
361
|
+
>>> pf.Config.disable_extras_checks
|
|
316
362
|
False
|
|
317
|
-
>>> pf.Config.
|
|
318
|
-
>>> pf.Config.
|
|
363
|
+
>>> pf.Config.disable_extras_checks = True
|
|
364
|
+
>>> pf.Config.disable_extras_checks
|
|
319
365
|
True
|
|
320
366
|
>>> pf.Config.reset_defaults()
|
|
321
|
-
>>> pf.Config.
|
|
367
|
+
>>> pf.Config.disable_extras_checks
|
|
322
368
|
False
|
|
323
369
|
"""
|
|
324
370
|
self._settings = ConfigDefaults()
|
|
@@ -389,8 +435,8 @@ class VType(Enum):
|
|
|
389
435
|
raise ValueError(f"Invalid variable type: {self}") # pragma: no cover
|
|
390
436
|
|
|
391
437
|
|
|
392
|
-
class
|
|
393
|
-
"""An enum to specify how to handle
|
|
438
|
+
class ExtrasStrategy(Enum):
|
|
439
|
+
"""An enum to specify how to handle extra values in expressions."""
|
|
394
440
|
|
|
395
441
|
UNSET = "not_set"
|
|
396
442
|
DROP = "drop"
|
|
@@ -404,7 +450,7 @@ VTypeValue = Literal["continuous", "binary", "integer"]
|
|
|
404
450
|
for enum, type in [(ObjSense, ObjSenseValue), (VType, VTypeValue)]:
|
|
405
451
|
assert set(typing.get_args(type)) == {vtype.value for vtype in enum}
|
|
406
452
|
|
|
407
|
-
SUPPORTED_SOLVER_TYPES = Literal["gurobi", "highs", "ipopt"]
|
|
453
|
+
SUPPORTED_SOLVER_TYPES = Literal["gurobi", "highs", "ipopt", "copt"]
|
|
408
454
|
assert set(typing.get_args(SUPPORTED_SOLVER_TYPES)) == {
|
|
409
455
|
s.name for s in SUPPORTED_SOLVERS
|
|
410
456
|
}
|