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/_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()")
91
96
  new._copy_flags(self)
92
- new._unmatched_strategy = UnmatchedStrategy.DROP
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()")
127
+ new._copy_flags(self)
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()
@@ -133,7 +169,7 @@ class SupportsMath(ModelElement, SupportsToExpr):
133
169
  └───────┴─────────┴──────────────────┘
134
170
 
135
171
  >>> m.v.rename({"city": "location"})
136
- <Expression height=12 terms=12 type=linear>
172
+ <Expression (linear) height=12 terms=12>
137
173
  ┌───────┬──────────┬──────────────────┐
138
174
  │ hour ┆ location ┆ expression │
139
175
  │ (4) ┆ (3) ┆ │
@@ -192,7 +228,7 @@ class SupportsMath(ModelElement, SupportsToExpr):
192
228
  ... ]
193
229
  ... )
194
230
  >>> m.v.pick(hour="06:00")
195
- <Expression height=3 terms=3 type=linear>
231
+ <Expression (linear) height=3 terms=3>
196
232
  ┌─────────┬──────────────────┐
197
233
  │ city ┆ expression │
198
234
  │ (3) ┆ │
@@ -202,7 +238,7 @@ class SupportsMath(ModelElement, SupportsToExpr):
202
238
  │ Paris ┆ v[06:00,Paris] │
203
239
  └─────────┴──────────────────┘
204
240
  >>> m.v.pick(hour="06:00", city="Toronto")
205
- <Expression terms=1 type=linear>
241
+ <Expression (linear) terms=1>
206
242
  v[06:00,Toronto]
207
243
 
208
244
  See Also:
@@ -254,7 +290,7 @@ class SupportsMath(ModelElement, SupportsToExpr):
254
290
  >>> m = pf.Model()
255
291
  >>> m.v = pf.Variable()
256
292
  >>> m.v**2
257
- <Expression terms=1 type=quadratic>
293
+ <Expression (quadratic) terms=1>
258
294
  v * v
259
295
  >>> m.v**3
260
296
  Traceback (most recent call last):
@@ -284,7 +320,7 @@ class SupportsMath(ModelElement, SupportsToExpr):
284
320
  >>> df = pl.DataFrame({"dim1": [1, 2, 3], "value": [1, 2, 3]})
285
321
  >>> m.v = pf.Variable(df["dim1"])
286
322
  >>> m.v - df
287
- <Expression height=3 terms=6 type=linear>
323
+ <Expression (linear) height=3 terms=6>
288
324
  ┌──────┬────────────┐
289
325
  │ dim1 ┆ expression │
290
326
  │ (3) ┆ │
@@ -295,7 +331,7 @@ class SupportsMath(ModelElement, SupportsToExpr):
295
331
  └──────┴────────────┘
296
332
  """
297
333
  if not isinstance(other, (int, float)):
298
- other = other.to_expr()
334
+ other = other.to_expr() # TODO don't rely on monkey patch
299
335
  return self.to_expr() + (-other)
300
336
 
301
337
  def __rmul__(self, other):
@@ -312,7 +348,7 @@ class SupportsMath(ModelElement, SupportsToExpr):
312
348
  >>> m = pf.Model()
313
349
  >>> m.v = Variable({"dim1": [1, 2, 3]})
314
350
  >>> m.v / 2
315
- <Expression height=3 terms=3 type=linear>
351
+ <Expression (linear) height=3 terms=3>
316
352
  ┌──────┬────────────┐
317
353
  │ dim1 ┆ expression │
318
354
  │ (3) ┆ │
@@ -324,6 +360,13 @@ class SupportsMath(ModelElement, SupportsToExpr):
324
360
  """
325
361
  return self.to_expr() * (1 / other)
326
362
 
363
+ def __rtruediv__(self, other):
364
+ # This just improves error messages when trying to divide by a Set or Variable.
365
+ # When dividing by an Expression, see the Expression.__rtruediv__ method.
366
+ raise PyoframeError(
367
+ f"Cannot divide by '{self.name}' because it is not a number or parameter."
368
+ )
369
+
327
370
  def __rsub__(self, other):
328
371
  """Supports right subtraction.
329
372
 
@@ -331,7 +374,7 @@ class SupportsMath(ModelElement, SupportsToExpr):
331
374
  >>> m = pf.Model()
332
375
  >>> m.v = Variable({"dim1": [1, 2, 3]})
333
376
  >>> 1 - m.v
334
- <Expression height=3 terms=6 type=linear>
377
+ <Expression (linear) height=3 terms=6>
335
378
  ┌──────┬────────────┐
336
379
  │ dim1 ┆ expression │
337
380
  │ (3) ┆ │
@@ -344,39 +387,22 @@ class SupportsMath(ModelElement, SupportsToExpr):
344
387
  return other + (-self.to_expr())
345
388
 
346
389
  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
390
  return Constraint(self - other, ConstraintSense.LE)
357
391
 
358
- def __ge__(self, other):
359
- """Equality constraint.
392
+ def __lt__(self, _):
393
+ raise PyoframeError(
394
+ "Constraints cannot be created with the '<' or '>' operators. Did you mean to use '<=' or '>=' instead?"
395
+ )
360
396
 
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
- """
397
+ def __ge__(self, other):
368
398
  return Constraint(self - other, ConstraintSense.GE)
369
399
 
370
- def __eq__(self, value: object): # type: ignore
371
- """Equality constraint.
400
+ def __gt__(self, _):
401
+ raise PyoframeError(
402
+ "Constraints cannot be created with the '<' or '>' operator. Did you mean to use '<=' or '>=' instead?"
403
+ )
372
404
 
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
- """
405
+ def __eq__(self, value: object): # type: ignore
380
406
  return Constraint(self - value, ConstraintSense.EQ)
381
407
 
382
408
 
@@ -384,14 +410,14 @@ SetTypes = Union[
384
410
  pl.DataFrame,
385
411
  pd.Index,
386
412
  pd.DataFrame,
387
- SupportsMath,
413
+ BaseOperableBlock,
388
414
  Mapping[str, Sequence[object]],
389
415
  "Set",
390
416
  "Constraint",
391
417
  ]
392
418
 
393
419
 
394
- class Set(SupportsMath):
420
+ class Set(BaseOperableBlock):
395
421
  """A set which can then be used to index variables.
