Daniel Varga commited on
Commit
cb62f63
·
1 Parent(s): eb9eaaf

precalculated supplier, spaghetti but fast

Browse files
Files changed (2) hide show
  1. v2/architecture.py +40 -13
  2. v2/supplier.py +26 -5
v2/architecture.py CHANGED
@@ -6,7 +6,7 @@ from enum import IntEnum
6
  import matplotlib.pyplot as plt
7
 
8
  # it's really just a network pricing model
9
- from supplier import Supplier
10
  from data_processing import read_datasets, add_production_field, interpolate_and_join, SolarParameters
11
 
12
 
@@ -98,18 +98,18 @@ class Decision(IntEnum):
98
  # mock class as usual
99
  # output_window_size is not yet used, always decides one timestep.
100
  class Decider:
101
- def __init__(self, supplier):
102
  self.input_window_size = STEPS_PER_HOUR * 24 # day long window.
103
  self.random_seed = 0
104
- self.supplier = supplier
105
 
106
  # prod_cons_pred is a dataframe starting at now, containing
107
  # fields Production and Consumption.
108
  # this function does not mutate its inputs.
109
  # battery_model is just queried for capacity and current soc.
110
  # the method returns a pd.Series of Decisions as integers.
111
- def decide(self, prod_pred, cons_pred, battery_model):
112
- return Decision.DISCHARGE
113
  next_prod = prod_pred[0]
114
  next_cons = cons_pred[0]
115
  deficit = next_cons - next_prod
@@ -143,17 +143,19 @@ class DummyPredictor:
143
 
144
  # this function does not mutate its inputs.
145
  # it makes a clone of battery_model and modifies that.
146
- def simulator(battery_model, supplier, prod_cons, decider):
147
  battery_model = copy.copy(battery_model)
148
 
149
  demand_np = prod_cons['Consumption'].to_numpy()
150
  production_np = prod_cons['Production'].to_numpy()
151
- demand_prediction_np = demand_np # prod_cons['Consumption_prediction'].to_numpy()
152
- production_prediction_np = production_np # prod_cons['Production_prediction'].to_numpy()
153
  assert len(demand_np) == len(production_np)
154
  step_in_minutes = prod_cons.index.freq.n
155
  assert step_in_minutes == 5
156
 
 
 
157
  print("Simulating for", len(demand_np), "time steps. Each step is", step_in_minutes, "minutes.")
158
  soc_series = []
159
  # by convention, we only call end user demand, demand,
@@ -194,7 +196,8 @@ def simulator(battery_model, supplier, prod_cons, decider):
194
  # 3. there should not be two of them.
195
  prod_prediction = production_prediction_np[i: i + decider.input_window_size]
196
  cons_prediction = demand_prediction_np[i: i + decider.input_window_size]
197
- decision = decider.decide(prod_prediction, cons_prediction, battery_model)
 
198
 
199
  production_used_to_charge = 0
200
  if unsatisfied_demand >= remaining_production:
@@ -243,9 +246,19 @@ def simulator(battery_model, supplier, prod_cons, decider):
243
  discarded_production_series = np.array(discarded_production_series)
244
 
245
  consumption_from_network_pandas_series = pd.Series(consumption_from_network_series, index=prod_cons.index)
 
 
 
246
  total_charge, consumption_charge_series, demand_charges = supplier.fee(
247
  consumption_from_network_pandas_series,
248
  provide_detail=True)
 
 
 
 
 
 
 
249
  print(f"All in all we have paid {total_charge} to network.")
250
 
251
  if DO_VIS:
@@ -266,9 +279,11 @@ def simulator(battery_model, supplier, prod_cons, decider):
266
 
267
 
268
  def main():
269
-
270
  supplier = Supplier(price=100) # Ft/kWh
271
- supplier.set_price_for_interval(9, 17, 150) # nine-to-five increased price.
 
 
 
272
  # peak_demand dimension is kWh, but it's interpreted as the full consumption
273
  # during a 15 minute timestep.
