Daniel Varga commited on
Commit
4e673fc
1 Parent(s): ababe23

big code drop with different main_* entry points, tuned evolution, and less logging.

Browse files
Files changed (3) hide show
  1. v2/architecture.py +80 -36
  2. v2/decider.py +5 -5
  3. v2/evolution_strategies.py +9 -14
v2/architecture.py CHANGED
@@ -29,6 +29,14 @@ def add_dummy_predictions(all_data_with_predictions):
29
  # we predict zero before we have data, no big deal:
30
  all_data_with_predictions.loc[all_data_with_predictions.index[:cons_shift], 'Consumption_prediction'] = 0
31
  all_data_with_predictions.loc[all_data_with_predictions.index[:prod_shift], 'Production_prediction'] = 0
 
 
 
 
 
 
 
 
32
 
33
 
34
  # even mock-er class than usual.
@@ -81,7 +89,7 @@ def simulator(battery_model, prod_cons, decider):
81
 
82
  consumption_fees_np = prod_cons['Consumption_fees'].to_numpy()
83
 
84
- print("Simulating for", len(demand_np), "time steps. Each step is", step_in_minutes, "minutes.")
85
  soc_series = []
86
  # by convention, we only call end user demand, demand,
87
  # and we only call end user consumption, consumption.
@@ -113,6 +121,7 @@ def simulator(battery_model, prod_cons, decider):
113
  consumption_from_network = 0
114
  discarded_production = 0
115
  network_used_to_charge = 0
 
116
 
117
  unsatisfied_demand = demand
118
  remaining_production = production
@@ -135,7 +144,9 @@ def simulator(battery_model, prod_cons, decider):
135
  remaining_production = 0
136
  if decision == Decision.DISCHARGE:
137
  # we try to cover the rest from BESS
138
- unsatisfied_demand = battery_model.satisfy_demand(unsatisfied_demand)
 
 
139
  # we cover the rest from network
140
  consumption_from_network = unsatisfied_demand
141
  unsatisfied_demand = 0
@@ -147,6 +158,7 @@ def simulator(battery_model, prod_cons, decider):
147
  if remaining_production > 0:
148
  # exploitable production still remains:
149
  discarded_production = battery_model.charge(remaining_production) # remaining_production [kW]
 
150
 
151
  if decision == Decision.NETWORK_CHARGE:
152
  # that is some random big number, the actual charge will be
@@ -167,6 +179,9 @@ def simulator(battery_model, prod_cons, decider):
167
  consumption_from_bess_series.append(consumption_from_bess)
168
  discarded_production_series.append(discarded_production)
169
 
 
 
 
170
  soc_series = np.array(soc_series)
171
  consumption_from_solar_series = np.array(consumption_from_solar_series)
172
  consumption_from_network_series = np.array(consumption_from_network_series)
@@ -189,7 +204,7 @@ def simulator(battery_model, prod_cons, decider):
189
  fifteen_minute_surdemands_in_kwh = (fifteen_minute_demands_in_kwh - decider.precalculated_supplier.peak_demand).clip(lower=0)
190
  demand_charges = fifteen_minute_surdemands_in_kwh * decider.precalculated_supplier.surcharge_per_kwh
191
  total_network_fee = consumption_charge_series.sum() + demand_charges.sum()
192
- print(f"Total network fee {total_network_fee / 10 ** 6} MHUF.")
193
 
194
  if DO_VIS:
195
  demand_charges.plot()
@@ -209,17 +224,20 @@ def simulator(battery_model, prod_cons, decider):
209
  return results, total_network_fee
210
 
211
 
212
-
213
- def optimizer(decider_class, battery_model, all_data_with_predictions, precalculated_supplier):
214
- number_of_generations = 1
215
- population_size = 10
216
  collected_loss_values = []
217
  def objective_function(params):
218
- print("Simulating with parameters", params)
 
 
 
219
  decider = decider_class(params, precalculated_supplier)
220
  t = time.perf_counter()
221
  results, total_network_fee = simulator(battery_model, all_data_with_predictions, decider)
222
  collected_loss_values.append((params, total_network_fee))
 
