mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2025-11-21 12:56:27 +00:00
chore: eosdash improve plan display (#739)
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
* chore: improve plan solution display Add genetic optimization results to general solution provided by EOSdash plan display. Add total results. Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com> * fix: genetic battery and home appliance device simulation Fix genetic solution to make ac_charge, dc_charge, discharge, ev_charge or home appliance start time reflect what the simulation was doing. Sometimes the simulation decided to charge less or to start the appliance at another time and this was not brought back to e.g. ac_charge. Make home appliance simulation activate time window for the next day if it can not be run today. Improve simulation speed. Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com> --------- Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
This commit is contained in:
@@ -69,7 +69,16 @@ class GeneticSimulation(PydanticBaseModel):
|
||||
|
||||
ac_charge_hours: Optional[NDArray[Shape["*"], float]] = Field(default=None, description="TBD")
|
||||
dc_charge_hours: Optional[NDArray[Shape["*"], float]] = Field(default=None, description="TBD")
|
||||
bat_discharge_hours: Optional[NDArray[Shape["*"], float]] = Field(
|
||||
default=None, description="TBD"
|
||||
)
|
||||
ev_charge_hours: Optional[NDArray[Shape["*"], float]] = Field(default=None, description="TBD")
|
||||
ev_discharge_hours: Optional[NDArray[Shape["*"], float]] = Field(
|
||||
default=None, description="TBD"
|
||||
)
|
||||
home_appliance_start_hour: Optional[int] = Field(
|
||||
default=None, description="Home appliance start hour - None denotes no start."
|
||||
)
|
||||
|
||||
def prepare(
|
||||
self,
|
||||
@@ -100,8 +109,11 @@ class GeneticSimulation(PydanticBaseModel):
|
||||
self.home_appliance = home_appliance
|
||||
self.inverter = inverter
|
||||
self.ac_charge_hours = np.full(self.prediction_hours, 0.0)
|
||||
self.dc_charge_hours = np.full(self.prediction_hours, 1.0)
|
||||
self.dc_charge_hours = np.full(self.prediction_hours, 0.0)
|
||||
self.bat_discharge_hours = np.full(self.prediction_hours, 0.0)
|
||||
self.ev_charge_hours = np.full(self.prediction_hours, 0.0)
|
||||
self.ev_discharge_hours = np.full(self.prediction_hours, 0.0)
|
||||
self.home_appliance_start_hour = None
|
||||
"""Prepare simulation runs."""
|
||||
self.load_energy_array = np.array(parameters.gesamtlast, float)
|
||||
self.pv_prediction_wh = np.array(parameters.pv_prognose_wh, float)
|
||||
@@ -114,28 +126,12 @@ class GeneticSimulation(PydanticBaseModel):
|
||||
)
|
||||
)
|
||||
|
||||
def set_akku_discharge_hours(self, ds: np.ndarray) -> None:
|
||||
if self.battery:
|
||||
self.battery.set_discharge_per_hour(ds)
|
||||
|
||||
def set_akku_ac_charge_hours(self, ds: np.ndarray) -> None:
|
||||
self.ac_charge_hours = ds
|
||||
|
||||
def set_akku_dc_charge_hours(self, ds: np.ndarray) -> None:
|
||||
self.dc_charge_hours = ds
|
||||
|
||||
def set_ev_charge_hours(self, ds: np.ndarray) -> None:
|
||||
self.ev_charge_hours = ds
|
||||
|
||||
def set_home_appliance_start(self, ds: int, global_start_hour: int = 0) -> None:
|
||||
if self.home_appliance:
|
||||
self.home_appliance.set_starting_time(ds, global_start_hour=global_start_hour)
|
||||
|
||||
def reset(self) -> None:
|
||||
if self.ev:
|
||||
self.ev.reset()
|
||||
if self.battery:
|
||||
self.battery.reset()
|
||||
self.home_appliance_start_hour = None
|
||||
|
||||
def simulate(self, start_hour: int) -> dict[str, Any]:
|
||||
"""Simulate energy usage and costs for the given start hour.
|
||||
@@ -146,45 +142,66 @@ class GeneticSimulation(PydanticBaseModel):
|
||||
# Remember start hour
|
||||
self.start_hour = start_hour
|
||||
|
||||
# Check for simulation integrity
|
||||
required_attrs = [
|
||||
"load_energy_array",
|
||||
"pv_prediction_wh",
|
||||
"elect_price_hourly",
|
||||
"ev_charge_hours",
|
||||
"ac_charge_hours",
|
||||
"dc_charge_hours",
|
||||
"elect_revenue_per_hour_arr",
|
||||
]
|
||||
missing_data = [
|
||||
attr.replace("_", " ").title() for attr in required_attrs if getattr(self, attr) is None
|
||||
]
|
||||
# Provide fast (3x..5x) local read access (vs. self.xxx) for repetitive read access
|
||||
load_energy_array_fast = self.load_energy_array
|
||||
ev_charge_hours_fast = self.ev_charge_hours
|
||||
ev_discharge_hours_fast = self.ev_discharge_hours
|
||||
ac_charge_hours_fast = self.ac_charge_hours
|
||||
dc_charge_hours_fast = self.dc_charge_hours
|
||||
bat_discharge_hours_fast = self.bat_discharge_hours
|
||||
elect_price_hourly_fast = self.elect_price_hourly
|
||||
elect_revenue_per_hour_arr_fast = self.elect_revenue_per_hour_arr
|
||||
pv_prediction_wh_fast = self.pv_prediction_wh
|
||||
battery_fast = self.battery
|
||||
ev_fast = self.ev
|
||||
home_appliance_fast = self.home_appliance
|
||||
inverter_fast = self.inverter
|
||||
|
||||
if missing_data:
|
||||
logger.error("Mandatory data missing - %s", ", ".join(missing_data))
|
||||
raise ValueError(f"Mandatory data missing: {', '.join(missing_data)}")
|
||||
# Check for simulation integrity (in a way that mypy understands)
|
||||
if (
|
||||
load_energy_array_fast is None
|
||||
or pv_prediction_wh_fast is None
|
||||
or elect_price_hourly_fast is None
|
||||
or ev_charge_hours_fast is None
|
||||
or ac_charge_hours_fast is None
|
||||
or dc_charge_hours_fast is None
|
||||
or elect_revenue_per_hour_arr_fast is None
|
||||
or bat_discharge_hours_fast is None
|
||||
or ev_discharge_hours_fast is None
|
||||
):
|
||||
missing = []
|
||||
if load_energy_array_fast is None:
|
||||
missing.append("Load Energy Array")
|
||||
if pv_prediction_wh_fast is None:
|
||||
missing.append("PV Prediction Wh")
|
||||
if elect_price_hourly_fast is None:
|
||||
missing.append("Electricity Price Hourly")
|
||||
if ev_charge_hours_fast is None:
|
||||
missing.append("EV Charge Hours")
|
||||
if ac_charge_hours_fast is None:
|
||||
missing.append("AC Charge Hours")
|
||||
if dc_charge_hours_fast is None:
|
||||
missing.append("DC Charge Hours")
|
||||
if elect_revenue_per_hour_arr_fast is None:
|
||||
missing.append("Electricity Revenue Per Hour")
|
||||
if bat_discharge_hours_fast is None:
|
||||
missing.append("Battery Discharge Hours")
|
||||
if ev_discharge_hours_fast is None:
|
||||
missing.append("EV Discharge Hours")
|
||||
msg = ", ".join(missing)
|
||||
logger.error("Mandatory data missing - %s", msg)
|
||||
raise ValueError(f"Mandatory data missing: {msg}")
|
||||
|
||||
# Pre-fetch data
|
||||
load_energy_array = np.array(self.load_energy_array)
|
||||
pv_prediction_wh = np.array(self.pv_prediction_wh)
|
||||
elect_price_hourly = np.array(self.elect_price_hourly)
|
||||
ev_charge_hours = np.array(self.ev_charge_hours)
|
||||
ac_charge_hours = np.array(self.ac_charge_hours)
|
||||
dc_charge_hours = np.array(self.dc_charge_hours)
|
||||
elect_revenue_per_hour_arr = np.array(self.elect_revenue_per_hour_arr)
|
||||
|
||||
# Fetch objects
|
||||
battery = self.battery
|
||||
ev = self.ev
|
||||
home_appliance = self.home_appliance
|
||||
inverter = self.inverter
|
||||
|
||||
if not (len(load_energy_array) == len(pv_prediction_wh) == len(elect_price_hourly)):
|
||||
error_msg = f"Array sizes do not match: Load Curve = {len(load_energy_array)}, PV Forecast = {len(pv_prediction_wh)}, Electricity Price = {len(elect_price_hourly)}"
|
||||
if not (
|
||||
len(load_energy_array_fast)
|
||||
== len(pv_prediction_wh_fast)
|
||||
== len(elect_price_hourly_fast)
|
||||
):
|
||||
error_msg = f"Array sizes do not match: Load Curve = {len(load_energy_array_fast)}, PV Forecast = {len(pv_prediction_wh_fast)}, Electricity Price = {len(elect_price_hourly_fast)}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg)
|
||||
|
||||
end_hour = len(load_energy_array)
|
||||
end_hour = len(load_energy_array_fast)
|
||||
total_hours = end_hour - start_hour
|
||||
|
||||
# Pre-allocate arrays for the results, optimized for speed
|
||||
@@ -200,82 +217,104 @@ class GeneticSimulation(PydanticBaseModel):
|
||||
electricity_price_per_hour = np.full((total_hours), np.nan)
|
||||
|
||||
# Set initial state
|
||||
if battery:
|
||||
soc_per_hour[0] = battery.current_soc_percentage()
|
||||
if ev:
|
||||
soc_ev_per_hour[0] = ev.current_soc_percentage()
|
||||
if battery_fast:
|
||||
soc_per_hour[0] = battery_fast.current_soc_percentage()
|
||||
# Fill the charge array of the battery
|
||||
dc_charge_hours_fast[0:start_hour] = 0
|
||||
dc_charge_hours_fast[end_hour:] = 0
|
||||
ac_charge_hours_fast[0:start_hour] = 0
|
||||
dc_charge_hours_fast[end_hour:] = 0
|
||||
battery_fast.charge_array = np.where(
|
||||
ac_charge_hours_fast != 0, ac_charge_hours_fast, dc_charge_hours_fast
|
||||
)
|
||||
# Fill the discharge array of the battery
|
||||
bat_discharge_hours_fast[0:start_hour] = 0
|
||||
bat_discharge_hours_fast[end_hour:] = 0
|
||||
battery_fast.discharge_array = bat_discharge_hours_fast
|
||||
|
||||
if ev_fast:
|
||||
soc_ev_per_hour[0] = ev_fast.current_soc_percentage()
|
||||
# Fill the charge array of the ev
|
||||
ev_charge_hours_fast[0:start_hour] = 0
|
||||
ev_charge_hours_fast[end_hour:] = 0
|
||||
ev_fast.charge_array = ev_charge_hours_fast
|
||||
# Fill the discharge array of the ev
|
||||
ev_discharge_hours_fast[0:start_hour] = 0
|
||||
ev_discharge_hours_fast[end_hour:] = 0
|
||||
ev_fast.discharge_array = ev_discharge_hours_fast
|
||||
|
||||
if home_appliance_fast and self.home_appliance_start_hour:
|
||||
home_appliance_enabled = True
|
||||
self.home_appliance_start_hour = home_appliance_fast.set_starting_time(
|
||||
self.home_appliance_start_hour, start_hour
|
||||
)
|
||||
else:
|
||||
home_appliance_enabled = False
|
||||
|
||||
for hour in range(start_hour, end_hour):
|
||||
hour_idx = hour - start_hour
|
||||
|
||||
# save begin states
|
||||
if battery:
|
||||
soc_per_hour[hour_idx] = battery.current_soc_percentage()
|
||||
if ev:
|
||||
soc_ev_per_hour[hour_idx] = ev.current_soc_percentage()
|
||||
|
||||
# Accumulate loads and PV generation
|
||||
consumption = load_energy_array[hour]
|
||||
consumption = load_energy_array_fast[hour]
|
||||
losses_wh_per_hour[hour_idx] = 0.0
|
||||
|
||||
# Home appliances
|
||||
if home_appliance:
|
||||
ha_load = home_appliance.get_load_for_hour(hour)
|
||||
if home_appliance_enabled:
|
||||
ha_load = home_appliance_fast.get_load_for_hour(hour) # type: ignore[union-attr]
|
||||
consumption += ha_load
|
||||
home_appliance_wh_per_hour[hour_idx] = ha_load
|
||||
|
||||
# E-Auto handling
|
||||
if ev and ev_charge_hours[hour] > 0:
|
||||
loaded_energy_ev, verluste_eauto = ev.charge_energy(
|
||||
None, hour, relative_power=ev_charge_hours[hour]
|
||||
)
|
||||
consumption += loaded_energy_ev
|
||||
losses_wh_per_hour[hour_idx] += verluste_eauto
|
||||
if ev_fast:
|
||||
soc_ev_per_hour[hour_idx] = ev_fast.current_soc_percentage() # save begin state
|
||||
if ev_charge_hours_fast[hour] > 0:
|
||||
loaded_energy_ev, verluste_eauto = ev_fast.charge_energy(
|
||||
wh=None, hour=hour, charge_factor=ev_charge_hours_fast[hour]
|
||||
)
|
||||
consumption += loaded_energy_ev
|
||||
losses_wh_per_hour[hour_idx] += verluste_eauto
|
||||
|
||||
# Process inverter logic
|
||||
energy_feedin_grid_actual = energy_consumption_grid_actual = losses = eigenverbrauch = (
|
||||
0.0
|
||||
)
|
||||
|
||||
hour_ac_charge = ac_charge_hours[hour]
|
||||
hour_dc_charge = dc_charge_hours[hour]
|
||||
hourly_electricity_price = elect_price_hourly[hour]
|
||||
hourly_energy_revenue = elect_revenue_per_hour_arr[hour]
|
||||
|
||||
if battery:
|
||||
battery.set_charge_allowed_for_hour(hour_dc_charge, hour)
|
||||
|
||||
if inverter:
|
||||
energy_produced = pv_prediction_wh[hour]
|
||||
if inverter_fast:
|
||||
energy_produced = pv_prediction_wh_fast[hour]
|
||||
(
|
||||
energy_feedin_grid_actual,
|
||||
energy_consumption_grid_actual,
|
||||
losses,
|
||||
eigenverbrauch,
|
||||
) = inverter.process_energy(energy_produced, consumption, hour)
|
||||
) = inverter_fast.process_energy(energy_produced, consumption, hour)
|
||||
|
||||
# AC PV Battery Charge
|
||||
if battery and hour_ac_charge > 0.0:
|
||||
battery.set_charge_allowed_for_hour(1, hour)
|
||||
battery_charged_energy_actual, battery_losses_actual = battery.charge_energy(
|
||||
None, hour, relative_power=hour_ac_charge
|
||||
)
|
||||
if battery_fast:
|
||||
soc_per_hour[hour_idx] = battery_fast.current_soc_percentage() # save begin state
|
||||
hour_ac_charge = ac_charge_hours_fast[hour]
|
||||
if hour_ac_charge > 0.0:
|
||||
battery_charged_energy_actual, battery_losses_actual = (
|
||||
battery_fast.charge_energy(None, hour, charge_factor=hour_ac_charge)
|
||||
)
|
||||
|
||||
total_battery_energy = battery_charged_energy_actual + battery_losses_actual
|
||||
consumption += total_battery_energy
|
||||
energy_consumption_grid_actual += total_battery_energy
|
||||
losses_wh_per_hour[hour_idx] += battery_losses_actual
|
||||
total_battery_energy = battery_charged_energy_actual + battery_losses_actual
|
||||
consumption += total_battery_energy
|
||||
energy_consumption_grid_actual += total_battery_energy
|
||||
losses_wh_per_hour[hour_idx] += battery_losses_actual
|
||||
|
||||
# Update hourly arrays
|
||||
feedin_energy_per_hour[hour_idx] = energy_feedin_grid_actual
|
||||
consumption_energy_per_hour[hour_idx] = energy_consumption_grid_actual
|
||||
losses_wh_per_hour[hour_idx] += losses
|
||||
loads_energy_per_hour[hour_idx] = consumption
|
||||
hourly_electricity_price = elect_price_hourly_fast[hour]
|
||||
electricity_price_per_hour[hour_idx] = hourly_electricity_price
|
||||
|
||||
# Financial calculations
|
||||
costs_per_hour[hour_idx] = energy_consumption_grid_actual * hourly_electricity_price
|
||||
revenue_per_hour[hour_idx] = energy_feedin_grid_actual * hourly_energy_revenue
|
||||
revenue_per_hour[hour_idx] = (
|
||||
energy_feedin_grid_actual * elect_revenue_per_hour_arr_fast[hour]
|
||||
)
|
||||
|
||||
total_cost = np.nansum(costs_per_hour)
|
||||
total_losses = np.nansum(losses_wh_per_hour)
|
||||
@@ -289,7 +328,7 @@ class GeneticSimulation(PydanticBaseModel):
|
||||
"Kosten_Euro_pro_Stunde": costs_per_hour,
|
||||
"akku_soc_pro_stunde": soc_per_hour,
|
||||
"Einnahmen_Euro_pro_Stunde": revenue_per_hour,
|
||||
"Gesamtbilanz_Euro": total_cost - total_revenue,
|
||||
"Gesamtbilanz_Euro": total_cost - total_revenue, # Fitness score ("FitnessMin")
|
||||
"EAuto_SoC_pro_Stunde": soc_ev_per_hour,
|
||||
"Gesamteinnahmen_Euro": total_revenue,
|
||||
"Gesamtkosten_Euro": total_cost,
|
||||
@@ -574,27 +613,33 @@ class GeneticOptimization(OptimizationBase):
|
||||
discharge_hours_bin, eautocharge_hours_index, washingstart_int = self.split_individual(
|
||||
individual
|
||||
)
|
||||
|
||||
if self.opti_param.get("home_appliance", 0) > 0 and washingstart_int:
|
||||
self.simulation.set_home_appliance_start(
|
||||
washingstart_int, global_start_hour=self.ems.start_datetime.hour
|
||||
)
|
||||
# Set start hour for appliance
|
||||
self.simulation.home_appliance_start_hour = washingstart_int
|
||||
|
||||
ac, dc, discharge = self.decode_charge_discharge(discharge_hours_bin)
|
||||
ac_charge_hours, dc_charge_hours, discharge = self.decode_charge_discharge(
|
||||
discharge_hours_bin
|
||||
)
|
||||
|
||||
self.simulation.set_akku_discharge_hours(discharge)
|
||||
self.simulation.bat_discharge_hours = discharge
|
||||
# Set DC charge hours only if DC optimization is enabled
|
||||
if self.optimize_dc_charge:
|
||||
self.simulation.set_akku_dc_charge_hours(dc)
|
||||
self.simulation.set_akku_ac_charge_hours(ac)
|
||||
self.simulation.dc_charge_hours = dc_charge_hours
|
||||
else:
|
||||
self.simulation.dc_charge_hours = np.full(self.config.prediction.hours, 1)
|
||||
self.simulation.ac_charge_hours = ac_charge_hours
|
||||
|
||||
if eautocharge_hours_index is not None:
|
||||
eautocharge_hours_float = np.array(
|
||||
[self.ev_possible_charge_values[i] for i in eautocharge_hours_index],
|
||||
float,
|
||||
)
|
||||
self.simulation.set_ev_charge_hours(eautocharge_hours_float)
|
||||
# discharge is set to 0 by default
|
||||
self.simulation.ev_charge_hours = eautocharge_hours_float
|
||||
else:
|
||||
self.simulation.set_ev_charge_hours(np.full(self.config.prediction.hours, 0))
|
||||
# discharge is set to 0 by default
|
||||
self.simulation.ev_charge_hours = np.full(self.config.prediction.hours, 0)
|
||||
|
||||
# Do the simulation and return result.
|
||||
return self.simulation.simulate(self.ems.start_datetime.hour)
|
||||
@@ -606,21 +651,57 @@ class GeneticOptimization(OptimizationBase):
|
||||
start_hour: int,
|
||||
worst_case: bool,
|
||||
) -> tuple[float]:
|
||||
"""Evaluate the fitness of an individual solution based on the simulation results."""
|
||||
"""Evaluate the fitness score of a single individual in the DEAP genetic algorithm.
|
||||
|
||||
This method runs a simulation based on the provided individual genome and
|
||||
optimization parameters. The resulting performance is converted into a
|
||||
fitness score compatible with DEAP (i.e., returned as a 1-tuple).
|
||||
|
||||
Args:
|
||||
individual (list[int]):
|
||||
The genome representing one candidate solution.
|
||||
parameters (GeneticOptimizationParameters):
|
||||
Optimization parameters that influence simulation behavior,
|
||||
constraints, and scoring logic.
|
||||
start_hour (int):
|
||||
The simulation start hour (0–23 or domain-specific).
|
||||
Used to initialize time-based scheduling or constraints.
|
||||
worst_case (bool):
|
||||
If True, evaluates the solution under worst-case assumptions
|
||||
(e.g., pessimistic forecasts or boundary conditions).
|
||||
If False, uses nominal assumptions.
|
||||
|
||||
Returns:
|
||||
tuple[float]:
|
||||
A single-element tuple containing the computed fitness score.
|
||||
Lower score is better: "FitnessMin".
|
||||
|
||||
Raises:
|
||||
ValueError: If input arguments are invalid or the individual structure
|
||||
is not compatible with the simulation.
|
||||
RuntimeError: If the simulation fails or cannot produce results.
|
||||
|
||||
Notes:
|
||||
The resulting score should match DEAP's expected format: a tuple, even
|
||||
if only a single scalar fitness value is returned.
|
||||
"""
|
||||
try:
|
||||
o = self.evaluate_inner(individual)
|
||||
simulation_result = self.evaluate_inner(individual)
|
||||
except Exception as e:
|
||||
return (100000.0,) # Return a high penalty in case of an exception
|
||||
# Return bad fitness score ("FitnessMin") in case of an exception
|
||||
return (100000.0,)
|
||||
|
||||
gesamtbilanz = o["Gesamtbilanz_Euro"] * (-1.0 if worst_case else 1.0)
|
||||
|
||||
discharge_hours_bin, eautocharge_hours_index, washingstart_int = self.split_individual(
|
||||
individual
|
||||
)
|
||||
gesamtbilanz = simulation_result["Gesamtbilanz_Euro"] * (-1.0 if worst_case else 1.0)
|
||||
|
||||
# EV 100% & charge not allowed
|
||||
if self.optimize_ev:
|
||||
eauto_soc_per_hour = np.array(o.get("EAuto_SoC_pro_Stunde", [])) # Beispielkey
|
||||
discharge_hours_bin, eautocharge_hours_index, washingstart_int = self.split_individual(
|
||||
individual
|
||||
)
|
||||
|
||||
eauto_soc_per_hour = np.array(
|
||||
simulation_result.get("EAuto_SoC_pro_Stunde", [])
|
||||
) # Beispielkey
|
||||
|
||||
if eauto_soc_per_hour is None or eautocharge_hours_index is None:
|
||||
raise ValueError("eauto_soc_per_hour or eautocharge_hours_index is None")
|
||||
@@ -686,8 +767,8 @@ class GeneticOptimization(OptimizationBase):
|
||||
|
||||
# More metrics
|
||||
individual.extra_data = ( # type: ignore[attr-defined]
|
||||
o["Gesamtbilanz_Euro"],
|
||||
o["Gesamt_Verluste"],
|
||||
simulation_result["Gesamtbilanz_Euro"],
|
||||
simulation_result["Gesamt_Verluste"],
|
||||
parameters.eauto.min_soc_percentage - self.simulation.ev.current_soc_percentage()
|
||||
if parameters.eauto and self.simulation.ev
|
||||
else 0,
|
||||
@@ -701,7 +782,7 @@ class GeneticOptimization(OptimizationBase):
|
||||
)
|
||||
gesamtbilanz += -restwert_akku
|
||||
|
||||
if self.optimize_ev:
|
||||
if self.optimize_ev and parameters.eauto and self.simulation.ev:
|
||||
try:
|
||||
penalty = self.config.optimization.genetic.penalties["ev_soc_miss"]
|
||||
except:
|
||||
@@ -710,16 +791,14 @@ class GeneticOptimization(OptimizationBase):
|
||||
logger.error(
|
||||
"Penalty function parameter `ev_soc_miss` not configured, using {}.", penalty
|
||||
)
|
||||
gesamtbilanz += max(
|
||||
0,
|
||||
(
|
||||
parameters.eauto.min_soc_percentage
|
||||
- self.simulation.ev.current_soc_percentage()
|
||||
if parameters.eauto and self.simulation.ev
|
||||
else 0
|
||||
ev_soc_percentage = self.simulation.ev.current_soc_percentage()
|
||||
if (
|
||||
ev_soc_percentage < parameters.eauto.min_soc_percentage
|
||||
or ev_soc_percentage > parameters.eauto.max_soc_percentage
|
||||
):
|
||||
gesamtbilanz += (
|
||||
abs(parameters.eauto.min_soc_percentage - ev_soc_percentage) * penalty
|
||||
)
|
||||
* penalty,
|
||||
)
|
||||
|
||||
return (gesamtbilanz,)
|
||||
|
||||
@@ -825,7 +904,7 @@ class GeneticOptimization(OptimizationBase):
|
||||
parameters.pv_akku,
|
||||
prediction_hours=self.config.prediction.hours,
|
||||
)
|
||||
akku.set_charge_per_hour(np.full(self.config.prediction.hours, 1))
|
||||
akku.set_charge_per_hour(np.full(self.config.prediction.hours, 0))
|
||||
|
||||
eauto: Optional[Battery] = None
|
||||
if parameters.eauto:
|
||||
@@ -917,7 +996,7 @@ class GeneticOptimization(OptimizationBase):
|
||||
)
|
||||
# home appliance may have choosen a different appliance start hour
|
||||
if self.simulation.home_appliance:
|
||||
washingstart_int = self.simulation.home_appliance.get_appliance_start()
|
||||
washingstart_int = self.simulation.home_appliance_start_hour
|
||||
|
||||
eautocharge_hours_float = (
|
||||
[self.ev_possible_charge_values[i] for i in eautocharge_hours_index]
|
||||
@@ -925,12 +1004,28 @@ class GeneticOptimization(OptimizationBase):
|
||||
else None
|
||||
)
|
||||
|
||||
ac_charge, dc_charge, discharge = self.decode_charge_discharge(discharge_hours_bin)
|
||||
# Simulation may have changed something, use simulation values
|
||||
ac_charge_hours = self.simulation.ac_charge_hours
|
||||
if ac_charge_hours is None:
|
||||
ac_charge_hours = []
|
||||
else:
|
||||
ac_charge_hours = ac_charge_hours.tolist()
|
||||
dc_charge_hours = self.simulation.dc_charge_hours
|
||||
if dc_charge_hours is None:
|
||||
dc_charge_hours = []
|
||||
else:
|
||||
dc_charge_hours = dc_charge_hours.tolist()
|
||||
discharge = self.simulation.bat_discharge_hours
|
||||
if discharge is None:
|
||||
discharge = []
|
||||
else:
|
||||
discharge = discharge.tolist()
|
||||
|
||||
# Visualize the results
|
||||
visualize = {
|
||||
"ac_charge": ac_charge.tolist(),
|
||||
"dc_charge": dc_charge.tolist(),
|
||||
"discharge_allowed": discharge.tolist(),
|
||||
"ac_charge": ac_charge_hours,
|
||||
"dc_charge": dc_charge_hours,
|
||||
"discharge_allowed": discharge,
|
||||
"eautocharge_hours_float": eautocharge_hours_float,
|
||||
"result": simulation_result,
|
||||
"eauto_obj": self.simulation.ev.to_dict() if self.simulation.ev else None,
|
||||
@@ -946,8 +1041,8 @@ class GeneticOptimization(OptimizationBase):
|
||||
|
||||
return GeneticSolution(
|
||||
**{
|
||||
"ac_charge": ac_charge,
|
||||
"dc_charge": dc_charge,
|
||||
"ac_charge": ac_charge_hours,
|
||||
"dc_charge": dc_charge_hours,
|
||||
"discharge_allowed": discharge,
|
||||
"eautocharge_hours_float": eautocharge_hours_float,
|
||||
"result": GeneticSimulationResult(**simulation_result),
|
||||
|
||||
@@ -74,6 +74,11 @@ class BaseBatteryParameters(DeviceParameters):
|
||||
le=100,
|
||||
description="An integer representing the maximum state of charge (SOC) of the battery in percentage.",
|
||||
)
|
||||
charge_rates: Optional[list[float]] = Field(
|
||||
default=None,
|
||||
description="Charge rates as factor of maximum charging power [0.00 ... 1.00]. None denotes all charge rates are available.",
|
||||
examples=[[0.0, 0.25, 0.5, 0.75, 1.0], None],
|
||||
)
|
||||
|
||||
|
||||
class SolarPanelBatteryParameters(BaseBatteryParameters):
|
||||
@@ -90,11 +95,6 @@ class ElectricVehicleParameters(BaseBatteryParameters):
|
||||
initial_soc_percentage: int = initial_soc_percentage_field(
|
||||
"An integer representing the current state of charge (SOC) of the battery in percentage."
|
||||
)
|
||||
charge_rates: Optional[list[float]] = Field(
|
||||
default=None,
|
||||
description="Charge rates as factor of maximum charging power [0.00 ... 1.00]. None denotes all charge rates are available.",
|
||||
examples=[[0.0, 0.25, 0.5, 0.75, 1.0], None],
|
||||
)
|
||||
|
||||
|
||||
class HomeApplianceParameters(DeviceParameters):
|
||||
|
||||
@@ -457,7 +457,7 @@ class GeneticOptimizationParameters(
|
||||
{
|
||||
"device_id": "ev11",
|
||||
"capacity_wh": 50000,
|
||||
"charge_rates": [0.0, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0],
|
||||
"charge_rates": [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
|
||||
"min_soc_percentage": 70,
|
||||
}
|
||||
]
|
||||
@@ -483,7 +483,7 @@ class GeneticOptimizationParameters(
|
||||
{
|
||||
"device_id": "ev12",
|
||||
"capacity_wh": 50000,
|
||||
"charge_rates": [0.0, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0],
|
||||
"charge_rates": [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
|
||||
"min_soc_percentage": 70,
|
||||
}
|
||||
]
|
||||
|
||||
@@ -6,7 +6,9 @@ import pandas as pd
|
||||
from loguru import logger
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
from akkudoktoreos.config.config import get_config
|
||||
from akkudoktoreos.core.coreabc import (
|
||||
ConfigMixin,
|
||||
)
|
||||
from akkudoktoreos.core.emplan import (
|
||||
DDBCInstruction,
|
||||
EnergyManagementPlan,
|
||||
@@ -109,7 +111,7 @@ class GeneticSimulationResult(GeneticParametersBaseModel):
|
||||
return NumpyEncoder.convert_numpy(field)[0]
|
||||
|
||||
|
||||
class GeneticSolution(GeneticParametersBaseModel):
|
||||
class GeneticSolution(ConfigMixin, GeneticParametersBaseModel):
|
||||
"""**Note**: The first value of "Last_Wh_per_hour", "Netzeinspeisung_Wh_per_hour", and "Netzbezug_Wh_per_hour", will be set to null in the JSON output and represented as NaN or None in the corresponding classes' data returns. This approach is adopted to ensure that the current hour's processing remains unchanged."""
|
||||
|
||||
ac_charge: list[float] = Field(
|
||||
@@ -228,18 +230,20 @@ class GeneticSolution(GeneticParametersBaseModel):
|
||||
"""
|
||||
from akkudoktoreos.core.ems import get_ems
|
||||
|
||||
config = get_config()
|
||||
start_datetime = get_ems().start_datetime
|
||||
start_day_hour = start_datetime.in_timezone(self.config.general.timezone).hour
|
||||
interval_hours = 1
|
||||
power_to_energy_per_interval_factor = 1.0
|
||||
|
||||
# --- Create index based on list length and interval ---
|
||||
n_points = len(self.result.Kosten_Euro_pro_Stunde)
|
||||
# Ensure we only use the minimum of results and commands if differing
|
||||
periods = min(len(self.result.Kosten_Euro_pro_Stunde), len(self.ac_charge) - start_day_hour)
|
||||
time_index = pd.date_range(
|
||||
start=start_datetime,
|
||||
periods=n_points,
|
||||
periods=periods,
|
||||
freq=f"{interval_hours}h",
|
||||
)
|
||||
n_points = len(time_index)
|
||||
end_datetime = start_datetime.add(hours=n_points)
|
||||
|
||||
# Fill solution into dataframe with correct column names
|
||||
@@ -256,26 +260,42 @@ class GeneticSolution(GeneticParametersBaseModel):
|
||||
solution = pd.DataFrame(
|
||||
{
|
||||
"date_time": time_index,
|
||||
"load_energy_wh": self.result.Last_Wh_pro_Stunde,
|
||||
"grid_feedin_energy_wh": self.result.Netzeinspeisung_Wh_pro_Stunde,
|
||||
"grid_consumption_energy_wh": self.result.Netzbezug_Wh_pro_Stunde,
|
||||
"elec_price_prediction_amt_kwh": [v * 1000 for v in self.result.Electricity_price],
|
||||
"costs_amt": self.result.Kosten_Euro_pro_Stunde,
|
||||
"revenue_amt": self.result.Einnahmen_Euro_pro_Stunde,
|
||||
"losses_energy_wh": self.result.Verluste_Pro_Stunde,
|
||||
# result starts at start_day_hour
|
||||
"load_energy_wh": self.result.Last_Wh_pro_Stunde[:n_points],
|
||||
"grid_feedin_energy_wh": self.result.Netzeinspeisung_Wh_pro_Stunde[:n_points],
|
||||
"grid_consumption_energy_wh": self.result.Netzbezug_Wh_pro_Stunde[:n_points],
|
||||
"costs_amt": self.result.Kosten_Euro_pro_Stunde[:n_points],
|
||||
"revenue_amt": self.result.Einnahmen_Euro_pro_Stunde[:n_points],
|
||||
"losses_energy_wh": self.result.Verluste_Pro_Stunde[:n_points],
|
||||
},
|
||||
index=time_index,
|
||||
)
|
||||
|
||||
# Add battery data
|
||||
solution["battery1_soc_factor"] = [v / 100 for v in self.result.akku_soc_pro_stunde]
|
||||
operation: dict[str, list[float]] = {}
|
||||
for hour, rate in enumerate(self.ac_charge):
|
||||
if hour >= n_points:
|
||||
solution["battery1_soc_factor"] = [
|
||||
v / 100
|
||||
for v in self.result.akku_soc_pro_stunde[:n_points] # result starts at start_day_hour
|
||||
]
|
||||
operation: dict[str, list[float]] = {
|
||||
"genetic_ac_charge_factor": [],
|
||||
"genetic_dc_charge_factor": [],
|
||||
"genetic_discharge_allowed_factor": [],
|
||||
}
|
||||
# ac_charge, dc_charge, discharge_allowed start at hour 0 of start day
|
||||
for hour_idx, rate in enumerate(self.ac_charge):
|
||||
if hour_idx < start_day_hour:
|
||||
continue
|
||||
if hour_idx >= start_day_hour + n_points:
|
||||
break
|
||||
ac_charge_hour = self.ac_charge[hour_idx]
|
||||
dc_charge_hour = self.dc_charge[hour_idx]
|
||||
discharge_allowed_hour = bool(self.discharge_allowed[hour_idx])
|
||||
operation_mode, operation_mode_factor = self._battery_operation_from_solution(
|
||||
self.ac_charge[hour], self.dc_charge[hour], bool(self.discharge_allowed[hour])
|
||||
ac_charge_hour, dc_charge_hour, discharge_allowed_hour
|
||||
)
|
||||
operation["genetic_ac_charge_factor"].append(ac_charge_hour)
|
||||
operation["genetic_dc_charge_factor"].append(dc_charge_hour)
|
||||
operation["genetic_discharge_allowed_factor"].append(discharge_allowed_hour)
|
||||
for mode in BatteryOperationMode:
|
||||
mode_key = f"battery1_{mode.lower()}_op_mode"
|
||||
factor_key = f"battery1_{mode.lower()}_op_factor"
|
||||
@@ -289,15 +309,22 @@ class GeneticSolution(GeneticParametersBaseModel):
|
||||
operation[mode_key].append(0.0)
|
||||
operation[factor_key].append(0.0)
|
||||
for key in operation.keys():
|
||||
if len(operation[key]) != n_points:
|
||||
error_msg = f"instruction {key} has invalid length {len(operation[key])} - expected {n_points}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg)
|
||||
solution[key] = operation[key]
|
||||
|
||||
# Add EV battery solution
|
||||
# eautocharge_hours_float start at hour 0 of start day
|
||||
# result.EAuto_SoC_pro_Stunde start at start_datetime.hour
|
||||
if self.eauto_obj:
|
||||
if self.eautocharge_hours_float is None:
|
||||
# Electric vehicle is full enough. No load times.
|
||||
solution[f"{self.eauto_obj.device_id}_soc_factor"] = [
|
||||
self.eauto_obj.initial_soc_percentage / 100.0
|
||||
] * n_points
|
||||
solution["genetic_ev_charge_factor"] = [0.0] * n_points
|
||||
# operation modes
|
||||
operation_mode = BatteryOperationMode.IDLE
|
||||
for mode in BatteryOperationMode:
|
||||
@@ -311,12 +338,17 @@ class GeneticSolution(GeneticParametersBaseModel):
|
||||
solution[factor_key] = [0.0] * n_points
|
||||
else:
|
||||
solution[f"{self.eauto_obj.device_id}_soc_factor"] = [
|
||||
v / 100 for v in self.result.EAuto_SoC_pro_Stunde
|
||||
v / 100 for v in self.result.EAuto_SoC_pro_Stunde[:n_points]
|
||||
]
|
||||
operation = {}
|
||||
for hour, rate in enumerate(self.eautocharge_hours_float):
|
||||
if hour >= n_points:
|
||||
operation = {
|
||||
"genetic_ev_charge_factor": [],
|
||||
}
|
||||
for hour_idx, rate in enumerate(self.eautocharge_hours_float):
|
||||
if hour_idx < start_day_hour:
|
||||
continue
|
||||
if hour_idx >= start_day_hour + n_points:
|
||||
break
|
||||
operation["genetic_ev_charge_factor"].append(rate)
|
||||
operation_mode, operation_mode_factor = self._battery_operation_from_solution(
|
||||
rate, 0.0, False
|
||||
)
|
||||
@@ -333,11 +365,16 @@ class GeneticSolution(GeneticParametersBaseModel):
|
||||
operation[mode_key].append(0.0)
|
||||
operation[factor_key].append(0.0)
|
||||
for key in operation.keys():
|
||||
if len(operation[key]) != n_points:
|
||||
error_msg = f"instruction {key} has invalid length {len(operation[key])} - expected {n_points}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg)
|
||||
solution[key] = operation[key]
|
||||
|
||||
# Add home appliance data
|
||||
if self.washingstart:
|
||||
solution["homeappliance1_energy_wh"] = self.result.Home_appliance_wh_per_hour
|
||||
# result starts at start_day_hour
|
||||
solution["homeappliance1_energy_wh"] = self.result.Home_appliance_wh_per_hour[:n_points]
|
||||
|
||||
# Fill prediction into dataframe with correct column names
|
||||
# - pvforecast_ac_energy_wh_energy_wh: PV energy prediction (positive) in wh
|
||||
@@ -445,10 +482,13 @@ class GeneticSolution(GeneticParametersBaseModel):
|
||||
generated_at=to_datetime(),
|
||||
comment="Optimization solution derived from GeneticSolution.",
|
||||
valid_from=start_datetime,
|
||||
valid_until=start_datetime.add(hours=config.optimization.horizon_hours),
|
||||
valid_until=start_datetime.add(hours=self.config.optimization.horizon_hours),
|
||||
total_losses_energy_wh=self.result.Gesamt_Verluste,
|
||||
total_revenues_amt=self.result.Gesamteinnahmen_Euro,
|
||||
total_costs_amt=self.result.Gesamtkosten_Euro,
|
||||
fitness_score={
|
||||
self.result.Gesamtkosten_Euro,
|
||||
},
|
||||
prediction=PydanticDateTimeDataFrame.from_dataframe(prediction),
|
||||
solution=PydanticDateTimeDataFrame.from_dataframe(solution),
|
||||
)
|
||||
@@ -460,6 +500,7 @@ class GeneticSolution(GeneticParametersBaseModel):
|
||||
from akkudoktoreos.core.ems import get_ems
|
||||
|
||||
start_datetime = get_ems().start_datetime
|
||||
start_day_hour = start_datetime.in_timezone(self.config.general.timezone).hour
|
||||
plan = EnergyManagementPlan(
|
||||
id=f"plan-genetic@{to_datetime(as_string=True)}",
|
||||
generated_at=to_datetime(),
|
||||
@@ -471,10 +512,15 @@ class GeneticSolution(GeneticParametersBaseModel):
|
||||
last_operation_mode: Optional[str] = None
|
||||
last_operation_mode_factor: Optional[float] = None
|
||||
resource_id = "battery1"
|
||||
logger.debug("BAT: {} - {}", resource_id, self.ac_charge)
|
||||
for hour, rate in enumerate(self.ac_charge):
|
||||
# ac_charge, dc_charge, discharge_allowed start at hour 0 of start day
|
||||
logger.debug("BAT: {} - {}", resource_id, self.ac_charge[start_day_hour:])
|
||||
for hour_idx, rate in enumerate(self.ac_charge):
|
||||
if hour_idx < start_day_hour:
|
||||
continue
|
||||
operation_mode, operation_mode_factor = self._battery_operation_from_solution(
|
||||
self.ac_charge[hour], self.dc_charge[hour], bool(self.discharge_allowed[hour])
|
||||
self.ac_charge[hour_idx],
|
||||
self.dc_charge[hour_idx],
|
||||
bool(self.discharge_allowed[hour_idx]),
|
||||
)
|
||||
if (
|
||||
operation_mode == last_operation_mode
|
||||
@@ -484,7 +530,7 @@ class GeneticSolution(GeneticParametersBaseModel):
|
||||
continue
|
||||
last_operation_mode = operation_mode
|
||||
last_operation_mode_factor = operation_mode_factor
|
||||
execution_time = start_datetime.add(hours=hour)
|
||||
execution_time = start_datetime.add(hours=hour_idx - start_day_hour)
|
||||
plan.add_instruction(
|
||||
FRBCInstruction(
|
||||
resource_id=resource_id,
|
||||
@@ -496,6 +542,7 @@ class GeneticSolution(GeneticParametersBaseModel):
|
||||
)
|
||||
|
||||
# Add EV battery instructions (fill rate based control)
|
||||
# eautocharge_hours_float start at hour 0 of start day
|
||||
if self.eauto_obj:
|
||||
resource_id = self.eauto_obj.device_id
|
||||
if self.eautocharge_hours_float is None:
|
||||
@@ -513,8 +560,12 @@ class GeneticSolution(GeneticParametersBaseModel):
|
||||
else:
|
||||
last_operation_mode = None
|
||||
last_operation_mode_factor = None
|
||||
logger.debug("EV: {} - {}", resource_id, self.eauto_obj.charge_array)
|
||||
for hour, rate in enumerate(self.eautocharge_hours_float):
|
||||
logger.debug(
|
||||
"EV: {} - {}", resource_id, self.eautocharge_hours_float[start_day_hour:]
|
||||
)
|
||||
for hour_idx, rate in enumerate(self.eautocharge_hours_float):
|
||||
if hour_idx < start_day_hour:
|
||||
continue
|
||||
operation_mode, operation_mode_factor = self._battery_operation_from_solution(
|
||||
rate, 0.0, False
|
||||
)
|
||||
@@ -526,7 +577,7 @@ class GeneticSolution(GeneticParametersBaseModel):
|
||||
continue
|
||||
last_operation_mode = operation_mode
|
||||
last_operation_mode_factor = operation_mode_factor
|
||||
execution_time = start_datetime.add(hours=hour)
|
||||
execution_time = start_datetime.add(hours=hour_idx - start_day_hour)
|
||||
plan.add_instruction(
|
||||
FRBCInstruction(
|
||||
resource_id=resource_id,
|
||||
@@ -542,7 +593,7 @@ class GeneticSolution(GeneticParametersBaseModel):
|
||||
resource_id = "homeappliance1"
|
||||
operation_mode = ApplianceOperationMode.RUN # type: ignore[assignment]
|
||||
operation_mode_factor = 1.0
|
||||
execution_time = start_datetime.add(hours=self.washingstart)
|
||||
execution_time = start_datetime.add(hours=self.washingstart - start_day_hour)
|
||||
plan.add_instruction(
|
||||
DDBCInstruction(
|
||||
resource_id=resource_id,
|
||||
|
||||
@@ -110,6 +110,8 @@ class OptimizationSolution(PydanticBaseModel):
|
||||
|
||||
total_costs_amt: float = Field(description="The total costs [money amount].")
|
||||
|
||||
fitness_score: set[float] = Field(description="The fitness score as a set of fitness values.")
|
||||
|
||||
prediction: PydanticDateTimeDataFrame = Field(
|
||||
description=(
|
||||
"Datetime data frame with time series prediction data per optimization interval:"
|
||||
|
||||
Reference in New Issue
Block a user