274
  supplier.set_demand_charge(peak_demand=25, surcharge_per_kwh=500) # kWh in a 15 minutes interval, Ft/kWh
@@ -279,16 +294,28 @@ def main():
279
  add_production_field(met_2021_data, parameters)
280
  all_data = interpolate_and_join(met_2021_data, cons_2021_data)
281
 
 
282
  all_data_with_predictions = all_data.copy()
 
 
 
 
 
 
 
 
283
 
284
  time_interval_min = all_data.index.freq.n
285
  time_interval_h = time_interval_min / 60
286
  battery_model = BatteryModel(capacity_Ah=600, time_interval_h=time_interval_h)
287
 
288
- decider = Decider(supplier)
 
 
 
289
 
290
  t = time.perf_counter()
291
- results = simulator(battery_model, supplier, all_data_with_predictions, decider)
292
  print("Simulation runtime", time.perf_counter() - t, "seconds.")
293
 
294
  if DO_VIS:
 
6
  import matplotlib.pyplot as plt
7
 
8
  # it's really just a network pricing model
9
+ from supplier import Supplier, precalculate_supplier
10
  from data_processing import read_datasets, add_production_field, interpolate_and_join, SolarParameters
11
 
12
 
 
98
  # mock class as usual
99
  # output_window_size is not yet used, always decides one timestep.
100
  class Decider:
101
+ def __init__(self, precalculated_supplier):
102
  self.input_window_size = STEPS_PER_HOUR * 24 # day long window.
103
  self.random_seed = 0
104
+ self.precalculated_supplier = precalculated_supplier
105
 
106
  # prod_cons_pred is a dataframe starting at now, containing
107
  # fields Production and Consumption.
108
  # this function does not mutate its inputs.
109
  # battery_model is just queried for capacity and current soc.
110
  # the method returns a pd.Series of Decisions as integers.
111
+ def decide(self, prod_pred, cons_pred, fees, battery_model):
112
+ return Decision.PASSIVE
113
  next_prod = prod_pred[0]
114
  next_cons = cons_pred[0]
115
  deficit = next_cons - next_prod
 
143
 
144
  # this function does not mutate its inputs.
145
  # it makes a clone of battery_model and modifies that.
146
+ def simulator(battery_model, prod_cons, decider):
147
  battery_model = copy.copy(battery_model)
148
 
149
  demand_np = prod_cons['Consumption'].to_numpy()
150
  production_np = prod_cons['Production'].to_numpy()
151
+ demand_prediction_np = prod_cons['Consumption_prediction'].to_numpy()
152
+ production_prediction_np = prod_cons['Production_prediction'].to_numpy()
153
  assert len(demand_np) == len(production_np)
154
  step_in_minutes = prod_cons.index.freq.n
155
  assert step_in_minutes == 5
156
 
157
+ consumption_fees_np = prod_cons['Consumption_fees'].to_numpy()
158
+
159
  print("Simulating for", len(demand_np), "time steps. Each step is", step_in_minutes, "minutes.")
160
  soc_series = []
161
  # by convention, we only call end user demand, demand,
 
196
  # 3. there should not be two of them.
197
  prod_prediction = production_prediction_np[i: i + decider.input_window_size]
198
  cons_prediction = demand_prediction_np[i: i + decider.input_window_size]
199
+ consumption_fees = consumption_fees_np[i: i + decider.input_window_size]
200
+ decision = decider.decide(prod_prediction, cons_prediction, consumption_fees, battery_model)
201
 
202
  production_used_to_charge = 0
203
  if unsatisfied_demand >= remaining_production:
 
246
  discarded_production_series = np.array(discarded_production_series)
247
 
248
  consumption_from_network_pandas_series = pd.Series(consumption_from_network_series, index=prod_cons.index)