223
  return total_network_fee
224
 
225
  def clipper_function(params):
@@ -246,9 +264,7 @@ def visualize_collected_loss_values(collected_loss_values):
246
 
247
  # losses -= losses.min() ; losses /= losses.max()
248
 
249
- plt.scatter(all_params[:, 0], all_params[:, 1], c=range(len(all_params)))
250
- plt.show()
251
-
252
 
253
  from mpl_toolkits.mplot3d import Axes3D
254
 
@@ -258,10 +274,52 @@ def visualize_collected_loss_values(collected_loss_values):
258
  ax = fig.add_subplot(111, projection='3d')
259
 
260
  # Scatter plot
261
- ax.scatter(all_params[:, 0], all_params[:, 1], losses)
262
  plt.show()
263
 
264
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
 
266
  def main():
267
  np.random.seed(1)
@@ -285,7 +343,7 @@ def main():
285
  time_interval_h = time_interval_min / 60
286
 
287
  # for faster testing:
288
- DATASET_TRUNCATED_SIZE = None
289
  if DATASET_TRUNCATED_SIZE is not None:
290
  print("Truncating dataset to", DATASET_TRUNCATED_SIZE, "datapoints, that is", DATASET_TRUNCATED_SIZE * time_interval_h / 24, "days")
291
  all_data = all_data.iloc[:DATASET_TRUNCATED_SIZE]
@@ -303,36 +361,22 @@ def main():
303
 
304
  battery_model = BatteryModel(capacity_Ah=600, time_interval_h=time_interval_h)
305
 
306
- decider_class = RandomDecider
307
 
308
- # TODO this is super unfortunate:
309
- # Consumption_fees travels via all_data_with_predictions,
310
- # peak_demand and surcharge_per_kwh travels via precalculated_supplier of decider.
311
- decider_init_mean, decider_init_scale = decider_class.initial_params()
312
- decider = decider_class(decider_init_mean, precalculated_supplier)
313
 
314
- t = time.perf_counter()
315
- results, total_network_fee = simulator(battery_model, all_data_with_predictions, decider)
316
- print("Simulation runtime", time.perf_counter() - t, "seconds.")
317
 
318
- if DO_VIS:
319
- results['soc_series'].plot()
320
- plt.title('soc_series')
321
- plt.show()
322
 
323
- best_params, collected_loss_values = optimizer(decider_class, battery_model, all_data_with_predictions, precalculated_supplier)
324
- visualize_collected_loss_values(collected_loss_values)
325
 
326
- decider = decider_class(best_params, precalculated_supplier)
327
- results, total_network_fee = simulator(battery_model, all_data_with_predictions, decider)
 
328
 
329
- date_range = ("2021-01-01", "2021-02-01")
330
- date_range = ("2021-07-01", "2021-08-01")
331
- plotly_fig = plotly_visualize_simulation(results, date_range=date_range)
332
- plotly_fig.show()
333
 
334
- plotly_fig_2 = plotly_visualize_monthly(results)
335
- plotly_fig_2.show()
336
 
337
  if __name__ == '__main__':
338
  main()
 
29
  # we predict zero before we have data, no big deal:
30
  all_data_with_predictions.loc[all_data_with_predictions.index[:cons_shift], 'Consumption_prediction'] = 0
31
  all_data_with_predictions.loc[all_data_with_predictions.index[:prod_shift], 'Production_prediction'] = 0
32
+ '''
33
+ all_data_with_predictions['Consumption'].plot()
34
+ all_data_with_predictions['Production'].plot()
35
+ plt.show()
36
+ all_data_with_predictions['Consumption_prediction'].plot()
37
+ all_data_with_predictions['Production_prediction'].plot()
38
+ plt.show()
39
+ '''
40
 
41
 
42
  # even mock-er class than usual.
 
89
 
90
  consumption_fees_np = prod_cons['Consumption_fees'].to_numpy()
91
 
92
+ # print("Simulating for", len(demand_np), "time steps. Each step is", step_in_minutes, "minutes.")
93
  soc_series = []
94
  # by convention, we only call end user demand, demand,