396
422
 
397
423
  Examples:
@@ -482,6 +508,45 @@ class Set(SupportsMath):
482
508
  name=self.name,
483
509
  )
484
510
 
511
+ def drop(self, *dims: str) -> Set:
512
+ """Returns a new Set with the given dimensions dropped.
513
+
514
+ Only unique rows are kept in the resulting Set.
515
+
516
+ Examples:
517
+ >>> xy = pf.Set(x=range(3), y=range(2))
518
+ >>> xy
519
+ <Set 'unnamed' height=6>
520
+ ┌─────┬─────┐
521
+ │ x ┆ y │
522
+ │ (3) ┆ (2) │
523
+ ╞═════╪═════╡
524
+ │ 0 ┆ 0 │
525
+ │ 0 ┆ 1 │
526
+ │ 1 ┆ 0 │
527
+ │ 1 ┆ 1 │
528
+ │ 2 ┆ 0 │
529
+ │ 2 ┆ 1 │
530
+ └─────┴─────┘
531
+ >>> x = xy.drop("y")
532
+ >>> x
533
+ <Set 'unnamed_set.drop(…)' height=3>
534
+ ┌─────┐
535
+ │ x │
536
+ │ (3) │
537
+ ╞═════╡
538
+ │ 0 │
539
+ │ 1 │
540
+ │ 2 │
541
+ └─────┘
542
+ """
543
+ if not dims:
544
+ raise ValueError("At least one dimension must be provided to drop.")
545
+ return self._new(
546
+ self.data.drop(dims).unique(maintain_order=Config.maintain_order),
547
+ name=f"{self.name}.drop(…)",
548
+ )
549
+
485
550
  def __mul__(self, other):
486
551
  if isinstance(other, Set):
487
552
  overlap_dims = set(self.data.columns) & set(other.data.columns)
@@ -513,7 +578,7 @@ class Set(SupportsMath):
513
578
  def __repr__(self):
514
579
  header = get_obj_repr(
515
580
  self,
516
- "unnamed" if self.name == "unnamed_set" else self.name,
581
+ "'unnamed'" if self.name == "unnamed_set" else f"'{self.name}'",
517
582
  height=self.data.height,
518
583
  )
519
584
  data = self._add_shape_to_columns(self.data)
@@ -529,7 +594,7 @@ class Set(SupportsMath):
529
594
  df = pl.DataFrame(set)
530
595
  elif isinstance(set, Constraint):
531
596
  df = set.data.select(set._dimensions_unsafe)
532
- elif isinstance(set, SupportsMath):
597
+ elif isinstance(set, BaseOperableBlock):
533
598
  df = (
534
599
  set.to_expr()
535
600
  .data.drop(RESERVED_COL_KEYS, strict=False)
@@ -570,7 +635,7 @@ class Set(SupportsMath):
570
635
  return df
571
636
 
572
637
 
573
- class Expression(SupportsMath):
638
+ class Expression(BaseOperableBlock):
574
639
  """Represents a linear or quadratic mathematical expression.
575
640
 
576
641
  Examples:
@@ -587,7 +652,7 @@ class Expression(SupportsMath):
587
652
  >>> m.Size = pf.Variable(df.index)
588
653
  >>> expr = df["cost"] * m.Time + df["cost"] * m.Size
589
654
  >>> expr
590
- <Expression height=5 terms=10 type=linear>
655
+ <Expression (linear) height=5 terms=10>
591
656
  ┌──────┬──────┬──────────────────────────────┐
592
657
  │ item ┆ time ┆ expression │
593
658
  │ (2) ┆ (3) ┆ │
@@ -605,14 +670,14 @@ class Expression(SupportsMath):
605
670
  assert VAR_KEY in data.columns, "Missing variable column."
606
671
  assert COEF_KEY in data.columns, "Missing coefficient column."
607
672
 
608
- # Sanity check no duplicates indices
673
+ # Sanity check no duplicates labels
609
674
  if Config.enable_is_duplicated_expression_safety_check:
610
675
  duplicated_mask = data.drop(COEF_KEY).is_duplicated()
611
676
  # In theory this should never happen unless there's a bug in the library
612
677
  if duplicated_mask.any():
613
678
  duplicated_data = data.filter(duplicated_mask)
614
679
  raise ValueError(
615
- f"Cannot create an expression with duplicate indices:\n{duplicated_data}."
680
+ f"Cannot create an expression with duplicate labels:\n{duplicated_data}."
616
681
  )
617
682
 
618
683
  data = _simplify_expr_df(data)
@@ -633,7 +698,7 @@ class Expression(SupportsMath):
633
698
 
634
699
  Examples:
635
700
  >>> pf.Expression.constant(5)
636
- <Expression terms=1 type=constant>
701
+ <Expression (parameter) terms=1>
637
702
  5
638
703
  """
639
704
  return cls(
@@ -642,7 +707,7 @@ class Expression(SupportsMath):
642
707
  COEF_KEY: [constant],
643
708
  VAR_KEY: [CONST_TERM],
644
709
  },
645
- schema={COEF_KEY: pl.Float64, VAR_KEY: KEY_TYPE},
710
+ schema={COEF_KEY: pl.Float64, VAR_KEY: Config.id_dtype},
646
711
  ),
