mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2026-08-05 16:36:12 +00:00
Change PDF visualization to be created on demand and per optimization algorithm. The PDF for the GENETIC0 optimization is provided by the /visualization_results.pdf endpoint. There is no change in the interface. By this the optimization algorithm is offloaded from the PDF generation which spares some time. To cope with several users may call the /visualization_results.pdf endpoint at the same time the PDF is generated on the fly without any intermediate file taking the stored GENETIC0 solution as an input. SVG picture generation is removed as this would again create intermediate files. Chart pictures can easily be taken from the PDF. To allow on demand creation of the optimization results visualization the optimisation solution stored is extended by several new attributes. To keep the deprecated /optimize endpoint compatible the optimization solution is stripped to the legacy content before returned. Due to the extension of the solution the optimization tests were adapted to cover the extended content. The optimization tests are adapted to test the generated visualization report by the pypdf reader. Pypdf is added to the development dependencies. Besides the adaptation several fixes and improvements are added: * feat: extend /v1/prediction/series endpoint by resampling and filling Add parameters for resampling and filling. Add the processing parameter to control wether raw data or resampled data shall be returned. * feat: extend /v1/measurement/series endpoint by resampling and filling Add parameters for resampling and filling: Add the processing parameter to control wether raw data or resampled data shall be returned. * feat: standardize and improve API error response Use FASTApi exception handlers to provide a standardized API exception handling. All exceptions are logged. Exception traces are only returned if the new logging configuration parameter logging.api_logging_level is set to "DEBUG" or "TRACE". Avoids unwanted leackage of server internals on exceptions. * fix: align to intervall when resampling Ensure resampling is aligned to interval also when the buckets are shifted due to the align_to_intervall parameter is set. * chore: make dropna mandatory and default to True * chore: refactor key_to_xxx data management methods Make key_to_series the central method for data resampling and fill. Add a new key_to_raw_series to retrieve the data as it is stored (without resampling and filling). Users of key_to_series were mostly moved to key_to_raw_series as this resembles the former interface. Especially in predictions and tests this was done. * chore: create test data sub-directory for each optimization algorithm To prevent cluttering the test data directory and ease test data management for optimization algorithms each algorithm got it's own sub-directory. The current test data was moved to these sub-directories. * chore: update version Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
172 lines
5.8 KiB
Python
172 lines
5.8 KiB
Python
import json
|
|
from io import BytesIO
|
|
from pathlib import Path
|
|
from typing import Any
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
from pypdf import PdfReader
|
|
|
|
from akkudoktoreos.config.config import ConfigEOS
|
|
from akkudoktoreos.core.cache import CacheEnergyManagementStore
|
|
from akkudoktoreos.core.coreabc import get_ems
|
|
from akkudoktoreos.optimization.genetic0.genetic0 import Genetic0Optimization
|
|
from akkudoktoreos.optimization.genetic0.genetic0params import (
|
|
Genetic0OptimizationParameters,
|
|
)
|
|
from akkudoktoreos.optimization.genetic0.genetic0solution import Genetic0Solution
|
|
from akkudoktoreos.optimization.genetic0.genetic0visualize import (
|
|
genetic0_prepare_visualize,
|
|
)
|
|
from akkudoktoreos.utils.datetimeutil import to_datetime
|
|
|
|
ems_eos = get_ems(init=True) # init once
|
|
|
|
DIR_TESTDATA = Path(__file__).parent / "testdata" / "genetic0"
|
|
|
|
|
|
def compare_dict(actual: dict[str, Any], expected: dict[str, Any]):
|
|
assert set(actual) == set(expected)
|
|
|
|
for key, value in expected.items():
|
|
if isinstance(value, dict):
|
|
assert isinstance(actual[key], dict)
|
|
compare_dict(actual[key], value)
|
|
elif isinstance(value, list):
|
|
assert isinstance(actual[key], list)
|
|
assert actual[key] == pytest.approx(value)
|
|
else:
|
|
assert actual[key] == pytest.approx(value)
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
"fn_in, fn_out, ngen, break_even",
|
|
[
|
|
("optimize_input_1.json", "optimize_result_1.json", 3, 0),
|
|
("optimize_input_2.json", "optimize_result_2.json", 3, 0),
|
|
("optimize_input_2.json", "optimize_result_2_full.json", 400, 0),
|
|
("optimize_input_1.json", "optimize_result_1_be.json", 3, 1),
|
|
("optimize_input_2.json", "optimize_result_2_be.json", 3, 1),
|
|
],
|
|
)
|
|
async def test_optimize(
|
|
fn_in: str,
|
|
fn_out: str,
|
|
ngen: int,
|
|
break_even: int,
|
|
config_eos: ConfigEOS,
|
|
is_finalize: bool,
|
|
):
|
|
"""Test optimize_ems."""
|
|
# Test parameters
|
|
fixed_start_hour = 10
|
|
fixed_seed = 42
|
|
|
|
# Assure configuration holds the correct values
|
|
config_eos.merge_settings_from_dict(
|
|
{
|
|
"prediction": {
|
|
"hours": 48
|
|
},
|
|
"optimization": {
|
|
"algorithm": "GENETIC0",
|
|
"genetic0": {
|
|
"horizon_hours": 48,
|
|
"individuals": 300,
|
|
"generations": 10,
|
|
"penalties": {
|
|
"ev_soc_miss": 10,
|
|
"ac_charge_break_even": break_even,
|
|
}
|
|
}
|
|
},
|
|
"devices": {
|
|
"max_electric_vehicles": 1,
|
|
"electric_vehicles": [
|
|
{
|
|
"charge_rates": [0.0, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0],
|
|
}
|
|
],
|
|
}
|
|
}
|
|
)
|
|
|
|
# Load input and output data
|
|
parameter_file = DIR_TESTDATA / fn_in
|
|
with parameter_file.open("r") as f_in:
|
|
input_data = Genetic0OptimizationParameters(**json.load(f_in))
|
|
|
|
# Fake energy management run start datetime
|
|
ems_eos.set_start_datetime(to_datetime().set(hour=fixed_start_hour))
|
|
|
|
# Throw away any cached results of the last energy management run.
|
|
CacheEnergyManagementStore().clear()
|
|
|
|
genetic0_optimization = Genetic0Optimization(fixed_seed=fixed_seed)
|
|
|
|
# Activate with pytest --finalize
|
|
if ngen > 10 and not is_finalize:
|
|
pytest.skip()
|
|
|
|
# Call the optimization function
|
|
genetic0_solution = genetic0_optimization.optimize_ems(
|
|
parameters=input_data, start_hour=fixed_start_hour, ngen=ngen
|
|
)
|
|
|
|
# Write test output to file, so we can take it as new data on intended change
|
|
TESTDATA_FILE = DIR_TESTDATA / f"new_{fn_out}"
|
|
with TESTDATA_FILE.open("w", encoding="utf-8", newline="\n") as f_out:
|
|
f_out.write(genetic0_solution.model_dump_json(indent=4, exclude_unset=True))
|
|
|
|
solution_file = DIR_TESTDATA / fn_out
|
|
# In case a new test case is added, we don't want to fail here, so the new output is written
|
|
# to disk before
|
|
try:
|
|
with solution_file.open("r") as f_out:
|
|
expected_data = json.load(f_out)
|
|
expected_result = Genetic0Solution(**expected_data)
|
|
except ValidationError:
|
|
# Expected genetic solution data does not fit to Genetic0Solution data schema
|
|
# Possibly the Genetic0Solution class changed.
|
|
pytest.fail(
|
|
f"ValidationError: Can not load expected solution from {solution_file}\n"
|
|
f"cp {TESTDATA_FILE} {solution_file}\n"
|
|
)
|
|
except FileNotFoundError:
|
|
# Should not happen
|
|
pytest.fail(
|
|
f"FileNotFoundError: Can not load expected solution from {solution_file}\n"
|
|
f"cp {TESTDATA_FILE} {solution_file}\n"
|
|
)
|
|
|
|
assert genetic0_solution.result.Gesamtbilanz_Euro == pytest.approx(
|
|
expected_result.result.Gesamtbilanz_Euro
|
|
)
|
|
|
|
# Assert that the output contains all expected entries.
|
|
# This does not assert that the optimization always gives the same result!
|
|
# Reproducibility and mathematical accuracy should be tested on the level of individual components.
|
|
compare_dict(genetic0_solution.model_dump(), expected_result.model_dump())
|
|
|
|
# Check the correct generic optimization solution is created
|
|
optimization_solution = await genetic0_solution.optimization_solution()
|
|
# @TODO
|
|
|
|
# Check the correct generic energy management plan is created
|
|
plan = genetic0_solution.energy_management_plan()
|
|
# @TODO
|
|
|
|
# Check visualization works
|
|
pdf = genetic0_prepare_visualize(
|
|
solution=genetic0_solution,
|
|
)
|
|
assert pdf.startswith(b"%PDF-")
|
|
|
|
reader = PdfReader(BytesIO(pdf))
|
|
assert len(reader.pages) == 6
|
|
|
|
|
|
# Everything passed, remove generated files
|
|
TESTDATA_FILE.unlink()
|