249
+
250
+ # TODO badly duplicating functionality
251
+ '''
252
  total_charge, consumption_charge_series, demand_charges = supplier.fee(
253
  consumption_from_network_pandas_series,
254
  provide_detail=True)
255
+ '''
256
+ consumption_charge_series = consumption_fees_np * consumption_from_network_pandas_series.to_numpy()
257
+ step_in_hour = consumption_from_network_pandas_series.index.freq.n / 60 # [hour], the length of a time step.
258
+ fifteen_minute_demands_in_kwh = consumption_from_network_pandas_series.resample('15T').sum() * step_in_hour
259
+ fifteen_minute_surdemands_in_kwh = (fifteen_minute_demands_in_kwh - decider.precalculated_supplier.peak_demand).clip(lower=0)
260
+ demand_charges = fifteen_minute_surdemands_in_kwh * decider.precalculated_supplier.surcharge_per_kwh
261
+ total_charge = consumption_charge_series.sum() + demand_charges.sum()
262
  print(f"All in all we have paid {total_charge} to network.")
263
 
264
  if DO_VIS:
 
279
 
280
 
281
  def main():
 
282
  supplier = Supplier(price=100) # Ft/kWh
283
+ # nine-to-five increased price.
284
+ supplier.set_price_for_daily_interval(9, 17, 150)
285
+ # midnight-to-three decreased price, to test network charge.
286
+ supplier.set_price_for_daily_interval(0, 3, 20)
287
  # peak_demand dimension is kWh, but it's interpreted as the full consumption
288
  # during a 15 minute timestep.
289
  supplier.set_demand_charge(peak_demand=25, surcharge_per_kwh=500) # kWh in a 15 minutes interval, Ft/kWh
 
294
  add_production_field(met_2021_data, parameters)
295
  all_data = interpolate_and_join(met_2021_data, cons_2021_data)
296
 
297
+ # we have perfect foresight, yet:
298
  all_data_with_predictions = all_data.copy()
299
+ all_data_with_predictions['Consumption_prediction'] = all_data_with_predictions['Consumption']
300
+ all_data_with_predictions['Production_prediction'] = all_data_with_predictions['Production']
301
+
302
+ precalculated_supplier = precalculate_supplier(supplier, all_data.index)
303
+ # we delete the supplier to avoid accidentally calling it instead of precalculated_supplier
304
+ supplier = None
305
+
306
+ all_data_with_predictions['Consumption_fees'] = precalculated_supplier.consumption_fees # [HUF / kWh]
307
 
308
  time_interval_min = all_data.index.freq.n
309
  time_interval_h = time_interval_min / 60
310
  battery_model = BatteryModel(capacity_Ah=600, time_interval_h=time_interval_h)
311
 
312
+ # TODO this is super unfortunate:
313
+ # Consumption_fees travels via all_data_with_predictions,
314
+ # peak_demand and surcharge_per_kwh travels via precalculated_supplier of decider.
315
+ decider = Decider(precalculated_supplier)
316
 
317
  t = time.perf_counter()
318
+ results = simulator(battery_model, all_data_with_predictions, decider)
319
  print("Simulation runtime", time.perf_counter() - t, "seconds.")
320
 
321
  if DO_VIS:
v2/supplier.py CHANGED
@@ -3,6 +3,7 @@
3
  import numpy as np
4
  import pandas as pd
5
  import datetime
 
6
  import unittest
7
 
8
 
@@ -16,19 +17,19 @@ class Supplier:
16
  self.surcharge_per_kwh = 0
17
 
18
  # start and end are indices of hours starting from Monday 00:00.
19
- def set_price_for_interval(self, start, end, price):
20
  self.hourly_prices[start:end] = price
21
 
22
  # start and end are indices of hours of the day. for each day, this interval is set to price
23
  def set_price_for_daily_interval(self, start, end, price):
24
  for day in range(7):
25
  h = day * 24
26
- self.set_price_for_interval(h + start, h + end, price)
27
 
28
  def set_price_for_daily_interval_on_workdays(self, start, end, price):
29
  for day in range(5):
30
  h = day * 24
31
- self.set_price_for_interval(h + start, h + end, price)
32
 