647
712
  name=str(constant),
648
713
  )
@@ -654,7 +719,7 @@ class Expression(SupportsMath):
654
719
  If no dimensions are specified, the sum is taken over all of the expression's dimensions.
655
720
 
656
721
  Examples:
657
- >>> expr = pl.DataFrame(
722
+ >>> expr = pf.Param(
658
723
  ... {
659
724
  ... "time": ["mon", "tue", "wed", "mon", "tue"],
660
725
  ... "place": [
@@ -666,9 +731,9 @@ class Expression(SupportsMath):
666
731
  ... ],
667
732
  ... "tiktok_posts": [1e6, 3e6, 2e6, 1e6, 2e6],
668
733
  ... }
669
- ... ).to_expr()
734
+ ... )
670
735
  >>> expr
671
- <Expression height=5 terms=5 type=constant>
736
+ <Expression (parameter) height=5 terms=5>
672
737
  ┌──────┬───────────┬────────────┐
673
738
  │ time ┆ place ┆ expression │
674
739
  │ (3) ┆ (2) ┆ │
@@ -680,7 +745,7 @@ class Expression(SupportsMath):
680
745
  │ tue ┆ Vancouver ┆ 2000000 │
681
746
  └──────┴───────────┴────────────┘
682
747
  >>> expr.sum("time")
683
- <Expression height=2 terms=2 type=constant>
748
+ <Expression (parameter) height=2 terms=2>
684
749
  ┌───────────┬────────────┐
685
750
  │ place ┆ expression │
686
751
  │ (2) ┆ │
@@ -689,7 +754,7 @@ class Expression(SupportsMath):
689
754
  │ Vancouver ┆ 3000000 │
690
755
  └───────────┴────────────┘
691
756
  >>> expr.sum()
692
- <Expression terms=1 type=constant>
757
+ <Expression (parameter) terms=1>
693
758
  9000000
694
759
 
695
760
  If the given dimensions don't exist, an error will be raised:
@@ -725,7 +790,7 @@ class Expression(SupportsMath):
725
790
  """Like [`Expression.sum`][pyoframe.Expression.sum], but the sum is taken over all dimensions *except* those specified in `by` (just like a `group_by().sum()` operation).
726
791
 
727
792
  Examples:
728
- >>> expr = pl.DataFrame(
793
+ >>> expr = pf.Param(
729
794
  ... {
730
795
  ... "time": ["mon", "tue", "wed", "mon", "tue"],
731
796
  ... "place": [
@@ -737,9 +802,9 @@ class Expression(SupportsMath):
737
802
  ... ],
738
803
  ... "tiktok_posts": [1e6, 3e6, 2e6, 1e6, 2e6],
739
804
  ... }
740
- ... ).to_expr()
805
+ ... )
741
806
  >>> expr
742
- <Expression height=5 terms=5 type=constant>
807
+ <Expression (parameter) height=5 terms=5>
743
808
  ┌──────┬───────────┬────────────┐
744
809
  │ time ┆ place ┆ expression │
745
810
  │ (3) ┆ (2) ┆ │
@@ -752,7 +817,7 @@ class Expression(SupportsMath):
752
817
  └──────┴───────────┴────────────┘
753
818
 
754
819
  >>> expr.sum_by("place")
755
- <Expression height=2 terms=2 type=constant>
820
+ <Expression (parameter) height=2 terms=2>
756
821
  ┌───────────┬────────────┐
757
822
  │ place ┆ expression │
758
823
  │ (2) ┆ │
@@ -815,13 +880,13 @@ class Expression(SupportsMath):
815
880
 
816
881
  Examples:
817
882
  >>> import polars as pl
818
- >>> pop_data = pl.DataFrame(
883
+ >>> pop_data = pf.Param(
819
884
  ... {
820
885
  ... "city": ["Toronto", "Vancouver", "Boston"],
821
886
  ... "year": [2024, 2024, 2024],
822
887
  ... "population": [10, 2, 8],
823
888
  ... }
824
- ... ).to_expr()
889
+ ... )
825
890
  >>> cities_and_countries = pl.DataFrame(
826
891
  ... {
827
892
  ... "city": ["Toronto", "Vancouver", "Boston"],
@@ -829,7 +894,7 @@ class Expression(SupportsMath):
829
894
  ... }
830
895
  ... )
831
896
  >>> pop_data.map(cities_and_countries)
832
- <Expression height=2 terms=2 type=constant>
897
+ <Expression (parameter) height=2 terms=2>
833
898
  ┌──────┬─────────┬────────────┐
834
899
  │ year ┆ country ┆ expression │
835
900
  │ (1) ┆ (2) ┆ │
@@ -839,7 +904,7 @@ class Expression(SupportsMath):
839
904
  └──────┴─────────┴────────────┘
840
905
 
841
906
  >>> pop_data.map(cities_and_countries, drop_shared_dims=False)
842
- <Expression height=3 terms=3 type=constant>
907
+ <Expression (parameter) height=3 terms=3>
843
908
  ┌───────────┬──────┬─────────┬────────────┐
844
909
  │ city ┆ year ┆ country ┆ expression │
845
910
  │ (3) ┆ (1) ┆ (2) ┆ │
@@ -909,7 +974,7 @@ class Expression(SupportsMath):
909
974
  >>> m = pf.Model()
910
975
  >>> m.quantity = pf.Variable(cost[["item", "time"]])
911
976
  >>> (m.quantity * cost).rolling_sum(over="time", window_size=2)
912
- <Expression height=5 terms=8 type=linear>
977
+ <Expression (linear) height=5 terms=8>
913
978
  ┌──────┬──────┬──────────────────────────────────┐
914
979
  │ item ┆ time ┆ expression │
915
980
  │ (2) ┆ (3) ┆ │
@@ -945,11 +1010,8 @@ class Expression(SupportsMath):
945
1010
  """Filters this expression to only include the dimensions within the provided set.
