Skip to content

Commit

Permalink
fixxing and testing specific daily profiles applied twice
Browse files Browse the repository at this point in the history
  • Loading branch information
lionel42 committed Nov 17, 2023
1 parent ea71b37 commit 81e2324
Show file tree
Hide file tree
Showing 4 changed files with 254 additions and 10 deletions.
118 changes: 110 additions & 8 deletions emiproc/profiles/temporal_profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import emiproc

from emiproc.profiles.utils import (
get_objects_of_same_type_from_list,
get_profiles_indexes,
merge_indexes,
read_profile_csv,
Expand Down Expand Up @@ -59,6 +60,35 @@ def _generate_next_value_(name: str, start, count, last_values):
# One of the 2 last days
WEEKEND = auto()

@classmethod
def from_day_number(cls, day_number: int) -> SpecificDay:
"""Return the day corresponding to the day number.
Day start at 0 for Monday and ends at 6 for Sunday.
"""
if not isinstance(day_number, int):
raise TypeError(f"{day_number=} must be an int.")

dict_day = {
0: cls.MONDAY,
1: cls.TUESDAY,
2: cls.WEDNESDAY,
3: cls.THURSDAY,
4: cls.FRIDAY,
5: cls.SATURDAY,
6: cls.SUNDAY,
}

if day_number not in dict_day:
raise ValueError(f"{day_number=} is not a valid day number.")

return dict_day[day_number]


def days_of_specific_day(specific_day: SpecificDay) -> list[SpecificDay]:
int_days = get_days_as_ints(specific_day)
return [SpecificDay.from_day_number(i) for i in int_days]


def get_days_as_ints(specific_day: SpecificDay) -> list[int]:
"""Return the days corresponding for a specific day."""
Expand Down Expand Up @@ -551,6 +581,70 @@ def make_composite_profiles(
return CompositeTemporalProfiles(extracted_profiles), out_indexes


def ensure_specific_days_consistency(
profiles: list[AnyTimeProfile],
) -> list[AnyTimeProfile]:
"""Make sure that there is not confilct between specific days profiles and normal daily profiles.
In case there is any conflict, this return a profile for each day of the week.
"""

if not any(isinstance(p, SpecificDayProfile) for p in profiles):
return profiles

# Get the specific days profiles

daily_profiles = [p for p in profiles if isinstance(p, DailyProfile)]
non_daily_profiles = [p for p in profiles if not isinstance(p, DailyProfile)]

weekdays_profiles = {
SpecificDay.MONDAY: None,
SpecificDay.TUESDAY: None,
SpecificDay.WEDNESDAY: None,
SpecificDay.THURSDAY: None,
SpecificDay.FRIDAY: None,
SpecificDay.SATURDAY: None,
SpecificDay.SUNDAY: None,
}
# First assign what we have sepcific
for p in daily_profiles:
if isinstance(p, SpecificDayProfile):
days = days_of_specific_day(p.specific_day)
if len(days) == 1:
# This day was concerned specifically
weekdays_profiles[p.specific_day] = p
else:
# This was a weekend or weekday profile
for day in days:
# Only override if not already defined
if weekdays_profiles[day] is None:
weekdays_profiles[day] = SpecificDayProfile(
specific_day=day, ratios=p.ratios
)

general_daily_profiles = [p for p in daily_profiles if type(p) == DailyProfile]

# Add a constant profile for missing day
for day, profile in weekdays_profiles.items():
if profile is not None:
continue
if len(general_daily_profiles) == 0:
p = SpecificDayProfile(specific_day=day)

elif len(general_daily_profiles) == 1:
p = SpecificDayProfile(
specific_day=day, ratios=general_daily_profiles[0].ratios
)
else:
raise ValueError(
f"Cannot assign {general_daily_profiles=} to {day=}, more than one"
f" general {type(DailyProfile)} was given."
)
weekdays_profiles[day] = p

return non_daily_profiles + list(weekdays_profiles.values())


def create_scaling_factors_time_serie(
start_time: datetime,
end_time: datetime,
Expand Down Expand Up @@ -584,6 +678,9 @@ def create_scaling_factors_time_serie(
# Create the scaling factors
scaling_factors = np.ones(len(time_serie))

# Correct profiles list with specific day profiles
profiles = ensure_specific_days_consistency(profiles)

# Apply the profiles
for profile in profiles:
scaling_factors *= profile_to_scaling_factors(
Expand Down Expand Up @@ -622,7 +719,19 @@ def profile_to_scaling_factors(
scaling_factors = np.ones(len(time_serie))

# Get the profile
if isinstance(profile, DailyProfile):
if isinstance(profile, SpecificDayProfile):
# Find the days corresponding to this factor
days_allowed = get_days_as_ints(profile.specific_day)
if len(days_allowed) != 1:
raise ValueError(
f"Cannot apply {profile=} to a time serie, it must have only one day."
"convert the time profiles with `ensure_specific_days_consistency`."
)
mask_matching_day = np.isin(time_serie.day_of_week, days_allowed)
for hour, factor in enumerate(factors):
# Other days will not have a scaling factor
scaling_factors[(time_serie.hour == hour) & mask_matching_day] *= factor
elif isinstance(profile, DailyProfile):
# Get the mask for each hour of day and apply the scaling factor
for hour, factor in enumerate(factors):
scaling_factors[time_serie.hour == hour] *= factor
Expand Down Expand Up @@ -657,13 +766,6 @@ def profile_to_scaling_factors(
# Months start with 1
month += 1
scaling_factors[time_serie.month == month] *= factor
elif isinstance(profile, SpecificDayProfile):
# Find the days corresponding to this factor
days_allowed = get_days_as_ints(profile.specific_day)
mask_matching_day = np.isin(time_serie.day_of_week, days_allowed)
for hour, factor in enumerate(factors):
# Other days will not have a scaling factor
scaling_factors[(time_serie.hour == hour) & mask_matching_day] *= factor
else:
raise NotImplementedError(
f"Cannot apply {profile=}, {type(profile)=} is not implemented."
Expand Down
42 changes: 40 additions & 2 deletions emiproc/profiles/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import logging
from os import PathLike
from pathlib import Path
from typing import Any, Type
from typing import TYPE_CHECKING, Any, Type

import pandas as pd
import xarray as xr
Expand All @@ -12,6 +12,10 @@
import emiproc
from emiproc.profiles import naming

if TYPE_CHECKING:
from emiproc.profiles.vertical_profiles import VerticalProfiles
from emiproc.profiles.temporal_profiles import CompositeTemporalProfiles

logger = logging.getLogger(__name__)


Expand All @@ -38,12 +42,46 @@ def remove_objects_of_type_from_list(object: Any, objects_list: list[Any]) -> li


def get_objects_of_same_type_from_list(
object: Any, objects_list: list[Any]
object: Any, objects_list: list[Any], exact_type: bool = False
) -> list[Any]:
"""Return the object of the same type from the list."""
func = isinstance if exact_type else lambda x, y: type(x) == y
return [o for o in objects_list if isinstance(object, type(o))]


def check_valid_indexes(
indexes: xr.DataArray, profiles: VerticalProfiles | CompositeTemporalProfiles = None
) -> None:
"""Check that the given indexes are valid.
:raises ValueError: if the indexes are not valid
"""

# check all the dims names are valid
dims_not_allowed = set(indexes.dims) - set(naming.type_of_dim.keys())
if len(dims_not_allowed) > 0:
raise ValueError(
f"Indexes are not allowed to contain {dims_not_allowed=}, "
f"allowed dims are {naming.type_of_dim.keys()}"
)
# Make sure no coords has duplicated values
for coord in indexes.coords:
if len(indexes.coords[coord]) != len(np.unique(indexes.coords[coord])):
raise ValueError(
f"Indexes are not valid, they contain duplicated values for {coord=}:"
f" {indexes.coords[coord]}"
)

if profiles is not None:
# Check that the max value of the index is given in the profiles
if indexes.max().values >= len(profiles):
raise ValueError(
"Indexes are not valid, they contain values that are not in the"
f" profiles.Got {indexes.max().values=} but profiles has"
f" {len(profiles)=}"
)


def get_desired_profile_index(
profiles_indexes: xr.DataArray,
cell: int | None = None,
Expand Down
75 changes: 75 additions & 0 deletions tests/profiles/test_create_timeserie.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import pytest
from emiproc import FILES_DIR
from datetime import datetime
import numpy as np
from emiproc.tests_utils.temporal_profiles import oem_const_profile
from emiproc.profiles.temporal_profiles import (
DailyProfile,
SpecificDay,
SpecificDayProfile,
days_of_specific_day,
ensure_specific_days_consistency,
from_yaml,
create_scaling_factors_time_serie,
profile_to_scaling_factors,
)


def test_ensure_specific_days_consistency_non_specific():
out = ensure_specific_days_consistency(oem_const_profile)
assert len(out) == len(oem_const_profile)

out = ensure_specific_days_consistency(
[SpecificDayProfile(specific_day=SpecificDay("monday"))]
)
assert len(out) == 7 # Contains the full week

half_double_ratios = np.concatenate((np.full(12, 0.5), np.full(12, 1.5))) / 24
thirds_triple_ratios = np.concatenate((np.full(12, 1 / 3), np.full(12, 5 / 3))) / 24
quaters_ratios = np.concatenate((np.full(12, 1 / 4), np.full(12, 7 / 4))) / 24
out = ensure_specific_days_consistency(
[
SpecificDayProfile(
ratios=half_double_ratios, specific_day=SpecificDay.MONDAY
),
SpecificDayProfile(
ratios=thirds_triple_ratios, specific_day=SpecificDay.WEEKEND
),
SpecificDayProfile(ratios=quaters_ratios, specific_day=SpecificDay.SUNDAY),
DailyProfile(),
],
)
for p in out:
assert isinstance(p, SpecificDayProfile)
assert len(days_of_specific_day(p.specific_day)) == 1
if p.specific_day == SpecificDay.MONDAY:
equalt_to = half_double_ratios
elif p.specific_day == SpecificDay.SATURDAY:
equalt_to = thirds_triple_ratios
elif p.specific_day == SpecificDay.SUNDAY:
equalt_to = quaters_ratios
else:
equalt_to = np.full(24, 1.0) / 24
np.testing.assert_array_equal(
p.ratios.reshape(-1), equalt_to, err_msg=f"Failed for {p.specific_day=}"
)


@pytest.mark.parametrize(
"profiles",
[
oem_const_profile,
from_yaml(FILES_DIR / "profiles" / "yamls" / "heat.yaml"),
from_yaml(FILES_DIR / "profiles" / "yamls" / "human_home.yaml"),
from_yaml(FILES_DIR / "profiles" / "yamls" / "human.yaml"),
from_yaml(FILES_DIR / "profiles" / "yamls" / "ship.yaml"),
from_yaml(FILES_DIR / "profiles" / "yamls" / "heavy.yaml"),
from_yaml(FILES_DIR / "profiles" / "yamls" / "light.yaml"),
],
)
def test_create_scaling_factors_time_serie(profiles):
start = datetime(2018, 1, 1)
end = datetime(2019, 1, 1)
ts = create_scaling_factors_time_serie(start, end, profiles)

assert np.isclose(ts.mean(), 1.0, atol=0.1)
29 changes: 29 additions & 0 deletions tests/profiles/test_profiles_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,34 @@
)

from emiproc.profiles.utils import get_desired_profile_index
from emiproc.profiles.temporal_profiles import SpecificDay, days_of_specific_day


@pytest.mark.parametrize(
"specific_days, expected",
(
(SpecificDay.MONDAY, [SpecificDay.MONDAY]),
(SpecificDay.TUESDAY, [SpecificDay.TUESDAY]),
(SpecificDay.WEDNESDAY, [SpecificDay.WEDNESDAY]),
(SpecificDay.THURSDAY, [SpecificDay.THURSDAY]),
(SpecificDay.FRIDAY, [SpecificDay.FRIDAY]),
(SpecificDay.SATURDAY, [SpecificDay.SATURDAY]),
(SpecificDay.SUNDAY, [SpecificDay.SUNDAY]),
(SpecificDay.WEEKEND, [SpecificDay.SATURDAY, SpecificDay.SUNDAY]),
(
SpecificDay.WEEKDAY,
[
SpecificDay.MONDAY,
SpecificDay.TUESDAY,
SpecificDay.WEDNESDAY,
SpecificDay.THURSDAY,
SpecificDay.FRIDAY,
],
),
),
)
def test_days_of_specific_day(specific_days, expected):
assert days_of_specific_day(specific_days) == expected


def test_get_desired_profile_index_working_cases():
Expand All @@ -30,6 +58,7 @@ def test_get_desired_profile_index_errors():
ValueError, get_desired_profile_index, da_profiles_indexes_catsub, cat="a"
)


def test_get_desired_profile_index_wrong_catsub():
# Category does not exist
pytest.raises(
Expand Down

0 comments on commit 81e2324

Please sign in to comment.