"""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()