946
1011
 
947
1012
  Examples:
948
- >>> import pandas as pd
949
- >>> general_expr = pd.DataFrame(
950
- ... {"dim1": [1, 2, 3], "value": [1, 2, 3]}
951
- ... ).to_expr()
952
- >>> filter_expr = pd.DataFrame({"dim1": [1, 3], "value": [5, 6]}).to_expr()
1013
+ >>> general_expr = pf.Param({"dim1": [1, 2, 3], "value": [1, 2, 3]})
1014
+ >>> filter_expr = pf.Param({"dim1": [1, 3], "value": [5, 6]})
953
1015
  >>> general_expr.within(filter_expr).data
954
1016
  shape: (2, 3)
955
1017
  ┌──────┬─────────┬───────────────┐
@@ -1010,11 +1072,10 @@ class Expression(SupportsMath):
1010
1072
  If `False`, returns the degree as an integer (0, 1, or 2).
1011
1073
 
1012
1074
  Examples:
1013
- >>> import pandas as pd
1014
1075
  >>> m = pf.Model()
1015
1076
  >>> m.v1 = pf.Variable()
1016
1077
  >>> m.v2 = pf.Variable()
1017
- >>> expr = pd.DataFrame({"dim1": [1, 2, 3], "value": [1, 2, 3]}).to_expr()
1078
+ >>> expr = pf.Param({"dim1": [1, 2, 3], "value": [1, 2, 3]})
1018
1079
  >>> expr.degree()
1019
1080
  0
1020
1081
  >>> expr *= m.v1
@@ -1032,18 +1093,17 @@ class Expression(SupportsMath):
1032
1093
  elif (self.data.get_column(VAR_KEY) != CONST_TERM).any():
1033
1094
  return "linear" if return_str else 1
1034
1095
  else:
1035
- return "constant" if return_str else 0
1096
+ return "parameter" if return_str else 0
1036
1097
 
1037
1098
  def __add__(self, other):
1038
1099
  """Adds another expression or a constant to this expression.
1039
1100
 
1040
1101
  Examples:
1041
- >>> import pandas as pd
1042
1102
  >>> m = pf.Model()
1043
- >>> add = pd.DataFrame({"dim1": [1, 2, 3], "add": [10, 20, 30]}).to_expr()
1103
+ >>> add = pf.Param({"dim1": [1, 2, 3], "add": [10, 20, 30]})
1044
1104
  >>> m.v = Variable(add)
1045
1105
  >>> m.v + add
1046
- <Expression height=3 terms=6 type=linear>
1106
+ <Expression (linear) height=3 terms=6>
1047
1107
  ┌──────┬────────────┐
1048
1108
  │ dim1 ┆ expression │
1049
1109
  │ (3) ┆ │
@@ -1054,7 +1114,7 @@ class Expression(SupportsMath):
1054
1114
  └──────┴────────────┘
1055
1115
 
1056
1116
  >>> m.v + add + 2
1057
- <Expression height=3 terms=6 type=linear>
1117
+ <Expression (linear) height=3 terms=6>
1058
1118
  ┌──────┬────────────┐
1059
1119
  │ dim1 ┆ expression │
1060
1120
  │ (3) ┆ │
@@ -1067,31 +1127,29 @@ class Expression(SupportsMath):
1067
1127
  >>> m.v + pd.DataFrame({"dim1": [1, 2], "add": [10, 20]})
1068
1128
  Traceback (most recent call last):
1069
1129
  ...
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().
1130
+ pyoframe._constants.PyoframeError: Cannot add the two expressions below because expression 1 has extra labels.
1131
+ Expression 1: v
1132
+ Expression 2: add
1133
+ Extra labels in expression 1:
1134
+ ┌──────┐
1135
+ │ dim1 │
1136
+ ╞══════╡
1137
+ 3
1138
+ └──────┘
1139
+ Use .drop_extras() or .keep_extras() to indicate how the extra labels should be handled. Learn more at
1140
+ https://bravos-power.github.io/pyoframe/latest/learn/concepts/addition
1083
1141
  >>> m.v2 = Variable()
1084
1142
  >>> 5 + 2 * m.v2
1085
- <Expression terms=2 type=linear>
1143
+ <Expression (linear) terms=2>
1086
1144
  2 v2 +5
1087
1145
  """
1088
1146
  if isinstance(other, (int, float)):
1089
1147
  return self._add_const(other)
1090
- other = other.to_expr()
1148
+ other = other.to_expr() # TODO don't rely on monkey patch
1091
1149
  self._learn_from_other(other)
1092
1150
  return add(self, other)
1093
1151
 
1094
- def __mul__(self: Expression, other: int | float | SupportsToExpr) -> Expression:
1152
+ def __mul__(self: Expression, other: Operable) -> Expression:
1095
1153
  if isinstance(other, (int, float)):
1096
1154
  if other == 1:
1097
1155
  return self
@@ -1100,10 +1158,25 @@ class Expression(SupportsMath):
1100
1158
  name=f"({other} * {self.name})",
1101
1159
  )
1102
1160
 