33
  def set_demand_charge(self, peak_demand, surcharge_per_kwh):
34
  self.peak_demand = peak_demand # [kWh]
@@ -81,6 +82,26 @@ class Supplier:
81
  return total_charge
82
 
83
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  class TestSupplier(unittest.TestCase):
85
 
86
  def setUp(self):
@@ -92,14 +113,14 @@ class TestSupplier(unittest.TestCase):
92
  self.assertTrue(np.array_equal(self.supplier.hourly_prices, expected_hourly_prices))
93
 
94
  def test_set_price_for_interval(self):
95
- self.supplier.set_price_for_interval(0, 24, 20)
96
  expected_hourly_prices = np.ones(168) * self.constant_price
97
  expected_hourly_prices[0:24] = 20
98
  self.assertTrue(np.array_equal(self.supplier.hourly_prices, expected_hourly_prices))
99
 
100
  def test_price(self):
101
  increased_price = 20
102
- self.supplier.set_price_for_interval(0, 24, increased_price)
103
 
104
  date = datetime.datetime(2023, 4, 30, 12, 0, 0) # Sunday noon
105
  expected_price = self.constant_price
 
3
  import numpy as np
4
  import pandas as pd
5
  import datetime
6
+ from dataclasses import dataclass
7
  import unittest
8
 
9
 
 
17
  self.surcharge_per_kwh = 0
18
 
19
  # start and end are indices of hours starting from Monday 00:00.
20
+ def set_price_for_weekly_interval(self, start, end, price):
21
  self.hourly_prices[start:end] = price
22
 
23
  # start and end are indices of hours of the day. for each day, this interval is set to price
24
  def set_price_for_daily_interval(self, start, end, price):
25
  for day in range(7):
26
  h = day * 24
27
+ self.set_price_for_weekly_interval(h + start, h + end, price)
28
 
29
  def set_price_for_daily_interval_on_workdays(self, start, end, price):
30
  for day in range(5):
31
  h = day * 24
32
+ self.set_price_for_weekly_interval(h + start, h + end, price)
33
 
34
  def set_demand_charge(self, peak_demand, surcharge_per_kwh):
35
  self.peak_demand = peak_demand # [kWh]
 
82
  return total_charge
83
 
84
 
85
+ @dataclass
86
+ class PrecalculatedSupplier:
87
+ peak_demand: float # [kWh]
88
+ surcharge_per_kwh: float # [HUF / kWh surplus over 15 min interval]
89
+ consumption_fees: np.ndarray
90
+ time_index: pd.DatetimeIndex
91
+
92
+
93
+ def precalculate_supplier(supplier, time_index):
94
+ ones = pd.Series(1, index=time_index)
95
+ total_charge, consumption_charge_series, demand_charges = supplier.fee(ones, provide_detail=True)
96
+
97
+ p = PrecalculatedSupplier(
98
+ peak_demand=supplier.peak_demand,
99
+ surcharge_per_kwh=supplier.surcharge_per_kwh,
100
+ consumption_fees=consumption_charge_series.to_numpy(),
101
+ time_index=time_index)
102
+ return p
103
+
104
+
105
  class TestSupplier(unittest.TestCase):
106
 
107
  def setUp(self):
 
113
  self.assertTrue(np.array_equal(self.supplier.hourly_prices, expected_hourly_prices))
114
 
115
  def test_set_price_for_interval(self):
116
+ self.supplier.set_price_for_weekly_interval(0, 24, 20)
117
  expected_hourly_prices = np.ones(168) * self.constant_price
118
  expected_hourly_prices[0:24] = 20
119
  self.assertTrue(np.array_equal(self.supplier.hourly_prices, expected_hourly_prices))
120
 
121
  def test_price(self):
122
  increased_price = 20
123
+ self.supplier.set_price_for_weekly_interval(0, 24, increased_price)
124
 
125
  date = datetime.datetime(2023, 4, 30, 12, 0, 0) # Sunday noon
126
  expected_price = self.constant_price