pyoframe 0.2.0__py3-none-any.whl → 1.0.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 +21 -14
- pyoframe/_arithmetic.py +346 -238
- pyoframe/_constants.py +463 -0
- pyoframe/_core.py +2652 -0
- pyoframe/_model.py +598 -0
- pyoframe/_model_element.py +189 -0
- pyoframe/_monkey_patch.py +82 -0
- pyoframe/{objective.py → _objective.py} +50 -17
- pyoframe/{util.py → _utils.py} +108 -129
- pyoframe/_version.py +16 -3
- {pyoframe-0.2.0.dist-info → pyoframe-1.0.0.dist-info}/METADATA +37 -31
- pyoframe-1.0.0.dist-info/RECORD +15 -0
- pyoframe/constants.py +0 -140
- pyoframe/core.py +0 -1794
- pyoframe/model.py +0 -408
- pyoframe/model_element.py +0 -184
- pyoframe/monkey_patch.py +0 -54
- pyoframe-0.2.0.dist-info/RECORD +0 -15
- {pyoframe-0.2.0.dist-info → pyoframe-1.0.0.dist-info}/WHEEL +0 -0
- {pyoframe-0.2.0.dist-info → pyoframe-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {pyoframe-0.2.0.dist-info → pyoframe-1.0.0.dist-info}/top_level.txt +0 -0
pyoframe/_constants.py
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
"""Contains shared constants which are used across the package."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typing
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Literal
|
|
9
|
+
|
|
10
|
+
import polars as pl
|
|
11
|
+
import pyoptinterface as poi
|
|
12
|
+
|
|
13
|
+
COEF_KEY = "__coeff"
|
|
14
|
+
VAR_KEY = "__variable_id"
|
|
15
|
+
QUAD_VAR_KEY = "__quadratic_variable_id"
|
|
16
|
+
CONSTRAINT_KEY = "__constraint_id"
|
|
17
|
+
SOLUTION_KEY = "solution"
|
|
18
|
+
DUAL_KEY = "dual"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class _Solver:
|
|
23
|
+
name: SUPPORTED_SOLVER_TYPES
|
|
24
|
+
supports_integer_variables: bool = True
|
|
25
|
+
supports_quadratic_constraints: bool = True
|
|
26
|
+
supports_non_convex: bool = True
|
|
27
|
+
supports_duals: bool = True
|
|
28
|
+
supports_objective_sense: bool = True
|
|
29
|
+
supports_write: bool = True
|
|
30
|
+
accelerate_with_repeat_names: bool = False
|
|
31
|
+
"""
|
|
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.
|
|
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
|
+
|
|
49
|
+
def __repr__(self):
|
|
50
|
+
return self.name
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
SUPPORTED_SOLVERS = [
|
|
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
|
+
),
|
|
61
|
+
_Solver(
|
|
62
|
+
"ipopt",
|
|
63
|
+
supports_integer_variables=False,
|
|
64
|
+
supports_objective_sense=False,
|
|
65
|
+
supports_write=False,
|
|
66
|
+
),
|
|
67
|
+
_Solver("copt", supports_non_convex=False),
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# Variable ID for constant terms. This variable ID is reserved.
|
|
72
|
+
CONST_TERM = 0
|
|
73
|
+
|
|
74
|
+
RESERVED_COL_KEYS = (
|
|
75
|
+
COEF_KEY,
|
|
76
|
+
VAR_KEY,
|
|
77
|
+
QUAD_VAR_KEY,
|
|
78
|
+
CONSTRAINT_KEY,
|
|
79
|
+
SOLUTION_KEY,
|
|
80
|
+
DUAL_KEY,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass
|
|
85
|
+
class ConfigDefaults:
|
|
86
|
+
default_solver: SUPPORTED_SOLVER_TYPES | _Solver | Literal["raise", "auto"] = "auto"
|
|
87
|
+
disable_extras_checks: bool = False
|
|
88
|
+
enable_is_duplicated_expression_safety_check: bool = False
|
|
89
|
+
integer_tolerance: float = 1e-8
|
|
90
|
+
float_to_str_precision: int | None = 5
|
|
91
|
+
print_polars_config: pl.Config = field(
|
|
92
|
+
default_factory=lambda: pl.Config(
|
|
93
|
+
tbl_hide_column_data_types=True,
|
|
94
|
+
tbl_hide_dataframe_shape=True,
|
|
95
|
+
fmt_str_lengths=100, # Set to a large value to avoid truncation (within reason)
|
|
96
|
+
apply_on_context_enter=True,
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
print_max_terms: int = 5
|
|
100
|
+
maintain_order: bool = True
|
|
101
|
+
id_dtype = pl.UInt32
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class _Config:
|
|
105
|
+
"""General settings for Pyoframe (for advanced users).
|
|
106
|
+
|
|
107
|
+
Accessible via `pf.Config` (see examples below).
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
def __init__(self):
|
|
111
|
+
self._settings = ConfigDefaults()
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def default_solver(
|
|
115
|
+
self,
|
|
116
|
+
) -> SUPPORTED_SOLVER_TYPES | _Solver | Literal["raise", "auto"]:
|
|
117
|
+
"""The solver to use when [Model][pyoframe.Model] is instantiated without specifying a solver.
|
|
118
|
+
|
|
119
|
+
If `auto`, Pyoframe will try to use whichever solver is installed.
|
|
120
|
+
If `raise`, an exception will be raised when [Model][pyoframe.Model] is instantiated without specifying a solver.
|
|
121
|
+
|
|
122
|
+
We recommend that users specify their solver when instantiating [Model][pyoframe.Model] rather than relying on this option.
|
|
123
|
+
"""
|
|
124
|
+
return self._settings.default_solver
|
|
125
|
+
|
|
126
|
+
@default_solver.setter
|
|
127
|
+
def default_solver(self, value):
|
|
128
|
+
self._settings.default_solver = value
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def disable_extras_checks(self) -> bool:
|
|
132
|
+
"""When `True`, improves performance by skipping checks for extra values (not recommended).
|
|
133
|
+
|
|
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).
|
|
137
|
+
|
|
138
|
+
!!! warning
|
|
139
|
+
This might improve performance, but it will suppress the errors that alert you of unexpected
|
|
140
|
+
behaviors ([learn more](../../learn/concepts/addition.md)).
|
|
141
|
+
Only consider enabling after you have thoroughly tested your code.
|
|
142
|
+
|
|
143
|
+
Examples:
|
|
144
|
+
>>> import polars as pl
|
|
145
|
+
>>> population = pl.DataFrame(
|
|
146
|
+
... {
|
|
147
|
+
... "city": ["Toronto", "Vancouver", "Montreal"],
|
|
148
|
+
... "pop": [2_731_571, 631_486, 1_704_694],
|
|
149
|
+
... }
|
|
150
|
+
... ).to_expr()
|
|
151
|
+
>>> population_influx = pl.DataFrame(
|
|
152
|
+
... {
|
|
153
|
+
... "city": ["Toronto", "Vancouver", "Montreal"],
|
|
154
|
+
... "influx": [100_000, 50_000, None],
|
|
155
|
+
... }
|
|
156
|
+
... ).to_expr()
|
|
157
|
+
|
|
158
|
+
Normally, an error warns users that the two expressions have conflicting labels:
|
|
159
|
+
>>> population + population_influx
|
|
160
|
+
Traceback (most recent call last):
|
|
161
|
+
...
|
|
162
|
+
pyoframe._constants.PyoframeError: Cannot add the two expressions below because expression 1 has extra labels.
|
|
163
|
+
Expression 1: pop
|
|
164
|
+
Expression 2: influx
|
|
165
|
+
Extra labels in expression 1:
|
|
166
|
+
┌──────────┐
|
|
167
|
+
│ city │
|
|
168
|
+
╞══════════╡
|
|
169
|
+
│ Montreal │
|
|
170
|
+
└──────────┘
|
|
171
|
+
Use .drop_extras() or .keep_extras() to indicate how the extra labels should be handled. Learn more at
|
|
172
|
+
https://bravos-power.github.io/pyoframe/latest/learn/concepts/addition
|
|
173
|
+
|
|
174
|
+
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
|
+
>>> pf.Config.disable_extras_checks = True
|
|
176
|
+
>>> population + population_influx
|
|
177
|
+
<Expression height=3 terms=3 type=constant>
|
|
178
|
+
┌───────────┬────────────┐
|
|
179
|
+
│ city ┆ expression │
|
|
180
|
+
│ (3) ┆ │
|
|
181
|
+
╞═══════════╪════════════╡
|
|
182
|
+
│ Toronto ┆ 2831571 │
|
|
183
|
+
│ Vancouver ┆ 681486 │
|
|
184
|
+
│ Montreal ┆ 1704694 │
|
|
185
|
+
└───────────┴────────────┘
|
|
186
|
+
"""
|
|
187
|
+
return self._settings.disable_extras_checks
|
|
188
|
+
|
|
189
|
+
@disable_extras_checks.setter
|
|
190
|
+
def disable_extras_checks(self, value: bool):
|
|
191
|
+
self._settings.disable_extras_checks = value
|
|
192
|
+
|
|
193
|
+
@property
|
|
194
|
+
def enable_is_duplicated_expression_safety_check(self) -> bool:
|
|
195
|
+
"""Setting for internal testing purposes only.
|
|
196
|
+
|
|
197
|
+
When `True`, pyoframe checks that there are no bugs leading to duplicated terms in expressions.
|
|
198
|
+
"""
|
|
199
|
+
return self._settings.enable_is_duplicated_expression_safety_check
|
|
200
|
+
|
|
201
|
+
@enable_is_duplicated_expression_safety_check.setter
|
|
202
|
+
def enable_is_duplicated_expression_safety_check(self, value: bool):
|
|
203
|
+
self._settings.enable_is_duplicated_expression_safety_check = value
|
|
204
|
+
|
|
205
|
+
@property
|
|
206
|
+
def integer_tolerance(self) -> float:
|
|
207
|
+
"""Tolerance for checking if a floating point value is an integer.
|
|
208
|
+
|
|
209
|
+
!!! info
|
|
210
|
+
For convenience, Pyoframe returns the solution of integer and binary variables as integers not floating point values.
|
|
211
|
+
To do so, Pyoframe must convert the solver-provided floating point values to integers. To avoid unexpected rounding errors,
|
|
212
|
+
Pyoframe uses this tolerance to check that the floating point result is an integer as expected. Overly tight tolerances can trigger
|
|
213
|
+
unexpected errors. Setting the tolerance to zero disables the check.
|
|
214
|
+
"""
|
|
215
|
+
return self._settings.integer_tolerance
|
|
216
|
+
|
|
217
|
+
@integer_tolerance.setter
|
|
218
|
+
def integer_tolerance(self, value: float):
|
|
219
|
+
self._settings.integer_tolerance = value
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
def float_to_str_precision(self) -> int | None:
|
|
223
|
+
"""Number of decimal places to use when displaying mathematical expressions.
|
|
224
|
+
|
|
225
|
+
Examples:
|
|
226
|
+
>>> pf.Config.float_to_str_precision = 3
|
|
227
|
+
>>> m = pf.Model()
|
|
228
|
+
>>> m.X = pf.Variable()
|
|
229
|
+
>>> expr = 100.752038759 * m.X
|
|
230
|
+
>>> expr
|
|
231
|
+
<Expression terms=1 type=linear>
|
|
232
|
+
100.752 X
|
|
233
|
+
>>> pf.Config.float_to_str_precision = None
|
|
234
|
+
>>> expr
|
|
235
|
+
<Expression terms=1 type=linear>
|
|
236
|
+
100.752038759 X
|
|
237
|
+
"""
|
|
238
|
+
return self._settings.float_to_str_precision
|
|
239
|
+
|
|
240
|
+
@float_to_str_precision.setter
|
|
241
|
+
def float_to_str_precision(self, value: int | None):
|
|
242
|
+
self._settings.float_to_str_precision = value
|
|
243
|
+
|
|
244
|
+
@property
|
|
245
|
+
def print_polars_config(self) -> pl.Config:
|
|
246
|
+
"""[`polars.Config`](https://docs.pola.rs/api/python/stable/reference/config.html) object to use when printing dimensioned Pyoframe objects.
|
|
247
|
+
|
|
248
|
+
Examples:
|
|
249
|
+
For example, to limit the number of rows printed in a table, use `set_tbl_rows`:
|
|
250
|
+
>>> pf.Config.print_polars_config.set_tbl_rows(5)
|
|
251
|
+
<class 'polars.config.Config'>
|
|
252
|
+
>>> m = pf.Model()
|
|
253
|
+
>>> m.X = pf.Variable(pf.Set(x=range(100)))
|
|
254
|
+
>>> m.X
|
|
255
|
+
<Variable 'X' height=100>
|
|
256
|
+
┌───────┬──────────┐
|
|
257
|
+
│ x ┆ variable │
|
|
258
|
+
│ (100) ┆ │
|
|
259
|
+
╞═══════╪══════════╡
|
|
260
|
+
│ 0 ┆ X[0] │
|
|
261
|
+
│ 1 ┆ X[1] │
|
|
262
|
+
│ 2 ┆ X[2] │
|
|
263
|
+
│ … ┆ … │
|
|
264
|
+
│ 98 ┆ X[98] │
|
|
265
|
+
│ 99 ┆ X[99] │
|
|
266
|
+
└───────┴──────────┘
|
|
267
|
+
"""
|
|
268
|
+
return self._settings.print_polars_config
|
|
269
|
+
|
|
270
|
+
@print_polars_config.setter
|
|
271
|
+
def print_polars_config(self, value: pl.Config):
|
|
272
|
+
self._settings.print_polars_config = value
|
|
273
|
+
|
|
274
|
+
@property
|
|
275
|
+
def print_max_terms(self) -> int:
|
|
276
|
+
"""Maximum number of terms to print in an expression before truncating it.
|
|
277
|
+
|
|
278
|
+
Examples:
|
|
279
|
+
>>> pf.Config.print_max_terms = 3
|
|
280
|
+
>>> m = pf.Model()
|
|
281
|
+
>>> m.X = pf.Variable(pf.Set(x=range(100)), pf.Set(y=range(100)))
|
|
282
|
+
>>> m.X.sum("y")
|
|
283
|
+
<Expression height=100 terms=10000 type=linear>
|
|
284
|
+
┌───────┬───────────────────────────────┐
|
|
285
|
+
│ x ┆ expression │
|
|
286
|
+
│ (100) ┆ │
|
|
287
|
+
╞═══════╪═══════════════════════════════╡
|
|
288
|
+
│ 0 ┆ X[0,0] + X[0,1] + X[0,2] … │
|
|
289
|
+
│ 1 ┆ X[1,0] + X[1,1] + X[1,2] … │
|
|
290
|
+
│ 2 ┆ X[2,0] + X[2,1] + X[2,2] … │
|
|
291
|
+
│ 3 ┆ X[3,0] + X[3,1] + X[3,2] … │
|
|
292
|
+
│ 4 ┆ X[4,0] + X[4,1] + X[4,2] … │
|
|
293
|
+
│ … ┆ … │
|
|
294
|
+
│ 95 ┆ X[95,0] + X[95,1] + X[95,2] … │
|
|
295
|
+
│ 96 ┆ X[96,0] + X[96,1] + X[96,2] … │
|
|
296
|
+
│ 97 ┆ X[97,0] + X[97,1] + X[97,2] … │
|
|
297
|
+
│ 98 ┆ X[98,0] + X[98,1] + X[98,2] … │
|
|
298
|
+
│ 99 ┆ X[99,0] + X[99,1] + X[99,2] … │
|
|
299
|
+
└───────┴───────────────────────────────┘
|
|
300
|
+
>>> m.X.sum()
|
|
301
|
+
<Expression terms=10000 type=linear>
|
|
302
|
+
X[0,0] + X[0,1] + X[0,2] …
|
|
303
|
+
"""
|
|
304
|
+
return self._settings.print_max_terms
|
|
305
|
+
|
|
306
|
+
@print_max_terms.setter
|
|
307
|
+
def print_max_terms(self, value: int):
|
|
308
|
+
self._settings.print_max_terms = value
|
|
309
|
+
|
|
310
|
+
@property
|
|
311
|
+
def maintain_order(self) -> bool:
|
|
312
|
+
"""Whether the order of variables, constraints, and mathematical terms is to be identical across runs.
|
|
313
|
+
|
|
314
|
+
If `False`, performance is improved, but your results may vary every so slightly across runs
|
|
315
|
+
since numerical errors can accumulate differently when the order of operations changes.
|
|
316
|
+
"""
|
|
317
|
+
return self._settings.maintain_order
|
|
318
|
+
|
|
319
|
+
@maintain_order.setter
|
|
320
|
+
def maintain_order(self, value: bool):
|
|
321
|
+
self._settings.maintain_order = value
|
|
322
|
+
|
|
323
|
+
@property
|
|
324
|
+
def id_dtype(self):
|
|
325
|
+
"""The Polars data type to use for variable and constraint IDs.
|
|
326
|
+
|
|
327
|
+
Defaults to `pl.UInt32` which should be ideal for most users.
|
|
328
|
+
|
|
329
|
+
Users with more than 4 billion variables or constraints can change this to `pl.UInt64`.
|
|
330
|
+
|
|
331
|
+
Users concerned with memory usage and with fewer than 65k variables or constraints can change this to `pl.UInt16`.
|
|
332
|
+
|
|
333
|
+
!!! warning
|
|
334
|
+
Changing this setting after creating a model will lead to errors.
|
|
335
|
+
You should only change this setting before creating any models.
|
|
336
|
+
|
|
337
|
+
Examples:
|
|
338
|
+
An error is automatically raised if the number of variables or constraints exceeds the chosen data type:
|
|
339
|
+
>>> pf.Config.id_dtype = pl.UInt8
|
|
340
|
+
>>> m = pf.Model()
|
|
341
|
+
>>> big_set = pf.Set(x=range(2**8 + 1))
|
|
342
|
+
>>> m.X = pf.Variable()
|
|
343
|
+
>>> m.constraint = m.X.over("x") <= big_set
|
|
344
|
+
Traceback (most recent call last):
|
|
345
|
+
...
|
|
346
|
+
TypeError: Number of constraints exceeds the current data type (UInt8). Consider increasing the data type by changing Config.id_dtype.
|
|
347
|
+
>>> m.X_large = pf.Variable(big_set)
|
|
348
|
+
Traceback (most recent call last):
|
|
349
|
+
...
|
|
350
|
+
TypeError: Number of variables exceeds the current data type (UInt8). Consider increasing the data type by changing Config.id_dtype.
|
|
351
|
+
"""
|
|
352
|
+
return self._settings.id_dtype
|
|
353
|
+
|
|
354
|
+
@id_dtype.setter
|
|
355
|
+
def id_dtype(self, value):
|
|
356
|
+
self._settings.id_dtype = value
|
|
357
|
+
|
|
358
|
+
def reset_defaults(self):
|
|
359
|
+
"""Resets all configuration options to their default values.
|
|
360
|
+
|
|
361
|
+
Examples:
|
|
362
|
+
>>> pf.Config.disable_extras_checks
|
|
363
|
+
False
|
|
364
|
+
>>> pf.Config.disable_extras_checks = True
|
|
365
|
+
>>> pf.Config.disable_extras_checks
|
|
366
|
+
True
|
|
367
|
+
>>> pf.Config.reset_defaults()
|
|
368
|
+
>>> pf.Config.disable_extras_checks
|
|
369
|
+
False
|
|
370
|
+
"""
|
|
371
|
+
self._settings = ConfigDefaults()
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
Config = _Config()
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
class ConstraintSense(Enum):
|
|
378
|
+
LE = "<="
|
|
379
|
+
GE = ">="
|
|
380
|
+
EQ = "="
|
|
381
|
+
|
|
382
|
+
def _to_poi(self):
|
|
383
|
+
"""Converts the constraint sense to its pyoptinterface equivalent."""
|
|
384
|
+
if self == ConstraintSense.LE:
|
|
385
|
+
return poi.ConstraintSense.LessEqual
|
|
386
|
+
elif self == ConstraintSense.EQ:
|
|
387
|
+
return poi.ConstraintSense.Equal
|
|
388
|
+
elif self == ConstraintSense.GE:
|
|
389
|
+
return poi.ConstraintSense.GreaterEqual
|
|
390
|
+
else:
|
|
391
|
+
raise ValueError(f"Invalid constraint type: {self}") # pragma: no cover
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
class ObjSense(Enum):
|
|
395
|
+
MIN = "min"
|
|
396
|
+
MAX = "max"
|
|
397
|
+
|
|
398
|
+
def _to_poi(self):
|
|
399
|
+
"""Converts the objective sense to its pyoptinterface equivalent."""
|
|
400
|
+
if self == ObjSense.MIN:
|
|
401
|
+
return poi.ObjectiveSense.Minimize
|
|
402
|
+
elif self == ObjSense.MAX:
|
|
403
|
+
return poi.ObjectiveSense.Maximize
|
|
404
|
+
else:
|
|
405
|
+
raise ValueError(f"Invalid objective sense: {self}") # pragma: no cover
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
class VType(Enum):
|
|
409
|
+
"""An [Enum](https://realpython.com/python-enum/) that can be used to specify the variable type.
|
|
410
|
+
|
|
411
|
+
Examples:
|
|
412
|
+
>>> m = pf.Model()
|
|
413
|
+
>>> m.X = pf.Variable(vtype=VType.BINARY)
|
|
414
|
+
|
|
415
|
+
The enum's string values can also be used directly although this is prone to typos:
|
|
416
|
+
|
|
417
|
+
>>> m.Y = pf.Variable(vtype="binary")
|
|
418
|
+
"""
|
|
419
|
+
|
|
420
|
+
CONTINUOUS = "continuous"
|
|
421
|
+
"""Variables that can be any real value."""
|
|
422
|
+
BINARY = "binary"
|
|
423
|
+
"""Variables that must be either 0 or 1."""
|
|
424
|
+
INTEGER = "integer"
|
|
425
|
+
"""Variables that must be integer values."""
|
|
426
|
+
|
|
427
|
+
def _to_poi(self):
|
|
428
|
+
"""Convert the Variable type to its pyoptinterface equivalent."""
|
|
429
|
+
if self == VType.CONTINUOUS:
|
|
430
|
+
return poi.VariableDomain.Continuous
|
|
431
|
+
elif self == VType.BINARY:
|
|
432
|
+
return poi.VariableDomain.Binary
|
|
433
|
+
elif self == VType.INTEGER:
|
|
434
|
+
return poi.VariableDomain.Integer
|
|
435
|
+
else:
|
|
436
|
+
raise ValueError(f"Invalid variable type: {self}") # pragma: no cover
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
class ExtrasStrategy(Enum):
|
|
440
|
+
"""An enum to specify how to handle extra values in expressions."""
|
|
441
|
+
|
|
442
|
+
UNSET = "not_set"
|
|
443
|
+
DROP = "drop"
|
|
444
|
+
KEEP = "keep"
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
# This is a hack to get the Literal type for VType
|
|
448
|
+
# See: https://stackoverflow.com/questions/67292470/type-hinting-enum-member-value-in-python
|
|
449
|
+
ObjSenseValue = Literal["min", "max"]
|
|
450
|
+
VTypeValue = Literal["continuous", "binary", "integer"]
|
|
451
|
+
for enum, type in [(ObjSense, ObjSenseValue), (VType, VTypeValue)]:
|
|
452
|
+
assert set(typing.get_args(type)) == {vtype.value for vtype in enum}
|
|
453
|
+
|
|
454
|
+
SUPPORTED_SOLVER_TYPES = Literal["gurobi", "highs", "ipopt", "copt"]
|
|
455
|
+
assert set(typing.get_args(SUPPORTED_SOLVER_TYPES)) == {
|
|
456
|
+
s.name for s in SUPPORTED_SOLVERS
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
class PyoframeError(Exception):
|
|
461
|
+
"""Class for all Pyoframe-specific errors, typically errors arising from improper arithmetic operations."""
|
|
462
|
+
|
|
463
|
+
pass
|