diff --git a/src/akkudoktoreos/prediction/load_aggregator.py b/src/akkudoktoreos/prediction/load_aggregator.py new file mode 100644 index 0000000..96c5906 --- /dev/null +++ b/src/akkudoktoreos/prediction/load_aggregator.py @@ -0,0 +1,37 @@ +from collections import defaultdict +from collections.abc import Sequence + + +class LoadAggregator: + def __init__(self, prediction_hours: int = 24) -> None: + """Initializes the LoadAggregator object with the number of prediction hours. + + :param prediction_hours: Number of hours to predict (default: 24) + """ + self.loads: defaultdict[str, list[float]] = defaultdict( + list + ) # Dictionary to hold load arrays for different sources + self.prediction_hours: int = prediction_hours + + def add_load(self, name: str, last_array: Sequence[float]) -> None: + """Adds a load array for a specific source. Accepts a Sequence of floats. + + :param name: Name of the load source (e.g., "Household", "Heat Pump"). + :param last_array: Sequence of loads, where each entry corresponds to an hour. + :raises ValueError: If the length of last_array doesn't match the prediction hours. + """ + # Check length of the array without converting + if len(last_array) != self.prediction_hours: + raise ValueError(f"Total load inconsistent lengths in arrays: {name} {len(last_array)}") + self.loads[name] = list(last_array) + + def calculate_total_load(self) -> list[float]: + """Calculates the total load for each hour by summing up the loads from all sources. + + :return: A list representing the total load for each hour. + Returns an empty list if no loads have been added. + """ + # Optimize the summation using a single loop with zip + total_load = [sum(hourly_loads) for hourly_loads in zip(*self.loads.values())] + + return total_load diff --git a/src/akkudoktoreos/prediction/load_container.py b/src/akkudoktoreos/prediction/load_container.py deleted file mode 100644 index ea6e911..0000000 --- a/src/akkudoktoreos/prediction/load_container.py +++ /dev/null @@ -1,39 +0,0 @@ -import numpy as np - - -class Gesamtlast: - def __init__(self, prediction_hours: int = 24): - self.lasten: dict[ - str, np.ndarray - ] = {} # Contains names and load arrays for different sources - self.prediction_hours = prediction_hours - - def hinzufuegen(self, name: str, last_array: np.ndarray) -> None: - """Adds an array of loads for a specific source. - - :param name: Name of the load source (e.g., "Household", "Heat Pump") - :param last_array: Array of loads, where each entry corresponds to an hour - """ - if len(last_array) != self.prediction_hours: - raise ValueError(f"Total load inconsistent lengths in arrays: {name} {len(last_array)}") - self.lasten[name] = last_array - - def gesamtlast_berechnen(self) -> np.ndarray: - """Calculates the total load for each hour and returns an array of total loads. - - :return: Array of total loads, where each entry corresponds to an hour - """ - if not self.lasten: - return np.ndarray(0) - - # Assumption: All load arrays have the same length - stunden = len(next(iter(self.lasten.values()))) - gesamtlast_array = [0] * stunden - - for last_array in self.lasten.values(): - gesamtlast_array = [ - gesamtlast + stundenlast - for gesamtlast, stundenlast in zip(gesamtlast_array, last_array) - ] - - return np.array(gesamtlast_array) diff --git a/src/akkudoktoreos/server/fastapi_server.py b/src/akkudoktoreos/server/fastapi_server.py index 0b2f1ab..2b4948e 100755 --- a/src/akkudoktoreos/server/fastapi_server.py +++ b/src/akkudoktoreos/server/fastapi_server.py @@ -23,7 +23,7 @@ from akkudoktoreos.optimization.genetic import ( ) # Still to be adapted -from akkudoktoreos.prediction.load_container import Gesamtlast +from akkudoktoreos.prediction.load_aggregator import LoadAggregator from akkudoktoreos.prediction.load_corrector import LoadPredictionAdjuster from akkudoktoreos.prediction.load_forecast import LoadForecast from akkudoktoreos.prediction.prediction import get_prediction @@ -160,14 +160,13 @@ def fastapi_gesamtlast(request: GesamtlastRequest) -> list[float]: future_predictions = adjuster.predict_next_hours(hours) leistung_haushalt = future_predictions["Adjusted Pred"].to_numpy() - gesamtlast = Gesamtlast(prediction_hours=hours) - gesamtlast.hinzufuegen( + gesamtlast = LoadAggregator(prediction_hours=hours) + gesamtlast.add_load( "Haushalt", - leistung_haushalt, + tuple(leistung_haushalt), ) - last = gesamtlast.gesamtlast_berechnen() - return last.tolist() + return gesamtlast.calculate_total_load() @app.get("/gesamtlast_simple") @@ -183,8 +182,10 @@ def fastapi_gesamtlast_simple(year_energy: float) -> list[float]: )[0] # Get expected household load for the date range prediction_hours = config_eos.prediction_hours if config_eos.prediction_hours else 48 - gesamtlast = Gesamtlast(prediction_hours=prediction_hours) # Create Gesamtlast instance - gesamtlast.hinzufuegen("Haushalt", leistung_haushalt) # Add household to total load calculation + gesamtlast = LoadAggregator(prediction_hours=prediction_hours) # Create Gesamtlast instance + gesamtlast.add_load( + "Haushalt", tuple(leistung_haushalt) + ) # Add household to total load calculation # ############### # # WP (Heat Pump) @@ -192,8 +193,7 @@ def fastapi_gesamtlast_simple(year_energy: float) -> list[float]: # leistung_wp = wp.simulate_24h(temperature_forecast) # Simulate heat pump load for 24 hours # gesamtlast.hinzufuegen("Heatpump", leistung_wp) # Add heat pump load to total load calculation - last = gesamtlast.gesamtlast_berechnen() # Calculate total load - return last.tolist() # Return total load as JSON + return gesamtlast.calculate_total_load() class ForecastResponse(PydanticBaseModel): diff --git a/tests/test_load_aggregator.py b/tests/test_load_aggregator.py new file mode 100644 index 0000000..83453f7 --- /dev/null +++ b/tests/test_load_aggregator.py @@ -0,0 +1,39 @@ +import pytest + +from akkudoktoreos.prediction.load_aggregator import LoadAggregator + + +def test_initialization(): + aggregator = LoadAggregator() + assert aggregator.prediction_hours == 24 + assert aggregator.loads == {} + + +def test_add_load_valid(): + aggregator = LoadAggregator(prediction_hours=3) + aggregator.add_load("Source1", [10.0, 20.0, 30.0]) + assert aggregator.loads["Source1"] == [10.0, 20.0, 30.0] + + +def test_add_load_invalid_length(): + aggregator = LoadAggregator(prediction_hours=3) + with pytest.raises(ValueError, match="Total load inconsistent lengths in arrays: Source1 2"): + aggregator.add_load("Source1", [10.0, 20.0]) + + +def test_calculate_total_load_empty(): + aggregator = LoadAggregator() + assert aggregator.calculate_total_load() == [] + + +def test_calculate_total_load(): + aggregator = LoadAggregator(prediction_hours=3) + aggregator.add_load("Source1", [10.0, 20.0, 30.0]) + aggregator.add_load("Source2", [5.0, 15.0, 25.0]) + assert aggregator.calculate_total_load() == [15.0, 35.0, 55.0] + + +def test_calculate_total_load_single_source(): + aggregator = LoadAggregator(prediction_hours=3) + aggregator.add_load("Source1", [10.0, 20.0, 30.0]) + assert aggregator.calculate_total_load() == [10.0, 20.0, 30.0]