Daniel Varga commited on
Commit
e733d30
·
1 Parent(s): 2f60d4a

kinda simulates

Browse files
Files changed (3) hide show
  1. v2/architecture.py +96 -104
  2. v2/data_processing.py +0 -1
  3. v2/data_processing.py +101 -0
v2/architecture.py CHANGED
@@ -5,42 +5,68 @@ from enum import IntEnum
5
 
6
  # it's really just a network pricing model
7
  from supplier import Supplier
8
- from data_processing import read_datasets, add_production_field, interpolate_and_join, Parameters
9
 
10
 
11
  STEPS_PER_HOUR = 12
12
 
13
 
14
- # mock model
 
15
  class BatteryModel:
16
- def __init__(self, capacity, efficiency=0.95):
17
- self.soc = 0
18
- self.capacity = capacity # kWh
19
- self.efficiency = efficiency
20
-
21
- def discharge(self, target):
22
- assert 0 <= self.soc <= 1
23
- if self.soc >= target:
24
- self.soc -= target
25
- amount = target
26
- else:
27
- amount = self.soc
28
- self.soc = 0
 
 
 
 
 
 
 
 
29
  assert 0 <= self.soc <= 1
30
- return amount
31
-
32
- # not very refined, whatevs.
33
- def charge(self, target):
34
- assert 0 <= self.soc <= 1
35
- target_to_add = target * self.efficiency
36
- if self.soc <= 1 - target_to_add:
37
- self.soc += target_to_add
38
- could_take = target
39
- else:
40
- could_take = (1 - self.soc) / self.efficiency
41
- self.soc = 1
 
 
 
 
42
  assert 0 <= self.soc <= 1
43
- return could_take
 
 
 
 
 
 
 
 
 
 
 
 
 
44
 
45
 
46
  class Decision(IntEnum):
@@ -67,7 +93,6 @@ class Decision(IntEnum):
67
  # output_window_size is not yet used, always decides one timestep.
68
  class Decider:
69
  def __init__(self):
70
- self.parameters = None
71
  self.input_window_size = STEPS_PER_HOUR * 24 # day long window.
72
  self.output_window_size = STEPS_PER_HOUR # only output decisions for the next hour
73
  self.random_seed = 0
@@ -98,18 +123,25 @@ class DummyPredictor:
98
  return prediction
99
 
100
 
 
 
 
 
 
 
 
 
 
 
101
  # this function does not mutate its inputs.
102
  # it makes a clone of battery_model and modifies that.
103
- # TODO even in a first mockup version, parameters should come from a single
104
- # place, not some from the parameters dataclass and some from the battery_model.
105
- def simulator(battery_model, supplier, prod_cons, prod_predictor, cons_predictor, decider, parameters):
106
  battery_model = copy.copy(battery_model)
107
 
108
  demand_np = prod_cons['Consumption'].to_numpy()
109
  production_np = prod_cons['Production'].to_numpy()
110
  assert len(demand_np) == len(production_np)
111
  step_in_minutes = prod_cons.index.freq.n
112
- print(step_in_minutes)
113
  assert step_in_minutes == 5
114
 
115
  print("Simulating for", len(demand_np), "time steps. Each step is", step_in_minutes, "minutes.")
