Files
EOS/tests/test_geneticsimulation.py
T

622 lines
19 KiB
Python
Raw Normal View History

from unittest.mock import Mock
import numpy as np
2024-10-04 10:57:01 +02:00
import pytest
2024-10-04 11:46:52 +02:00
from akkudoktoreos.config.configabc import TimeWindow, TimeWindowSequence
2025-10-28 02:50:31 +01:00
from akkudoktoreos.devices.genetic.battery import Battery
from akkudoktoreos.devices.genetic.homeappliance import HomeAppliance
from akkudoktoreos.devices.genetic.inverter import Inverter
from akkudoktoreos.optimization.genetic.genetic import GeneticSimulation
from akkudoktoreos.optimization.genetic.geneticdevices import (
2024-12-19 14:50:19 +01:00
ElectricVehicleParameters,
2025-10-28 02:50:31 +01:00
HomeApplianceParameters,
InverterParameters,
2024-12-19 14:50:19 +01:00
SolarPanelBatteryParameters,
)
2025-10-28 02:50:31 +01:00
from akkudoktoreos.optimization.genetic.geneticparams import (
GeneticEnergyManagementParameters,
GeneticOptimizationParameters,
)
from akkudoktoreos.optimization.genetic.geneticsolution import GeneticSimulationResult
from akkudoktoreos.utils.datetimeutil import to_duration, to_time
2024-10-06 14:32:49 +02:00
2024-10-06 14:29:23 +02:00
start_hour = 1
2024-10-04 11:46:52 +02:00
2024-10-06 14:32:49 +02:00
2024-10-04 10:57:51 +02:00
# Example initialization of necessary components
2024-10-04 10:57:01 +02:00
@pytest.fixture
2025-10-28 02:50:31 +01:00
def genetic_simulation(config_eos) -> GeneticSimulation:
2025-02-12 21:35:51 +01:00
"""Fixture to create an EnergyManagement instance with given test parameters."""
# Assure configuration holds the correct values
2025-01-12 05:19:37 +01:00
config_eos.merge_settings_from_dict(
{"prediction": {"hours": 48}, "optimization": {"tail_horizon_hours": 0, "hours": 24}}
2025-01-12 05:19:37 +01:00
)
2025-01-18 14:26:34 +01:00
assert config_eos.prediction.hours == 48
2025-10-28 02:50:31 +01:00
assert config_eos.optimization.horizon_hours == 24
2024-10-04 10:57:51 +02:00
# Initialize the battery and the inverter
2024-12-19 14:50:19 +01:00
akku = Battery(
SolarPanelBatteryParameters(
2025-01-12 05:19:37 +01:00
device_id="battery1",
capacity_wh=5000,
initial_soc_percentage=80,
min_soc_percentage=10,
2025-10-28 02:50:31 +01:00
),
prediction_hours=config_eos.prediction.hours,
2024-11-15 22:27:25 +01:00
)
2024-10-04 10:57:01 +02:00
akku.reset()
2025-01-12 05:19:37 +01:00
inverter = Inverter(
InverterParameters(
device_id="inverter1", max_power_wh=10000, battery_id=akku.parameters.device_id
),
battery=akku,
2025-01-12 05:19:37 +01:00
)
2024-10-04 10:57:51 +02:00
# Flexible consumer (fixed start at slot 2 for this deterministic test)
home_appliance = HomeAppliance(
HomeApplianceParameters(
2025-01-12 05:19:37 +01:00
device_id="dishwasher1",
consumption_wh=2000,
duration_h=2,
2025-10-28 02:50:31 +01:00
time_windows=None,
2024-11-15 22:27:25 +01:00
),
optimization_hours=config_eos.optimization.horizon_hours,
prediction_hours=config_eos.prediction.hours,
2024-10-06 14:32:49 +02:00
)
home_appliance.build_load_curve([2])
2024-10-06 14:32:49 +02:00
2024-10-04 10:57:51 +02:00
# Example initialization of electric car battery
2024-12-19 14:50:19 +01:00
eauto = Battery(
ElectricVehicleParameters(
2025-01-12 05:19:37 +01:00
device_id="ev1", capacity_wh=26400, initial_soc_percentage=10, min_soc_percentage=10
2024-12-19 14:50:19 +01:00
),
prediction_hours=config_eos.prediction.hours,
2024-11-15 22:27:25 +01:00
)
2025-01-18 14:26:34 +01:00
eauto.set_charge_per_hour(np.full(config_eos.prediction.hours, 1))
2024-11-15 22:27:25 +01:00
2024-10-04 10:57:51 +02:00
# Parameters based on previous example data
2024-10-04 10:57:01 +02:00
pv_prognose_wh = [
2024-10-04 11:46:52 +02:00
0,
0,
0,
0,
0,
0,
0,
8.05,
352.91,
728.51,
930.28,
1043.25,
1106.74,
1161.69,
6018.82,
5519.07,
3969.88,
3017.96,
1943.07,
1007.17,
319.67,
7.88,
0,
0,
0,
0,
0,
0,
0,
0,
0,
5.04,
335.59,
705.32,
1121.12,
1604.79,
2157.38,
1433.25,
5718.49,
4553.96,
3027.55,
2574.46,
1720.4,
963.4,
383.3,
0,
0,
0,
2024-10-04 10:57:01 +02:00
]
2024-10-04 10:57:51 +02:00
2024-10-04 10:57:01 +02:00
strompreis_euro_pro_wh = [
2024-10-04 11:46:52 +02:00
0.0003384,
0.0003318,
0.0003284,
0.0003283,
0.0003289,
0.0003334,
0.0003290,
0.0003302,
0.0003042,
0.0002430,
0.0002280,
0.0002212,
0.0002093,
0.0001879,
0.0001838,
0.0002004,
0.0002198,
0.0002270,
0.0002997,
0.0003195,
0.0003081,
0.0002969,
0.0002921,
0.0002780,
0.0003384,
0.0003318,
0.0003284,
0.0003283,
0.0003289,
0.0003334,
0.0003290,
0.0003302,
0.0003042,
0.0002430,
0.0002280,
0.0002212,
0.0002093,
0.0001879,
0.0001838,
0.0002004,
0.0002198,
0.0002270,
0.0002997,
0.0003195,
0.0003081,
0.0002969,
0.0002921,
0.0002780,
2024-10-04 10:57:01 +02:00
]
2024-10-04 10:57:51 +02:00
2024-11-15 22:27:25 +01:00
einspeiseverguetung_euro_pro_wh = 0.00007
preis_euro_pro_wh_akku = 0.0001
2024-10-04 10:57:01 +02:00
gesamtlast = [
2024-10-04 11:46:52 +02:00
676.71,
876.19,
527.13,
468.88,
531.38,
517.95,
483.15,
472.28,
1011.68,
995.00,
1053.07,
1063.91,
1320.56,
1132.03,
1163.67,
1176.82,
1216.22,
1103.78,
1129.12,
1178.71,
1050.98,
988.56,
912.38,
704.61,
516.37,
868.05,
694.34,
608.79,
556.31,
488.89,
506.91,
804.89,
1141.98,
1056.97,
992.46,
1155.99,
827.01,
1257.98,
1232.67,
871.26,
860.88,
1158.03,
1222.72,
1221.04,
949.99,
987.01,
733.99,
592.97,
2024-10-04 10:57:01 +02:00
]
2024-10-04 10:57:51 +02:00
# Initialize the energy management system with the respective parameters
2025-10-28 02:50:31 +01:00
simulation = GeneticSimulation()
simulation.prepare(
GeneticEnergyManagementParameters(
2024-11-15 22:27:25 +01:00
pv_prognose_wh=pv_prognose_wh,
strompreis_euro_pro_wh=strompreis_euro_pro_wh,
einspeiseverguetung_euro_pro_wh=einspeiseverguetung_euro_pro_wh,
preis_euro_pro_wh_akku=preis_euro_pro_wh_akku,
gesamtlast=gesamtlast,
),
optimization_hours=config_eos.optimization.horizon_hours,
prediction_hours=config_eos.prediction.hours,
2024-12-16 15:33:00 +01:00
inverter=inverter,
ev=eauto,
home_appliances=[home_appliance],
2024-10-04 10:57:01 +02:00
)
2024-10-16 15:40:04 +02:00
2025-11-08 15:42:18 +01:00
# Init for test
assert simulation.ac_charge_hours is not None
assert simulation.dc_charge_hours is not None
assert simulation.bat_discharge_hours is not None
assert simulation.bat_grid_export_hours is not None
2025-11-08 15:42:18 +01:00
assert simulation.ev_charge_hours is not None
simulation.ac_charge_hours[start_hour] = 1.0
simulation.dc_charge_hours[start_hour] = 1.0
simulation.bat_discharge_hours[start_hour] = 1.0
simulation.ev_charge_hours[start_hour] = 1.0
2025-10-28 02:50:31 +01:00
return simulation
2024-10-04 10:57:01 +02:00
2025-10-28 02:50:31 +01:00
def test_simulation(genetic_simulation):
2025-02-12 21:35:51 +01:00
"""Test the EnergyManagement simulation method."""
2025-10-28 02:50:31 +01:00
simulation = genetic_simulation
2024-10-04 10:57:51 +02:00
# Simulate starting from hour 1 (this value can be adjusted)
2024-10-06 14:32:49 +02:00
2025-10-28 02:50:31 +01:00
result = simulation.simulate(start_hour=start_hour)
2024-10-06 14:32:49 +02:00
2024-10-22 10:22:50 +02:00
# visualisiere_ergebnisse(
2025-10-28 02:50:31 +01:00
# simulation.gesamtlast,
# simulation.pv_prognose_wh,
# simulation.strompreis_euro_pro_wh,
2024-10-22 10:22:50 +02:00
# result,
2025-10-28 02:50:31 +01:00
# simulation.akku.discharge_array+simulation.akku.charge_array,
2024-10-22 10:22:50 +02:00
# None,
2025-10-28 02:50:31 +01:00
# simulation.pv_prognose_wh,
2024-10-22 10:22:50 +02:00
# start_hour,
# 48,
# np.full(48, 0.0),
# filename="visualization_results.pdf",
# extra_data=None,
# )
2024-10-16 15:40:04 +02:00
2024-10-04 10:57:01 +02:00
# Assertions to validate results
2024-10-04 10:57:51 +02:00
assert result is not None, "Result should not be None"
assert isinstance(result, dict), "Result should be a dictionary"
assert "Last_Wh_pro_Stunde" in result, "Result should contain 'Last_Wh_pro_Stunde'"
2024-10-04 10:57:01 +02:00
"""
2024-10-04 10:57:51 +02:00
Check the result of the simulation based on expected values.
2024-10-04 10:57:01 +02:00
"""
2024-10-04 10:57:51 +02:00
# Example result returned from the simulation (used for assertions)
assert result is not None, "Result should not be None."
# Check that the result is a dictionary
assert isinstance(result, dict), "Result should be a dictionary."
2025-10-28 02:50:31 +01:00
assert GeneticSimulationResult(**result) is not None
2024-10-04 10:57:01 +02:00
2024-10-04 10:57:51 +02:00
# Check the length of the main arrays
assert (
len(result["Last_Wh_pro_Stunde"]) == 47
), "The length of 'Last_Wh_pro_Stunde' should be 48."
assert (
len(result["Netzeinspeisung_Wh_pro_Stunde"]) == 47
), "The length of 'Netzeinspeisung_Wh_pro_Stunde' should be 48."
assert (
len(result["Netzbezug_Wh_pro_Stunde"]) == 47
), "The length of 'Netzbezug_Wh_pro_Stunde' should be 48."
assert (
len(result["Kosten_Euro_pro_Stunde"]) == 47
), "The length of 'Kosten_Euro_pro_Stunde' should be 48."
assert (
len(result["akku_soc_pro_stunde"]) == 47
), "The length of 'akku_soc_pro_stunde' should be 48."
2024-10-04 10:57:01 +02:00
2024-10-04 10:57:51 +02:00
# Verify specific values in the 'Last_Wh_pro_Stunde' array
assert (
result["Last_Wh_pro_Stunde"][1] == 1527.13
), "The value at index 1 of 'Last_Wh_pro_Stunde' should be 1527.13."
assert (
result["Last_Wh_pro_Stunde"][2] == 1468.88
), "The value at index 2 of 'Last_Wh_pro_Stunde' should be 1468.88."
assert (
result["Last_Wh_pro_Stunde"][12] == 1132.03
), "The value at index 12 of 'Last_Wh_pro_Stunde' should be 1132.03."
2024-10-04 10:57:01 +02:00
2024-10-04 10:57:51 +02:00
# Verify that the value at index 0 is 'None'
# Check that 'Netzeinspeisung_Wh_pro_Stunde' and 'Netzbezug_Wh_pro_Stunde' are consistent
assert (
result["Netzbezug_Wh_pro_Stunde"][1] == 1527.13
), "The value at index 1 of 'Netzbezug_Wh_pro_Stunde' should be 1527.13."
2024-10-04 10:57:01 +02:00
2024-10-04 10:57:51 +02:00
# Verify the total balance
assert (
abs(result["Gesamtbilanz_Euro"] - 7.224316588371922) < 1e-5
), "Total balance should reflect the shared per-slot battery power limit."
2024-10-04 10:57:51 +02:00
# Check total revenue and total costs
assert (
abs(result["Gesamteinnahmen_Euro"] - 2.3247787887715) < 1e-5
), "Total revenue should respect the shared per-slot battery power limit."
assert (
abs(result["Gesamtkosten_Euro"] - 9.549095377143422) < 1e-5
), "Total costs should respect the shared per-slot battery power limit."
2024-10-04 10:57:51 +02:00
# Check the losses
assert abs(result["Gesamt_Verluste"] - 1620.0) < 1e-5, "Total losses should be 1620.0 ."
2024-10-04 10:57:51 +02:00
# Check the values in 'akku_soc_pro_stunde'
assert (
result["akku_soc_pro_stunde"][-1] == 98.0
), "The value at index -1 of 'akku_soc_pro_stunde' should be 98.0."
assert (
result["akku_soc_pro_stunde"][1] == 98.0
), "The value at index 1 of 'akku_soc_pro_stunde' should be 98.0."
2024-10-04 10:57:51 +02:00
2024-10-06 14:04:26 +02:00
# Check home appliances
assert (
sum(simulation.home_appliances[0].get_load_curve()) == 2000
), "The sum of 'simulation.home_appliances[0].get_load_curve()' should be 2000."
2024-10-06 14:29:23 +02:00
2024-10-06 14:04:26 +02:00
assert (
np.nansum(
np.where(
2024-11-26 22:28:05 +01:00
result["Home_appliance_wh_per_hour"] is None,
np.nan,
np.array(result["Home_appliance_wh_per_hour"]),
)
)
== 2000
), "The sum of 'Home_appliance_wh_per_hour' should be 2000."
2024-10-06 14:04:26 +02:00
2024-10-04 10:57:51 +02:00
print("All tests passed successfully.")
def test_ev_charging_uses_raw_input_energy_for_load_and_grid(config_eos):
config_eos.merge_settings_from_dict(
{"prediction": {"hours": 1}, "optimization": {"tail_horizon_hours": 0, "horizon_hours": 1}}
)
ev = Battery(
ElectricVehicleParameters(
device_id="ev1",
capacity_wh=1000,
charging_efficiency=0.8,
max_charge_power_w=100,
initial_soc_percentage=0,
min_soc_percentage=0,
),
prediction_hours=1,
)
inverter = Inverter(InverterParameters(device_id="inverter1", max_power_wh=1000.0))
simulation = GeneticSimulation()
simulation.prepare(
GeneticEnergyManagementParameters(
pv_prognose_wh=[0.0],
strompreis_euro_pro_wh=[0.001],
einspeiseverguetung_euro_pro_wh=[0.0],
preis_euro_pro_wh_akku=0.0,
gesamtlast=[0.0],
),
optimization_hours=1,
prediction_hours=1,
inverter=inverter,
ev=ev,
)
simulation.ev_charge_hours = np.array([1.0])
result = simulation.simulate(start_hour=0)
assert result["Last_Wh_pro_Stunde"][0] == pytest.approx(100.0)
assert result["Netzbezug_Wh_pro_Stunde"][0] == pytest.approx(100.0)
assert result["Kosten_Euro_pro_Stunde"][0] == pytest.approx(0.1)
assert result["Verluste_Pro_Stunde"][0] == pytest.approx(20.0)
assert ev.current_soc_percentage() == pytest.approx(8.0)
def test_direct_marketing_curtails_negative_feed_in(config_eos, monkeypatch):
config_eos.merge_settings_from_dict(
{"prediction": {"hours": 2}, "optimization": {"tail_horizon_hours": 0, "horizon_hours": 2}}
)
inverter = Inverter(InverterParameters(device_id="inverter1", max_power_wh=1000.0))
monkeypatch.setattr(
inverter.self_consumption_predictor,
"calculate_expected_direct_consumption",
Mock(side_effect=min),
)
simulation = GeneticSimulation()
simulation.prepare(
GeneticEnergyManagementParameters(
pv_prognose_wh=[500.0, 500.0],
strompreis_euro_pro_wh=[-0.0001, -0.0001],
einspeiseverguetung_euro_pro_wh=[-0.0001, -0.0001],
preis_euro_pro_wh_akku=0.0,
gesamtlast=[0.0, 0.0],
),
optimization_hours=config_eos.optimization.horizon_hours,
prediction_hours=config_eos.prediction.hours,
inverter=inverter,
direct_marketing_enabled=True,
)
result = simulation.simulate(start_hour=0)
assert result["Netzeinspeisung_Wh_pro_Stunde"][0] == 0.0
assert result["Einnahmen_Euro_pro_Stunde"][0] == 0.0
assert result["Verluste_Pro_Stunde"][0] == pytest.approx(500.0)
def _direct_marketing_battery_export_simulation(
config_eos,
levelized_cost_of_storage_kwh: float = 0.0,
dc_to_ac_efficiency: float = 1.0,
) -> GeneticSimulation:
config_eos.merge_settings_from_dict(
{"prediction": {"hours": 2}, "optimization": {"tail_horizon_hours": 0, "horizon_hours": 2}}
)
battery = Battery(
SolarPanelBatteryParameters(
device_id="battery1",
capacity_wh=1000,
initial_soc_percentage=100,
min_soc_percentage=0,
charging_efficiency=1.0,
discharging_efficiency=1.0,
levelized_cost_of_storage_kwh=levelized_cost_of_storage_kwh,
max_charge_power_w=500,
),
prediction_hours=config_eos.prediction.hours,
)
inverter = Inverter(
InverterParameters(
device_id="inverter1",
max_power_wh=500.0,
battery_id=battery.parameters.device_id,
dc_to_ac_efficiency=dc_to_ac_efficiency,
),
battery=battery,
)
simulation = GeneticSimulation()
simulation.prepare(
GeneticEnergyManagementParameters(
pv_prognose_wh=[0.0, 0.0],
strompreis_euro_pro_wh=[0.0, 0.0],
einspeiseverguetung_euro_pro_wh=[0.0002, 0.0002],
preis_euro_pro_wh_akku=0.0,
gesamtlast=[0.0, 0.0],
),
optimization_hours=config_eos.optimization.horizon_hours,
prediction_hours=config_eos.prediction.hours,
inverter=inverter,
direct_marketing_enabled=True,
)
return simulation
def test_direct_marketing_discharge_allowed_does_not_export_battery(config_eos):
simulation = _direct_marketing_battery_export_simulation(config_eos)
assert simulation.bat_discharge_hours is not None
simulation.bat_discharge_hours[0] = 1
result = simulation.simulate(start_hour=0)
assert result["Netzeinspeisung_Wh_pro_Stunde"][0] == 0.0
assert simulation.battery is not None
assert simulation.battery.current_soc_percentage() == 100.0
def test_direct_marketing_battery_grid_export_uses_separate_signal(config_eos):
simulation = _direct_marketing_battery_export_simulation(config_eos)
assert simulation.bat_grid_export_hours is not None
simulation.bat_grid_export_hours[0] = 1
result = simulation.simulate(start_hour=0)
assert result["Netzeinspeisung_Wh_pro_Stunde"][0] == pytest.approx(500.0)
assert result["Einnahmen_Euro_pro_Stunde"][0] == pytest.approx(0.1)
assert simulation.battery is not None
assert simulation.battery.current_soc_percentage() == 50.0
def test_direct_marketing_grid_export_rate_limits_exported_energy(config_eos):
"""A partial export level exports that share of the rated discharge power."""
simulation = _direct_marketing_battery_export_simulation(config_eos)
assert simulation.bat_grid_export_hours is not None
# 500 W rated discharge power over a one hour slot -> 500 Wh at rate 1.0.
simulation.bat_grid_export_hours[0] = 0.5
result = simulation.simulate(start_hour=0)
assert result["Netzeinspeisung_Wh_pro_Stunde"][0] == pytest.approx(250.0)
assert simulation.battery is not None
assert simulation.battery.current_soc_percentage() == 75.0
def test_battery_lcos_is_charged_once_on_delivered_energy(config_eos):
simulation = _direct_marketing_battery_export_simulation(
config_eos,
levelized_cost_of_storage_kwh=0.12,
dc_to_ac_efficiency=0.8,
)
assert simulation.bat_grid_export_hours is not None
simulation.bat_grid_export_hours[0] = 1
result = simulation.simulate(start_hour=0)
# The battery delivers 500 Wh DC, so LCOS is 0.5 kWh * 0.12 EUR/kWh
# = 0.06 EUR exactly once. After the 80% inverter, 400 Wh AC reaches
# the grid and earns 400 Wh * 0.0002 EUR/Wh = 0.08 EUR.
assert result["Kosten_Euro_pro_Stunde"][0] == pytest.approx(0.06)
assert result["Gesamtkosten_Euro"] == pytest.approx(0.06)
assert result["Einnahmen_Euro_pro_Stunde"][0] == pytest.approx(0.08)
assert result["Gesamtbilanz_Euro"] == pytest.approx(-0.02)
def test_disabled_ac_charging_clears_the_reported_plan(config_eos):
"""With AC charging off the reported plan must not keep charge commands.
The simulation ignores the AC charge genes when the inverter forbids grid
charging. The solution is read back from the same array, so a controller
acting on it would grid-charge the battery although no such charge was ever
simulated or paid for.
"""
config_eos.merge_settings_from_dict(
{"prediction": {"hours": 2}, "optimization": {"tail_horizon_hours": 0, "horizon_hours": 2}}
)
battery = Battery(
SolarPanelBatteryParameters(
device_id="battery1",
capacity_wh=10000,
initial_soc_percentage=50,
min_soc_percentage=0,
charging_efficiency=1.0,
discharging_efficiency=1.0,
max_charge_power_w=5000,
),
prediction_hours=config_eos.prediction.hours,
)
inverter = Inverter(
InverterParameters(
device_id="inverter1",
max_power_wh=5000.0,
battery_id=battery.parameters.device_id,
max_ac_charge_power_w=0, # Netzladen deaktiviert
),
battery=battery,
)
simulation = GeneticSimulation()
simulation.prepare(
GeneticEnergyManagementParameters(
pv_prognose_wh=[0.0, 0.0],
strompreis_euro_pro_wh=[0.0003, 0.0003],
einspeiseverguetung_euro_pro_wh=[0.0001, 0.0001],
preis_euro_pro_wh_akku=0.0,
gesamtlast=[0.0, 0.0],
),
optimization_hours=config_eos.optimization.horizon_hours,
prediction_hours=config_eos.prediction.hours,
inverter=inverter,
)
simulation.ac_charge_hours = np.array([0.8, 0.0])
soc_before = battery.current_soc_percentage()
simulation.simulate(start_hour=0)
# Nothing was charged ...
assert battery.current_soc_percentage() == pytest.approx(soc_before)
# ... and the plan says so.
assert simulation.ac_charge_hours is not None
assert list(simulation.ac_charge_hours) == [0.0, 0.0]