From 21591a643e37655f82e482e04382cb1db354204e Mon Sep 17 00:00:00 2001 From: Erika Nesse Date: Wed, 20 Dec 2023 01:55:11 +0000 Subject: [PATCH] Made convert_to_intermediate_billing_periods Added test for convert_to_intermediate_billing_periods Filled out date_to_analysis_type Removed numpy Co-authored-by: harry Co-authored-by: Debajyoti Debnath Co-authored-by: dwindleduck Co-authored-by: Alan Pinkert Co-authored-by: wertheis --- rules-engine/src/rules_engine/engine.py | 83 +++++++----- .../src/rules_engine/pydantic_models.py | 12 +- .../tests/test_rules_engine/test_engine.py | 120 ++++++++++++++++++ 3 files changed, 175 insertions(+), 40 deletions(-) diff --git a/rules-engine/src/rules_engine/engine.py b/rules-engine/src/rules_engine/engine.py index e306f04b..41e3a928 100644 --- a/rules-engine/src/rules_engine/engine.py +++ b/rules-engine/src/rules_engine/engine.py @@ -5,11 +5,10 @@ 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, @@ -81,31 +80,10 @@ def get_outputs_normalized( temperature_input: TemperatureInput, billing_periods: List[NormalizedBillingPeriodRecordInput], ) -> Tuple[SummaryOutput, BalancePointGraph]: - # Build a list of lists of temperatures, where each list of temperatures contains all the temperatures - # in the corresponding billing period - intermediate_billing_periods = [] initial_balance_point = 60 - - 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 - - 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) + intermediate_billing_periods = convert_to_intermediate_billing_periods( + temperature_input=temperature_input, billing_periods=billing_periods + ) home = Home( summary_input=summary_input, @@ -145,8 +123,42 @@ def get_outputs_normalized( average_heat_load=average_heat_load, maximum_heat_load=maximum_heat_load, ) - # return summary_output # TODO: add BalancePointGraph - raise NotImplementedError + + # TODO: fill out balance point graph + balance_point_graph = BalancePointGraph(records=[]) + return (summary_output, balance_point_graph) + + +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 + + 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) + + return intermediate_billing_periods def date_to_analysis_type(d: date) -> AnalysisType: @@ -164,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: @@ -213,7 +223,7 @@ def get_average_indoor_temperature( """ if setback_temperature is None: setback_temperature = thermostat_set_point - + if setback_hours_per_day is None: setback_hours_per_day = 0 @@ -370,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 @@ -420,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, diff --git a/rules-engine/src/rules_engine/pydantic_models.py b/rules-engine/src/rules_engine/pydantic_models.py index 3b3199c0..ab40b66e 100644 --- a/rules-engine/src/rules_engine/pydantic_models.py +++ b/rules-engine/src/rules_engine/pydantic_models.py @@ -122,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): diff --git a/rules-engine/tests/test_rules_engine/test_engine.py b/rules-engine/tests/test_rules_engine/test_engine.py index d49d2bdc..57603850 100644 --- a/rules-engine/tests/test_rules_engine/test_engine.py +++ b/rules-engine/tests/test_rules_engine/test_engine.py @@ -1,3 +1,5 @@ +from datetime import date + import pytest from pytest import approx @@ -8,8 +10,10 @@ DhwInput, FuelType, NaturalGasBillingInput, + NormalizedBillingPeriodRecordInput, SummaryInput, SummaryOutput, + TemperatureInput, ) @@ -37,6 +41,20 @@ def test_period_hdd(temps, expected_result): assert engine.period_hdd(temps, 60) == expected_result +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 @@ -132,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