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.

@@ -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,8 @@
1
+ """Constants for the energysharing module."""
2
+
3
+ GROSS_INJECTION = "Gross Injection"
4
+ NET_INJECTION = "Net Injection"
5
+ GROSS_OFFTAKE = "Gross Offtake"
6
+ NET_OFFTAKE = "Net Offtake"
7
+ KEY = "Key"
8
+ SHARED_ENERGY = "Shared Energy"
@@ -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