Price Prediction as JSON simulation output, config fixed electricty fees configurable + MyPy & Ruff

This commit is contained in:
Andreas 2024-12-21 13:26:41 +01:00 committed by Andreas
parent 43da234a06
commit 3f67da1326
9 changed files with 78 additions and 54 deletions

View File

@ -56,6 +56,7 @@ class EOSConfig(BaseModel):
penalty: int penalty: int
available_charging_rates_in_percentage: list[float] available_charging_rates_in_percentage: list[float]
feed_in_tariff_eur_per_wh: int feed_in_tariff_eur_per_wh: int
electricty_price_fixed_fee: float
class BaseConfig(BaseModel): class BaseConfig(BaseModel):

View File

@ -10,6 +10,7 @@
"available_charging_rates_in_percentage": [ "available_charging_rates_in_percentage": [
0.0, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0 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
} }
} }

View File

@ -1,13 +1,13 @@
import random import random
from tabnanny import verbose
from typing import Any, Optional, Tuple
import time import time
from pathlib import Path
from typing import Any, Optional, Tuple
import numpy as np import numpy as np
from deap import algorithms, base, creator, tools from deap import algorithms, base, creator, tools
from pydantic import BaseModel, Field, field_validator, model_validator from pydantic import BaseModel, Field, field_validator, model_validator
from typing_extensions import Self from typing_extensions import Self
from pathlib import Path
import sys
from akkudoktoreos.config import AppConfig from akkudoktoreos.config import AppConfig
from akkudoktoreos.devices.battery import ( from akkudoktoreos.devices.battery import (
EAutoParameters, EAutoParameters,
@ -15,9 +15,6 @@ from akkudoktoreos.devices.battery import (
PVAkku, PVAkku,
PVAkkuParameters, PVAkkuParameters,
) )
from akkudoktoreos.prediction.self_consumption_probability import (
self_consumption_probability_interpolator,
)
from akkudoktoreos.devices.generic import HomeAppliance, HomeApplianceParameters from akkudoktoreos.devices.generic import HomeAppliance, HomeApplianceParameters
from akkudoktoreos.devices.inverter import Wechselrichter, WechselrichterParameters from akkudoktoreos.devices.inverter import Wechselrichter, WechselrichterParameters
from akkudoktoreos.prediction.ems import ( from akkudoktoreos.prediction.ems import (
@ -25,6 +22,9 @@ from akkudoktoreos.prediction.ems import (
EnergieManagementSystemParameters, EnergieManagementSystemParameters,
SimulationResult, SimulationResult,
) )
from akkudoktoreos.prediction.self_consumption_probability import (
self_consumption_probability_interpolator,
)
from akkudoktoreos.utils.utils import NumpyEncoder from akkudoktoreos.utils.utils import NumpyEncoder
from akkudoktoreos.visualize import visualisiere_ergebnisse from akkudoktoreos.visualize import visualisiere_ergebnisse
@ -126,7 +126,7 @@ class optimization_problem:
random.seed(fixed_seed) random.seed(fixed_seed)
def decode_charge_discharge( def decode_charge_discharge(
self, discharge_hours_bin: list[int] self, discharge_hours_bin: np.ndarray
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
"""Decode the input array into ac_charge, dc_charge, and discharge arrays.""" """Decode the input array into ac_charge, dc_charge, and discharge arrays."""
discharge_hours_bin_np = np.array(discharge_hours_bin) discharge_hours_bin_np = np.array(discharge_hours_bin)
@ -229,8 +229,7 @@ class optimization_problem:
eautocharge_hours_index: Optional[np.ndarray], eautocharge_hours_index: Optional[np.ndarray],
washingstart_int: Optional[int], washingstart_int: Optional[int],
) -> list[int]: ) -> list[int]:
""" """Merge the individual components back into a single solution list.
Merge the individual components back into a single solution list.
Parameters: Parameters:
discharge_hours_bin (np.ndarray): Binary discharge hours. discharge_hours_bin (np.ndarray): Binary discharge hours.
@ -262,8 +261,7 @@ class optimization_problem:
def split_individual( def split_individual(
self, individual: list[int] self, individual: list[int]
) -> Tuple[np.ndarray, Optional[np.ndarray], Optional[int]]: ) -> Tuple[np.ndarray, Optional[np.ndarray], Optional[int]]:
""" """Split the individual solution into its components.
Split the individual solution into its components.
Components: Components:
1. Discharge hours (binary as int NumPy array), 1. Discharge hours (binary as int NumPy array),
@ -398,7 +396,6 @@ class optimization_problem:
worst_case: bool, worst_case: bool,
) -> Tuple[float]: ) -> Tuple[float]:
"""Evaluate the fitness of an individual solution based on the simulation results.""" """Evaluate the fitness of an individual solution based on the simulation results."""
try: try:
o = self.evaluate_inner(individual, ems, start_hour) o = self.evaluate_inner(individual, ems, start_hour)
except Exception as e: except Exception as e:
@ -414,36 +411,29 @@ class optimization_problem:
if self.optimize_ev: if self.optimize_ev:
eauto_soc_per_hour = np.array(o.get("EAuto_SoC_pro_Stunde", [])) # Beispielkey eauto_soc_per_hour = np.array(o.get("EAuto_SoC_pro_Stunde", [])) # Beispielkey
# Angleichung von hinten if eauto_soc_per_hour is None or eautocharge_hours_index is None:
min_length = min(len(eauto_soc_per_hour), len(eautocharge_hours_index)) 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:] 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) & ( invalid_charge_mask = (eauto_soc_per_hour_tail == 100) & (
eautocharge_hours_index_tail > 0 eautocharge_hours_index_tail > 0
) )
# Überprüfen und anpassen der Ladezeiten
if np.any(invalid_charge_mask): if np.any(invalid_charge_mask):
# Ignoriere den ersten ungültigen Eintrag
invalid_indices = np.where(invalid_charge_mask)[0] invalid_indices = np.where(invalid_charge_mask)[0]
if ( if len(invalid_indices) > 1:
len(invalid_indices) > 1
): # Nur anpassen, wenn mehr als ein ungültiger Eintrag vorliegt
eautocharge_hours_index_tail[invalid_indices[1:]] = 0 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() eautocharge_hours_index[-min_length:] = eautocharge_hours_index_tail.tolist()
# Rückschreiben der Anpassungen in `individual`
adjusted_individual = self.merge_individual( adjusted_individual = self.merge_individual(
discharge_hours_bin, eautocharge_hours_index, washingstart_int discharge_hours_bin, eautocharge_hours_index, washingstart_int
) )
# print("Vor:", individual)
individual[:] = adjusted_individual # Aktualisiere das ursprüngliche individual individual[:] = adjusted_individual # Aktualisiere das ursprüngliche individual
# print("Nach:", individual)#
# Berechnung weiterer Metriken # Berechnung weiterer Metriken
individual.extra_data = ( # type: ignore[attr-defined] individual.extra_data = ( # type: ignore[attr-defined]
@ -472,7 +462,7 @@ class optimization_problem:
return (gesamtbilanz,) return (gesamtbilanz,)
def optimize( 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]]]: ) -> Tuple[Any, dict[str, list[Any]]]:
"""Run the optimization process using a genetic algorithm.""" """Run the optimization process using a genetic algorithm."""
population = self.toolbox.population(n=300) population = self.toolbox.population(n=300)
@ -588,13 +578,11 @@ class optimization_problem:
elapsed_time = time.time() - start_time elapsed_time = time.time() - start_time
print(f"Time evaluate inner: {elapsed_time:.4f} sec.") print(f"Time evaluate inner: {elapsed_time:.4f} sec.")
# Perform final evaluation on the best solution # Perform final evaluation on the best solution
print(start_solution[start_hour:])
print(start_hour)
o = self.evaluate_inner(start_solution, ems, start_hour) o = self.evaluate_inner(start_solution, ems, start_hour)
discharge_hours_bin, eautocharge_hours_index, washingstart_int = self.split_individual( discharge_hours_bin, eautocharge_hours_index, washingstart_int = self.split_individual(
start_solution start_solution
) )
eautocharge_hours_float = ( eautocharge_hours_float = (
[ [
self._config.eos.available_charging_rates_in_percentage[i] self._config.eos.available_charging_rates_in_percentage[i]
@ -620,7 +608,6 @@ class optimization_problem:
config=self._config, config=self._config,
extra_data=extra_data, extra_data=extra_data,
) )
return OptimizeResponse( return OptimizeResponse(
**{ **{
"ac_charge": ac_charge, "ac_charge": ac_charge,

View File

@ -78,6 +78,9 @@ class SimulationResult(BaseModel):
akku_soc_pro_stunde: list[Optional[float]] = Field( akku_soc_pro_stunde: list[Optional[float]] = Field(
description="The state of charge of the battery (not the EV) in percentage per hour." 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( @field_validator(
"Last_Wh_pro_Stunde", "Last_Wh_pro_Stunde",
@ -89,6 +92,7 @@ class SimulationResult(BaseModel):
"EAuto_SoC_pro_Stunde", "EAuto_SoC_pro_Stunde",
"Verluste_Pro_Stunde", "Verluste_Pro_Stunde",
"Home_appliance_wh_per_hour", "Home_appliance_wh_per_hour",
"Electricity_price",
mode="before", mode="before",
) )
def convert_numpy(cls, field: Any) -> Any: def convert_numpy(cls, field: Any) -> Any:
@ -171,6 +175,7 @@ class EnergieManagementSystem:
eauto_soc_pro_stunde = np.full((total_hours), np.nan) eauto_soc_pro_stunde = np.full((total_hours), np.nan)
verluste_wh_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) home_appliance_wh_per_hour = np.full((total_hours), np.nan)
electricity_price_per_hour = np.full((total_hours), np.nan)
# Set initial state # Set initial state
akku_soc_pro_stunde[0] = self.akku.ladezustand_in_prozent() akku_soc_pro_stunde[0] = self.akku.ladezustand_in_prozent()
@ -222,7 +227,7 @@ class EnergieManagementSystem:
netzbezug_wh_pro_stunde[stunde_since_now] = netzbezug netzbezug_wh_pro_stunde[stunde_since_now] = netzbezug
verluste_wh_pro_stunde[stunde_since_now] += verluste verluste_wh_pro_stunde[stunde_since_now] += verluste
last_wh_pro_stunde[stunde_since_now] = verbrauch last_wh_pro_stunde[stunde_since_now] = verbrauch
electricity_price_per_hour[stunde_since_now] = self.strompreis_euro_pro_wh[stunde]
# Financial calculations # Financial calculations
kosten_euro_pro_stunde[stunde_since_now] = ( kosten_euro_pro_stunde[stunde_since_now] = (
netzbezug * self.strompreis_euro_pro_wh[stunde] netzbezug * self.strompreis_euro_pro_wh[stunde]
@ -252,6 +257,6 @@ class EnergieManagementSystem:
"Verluste_Pro_Stunde": verluste_wh_pro_stunde, "Verluste_Pro_Stunde": verluste_wh_pro_stunde,
"Gesamt_Verluste": np.nansum(verluste_wh_pro_stunde), "Gesamt_Verluste": np.nansum(verluste_wh_pro_stunde),
"Home_appliance_wh_per_hour": home_appliance_wh_per_hour, "Home_appliance_wh_per_hour": home_appliance_wh_per_hour,
"Electricity_price": electricity_price_per_hour,
} }
return out return out

View File

@ -3,7 +3,8 @@ import json
import zoneinfo import zoneinfo
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from pathlib import Path from pathlib import Path
from typing import Any, Sequence, Optional from typing import Any, Optional, Sequence
import numpy as np import numpy as np
import requests import requests
@ -28,7 +29,7 @@ class HourlyElectricityPriceForecast:
self, self,
source: str | Path, source: str | Path,
config: AppConfig, config: AppConfig,
charges: float = 0.000228, charges: float = 0.00021,
use_cache: bool = True, use_cache: bool = True,
): # 228 ): # 228
self.cache_dir = config.working_dir / config.directories.cache self.cache_dir = config.working_dir / config.directories.cache
@ -36,7 +37,7 @@ class HourlyElectricityPriceForecast:
if not self.cache_dir.is_dir(): if not self.cache_dir.is_dir():
raise SetupIncomplete(f"Output path does not exist: {self.cache_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.cache_time_file = self.cache_dir / "cache_timestamp.txt"
self.prices = self.load_data(source) self.prices = self.load_data(source)
self.charges = charges self.charges = charges
@ -97,7 +98,7 @@ class HourlyElectricityPriceForecast:
# Extract the price from 00:00 of the previous day # Extract the price from 00:00 of the previous day
previous_day_prices = [ previous_day_prices = [
entry["marketpriceEurocentPerKWh"] + self.charges entry["marketpriceEurocentPerKWh"] # + self.charges
for entry in self.prices for entry in self.prices
if previous_day_str in entry["end"] if previous_day_str in entry["end"]
] ]
@ -105,21 +106,22 @@ class HourlyElectricityPriceForecast:
# Extract all prices for the specified date # Extract all prices for the specified date
date_prices = [ date_prices = [
entry["marketpriceEurocentPerKWh"] + self.charges entry["marketpriceEurocentPerKWh"] # + self.charges
for entry in self.prices for entry in self.prices
if date_str in entry["end"] 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 # Add the last price of the previous day at the start of the list
if len(date_prices) == 23: if len(date_prices) == 23:
date_prices.insert(0, last_price_of_previous_day) 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 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: 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: Parameters:
end_date_str (Optional[str]): End date in the format "YYYY-MM-DD". end_date_str (Optional[str]): End date in the format "YYYY-MM-DD".
@ -138,8 +140,8 @@ class HourlyElectricityPriceForecast:
else: else:
end_date = datetime.strptime(end_date_str, "%Y-%m-%d").date() end_date = datetime.strptime(end_date_str, "%Y-%m-%d").date()
if self.seven_day_mean != None: if self.seven_day_mean.size > 0:
return self.seven_day_mean return np.array([self.seven_day_mean])
# Calculate the start date (7 days before the end date) # Calculate the start date (7 days before the end date)
start_date = end_date - timedelta(days=7) 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 self, start_date_str: str, end_date_str: str, repeat: bool = False
) -> np.ndarray: ) -> np.ndarray:
"""Returns all prices between the start and end dates.""" """Returns all prices between the start and end dates."""
start_date_utc = datetime.strptime(start_date_str, "%Y-%m-%d").replace(tzinfo=timezone.utc) 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) 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")) start_date = start_date_utc.astimezone(zoneinfo.ZoneInfo("Europe/Berlin"))
@ -194,9 +195,9 @@ class HourlyElectricityPriceForecast:
# print(date_str, ":", daily_prices) # print(date_str, ":", daily_prices)
price_list_np = np.array(price_list) 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 # If prediction hours are greater than 0 and repeat is True
# print(price_list_np)
if self.prediction_hours > 0 and repeat: if self.prediction_hours > 0 and repeat:
# Check if price_list_np is shorter than prediction_hours # Check if price_list_np is shorter than prediction_hours
if price_list_np.size < self.prediction_hours: if price_list_np.size < self.prediction_hours:
@ -208,5 +209,5 @@ class HourlyElectricityPriceForecast:
# Concatenate existing values with the repeated values # Concatenate existing values with the repeated values
price_list_np = np.concatenate((price_list_np, additional_values)) price_list_np = np.concatenate((price_list_np, additional_values))
# print(price_list_np)
return price_list_np return price_list_np

View File

@ -1,22 +1,24 @@
#!/usr/bin/env python #!/usr/bin/env python
import numpy as np
import pickle import pickle
from functools import lru_cache from functools import lru_cache
# from scipy.interpolate import RegularGridInterpolator # from scipy.interpolate import RegularGridInterpolator
from pathlib import Path from pathlib import Path
from typing import Tuple
import numpy as np
class self_consumption_probability_interpolator: class self_consumption_probability_interpolator:
def __init__(self, filepath: str | Path): def __init__(self, filepath: str | Path):
self.filepath = filepath self.filepath = filepath
self.interpolator = None # self.interpolator = None
# Load the RegularGridInterpolator # Load the RegularGridInterpolator
with open(self.filepath, "rb") as file: with open(self.filepath, "rb") as file:
self.interpolator = pickle.load(file) self.interpolator = pickle.load(file)
@lru_cache(maxsize=128) @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.""" """Generate the grid points for interpolation."""
partial_loads = np.arange(0, pv_power + 50, 50) partial_loads = np.arange(0, pv_power + 50, 50)
points = np.array([np.full_like(partial_loads, load_1h_power), partial_loads]).T points = np.array([np.full_like(partial_loads, load_1h_power), partial_loads]).T

View File

@ -67,6 +67,7 @@ def fastapi_strompreis() -> list[float]:
source=f"https://api.akkudoktor.net/prices?start={date_start}&end={date_end}", source=f"https://api.akkudoktor.net/prices?start={date_start}&end={date_end}",
config=config, config=config,
use_cache=False, use_cache=False,
charges=config.eos.electricty_price_fixed_fee,
) )
# seven Day mean # seven Day mean
specific_date_prices = price_forecast.get_price_for_daterange( specific_date_prices = price_forecast.get_price_for_daterange(

View File

@ -1,3 +1,5 @@
from pathlib import Path
import numpy as np import numpy as np
import pytest import pytest
@ -10,6 +12,9 @@ from akkudoktoreos.prediction.ems import (
EnergieManagementSystemParameters, EnergieManagementSystemParameters,
SimulationResult, SimulationResult,
) )
from akkudoktoreos.prediction.self_consumption_probability import (
self_consumption_probability_interpolator,
)
prediction_hours = 48 prediction_hours = 48
optimization_hours = 24 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), PVAkkuParameters(kapazitaet_wh=5000, start_soc_prozent=80, min_soc_prozent=10),
hours=prediction_hours, 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() 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) # Household device (currently not used, set to None)
home_appliance = HomeAppliance( home_appliance = HomeAppliance(

View File

@ -1,3 +1,5 @@
from pathlib import Path
import numpy as np import numpy as np
import pytest import pytest
@ -9,6 +11,9 @@ from akkudoktoreos.prediction.ems import (
EnergieManagementSystem, EnergieManagementSystem,
EnergieManagementSystemParameters, EnergieManagementSystemParameters,
) )
from akkudoktoreos.prediction.self_consumption_probability import (
self_consumption_probability_interpolator,
)
prediction_hours = 48 prediction_hours = 48
optimization_hours = 24 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), PVAkkuParameters(kapazitaet_wh=5000, start_soc_prozent=80, min_soc_prozent=10),
hours=prediction_hours, 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() 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) # Household device (currently not used, set to None)
home_appliance = HomeAppliance( home_appliance = HomeAppliance(