pyoframe 1.0.1__py3-none-any.whl → 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pyoframe/__init__.py CHANGED
@@ -11,6 +11,7 @@ from pyoframe._core import Constraint, Expression, Set, Variable, sum, sum_by
11
11
  from pyoframe._model import Model
12
12
  from pyoframe._monkey_patch import patch_dataframe_libraries
13
13
  from pyoframe._objective import Objective
14
+ from pyoframe._param import Param
14
15
 
15
16
  try:
16
17
  from pyoframe._version import __version__, __version_tuple__ # noqa: F401
@@ -25,6 +26,7 @@ __all__ = [
25
26
  "Expression",
26
27
  "Constraint",
27
28
  "Objective",
29
+ "Param",
28
30
  "Set",
29
31
  "Config",
30
32
  "sum",
pyoframe/_arithmetic.py CHANGED
@@ -42,7 +42,7 @@ def multiply(self: Expression, other: Expression) -> Expression:
42
42
  >>> m.x3 = pf.Variable()
43
43
  >>> result = 5 * m.x1 * m.x2
44
44
  >>> result
45
- <Expression terms=1 type=quadratic>
45
+ <Expression (quadratic) terms=1>
46
46
  5 x2 * x1
47
47
  >>> result * m.x3
48
48
  Traceback (most recent call last):
@@ -112,7 +112,7 @@ def _quadratic_multiplication(self: Expression, other: Expression) -> Expression
112
112
  >>> expr1 = df * m.x1
113
113
  >>> expr2 = df * m.x2 * 2 + 4
114
114
  >>> expr1 * expr2
115
- <Expression height=3 terms=6 type=quadratic>
115
+ <Expression (quadratic) height=3 terms=6>
116
116
  ┌─────┬───────────────────┐
117
117
  │ dim ┆ expression │
118
118
  │ (3) ┆ │
@@ -122,7 +122,7 @@ def _quadratic_multiplication(self: Expression, other: Expression) -> Expression
122
122
  │ 3 ┆ 12 x1 +18 x2 * x1 │
123
123
  └─────┴───────────────────┘
124
124
  >>> (expr1 * expr2) - df * m.x1 * df * m.x2 * 2
125
- <Expression height=3 terms=3 type=linear>
125
+ <Expression (linear) height=3 terms=3>
126
126
  ┌─────┬────────────┐
127
127
  │ dim ┆ expression │
128
128
  │ (3) ┆ │
pyoframe/_constants.py CHANGED
@@ -141,19 +141,18 @@ class _Config:
141
141
  Only consider enabling after you have thoroughly tested your code.
142
142
 
143
143
  Examples:
144
- >>> import polars as pl
145
- >>> population = pl.DataFrame(
144
+ >>> population = pf.Param(
146
145
  ... {
147
146
  ... "city": ["Toronto", "Vancouver", "Montreal"],
148
147
  ... "pop": [2_731_571, 631_486, 1_704_694],
149
148
  ... }
150
- ... ).to_expr()
151
- >>> population_influx = pl.DataFrame(
149
+ ... )
150
+ >>> population_influx = pf.Param(
152
151
  ... {
153
152
  ... "city": ["Toronto", "Vancouver", "Montreal"],
154
153
  ... "influx": [100_000, 50_000, None],
155
154
  ... }
156
- ... ).to_expr()
155
+ ... )
157
156
 
158
157
  Normally, an error warns users that the two expressions have conflicting labels:
159
158
  >>> population + population_influx
@@ -174,7 +173,7 @@ class _Config:
174
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()`:
175
174
  >>> pf.Config.disable_extras_checks = True
176
175
  >>> population + population_influx
177
- <Expression height=3 terms=3 type=constant>
176
+ <Expression (parameter) height=3 terms=3>
178
177
  ┌───────────┬────────────┐
179
178
  │ city ┆ expression │
180
179
  │ (3) ┆ │
@@ -228,11 +227,11 @@ class _Config:
228
227
  >>> m.X = pf.Variable()
229
228
  >>> expr = 100.752038759 * m.X
230
229
  >>> expr
231
- <Expression terms=1 type=linear>
230
+ <Expression (linear) terms=1>
232
231
  100.752 X
233
232
  >>> pf.Config.float_to_str_precision = None
234
233
  >>> expr
235
- <Expression terms=1 type=linear>
234
+ <Expression (linear) terms=1>
236
235
  100.752038759 X
237
236
  """
238
237
  return self._settings.float_to_str_precision
@@ -280,7 +279,7 @@ class _Config:
280
279
  >>> m = pf.Model()
281
280
  >>> m.X = pf.Variable(pf.Set(x=range(100)), pf.Set(y=range(100)))
282
281
  >>> m.X.sum("y")
283
- <Expression height=100 terms=10000 type=linear>
282
+ <Expression (linear) height=100 terms=10000>
284
283
  ┌───────┬───────────────────────────────┐
285
284
  │ x ┆ expression │
286
285
  │ (100) ┆ │
@@ -298,7 +297,7 @@ class _Config:
298
297
  │ 99 ┆ X[99,0] + X[99,1] + X[99,2] … │
299
298
  └───────┴───────────────────────────────┘
300
299
  >>> m.X.sum()
301
- <Expression terms=10000 type=linear>
300
+ <Expression (linear) terms=10000>
302
301
  X[0,0] + X[0,1] + X[0,2] …
303
302
  """
304
303
  return self._settings.print_max_terms
pyoframe/_core.py CHANGED
@@ -169,7 +169,7 @@ class BaseOperableBlock(BaseBlock):
169
169
  └───────┴─────────┴──────────────────┘
170
170
 
171
171
  >>> m.v.rename({"city": "location"})
172
- <Expression height=12 terms=12 type=linear>
172
+ <Expression (linear) height=12 terms=12>
173
173
  ┌───────┬──────────┬──────────────────┐
174
174
  │ hour ┆ location ┆ expression │
175
175
  │ (4) ┆ (3) ┆ │
@@ -228,7 +228,7 @@ class BaseOperableBlock(BaseBlock):
228
228
  ... ]
229
229
  ... )
230
230
  >>> m.v.pick(hour="06:00")
231
- <Expression height=3 terms=3 type=linear>
231
+ <Expression (linear) height=3 terms=3>
232
232
  ┌─────────┬──────────────────┐
233
233
  │ city ┆ expression │
234
234
  │ (3) ┆ │
@@ -238,7 +238,7 @@ class BaseOperableBlock(BaseBlock):
238
238
  │ Paris ┆ v[06:00,Paris] │
239
239
  └─────────┴──────────────────┘
240
240
  >>> m.v.pick(hour="06:00", city="Toronto")
241
- <Expression terms=1 type=linear>
241
+ <Expression (linear) terms=1>
242
242
  v[06:00,Toronto]
243
243
 
244
244
  See Also:
@@ -290,7 +290,7 @@ class BaseOperableBlock(BaseBlock):
290
290
  >>> m = pf.Model()
291
291
  >>> m.v = pf.Variable()
292
292
  >>> m.v**2
293
- <Expression terms=1 type=quadratic>
293
+ <Expression (quadratic) terms=1>
294
294
  v * v
295
295
  >>> m.v**3
296
296
  Traceback (most recent call last):
@@ -320,7 +320,7 @@ class BaseOperableBlock(BaseBlock):
320
320
  >>> df = pl.DataFrame({"dim1": [1, 2, 3], "value": [1, 2, 3]})
321
321
  >>> m.v = pf.Variable(df["dim1"])
322
322
  >>> m.v - df
323
- <Expression height=3 terms=6 type=linear>
323
+ <Expression (linear) height=3 terms=6>
324
324
  ┌──────┬────────────┐
325
325
  │ dim1 ┆ expression │
326
326
  │ (3) ┆ │
@@ -331,7 +331,7 @@ class BaseOperableBlock(BaseBlock):
331
331
  └──────┴────────────┘
332
332
  """
333
333
  if not isinstance(other, (int, float)):
334
- other = other.to_expr()
334
+ other = other.to_expr() # TODO don't rely on monkey patch
335
335
  return self.to_expr() + (-other)
336
336
 
337
337
  def __rmul__(self, other):
@@ -348,7 +348,7 @@ class BaseOperableBlock(BaseBlock):
348
348
  >>> m = pf.Model()
349
349
  >>> m.v = Variable({"dim1": [1, 2, 3]})
350
350
  >>> m.v / 2
351
- <Expression height=3 terms=3 type=linear>
351
+ <Expression (linear) height=3 terms=3>
352
352
  ┌──────┬────────────┐
353
353
  │ dim1 ┆ expression │
354
354
  │ (3) ┆ │
@@ -360,6 +360,13 @@ class BaseOperableBlock(BaseBlock):
360
360
  """
361
361
  return self.to_expr() * (1 / other)
362
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
+
363
370
  def __rsub__(self, other):
364
371
  """Supports right subtraction.
365
372
 
@@ -367,7 +374,7 @@ class BaseOperableBlock(BaseBlock):
367
374
  >>> m = pf.Model()
368
375
  >>> m.v = Variable({"dim1": [1, 2, 3]})
369
376
  >>> 1 - m.v
370
- <Expression height=3 terms=6 type=linear>
377
+ <Expression (linear) height=3 terms=6>
371
378
  ┌──────┬────────────┐
372
379
  │ dim1 ┆ expression │
373
380
  │ (3) ┆ │
@@ -571,7 +578,7 @@ class Set(BaseOperableBlock):
571
578
  def __repr__(self):
572
579
  header = get_obj_repr(
573
580
  self,
574
- "unnamed" if self.name == "unnamed_set" else self.name,
581
+ "'unnamed'" if self.name == "unnamed_set" else f"'{self.name}'",
575
582
  height=self.data.height,
576
583
  )
577
584
  data = self._add_shape_to_columns(self.data)
@@ -645,7 +652,7 @@ class Expression(BaseOperableBlock):
645
652
  >>> m.Size = pf.Variable(df.index)
646
653
  >>> expr = df["cost"] * m.Time + df["cost"] * m.Size
647
654
  >>> expr
648
- <Expression height=5 terms=10 type=linear>
655
+ <Expression (linear) height=5 terms=10>
649
656
  ┌──────┬──────┬──────────────────────────────┐
650
657
  │ item ┆ time ┆ expression │
651
658
  │ (2) ┆ (3) ┆ │
@@ -691,7 +698,7 @@ class Expression(BaseOperableBlock):
691
698
 
692
699
  Examples:
693
700
  >>> pf.Expression.constant(5)
694
- <Expression terms=1 type=constant>
701
+ <Expression (parameter) terms=1>
695
702
  5
696
703
  """
697
704
  return cls(
@@ -712,7 +719,7 @@ class Expression(BaseOperableBlock):
712
719
  If no dimensions are specified, the sum is taken over all of the expression's dimensions.
713
720
 
714
721
  Examples:
715
- >>> expr = pl.DataFrame(
722
+ >>> expr = pf.Param(
716
723
  ... {
717
724
  ... "time": ["mon", "tue", "wed", "mon", "tue"],
718
725
  ... "place": [
@@ -724,9 +731,9 @@ class Expression(BaseOperableBlock):
724
731
  ... ],
725
732
  ... "tiktok_posts": [1e6, 3e6, 2e6, 1e6, 2e6],
726
733
  ... }
727
- ... ).to_expr()
734
+ ... )
728
735
  >>> expr
729
- <Expression height=5 terms=5 type=constant>
736
+ <Expression (parameter) height=5 terms=5>
730
737
  ┌──────┬───────────┬────────────┐
731
738
  │ time ┆ place ┆ expression │
732
739
  │ (3) ┆ (2) ┆ │
@@ -738,7 +745,7 @@ class Expression(BaseOperableBlock):
738
745
  │ tue ┆ Vancouver ┆ 2000000 │
739
746
  └──────┴───────────┴────────────┘
740
747
  >>> expr.sum("time")
741
- <Expression height=2 terms=2 type=constant>
748
+ <Expression (parameter) height=2 terms=2>
742
749
  ┌───────────┬────────────┐
743
750
  │ place ┆ expression │
744
751
  │ (2) ┆ │
@@ -747,7 +754,7 @@ class Expression(BaseOperableBlock):
747
754
  │ Vancouver ┆ 3000000 │
748
755
  └───────────┴────────────┘
749
756
  >>> expr.sum()
750
- <Expression terms=1 type=constant>
757
+ <Expression (parameter) terms=1>
751
758
  9000000
752
759
 
753
760
  If the given dimensions don't exist, an error will be raised:
@@ -783,7 +790,7 @@ class Expression(BaseOperableBlock):
783
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).
784
791
 
785
792
  Examples:
786
- >>> expr = pl.DataFrame(
793
+ >>> expr = pf.Param(
787
794
  ... {
788
795
  ... "time": ["mon", "tue", "wed", "mon", "tue"],
789
796
  ... "place": [
@@ -795,9 +802,9 @@ class Expression(BaseOperableBlock):
795
802
  ... ],
796
803
  ... "tiktok_posts": [1e6, 3e6, 2e6, 1e6, 2e6],
797
804
  ... }
798
- ... ).to_expr()
805
+ ... )
799
806
  >>> expr
800
- <Expression height=5 terms=5 type=constant>
807
+ <Expression (parameter) height=5 terms=5>
801
808
  ┌──────┬───────────┬────────────┐
802
809
  │ time ┆ place ┆ expression │
803
810
  │ (3) ┆ (2) ┆ │
@@ -810,7 +817,7 @@ class Expression(BaseOperableBlock):
810
817
  └──────┴───────────┴────────────┘
811
818
 
812
819
  >>> expr.sum_by("place")
813
- <Expression height=2 terms=2 type=constant>
820
+ <Expression (parameter) height=2 terms=2>
814
821
  ┌───────────┬────────────┐
815
822
  │ place ┆ expression │
816
823
  │ (2) ┆ │
@@ -873,13 +880,13 @@ class Expression(BaseOperableBlock):
873
880
 
874
881
  Examples:
875
882
  >>> import polars as pl
876
- >>> pop_data = pl.DataFrame(
883
+ >>> pop_data = pf.Param(
877
884
  ... {
878
885
  ... "city": ["Toronto", "Vancouver", "Boston"],
879
886
  ... "year": [2024, 2024, 2024],
880
887
  ... "population": [10, 2, 8],
881
888
  ... }
882
- ... ).to_expr()
889
+ ... )
883
890
  >>> cities_and_countries = pl.DataFrame(
884
891
  ... {
885
892
  ... "city": ["Toronto", "Vancouver", "Boston"],
@@ -887,7 +894,7 @@ class Expression(BaseOperableBlock):
887
894
  ... }
888
895
  ... )
889
896
  >>> pop_data.map(cities_and_countries)
890
- <Expression height=2 terms=2 type=constant>
897
+ <Expression (parameter) height=2 terms=2>
891
898
  ┌──────┬─────────┬────────────┐
892
899
  │ year ┆ country ┆ expression │
893
900
  │ (1) ┆ (2) ┆ │
@@ -897,7 +904,7 @@ class Expression(BaseOperableBlock):
897
904
  └──────┴─────────┴────────────┘
898
905
 
899
906
  >>> pop_data.map(cities_and_countries, drop_shared_dims=False)
900
- <Expression height=3 terms=3 type=constant>
907
+ <Expression (parameter) height=3 terms=3>
901
908
  ┌───────────┬──────┬─────────┬────────────┐
902
909
  │ city ┆ year ┆ country ┆ expression │
903
910
  │ (3) ┆ (1) ┆ (2) ┆ │
@@ -967,7 +974,7 @@ class Expression(BaseOperableBlock):
967
974
  >>> m = pf.Model()
968
975
  >>> m.quantity = pf.Variable(cost[["item", "time"]])
969
976
  >>> (m.quantity * cost).rolling_sum(over="time", window_size=2)
970
- <Expression height=5 terms=8 type=linear>
977
+ <Expression (linear) height=5 terms=8>
971
978
  ┌──────┬──────┬──────────────────────────────────┐
972
979
  │ item ┆ time ┆ expression │
973
980
  │ (2) ┆ (3) ┆ │
@@ -1003,11 +1010,8 @@ class Expression(BaseOperableBlock):
1003
1010
  """Filters this expression to only include the dimensions within the provided set.
1004
1011
 
1005
1012
  Examples:
1006
- >>> import pandas as pd
1007
- >>> general_expr = pd.DataFrame(
1008
- ... {"dim1": [1, 2, 3], "value": [1, 2, 3]}
1009
- ... ).to_expr()
1010
- >>> 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]})
1011
1015
  >>> general_expr.within(filter_expr).data
1012
1016
  shape: (2, 3)
1013
1017
  ┌──────┬─────────┬───────────────┐
@@ -1068,11 +1072,10 @@ class Expression(BaseOperableBlock):
1068
1072
  If `False`, returns the degree as an integer (0, 1, or 2).
1069
1073
 
1070
1074
  Examples:
1071
- >>> import pandas as pd
1072
1075
  >>> m = pf.Model()
1073
1076
  >>> m.v1 = pf.Variable()
1074
1077
  >>> m.v2 = pf.Variable()
1075
- >>> 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]})
1076
1079
  >>> expr.degree()
1077
1080
  0
1078
1081
  >>> expr *= m.v1
@@ -1090,18 +1093,17 @@ class Expression(BaseOperableBlock):
1090
1093
  elif (self.data.get_column(VAR_KEY) != CONST_TERM).any():
1091
1094
  return "linear" if return_str else 1
1092
1095
  else:
1093
- return "constant" if return_str else 0
1096
+ return "parameter" if return_str else 0
1094
1097
 
1095
1098
  def __add__(self, other):
1096
1099
  """Adds another expression or a constant to this expression.
1097
1100
 
1098
1101
  Examples:
1099
- >>> import pandas as pd
1100
1102
  >>> m = pf.Model()
1101
- >>> 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]})
1102
1104
  >>> m.v = Variable(add)
1103
1105
  >>> m.v + add
1104
- <Expression height=3 terms=6 type=linear>
1106
+ <Expression (linear) height=3 terms=6>
1105
1107
  ┌──────┬────────────┐
1106
1108
  │ dim1 ┆ expression │
1107
1109
  │ (3) ┆ │
@@ -1112,7 +1114,7 @@ class Expression(BaseOperableBlock):
1112
1114
  └──────┴────────────┘
1113
1115
 
1114
1116
  >>> m.v + add + 2
1115
- <Expression height=3 terms=6 type=linear>
1117
+ <Expression (linear) height=3 terms=6>
1116
1118
  ┌──────┬────────────┐
1117
1119
  │ dim1 ┆ expression │
1118
1120
  │ (3) ┆ │
@@ -1138,12 +1140,12 @@ class Expression(BaseOperableBlock):
1138
1140
  https://bravos-power.github.io/pyoframe/latest/learn/concepts/addition
1139
1141
  >>> m.v2 = Variable()
1140
1142
  >>> 5 + 2 * m.v2
1141
- <Expression terms=2 type=linear>
1143
+ <Expression (linear) terms=2>
1142
1144
  2 v2 +5
1143
1145
  """
1144
1146
  if isinstance(other, (int, float)):
1145
1147
  return self._add_const(other)
1146
- other = other.to_expr()
1148
+ other = other.to_expr() # TODO don't rely on monkey patch
1147
1149
  self._learn_from_other(other)
1148
1150
  return add(self, other)
1149
1151
 
@@ -1156,10 +1158,25 @@ class Expression(BaseOperableBlock):
1156
1158
  name=f"({other} * {self.name})",
1157
1159
  )
1158
1160
 
1159
- other = other.to_expr()
1161
+ other: Expression = other.to_expr() # TODO don't rely on monkey patch
1160
1162
  self._learn_from_other(other)
1161
1163
  return multiply(self, other)
1162
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
+
1163
1180
  def to_expr(self) -> Expression:
1164
1181
  """Returns the expression itself."""
1165
1182
  return self
@@ -1181,13 +1198,13 @@ class Expression(BaseOperableBlock):
1181
1198
  >>> m.x1 = Variable()
1182
1199
  >>> m.x2 = Variable()
1183
1200
  >>> m.x1 + 5
1184
- <Expression terms=2 type=linear>
1201
+ <Expression (linear) terms=2>
1185
1202
  x1 +5
1186
1203
  >>> m.x1**2 + 5
1187
- <Expression terms=2 type=quadratic>
1204
+ <Expression (quadratic) terms=2>
1188
1205
  x1 * x1 +5
1189
1206
  >>> m.x1**2 + m.x2 + 5
1190
- <Expression terms=3 type=quadratic>
1207
+ <Expression (quadratic) terms=3>
1191
1208
  x1 * x1 + x2 +5
1192
1209
 
1193
1210
  It also works with dimensions
@@ -1195,7 +1212,7 @@ class Expression(BaseOperableBlock):
1195
1212
  >>> m = pf.Model()
1196
1213
  >>> m.v = Variable({"dim1": [1, 2, 3]})
1197
1214
  >>> m.v * m.v + 5
1198
- <Expression height=3 terms=6 type=quadratic>
1215
+ <Expression (quadratic) height=3 terms=6>
1199
1216
  ┌──────┬─────────────────┐
1200
1217
  │ dim1 ┆ expression │
1201
1218
  │ (3) ┆ │
@@ -1558,9 +1575,9 @@ class Expression(BaseOperableBlock):
1558
1575
  """Returns a string representation of the expression's header."""
1559
1576
  return get_obj_repr(
1560
1577
  self,
1578
+ f"({self.degree(return_str=True)})",
1561
1579
  height=len(self) if self.dimensions else None,
1562
1580
  terms=self.terms,
1563
- type=self.degree(return_str=True),
1564
1581
  )
1565
1582
 
1566
1583
  def __repr__(self) -> str:
@@ -1581,7 +1598,7 @@ class Expression(BaseOperableBlock):
1581
1598
  >>> m.v = pf.Variable({"t": [1, 2]})
1582
1599
  >>> coef = pl.DataFrame({"t": [1, 2], "coef": [0, 1]})
1583
1600
  >>> coef * (m.v + 4)
1584
- <Expression height=2 terms=3 type=linear>
1601
+ <Expression (linear) height=2 terms=3>
1585
1602
  ┌─────┬────────────┐
1586
1603
  │ t ┆ expression │
1587
1604
  │ (2) ┆ │
@@ -2189,10 +2206,10 @@ class Constraint(BaseBlock):
2189
2206
  return (
2190
2207
  get_obj_repr(
2191
2208
  self,
2192
- self.name,
2209
+ f"'{self.name}'",
2210
+ f"({self.lhs.degree(return_str=True)})",
2193
2211
  height=len(self) if self.dimensions else None,
2194
2212
  terms=len(self.lhs.data),
2195
- type=self.lhs.degree(return_str=True),
2196
2213
  )
2197
2214
  + "\n"
2198
2215
  + self.to_str()
@@ -2202,18 +2219,22 @@ class Constraint(BaseBlock):
2202
2219
  class Variable(BaseOperableBlock):
2203
2220
  """A decision variable for an optimization model.
2204
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
+
2205
2225
  Parameters:
2206
2226
  *indexing_sets:
2207
2227
  If no indexing_sets are provided, a single variable with no dimensions is created.
2208
2228
  Otherwise, a variable is created for each element in the Cartesian product of the indexing_sets (see Set for details on behaviour).
2209
- lb:
2210
- The lower bound for all variables.
2211
- ub:
2212
- The upper bound for all variables.
2213
2229
  vtype:
2214
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.
2215
2235
  equals:
2216
- 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`.
2217
2238
 
2218
2239
  Examples:
2219
2240
  >>> import pandas as pd
@@ -2298,7 +2319,7 @@ class Variable(BaseOperableBlock):
2298
2319
  assert len(indexing_sets) == 0, (
2299
2320
  "Cannot specify both 'equals' and 'indexing_sets'"
2300
2321
  )
2301
- equals = equals.to_expr()
2322
+ equals = equals.to_expr() # TODO don't rely on monkey patch
2302
2323
  indexing_sets = (equals,)
2303
2324
 
2304
2325
  data = Set(*indexing_sets).data if len(indexing_sets) > 0 else pl.DataFrame()
@@ -2309,10 +2330,16 @@ class Variable(BaseOperableBlock):
2309
2330
  self._equals: Expression | None = equals
2310
2331
 
2311
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)
2312
2336
  self._lb_expr, self.lb = lb, None