1103
- other = other.to_expr()
1161
+ other: Expression = other.to_expr() # TODO don't rely on monkey patch
1104
1162
  self._learn_from_other(other)
1105
1163
  return multiply(self, other)
1106
1164
 
1165
+ def __rtruediv__(self, other):
1166
+ """Support dividing by an expression when that expression is a constant."""
1167
+ assert isinstance(other, (int, float)), (
1168
+ f"Expected a number not a {type(other)} when dividing by an expression."
1169
+ )
1170
+ if self.degree() != 0:
1171
+ raise PyoframeError(
1172
+ f"Cannot divide by '{self.name}' because denominators cannot contain variables."
1173
+ )
1174
+
1175
+ return self._new(
1176
+ self.data.with_columns((pl.lit(other) / pl.col(COEF_KEY)).alias(COEF_KEY)),
1177
+ name=f"({other} / {self.name})",
1178
+ )
1179
+
1107
1180
  def to_expr(self) -> Expression:
1108
1181
  """Returns the expression itself."""
1109
1182
  return self
@@ -1125,13 +1198,13 @@ class Expression(SupportsMath):
1125
1198
  >>> m.x1 = Variable()
1126
1199
  >>> m.x2 = Variable()
1127
1200
  >>> m.x1 + 5
1128
- <Expression terms=2 type=linear>
1201
+ <Expression (linear) terms=2>
1129
1202
  x1 +5
1130
1203
  >>> m.x1**2 + 5
1131
- <Expression terms=2 type=quadratic>
1204
+ <Expression (quadratic) terms=2>
1132
1205
  x1 * x1 +5
1133
1206
  >>> m.x1**2 + m.x2 + 5
1134
- <Expression terms=3 type=quadratic>
1207
+ <Expression (quadratic) terms=3>
1135
1208
  x1 * x1 + x2 +5
1136
1209
 
1137
1210
  It also works with dimensions
@@ -1139,7 +1212,7 @@ class Expression(SupportsMath):
1139
1212
  >>> m = pf.Model()
1140
1213
  >>> m.v = Variable({"dim1": [1, 2, 3]})
1141
1214
  >>> m.v * m.v + 5
1142
- <Expression height=3 terms=6 type=quadratic>
1215
+ <Expression (quadratic) height=3 terms=6>
1143
1216
  ┌──────┬─────────────────┐
1144
1217
  │ dim1 ┆ expression │
1145
1218
  │ (3) ┆ │
@@ -1158,11 +1231,11 @@ class Expression(SupportsMath):
1158
1231
  if CONST_TERM not in data[VAR_KEY]:
1159
1232
  const_df = pl.DataFrame(
1160
1233
  {COEF_KEY: [0.0], VAR_KEY: [CONST_TERM]},
1161
- schema={COEF_KEY: pl.Float64, VAR_KEY: KEY_TYPE},
1234
+ schema={COEF_KEY: pl.Float64, VAR_KEY: Config.id_dtype},
1162
1235
  )
1163
1236
  if self.is_quadratic:
1164
1237
  const_df = const_df.with_columns(
1165
- pl.lit(CONST_TERM).alias(QUAD_VAR_KEY).cast(KEY_TYPE)
1238
+ pl.lit(CONST_TERM).alias(QUAD_VAR_KEY).cast(Config.id_dtype)
1166
1239
  )
1167
1240
  data = pl.concat(
1168
1241
  [data, const_df],
@@ -1172,11 +1245,11 @@ class Expression(SupportsMath):
1172
1245
  keys = (
1173
1246
  data.select(dim)
1174
1247
  .unique(maintain_order=Config.maintain_order)
1175
- .with_columns(pl.lit(CONST_TERM).alias(VAR_KEY).cast(KEY_TYPE))
1248
+ .with_columns(pl.lit(CONST_TERM).alias(VAR_KEY).cast(Config.id_dtype))
1176
1249
  )
1177
1250
  if self.is_quadratic:
1178
1251
  keys = keys.with_columns(
1179
- pl.lit(CONST_TERM).alias(QUAD_VAR_KEY).cast(KEY_TYPE)
1252
+ pl.lit(CONST_TERM).alias(QUAD_VAR_KEY).cast(Config.id_dtype)
1180
1253
  )
1181
1254
  data = data.join(
1182
1255
  keys,
@@ -1219,7 +1292,7 @@ class Expression(SupportsMath):
1219
1292
  if len(constant_terms) == 0:
1220
1293
  return pl.DataFrame(
1221
1294
  {COEF_KEY: [0.0], VAR_KEY: [CONST_TERM]},
1222
- schema={COEF_KEY: pl.Float64, VAR_KEY: KEY_TYPE},
1295
+ schema={COEF_KEY: pl.Float64, VAR_KEY: Config.id_dtype},
1223
1296
  )
1224
1297
  return constant_terms
1225
1298
 
@@ -1314,16 +1387,22 @@ class Expression(SupportsMath):
1314
1387
  "._to_poi() only works for non-dimensioned expressions."
1315
1388
  )
1316
1389
 
1390
+ data = self.data
1391
+
1317
1392
  if self.is_quadratic:
1393
+ # Workaround for bug https://github.com/metab0t/PyOptInterface/issues/59
1394
+ if self._model is None or self._model.solver.name == "highs":
1395
+ data = data.sort(VAR_KEY, QUAD_VAR_KEY, descending=False)
1396
+
1318
1397
  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(),
1398
+ coefficients=data.get_column(COEF_KEY).to_numpy(),
1399
+ var1s=data.get_column(VAR_KEY).to_numpy(),
1400
+ var2s=data.get_column(QUAD_VAR_KEY).to_numpy(),
1322
1401
  )
1323
1402
  else:
