mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2025-11-25 06:46:25 +00:00
fix: load data for automatic optimization (#731)
Automatic optimization used to take the adjusted load data even if there were no measurements leading to 0 load values. Split LoadAkkudoktor into LoadAkkudoktor and LoadAkkudoktorAdjusted. This allows to select load data either purely from the load data database or load data additionally adjusted by load measurements. Some value names have been adapted to denote also the unit of a value. For better load bug squashing the optimization solution data availability was improved. For better data visbility prediction data can now be distinguished from solution data in the generic optimization solution. Some predictions that may be of interest to understand the solution were added. Documentation was updated to resemble the addition load prediction provider and the value name changes. Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
This commit is contained in:
@@ -7,26 +7,97 @@ from loguru import logger
|
||||
from pydantic import Field
|
||||
|
||||
from akkudoktoreos.config.configabc import SettingsBaseModel
|
||||
from akkudoktoreos.prediction.loadabc import LoadProvider
|
||||
from akkudoktoreos.prediction.loadabc import LoadDataRecord, LoadProvider
|
||||
from akkudoktoreos.utils.datetimeutil import compare_datetimes, to_datetime, to_duration
|
||||
|
||||
|
||||
class LoadAkkudoktorCommonSettings(SettingsBaseModel):
|
||||
"""Common settings for load data import from file."""
|
||||
|
||||
loadakkudoktor_year_energy: Optional[float] = Field(
|
||||
loadakkudoktor_year_energy_kwh: Optional[float] = Field(
|
||||
default=None, description="Yearly energy consumption (kWh).", examples=[40421]
|
||||
)
|
||||
|
||||
|
||||
class LoadAkkudoktorDataRecord(LoadDataRecord):
|
||||
"""Represents a load data record with extra fields for LoadAkkudoktor."""
|
||||
|
||||
loadakkudoktor_mean_power_w: Optional[float] = Field(
|
||||
default=None, description="Predicted load mean value (W)."
|
||||
)
|
||||
|
||||
loadakkudoktor_std_power_w: Optional[float] = Field(
|
||||
default=None, description="Predicted load standard deviation (W)."
|
||||
)
|
||||
|
||||
|
||||
class LoadAkkudoktor(LoadProvider):
|
||||
"""Fetch Load forecast data from Akkudoktor load profiles."""
|
||||
|
||||
records: list[LoadAkkudoktorDataRecord] = Field(
|
||||
default_factory=list, description="List of LoadAkkudoktorDataRecord records"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def provider_id(cls) -> str:
|
||||
"""Return the unique identifier for the LoadAkkudoktor provider."""
|
||||
return "LoadAkkudoktor"
|
||||
|
||||
def load_data(self) -> np.ndarray:
|
||||
"""Loads data from the Akkudoktor load file."""
|
||||
load_file = self.config.package_root_path.joinpath("data/load_profiles.npz")
|
||||
data_year_energy = None
|
||||
try:
|
||||
file_data = np.load(load_file)
|
||||
profile_data = np.array(
|
||||
list(zip(file_data["yearly_profiles"], file_data["yearly_profiles_std"]))
|
||||
)
|
||||
# Calculate values in W by relative profile data and yearly consumption given in kWh
|
||||
data_year_energy = (
|
||||
profile_data
|
||||
* self.config.load.provider_settings.LoadAkkudoktor.loadakkudoktor_year_energy_kwh
|
||||
* 1000
|
||||
)
|
||||
except FileNotFoundError:
|
||||
error_msg = f"Error: File {load_file} not found."
|
||||
logger.error(error_msg)
|
||||
raise FileNotFoundError(error_msg)
|
||||
except Exception as e:
|
||||
error_msg = f"An error occurred while loading data: {e}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg)
|
||||
return data_year_energy
|
||||
|
||||
def _update_data(self, force_update: Optional[bool] = False) -> None:
|
||||
"""Adds the load means and standard deviations."""
|
||||
data_year_energy = self.load_data()
|
||||
# We provide prediction starting at start of day, to be compatible to old system.
|
||||
# End date for prediction is prediction hours from now.
|
||||
date = self.ems_start_datetime.start_of("day")
|
||||
end_date = self.ems_start_datetime.add(hours=self.config.prediction.hours)
|
||||
while compare_datetimes(date, end_date).lt:
|
||||
# Extract mean (index 0) and standard deviation (index 1) for the given day and hour
|
||||
# Day indexing starts at 0, -1 because of that
|
||||
hourly_stats = data_year_energy[date.day_of_year - 1, :, date.hour]
|
||||
values = {
|
||||
"loadforecast_power_w": hourly_stats[0],
|
||||
"loadakkudoktor_mean_power_w": hourly_stats[0],
|
||||
"loadakkudoktor_std_power_w": hourly_stats[1],
|
||||
}
|
||||
self.update_value(date, values)
|
||||
date += to_duration("1 hour")
|
||||
# We are working on fresh data (no cache), report update time
|
||||
self.update_datetime = to_datetime(in_timezone=self.config.general.timezone)
|
||||
|
||||
|
||||
class LoadAkkudoktorAdjusted(LoadAkkudoktor):
|
||||
"""Fetch Load forecast data from Akkudoktor load profiles with adjustment by measurements."""
|
||||
|
||||
@classmethod
|
||||
def provider_id(cls) -> str:
|
||||
"""Return the unique identifier for the LoadAkkudoktor provider."""
|
||||
return "LoadAkkudoktorAdjusted"
|
||||
|
||||
def _calculate_adjustment(self, data_year_energy: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""Calculate weekday and week end adjustment from total load measurement data.
|
||||
|
||||
@@ -79,31 +150,6 @@ class LoadAkkudoktor(LoadProvider):
|
||||
|
||||
return (weekday_adjust, weekend_adjust)
|
||||
|
||||
def load_data(self) -> np.ndarray:
|
||||
"""Loads data from the Akkudoktor load file."""
|
||||
load_file = self.config.package_root_path.joinpath("data/load_profiles.npz")
|
||||
data_year_energy = None
|
||||
try:
|
||||
file_data = np.load(load_file)
|
||||
profile_data = np.array(
|
||||
list(zip(file_data["yearly_profiles"], file_data["yearly_profiles_std"]))
|
||||
)
|
||||
# Calculate values in W by relative profile data and yearly consumption given in kWh
|
||||
data_year_energy = (
|
||||
profile_data
|
||||
* self.config.load.provider_settings.LoadAkkudoktor.loadakkudoktor_year_energy
|
||||
* 1000
|
||||
)
|
||||
except FileNotFoundError:
|
||||
error_msg = f"Error: File {load_file} not found."
|
||||
logger.error(error_msg)
|
||||
raise FileNotFoundError(error_msg)
|
||||
except Exception as e:
|
||||
error_msg = f"An error occurred while loading data: {e}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg)
|
||||
return data_year_energy
|
||||
|
||||
def _update_data(self, force_update: Optional[bool] = False) -> None:
|
||||
"""Adds the load means and standard deviations."""
|
||||
data_year_energy = self.load_data()
|
||||
@@ -117,8 +163,8 @@ class LoadAkkudoktor(LoadProvider):
|
||||
# Day indexing starts at 0, -1 because of that
|
||||
hourly_stats = data_year_energy[date.day_of_year - 1, :, date.hour]
|
||||
values = {
|
||||
"load_mean": hourly_stats[0],
|
||||
"load_std": hourly_stats[1],
|
||||
"loadakkudoktor_mean_power_w": hourly_stats[0],
|
||||
"loadakkudoktor_std_power_w": hourly_stats[1],
|
||||
}
|
||||
if date.day_of_week < 5:
|
||||
# Monday to Friday (0..4)
|
||||
@@ -126,7 +172,7 @@ class LoadAkkudoktor(LoadProvider):
|
||||
else:
|
||||
# Saturday, Sunday (5, 6)
|
||||
value_adjusted = hourly_stats[0] + weekend_adjust[date.hour]
|
||||
values["load_mean_adjusted"] = max(0, value_adjusted)
|
||||
values["loadforecast_power_w"] = max(0, value_adjusted)
|
||||
self.update_value(date, values)
|
||||
date += to_duration("1 hour")
|
||||
# We are working on fresh data (no cache), report update time
|
||||
|
||||
Reference in New Issue
Block a user