From 9c6a2ec85a17b4a4ddc90b66e01cdf0ab8f2d9d4 Mon Sep 17 00:00:00 2001 From: eriksynn <159293101+eriksynn@users.noreply.github.com> Date: Tue, 27 Aug 2024 10:22:54 -0400 Subject: [PATCH] Fixes issue236 by implementing inclusion_override logic --- rules-engine/src/rules_engine/engine.py | 43 +++++++++++-------- .../src/rules_engine/pydantic_models.py | 17 ++------ .../tests/test_rules_engine/test_engine.py | 35 +++++++++------ .../tests/test_rules_engine/test_utils.py | 6 +-- 4 files changed, 50 insertions(+), 51 deletions(-) diff --git a/rules-engine/src/rules_engine/engine.py b/rules-engine/src/rules_engine/engine.py index c9554b55..46e56271 100644 --- a/rules-engine/src/rules_engine/engine.py +++ b/rules-engine/src/rules_engine/engine.py @@ -46,8 +46,7 @@ def get_outputs_oil_propane( period_start_date=start_date, period_end_date=input_val.period_end_date, usage=input_val.gallons, - analysis_type_override=input_val.inclusion_override, - inclusion_override=True, + inclusion_override=input_val.inclusion_override, ) ) last_date = input_val.period_end_date @@ -73,8 +72,7 @@ def get_outputs_natural_gas( period_start_date=input_val.period_start_date, period_end_date=input_val.period_end_date, usage=input_val.usage_therms, - analysis_type_override=input_val.inclusion_override, - inclusion_override=True, + inclusion_override=input_val.inclusion_override, ) ) @@ -141,19 +139,13 @@ def get_outputs_normalized( billing_records = [] for billing_period in intermediate_billing_periods: - # TODO: fix inclusion override to come from inputs - default_inclusion_by_calculation = True - if billing_period.analysis_type == AnalysisType.NOT_ALLOWED_IN_CALCULATIONS: - default_inclusion_by_calculation = False billing_record = NormalizedBillingPeriodRecord( period_start_date=billing_period.input.period_start_date, period_end_date=billing_period.input.period_end_date, usage=billing_period.input.usage, - analysis_type_override=billing_period.input.analysis_type_override, - inclusion_override=False, + inclusion_override=billing_period.input.inclusion_override, analysis_type=billing_period.analysis_type, - default_inclusion_by_calculation=default_inclusion_by_calculation, eliminated_as_outlier=billing_period.eliminated_as_outlier, whole_home_heat_loss_rate=billing_period.ua, ) @@ -199,15 +191,15 @@ def convert_to_intermediate_billing_periods( analysis_type = _date_to_analysis_type_oil_propane( billing_period.period_start_date, billing_period.period_end_date ) - - if billing_period.analysis_type_override is not None: - analysis_type = billing_period.analysis_type_override + else: + raise ValueError("Unsupported fuel type.") intermediate_billing_period = BillingPeriod( input=billing_period, avg_temps=temperature_input.temperatures[start_idx:end_idx], usage=billing_period.usage, analysis_type=analysis_type, + inclusion_override=billing_period.inclusion_override, ) intermediate_billing_periods.append(intermediate_billing_period) @@ -402,15 +394,26 @@ def _initialize_billing_periods(self, billing_periods: list[BillingPeriod]) -> N self.bills_summer = [] self.bills_shoulder = [] - # winter months 1; summer months -1; shoulder months 0 + # winter months 1 (ALLOWED_HEATING_USAGE); summer months -1 (ALLOWED_NON_HEATING_USAGE); shoulder months 0 (NOT_ALLOWED...) for billing_period in billing_periods: billing_period.set_initial_balance_point(self.balance_point) - if billing_period.analysis_type == AnalysisType.ALLOWED_HEATING_USAGE: + _analysis_type = billing_period.analysis_type + + if billing_period.inclusion_override: # The user has requested we override an inclusion decision + if _analysis_type == AnalysisType.ALLOWED_HEATING_USAGE: + # In this case we assume the intent is to exclude this bill from winter calculations + _analysis_type = AnalysisType.NOT_ALLOWED_IN_CALCULATIONS + elif _analysis_type == AnalysisType.NOT_ALLOWED_IN_CALCULATIONS: + # In this case we assume the intent is to include this bill in heating calculations + _analysis_type = AnalysisType.ALLOWED_HEATING_USAGE + else: + # In this case we assume the intent is to exclude this bill from summer calculations + _analysis_type = AnalysisType.NOT_ALLOWED_IN_CALCULATIONS + + if _analysis_type == AnalysisType.ALLOWED_HEATING_USAGE: self.bills_winter.append(billing_period) - elif ( - billing_period.analysis_type == AnalysisType.NOT_ALLOWED_IN_CALCULATIONS - ): + elif _analysis_type == AnalysisType.NOT_ALLOWED_IN_CALCULATIONS: self.bills_shoulder.append(billing_period) else: self.bills_summer.append(billing_period) @@ -633,11 +636,13 @@ def __init__( avg_temps: list[float], usage: float, analysis_type: AnalysisType, + inclusion_override: bool, ) -> None: self.input = input self.avg_temps = avg_temps self.usage = usage self.analysis_type = analysis_type + self.inclusion_override = inclusion_override self.eliminated_as_outlier = False self.ua = None diff --git a/rules-engine/src/rules_engine/pydantic_models.py b/rules-engine/src/rules_engine/pydantic_models.py index 877dfab0..b9088799 100644 --- a/rules-engine/src/rules_engine/pydantic_models.py +++ b/rules-engine/src/rules_engine/pydantic_models.py @@ -16,7 +16,7 @@ class AnalysisType(Enum): Enum for analysis type. 'Inclusion' in calculations is now determined by - the default_inclusion_by_calculation and inclusion_override variables + the date_to_analysis_type calculation and the inclusion_override variable Use HDDs to determine if shoulder months are heating or non-heating or not allowed, or included or excluded @@ -83,10 +83,7 @@ class OilPropaneBillingRecordInput(BaseModel): period_end_date: date = Field(description="Oil-Propane!B") gallons: float = Field(description="Oil-Propane!C") - inclusion_override: Optional[ - Literal[AnalysisType.ALLOWED_HEATING_USAGE] - | Literal[AnalysisType.NOT_ALLOWED_IN_CALCULATIONS] - ] = Field(description="Oil-Propane!F") + inclusion_override: Optional[bool] = Field(description="Oil-Propane!F") class OilPropaneBillingInput(BaseModel): @@ -102,7 +99,7 @@ class NaturalGasBillingRecordInput(BaseModel): period_start_date: date = Field(description="Natural Gas!A") period_end_date: date = Field(description="Natural Gas!B") usage_therms: float = Field(description="Natural Gas!D") - inclusion_override: Optional[AnalysisType] = Field(description="Natural Gas!E") + inclusion_override: Optional[bool] = Field(description="Natural Gas!E") class NaturalGasBillingInput(BaseModel): @@ -148,10 +145,6 @@ class NormalizedBillingPeriodRecordBase(BaseModel): Base class for a normalized billing period record. Holds data as fields. - - analysis_type_override - for testing only, preserving compatibility - with the original heat calc spreadsheet, which allows users to override - the analysis type whereas the rules engine does not """ model_config = ConfigDict(validate_assignment=True) @@ -159,8 +152,7 @@ class NormalizedBillingPeriodRecordBase(BaseModel): period_start_date: date = Field(frozen=True) period_end_date: date = Field(frozen=True) usage: float = Field(frozen=True) - analysis_type_override: Optional[AnalysisType] = Field(frozen=True) - inclusion_override: bool + inclusion_override: bool = Field(frozen=True) class NormalizedBillingPeriodRecord(NormalizedBillingPeriodRecordBase): @@ -173,7 +165,6 @@ class NormalizedBillingPeriodRecord(NormalizedBillingPeriodRecordBase): model_config = ConfigDict(validate_assignment=True) analysis_type: AnalysisType = Field(frozen=True) - default_inclusion_by_calculation: bool = Field(frozen=True) eliminated_as_outlier: bool = Field(frozen=True) whole_home_heat_loss_rate: Optional[float] = Field(frozen=True) diff --git a/rules-engine/tests/test_rules_engine/test_engine.py b/rules-engine/tests/test_rules_engine/test_engine.py index 4aeafda4..89b4f9fe 100644 --- a/rules-engine/tests/test_rules_engine/test_engine.py +++ b/rules-engine/tests/test_rules_engine/test_engine.py @@ -21,8 +21,7 @@ period_start_date=date(2024, 1, 1), period_end_date=date(2024, 2, 1), usage=1.0, - analysis_type_override=None, - inclusion_override=True, + inclusion_override=False, ) @@ -34,24 +33,28 @@ def sample_billing_periods() -> list[engine.BillingPeriod]: [28, 29, 30, 29], 50, AnalysisType.ALLOWED_HEATING_USAGE, + False, ), engine.BillingPeriod( dummy_billing_period_record, [32, 35, 35, 38], 45, AnalysisType.ALLOWED_HEATING_USAGE, + False, ), engine.BillingPeriod( dummy_billing_period_record, [41, 43, 42, 42], 30, AnalysisType.ALLOWED_HEATING_USAGE, + False, ), engine.BillingPeriod( dummy_billing_period_record, [72, 71, 70, 69], 0.96, AnalysisType.NOT_ALLOWED_IN_CALCULATIONS, + False, ), ] return billing_periods @@ -65,30 +68,35 @@ def sample_billing_periods_with_outlier() -> list[engine.BillingPeriod]: [41.7, 41.6, 32, 25.4], 60, AnalysisType.ALLOWED_HEATING_USAGE, + False, ), engine.BillingPeriod( dummy_billing_period_record, [28, 29, 30, 29], 50, AnalysisType.ALLOWED_HEATING_USAGE, + False, ), engine.BillingPeriod( dummy_billing_period_record, [32, 35, 35, 38], 45, AnalysisType.ALLOWED_HEATING_USAGE, + False, ), engine.BillingPeriod( dummy_billing_period_record, [41, 43, 42, 42], 30, AnalysisType.ALLOWED_HEATING_USAGE, + False, ), engine.BillingPeriod( dummy_billing_period_record, [72, 71, 70, 69], 0.96, AnalysisType.NOT_ALLOWED_IN_CALCULATIONS, + False, ), ] @@ -176,43 +184,37 @@ def sample_normalized_billing_periods() -> list[NormalizedBillingPeriodRecordBas "period_start_date": "2022-12-01", "period_end_date": "2022-12-04", "usage": 60, - "analysis_type_override": None, - "inclusion_override": True, + "inclusion_override": False, }, { "period_start_date": "2023-01-01", "period_end_date": "2023-01-04", "usage": 50, - "analysis_type_override": None, - "inclusion_override": True, + "inclusion_override": False, }, { "period_start_date": "2023-02-01", "period_end_date": "2023-02-04", "usage": 45, - "analysis_type_override": None, - "inclusion_override": True, + "inclusion_override": False, }, { "period_start_date": "2023-03-01", "period_end_date": "2023-03-04", "usage": 30, - "analysis_type_override": None, - "inclusion_override": True, + "inclusion_override": False, }, { "period_start_date": "2023-04-01", "period_end_date": "2023-04-04", "usage": 0.96, - "analysis_type_override": None, - "inclusion_override": True, + "inclusion_override": False, }, { "period_start_date": "2023-05-01", "period_end_date": "2023-05-04", "usage": 0.96, - "analysis_type_override": None, - "inclusion_override": True, + "inclusion_override": False, }, ] @@ -332,30 +334,35 @@ def test_convert_to_intermediate_billing_periods( [41.7, 41.6, 32, 25.4], 60, AnalysisType.ALLOWED_HEATING_USAGE, + False, ), engine.BillingPeriod( dummy_billing_period_record, [28, 29, 30, 29], 50, AnalysisType.ALLOWED_HEATING_USAGE, + False, ), engine.BillingPeriod( dummy_billing_period_record, [32, 35, 35, 38], 45, AnalysisType.ALLOWED_HEATING_USAGE, + False, ), engine.BillingPeriod( dummy_billing_period_record, [41, 43, 42, 42], 30, AnalysisType.ALLOWED_HEATING_USAGE, + False, ), engine.BillingPeriod( dummy_billing_period_record, [72, 71, 70, 69], 0.96, AnalysisType.NOT_ALLOWED_IN_CALCULATIONS, + False, ), ] diff --git a/rules-engine/tests/test_rules_engine/test_utils.py b/rules-engine/tests/test_rules_engine/test_utils.py index d4a10db9..be42d2c0 100644 --- a/rules-engine/tests/test_rules_engine/test_utils.py +++ b/rules-engine/tests/test_rules_engine/test_utils.py @@ -94,11 +94,7 @@ def load_fuel_billing_example_input( reader = csv.DictReader(f) row: Any for i, row in enumerate(reader): - inclusion_override = row["inclusion_override"] - if inclusion_override == "": - inclusion_override = None - else: - inclusion_override = int(inclusion_override) + inclusion_override = bool(row["inclusion_override"]) # Choose the correct billing period heat loss (aka "ua") # column based on the estimated balance point provided in