95
  # and we only call end user consumption, consumption.
 
121
  consumption_from_network = 0
122
  discarded_production = 0
123
  network_used_to_charge = 0
124
+ bess_from_solar = 0
125
 
126
  unsatisfied_demand = demand
127
  remaining_production = production
 
144
  remaining_production = 0
145
  if decision == Decision.DISCHARGE:
146
  # we try to cover the rest from BESS
147
+ still_unsatisfied_demand = battery_model.satisfy_demand(unsatisfied_demand)
148
+ consumption_from_bess = unsatisfied_demand - still_unsatisfied_demand
149
+ unsatisfied_demand = still_unsatisfied_demand
150
  # we cover the rest from network
151
  consumption_from_network = unsatisfied_demand
152
  unsatisfied_demand = 0
 
158
  if remaining_production > 0:
159
  # exploitable production still remains:
160
  discarded_production = battery_model.charge(remaining_production) # remaining_production [kW]
161
+ bess_from_solar = remaining_production - discarded_production
162
 
163
  if decision == Decision.NETWORK_CHARGE:
164
  # that is some random big number, the actual charge will be
 
179
  consumption_from_bess_series.append(consumption_from_bess)
180
  discarded_production_series.append(discarded_production)
181
 
182
+ assert np.isclose(consumption_from_solar + consumption_from_bess + consumption_from_network, demand + consumption_from_network_to_bess)
183
+ assert np.isclose(consumption_from_solar + discarded_production + bess_from_solar, production)
184
+
185
  soc_series = np.array(soc_series)
186
  consumption_from_solar_series = np.array(consumption_from_solar_series)
187
  consumption_from_network_series = np.array(consumption_from_network_series)
 
204
  fifteen_minute_surdemands_in_kwh = (fifteen_minute_demands_in_kwh - decider.precalculated_supplier.peak_demand).clip(lower=0)
205
  demand_charges = fifteen_minute_surdemands_in_kwh * decider.precalculated_supplier.surcharge_per_kwh
206
  total_network_fee = consumption_charge_series.sum() + demand_charges.sum()
207
+ # print(f"Total network fee {total_network_fee / 10 ** 6} MHUF.")
208
 
209
  if DO_VIS:
210
  demand_charges.plot()
 
224
  return results, total_network_fee
225
 
226
 
227
+ def optimizer(decider_class, precalculated_supplier, battery_model, all_data_with_predictions):
228
+ number_of_generations = 10
229
+ population_size = 20
 
230
  collected_loss_values = []
231
  def objective_function(params):
232
+ # there was an evil numpy view bug that shuffled all results randomly.
233
+ params = params.copy()
234
+
235
+ # print("Simulating with parameters", params)
236
  decider = decider_class(params, precalculated_supplier)
237
  t = time.perf_counter()
238
  results, total_network_fee = simulator(battery_model, all_data_with_predictions, decider)
239
  collected_loss_values.append((params, total_network_fee))
240
+ print(params, total_network_fee)
241
  return total_network_fee
242
 
243
  def clipper_function(params):
 
264
 
265
  # losses -= losses.min() ; losses /= losses.max()
266
 
267
+ # plt.scatter(all_params[:, 0], all_params[:, 1], c=range(len(all_params))) ; plt.show()
 
 
268
 
269
  from mpl_toolkits.mplot3d import Axes3D
270
 
 
274
  ax = fig.add_subplot(111, projection='3d')
275
 
276
  # Scatter plot
277
+ ax.scatter(all_params[:, 0], all_params[:, 1], losses, c=range(len(all_params)))
278
  plt.show()
279
 
280
 
