Skip to content

Commit

Permalink
refactor: merge functionality for results
Browse files Browse the repository at this point in the history
Make the merge functionality easier to reuse and extend.
  • Loading branch information
jsolaas committed Sep 21, 2023
1 parent 24c27bb commit d45765d
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 205 deletions.
55 changes: 55 additions & 0 deletions src/libecalc/common/tabular_time_series.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import itertools
from datetime import datetime
from typing import List, Protocol, TypeVar

from libecalc.common.utils.rates import TimeSeries
from typing_extensions import Self


class TabularTimeSeries(Protocol):
timesteps: List[datetime]

def copy(self) -> Self:
...


ObjectWithTimeSeries = TypeVar("ObjectWithTimeSeries", bound=TabularTimeSeries)


class TabularTimeSeriesUtils:
"""
Utility functions for objects containing TimeSeries
"""

@classmethod
def _merge_helper(cls, *objects_with_timeseries: ObjectWithTimeSeries) -> ObjectWithTimeSeries:
first, *others = objects_with_timeseries
merged_object = first.copy()

for key, value in first.__dict__.items():
for other in others:
accumulated_value = merged_object.__getattribute__(key)
other_value = other.__getattribute__(key)
if key == "timesteps":
merged_timesteps = sorted(itertools.chain(accumulated_value, other_value))
merged_object.__setattr__(key, merged_timesteps)
elif isinstance(value, TimeSeries):
merged_object.__setattr__(key, accumulated_value.merge(other_value))

return merged_object

@classmethod
def merge(cls, *tabular_time_series_list: TabularTimeSeries):
"""
Merge objects containing TimeSeries. Other attributes will be copied from the first object.
Args:
*tabular_time_series_list: list of objects to merge
Returns: a merged object of the same type
"""
# Verify that we are merging the same types
if len({type(tabular_time_series) for tabular_time_series in tabular_time_series_list}) != 1:
raise ValueError("Can not merge objects of differing types.")

return cls._merge_helper(*tabular_time_series_list)
91 changes: 90 additions & 1 deletion src/libecalc/common/utils/rates.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import itertools
import math
from abc import ABC, abstractmethod
from collections import defaultdict
Expand Down Expand Up @@ -173,6 +174,41 @@ def extend(self, other: TimeSeries) -> Self:
unit=self.unit,
)

def merge(self, other: TimeSeries) -> Self:
"""
Merge two TimeSeries with differing timesteps
Args:
other:
Returns:
"""
if not isinstance(other, type(self)):
raise ValueError(f"Can not merge {type(self)} with {type(other)}")

if self.unit != other.unit:
raise ValueError(f"Mismatching units: '{self.unit}' != `{other.unit}`")

if len(set(self.timesteps).intersection(other.timesteps)) != 0:
raise ValueError("Can not merge two TimeSeries with common timesteps")

merged_timesteps = sorted(itertools.chain(self.timesteps, other.timesteps))
merged_values = []

for timestep in merged_timesteps:
if timestep in self.timesteps:
timestep_index = self.timesteps.index(timestep)
merged_values.append(self.values[timestep_index])
else:
timestep_index = other.timesteps.index(timestep)
merged_values.append(other.values[timestep_index])

return self.__class__(
timesteps=merged_timesteps,
values=merged_values,
unit=self.unit,
)

def datapoints(self) -> Iterator[Tuple[datetime, TimeSeriesValue]]:
yield from zip(self.timesteps, self.values)

Expand Down Expand Up @@ -426,7 +462,8 @@ def resample(self, freq: Frequency) -> TimeSeriesVolumesCumulative:
ds_resampled = ds_interpolated.reindex(new_index)

return TimeSeriesVolumesCumulative(
timesteps=ds_resampled.index.to_pydatetime().tolist(), # Are we sure this is always an DatetimeIndex? type: ignore
timesteps=ds_resampled.index.to_pydatetime().tolist(),
# Are we sure this is always an DatetimeIndex? type: ignore
values=ds_resampled.values.tolist(),
unit=self.unit,
)
Expand Down Expand Up @@ -669,6 +706,58 @@ def extend(self, other: TimeSeriesRate) -> Self: # type: ignore[override]
rate_type=self.rate_type,
)

def merge(self, other: TimeSeries) -> TimeSeriesRate:
"""
Merge two TimeSeries with differing timesteps
Args:
other:
Returns:
"""

if not isinstance(other, TimeSeriesRate):
raise ValueError(f"Can not merge {type(self.__class__)} with {type(self.__class__)}")

if self.unit != other.unit:
raise ValueError(f"Mismatching units: '{self.unit}' != `{other.unit}`")

if not self.rate_type == other.rate_type:
raise ValueError(
"Mismatching rate type. Currently you can not extend stream/calendar day rates with calendar/stream day rates."
)

if len(set(self.timesteps).intersection(other.timesteps)) != 0:
raise ValueError("Can not merge two TimeSeries with common timesteps")

merged_timesteps = sorted(itertools.chain(self.timesteps, other.timesteps))
merged_values = []
merged_regularity = []

for timestep in merged_timesteps:
if timestep in self.timesteps:
timestep_index = self.timesteps.index(timestep)
merged_values.append(self.values[timestep_index])
if self.regularity is not None:
merged_regularity.append(self.regularity[timestep_index])
else:
merged_regularity.append(1) # whaaaaaaaaaa
else:
timestep_index = other.timesteps.index(timestep)
merged_values.append(other.values[timestep_index])
if other.regularity is not None:
merged_regularity.append(other.regularity[timestep_index])
else:
merged_regularity.append(1) # whaaaaaaaaaa

return self.__class__(
timesteps=merged_timesteps,
values=merged_values,
regularity=merged_regularity,
unit=self.unit,
rate_type=self.rate_type,
)

def for_period(self, period: Period) -> Self:
start_index, end_index = period.get_timestep_indices(self.timesteps)
end_index = end_index + 1 # Include end as we need it to calculate cumulative correctly
Expand Down
Loading

0 comments on commit d45765d

Please sign in to comment.