pyoframe 0.0.4__py3-none-any.whl → 0.0.6__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/model_element.py CHANGED
@@ -1,27 +1,18 @@
1
1
  from __future__ import annotations
2
2
  from abc import ABC, abstractmethod
3
- from typing import Dict, List, Optional
3
+ from collections import defaultdict
4
+ from typing import Any, Dict, List, Optional
4
5
  import polars as pl
5
6
  from typing import TYPE_CHECKING
6
7
 
7
8
  from pyoframe.constants import COEF_KEY, RESERVED_COL_KEYS, VAR_KEY
8
9
  from pyoframe._arithmetic import _get_dimensions
10
+ from pyoframe.user_defined import AttrContainerMixin
9
11
 
10
12
  if TYPE_CHECKING: # pragma: no cover
11
13
  from pyoframe.model import Model
12
14
 
13
15
 
14
- def _pass_polars_method(method_name: str):
15
- """
16
- Wrapper to add a method to ModelElement that simply calls the underlying Polars method on the data attribute.
17
- """
18
-
19
- def method(self, *args, **kwargs):
20
- return self._new(getattr(self.data, method_name)(*args, **kwargs))
21
-
22
- return method
23
-
24
-
25
16
  class ModelElement(ABC):
26
17
  def __init__(self, data: pl.DataFrame, **kwargs) -> None:
27
18
  # Sanity checks, no duplicate column names
@@ -48,17 +39,25 @@ class ModelElement(ABC):
48
39
  self.name = None
49
40
  super().__init__(**kwargs)
50
41
 
42
+ def on_add_to_model(self, model: "Model", name: str):
43
+ self.name = name
44
+ self._model = model
45
+
51
46
  @property
52
47
  def data(self) -> pl.DataFrame:
53
48
  return self._data
54
49
 
50
+ @property
51
+ def friendly_name(self) -> str:
52
+ return self.name if self.name is not None else "unnamed"
53
+
55
54
  @property
56
55
  def dimensions(self) -> Optional[List[str]]:
57
56
  """
58
57
  The names of the data's dimensions.
59
58
 
60
59
  Examples:
61
- >>> from pyoframe.variables import Variable
60
+ >>> from pyoframe.core import Variable
62
61
  >>> # A variable with no dimensions
63
62
  >>> Variable().dimensions
64
63
 
@@ -85,7 +84,7 @@ class ModelElement(ABC):
85
84
  The number of indices in each dimension.
86
85
 
87
86
  Examples:
88
- >>> from pyoframe.variables import Variable
87
+ >>> from pyoframe.core import Variable
89
88
  >>> # A variable with no dimensions
90
89
  >>> Variable().shape
91
90
  {}
@@ -104,13 +103,124 @@ class ModelElement(ABC):
104
103
  return 1
105
104
  return self.data.select(dims).n_unique()
106
105
 
106
+
107
+ def _support_polars_method(method_name: str):
108
+ """
109
+ Wrapper to add a method to ModelElement that simply calls the underlying Polars method on the data attribute.
110
+ """
111
+
112
+ def method(self: "SupportPolarsMethodMixin", *args, **kwargs) -> Any:
113
+ result_from_polars = getattr(self.data, method_name)(*args, **kwargs)
114
+ if isinstance(result_from_polars, pl.DataFrame):
115
+ return self._new(result_from_polars)
116
+ else:
117
+ return result_from_polars
118
+
119
+ return method
120
+
121
+
122
+ class SupportPolarsMethodMixin(ABC):
123
+ rename = _support_polars_method("rename")
124
+ with_columns = _support_polars_method("with_columns")
125
+ filter = _support_polars_method("filter")
126
+ estimated_size = _support_polars_method("estimated_size")
127
+
107
128
  @abstractmethod
108
129
  def _new(self, data: pl.DataFrame):
109
130
  """
110
131
  Used to create a new instance of the same class with the given data (for e.g. on .rename(), .with_columns(), etc.).