2313
2337
  else:
2314
2338
  self._lb_expr, self.lb = None, lb
2315
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]
2316
2343
  self._ub_expr, self.ub = ub, None
2317
2344
  else:
2318
2345
  self._ub_expr, self.ub = None, ub
@@ -2518,7 +2545,7 @@ class Variable(BaseOperableBlock):
2518
2545
  result = (
2519
2546
  get_obj_repr(
2520
2547
  self,
2521
- self.name,
2548
+ f"'{self.name}'",
2522
2549
  lb=self.lb,
2523
2550
  ub=self.ub,
2524
2551
  height=self.data.height if self.dimensions else None,
@@ -2581,7 +2608,7 @@ class Variable(BaseOperableBlock):
2581
2608
  https://bravos-power.github.io/pyoframe/latest/learn/concepts/addition
2582
2609
 
2583
2610
  >>> (m.bat_charge + m.bat_flow).drop_extras() == m.bat_charge.next("time")
2584
- <Constraint 'unnamed' height=6 terms=18 type=linear>
2611
+ <Constraint 'unnamed' (linear) height=6 terms=18>
2585
2612
  ┌───────┬─────────┬────────────────────────────────────────────────────────────────────────────────┐
2586
2613
  │ time ┆ city ┆ constraint │
2587
2614
  │ (3) ┆ (2) ┆ │
@@ -2603,7 +2630,7 @@ class Variable(BaseOperableBlock):
2603
2630
  >>> (m.bat_charge + m.bat_flow) == m.bat_charge.next(
2604
2631
  ... "time", wrap_around=True
2605
2632
  ... )
2606
- <Constraint 'unnamed' height=8 terms=24 type=linear>
2633
+ <Constraint 'unnamed' (linear) height=8 terms=24>
2607
2634
  ┌───────┬─────────┬────────────────────────────────────────────────────────────────────────────────┐
2608
2635
  │ time ┆ city ┆ constraint │
2609
2636
  │ (4) ┆ (2) ┆ │
pyoframe/_model.py CHANGED
@@ -234,7 +234,7 @@ class Model:
234
234
  from pyoptinterface import ipopt
235
235
  except ModuleNotFoundError as e: # pragma: no cover
236
236
  raise ModuleNotFoundError(
237
- "Failed to import the Ipopt solver. Did you run `pip install pyoptinterface[ipopt]`?"
237
+ "Failed to import the Ipopt solver. Did you run `pip install pyoptinterface[nlp]`?"
238
238
  ) from e
239
239
 
240
240
  try:
@@ -335,7 +335,7 @@ class Model:
335
335
  ValueError: Objective is not defined.
336
336
  >>> m.maximize = m.X
337
337
  >>> m.objective
338
- <Objective terms=1 type=linear>
338
+ <Objective (linear) terms=1>
339
339
  X
340
340
 
341
341
  See Also:
@@ -417,7 +417,7 @@ class Model:
417
417
  def __repr__(self) -> str:
418
418
  return get_obj_repr(
419
419
  self,
420
- self.name,
420
+ f"'{self.name}'" if self.name is not None else None,
421
421
  vars=len(self.variables),
422
422
  constrs=len(self.constraints),
423
423
  has_objective=self.has_objective,
@@ -48,6 +48,8 @@ class BaseBlock(ABC):
48
48
  self._data = data
49
49
  self._model: Model | None = None
50
50
  self.name: str = name # gets overwritten if object is added to model
51
+ """A user-friendly name that is displayed when printing the object or in error messages.
52
+ When an object is added to a model, this name is updated to the name used in the model."""
51
53
 
52
54
  def _on_add_to_model(self, model: Model, name: str):
53
55
  self.name = name
pyoframe/_monkey_patch.py CHANGED
@@ -5,8 +5,8 @@ from functools import wraps
5
5
  import pandas as pd
6
6
  import polars as pl
7
7
 
8
- from pyoframe._constants import COEF_KEY, CONST_TERM, VAR_KEY
9
- from pyoframe._core import BaseOperableBlock, Expression
8
+ from pyoframe._core import BaseOperableBlock
9
+ from pyoframe._param import Param
10
10
 
11
11
 
12
12
  def _patch_class(cls):
@@ -29,54 +29,10 @@ def _patch_class(cls):
29
29
  cls.__contains__ = _patch_method(cls.__contains__)
30
30
 
31
31
 
32
- def polars_df_to_expr(self: pl.DataFrame) -> Expression:
33
- """Converts a [polars](https://pola.rs/) `DataFrame` to a Pyoframe [Expression][pyoframe.Expression] by using the last column for values and the previous columns as dimensions.
34
-
35
- See [Special Functions](../../learn/concepts/special-functions.md#dataframeto_expr) for more details.
36
-
37
- Examples:
38
- >>> import polars as pl
39
- >>> df = pl.DataFrame({"x": [1, 2, 3], "y": [4, 5, 6], "z": [7, 8, 9]})
40
- >>> df.to_expr()
41
- <Expression height=3 terms=3 type=constant>
42
- ┌─────┬─────┬────────────┐
43
- │ x ┆ y ┆ expression │
44
- │ (3) ┆ (3) ┆ │
45
- ╞═════╪═════╪════════════╡
46
- │ 1 ┆ 4 ┆ 7 │
47
- │ 2 ┆ 5 ┆ 8 │
48
- │ 3 ┆ 6 ┆ 9 │
49
- └─────┴─────┴────────────┘
50
- """
51
- name = self.columns[-1]
52
- return Expression(
53
- self.rename({name: COEF_KEY})
54
- .drop_nulls(COEF_KEY)
55
- .with_columns(pl.lit(CONST_TERM).alias(VAR_KEY)),
56
- name=name,
57
- )
58
-
59
-
60
- def pandas_df_to_expr(self: pd.DataFrame) -> Expression:
61
- """Same as [`polars.DataFrame.to_expr`](./polars.DataFrame.to_expr.md), but for [pandas](https://pandas.pydata.org/) DataFrames."""
62
- return polars_df_to_expr(pl.from_pandas(self))
63
-
64
-
65
- def pandas_series_to_expr(self: pd.Series) -> Expression:
66
- """Converts a [pandas](https://pandas.pydata.org/) `Series` to a Pyoframe [Expression][pyoframe.Expression], using the index for labels.
67
-
68
- See [Special Functions](../../learn/concepts/special-functions.md#dataframeto_expr) for more details.
69
-
70
- Note that no equivalent method exists for Polars Series, as Polars does not support indexes.
71
- """
72
- return pandas_df_to_expr(self.to_frame().reset_index())
73
-
74
-
75
32
  def patch_dataframe_libraries():
76
33
  _patch_class(pd.DataFrame)
77
34
  _patch_class(pd.Series)
78
35
  _patch_class(pl.DataFrame)
79
- _patch_class(pl.Series)
80
- pl.DataFrame.to_expr = polars_df_to_expr
81
- pd.DataFrame.to_expr = pandas_df_to_expr
82
- pd.Series.to_expr = pandas_series_to_expr
36
+ pl.DataFrame.to_expr = lambda self: Param(self) # type: ignore
37
+ pd.DataFrame.to_expr = lambda self: Param(self) # type: ignore
38
+ pd.Series.to_expr = lambda self: Param(self) # type: ignore
pyoframe/_objective.py CHANGED
@@ -20,7 +20,7 @@ class Objective(Expression):
20
20
  >>> m.con = m.A + m.B <= 10
21
21
  >>> m.maximize = 2 * m.B + 4
22
22
  >>> m.maximize
23
- <Objective terms=2 type=linear>
23
+ <Objective (linear) terms=2>
24
24
  2 B +4
25
25
 
26
26
  The objective value can be retrieved with from the solver once the model is solved using `.value`.
@@ -67,7 +67,7 @@ class Objective(Expression):
67
67
  if isinstance(expr, (int, float)):
68
68
  expr = Expression.constant(expr)
69
69
  else:
70
- expr = expr.to_expr()
70
+ expr = expr.to_expr() # TODO don't rely on monkey patch
71
71
  super().__init__(expr.data, name="objective")
72
72
  self._model = expr._model
73
73
  if self.dimensions is not None:
pyoframe/_param.py ADDED
@@ -0,0 +1,99 @@
1
+ """Defines the function for creating model parameters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import pandas as pd
8
+ import polars as pl
9
+
10
+ from pyoframe._constants import COEF_KEY, CONST_TERM, VAR_KEY
11
+ from pyoframe._core import Expression
12
+
13
+
14
+ def Param(
15
+ data: pl.DataFrame | pd.DataFrame | pd.Series | dict | str | Path,
16
+ ) -> Expression:
17
+ """Creates a model parameter, i.e. an [Expression][pyoframe.Expression] that doesn't involve any variables.
18
+
19
+ A Parameter can be created from a DataFrame, CSV file, Parquet file, data dictionary, or a Pandas Series.
20
+
21
+ !!! info "`Param` is a function, not a class"
22
+ Technically, `Param(data)` is a function that returns an [Expression][pyoframe.Expression], not a class.
23
+ However, for consistency with other modeling frameworks, we provide it as a class-like function (i.e. an uppercase function).
24
+
25
+ !!! tip "Smart naming"
26
+ If a Param is not given a name (i.e. if it is not assigned to a model: `m.my_name = Param(...)`),
27
+ then its [name][pyoframe._model_element.BaseBlock.name] is inferred from the name of the column in `data` that contains the parameter values.
28
+ This makes debugging models with inline parameters easier.
29
+
30
+ Args:
31
+ data: The data to use for the parameter.
32
+
33
+ If `data` is a polars or pandas `DataFrame`, the last column will be treated as the values of the parameter, and all other columns as labels.
34
+
35
+ If `data` is a string or `Path`, it will be interpreted as a path to a CSV or Parquet file that will be read and used as a `DataFrame`. The file extension must be `.csv` or `.parquet`.
36
+
37
+ If `data` is a `pandas.Series`, the index(es) will be treated as columns for labels and the series values as the parameter values.
38
+
39
+ If `data` is of any other type (e.g. a dictionary), it will be used as if you had called `Param(pl.DataFrame(data))`.
40
+
41
+ Returns:
42
+ An Expression representing the parameter.
43
+
44
+ Examples:
45
+ >>> m = pf.Model()
46
+ >>> m.fixed_cost = pf.Param({"plant": ["A", "B"], "cost": [1000, 1500]})
47
+ >>> m.fixed_cost
48
+ <Expression (parameter) height=2 terms=2>
49
+ ┌───────┬────────────┐
50
+ │ plant ┆ expression │
51
+ │ (2) ┆ │
52
+ ╞═══════╪════════════╡
53
+ │ A ┆ 1000 │
54
+ │ B ┆ 1500 │
55
+ └───────┴────────────┘
56
+
57
+ Since `Param` simply returns an Expression, you can use it in building larger expressions as usual:
58
+
59
+ >>> m.variable_cost = pf.Param(
60
+ ... pl.DataFrame({"plant": ["A", "B"], "cost": [50, 60]})
61
+ ... )
62
+ >>> m.total_cost = m.fixed_cost + m.variable_cost
63
+ >>> m.total_cost
64
+ <Expression (parameter) height=2 terms=2>
65
+ ┌───────┬────────────┐
66
+ │ plant ┆ expression │
67
+ │ (2) ┆ │
68
+ ╞═══════╪════════════╡
69
+ │ A ┆ 1050 │
70
+ │ B ┆ 1560 │
71
+ └───────┴────────────┘
72
+ """
73
+ if isinstance(data, pd.Series):
74
+ data = data.to_frame().reset_index()
75
+ if isinstance(data, pd.DataFrame):
76
+ data = pl.from_pandas(data)
77
+
78
+ if isinstance(data, (str, Path)):
79
+ data = Path(data)
80
+ if data.suffix.lower() == ".csv":
81
+ data = pl.read_csv(data)
82
+ elif data.suffix.lower() in {".parquet"}:
83
+ data = pl.read_parquet(data)
84
+ else:
85
+ raise NotImplementedError(
86
+ f"Could not create parameter. Unsupported file format: {data.suffix}"
87
+ )
88
+
89
+ if not isinstance(data, pl.DataFrame):
90
+ data = pl.DataFrame(data)
91
+
92
+ value_col = data.columns[-1]
93
+
94
+ return Expression(
95
+ data.rename({value_col: COEF_KEY})
96
+ .drop_nulls(COEF_KEY)
97
+ .with_columns(pl.lit(CONST_TERM).alias(VAR_KEY)),
98
+ name=value_col,
99
+ )
pyoframe/_utils.py CHANGED
@@ -42,7 +42,7 @@ def get_obj_repr(obj: object, *props: str | None, **kwargs):
42
42
 
43
43
  See usage for examples.
44
44
  """
45
- props_str = " ".join(f"'{v}'" for v in props if v is not None)
45
+ props_str = " ".join(v for v in props if v is not None)
46
46
  if props_str:
47
47
  props_str += " "
48
48
  kwargs_str = " ".join(f"{k}={v}" for k, v in kwargs.items() if v is not None)
@@ -302,7 +302,7 @@ class NamedVariableMapper:
302
302
  >>> m = pf.Model()
303
303
  >>> m.foo = pf.Variable(pl.DataFrame({"t": range(4)}))
304
304
  >>> m.foo.sum()
305
- <Expression terms=4 type=linear>
305
+ <Expression (linear) terms=4>
306
306
  foo[0] + foo[1] + foo[2] + foo[3]
307
307
  """
308
308
 
pyoframe/_version.py CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '1.0.1'
32
- __version_tuple__ = version_tuple = (1, 0, 1)
31
+ __version__ = version = '1.1.0'
32
+ __version_tuple__ = version_tuple = (1, 1, 0)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyoframe
3
- Version: 1.0.1
3
+ Version: 1.1.0
4
4
  Summary: Blazing fast linear program interface
5
5
  Author-email: Bravos Power <dev@bravospower.com>
6
6
  License-Expression: MIT
@@ -20,21 +20,21 @@ Requires-Dist: pyarrow
20
20
  Requires-Dist: pandas<3
21
21
  Requires-Dist: pyoptinterface==0.5.1
22
22
  Provides-Extra: highs
23
- Requires-Dist: highsbox<=1.11.0; extra == "highs"
23
+ Requires-Dist: highsbox<=1.12.0; extra == "highs"
24
24
  Provides-Extra: ipopt
25
25
  Requires-Dist: pyoptinterface[nlp]; extra == "ipopt"
26
- Requires-Dist: llvmlite<=0.44.0; extra == "ipopt"
26
+ Requires-Dist: llvmlite<=0.46.0; extra == "ipopt"
27
27
  Provides-Extra: dev
28
28
  Requires-Dist: ruff==0.12.11; extra == "dev"
29
29
  Requires-Dist: polars>=1.32.3; extra == "dev"
30
30
  Requires-Dist: pytest==8.4.1; extra == "dev"
31
31
  Requires-Dist: pytest-cov==6.2.1; extra == "dev"
32
- Requires-Dist: sybil[pytest]==9.2.0; extra == "dev"
32
+ Requires-Dist: sybil[pytest]==9.3.0; extra == "dev"
33
33
  Requires-Dist: pre-commit==4.3.0; extra == "dev"
34
34
  Requires-Dist: gurobipy==12.0.3; extra == "dev"
35
35
  Requires-Dist: coverage==7.10.6; extra == "dev"
36
36
  Requires-Dist: ipykernel==6.30.1; extra == "dev"
37
- Requires-Dist: highsbox<=1.11.0; extra == "dev"
37
+ Requires-Dist: highsbox<=1.12.0; extra == "dev"
38
38
  Requires-Dist: pyoptinterface[nlp]; extra == "dev"
39
39
  Requires-Dist: numpy; extra == "dev"
40
40
  Provides-Extra: docs
@@ -0,0 +1,16 @@
1
+ pyoframe/__init__.py,sha256=Ij-9priyKTHaGzVhMhtZlDKWz0ggAwGAS9DqB4O6zWU,886
2
+ pyoframe/_arithmetic.py,sha256=VZAlZZQ7StYMRbJcfWGZ7he2Ds6b6zoY0J1GxG99UWM,20549
3
+ pyoframe/_constants.py,sha256=LWlry4K5w-3vVyq7CpEQ28UfM3LulbKxkO-nBlWWJzE,17847
4
+ pyoframe/_core.py,sha256=EiuAhW9M2616gjW8htJqP4FuSlUb3zfwWP1ydTsQ1Qo,118507
5
+ pyoframe/_model.py,sha256=IcmqfJ1agNF4LzHUR-bXr-K-3NXsJGcBU6Ga55jcmik,22503
6
+ pyoframe/_model_element.py,sha256=VVRqh2uM8HFvRFvqQmgM93jqofa-8mPwyB-qYA0YjRU,6345
7
+ pyoframe/_monkey_patch.py,sha256=7FWMRXZIHK7mRkZfOKQ8Y724q1sPbq_EiPjlJCTfYoA,1168
8
+ pyoframe/_objective.py,sha256=HeiP4KjlXn-IplqV-MALF26yMmh41JyHXjZhhtKJIsQ,4367
9
+ pyoframe/_param.py,sha256=FUSfITPb-WZA-xwVcF9dCCHO2K_pky5GBWooImsSy6I,4147
10
+ pyoframe/_utils.py,sha256=XaPZ8j9YQ-HnAuT2NLAvDCJGVzKSjUmRxARNuGWykIM,12508
11
+ pyoframe/_version.py,sha256=ePNVzJOkxR8FY5bezqKQ_fgBRbzH1G7QTaRDHvGQRAY,704
12
+ pyoframe-1.1.0.dist-info/licenses/LICENSE,sha256=u_Spw4ynlwTMRZeCX-uacv_hBU547pBygiA6d2ONNV4,1074
13
+ pyoframe-1.1.0.dist-info/METADATA,sha256=cO4Wu3bBxixzFjhmkzi40kRkPbAmRiQ161L8dz0MQjo,4060
14
+ pyoframe-1.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
15
+ pyoframe-1.1.0.dist-info/top_level.txt,sha256=10z3OOJSVLriQ0IrFLMH8CH9zByugPWolqhlHlkNjV4,9
16
+ pyoframe-1.1.0.dist-info/RECORD,,
@@ -1,15 +0,0 @@
1
- pyoframe/__init__.py,sha256=Nlql3FYed7bXWumvUeMd3rjnoL4l8XC5orO4uxWrDAc,839
2
- pyoframe/_arithmetic.py,sha256=9V3N2Yq7Ib13UfTQnINxa9oS7786f5DnRsrAPcFlYYE,20558
3
- pyoframe/_constants.py,sha256=n80so80usutTJpeDlDXkHRE-_dpPD1LxF2plscXhbwQ,17925
4
- pyoframe/_core.py,sha256=NXH_ze1iN61vk38Fj6Ire0uqKUJRSDUlD6PVc8mTHrQ,116975
5
- pyoframe/_model.py,sha256=MtA9gleQoUAqO2dxhCFZ8GOZvyyJB_PIYBzvQ0m8enc,22466
6
- pyoframe/_model_element.py,sha256=oQ7nykJ5XEzJ6Klq3lT6ZwQvDrxY_wgZYVaN7pgyZOs,6149
7
- pyoframe/_monkey_patch.py,sha256=Y2zXN5MpqDeAWELddyaFQNam57fehSXHiza1PFaZ-QY,3128
8
- pyoframe/_objective.py,sha256=yIHoaBLsjGCKzIB6RQErV3vzE2U5DGORlhifRStB_Mc,4335
9
- pyoframe/_utils.py,sha256=5yy-5DOWCW7q3QzPv9tKquLUQJtNdpJJilEqxAq7TN8,12518
10
- pyoframe/_version.py,sha256=JvmBpae6cHui8lSCsCcZQAxzawN2NERHGsr-rIUeJMo,704
11
- pyoframe-1.0.1.dist-info/licenses/LICENSE,sha256=u_Spw4ynlwTMRZeCX-uacv_hBU547pBygiA6d2ONNV4,1074
12
- pyoframe-1.0.1.dist-info/METADATA,sha256=UtnvQdR7vJQvLm8p0RvgwdGfR7yCISswH_V4KuihBr4,4060
13
- pyoframe-1.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
- pyoframe-1.0.1.dist-info/top_level.txt,sha256=10z3OOJSVLriQ0IrFLMH8CH9zByugPWolqhlHlkNjV4,9
15
- pyoframe-1.0.1.dist-info/RECORD,,