1324
1403
  return poi.ScalarAffineFunction(
1325
- coefficients=self.data.get_column(COEF_KEY).to_numpy(),
1326
- variables=self.data.get_column(VAR_KEY).to_numpy(),
1404
+ coefficients=data.get_column(COEF_KEY).to_numpy(),
1405
+ variables=data.get_column(VAR_KEY).to_numpy(),
1327
1406
  )
1328
1407
 
1329
1408
  @overload
@@ -1496,9 +1575,9 @@ class Expression(SupportsMath):
1496
1575
  """Returns a string representation of the expression's header."""
1497
1576
  return get_obj_repr(
1498
1577
  self,
1578
+ f"({self.degree(return_str=True)})",
1499
1579
  height=len(self) if self.dimensions else None,
1500
1580
  terms=self.terms,
1501
- type=self.degree(return_str=True),
1502
1581
  )
1503
1582
 
1504
1583
  def __repr__(self) -> str:
@@ -1519,7 +1598,7 @@ class Expression(SupportsMath):
1519
1598
  >>> m.v = pf.Variable({"t": [1, 2]})
1520
1599
  >>> coef = pl.DataFrame({"t": [1, 2], "coef": [0, 1]})
1521
1600
  >>> coef * (m.v + 4)
1522
- <Expression height=2 terms=3 type=linear>
1601
+ <Expression (linear) height=2 terms=3>
1523
1602
  ┌─────┬────────────┐
1524
1603
  │ t ┆ expression │
1525
1604
  │ (2) ┆ │
@@ -1534,16 +1613,16 @@ class Expression(SupportsMath):
1534
1613
 
1535
1614
 
1536
1615
  @overload
1537
- def sum(over: str | Sequence[str], expr: SupportsToExpr) -> Expression: ...
1616
+ def sum(over: str | Sequence[str], expr: Operable) -> Expression: ...
1538
1617
 
1539
1618
 
1540
1619
  @overload
1541
- def sum(over: SupportsToExpr) -> Expression: ...
1620
+ def sum(over: Operable) -> Expression: ...
1542
1621
 
1543
1622
 
1544
1623
  def sum(
1545
- over: str | Sequence[str] | SupportsToExpr,
1546
- expr: SupportsToExpr | None = None,
1624
+ over: str | Sequence[str] | Operable,
1625
+ expr: Operable | None = None,
1547
1626
  ) -> Expression: # pragma: no cover
1548
1627
  """Deprecated: Use Expression.sum() or Variable.sum() instead.
1549
1628
 
@@ -1560,7 +1639,7 @@ def sum(
1560
1639
  )
1561
1640
 
1562
1641
  if expr is None:
1563
- assert isinstance(over, SupportsMath)
1642
+ assert isinstance(over, BaseOperableBlock)
1564
1643
  return over.to_expr().sum()
1565
1644
  else:
1566
1645
  assert isinstance(over, (str, Sequence))
@@ -1569,9 +1648,7 @@ def sum(
1569
1648
  return expr.to_expr().sum(*over)
1570
1649
 
1571
1650
 
1572
- def sum_by(
1573
- by: str | Sequence[str], expr: SupportsToExpr
1574
- ) -> Expression: # pragma: no cover
1651
+ def sum_by(by: str | Sequence[str], expr: Operable) -> Expression: # pragma: no cover
1575
1652
  """Deprecated: Use Expression.sum() or Variable.sum() instead."""
1576
1653
  warnings.warn(
1577
1654
  "pf.sum_by() is deprecated. Use Expression.sum_by() or Variable.sum_by() instead.",
@@ -1583,7 +1660,7 @@ def sum_by(
1583
1660
  return expr.to_expr().sum_by(*by)
1584
1661
 
1585
1662
 
1586
- class Constraint(ModelElementWithId):
1663
+ class Constraint(BaseBlock):
1587
1664
  """An optimization constraint that can be added to a [Model][pyoframe.Model].
1588
1665
 
1589
1666
  Tip: Implementation Note
@@ -1699,7 +1776,7 @@ class Constraint(ModelElementWithId):
1699
1776
  # GRBaddconstr uses sprintf when no name or "" is given. sprintf is slow. As such, we specify "C" as the name.
1700
1777
  # Specifying "" is the same as not specifying anything, see pyoptinterface:
1701
1778
  # https://github.com/metab0t/PyOptInterface/blob/6d61f3738ad86379cff71fee77077d4ea919f2d5/lib/gurobi_model.cpp#L338
1702
- name = "C" if self._model.solver.block_auto_names else ""
1779
+ name = "C" if self._model.solver.accelerate_with_repeat_names else ""
1703
1780
 
1704
1781
  if dims is None:
1705
1782
  if self._model.solver_uses_variable_names:
@@ -1709,23 +1786,25 @@ class Constraint(ModelElementWithId):
1709
1786
  if is_quadratic
1710
1787
  else poi.ScalarAffineFunction.from_numpy # when called only once from_numpy is faster
1711
1788
  )
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
1789
+ constr_id = add_constraint(
1790
+ create_expression(
1791
+ *(
1792
+ df.get_column(c).to_numpy()
1793
+ for c in ([COEF_KEY] + self.lhs._variable_columns)
1794
+ )
1795
+ ),
1796
+ sense,
1797
+ 0,
1798
+ name,
1799
+ ).index
1800
+ try:
1801
+ df = self.data.with_columns(
1802
+ pl.lit(constr_id).alias(CONSTRAINT_KEY).cast(Config.id_dtype)
1725
1803
  )