281
+ def main_param_grid(precalculated_supplier, battery_model, all_data_with_predictions):
282
+ N = 11
283
+ losses = np.zeros((N, N))
284
+ losses[:, :] = np.nan
285
+ xlim = [5, 60*24*3 - 1]
286
+ ylim = [-50, 50]
287
+ for i, x in enumerate(np.linspace(*xlim, N)):
288
+ for j, y in enumerate(np.linspace(*ylim, N)):
289
+ decider = Decider(np.array([x, y]), precalculated_supplier)
290
+ results, total_network_fee = simulator(battery_model, all_data_with_predictions, decider)
291
+ # print(x, y, total_network_fee)
292
+ losses[j, i] = total_network_fee
293
+ # losses[0, -1] = np.nan
294
+
295
+ # + is list extend.
296
+ # the aspect means square pixels
297
+ plt.imshow(losses, extent=xlim + ylim, aspect=(xlim[1]-xlim[0])/(ylim[1]-ylim[0]))
298
+ plt.xlabel('Surdemand lookahead window size [minutes]')
299
+ plt.ylabel('Lookahead surdemand [kW]')
300
+ plt.show()
301
+
302
+
303
+ def main_inspect_params(precalculated_supplier, battery_model, all_data_with_predictions):
304
+ def inspect_params(params):
305
+ decider = Decider(params, precalculated_supplier)
306
+ results, total_network_fee = simulator(battery_model, all_data_with_predictions, decider)
307
+ print(params, total_network_fee)
308
+ print(np.histogram(results['decisions'].to_numpy()))
309
+ date_range = ("2021-01-15", "2021-02-01")
310
+ date_range = ("2021-08-15", "2021-09-01")
311
+
312
+ plotly_fig = plotly_visualize_simulation(results, date_range=date_range)
313
+ plotly_fig.update_layout(title=dict(text=f"{total_network_fee/1e6:0.2f} MFt"))
314
+ plotly_fig.show()
315
+
316
+ plotly_fig_2 = plotly_visualize_monthly(results)
317
+ plotly_fig_2.update_layout(title=dict(text=f"{total_network_fee/1e6:0.2f} MFt"))
318
+ plotly_fig_2.show()
319
+
320
+ inspect_params(np.array([5, 39]))
321
+ inspect_params(np.array([1400, 50]))
322
+
323
 
324
  def main():
325
  np.random.seed(1)
 
343
  time_interval_h = time_interval_min / 60
344
 
345
  # for faster testing:
346
+ DATASET_TRUNCATED_SIZE = 10000
347
  if DATASET_TRUNCATED_SIZE is not None:
348
  print("Truncating dataset to", DATASET_TRUNCATED_SIZE, "datapoints, that is", DATASET_TRUNCATED_SIZE * time_interval_h / 24, "days")
349
  all_data = all_data.iloc[:DATASET_TRUNCATED_SIZE]
 
361
 
362
  battery_model = BatteryModel(capacity_Ah=600, time_interval_h=time_interval_h)
363
 
364
+ # now that we've finally set everything up, we can do various things, hence several main_*().
365
 
366
+ # main_param_grid(precalculated_supplier, battery_model, all_data_with_predictions) ; exit()
 
 
 
 
367
 
368
+ # main_inspect_params(precalculated_supplier, battery_model, all_data_with_predictions) ; exit()
 
 
369
 
 
 
 
 
370
 
371
+ decider_class = Decider
 
372
 
373
+ # TODO this is super unfortunate:
374
+ # Consumption_fees travels via all_data_with_predictions,
375
+ # peak_demand and surcharge_per_kwh travels via precalculated_supplier of decider.
376
 
377
+ best_params, collected_loss_values = optimizer(decider_class, precalculated_supplier, battery_model, all_data_with_predictions)
378
+ visualize_collected_loss_values(collected_loss_values)
 
 
379
 
 
 
380
 
381
  if __name__ == '__main__':
382
  main()
v2/decider.py CHANGED
@@ -95,13 +95,13 @@ class Decider:
95
  # it should not be hardwired.
96
  HARDWIRED_THRESHOLD_FOR_CHEAP_POWER_HUF_PER_KWH = 20 # [HUF/kWh]
97
  if mean_surdemand_kw > self.lookahead_surdemand_kw and current_fee <= HARDWIRED_THRESHOLD_FOR_CHEAP_POWER_HUF_PER_KWH:
98
- return Decision.NETWORK_CHARGE
99
-
100
  # peak shaving
101
- if deficit_kwh > self.precalculated_supplier.peak_demand:
102
- return Decision.DISCHARGE
103
  else:
