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/_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
- supports_quadratics: bool = True
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
- block_auto_names: bool = False
30
+ accelerate_with_repeat_names: bool = False
33
31
  """
34
- When True, Pyoframe blocks automatic variable and constraint name
35
- generation to improve performance by setting all the variable names to 'V'
36
- and all the constraint names to 'C'. This should only be True for solvers
37
- that support conflicting variable and constraint names. Benchmarking
38
- should be performed to verify that this improves performance before turning
39
- this on for other solvers.
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", block_auto_names=True),
48
- _Solver("highs", supports_quadratics=False, supports_duals=False),
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
- disable_unmatched_checks: bool = False
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 disable_unmatched_checks(self) -> bool:
118
- """When `True`, improves performance by skipping unmatched checks (not recommended).
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`, unmatched checks are disabled which effectively means that all expressions
121
- are treated as if they contained [`.keep_unmatched()`][pyoframe.Expression.keep_unmatched]
122
- (unless [`.drop_unmatched()`][pyoframe.Expression.drop_unmatched] was applied).
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 "unmatched" errors that alert developers to unexpected
126
- behaviors (see [here](../learn/concepts/special-functions.md#drop_unmatched-and-keep_unmatched)).
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
- >>> import polars as pl
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
- ... ).to_expr()
137
- >>> population_influx = pl.DataFrame(
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
- ... ).to_expr()
155
+ ... )
143
156
 
144
- Normally, an error warns users that the two expressions have conflicting indices:
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 of unmatched values.
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
- Unmatched values:
152
- shape: (1, 2)
153
- ┌──────────┬────────────┐
154
- │ city ┆ city_right │
155
- --- ┆ ---
156
- │ str ┆ str │
157
- ╞══════════╪════════════╡
158
- │ Montreal ┆ null │
159
- └──────────┴────────────┘
160
- If this is intentional, use .drop_unmatched() or .keep_unmatched().
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 type=constant>
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.disable_unmatched_checks
186
+ return self._settings.disable_extras_checks
176
187
 
177
- @disable_unmatched_checks.setter
178
- def disable_unmatched_checks(self, value: bool):
179
- self._settings.disable_unmatched_checks = value
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 type=linear>
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 type=linear>
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 type=linear>
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 type=linear>
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.disable_unmatched_checks
361
+ >>> pf.Config.disable_extras_checks
316
362
  False
317
- >>> pf.Config.disable_unmatched_checks = True
318
- >>> pf.Config.disable_unmatched_checks
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.disable_unmatched_checks
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 UnmatchedStrategy(Enum):
393
- """An enum to specify how to handle unmatched values in expressions."""
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
  }