Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: delta profile when 2 result sets have different periods #741

Merged
merged 1 commit into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/libecalc/common/time_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import enum
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import date, datetime, timedelta
from typing import Any, Optional, Self, Union
Expand Down Expand Up @@ -160,7 +161,7 @@ def create_periods(times: list[datetime], include_before: bool = True, include_a

return Periods(periods=periods)

def __iter__(self):
def __iter__(self) -> Iterator[Period]:
return self.periods.__iter__()

def get_period(self, period: Period) -> Optional[Period]:
Expand Down
35 changes: 24 additions & 11 deletions src/libecalc/presentation/simple_result/simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

from libecalc.common.component_info.component_level import ComponentLevel
from libecalc.common.component_type import ComponentType
from libecalc.common.errors.exceptions import ProgrammingError
from libecalc.common.logger import logger
from libecalc.common.string.string_utils import to_camel_case
from libecalc.common.time_utils import Period, Periods
Expand Down Expand Up @@ -132,11 +131,11 @@ def fit_to_periods(
delta profile comparisons easier/possible.

Args:
component (SimpleComponentResult): The component that should be fitted.
periods (Periods): The target periods. The provided periods should all exist in the component.
component (SimpleComponentResult): The component that should be fitted (should intersect all periods in the periods list). Can have periods before and after the *periods*. Those will be trimmed.
periods (Periods): The target periods. The provided periods should all exist in the component. If the above component are missing periods, we will extrapolate with values from the bigger period

Returns:
SimpleComponentResult: The component with the new periods.
SimpleComponentResult: The component with the new periods, ie. start and end will be trimmed, and mid-periods will be added if the component has a longer period than the global period.
"""
power = []
energy_usage = []
Expand All @@ -145,11 +144,17 @@ def fit_to_periods(
emission.name: SimpleEmissionResult(name=emission.name, rate=[])
for emission in component.emissions.values()
}
# We loop through a components periods and try to fit it to the common global period.
for period_index, _period in enumerate(component.periods):
period = Period.intersection(_period, periods.period)
# If the period is not in the global period, we skip it. ie before or after the global common period.
if period:
start = periods.start_dates.index(period.start)
end = periods.end_dates.index(period.end)

# In case we have a longer period in component than the global period, we have to add missing steps/periods
# e.g. if the common period has 2022-2023, but the component has 2022-2024, we have to loop twice to extrapolate the missing period with same values as the bigger period.
# Usually this will only loop once.
for _ in range(start, end + 1):
if component.power is not None:
power.append(component.power[period_index])
Expand All @@ -159,10 +164,9 @@ def fit_to_periods(
# Assume index exist if emission exist
emission.rate.append(component.emissions[emission.name].rate[period_index])
else:
# This is a developer error, we should provide the correct period.
raise ProgrammingError(
f"Provided periods includes period not found in component {component.id}. "
f"Extraneous period: {period}. This should not happen, contact support."
# We do not trim extraneous periods in beginning and end for a component. We only try to fit to the common global period.
TeeeJay marked this conversation as resolved.
Show resolved Hide resolved
logger.warning(
f"Period {component.periods[period_index]} from {component.name} not in {periods.period}. Skipping."
)

return cls(
Expand Down Expand Up @@ -256,7 +260,7 @@ def fit_to_periods(
cls,
model: "SimpleResultData",
periods: Periods,
):
) -> "SimpleResultData":
"""
Fit result to periods. Only a subset or the same set of periods is supported.
Args:
Expand Down Expand Up @@ -323,7 +327,7 @@ def normalize_components(
reference_model: "SimpleResultData",
changed_model: "SimpleResultData",
exclude: Optional[list[ComponentType]] = None,
):
) -> tuple["SimpleResultData", "SimpleResultData"]:
if exclude is None:
exclude = []

Expand Down Expand Up @@ -367,6 +371,11 @@ def delta_profile(
changed_model: "SimpleResultData",
) -> tuple["SimpleResultData", "SimpleResultData", "SimpleResultData", list[str]]:
"""
Calculate delta profile between two models. We will make sure that both models have the same
periods and components before calculating the delta profile. Different start and end will be trimmed.
This delta-profile methods supports both periods with fixed interval (monthly, yearly) and irregular intervals (data-defined).
The resulting delta-profile will have the union of the periods from both models, except differing start and end, that will be trimmed,
in order to have same start and end for both models.

Args:
reference_model:
Expand All @@ -379,12 +388,14 @@ def delta_profile(
first_date = max(changed_model.periods.first_date, reference_model.periods.first_date)
last_date = min(changed_model.periods.last_date, reference_model.periods.last_date)

# union of the dates in the 2 models
all_dates_in_models = sorted(
{date for date in reference_model.periods.all_dates if first_date <= date <= last_date}.union(
{date for date in changed_model.periods.all_dates if first_date <= date <= last_date}
)
)
# define new periods using all dates in both models

# define new periods using all dates in both models, skip extra periods before and after
periods = Periods.create_periods(
times=all_dates_in_models,
include_after=False,
Expand All @@ -396,6 +407,8 @@ def delta_profile(
reference_model=reference_model, changed_model=changed_model
)

# Now as we have found the union of the dates/periods in the models, and trimming
# start end, if they differ, we can fill out missing periods for the models
changed_model = cls.fit_to_periods(changed_model, periods)
reference_model = cls.fit_to_periods(reference_model, periods)

Expand Down