Files
EOS/tests/test_genetic_complete_timegrid.py
T

166 lines
6.7 KiB
Python
Raw Normal View History

"""Native GENETIC results retain the elapsed-time grid and immutable run inputs."""
from unittest.mock import patch
import numpy as np
import pytest
from akkudoktoreos.core.coreabc import get_ems
from akkudoktoreos.optimization.genetic.genetic import GeneticOptimization
from akkudoktoreos.optimization.genetic.geneticparams import (
GeneticOptimizationParameters,
)
from akkudoktoreos.utils.datetimeutil import to_datetime
@pytest.mark.asyncio
@pytest.mark.parametrize(
"timestamp",
["2026-03-29T03:15:00+02:00", "2026-10-25T02:30:00+02:00", "2026-10-25T02:30:00+01:00"],
)
async def test_native_result_retains_dst_grid_and_owned_inputs(config_eos, timestamp):
config_eos.merge_settings_from_dict(
{
"general": {"latitude": 52.52, "longitude": 13.405},
"prediction": {"hours": 24},
"optimization": {
"genetic": {
"interval_sec": 900,
"horizon_hours": 1,
"tail_horizon_hours": 0,
"terminal_value_mode": "FIXED",
}
},
}
)
start = to_datetime(timestamp).in_timezone("Europe/Berlin")
get_ems(init=True).set_start_datetime(start)
elapsed_slots = int((start - start.start_of("day")).total_seconds() // 900)
count = elapsed_slots + 4
parameters = GeneticOptimizationParameters.model_validate(
{
"forecast_interval_seconds": 900,
"ems": {
"pv_forecast_wh": [0.0] * count,
"total_load": [25.0] * count,
"electricity_price_per_wh": [0.0003] * count,
"feed_in_tariff_per_wh": [0.00005] * count,
"price_per_wh_battery": 0.0,
},
"pv_battery": {
"device_id": "owned_battery",
"capacity_wh": 1000,
"initial_soc_percentage": 50,
},
"inverter": {
"device_id": "owned_inverter",
"battery_id": "owned_battery",
"max_power_wh": 1000,
},
"ev": None,
}
)
optimizer = GeneticOptimization(fixed_seed=42)
native = optimizer.optimize_ems(parameters, ngen=1, individuals=6)
repeat = GeneticOptimization(fixed_seed=42).optimize_ems(parameters, ngen=1, individuals=6)
assert native.start_solution == repeat.start_solution
assert native.interval_seconds == 900
assert native.start_solution_datetime == start
assert native.controls_start_at_now
assert len(native.result.load_wh_per_hour) == 4
assert native.parameters.ems.total_load == [25.0] * 4
parameters.ems.total_load[elapsed_slots] = 999.0
get_ems().set_start_datetime(start.add(days=1))
config_eos.optimization.genetic.interval_sec = 3600
with patch(
"akkudoktoreos.optimization.genetic.geneticsolution.get_prediction",
side_effect=AssertionError("Native serialization must not reread providers"),
):
generic = await native.optimization_solution()
plan = native.energy_management_plan()
assert generic.valid_from == start
assert generic.valid_until == start.add(hours=1)
assert plan.valid_from == start
assert plan.valid_until is None
assert all(
start.timestamp() <= item.execution_time.timestamp() < start.add(hours=1).timestamp()
for item in plan.instructions
)
forecast = generic.prediction.to_dataframe()
np.testing.assert_allclose(forecast["loadforecast_energy_wh"], [25.0] * 4)
np.testing.assert_allclose(forecast["elec_price_amt_kwh"], [0.3] * 4)
assert all(
(right - left).total_seconds() == 900
for left, right in zip(forecast.index, forecast.index[1:])
)
assert "owned_battery_soc_factor" in generic.solution.to_dataframe().columns
def test_native_quarter_hour_temperatures_average_when_coarsened(config_eos):
config_eos.merge_settings_from_dict(
{"optimization": {"genetic": {"interval_sec": 3600, "horizon_hours": 1}}}
)
get_ems(init=True).set_start_datetime(
to_datetime("2026-09-16T00:00:00+02:00", in_timezone="Europe/Berlin")
)
parameters = GeneticOptimizationParameters.model_validate(
{
"forecast_interval_seconds": 900,
"temperature_forecast": [10, 12, 14, 16],
"ems": {
"pv_forecast_wh": [25.0] * 4,
"total_load": [50.0] * 4,
"electricity_price_per_wh": [0.0003] * 4,
"feed_in_tariff_per_wh": 0.00005,
"price_per_wh_battery": 0.0,
},
"pv_battery": None,
"ev": None,
"inverter": None,
}
)
normalized = GeneticOptimization()._parameters_for_slot_grid(parameters)
assert normalized.temperature_forecast == [13.0]
assert normalized.ems.pv_forecast_wh == [100.0]
assert normalized.ems.total_load == [200.0]
@pytest.mark.parametrize("host_timezone", ["UTC", "Europe/Berlin"])
@pytest.mark.parametrize("offset", ["+02:00", "+01:00"])
@pytest.mark.parametrize("as_json_string", [False, True])
def test_snapshot_and_warm_start_preserve_aware_instants(
config_eos, set_other_timezone, host_timezone, offset, as_json_string
):
import json
from pathlib import Path
from akkudoktoreos.optimization.genetic.configrequest import (
ConfigOptimizationRequest,
)
from akkudoktoreos.optimization.genetic.geneticsolution import GeneticSolution
set_other_timezone(host_timezone)
expected = to_datetime(f"2026-10-25T02:30:00{offset}", in_timezone="Europe/Berlin")
supplied = expected.to_iso8601_string() if as_json_string else expected
payload = json.loads(
(Path(__file__).parent / "testdata/genetic/optimize_result_1.json").read_text()
)
payload["start_solution_datetime"] = supplied
native = GeneticSolution.model_validate(payload)
parameters = GeneticOptimizationParameters.model_validate(
native.parameters.model_dump() | {"start_solution_datetime": supplied}
)
request = ConfigOptimizationRequest.model_validate({"start_solution_datetime": supplied})
for model in (native, parameters, request):
value = model.start_solution_datetime
assert value is not None
assert value.timestamp() == expected.timestamp()
assert value.utcoffset() == expected.utcoffset()
if not as_json_string:
assert value.timezone_name == "Europe/Berlin"
assert value.fold == expected.fold
restored = type(model).model_validate_json(model.model_dump_json())
assert restored.start_solution_datetime is not None
assert restored.start_solution_datetime.timestamp() == expected.timestamp()
assert restored.start_solution_datetime.utcoffset() == expected.utcoffset()