From 2b5f0ee53cfb128b5e1058d2e98e501310dea721 Mon Sep 17 00:00:00 2001 From: Andreas Date: Mon, 14 Oct 2024 10:10:12 +0200 Subject: [PATCH] EV Charge Parameters optional + AC Charge first try (Parameter Reduction) --- src/akkudoktoreos/class_akku.py | 15 ++- src/akkudoktoreos/class_ems.py | 14 ++- src/akkudoktoreos/class_optimize.py | 154 +++++++++++++++++++--------- src/akkudoktoreos/config.py | 12 +-- src/akkudoktoreos/visualize.py | 69 ++++++++++--- 5 files changed, 187 insertions(+), 77 deletions(-) diff --git a/src/akkudoktoreos/class_akku.py b/src/akkudoktoreos/class_akku.py index 5ec19fb..5c190dd 100644 --- a/src/akkudoktoreos/class_akku.py +++ b/src/akkudoktoreos/class_akku.py @@ -70,21 +70,32 @@ class PVAkku: self.soc_wh = min(max(self.soc_wh, self.min_soc_wh), self.max_soc_wh) self.discharge_array = np.full(self.hours, 1) - self.charge_array = np.full(self.hours, 1) + self.charge_array = np.full(self.hours, 0) def set_discharge_per_hour(self, discharge_array): assert len(discharge_array) == self.hours self.discharge_array = np.array(discharge_array) + # Ensure no simultaneous charging and discharging in the same hour using NumPy mask + conflict_mask = (self.charge_array > 0) & (self.discharge_array > 0) + # Prioritize discharge by setting charge to 0 where both are > 0 + self.charge_array[conflict_mask] = 0 + def set_charge_per_hour(self, charge_array): assert len(charge_array) == self.hours self.charge_array = np.array(charge_array) + # Ensure no simultaneous charging and discharging in the same hour using NumPy mask + conflict_mask = (self.charge_array > 0) & (self.discharge_array > 0) + # Prioritize discharge by setting charge to 0 where both are > 0 + self.charge_array[conflict_mask] = 0 + + def ladezustand_in_prozent(self): return (self.soc_wh / self.kapazitaet_wh) * 100 def energie_abgeben(self, wh, hour): - if self.discharge_array[hour] == 0 and self.discharge_array[hour] == -1: + if self.discharge_array[hour] == 0 : return 0.0, 0.0 # No energy discharge and no losses # Calculate the maximum energy that can be discharged considering min_soc and efficiency diff --git a/src/akkudoktoreos/class_ems.py b/src/akkudoktoreos/class_ems.py index 229bc9d..9a983e7 100644 --- a/src/akkudoktoreos/class_ems.py +++ b/src/akkudoktoreos/class_ems.py @@ -27,8 +27,10 @@ class EnergieManagementSystem: def set_akku_discharge_hours(self, ds: List[int]) -> None: self.akku.set_discharge_per_hour(ds) + def set_akku_charge_hours(self, ds: List[int]) -> None: + self.akku.set_charge_per_hour(ds) + def set_eauto_charge_hours(self, ds: List[int]) -> None: - self.eauto.set_charge_per_hour(ds) def set_haushaltsgeraet_start(self, ds: List[int], global_start_hour: int = 0) -> None: @@ -69,7 +71,7 @@ class EnergieManagementSystem: akku_soc_pro_stunde[0] = self.akku.ladezustand_in_prozent() if self.eauto: eauto_soc_pro_stunde[0] = self.eauto.ladezustand_in_prozent() - + for stunde in range(start_stunde + 1, ende): stunde_since_now = stunde - start_stunde @@ -88,6 +90,14 @@ class EnergieManagementSystem: verluste_wh_pro_stunde[stunde_since_now] += verluste_eauto eauto_soc_pro_stunde[stunde_since_now] = self.eauto.ladezustand_in_prozent() + # AC PV Battery Charge + if self.akku.charge_array[stunde] > 0.0: + #soc_pre = self.akku.ladezustand_in_prozent() + geladene_menge, verluste_wh = self.akku.energie_laden(None,stunde) + #print(self.akku.charge_array[stunde], " ",geladene_menge," ",soc_pre," ",self.akku.ladezustand_in_prozent()) + verbrauch += geladene_menge + verluste_wh_pro_stunde[stunde_since_now] += verluste_wh + # Process inverter logic erzeugung = self.pv_prognose_wh[stunde] netzeinspeisung, netzbezug, verluste, eigenverbrauch = ( diff --git a/src/akkudoktoreos/class_optimize.py b/src/akkudoktoreos/class_optimize.py index 120f72f..d100d92 100644 --- a/src/akkudoktoreos/class_optimize.py +++ b/src/akkudoktoreos/class_optimize.py @@ -8,7 +8,7 @@ from akkudoktoreos.class_akku import PVAkku from akkudoktoreos.class_ems import EnergieManagementSystem from akkudoktoreos.class_haushaltsgeraet import Haushaltsgeraet from akkudoktoreos.class_inverter import Wechselrichter -from akkudoktoreos.config import moegliche_ladestroeme_in_prozent +from akkudoktoreos.config import possible_ev_charge_currents from akkudoktoreos.visualize import visualisiere_ergebnisse @@ -26,21 +26,47 @@ class optimization_problem: self.strafe = strafe self.opti_param = None self.fixed_eauto_hours = prediction_hours - optimization_hours - self.possible_charge_values = moegliche_ladestroeme_in_prozent + self.possible_charge_values = possible_ev_charge_currents self.verbose = verbose self.fix_seed = fixed_seed + self.optimize_ev = True # Set a fixed seed for random operations if provided if fixed_seed is not None: random.seed(fixed_seed) + def split_charge_discharge(self, discharge_hours_bin: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: + """ + Split the input array `discharge_hours_bin` into two separate arrays: + - `charge`: Contains only the negative values from `discharge_hours_bin` (charging values). + - `discharge`: Contains only the positive values from `discharge_hours_bin` (discharging values). + + Parameters: + - discharge_hours_bin (np.ndarray): Input array with both positive and negative values. + + Returns: + - charge (np.ndarray): Array with negative values from `discharge_hours_bin`, other values set to 0. + - discharge (np.ndarray): Array with positive values from `discharge_hours_bin`, other values set to 0. + """ + # Convert the input list to a NumPy array, if it's not already + discharge_hours_bin = np.array(discharge_hours_bin) + + # Create charge array: Keep only negative values, set the rest to 0 + charge = -np.where(discharge_hours_bin < 0, discharge_hours_bin, 0) + charge = charge / np.max(charge) + + # Create discharge array: Keep only positive values, set the rest to 0 + discharge = np.where(discharge_hours_bin > 0, discharge_hours_bin, 0) + + return charge, discharge + def split_individual( self, individual: List[float] ) -> Tuple[List[int], List[float], Optional[int]]: """ Split the individual solution into its components: 1. Discharge hours (-1 (Charge),0 (Nothing),1 (Discharge)), - 2. Electric vehicle charge hours (float), + 2. Electric vehicle charge hours (possible_charge_values), 3. Dishwasher start time (integer if applicable). """ discharge_hours_bin = individual[: self.prediction_hours] @@ -69,40 +95,60 @@ class optimization_problem: # Initialize toolbox with attributes and operations self.toolbox = base.Toolbox() - self.toolbox.register("attr_discharge_state", random.randint, -1, 1) - self.toolbox.register("attr_ev_charge_index", random.randint, 0, len(moegliche_ladestroeme_in_prozent) - 1) + self.toolbox.register("attr_discharge_state", random.randint, -5, 1) + if self.optimize_ev: + self.toolbox.register("attr_ev_charge_index", random.randint, 0, len(possible_ev_charge_currents) - 1) self.toolbox.register("attr_int", random.randint, start_hour, 23) - # Register individual creation method based on household appliance parameter - if opti_param["haushaltsgeraete"] > 0: - self.toolbox.register( - "individual", - lambda: creator.Individual( - [self.toolbox.attr_discharge_state() for _ in range(self.prediction_hours)] - + [self.toolbox.attr_ev_charge_index() for _ in range(self.prediction_hours)] - + [self.toolbox.attr_int()] - ), - ) - else: - self.toolbox.register( - "individual", - lambda: creator.Individual( - [self.toolbox.attr_discharge_state() for _ in range(self.prediction_hours)] - + [self.toolbox.attr_ev_charge_index() for _ in range(self.prediction_hours)] - ), - ) + # Function to create an individual based on the conditions + def create_individual(): + # Start with discharge states for the individual + individual_components = [self.toolbox.attr_discharge_state() for _ in range(self.prediction_hours)] + + # Add EV charge index values if optimize_ev is True + if self.optimize_ev: + individual_components += [self.toolbox.attr_ev_charge_index() for _ in range(self.prediction_hours)] + + # Add the start time of the household appliance if it's being optimized + if self.opti_param["haushaltsgeraete"] > 0: + individual_components += [self.toolbox.attr_int()] + + return creator.Individual(individual_components) + + # Register individual creation function + self.toolbox.register("individual", create_individual) + + + # # Register individual creation method based on household appliance parameter + # if opti_param["haushaltsgeraete"] > 0: + # self.toolbox.register( + # "individual", + # lambda: creator.Individual( + # [self.toolbox.attr_discharge_state() for _ in range(self.prediction_hours)] + # + [self.toolbox.attr_ev_charge_index() for _ in range(self.prediction_hours)] + # + [self.toolbox.attr_int()] + # ), + # ) + # else: + # self.toolbox.register( + # "individual", + # lambda: creator.Individual( + # [self.toolbox.attr_discharge_state() for _ in range(self.prediction_hours)] + # + [self.toolbox.attr_ev_charge_index() for _ in range(self.prediction_hours)] + # ), + # ) # Register population, mating, mutation, and selection functions self.toolbox.register("population", tools.initRepeat, list, self.toolbox.individual) self.toolbox.register("mate", tools.cxTwoPoint) #self.toolbox.register("mutate", tools.mutFlipBit, indpb=0.1) # Register separate mutation functions for each type of value: - # - Discharge state mutation (-1, 0, 1) - self.toolbox.register("mutate_discharge", tools.mutUniformInt, low=0, up=1, indpb=0.1) + # - Discharge state mutation (-5, 0, 1) + self.toolbox.register("mutate_discharge", tools.mutUniformInt, low=-5, up=1, indpb=0.1) # - Float mutation for EV charging values - self.toolbox.register("mutate_ev_charge_index", tools.mutUniformInt, low=0, up=len(moegliche_ladestroeme_in_prozent) - 1, indpb=0.1) + self.toolbox.register("mutate_ev_charge_index", tools.mutUniformInt, low=0, up=len(possible_ev_charge_currents) - 1, indpb=0.1) # - Start hour mutation for household devices - self.toolbox.register("mutate_hour", tools.mutUniformInt, low=start_hour, up=23, indpb=0.3) + self.toolbox.register("mutate_hour", tools.mutUniformInt, low=start_hour, up=23, indpb=0.1) # Custom mutation function that applies type-specific mutations def mutate(individual): @@ -111,13 +157,15 @@ class optimization_problem: individual[:self.prediction_hours] ) - # Mutate the EV charging indices - ev_charge_part = individual[self.prediction_hours : self.prediction_hours * 2] - ev_charge_part_mutated, = self.toolbox.mutate_ev_charge_index(ev_charge_part) - individual[self.prediction_hours : self.prediction_hours * 2] = ev_charge_part_mutated + if self.optimize_ev: + # Mutate the EV charging indices + ev_charge_part = individual[self.prediction_hours : self.prediction_hours * 2] + ev_charge_part_mutated, = self.toolbox.mutate_ev_charge_index(ev_charge_part) + ev_charge_part_mutated[self.prediction_hours - self.fixed_eauto_hours :] = [0] * self.fixed_eauto_hours + individual[self.prediction_hours : self.prediction_hours * 2] = ev_charge_part_mutated # Mutate the appliance start hour if present - if len(individual) > self.prediction_hours * 2: + if self.opti_param["haushaltsgeraete"] > 0: appliance_part = [individual[-1]] appliance_part_mutated, = self.toolbox.mutate_hour(appliance_part) individual[-1] = appliance_part_mutated[0] @@ -145,17 +193,18 @@ class optimization_problem: if self.opti_param.get("haushaltsgeraete", 0) > 0: ems.set_haushaltsgeraet_start(spuelstart_int, global_start_hour=start_hour) - ems.set_akku_discharge_hours(discharge_hours_bin) - eautocharge_hours_index[self.prediction_hours - self.fixed_eauto_hours :] = [ - 0 - ] * self.fixed_eauto_hours + charge, discharge = self.split_charge_discharge(discharge_hours_bin) + + + ems.set_akku_discharge_hours(discharge) + ems.set_akku_charge_hours(charge) + #print(charge) eautocharge_hours_float = [ - moegliche_ladestroeme_in_prozent[i] for i in eautocharge_hours_index + possible_ev_charge_currents[i] for i in eautocharge_hours_index ] - - - ems.set_eauto_charge_hours(eautocharge_hours_float) + if self.optimize_ev: + ems.set_eauto_charge_hours(eautocharge_hours_float) return ems.simuliere(start_hour) def evaluate( @@ -177,7 +226,7 @@ class optimization_problem: gesamtbilanz = o["Gesamtbilanz_Euro"] * (-1.0 if worst_case else 1.0) discharge_hours_bin, eautocharge_hours_float, _ = self.split_individual(individual) - max_ladeleistung = np.max(moegliche_ladestroeme_in_prozent) + #max_ladeleistung = np.max(possible_ev_charge_currents) # Small Penalty for not discharging gesamtbilanz += sum( @@ -185,11 +234,11 @@ class optimization_problem: ) # Penalty for charging the electric vehicle during restricted hours - gesamtbilanz += sum( - self.strafe - for i in range(self.prediction_hours - self.fixed_eauto_hours, self.prediction_hours) - if eautocharge_hours_float[i] != 0.0 - ) + # gesamtbilanz += sum( + # self.strafe + # for i in range(self.prediction_hours - self.fixed_eauto_hours, self.prediction_hours) + # if eautocharge_hours_float[i] != 0.0 + # ) # Penalty for not meeting the minimum SOC (State of Charge) requirement if parameter["eauto_min_soc"] - ems.eauto.ladezustand_in_prozent() <= 0.0: @@ -232,14 +281,14 @@ class optimization_problem: for _ in range(3): population.insert(0, creator.Individual(start_solution)) - # Run the evolutionary algorithm + #Run the evolutionary algorithm algorithms.eaMuPlusLambda( population, self.toolbox, mu=100, - lambda_=200, - cxpb=0.7, - mutpb=0.3, + lambda_=150, + cxpb=0.5, + mutpb=0.5, ngen=ngen, stats=stats, halloffame=hof, @@ -282,6 +331,10 @@ class optimization_problem: ) akku.set_charge_per_hour(np.full(self.prediction_hours, 1)) + self.optimize_ev = True + if parameter["eauto_min_soc"] - parameter["eauto_soc"] <0: + self.optimize_ev = False + eauto = PVAkku( kapazitaet_wh=parameter["eauto_cap"], hours=self.prediction_hours, @@ -382,3 +435,4 @@ class optimization_problem: "spuelstart": spuelstart_int, "simulation_data": o, } + diff --git a/src/akkudoktoreos/config.py b/src/akkudoktoreos/config.py index 2e58deb..b9d0b64 100644 --- a/src/akkudoktoreos/config.py +++ b/src/akkudoktoreos/config.py @@ -5,18 +5,18 @@ output_dir = "output" prediction_hours = 48 optimization_hours = 24 strafe = 10 -moegliche_ladestroeme_in_prozent = [ +possible_ev_charge_currents = [ 0.0, 6.0 / 16.0, - 7.0 / 16.0, + #7.0 / 16.0, 8.0 / 16.0, - 9.0 / 16.0, + #9.0 / 16.0, 10.0 / 16.0, - 11.0 / 16.0, + #11.0 / 16.0, 12.0 / 16.0, - 13.0 / 16.0, + #13.0 / 16.0, 14.0 / 16.0, - 15.0 / 16.0, + #15.0 / 16.0, 1.0, ] diff --git a/src/akkudoktoreos/visualize.py b/src/akkudoktoreos/visualize.py index f5a2d88..a97a6a3 100644 --- a/src/akkudoktoreos/visualize.py +++ b/src/akkudoktoreos/visualize.py @@ -160,26 +160,40 @@ def visualisiere_ergebnisse( plt.grid(True, which="both", axis="x") # Grid for every hour ax1 = plt.subplot(3, 2, 3) + # Plot für die discharge_hours-Werte for hour, value in enumerate(discharge_hours): + # Festlegen der Farbe und des Labels basierend auf dem Wert + if value > 0: # Positive Werte (Entladung) + color = "red" + label = "Discharge" if hour == 0 else "" # Label nur beim ersten Eintrag hinzufügen + elif value < 0: # Negative Werte (Ladung) + color = "blue" + label = "Charge" if hour == 0 else "" + else: + continue # Überspringe 0-Werte + + # Erstellen der Farbbereiche mit `axvspan` ax1.axvspan( - hour, - hour + 1, - color="red", - ymax=value, + hour, # Start der Stunde + hour + 1, # Ende der Stunde + ymin=0, # Untere Grenze + ymax=abs(value), # Obere Grenze: abs(value), um die Höhe richtig darzustellen + color=color, alpha=0.3, - label="Discharge Possibility" if hour == 0 else "", + label=label ) - for hour, value in enumerate(laden_moeglich): - ax1.axvspan( - hour, - hour + 1, - color="green", - ymax=value, - alpha=0.3, - label="Charging Possibility" if hour == 0 else "", + + # Annotieren der Werte in der Mitte des Farbbereichs + ax1.text( + hour + 0.5, # In der Mitte des Bereichs + abs(value) / 2, # In der Mitte der Höhe + f'{value:.2f}', # Wert mit zwei Dezimalstellen + ha='center', + va='center', + fontsize=8, + color='black' ) - ax1.legend(loc="upper left") - ax1.set_xlim(0, prediction_hours) + pdf.savefig() # Save the current figure state to the PDF plt.close() # Close the current figure to free up memory @@ -192,26 +206,47 @@ def visualisiere_ergebnisse( losses = ergebnisse["Gesamt_Verluste"] # Costs and revenues per hour on the first axis (axs[0]) + costs = ergebnisse["Kosten_Euro_pro_Stunde"] + revenues = ergebnisse["Einnahmen_Euro_pro_Stunde"] + + # Plot costs axs[0].plot( hours, - ergebnisse["Kosten_Euro_pro_Stunde"], + costs, label="Costs (Euro)", marker="o", color="red", ) + # Annotate costs + for hour, value in enumerate(costs): + print(hour, " ", value) + if value == None or np.isnan(value): + value=0 + axs[0].annotate(f'{value:.2f}', (hour, value), textcoords="offset points", xytext=(0,5), ha='center', fontsize=8, color='red') + + # Plot revenues axs[0].plot( hours, - ergebnisse["Einnahmen_Euro_pro_Stunde"], + revenues, label="Revenue (Euro)", marker="x", color="green", ) + # Annotate revenues + for hour, value in enumerate(revenues): + if value == None or np.isnan(value): + value=0 + axs[0].annotate(f'{value:.2f}', (hour, value), textcoords="offset points", xytext=(0,5), ha='center', fontsize=8, color='green') + + # Title and labels axs[0].set_title("Financial Balance per Hour") axs[0].set_xlabel("Hour") axs[0].set_ylabel("Euro") axs[0].legend() axs[0].grid(True) + + # Summary of finances on the second axis (axs[1]) labels = ["Total Costs [€]", "Total Revenue [€]", "Total Balance [€]"] values = [total_costs, total_revenue, total_balance]