1726
- .alias(CONSTRAINT_KEY)
1727
- .cast(KEY_TYPE)
1728
- )
1804
+ except TypeError as e:
1805
+ raise TypeError(
1806
+ f"Number of constraints exceeds the current data type ({Config.id_dtype}). Consider increasing the data type by changing Config.id_dtype."
1807
+ ) from e
1729
1808
  else:
1730
1809
  create_expression = (
1731
1810
  poi.ScalarQuadraticFunction
@@ -1814,9 +1893,14 @@ class Constraint(ModelElementWithId):
1814
1893
  ).index
1815
1894
  for s0, s1 in pairwise(split)
1816
1895
  ]
1817
- df = df_unique.with_columns(
1818
- pl.Series(ids, dtype=KEY_TYPE).alias(CONSTRAINT_KEY)
1819
- )
1896
+ try:
1897
+ df = df_unique.with_columns(
1898
+ pl.Series(ids, dtype=Config.id_dtype).alias(CONSTRAINT_KEY)
1899
+ )
1900
+ except TypeError as e:
1901
+ raise TypeError(
1902
+ f"Number of constraints exceeds the current data type ({Config.id_dtype}). Consider increasing the data type by changing Config.id_dtype."
1903
+ ) from e
1820
1904
 
1821
1905
  self._data = df
1822
1906
 
@@ -1866,9 +1950,7 @@ class Constraint(ModelElementWithId):
1866
1950
  """Syntactic sugar on `Constraint.lhs.data.filter()`, to help debugging."""
1867
1951
  return self.lhs.data.filter(*args, **kwargs)
1868
1952
 
1869
- def relax(
1870
- self, cost: SupportsToExpr, max: SupportsToExpr | None = None
1871
- ) -> Constraint:
1953
+ def relax(self, cost: Operable, max: Operable | None = None) -> Constraint:
1872
1954
  """Allows the constraint to be violated at a `cost` and, optionally, up to a maximum.
1873
1955
 
1874
1956
  Warning:
@@ -2124,31 +2206,35 @@ class Constraint(ModelElementWithId):
2124
2206
  return (
2125
2207
  get_obj_repr(
2126
2208
  self,
2127
- self.name,
2209
+ f"'{self.name}'",
2210
+ f"({self.lhs.degree(return_str=True)})",
2128
2211
  height=len(self) if self.dimensions else None,
2129
2212
  terms=len(self.lhs.data),
2130
- type=self.lhs.degree(return_str=True),
2131
2213
  )
2132
2214
  + "\n"
2133
2215
  + self.to_str()
2134
2216
  )
2135
2217
 
2136
2218
 
2137
- class Variable(ModelElementWithId, SupportsMath):
2219
+ class Variable(BaseOperableBlock):
2138
2220
  """A decision variable for an optimization model.
2139
2221
 
