Spaces:
Sleeping
Sleeping
Daniel Varga
commited on
Commit
•
c5bd69c
1
Parent(s):
ffa5234
does something end-to-end
Browse files- data_processing.py +1 -1
- v2/architecture.py +39 -8
- v2/data_processing.py +1 -1
- v2/decider.py +37 -2
- v2/evolution_strategies.py +14 -2
data_processing.py
CHANGED
@@ -43,7 +43,7 @@ def read_datasets(mini=False):
|
|
43 |
|
44 |
@dataclass
|
45 |
class Parameters:
|
46 |
-
solar_cell_num: float =
|
47 |
solar_efficiency: float = 0.93 * 0.96 # [dimensionless]
|
48 |
NOCT: float = 280 # [W]
|
49 |
NOCT_irradiation: float = 800 # [W/m^2]
|
|
|
43 |
|
44 |
@dataclass
|
45 |
class Parameters:
|
46 |
+
solar_cell_num: float = 1140 # units
|
47 |
solar_efficiency: float = 0.93 * 0.96 # [dimensionless]
|
48 |
NOCT: float = 280 # [W]
|
49 |
NOCT_irradiation: float = 800 # [W/m^2]
|
v2/architecture.py
CHANGED
@@ -14,6 +14,7 @@ from evolution_strategies import evolution_strategies_optimizer
|
|
14 |
|
15 |
DO_VIS = False
|
16 |
|
|
|
17 |
# we predict last week same time for consumption, and yesterday same time for production.
|
18 |
# adds fields in-place
|
19 |
def add_dummy_predictions(all_data_with_predictions):
|
@@ -52,6 +53,29 @@ def simulator(battery_model, prod_cons, decider):
|
|
52 |
step_in_minutes = prod_cons.index.freq.n
|
53 |
assert step_in_minutes == 5
|
54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
55 |
consumption_fees_np = prod_cons['Consumption_fees'].to_numpy()
|
56 |
|
57 |
print("Simulating for", len(demand_np), "time steps. Each step is", step_in_minutes, "minutes.")
|
@@ -157,7 +181,7 @@ def simulator(battery_model, prod_cons, decider):
|
|
157 |
fifteen_minute_surdemands_in_kwh = (fifteen_minute_demands_in_kwh - decider.precalculated_supplier.peak_demand).clip(lower=0)
|
158 |
demand_charges = fifteen_minute_surdemands_in_kwh * decider.precalculated_supplier.surcharge_per_kwh
|
159 |
total_network_fee = consumption_charge_series.sum() + demand_charges.sum()
|
160 |
-
print(f"
|
161 |
|
162 |
if DO_VIS:
|
163 |
demand_charges.plot()
|
@@ -176,17 +200,21 @@ def simulator(battery_model, prod_cons, decider):
|
|
176 |
return results, total_network_fee
|
177 |
|
178 |
|
|
|
179 |
def optimizer(battery_model, all_data_with_predictions, precalculated_supplier):
|
180 |
def objective_function(params):
|
|
|
181 |
# TODO params completely ignored right now.
|
182 |
-
decider = Decider(precalculated_supplier)
|
183 |
t = time.perf_counter()
|
184 |
results, total_network_fee = simulator(battery_model, all_data_with_predictions, decider)
|
185 |
return total_network_fee
|
186 |
|
187 |
-
|
188 |
-
|
189 |
-
|
|
|
|
|
190 |
|
191 |
|
192 |
def main():
|
@@ -199,12 +227,14 @@ def main():
|
|
199 |
# during a 15 minute timestep.
|
200 |
supplier.set_demand_charge(peak_demand=2.5, surcharge_per_kwh=500) # kWh in a 15 minutes interval, Ft/kWh
|
201 |
|
202 |
-
|
203 |
|
204 |
met_2021_data, cons_2021_data = read_datasets()
|
205 |
-
add_production_field(met_2021_data,
|
206 |
all_data = interpolate_and_join(met_2021_data, cons_2021_data)
|
207 |
|
|
|
|
|
208 |
time_interval_min = all_data.index.freq.n
|
209 |
time_interval_h = time_interval_min / 60
|
210 |
|
@@ -222,7 +252,8 @@ def main():
|
|
222 |
# TODO this is super unfortunate:
|
223 |
# Consumption_fees travels via all_data_with_predictions,
|
224 |
# peak_demand and surcharge_per_kwh travels via precalculated_supplier of decider.
|
225 |
-
|
|
|
226 |
|
227 |
t = time.perf_counter()
|
228 |
results, total_network_fee = simulator(battery_model, all_data_with_predictions, decider)
|
|
|
14 |
|
15 |
DO_VIS = False
|
16 |
|
17 |
+
|
18 |
# we predict last week same time for consumption, and yesterday same time for production.
|
19 |
# adds fields in-place
|
20 |
def add_dummy_predictions(all_data_with_predictions):
|
|
|
53 |
step_in_minutes = prod_cons.index.freq.n
|
54 |
assert step_in_minutes == 5
|
55 |
|
56 |
+
'''
|
57 |
+
surdemand_np = demand_np - production_np
|
58 |
+
plt.plot(demand_np, c="r")
|
59 |
+
plt.plot(production_np, c="g")
|
60 |
+
plt.plot(surdemand_np, c="b")
|
61 |
+
plt.show()
|
62 |
+
print("max prod", production_np.max())
|
63 |
+
exit()
|
64 |
+
'''
|
65 |
+
|
66 |
+
'''
|
67 |
+
surdemand_window = 24 * 60 // 5 # one day
|
68 |
+
mean_surdemands_kw = []
|
69 |
+
for i in range(len(demand_prediction_np) - surdemand_window):
|
70 |
+
mean_surdemand_kw = (demand_prediction_np[i: i+surdemand_window] - production_prediction_np[i: i+surdemand_window]).mean()
|
71 |
+
mean_surdemands_kw.append(mean_surdemand_kw)
|
72 |
+
mean_surdemands_kw = pd.Series(mean_surdemands_kw, prod_cons.index[:len(mean_surdemands_kw)])
|
73 |
+
mean_surdemands_kw.plot()
|
74 |
+
plt.title("mean_surdemands_kw")
|
75 |
+
plt.show()
|
76 |
+
exit()
|
77 |
+
'''
|
78 |
+
|
79 |
consumption_fees_np = prod_cons['Consumption_fees'].to_numpy()
|
80 |
|
81 |
print("Simulating for", len(demand_np), "time steps. Each step is", step_in_minutes, "minutes.")
|
|
|
181 |
fifteen_minute_surdemands_in_kwh = (fifteen_minute_demands_in_kwh - decider.precalculated_supplier.peak_demand).clip(lower=0)
|
182 |
demand_charges = fifteen_minute_surdemands_in_kwh * decider.precalculated_supplier.surcharge_per_kwh
|
183 |
total_network_fee = consumption_charge_series.sum() + demand_charges.sum()
|
184 |
+
print(f"Total network fee {total_network_fee / 10 ** 6} MHUF.")
|
185 |
|
186 |
if DO_VIS:
|
187 |
demand_charges.plot()
|
|
|
200 |
return results, total_network_fee
|
201 |
|
202 |
|
203 |
+
|
204 |
def optimizer(battery_model, all_data_with_predictions, precalculated_supplier):
|
205 |
def objective_function(params):
|
206 |
+
print("Simulating with parameters", params)
|
207 |
# TODO params completely ignored right now.
|
208 |
+
decider = Decider(params, precalculated_supplier)
|
209 |
t = time.perf_counter()
|
210 |
results, total_network_fee = simulator(battery_model, all_data_with_predictions, decider)
|
211 |
return total_network_fee
|
212 |
|
213 |
+
def clipper_function(params):
|
214 |
+
return Decider.clip_params(params)
|
215 |
+
|
216 |
+
init_mean, init_scale = Decider.initial_params()
|
217 |
+
best_params = evolution_strategies_optimizer(objective_function, clipper_function, init_mean=init_mean, init_scale=init_scale)
|
218 |
|
219 |
|
220 |
def main():
|
|
|
227 |
# during a 15 minute timestep.
|
228 |
supplier.set_demand_charge(peak_demand=2.5, surcharge_per_kwh=500) # kWh in a 15 minutes interval, Ft/kWh
|
229 |
|
230 |
+
solar_parameters = SolarParameters()
|
231 |
|
232 |
met_2021_data, cons_2021_data = read_datasets()
|
233 |
+
add_production_field(met_2021_data, solar_parameters)
|
234 |
all_data = interpolate_and_join(met_2021_data, cons_2021_data)
|
235 |
|
236 |
+
print("Working with", solar_parameters.solar_cell_num, "solar cells, that's a maximum production of", all_data['Production'].max(), "kW.")
|
237 |
+
|
238 |
time_interval_min = all_data.index.freq.n
|
239 |
time_interval_h = time_interval_min / 60
|
240 |
|
|
|
252 |
# TODO this is super unfortunate:
|
253 |
# Consumption_fees travels via all_data_with_predictions,
|
254 |
# peak_demand and surcharge_per_kwh travels via precalculated_supplier of decider.
|
255 |
+
decider_init_mean, decider_init_scale = Decider.initial_params()
|
256 |
+
decider = Decider(decider_init_mean, precalculated_supplier)
|
257 |
|
258 |
t = time.perf_counter()
|
259 |
results, total_network_fee = simulator(battery_model, all_data_with_predictions, decider)
|
v2/data_processing.py
CHANGED
@@ -44,7 +44,7 @@ def read_datasets(mini=False):
|
|
44 |
# BESS parameters are now in BatteryModel
|
45 |
@dataclass
|
46 |
class SolarParameters:
|
47 |
-
solar_cell_num: float =
|
48 |
solar_efficiency: float = 0.93 * 0.96 # [dimensionless]
|
49 |
NOCT: float = 280 # [W]
|
50 |
NOCT_irradiation: float = 800 # [W/m^2]
|
|
|
44 |
# BESS parameters are now in BatteryModel
|
45 |
@dataclass
|
46 |
class SolarParameters:
|
47 |
+
solar_cell_num: float = 1140 # units
|
48 |
solar_efficiency: float = 0.93 * 0.96 # [dimensionless]
|
49 |
NOCT: float = 280 # [W]
|
50 |
NOCT_irradiation: float = 800 # [W/m^2]
|
v2/decider.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1 |
from enum import IntEnum
|
2 |
-
|
3 |
|
4 |
|
5 |
STEPS_PER_HOUR = 12
|
@@ -28,10 +28,16 @@ class Decision(IntEnum):
|
|
28 |
# mock class as usual
|
29 |
# output_window_size is not yet used, always decides one timestep.
|
30 |
class Decider:
|
31 |
-
def __init__(self, precalculated_supplier):
|
32 |
self.input_window_size = STEPS_PER_HOUR * 24 # day long window.
|
33 |
self.random_seed = 0
|
34 |
self.precalculated_supplier = precalculated_supplier
|
|
|
|
|
|
|
|
|
|
|
|
|
35 |
|
36 |
def decide(self, prod_pred, cons_pred, fees, battery_model):
|
37 |
# TODO 15 minutes demand charge window hardwired at this weird place
|
@@ -42,9 +48,38 @@ class Decider:
|
|
42 |
step_in_hour = time_interval_min / 60 # [hour], the length of a time step.
|
43 |
deficit_kw = (cons_pred[:peak_shaving_window] - prod_pred[:peak_shaving_window]).clip(min=0)
|
44 |
deficit_kwh = (step_in_hour * deficit_kw).sum()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
45 |
if deficit_kwh > self.precalculated_supplier.peak_demand:
|
46 |
return Decision.DISCHARGE
|
47 |
else:
|
48 |
return Decision.PASSIVE
|
49 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
50 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
from enum import IntEnum
|
2 |
+
import numpy as np
|
3 |
|
4 |
|
5 |
STEPS_PER_HOUR = 12
|
|
|
28 |
# mock class as usual
|
29 |
# output_window_size is not yet used, always decides one timestep.
|
30 |
class Decider:
|
31 |
+
def __init__(self, params, precalculated_supplier):
|
32 |
self.input_window_size = STEPS_PER_HOUR * 24 # day long window.
|
33 |
self.random_seed = 0
|
34 |
self.precalculated_supplier = precalculated_supplier
|
35 |
+
assert params.shape == (2, )
|
36 |
+
# param_1 is how many minutes do we look ahead to decide if there's
|
37 |
+
# an upcoming shortage.
|
38 |
+
# param_2 is the threshhold for shortage, in kW not kWh,
|
39 |
+
# that is, parametrized as an average over the window.
|
40 |
+
self.surdemand_lookahead_window_min, self.lookahead_surdemand_kw = params
|
41 |
|
42 |
def decide(self, prod_pred, cons_pred, fees, battery_model):
|
43 |
# TODO 15 minutes demand charge window hardwired at this weird place
|
|
|
48 |
step_in_hour = time_interval_min / 60 # [hour], the length of a time step.
|
49 |
deficit_kw = (cons_pred[:peak_shaving_window] - prod_pred[:peak_shaving_window]).clip(min=0)
|
50 |
deficit_kwh = (step_in_hour * deficit_kw).sum()
|
51 |
+
|
52 |
+
surdemand_window = int(self.surdemand_lookahead_window_min // time_interval_min)
|
53 |
+
mean_surdemand_kw = (cons_pred[:surdemand_window] - prod_pred[:surdemand_window]).mean()
|
54 |
+
|
55 |
+
current_fee = fees[0]
|
56 |
+
# TODO this should not be a hard threashold, and more importantly,
|
57 |
+
# it should not be hardwired.
|
58 |
+
HARDWIRED_THRESHOLD_FOR_CHEAP_POWER_HUF_PER_KWH = 20 # [HUF/kWh]
|
59 |
+
if mean_surdemand_kw > self.lookahead_surdemand_kw and current_fee <= HARDWIRED_THRESHOLD_FOR_CHEAP_POWER_HUF_PER_KWH:
|
60 |
+
return Decision.NETWORK_CHARGE
|
61 |
+
|
62 |
if deficit_kwh > self.precalculated_supplier.peak_demand:
|
63 |
return Decision.DISCHARGE
|
64 |
else:
|
65 |
return Decision.PASSIVE
|
66 |
|
67 |
+
# this is called by the optimizer so that meaningless parameter settings are not attempted
|
68 |
+
# we could vectorize this easily, but it's not a bottleneck, the simulation is.
|
69 |
+
@staticmethod
|
70 |
+
def clip_params(params):
|
71 |
+
assert params.shape == (2, )
|
72 |
+
surdemand_lookahead_window_min, lookahead_surdemand_kw = params
|
73 |
+
surdemand_lookahead_window_min = np.clip(surdemand_lookahead_window_min, 5, 60 * 24 * 3)
|
74 |
+
# no-op right now:
|
75 |
+
lookahead_surdemand_kw = np.clip(lookahead_surdemand_kw, -np.inf, np.inf)
|
76 |
+
return np.array([surdemand_lookahead_window_min, lookahead_surdemand_kw])
|
77 |
|
78 |
+
@staticmethod
|
79 |
+
def initial_params():
|
80 |
+
# surdemand_lookahead_window_min, lookahead_surdemand_kw
|
81 |
+
# param1 [minutes]. one day mean, half day scale for lookahead horizon.
|
82 |
+
# param2 [kWh], averaged over the horizon. negative values are meaningful.
|
83 |
+
init_mean = np.array([1440.0, 0.0])
|
84 |
+
init_scale = np.array([720.0, 100.0])
|
85 |
+
return init_mean, init_scale
|
v2/evolution_strategies.py
CHANGED
@@ -1,7 +1,13 @@
|
|
1 |
import numpy as np
|
2 |
|
3 |
|
4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
5 |
# Initialize parameters
|
6 |
population_size = 100
|
7 |
number_of_generations = 30
|
@@ -11,6 +17,7 @@ def evolution_strategies_optimizer(objective_function, init_mean, init_scale):
|
|
11 |
|
12 |
# Initialize population (randomly)
|
13 |
population = np.random.normal(loc=init_mean, scale=init_scale, size=(population_size, len(init_mean)))
|
|
|
14 |
|
15 |
for generation in range(number_of_generations):
|
16 |
# Evaluate fitness
|
@@ -22,6 +29,7 @@ def evolution_strategies_optimizer(objective_function, init_mean, init_scale):
|
|
22 |
|
23 |
# Reproduce (mutate)
|
24 |
offspring = selected + np.random.randn(selected_size, 2) * mutation_scale
|
|
|
25 |
|
26 |
# Replacement: Here we simply generate new candidates around the selected ones
|
27 |
population[:selected_size] = selected
|
@@ -45,10 +53,14 @@ def toy_objective_function(x):
|
|
45 |
return (x[0] - 3)**2 + (x[1] + 2)**2
|
46 |
|
47 |
|
|
|
|
|
|
|
|
|
48 |
def main():
|
49 |
init_mean = np.array([0.0, 0.0])
|
50 |
init_scale = np.array([10.0, 10.0])
|
51 |
-
best_solution = evolution_strategies_optimizer(toy_objective_function, init_mean, init_scale)
|
52 |
|
53 |
|
54 |
if __name__ == '__main__':
|
|
|
1 |
import numpy as np
|
2 |
|
3 |
|
4 |
+
# in-place
|
5 |
+
def clip_params(population, clipper_function):
|
6 |
+
for i, individual in enumerate(population):
|
7 |
+
population[i] = clipper_function(individual)
|
8 |
+
|
9 |
+
|
10 |
+
def evolution_strategies_optimizer(objective_function, clipper_function, init_mean, init_scale):
|
11 |
# Initialize parameters
|
12 |
population_size = 100
|
13 |
number_of_generations = 30
|
|
|
17 |
|
18 |
# Initialize population (randomly)
|
19 |
population = np.random.normal(loc=init_mean, scale=init_scale, size=(population_size, len(init_mean)))
|
20 |
+
clip_params(population, clipper_function)
|
21 |
|
22 |
for generation in range(number_of_generations):
|
23 |
# Evaluate fitness
|
|
|
29 |
|
30 |
# Reproduce (mutate)
|
31 |
offspring = selected + np.random.randn(selected_size, 2) * mutation_scale
|
32 |
+
clip_params(offspring, clipper_function) # in-place
|
33 |
|
34 |
# Replacement: Here we simply generate new candidates around the selected ones
|
35 |
population[:selected_size] = selected
|
|
|
53 |
return (x[0] - 3)**2 + (x[1] + 2)**2
|
54 |
|
55 |
|
56 |
+
def toy_clipper_function(x):
|
57 |
+
return x
|
58 |
+
|
59 |
+
|
60 |
def main():
|
61 |
init_mean = np.array([0.0, 0.0])
|
62 |
init_scale = np.array([10.0, 10.0])
|
63 |
+
best_solution = evolution_strategies_optimizer(toy_objective_function, toy_clipper_function, init_mean, init_scale)
|
64 |
|
65 |
|
66 |
if __name__ == '__main__':
|