Skip to content

Commit

Permalink
refactor: remove regularity our of core/domain (#246)
Browse files Browse the repository at this point in the history
Regularity should not be transferred or handled in
core. At the time data arrives in core, it should only
be stream day, and we should not have to relate to "what
type of rate" is it, until we decide whether the user
output should be calendar or stream day (default) based
on user needs.

Refs ECALC-196
  • Loading branch information
TeeeJay committed Oct 20, 2023
1 parent a1d2ce6 commit 714888b
Show file tree
Hide file tree
Showing 44 changed files with 1,218 additions and 3,706 deletions.
4 changes: 2 additions & 2 deletions src/libecalc/common/stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from libecalc.common.logger import logger
from libecalc.common.string_utils import to_camel_case
from libecalc.common.utils.rates import TimeSeriesFloat, TimeSeriesRate
from libecalc.common.utils.rates import TimeSeriesFloat, TimeSeriesStreamDayRate
from pydantic import BaseModel, Extra
from typing_extensions import Self

Expand All @@ -16,7 +16,7 @@ class Config:
allow_population_by_field_name = True

name: Optional[str]
rate: TimeSeriesRate
rate: TimeSeriesStreamDayRate
pressure: TimeSeriesFloat
fluid_density: Optional[TimeSeriesFloat] = None

Expand Down
18 changes: 18 additions & 0 deletions src/libecalc/common/time_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,28 @@ def __str__(self) -> str:
return f"{self.start}:{self.end}"

def __contains__(self, time: datetime) -> bool:
"""
A period of time is defined as [start, end>,
ie inclusive start and exclusive end.
Args:
time:
Returns:
"""
return self.start <= time < self.end

@staticmethod
def intersects(first: Period, second: Period) -> bool:
"""
Args:
first:
second:
Returns:
"""
return first.start in second or second.start in first

def get_timestep_indices(self, timesteps: List[datetime]) -> Tuple[int, int]:
Expand Down
11 changes: 6 additions & 5 deletions src/libecalc/common/utils/calculate_emission_intensity.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
import numpy as np
import pandas as pd
from libecalc.common.units import Unit
from libecalc.common.utils.rates import TimeSeriesRate, TimeSeriesVolumesCumulative
from libecalc.dto.types import RateType
from libecalc.common.utils.rates import (
TimeSeriesCalendarDayRate,
TimeSeriesVolumesCumulative,
)


def compute_emission_intensity_yearly(
Expand Down Expand Up @@ -64,7 +66,7 @@ def compute_emission_intensity_yearly(
def compute_emission_intensity_by_yearly_buckets(
emission_cumulative: TimeSeriesVolumesCumulative,
hydrocarbon_export_cumulative: TimeSeriesVolumesCumulative,
) -> TimeSeriesRate:
) -> TimeSeriesCalendarDayRate:
"""Legacy code that computes yearly intensity and casts the results back to the original time-vector."""
timesteps = emission_cumulative.timesteps
yearly_buckets = range(timesteps[0].year, timesteps[-1].year + 1)
Expand All @@ -73,9 +75,8 @@ def compute_emission_intensity_by_yearly_buckets(
hydrocarbon_export_cumulative=hydrocarbon_export_cumulative.values,
time_vector=timesteps,
)
return TimeSeriesRate(
return TimeSeriesCalendarDayRate(
timesteps=timesteps,
values=[yearly_intensity[yearly_buckets.index(t.year)] for t in timesteps],
unit=Unit.KG_SM3,
rate_type=RateType.CALENDAR_DAY,
)
142 changes: 114 additions & 28 deletions src/libecalc/common/utils/rates.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from typing import (
Any,
DefaultDict,
Dict,
Generic,
Iterable,
Iterator,
Expand Down Expand Up @@ -243,6 +242,22 @@ def for_timestep(self, current_timestep: datetime) -> Self:
unit=self.unit,
)

def for_timesteps(self, timesteps: List[datetime]) -> Self:
"""For a given list of datetime, return corresponding values
Args:
timesteps:
Returns:
"""
values: List[TimeSeriesValue] = []
for timestep in timesteps:
timestep_index = self.timesteps.index(timestep)
values.append(self.values[timestep_index])

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

def to_unit(self, unit: Unit) -> Self:
if unit == self.unit:
return self.copy()
Expand Down Expand Up @@ -476,7 +491,7 @@ def resample(self, freq: Frequency) -> TimeSeriesVolumesCumulative:
unit=self.unit,
)

def __truediv__(self, other: object) -> TimeSeriesRate:
def __truediv__(self, other: object) -> TimeSeriesCalendarDayRate:
if not isinstance(other, TimeSeriesVolumesCumulative):
raise TypeError(f"Dividing TimeSeriesVolumesCumulative by '{str(other.__class__)}' is not supported.")

Expand All @@ -486,7 +501,7 @@ def __truediv__(self, other: object) -> TimeSeriesRate:
raise ProgrammingError(
f"Unable to divide unit '{self.unit}' by unit '{other.unit}'. Please add unit conversion."
)
return TimeSeriesRate(
return TimeSeriesCalendarDayRate(
timesteps=self.timesteps,
values=list(
np.divide(
Expand All @@ -497,7 +512,6 @@ def __truediv__(self, other: object) -> TimeSeriesRate:
)
),
unit=unit,
rate_type=RateType.CALENDAR_DAY,
)

def to_volumes(self) -> TimeSeriesVolumes:
Expand Down Expand Up @@ -588,11 +602,12 @@ def to_rate(self, regularity: Optional[List[float]] = None) -> TimeSeriesRate:
delta_days = calculate_delta_days(self.timesteps).tolist()
average_rates = [volume / days for volume, days in zip(self.values, delta_days)]

if regularity and len(regularity) == len(self.timesteps) - 1:
if regularity is not None and isinstance(regularity, list) and len(regularity) == len(self.timesteps) - 1:
regularity.append(0.0)
average_rates.append(0.0)
else:
average_rates = self.values
regularity = [1.0] * len(self.timesteps)

return TimeSeriesRate(
timesteps=self.timesteps,
Expand Down Expand Up @@ -632,6 +647,50 @@ def resample(self, freq: Frequency) -> TimeSeriesIntensity:
)


class TimeSeriesStreamDayRate(TimeSeriesFloat):
"""
Domain/core layer only.
Explicit class for only internal core usage. Makes it easy to catch that the
type of data is rate, and has been converted to Stream Day for internal usage.
When used internally, rate is handled as a "point in time float". It is only needed to
be handled specifically when reporting, e.g. converting to calendar day rate, if needed.
"""

def __add__(self, other: TimeSeriesStreamDayRate) -> TimeSeriesStreamDayRate:
"""
Args:
other:
Returns:
"""
# Check for same unit
if not self.unit == other.unit:
raise ValueError(f"Mismatching units: '{self.unit}' != `{other.unit}`")

if isinstance(other, TimeSeriesStreamDayRate):
return TimeSeriesStreamDayRate(
timesteps=self.timesteps,
values=list(elementwise_sum(self.values, other.values)),
unit=self.unit,
)
else:
raise TypeError(
f"TimeSeriesRate can only be added to another TimeSeriesRate. Received type '{str(other.__class__)}'."
)


class TimeSeriesCalendarDayRate(TimeSeriesFloat):
"""
Application layer only - only calendar day rate/used for reporting
Probably not needed, as we want to provide info on regularity etc for the fixed calendar rate data too
"""

...


class TimeSeriesRate(TimeSeries[float]):
"""A rate time series with can be either in RateType.STREAM_DAY (default) or RateType.CALENDAR_DAY.
Expand All @@ -645,20 +704,19 @@ class TimeSeriesRate(TimeSeries[float]):
Stream day rates are not relevant for fuel consumption, tax and emissions.
"""

rate_type: Optional[RateType] = RateType.STREAM_DAY
regularity: Optional[List[float]] # TODO: Consider to set explicitly as a fallback to 1 may easily lead to errors

@validator("regularity", pre=True, always=True)
def set_regularity(cls, regularity: Optional[List[float]], values: Dict[str, Any]) -> List[float]:
if (
regularity is not None and regularity != []
): # TODO: Current workaround. To be handled when regularity is handled correctly
return regularity
try:
return [1] * len(values["values"])
except KeyError:
# 'Values' of timeseries is not defined. Not this validators responsibility.
return []
rate_type: RateType
regularity: List[float]

@validator("regularity")
def check_regularity_length(cls, regularity: List[float], values: Any) -> List[float]:
regularity_length = len(regularity)
timesteps_length = len(values.get("timesteps", []))
if regularity_length != timesteps_length:
raise ProgrammingError(
f"Regularity must correspond to nr of timesteps. Length of timesteps ({timesteps_length}) != length of regularity ({regularity_length})."
)

return regularity

def __add__(self, other: TimeSeriesRate) -> TimeSeriesRate:
# Check for same unit
Expand Down Expand Up @@ -711,7 +769,7 @@ def extend(self, other: TimeSeriesRate) -> Self: # type: ignore[override]
timesteps=self.timesteps + other.timesteps,
values=self.values + other.values,
unit=self.unit,
regularity=self.regularity + other.regularity, # type: ignore
regularity=self.regularity + other.regularity,
rate_type=self.rate_type,
)

Expand Down Expand Up @@ -773,7 +831,7 @@ def for_period(self, period: Period) -> Self:
return self.__class__(
timesteps=self.timesteps[start_index:end_index],
values=self.values[start_index:end_index],
regularity=self.regularity[start_index:end_index], # type: ignore
regularity=self.regularity[start_index:end_index],
unit=self.unit,
rate_type=self.rate_type,
)
Expand All @@ -788,7 +846,7 @@ def for_timestep(self, current_timestep: datetime) -> Self:
return self.__class__(
timesteps=self.timesteps[timestep_index : timestep_index + 1],
values=self.values[timestep_index : timestep_index + 1],
regularity=self.regularity[timestep_index : timestep_index + 1], # type: ignore
regularity=self.regularity[timestep_index : timestep_index + 1],
unit=self.unit,
rate_type=self.rate_type,
)
Expand All @@ -801,7 +859,7 @@ def to_calendar_day(self) -> Self:
calendar_day_rates = list(
Rates.to_calendar_day(
stream_day_rates=np.asarray(self.values),
regularity=self.regularity, # type: ignore[arg-type]
regularity=self.regularity,
),
)
return self.__class__(
Expand All @@ -820,7 +878,7 @@ def to_stream_day(self) -> Self:
stream_day_rates = list(
Rates.to_stream_day(
calendar_day_rates=np.asarray(self.values),
regularity=self.regularity, # type: ignore[arg-type]
regularity=self.regularity,
),
)
return self.__class__(
Expand Down Expand Up @@ -914,15 +972,15 @@ def __getitem__(self, indices: Union[slice, int, List[int], NDArray[np.float64]]
return self.__class__(
timesteps=self.timesteps[indices],
values=self.values[indices],
regularity=self.regularity[indices], # type: ignore
regularity=self.regularity[indices],
unit=self.unit,
rate_type=self.rate_type,
)
elif isinstance(indices, int):
return self.__class__(
timesteps=[self.timesteps[indices]],
values=[self.values[indices]],
regularity=[self.regularity[indices]], # type: ignore
regularity=[self.regularity[indices]],
unit=self.unit,
rate_type=self.rate_type,
)
Expand All @@ -931,7 +989,7 @@ def __getitem__(self, indices: Union[slice, int, List[int], NDArray[np.float64]]
return self.__class__(
timesteps=[self.timesteps[i] for i in indices],
values=[self.values[i] for i in indices],
regularity=[self.regularity[i] for i in indices], # type: ignore
regularity=[self.regularity[i] for i in indices],
unit=self.unit,
rate_type=self.rate_type,
)
Expand All @@ -957,5 +1015,33 @@ def reindex(self, new_time_vector: Iterable[datetime]) -> TimeSeriesRate:
"""
reindex_values = self.reindex_time_vector(new_time_vector)
return TimeSeriesRate(
timesteps=new_time_vector, values=reindex_values.tolist(), unit=self.unit, regularity=self.regularity
timesteps=new_time_vector,
values=reindex_values.tolist(),
unit=self.unit,
regularity=self.regularity,
rate_type=self.rate_type,
)

@classmethod
def from_timeseries_stream_day_rate(
cls, time_series_stream_day_rate: TimeSeriesStreamDayRate, regularity: TimeSeriesFloat
) -> Self:
if time_series_stream_day_rate is None:
return None

regularity = regularity.for_timesteps(time_series_stream_day_rate.timesteps)

return cls(
timesteps=time_series_stream_day_rate.timesteps,
values=time_series_stream_day_rate.values,
unit=time_series_stream_day_rate.unit,
rate_type=RateType.STREAM_DAY,
regularity=regularity.values,
)

def to_stream_day_timeseries(self) -> TimeSeriesStreamDayRate:
"""Convert to fixed stream day rate timeseries"""
stream_day_rate = self.to_stream_day()
return TimeSeriesStreamDayRate(
timesteps=stream_day_rate.timesteps, values=stream_day_rate.values, unit=stream_day_rate.unit
)
Loading

0 comments on commit 714888b

Please sign in to comment.