diff --git a/src/akkudoktoreos/config.py b/src/akkudoktoreos/config.py index 442d362..2071e79 100644 --- a/src/akkudoktoreos/config.py +++ b/src/akkudoktoreos/config.py @@ -56,6 +56,7 @@ class EOSConfig(BaseModel): penalty: int available_charging_rates_in_percentage: list[float] feed_in_tariff_eur_per_wh: int + electricty_price_fixed_fee: float class BaseConfig(BaseModel): diff --git a/src/akkudoktoreos/default.config.json b/src/akkudoktoreos/default.config.json index d05c20a..2d9f1ac 100644 --- a/src/akkudoktoreos/default.config.json +++ b/src/akkudoktoreos/default.config.json @@ -10,6 +10,7 @@ "available_charging_rates_in_percentage": [ 0.0, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0 ], - "feed_in_tariff_eur_per_wh": 48 + "feed_in_tariff_eur_per_wh": 48, + "electricty_price_fixed_fee": 0.00021 } } diff --git a/src/akkudoktoreos/optimization/genetic.py b/src/akkudoktoreos/optimization/genetic.py index 871bb2d..4f31c39 100644 --- a/src/akkudoktoreos/optimization/genetic.py +++ b/src/akkudoktoreos/optimization/genetic.py @@ -1,13 +1,13 @@ import random -from tabnanny import verbose -from typing import Any, Optional, Tuple import time +from pathlib import Path +from typing import Any, Optional, Tuple + import numpy as np from deap import algorithms, base, creator, tools from pydantic import BaseModel, Field, field_validator, model_validator from typing_extensions import Self -from pathlib import Path -import sys + from akkudoktoreos.config import AppConfig from akkudoktoreos.devices.battery import ( EAutoParameters, @@ -15,9 +15,6 @@ from akkudoktoreos.devices.battery import ( PVAkku, PVAkkuParameters, ) -from akkudoktoreos.prediction.self_consumption_probability import ( - self_consumption_probability_interpolator, -) from akkudoktoreos.devices.generic import HomeAppliance, HomeApplianceParameters from akkudoktoreos.devices.inverter import Wechselrichter, WechselrichterParameters from akkudoktoreos.prediction.ems import ( @@ -25,6 +22,9 @@ from akkudoktoreos.prediction.ems import ( EnergieManagementSystemParameters, SimulationResult, ) +from akkudoktoreos.prediction.self_consumption_probability import ( + self_consumption_probability_interpolator, +) from akkudoktoreos.utils.utils import NumpyEncoder from akkudoktoreos.visualize import visualisiere_ergebnisse @@ -126,7 +126,7 @@ class optimization_problem: random.seed(fixed_seed) def decode_charge_discharge( - self, discharge_hours_bin: list[int] + self, discharge_hours_bin: np.ndarray ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: """Decode the input array into ac_charge, dc_charge, and discharge arrays.""" discharge_hours_bin_np = np.array(discharge_hours_bin) @@ -229,8 +229,7 @@ class optimization_problem: eautocharge_hours_index: Optional[np.ndarray], washingstart_int: Optional[int], ) -> list[int]: - """ - Merge the individual components back into a single solution list. + """Merge the individual components back into a single solution list. Parameters: discharge_hours_bin (np.ndarray): Binary discharge hours. @@ -262,8 +261,7 @@ class optimization_problem: def split_individual( self, individual: list[int] ) -> Tuple[np.ndarray, Optional[np.ndarray], Optional[int]]: - """ - Split the individual solution into its components. + """Split the individual solution into its components. Components: 1. Discharge hours (binary as int NumPy array), @@ -398,7 +396,6 @@ class optimization_problem: worst_case: bool, ) -> Tuple[float]: """Evaluate the fitness of an individual solution based on the simulation results.""" - try: o = self.evaluate_inner(individual, ems, start_hour) except Exception as e: @@ -414,36 +411,29 @@ class optimization_problem: if self.optimize_ev: eauto_soc_per_hour = np.array(o.get("EAuto_SoC_pro_Stunde", [])) # Beispielkey - # Angleichung von hinten - min_length = min(len(eauto_soc_per_hour), len(eautocharge_hours_index)) + 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") + min_length = min(eauto_soc_per_hour.size, eautocharge_hours_index.size) eauto_soc_per_hour_tail = eauto_soc_per_hour[-min_length:] - eautocharge_hours_index_tail = np.array(eautocharge_hours_index[-min_length:]) + eautocharge_hours_index_tail = eautocharge_hours_index[-min_length:] - # Erstelle die Maske für die relevanten Abschnitte + # Mask invalid_charge_mask = (eauto_soc_per_hour_tail == 100) & ( eautocharge_hours_index_tail > 0 ) - # Überprüfen und anpassen der Ladezeiten if np.any(invalid_charge_mask): - # Ignoriere den ersten ungültigen Eintrag invalid_indices = np.where(invalid_charge_mask)[0] - if ( - len(invalid_indices) > 1 - ): # Nur anpassen, wenn mehr als ein ungültiger Eintrag vorliegt + if len(invalid_indices) > 1: eautocharge_hours_index_tail[invalid_indices[1:]] = 0 - # Aktualisiere die letzten min_length-Einträge von eautocharge_hours_index eautocharge_hours_index[-min_length:] = eautocharge_hours_index_tail.tolist() - # Rückschreiben der Anpassungen in `individual` adjusted_individual = self.merge_individual( discharge_hours_bin, eautocharge_hours_index, washingstart_int ) - # print("Vor:", individual) individual[:] = adjusted_individual # Aktualisiere das ursprüngliche individual - # print("Nach:", individual)# # Berechnung weiterer Metriken individual.extra_data = ( # type: ignore[attr-defined] @@ -472,7 +462,7 @@ class optimization_problem: return (gesamtbilanz,) def optimize( - self, start_solution: Optional[list[float]] = None, ngen: int = 400 + self, start_solution: Optional[list[float]] = None, ngen: int = 200 ) -> Tuple[Any, dict[str, list[Any]]]: """Run the optimization process using a genetic algorithm.""" population = self.toolbox.population(n=300) @@ -588,13 +578,11 @@ class optimization_problem: elapsed_time = time.time() - start_time print(f"Time evaluate inner: {elapsed_time:.4f} sec.") # Perform final evaluation on the best solution - print(start_solution[start_hour:]) - print(start_hour) + o = self.evaluate_inner(start_solution, ems, start_hour) discharge_hours_bin, eautocharge_hours_index, washingstart_int = self.split_individual( start_solution ) - eautocharge_hours_float = ( [ self._config.eos.available_charging_rates_in_percentage[i] @@ -620,7 +608,6 @@ class optimization_problem: config=self._config, extra_data=extra_data, ) - return OptimizeResponse( **{ "ac_charge": ac_charge, diff --git a/src/akkudoktoreos/prediction/ems.py b/src/akkudoktoreos/prediction/ems.py index 833142c..e9998d4 100644 --- a/src/akkudoktoreos/prediction/ems.py +++ b/src/akkudoktoreos/prediction/ems.py @@ -78,6 +78,9 @@ class SimulationResult(BaseModel): akku_soc_pro_stunde: list[Optional[float]] = Field( description="The state of charge of the battery (not the EV) in percentage per hour." ) + Electricity_price: list[Optional[float]] = Field( + description="Used Electricity Price, including predictions" + ) @field_validator( "Last_Wh_pro_Stunde", @@ -89,6 +92,7 @@ class SimulationResult(BaseModel): "EAuto_SoC_pro_Stunde", "Verluste_Pro_Stunde", "Home_appliance_wh_per_hour", + "Electricity_price", mode="before", ) def convert_numpy(cls, field: Any) -> Any: @@ -171,6 +175,7 @@ class EnergieManagementSystem: eauto_soc_pro_stunde = np.full((total_hours), np.nan) verluste_wh_pro_stunde = np.full((total_hours), np.nan) home_appliance_wh_per_hour = np.full((total_hours), np.nan) + electricity_price_per_hour = np.full((total_hours), np.nan) # Set initial state akku_soc_pro_stunde[0] = self.akku.ladezustand_in_prozent() @@ -222,7 +227,7 @@ class EnergieManagementSystem: netzbezug_wh_pro_stunde[stunde_since_now] = netzbezug verluste_wh_pro_stunde[stunde_since_now] += verluste last_wh_pro_stunde[stunde_since_now] = verbrauch - + electricity_price_per_hour[stunde_since_now] = self.strompreis_euro_pro_wh[stunde] # Financial calculations kosten_euro_pro_stunde[stunde_since_now] = ( netzbezug * self.strompreis_euro_pro_wh[stunde] @@ -252,6 +257,6 @@ class EnergieManagementSystem: "Verluste_Pro_Stunde": verluste_wh_pro_stunde, "Gesamt_Verluste": np.nansum(verluste_wh_pro_stunde), "Home_appliance_wh_per_hour": home_appliance_wh_per_hour, + "Electricity_price": electricity_price_per_hour, } - return out diff --git a/src/akkudoktoreos/prediction/price_forecast.py b/src/akkudoktoreos/prediction/price_forecast.py index c1701fe..e2313d2 100644 --- a/src/akkudoktoreos/prediction/price_forecast.py +++ b/src/akkudoktoreos/prediction/price_forecast.py @@ -3,7 +3,8 @@ import json import zoneinfo from datetime import datetime, timedelta, timezone from pathlib import Path -from typing import Any, Sequence, Optional +from typing import Any, Optional, Sequence + import numpy as np import requests @@ -28,7 +29,7 @@ class HourlyElectricityPriceForecast: self, source: str | Path, config: AppConfig, - charges: float = 0.000228, + charges: float = 0.00021, use_cache: bool = True, ): # 228 self.cache_dir = config.working_dir / config.directories.cache @@ -36,7 +37,7 @@ class HourlyElectricityPriceForecast: if not self.cache_dir.is_dir(): raise SetupIncomplete(f"Output path does not exist: {self.cache_dir}.") - self.seven_day_mean = None + self.seven_day_mean = np.array([]) self.cache_time_file = self.cache_dir / "cache_timestamp.txt" self.prices = self.load_data(source) self.charges = charges @@ -97,7 +98,7 @@ class HourlyElectricityPriceForecast: # Extract the price from 00:00 of the previous day previous_day_prices = [ - entry["marketpriceEurocentPerKWh"] + self.charges + entry["marketpriceEurocentPerKWh"] # + self.charges for entry in self.prices if previous_day_str in entry["end"] ] @@ -105,21 +106,22 @@ class HourlyElectricityPriceForecast: # Extract all prices for the specified date date_prices = [ - entry["marketpriceEurocentPerKWh"] + self.charges + entry["marketpriceEurocentPerKWh"] # + self.charges for entry in self.prices if date_str in entry["end"] ] - print(f"getPrice: {len(date_prices)}") + # print(f"getPrice: {len(date_prices)}") # Add the last price of the previous day at the start of the list if len(date_prices) == 23: date_prices.insert(0, last_price_of_previous_day) - + # print(np.array(date_prices) / (1000.0 * 100.0)) + # print("PRICE:") + # print(np.array(date_prices) / (1000.0 * 100.0) + self.charges) return np.array(date_prices) / (1000.0 * 100.0) + self.charges def get_average_price_last_7_days(self, end_date_str: Optional[str] = None) -> np.ndarray: - """ - Calculate the hourly average electricity price for the last 7 days. + """Calculate the hourly average electricity price for the last 7 days. Parameters: end_date_str (Optional[str]): End date in the format "YYYY-MM-DD". @@ -138,8 +140,8 @@ class HourlyElectricityPriceForecast: else: end_date = datetime.strptime(end_date_str, "%Y-%m-%d").date() - if self.seven_day_mean != None: - return self.seven_day_mean + if self.seven_day_mean.size > 0: + return np.array([self.seven_day_mean]) # Calculate the start date (7 days before the end date) start_date = end_date - timedelta(days=7) @@ -176,7 +178,6 @@ class HourlyElectricityPriceForecast: self, start_date_str: str, end_date_str: str, repeat: bool = False ) -> np.ndarray: """Returns all prices between the start and end dates.""" - start_date_utc = datetime.strptime(start_date_str, "%Y-%m-%d").replace(tzinfo=timezone.utc) end_date_utc = datetime.strptime(end_date_str, "%Y-%m-%d").replace(tzinfo=timezone.utc) start_date = start_date_utc.astimezone(zoneinfo.ZoneInfo("Europe/Berlin")) @@ -194,9 +195,9 @@ class HourlyElectricityPriceForecast: # print(date_str, ":", daily_prices) price_list_np = np.array(price_list) - print(price_list_np) + # print(price_list_np.shape, " ", self.prediction_hours) # If prediction hours are greater than 0 and repeat is True - + # print(price_list_np) if self.prediction_hours > 0 and repeat: # Check if price_list_np is shorter than prediction_hours if price_list_np.size < self.prediction_hours: @@ -208,5 +209,5 @@ class HourlyElectricityPriceForecast: # Concatenate existing values with the repeated values price_list_np = np.concatenate((price_list_np, additional_values)) - + # print(price_list_np) return price_list_np diff --git a/src/akkudoktoreos/prediction/self_consumption_probability.py b/src/akkudoktoreos/prediction/self_consumption_probability.py index 65261dc..a97b03e 100644 --- a/src/akkudoktoreos/prediction/self_consumption_probability.py +++ b/src/akkudoktoreos/prediction/self_consumption_probability.py @@ -1,22 +1,24 @@ #!/usr/bin/env python -import numpy as np import pickle from functools import lru_cache # from scipy.interpolate import RegularGridInterpolator from pathlib import Path +from typing import Tuple + +import numpy as np class self_consumption_probability_interpolator: def __init__(self, filepath: str | Path): self.filepath = filepath - self.interpolator = None + # self.interpolator = None # Load the RegularGridInterpolator with open(self.filepath, "rb") as file: self.interpolator = pickle.load(file) @lru_cache(maxsize=128) - def generate_points(self, load_1h_power: float, pv_power: float): + def generate_points(self, load_1h_power: float, pv_power: float) -> Tuple: """Generate the grid points for interpolation.""" partial_loads = np.arange(0, pv_power + 50, 50) points = np.array([np.full_like(partial_loads, load_1h_power), partial_loads]).T diff --git a/src/akkudoktoreos/server/fastapi_server.py b/src/akkudoktoreos/server/fastapi_server.py index 6b470ec..96b90f7 100755 --- a/src/akkudoktoreos/server/fastapi_server.py +++ b/src/akkudoktoreos/server/fastapi_server.py @@ -67,6 +67,7 @@ def fastapi_strompreis() -> list[float]: source=f"https://api.akkudoktor.net/prices?start={date_start}&end={date_end}", config=config, use_cache=False, + charges=config.eos.electricty_price_fixed_fee, ) # seven Day mean specific_date_prices = price_forecast.get_price_for_daterange( diff --git a/tests/test_class_ems.py b/tests/test_class_ems.py index 70c92b8..6a7c8d6 100644 --- a/tests/test_class_ems.py +++ b/tests/test_class_ems.py @@ -1,3 +1,5 @@ +from pathlib import Path + import numpy as np import pytest @@ -10,6 +12,9 @@ from akkudoktoreos.prediction.ems import ( EnergieManagementSystemParameters, SimulationResult, ) +from akkudoktoreos.prediction.self_consumption_probability import ( + self_consumption_probability_interpolator, +) prediction_hours = 48 optimization_hours = 24 @@ -25,8 +30,16 @@ def create_ems_instance(tmp_config: AppConfig) -> EnergieManagementSystem: PVAkkuParameters(kapazitaet_wh=5000, start_soc_prozent=80, min_soc_prozent=10), hours=prediction_hours, ) + + # 1h Load to Sub 1h Load Distribution -> SelfConsumptionRate + sc = self_consumption_probability_interpolator( + Path(__file__).parent.resolve() / ".." / "data" / "regular_grid_interpolator.pkl" + ) + akku.reset() - wechselrichter = Wechselrichter(WechselrichterParameters(max_leistung_wh=10000), akku) + wechselrichter = Wechselrichter( + WechselrichterParameters(max_leistung_wh=10000), akku, self_consumption_predictor=sc + ) # Household device (currently not used, set to None) home_appliance = HomeAppliance( diff --git a/tests/test_class_ems_2.py b/tests/test_class_ems_2.py index 11de678..9a05aa4 100644 --- a/tests/test_class_ems_2.py +++ b/tests/test_class_ems_2.py @@ -1,3 +1,5 @@ +from pathlib import Path + import numpy as np import pytest @@ -9,6 +11,9 @@ from akkudoktoreos.prediction.ems import ( EnergieManagementSystem, EnergieManagementSystemParameters, ) +from akkudoktoreos.prediction.self_consumption_probability import ( + self_consumption_probability_interpolator, +) prediction_hours = 48 optimization_hours = 24 @@ -24,8 +29,16 @@ def create_ems_instance(tmp_config: AppConfig) -> EnergieManagementSystem: PVAkkuParameters(kapazitaet_wh=5000, start_soc_prozent=80, min_soc_prozent=10), hours=prediction_hours, ) + + # 1h Load to Sub 1h Load Distribution -> SelfConsumptionRate + sc = self_consumption_probability_interpolator( + Path(__file__).parent.resolve() / ".." / "data" / "regular_grid_interpolator.pkl" + ) + akku.reset() - wechselrichter = Wechselrichter(WechselrichterParameters(max_leistung_wh=10000), akku) + wechselrichter = Wechselrichter( + WechselrichterParameters(max_leistung_wh=10000), akku, self_consumption_predictor=sc + ) # Household device (currently not used, set to None) home_appliance = HomeAppliance(