openenergyid 0.1.31__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.
- openenergyid/__init__.py +8 -0
- openenergyid/abstractsim/__init__.py +5 -0
- openenergyid/abstractsim/abstract.py +102 -0
- openenergyid/baseload/__init__.py +15 -0
- openenergyid/baseload/analysis.py +190 -0
- openenergyid/baseload/exceptions.py +9 -0
- openenergyid/baseload/models.py +32 -0
- openenergyid/capacity/__init__.py +6 -0
- openenergyid/capacity/main.py +103 -0
- openenergyid/capacity/models.py +32 -0
- openenergyid/const.py +29 -0
- openenergyid/dyntar/__init__.py +20 -0
- openenergyid/dyntar/const.py +31 -0
- openenergyid/dyntar/main.py +313 -0
- openenergyid/dyntar/models.py +101 -0
- openenergyid/elia/__init__.py +4 -0
- openenergyid/elia/api.py +91 -0
- openenergyid/elia/const.py +18 -0
- openenergyid/energysharing/__init__.py +12 -0
- openenergyid/energysharing/const.py +8 -0
- openenergyid/energysharing/data_formatting.py +77 -0
- openenergyid/energysharing/main.py +122 -0
- openenergyid/energysharing/models.py +80 -0
- openenergyid/enums.py +16 -0
- openenergyid/models.py +174 -0
- openenergyid/mvlr/__init__.py +19 -0
- openenergyid/mvlr/helpers.py +30 -0
- openenergyid/mvlr/main.py +34 -0
- openenergyid/mvlr/models.py +227 -0
- openenergyid/mvlr/mvlr.py +450 -0
- openenergyid/pvsim/__init__.py +8 -0
- openenergyid/pvsim/abstract.py +60 -0
- openenergyid/pvsim/elia/__init__.py +3 -0
- openenergyid/pvsim/elia/main.py +89 -0
- openenergyid/pvsim/main.py +49 -0
- openenergyid/pvsim/pvlib/__init__.py +11 -0
- openenergyid/pvsim/pvlib/main.py +115 -0
- openenergyid/pvsim/pvlib/models.py +235 -0
- openenergyid/pvsim/pvlib/quickscan.py +99 -0
- openenergyid/pvsim/pvlib/weather.py +91 -0
- openenergyid/sim/__init__.py +5 -0
- openenergyid/sim/main.py +67 -0
- openenergyid/simeval/__init__.py +6 -0
- openenergyid/simeval/main.py +148 -0
- openenergyid/simeval/models.py +162 -0
- openenergyid-0.1.31.dist-info/METADATA +32 -0
- openenergyid-0.1.31.dist-info/RECORD +50 -0
- openenergyid-0.1.31.dist-info/WHEEL +5 -0
- openenergyid-0.1.31.dist-info/licenses/LICENSE +21 -0
- openenergyid-0.1.31.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""Main module of the DynTar package."""
|
|
2
|
+
|
|
3
|
+
from typing import cast
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
from openenergyid.const import (
|
|
8
|
+
ELECTRICITY_DELIVERED,
|
|
9
|
+
ELECTRICITY_EXPORTED,
|
|
10
|
+
PRICE_ELECTRICITY_DELIVERED,
|
|
11
|
+
PRICE_ELECTRICITY_EXPORTED,
|
|
12
|
+
RLP,
|
|
13
|
+
SPP,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from .const import (
|
|
17
|
+
COST_ELECTRICITY_DELIVERED_SMR2,
|
|
18
|
+
COST_ELECTRICITY_DELIVERED_SMR3,
|
|
19
|
+
COST_ELECTRICITY_EXPORTED_SMR2,
|
|
20
|
+
COST_ELECTRICITY_EXPORTED_SMR3,
|
|
21
|
+
ELECTRICITY_DELIVERED_SMR2,
|
|
22
|
+
ELECTRICITY_DELIVERED_SMR3,
|
|
23
|
+
ELECTRICITY_EXPORTED_SMR2,
|
|
24
|
+
ELECTRICITY_EXPORTED_SMR3,
|
|
25
|
+
HEATMAP_DELIVERED,
|
|
26
|
+
HEATMAP_DELIVERED_DESCRIPTION,
|
|
27
|
+
HEATMAP_EXPORTED,
|
|
28
|
+
HEATMAP_EXPORTED_DESCRIPTION,
|
|
29
|
+
HEATMAP_TOTAL,
|
|
30
|
+
HEATMAP_TOTAL_DESCRIPTION,
|
|
31
|
+
RLP_WEIGHTED_PRICE_DELIVERED,
|
|
32
|
+
SPP_WEIGHTED_PRICE_EXPORTED,
|
|
33
|
+
Register,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def weigh_by_monthly_profile(df: pd.DataFrame, series_name, profile_name) -> pd.Series:
|
|
38
|
+
"""Weigh a time series by a monthly profile."""
|
|
39
|
+
grouped = df.groupby(pd.Grouper(freq="MS"))
|
|
40
|
+
return grouped[series_name].transform("sum") * grouped[profile_name].transform(
|
|
41
|
+
lambda x: x / x.sum()
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def extend_dataframe_with_smr2(
|
|
46
|
+
df: pd.DataFrame,
|
|
47
|
+
inplace: bool = False,
|
|
48
|
+
registers: list[Register] | None = None,
|
|
49
|
+
) -> pd.DataFrame | None:
|
|
50
|
+
"""Extend a DataFrame with the SMR2 columns."""
|
|
51
|
+
if not inplace:
|
|
52
|
+
result_df = df.copy()
|
|
53
|
+
else:
|
|
54
|
+
result_df = df
|
|
55
|
+
|
|
56
|
+
if registers is None:
|
|
57
|
+
registers = [Register.DELIVERY, Register.EXPORT]
|
|
58
|
+
|
|
59
|
+
if Register.DELIVERY in registers:
|
|
60
|
+
result_df[ELECTRICITY_DELIVERED_SMR2] = weigh_by_monthly_profile(
|
|
61
|
+
df, ELECTRICITY_DELIVERED, RLP
|
|
62
|
+
)
|
|
63
|
+
if Register.EXPORT in registers:
|
|
64
|
+
result_df[ELECTRICITY_EXPORTED_SMR2] = weigh_by_monthly_profile(
|
|
65
|
+
df, ELECTRICITY_EXPORTED, SPP
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
result_df.rename(
|
|
69
|
+
columns={
|
|
70
|
+
ELECTRICITY_DELIVERED: ELECTRICITY_DELIVERED_SMR3,
|
|
71
|
+
ELECTRICITY_EXPORTED: ELECTRICITY_EXPORTED_SMR3,
|
|
72
|
+
},
|
|
73
|
+
inplace=True,
|
|
74
|
+
errors="ignore",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
if not inplace:
|
|
78
|
+
return result_df
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def extend_dataframe_with_costs(
|
|
83
|
+
df: pd.DataFrame, inplace: bool = False, registers: list[Register] | None = None
|
|
84
|
+
) -> pd.DataFrame | None:
|
|
85
|
+
"""Extend a DataFrame with the cost columns."""
|
|
86
|
+
if not inplace:
|
|
87
|
+
result_df = df.copy()
|
|
88
|
+
else:
|
|
89
|
+
result_df = df
|
|
90
|
+
|
|
91
|
+
if registers is None:
|
|
92
|
+
registers = [Register.DELIVERY, Register.EXPORT]
|
|
93
|
+
|
|
94
|
+
if Register.DELIVERY in registers:
|
|
95
|
+
result_df[COST_ELECTRICITY_DELIVERED_SMR2] = (
|
|
96
|
+
df[ELECTRICITY_DELIVERED_SMR2] * df[PRICE_ELECTRICITY_DELIVERED]
|
|
97
|
+
)
|
|
98
|
+
result_df[COST_ELECTRICITY_DELIVERED_SMR3] = (
|
|
99
|
+
df[ELECTRICITY_DELIVERED_SMR3] * df[PRICE_ELECTRICITY_DELIVERED]
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if Register.EXPORT in registers:
|
|
103
|
+
result_df[COST_ELECTRICITY_EXPORTED_SMR2] = (
|
|
104
|
+
df[ELECTRICITY_EXPORTED_SMR2] * df[PRICE_ELECTRICITY_EXPORTED] * -1
|
|
105
|
+
)
|
|
106
|
+
result_df[COST_ELECTRICITY_EXPORTED_SMR3] = (
|
|
107
|
+
df[ELECTRICITY_EXPORTED_SMR3] * df[PRICE_ELECTRICITY_EXPORTED] * -1
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
if not inplace:
|
|
111
|
+
return result_df
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def extend_dataframe_with_weighted_prices(
|
|
116
|
+
df: pd.DataFrame, inplace: bool = False, registers: list[Register] | None = None
|
|
117
|
+
) -> pd.DataFrame | None:
|
|
118
|
+
"""Extend a DataFrame with the weighted price columns."""
|
|
119
|
+
if not inplace:
|
|
120
|
+
df = df.copy()
|
|
121
|
+
|
|
122
|
+
if registers is None:
|
|
123
|
+
registers = [Register.DELIVERY, Register.EXPORT]
|
|
124
|
+
|
|
125
|
+
if Register.DELIVERY in registers:
|
|
126
|
+
rlp_weighted_price_delivered = (df[PRICE_ELECTRICITY_DELIVERED] * df[RLP]).resample(
|
|
127
|
+
"MS"
|
|
128
|
+
).sum() / df[RLP].resample("MS").sum()
|
|
129
|
+
df[RLP_WEIGHTED_PRICE_DELIVERED] = rlp_weighted_price_delivered.reindex_like(
|
|
130
|
+
df[RLP], method="ffill"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
if Register.EXPORT in registers:
|
|
134
|
+
spp_weighted_price_exported = (df[PRICE_ELECTRICITY_EXPORTED] * df[SPP]).resample(
|
|
135
|
+
"MS"
|
|
136
|
+
).sum() / df[SPP].resample("MS").sum()
|
|
137
|
+
df[SPP_WEIGHTED_PRICE_EXPORTED] = spp_weighted_price_exported.reindex_like(
|
|
138
|
+
df[SPP], method="ffill"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if not inplace:
|
|
142
|
+
return df
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def extend_dataframe_with_heatmap(
|
|
147
|
+
df: pd.DataFrame, inplace: bool = False, registers: list[Register] | None = None
|
|
148
|
+
) -> pd.DataFrame | None:
|
|
149
|
+
"""Extend a DataFrame with the heatmap columns."""
|
|
150
|
+
if not inplace:
|
|
151
|
+
df = df.copy()
|
|
152
|
+
|
|
153
|
+
if registers is None:
|
|
154
|
+
registers = [Register.DELIVERY, Register.EXPORT]
|
|
155
|
+
|
|
156
|
+
if Register.DELIVERY in registers:
|
|
157
|
+
energy_delta_delivered = df[ELECTRICITY_DELIVERED_SMR2] - df[ELECTRICITY_DELIVERED_SMR3]
|
|
158
|
+
price_delta_delivered = df[RLP_WEIGHTED_PRICE_DELIVERED] - df[PRICE_ELECTRICITY_DELIVERED]
|
|
159
|
+
heatmap_score_delivered = energy_delta_delivered * price_delta_delivered
|
|
160
|
+
heatmap_score_delivered.fillna(0, inplace=True)
|
|
161
|
+
# Invert score so that positive values indicate a positive impact
|
|
162
|
+
heatmap_score_delivered = -heatmap_score_delivered
|
|
163
|
+
df[HEATMAP_DELIVERED] = heatmap_score_delivered
|
|
164
|
+
|
|
165
|
+
if Register.EXPORT in registers:
|
|
166
|
+
energy_delta_exported = df[ELECTRICITY_EXPORTED_SMR2] - df[ELECTRICITY_EXPORTED_SMR3]
|
|
167
|
+
price_delta_exported = df[SPP_WEIGHTED_PRICE_EXPORTED] - df[PRICE_ELECTRICITY_EXPORTED]
|
|
168
|
+
heatmap_score_exported = energy_delta_exported * price_delta_exported
|
|
169
|
+
heatmap_score_exported.fillna(0, inplace=True)
|
|
170
|
+
df[HEATMAP_EXPORTED] = heatmap_score_exported
|
|
171
|
+
|
|
172
|
+
if Register.DELIVERY in registers and Register.EXPORT in registers:
|
|
173
|
+
heatmap_score_delivered = cast(pd.Series, df[HEATMAP_DELIVERED])
|
|
174
|
+
heatmap_score_exported = cast(pd.Series, df[HEATMAP_EXPORTED])
|
|
175
|
+
heatmap_score_combined = heatmap_score_delivered + heatmap_score_exported
|
|
176
|
+
elif Register.DELIVERY in registers:
|
|
177
|
+
heatmap_score_combined = heatmap_score_delivered
|
|
178
|
+
else:
|
|
179
|
+
heatmap_score_combined = heatmap_score_exported
|
|
180
|
+
df[HEATMAP_TOTAL] = heatmap_score_combined
|
|
181
|
+
|
|
182
|
+
if not inplace:
|
|
183
|
+
return df
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def map_delivery_description(
|
|
188
|
+
price_delivered, price_rlp, electricity_delivered_smr3, electricity_delivered_smr2
|
|
189
|
+
):
|
|
190
|
+
"""Map the delivery description."""
|
|
191
|
+
if price_delivered > price_rlp and electricity_delivered_smr3 > electricity_delivered_smr2:
|
|
192
|
+
return 1
|
|
193
|
+
if price_delivered > price_rlp and electricity_delivered_smr3 < electricity_delivered_smr2:
|
|
194
|
+
return 2
|
|
195
|
+
if price_delivered < price_rlp and electricity_delivered_smr3 > electricity_delivered_smr2:
|
|
196
|
+
return 3
|
|
197
|
+
if price_delivered < price_rlp and electricity_delivered_smr3 < electricity_delivered_smr2:
|
|
198
|
+
return 4
|
|
199
|
+
return 0
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def map_export_description(
|
|
203
|
+
price_exported, price_spp, electricity_exported_smr3, electricity_exported_smr2
|
|
204
|
+
):
|
|
205
|
+
"""Map the export description."""
|
|
206
|
+
if price_exported > price_spp and electricity_exported_smr3 > electricity_exported_smr2:
|
|
207
|
+
return 5
|
|
208
|
+
if price_exported > price_spp and electricity_exported_smr3 < electricity_exported_smr2:
|
|
209
|
+
return 6
|
|
210
|
+
if price_exported < price_spp and electricity_exported_smr3 > electricity_exported_smr2:
|
|
211
|
+
return 7
|
|
212
|
+
if price_exported < price_spp and electricity_exported_smr3 < electricity_exported_smr2:
|
|
213
|
+
return 8
|
|
214
|
+
return 0
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def map_total_description(
|
|
218
|
+
abs_heatmap_delivered, abs_heatmap_exported, delivered_description, exported_description
|
|
219
|
+
):
|
|
220
|
+
"""Map the total description."""
|
|
221
|
+
if abs_heatmap_delivered > abs_heatmap_exported:
|
|
222
|
+
return delivered_description
|
|
223
|
+
return exported_description
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def extend_dataframe_with_heatmap_description(
|
|
227
|
+
df: pd.DataFrame, inplace: bool = False, registers: list[Register] | None = None
|
|
228
|
+
) -> pd.DataFrame | None:
|
|
229
|
+
"""Extend a DataFrame with the heatmap description columns."""
|
|
230
|
+
if not inplace:
|
|
231
|
+
df = df.copy()
|
|
232
|
+
|
|
233
|
+
if registers is None:
|
|
234
|
+
registers = [Register.DELIVERY, Register.EXPORT]
|
|
235
|
+
|
|
236
|
+
if Register.DELIVERY in registers:
|
|
237
|
+
df[HEATMAP_DELIVERED_DESCRIPTION] = list(
|
|
238
|
+
map(
|
|
239
|
+
map_delivery_description,
|
|
240
|
+
df[PRICE_ELECTRICITY_DELIVERED],
|
|
241
|
+
df[RLP_WEIGHTED_PRICE_DELIVERED],
|
|
242
|
+
df[ELECTRICITY_DELIVERED_SMR3],
|
|
243
|
+
df[ELECTRICITY_DELIVERED_SMR2],
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
if Register.EXPORT in registers:
|
|
248
|
+
df[HEATMAP_EXPORTED_DESCRIPTION] = list(
|
|
249
|
+
map(
|
|
250
|
+
map_export_description,
|
|
251
|
+
df[PRICE_ELECTRICITY_EXPORTED],
|
|
252
|
+
df[SPP_WEIGHTED_PRICE_EXPORTED],
|
|
253
|
+
df[ELECTRICITY_EXPORTED_SMR3],
|
|
254
|
+
df[ELECTRICITY_EXPORTED_SMR2],
|
|
255
|
+
)
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
if Register.DELIVERY in registers and Register.EXPORT in registers:
|
|
259
|
+
df[HEATMAP_TOTAL_DESCRIPTION] = list(
|
|
260
|
+
map(
|
|
261
|
+
map_total_description,
|
|
262
|
+
df[HEATMAP_DELIVERED].abs(),
|
|
263
|
+
df[HEATMAP_EXPORTED].abs(),
|
|
264
|
+
df[HEATMAP_DELIVERED_DESCRIPTION],
|
|
265
|
+
df[HEATMAP_EXPORTED_DESCRIPTION],
|
|
266
|
+
)
|
|
267
|
+
)
|
|
268
|
+
elif Register.DELIVERY in registers:
|
|
269
|
+
df[HEATMAP_TOTAL_DESCRIPTION] = df[HEATMAP_DELIVERED_DESCRIPTION]
|
|
270
|
+
else:
|
|
271
|
+
df[HEATMAP_TOTAL_DESCRIPTION] = df[HEATMAP_EXPORTED_DESCRIPTION]
|
|
272
|
+
|
|
273
|
+
if not inplace:
|
|
274
|
+
return df
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def calculate_dyntar_columns(
|
|
278
|
+
df: pd.DataFrame,
|
|
279
|
+
inplace: bool = False,
|
|
280
|
+
registers: list[Register] | None = None,
|
|
281
|
+
) -> pd.DataFrame | None:
|
|
282
|
+
"""Calculate all columns required for the dynamic tariff analysis."""
|
|
283
|
+
if not inplace:
|
|
284
|
+
df = df.copy()
|
|
285
|
+
|
|
286
|
+
if registers is None:
|
|
287
|
+
registers = [Register.DELIVERY, Register.EXPORT]
|
|
288
|
+
|
|
289
|
+
extend_dataframe_with_smr2(df, inplace=True, registers=registers)
|
|
290
|
+
extend_dataframe_with_costs(df, inplace=True, registers=registers)
|
|
291
|
+
extend_dataframe_with_weighted_prices(df, inplace=True, registers=registers)
|
|
292
|
+
extend_dataframe_with_heatmap(df, inplace=True, registers=registers)
|
|
293
|
+
extend_dataframe_with_heatmap_description(df, inplace=True, registers=registers)
|
|
294
|
+
|
|
295
|
+
if not inplace:
|
|
296
|
+
return df
|
|
297
|
+
return None
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def summarize_result(df: pd.DataFrame) -> pd.Series:
|
|
301
|
+
"""Summarize the dynamic tariff analysis result."""
|
|
302
|
+
summary = df.filter(like="cost").sum()
|
|
303
|
+
|
|
304
|
+
abs_smr2 = summary.filter(like="smr2").abs().sum()
|
|
305
|
+
|
|
306
|
+
summary["cost_electricity_total_smr2"] = summary.filter(like="smr2").sum()
|
|
307
|
+
summary["cost_electricity_total_smr3"] = summary.filter(like="smr3").sum()
|
|
308
|
+
|
|
309
|
+
summary["ratio"] = (
|
|
310
|
+
summary["cost_electricity_total_smr3"] - summary["cost_electricity_total_smr2"]
|
|
311
|
+
) / abs_smr2
|
|
312
|
+
|
|
313
|
+
return summary
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Models for dynamic tariff analysis."""
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field, confloat, conlist
|
|
6
|
+
|
|
7
|
+
from openenergyid.models import TimeDataFrame
|
|
8
|
+
|
|
9
|
+
from .const import Register
|
|
10
|
+
|
|
11
|
+
RequiredColumns = Literal[
|
|
12
|
+
"electricity_delivered",
|
|
13
|
+
"electricity_exported",
|
|
14
|
+
"price_electricity_delivered",
|
|
15
|
+
"price_electricity_exported",
|
|
16
|
+
"RLP",
|
|
17
|
+
"SPP",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
OutputColumns = Literal[
|
|
21
|
+
"electricity_delivered_smr3",
|
|
22
|
+
"electricity_exported_smr3",
|
|
23
|
+
"price_electricity_delivered",
|
|
24
|
+
"price_electricity_exported",
|
|
25
|
+
"RLP",
|
|
26
|
+
"SPP",
|
|
27
|
+
"electricity_delivered_smr2",
|
|
28
|
+
"electricity_exported_smr2",
|
|
29
|
+
"cost_electricity_delivered_smr2",
|
|
30
|
+
"cost_electricity_exported_smr2",
|
|
31
|
+
"cost_electricity_delivered_smr3",
|
|
32
|
+
"cost_electricity_exported_smr3",
|
|
33
|
+
"rlp_weighted_price_delivered",
|
|
34
|
+
"spp_weighted_price_exported",
|
|
35
|
+
"heatmap_delivered",
|
|
36
|
+
"heatmap_exported",
|
|
37
|
+
"heatmap_total",
|
|
38
|
+
"heatmap_delivered_description",
|
|
39
|
+
"heatmap_exported_description",
|
|
40
|
+
"heatmap_total_description",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class DynamicTariffAnalysisInput(TimeDataFrame):
|
|
45
|
+
"""Input frame for dynamic tariff analysis."""
|
|
46
|
+
|
|
47
|
+
columns: list[RequiredColumns] = Field(
|
|
48
|
+
min_length=3,
|
|
49
|
+
max_length=len(RequiredColumns.__args__),
|
|
50
|
+
examples=[RequiredColumns.__args__],
|
|
51
|
+
)
|
|
52
|
+
data: list[
|
|
53
|
+
conlist(
|
|
54
|
+
item_type=confloat(allow_inf_nan=True),
|
|
55
|
+
min_length=3,
|
|
56
|
+
max_length=len(RequiredColumns.__args__),
|
|
57
|
+
) # type: ignore
|
|
58
|
+
] = Field(examples=[[0.0] * len(RequiredColumns.__args__)])
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def registers(self) -> list[Register]:
|
|
62
|
+
"""Check which registers are present in the input data."""
|
|
63
|
+
registers = []
|
|
64
|
+
columns = list(self.columns)
|
|
65
|
+
# if "electricity_delivered", "price_electricity_delivered" and "RLP" are present
|
|
66
|
+
if all(
|
|
67
|
+
column in columns
|
|
68
|
+
for column in [
|
|
69
|
+
"electricity_delivered",
|
|
70
|
+
"price_electricity_delivered",
|
|
71
|
+
"RLP",
|
|
72
|
+
]
|
|
73
|
+
):
|
|
74
|
+
registers.append(Register.DELIVERY)
|
|
75
|
+
# if "electricity_exported", "price_electricity_exported" and "SPP" are present
|
|
76
|
+
if all(
|
|
77
|
+
column in columns
|
|
78
|
+
for column in ["electricity_exported", "price_electricity_exported", "SPP"]
|
|
79
|
+
):
|
|
80
|
+
registers.append(Register.EXPORT)
|
|
81
|
+
return registers
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class DynamicTariffAnalysisOutputSummary(BaseModel):
|
|
85
|
+
"""Summary of the dynamic tariff analysis output."""
|
|
86
|
+
|
|
87
|
+
cost_electricity_delivered_smr2: float | None = None
|
|
88
|
+
cost_electricity_delivered_smr3: float | None = None
|
|
89
|
+
cost_electricity_exported_smr2: float | None = None
|
|
90
|
+
cost_electricity_exported_smr3: float | None = None
|
|
91
|
+
cost_electricity_total_smr2: float | None = None
|
|
92
|
+
cost_electricity_total_smr3: float | None = None
|
|
93
|
+
ratio: float | None = None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class DynamicTariffAnalysisOutput(TimeDataFrame):
|
|
97
|
+
"""Output frame for dynamic tariff analysis."""
|
|
98
|
+
|
|
99
|
+
columns: list[str]
|
|
100
|
+
data: list[list[float | None]]
|
|
101
|
+
summary: DynamicTariffAnalysisOutputSummary | None = None
|
openenergyid/elia/api.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module contains the functions to interact with the Elia API.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import datetime as dt
|
|
6
|
+
|
|
7
|
+
import aiohttp
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
from .const import Region
|
|
11
|
+
|
|
12
|
+
DATE_FORMAT = "%Y-%m-%d"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def get_dataset(
|
|
16
|
+
dataset: str,
|
|
17
|
+
start: dt.date,
|
|
18
|
+
end: dt.date,
|
|
19
|
+
region: Region,
|
|
20
|
+
select: set[str],
|
|
21
|
+
session: aiohttp.ClientSession,
|
|
22
|
+
timezone: str = "Europe/Brussels",
|
|
23
|
+
) -> dict:
|
|
24
|
+
"""
|
|
25
|
+
Fetches a dataset from the Elia open data API within a specified date range and region.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
dataset (str): The name of the dataset to fetch.
|
|
29
|
+
start (dt.date): The start date for the data range.
|
|
30
|
+
end (dt.date): The end date for the data range.
|
|
31
|
+
region (Region): The region for which to fetch the data.
|
|
32
|
+
select (set[str]): A set of fields to select in the dataset.
|
|
33
|
+
session (aiohttp.ClientSession): The aiohttp session to use for making the request.
|
|
34
|
+
timezone (str, optional): The timezone to use for the data. Defaults to "Europe/Brussels".
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
dict: The dataset fetched from the API.
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
aiohttp.ClientError: If there is an error making the request.
|
|
41
|
+
"""
|
|
42
|
+
url = f"https://opendata.elia.be/api/explore/v2.1/catalog/datasets/{dataset}/exports/json"
|
|
43
|
+
|
|
44
|
+
if "datetime" not in select:
|
|
45
|
+
select.add("datetime")
|
|
46
|
+
select_str = ",".join(select)
|
|
47
|
+
|
|
48
|
+
params = {
|
|
49
|
+
"where": (
|
|
50
|
+
f"datetime IN [date'{start.strftime(DATE_FORMAT)}'..date'{end.strftime(DATE_FORMAT)}'] "
|
|
51
|
+
f"AND region='{region.value}'"
|
|
52
|
+
),
|
|
53
|
+
"timezone": timezone,
|
|
54
|
+
"select": select_str,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async with session.get(url, params=params) as response:
|
|
58
|
+
data = await response.json()
|
|
59
|
+
|
|
60
|
+
return data
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def parse_response(
|
|
64
|
+
data: dict, index: str, columns: list[str], timezone: str = "Europe/Brussels"
|
|
65
|
+
) -> pd.DataFrame:
|
|
66
|
+
"""
|
|
67
|
+
Parses a response dictionary into a pandas DataFrame.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
data (dict): The input data where each key is a column name
|
|
71
|
+
and each value is a list of column values.
|
|
72
|
+
index (str): The key in the data dictionary to be used as the index for the DataFrame.
|
|
73
|
+
columns (list[str]): The list of column names for the DataFrame.
|
|
74
|
+
timezone (str, optional): The timezone to convert the DataFrame index to.
|
|
75
|
+
Defaults to "Europe/Brussels".
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
pd.DataFrame: A pandas DataFrame with the specified columns and index,
|
|
79
|
+
converted to the specified timezone.
|
|
80
|
+
"""
|
|
81
|
+
df = pd.DataFrame(
|
|
82
|
+
data,
|
|
83
|
+
index=pd.to_datetime([x[index] for x in data], utc=True),
|
|
84
|
+
columns=columns,
|
|
85
|
+
)
|
|
86
|
+
df.index = pd.DatetimeIndex(df.index)
|
|
87
|
+
df = df.tz_convert(timezone)
|
|
88
|
+
df.sort_index(inplace=True)
|
|
89
|
+
df.dropna(inplace=True)
|
|
90
|
+
|
|
91
|
+
return df
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Region(Enum):
|
|
5
|
+
Belgium = "Belgium"
|
|
6
|
+
Brussels = "Brussels"
|
|
7
|
+
Flanders = "Flanders"
|
|
8
|
+
Wallonia = "Wallonia"
|
|
9
|
+
Antwerp = "Antwerp"
|
|
10
|
+
East_Flanders = "East-Flanders"
|
|
11
|
+
Flemish_Brabant = "Flemish-Brabant"
|
|
12
|
+
Limburg = "Limburg"
|
|
13
|
+
West_Flanders = "West-Flanders"
|
|
14
|
+
Hainaut = "Hainaut"
|
|
15
|
+
Liège = "Liège"
|
|
16
|
+
Luxembourg = "Luxembourg"
|
|
17
|
+
Namur = "Namur"
|
|
18
|
+
Walloon_Brabant = "Walloon-Brabant"
|
|
@@ -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,77 @@
|
|
|
1
|
+
"""Functions to create multi-indexed DataFrames for input and output data for energy sharing."""
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
from .const import (
|
|
6
|
+
GROSS_INJECTION,
|
|
7
|
+
GROSS_OFFTAKE,
|
|
8
|
+
KEY,
|
|
9
|
+
NET_INJECTION,
|
|
10
|
+
NET_OFFTAKE,
|
|
11
|
+
SHARED_ENERGY,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def create_multi_index_input_frame(
|
|
16
|
+
gross_injection: pd.DataFrame,
|
|
17
|
+
gross_offtake: pd.DataFrame,
|
|
18
|
+
key: pd.DataFrame,
|
|
19
|
+
) -> pd.DataFrame:
|
|
20
|
+
"""Create a multi-indexed DataFrame with the input data for energy sharing."""
|
|
21
|
+
gross_injection = gross_injection.copy()
|
|
22
|
+
gross_offtake = gross_offtake.copy()
|
|
23
|
+
key = key.copy()
|
|
24
|
+
|
|
25
|
+
gross_injection.columns = pd.MultiIndex.from_product(
|
|
26
|
+
[[GROSS_INJECTION], gross_injection.columns]
|
|
27
|
+
)
|
|
28
|
+
gross_offtake.columns = pd.MultiIndex.from_product([[GROSS_OFFTAKE], gross_offtake.columns])
|
|
29
|
+
key.columns = pd.MultiIndex.from_product([[KEY], key.columns])
|
|
30
|
+
|
|
31
|
+
df = pd.concat([gross_injection, gross_offtake, key], axis=1)
|
|
32
|
+
|
|
33
|
+
return df
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def create_multi_index_output_frame(
|
|
37
|
+
net_injection: pd.DataFrame,
|
|
38
|
+
net_offtake: pd.DataFrame,
|
|
39
|
+
shared_energy: pd.DataFrame,
|
|
40
|
+
) -> pd.DataFrame:
|
|
41
|
+
"""Create a multi-indexed DataFrame with the output data for energy sharing."""
|
|
42
|
+
net_injection = net_injection.copy()
|
|
43
|
+
net_offtake = net_offtake.copy()
|
|
44
|
+
shared_energy = shared_energy.copy()
|
|
45
|
+
|
|
46
|
+
net_injection.columns = pd.MultiIndex.from_product([[NET_INJECTION], net_injection.columns])
|
|
47
|
+
net_offtake.columns = pd.MultiIndex.from_product([[NET_OFFTAKE], net_offtake.columns])
|
|
48
|
+
shared_energy.columns = pd.MultiIndex.from_product([[SHARED_ENERGY], shared_energy.columns])
|
|
49
|
+
|
|
50
|
+
df = pd.concat([net_injection, net_offtake, shared_energy], axis=1)
|
|
51
|
+
|
|
52
|
+
return df
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def result_to_input_for_reiteration(result: pd.DataFrame, key: pd.DataFrame) -> pd.DataFrame:
|
|
56
|
+
"""Create a multi-indexed DataFrame with the input data for energy sharing after the first iteration."""
|
|
57
|
+
# We iterate again. The net injection of the previous result is taken as gross injection input
|
|
58
|
+
# And the net offtake is taken as the gross offtake input
|
|
59
|
+
# When a user's net offtake is 0, the key is set to 0; and the keys are re-normalized
|
|
60
|
+
|
|
61
|
+
gross_injection = result[NET_INJECTION].copy()
|
|
62
|
+
gross_offtake = result[NET_OFFTAKE].copy()
|
|
63
|
+
|
|
64
|
+
# Take the original key, but replace the value with 0.0 if result[NET_OFFTAKE] is 0.0
|
|
65
|
+
|
|
66
|
+
key = key.copy()
|
|
67
|
+
key = key.where(~result[NET_OFFTAKE].eq(0), 0)
|
|
68
|
+
|
|
69
|
+
# Re-normalize the keys
|
|
70
|
+
|
|
71
|
+
key = key.div(key.sum(axis=1), axis=0)
|
|
72
|
+
|
|
73
|
+
df = create_multi_index_input_frame(
|
|
74
|
+
gross_injection=gross_injection, gross_offtake=gross_offtake, key=key
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return df
|