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/_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, Protocol, Union, overload
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 ModelElement, ModelElementWithId
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
- def to_expr(self) -> Expression:
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._unmatched_strategy = UnmatchedStrategy.UNSET
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) -> SupportsMath:
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: SupportsMath):
77
- """Copies the flags from another SupportsMath object."""
78
- self._unmatched_strategy = other._unmatched_strategy
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 keep_unmatched(self):
82
- """Indicates that all rows should be kept during addition or subtraction, even if they are not matched in the other expression."""
83
- new = self._new(self.data, name=f"{self.name}.keep_unmatched()")
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._unmatched_strategy = UnmatchedStrategy.KEEP
84
+ new._extras_strategy = ExtrasStrategy.KEEP
86
85
  return new
87
86
 
88
- def drop_unmatched(self):
89
- """Indicates that rows that are not matched in the other expression during addition or subtraction should be dropped."""
90
- new = self._new(self.data, name=f"{self.name}.drop_unmatched()")
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._unmatched_strategy = UnmatchedStrategy.DROP
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](../examples/portfolio_optimization.md) for a usage 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 __ge__(self, other):
359
- """Equality constraint.
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
- Examples:
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 __eq__(self, value: object): # type: ignore
371
- """Equality constraint.
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
- Examples:
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
- SupportsMath,
406
+ BaseOperableBlock,
388
407
  Mapping[str, Sequence[object]],
389
408
  "Set",
390
409
  "Constraint",
391
410
  ]
392
411
 
393
412
 
394
- class Set(SupportsMath):
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, SupportsMath):
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(SupportsMath):
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 indices
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 indices:\n{duplicated_data}."
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: KEY_TYPE},
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 of unmatched values.
1071
- Expression 1: v
1072
- Expression 2: add
1073
- Unmatched values:
1074
- shape: (1, 2)
1075
- ┌──────┬────────────┐
1076
- │ dim1 ┆ dim1_right │
1077
- --- ┆ ---
1078
- │ i64 ┆ i64 │
1079
- ╞══════╪════════════╡
1080
- │ 3 ┆ null │
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: int | float | SupportsToExpr) -> Expression:
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: KEY_TYPE},
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(KEY_TYPE)
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(KEY_TYPE))
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(KEY_TYPE)
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: KEY_TYPE},
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=self.data.get_column(COEF_KEY).to_numpy(),
1320
- var1s=self.data.get_column(VAR_KEY).to_numpy(),
1321
- var2s=self.data.get_column(QUAD_VAR_KEY).to_numpy(),
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=self.data.get_column(COEF_KEY).to_numpy(),
1326
- variables=self.data.get_column(VAR_KEY).to_numpy(),
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: SupportsToExpr) -> Expression: ...
1599
+ def sum(over: str | Sequence[str], expr: Operable) -> Expression: ...
1538
1600
 
1539
1601
 
1540
1602
  @overload
1541
- def sum(over: SupportsToExpr) -> Expression: ...
1603
+ def sum(over: Operable) -> Expression: ...
1542
1604
 
1543
1605
 
1544
1606
  def sum(
1545
- over: str | Sequence[str] | SupportsToExpr,
1546
- expr: SupportsToExpr | None = None,
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, SupportsMath)
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(ModelElementWithId):
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.block_auto_names else ""
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
- df = self.data.with_columns(
1713
- pl.lit(
1714
- add_constraint(
1715
- create_expression(
1716
- *(
1717
- df.get_column(c).to_numpy()
1718
- for c in ([COEF_KEY] + self.lhs._variable_columns)
1719
- )
1720
- ),
1721
- sense,
1722
- 0,
1723
- name,
1724
- ).index
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
- .alias(CONSTRAINT_KEY)
1727
- .cast(KEY_TYPE)
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
- df = df_unique.with_columns(
1818
- pl.Series(ids, dtype=KEY_TYPE).alias(CONSTRAINT_KEY)
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(ModelElementWithId, SupportsMath):
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: float | int | SupportsToExpr | None = None,
2220
- ub: float | int | SupportsToExpr | None = None,
2284
+ lb: Operable | None = None,
2285
+ ub: Operable | None = None,
2221
2286
  vtype: VType | VTypeValue = VType.CONTINUOUS,
2222
- equals: SupportsToExpr | None = None,
2287
+ equals: Operable | None = None,
2223
2288
  ):
2224
2289
  if equals is not None:
2225
- assert len(indexing_sets) == 0, (
2226
- "Cannot specify both 'equals' and 'indexing_sets'"
2227
- )
2228
- indexing_sets = (equals,)
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.block_auto_names:
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
- df = self.data.with_columns(pl.Series(ids, dtype=KEY_TYPE).alias(VAR_KEY))
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 index is the next variable in the specified dimension.
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 index in the dimension is connected to the first index.
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 of unmatched values.
2490
- Expression 1: (bat_charge + bat_flow)
2491
- Expression 2: bat_charge.next(…)
2492
- Unmatched values:
2493
- shape: (2, 4)
2494
- ┌───────┬─────────┬────────────┬────────────┐
2495
- │ time ┆ city ┆ time_right ┆ city_right │
2496
- --- ┆ --- --- ┆ ---
2497
- str ┆ str str ┆ str
2498
- ╞═══════╪═════════╪════════════╪════════════╡
2499
- 18:00 Toronto null ┆ null │
2500
- │ 18:00 ┆ Berlin ┆ null ┆ null │
2501
- └───────┴─────────┴────────────┴────────────┘
2502
- If this is intentional, use .drop_unmatched() or .keep_unmatched().
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:00Toronto
2578
+ 18:00Berlin
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 │