mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2026-11-13 09:13:30 +01:00
feat: add filename linter and AI coding guidelines (#1477)
Add a configurable filename linter to enforce naming conventions and an AGENTS.md to guide AI coding agents in following project conventions. The guidelines steer code generation; the linter independently verifies filename compliance. Together, they improve consistency and reduce unnecessary files and review effort. Conflicting file names were renamed. Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user