openenergyid 0.1.21__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.
Potentially problematic release.
This version of openenergyid might be problematic. Click here for more details.
- openenergyid/__init__.py +8 -0
- openenergyid/baseload/__init__.py +15 -0
- openenergyid/baseload/analysis.py +173 -0
- openenergyid/baseload/exceptions.py +9 -0
- openenergyid/baseload/models.py +31 -0
- openenergyid/capacity/__init__.py +6 -0
- openenergyid/capacity/main.py +102 -0
- openenergyid/capacity/models.py +30 -0
- openenergyid/const.py +18 -0
- openenergyid/dyntar/__init__.py +20 -0
- openenergyid/dyntar/const.py +31 -0
- openenergyid/dyntar/main.py +312 -0
- openenergyid/dyntar/models.py +110 -0
- openenergyid/energysharing/__init__.py +12 -0
- openenergyid/energysharing/const.py +8 -0
- openenergyid/energysharing/data_formatting.py +69 -0
- openenergyid/energysharing/main.py +111 -0
- openenergyid/energysharing/models.py +79 -0
- openenergyid/enums.py +16 -0
- openenergyid/models.py +164 -0
- openenergyid/mvlr/__init__.py +19 -0
- openenergyid/mvlr/helpers.py +30 -0
- openenergyid/mvlr/main.py +34 -0
- openenergyid/mvlr/models.py +228 -0
- openenergyid/mvlr/mvlr.py +450 -0
- openenergyid-0.1.21.dist-info/METADATA +50 -0
- openenergyid-0.1.21.dist-info/RECORD +29 -0
- openenergyid-0.1.21.dist-info/WHEEL +5 -0
- openenergyid-0.1.21.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""Main module of the DynTar package."""
|
|
2
|
+
|
|
3
|
+
from typing import cast
|
|
4
|
+
import pandas as pd
|
|
5
|
+
|
|
6
|
+
from openenergyid.const import (
|
|
7
|
+
ELECTRICITY_DELIVERED,
|
|
8
|
+
ELECTRICITY_EXPORTED,
|
|
9
|
+
PRICE_ELECTRICITY_DELIVERED,
|
|
10
|
+
PRICE_ELECTRICITY_EXPORTED,
|
|
11
|
+
RLP,
|
|
12
|
+
SPP,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from .const import (
|
|
16
|
+
ELECTRICITY_DELIVERED_SMR3,
|
|
17
|
+
ELECTRICITY_EXPORTED_SMR3,
|
|
18
|
+
ELECTRICITY_DELIVERED_SMR2,
|
|
19
|
+
ELECTRICITY_EXPORTED_SMR2,
|
|
20
|
+
COST_ELECTRICITY_DELIVERED_SMR2,
|
|
21
|
+
COST_ELECTRICITY_EXPORTED_SMR2,
|
|
22
|
+
COST_ELECTRICITY_DELIVERED_SMR3,
|
|
23
|
+
COST_ELECTRICITY_EXPORTED_SMR3,
|
|
24
|
+
RLP_WEIGHTED_PRICE_DELIVERED,
|
|
25
|
+
SPP_WEIGHTED_PRICE_EXPORTED,
|
|
26
|
+
HEATMAP_DELIVERED,
|
|
27
|
+
HEATMAP_EXPORTED,
|
|
28
|
+
HEATMAP_TOTAL,
|
|
29
|
+
HEATMAP_DELIVERED_DESCRIPTION,
|
|
30
|
+
HEATMAP_EXPORTED_DESCRIPTION,
|
|
31
|
+
HEATMAP_TOTAL_DESCRIPTION,
|
|
32
|
+
Register,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def weigh_by_monthly_profile(df: pd.DataFrame, series_name, profile_name) -> pd.Series:
|
|
37
|
+
"""Weigh a time series by a monthly profile."""
|
|
38
|
+
grouped = df.groupby(pd.Grouper(freq="MS"))
|
|
39
|
+
return grouped[series_name].transform("sum") * grouped[profile_name].transform(
|
|
40
|
+
lambda x: x / x.sum()
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def extend_dataframe_with_smr2(
|
|
45
|
+
df: pd.DataFrame,
|
|
46
|
+
inplace: bool = False,
|
|
47
|
+
registers: list[Register] | None = None,
|
|
48
|
+
) -> pd.DataFrame | None:
|
|
49
|
+
"""Extend a DataFrame with the SMR2 columns."""
|
|
50
|
+
if not inplace:
|
|
51
|
+
result_df = df.copy()
|
|
52
|
+
else:
|
|
53
|
+
result_df = df
|
|
54
|
+
|
|
55
|
+
if registers is None:
|
|
56
|
+
registers = [Register.DELIVERY, Register.EXPORT]
|
|
57
|
+
|
|
58
|
+
if Register.DELIVERY in registers:
|
|
59
|
+
result_df[ELECTRICITY_DELIVERED_SMR2] = weigh_by_monthly_profile(
|
|
60
|
+
df, ELECTRICITY_DELIVERED, RLP
|
|
61
|
+
)
|
|
62
|
+
if Register.EXPORT in registers:
|
|
63
|
+
result_df[ELECTRICITY_EXPORTED_SMR2] = weigh_by_monthly_profile(
|
|
64
|
+
df, ELECTRICITY_EXPORTED, SPP
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
result_df.rename(
|
|
68
|
+
columns={
|
|
69
|
+
ELECTRICITY_DELIVERED: ELECTRICITY_DELIVERED_SMR3,
|
|
70
|
+
ELECTRICITY_EXPORTED: ELECTRICITY_EXPORTED_SMR3,
|
|
71
|
+
},
|
|
72
|
+
inplace=True,
|
|
73
|
+
errors="ignore",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if not inplace:
|
|
77
|
+
return result_df
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def extend_dataframe_with_costs(
|
|
82
|
+
df: pd.DataFrame, inplace: bool = False, registers: list[Register] | None = None
|
|
83
|
+
) -> pd.DataFrame | None:
|
|
84
|
+
"""Extend a DataFrame with the cost columns."""
|
|
85
|
+
if not inplace:
|
|
86
|
+
result_df = df.copy()
|
|
87
|
+
else:
|
|
88
|
+
result_df = df
|
|
89
|
+
|
|
90
|
+
if registers is None:
|
|
91
|
+
registers = [Register.DELIVERY, Register.EXPORT]
|
|
92
|
+
|
|
93
|
+
if Register.DELIVERY in registers:
|
|
94
|
+
result_df[COST_ELECTRICITY_DELIVERED_SMR2] = (
|
|
95
|
+
df[ELECTRICITY_DELIVERED_SMR2] * df[PRICE_ELECTRICITY_DELIVERED]
|
|
96
|
+
)
|
|
97
|
+
result_df[COST_ELECTRICITY_DELIVERED_SMR3] = (
|
|
98
|
+
df[ELECTRICITY_DELIVERED_SMR3] * df[PRICE_ELECTRICITY_DELIVERED]
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if Register.EXPORT in registers:
|
|
102
|
+
result_df[COST_ELECTRICITY_EXPORTED_SMR2] = (
|
|
103
|
+
df[ELECTRICITY_EXPORTED_SMR2] * df[PRICE_ELECTRICITY_EXPORTED] * -1
|
|
104
|
+
)
|
|
105
|
+
result_df[COST_ELECTRICITY_EXPORTED_SMR3] = (
|
|
106
|
+
df[ELECTRICITY_EXPORTED_SMR3] * df[PRICE_ELECTRICITY_EXPORTED] * -1
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
if not inplace:
|
|
110
|
+
return result_df
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def extend_dataframe_with_weighted_prices(
|
|
115
|
+
df: pd.DataFrame, inplace: bool = False, registers: list[Register] | None = None
|
|
116
|
+
) -> pd.DataFrame | None:
|
|
117
|
+
"""Extend a DataFrame with the weighted price columns."""
|
|
118
|
+
if not inplace:
|
|
119
|
+
df = df.copy()
|
|
120
|
+
|
|
121
|
+
if registers is None:
|
|
122
|
+
registers = [Register.DELIVERY, Register.EXPORT]
|
|
123
|
+
|
|
124
|
+
if Register.DELIVERY in registers:
|
|
125
|
+
rlp_weighted_price_delivered = (df[PRICE_ELECTRICITY_DELIVERED] * df[RLP]).resample(
|
|
126
|
+
"MS"
|
|
127
|
+
).sum() / df[RLP].resample("MS").sum()
|
|
128
|
+
df[RLP_WEIGHTED_PRICE_DELIVERED] = rlp_weighted_price_delivered.reindex_like(
|
|
129
|
+
df[RLP], method="ffill"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if Register.EXPORT in registers:
|
|
133
|
+
spp_weighted_price_exported = (df[PRICE_ELECTRICITY_EXPORTED] * df[SPP]).resample(
|
|
134
|
+
"MS"
|
|
135
|
+
).sum() / df[SPP].resample("MS").sum()
|
|
136
|
+
df[SPP_WEIGHTED_PRICE_EXPORTED] = spp_weighted_price_exported.reindex_like(
|
|
137
|
+
df[SPP], method="ffill"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
if not inplace:
|
|
141
|
+
return df
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def extend_dataframe_with_heatmap(
|
|
146
|
+
df: pd.DataFrame, inplace: bool = False, registers: list[Register] | None = None
|
|
147
|
+
) -> pd.DataFrame | None:
|
|
148
|
+
"""Extend a DataFrame with the heatmap columns."""
|
|
149
|
+
if not inplace:
|
|
150
|
+
df = df.copy()
|
|
151
|
+
|
|
152
|
+
if registers is None:
|
|
153
|
+
registers = [Register.DELIVERY, Register.EXPORT]
|
|
154
|
+
|
|
155
|
+
if Register.DELIVERY in registers:
|
|
156
|
+
energy_delta_delivered = df[ELECTRICITY_DELIVERED_SMR2] - df[ELECTRICITY_DELIVERED_SMR3]
|
|
157
|
+
price_delta_delivered = df[RLP_WEIGHTED_PRICE_DELIVERED] - df[PRICE_ELECTRICITY_DELIVERED]
|
|
158
|
+
heatmap_score_delivered = energy_delta_delivered * price_delta_delivered
|
|
159
|
+
heatmap_score_delivered.fillna(0, inplace=True)
|
|
160
|
+
# Invert score so that positive values indicate a positive impact
|
|
161
|
+
heatmap_score_delivered = -heatmap_score_delivered
|
|
162
|
+
df[HEATMAP_DELIVERED] = heatmap_score_delivered
|
|
163
|
+
|
|
164
|
+
if Register.EXPORT in registers:
|
|
165
|
+
energy_delta_exported = df[ELECTRICITY_EXPORTED_SMR2] - df[ELECTRICITY_EXPORTED_SMR3]
|
|
166
|
+
price_delta_exported = df[SPP_WEIGHTED_PRICE_EXPORTED] - df[PRICE_ELECTRICITY_EXPORTED]
|
|
167
|
+
heatmap_score_exported = energy_delta_exported * price_delta_exported
|
|
168
|
+
heatmap_score_exported.fillna(0, inplace=True)
|
|
169
|
+
df[HEATMAP_EXPORTED] = heatmap_score_exported
|
|
170
|
+
|
|
171
|
+
if Register.DELIVERY in registers and Register.EXPORT in registers:
|
|
172
|
+
heatmap_score_delivered = cast(pd.Series, df[HEATMAP_DELIVERED])
|
|
173
|
+
heatmap_score_exported = cast(pd.Series, df[HEATMAP_EXPORTED])
|
|
174
|
+
heatmap_score_combined = heatmap_score_delivered + heatmap_score_exported
|
|
175
|
+
elif Register.DELIVERY in registers:
|
|
176
|
+
heatmap_score_combined = heatmap_score_delivered
|
|
177
|
+
else:
|
|
178
|
+
heatmap_score_combined = heatmap_score_exported
|
|
179
|
+
df[HEATMAP_TOTAL] = heatmap_score_combined
|
|
180
|
+
|
|
181
|
+
if not inplace:
|
|
182
|
+
return df
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def map_delivery_description(
|
|
187
|
+
price_delivered, price_rlp, electricity_delivered_smr3, electricity_delivered_smr2
|
|
188
|
+
):
|
|
189
|
+
"""Map the delivery description."""
|
|
190
|
+
if price_delivered > price_rlp and electricity_delivered_smr3 > electricity_delivered_smr2:
|
|
191
|
+
return 1
|
|
192
|
+
if price_delivered > price_rlp and electricity_delivered_smr3 < electricity_delivered_smr2:
|
|
193
|
+
return 2
|
|
194
|
+
if price_delivered < price_rlp and electricity_delivered_smr3 > electricity_delivered_smr2:
|
|
195
|
+
return 3
|
|
196
|
+
if price_delivered < price_rlp and electricity_delivered_smr3 < electricity_delivered_smr2:
|
|
197
|
+
return 4
|
|
198
|
+
return 0
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def map_export_description(
|
|
202
|
+
price_exported, price_spp, electricity_exported_smr3, electricity_exported_smr2
|
|
203
|
+
):
|
|
204
|
+
"""Map the export description."""
|
|
205
|
+
if price_exported > price_spp and electricity_exported_smr3 > electricity_exported_smr2:
|
|
206
|
+
return 5
|
|
207
|
+
if price_exported > price_spp and electricity_exported_smr3 < electricity_exported_smr2:
|
|
208
|
+
return 6
|
|
209
|
+
if price_exported < price_spp and electricity_exported_smr3 > electricity_exported_smr2:
|
|
210
|
+
return 7
|
|
211
|
+
if price_exported < price_spp and electricity_exported_smr3 < electricity_exported_smr2:
|
|
212
|
+
return 8
|
|
213
|
+
return 0
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def map_total_description(
|
|
217
|
+
abs_heatmap_delivered, abs_heatmap_exported, delivered_description, exported_description
|
|
218
|
+
):
|
|
219
|
+
"""Map the total description."""
|
|
220
|
+
if abs_heatmap_delivered > abs_heatmap_exported:
|
|
221
|
+
return delivered_description
|
|
222
|
+
return exported_description
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def extend_dataframe_with_heatmap_description(
|
|
226
|
+
df: pd.DataFrame, inplace: bool = False, registers: list[Register] | None = None
|
|
227
|
+
) -> pd.DataFrame | None:
|
|
228
|
+
"""Extend a DataFrame with the heatmap description columns."""
|
|
229
|
+
if not inplace:
|
|
230
|
+
df = df.copy()
|
|
231
|
+
|
|
232
|
+
if registers is None:
|
|
233
|
+
registers = [Register.DELIVERY, Register.EXPORT]
|
|
234
|
+
|
|
235
|
+
if Register.DELIVERY in registers:
|
|
236
|
+
df[HEATMAP_DELIVERED_DESCRIPTION] = list(
|
|
237
|
+
map(
|
|
238
|
+
map_delivery_description,
|
|
239
|
+
df[PRICE_ELECTRICITY_DELIVERED],
|
|
240
|
+
df[RLP_WEIGHTED_PRICE_DELIVERED],
|
|
241
|
+
df[ELECTRICITY_DELIVERED_SMR3],
|
|
242
|
+
df[ELECTRICITY_DELIVERED_SMR2],
|
|
243
|
+
)
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
if Register.EXPORT in registers:
|
|
247
|
+
df[HEATMAP_EXPORTED_DESCRIPTION] = list(
|
|
248
|
+
map(
|
|
249
|
+
map_export_description,
|
|
250
|
+
df[PRICE_ELECTRICITY_EXPORTED],
|
|
251
|
+
df[SPP_WEIGHTED_PRICE_EXPORTED],
|
|
252
|
+
df[ELECTRICITY_EXPORTED_SMR3],
|
|
253
|
+
df[ELECTRICITY_EXPORTED_SMR2],
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
if Register.DELIVERY in registers and Register.EXPORT in registers:
|
|
258
|
+
df[HEATMAP_TOTAL_DESCRIPTION] = list(
|
|
259
|
+
map(
|
|
260
|
+
map_total_description,
|
|
261
|
+
df[HEATMAP_DELIVERED].abs(),
|
|
262
|
+
df[HEATMAP_EXPORTED].abs(),
|
|
263
|
+
df[HEATMAP_DELIVERED_DESCRIPTION],
|
|
264
|
+
df[HEATMAP_EXPORTED_DESCRIPTION],
|
|
265
|
+
)
|
|
266
|
+
)
|
|
267
|
+
elif Register.DELIVERY in registers:
|
|
268
|
+
df[HEATMAP_TOTAL_DESCRIPTION] = df[HEATMAP_DELIVERED_DESCRIPTION]
|
|
269
|
+
else:
|
|
270
|
+
df[HEATMAP_TOTAL_DESCRIPTION] = df[HEATMAP_EXPORTED_DESCRIPTION]
|
|
271
|
+
|
|
272
|
+
if not inplace:
|
|
273
|
+
return df
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def calculate_dyntar_columns(
|
|
277
|
+
df: pd.DataFrame,
|
|
278
|
+
inplace: bool = False,
|
|
279
|
+
registers: list[Register] | None = None,
|
|
280
|
+
) -> pd.DataFrame | None:
|
|
281
|
+
"""Calculate all columns required for the dynamic tariff analysis."""
|
|
282
|
+
if not inplace:
|
|
283
|
+
df = df.copy()
|
|
284
|
+
|
|
285
|
+
if registers is None:
|
|
286
|
+
registers = [Register.DELIVERY, Register.EXPORT]
|
|
287
|
+
|
|
288
|
+
extend_dataframe_with_smr2(df, inplace=True, registers=registers)
|
|
289
|
+
extend_dataframe_with_costs(df, inplace=True, registers=registers)
|
|
290
|
+
extend_dataframe_with_weighted_prices(df, inplace=True, registers=registers)
|
|
291
|
+
extend_dataframe_with_heatmap(df, inplace=True, registers=registers)
|
|
292
|
+
extend_dataframe_with_heatmap_description(df, inplace=True, registers=registers)
|
|
293
|
+
|
|
294
|
+
if not inplace:
|
|
295
|
+
return df
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def summarize_result(df: pd.DataFrame) -> pd.Series:
|
|
300
|
+
"""Summarize the dynamic tariff analysis result."""
|
|
301
|
+
summary = df.filter(like="cost").sum()
|
|
302
|
+
|
|
303
|
+
abs_smr2 = summary.filter(like="smr2").abs().sum()
|
|
304
|
+
|
|
305
|
+
summary["cost_electricity_total_smr2"] = summary.filter(like="smr2").sum()
|
|
306
|
+
summary["cost_electricity_total_smr3"] = summary.filter(like="smr3").sum()
|
|
307
|
+
|
|
308
|
+
summary["ratio"] = (
|
|
309
|
+
summary["cost_electricity_total_smr3"] - summary["cost_electricity_total_smr2"]
|
|
310
|
+
) / abs_smr2
|
|
311
|
+
|
|
312
|
+
return summary
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Models for dynamic tariff analysis."""
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
from pydantic import Field, conlist, confloat, BaseModel
|
|
5
|
+
|
|
6
|
+
from openenergyid.models import TimeDataFrame
|
|
7
|
+
from .const import Register
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
RequiredColumns = Literal[
|
|
11
|
+
"electricity_delivered",
|
|
12
|
+
"electricity_exported",
|
|
13
|
+
"price_electricity_delivered",
|
|
14
|
+
"price_electricity_exported",
|
|
15
|
+
"RLP",
|
|
16
|
+
"SPP",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
OutputColumns = Literal[
|
|
20
|
+
"electricity_delivered_smr3",
|
|
21
|
+
"electricity_exported_smr3",
|
|
22
|
+
"price_electricity_delivered",
|
|
23
|
+
"price_electricity_exported",
|
|
24
|
+
"RLP",
|
|
25
|
+
"SPP",
|
|
26
|
+
"electricity_delivered_smr2",
|
|
27
|
+
"electricity_exported_smr2",
|
|
28
|
+
"cost_electricity_delivered_smr2",
|
|
29
|
+
"cost_electricity_exported_smr2",
|
|
30
|
+
"cost_electricity_delivered_smr3",
|
|
31
|
+
"cost_electricity_exported_smr3",
|
|
32
|
+
"rlp_weighted_price_delivered",
|
|
33
|
+
"spp_weighted_price_exported",
|
|
34
|
+
"heatmap_delivered",
|
|
35
|
+
"heatmap_exported",
|
|
36
|
+
"heatmap_total",
|
|
37
|
+
"heatmap_delivered_description",
|
|
38
|
+
"heatmap_exported_description",
|
|
39
|
+
"heatmap_total_description",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class DynamicTariffAnalysisInput(TimeDataFrame):
|
|
44
|
+
"""Input frame for dynamic tariff analysis."""
|
|
45
|
+
|
|
46
|
+
columns: list[RequiredColumns] = Field(
|
|
47
|
+
min_length=3,
|
|
48
|
+
max_length=len(RequiredColumns.__args__),
|
|
49
|
+
examples=[RequiredColumns.__args__],
|
|
50
|
+
)
|
|
51
|
+
data: list[
|
|
52
|
+
conlist(
|
|
53
|
+
item_type=confloat(allow_inf_nan=True),
|
|
54
|
+
min_length=3,
|
|
55
|
+
max_length=len(RequiredColumns.__args__),
|
|
56
|
+
) # type: ignore
|
|
57
|
+
] = Field(examples=[[0.0] * len(RequiredColumns.__args__)])
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def registers(self) -> list[Register]:
|
|
61
|
+
"""Check which registers are present in the input data."""
|
|
62
|
+
registers = []
|
|
63
|
+
columns = list(self.columns)
|
|
64
|
+
# if "electricity_delivered", "price_electricity_delivered" and "RLP" are present
|
|
65
|
+
if all(
|
|
66
|
+
column in columns
|
|
67
|
+
for column in [
|
|
68
|
+
"electricity_delivered",
|
|
69
|
+
"price_electricity_delivered",
|
|
70
|
+
"RLP",
|
|
71
|
+
]
|
|
72
|
+
):
|
|
73
|
+
registers.append(Register.DELIVERY)
|
|
74
|
+
# if "electricity_exported", "price_electricity_exported" and "SPP" are present
|
|
75
|
+
if all(
|
|
76
|
+
column in columns
|
|
77
|
+
for column in ["electricity_exported", "price_electricity_exported", "SPP"]
|
|
78
|
+
):
|
|
79
|
+
registers.append(Register.EXPORT)
|
|
80
|
+
return registers
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class DynamicTariffAnalysisOutputSummary(BaseModel):
|
|
84
|
+
"""Summary of the dynamic tariff analysis output."""
|
|
85
|
+
|
|
86
|
+
cost_electricity_delivered_smr2: float | None = None
|
|
87
|
+
cost_electricity_delivered_smr3: float | None = None
|
|
88
|
+
cost_electricity_exported_smr2: float | None = None
|
|
89
|
+
cost_electricity_exported_smr3: float | None = None
|
|
90
|
+
cost_electricity_total_smr2: float | None = None
|
|
91
|
+
cost_electricity_total_smr3: float | None = None
|
|
92
|
+
ratio: float | None = None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class DynamicTariffAnalysisOutput(TimeDataFrame):
|
|
96
|
+
"""Output frame for dynamic tariff analysis."""
|
|
97
|
+
|
|
98
|
+
columns: list[OutputColumns] = Field(
|
|
99
|
+
min_length=1,
|
|
100
|
+
max_length=len(OutputColumns.__args__),
|
|
101
|
+
examples=[OutputColumns.__args__],
|
|
102
|
+
)
|
|
103
|
+
data: list[
|
|
104
|
+
conlist(
|
|
105
|
+
item_type=confloat(allow_inf_nan=True),
|
|
106
|
+
min_length=1,
|
|
107
|
+
max_length=len(OutputColumns.__args__),
|
|
108
|
+
) # type: ignore
|
|
109
|
+
] = Field(examples=[[0.0] * len(OutputColumns.__args__)])
|
|
110
|
+
summary: DynamicTariffAnalysisOutputSummary | None = None
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Energy Sharing package."""
|
|
2
|
+
|
|
3
|
+
from .main import calculate
|
|
4
|
+
from .models import CalculationMethod, EnergySharingInput, EnergySharingOutput, KeyInput
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"calculate",
|
|
8
|
+
"CalculationMethod",
|
|
9
|
+
"EnergySharingInput",
|
|
10
|
+
"EnergySharingOutput",
|
|
11
|
+
"KeyInput",
|
|
12
|
+
]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Functions to create multi-indexed DataFrames for input and output data for energy sharing."""
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from .const import GROSS_INJECTION, GROSS_OFFTAKE, KEY, NET_INJECTION, NET_OFFTAKE, SHARED_ENERGY
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def create_multi_index_input_frame(
|
|
8
|
+
gross_injection: pd.DataFrame,
|
|
9
|
+
gross_offtake: pd.DataFrame,
|
|
10
|
+
key: pd.DataFrame,
|
|
11
|
+
) -> pd.DataFrame:
|
|
12
|
+
"""Create a multi-indexed DataFrame with the input data for energy sharing."""
|
|
13
|
+
gross_injection = gross_injection.copy()
|
|
14
|
+
gross_offtake = gross_offtake.copy()
|
|
15
|
+
key = key.copy()
|
|
16
|
+
|
|
17
|
+
gross_injection.columns = pd.MultiIndex.from_product(
|
|
18
|
+
[[GROSS_INJECTION], gross_injection.columns]
|
|
19
|
+
)
|
|
20
|
+
gross_offtake.columns = pd.MultiIndex.from_product([[GROSS_OFFTAKE], gross_offtake.columns])
|
|
21
|
+
key.columns = pd.MultiIndex.from_product([[KEY], key.columns])
|
|
22
|
+
|
|
23
|
+
df = pd.concat([gross_injection, gross_offtake, key], axis=1)
|
|
24
|
+
|
|
25
|
+
return df
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def create_multi_index_output_frame(
|
|
29
|
+
net_injection: pd.DataFrame,
|
|
30
|
+
net_offtake: pd.DataFrame,
|
|
31
|
+
shared_energy: pd.DataFrame,
|
|
32
|
+
) -> pd.DataFrame:
|
|
33
|
+
"""Create a multi-indexed DataFrame with the output data for energy sharing."""
|
|
34
|
+
net_injection = net_injection.copy()
|
|
35
|
+
net_offtake = net_offtake.copy()
|
|
36
|
+
shared_energy = shared_energy.copy()
|
|
37
|
+
|
|
38
|
+
net_injection.columns = pd.MultiIndex.from_product([[NET_INJECTION], net_injection.columns])
|
|
39
|
+
net_offtake.columns = pd.MultiIndex.from_product([[NET_OFFTAKE], net_offtake.columns])
|
|
40
|
+
shared_energy.columns = pd.MultiIndex.from_product([[SHARED_ENERGY], shared_energy.columns])
|
|
41
|
+
|
|
42
|
+
df = pd.concat([net_injection, net_offtake, shared_energy], axis=1)
|
|
43
|
+
|
|
44
|
+
return df
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def result_to_input_for_reiteration(result: pd.DataFrame, key: pd.DataFrame) -> pd.DataFrame:
|
|
48
|
+
"""Create a multi-indexed DataFrame with the input data for energy sharing after the first iteration."""
|
|
49
|
+
# We iterate again. The net injection of the previous result is taken as gross injection input
|
|
50
|
+
# And the net offtake is taken as the gross offtake input
|
|
51
|
+
# When a user's net offtake is 0, the key is set to 0; and the keys are re-normalized
|
|
52
|
+
|
|
53
|
+
gross_injection = result[NET_INJECTION].copy()
|
|
54
|
+
gross_offtake = result[NET_OFFTAKE].copy()
|
|
55
|
+
|
|
56
|
+
# Take the original key, but replace the value with 0.0 if result[NET_OFFTAKE] is 0.0
|
|
57
|
+
|
|
58
|
+
key = key.copy()
|
|
59
|
+
key = key.where(~result[NET_OFFTAKE].eq(0), 0)
|
|
60
|
+
|
|
61
|
+
# Re-normalize the keys
|
|
62
|
+
|
|
63
|
+
key = key.div(key.sum(axis=1), axis=0)
|
|
64
|
+
|
|
65
|
+
df = create_multi_index_input_frame(
|
|
66
|
+
gross_injection=gross_injection, gross_offtake=gross_offtake, key=key
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return df
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Main Calcuation Module for Energy Sharing."""
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from .models import CalculationMethod
|
|
5
|
+
from .const import GROSS_INJECTION, GROSS_OFFTAKE, KEY, NET_INJECTION, NET_OFFTAKE, SHARED_ENERGY
|
|
6
|
+
from .data_formatting import create_multi_index_output_frame, result_to_input_for_reiteration
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _calculate(df: pd.DataFrame, method: CalculationMethod) -> pd.DataFrame:
|
|
10
|
+
"""Calculate the energy sharing for the given input data. This function is not iterative."""
|
|
11
|
+
# Step 1: Calculate the maximum available gross injection that can be shared
|
|
12
|
+
# A participant cannot share their injection with themselves
|
|
13
|
+
|
|
14
|
+
# Take the injection of P1, and divide it per participant as per their key
|
|
15
|
+
|
|
16
|
+
injections_to_share = []
|
|
17
|
+
rest = {}
|
|
18
|
+
|
|
19
|
+
for participant in df[GROSS_INJECTION].columns:
|
|
20
|
+
injection_to_share = df[GROSS_INJECTION][participant].copy()
|
|
21
|
+
|
|
22
|
+
key = df[KEY].copy()
|
|
23
|
+
if method == CalculationMethod.RELATIVE or method == CalculationMethod.OPTIMAL:
|
|
24
|
+
# Set the key of the current participant to 0
|
|
25
|
+
# Re-normalize the keys for the other participants
|
|
26
|
+
if participant in df[KEY].columns:
|
|
27
|
+
key.loc[:, participant] = 0
|
|
28
|
+
key = key.div(key.sum(axis=1), axis=0)
|
|
29
|
+
|
|
30
|
+
# Multiply injection_to_share with the key of each participant
|
|
31
|
+
shared_by_participant = (injection_to_share * key.T).T
|
|
32
|
+
shared_by_participant.fillna(0, inplace=True)
|
|
33
|
+
|
|
34
|
+
# Set the value for the current participant to 0
|
|
35
|
+
if participant in shared_by_participant.columns:
|
|
36
|
+
shared_by_participant.loc[:, participant] = 0
|
|
37
|
+
|
|
38
|
+
# Put the not shared injection in the rest
|
|
39
|
+
rest[participant] = injection_to_share - shared_by_participant.sum(axis=1)
|
|
40
|
+
|
|
41
|
+
injections_to_share.append(shared_by_participant)
|
|
42
|
+
|
|
43
|
+
# Sum the injections to share
|
|
44
|
+
max_allocated_injection = sum(injections_to_share)
|
|
45
|
+
|
|
46
|
+
# Concat the rest
|
|
47
|
+
injection_that_cannot_be_shared = pd.concat(rest, axis=1)
|
|
48
|
+
|
|
49
|
+
# Step 2: Calculate the Net Offtake, by assigning the injections to each participant
|
|
50
|
+
# But, a participant cannot receive more than their offtake
|
|
51
|
+
|
|
52
|
+
net_offtake = df[GROSS_OFFTAKE] - max_allocated_injection
|
|
53
|
+
|
|
54
|
+
# Sum all negative values into a column "Not Shared"
|
|
55
|
+
not_shared_after_assignment = net_offtake.clip(upper=0).sum(axis=1).abs()
|
|
56
|
+
|
|
57
|
+
# Clip the values to 0
|
|
58
|
+
net_offtake = net_offtake.clip(lower=0)
|
|
59
|
+
|
|
60
|
+
# Calculate the amount of actual shared energy
|
|
61
|
+
# This is the difference between the gross offtake and the net offtake
|
|
62
|
+
shared_energy = df[GROSS_OFFTAKE] - net_offtake
|
|
63
|
+
|
|
64
|
+
# Step 3: Assign the Rests back to the original injectors
|
|
65
|
+
|
|
66
|
+
# The energy that is not shared after assignment
|
|
67
|
+
# should be divided back to the original injectors
|
|
68
|
+
# A ratio of the original injection should be used
|
|
69
|
+
|
|
70
|
+
re_distributed_not_shared = (
|
|
71
|
+
(df[GROSS_INJECTION].T / df[GROSS_INJECTION].sum(axis=1)) * not_shared_after_assignment
|
|
72
|
+
).T
|
|
73
|
+
re_distributed_not_shared.fillna(0, inplace=True)
|
|
74
|
+
|
|
75
|
+
# The nett injection is the sum of:
|
|
76
|
+
# the injection that cannot be shared to begin with
|
|
77
|
+
# (because participants cannot share with themselves)
|
|
78
|
+
# and the injection that cannot be shared after assignment
|
|
79
|
+
# (because participants cannot receive more than their offtake)
|
|
80
|
+
|
|
81
|
+
net_injection = injection_that_cannot_be_shared + re_distributed_not_shared
|
|
82
|
+
|
|
83
|
+
result = create_multi_index_output_frame(
|
|
84
|
+
net_injection=net_injection, net_offtake=net_offtake, shared_energy=shared_energy
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return result
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def calculate(df: pd.DataFrame, method: CalculationMethod) -> pd.DataFrame:
|
|
91
|
+
"""Calculate the energy sharing for the given input data.
|
|
92
|
+
|
|
93
|
+
This function is iterative if the method is optimal."""
|
|
94
|
+
result = _calculate(df, method)
|
|
95
|
+
|
|
96
|
+
if method in [CalculationMethod.FIXED, CalculationMethod.RELATIVE]:
|
|
97
|
+
return result
|
|
98
|
+
|
|
99
|
+
# Optimal method, we iterate until the amount of shared energy is 0
|
|
100
|
+
final_result = result.copy()
|
|
101
|
+
while not result[SHARED_ENERGY].eq(0).all().all():
|
|
102
|
+
df = result_to_input_for_reiteration(result, df[KEY])
|
|
103
|
+
result = _calculate(df, method)
|
|
104
|
+
|
|
105
|
+
# Add the result to the final result
|
|
106
|
+
# Overwrite NET_INJECTION and NET_OFFTAKE, Sum SHARED_ENERGY
|
|
107
|
+
final_result[NET_INJECTION] = result[NET_INJECTION]
|
|
108
|
+
final_result[NET_OFFTAKE] = result[NET_OFFTAKE]
|
|
109
|
+
final_result[SHARED_ENERGY] += result[SHARED_ENERGY]
|
|
110
|
+
|
|
111
|
+
return final_result
|