@@ -124,24 +156,18 @@ def simulator(battery_model, supplier, prod_cons, prod_predictor, cons_predictor
124
  consumption_from_network_to_bess_series = [] # network used to charge BESS, also included in consumption_from_network.
125
  # the previous three must sum to demand_series.
126
 
127
- # power taken from solar by BESS.
128
- # note: in inital mock version power is never taken from network by BESS.
129
- charge_of_bess_series = []
130
  discarded_production_series = [] # solar power thrown away
131
 
132
  # 1 is not nominal but targeted (healthy) maximum charge.
133
  # we start with an empty battery, but not emptier than what's healthy for the batteries.
134
 
135
- # For the sake of simplicity 0 <= soc <=1
136
  # soc=0 means battery is emptied till it's 20% and soc=1 means battery is charged till 80% of its capacity
137
  # soc = 1 - maximal_depth_of_discharge
138
  # and will use only maximal_depth_of_discharge percent of the real battery capacity
139
 
140
- max_cap_of_battery = parameters.bess_capacity * parameters.maximal_depth_of_discharge
141
-
142
  time_interval = step_in_minutes / 60 # amount of time step in hours
143
  for i, (demand, production) in enumerate(zip(demand_np, production_np)):
144
- cap_of_battery = battery_model.soc * max_cap_of_battery
145
  # these five are modified on the appropriate codepaths:
146
  consumption_from_solar = 0
147
  consumption_from_bess = 0
@@ -151,10 +177,6 @@ def simulator(battery_model, supplier, prod_cons, prod_predictor, cons_predictor
151
 
152
  unsatisfied_demand = demand
153
  remaining_production = production
154
-
155
- assert parameters.bess_present
156
- is_battery_charged_enough = battery_model.soc > 0
157
- is_battery_chargeable = battery_model.soc < 1.0
158
 
159
  prod_prediction = prod_predictor.predict(i, decider.input_window_size)
160
  cons_prediction = cons_predictor.predict(i, decider.input_window_size)
@@ -162,78 +184,41 @@ def simulator(battery_model, supplier, prod_cons, prod_predictor, cons_predictor
162
 
163
  production_used_to_charge = 0
164
  if unsatisfied_demand >= remaining_production:
165
- # all goes to demand
166
  consumption_from_solar = remaining_production
167
  unsatisfied_demand -= consumption_from_solar
168
  remaining_production = 0
169
- # we try to cover the rest from BESS
170
- if unsatisfied_demand > 0:
171
- if is_battery_charged_enough and decision == Decision.DISCHARGE:
172
- # battery capacity is limited!
173
- if cap_of_battery >= unsatisfied_demand * time_interval:
174
- consumption_from_bess = unsatisfied_demand
175
- unsatisfied_demand = 0
176
- cap_of_battery -= consumption_from_bess * time_interval
177
- soc = cap_of_battery / max_cap_of_battery
178
- else:
179
- discharge_of_bess = cap_of_battery / time_interval
180
- discharge = min(parameters.bess_discharge, discharge_of_bess)
181
- consumption_from_bess = discharge
182
- unsatisfied_demand -= consumption_from_bess
183
- cap_of_battery -= consumption_from_bess * time_interval
184
- soc = cap_of_battery / max_cap_of_battery
185
- consumption_from_network = unsatisfied_demand
186
- unsatisfied_demand = 0
187
- else:
188
- # we cover the rest from network
189
- consumption_from_network = unsatisfied_demand
190
- unsatisfied_demand = 0
191
  else:
192
- # demand fully satisfied by production
193
  consumption_from_solar = unsatisfied_demand
194
  remaining_production -= unsatisfied_demand
195
  unsatisfied_demand = 0
196
  if remaining_production > 0:
197
  # exploitable production still remains:
198
- if is_battery_chargeable:
199
- # we try to specify the BESS modell
200
- if parameters.bess_charge <= remaining_production :
201
- energy = parameters.bess_charge * time_interval
202
- remaining_production = remaining_production - parameters.bess_charge
203
- production_used_to_charge = parameters.bess_charge
204
- else :
205
- production_used_to_charge = remaining_production
206
- energy = remaining_production * time_interval
207
- remaining_production = 0
208
- cap_of_battery += energy
209
- soc = cap_of_battery / max_cap_of_battery
210
-
211
- discarded_production = remaining_production
212
 
213
  if decision == Decision.NETWORK_CHARGE:
214
- # there are two things that can limit charging at this point,
215
- # one is a distance-like, the other is a velocity-like quantity.
216
- # 1. the battery is fully charged
217
- # 2. the battery is being charged from solar, no bandwidth left.
218
- is_battery_chargeable = battery_model.soc < 1.0
219
- remaining_network_charge = parameters.bess_charge - production_used_to_charge
220
- if is_battery_chargeable:
221
- # we try to specify the BESS modell
222
- if parameters.bess_charge <= remaining_network_charge :
223
- energy = parameters.bess_charge * time_interval
224
- remaining_network_charge = remaining_network_charge - parameters.bess_charge
225
- network_used_to_charge = parameters.bess_charge
226
- else :
227
- network_used_to_charge = remaining_production
228
- energy = remaining_production * time_interval
229
- cap_of_battery += energy
230
- soc = cap_of_battery / max_cap_of_battery
231
- consumption_from_network += network_used_to_charge
232
 
233
  soc_series.append(battery_model.soc)
234
  consumption_from_solar_series.append(consumption_from_solar)
235
  consumption_from_network_series.append(consumption_from_network)
236
- consumption_from_network_to_bess_series.append(network_used_to_charge)
237
  consumption_from_bess_series.append(consumption_from_bess)
238
  discarded_production_series.append(discarded_production)
239
 
@@ -256,23 +241,30 @@ def simulator(battery_model, supplier, prod_cons, prod_predictor, cons_predictor
256
 
257
 
258
  def main():
259
- battery_model = BatteryModel(capacity=200, efficiency=0.95)
260
 
261
  supplier = Supplier(price=100) # Ft/kWh
262
  supplier.set_price_for_interval(9, 17, 150) # nine-to-five increased price.
263
 
264
- parameters = Parameters()
265
 
266
  met_2021_data, cons_2021_data = read_datasets()
267
  add_production_field(met_2021_data, parameters)
268
  all_2021_data = interpolate_and_join(met_2021_data, cons_2021_data)
269
 
 
 
 
 
270
  prod_predictor = DummyPredictor(pd.Series(all_2021_data['Production']))
271
  cons_predictor = DummyPredictor(pd.Series(all_2021_data['Consumption']))
272
 
273
  decider = Decider()
274
 
275
- results = simulator(battery_model, supplier, all_2021_data, prod_predictor, cons_predictor, decider, parameters)
 
 
 
 
276
 
277
 
278
  if __name__ == '__main__':
 
5
 
6
  # it's really just a network pricing model
7
  from supplier import Supplier
8
+ from data_processing import read_datasets, add_production_field, interpolate_and_join, SolarParameters
9
 
10
 
11
  STEPS_PER_HOUR = 12
12
 
13
 
14
+ # SOC is normalized so that minimal_depth_of_discharge = 0 and maximal_depth_of_discharge = 1.
15
+ # please set capacity_Ah = nominal_capacity_Ah * (max_dod - min_dod)
16
  class BatteryModel:
17
+ def __init__(self, capacity_Ah, time_interval_h):
18
+ self.capacity_Ah = capacity_Ah
19
+ self.efficiency = 0.95 # [dimensionless]
20
+ self.voltage_V = 600
21
+ self.charge_kW = 50
22
+ self.discharge_kW = 60
23
+ self.time_interval_h = time_interval_h
24
+
25
+ # the only non-constant member variable!
26
+ # ratio of self.current_capacity_kWh and self.maximal_capacity_kWh
27
+ self.soc = 0.0
28
+
29
+ @property
30
+ def maximal_capacity_kWh(self):
31
+ return self.capacity_Ah * self.voltage_V / 1000
32
+
33
+ @property
34
+ def current_capacity_kWh(self):
35
+ return self.soc * self.maximal_capacity_kWh
36
+
37
+ def satisfy_demand(self, demand_kW):
38
  assert 0 <= self.soc <= 1
39
+ assert demand_kW >= 0
40
+ # rate limited:
41
+ possible_discharge_in_timestep_kWh = self.discharge_kW * self.time_interval_h
42
+ # limited by current capacity:
43
+ possible_discharge_in_timestep_kWh = min((possible_discharge_in_timestep_kWh, self.current_capacity_kWh))
44
+ # limited by need:
45
+ discharge_in_timestep_kWh = min((possible_discharge_in_timestep_kWh, demand_kW * self.time_interval_h))
46
+ consumption_from_bess_kW = discharge_in_timestep_kWh / self.time_interval_h
47
+ unsatisfied_demand_kW = demand_kW - consumption_from_bess_kW
48
+ cap_of_battery_kWh = self.current_capacity_kWh - discharge_in_timestep_kWh
49
+ soc = cap_of_battery_kWh / self.maximal_capacity_kWh
50
+ assert 0 <= soc <= self.soc <= 1
51
+ self.soc = soc
52
+ return unsatisfied_demand_kW
53
+
54
+ def charge(self, charge_kW):
55
  assert 0 <= self.soc <= 1
56
+ assert charge_kW >= 0
57
+ # rate limited:
58
+ possible_charge_in_timestep_kWh = self.charge_kW * self.time_interval_h
59
+ # limited by current capacity:
60
+ possible_charge_in_timestep_kWh = min((possible_charge_in_timestep_kWh, self.maximal_capacity_kWh - self.current_capacity_kWh))
61
+ # limited by supply:
62
+ charge_in_timestep_kWh = min((possible_charge_in_timestep_kWh, charge_kW * self.time_interval_h))
63
+ actual_charge_kW = charge_in_timestep_kWh / self.time_interval_h
64
+ unused_charge_kW = charge_kW - actual_charge_kW
65
+ cap_of_battery_kWh = self.current_capacity_kWh + charge_in_timestep_kWh
66
+ soc = cap_of_battery_kWh / self.maximal_capacity_kWh
67
+ assert 0 <= self.soc <= soc <= 1
68
+ self.soc = soc
69
+ return unused_charge_kW
70
 
71
 
72
  class Decision(IntEnum):
 
93
  # output_window_size is not yet used, always decides one timestep.
94
  class Decider:
95
  def __init__(self):
 
96
  self.input_window_size = STEPS_PER_HOUR * 24 # day long window.
97
  self.output_window_size = STEPS_PER_HOUR # only output decisions for the next hour
98
  self.random_seed = 0
 
123
  return prediction
124
 
125
 
126
+ '''
127
+ bess_nominal_capacity: float = 330 # [Ah]
128
+ bess_charge: float = 50 # [kW]
129
+ bess_discharge: float = 60 # [kW]
130
+ voltage: float = 600 # [V]
131
+ maximal_depth_of_discharge: float = 0.75 # [dimensionless]
132
+ energy_loss: float = 0.1 # [dimensionless]
133
+ bess_present: bool = True # [boolean]
134
+ '''
135
+
136
  # this function does not mutate its inputs.
137
  # it makes a clone of battery_model and modifies that.
138
+ def simulator(battery_model, supplier, prod_cons, prod_predictor, cons_predictor, decider):
 
 
139
  battery_model = copy.copy(battery_model)
140
 
141
  demand_np = prod_cons['Consumption'].to_numpy()
142
  production_np = prod_cons['Production'].to_numpy()
143
  assert len(demand_np) == len(production_np)
144
  step_in_minutes = prod_cons.index.freq.n
 
145
  assert step_in_minutes == 5
146
 
147
  print("Simulating for", len(demand_np), "time steps. Each step is", step_in_minutes, "minutes.")
 
156
  consumption_from_network_to_bess_series = [] # network used to charge BESS, also included in consumption_from_network.
157
  # the previous three must sum to demand_series.
158
 
 
 
 
159
  discarded_production_series = [] # solar power thrown away
160
 
161
  # 1 is not nominal but targeted (healthy) maximum charge.
162
  # we start with an empty battery, but not emptier than what's healthy for the batteries.
163
 
164
+ # For the sake of simplicity 0 <= soc <= 1
165
  # soc=0 means battery is emptied till it's 20% and soc=1 means battery is charged till 80% of its capacity
166
  # soc = 1 - maximal_depth_of_discharge
167
  # and will use only maximal_depth_of_discharge percent of the real battery capacity
168
 
 
 
169
  time_interval = step_in_minutes / 60 # amount of time step in hours
170
  for i, (demand, production) in enumerate(zip(demand_np, production_np)):
 
171
  # these five are modified on the appropriate codepaths:
172
  consumption_from_solar = 0
173
  consumption_from_bess = 0
 
177
 
178
  unsatisfied_demand = demand
179
  remaining_production = production
 
 
 
 
180
 
181
  prod_prediction = prod_predictor.predict(i, decider.input_window_size)
182
  cons_prediction = cons_predictor.predict(i, decider.input_window_size)
 
184
 
185
  production_used_to_charge = 0
186
  if unsatisfied_demand >= remaining_production:
187
+ # all goes to demand, no rate limit between solar and consumer
188
  consumption_from_solar = remaining_production
189
  unsatisfied_demand -= consumption_from_solar
190
  remaining_production = 0
191
+ if decision == Decision.DISCHARGE:
192
+ # we try to cover the rest from BESS
193
+ unsatisfied_demand = battery_model.satisfy_demand(unsatisfied_demand)
194
+ # we cover the rest from network
195
+ consumption_from_network = unsatisfied_demand
196
+ unsatisfied_demand = 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  else:
198
+ # demand fully satisfied by production, no rate limit between solar and consumer
199
  consumption_from_solar = unsatisfied_demand
200
  remaining_production -= unsatisfied_demand
201
  unsatisfied_demand = 0
202
  if remaining_production > 0:
203
  # exploitable production still remains:
204
+ discarded_production = battery_model.charge(remaining_production) # remaining_production [kW]
 
 
 
 
 
 
 
 
 
 
 
 
 
205
 
206
  if decision == Decision.NETWORK_CHARGE:
207
+ # that is some random big number, the actual charge will be
208
+ # determined by the combination of the BESS rate limit (battery_model.charge_kW)
209
+ # and the BESS capacity
210
+ fictional_network_charge_kW = 1000
211
+ discarded_network_charge_kW = battery_model.charge(fictional_network_charge_kW)
212
+ actual_network_charge_kW = fictional_network_charge_kW - discarded_network_charge_kW
213
+ consumption_from_network_to_bess = actual_network_charge_kW # just a renaming.
214
+ consumption_from_network += actual_network_charge_kW
215
+ else:
216
+ consumption_from_network_to_bess = 0
 
 
 
 
 
 
 
 
217
 
218
  soc_series.append(battery_model.soc)
219
  consumption_from_solar_series.append(consumption_from_solar)
220
  consumption_from_network_series.append(consumption_from_network)
221
+ consumption_from_network_to_bess_series.append(consumption_from_network_to_bess)
222
  consumption_from_bess_series.append(consumption_from_bess)
223
  discarded_production_series.append(discarded_production)
224
 
 
241
 
242
 
243
  def main():
 
244
 
245
  supplier = Supplier(price=100) # Ft/kWh
246
  supplier.set_price_for_interval(9, 17, 150) # nine-to-five increased price.
247
 
248
+ parameters = SolarParameters()
249
 
250
  met_2021_data, cons_2021_data = read_datasets()
251
  add_production_field(met_2021_data, parameters)
252
  all_2021_data = interpolate_and_join(met_2021_data, cons_2021_data)
253
 
254
+ time_interval_min = all_2021_data.index.freq.n
255
+ time_interval_h = time_interval_min / 60
256
+ battery_model = BatteryModel(capacity_Ah=600, time_interval_h=time_interval_h)
257
+
258
  prod_predictor = DummyPredictor(pd.Series(all_2021_data['Production']))
259
  cons_predictor = DummyPredictor(pd.Series(all_2021_data['Consumption']))
260
 
261
  decider = Decider()
262
 
263
+ results = simulator(battery_model, supplier, all_2021_data, prod_predictor, cons_predictor, decider)
264
+
265
+ import matplotlib.pyplot as plt
266
+ results['soc_series'].plot()
267
+ plt.show()
268
 
269
 
270
  if __name__ == '__main__':
v2/data_processing.py DELETED
@@ -1 +0,0 @@
1
- ../data_processing.py
 
 
v2/data_processing.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ import numpy as np
3
+ import pandas as pd
4
+ from scipy.interpolate import interp1d
5
+
6
+
7
+ PATH_PREFIX = "./"
8
+
9
+ START = f"2021-01-01"
10
+ END = f"2022-01-01"
11
+
12
+
13
+ def read_datasets(mini=False):
14
+ if mini:
15
+ met_filename = 'PL_44527.2101.csv.gz'
16
+ cons_filename = 'pq_terheles_202101_adatok.tsv'
17
+ else:
18
+ met_filename = 'PL_44527.19-21.csv.gz'
19
+ cons_filename = 'pq_terheles_2021_adatok.tsv'
20
+
21
+ #@title ### Preprocessing meteorologic data
22
+ met_data = pd.read_csv(PATH_PREFIX + met_filename, compression='gzip', sep=';', skipinitialspace=True, na_values='n/a', skiprows=[0, 1, 2, 3, 4])
23
+ met_data['Time'] = met_data['Time'].astype(str)
24
+ date_time = met_data['Time'] = pd.to_datetime(met_data['Time'], format='%Y%m%d%H%M')
25
+ met_data = met_data.set_index('Time')
26
+
27
+
28
+ #@title ### Preprocessing consumption data
29
+ cons_data = pd.read_csv(PATH_PREFIX + cons_filename, sep='\t', skipinitialspace=True, na_values='n/a', decimal=',')
30
+ cons_data['Time'] = pd.to_datetime(cons_data['Korrigált időpont'], format='%m/%d/%y %H:%M')
31
+ cons_data = cons_data.set_index('Time')
32
+ cons_data['Consumption'] = cons_data['Hatásos teljesítmény [kW]']
33
+
34
+ # consumption data is at 14 29 44 59 minutes, we move it by 1 minute
35
+ # to sync it with production data:
36
+ cons_data.index = cons_data.index + pd.DateOffset(minutes=1)
37
+
38
+ met_2021_data = met_data[(met_data.index >= START) & (met_data.index < END)]
39
+ cons_2021_data = cons_data[(cons_data.index >= START) & (cons_data.index < END)]
40
+
41
+ return met_2021_data, cons_2021_data
42
+
43
+
44
+ # BESS parameters are now in BatteryModel
45
+ @dataclass
46
+ class SolarParameters:
47
+ solar_cell_num: float = 114 # units
48
+ solar_efficiency: float = 0.93 * 0.96 # [dimensionless]
49
+ NOCT: float = 280 # [W]
50
+ NOCT_irradiation: float = 800 # [W/m^2]
51
+
52
+
53
+ # mutates met_2021_data
54
+ def add_production_field(met_2021_data, parameters):
55
+ sr = met_2021_data['sr']
56
+
57
+ nop_total = sr * parameters.solar_cell_num * parameters.solar_efficiency * parameters.NOCT / parameters.NOCT_irradiation / 1e3
58
+ nop_total = nop_total.clip(0)
59
+ met_2021_data['Production'] = nop_total
60
+
61
+
62
+ def interpolate_and_join(met_2021_data, cons_2021_data):
63
+ applicable = 24*60*365 - 15 + 5
64
+
65
+ demand_f = interp1d(range(0, 365*24*60, 15), cons_2021_data['Consumption'])
66
+ #demand_f = interp1d(range(0, 6*24*60, 15), cons_2021_data['Consumption'])
67
+ demand_interp = demand_f(range(0, applicable, 5))
68
+
69
+ production_f = interp1d(range(0, 365*24*60, 10), met_2021_data['Production'])
70
+ #production_f = interp1d(range(0, 6*24*60, 10), met_2021_data['Production'])
71
+ production_interp = production_f(range(0, applicable, 5))
72
+
73
+ all_2021_datetimeindex = pd.date_range(start=START, end=END, freq='5min')[:len(production_interp)]
74
+
75
+ all_2021_data = pd.DataFrame({'Consumption': demand_interp, 'Production': production_interp})
76
+ all_2021_data = all_2021_data.set_index(all_2021_datetimeindex)
77
+ return all_2021_data
78
+
79
+
80
+ # TODO build a dataframe instead
81
+ def monthly_analysis(results):
82
+ consumptions = []
83
+ for month in range(1, 13):
84
+ start = f"2021-{month:02}-01"
85
+ end = f"2021-{month+1:02}-01"
86
+ if month == 12:
87
+ end = "2022-01-01"
88
+ results_in_month = results[(results.index >= start) & (results.index < end)]
89
+
90
+ total = results_in_month['Consumption'].sum()
91
+ network = results_in_month['consumption_from_network'].sum()
92
+ solar = results_in_month['consumption_from_solar'].sum()
93
+ bess = results_in_month['consumption_from_bess'].sum()
94
+ consumptions.append([network, solar, bess])
95
+
96
+ consumptions = np.array(consumptions)
97
+ step_in_minutes = results.index.freq.n
98
+ # consumption is given in kW. each tick is step_in_minutes long (5mins, in fact)
99
+ # we get consumption in kWh if we multiply sum by step_in_minutes/60
100
+ consumptions_in_mwh = consumptions * (step_in_minutes / 60) / 1000
101
+ return consumptions_in_mwh