diff --git a/rules-engine/src/rules_engine/engine.py b/rules-engine/src/rules_engine/engine.py index 8e43e156..0f9fa9a4 100644 --- a/rules-engine/src/rules_engine/engine.py +++ b/rules-engine/src/rules_engine/engine.py @@ -3,6 +3,7 @@ import bisect import statistics as sts from datetime import date, timedelta +from pprint import pprint from typing import Any, List, Optional, Tuple from rules_engine.pydantic_models import ( @@ -26,7 +27,7 @@ def get_outputs_oil_propane( dhw_input: Optional[DhwInput], temperature_input: TemperatureInput, oil_propane_billing_input: OilPropaneBillingInput, -) -> Tuple[SummaryOutput, BalancePointGraph]: +) -> SummaryOutput: billing_periods: List[NormalizedBillingPeriodRecordInput] = [] last_date = oil_propane_billing_input.preceding_delivery_date @@ -56,7 +57,7 @@ def get_outputs_natural_gas( summary_input: SummaryInput, temperature_input: TemperatureInput, natural_gas_billing_input: NaturalGasBillingInput, -) -> Tuple[SummaryOutput, BalancePointGraph]: +) -> SummaryOutput: billing_periods: List[NormalizedBillingPeriodRecordInput] = [] for input_val in natural_gas_billing_input.records: @@ -79,7 +80,7 @@ def get_outputs_normalized( dhw_input: Optional[DhwInput], temperature_input: TemperatureInput, billing_periods: List[NormalizedBillingPeriodRecordInput], -) -> Tuple[SummaryOutput, BalancePointGraph]: +) -> SummaryOutput: initial_balance_point = 60 intermediate_billing_periods = convert_to_intermediate_billing_periods( temperature_input=temperature_input, billing_periods=billing_periods @@ -125,7 +126,8 @@ def get_outputs_normalized( ) balance_point_graph = home.balance_point_graph - return (summary_output, balance_point_graph) + + return summary_output def convert_to_intermediate_billing_periods( @@ -363,7 +365,7 @@ def _calculate_avg_non_heating_usage(self) -> None: def _calculate_balance_point_and_ua( self, - initial_balance_point_sensitivity: float = 2, + initial_balance_point_sensitivity: float = 0.5, stdev_pct_max: float = 0.10, max_stdev_pct_diff: float = 0.01, next_balance_point_sensitivity: float = 0.5, @@ -440,17 +442,21 @@ def _refine_balance_point(self, balance_point_sensitivity: float) -> None: avg_ua_i = sts.mean(uas_i) stdev_pct_i = sts.pstdev(uas_i) / avg_ua_i + change_in_heat_loss_rate = avg_ua_i - self.avg_ua + percent_change_in_heat_loss_rate = 100 * change_in_heat_loss_rate / avg_ua_i + + balance_point_graph_row = BalancePointGraphRow( + balance_point=bp_i, + heat_loss_rate=avg_ua_i, + change_in_heat_loss_rate=change_in_heat_loss_rate, + percent_change_in_heat_loss_rate=percent_change_in_heat_loss_rate, + standard_deviation=stdev_pct_i, + ) + self.balance_point_graph.records.append(balance_point_graph_row) + 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? - - change_in_heat_loss_rate = avg_ua_i - self.avg_ua - percent_change_in_heat_loss_rate = ( - 100 * change_in_heat_loss_rate / avg_ua_i - ) self.balance_point, self.avg_ua, self.stdev_pct = ( bp_i, avg_ua_i, @@ -475,7 +481,7 @@ def _refine_balance_point(self, balance_point_sensitivity: float) -> None: def calculate( self, - initial_balance_point_sensitivity: float = 2, + initial_balance_point_sensitivity: float = 0.5, stdev_pct_max: float = 0.10, max_stdev_pct_diff: float = 0.01, next_balance_point_sensitivity: float = 0.5, diff --git a/rules-engine/tests/test_rules_engine/cases/examples/cali/summary.json b/rules-engine/tests/test_rules_engine/cases/examples/cali/summary.json index 119d4de8..d64361d9 100644 --- a/rules-engine/tests/test_rules_engine/cases/examples/cali/summary.json +++ b/rules-engine/tests/test_rules_engine/cases/examples/cali/summary.json @@ -9,13 +9,13 @@ "thermostat_set_point": 69.0, "setback_temperature": 62.0, "setback_hours_per_day": 8.0, - "estimated_balance_point": 56.0, + "estimated_balance_point": 55.5, "balance_point_sensitivity": 2.0, "average_indoor_temperature": 66.7, - "difference_between_ti_and_tbp": 10.7, + "difference_between_ti_and_tbp": 11.2, "design_temperature": 8.4, - "whole_home_heat_loss_rate": 733, + "whole_home_heat_loss_rate": 748, "standard_deviation_of_heat_loss_rate": 0.0651, - "average_heat_load": 37318, - "maximum_heat_load": 45133 + "average_heat_load": 37743, + "maximum_heat_load": 46099 } \ No newline at end of file diff --git a/rules-engine/tests/test_rules_engine/cases/examples/feldman/summary.json b/rules-engine/tests/test_rules_engine/cases/examples/feldman/summary.json index 0ad73aea..a2cc9d1b 100644 --- a/rules-engine/tests/test_rules_engine/cases/examples/feldman/summary.json +++ b/rules-engine/tests/test_rules_engine/cases/examples/feldman/summary.json @@ -9,13 +9,13 @@ "thermostat_set_point": 67.0, "setback_temperature": 63.0, "setback_hours_per_day": 7.0, - "estimated_balance_point": 61.0, + "estimated_balance_point": 61.5, "balance_point_sensitivity": 1.0, "average_indoor_temperature": 65.8, - "difference_between_ti_and_tbp": 4.8, + "difference_between_ti_and_tbp": 4.3, "design_temperature": 8.4, - "whole_home_heat_loss_rate": 775, + "whole_home_heat_loss_rate": 761, "standard_deviation_of_heat_loss_rate": 0.0776, - "average_heat_load": 43987, - "maximum_heat_load": 47732 + "average_heat_load": 43564, + "maximum_heat_load": 46860 } \ No newline at end of file diff --git a/rules-engine/tests/test_rules_engine/test_engine.py b/rules-engine/tests/test_rules_engine/test_engine.py index 06b2c70d..6e516276 100644 --- a/rules-engine/tests/test_rules_engine/test_engine.py +++ b/rules-engine/tests/test_rules_engine/test_engine.py @@ -217,12 +217,12 @@ def test_bp_ua_estimates(sample_summary_inputs, sample_billing_periods): ua_1, ua_2, ua_3 = [bill.ua for bill in home.bills_winter] - assert home.balance_point == 60 - assert ua_1 == approx(1478.50, abs=0.01) - assert ua_2 == approx(1650.00, abs=0.01) - assert ua_3 == approx(1527.78, abs=0.01) - assert home.avg_ua == approx(1552.09, abs=0.01) - assert home.stdev_pct == approx(0.0465, abs=0.01) + assert home.balance_point == 60.5 + assert ua_1 == approx(1455.03, abs=0.01) + assert ua_2 == approx(1617.65, abs=0.01) + assert ua_3 == approx(1486.49, abs=0.01) + assert home.avg_ua == approx(1519.72, abs=1) + assert home.stdev_pct == approx(0.0463, abs=0.01) def test_bp_ua_with_outlier(sample_summary_inputs, sample_billing_periods_with_outlier): @@ -272,7 +272,7 @@ def test_convert_to_intermediate_billing_periods( def test_get_outputs_normalized( sample_summary_inputs, sample_temp_inputs, sample_normalized_billing_periods ): - summary_output, balance_point_graph = engine.get_outputs_normalized( + summary_output = engine.get_outputs_normalized( sample_summary_inputs, None, sample_temp_inputs, diff --git a/rules-engine/tests/test_rules_engine/test_examples.py b/rules-engine/tests/test_rules_engine/test_examples.py index 0b7ec9a9..bd2dd5a5 100644 --- a/rules-engine/tests/test_rules_engine/test_examples.py +++ b/rules-engine/tests/test_rules_engine/test_examples.py @@ -25,6 +25,7 @@ # Filter out example 2 for now, since it's for oil fuel type INPUT_DATA = filter(lambda d: d != "example-2", next(os.walk(ROOT_DIR))[1]) +# INPUT_DATA = filter(lambda d: d == "cali", next(os.walk(ROOT_DIR))[1]) class Summary(SummaryInput, SummaryOutput): @@ -112,67 +113,64 @@ def test_average_indoor_temp(data: Example) -> None: assert data.summary.average_indoor_temperature == approx(avg_indoor_temp, rel=0.01) -def test_get_outputs_natural_gas(data: Example) -> None: - summary_output, balance_point_graph = engine.get_outputs_natural_gas( +def test_balance_point_natural_gas(data: Example) -> None: + summary_output = engine.get_outputs_natural_gas( data.summary, data.temperature_data, data.natural_gas_usage ) assert data.summary.estimated_balance_point == approx( - summary_output.estimated_balance_point, rel=0.05 - ) - - -# def test_ua(data: Example) -> None: -# """ -# Test how the rules engine calculates UA from energy bills. - -# Pulls in data and pre-calculated results from example spreadsheets -# and compares them to the UA calculated from that data by the -# engine. -# """ -# # TODO: Handle oil and propane fuel types too -# usage_data = None -# if data.summary.fuel_type is engine.FuelType.GAS: -# usage_data = data.natural_gas_usage -# else: -# raise NotImplementedError("Fuel type {}".format(data.summary.fuel_type)) - -# # build Home instance - input summary information and bills -# home = engine.Home( -# data.summary, -# data.summary.fuel_type, -# data.summary.heating_system_efficiency, -# thermostat_set_point=data.summary.thermostat_set_point, -# ) -# temps = [] -# usages = [] -# inclusion_codes = [] -# for usage in usage_data.records: -# temps_for_period = [] -# for i in range(usage.days_in_bill): -# date_in_period = usage.start_date + timedelta(days=i) -# matching_records = [ -# d for d in data.temperature_data if d.date == date_in_period -# ] -# assert len(matching_records) == 1 -# temps_for_period.append(matching_records[0].temperature) -# assert date_in_period == usage.end_date - -# inclusion_code = usage.inclusion_code -# if usage.inclusion_override is not None: -# inclusion_code = usage.inclusion_override - -# temps.append(temps_for_period) -# usages.append(usage.usage) -# inclusion_codes.append(inclusion_code) - -# home.initialize_billing_periods(temps, usages, inclusion_codes) - -# # now check outputs -# home.calculate_balance_point_and_ua( -# initial_balance_point_sensitivity=data.summary.balance_point_sensitivity -# ) - -# assert home.balance_point == approx(data.summary.estimated_balance_point, abs=0.01) -# assert home.avg_ua == approx(data.summary.whole_home_heat_loss_rate, abs=1) -# assert home.stdev_pct == approx(data.summary.standard_deviation_of_heat_loss_rate, abs=0.01) -# # TODO: check average heat load and max heat load + summary_output.estimated_balance_point, abs=0.1 + ) + + +def test_whole_home_heat_loss_rate_natural_gas(data: Example) -> None: + summary_output = engine.get_outputs_natural_gas( + data.summary, data.temperature_data, data.natural_gas_usage + ) + assert summary_output.whole_home_heat_loss_rate == approx( + data.summary.whole_home_heat_loss_rate, abs=1 + ) + + +def test_standard_deviation_of_heat_loss_rate_natural_gas(data: Example) -> None: + summary_output = engine.get_outputs_natural_gas( + data.summary, data.temperature_data, data.natural_gas_usage + ) + assert summary_output.standard_deviation_of_heat_loss_rate == approx( + data.summary.standard_deviation_of_heat_loss_rate, abs=0.01 + ) + + +def test_difference_between_ti_and_tbp_natural_gas(data: Example) -> None: + summary_output = engine.get_outputs_natural_gas( + data.summary, data.temperature_data, data.natural_gas_usage + ) + assert summary_output.difference_between_ti_and_tbp == approx( + data.summary.difference_between_ti_and_tbp, abs=0.1 + ) + + +def test_average_heat_load_natural_gas(data: Example) -> None: + summary_output = engine.get_outputs_natural_gas( + data.summary, data.temperature_data, data.natural_gas_usage + ) + assert summary_output.average_heat_load == approx( + data.summary.average_heat_load, abs=1 + ) + + +def test_design_temperaure_natural_gas(data: Example) -> None: + summary_output = engine.get_outputs_natural_gas( + data.summary, data.temperature_data, data.natural_gas_usage + ) + assert summary_output.design_temperature == approx( + data.summary.design_temperature, abs=0.1 + ) + + +def test_maximum_heat_load_natural_gas(data: Example) -> None: + summary_output = engine.get_outputs_natural_gas( + data.summary, data.temperature_data, data.natural_gas_usage + ) + assert summary_output.maximum_heat_load == approx( + data.summary.maximum_heat_load, abs=1 + )