Spaces:
Sleeping
Sleeping
File size: 17,547 Bytes
5ef652c 9c39ff0 eb9eaaf 5ef652c 7f927c0 cb62f63 e733d30 ffa5234 e75e97c ababe23 5ef652c ffa5234 5ef652c c5bd69c e75e97c 3705ebb e75e97c 7f927c0 4e673fc e75e97c dd725fc 5ef652c dd725fc cb62f63 0c694ac cb62f63 0c694ac c5bd69c cb62f63 4e673fc 0c694ac dd725fc 0c694ac dd725fc 0c694ac 83ff599 0c694ac e733d30 0c694ac dd725fc 4e673fc 0c694ac dd725fc f1abbf3 cb62f63 83ff599 0c694ac dd725fc 0c694ac e733d30 0c694ac e733d30 4e673fc e733d30 0c694ac e733d30 0c694ac dd725fc 0c694ac e733d30 4e673fc 0c694ac dd725fc e733d30 dd725fc 0c694ac e733d30 0c694ac 4e673fc 0c694ac 83ff599 0c694ac 4da0b0d cb62f63 a13327d 4da0b0d a13327d cb62f63 7f927c0 cb62f63 e75e97c 4e673fc eb9eaaf a13327d 0c694ac dd725fc 0c694ac dd725fc 83ff599 0c694ac dd725fc e75e97c 4e673fc 7f927c0 e75e97c 4e673fc 7f927c0 e75e97c 7f927c0 4e673fc e75e97c c5bd69c 7f927c0 4e673fc 7f927c0 4e673fc 7f927c0 c5bd69c 0c694ac 4e673fc 0c694ac 5ef652c 7f927c0 5ef652c cb62f63 4da0b0d fee109b 5ef652c c5bd69c 5ef652c c5bd69c f1abbf3 5ef652c fee109b 7f927c0 4e673fc 7f927c0 f1abbf3 e75e97c cb62f63 f1abbf3 e733d30 4e673fc 7f927c0 4e673fc 5ef652c 4e673fc e733d30 5ef652c 4e673fc 7f927c0 4e673fc 5ef652c 4e673fc ababe23 5ef652c |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 |
import numpy as np
import pandas as pd
import copy
import time
import matplotlib.pyplot as plt
# it's really just a network pricing model
from decider import Decision, Decider, RandomDecider
from supplier import Supplier, precalculate_supplier
from data_processing import read_datasets, add_production_field, interpolate_and_join, SolarParameters
from bess import BatteryModel
from evolution_strategies import evolution_strategies_optimizer
from visualization import plotly_visualize_simulation, plotly_visualize_monthly
DO_VIS = False
# we predict last week same time for consumption, and yesterday same time for production.
# adds fields in-place
def add_dummy_predictions(all_data_with_predictions):
time_interval_min = all_data_with_predictions.index.freq.n
MESSING_IT_UP_SHIFT = 0 * 60 // time_interval_min
if MESSING_IT_UP_SHIFT != 0:
print("To test dependence on prediction quality, we artificially mess up t-168 prediction with a", MESSING_IT_UP_SHIFT * time_interval_min / 60, "hour shift.")
cons_shift = 60 * 168 // time_interval_min + MESSING_IT_UP_SHIFT
prod_shift = 60 * 24 // time_interval_min + MESSING_IT_UP_SHIFT
all_data_with_predictions['Consumption_prediction'] = all_data_with_predictions['Consumption'].shift(periods=cons_shift)
all_data_with_predictions['Production_prediction'] = all_data_with_predictions['Production'].shift(periods=prod_shift)
# we predict zero before we have data, no big deal:
all_data_with_predictions.loc[all_data_with_predictions.index[:cons_shift], 'Consumption_prediction'] = 0
all_data_with_predictions.loc[all_data_with_predictions.index[:prod_shift], 'Production_prediction'] = 0
'''
all_data_with_predictions['Consumption'].plot()
all_data_with_predictions['Production'].plot()
plt.show()
all_data_with_predictions['Consumption_prediction'].plot()
all_data_with_predictions['Production_prediction'].plot()
plt.show()
'''
# even mock-er class than usual.
# knows the future in advance, so it predicts it very well.
# it's also unrealistic in that it takes row index instead of date.
class DummyPredictor:
def __init__(self, series):
self.series = series
def predict(self, indx, window_size):
prediction = self.series.iloc[indx: indx + window_size]
return prediction
# this function does not mutate its inputs.
# it makes a clone of battery_model and modifies that.
def simulator(battery_model, prod_cons, decider):
battery_model = copy.copy(battery_model)
demand_np = prod_cons['Consumption'].to_numpy()
production_np = prod_cons['Production'].to_numpy()
demand_prediction_np = prod_cons['Consumption_prediction'].to_numpy()
production_prediction_np = prod_cons['Production_prediction'].to_numpy()
assert len(demand_np) == len(production_np)
step_in_minutes = prod_cons.index.freq.n
assert step_in_minutes == 5
'''
surdemand_np = demand_np - production_np
plt.plot(demand_np, c="r")
plt.plot(production_np, c="g")
plt.plot(surdemand_np, c="b")
plt.show()
print("max prod", production_np.max())
exit()
'''
'''
surdemand_window = 24 * 60 // 5 # one day
mean_surdemands_kw = []
for i in range(len(demand_prediction_np) - surdemand_window):
mean_surdemand_kw = (demand_prediction_np[i: i+surdemand_window] - production_prediction_np[i: i+surdemand_window]).mean()
mean_surdemands_kw.append(mean_surdemand_kw)
mean_surdemands_kw = pd.Series(mean_surdemands_kw, prod_cons.index[:len(mean_surdemands_kw)])
mean_surdemands_kw.plot()
plt.title("mean_surdemands_kw")
plt.show()
exit()
'''
consumption_fees_np = prod_cons['Consumption_fees'].to_numpy()
# print("Simulating for", len(demand_np), "time steps. Each step is", step_in_minutes, "minutes.")
soc_series = []
# by convention, we only call end user demand, demand,
# and we only call end user consumption, consumption.
# in our simple model, demand is always satisfied, hence demand=consumption.
# BESS demand is called charge.
consumption_from_solar_series = [] # demand satisfied by solar production
consumption_from_network_series = [] # full demand satisfied by network, also includes consumption_from_network_to_bess.
consumption_from_bess_series = [] # demand satisfied by BESS
consumption_from_network_to_bess_series = [] # network used to charge BESS, also included in consumption_from_network.
# the previous three must sum to demand_series.
discarded_production_series = [] # solar power thrown away
decisions = []
# 1 is not nominal but targeted (healthy) maximum charge.
# we start with an empty battery, but not emptier than what's healthy for the batteries.
# For the sake of simplicity 0 <= soc <= 1
# soc=0 means battery is emptied till it's 20% and soc=1 means battery is charged till 80% of its capacity
# soc = 1 - maximal_depth_of_discharge
# and will use only maximal_depth_of_discharge percent of the real battery capacity
time_interval = step_in_minutes / 60 # amount of time step in hours
for i, (demand, production) in enumerate(zip(demand_np, production_np)):
# these five are modified on the appropriate codepaths:
consumption_from_solar = 0
consumption_from_bess = 0
consumption_from_network = 0
discarded_production = 0
network_used_to_charge = 0
bess_from_solar = 0
unsatisfied_demand = demand
remaining_production = production
# TODO what to call it, demand or consumption?
# 1. sometimes demand is inappropriate, like consumption_from_solar vs demand_from_solar.
# 2. sometimes consumption is inappropriate, like unsatisfied_demand vs unsatisfied_consumption.
# 3. there should not be two of them.
prod_prediction = production_prediction_np[i: i + decider.input_window_size]
cons_prediction = demand_prediction_np[i: i + decider.input_window_size]
consumption_fees = consumption_fees_np[i: i + decider.input_window_size]
decision = decider.decide(prod_prediction, cons_prediction, consumption_fees, battery_model)
decisions.append(decision)
production_used_to_charge = 0
if unsatisfied_demand >= remaining_production:
# all goes to demand, no rate limit between solar and consumer
consumption_from_solar = remaining_production
unsatisfied_demand -= consumption_from_solar
remaining_production = 0
if decision == Decision.DISCHARGE:
# we try to cover the rest from BESS
still_unsatisfied_demand = battery_model.satisfy_demand(unsatisfied_demand)
consumption_from_bess = unsatisfied_demand - still_unsatisfied_demand
unsatisfied_demand = still_unsatisfied_demand
# we cover the rest from network
consumption_from_network = unsatisfied_demand
unsatisfied_demand = 0
else:
# demand fully satisfied by production, no rate limit between solar and consumer
consumption_from_solar = unsatisfied_demand
remaining_production -= unsatisfied_demand
unsatisfied_demand = 0
if remaining_production > 0:
# exploitable production still remains:
discarded_production = battery_model.charge(remaining_production) # remaining_production [kW]
bess_from_solar = remaining_production - discarded_production
if decision == Decision.NETWORK_CHARGE:
# that is some random big number, the actual charge will be
# determined by the combination of the BESS rate limit (battery_model.charge_kW)
# and the BESS capacity
fictional_network_charge_kW = 1000
discarded_network_charge_kW = battery_model.charge(fictional_network_charge_kW)
actual_network_charge_kW = fictional_network_charge_kW - discarded_network_charge_kW
consumption_from_network_to_bess = actual_network_charge_kW # just a renaming.
consumption_from_network += actual_network_charge_kW
else:
consumption_from_network_to_bess = 0
soc_series.append(battery_model.soc)
consumption_from_solar_series.append(consumption_from_solar)
consumption_from_network_series.append(consumption_from_network)
consumption_from_network_to_bess_series.append(consumption_from_network_to_bess)
consumption_from_bess_series.append(consumption_from_bess)
discarded_production_series.append(discarded_production)
assert np.isclose(consumption_from_solar + consumption_from_bess + consumption_from_network, demand + consumption_from_network_to_bess)
assert np.isclose(consumption_from_solar + discarded_production + bess_from_solar, production)
soc_series = np.array(soc_series)
consumption_from_solar_series = np.array(consumption_from_solar_series)
consumption_from_network_series = np.array(consumption_from_network_series)
consumption_from_bess_series = np.array(consumption_from_bess_series)
discarded_production_series = np.array(discarded_production_series)
decisions = np.array(decisions)
consumption_from_network_pandas_series = pd.Series(consumption_from_network_series, index=prod_cons.index)
# TODO badly duplicating functionality
'''
total_charge, consumption_charge_series, demand_charges = supplier.fee(
consumption_from_network_pandas_series,
provide_detail=True)
'''
consumption_charge_series = consumption_fees_np * consumption_from_network_pandas_series.to_numpy()
step_in_hour = consumption_from_network_pandas_series.index.freq.n / 60 # [hour], the length of a time step.
fifteen_minute_demands_in_kwh = consumption_from_network_pandas_series.resample('15min').sum() * step_in_hour
fifteen_minute_surdemands_in_kwh = (fifteen_minute_demands_in_kwh - decider.precalculated_supplier.peak_demand).clip(lower=0)
demand_charges = fifteen_minute_surdemands_in_kwh * decider.precalculated_supplier.surcharge_per_kwh
total_network_fee = consumption_charge_series.sum() + demand_charges.sum()
# print(f"Total network fee {total_network_fee / 10 ** 6} MHUF.")
if DO_VIS:
demand_charges.plot()
plt.title("demand_charges")
plt.show()
results = pd.DataFrame({'soc_series': soc_series, 'consumption_from_solar': consumption_from_solar_series,
'consumption_from_network': consumption_from_network_series,
'consumption_from_bess': consumption_from_bess_series,
'consumption_from_network_to_bess_series': consumption_from_network_to_bess_series,
'discarded_production': discarded_production_series,
'Consumption': prod_cons['Consumption'],
'Production': prod_cons['Production'],
'decisions': decisions
})
results = results.set_index(prod_cons.index)
return results, total_network_fee
def optimizer(decider_class, precalculated_supplier, battery_model, all_data_with_predictions):
number_of_generations = 10
population_size = 20
collected_loss_values = []
def objective_function(params):
# there was an evil numpy view bug that shuffled all results randomly.
params = params.copy()
# print("Simulating with parameters", params)
decider = decider_class(params, precalculated_supplier)
t = time.perf_counter()
results, total_network_fee = simulator(battery_model, all_data_with_predictions, decider)
collected_loss_values.append((params, total_network_fee))
print(params, total_network_fee)
return total_network_fee
def clipper_function(params):
return decider_class.clip_params(params)
init_mean, init_scale = decider_class.initial_params()
best_params = evolution_strategies_optimizer(objective_function, clipper_function,
init_mean=init_mean, init_scale=init_scale,
number_of_generations=number_of_generations,
population_size=population_size)
return best_params, collected_loss_values
def visualize_collected_loss_values(collected_loss_values):
all_params = []
losses = []
for row in collected_loss_values:
params, loss = row
all_params.append(params)
losses.append(loss)
all_params = np.array(all_params)
losses = np.array(losses)
# losses -= losses.min() ; losses /= losses.max()
# plt.scatter(all_params[:, 0], all_params[:, 1], c=range(len(all_params))) ; plt.show()
from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure()
# Add a 3D subplot
ax = fig.add_subplot(111, projection='3d')
# Scatter plot
ax.scatter(all_params[:, 0], all_params[:, 1], losses, c=range(len(all_params)))
plt.show()
def main_param_grid(precalculated_supplier, battery_model, all_data_with_predictions):
N = 11
losses = np.zeros((N, N))
losses[:, :] = np.nan
xlim = [5, 60*24*3 - 1]
ylim = [-50, 50]
for i, x in enumerate(np.linspace(*xlim, N)):
for j, y in enumerate(np.linspace(*ylim, N)):
decider = Decider(np.array([x, y]), precalculated_supplier)
results, total_network_fee = simulator(battery_model, all_data_with_predictions, decider)
# print(x, y, total_network_fee)
losses[j, i] = total_network_fee
# losses[0, -1] = np.nan
# + is list extend.
# the aspect means square pixels
plt.imshow(losses, extent=xlim + ylim, aspect=(xlim[1]-xlim[0])/(ylim[1]-ylim[0]))
plt.xlabel('Surdemand lookahead window size [minutes]')
plt.ylabel('Lookahead surdemand [kW]')
plt.show()
def main_inspect_params(precalculated_supplier, battery_model, all_data_with_predictions):
def inspect_params(params):
decider = Decider(params, precalculated_supplier)
results, total_network_fee = simulator(battery_model, all_data_with_predictions, decider)
print(params, total_network_fee)
print(np.histogram(results['decisions'].to_numpy()))
date_range = ("2021-01-15", "2021-02-01")
date_range = ("2021-08-15", "2021-09-01")
plotly_fig = plotly_visualize_simulation(results, date_range=date_range)
plotly_fig.update_layout(title=dict(text=f"{total_network_fee/1e6:0.2f} MFt"))
plotly_fig.show()
plotly_fig_2 = plotly_visualize_monthly(results)
plotly_fig_2.update_layout(title=dict(text=f"{total_network_fee/1e6:0.2f} MFt"))
plotly_fig_2.show()
inspect_params(np.array([5, 39]))
inspect_params(np.array([1400, 50]))
def main():
np.random.seed(1)
supplier = Supplier(price=100) # Ft/kWh
# nine-to-five increased price.
supplier.set_price_for_daily_interval(9, 17, 150)
# midnight-to-three decreased price, to test network charge.
supplier.set_price_for_daily_interval(0, 3, 20)
# peak_demand dimension is kWh, but it's interpreted as the full consumption
# during a 15 minute timestep.
supplier.set_demand_charge(peak_demand=2.5, surcharge_per_kwh=500) # kWh in a 15 minutes interval, Ft/kWh
solar_parameters = SolarParameters()
met_2021_data, cons_2021_data = read_datasets()
add_production_field(met_2021_data, solar_parameters)
all_data = interpolate_and_join(met_2021_data, cons_2021_data)
time_interval_min = all_data.index.freq.n
time_interval_h = time_interval_min / 60
# for faster testing:
DATASET_TRUNCATED_SIZE = 10000
if DATASET_TRUNCATED_SIZE is not None:
print("Truncating dataset to", DATASET_TRUNCATED_SIZE, "datapoints, that is", DATASET_TRUNCATED_SIZE * time_interval_h / 24, "days")
all_data = all_data.iloc[:DATASET_TRUNCATED_SIZE]
print("Working with", solar_parameters.solar_cell_num, "solar cells, that's a maximum production of", all_data['Production'].max(), "kW.")
all_data_with_predictions = all_data.copy()
add_dummy_predictions(all_data_with_predictions)
precalculated_supplier = precalculate_supplier(supplier, all_data.index)
# we delete the supplier to avoid accidentally calling it instead of precalculated_supplier
supplier = None
all_data_with_predictions['Consumption_fees'] = precalculated_supplier.consumption_fees # [HUF / kWh]
battery_model = BatteryModel(capacity_Ah=600, time_interval_h=time_interval_h)
# now that we've finally set everything up, we can do various things, hence several main_*().
# main_param_grid(precalculated_supplier, battery_model, all_data_with_predictions) ; exit()
# main_inspect_params(precalculated_supplier, battery_model, all_data_with_predictions) ; exit()
decider_class = Decider
# TODO this is super unfortunate:
# Consumption_fees travels via all_data_with_predictions,
# peak_demand and surcharge_per_kwh travels via precalculated_supplier of decider.
best_params, collected_loss_values = optimizer(decider_class, precalculated_supplier, battery_model, all_data_with_predictions)
visualize_collected_loss_values(collected_loss_values)
if __name__ == '__main__':
main()
|