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/__init__.py +12 -3
- pyoframe/_arithmetic.py +3 -6
- pyoframe/constants.py +20 -14
- pyoframe/{constraints.py → core.py} +504 -74
- pyoframe/io.py +66 -30
- pyoframe/io_mappers.py +66 -34
- pyoframe/model.py +65 -41
- pyoframe/model_element.py +128 -18
- pyoframe/monkey_patch.py +2 -2
- pyoframe/objective.py +16 -13
- pyoframe/solvers.py +300 -109
- pyoframe/user_defined.py +60 -0
- pyoframe/util.py +56 -55
- {pyoframe-0.0.4.dist-info → pyoframe-0.0.6.dist-info}/METADATA +9 -2
- pyoframe-0.0.6.dist-info/RECORD +18 -0
- pyoframe/variables.py +0 -193
- pyoframe-0.0.4.dist-info/RECORD +0 -18
- {pyoframe-0.0.4.dist-info → pyoframe-0.0.6.dist-info}/LICENSE +0 -0
- {pyoframe-0.0.4.dist-info → pyoframe-0.0.6.dist-info}/WHEEL +0 -0
- {pyoframe-0.0.4.dist-info → pyoframe-0.0.6.dist-info}/top_level.txt +0 -0
pyoframe/model_element.py
CHANGED
|
@@ -1,27 +1,18 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
from abc import ABC, abstractmethod
|
|
3
|
-
from
|
|
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.
|
|
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.
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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.
|
|
4
|
-
from pyoframe.
|
|
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
|
|
2
|
-
from pyoframe.constants import
|
|
3
|
-
from pyoframe.
|
|
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.
|
|
14
|
-
>>> m.
|
|
13
|
+
>>> m.objective = m.a + sum("dim1", m.b)
|
|
14
|
+
>>> m.objective
|
|
15
15
|
<Objective size=1 dimensions={} terms=4>
|
|
16
|
-
|
|
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.
|
|
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
|