diff --git a/rules-engine/pyproject.toml b/rules-engine/pyproject.toml index 7e33cc9c..b6a3c442 100644 --- a/rules-engine/pyproject.toml +++ b/rules-engine/pyproject.toml @@ -7,7 +7,6 @@ name="rules-engine" version="0.0.1" requires-python=">=3.8" dependencies = [ - "numpy", "pydantic" ] diff --git a/rules-engine/requirements-dev.txt b/rules-engine/requirements-dev.txt index 8f7d6896..039ada18 100644 --- a/rules-engine/requirements-dev.txt +++ b/rules-engine/requirements-dev.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.8 +# This file is autogenerated by pip-compile with Python 3.10 # by the following command: # # pip-compile --extra=dev --output-file=requirements-dev.txt pyproject.toml @@ -10,10 +10,6 @@ black==23.3.0 # via rules-engine (pyproject.toml) click==8.1.3 # via black -colorama==0.4.6 - # via - # click - # pytest exceptiongroup==1.1.1 # via pytest iniconfig==2.0.0 @@ -26,8 +22,6 @@ mypy-extensions==1.0.0 # via # black # mypy -numpy==1.24.4 - # via rules-engine (pyproject.toml) packaging==23.1 # via # black @@ -55,8 +49,6 @@ tomli==2.0.1 # pytest typing-extensions==4.6.3 # via - # annotated-types - # black # mypy # pydantic # pydantic-core diff --git a/rules-engine/requirements.txt b/rules-engine/requirements.txt index e0560696..dff10bbb 100644 --- a/rules-engine/requirements.txt +++ b/rules-engine/requirements.txt @@ -6,8 +6,6 @@ # annotated-types==0.5.0 # via pydantic -numpy==1.24.4 - # via rules-engine (pyproject.toml) pydantic==2.3.0 # via rules-engine (pyproject.toml) pydantic-core==2.6.3 diff --git a/rules-engine/src/rules_engine/engine.py b/rules-engine/src/rules_engine/engine.py index dea4a06e..41e3a928 100644 --- a/rules-engine/src/rules_engine/engine.py +++ b/rules-engine/src/rules_engine/engine.py @@ -1,17 +1,19 @@ from __future__ import annotations +import bisect import statistics as sts -from datetime import date +from datetime import date, timedelta from typing import Any, List, Optional, Tuple -import numpy as np - from rules_engine.pydantic_models import ( AnalysisType, BalancePointGraph, + BalancePointGraphRow, + Constants, DhwInput, FuelType, NaturalGasBillingInput, + NormalizedBillingPeriodRecordInput, OilPropaneBillingInput, SummaryInput, SummaryOutput, @@ -25,8 +27,25 @@ def get_outputs_oil_propane( temperature_input: TemperatureInput, oil_propane_billing_input: OilPropaneBillingInput, ) -> Tuple[SummaryOutput, BalancePointGraph]: - # TODO: normalize oil & propane to billing periods - billing_periods = NotImplementedError() + billing_periods: List[NormalizedBillingPeriodRecordInput] = [] + + last_date = oil_propane_billing_input.preceding_delivery_date + for input_val in oil_propane_billing_input.records: + start_date = last_date + timedelta(days=1) + inclusion = ( + AnalysisType.INCLUDE + if input_val.inclusion_override + else AnalysisType.DO_NOT_INCLUDE + ) + billing_periods.append( + NormalizedBillingPeriodRecordInput( + period_start_date=start_date, + period_end_date=input_val.period_end_date, + usage=input_val.gallons, + inclusion_override=inclusion, + ) + ) + last_date = input_val.period_end_date return get_outputs_normalized( summary_input, dhw_input, temperature_input, billing_periods @@ -35,15 +54,23 @@ def get_outputs_oil_propane( def get_outputs_natural_gas( summary_input: SummaryInput, - dhw_input: Optional[DhwInput], temperature_input: TemperatureInput, natural_gas_billing_input: NaturalGasBillingInput, ) -> Tuple[SummaryOutput, BalancePointGraph]: - # TODO: normalize natural gas to billing periods - billing_periods = NotImplementedError() + billing_periods: List[NormalizedBillingPeriodRecordInput] = [] + + for input_val in natural_gas_billing_input.records: + billing_periods.append( + NormalizedBillingPeriodRecordInput( + period_start_date=input_val.period_start_date, + period_end_date=input_val.period_end_date, + usage=input_val.usage_therms, + inclusion_override=input_val.inclusion_override, + ) + ) return get_outputs_normalized( - summary_input, dhw_input, temperature_input, billing_periods + summary_input, None, temperature_input, billing_periods ) @@ -51,24 +78,87 @@ def get_outputs_normalized( summary_input: SummaryInput, dhw_input: Optional[DhwInput], temperature_input: TemperatureInput, - billing_periods: Any, + billing_periods: List[NormalizedBillingPeriodRecordInput], ) -> Tuple[SummaryOutput, BalancePointGraph]: - # smush together temp and billing periods + initial_balance_point = 60 + intermediate_billing_periods = convert_to_intermediate_billing_periods( + temperature_input=temperature_input, billing_periods=billing_periods + ) + + home = Home( + summary_input=summary_input, + billing_periods=intermediate_billing_periods, + initial_balance_point=initial_balance_point, + has_boiler_for_dhw=dhw_input is not None, + same_fuel_dhw_heating=dhw_input is not None, + ) + home.calculate() + + average_indoor_temperature = get_average_indoor_temperature( + thermostat_set_point=summary_input.thermostat_set_point, + setback_temperature=summary_input.setback_temperature, + setback_hours_per_day=summary_input.setback_hours_per_day, + ) + average_heat_load = get_average_heat_load( + design_set_point=Constants.DESIGN_SET_POINT, + avg_indoor_temp=average_indoor_temperature, + balance_point=home.balance_point, + design_temp=summary_input.design_temperature, + ua=home.avg_ua, + ) + maximum_heat_load = get_maximum_heat_load( + design_set_point=Constants.DESIGN_SET_POINT, + design_temp=summary_input.design_temperature, + ua=home.avg_ua, + ) + + summary_output = SummaryOutput( + estimated_balance_point=home.balance_point, + other_fuel_usage=home.avg_non_heating_usage, + average_indoor_temperature=average_indoor_temperature, + difference_between_ti_and_tbp=average_indoor_temperature - home.balance_point, + design_temperature=summary_input.design_temperature, + whole_home_heat_loss_rate=home.avg_ua, + standard_deviation_of_heat_loss_rate=home.stdev_pct, + average_heat_load=average_heat_load, + maximum_heat_load=maximum_heat_load, + ) + + # TODO: fill out balance point graph + balance_point_graph = BalancePointGraph(records=[]) + return (summary_output, balance_point_graph) - # home = Home(summary_input, temperature_input, dhw_input, billing_periods) - # home.calculate() - # summary_input: SummaryInput, summary_input - # temps: List[List[float]], temperature_input - # usages: List[float], billing_periods - # inclusion_codes: List[int], billing_periods* - # initial_balance_point: float = 60, n/a - # has_boiler_for_dhw: bool = False, dhw_input - # same_fuel_dhw_heating: bool = False, dhw_input +def convert_to_intermediate_billing_periods( + temperature_input: TemperatureInput, + billing_periods: List[NormalizedBillingPeriodRecordInput], +) -> List[BillingPeriod]: + # Build a list of lists of temperatures, where each list of temperatures contains all the temperatures + # in the corresponding billing period + intermediate_billing_periods = [] + + for billing_period in billing_periods: + # the HEAT Excel sheet is inclusive of the temperatures that fall on both the start and end dates + start_idx = bisect.bisect_left( + temperature_input.dates, billing_period.period_start_date + ) + end_idx = ( + bisect.bisect_left(temperature_input.dates, billing_period.period_end_date) + + 1 + ) + + analysis_type = date_to_analysis_type(billing_period.period_end_date) + if billing_period.inclusion_override: + analysis_type = billing_period.inclusion_override - # return (home.summaryOutput, home.balancePointGraph) + intermediate_billing_period = BillingPeriod( + avg_temps=temperature_input.temperatures[start_idx:end_idx], + usage=billing_period.usage, + analysis_type=analysis_type, + ) + intermediate_billing_periods.append(intermediate_billing_period) - raise NotImplementedError + return intermediate_billing_periods def date_to_analysis_type(d: date) -> AnalysisType: @@ -86,9 +176,7 @@ def date_to_analysis_type(d: date) -> AnalysisType: 11: AnalysisType.DO_NOT_INCLUDE, 12: AnalysisType.INCLUDE, } - - # TODO: finish implementation and unit test - raise NotImplementedError + return months[d.month] def hdd(avg_temp: float, balance_point: float) -> float: @@ -118,27 +206,36 @@ def period_hdd(avg_temps: List[float], balance_point: float) -> float: return sum([hdd(temp, balance_point) for temp in avg_temps]) -def average_indoor_temp( - tstat_set: float, tstat_setback: float, setback_daily_hrs: float +def get_average_indoor_temperature( + thermostat_set_point: float, + setback_temperature: Optional[float], + setback_hours_per_day: Optional[float], ) -> float: """ Calculates the average indoor temperature. Args: - tstat_set: the temp in F at which the home is normally set - tstat_setback: temp in F at which the home is set during off + thermostat_set_point: the temp in F at which the home is normally set + setback_temperature: temp in F at which the home is set during off hours - setback_daily_hrs: average # of hours per day the home is at + setback_hours_per_day: average # of hours per day the home is at setback temp """ + if setback_temperature is None: + setback_temperature = thermostat_set_point + + if setback_hours_per_day is None: + setback_hours_per_day = 0 + # again, not sure if we should check for valid values here or whether we can # assume those kinds of checks will be handled at the point of user entry return ( - (24 - setback_daily_hrs) * tstat_set + setback_daily_hrs * tstat_setback + (24 - setback_hours_per_day) * thermostat_set_point + + setback_hours_per_day * setback_temperature ) / 24 -def average_heat_load( +def get_average_heat_load( design_set_point: float, avg_indoor_temp: float, balance_point: float, @@ -162,7 +259,9 @@ def average_heat_load( return (design_set_point - (avg_indoor_temp - balance_point) - design_temp) * ua -def max_heat_load(design_set_point: float, design_temp: float, ua: float) -> float: +def get_maximum_heat_load( + design_set_point: float, design_temp: float, ua: float +) -> float: """ Calculate the max heat load. @@ -190,9 +289,7 @@ class Home: def __init__( self, summary_input: SummaryInput, - temps: List[List[float]], - usages: List[float], - inclusion_codes: List[int], + billing_periods: List[BillingPeriod], initial_balance_point: float = 60, has_boiler_for_dhw: bool = False, same_fuel_dhw_heating: bool = False, @@ -203,72 +300,20 @@ def __init__( self.balance_point = initial_balance_point self.has_boiler_for_dhw = has_boiler_for_dhw self.same_fuel_dhw_heating = same_fuel_dhw_heating - self._initialize_billing_periods(temps, usages, inclusion_codes) - - def _initialize_billing_periods( - self, temps: List[List[float]], usages: List[float], inclusion_codes: List[int] - ) -> None: - """ - TODO - """ - # assume for now that temps and usages have the same number of elements - - self.bills_winter = [] - self.bills_summer = [] - self.bills_shoulder = [] - - # winter months 1; summer months -1; shoulder months 0 - for i, usage in enumerate(usages): - billing_period = BillingPeriod( - avg_temps=temps[i], - usage=usage, - balance_point=self.balance_point, - inclusion_code=inclusion_codes[i], - ) - if inclusion_codes[i] == 1: - self.bills_winter.append(billing_period) - elif inclusion_codes[i] == -1: - self.bills_summer.append(billing_period) - else: - self.bills_shoulder.append(billing_period) - - self._calculate_avg_summer_usage() - self._calculate_avg_non_heating_usage() - for billing_period in self.bills_winter: - self.initialize_ua(billing_period) - - def _initialize_billing_periods_reworked( - self, billingperiods: NaturalGasBillingInput - ) -> None: - """ - TODO - """ - # assume for now that temps and usages have the same number of elements + self._initialize_billing_periods(billing_periods) + def _initialize_billing_periods(self, billing_periods: List[BillingPeriod]) -> None: self.bills_winter = [] self.bills_summer = [] self.bills_shoulder = [] - # ngb_start_date = billingperiods.period_start_date - # ngbs = billingperiods.records - - # TODO: fix these - usages: List[float] = [] - inclusion_codes: List[int] = [] - temps: List[List[float]] = [] - # winter months 1; summer months -1; shoulder months 0 - for i, usage in enumerate(usages): - billing_period = BillingPeriod( - avg_temps=temps[i], - usage=usage, - balance_point=self.balance_point, - inclusion_code=inclusion_codes[i], - ) + for billing_period in billing_periods: + billing_period.set_initial_balance_point(self.balance_point) - if inclusion_codes[i] == 1: + if billing_period.analysis_type == AnalysisType.INCLUDE: self.bills_winter.append(billing_period) - elif inclusion_codes[i] == -1: + elif billing_period.analysis_type == AnalysisType.DO_NOT_INCLUDE: self.bills_summer.append(billing_period) else: self.bills_shoulder.append(billing_period) @@ -302,21 +347,9 @@ def _calculate_boiler_usage(self, fuel_multiplier: float) -> float: return 0 * fuel_multiplier - """ - your pseudocode looks correct provided there's outer logic that - check whether the home uses the same fuel for DHW as for heating. If not, anhu=0. - - From an OO design viewpoint, I don't see Summer_billingPeriods as a direct property - of the home. Rather, it's a property of the Location (an object defining the weather - station, and the Winter, Summer and Shoulder billing periods. Of course, Location - would be a property of the Home. - """ - def _calculate_avg_non_heating_usage(self) -> None: """ - Calculate avg non heating usage for this Home - Args: - #use_same_fuel_DHW_heating + Calculate avg non heating usage for this home """ if self.fuel_type == FuelType.GAS: @@ -347,9 +380,9 @@ def _calculate_balance_point_and_ua( self._refine_balance_point(initial_balance_point_sensitivity) while self.stdev_pct > stdev_pct_max: - biggest_outlier_idx = np.argmax( - [abs(bill.ua - self.avg_ua) for bill in self.bills_winter] - ) + outliers = [abs(bill.ua - self.avg_ua) for bill in self.bills_winter] + biggest_outlier = max(outliers) + biggest_outlier_idx = outliers.index(biggest_outlier) outlier = self.bills_winter.pop( biggest_outlier_idx ) # removes the biggest outlier @@ -397,6 +430,9 @@ def _refine_balance_point(self, balance_point_sensitivity: float) -> None: if stdev_pct_i >= self.stdev_pct: directions_to_check.pop(0) else: + # TODO: For balance point graph, store the old balance + # point in a list to keep track of all intermediate balance + # point temperatures? self.balance_point, self.avg_ua, self.stdev_pct = ( bp_i, avg_ua_i, @@ -456,20 +492,22 @@ def calculate_partial_ua(self, billing_period: BillingPeriod) -> float: class BillingPeriod: avg_heating_usage: float + balance_point: float partial_ua: float ua: float + total_hdd: float def __init__( self, avg_temps: List[float], usage: float, - balance_point: float, - inclusion_code: int, + analysis_type: AnalysisType, ) -> None: self.avg_temps = avg_temps self.usage = usage - self.balance_point = balance_point - self.inclusion_code = inclusion_code - + self.analysis_type = analysis_type self.days = len(self.avg_temps) + + def set_initial_balance_point(self, balance_point: float) -> None: + self.balance_point = balance_point self.total_hdd = period_hdd(self.avg_temps, self.balance_point) diff --git a/rules-engine/src/rules_engine/pydantic_models.py b/rules-engine/src/rules_engine/pydantic_models.py index 5d4e1c1e..ab40b66e 100644 --- a/rules-engine/src/rules_engine/pydantic_models.py +++ b/rules-engine/src/rules_engine/pydantic_models.py @@ -2,6 +2,7 @@ Data models for input and output data in the rules engine. """ +from dataclasses import dataclass from datetime import date from enum import Enum from typing import Annotated, Any, List, Optional @@ -18,9 +19,9 @@ class AnalysisType(Enum): class FuelType(Enum): """Enum for fuel types. Values are BTU per usage""" - GAS = 100000 - OIL = 139600 - PROPANE = 91333 + GAS = 100000 # BTU / therm + OIL = 139600 # BTU / gal + PROPANE = 91333 # BTU / gal def validate_fuel_type(value: Any) -> FuelType: @@ -47,6 +48,7 @@ class SummaryInput(BaseModel): thermostat_set_point: float = Field(description="Summary!B17") setback_temperature: Optional[float] = Field(description="Summary!B18") setback_hours_per_day: Optional[float] = Field(description="Summary!B19") + design_temperature: float = Field(description="TDesign") class DhwInput(BaseModel): @@ -57,7 +59,7 @@ class DhwInput(BaseModel): stand_by_losses: float = Field(description="DHW!B6") -class OilPropaneBillingInput(BaseModel): +class OilPropaneBillingRecordInput(BaseModel): """From Oil-Propane tab""" period_end_date: date = Field(description="Oil-Propane!B") @@ -65,6 +67,13 @@ class OilPropaneBillingInput(BaseModel): inclusion_override: Optional[bool] = Field(description="Oil-Propane!F") +class OilPropaneBillingInput(BaseModel): + """From Oil-Propane tab. Container for holding all rows of the billing input table.""" + + records: List[OilPropaneBillingRecordInput] + preceding_delivery_date: date = Field(description="Oil-Propane!B6") + + class NaturalGasBillingRecordInput(BaseModel): """From Natural Gas tab. A single row of the Billing input table.""" @@ -80,6 +89,13 @@ class NaturalGasBillingInput(BaseModel): records: List[NaturalGasBillingRecordInput] +class NormalizedBillingPeriodRecordInput(BaseModel): + period_start_date: date + period_end_date: date + usage: float + inclusion_override: Optional[AnalysisType] + + class TemperatureInput(BaseModel): dates: List[date] temperatures: List[float] @@ -106,11 +122,13 @@ class SummaryOutput(BaseModel): class BalancePointGraphRow(BaseModel): """From Summary page""" - balance_pt: float = Field(description="Summary!G33:35") - ua: float = Field(description="Summary!H33:35") - change_in_ua: float = Field(description="Summary!I33:35") - pct_change: float = Field(description="Summary!J33:35") - std_dev: float = Field(description="Summary!K33:35") + balance_point: float = Field(description="Summary!G33:35") # degree F + heat_loss_rate: float = Field(description="Summary!H33:35") # BTU / (hr-deg. F) + change_in_heat_loss_rate: float = Field( + description="Summary!I33:35" + ) # BTU / (hr-deg. F) + percent_change_in_heat_loss_rate: float = Field(description="Summary!J33:35") + standard_deviation: float = Field(description="Summary!K33:35") class BalancePointGraph(BaseModel): @@ -119,5 +137,7 @@ class BalancePointGraph(BaseModel): records: List[BalancePointGraphRow] +@dataclass class Constants: - balance_point_sensitivity: float = 0.5 + BALANCE_POINT_SENSITIVITY: float = 0.5 + DESIGN_SET_POINT: float = 70 diff --git a/rules-engine/tests/test_rules_engine/test_engine.py b/rules-engine/tests/test_rules_engine/test_engine.py index 0f756a7e..57603850 100644 --- a/rules-engine/tests/test_rules_engine/test_engine.py +++ b/rules-engine/tests/test_rules_engine/test_engine.py @@ -1,14 +1,19 @@ +from datetime import date + import pytest from pytest import approx from rules_engine import engine from rules_engine.pydantic_models import ( + AnalysisType, BalancePointGraph, DhwInput, FuelType, NaturalGasBillingInput, + NormalizedBillingPeriodRecordInput, SummaryInput, SummaryOutput, + TemperatureInput, ) @@ -36,32 +41,44 @@ def test_period_hdd(temps, expected_result): assert engine.period_hdd(temps, 60) == expected_result -def test_average_indoor_temp(): +def test_date_to_analysis_type(): + test_date = date.fromisoformat("2019-01-04") + assert engine.date_to_analysis_type(test_date) == AnalysisType.INCLUDE + + dates = ["2019-01-04", "2019-07-04", "2019-12-04"] + types = [engine.date_to_analysis_type(date.fromisoformat(d)) for d in dates] + expected_types = [ + AnalysisType.INCLUDE, + AnalysisType.INCLUDE_IN_OTHER_ANALYSIS, + AnalysisType.INCLUDE, + ] + assert types == expected_types + + +def test_get_average_indoor_temperature(): set_temp = 68 setback = 62 setback_hrs = 8 # when there is no setback, just put 0 for the setback parameters - assert engine.average_indoor_temp(set_temp, 0, 0) == set_temp - assert engine.average_indoor_temp(set_temp, setback, setback_hrs) == 66 + assert engine.get_average_indoor_temperature(set_temp, 0, 0) == set_temp + assert engine.get_average_indoor_temperature(set_temp, setback, setback_hrs) == 66 def test_bp_ua_estimates(): - daily_temps_lists = [ - [28, 29, 30, 29], - [32, 35, 35, 38], - [41, 43, 42, 42], - [72, 71, 70, 69], + billing_periods = [ + engine.BillingPeriod([28, 29, 30, 29], 50, AnalysisType.INCLUDE), + engine.BillingPeriod([32, 35, 35, 38], 45, AnalysisType.INCLUDE), + engine.BillingPeriod([41, 43, 42, 42], 30, AnalysisType.INCLUDE), + engine.BillingPeriod([72, 71, 70, 69], 0.96, AnalysisType.DO_NOT_INCLUDE), ] - - usages = [50, 45, 30, 0.96] - inclusion_codes = [1, 1, 1, -1] heat_sys_efficiency = 0.88 living_area = 1000 thermostat_set_point = 68 setback_temperature = 60 setback_hours_per_day = 8 fuel_type = FuelType.GAS + design_temperature = 60 summary_input = SummaryInput( living_area=living_area, fuel_type=fuel_type, @@ -69,13 +86,12 @@ def test_bp_ua_estimates(): thermostat_set_point=thermostat_set_point, setback_temperature=setback_temperature, setback_hours_per_day=setback_hours_per_day, + design_temperature=design_temperature, ) home = engine.Home( summary_input, - daily_temps_lists, - usages, - inclusion_codes, + billing_periods, initial_balance_point=58, ) @@ -92,15 +108,14 @@ def test_bp_ua_estimates(): def test_bp_ua_with_outlier(): - daily_temps_lists = [ - [41.7, 41.6, 32, 25.4], - [28, 29, 30, 29], - [32, 35, 35, 38], - [41, 43, 42, 42], - [72, 71, 70, 69], + billing_periods = [ + engine.BillingPeriod([41.7, 41.6, 32, 25.4], 60, AnalysisType.INCLUDE), + engine.BillingPeriod([28, 29, 30, 29], 50, AnalysisType.INCLUDE), + engine.BillingPeriod([32, 35, 35, 38], 45, AnalysisType.INCLUDE), + engine.BillingPeriod([41, 43, 42, 42], 30, AnalysisType.INCLUDE), + engine.BillingPeriod([72, 71, 70, 69], 0.96, AnalysisType.DO_NOT_INCLUDE), ] - usages = [60, 50, 45, 30, 0.96] - inclusion_codes = [1, 1, 1, 1, -1] + heat_sys_efficiency = 0.88 living_area = 1000 @@ -108,6 +123,7 @@ def test_bp_ua_with_outlier(): setback_temperature = 60 setback_hours_per_day = 8 fuel_type = FuelType.GAS + design_temperature = 60 summary_input = SummaryInput( living_area=living_area, fuel_type=fuel_type, @@ -115,13 +131,12 @@ def test_bp_ua_with_outlier(): thermostat_set_point=thermostat_set_point, setback_temperature=setback_temperature, setback_hours_per_day=setback_hours_per_day, + design_temperature=design_temperature, ) home = engine.Home( summary_input, - daily_temps_lists, - usages, - inclusion_codes, + billing_periods, initial_balance_point=58, ) @@ -135,3 +150,105 @@ def test_bp_ua_with_outlier(): assert ua_3 == approx(1479.6, abs=1) assert home.avg_ua == approx(1515.1, abs=1) assert home.stdev_pct == approx(0.0474, abs=0.01) + + +def test_convert_to_intermediate_billing_periods(): + temperature_input = TemperatureInput( + dates=[ + date(2022, 12, 1), + date(2022, 12, 2), + date(2022, 12, 3), + date(2022, 12, 4), + date(2023, 1, 1), + date(2023, 1, 2), + date(2023, 1, 3), + date(2023, 1, 4), + date(2023, 2, 1), + date(2023, 2, 2), + date(2023, 2, 3), + date(2023, 2, 4), + date(2023, 3, 1), + date(2023, 3, 2), + date(2023, 3, 3), + date(2023, 3, 4), + date(2023, 4, 1), + date(2023, 4, 2), + date(2023, 4, 3), + date(2023, 4, 4), + ], + temperatures=[ + 41.7, + 41.6, + 32, + 25.4, + 28, + 29, + 30, + 29, + 32, + 35, + 35, + 38, + 41, + 43, + 42, + 42, + 72, + 71, + 70, + 69, + ], + ) + + billing_periods = [ + NormalizedBillingPeriodRecordInput( + period_start_date=date(2022, 12, 1), + period_end_date=date(2022, 12, 4), + usage=60, + inclusion_override=None, + ), + NormalizedBillingPeriodRecordInput( + period_start_date=date(2023, 1, 1), + period_end_date=date(2023, 1, 4), + usage=50, + inclusion_override=None, + ), + NormalizedBillingPeriodRecordInput( + period_start_date=date(2023, 2, 1), + period_end_date=date(2023, 2, 4), + usage=45, + inclusion_override=None, + ), + NormalizedBillingPeriodRecordInput( + period_start_date=date(2023, 3, 1), + period_end_date=date(2023, 3, 4), + usage=30, + inclusion_override=None, + ), + NormalizedBillingPeriodRecordInput( + period_start_date=date(2023, 4, 1), + period_end_date=date(2023, 4, 4), + usage=0.96, + inclusion_override=None, + ), + ] + + results = engine.convert_to_intermediate_billing_periods( + temperature_input, billing_periods + ) + + expected_results = [ + engine.BillingPeriod([41.7, 41.6, 32, 25.4], 60, AnalysisType.INCLUDE), + engine.BillingPeriod([28, 29, 30, 29], 50, AnalysisType.INCLUDE), + engine.BillingPeriod([32, 35, 35, 38], 45, AnalysisType.INCLUDE), + engine.BillingPeriod([41, 43, 42, 42], 30, AnalysisType.INCLUDE), + engine.BillingPeriod([72, 71, 70, 69], 0.96, AnalysisType.DO_NOT_INCLUDE), + ] + + for i in range(len(expected_results)): + result = results[i] + expected_result = expected_results[i] + + assert result.avg_temps == expected_result.avg_temps + assert result.usage == expected_result.usage + assert result.analysis_type == expected_result.analysis_type diff --git a/rules-engine/tests/test_rules_engine/test_examples.py b/rules-engine/tests/test_rules_engine/test_examples.py index b4c0520e..84f320ee 100644 --- a/rules-engine/tests/test_rules_engine/test_examples.py +++ b/rules-engine/tests/test_rules_engine/test_examples.py @@ -103,7 +103,7 @@ def data(request): def test_average_indoor_temp(data: Example) -> None: - avg_indoor_temp = engine.average_indoor_temp( + avg_indoor_temp = engine.get_average_indoor_temperature( data.summary.thermostat_set_point, data.summary.setback_temperature or 0, data.summary.setback_hours_per_day or 0,