111
132
  """
112
- raise NotImplementedError
113
133
 
114
- rename = _pass_polars_method("rename")
115
- with_columns = _pass_polars_method("with_columns")
116
- filter = _pass_polars_method("filter")
134
+ @property
135
+ @abstractmethod
136
+ def data(self): ...
137
+
138
+
139
+ class ModelElementWithId(ModelElement, AttrContainerMixin):
140
+ """
141
+ Provides a method that assigns a unique ID to each row in a DataFrame.
142
+ IDs start at 1 and go up consecutively. No zero ID is assigned since it is reserved for the constant variable term.
143
+ IDs are only unique for the subclass since different subclasses have different counters.
144
+ """
145
+
146
+ # Keys are the subclass names and values are the next unasigned ID.
147
+ _id_counters: Dict[str, int] = defaultdict(lambda: 1)
148
+
149
+ @classmethod
150
+ def reset_counters(cls):
151
+ """
152
+ Resets all the ID counters.
153
+ This function is called before every unit test to reset the code state.
154
+ """
155
+ cls._id_counters = defaultdict(lambda: 1)
156
+
157
+ def __init__(self, data: pl.DataFrame, **kwargs) -> None:
158
+ super().__init__(data, **kwargs)
159
+ self._data = self._assign_ids(self.data)
160
+
161
+ @classmethod
162
+ def _assign_ids(cls, df: pl.DataFrame) -> pl.DataFrame:
163
+ """
164
+ Adds the column `to_column` to the DataFrame `df` with the next batch
165
+ of unique consecutive IDs.
166
+ """
167
+ cls_name = cls.__name__
168
+ cur_count = cls._id_counters[cls_name]
169
+ id_col_name = cls.get_id_column_name()
170
+
171
+ if df.height == 0:
172
+ df = df.with_columns(pl.lit(cur_count).alias(id_col_name))
173
+ else:
174
+ df = df.with_columns(
175
+ pl.int_range(cur_count, cur_count + pl.len()).alias(id_col_name)
176
+ )
177
+ df = df.with_columns(pl.col(id_col_name).cast(pl.UInt32))
178
+ cls._id_counters[cls_name] += df.height
179
+ return df
180
+
181
+ @classmethod
182
+ @abstractmethod
183
+ def get_id_column_name(cls) -> str:
184
+ """
185
+ Returns the name of the column containing the IDs.
186
+ """
187
+
188
+ @property
189
+ def ids(self) -> pl.DataFrame:
190
+ return self.data.select(self.dimensions_unsafe + [self.get_id_column_name()])
191
+
192
+ def _extend_dataframe_by_id(self, addition: pl.DataFrame):
193
+ cols = addition.columns
194
+ assert len(cols) == 2
195
+ id_col = self.get_id_column_name()
196
+ assert id_col in cols
197
+ cols.remove(id_col)
198
+ new_col = cols[0]
199
+
200
+ original = self.data
201
+
202
+ if new_col in original.columns:
203
+ original = original.drop(new_col)
204
+ self._data = original.join(addition, on=id_col, how="left", validate="1:1")
205
+
206
+ def _preprocess_attr(self, name: str, value: Any) -> Any:
207
+ dims = self.dimensions
208
+ ids = self.ids
209
+ id_col = self.get_id_column_name()
210
+
211
+ if isinstance(value, pl.DataFrame):
212
+ if value.shape == (1, 1):
213
+ value = value.item()
214
+ else:
215
+ assert (
216
+ dims is not None
217
+ ), "Attribute must be a scalar since there are no dimensions"
218
+ result = value.join(ids, on=dims, validate="1:1", how="inner").drop(
219
+ dims
220
+ )
221
+ assert len(result.columns) == 2, "Attribute has too many columns"
222
+ value_col = [c for c in result.columns if c != id_col][0]
223
+ return result.rename({value_col: name})
224
+
225
+ assert ids.height == 1, "Attribute is a scalar but there are multiple IDs."
226
+ return pl.DataFrame({name: [value], id_col: ids.get_column(id_col)})
pyoframe/monkey_patch.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import pandas as pd
2
2
  import polars as pl
3
- from pyoframe.constraints import SupportsMath
4
- from pyoframe.constraints import Expression
3
+ from pyoframe.core import SupportsMath
4
+ from pyoframe.core import Expression
5
5
  from functools import wraps
6
6
 
7
7
  from pyoframe.constants import COEF_KEY, CONST_TERM, VAR_KEY
pyoframe/objective.py CHANGED
@@ -1,28 +1,24 @@
1
- from typing import Optional, Union
2
- from pyoframe.constants import ObjSense, ObjSenseValue
3
- from pyoframe.constraints import SupportsMath, Expression
1
+ from typing import Optional
2
+ from pyoframe.constants import COEF_KEY
3
+ from pyoframe.core import SupportsMath, Expression
4
4
 
5
5
 
6
6
  class Objective(Expression):
7
7
  r"""
8
8
  Examples:
9
9
  >>> from pyoframe import Variable, Model, sum
10
- >>> m = Model()
10
+ >>> m = Model("max")
11
11
  >>> m.a = Variable()
12
12
  >>> m.b = Variable({"dim1": [1, 2, 3]})
13
- >>> m.maximize = m.a + sum("dim1", m.b)
14
- >>> m.maximize
13
+ >>> m.objective = m.a + sum("dim1", m.b)
14
+ >>> m.objective
15
15
  <Objective size=1 dimensions={} terms=4>
16
- maximize: a + b[1] + b[2] + b[3]
16
+ objective: a + b[1] + b[2] + b[3]
17
17
  """
18
18
 
19
- def __init__(
20
- self, expr: SupportsMath, sense: Union[ObjSense, ObjSenseValue]
21
- ) -> None:
22
- self.sense = ObjSense(sense)
23
-
19
+ def __init__(self, expr: SupportsMath) -> None:
24
20
  expr = expr.to_expr()
25
- super().__init__(expr.to_expr().data)
21
+ super().__init__(expr.data)
26
22
  self._model = expr._model
27
23
  assert (
28
24
  self.dimensions is None
@@ -40,3 +36,10 @@ class Objective(Expression):
40
36
  @value.setter
41
37
  def value(self, value):
42
38
  self._value = value
39
+
40
+ @property
41
+ def has_constant(self):
42
+ constant_terms = self.constant_terms
43
+ if len(constant_terms) == 0:
44
+ return False
45
+ return constant_terms.get_column(COEF_KEY).item() != 0