Files
EOS/src/akkudoktoreos/optimization/genetic.py
T

571 lines
23 KiB
Python
Raw Normal View History

2024-10-04 03:11:24 +02:00
import random
2024-11-15 22:27:25 +01:00
from typing import Any, Optional, Tuple
2024-10-03 11:05:44 +02:00
import numpy as np
2024-10-04 03:11:24 +02:00
from deap import algorithms, base, creator, tools
from pydantic import BaseModel, Field, field_validator, model_validator
2024-11-15 22:27:25 +01:00
from typing_extensions import Self
2024-10-03 11:05:44 +02:00
from akkudoktoreos.config import AppConfig
2024-11-26 22:28:05 +01:00
from akkudoktoreos.devices.battery import (
EAutoParameters,
EAutoResult,
PVAkku,
PVAkkuParameters,
)
from akkudoktoreos.devices.generic import HomeAppliance, HomeApplianceParameters
from akkudoktoreos.devices.inverter import Wechselrichter, WechselrichterParameters
from akkudoktoreos.prediction.ems import (
2024-11-15 22:27:25 +01:00
EnergieManagementSystem,
EnergieManagementSystemParameters,
2024-11-26 22:28:05 +01:00
SimulationResult,
2024-11-15 22:27:25 +01:00
)
2024-11-26 22:28:05 +01:00
from akkudoktoreos.utils.utils import NumpyEncoder
from akkudoktoreos.visualize import visualisiere_ergebnisse
2024-10-07 19:59:31 +02:00
2024-11-15 22:27:25 +01:00
class OptimizationParameters(BaseModel):
ems: EnergieManagementSystemParameters
pv_akku: PVAkkuParameters
wechselrichter: WechselrichterParameters = WechselrichterParameters()
2024-11-26 22:28:05 +01:00
eauto: Optional[EAutoParameters]
dishwasher: Optional[HomeApplianceParameters] = None
2024-11-26 22:28:05 +01:00
temperature_forecast: Optional[list[float]] = Field(
default=None,
description="An array of floats representing the temperature forecast in degrees Celsius for different time intervals.",
2024-11-15 22:27:25 +01:00
)
start_solution: Optional[list[float]] = Field(
2024-11-26 22:28:05 +01:00
default=None, description="Can be `null` or contain a previous solution (if available)."
2024-11-15 22:27:25 +01:00
)
@model_validator(mode="after")
def validate_list_length(self) -> Self:
arr_length = len(self.ems.pv_prognose_wh)
2024-11-26 22:28:05 +01:00
if self.temperature_forecast is not None and arr_length != len(self.temperature_forecast):
2024-11-15 22:27:25 +01:00
raise ValueError("Input lists have different lenghts")
return self
@field_validator("start_solution")
def validate_start_solution(
cls, start_solution: Optional[list[float]]
) -> Optional[list[float]]:
if start_solution is not None and len(start_solution) < 2:
raise ValueError("Requires at least two values.")
return start_solution
2024-11-15 22:27:25 +01:00
class OptimizeResponse(BaseModel):
"""**Note**: The first value of "Last_Wh_pro_Stunde", "Netzeinspeisung_Wh_pro_Stunde" and "Netzbezug_Wh_pro_Stunde", 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(
description="Array with AC charging values as relative power (0-1), other values set to 0."
)
dc_charge: list[float] = Field(
description="Array with DC charging values as relative power (0-1), other values set to 0."
)
discharge_allowed: list[int] = Field(
description="Array with discharge values (1 for discharge, 0 otherwise)."
)
2024-11-26 22:28:05 +01:00
eautocharge_hours_float: Optional[list[float]] = Field(description="TBD")
2024-11-15 22:27:25 +01:00
result: SimulationResult
2024-11-26 22:28:05 +01:00
eauto_obj: Optional[EAutoResult]
2024-11-15 22:27:25 +01:00
start_solution: Optional[list[float]] = Field(
2024-11-26 22:28:05 +01:00
default=None,
2024-11-15 22:27:25 +01:00
description="An array of binary values (0 or 1) representing a possible starting solution for the simulation.",
)
washingstart: Optional[int] = Field(
2024-11-26 22:28:05 +01:00
default=None,
2024-11-15 22:27:25 +01:00
description="Can be `null` or contain an object representing the start of washing (if applicable).",
)
2024-11-26 22:28:05 +01:00
@field_validator(
"ac_charge",
"dc_charge",
"discharge_allowed",
mode="before",
)
def convert_numpy(cls, field: Any) -> Any:
return NumpyEncoder.convert_numpy(field)[0]
@field_validator(
"eauto_obj",
mode="before",
)
def convert_eauto(cls, field: Any) -> Any:
if isinstance(field, PVAkku):
return EAutoResult(**field.to_dict())
return field
2024-11-15 22:27:25 +01:00
class optimization_problem:
2024-10-04 03:11:24 +02:00
def __init__(
self,
2024-11-11 21:38:13 +01:00
config: AppConfig,
2024-10-04 03:11:24 +02:00
verbose: bool = False,
fixed_seed: Optional[int] = None,
):
"""Initialize the optimization problem with the required parameters."""
2024-11-11 21:38:13 +01:00
self._config = config
self.prediction_hours = config.eos.prediction_hours
self.strafe = config.eos.penalty
2024-11-26 22:28:05 +01:00
self.opti_param: dict[str, Any] = {}
2024-11-11 21:38:13 +01:00
self.fixed_eauto_hours = config.eos.prediction_hours - config.eos.optimization_hours
self.possible_charge_values = config.eos.available_charging_rates_in_percentage
2024-10-04 03:11:24 +02:00
self.verbose = verbose
self.fix_seed = fixed_seed
self.optimize_ev = True
2024-10-20 18:18:06 +02:00
self.optimize_dc_charge = False
2024-10-04 03:11:24 +02:00
# Set a fixed seed for random operations if provided
if fixed_seed is not None:
random.seed(fixed_seed)
2024-10-22 10:29:57 +02:00
def decode_charge_discharge(
2024-11-26 22:28:05 +01:00
self, discharge_hours_bin: list[int]
2024-10-22 10:29:57 +02:00
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
"""Decode the input array `discharge_hours_bin` into three separate arrays for AC charging, DC charging, and discharge.
2024-10-16 15:40:04 +02:00
The function maps AC and DC charging values to relative power levels (0 to 1), while the discharge remains binary (0 or 1).
Parameters:
2024-10-16 15:40:04 +02:00
- discharge_hours_bin (np.ndarray): Input array with integer values representing the different states.
The states are:
0: No action ("idle")
1: Discharge ("discharge")
2-6: AC charging with different power levels ("ac_charge")
7-8: DC charging Dissallowed/allowed ("dc_charge")
2024-10-22 10:29:57 +02:00
Returns:
2024-10-16 15:40:04 +02:00
- ac_charge (np.ndarray): Array with AC charging values as relative power (0-1), other values set to 0.
- dc_charge (np.ndarray): Array with DC charging values as relative power (0-1), other values set to 0.
- discharge (np.ndarray): Array with discharge values (1 for discharge, 0 otherwise).
"""
# Convert the input list to a NumPy array, if it's not already
2024-11-26 22:28:05 +01:00
discharge_hours_bin_np = np.array(discharge_hours_bin)
2024-10-16 15:40:04 +02:00
# Create ac_charge array: Only consider values between 2 and 6 (AC charging power levels), set the rest to 0
2024-10-22 10:29:57 +02:00
ac_charge = np.where(
2024-11-26 22:28:05 +01:00
(discharge_hours_bin_np >= 2) & (discharge_hours_bin_np <= 6),
discharge_hours_bin_np - 1,
0,
2024-10-22 10:29:57 +02:00
)
2024-10-16 15:40:04 +02:00
ac_charge = ac_charge / 5.0 # Normalize AC charge to range between 0 and 1
# Create dc_charge array: 7 = Not allowed (mapped to 0), 8 = Allowed (mapped to 1)
2024-10-20 18:18:06 +02:00
# Create dc_charge array: Only if DC charge optimization is enabled
if self.optimize_dc_charge:
2024-11-26 22:28:05 +01:00
dc_charge = np.where(discharge_hours_bin_np == 8, 1, 0)
2024-10-20 18:18:06 +02:00
else:
2024-10-22 10:29:57 +02:00
dc_charge = np.ones_like(
2024-11-26 22:28:05 +01:00
discharge_hours_bin_np
2024-10-22 10:29:57 +02:00
) # Set DC charge to 0 if optimization is disabled
2024-10-16 15:40:04 +02:00
# Create discharge array: Only consider value 1 (Discharge), set the rest to 0 (binary output)
2024-11-26 22:28:05 +01:00
discharge = np.where(discharge_hours_bin_np == 1, 1, 0)
2024-10-16 15:40:04 +02:00
return ac_charge, dc_charge, discharge
2024-10-14 10:46:14 +02:00
# Custom mutation function that applies type-specific mutations
2024-11-26 22:28:05 +01:00
def mutate(self, individual: list[int]) -> tuple[list[int]]:
"""Custom mutation function for the individual.
This function mutates different parts of the individual:
2024-10-16 15:40:04 +02:00
- Mutates the discharge and charge states (AC, DC, idle) using the split_charge_discharge method.
- Mutates the EV charging schedule if EV optimization is enabled.
- Mutates appliance start times if household appliances are part of the optimization.
2024-10-14 10:46:14 +02:00
2024-10-16 15:40:04 +02:00
Parameters:
- individual (list): The individual being mutated, which includes different optimization parameters.
Returns:
- (tuple): The mutated individual as a tuple (required by DEAP).
"""
# Step 1: Mutate the charge/discharge states (idle, discharge, AC charge, DC charge)
# Extract the relevant part of the individual for prediction hours, which represents the charge/discharge behavior.
2024-10-22 10:29:57 +02:00
charge_discharge_part = individual[: self.prediction_hours]
2024-10-16 15:40:04 +02:00
# Apply the mutation to the charge/discharge part
2024-10-22 10:29:57 +02:00
(charge_discharge_mutated,) = self.toolbox.mutate_charge_discharge(charge_discharge_part)
2024-10-16 15:40:04 +02:00
# Ensure that no invalid states are introduced during mutation (valid values: 0-8)
2024-10-20 18:18:06 +02:00
if self.optimize_dc_charge:
charge_discharge_mutated = np.clip(charge_discharge_mutated, 0, 8)
else:
charge_discharge_mutated = np.clip(charge_discharge_mutated, 0, 6)
2024-10-22 10:29:57 +02:00
2024-10-16 15:40:04 +02:00
# Use split_charge_discharge to split the mutated array into AC charge, DC charge, and discharge components
2024-10-22 10:29:57 +02:00
# ac_charge, dc_charge, discharge = self.split_charge_discharge(charge_discharge_mutated)
2024-10-16 15:40:04 +02:00
# Optionally: You can process the split arrays further if needed, for example,
# applying additional constraints or penalties, or keeping track of charging limits.
# Reassign the mutated values back to the individual
2024-10-22 10:29:57 +02:00
individual[: self.prediction_hours] = charge_discharge_mutated
2024-10-16 15:40:04 +02:00
# Step 2: Mutate EV charging schedule if enabled
2024-10-14 10:46:14 +02:00
if self.optimize_ev:
2024-10-16 15:40:04 +02:00
# Extract the relevant part for EV charging schedule
2024-10-14 10:46:14 +02:00
ev_charge_part = individual[self.prediction_hours : self.prediction_hours * 2]
2024-10-22 10:29:57 +02:00
2024-10-16 15:40:04 +02:00
# Apply mutation on the EV charging schedule
2024-10-22 10:29:57 +02:00
(ev_charge_part_mutated,) = self.toolbox.mutate_ev_charge_index(ev_charge_part)
2024-10-16 15:40:04 +02:00
# Ensure the EV does not charge during fixed hours (set those hours to 0)
2024-10-22 10:29:57 +02:00
ev_charge_part_mutated[self.prediction_hours - self.fixed_eauto_hours :] = [
0
] * self.fixed_eauto_hours
2024-10-16 15:40:04 +02:00
# Reassign the mutated EV charging part back to the individual
2024-10-14 10:46:14 +02:00
individual[self.prediction_hours : self.prediction_hours * 2] = ev_charge_part_mutated
2024-10-16 15:40:04 +02:00
# Step 3: Mutate appliance start times if household appliances are part of the optimization
if self.opti_param["home_appliance"] > 0:
2024-10-16 15:40:04 +02:00
# Extract the appliance part (typically a single value for the start hour)
2024-10-14 10:46:14 +02:00
appliance_part = [individual[-1]]
2024-10-22 10:29:57 +02:00
2024-10-16 15:40:04 +02:00
# Apply mutation on the appliance start hour
2024-10-22 10:29:57 +02:00
(appliance_part_mutated,) = self.toolbox.mutate_hour(appliance_part)
2024-10-16 15:40:04 +02:00
# Reassign the mutated appliance part back to the individual
2024-10-14 10:46:14 +02:00
individual[-1] = appliance_part_mutated[0]
return (individual,)
# Method to create an individual based on the conditions
2024-11-26 22:28:05 +01:00
def create_individual(self) -> list[int]:
2024-10-14 10:46:14 +02:00
# Start with discharge states for the individual
2024-10-22 10:29:57 +02:00
individual_components = [
self.toolbox.attr_discharge_state() for _ in range(self.prediction_hours)
]
2024-10-14 10:46:14 +02:00
# Add EV charge index values if optimize_ev is True
if self.optimize_ev:
2024-10-22 10:29:57 +02:00
individual_components += [
self.toolbox.attr_ev_charge_index() for _ in range(self.prediction_hours)
]
2024-10-14 10:46:14 +02:00
# Add the start time of the household appliance if it's being optimized
if self.opti_param["home_appliance"] > 0:
2024-10-14 10:46:14 +02:00
individual_components += [self.toolbox.attr_int()]
return creator.Individual(individual_components)
2024-10-04 03:11:24 +02:00
def split_individual(
2024-11-26 22:28:05 +01:00
self, individual: list[int]
) -> tuple[list[int], Optional[list[int]], Optional[int]]:
"""Split the individual solution into its components.
Components:
1. Discharge hours (binary),
2. Electric vehicle charge hours (float),
2024-10-04 03:11:24 +02:00
3. Dishwasher start time (integer if applicable).
"""
2024-10-04 03:11:24 +02:00
discharge_hours_bin = individual[: self.prediction_hours]
2024-11-26 22:28:05 +01:00
eautocharge_hours_index = (
2024-10-16 15:40:04 +02:00
individual[self.prediction_hours : self.prediction_hours * 2]
2024-10-22 10:29:57 +02:00
if self.optimize_ev
2024-10-16 15:40:04 +02:00
else None
)
washingstart_int = (
2024-11-26 22:28:05 +01:00
int(individual[-1])
if self.opti_param and self.opti_param.get("home_appliance", 0) > 0
2024-10-04 03:11:24 +02:00
else None
)
2024-11-26 22:28:05 +01:00
return discharge_hours_bin, eautocharge_hours_index, washingstart_int
2024-08-24 10:22:49 +02:00
2024-11-15 22:27:25 +01:00
def setup_deap_environment(self, opti_param: dict[str, Any], start_hour: int) -> None:
"""Set up the DEAP environment with fitness and individual creation rules."""
2024-04-02 16:46:16 +02:00
self.opti_param = opti_param
2024-10-03 11:05:44 +02:00
2024-10-04 03:11:24 +02:00
# Remove existing FitnessMin and Individual classes from creator if present
for attr in ["FitnessMin", "Individual"]:
if attr in creator.__dict__:
del creator.__dict__[attr]
2024-10-04 03:11:24 +02:00
# Create new FitnessMin and Individual classes
2024-04-02 16:46:16 +02:00
creator.create("FitnessMin", base.Fitness, weights=(-1.0,))
creator.create("Individual", list, fitness=creator.FitnessMin)
2024-10-03 11:05:44 +02:00
2024-10-04 03:11:24 +02:00
# Initialize toolbox with attributes and operations
2024-04-02 16:46:16 +02:00
self.toolbox = base.Toolbox()
2024-10-20 18:18:06 +02:00
if self.optimize_dc_charge:
2024-10-22 10:29:57 +02:00
self.toolbox.register("attr_discharge_state", random.randint, 0, 8)
2024-10-20 18:18:06 +02:00
else:
2024-10-22 10:29:57 +02:00
self.toolbox.register("attr_discharge_state", random.randint, 0, 6)
2024-10-20 18:18:06 +02:00
if self.optimize_ev:
2024-10-22 10:29:57 +02:00
self.toolbox.register(
2024-11-11 21:38:13 +01:00
"attr_ev_charge_index",
random.randint,
0,
len(self._config.eos.available_charging_rates_in_percentage) - 1,
2024-10-22 10:29:57 +02:00
)
2024-05-03 10:56:13 +02:00
self.toolbox.register("attr_int", random.randint, start_hour, 23)
2024-10-03 11:05:44 +02:00
# Register individual creation function
2024-10-14 10:46:14 +02:00
self.toolbox.register("individual", self.create_individual)
2024-04-02 16:46:16 +02:00
2024-10-04 03:11:24 +02:00
# Register population, mating, mutation, and selection functions
2024-10-10 15:00:32 +02:00
self.toolbox.register("population", tools.initRepeat, list, self.toolbox.individual)
self.toolbox.register("mate", tools.cxTwoPoint)
2024-10-22 10:29:57 +02:00
# self.toolbox.register("mutate", tools.mutFlipBit, indpb=0.1)
# Register separate mutation functions for each type of value:
# - Discharge state mutation (-5, 0, 1)
2024-10-20 18:18:06 +02:00
if self.optimize_dc_charge:
2024-10-22 10:29:57 +02:00
self.toolbox.register(
"mutate_charge_discharge", tools.mutUniformInt, low=0, up=8, indpb=0.2
)
2024-10-20 18:18:06 +02:00
else:
2024-10-22 10:29:57 +02:00
self.toolbox.register(
"mutate_charge_discharge", tools.mutUniformInt, low=0, up=6, indpb=0.2
)
# - Float mutation for EV charging values
2024-10-22 10:29:57 +02:00
self.toolbox.register(
"mutate_ev_charge_index",
tools.mutUniformInt,
low=0,
2024-11-11 21:38:13 +01:00
up=len(self._config.eos.available_charging_rates_in_percentage) - 1,
2024-10-22 10:29:57 +02:00
indpb=0.2,
)
# - Start hour mutation for household devices
2024-10-22 08:58:07 +02:00
self.toolbox.register("mutate_hour", tools.mutUniformInt, low=start_hour, up=23, indpb=0.2)
# Register custom mutation function
2024-10-14 10:46:14 +02:00
self.toolbox.register("mutate", self.mutate)
2024-10-03 11:05:44 +02:00
self.toolbox.register("select", tools.selTournament, tournsize=3)
2024-10-04 03:11:24 +02:00
def evaluate_inner(
2024-11-26 22:28:05 +01:00
self, individual: list[int], ems: EnergieManagementSystem, start_hour: int
2024-11-15 22:27:25 +01:00
) -> dict[str, Any]:
"""Simulates the energy management system (EMS) using the provided individual solution.
This is an internal function.
2024-10-04 03:11:24 +02:00
"""
2024-04-02 16:46:16 +02:00
ems.reset()
discharge_hours_bin, eautocharge_hours_index, washingstart_int = self.split_individual(
2024-10-10 15:00:32 +02:00
individual
2024-10-03 11:05:44 +02:00
)
2024-11-26 22:28:05 +01:00
if washingstart_int is not None:
ems.set_home_appliance_start(washingstart_int, global_start_hour=start_hour)
2024-10-22 10:29:57 +02:00
ac, dc, discharge = self.decode_charge_discharge(discharge_hours_bin)
ems.set_akku_discharge_hours(discharge)
2024-10-20 18:18:06 +02:00
# Set DC charge hours only if DC optimization is enabled
if self.optimize_dc_charge:
ems.set_akku_dc_charge_hours(dc)
2024-10-16 15:40:04 +02:00
ems.set_akku_ac_charge_hours(ac)
2024-10-14 10:46:14 +02:00
2024-11-26 22:28:05 +01:00
if eautocharge_hours_index is not None:
2024-10-14 10:46:14 +02:00
eautocharge_hours_float = [
2024-11-11 21:38:13 +01:00
self._config.eos.available_charging_rates_in_percentage[i]
for i in eautocharge_hours_index
2024-10-22 10:29:57 +02:00
]
2024-11-26 22:28:05 +01:00
ems.set_ev_charge_hours(np.array(eautocharge_hours_float))
2024-10-20 18:18:06 +02:00
else:
2024-10-22 10:29:57 +02:00
ems.set_ev_charge_hours(np.full(self.prediction_hours, 0))
2024-10-04 03:11:24 +02:00
return ems.simuliere(start_hour)
2024-10-03 11:05:44 +02:00
2024-10-04 03:11:24 +02:00
def evaluate(
self,
2024-11-26 22:28:05 +01:00
individual: list[int],
2024-10-04 03:11:24 +02:00
ems: EnergieManagementSystem,
2024-11-15 22:27:25 +01:00
parameters: OptimizationParameters,
2024-10-04 03:11:24 +02:00
start_hour: int,
worst_case: bool,
) -> Tuple[float]:
"""Evaluate the fitness of an individual solution based on the simulation results."""
try:
2024-10-03 11:05:44 +02:00
o = self.evaluate_inner(individual, ems, start_hour)
except Exception as e:
2024-10-04 03:11:24 +02:00
return (100000.0,) # Return a high penalty in case of an exception
2024-10-22 10:29:57 +02:00
2024-10-04 03:11:24 +02:00
gesamtbilanz = o["Gesamtbilanz_Euro"] * (-1.0 if worst_case else 1.0)
2024-10-22 10:29:57 +02:00
2024-11-26 22:28:05 +01:00
discharge_hours_bin, eautocharge_hours_index, _ = self.split_individual(individual)
# Small Penalty for not discharging
2024-10-04 03:11:24 +02:00
gesamtbilanz += sum(
0.01 for i in range(self.prediction_hours) if discharge_hours_bin[i] == 0.0
)
2024-10-22 10:29:57 +02:00
2024-10-04 03:11:24 +02:00
# Penalty for not meeting the minimum SOC (State of Charge) requirement
2024-11-15 22:27:25 +01:00
# if parameters.eauto_min_soc_prozent - ems.eauto.ladezustand_in_prozent() <= 0.0 and self.optimize_ev:
# gesamtbilanz += sum(
2024-11-26 22:28:05 +01:00
# self.strafe for ladeleistung in eautocharge_hours_index if ladeleistung != 0.0
# )
2024-10-03 11:05:44 +02:00
2024-11-26 22:28:05 +01:00
individual.extra_data = ( # type: ignore[attr-defined]
2024-10-03 11:05:44 +02:00
o["Gesamtbilanz_Euro"],
o["Gesamt_Verluste"],
2024-11-26 22:28:05 +01:00
parameters.eauto.min_soc_prozent - ems.eauto.ladezustand_in_prozent()
if parameters.eauto and ems.eauto
else 0,
2024-10-03 11:05:44 +02:00
)
2024-10-04 03:11:24 +02:00
# Adjust total balance with battery value and penalties for unmet SOC
2024-11-15 22:27:25 +01:00
restwert_akku = ems.akku.aktueller_energieinhalt() * parameters.ems.preis_euro_pro_wh_akku
# print(ems.akku.aktueller_energieinhalt()," * ", parameters.ems.preis_euro_pro_wh_akku , " ", restwert_akku, " ", gesamtbilanz)
2024-10-22 10:29:57 +02:00
gesamtbilanz += -restwert_akku
# print(gesamtbilanz)
if self.optimize_ev:
gesamtbilanz += max(
0,
2024-11-26 22:28:05 +01:00
(
parameters.eauto.min_soc_prozent - ems.eauto.ladezustand_in_prozent()
if parameters.eauto and ems.eauto
else 0
)
2024-11-15 22:27:25 +01:00
* self.strafe,
2024-10-22 10:29:57 +02:00
)
2024-10-03 11:05:44 +02:00
return (gesamtbilanz,)
2024-10-04 03:11:24 +02:00
def optimize(
2024-11-15 22:27:25 +01:00
self, start_solution: Optional[list[float]] = None, ngen: int = 400
) -> Tuple[Any, dict[str, list[Any]]]:
2024-10-04 03:11:24 +02:00
"""Run the optimization process using a genetic algorithm."""
2024-09-15 11:08:00 +02:00
population = self.toolbox.population(n=300)
hof = tools.HallOfFame(1)
stats = tools.Statistics(lambda ind: ind.fitness.values)
stats.register("min", np.min)
2024-10-03 11:05:44 +02:00
2024-10-04 03:11:24 +02:00
if self.verbose:
print("Start optimize:", start_solution)
2024-10-03 11:05:44 +02:00
2024-10-04 03:11:24 +02:00
# Insert the start solution into the population if provided
2024-11-26 22:28:05 +01:00
if start_solution is not None:
2024-10-04 03:11:24 +02:00
for _ in range(3):
population.insert(0, creator.Individual(start_solution))
2024-09-15 11:08:00 +02:00
2024-10-22 10:29:57 +02:00
# Run the evolutionary algorithm
2024-10-03 11:05:44 +02:00
algorithms.eaMuPlusLambda(
population,
self.toolbox,
mu=100,
lambda_=150,
2024-10-20 18:18:06 +02:00
cxpb=0.6,
mutpb=0.4,
ngen=ngen,
2024-10-03 11:05:44 +02:00
stats=stats,
halloffame=hof,
2024-10-04 03:11:24 +02:00
verbose=self.verbose,
2024-10-03 11:05:44 +02:00
)
2024-09-15 11:08:00 +02:00
2024-11-26 22:28:05 +01:00
member: dict[str, list[float]] = {"bilanz": [], "verluste": [], "nebenbedingung": []}
for ind in population:
2024-10-03 11:05:44 +02:00
if hasattr(ind, "extra_data"):
extra_value1, extra_value2, extra_value3 = ind.extra_data
member["bilanz"].append(extra_value1)
member["verluste"].append(extra_value2)
member["nebenbedingung"].append(extra_value3)
return hof[0], member
2024-10-03 11:05:44 +02:00
def optimierung_ems(
2024-10-04 03:11:24 +02:00
self,
2024-11-15 22:27:25 +01:00
parameters: OptimizationParameters,
2024-11-26 22:28:05 +01:00
start_hour: int,
2024-10-04 03:11:24 +02:00
worst_case: bool = False,
ngen: int = 600,
2024-11-26 22:28:05 +01:00
) -> OptimizeResponse:
"""Perform EMS (Energy Management System) optimization and visualize results."""
2024-10-03 11:05:44 +02:00
einspeiseverguetung_euro_pro_wh = np.full(
2024-11-15 22:27:25 +01:00
self.prediction_hours, parameters.ems.einspeiseverguetung_euro_pro_wh
2024-10-04 03:11:24 +02:00
)
# Initialize PV and EV batteries
2024-10-03 11:05:44 +02:00
akku = PVAkku(
2024-11-15 22:27:25 +01:00
parameters.pv_akku,
2024-10-03 11:05:44 +02:00
hours=self.prediction_hours,
)
2024-10-04 03:11:24 +02:00
akku.set_charge_per_hour(np.full(self.prediction_hours, 1))
2024-10-03 11:05:44 +02:00
2024-11-26 22:28:05 +01:00
eauto: Optional[PVAkku] = None
if parameters.eauto:
eauto = PVAkku(
parameters.eauto,
hours=self.prediction_hours,
)
eauto.set_charge_per_hour(np.full(self.prediction_hours, 1))
self.optimize_ev = (
parameters.eauto.min_soc_prozent - parameters.eauto.start_soc_prozent >= 0
)
else:
self.optimize_ev = False
2024-10-04 03:11:24 +02:00
# Initialize household appliance if applicable
dishwasher = (
HomeAppliance(
parameters=parameters.dishwasher,
2024-10-03 11:05:44 +02:00
hours=self.prediction_hours,
2024-10-06 14:29:23 +02:00
)
if parameters.dishwasher is not None
2024-10-04 03:11:24 +02:00
else None
)
2024-10-04 03:11:24 +02:00
# Initialize the inverter and energy management system
2024-11-15 22:27:25 +01:00
wr = Wechselrichter(parameters.wechselrichter, akku)
2024-10-03 11:05:44 +02:00
ems = EnergieManagementSystem(
2024-11-15 22:27:25 +01:00
self._config.eos,
parameters.ems,
2024-11-26 22:28:05 +01:00
wechselrichter=wr,
2024-10-03 11:05:44 +02:00
eauto=eauto,
home_appliance=dishwasher,
2024-10-03 11:05:44 +02:00
)
2024-10-04 03:11:24 +02:00
# Setup the DEAP environment and optimization process
self.setup_deap_environment({"home_appliance": 1 if dishwasher else 0}, start_hour)
2024-10-04 03:11:24 +02:00
self.toolbox.register(
"evaluate",
2024-11-15 22:27:25 +01:00
lambda ind: self.evaluate(ind, ems, parameters, start_hour, worst_case),
2024-10-04 03:11:24 +02:00
)
2024-11-15 22:27:25 +01:00
start_solution, extra_data = self.optimize(parameters.start_solution, ngen=ngen)
2024-10-03 11:05:44 +02:00
2024-10-04 03:11:24 +02:00
# Perform final evaluation on the best solution
o = self.evaluate_inner(start_solution, ems, start_hour)
2024-11-26 22:28:05 +01:00
discharge_hours_bin, eautocharge_hours_index, washingstart_int = self.split_individual(
2024-10-10 15:00:32 +02:00
start_solution
2024-10-03 11:05:44 +02:00
)
2024-11-26 22:28:05 +01:00
eautocharge_hours_float = (
[
2024-11-11 21:38:13 +01:00
self._config.eos.available_charging_rates_in_percentage[i]
2024-11-26 22:28:05 +01:00
for i in eautocharge_hours_index
2024-10-22 10:29:57 +02:00
]
2024-11-26 22:28:05 +01:00
if eautocharge_hours_index is not None
else None
)
2024-10-16 15:40:04 +02:00
ac_charge, dc_charge, discharge = self.decode_charge_discharge(discharge_hours_bin)
2024-10-04 03:11:24 +02:00
# Visualize the results
2024-10-03 11:05:44 +02:00
visualisiere_ergebnisse(
2024-11-15 22:27:25 +01:00
parameters.ems.gesamtlast,
parameters.ems.pv_prognose_wh,
parameters.ems.strompreis_euro_pro_wh,
o,
ac_charge,
dc_charge,
discharge,
parameters.temperature_forecast,
start_hour,
einspeiseverguetung_euro_pro_wh,
2024-11-11 21:38:13 +01:00
config=self._config,
2024-10-03 11:05:44 +02:00
extra_data=extra_data,
)
2024-10-04 03:11:24 +02:00
2024-11-26 22:28:05 +01:00
return OptimizeResponse(
**{
"ac_charge": ac_charge,
"dc_charge": dc_charge,
"discharge_allowed": discharge,
"eautocharge_hours_float": eautocharge_hours_float,
"result": SimulationResult(**o),
"eauto_obj": ems.eauto,
"start_solution": start_solution,
"washingstart": washingstart_int,
}
)