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__':
|