2222
+ !!! tip
2223
+ If `lb` or `ub` are a dimensioned object (e.g. an [Expression][pyoframe.Expression]), they will automatically be [broadcasted](../../learn/concepts/addition.md#adding-expressions-with-differing-dimensions-using-over) to match the variable's dimensions.
2224
+
2140
2225
  Parameters:
2141
2226
  *indexing_sets:
2142
2227
  If no indexing_sets are provided, a single variable with no dimensions is created.
2143
2228
  Otherwise, a variable is created for each element in the Cartesian product of the indexing_sets (see Set for details on behaviour).
2144
- lb:
2145
- The lower bound for all variables.
2146
- ub:
2147
- The upper bound for all variables.
2148
2229
  vtype:
2149
2230
  The type of the variable. Can be either a VType enum or a string. Default is VType.CONTINUOUS.
2231
+ lb:
2232
+ The lower bound for the variables.
2233
+ ub:
2234
+ The upper bound for the variables.
2150
2235
  equals:
2151
- When specified, a variable is created and a constraint is added to make the variable equal to the provided expression.
2236
+ When specified, a variable is created for every label in `equals` and a constraint is added to make the variable equal to the provided expression.
2237
+ `indexing_sets` cannot be provided when using `equals`.
2152
2238
 
2153
2239
  Examples:
2154
2240
  >>> import pandas as pd
@@ -2216,29 +2302,44 @@ class Variable(ModelElementWithId, SupportsMath):
2216
2302
  def __init__(
2217
2303
  self,
2218
2304
  *indexing_sets: SetTypes | Iterable[SetTypes],
2219
- lb: float | int | SupportsToExpr | None = None,
2220
- ub: float | int | SupportsToExpr | None = None,
2305
+ lb: Operable | None = None,
2306
+ ub: Operable | None = None,
2221
2307
  vtype: VType | VTypeValue = VType.CONTINUOUS,
2222
- equals: SupportsToExpr | None = None,
2308
+ equals: Operable | None = None,
2223
2309
  ):
2224
2310
  if equals is not None:
2225
- assert len(indexing_sets) == 0, (
2226
- "Cannot specify both 'equals' and 'indexing_sets'"
2227
- )
2228
- indexing_sets = (equals,)
2311
+ if isinstance(equals, (float, int)):
2312
+ if lb is not None:
2313
+ raise ValueError("Cannot specify 'lb' when 'equals' is a constant.")
2314
+ if ub is not None:
2315
+ raise ValueError("Cannot specify 'ub' when 'equals' is a constant.")
2316
+ lb = ub = equals
2317
+ equals = None
2318
+ else:
2319
+ assert len(indexing_sets) == 0, (
2320
+ "Cannot specify both 'equals' and 'indexing_sets'"
2321
+ )
2322
+ equals = equals.to_expr() # TODO don't rely on monkey patch
2323
+ indexing_sets = (equals,)
2229
2324
 
2230
2325
  data = Set(*indexing_sets).data if len(indexing_sets) > 0 else pl.DataFrame()
2231
2326
  super().__init__(data)
2232
2327
 
2233
2328
  self.vtype: VType = VType(vtype)
2234
2329
  self._attr = Container(self._set_attribute, self._get_attribute)
2235
- self._equals = equals
2330
+ self._equals: Expression | None = equals
2236
2331
 
2237
2332
  if lb is not None and not isinstance(lb, (float, int)):
2333
+ lb: Expression = lb.to_expr() # TODO don't rely on monkey patch
2334
+ if not self.dimensionless:
2335
+ lb = lb.over(*self.dimensions)
2238
2336
  self._lb_expr, self.lb = lb, None
2239
2337
  else:
2240
2338
  self._lb_expr, self.lb = None, lb
2241
2339
  if ub is not None and not isinstance(ub, (float, int)):
2340
+ ub = ub.to_expr() # TODO don't rely on monkey patch
2341
+ if not self.dimensionless:
2342
+ ub = ub.over(*self.dimensions) # pyright: ignore[reportOptionalIterable]
2242
2343
  self._ub_expr, self.ub = ub, None
2243
2344
  else:
2244
2345
  self._ub_expr, self.ub = None, ub
@@ -2319,7 +2420,7 @@ class Variable(ModelElementWithId, SupportsMath):
2319
2420
  else:
2320
2421
  if self._model.solver_uses_variable_names:
2321
2422
  name = self.name
2322
- elif solver.block_auto_names:
2423
+ elif solver.accelerate_with_repeat_names:
2323
2424
  name = "V"
2324
2425
  else:
2325
2426
  name = ""
@@ -2331,7 +2432,14 @@ class Variable(ModelElementWithId, SupportsMath):
2331
2432
  else:
2332
2433
  ids = [poi_add_var(lb, ub, name=name).index for _ in range(n)]
2333
2434
 
2334
- df = self.data.with_columns(pl.Series(ids, dtype=KEY_TYPE).alias(VAR_KEY))
2435
+ try:
2436
+ df = self.data.with_columns(
2437
+ pl.Series(ids, dtype=Config.id_dtype).alias(VAR_KEY)
2438
+ )
2439
+ except TypeError as e:
2440
+ raise TypeError(
2441
+ f"Number of variables exceeds the current data type ({Config.id_dtype}). Consider increasing the data type by changing Config.id_dtype."
2442
+ ) from e
2335
2443
 
2336
2444
  self._data = df
2337
2445
 
@@ -2437,7 +2545,7 @@ class Variable(ModelElementWithId, SupportsMath):
2437
2545
  result = (
2438
2546
  get_obj_repr(
2439
2547
  self,
2440
- self.name,
2548
+ f"'{self.name}'",
2441
2549
  lb=self.lb,
2442
2550
  ub=self.ub,
2443
2551
  height=self.data.height if self.dimensions else None,
@@ -2467,13 +2575,13 @@ class Variable(ModelElementWithId, SupportsMath):
2467
2575
 
2468
2576
  @return_new
2469
2577
  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.
2578
+ """Creates an expression where the variable at each label is the next variable in the specified dimension.
2471
2579
 
2472
2580
  Parameters:
2473
2581
  dim:
2474
2582
  The dimension over which to shift the variable.
2475
2583
  wrap_around:
2476
- If `True`, the last index in the dimension is connected to the first index.
2584
+ If `True`, the last label in the dimension is connected to the first label.
2477
2585
 
2478
2586
  Examples:
2479
2587
  >>> import pandas as pd
@@ -2486,25 +2594,21 @@ class Variable(ModelElementWithId, SupportsMath):
2486
2594
  >>> m.bat_charge + m.bat_flow == m.bat_charge.next("time")
2487
2595
  Traceback (most recent call last):
2488
2596
  ...
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
- ... )
2507
- <Constraint 'unnamed' height=6 terms=18 type=linear>
2597
+ pyoframe._constants.PyoframeError: Cannot subtract the two expressions below because expression 1 has extra labels.
2598
+ Expression 1: (bat_charge + bat_flow)
2599
+ Expression 2: bat_charge.next(…)
2600
+ Extra labels in expression 1:
2601
+ ┌───────┬─────────┐
2602
+ │ time ┆ city │
2603
+ ╞═══════╪═════════╡
2604
+ 18:00Toronto
2605
+ 18:00Berlin
2606
+ └───────┴─────────┘
2607
+ Use .drop_extras() or .keep_extras() to indicate how the extra labels should be handled. Learn more at
2608
+ https://bravos-power.github.io/pyoframe/latest/learn/concepts/addition
2609
+
2610
+ >>> (m.bat_charge + m.bat_flow).drop_extras() == m.bat_charge.next("time")
2611
+ <Constraint 'unnamed' (linear) height=6 terms=18>
2508
2612
  ┌───────┬─────────┬────────────────────────────────────────────────────────────────────────────────┐
2509
2613
  │ time ┆ city ┆ constraint │
2510
2614
  │ (3) ┆ (2) ┆ │
@@ -2526,7 +2630,7 @@ class Variable(ModelElementWithId, SupportsMath):
2526
2630
  >>> (m.bat_charge + m.bat_flow) == m.bat_charge.next(
2527
2631
  ... "time", wrap_around=True
2528
2632
  ... )
2529
- <Constraint 'unnamed' height=8 terms=24 type=linear>
2633
+ <Constraint 'unnamed' (linear) height=8 terms=24>
2530
2634
  ┌───────┬─────────┬────────────────────────────────────────────────────────────────────────────────┐
2531
2635
  │ time ┆ city ┆ constraint │
2532
2636
  │ (4) ┆ (2) ┆ │