From 54a8fcb9a1af0f87f08b99ad8109ae1d2162ef34 Mon Sep 17 00:00:00 2001 From: Jonathan Kwan Date: Wed, 25 Oct 2023 00:53:23 +0000 Subject: [PATCH] Refactor Home class in engine.py. --- rules-engine/src/rules_engine/engine.py | 99 +++++++++++++++---- .../src/rules_engine/pydantic_models.py | 5 +- .../tests/test_rules_engine/test_engine.py | 35 ++++--- 3 files changed, 104 insertions(+), 35 deletions(-) diff --git a/rules-engine/src/rules_engine/engine.py b/rules-engine/src/rules_engine/engine.py index e991c255..a9a751ce 100644 --- a/rules-engine/src/rules_engine/engine.py +++ b/rules-engine/src/rules_engine/engine.py @@ -7,10 +7,13 @@ import numpy as np from pydantic import BaseModel, Field -from rules_engine.pydantic_models import SummaryInput, DhwInput, NaturalGasBillingInput, SummaryOutput, SensitivityGraph +from rules_engine.pydantic_models import SummaryInput, DhwInput, NaturalGasBillingInput, SummaryOutput, BalancePointGraph -def getOutputsNaturalGas(summaryInput: SummaryInput, dhwInput: Optional[DhwInput], naturalGasBillingInput: NaturalGasBillingInput)->(SummaryOutput, SensitivityGraph): - """""" +def getOutputsNaturalGas(summaryInput: SummaryInput, dhwInput: Optional[DhwInput], naturalGasBillingInput: NaturalGasBillingInput)->(SummaryOutput, BalancePointGraph): + + # home = Home(summaryInput, naturalGasBillingInput, dhwInput, naturalGasBillingInput) + # home.calculate() + # return(home.summaryOutput, home.balancePointGraph) pass @@ -123,6 +126,9 @@ def __init__( self, fuel_type: FuelType, heat_sys_efficiency: float, + temps: List[List[float]], + usages: List[float], + inclusion_codes: List[int], initial_balance_point: float = 60, thermostat_set_point: float = 68, has_boiler_for_dhw: bool = False, @@ -134,15 +140,46 @@ def __init__( self.thermostat_set_point = thermostat_set_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( + def _initialize_billing_periods( self, temps: List[List[float]], usages: List[float], inclusion_codes: List[int] ) -> None: """ - Eventually, this method should categorize the billing periods by - season and calculate avg_non_heating_usage based on that. For - now, we just pass in winter-only heating periods and manually - define non-heating + 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 in range(len(usages)): + if inclusion_codes[i] == 1: + self.bills_winter.append( + BillingPeriod(temps[i], usages[i], self, inclusion_codes[i]) + ) + elif inclusion_codes[i] == -1: + self.bills_summer.append( + BillingPeriod(temps[i], usages[i], self, inclusion_codes[i]) + ) + else: + self.bills_shoulder.append( + BillingPeriod(temps[i], usages[i], self, inclusion_codes[i]) + ) + + self._calculate_avg_summer_usage() + self._calculate_avg_non_heating_usage() + for bill in self.bills_winter: + bill.initialize_ua() + + + def _initialize_billing_periods_reworked( + self, billingperiods: NaturalGasBillingInput + ) -> None: + """ + TODO """ # assume for now that temps and usages have the same number of elements @@ -150,6 +187,10 @@ def initialize_billing_periods( self.bills_summer = [] self.bills_shoulder = [] + ngb_start_date = billingperiods.period_start_date + ngbs = billingperiods.records + + # winter months 1; summer months -1; shoulder months 0 for i in range(len(usages)): if inclusion_codes[i] == 1: @@ -165,12 +206,13 @@ def initialize_billing_periods( BillingPeriod(temps[i], usages[i], self, inclusion_codes[i]) ) - self.calculate_avg_summer_usage() - self.calculate_avg_non_heating_usage() + self._calculate_avg_summer_usage() + self._calculate_avg_non_heating_usage() for bill in self.bills_winter: bill.initialize_ua() - def calculate_avg_summer_usage(self) -> None: + + def _calculate_avg_summer_usage(self) -> None: """ Calculate average daily summer usage """ @@ -181,7 +223,7 @@ def calculate_avg_summer_usage(self) -> None: else: self.avg_summer_usage = 0 - def calculate_boiler_usage(self, fuel_multiplier: float) -> float: + def _calculate_boiler_usage(self, fuel_multiplier: float) -> float: """ Calculate boiler usage with oil or propane Args: @@ -204,7 +246,7 @@ def calculate_boiler_usage(self, fuel_multiplier: float) -> float: would be a property of the Home. """ - def calculate_avg_non_heating_usage(self) -> None: + def _calculate_avg_non_heating_usage(self) -> None: """ Calculate avg non heating usage for this Home Args: @@ -217,11 +259,11 @@ def calculate_avg_non_heating_usage(self) -> None: fuel_multiplier = 1 # default multiplier, for oil, placeholder number if self.fuel_type == FuelType.PROPANE: fuel_multiplier = 2 # a placeholder number - self.avg_non_heating_usage = self.calculate_boiler_usage(fuel_multiplier) + self.avg_non_heating_usage = self._calculate_boiler_usage(fuel_multiplier) else: self.avg_non_heating_usage = 0 - def calculate_balance_point_and_ua( + def _calculate_balance_point_and_ua( self, initial_balance_point_sensitivity: float = 2, stdev_pct_max: float = 0.10, @@ -236,7 +278,7 @@ def calculate_balance_point_and_ua( self.uas = [bp.ua for bp in self.bills_winter] self.avg_ua = sts.mean(self.uas) self.stdev_pct = sts.pstdev(self.uas) / self.avg_ua - self.refine_balance_point(initial_balance_point_sensitivity) + self._refine_balance_point(initial_balance_point_sensitivity) while self.stdev_pct > stdev_pct_max: biggest_outlier_idx = np.argmax( @@ -258,14 +300,17 @@ def calculate_balance_point_and_ua( else: self.uas, self.avg_ua, self.stdev_pct = uas_i, avg_ua_i, stdev_pct_i - self.refine_balance_point(next_balance_point_sensitivity) + self._refine_balance_point(next_balance_point_sensitivity) - def calculate_balance_point_and_ua_customizable( + def _calculate_balance_point_and_ua_customizable( self, bps_to_remove: List[BillingPeriod], balance_point_sensitivity: float = 2, ) -> None: """ + QUESTIONABLE if this is still needed when frontend only sends datapoints selected by user, + so _calculate_balance_point_and_ua() should suffice + Calculates the estimated balance point and UA coefficient for the home based on user input @@ -282,9 +327,9 @@ def calculate_balance_point_and_ua_customizable( self.stdev_pct = sts.pstdev(self.uas) / self.avg_ua self.bills_winter = customized_bills - self.refine_balance_point(balance_point_sensitivity) + self._refine_balance_point(balance_point_sensitivity) - def refine_balance_point(self, balance_point_sensitivity: float) -> None: + def _refine_balance_point(self, balance_point_sensitivity: float) -> None: """ Tries different balance points plus or minus a given number of degrees, choosing whichever one minimizes the standard @@ -326,6 +371,20 @@ def refine_balance_point(self, balance_point_sensitivity: float) -> None: if len(directions_to_check) == 2: directions_to_check.pop(-1) + def calculate(self, + initial_balance_point_sensitivity: float = 2, + stdev_pct_max: float = 0.10, + max_stdev_pct_diff: float = 0.01, + next_balance_point_sensitivity: float = 0.5): + """ + For this Home, calculates avg non heating usage and then the estimated balance point + and UA coefficient for the home, removing UA outliers based on a normalized standard + deviation threshold. + """ + self._calculate_avg_non_heating_usage() + self._calculate_balance_point_and_ua(initial_balance_point_sensitivity, + stdev_pct_max, max_stdev_pct_diff, next_balance_point_sensitivity) + class BillingPeriod: def __init__( diff --git a/rules-engine/src/rules_engine/pydantic_models.py b/rules-engine/src/rules_engine/pydantic_models.py index 0db73cd4..d9c8a178 100644 --- a/rules-engine/src/rules_engine/pydantic_models.py +++ b/rules-engine/src/rules_engine/pydantic_models.py @@ -36,13 +36,14 @@ class OilPropaneBillingInput(BaseModel): """From Oil-Propane tab""" period_end_date: date = Field(description="Oil-Propane!B") gallons: float = Field(description="Oil-Propane!C") + exclude: bool class NaturalGasBillingRecordInput(BaseModel): """From Natural Gas tab. A single row of the Billing input table.""" period_end_date: date = Field(description="Natural Gas!B") usage_therms: float = Field(description="Natural Gas!D") - + exclude: bool class NaturalGasBillingInput(BaseModel): """From Natural Gas tab. Container for holding all rows of the billing input table.""" @@ -57,7 +58,7 @@ class SummaryOutput(BaseModel): average_indoor_temperature: float = Field(description="Summary!B24") difference_between_ti_and_tbp: float = Field(description="Summary!B25") design_temperature: float = Field(description="Summary!B26") - whole_home_heat_loss_rate: float = sField(description="Summary!B27") # UA = heat loss rate + whole_home_heat_loss_rate: float = Field(description="Summary!B27") # UA = heat loss rate standard_deviation_of_heat_loss_rate: float = Field(description="Summary!B28") average_heat_load: float = Field(description="Summary!B29") maximum_heat_load: float = Field(description="Summary!B30") diff --git a/rules-engine/tests/test_rules_engine/test_engine.py b/rules-engine/tests/test_rules_engine/test_engine.py index cf30be84..0732d886 100644 --- a/rules-engine/tests/test_rules_engine/test_engine.py +++ b/rules-engine/tests/test_rules_engine/test_engine.py @@ -39,21 +39,24 @@ def test_average_indoor_temp(): def test_bp_ua_estimates(): - home = engine.Home( - engine.FuelType.GAS, heat_sys_efficiency=0.88, initial_balance_point=58 - ) - daily_temps_lists = [ [28, 29, 30, 29], [32, 35, 35, 38], [41, 43, 42, 42], [72, 71, 70, 69], ] + usages = [50, 45, 30, 0.96] inclusion_codes = [1, 1, 1, -1] - home.initialize_billing_periods(daily_temps_lists, usages, inclusion_codes) - home.calculate_avg_non_heating_usage() - home.calculate_balance_point_and_ua() + heat_sys_efficiency = 0.88 + home = engine.Home( + engine.FuelType.GAS,heat_sys_efficiency,daily_temps_lists, usages, inclusion_codes, initial_balance_point=58 + ) + + #home.initialize_billing_periods(daily_temps_lists, usages, inclusion_codes) + #home.calculate_avg_non_heating_usage() + #home.calculate_balance_point_and_ua() + home.calculate() ua_1, ua_2, ua_3 = [bill.ua for bill in home.bills_winter] @@ -66,9 +69,8 @@ def test_bp_ua_estimates(): def test_bp_ua_with_outlier(): - home = engine.Home( - engine.FuelType.GAS, heat_sys_efficiency=0.88, initial_balance_point=58 - ) + + daily_temps_lists = [ [41.7, 41.6, 32, 25.4], [28, 29, 30, 29], @@ -78,9 +80,16 @@ def test_bp_ua_with_outlier(): ] usages = [60, 50, 45, 30, 0.96] inclusion_codes = [1, 1, 1, 1, -1] - home.initialize_billing_periods(daily_temps_lists, usages, inclusion_codes) - home.calculate_avg_non_heating_usage() - home.calculate_balance_point_and_ua() + heat_sys_efficiency=0.88 + home = engine.Home( + engine.FuelType.GAS,heat_sys_efficiency,daily_temps_lists, usages, inclusion_codes, initial_balance_point=58 + ) + + # home.initialize_billing_periods(daily_temps_lists, usages, inclusion_codes) + # home.calculate_avg_non_heating_usage() + # home.calculate_balance_point_and_ua() + home.calculate() + ua_1, ua_2, ua_3 = [bill.ua for bill in home.bills_winter] assert home.balance_point == 60