mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2026-10-15 10:31:57 +00:00
feat: rename genetic optimization API fields to English (#675)
Bump Version / Bump Version Workflow (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
Bump Version / Bump Version Workflow (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
Rename the German field names of the genetic optimization API to English with full backward compatibility: - English names are canonical and documented in the OpenAPI schema; the German names are still accepted on input via validation aliases and re-emitted in responses as deprecated computed fields - fix visualization receiving the raw German-keyed simulation dict (KeyError: load_wh_per_hour) - rename internal German identifiers (optimize_ems, total_balance, battery_residual_value, self_consumption, extra_data keys) - use amount instead of currency/euro in chart labels and schema descriptions; document currency handling (whole currency units, never cents) in the API description - regenerate openapi.json and generated docs Co-authored-by: Tobias Welz <tobias.wizneteu@gmail.com>
This commit is contained in:
@@ -67,13 +67,13 @@ class GeneticSimulation(PydanticBaseModel):
|
||||
elect_price_hourly: Optional[NDArray[Shape["*"], float]] = Field(
|
||||
default=None,
|
||||
json_schema_extra={
|
||||
"description": "An array of floats representing the electricity price in euros per watt-hour for different time intervals."
|
||||
"description": "An array of floats representing the electricity price per watt-hour for different time intervals."
|
||||
},
|
||||
)
|
||||
elect_revenue_per_hour_arr: Optional[NDArray[Shape["*"], float]] = Field(
|
||||
default=None,
|
||||
json_schema_extra={
|
||||
"description": "An array of floats representing the feed-in compensation in euros per watt-hour."
|
||||
"description": "An array of floats representing the feed-in compensation per watt-hour."
|
||||
},
|
||||
)
|
||||
|
||||
@@ -121,15 +121,13 @@ class GeneticSimulation(PydanticBaseModel):
|
||||
self.prediction_hours = prediction_hours
|
||||
|
||||
# Load arrays from provided EMS parameters
|
||||
self.load_energy_array = np.array(parameters.gesamtlast, float)
|
||||
self.pv_prediction_wh = np.array(parameters.pv_prognose_wh, float)
|
||||
self.elect_price_hourly = np.array(parameters.strompreis_euro_pro_wh, float)
|
||||
self.load_energy_array = np.array(parameters.total_load, float)
|
||||
self.pv_prediction_wh = np.array(parameters.pv_forecast_wh, float)
|
||||
self.elect_price_hourly = np.array(parameters.electricity_price_per_wh, float)
|
||||
self.elect_revenue_per_hour_arr = (
|
||||
parameters.einspeiseverguetung_euro_pro_wh
|
||||
if isinstance(parameters.einspeiseverguetung_euro_pro_wh, list)
|
||||
else np.full(
|
||||
len(self.load_energy_array), parameters.einspeiseverguetung_euro_pro_wh, float
|
||||
)
|
||||
parameters.feed_in_tariff_per_wh
|
||||
if isinstance(parameters.feed_in_tariff_per_wh, list)
|
||||
else np.full(len(self.load_energy_array), parameters.feed_in_tariff_per_wh, float)
|
||||
)
|
||||
|
||||
# Associate devices
|
||||
@@ -159,8 +157,8 @@ class GeneticSimulation(PydanticBaseModel):
|
||||
def simulate(self, start_hour: int) -> dict[str, Any]:
|
||||
"""Simulate energy usage and costs for the given start hour.
|
||||
|
||||
akku_soc_pro_stunde begin of the hour, initial hour state!
|
||||
last_wh_pro_stunde integral of last hour (end state)
|
||||
battery_soc_per_hour begin of the hour, initial hour state!
|
||||
load_wh_per_hour integral of last hour (end state)
|
||||
"""
|
||||
# Remember start hour
|
||||
self.start_hour = start_hour
|
||||
@@ -328,11 +326,11 @@ class GeneticSimulation(PydanticBaseModel):
|
||||
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(
|
||||
loaded_energy_ev, ev_charge_losses = 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
|
||||
losses_wh_per_hour[hour_idx] += ev_charge_losses
|
||||
|
||||
# Save battery SOC before inverter processing = true begin-of-interval state.
|
||||
# Must be recorded here (before DC charge/discharge) so the displayed SOC at
|
||||
@@ -342,9 +340,9 @@ class GeneticSimulation(PydanticBaseModel):
|
||||
soc_per_hour[hour_idx] = battery_fast.current_soc_percentage()
|
||||
|
||||
# Process inverter logic
|
||||
energy_feedin_grid_actual = energy_consumption_grid_actual = losses = eigenverbrauch = (
|
||||
0.0
|
||||
)
|
||||
energy_feedin_grid_actual = energy_consumption_grid_actual = losses = (
|
||||
self_consumption
|
||||
) = 0.0
|
||||
|
||||
if inverter_fast:
|
||||
energy_produced = pv_prediction_wh_fast[hour]
|
||||
@@ -352,7 +350,7 @@ class GeneticSimulation(PydanticBaseModel):
|
||||
energy_feedin_grid_actual,
|
||||
energy_consumption_grid_actual,
|
||||
losses,
|
||||
eigenverbrauch,
|
||||
self_consumption,
|
||||
) = inverter_fast.process_energy(energy_produced, consumption, hour)
|
||||
|
||||
# AC PV Battery Charge
|
||||
@@ -442,7 +440,7 @@ class GeneticOptimization(OptimizationBase):
|
||||
)
|
||||
self.ev_possible_charge_values: list[float] = [1.0]
|
||||
# Separate charge-level list for battery AC charging (independent of EV rates).
|
||||
# Populated from parameters.pv_akku.charge_rates in optimierung_ems.
|
||||
# Populated from parameters.pv_akku.charge_rates in optimize_ems.
|
||||
self.bat_possible_charge_values: list[float] = [1.0]
|
||||
self.verbose = verbose
|
||||
self.fix_seed = fixed_seed
|
||||
@@ -585,14 +583,14 @@ class GeneticOptimization(OptimizationBase):
|
||||
if self.optimize_ev and eautocharge_hours_index is not None:
|
||||
individual.extend(eautocharge_hours_index.tolist())
|
||||
elif self.optimize_ev:
|
||||
# Falls optimize_ev aktiv ist, aber keine EV-Daten vorhanden sind, fügen wir Nullen hinzu
|
||||
# If optimize_ev is active but no EV data is available, append zeros
|
||||
individual.extend([0] * self.config.prediction.hours)
|
||||
|
||||
# Add dishwasher start time if applicable
|
||||
if self.opti_param.get("home_appliance", 0) > 0 and washingstart_int is not None:
|
||||
individual.append(washingstart_int)
|
||||
elif self.opti_param.get("home_appliance", 0) > 0:
|
||||
# Falls ein Haushaltsgerät optimiert wird, aber kein Startzeitpunkt vorhanden ist
|
||||
# If a home appliance is optimized but no start time is available
|
||||
individual.append(0)
|
||||
|
||||
return individual
|
||||
@@ -784,7 +782,7 @@ class GeneticOptimization(OptimizationBase):
|
||||
# Return bad fitness score ("FitnessMin") in case of an exception
|
||||
return (100000.0,)
|
||||
|
||||
gesamtbilanz = simulation_result["Gesamtbilanz_Euro"] * (-1.0 if worst_case else 1.0)
|
||||
total_balance = simulation_result["Gesamtbilanz_Euro"] * (-1.0 if worst_case else 1.0)
|
||||
|
||||
# EV 100% & charge not allowed
|
||||
if self.optimize_ev:
|
||||
@@ -846,7 +844,7 @@ class GeneticOptimization(OptimizationBase):
|
||||
# # discharge_hours_bin_tail[zero_soc_mask] = (
|
||||
# # len_ac + 2
|
||||
# # ) # Activate discharge for these hours
|
||||
# set_to_len_ac_plus_2 = np.random.rand() < 0.5 # True mit 50% Wahrscheinlichkeit
|
||||
# set_to_len_ac_plus_2 = np.random.rand() < 0.5 # True with 50% probability
|
||||
|
||||
# # Werte setzen basierend auf der zufälligen Entscheidung
|
||||
# value_to_set = len_ac + 2 if set_to_len_ac_plus_2 else 0
|
||||
@@ -874,8 +872,8 @@ class GeneticOptimization(OptimizationBase):
|
||||
# (stored DC energy must pass through inverter to be usable as AC)
|
||||
if self.simulation.inverter:
|
||||
battery_energy_content *= self.simulation.inverter.dc_to_ac_efficiency
|
||||
restwert_akku = battery_energy_content * parameters.ems.preis_euro_pro_wh_akku
|
||||
gesamtbilanz += -restwert_akku
|
||||
battery_residual_value = battery_energy_content * parameters.ems.price_per_wh_battery
|
||||
total_balance += -battery_residual_value
|
||||
|
||||
# --- AC charging break-even penalty ---
|
||||
# Penalise AC charging decisions that cannot be economically justified given the
|
||||
@@ -924,7 +922,7 @@ class GeneticOptimization(OptimizationBase):
|
||||
* inv.dc_to_ac_efficiency
|
||||
)
|
||||
|
||||
# Configurable penalty multiplier (default 1 = economic loss in €)
|
||||
# Configurable penalty multiplier (default 1 = economic loss in currency units)
|
||||
try:
|
||||
ac_penalty_factor = float(
|
||||
self.config.optimization.genetic.penalties["ac_charge_break_even"]
|
||||
@@ -971,7 +969,7 @@ class GeneticOptimization(OptimizationBase):
|
||||
dc_wh = bat.max_charge_power_w * ac_factor
|
||||
ac_wh = dc_wh / max(inv.ac_to_dc_efficiency, 1e-9)
|
||||
excess_cost_per_wh = break_even_price - best_uncovered_price
|
||||
gesamtbilanz += ac_wh * excess_cost_per_wh * ac_penalty_factor
|
||||
total_balance += ac_wh * excess_cost_per_wh * ac_penalty_factor
|
||||
|
||||
if self.optimize_ev and parameters.eauto and self.simulation.ev:
|
||||
try:
|
||||
@@ -987,11 +985,11 @@ class GeneticOptimization(OptimizationBase):
|
||||
ev_soc_percentage < parameters.eauto.min_soc_percentage
|
||||
or ev_soc_percentage > parameters.eauto.max_soc_percentage
|
||||
):
|
||||
gesamtbilanz += (
|
||||
total_balance += (
|
||||
abs(parameters.eauto.min_soc_percentage - ev_soc_percentage) * penalty
|
||||
)
|
||||
|
||||
return (gesamtbilanz,)
|
||||
return (total_balance,)
|
||||
|
||||
def optimize(
|
||||
self,
|
||||
@@ -1000,7 +998,7 @@ class GeneticOptimization(OptimizationBase):
|
||||
) -> tuple[Any, dict[str, list[Any]]]:
|
||||
"""Run the optimization process using a genetic algorithm.
|
||||
|
||||
@TODO: optimize() ngen default (200) is different from optimierung_ems() ngen default (400).
|
||||
@TODO: optimize() ngen default (200) is different from optimize_ems() ngen default (400).
|
||||
"""
|
||||
# Set the number of inviduals in a generation
|
||||
try:
|
||||
@@ -1047,17 +1045,17 @@ class GeneticOptimization(OptimizationBase):
|
||||
"min": log.select("min"), # Minimum fitness for each generation (Y-axis)
|
||||
}
|
||||
|
||||
member: dict[str, list[float]] = {"bilanz": [], "verluste": [], "nebenbedingung": []}
|
||||
member: dict[str, list[float]] = {"balance": [], "losses": [], "constraints": []}
|
||||
for ind in population:
|
||||
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)
|
||||
member["balance"].append(extra_value1)
|
||||
member["losses"].append(extra_value2)
|
||||
member["constraints"].append(extra_value3)
|
||||
|
||||
return hof[0], member
|
||||
|
||||
def optimierung_ems(
|
||||
def optimize_ems(
|
||||
self,
|
||||
parameters: GeneticOptimizationParameters,
|
||||
start_hour: Optional[int] = None,
|
||||
@@ -1082,28 +1080,24 @@ class GeneticOptimization(OptimizationBase):
|
||||
generations = 400
|
||||
logger.error("Generations not configured. Using {}.", generations)
|
||||
|
||||
einspeiseverguetung_euro_pro_wh = np.full(
|
||||
self.config.prediction.hours, parameters.ems.einspeiseverguetung_euro_pro_wh
|
||||
)
|
||||
|
||||
self.simulation.reset()
|
||||
|
||||
# Initialize PV and EV batteries
|
||||
akku: Optional[Battery] = None
|
||||
battery: Optional[Battery] = None
|
||||
if parameters.pv_akku:
|
||||
akku = Battery(
|
||||
battery = Battery(
|
||||
parameters.pv_akku,
|
||||
prediction_hours=self.config.prediction.hours,
|
||||
)
|
||||
akku.set_charge_per_hour(np.full(self.config.prediction.hours, 0))
|
||||
battery.set_charge_per_hour(np.full(self.config.prediction.hours, 0))
|
||||
|
||||
eauto: Optional[Battery] = None
|
||||
ev: Optional[Battery] = None
|
||||
if parameters.eauto:
|
||||
eauto = Battery(
|
||||
ev = Battery(
|
||||
parameters.eauto,
|
||||
prediction_hours=self.config.prediction.hours,
|
||||
)
|
||||
eauto.set_charge_per_hour(np.full(self.config.prediction.hours, 1))
|
||||
ev.set_charge_per_hour(np.full(self.config.prediction.hours, 1))
|
||||
self.optimize_ev = (
|
||||
parameters.eauto.min_soc_percentage - parameters.eauto.initial_soc_percentage >= 0
|
||||
)
|
||||
@@ -1172,7 +1166,7 @@ class GeneticOptimization(OptimizationBase):
|
||||
if parameters.inverter:
|
||||
inverter = Inverter(
|
||||
parameters.inverter,
|
||||
battery=akku,
|
||||
battery=battery,
|
||||
)
|
||||
|
||||
# Prepare device simulation
|
||||
@@ -1181,7 +1175,7 @@ class GeneticOptimization(OptimizationBase):
|
||||
optimization_hours=self.config.optimization.horizon_hours,
|
||||
prediction_hours=self.config.prediction.hours,
|
||||
inverter=inverter, # battery is part of inverter
|
||||
ev=eauto,
|
||||
ev=ev,
|
||||
home_appliance=dishwasher,
|
||||
)
|
||||
|
||||
@@ -1240,10 +1234,10 @@ class GeneticOptimization(OptimizationBase):
|
||||
"dc_charge": dc_charge_hours,
|
||||
"discharge_allowed": discharge,
|
||||
"eautocharge_hours_float": eautocharge_hours_float,
|
||||
"result": simulation_result,
|
||||
"result": GeneticSimulationResult(**simulation_result).model_dump(),
|
||||
"eauto_obj": self.simulation.ev.to_dict() if self.simulation.ev else None,
|
||||
"start_solution": start_solution,
|
||||
"spuelstart": washingstart_int,
|
||||
"washingstart": washingstart_int,
|
||||
"extra_data": extra_data,
|
||||
"fitness_history": self.fitness_history,
|
||||
"fixed_seed": self.fix_seed,
|
||||
|
||||
@@ -11,7 +11,14 @@ forecasts, and fallback defaults, preparing them for optimization runs.
|
||||
from typing import Optional, Union
|
||||
|
||||
from loguru import logger
|
||||
from pydantic import Field, field_validator, model_validator
|
||||
from pydantic import (
|
||||
AliasChoices,
|
||||
ConfigDict,
|
||||
Field,
|
||||
computed_field,
|
||||
field_validator,
|
||||
model_validator,
|
||||
)
|
||||
from typing_extensions import Self
|
||||
|
||||
from akkudoktoreos.core.coreabc import (
|
||||
@@ -37,32 +44,65 @@ from akkudoktoreos.utils.datetimeutil import to_duration
|
||||
class GeneticEnergyManagementParameters(GeneticParametersBaseModel):
|
||||
"""Encapsulates energy-related forecasts and costs used in GENETIC optimization."""
|
||||
|
||||
pv_prognose_wh: list[float] = Field(
|
||||
model_config = ConfigDict(populate_by_name=True, extra="ignore")
|
||||
|
||||
pv_forecast_wh: list[float] = Field(
|
||||
validation_alias=AliasChoices("pv_forecast_wh", "pv_prognose_wh"),
|
||||
json_schema_extra={
|
||||
"description": "An array of floats representing the forecasted photovoltaic output in watts for different time intervals."
|
||||
}
|
||||
},
|
||||
)
|
||||
strompreis_euro_pro_wh: list[float] = Field(
|
||||
electricity_price_per_wh: list[float] = Field(
|
||||
validation_alias=AliasChoices("electricity_price_per_wh", "strompreis_euro_pro_wh"),
|
||||
json_schema_extra={
|
||||
"description": "An array of floats representing the electricity price in euros per watt-hour for different time intervals."
|
||||
}
|
||||
"description": "An array of floats representing the electricity price per watt-hour for different time intervals."
|
||||
},
|
||||
)
|
||||
einspeiseverguetung_euro_pro_wh: Union[list[float], float] = Field(
|
||||
feed_in_tariff_per_wh: Union[list[float], float] = Field(
|
||||
validation_alias=AliasChoices("feed_in_tariff_per_wh", "einspeiseverguetung_euro_pro_wh"),
|
||||
json_schema_extra={
|
||||
"description": "A float or array of floats representing the feed-in compensation in euros per watt-hour."
|
||||
}
|
||||
"description": "A float or array of floats representing the feed-in compensation per watt-hour."
|
||||
},
|
||||
)
|
||||
preis_euro_pro_wh_akku: float = Field(
|
||||
price_per_wh_battery: float = Field(
|
||||
validation_alias=AliasChoices("price_per_wh_battery", "preis_euro_pro_wh_akku"),
|
||||
json_schema_extra={
|
||||
"description": "A float representing the cost of battery energy per watt-hour."
|
||||
}
|
||||
},
|
||||
)
|
||||
gesamtlast: list[float] = Field(
|
||||
total_load: list[float] = Field(
|
||||
validation_alias=AliasChoices("total_load", "gesamtlast"),
|
||||
json_schema_extra={
|
||||
"description": "An array of floats representing the total load (consumption) in watts for different time intervals."
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Computed fields for backward compatibility (deprecated German names)
|
||||
@computed_field(json_schema_extra={"deprecated": True})
|
||||
def pv_prognose_wh(self) -> list[float]:
|
||||
"""Deprecated: Use pv_forecast_wh instead."""
|
||||
return self.pv_forecast_wh
|
||||
|
||||
@computed_field(json_schema_extra={"deprecated": True})
|
||||
def strompreis_euro_pro_wh(self) -> list[float]:
|
||||
"""Deprecated: Use electricity_price_per_wh instead."""
|
||||
return self.electricity_price_per_wh
|
||||
|
||||
@computed_field(json_schema_extra={"deprecated": True})
|
||||
def einspeiseverguetung_euro_pro_wh(self) -> Union[list[float], float]:
|
||||
"""Deprecated: Use feed_in_tariff_per_wh instead."""
|
||||
return self.feed_in_tariff_per_wh
|
||||
|
||||
@computed_field(json_schema_extra={"deprecated": True})
|
||||
def preis_euro_pro_wh_akku(self) -> float:
|
||||
"""Deprecated: Use price_per_wh_battery instead."""
|
||||
return self.price_per_wh_battery
|
||||
|
||||
@computed_field(json_schema_extra={"deprecated": True})
|
||||
def gesamtlast(self) -> list[float]:
|
||||
"""Deprecated: Use total_load instead."""
|
||||
return self.total_load
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_list_length(self) -> Self:
|
||||
"""Validate that all input lists are of the same length.
|
||||
@@ -70,13 +110,13 @@ class GeneticEnergyManagementParameters(GeneticParametersBaseModel):
|
||||
Raises:
|
||||
ValueError: If input list lengths differ.
|
||||
"""
|
||||
pv_prognose_length = len(self.pv_prognose_wh)
|
||||
pv_forecast_length = len(self.pv_forecast_wh)
|
||||
if (
|
||||
pv_prognose_length != len(self.strompreis_euro_pro_wh)
|
||||
or pv_prognose_length != len(self.gesamtlast)
|
||||
pv_forecast_length != len(self.electricity_price_per_wh)
|
||||
or pv_forecast_length != len(self.total_load)
|
||||
or (
|
||||
isinstance(self.einspeiseverguetung_euro_pro_wh, list)
|
||||
and pv_prognose_length != len(self.einspeiseverguetung_euro_pro_wh)
|
||||
isinstance(self.feed_in_tariff_per_wh, list)
|
||||
and pv_forecast_length != len(self.feed_in_tariff_per_wh)
|
||||
)
|
||||
):
|
||||
raise ValueError("Input lists have different lengths")
|
||||
@@ -122,7 +162,7 @@ class GeneticOptimizationParameters(
|
||||
Raises:
|
||||
ValueError: If list lengths mismatch.
|
||||
"""
|
||||
arr_length = len(self.ems.pv_prognose_wh)
|
||||
arr_length = len(self.ems.pv_forecast_wh)
|
||||
if self.temperature_forecast is not None and arr_length != len(self.temperature_forecast):
|
||||
raise ValueError("Input lists have different lengths")
|
||||
return self
|
||||
@@ -615,11 +655,11 @@ class GeneticOptimizationParameters(
|
||||
try:
|
||||
oparams = GeneticOptimizationParameters(
|
||||
ems=GeneticEnergyManagementParameters(
|
||||
pv_prognose_wh=pvforecast_ac_power,
|
||||
strompreis_euro_pro_wh=elecprice_marketprice_wh,
|
||||
einspeiseverguetung_euro_pro_wh=feed_in_tariff_wh,
|
||||
gesamtlast=loadforecast_power_w,
|
||||
preis_euro_pro_wh_akku=battery_lcos_kwh / 1000,
|
||||
pv_forecast_wh=pvforecast_ac_power,
|
||||
electricity_price_per_wh=elecprice_marketprice_wh,
|
||||
feed_in_tariff_per_wh=feed_in_tariff_wh,
|
||||
total_load=loadforecast_power_w,
|
||||
price_per_wh_battery=battery_lcos_kwh / 1000,
|
||||
),
|
||||
temperature_forecast=weather_temp_air,
|
||||
pv_akku=battery_params,
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Any, Optional
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from loguru import logger
|
||||
from pydantic import Field, field_validator
|
||||
from pydantic import AliasChoices, ConfigDict, Field, computed_field, field_validator
|
||||
|
||||
from akkudoktoreos.core.coreabc import (
|
||||
ConfigMixin,
|
||||
@@ -86,62 +86,153 @@ class ElectricVehicleResult(DeviceOptimizeResult):
|
||||
class GeneticSimulationResult(GeneticParametersBaseModel):
|
||||
"""This object contains the results of the simulation and provides insights into various parameters over the entire forecast period."""
|
||||
|
||||
Last_Wh_pro_Stunde: list[float] = Field(json_schema_extra={"description": "TBD"})
|
||||
EAuto_SoC_pro_Stunde: list[float] = Field(
|
||||
json_schema_extra={"description": "The state of charge of the EV for each hour."}
|
||||
model_config = ConfigDict(populate_by_name=True, extra="ignore")
|
||||
|
||||
load_wh_per_hour: list[float] = Field(
|
||||
validation_alias=AliasChoices("load_wh_per_hour", "Last_Wh_pro_Stunde"),
|
||||
json_schema_extra={"description": "The load in watt-hours per hour."},
|
||||
)
|
||||
Einnahmen_Euro_pro_Stunde: list[float] = Field(
|
||||
ev_soc_per_hour: list[float] = Field(
|
||||
validation_alias=AliasChoices("ev_soc_per_hour", "EAuto_SoC_pro_Stunde"),
|
||||
json_schema_extra={"description": "The state of charge of the EV for each hour."},
|
||||
)
|
||||
revenue_per_hour: list[float] = Field(
|
||||
validation_alias=AliasChoices("revenue_per_hour", "Einnahmen_Euro_pro_Stunde"),
|
||||
json_schema_extra={
|
||||
"description": "The revenue from grid feed-in or other sources in euros per hour."
|
||||
}
|
||||
"description": "The revenue from grid feed-in or other sources per hour."
|
||||
},
|
||||
)
|
||||
Gesamt_Verluste: float = Field(
|
||||
json_schema_extra={"description": "The total losses in watt-hours over the entire period."}
|
||||
total_losses: float = Field(
|
||||
validation_alias=AliasChoices("total_losses", "Gesamt_Verluste"),
|
||||
json_schema_extra={"description": "The total losses in watt-hours over the entire period."},
|
||||
)
|
||||
Gesamtbilanz_Euro: float = Field(
|
||||
json_schema_extra={"description": "The total balance of revenues minus costs in euros."}
|
||||
total_balance: float = Field(
|
||||
validation_alias=AliasChoices("total_balance", "Gesamtbilanz_Euro"),
|
||||
json_schema_extra={"description": "The total balance of revenues minus costs."},
|
||||
)
|
||||
Gesamteinnahmen_Euro: float = Field(
|
||||
json_schema_extra={"description": "The total revenues in euros."}
|
||||
total_revenue: float = Field(
|
||||
validation_alias=AliasChoices("total_revenue", "Gesamteinnahmen_Euro"),
|
||||
json_schema_extra={"description": "The total revenues."},
|
||||
)
|
||||
Gesamtkosten_Euro: float = Field(json_schema_extra={"description": "The total costs in euros."})
|
||||
Home_appliance_wh_per_hour: list[Optional[float]] = Field(
|
||||
total_costs: float = Field(
|
||||
validation_alias=AliasChoices("total_costs", "Gesamtkosten_Euro"),
|
||||
json_schema_extra={"description": "The total costs."},
|
||||
)
|
||||
home_appliance_wh_per_hour: list[Optional[float]] = Field(
|
||||
validation_alias=AliasChoices("home_appliance_wh_per_hour", "Home_appliance_wh_per_hour"),
|
||||
json_schema_extra={
|
||||
"description": "The energy consumption of a household appliance in watt-hours per hour."
|
||||
}
|
||||
},
|
||||
)
|
||||
Kosten_Euro_pro_Stunde: list[float] = Field(
|
||||
json_schema_extra={"description": "The costs in euros per hour."}
|
||||
costs_per_hour: list[float] = Field(
|
||||
validation_alias=AliasChoices("costs_per_hour", "Kosten_Euro_pro_Stunde"),
|
||||
json_schema_extra={"description": "The costs per hour."},
|
||||
)
|
||||
Netzbezug_Wh_pro_Stunde: list[float] = Field(
|
||||
json_schema_extra={"description": "The grid energy drawn in watt-hours per hour."}
|
||||
grid_consumption_wh_per_hour: list[float] = Field(
|
||||
validation_alias=AliasChoices("grid_consumption_wh_per_hour", "Netzbezug_Wh_pro_Stunde"),
|
||||
json_schema_extra={"description": "The grid energy drawn in watt-hours per hour."},
|
||||
)
|
||||
Netzeinspeisung_Wh_pro_Stunde: list[float] = Field(
|
||||
json_schema_extra={"description": "The energy fed into the grid in watt-hours per hour."}
|
||||
grid_feed_in_wh_per_hour: list[float] = Field(
|
||||
validation_alias=AliasChoices("grid_feed_in_wh_per_hour", "Netzeinspeisung_Wh_pro_Stunde"),
|
||||
json_schema_extra={"description": "The energy fed into the grid in watt-hours per hour."},
|
||||
)
|
||||
Verluste_Pro_Stunde: list[float] = Field(
|
||||
json_schema_extra={"description": "The losses in watt-hours per hour."}
|
||||
losses_per_hour: list[float] = Field(
|
||||
validation_alias=AliasChoices("losses_per_hour", "Verluste_Pro_Stunde"),
|
||||
json_schema_extra={"description": "The losses in watt-hours per hour."},
|
||||
)
|
||||
akku_soc_pro_stunde: list[float] = Field(
|
||||
battery_soc_per_hour: list[float] = Field(
|
||||
validation_alias=AliasChoices("battery_soc_per_hour", "akku_soc_pro_stunde"),
|
||||
json_schema_extra={
|
||||
"description": "The state of charge of the battery (not the EV) in percentage per hour."
|
||||
}
|
||||
},
|
||||
)
|
||||
Electricity_price: list[float] = Field(
|
||||
json_schema_extra={"description": "Used Electricity Price, including predictions"}
|
||||
electricity_price: list[float] = Field(
|
||||
validation_alias=AliasChoices("electricity_price", "Electricity_price"),
|
||||
json_schema_extra={"description": "Used Electricity Price, including predictions"},
|
||||
)
|
||||
|
||||
# Computed fields for backward compatibility (deprecated German names)
|
||||
@computed_field(json_schema_extra={"deprecated": True})
|
||||
def Last_Wh_pro_Stunde(self) -> list[float]:
|
||||
"""Deprecated: Use load_wh_per_hour instead."""
|
||||
return self.load_wh_per_hour
|
||||
|
||||
@computed_field(json_schema_extra={"deprecated": True})
|
||||
def EAuto_SoC_pro_Stunde(self) -> list[float]:
|
||||
"""Deprecated: Use ev_soc_per_hour instead."""
|
||||
return self.ev_soc_per_hour
|
||||
|
||||
@computed_field(json_schema_extra={"deprecated": True})
|
||||
def Einnahmen_Euro_pro_Stunde(self) -> list[float]:
|
||||
"""Deprecated: Use revenue_per_hour instead."""
|
||||
return self.revenue_per_hour
|
||||
|
||||
@computed_field(json_schema_extra={"deprecated": True})
|
||||
def Gesamt_Verluste(self) -> float:
|
||||
"""Deprecated: Use total_losses instead."""
|
||||
return self.total_losses
|
||||
|
||||
@computed_field(json_schema_extra={"deprecated": True})
|
||||
def Gesamtbilanz_Euro(self) -> float:
|
||||
"""Deprecated: Use total_balance instead."""
|
||||
return self.total_balance
|
||||
|
||||
@computed_field(json_schema_extra={"deprecated": True})
|
||||
def Gesamteinnahmen_Euro(self) -> float:
|
||||
"""Deprecated: Use total_revenue instead."""
|
||||
return self.total_revenue
|
||||
|
||||
@computed_field(json_schema_extra={"deprecated": True})
|
||||
def Gesamtkosten_Euro(self) -> float:
|
||||
"""Deprecated: Use total_costs instead."""
|
||||
return self.total_costs
|
||||
|
||||
@computed_field(json_schema_extra={"deprecated": True})
|
||||
def Home_appliance_wh_per_hour(self) -> list[Optional[float]]:
|
||||
"""Deprecated: Use home_appliance_wh_per_hour instead."""
|
||||
return self.home_appliance_wh_per_hour
|
||||
|
||||
@computed_field(json_schema_extra={"deprecated": True})
|
||||
def Kosten_Euro_pro_Stunde(self) -> list[float]:
|
||||
"""Deprecated: Use costs_per_hour instead."""
|
||||
return self.costs_per_hour
|
||||
|
||||
@computed_field(json_schema_extra={"deprecated": True})
|
||||
def Netzbezug_Wh_pro_Stunde(self) -> list[float]:
|
||||
"""Deprecated: Use grid_consumption_wh_per_hour instead."""
|
||||
return self.grid_consumption_wh_per_hour
|
||||
|
||||
@computed_field(json_schema_extra={"deprecated": True})
|
||||
def Netzeinspeisung_Wh_pro_Stunde(self) -> list[float]:
|
||||
"""Deprecated: Use grid_feed_in_wh_per_hour instead."""
|
||||
return self.grid_feed_in_wh_per_hour
|
||||
|
||||
@computed_field(json_schema_extra={"deprecated": True})
|
||||
def Verluste_Pro_Stunde(self) -> list[float]:
|
||||
"""Deprecated: Use losses_per_hour instead."""
|
||||
return self.losses_per_hour
|
||||
|
||||
@computed_field(json_schema_extra={"deprecated": True})
|
||||
def akku_soc_pro_stunde(self) -> list[float]:
|
||||
"""Deprecated: Use battery_soc_per_hour instead."""
|
||||
return self.battery_soc_per_hour
|
||||
|
||||
@computed_field(json_schema_extra={"deprecated": True})
|
||||
def Electricity_price(self) -> list[float]:
|
||||
"""Deprecated: Use electricity_price instead."""
|
||||
return self.electricity_price
|
||||
|
||||
@field_validator(
|
||||
"Last_Wh_pro_Stunde",
|
||||
"Netzeinspeisung_Wh_pro_Stunde",
|
||||
"akku_soc_pro_stunde",
|
||||
"Netzbezug_Wh_pro_Stunde",
|
||||
"Kosten_Euro_pro_Stunde",
|
||||
"Einnahmen_Euro_pro_Stunde",
|
||||
"EAuto_SoC_pro_Stunde",
|
||||
"Verluste_Pro_Stunde",
|
||||
"Home_appliance_wh_per_hour",
|
||||
"Electricity_price",
|
||||
"load_wh_per_hour",
|
||||
"grid_feed_in_wh_per_hour",
|
||||
"battery_soc_per_hour",
|
||||
"grid_consumption_wh_per_hour",
|
||||
"costs_per_hour",
|
||||
"revenue_per_hour",
|
||||
"ev_soc_per_hour",
|
||||
"losses_per_hour",
|
||||
"home_appliance_wh_per_hour",
|
||||
"electricity_price",
|
||||
mode="before",
|
||||
)
|
||||
def convert_numpy(cls, field: Any) -> Any:
|
||||
@@ -149,7 +240,7 @@ class GeneticSimulationResult(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."""
|
||||
"""**Note**: The first value of "load_wh_per_hour", "grid_feed_in_wh_per_hour", and "grid_consumption_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(
|
||||
json_schema_extra={
|
||||
@@ -363,7 +454,7 @@ class GeneticSolution(ConfigMixin, GeneticParametersBaseModel):
|
||||
|
||||
# --- Create index based on list length and interval ---
|
||||
# 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)
|
||||
periods = min(len(self.result.costs_per_hour), len(self.ac_charge) - start_day_hour)
|
||||
time_index = pd.date_range(
|
||||
start=start_datetime,
|
||||
periods=periods,
|
||||
@@ -387,12 +478,12 @@ class GeneticSolution(ConfigMixin, GeneticParametersBaseModel):
|
||||
{
|
||||
"date_time": time_index,
|
||||
# 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],
|
||||
"load_energy_wh": self.result.load_wh_per_hour[:n_points],
|
||||
"grid_feedin_energy_wh": self.result.grid_feed_in_wh_per_hour[:n_points],
|
||||
"grid_consumption_energy_wh": self.result.grid_consumption_wh_per_hour[:n_points],
|
||||
"costs_amt": self.result.costs_per_hour[:n_points],
|
||||
"revenue_amt": self.result.revenue_per_hour[:n_points],
|
||||
"losses_energy_wh": self.result.losses_per_hour[:n_points],
|
||||
},
|
||||
index=time_index,
|
||||
)
|
||||
@@ -401,7 +492,7 @@ class GeneticSolution(ConfigMixin, GeneticParametersBaseModel):
|
||||
battery_device_id = self._battery_device_id()
|
||||
solution[f"{battery_device_id}_soc_factor"] = [
|
||||
v / 100
|
||||
for v in self.result.akku_soc_pro_stunde[:n_points] # result starts at start_day_hour
|
||||
for v in self.result.battery_soc_per_hour[:n_points] # result starts at start_day_hour
|
||||
]
|
||||
operation: dict[str, list[float]] = {
|
||||
"genetic_ac_charge_factor": [],
|
||||
@@ -427,8 +518,8 @@ class GeneticSolution(ConfigMixin, GeneticParametersBaseModel):
|
||||
# this hour given the expected battery state of charge.
|
||||
result_idx = hour_idx - start_day_hour
|
||||
soc_h_pct = (
|
||||
self.result.akku_soc_pro_stunde[result_idx]
|
||||
if result_idx < len(self.result.akku_soc_pro_stunde)
|
||||
self.result.battery_soc_per_hour[result_idx]
|
||||
if result_idx < len(self.result.battery_soc_per_hour)
|
||||
else 0.0
|
||||
)
|
||||
eff_ac, eff_dc, eff_dis = self._soc_clamped_operation_factors(
|
||||
@@ -458,7 +549,7 @@ class GeneticSolution(ConfigMixin, GeneticParametersBaseModel):
|
||||
|
||||
# Add EV battery solution
|
||||
# eautocharge_hours_float start at hour 0 of start day
|
||||
# result.EAuto_SoC_pro_Stunde start at start_datetime.hour
|
||||
# result.ev_soc_per_hour start at start_datetime.hour
|
||||
if self.eauto_obj:
|
||||
ev_device_id = self._ev_device_id()
|
||||
if self.eautocharge_hours_float is None:
|
||||
@@ -480,7 +571,7 @@ class GeneticSolution(ConfigMixin, GeneticParametersBaseModel):
|
||||
solution[factor_key] = [0.0] * n_points
|
||||
else:
|
||||
solution[f"{ev_device_id}_soc_factor"] = [
|
||||
v / 100 for v in self.result.EAuto_SoC_pro_Stunde[:n_points]
|
||||
v / 100 for v in self.result.ev_soc_per_hour[:n_points]
|
||||
]
|
||||
operation = {
|
||||
"genetic_ev_charge_factor": [],
|
||||
@@ -520,7 +611,7 @@ class GeneticSolution(ConfigMixin, GeneticParametersBaseModel):
|
||||
homeappliance_device_id = self._homeappliance_device_id()
|
||||
# result starts at start_day_hour
|
||||
solution[f"{homeappliance_device_id}_energy_wh"] = (
|
||||
self.result.Home_appliance_wh_per_hour[:n_points]
|
||||
self.result.home_appliance_wh_per_hour[:n_points]
|
||||
)
|
||||
operation = {
|
||||
f"{homeappliance_device_id}_run_op_mode": [],
|
||||
@@ -629,11 +720,11 @@ class GeneticSolution(ConfigMixin, GeneticParametersBaseModel):
|
||||
comment="Optimization solution derived from GeneticSolution.",
|
||||
valid_from=start_datetime,
|
||||
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,
|
||||
total_losses_energy_wh=self.result.total_losses,
|
||||
total_revenues_amt=self.result.total_revenue,
|
||||
total_costs_amt=self.result.total_costs,
|
||||
fitness_score={
|
||||
self.result.Gesamtkosten_Euro,
|
||||
self.result.total_costs,
|
||||
},
|
||||
prediction=PydanticDateTimeDataFrame.from_dataframe(prediction),
|
||||
solution=PydanticDateTimeDataFrame.from_dataframe(solution),
|
||||
@@ -667,8 +758,8 @@ class GeneticSolution(ConfigMixin, GeneticParametersBaseModel):
|
||||
# dataframe (genetic_*_factor columns).
|
||||
result_idx = hour_idx - start_day_hour
|
||||
soc_h_pct = (
|
||||
self.result.akku_soc_pro_stunde[result_idx]
|
||||
if result_idx < len(self.result.akku_soc_pro_stunde)
|
||||
self.result.battery_soc_per_hour[result_idx]
|
||||
if result_idx < len(self.result.battery_soc_per_hour)
|
||||
else 0.0
|
||||
)
|
||||
eff_ac, eff_dc, eff_dis = self._soc_clamped_operation_factors(
|
||||
@@ -752,11 +843,11 @@ class GeneticSolution(ConfigMixin, GeneticParametersBaseModel):
|
||||
# even if configured to be started.
|
||||
resource_id = self._homeappliance_device_id()
|
||||
last_energy: Optional[float] = None
|
||||
for hours, energy in enumerate(self.result.Home_appliance_wh_per_hour):
|
||||
for hours, energy in enumerate(self.result.home_appliance_wh_per_hour):
|
||||
# hours starts at start_datetime with 0
|
||||
if energy is None:
|
||||
raise ValueError(
|
||||
f"Unexpected value {energy} in {self.result.Home_appliance_wh_per_hour}"
|
||||
f"Unexpected value {energy} in {self.result.home_appliance_wh_per_hour}"
|
||||
)
|
||||
if last_energy is None or energy != last_energy:
|
||||
if energy > 0.0:
|
||||
|
||||
Reference in New Issue
Block a user