104
- return Decision.PASSIVE
 
105
 
106
  # this is called by the optimizer so that meaningless parameter settings are not attempted
107
  # we could vectorize this easily, but it's not a bottleneck, the simulation is.
 
95
  # it should not be hardwired.
96
  HARDWIRED_THRESHOLD_FOR_CHEAP_POWER_HUF_PER_KWH = 20 # [HUF/kWh]
97
  if mean_surdemand_kw > self.lookahead_surdemand_kw and current_fee <= HARDWIRED_THRESHOLD_FOR_CHEAP_POWER_HUF_PER_KWH:
98
+ decision = Decision.NETWORK_CHARGE
 
99
  # peak shaving
100
+ elif deficit_kwh > self.precalculated_supplier.peak_demand:
101
+ decision = Decision.DISCHARGE
102
  else:
103
+ decision = Decision.PASSIVE
104
+ return decision
105
 
106
  # this is called by the optimizer so that meaningless parameter settings are not attempted
107
  # we could vectorize this easily, but it's not a bottleneck, the simulation is.
v2/evolution_strategies.py CHANGED
@@ -16,7 +16,7 @@ def evolution_strategies_optimizer(objective_function, clipper_function,
16
  number_of_generations,
17
  population_size):
18
  # Initialize parameters
19
- mutation_scale = 0.05
20
  selection_ratio = 0.5
21
  selected_size = int(population_size * selection_ratio)
22
 
@@ -27,30 +27,25 @@ def evolution_strategies_optimizer(objective_function, clipper_function,
27
  for generation in range(number_of_generations):
28
  # Evaluate fitness
29
  fitness = np.array([objective_function(individual) for individual in population])
30
-
31
  # Select the best individuals
32
  selected_indices = np.argsort(fitness)[:selected_size]
33
  selected = population[selected_indices]
34
-
35
  # Reproduce (mutate)
36
  offspring = selected + np.random.normal(loc=0, scale=init_scale * mutation_scale, size=(selected_size, len(init_mean)))
37
  clip_params(offspring, clipper_function) # in-place
38
-
39
- # Replacement: Here we simply generate new candidates around the selected ones
40
- population[:selected_size] = selected
41
- population[selected_size:] = offspring
42
-
43
  # Logging
44
  best_fitness = fitness[selected_indices[0]]
45
- best_index = np.argmin(fitness)
46
- best_solution = population[best_index]
47
  print(f"Generation {generation + 1}: Best Fitness = {best_fitness}", f"Best solution so far: {best_solution}")
48
 
 
 
 
49
 
50
- # Best solution
51
- best_index = np.argmin(fitness)
52
- best_solution = population[best_index]
53
- print(f"Best solution found: {best_solution}")
54
  return best_solution
55
 
56
 
 
16
  number_of_generations,
17
  population_size):
18
  # Initialize parameters
19
+ mutation_scale = 0.1
20
  selection_ratio = 0.5
21
  selected_size = int(population_size * selection_ratio)
22
 
 
27
  for generation in range(number_of_generations):
28
  # Evaluate fitness
29
  fitness = np.array([objective_function(individual) for individual in population])
30
+
31
  # Select the best individuals
32
  selected_indices = np.argsort(fitness)[:selected_size]
33
  selected = population[selected_indices]
34
+
35
  # Reproduce (mutate)
36
  offspring = selected + np.random.normal(loc=0, scale=init_scale * mutation_scale, size=(selected_size, len(init_mean)))
37
  clip_params(offspring, clipper_function) # in-place
38
+
 
 
 
 
39
  # Logging
40
  best_fitness = fitness[selected_indices[0]]
41
+ best_solution = selected[0].copy()
 
42
  print(f"Generation {generation + 1}: Best Fitness = {best_fitness}", f"Best solution so far: {best_solution}")
43
 
44
+ # Replacement: Here we simply generate new candidates around the selected ones
45
+ population[:selected_size] = selected
46
+ population[selected_size:] = offspring
47
 
48
+ print(f"Best solution found: {best_solution} with loss {best_fitness}")
 
 
 
49
  return best_solution
50
 
51