pyoframe 0.0.4__py3-none-any.whl → 0.0.5__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 +2 -5
- pyoframe/constants.py +15 -12
- pyoframe/{constraints.py → core.py} +490 -74
- pyoframe/io.py +51 -25
- pyoframe/io_mappers.py +49 -18
- pyoframe/model.py +65 -42
- pyoframe/model_element.py +124 -18
- pyoframe/monkey_patch.py +2 -2
- pyoframe/objective.py +16 -13
- pyoframe/solvers.py +276 -109
- pyoframe/user_defined.py +60 -0
- pyoframe/util.py +56 -55
- {pyoframe-0.0.4.dist-info → pyoframe-0.0.5.dist-info}/METADATA +9 -2
- pyoframe-0.0.5.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.5.dist-info}/LICENSE +0 -0
- {pyoframe-0.0.4.dist-info → pyoframe-0.0.5.dist-info}/WHEEL +0 -0
- {pyoframe-0.0.4.dist-info → pyoframe-0.0.5.dist-info}/top_level.txt +0 -0
pyoframe/variables.py
DELETED
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
File containing Variable class representing decision variables in optimization models.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from __future__ import annotations
|
|
6
|
-
from typing import Iterable
|
|
7
|
-
|
|
8
|
-
import polars as pl
|
|
9
|
-
|
|
10
|
-
from pyoframe.constraints import SupportsMath, Set
|
|
11
|
-
|
|
12
|
-
from pyoframe.constants import COEF_KEY, SOLUTION_KEY, VAR_KEY, VType, VTypeValue
|
|
13
|
-
from pyoframe.constraints import Expression
|
|
14
|
-
from pyoframe.model_element import ModelElement
|
|
15
|
-
from pyoframe.constraints import SetTypes
|
|
16
|
-
from pyoframe.util import IdCounterMixin, get_obj_repr
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class Variable(ModelElement, SupportsMath, IdCounterMixin):
|
|
20
|
-
"""
|
|
21
|
-
Represents one or many decision variable in an optimization model.
|
|
22
|
-
|
|
23
|
-
Parameters:
|
|
24
|
-
*indexing_sets: SetTypes (typically a DataFrame or Set)
|
|
25
|
-
If no indexing_sets are provided, a single variable with no dimensions is created.
|
|
26
|
-
Otherwise, a variable is created for each element in the Cartesian product of the indexing_sets (see Set for details on behaviour).
|
|
27
|
-
lb: float
|
|
28
|
-
The lower bound for all variables.
|
|
29
|
-
ub: float
|
|
30
|
-
The upper bound for all variables.
|
|
31
|
-
vtype: VType | VTypeValue
|
|
32
|
-
The type of the variable. Can be either a VType enum or a string. Default is VType.CONTINUOUS.
|
|
33
|
-
|
|
34
|
-
Examples:
|
|
35
|
-
>>> import pandas as pd
|
|
36
|
-
>>> from pyoframe import Variable
|
|
37
|
-
>>> df = pd.DataFrame({"dim1": [1, 1, 2, 2, 3, 3], "dim2": ["a", "b", "a", "b", "a", "b"]})
|
|
38
|
-
>>> Variable(df)
|
|
39
|
-
<Variable lb=-inf ub=inf size=6 dimensions={'dim1': 3, 'dim2': 2}>
|
|
40
|
-
[1,a]: x1
|
|
41
|
-
[1,b]: x2
|
|
42
|
-
[2,a]: x3
|
|
43
|
-
[2,b]: x4
|
|
44
|
-
[3,a]: x5
|
|
45
|
-
[3,b]: x6
|
|
46
|
-
>>> Variable(df[["dim1"]])
|
|
47
|
-
Traceback (most recent call last):
|
|
48
|
-
...
|
|
49
|
-
ValueError: Duplicate rows found in input data.
|
|
50
|
-
>>> Variable(df[["dim1"]].drop_duplicates())
|
|
51
|
-
<Variable lb=-inf ub=inf size=3 dimensions={'dim1': 3}>
|
|
52
|
-
[1]: x7
|
|
53
|
-
[2]: x8
|
|
54
|
-
[3]: x9
|
|
55
|
-
"""
|
|
56
|
-
|
|
57
|
-
# TODO: Breaking change, remove support for Iterable[AcceptableSets]
|
|
58
|
-
def __init__(
|
|
59
|
-
self,
|
|
60
|
-
*indexing_sets: SetTypes | Iterable[SetTypes],
|
|
61
|
-
lb: float = float("-inf"),
|
|
62
|
-
ub: float = float("inf"),
|
|
63
|
-
vtype: VType | VTypeValue = VType.CONTINUOUS,
|
|
64
|
-
):
|
|
65
|
-
data = Set(*indexing_sets).data if len(indexing_sets) > 0 else pl.DataFrame()
|
|
66
|
-
data = self._assign_ids(data)
|
|
67
|
-
|
|
68
|
-
super().__init__(data)
|
|
69
|
-
|
|
70
|
-
self.vtype: VType = VType(vtype)
|
|
71
|
-
|
|
72
|
-
# Tightening the bounds is not strictly necessary, but it adds clarity
|
|
73
|
-
if self.vtype == VType.BINARY:
|
|
74
|
-
lb, ub = 0, 1
|
|
75
|
-
|
|
76
|
-
self.lb = lb
|
|
77
|
-
self.ub = ub
|
|
78
|
-
|
|
79
|
-
@classmethod
|
|
80
|
-
def get_id_column_name(cls):
|
|
81
|
-
return VAR_KEY
|
|
82
|
-
|
|
83
|
-
@property
|
|
84
|
-
def solution(self):
|
|
85
|
-
if SOLUTION_KEY not in self.data.columns:
|
|
86
|
-
raise ValueError(f"No solution solution found for Variable '{self.name}'.")
|
|
87
|
-
df = self.data.select(self.dimensions_unsafe + [SOLUTION_KEY])
|
|
88
|
-
if df.shape == (1, 1):
|
|
89
|
-
return df.item()
|
|
90
|
-
return df
|
|
91
|
-
|
|
92
|
-
@solution.setter
|
|
93
|
-
def solution(self, value):
|
|
94
|
-
assert sorted(value.columns) == sorted([SOLUTION_KEY, VAR_KEY])
|
|
95
|
-
df = self.data
|
|
96
|
-
if SOLUTION_KEY in self.data.columns:
|
|
97
|
-
df = df.drop(SOLUTION_KEY)
|
|
98
|
-
self._data = df.join(value, on=VAR_KEY, how="left", validate="1:1")
|
|
99
|
-
|
|
100
|
-
@property
|
|
101
|
-
def ids(self):
|
|
102
|
-
return self.data.select(self.dimensions_unsafe + [VAR_KEY])
|
|
103
|
-
|
|
104
|
-
def __repr__(self):
|
|
105
|
-
return (
|
|
106
|
-
get_obj_repr(
|
|
107
|
-
self, ("name", "lb", "ub"), size=self.data.height, dimensions=self.shape
|
|
108
|
-
)
|
|
109
|
-
+ "\n"
|
|
110
|
-
+ self.to_expr().to_str(max_line_len=80, max_rows=10)
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
def to_expr(self) -> Expression:
|
|
114
|
-
return self._new(self.data.drop(SOLUTION_KEY))
|
|
115
|
-
|
|
116
|
-
def _new(self, data: pl.DataFrame):
|
|
117
|
-
e = Expression(data.with_columns(pl.lit(1.0).alias(COEF_KEY)))
|
|
118
|
-
e._model = self._model
|
|
119
|
-
# We propogate the unmatched strategy intentionally. Without this a .keep_unmatched() on a variable would always be lost.
|
|
120
|
-
e.unmatched_strategy = self.unmatched_strategy
|
|
121
|
-
e.allowed_new_dims = self.allowed_new_dims
|
|
122
|
-
return e
|
|
123
|
-
|
|
124
|
-
def next(self, dim: str, wrap_around: bool = False) -> Expression:
|
|
125
|
-
"""
|
|
126
|
-
Creates an expression where the variable at each index is the next variable in the specified dimension.
|
|
127
|
-
|
|
128
|
-
Parameters:
|
|
129
|
-
dim:
|
|
130
|
-
The dimension over which to shift the variable.
|
|
131
|
-
wrap_around:
|
|
132
|
-
If True, the last index in the dimension is connected to the first index.
|
|
133
|
-
|
|
134
|
-
Examples:
|
|
135
|
-
>>> import pandas as pd
|
|
136
|
-
>>> from pyoframe import Variable, Model
|
|
137
|
-
>>> time_dim = pd.DataFrame({"time": ["00:00", "06:00", "12:00", "18:00"]})
|
|
138
|
-
>>> space_dim = pd.DataFrame({"city": ["Toronto", "Berlin"]})
|
|
139
|
-
>>> m = Model()
|
|
140
|
-
>>> m.bat_charge = Variable(time_dim, space_dim)
|
|
141
|
-
>>> m.bat_flow = Variable(time_dim, space_dim)
|
|
142
|
-
>>> # Fails because the dimensions are not the same
|
|
143
|
-
>>> m.bat_charge + m.bat_flow == m.bat_charge.next("time")
|
|
144
|
-
Traceback (most recent call last):
|
|
145
|
-
...
|
|
146
|
-
pyoframe._arithmetic.PyoframeError: Failed to add expressions:
|
|
147
|
-
<Expression size=8 dimensions={'time': 4, 'city': 2} terms=16> + <Expression size=6 dimensions={'city': 2, 'time': 3} terms=6>
|
|
148
|
-
Due to error:
|
|
149
|
-
Dataframe has unmatched values. If this is intentional, use .drop_unmatched() or .keep_unmatched()
|
|
150
|
-
shape: (2, 4)
|
|
151
|
-
┌───────┬─────────┬────────────┬────────────┐
|
|
152
|
-
│ time ┆ city ┆ time_right ┆ city_right │
|
|
153
|
-
│ --- ┆ --- ┆ --- ┆ --- │
|
|
154
|
-
│ str ┆ str ┆ str ┆ str │
|
|
155
|
-
╞═══════╪═════════╪════════════╪════════════╡
|
|
156
|
-
│ 18:00 ┆ Toronto ┆ null ┆ null │
|
|
157
|
-
│ 18:00 ┆ Berlin ┆ null ┆ null │
|
|
158
|
-
└───────┴─────────┴────────────┴────────────┘
|
|
159
|
-
|
|
160
|
-
>>> (m.bat_charge + m.bat_flow).drop_unmatched() == m.bat_charge.next("time")
|
|
161
|
-
<Constraint sense='=' size=6 dimensions={'time': 3, 'city': 2} terms=18>
|
|
162
|
-
[00:00,Berlin]: bat_charge[00:00,Berlin] + bat_flow[00:00,Berlin] - bat_charge[06:00,Berlin] = 0
|
|
163
|
-
[00:00,Toronto]: bat_charge[00:00,Toronto] + bat_flow[00:00,Toronto] - bat_charge[06:00,Toronto] = 0
|
|
164
|
-
[06:00,Berlin]: bat_charge[06:00,Berlin] + bat_flow[06:00,Berlin] - bat_charge[12:00,Berlin] = 0
|
|
165
|
-
[06:00,Toronto]: bat_charge[06:00,Toronto] + bat_flow[06:00,Toronto] - bat_charge[12:00,Toronto] = 0
|
|
166
|
-
[12:00,Berlin]: bat_charge[12:00,Berlin] + bat_flow[12:00,Berlin] - bat_charge[18:00,Berlin] = 0
|
|
167
|
-
[12:00,Toronto]: bat_charge[12:00,Toronto] + bat_flow[12:00,Toronto] - bat_charge[18:00,Toronto] = 0
|
|
168
|
-
|
|
169
|
-
>>> (m.bat_charge + m.bat_flow) == m.bat_charge.next("time", wrap_around=True)
|
|
170
|
-
<Constraint sense='=' size=8 dimensions={'time': 4, 'city': 2} terms=24>
|
|
171
|
-
[00:00,Berlin]: bat_charge[00:00,Berlin] + bat_flow[00:00,Berlin] - bat_charge[06:00,Berlin] = 0
|
|
172
|
-
[00:00,Toronto]: bat_charge[00:00,Toronto] + bat_flow[00:00,Toronto] - bat_charge[06:00,Toronto] = 0
|
|
173
|
-
[06:00,Berlin]: bat_charge[06:00,Berlin] + bat_flow[06:00,Berlin] - bat_charge[12:00,Berlin] = 0
|
|
174
|
-
[06:00,Toronto]: bat_charge[06:00,Toronto] + bat_flow[06:00,Toronto] - bat_charge[12:00,Toronto] = 0
|
|
175
|
-
[12:00,Berlin]: bat_charge[12:00,Berlin] + bat_flow[12:00,Berlin] - bat_charge[18:00,Berlin] = 0
|
|
176
|
-
[12:00,Toronto]: bat_charge[12:00,Toronto] + bat_flow[12:00,Toronto] - bat_charge[18:00,Toronto] = 0
|
|
177
|
-
[18:00,Berlin]: bat_charge[18:00,Berlin] + bat_flow[18:00,Berlin] - bat_charge[00:00,Berlin] = 0
|
|
178
|
-
[18:00,Toronto]: bat_charge[18:00,Toronto] + bat_flow[18:00,Toronto] - bat_charge[00:00,Toronto] = 0
|
|
179
|
-
"""
|
|
180
|
-
|
|
181
|
-
wrapped = self.data.select(dim).unique(maintain_order=True).sort(by=dim)
|
|
182
|
-
wrapped = wrapped.with_columns(pl.col(dim).shift(-1).alias("__next"))
|
|
183
|
-
if wrap_around:
|
|
184
|
-
wrapped = wrapped.with_columns(pl.col("__next").fill_null(pl.first(dim)))
|
|
185
|
-
else:
|
|
186
|
-
wrapped = wrapped.drop_nulls(dim)
|
|
187
|
-
|
|
188
|
-
expr = self.to_expr()
|
|
189
|
-
data = expr.data.rename({dim: "__prev"})
|
|
190
|
-
data = data.join(
|
|
191
|
-
wrapped, left_on="__prev", right_on="__next", how="inner"
|
|
192
|
-
).drop(["__prev", "__next"])
|
|
193
|
-
return expr._new(data)
|
pyoframe-0.0.4.dist-info/RECORD
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
pyoframe/__init__.py,sha256=GCcDnYaiuPJgFbVznihTvYku7Hs39ayAqo5P0UvSKmM,479
|
|
2
|
-
pyoframe/_arithmetic.py,sha256=9qJRuelwU2ycVjDiUSBf3F-jmS0D4Gq_MKb79tiaU_0,9061
|
|
3
|
-
pyoframe/constants.py,sha256=dPvt90WP2oYTRf32y7hj_W6McnjyJR8fs7NUZtE6dXs,7076
|
|
4
|
-
pyoframe/constraints.py,sha256=jfX4N1XMzOlVYoPRFLP4T0vYxTqM9owsWDlgCK6vVMQ,32229
|
|
5
|
-
pyoframe/io.py,sha256=syecN1aT_d02Hu4D42IAgA13LCrQhWkrSwfONN6y9E8,4067
|
|
6
|
-
pyoframe/io_mappers.py,sha256=Il0lRGv-EvVDa3_M7vCwp0hajQfAbOVZEBDqNIwzNpg,6208
|
|
7
|
-
pyoframe/model.py,sha256=ZkVDWVorAFTXb6Be_04G707jaFqml4eVhkM3Sj1GnNU,3236
|
|
8
|
-
pyoframe/model_element.py,sha256=lj5kd5XmhPDepP4lGhsHG1m1qN_8TS-Dl1cTcfveVHo,3753
|
|
9
|
-
pyoframe/monkey_patch.py,sha256=lSsJir2fXkEl_uScYbfVle99r_WvlWvD298alP2uWlE,2082
|
|
10
|
-
pyoframe/objective.py,sha256=9vDZeDu7fT2A7-DY5WatSJrARQVRHcEWkF7GixpVMa4,1247
|
|
11
|
-
pyoframe/solvers.py,sha256=onudQUoGilQ4oOrMA7vz64a08So82dF28hjxHPQooTM,5532
|
|
12
|
-
pyoframe/util.py,sha256=iMRvnAFEFzpd23ZjgLy30ZiZGsWXpVzsCNtT_BXZYi4,9962
|
|
13
|
-
pyoframe/variables.py,sha256=tPgK3S_Csu-JqY7L5ybs0GdRB7Ha1mG0mAsHdlW0hQI,8878
|
|
14
|
-
pyoframe-0.0.4.dist-info/LICENSE,sha256=L1pXz6p_1OW5XGWb2UCR6PNu6k3JAT0XWhi8jV0cuRg,1137
|
|
15
|
-
pyoframe-0.0.4.dist-info/METADATA,sha256=Fmo2mZ0lji7wMP3lPZ4JsxuH_bCK2bCHUaMGiRMCaeE,3106
|
|
16
|
-
pyoframe-0.0.4.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
17
|
-
pyoframe-0.0.4.dist-info/top_level.txt,sha256=10z3OOJSVLriQ0IrFLMH8CH9zByugPWolqhlHlkNjV4,9
|
|
18
|
-
pyoframe-0.0.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|