From 5ceb21819befab397a2cf1cc68c6a23599e25af3 Mon Sep 17 00:00:00 2001 From: sarankumar <120080287+sarankumar1325@users.noreply.github.com> Date: Mon, 26 Oct 2026 11:46:21 +0530 Subject: [PATCH] fix(genetic0): keep parameter arrays aligned with the start hour on DST days (#1455) --- .../optimization/genetic0/genetic0params.py | 10 ++- tests/test_genetic0params_dst.py | 76 +++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 tests/test_genetic0params_dst.py diff --git a/src/akkudoktoreos/optimization/genetic0/genetic0params.py b/src/akkudoktoreos/optimization/genetic0/genetic0params.py index b72782da..f03c6269 100644 --- a/src/akkudoktoreos/optimization/genetic0/genetic0params.py +++ b/src/akkudoktoreos/optimization/genetic0/genetic0params.py @@ -304,7 +304,15 @@ class Genetic0OptimizationParameters( # Add forecast and device data interval = to_duration(cls.config.optimization.genetic0.interval_sec) power_to_energy_per_interval_factor = cls.config.optimization.genetic0.interval_sec / 3600 - parameter_start_datetime = ems.start_datetime.set(hour=0, second=0, microsecond=0) + # GENETIC0 indexes the parameter arrays by wall-clock hour (the simulation + # starts at ``ems.start_datetime.hour``), while the arrays themselves are + # sampled in elapsed time. On a DST transition day local midnight is 23 or + # 25 elapsed hours away from the same wall-clock hour, so step back + # ``hour`` elapsed hours instead of setting the hour to midnight. This keeps + # array index ``ems.start_datetime.hour`` aligned with the start slot. + parameter_start_datetime = ems.start_datetime.set(second=0, microsecond=0).subtract( + hours=ems.start_datetime.hour + ) parameter_end_datetime = parameter_start_datetime.add(hours=cls.config.prediction.hours) max_retries = 10 diff --git a/tests/test_genetic0params_dst.py b/tests/test_genetic0params_dst.py new file mode 100644 index 00000000..52ce9eca --- /dev/null +++ b/tests/test_genetic0params_dst.py @@ -0,0 +1,76 @@ +"""GENETIC0 parameter arrays must stay aligned with the start hour on DST days.""" + +from unittest.mock import AsyncMock, Mock, patch + +import numpy as np +import pandas as pd +import pendulum +import pytest + +from akkudoktoreos.core.coreabc import get_ems +from akkudoktoreos.optimization.genetic0.genetic0params import ( + Genetic0OptimizationParameters, +) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "start", + [ + # Normal day: 23 elapsed hours since local midnight. + pendulum.datetime(2026, 10, 24, 23, tz="Europe/Berlin"), + # Fall back (CEST -> CET): 24 elapsed hours since local midnight. + pendulum.datetime(2026, 10, 25, 23, tz="Europe/Berlin"), + # Spring forward (CET -> CEST): 22 elapsed hours since local midnight. + pendulum.datetime(2026, 3, 29, 23, tz="Europe/Berlin"), + ], + ids=["normal", "fall-back", "spring-forward"], +) +async def test_parameter_arrays_start_hour_aligned_on_dst_days(config_eos, start): + config_eos.merge_settings_from_dict( + { + "general": {"timezone": "Europe/Berlin"}, + "prediction": {"hours": 48}, + "optimization": {"genetic0": {"horizon_hours": 24}}, + "feedintariff": {"provider": "FeedInTariffDvhubOnline"}, + "elecfee": {"provider": None}, + "devices": { + "max_batteries": 0, + "max_electric_vehicles": 0, + "max_inverters": 0, + "max_home_appliances": 0, + }, + } + ) + get_ems().set_start_datetime(start) + ems_start = get_ems().start_datetime + assert ems_start.hour == 23 + + # The tariff is only published from the start slot on, as a live + # provider would do late in the evening. + prices = pd.Series( + np.full(24 * 4, 0.0001), + index=pd.date_range(ems_start, periods=24 * 4, freq="15min"), + ) + array_starts = [] + + async def read_array(**kwargs): + array_starts.append(kwargs["start_datetime"]) + return np.full(48, 1.0) + + prediction = Mock( + update_data=AsyncMock(), + key_to_array=AsyncMock(side_effect=read_array), + key_to_raw_series=AsyncMock(return_value=prices), + ) + with patch.object(Genetic0OptimizationParameters, "prediction", prediction): + parameters = await Genetic0OptimizationParameters.prepare() + + assert parameters is not None + # Index ``start.hour`` of every array is the start slot. + assert array_starts + for array_start in array_starts: + assert array_start.add(hours=ems_start.hour) == ems_start + tariffs = parameters.ems.feed_in_tariff_per_wh + assert isinstance(tariffs, list) + assert np.isfinite(tariffs[ems_start.hour : ems_start.hour + 24]).all()