mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2026-07-21 17:28:11 +00:00
feat: rename genetic optimization API fields to English (#675)
Some checks failed
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
Some checks failed
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:
@@ -278,7 +278,7 @@ class EnergyManagement(
|
||||
start_hour = EnergyManagement._start_datetime.hour
|
||||
solution = await loop.run_in_executor(
|
||||
None,
|
||||
lambda: optimization.optimierung_ems(
|
||||
lambda: optimization.optimize_ems(
|
||||
start_hour=start_hour,
|
||||
parameters=cast(
|
||||
GeneticOptimizationParameters, genetic_parameters
|
||||
@@ -355,7 +355,7 @@ class EnergyManagement(
|
||||
start_hour = EnergyManagement._start_datetime.hour
|
||||
solution = await loop.run_in_executor(
|
||||
None,
|
||||
lambda: optimization.optimierung_ems(
|
||||
lambda: optimization.optimize_ems(
|
||||
start_hour=start_hour,
|
||||
parameters=genetic_parameters,
|
||||
ngen=genetic_individuals,
|
||||
|
||||
@@ -51,7 +51,7 @@ class BatteriesCommonSettings(DevicesBaseSettings):
|
||||
levelized_cost_of_storage_kwh: float = Field(
|
||||
default=0.0,
|
||||
json_schema_extra={
|
||||
"description": "Levelized cost of storage (LCOS), the average lifetime cost of delivering one kWh [€/kWh].",
|
||||
"description": "Levelized cost of storage (LCOS), the average lifetime cost of delivering one kWh [amount/kWh].",
|
||||
"examples": [0.12],
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -43,7 +43,7 @@ class ElecPriceCommonSettings(SettingsBaseModel):
|
||||
default=None,
|
||||
ge=0,
|
||||
json_schema_extra={
|
||||
"description": "Electricity price charges [€/kWh]. Will be added to variable market price.",
|
||||
"description": "Electricity price charges [amount/kWh]. Will be added to variable market price.",
|
||||
"examples": [0.21],
|
||||
},
|
||||
)
|
||||
|
||||
@@ -21,14 +21,14 @@ class ElecPriceDataRecord(PredictionRecord):
|
||||
"""
|
||||
|
||||
elecprice_marketprice_wh: Optional[float] = Field(
|
||||
None, json_schema_extra={"description": "Electricity market price per Wh (€/Wh)"}
|
||||
None, json_schema_extra={"description": "Electricity market price per Wh [amount/Wh]"}
|
||||
)
|
||||
|
||||
# Computed fields
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def elecprice_marketprice_kwh(self) -> Optional[float]:
|
||||
"""Electricity market price per kWh (€/kWh).
|
||||
"""Electricity market price per kWh [amount/kWh].
|
||||
|
||||
Convenience attribute calculated from `elecprice_marketprice_wh`.
|
||||
"""
|
||||
|
||||
@@ -21,14 +21,14 @@ class FeedInTariffDataRecord(PredictionRecord):
|
||||
"""
|
||||
|
||||
feed_in_tariff_wh: Optional[float] = Field(
|
||||
None, json_schema_extra={"description": "Feed in tariff per Wh (€/Wh)"}
|
||||
None, json_schema_extra={"description": "Feed in tariff per Wh [amount/Wh]"}
|
||||
)
|
||||
|
||||
# Computed fields
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def feed_in_tariff_kwh(self) -> Optional[float]:
|
||||
"""Feed in tariff per kWh (€/kWh).
|
||||
"""Feed in tariff per kWh [amount/kWh].
|
||||
|
||||
Convenience attribute calculated from `feed_in_tariff_wh`.
|
||||
"""
|
||||
|
||||
@@ -17,7 +17,7 @@ class FeedInTariffFixedCommonSettings(SettingsBaseModel):
|
||||
default=None,
|
||||
ge=0,
|
||||
json_schema_extra={
|
||||
"description": "Electricity price feed in tariff [€/kWH].",
|
||||
"description": "Electricity price feed in tariff [amount/kWh].",
|
||||
"examples": [0.078],
|
||||
},
|
||||
)
|
||||
|
||||
@@ -265,11 +265,11 @@ def SolutionCard(solution: OptimizationSolution, config: SettingsEOS, data: Opti
|
||||
y2_axis = LinearAxis(y_range_name="factor", axis_label="Factor [0.0..1.0]")
|
||||
plot.add_layout(y2_axis, "left")
|
||||
# y3 axis
|
||||
y3_axis = LinearAxis(y_range_name="amt_kwh", axis_label="Electricty Price [currency/kWh]")
|
||||
y3_axis = LinearAxis(y_range_name="amt_kwh", axis_label="Electricity Price [amount/kWh]")
|
||||
y3_axis.axis_label_text_color = "red"
|
||||
plot.add_layout(y3_axis, "right")
|
||||
# y4 axis
|
||||
y4_axis = LinearAxis(y_range_name="amt", axis_label="Amount [currency]")
|
||||
y4_axis = LinearAxis(y_range_name="amt", axis_label="Amount")
|
||||
plot.add_layout(y4_axis, "right")
|
||||
|
||||
plot.toolbar.autohide = True
|
||||
|
||||
@@ -54,7 +54,7 @@ def ElectricityPriceForecast(
|
||||
),
|
||||
title=f"Electricity Price Prediction ({provider})",
|
||||
x_axis_label=f"Datetime [localtime {date_time_tz}]",
|
||||
y_axis_label="Price [€/kWh]",
|
||||
y_axis_label="Price [amount/kWh]",
|
||||
sizing_mode="stretch_width",
|
||||
height=400,
|
||||
)
|
||||
|
||||
@@ -204,7 +204,20 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||
|
||||
app = FastAPI(
|
||||
title="Akkudoktor-EOS",
|
||||
description="This project provides a comprehensive solution for simulating and optimizing an energy system based on renewable energy sources. With a focus on photovoltaic (PV) systems, battery storage (batteries), load management (consumer requirements), heat pumps, electric vehicles, and consideration of electricity price data, this system enables forecasting and optimization of energy flow and costs over a specified period.",
|
||||
description="""This project provides a comprehensive solution for simulating and optimizing an energy system based on renewable energy sources. With a focus on photovoltaic (PV) systems, battery storage (batteries), load management (consumer requirements), heat pumps, electric vehicles, and consideration of electricity price data, this system enables forecasting and optimization of energy flow and costs over a specified period.
|
||||
|
||||
## Currency Information
|
||||
|
||||
All monetary values in this API are expressed in the locally configured currency of the EOS installation. The system is designed to work with any currency (e.g., EUR, SEK, CHF, USD, GBP, etc.). Each installation uses a single, consistent currency throughout all endpoints and responses.
|
||||
|
||||
Values are given in whole currency units, not in hundredth subunits: a value of `0.0003` in an installation configured for Euro means 0.0003 EUR/Wh (i.e. 0.30 EUR/kWh), never cents. The same applies to all totals (e.g. `total_costs` of `1.5` = 1.50 EUR).
|
||||
|
||||
Field names containing cost, price, revenue, tariff, or similar monetary terms (e.g., `total_costs`, `electricity_price_per_wh`, `revenue_per_hour`) represent amounts in the configured currency, without explicit currency designation in the field name to maintain currency-neutrality.
|
||||
|
||||
## Deprecated Field Names
|
||||
|
||||
The genetic optimization API fields were renamed from German to English. For backward compatibility the old German field names (e.g. `gesamtlast`, `pv_prognose_wh`, `Gesamtbilanz_Euro`) are still accepted on input and are emitted in responses alongside the English names, marked as deprecated in the schema. They will be removed in a future release — new clients should use the English field names only.
|
||||
""",
|
||||
summary="Comprehensive solution for simulating and optimizing an energy system based on renewable energy sources",
|
||||
version=f"v{__version__}",
|
||||
license_info={
|
||||
@@ -1160,7 +1173,7 @@ def fastapi_energy_management_plan_get() -> EnergyManagementPlan:
|
||||
|
||||
@app.get("/strompreis", tags=["prediction"])
|
||||
async def fastapi_strompreis() -> list[float]:
|
||||
"""Deprecated: Electricity Market Price Prediction per Wh (€/Wh).
|
||||
"""Deprecated: Electricity Market Price Prediction per Wh [amount/Wh].
|
||||
|
||||
Electricity prices start at 00.00.00 today and are provided for 48 hours.
|
||||
If no prices are available the missing ones at the start of the series are
|
||||
|
||||
@@ -448,7 +448,7 @@ def prepare_visualize(
|
||||
report.create_line_chart_date(
|
||||
next_full_hour_date,
|
||||
[
|
||||
parameters.ems.gesamtlast[start_hour:],
|
||||
parameters.ems.total_load[start_hour:],
|
||||
],
|
||||
title="Load Profile",
|
||||
# xlabel="Hours", # not enough space
|
||||
@@ -458,7 +458,7 @@ def prepare_visualize(
|
||||
report.create_line_chart_date(
|
||||
next_full_hour_date,
|
||||
[
|
||||
parameters.ems.pv_prognose_wh[start_hour:],
|
||||
parameters.ems.pv_forecast_wh[start_hour:],
|
||||
],
|
||||
title="PV Forecast",
|
||||
# xlabel="Hours", # not enough space
|
||||
@@ -469,15 +469,15 @@ def prepare_visualize(
|
||||
next_full_hour_date,
|
||||
[
|
||||
np.full(
|
||||
len(parameters.ems.gesamtlast) - start_hour,
|
||||
parameters.ems.einspeiseverguetung_euro_pro_wh[start_hour:]
|
||||
if isinstance(parameters.ems.einspeiseverguetung_euro_pro_wh, list)
|
||||
else parameters.ems.einspeiseverguetung_euro_pro_wh,
|
||||
len(parameters.ems.total_load) - start_hour,
|
||||
parameters.ems.feed_in_tariff_per_wh[start_hour:]
|
||||
if isinstance(parameters.ems.feed_in_tariff_per_wh, list)
|
||||
else parameters.ems.feed_in_tariff_per_wh,
|
||||
)
|
||||
],
|
||||
title="Remuneration",
|
||||
# xlabel="Hours", # not enough space
|
||||
ylabel="€/Wh",
|
||||
ylabel="amount/Wh",
|
||||
x2label=None, # not enough space
|
||||
)
|
||||
if parameters.temperature_forecast:
|
||||
@@ -497,11 +497,11 @@ def prepare_visualize(
|
||||
report.create_line_chart_date(
|
||||
next_full_hour_date, # start_date
|
||||
[
|
||||
results["result"]["Last_Wh_pro_Stunde"],
|
||||
results["result"]["Home_appliance_wh_per_hour"],
|
||||
results["result"]["Netzeinspeisung_Wh_pro_Stunde"],
|
||||
results["result"]["Netzbezug_Wh_pro_Stunde"],
|
||||
results["result"]["Verluste_Pro_Stunde"],
|
||||
results["result"]["load_wh_per_hour"],
|
||||
results["result"]["home_appliance_wh_per_hour"],
|
||||
results["result"]["grid_feed_in_wh_per_hour"],
|
||||
results["result"]["grid_consumption_wh_per_hour"],
|
||||
results["result"]["losses_per_hour"],
|
||||
],
|
||||
title="Energy Flow per Hour",
|
||||
# xlabel="Date", # not enough space
|
||||
@@ -521,7 +521,7 @@ def prepare_visualize(
|
||||
# Group 3:
|
||||
report.create_line_chart_date(
|
||||
next_full_hour_date, # start_date
|
||||
[results["result"]["akku_soc_pro_stunde"], results["result"]["EAuto_SoC_pro_Stunde"]],
|
||||
[results["result"]["battery_soc_per_hour"], results["result"]["ev_soc_per_hour"]],
|
||||
title="Battery SOC",
|
||||
# xlabel="Date", # not enough space
|
||||
ylabel="%",
|
||||
@@ -533,10 +533,10 @@ def prepare_visualize(
|
||||
)
|
||||
report.create_line_chart_date(
|
||||
next_full_hour_date, # start_date
|
||||
[parameters.ems.strompreis_euro_pro_wh[start_hour:]],
|
||||
[parameters.ems.electricity_price_per_wh[start_hour:]],
|
||||
# title="Electricity Price", # not enough space
|
||||
# xlabel="Date", # not enough space
|
||||
ylabel="Electricity Price (€/Wh)",
|
||||
ylabel="Electricity Price (amount/Wh)",
|
||||
x2label=None, # not enough space
|
||||
)
|
||||
|
||||
@@ -570,50 +570,50 @@ def prepare_visualize(
|
||||
report.create_line_chart_date(
|
||||
next_full_hour_date, # start_date
|
||||
[
|
||||
results["result"]["Kosten_Euro_pro_Stunde"],
|
||||
results["result"]["Einnahmen_Euro_pro_Stunde"],
|
||||
results["result"]["costs_per_hour"],
|
||||
results["result"]["revenue_per_hour"],
|
||||
],
|
||||
title="Financial Balance per Hour",
|
||||
# xlabel="Date", # not enough space
|
||||
ylabel="Euro",
|
||||
ylabel="Amount",
|
||||
labels=["Costs", "Revenue"],
|
||||
)
|
||||
|
||||
extra_data = results["extra_data"]
|
||||
report.create_scatter_plot(
|
||||
extra_data["verluste"],
|
||||
extra_data["bilanz"],
|
||||
extra_data["losses"],
|
||||
extra_data["balance"],
|
||||
title="Scatter Plot",
|
||||
xlabel="losses",
|
||||
ylabel="balance",
|
||||
c=extra_data["nebenbedingung"],
|
||||
c=extra_data["constraints"],
|
||||
)
|
||||
|
||||
values_list = [
|
||||
[
|
||||
results["result"]["Gesamtkosten_Euro"],
|
||||
results["result"]["Gesamteinnahmen_Euro"],
|
||||
results["result"]["Gesamtbilanz_Euro"],
|
||||
results["result"]["total_costs"],
|
||||
results["result"]["total_revenue"],
|
||||
results["result"]["total_balance"],
|
||||
]
|
||||
]
|
||||
labels = ["Total Costs [€]", "Total Revenue [€]", "Total Balance [€]"]
|
||||
labels = ["Total Costs [amount]", "Total Revenue [amount]", "Total Balance [amount]"]
|
||||
|
||||
report.create_bar_chart(
|
||||
labels=labels,
|
||||
values_list=values_list,
|
||||
title="Financial Overview",
|
||||
ylabel="Euro",
|
||||
xlabels=["Total Costs [€]", "Total Revenue [€]", "Total Balance [€]"],
|
||||
ylabel="Amount",
|
||||
xlabels=["Total Costs [amount]", "Total Revenue [amount]", "Total Balance [amount]"],
|
||||
)
|
||||
|
||||
report.finalize_group()
|
||||
|
||||
# Group 1: Scatter plot of losses vs balance with color-coded constraints
|
||||
f1 = np.array(extra_data["verluste"]) # Losses
|
||||
f2 = np.array(extra_data["bilanz"]) # Balance
|
||||
n1 = np.array(extra_data["nebenbedingung"]) # Constraints
|
||||
f1 = np.array(extra_data["losses"]) # Losses
|
||||
f2 = np.array(extra_data["balance"]) # Balance
|
||||
n1 = np.array(extra_data["constraints"]) # Constraints
|
||||
|
||||
# Filter data where 'nebenbedingung' < 0.01
|
||||
# Filter data where 'constraints' < 0.01
|
||||
filtered_indices = n1 < 0.01
|
||||
filtered_losses = f1[filtered_indices]
|
||||
filtered_balance = f2[filtered_indices]
|
||||
|
||||
Reference in New Issue
Block a user