Spaces:
Sleeping
Sleeping
Daniel Varga commited on
Commit ·
e733d30
1
Parent(s): 2f60d4a
kinda simulates
Browse files- v2/architecture.py +96 -104
- v2/data_processing.py +0 -1
- v2/data_processing.py +101 -0
v2/architecture.py
CHANGED
|
@@ -5,42 +5,68 @@ from enum import IntEnum
|
|
| 5 |
|
| 6 |
# it's really just a network pricing model
|
| 7 |
from supplier import Supplier
|
| 8 |
-
from data_processing import read_datasets, add_production_field, interpolate_and_join,
|
| 9 |
|
| 10 |
|
| 11 |
STEPS_PER_HOUR = 12
|
| 12 |
|
| 13 |
|
| 14 |
-
#
|
|
|
|
| 15 |
class BatteryModel:
|
| 16 |
-
def __init__(self,
|
| 17 |
-
self.
|
| 18 |
-
self.
|
| 19 |
-
self.
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
assert 0 <= self.soc <= 1
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
assert 0 <= self.soc <= 1
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
|
| 46 |
class Decision(IntEnum):
|
|
@@ -67,7 +93,6 @@ class Decision(IntEnum):
|
|
| 67 |
# output_window_size is not yet used, always decides one timestep.
|
| 68 |
class Decider:
|
| 69 |
def __init__(self):
|
| 70 |
-
self.parameters = None
|
| 71 |
self.input_window_size = STEPS_PER_HOUR * 24 # day long window.
|
| 72 |
self.output_window_size = STEPS_PER_HOUR # only output decisions for the next hour
|
| 73 |
self.random_seed = 0
|
|
@@ -98,18 +123,25 @@ class DummyPredictor:
|
|
| 98 |
return prediction
|
| 99 |
|
| 100 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
# this function does not mutate its inputs.
|
| 102 |
# it makes a clone of battery_model and modifies that.
|
| 103 |
-
|
| 104 |
-
# place, not some from the parameters dataclass and some from the battery_model.
|
| 105 |
-
def simulator(battery_model, supplier, prod_cons, prod_predictor, cons_predictor, decider, parameters):
|
| 106 |
battery_model = copy.copy(battery_model)
|
| 107 |
|
| 108 |
demand_np = prod_cons['Consumption'].to_numpy()
|
| 109 |
production_np = prod_cons['Production'].to_numpy()
|
| 110 |
assert len(demand_np) == len(production_np)
|
| 111 |
step_in_minutes = prod_cons.index.freq.n
|
| 112 |
-
print(step_in_minutes)
|
| 113 |
assert step_in_minutes == 5
|
| 114 |
|
| 115 |
print("Simulating for", len(demand_np), "time steps. Each step is", step_in_minutes, "minutes.")
|
|
@@ -124,24 +156,18 @@ def simulator(battery_model, supplier, prod_cons, prod_predictor, cons_predictor
|
|
| 124 |
consumption_from_network_to_bess_series = [] # network used to charge BESS, also included in consumption_from_network.
|
| 125 |
# the previous three must sum to demand_series.
|
| 126 |
|
| 127 |
-
# power taken from solar by BESS.
|
| 128 |
-
# note: in inital mock version power is never taken from network by BESS.
|
| 129 |
-
charge_of_bess_series = []
|
| 130 |
discarded_production_series = [] # solar power thrown away
|
| 131 |
|
| 132 |
# 1 is not nominal but targeted (healthy) maximum charge.
|
| 133 |
# we start with an empty battery, but not emptier than what's healthy for the batteries.
|
| 134 |
|
| 135 |
-
# For the sake of simplicity 0 <= soc <=1
|
| 136 |
# soc=0 means battery is emptied till it's 20% and soc=1 means battery is charged till 80% of its capacity
|
| 137 |
# soc = 1 - maximal_depth_of_discharge
|
| 138 |
# and will use only maximal_depth_of_discharge percent of the real battery capacity
|
| 139 |
|
| 140 |
-
max_cap_of_battery = parameters.bess_capacity * parameters.maximal_depth_of_discharge
|
| 141 |
-
|
| 142 |
time_interval = step_in_minutes / 60 # amount of time step in hours
|
| 143 |
for i, (demand, production) in enumerate(zip(demand_np, production_np)):
|
| 144 |
-
cap_of_battery = battery_model.soc * max_cap_of_battery
|
| 145 |
# these five are modified on the appropriate codepaths:
|
| 146 |
consumption_from_solar = 0
|
| 147 |
consumption_from_bess = 0
|
|
@@ -151,10 +177,6 @@ def simulator(battery_model, supplier, prod_cons, prod_predictor, cons_predictor
|
|
| 151 |
|
| 152 |
unsatisfied_demand = demand
|
| 153 |
remaining_production = production
|
| 154 |
-
|
| 155 |
-
assert parameters.bess_present
|
| 156 |
-
is_battery_charged_enough = battery_model.soc > 0
|
| 157 |
-
is_battery_chargeable = battery_model.soc < 1.0
|
| 158 |
|
| 159 |
prod_prediction = prod_predictor.predict(i, decider.input_window_size)
|
| 160 |
cons_prediction = cons_predictor.predict(i, decider.input_window_size)
|
|
@@ -162,78 +184,41 @@ def simulator(battery_model, supplier, prod_cons, prod_predictor, cons_predictor
|
|
| 162 |
|
| 163 |
production_used_to_charge = 0
|
| 164 |
if unsatisfied_demand >= remaining_production:
|
| 165 |
-
# all goes to demand
|
| 166 |
consumption_from_solar = remaining_production
|
| 167 |
unsatisfied_demand -= consumption_from_solar
|
| 168 |
remaining_production = 0
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
unsatisfied_demand = 0
|
| 176 |
-
cap_of_battery -= consumption_from_bess * time_interval
|
| 177 |
-
soc = cap_of_battery / max_cap_of_battery
|
| 178 |
-
else:
|
| 179 |
-
discharge_of_bess = cap_of_battery / time_interval
|
| 180 |
-
discharge = min(parameters.bess_discharge, discharge_of_bess)
|
| 181 |
-
consumption_from_bess = discharge
|
| 182 |
-
unsatisfied_demand -= consumption_from_bess
|
| 183 |
-
cap_of_battery -= consumption_from_bess * time_interval
|
| 184 |
-
soc = cap_of_battery / max_cap_of_battery
|
| 185 |
-
consumption_from_network = unsatisfied_demand
|
| 186 |
-
unsatisfied_demand = 0
|
| 187 |
-
else:
|
| 188 |
-
# we cover the rest from network
|
| 189 |
-
consumption_from_network = unsatisfied_demand
|
| 190 |
-
unsatisfied_demand = 0
|
| 191 |
else:
|
| 192 |
-
# demand fully satisfied by production
|
| 193 |
consumption_from_solar = unsatisfied_demand
|
| 194 |
remaining_production -= unsatisfied_demand
|
| 195 |
unsatisfied_demand = 0
|
| 196 |
if remaining_production > 0:
|
| 197 |
# exploitable production still remains:
|
| 198 |
-
|
| 199 |
-
# we try to specify the BESS modell
|
| 200 |
-
if parameters.bess_charge <= remaining_production :
|
| 201 |
-
energy = parameters.bess_charge * time_interval
|
| 202 |
-
remaining_production = remaining_production - parameters.bess_charge
|
| 203 |
-
production_used_to_charge = parameters.bess_charge
|
| 204 |
-
else :
|
| 205 |
-
production_used_to_charge = remaining_production
|
| 206 |
-
energy = remaining_production * time_interval
|
| 207 |
-
remaining_production = 0
|
| 208 |
-
cap_of_battery += energy
|
| 209 |
-
soc = cap_of_battery / max_cap_of_battery
|
| 210 |
-
|
| 211 |
-
discarded_production = remaining_production
|
| 212 |
|
| 213 |
if decision == Decision.NETWORK_CHARGE:
|
| 214 |
-
#
|
| 215 |
-
#
|
| 216 |
-
#
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
remaining_network_charge = remaining_network_charge - parameters.bess_charge
|
| 225 |
-
network_used_to_charge = parameters.bess_charge
|
| 226 |
-
else :
|
| 227 |
-
network_used_to_charge = remaining_production
|
| 228 |
-
energy = remaining_production * time_interval
|
| 229 |
-
cap_of_battery += energy
|
| 230 |
-
soc = cap_of_battery / max_cap_of_battery
|
| 231 |
-
consumption_from_network += network_used_to_charge
|
| 232 |
|
| 233 |
soc_series.append(battery_model.soc)
|
| 234 |
consumption_from_solar_series.append(consumption_from_solar)
|
| 235 |
consumption_from_network_series.append(consumption_from_network)
|
| 236 |
-
consumption_from_network_to_bess_series.append(
|
| 237 |
consumption_from_bess_series.append(consumption_from_bess)
|
| 238 |
discarded_production_series.append(discarded_production)
|
| 239 |
|
|
@@ -256,23 +241,30 @@ def simulator(battery_model, supplier, prod_cons, prod_predictor, cons_predictor
|
|
| 256 |
|
| 257 |
|
| 258 |
def main():
|
| 259 |
-
battery_model = BatteryModel(capacity=200, efficiency=0.95)
|
| 260 |
|
| 261 |
supplier = Supplier(price=100) # Ft/kWh
|
| 262 |
supplier.set_price_for_interval(9, 17, 150) # nine-to-five increased price.
|
| 263 |
|
| 264 |
-
parameters =
|
| 265 |
|
| 266 |
met_2021_data, cons_2021_data = read_datasets()
|
| 267 |
add_production_field(met_2021_data, parameters)
|
| 268 |
all_2021_data = interpolate_and_join(met_2021_data, cons_2021_data)
|
| 269 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 270 |
prod_predictor = DummyPredictor(pd.Series(all_2021_data['Production']))
|
| 271 |
cons_predictor = DummyPredictor(pd.Series(all_2021_data['Consumption']))
|
| 272 |
|
| 273 |
decider = Decider()
|
| 274 |
|
| 275 |
-
results = simulator(battery_model, supplier, all_2021_data, prod_predictor, cons_predictor, decider
|
|
|
|
|
|
|
|
|
|
|
|
|
| 276 |
|
| 277 |
|
| 278 |
if __name__ == '__main__':
|
|
|
|
| 5 |
|
| 6 |
# it's really just a network pricing model
|
| 7 |
from supplier import Supplier
|
| 8 |
+
from data_processing import read_datasets, add_production_field, interpolate_and_join, SolarParameters
|
| 9 |
|
| 10 |
|
| 11 |
STEPS_PER_HOUR = 12
|
| 12 |
|
| 13 |
|
| 14 |
+
# SOC is normalized so that minimal_depth_of_discharge = 0 and maximal_depth_of_discharge = 1.
|
| 15 |
+
# please set capacity_Ah = nominal_capacity_Ah * (max_dod - min_dod)
|
| 16 |
class BatteryModel:
|
| 17 |
+
def __init__(self, capacity_Ah, time_interval_h):
|
| 18 |
+
self.capacity_Ah = capacity_Ah
|
| 19 |
+
self.efficiency = 0.95 # [dimensionless]
|
| 20 |
+
self.voltage_V = 600
|
| 21 |
+
self.charge_kW = 50
|
| 22 |
+
self.discharge_kW = 60
|
| 23 |
+
self.time_interval_h = time_interval_h
|
| 24 |
+
|
| 25 |
+
# the only non-constant member variable!
|
| 26 |
+
# ratio of self.current_capacity_kWh and self.maximal_capacity_kWh
|
| 27 |
+
self.soc = 0.0
|
| 28 |
+
|
| 29 |
+
@property
|
| 30 |
+
def maximal_capacity_kWh(self):
|
| 31 |
+
return self.capacity_Ah * self.voltage_V / 1000
|
| 32 |
+
|
| 33 |
+
@property
|
| 34 |
+
def current_capacity_kWh(self):
|
| 35 |
+
return self.soc * self.maximal_capacity_kWh
|
| 36 |
+
|
| 37 |
+
def satisfy_demand(self, demand_kW):
|
| 38 |
assert 0 <= self.soc <= 1
|
| 39 |
+
assert demand_kW >= 0
|
| 40 |
+
# rate limited:
|
| 41 |
+
possible_discharge_in_timestep_kWh = self.discharge_kW * self.time_interval_h
|
| 42 |
+
# limited by current capacity:
|
| 43 |
+
possible_discharge_in_timestep_kWh = min((possible_discharge_in_timestep_kWh, self.current_capacity_kWh))
|
| 44 |
+
# limited by need:
|
| 45 |
+
discharge_in_timestep_kWh = min((possible_discharge_in_timestep_kWh, demand_kW * self.time_interval_h))
|
| 46 |
+
consumption_from_bess_kW = discharge_in_timestep_kWh / self.time_interval_h
|
| 47 |
+
unsatisfied_demand_kW = demand_kW - consumption_from_bess_kW
|
| 48 |
+
cap_of_battery_kWh = self.current_capacity_kWh - discharge_in_timestep_kWh
|
| 49 |
+
soc = cap_of_battery_kWh / self.maximal_capacity_kWh
|
| 50 |
+
assert 0 <= soc <= self.soc <= 1
|
| 51 |
+
self.soc = soc
|
| 52 |
+
return unsatisfied_demand_kW
|
| 53 |
+
|
| 54 |
+
def charge(self, charge_kW):
|
| 55 |
assert 0 <= self.soc <= 1
|
| 56 |
+
assert charge_kW >= 0
|
| 57 |
+
# rate limited:
|
| 58 |
+
possible_charge_in_timestep_kWh = self.charge_kW * self.time_interval_h
|
| 59 |
+
# limited by current capacity:
|
| 60 |
+
possible_charge_in_timestep_kWh = min((possible_charge_in_timestep_kWh, self.maximal_capacity_kWh - self.current_capacity_kWh))
|
| 61 |
+
# limited by supply:
|
| 62 |
+
charge_in_timestep_kWh = min((possible_charge_in_timestep_kWh, charge_kW * self.time_interval_h))
|
| 63 |
+
actual_charge_kW = charge_in_timestep_kWh / self.time_interval_h
|
| 64 |
+
unused_charge_kW = charge_kW - actual_charge_kW
|
| 65 |
+
cap_of_battery_kWh = self.current_capacity_kWh + charge_in_timestep_kWh
|
| 66 |
+
soc = cap_of_battery_kWh / self.maximal_capacity_kWh
|
| 67 |
+
assert 0 <= self.soc <= soc <= 1
|
| 68 |
+
self.soc = soc
|
| 69 |
+
return unused_charge_kW
|
| 70 |
|
| 71 |
|
| 72 |
class Decision(IntEnum):
|
|
|
|
| 93 |
# output_window_size is not yet used, always decides one timestep.
|
| 94 |
class Decider:
|
| 95 |
def __init__(self):
|
|
|
|
| 96 |
self.input_window_size = STEPS_PER_HOUR * 24 # day long window.
|
| 97 |
self.output_window_size = STEPS_PER_HOUR # only output decisions for the next hour
|
| 98 |
self.random_seed = 0
|
|
|
|
| 123 |
return prediction
|
| 124 |
|
| 125 |
|
| 126 |
+
'''
|
| 127 |
+
bess_nominal_capacity: float = 330 # [Ah]
|
| 128 |
+
bess_charge: float = 50 # [kW]
|
| 129 |
+
bess_discharge: float = 60 # [kW]
|
| 130 |
+
voltage: float = 600 # [V]
|
| 131 |
+
maximal_depth_of_discharge: float = 0.75 # [dimensionless]
|
| 132 |
+
energy_loss: float = 0.1 # [dimensionless]
|
| 133 |
+
bess_present: bool = True # [boolean]
|
| 134 |
+
'''
|
| 135 |
+
|
| 136 |
# this function does not mutate its inputs.
|
| 137 |
# it makes a clone of battery_model and modifies that.
|
| 138 |
+
def simulator(battery_model, supplier, prod_cons, prod_predictor, cons_predictor, decider):
|
|
|
|
|
|
|
| 139 |
battery_model = copy.copy(battery_model)
|
| 140 |
|
| 141 |
demand_np = prod_cons['Consumption'].to_numpy()
|
| 142 |
production_np = prod_cons['Production'].to_numpy()
|
| 143 |
assert len(demand_np) == len(production_np)
|
| 144 |
step_in_minutes = prod_cons.index.freq.n
|
|
|
|
| 145 |
assert step_in_minutes == 5
|
| 146 |
|
| 147 |
print("Simulating for", len(demand_np), "time steps. Each step is", step_in_minutes, "minutes.")
|
|
|
|
| 156 |
consumption_from_network_to_bess_series = [] # network used to charge BESS, also included in consumption_from_network.
|
| 157 |
# the previous three must sum to demand_series.
|
| 158 |
|
|
|
|
|
|
|
|
|
|
| 159 |
discarded_production_series = [] # solar power thrown away
|
| 160 |
|
| 161 |
# 1 is not nominal but targeted (healthy) maximum charge.
|
| 162 |
# we start with an empty battery, but not emptier than what's healthy for the batteries.
|
| 163 |
|
| 164 |
+
# For the sake of simplicity 0 <= soc <= 1
|
| 165 |
# soc=0 means battery is emptied till it's 20% and soc=1 means battery is charged till 80% of its capacity
|
| 166 |
# soc = 1 - maximal_depth_of_discharge
|
| 167 |
# and will use only maximal_depth_of_discharge percent of the real battery capacity
|
| 168 |
|
|
|
|
|
|
|
| 169 |
time_interval = step_in_minutes / 60 # amount of time step in hours
|
| 170 |
for i, (demand, production) in enumerate(zip(demand_np, production_np)):
|
|
|
|
| 171 |
# these five are modified on the appropriate codepaths:
|
| 172 |
consumption_from_solar = 0
|
| 173 |
consumption_from_bess = 0
|
|
|
|
| 177 |
|
| 178 |
unsatisfied_demand = demand
|
| 179 |
remaining_production = production
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
|
| 181 |
prod_prediction = prod_predictor.predict(i, decider.input_window_size)
|
| 182 |
cons_prediction = cons_predictor.predict(i, decider.input_window_size)
|
|
|
|
| 184 |
|
| 185 |
production_used_to_charge = 0
|
| 186 |
if unsatisfied_demand >= remaining_production:
|
| 187 |
+
# all goes to demand, no rate limit between solar and consumer
|
| 188 |
consumption_from_solar = remaining_production
|
| 189 |
unsatisfied_demand -= consumption_from_solar
|
| 190 |
remaining_production = 0
|
| 191 |
+
if decision == Decision.DISCHARGE:
|
| 192 |
+
# we try to cover the rest from BESS
|
| 193 |
+
unsatisfied_demand = battery_model.satisfy_demand(unsatisfied_demand)
|
| 194 |
+
# we cover the rest from network
|
| 195 |
+
consumption_from_network = unsatisfied_demand
|
| 196 |
+
unsatisfied_demand = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
else:
|
| 198 |
+
# demand fully satisfied by production, no rate limit between solar and consumer
|
| 199 |
consumption_from_solar = unsatisfied_demand
|
| 200 |
remaining_production -= unsatisfied_demand
|
| 201 |
unsatisfied_demand = 0
|
| 202 |
if remaining_production > 0:
|
| 203 |
# exploitable production still remains:
|
| 204 |
+
discarded_production = battery_model.charge(remaining_production) # remaining_production [kW]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
|
| 206 |
if decision == Decision.NETWORK_CHARGE:
|
| 207 |
+
# that is some random big number, the actual charge will be
|
| 208 |
+
# determined by the combination of the BESS rate limit (battery_model.charge_kW)
|
| 209 |
+
# and the BESS capacity
|
| 210 |
+
fictional_network_charge_kW = 1000
|
| 211 |
+
discarded_network_charge_kW = battery_model.charge(fictional_network_charge_kW)
|
| 212 |
+
actual_network_charge_kW = fictional_network_charge_kW - discarded_network_charge_kW
|
| 213 |
+
consumption_from_network_to_bess = actual_network_charge_kW # just a renaming.
|
| 214 |
+
consumption_from_network += actual_network_charge_kW
|
| 215 |
+
else:
|
| 216 |
+
consumption_from_network_to_bess = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
|
| 218 |
soc_series.append(battery_model.soc)
|
| 219 |
consumption_from_solar_series.append(consumption_from_solar)
|
| 220 |
consumption_from_network_series.append(consumption_from_network)
|
| 221 |
+
consumption_from_network_to_bess_series.append(consumption_from_network_to_bess)
|
| 222 |
consumption_from_bess_series.append(consumption_from_bess)
|
| 223 |
discarded_production_series.append(discarded_production)
|
| 224 |
|
|
|
|
| 241 |
|
| 242 |
|
| 243 |
def main():
|
|
|
|
| 244 |
|
| 245 |
supplier = Supplier(price=100) # Ft/kWh
|
| 246 |
supplier.set_price_for_interval(9, 17, 150) # nine-to-five increased price.
|
| 247 |
|
| 248 |
+
parameters = SolarParameters()
|
| 249 |
|
| 250 |
met_2021_data, cons_2021_data = read_datasets()
|
| 251 |
add_production_field(met_2021_data, parameters)
|
| 252 |
all_2021_data = interpolate_and_join(met_2021_data, cons_2021_data)
|
| 253 |
|
| 254 |
+
time_interval_min = all_2021_data.index.freq.n
|
| 255 |
+
time_interval_h = time_interval_min / 60
|
| 256 |
+
battery_model = BatteryModel(capacity_Ah=600, time_interval_h=time_interval_h)
|
| 257 |
+
|
| 258 |
prod_predictor = DummyPredictor(pd.Series(all_2021_data['Production']))
|
| 259 |
cons_predictor = DummyPredictor(pd.Series(all_2021_data['Consumption']))
|
| 260 |
|
| 261 |
decider = Decider()
|
| 262 |
|
| 263 |
+
results = simulator(battery_model, supplier, all_2021_data, prod_predictor, cons_predictor, decider)
|
| 264 |
+
|
| 265 |
+
import matplotlib.pyplot as plt
|
| 266 |
+
results['soc_series'].plot()
|
| 267 |
+
plt.show()
|
| 268 |
|
| 269 |
|
| 270 |
if __name__ == '__main__':
|
v2/data_processing.py
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
../data_processing.py
|
|
|
|
|
|
v2/data_processing.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass
|
| 2 |
+
import numpy as np
|
| 3 |
+
import pandas as pd
|
| 4 |
+
from scipy.interpolate import interp1d
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
PATH_PREFIX = "./"
|
| 8 |
+
|
| 9 |
+
START = f"2021-01-01"
|
| 10 |
+
END = f"2022-01-01"
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def read_datasets(mini=False):
|
| 14 |
+
if mini:
|
| 15 |
+
met_filename = 'PL_44527.2101.csv.gz'
|
| 16 |
+
cons_filename = 'pq_terheles_202101_adatok.tsv'
|
| 17 |
+
else:
|
| 18 |
+
met_filename = 'PL_44527.19-21.csv.gz'
|
| 19 |
+
cons_filename = 'pq_terheles_2021_adatok.tsv'
|
| 20 |
+
|
| 21 |
+
#@title ### Preprocessing meteorologic data
|
| 22 |
+
met_data = pd.read_csv(PATH_PREFIX + met_filename, compression='gzip', sep=';', skipinitialspace=True, na_values='n/a', skiprows=[0, 1, 2, 3, 4])
|
| 23 |
+
met_data['Time'] = met_data['Time'].astype(str)
|
| 24 |
+
date_time = met_data['Time'] = pd.to_datetime(met_data['Time'], format='%Y%m%d%H%M')
|
| 25 |
+
met_data = met_data.set_index('Time')
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
#@title ### Preprocessing consumption data
|
| 29 |
+
cons_data = pd.read_csv(PATH_PREFIX + cons_filename, sep='\t', skipinitialspace=True, na_values='n/a', decimal=',')
|
| 30 |
+
cons_data['Time'] = pd.to_datetime(cons_data['Korrigált időpont'], format='%m/%d/%y %H:%M')
|
| 31 |
+
cons_data = cons_data.set_index('Time')
|
| 32 |
+
cons_data['Consumption'] = cons_data['Hatásos teljesítmény [kW]']
|
| 33 |
+
|
| 34 |
+
# consumption data is at 14 29 44 59 minutes, we move it by 1 minute
|
| 35 |
+
# to sync it with production data:
|
| 36 |
+
cons_data.index = cons_data.index + pd.DateOffset(minutes=1)
|
| 37 |
+
|
| 38 |
+
met_2021_data = met_data[(met_data.index >= START) & (met_data.index < END)]
|
| 39 |
+
cons_2021_data = cons_data[(cons_data.index >= START) & (cons_data.index < END)]
|
| 40 |
+
|
| 41 |
+
return met_2021_data, cons_2021_data
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
# BESS parameters are now in BatteryModel
|
| 45 |
+
@dataclass
|
| 46 |
+
class SolarParameters:
|
| 47 |
+
solar_cell_num: float = 114 # units
|
| 48 |
+
solar_efficiency: float = 0.93 * 0.96 # [dimensionless]
|
| 49 |
+
NOCT: float = 280 # [W]
|
| 50 |
+
NOCT_irradiation: float = 800 # [W/m^2]
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
# mutates met_2021_data
|
| 54 |
+
def add_production_field(met_2021_data, parameters):
|
| 55 |
+
sr = met_2021_data['sr']
|
| 56 |
+
|
| 57 |
+
nop_total = sr * parameters.solar_cell_num * parameters.solar_efficiency * parameters.NOCT / parameters.NOCT_irradiation / 1e3
|
| 58 |
+
nop_total = nop_total.clip(0)
|
| 59 |
+
met_2021_data['Production'] = nop_total
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def interpolate_and_join(met_2021_data, cons_2021_data):
|
| 63 |
+
applicable = 24*60*365 - 15 + 5
|
| 64 |
+
|
| 65 |
+
demand_f = interp1d(range(0, 365*24*60, 15), cons_2021_data['Consumption'])
|
| 66 |
+
#demand_f = interp1d(range(0, 6*24*60, 15), cons_2021_data['Consumption'])
|
| 67 |
+
demand_interp = demand_f(range(0, applicable, 5))
|
| 68 |
+
|
| 69 |
+
production_f = interp1d(range(0, 365*24*60, 10), met_2021_data['Production'])
|
| 70 |
+
#production_f = interp1d(range(0, 6*24*60, 10), met_2021_data['Production'])
|
| 71 |
+
production_interp = production_f(range(0, applicable, 5))
|
| 72 |
+
|
| 73 |
+
all_2021_datetimeindex = pd.date_range(start=START, end=END, freq='5min')[:len(production_interp)]
|
| 74 |
+
|
| 75 |
+
all_2021_data = pd.DataFrame({'Consumption': demand_interp, 'Production': production_interp})
|
| 76 |
+
all_2021_data = all_2021_data.set_index(all_2021_datetimeindex)
|
| 77 |
+
return all_2021_data
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
# TODO build a dataframe instead
|
| 81 |
+
def monthly_analysis(results):
|
| 82 |
+
consumptions = []
|
| 83 |
+
for month in range(1, 13):
|
| 84 |
+
start = f"2021-{month:02}-01"
|
| 85 |
+
end = f"2021-{month+1:02}-01"
|
| 86 |
+
if month == 12:
|
| 87 |
+
end = "2022-01-01"
|
| 88 |
+
results_in_month = results[(results.index >= start) & (results.index < end)]
|
| 89 |
+
|
| 90 |
+
total = results_in_month['Consumption'].sum()
|
| 91 |
+
network = results_in_month['consumption_from_network'].sum()
|
| 92 |
+
solar = results_in_month['consumption_from_solar'].sum()
|
| 93 |
+
bess = results_in_month['consumption_from_bess'].sum()
|
| 94 |
+
consumptions.append([network, solar, bess])
|
| 95 |
+
|
| 96 |
+
consumptions = np.array(consumptions)
|
| 97 |
+
step_in_minutes = results.index.freq.n
|
| 98 |
+
# consumption is given in kW. each tick is step_in_minutes long (5mins, in fact)
|
| 99 |
+
# we get consumption in kWh if we multiply sum by step_in_minutes/60
|
| 100 |
+
consumptions_in_mwh = consumptions * (step_in_minutes / 60) / 1000
|
| 101 |
+
return consumptions_in_mwh
|