diff --git a/rules-engine/src/rules_engine/engine.py b/rules-engine/src/rules_engine/engine.py index 6966af7b..c9554b55 100644 --- a/rules-engine/src/rules_engine/engine.py +++ b/rules-engine/src/rules_engine/engine.py @@ -1,6 +1,7 @@ """ TODO: Add module description """ + from __future__ import annotations import bisect @@ -40,17 +41,12 @@ def get_outputs_oil_propane( 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.ALLOWED_HEATING_USAGE - if input_val.inclusion_override - else AnalysisType.NOT_ALLOWED_IN_CALCULATIONS - ) billing_periods.append( NormalizedBillingPeriodRecordBase( period_start_date=start_date, period_end_date=input_val.period_end_date, usage=input_val.gallons, - analysis_type_override=inclusion, + analysis_type_override=input_val.inclusion_override, inclusion_override=True, ) ) @@ -98,7 +94,9 @@ def get_outputs_normalized( """ initial_balance_point = 60 intermediate_billing_periods = convert_to_intermediate_billing_periods( - temperature_input=temperature_input, billing_periods=billing_periods + temperature_input=temperature_input, + billing_periods=billing_periods, + fuel_type=summary_input.fuel_type, ) home = Home( @@ -172,6 +170,7 @@ def get_outputs_normalized( def convert_to_intermediate_billing_periods( temperature_input: TemperatureInput, billing_periods: list[NormalizedBillingPeriodRecordBase], + fuel_type: FuelType, ) -> list[BillingPeriod]: """ Converts temperature data and billing period inputs into internal classes used for heat loss calculations. @@ -192,8 +191,16 @@ def convert_to_intermediate_billing_periods( + 1 ) - analysis_type = date_to_analysis_type(billing_period.period_end_date) - if billing_period.analysis_type_override: + if fuel_type == FuelType.GAS: + analysis_type = _date_to_analysis_type_natural_gas( + billing_period.period_end_date + ) + elif fuel_type == FuelType.OIL or fuel_type == FuelType.PROPANE: + 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 intermediate_billing_period = BillingPeriod( @@ -207,11 +214,26 @@ def convert_to_intermediate_billing_periods( return intermediate_billing_periods -def date_to_analysis_type(d: date) -> AnalysisType: +def _date_to_analysis_type_oil_propane( + start_date: date, end_date: date +) -> AnalysisType: """ - Converts the date from a billing period into an enum representing the period's usage in the rules engine. + Converts the dates from a billing period into an enum representing the period's usage in the rules engine. + """ + if ( + (end_date.month > 4 and end_date.month < 11) + or (start_date.month > 3 and start_date.month < 10) + or (start_date.month < 7 and end_date.month > 7) + or (start_date.month < 7 and start_date.year < end_date.year) + ): + return AnalysisType.NOT_ALLOWED_IN_CALCULATIONS + else: + return AnalysisType.ALLOWED_HEATING_USAGE + - TODO: Extract this method to another class or make it private. +def _date_to_analysis_type_natural_gas(d: date) -> AnalysisType: + """ + Converts the dates from a billing period into an enum representing the period's usage in the rules engine. """ months = { 1: AnalysisType.ALLOWED_HEATING_USAGE, @@ -624,3 +646,9 @@ def __init__( 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) + + def __str__(self) -> str: + return f"{self.input}, {self.ua}, {self.eliminated_as_outlier}, {self.days}, {self.avg_temps}, {self.usage}, {self.analysis_type}" + + def __repr__(self) -> str: + return self.__str__() diff --git a/rules-engine/src/rules_engine/parser.py b/rules-engine/src/rules_engine/parser.py index ca966e65..cb75a647 100644 --- a/rules-engine/src/rules_engine/parser.py +++ b/rules-engine/src/rules_engine/parser.py @@ -2,6 +2,7 @@ Return lists of gas billing data parsed from Eversource and National Grid CSVs. """ + import csv import io import re diff --git a/rules-engine/src/rules_engine/pydantic_models.py b/rules-engine/src/rules_engine/pydantic_models.py index dfb5a081..877dfab0 100644 --- a/rules-engine/src/rules_engine/pydantic_models.py +++ b/rules-engine/src/rules_engine/pydantic_models.py @@ -6,7 +6,7 @@ from datetime import date from enum import Enum from functools import cached_property -from typing import Annotated, Any, Optional, Sequence +from typing import Annotated, Any, Literal, Optional, Sequence from pydantic import BaseModel, BeforeValidator, ConfigDict, Field, computed_field @@ -83,13 +83,16 @@ class OilPropaneBillingRecordInput(BaseModel): period_end_date: date = Field(description="Oil-Propane!B") gallons: float = Field(description="Oil-Propane!C") - inclusion_override: Optional[bool] = Field(description="Oil-Propane!F") + inclusion_override: Optional[ + Literal[AnalysisType.ALLOWED_HEATING_USAGE] + | Literal[AnalysisType.NOT_ALLOWED_IN_CALCULATIONS] + ] = 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] + records: Sequence[OilPropaneBillingRecordInput] preceding_delivery_date: date = Field(description="Oil-Propane!B6") diff --git a/rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/cowen/Heat Load Analysis Beta 7.xlsx b/rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/cowen/Heat Load Analysis Beta 7.xlsx new file mode 100644 index 00000000..946aa3d3 Binary files /dev/null and b/rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/cowen/Heat Load Analysis Beta 7.xlsx differ diff --git a/rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/cowen/oil-propane.csv b/rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/cowen/oil-propane.csv new file mode 100644 index 00000000..74ad52ae --- /dev/null +++ b/rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/cowen/oil-propane.csv @@ -0,0 +1,5 @@ +start_date,end_date,usage,days_in_bill,inclusion_code,inclusion_override,avg_daily_usage,daily_htg_usage,hdd_at_58f,hdd_at_60f,hdd_at_62f,ua_at_58f,ua_at_60f,ua_at_62f,ua_sensitivity_at_-0.1_therms,ua_sensitivity_at_0.1_therms +2019-08-30 00:00:00,2019-12-11 00:00:00,149,104,0,,1.4326923076923077,1.4326923076923077,1000.4999999999998,1146.1999999999998,1306.1,,,,, +2019-12-12 00:00:00,2020-01-17 00:00:00,105,37,1,,2.8378378378378377,2.8378378378378377,829.1999999999999,900.6,974.6,589.2426435118186,542.5272040861647,501.3338805663862,, +2020-01-18 00:00:00,2020-03-21 00:00:00,141,64,1,,2.203125,2.203125,1394.500000000001,1522.5000000000011,1650.5000000000011,470.50555754750775,430.94909688013104,397.52802181157193,, +2020-03-22 00:00:00,2020-06-04 00:00:00,82,75,0,,1.0933333333333333,1.0933333333333333,726.7999999999998,849.2999999999996,980.1999999999997,,,,, diff --git a/rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/cowen/summary.json b/rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/cowen/summary.json new file mode 100644 index 00000000..10ec3b57 --- /dev/null +++ b/rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/cowen/summary.json @@ -0,0 +1,21 @@ +{ + "local_weather_station": "KBVY-Beverly", + "design_temperature_override": null, + "living_area": 1205, + "fuel_type": "OIL", + "heating_system_efficiency": 0.8, + "other_fuel_usage": 0, + "other_fuel_usage_override": null, + "thermostat_set_point": 68, + "setback_temperature": null, + "setback_hours_per_day": null, + "estimated_balance_point": 60, + "balance_point_sensitivity": 2, + "average_indoor_temperature": 68, + "difference_between_ti_and_tbp": 8, + "design_temperature": 9.5, + "whole_home_heat_loss_rate": 487.0, + "standard_deviation_of_heat_loss_rate": 0.1146, + "average_heat_load": 25554.0, + "maximum_heat_load": 29448.0 +} \ No newline at end of file diff --git a/rules-engine/tests/test_rules_engine/cases/examples/example-2/Heat Load Analysis Beta 7.xlsx b/rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/example-2/Heat Load Analysis Beta 7.xlsx similarity index 100% rename from rules-engine/tests/test_rules_engine/cases/examples/example-2/Heat Load Analysis Beta 7.xlsx rename to rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/example-2/Heat Load Analysis Beta 7.xlsx diff --git a/rules-engine/tests/test_rules_engine/cases/examples/example-2/oil-propane.csv b/rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/example-2/oil-propane.csv similarity index 100% rename from rules-engine/tests/test_rules_engine/cases/examples/example-2/oil-propane.csv rename to rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/example-2/oil-propane.csv diff --git a/rules-engine/tests/test_rules_engine/cases/examples/example-2/summary.json b/rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/example-2/summary.json similarity index 100% rename from rules-engine/tests/test_rules_engine/cases/examples/example-2/summary.json rename to rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/example-2/summary.json diff --git a/rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/gelfand/Heat Load Analysis Beta 7.xlsx b/rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/gelfand/Heat Load Analysis Beta 7.xlsx new file mode 100644 index 00000000..279e70fc Binary files /dev/null and b/rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/gelfand/Heat Load Analysis Beta 7.xlsx differ diff --git a/rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/gelfand/oil-propane.csv b/rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/gelfand/oil-propane.csv new file mode 100644 index 00000000..7e4b1bff --- /dev/null +++ b/rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/gelfand/oil-propane.csv @@ -0,0 +1,9 @@ +start_date,end_date,usage,days_in_bill,inclusion_code,inclusion_override,avg_daily_usage,daily_htg_usage,hdd_at_57f,hdd_at_58f,hdd_at_59f,ua_at_57f,ua_at_58f,ua_at_59f,ua_sensitivity_at_0.3_therms,ua_sensitivity_at_0.5_therms +2019-10-02 00:00:00,2019-11-18 00:00:00,191.1,48,0,,3.9812499999999997,3.58125,439.49999999999994,481.49999999999994,523.5000000000001,,,,, +2019-11-19 00:00:00,2019-12-23 00:00:00,154.6,35,1,,4.417142857142857,4.017142857142857,803.1000000000003,838.1000000000003,873.1000000000003,814.6665006433402,780.6451099709659,749.3513534150344,800.0779541025332,761.2122658393985 +2019-12-24 00:00:00,2020-02-07 00:00:00,197,46,1,,4.282608695652174,3.882608695652174,1054.1000000000001,1098.1000000000001,1142.3,788.4312051355026,756.8393892480951,727.5543494120051,776.3324530249217,737.3463254712684 +2020-02-08 00:00:00,2020-03-23 00:00:00,163,45,1,,3.6222222222222222,3.2222222222222223,856.4000000000001,899.6000000000001,943.6,787.8717110384555,750.0370535052615,715.062879751307,773.3140655105972,726.7600414999258 +2020-03-24 00:00:00,2020-05-15 00:00:00,128,53,0,,2.4150943396226414,2.0150943396226415,548.3,595.4,644.6999999999998,,,,, +2020-05-16 00:00:00,2020-10-15 00:00:00,66.4,153,0,,0.4339869281045752,0.033986928104575154,95.79999999999998,119.19999999999999,145,,,,, +2020-10-16 00:00:00,2020-12-07 00:00:00,132.5,53,1,,2.5,2.1,628.8,671.9999999999999,717.7999999999998,823.6577608142495,770.7083333333336,721.5324602953472,807.4087301587305,734.0079365079367 +2020-12-08 00:00:00,2021-01-18 00:00:00,194.2,42,1,,4.623809523809523,4.223809523809523,1021.3000000000001,1063.3,1105.2999999999997,808.2848656940499,776.3578795573528,746.8572634880425,794.7383930530737,757.977366061632 diff --git a/rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/gelfand/summary.json b/rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/gelfand/summary.json new file mode 100644 index 00000000..0e5a258c --- /dev/null +++ b/rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/gelfand/summary.json @@ -0,0 +1,21 @@ +{ + "local_weather_station": "KBED-Bedford", + "design_temperature_override": null, + "living_area": 1500, + "fuel_type": "OIL", + "heating_system_efficiency": 0.8, + "other_fuel_usage": 0, + "other_fuel_usage_override": 0.4, + "thermostat_set_point": 68, + "setback_temperature": null, + "setback_hours_per_day": null, + "estimated_balance_point": 58, + "balance_point_sensitivity": 1, + "average_indoor_temperature": 68, + "difference_between_ti_and_tbp": 10, + "design_temperature": 8.4, + "whole_home_heat_loss_rate": 767.0, + "standard_deviation_of_heat_loss_rate": 0.0152, + "average_heat_load": 39573.0, + "maximum_heat_load": 47242.0 +} \ No newline at end of file diff --git a/rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/harris/Heat Load Analysis Beta 7.xlsx b/rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/harris/Heat Load Analysis Beta 7.xlsx new file mode 100644 index 00000000..344df521 Binary files /dev/null and b/rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/harris/Heat Load Analysis Beta 7.xlsx differ diff --git a/rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/harris/oil-propane.csv b/rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/harris/oil-propane.csv new file mode 100644 index 00000000..74375e03 --- /dev/null +++ b/rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/harris/oil-propane.csv @@ -0,0 +1,17 @@ +start_date,end_date,usage,days_in_bill,inclusion_code,inclusion_override,avg_daily_usage,daily_htg_usage,hdd_at_58f,hdd_at_60f,hdd_at_62f,ua_at_58f,ua_at_60f,ua_at_62f,ua_sensitivity_at_0.2_therms,ua_sensitivity_at_0.4_therms +2018-08-09 00:00:00,2018-11-16 00:00:00,137.4,100,0,,1.374,1.074,516.6000000000001,614.2000000000002,713.9000000000002,,,,, +2018-11-17 00:00:00,2018-12-17 00:00:00,140.8,31,1,,4.541935483870968,4.241935483870968,752.8,814.8,876.7999999999998,812.8498051718032,750.9981999672723,697.8938564476887,768.7023400425463,733.2940598919982 +2018-12-18 00:00:00,2019-01-15 00:00:00,123.1,29,1,,4.244827586206896,3.9448275862068964,737.4999999999999,795.4999999999999,853.4999999999999,721.818757062147,669.1908652838887,623.7156805311464,686.1546197360152,652.227110831762 +2019-01-16 00:00:00,2019-02-09 00:00:00,133.1,25,1,,5.324,5.024,745.8000000000001,795.8,845.8,783.6667560561366,734.4290860350172,691.0128477969577,749.0474993717015,719.8106726983331 +2019-02-10 00:00:00,2019-03-12 00:00:00,147.9,31,1,,4.770967741935484,4.4709677419354845,888.7999999999998,950.7999999999998,1012.7999999999998,725.643564356436,678.3256205300802,636.8009478672988,693.4974056934514,663.153835366709 +2019-03-13 00:00:00,2019-05-16 00:00:00,135.8,65,0,,2.0892307692307694,1.7892307692307694,699.8999999999999,818.0999999999998,938.8999999999997,,,,, +2019-05-17 00:00:00,2019-11-13 00:00:00,129.7,181,0,,0.716574585635359,0.41657458563535904,375.49999999999994,470.80000000000007,588.4000000000001,,,,, +2019-11-14 00:00:00,2019-12-17 00:00:00,121,34,1,,3.5588235294117645,3.2588235294117647,780.5,848.5,916.5,660.588511637839,607.648006285602,562.5633751591198,626.2942447456295,589.0017678255745 +2019-12-18 00:00:00,2020-01-16 00:00:00,111.5,30,1,,3.716666666666667,3.416666666666667,679.8000000000001,737.4000000000001,797.4000000000001,701.6279297832697,646.8221679775789,598.1523284006354,665.753548503752,627.8907874514058 +2020-01-17 00:00:00,2020-02-20 00:00:00,170.6,35,1,,4.8742857142857146,4.574285714285715,952.4000000000002,1022.4000000000003,1092.4,782.2329553408931,728.6763171622326,681.9834004638107,744.6061554512257,712.7464788732394 +2020-02-21 00:00:00,2020-03-27 00:00:00,121.3,36,1,,3.3694444444444445,3.0694444444444446,608.5999999999999,679,750.9999999999999,844.8789571694601,757.2803141875307,684.6782068353308,781.9518900343643,732.6087383406972 +2020-03-28 00:00:00,2020-06-25 00:00:00,119.1,90,0,,1.3233333333333333,1.0233333333333332,559.0000000000001,667.1,783.5999999999999,,,,, +2020-06-26 00:00:00,2020-11-12 00:00:00,111.7,140,0,,0.7978571428571429,0.49785714285714294,345.4,426.69999999999993,523.3,,,,, +2020-11-13 00:00:00,2020-12-18 00:00:00,111.5,36,1,,3.0972222222222223,2.7972222222222225,724.1999999999998,796.1999999999998,868.1999999999998,647.0459357451904,588.533869212091,539.7266374875222,609.5738089257308,567.4939294984512 +2020-12-19 00:00:00,2021-01-14 00:00:00,109.6,27,1,,4.059259259259259,3.7592592592592595,689.6999999999999,743.6999999999999,797.6999999999999,684.8098206949882,635.0858321007574,592.0939367347792,651.9797409349648,618.19192326655 +2021-01-15 00:00:00,2021-02-13 00:00:00,113.8,30,0,,3.7933333333333334,3.4933333333333336,932.7,992.7,1052.7,,,,, diff --git a/rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/harris/summary.json b/rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/harris/summary.json new file mode 100644 index 00000000..86a32f22 --- /dev/null +++ b/rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/harris/summary.json @@ -0,0 +1,21 @@ +{ + "local_weather_station": "KBED-Bedford", + "design_temperature_override": null, + "living_area": 2200, + "fuel_type": "OIL", + "heating_system_efficiency": 0.8, + "other_fuel_usage": 0, + "other_fuel_usage_override": 0.3, + "thermostat_set_point": 68, + "setback_temperature": null, + "setback_hours_per_day": null, + "estimated_balance_point": 60, + "balance_point_sensitivity": 2, + "average_indoor_temperature": 68, + "difference_between_ti_and_tbp": 8, + "design_temperature": 8.4, + "whole_home_heat_loss_rate": 680.0, + "standard_deviation_of_heat_loss_rate": 0.0847, + "average_heat_load": 36432.0, + "maximum_heat_load": 41869.0 +} \ No newline at end of file diff --git a/rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/karle/Heat Load Analysis Beta 7.xlsx b/rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/karle/Heat Load Analysis Beta 7.xlsx new file mode 100644 index 00000000..af879739 Binary files /dev/null and b/rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/karle/Heat Load Analysis Beta 7.xlsx differ diff --git a/rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/karle/oil-propane.csv b/rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/karle/oil-propane.csv new file mode 100644 index 00000000..43de8749 --- /dev/null +++ b/rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/karle/oil-propane.csv @@ -0,0 +1,17 @@ +start_date,end_date,usage,days_in_bill,inclusion_code,inclusion_override,avg_daily_usage,daily_htg_usage,hdd_at_58.5f,hdd_at_60.5f,hdd_at_62.5f,ua_at_58.5f,ua_at_60.5f,ua_at_62.5f,ua_sensitivity_at_-0.1_therms,ua_sensitivity_at_0.1_therms +2019-01-16 00:00:00,2019-02-01 00:00:00,132,17,1,,7.764705882352941,7.764705882352941,576.1,610.0999999999999,644.0999999999999,1066.2037840652663,1006.7857728241273,953.6407390156809,, +2019-02-02 00:00:00,2019-02-21 00:00:00,130,20,1,,6.5,6.5,524.8,564.8000000000001,604.8000000000001,1152.6930894308944,1071.0576015108593,1000.2204585537918,, +2019-02-22 00:00:00,2019-03-12 00:00:00,129,19,1,,6.7894736842105265,6.7894736842105265,561.6999999999999,599.6999999999999,637.6999999999999,1068.6843510770875,1000.9671502417876,941.320370079975,, +2019-03-13 00:00:00,2019-04-08 00:00:00,105,27,1,,3.888888888888889,3.888888888888889,446.50000000000006,500.50000000000006,554.5000000000001,1094.288913773796,976.2237762237761,881.1541929666364,, +2019-04-09 00:00:00,2019-10-25 00:00:00,36,200,0,,0.18,0.18,411.2000000000001,541.4000000000002,696.1000000000001,,,,, +2019-10-26 00:00:00,2019-11-25 00:00:00,143,31,1,,4.612903225806452,4.612903225806452,530.1999999999999,588.9999999999999,649,1255.048409405256,1129.7566496887382,1025.3107344632767,, +2019-11-26 00:00:00,2019-12-17 00:00:00,129,22,1,,5.863636363636363,5.863636363636363,536.6,580.6,624.6,1118.6731270965336,1033.895969686531,961.0630803714376,, +2019-12-18 00:00:00,2021-01-12 00:00:00,205,392,0,,0.5229591836734694,0.5229591836734694,4634.700000000003,5150.4000000000015,5696.200000000001,,,,, +2021-01-13 00:00:00,2021-02-04 00:00:00,162,23,1,,7.043478260869565,7.043478260869565,682.2,728.2,774.2,1105.0131926121371,1035.2101071134302,973.7018858176181,, +2021-02-05 00:00:00,2021-02-25 00:00:00,147,21,1,,7,7,648.2,690.2,732.2,1055.291576673866,991.075050709939,934.2256214149138,, +2021-02-26 00:00:00,2021-03-23 00:00:00,141,26,1,,5.423076923076923,5.423076923076923,579.3,631.3,683.2999999999998,1132.6083203866738,1039.3156977665137,960.2224498756038,, +2021-03-24 00:00:00,2021-12-01 00:00:00,157,253,0,,0.6205533596837944,0.6205533596837944,1075.7,1294.9000000000003,1541.7,,,,, +2021-12-02 00:00:00,2022-01-06 00:00:00,202,36,1,,5.611111111111111,5.611111111111111,783.5,855.5000000000001,927.5000000000002,1199.7106998510956,1098.7414767192672,1013.448337825696,, +2022-01-07 00:00:00,2022-01-25 00:00:00,165,19,1,,8.68421052631579,8.68421052631579,641.3000000000001,679.3,717.3,1197.2555746140652,1130.2811717944944,1070.4028997630003,, +2022-01-26 00:00:00,2022-02-15 00:00:00,172,21,1,,8.19047619047619,8.19047619047619,675.1,717.0999999999999,759.0999999999999,1185.5626326963907,1116.1251336401247,1054.371404733676,, +2022-02-16 00:00:00,2022-03-10 00:00:00,146,23,1,,6.3478260869565215,6.3478260869565215,562.7,608.7000000000002,654.7,1207.3692316805875,1116.1272657576253,1037.706837737386,, diff --git a/rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/karle/summary.json b/rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/karle/summary.json new file mode 100644 index 00000000..0573d41b --- /dev/null +++ b/rules-engine/tests/test_rules_engine/cases/examples/fuel_oil/karle/summary.json @@ -0,0 +1,21 @@ +{ + "local_weather_station": "KBED-Bedford", + "design_temperature_override": null, + "living_area": 3800, + "fuel_type": "OIL", + "heating_system_efficiency": 0.8, + "other_fuel_usage": 0, + "other_fuel_usage_override": null, + "thermostat_set_point": 67, + "setback_temperature": 55, + "setback_hours_per_day": 7, + "estimated_balance_point": 60.5, + "balance_point_sensitivity": 2, + "average_indoor_temperature": 63.5, + "difference_between_ti_and_tbp": 3, + "design_temperature": 8.4, + "whole_home_heat_loss_rate": 1057.0, + "standard_deviation_of_heat_loss_rate": 0.0508, + "average_heat_load": 61961.0, + "maximum_heat_load": 65133.0 +} \ No newline at end of file diff --git a/rules-engine/tests/test_rules_engine/cases/examples/breslow/Heat Load Analysis Beta 7.xlsx b/rules-engine/tests/test_rules_engine/cases/examples/natural_gas/breslow/Heat Load Analysis Beta 7.xlsx similarity index 100% rename from rules-engine/tests/test_rules_engine/cases/examples/breslow/Heat Load Analysis Beta 7.xlsx rename to rules-engine/tests/test_rules_engine/cases/examples/natural_gas/breslow/Heat Load Analysis Beta 7.xlsx diff --git a/rules-engine/tests/test_rules_engine/cases/examples/breslow/natural-gas.csv b/rules-engine/tests/test_rules_engine/cases/examples/natural_gas/breslow/natural-gas.csv similarity index 100% rename from rules-engine/tests/test_rules_engine/cases/examples/breslow/natural-gas.csv rename to rules-engine/tests/test_rules_engine/cases/examples/natural_gas/breslow/natural-gas.csv diff --git a/rules-engine/tests/test_rules_engine/cases/examples/breslow/summary.json b/rules-engine/tests/test_rules_engine/cases/examples/natural_gas/breslow/summary.json similarity index 100% rename from rules-engine/tests/test_rules_engine/cases/examples/breslow/summary.json rename to rules-engine/tests/test_rules_engine/cases/examples/natural_gas/breslow/summary.json diff --git a/rules-engine/tests/test_rules_engine/cases/examples/cali/Heat Load Analysis Beta 7.xlsx b/rules-engine/tests/test_rules_engine/cases/examples/natural_gas/cali/Heat Load Analysis Beta 7.xlsx similarity index 100% rename from rules-engine/tests/test_rules_engine/cases/examples/cali/Heat Load Analysis Beta 7.xlsx rename to rules-engine/tests/test_rules_engine/cases/examples/natural_gas/cali/Heat Load Analysis Beta 7.xlsx diff --git a/rules-engine/tests/test_rules_engine/cases/examples/cali/natural-gas.csv b/rules-engine/tests/test_rules_engine/cases/examples/natural_gas/cali/natural-gas.csv similarity index 100% rename from rules-engine/tests/test_rules_engine/cases/examples/cali/natural-gas.csv rename to rules-engine/tests/test_rules_engine/cases/examples/natural_gas/cali/natural-gas.csv diff --git a/rules-engine/tests/test_rules_engine/cases/examples/cali/summary.json b/rules-engine/tests/test_rules_engine/cases/examples/natural_gas/cali/summary.json similarity index 100% rename from rules-engine/tests/test_rules_engine/cases/examples/cali/summary.json rename to rules-engine/tests/test_rules_engine/cases/examples/natural_gas/cali/summary.json diff --git a/rules-engine/tests/test_rules_engine/cases/examples/example-1/Heat Load Analysis Beta 7.xlsx b/rules-engine/tests/test_rules_engine/cases/examples/natural_gas/example-1/Heat Load Analysis Beta 7.xlsx similarity index 100% rename from rules-engine/tests/test_rules_engine/cases/examples/example-1/Heat Load Analysis Beta 7.xlsx rename to rules-engine/tests/test_rules_engine/cases/examples/natural_gas/example-1/Heat Load Analysis Beta 7.xlsx diff --git a/rules-engine/tests/test_rules_engine/cases/examples/example-1/natural-gas.csv b/rules-engine/tests/test_rules_engine/cases/examples/natural_gas/example-1/natural-gas.csv similarity index 100% rename from rules-engine/tests/test_rules_engine/cases/examples/example-1/natural-gas.csv rename to rules-engine/tests/test_rules_engine/cases/examples/natural_gas/example-1/natural-gas.csv diff --git a/rules-engine/tests/test_rules_engine/cases/examples/example-1/summary.json b/rules-engine/tests/test_rules_engine/cases/examples/natural_gas/example-1/summary.json similarity index 100% rename from rules-engine/tests/test_rules_engine/cases/examples/example-1/summary.json rename to rules-engine/tests/test_rules_engine/cases/examples/natural_gas/example-1/summary.json diff --git a/rules-engine/tests/test_rules_engine/cases/examples/example-4/Heat Load Analysis Beta 7.xlsx b/rules-engine/tests/test_rules_engine/cases/examples/natural_gas/example-4/Heat Load Analysis Beta 7.xlsx similarity index 100% rename from rules-engine/tests/test_rules_engine/cases/examples/example-4/Heat Load Analysis Beta 7.xlsx rename to rules-engine/tests/test_rules_engine/cases/examples/natural_gas/example-4/Heat Load Analysis Beta 7.xlsx diff --git a/rules-engine/tests/test_rules_engine/cases/examples/example-4/natural-gas.csv b/rules-engine/tests/test_rules_engine/cases/examples/natural_gas/example-4/natural-gas.csv similarity index 100% rename from rules-engine/tests/test_rules_engine/cases/examples/example-4/natural-gas.csv rename to rules-engine/tests/test_rules_engine/cases/examples/natural_gas/example-4/natural-gas.csv diff --git a/rules-engine/tests/test_rules_engine/cases/examples/example-4/summary.json b/rules-engine/tests/test_rules_engine/cases/examples/natural_gas/example-4/summary.json similarity index 100% rename from rules-engine/tests/test_rules_engine/cases/examples/example-4/summary.json rename to rules-engine/tests/test_rules_engine/cases/examples/natural_gas/example-4/summary.json diff --git a/rules-engine/tests/test_rules_engine/cases/examples/feldman/Heat Load Analysis Beta 7.xlsx b/rules-engine/tests/test_rules_engine/cases/examples/natural_gas/feldman/Heat Load Analysis Beta 7.xlsx similarity index 100% rename from rules-engine/tests/test_rules_engine/cases/examples/feldman/Heat Load Analysis Beta 7.xlsx rename to rules-engine/tests/test_rules_engine/cases/examples/natural_gas/feldman/Heat Load Analysis Beta 7.xlsx diff --git a/rules-engine/tests/test_rules_engine/cases/examples/feldman/natural-gas-eversource.csv b/rules-engine/tests/test_rules_engine/cases/examples/natural_gas/feldman/natural-gas-eversource.csv similarity index 100% rename from rules-engine/tests/test_rules_engine/cases/examples/feldman/natural-gas-eversource.csv rename to rules-engine/tests/test_rules_engine/cases/examples/natural_gas/feldman/natural-gas-eversource.csv diff --git a/rules-engine/tests/test_rules_engine/cases/examples/feldman/natural-gas.csv b/rules-engine/tests/test_rules_engine/cases/examples/natural_gas/feldman/natural-gas.csv similarity index 100% rename from rules-engine/tests/test_rules_engine/cases/examples/feldman/natural-gas.csv rename to rules-engine/tests/test_rules_engine/cases/examples/natural_gas/feldman/natural-gas.csv diff --git a/rules-engine/tests/test_rules_engine/cases/examples/feldman/summary.json b/rules-engine/tests/test_rules_engine/cases/examples/natural_gas/feldman/summary.json similarity index 100% rename from rules-engine/tests/test_rules_engine/cases/examples/feldman/summary.json rename to rules-engine/tests/test_rules_engine/cases/examples/natural_gas/feldman/summary.json diff --git a/rules-engine/tests/test_rules_engine/cases/examples/lewitus/Heat Load Analysis Beta 7.xlsx b/rules-engine/tests/test_rules_engine/cases/examples/natural_gas/lewitus/Heat Load Analysis Beta 7.xlsx similarity index 100% rename from rules-engine/tests/test_rules_engine/cases/examples/lewitus/Heat Load Analysis Beta 7.xlsx rename to rules-engine/tests/test_rules_engine/cases/examples/natural_gas/lewitus/Heat Load Analysis Beta 7.xlsx diff --git a/rules-engine/tests/test_rules_engine/cases/examples/lewitus/natural-gas.csv b/rules-engine/tests/test_rules_engine/cases/examples/natural_gas/lewitus/natural-gas.csv similarity index 100% rename from rules-engine/tests/test_rules_engine/cases/examples/lewitus/natural-gas.csv rename to rules-engine/tests/test_rules_engine/cases/examples/natural_gas/lewitus/natural-gas.csv diff --git a/rules-engine/tests/test_rules_engine/cases/examples/lewitus/summary.json b/rules-engine/tests/test_rules_engine/cases/examples/natural_gas/lewitus/summary.json similarity index 100% rename from rules-engine/tests/test_rules_engine/cases/examples/lewitus/summary.json rename to rules-engine/tests/test_rules_engine/cases/examples/natural_gas/lewitus/summary.json diff --git a/rules-engine/tests/test_rules_engine/cases/examples/quateman/Heat Load Analysis Beta 7.xlsx b/rules-engine/tests/test_rules_engine/cases/examples/natural_gas/quateman/Heat Load Analysis Beta 7.xlsx similarity index 100% rename from rules-engine/tests/test_rules_engine/cases/examples/quateman/Heat Load Analysis Beta 7.xlsx rename to rules-engine/tests/test_rules_engine/cases/examples/natural_gas/quateman/Heat Load Analysis Beta 7.xlsx diff --git a/rules-engine/tests/test_rules_engine/cases/examples/quateman/natural-gas-national-grid.csv b/rules-engine/tests/test_rules_engine/cases/examples/natural_gas/quateman/natural-gas-national-grid.csv similarity index 100% rename from rules-engine/tests/test_rules_engine/cases/examples/quateman/natural-gas-national-grid.csv rename to rules-engine/tests/test_rules_engine/cases/examples/natural_gas/quateman/natural-gas-national-grid.csv diff --git a/rules-engine/tests/test_rules_engine/cases/examples/quateman/natural-gas.csv b/rules-engine/tests/test_rules_engine/cases/examples/natural_gas/quateman/natural-gas.csv similarity index 100% rename from rules-engine/tests/test_rules_engine/cases/examples/quateman/natural-gas.csv rename to rules-engine/tests/test_rules_engine/cases/examples/natural_gas/quateman/natural-gas.csv diff --git a/rules-engine/tests/test_rules_engine/cases/examples/quateman/summary.json b/rules-engine/tests/test_rules_engine/cases/examples/natural_gas/quateman/summary.json similarity index 100% rename from rules-engine/tests/test_rules_engine/cases/examples/quateman/summary.json rename to rules-engine/tests/test_rules_engine/cases/examples/natural_gas/quateman/summary.json diff --git a/rules-engine/tests/test_rules_engine/cases/examples/shen/Heat Load Analysis Beta 7.xlsx b/rules-engine/tests/test_rules_engine/cases/examples/natural_gas/shen/Heat Load Analysis Beta 7.xlsx similarity index 100% rename from rules-engine/tests/test_rules_engine/cases/examples/shen/Heat Load Analysis Beta 7.xlsx rename to rules-engine/tests/test_rules_engine/cases/examples/natural_gas/shen/Heat Load Analysis Beta 7.xlsx diff --git a/rules-engine/tests/test_rules_engine/cases/examples/shen/natural-gas.csv b/rules-engine/tests/test_rules_engine/cases/examples/natural_gas/shen/natural-gas.csv similarity index 100% rename from rules-engine/tests/test_rules_engine/cases/examples/shen/natural-gas.csv rename to rules-engine/tests/test_rules_engine/cases/examples/natural_gas/shen/natural-gas.csv diff --git a/rules-engine/tests/test_rules_engine/cases/examples/shen/summary.json b/rules-engine/tests/test_rules_engine/cases/examples/natural_gas/shen/summary.json similarity index 100% rename from rules-engine/tests/test_rules_engine/cases/examples/shen/summary.json rename to rules-engine/tests/test_rules_engine/cases/examples/natural_gas/shen/summary.json diff --git a/rules-engine/tests/test_rules_engine/cases/examples/vitti/Heat Load Analysis Beta 7.xlsx b/rules-engine/tests/test_rules_engine/cases/examples/natural_gas/vitti/Heat Load Analysis Beta 7.xlsx similarity index 100% rename from rules-engine/tests/test_rules_engine/cases/examples/vitti/Heat Load Analysis Beta 7.xlsx rename to rules-engine/tests/test_rules_engine/cases/examples/natural_gas/vitti/Heat Load Analysis Beta 7.xlsx diff --git a/rules-engine/tests/test_rules_engine/cases/examples/vitti/natural-gas.csv b/rules-engine/tests/test_rules_engine/cases/examples/natural_gas/vitti/natural-gas.csv similarity index 100% rename from rules-engine/tests/test_rules_engine/cases/examples/vitti/natural-gas.csv rename to rules-engine/tests/test_rules_engine/cases/examples/natural_gas/vitti/natural-gas.csv diff --git a/rules-engine/tests/test_rules_engine/cases/examples/vitti/summary.json b/rules-engine/tests/test_rules_engine/cases/examples/natural_gas/vitti/summary.json similarity index 100% rename from rules-engine/tests/test_rules_engine/cases/examples/vitti/summary.json rename to rules-engine/tests/test_rules_engine/cases/examples/natural_gas/vitti/summary.json diff --git a/rules-engine/tests/test_rules_engine/cases/examples/yellepeddi/Heat Load Analysis Beta 7.xlsx b/rules-engine/tests/test_rules_engine/cases/examples/natural_gas/yellepeddi/Heat Load Analysis Beta 7.xlsx similarity index 100% rename from rules-engine/tests/test_rules_engine/cases/examples/yellepeddi/Heat Load Analysis Beta 7.xlsx rename to rules-engine/tests/test_rules_engine/cases/examples/natural_gas/yellepeddi/Heat Load Analysis Beta 7.xlsx diff --git a/rules-engine/tests/test_rules_engine/cases/examples/yellepeddi/natural-gas.csv b/rules-engine/tests/test_rules_engine/cases/examples/natural_gas/yellepeddi/natural-gas.csv similarity index 100% rename from rules-engine/tests/test_rules_engine/cases/examples/yellepeddi/natural-gas.csv rename to rules-engine/tests/test_rules_engine/cases/examples/natural_gas/yellepeddi/natural-gas.csv diff --git a/rules-engine/tests/test_rules_engine/cases/examples/yellepeddi/summary.json b/rules-engine/tests/test_rules_engine/cases/examples/natural_gas/yellepeddi/summary.json similarity index 100% rename from rules-engine/tests/test_rules_engine/cases/examples/yellepeddi/summary.json rename to rules-engine/tests/test_rules_engine/cases/examples/natural_gas/yellepeddi/summary.json diff --git a/rules-engine/tests/test_rules_engine/generate_example_data.py b/rules-engine/tests/test_rules_engine/generate_example_data.py index 8d3998a6..72fc9c40 100644 --- a/rules-engine/tests/test_rules_engine/generate_example_data.py +++ b/rules-engine/tests/test_rules_engine/generate_example_data.py @@ -9,7 +9,6 @@ Each folder contains an excel file (named "Heat Load Analysis Beta 7.xlsx") which specifies the example inputs Once this module is run, each folder will contain three files, the original excel, a summary.json, and a fuel-specific csv file -NOTE: This module depends on the existing "cases/examples" directory structure. Should this be changed, updates will be needed. NOTE: Due to the variety of types in the Summary and Fuel specific worksheets, the workbook object is purposely of type Any. """ @@ -18,22 +17,15 @@ import os import pathlib import re +from pathlib import Path from typing import Any import openpyxl ROOT_DIR = pathlib.Path(__file__).parent / "cases" / "examples" -# As of right now, all examples can and should be generated -YET_TO_BE_UPDATED_EXAMPLES = "" -# Filter in/out failing examples, if any -INPUT_DATA = filter( - lambda d: d not in YET_TO_BE_UPDATED_EXAMPLES, next(os.walk(ROOT_DIR))[1] -) - - -def generate_summary_json(workbook: Any, folder: str) -> str: +def generate_summary_json(workbook: Any, working_directory: Path) -> str: """ Read the heat load analysis spreadsheet and write information from the "Summary" tab into a json file We do this so our test runners use the json file for faster processing of our example data @@ -91,7 +83,7 @@ def generate_summary_json(workbook: Any, folder: str) -> str: break # Now that we have accumulated all the relevant fields into a data dictionary, write out summary.json - with open(ROOT_DIR / folder / "summary.json", "w") as json_file: + with open(working_directory / "summary.json", "w") as json_file: json.dump(data, json_file, indent=4) # Return the fuel type we found in the Summary worksheet so we can operate on the correct worksheet next @@ -99,7 +91,7 @@ def generate_summary_json(workbook: Any, folder: str) -> str: def generate_billing_record_input_csv( - workbook: Any, fuel_type: str, folder: str + workbook: Any, fuel_type: str, working_directory: Path ) -> None: """ Read the heat load analysis spreadsheet and write data from the appropriate "fuel type" worksheet into a csv file @@ -107,7 +99,7 @@ def generate_billing_record_input_csv( """ # Choose the appropriate fuel-type worksheet, set header and data row locations and output filename for each type if fuel_type == "GAS": - output_file_path = ROOT_DIR / folder / "natural-gas.csv" + output_file_path = working_directory / "natural-gas.csv" worksheet = workbook["Natural Gas"] header_row = 4 billing_row = 5 @@ -123,7 +115,7 @@ def generate_billing_record_input_csv( "daily_htg_usage", ] elif fuel_type == "OIL": - output_file_path = ROOT_DIR / folder / "oil-propane.csv" + output_file_path = working_directory / "oil-propane.csv" worksheet = workbook["Oil-Propane"] header_row = 5 billing_row = 7 @@ -187,12 +179,19 @@ def generate_billing_record_input_csv( if __name__ == "__main__": # For each example folder, read the excel sheet and write summary.json and "fuel_type".csv - for folder in INPUT_DATA: - workbook = openpyxl.load_workbook( - filename=ROOT_DIR / folder / "Heat Load Analysis Beta 7.xlsx", - data_only=True, - ) - fuel_type = generate_summary_json(workbook, folder) - generate_billing_record_input_csv(workbook, fuel_type, folder) + for root, dirs, files in os.walk(ROOT_DIR): + working_directory = Path(root) + try: + workbook = openpyxl.load_workbook( + filename=working_directory / "Heat Load Analysis Beta 7.xlsx", + data_only=True, + ) + except FileNotFoundError as fnf_error: + # It's ok to iterate over directories that do not contain an excel file + print("Skipping test directory:", working_directory) + continue + print("Processing test directory:", working_directory) + fuel_type = generate_summary_json(workbook, working_directory) + generate_billing_record_input_csv(workbook, fuel_type, working_directory) workbook.close() del workbook diff --git a/rules-engine/tests/test_rules_engine/test_engine.py b/rules-engine/tests/test_rules_engine/test_engine.py index 19471d3b..4aeafda4 100644 --- a/rules-engine/tests/test_rules_engine/test_engine.py +++ b/rules-engine/tests/test_rules_engine/test_engine.py @@ -247,12 +247,17 @@ def test_period_hdd(temps, expected_result): assert engine.period_hdd(temps, 60) == expected_result -def test_date_to_analysis_type(): +def test_date_to_analysis_type_natural_gas(): test_date = date.fromisoformat("2019-01-04") - assert engine.date_to_analysis_type(test_date) == AnalysisType.ALLOWED_HEATING_USAGE + assert ( + engine._date_to_analysis_type_natural_gas(test_date) + == AnalysisType.ALLOWED_HEATING_USAGE + ) dates = ["2019-01-04", "2019-07-04", "2019-12-04"] - types = [engine.date_to_analysis_type(date.fromisoformat(d)) for d in dates] + types = [ + engine._date_to_analysis_type_natural_gas(date.fromisoformat(d)) for d in dates + ] expected_types = [ AnalysisType.ALLOWED_HEATING_USAGE, AnalysisType.ALLOWED_NON_HEATING_USAGE, @@ -316,7 +321,9 @@ def test_convert_to_intermediate_billing_periods( sample_temp_inputs, sample_normalized_billing_periods ): results = engine.convert_to_intermediate_billing_periods( - sample_temp_inputs, sample_normalized_billing_periods + sample_temp_inputs, + sample_normalized_billing_periods, + FuelType.GAS, ) expected_results = [ diff --git a/rules-engine/tests/test_rules_engine/test_fuel_oil.py b/rules-engine/tests/test_rules_engine/test_fuel_oil.py new file mode 100644 index 00000000..60c2c76d --- /dev/null +++ b/rules-engine/tests/test_rules_engine/test_fuel_oil.py @@ -0,0 +1,78 @@ +""" +Tests for fuel-oil related methods. +""" + +import json +import os +import pathlib + +import pytest +from pydantic import BaseModel + +from rules_engine import engine +from rules_engine.pydantic_models import FuelType, TemperatureInput + +from .test_utils import ( + OilPropaneBillingExampleInput, + Summary, + load_fuel_billing_example_input, + load_temperature_data, +) + +# Test inputs are provided as separate directory within the "cases/examples" directory +# Each subdirectory contains a JSON file (named summary.json) which specifies the inputs for the test runner +ROOT_DIR = pathlib.Path(__file__).parent / "cases" / "examples" +FUEL_OIL_DIR = ROOT_DIR / "fuel_oil" + +INPUT_DATA = next(os.walk(FUEL_OIL_DIR))[1] + + +class Example(BaseModel): + summary: Summary + fuel_oil_usage: OilPropaneBillingExampleInput + temperature_data: TemperatureInput + + +@pytest.fixture(scope="module", params=INPUT_DATA) +def data(request): + """ + Loads the usage and temperature data and summary inputs into an + Example instance. + """ + summary = load_summary(request.param) + + if summary.fuel_type == FuelType.OIL: + fuel_oil_usage = load_fuel_billing_example_input( + FUEL_OIL_DIR / request.param, FuelType.OIL, summary.estimated_balance_point + ) + else: + fuel_oil_usage = None + + weather_station_short_name = summary.local_weather_station[:4] + temperature_data = load_temperature_data( + ROOT_DIR / "temperature-data.csv", weather_station_short_name + ) + + example = Example( + summary=summary, + fuel_oil_usage=fuel_oil_usage, + temperature_data=temperature_data, + ) + yield example + + +def load_summary(folder: str) -> Summary: + with open(FUEL_OIL_DIR / folder / "summary.json") as fh: + data = json.load(fh) + return Summary(**data) + + +def test_get_outputs_oil_propane(data: Example) -> None: + rezzy = engine.get_outputs_oil_propane( + data.summary, + None, + data.temperature_data, + data.fuel_oil_usage, + ) + # assert rezzy things here + # assert rezzy things here diff --git a/rules-engine/tests/test_rules_engine/test_examples.py b/rules-engine/tests/test_rules_engine/test_natural_gas.py similarity index 54% rename from rules-engine/tests/test_rules_engine/test_examples.py rename to rules-engine/tests/test_rules_engine/test_natural_gas.py index ac914298..d778ae6a 100644 --- a/rules-engine/tests/test_rules_engine/test_examples.py +++ b/rules-engine/tests/test_rules_engine/test_natural_gas.py @@ -2,50 +2,37 @@ import json import os import pathlib -from datetime import date, datetime, timedelta -from typing import Any, Literal, Optional +from datetime import datetime +from typing import Any import pytest from pydantic import BaseModel from pytest import approx -from typing_extensions import Annotated from rules_engine import engine -from rules_engine.pydantic_models import ( - NaturalGasBillingInput, - NaturalGasBillingRecordInput, - SummaryInput, - SummaryOutput, - TemperatureInput, +from rules_engine.pydantic_models import FuelType, TemperatureInput + +from .test_utils import ( + NaturalGasBillingExampleInput, + Summary, + load_fuel_billing_example_input, + load_temperature_data, ) # Test inputs are provided as separate directory within the "cases/examples" directory # Each subdirectory contains a JSON file (named summary.json) which specifies the inputs for the test runner ROOT_DIR = pathlib.Path(__file__).parent / "cases" / "examples" +NATURAL_GAS_DIR = ROOT_DIR / "natural_gas" # TODO: example-2 is OIL; all others are Natural Gas YET_TO_BE_UPDATED_EXAMPLES = "example-2" # Filter out failing examples for now INPUT_DATA = filter( - lambda d: d not in YET_TO_BE_UPDATED_EXAMPLES, next(os.walk(ROOT_DIR))[1] + lambda d: d not in YET_TO_BE_UPDATED_EXAMPLES, next(os.walk(NATURAL_GAS_DIR))[1] ) -class Summary(SummaryInput, SummaryOutput): - local_weather_station: str - - -# Extend NG Billing Record Input to capture whole home heat loss input from example data -class NaturalGasBillingRecordExampleInput(NaturalGasBillingRecordInput): - whole_home_heat_loss_rate: float - - -# Then overload NG Billing Input to contain new NG Billing Record Example Input subclass -class NaturalGasBillingExampleInput(NaturalGasBillingInput): - records: list[NaturalGasBillingRecordExampleInput] - - class Example(BaseModel): summary: Summary natural_gas_usage: NaturalGasBillingExampleInput @@ -53,93 +40,32 @@ class Example(BaseModel): def load_summary(folder: str) -> Summary: - with open(ROOT_DIR / folder / "summary.json") as f: + with open(NATURAL_GAS_DIR / folder / "summary.json") as f: d = json.load(f) return Summary(**d) -def load_natural_gas( - folder: str, estimated_balance_point: float -) -> NaturalGasBillingExampleInput: - records = [] - - with open(ROOT_DIR / folder / "natural-gas.csv") as f: - reader = csv.DictReader(f) - row: Any - for row in reader: - inclusion_override = row["inclusion_override"] - if inclusion_override == "": - inclusion_override = None - else: - inclusion_override = int(inclusion_override) - - # Choose the correct billing period heat loss (aka "ua") column based on the estimated balance point provided in SummaryOutput - ua_column_name = None - # First we will look for an exact match to the value of the estimated balance point - for column_name in row: - if ( - "ua_at_" in column_name - and str(estimated_balance_point) in column_name - ): - ua_column_name = column_name - break - # If we don't find that exact match, we round the balance point up to find our match - # It's possible that with further updates to summary data in xls and regen csv files, we wouldn't have this case - if ua_column_name == None: - ua_column_name = ( - "ua_at_" + str(int(round(estimated_balance_point, 0))) + "f" - ) - ua = ( - row[ua_column_name].replace(",", "").strip() - ) # Remove commas and whitespace to cleanup the data - if bool(ua): - whole_home_heat_loss_rate = float(ua) - else: - whole_home_heat_loss_rate = 0 - - item = NaturalGasBillingRecordExampleInput( - period_start_date=datetime.strptime( - row["start_date"].split(maxsplit=1)[0], "%Y-%m-%d" - ).date(), - period_end_date=datetime.strptime( - row["end_date"].split(maxsplit=1)[0], "%Y-%m-%d" - ).date(), - usage_therms=row["usage"], - inclusion_override=inclusion_override, - whole_home_heat_loss_rate=whole_home_heat_loss_rate, - ) - records.append(item) - - return NaturalGasBillingExampleInput(records=records) - - -def load_temperature_data(weather_station: str) -> TemperatureInput: - with open(ROOT_DIR / "temperature-data.csv", encoding="utf-8-sig") as f: - reader = csv.DictReader(f) - dates = [] - temperatures = [] - - row: Any - for row in reader: - dates.append(datetime.strptime(row["Date"], "%Y-%m-%d").date()) - temperatures.append(row[weather_station]) - - return TemperatureInput(dates=dates, temperatures=temperatures) - - @pytest.fixture(scope="module", params=INPUT_DATA) def data(request): + """ + Loads the usage and temperature data and summary inputs into an + Example instance. + """ summary = load_summary(request.param) - if summary.fuel_type == engine.FuelType.GAS: - natural_gas_usage = load_natural_gas( - request.param, summary.estimated_balance_point + if summary.fuel_type == FuelType.GAS: + natural_gas_usage = load_fuel_billing_example_input( + NATURAL_GAS_DIR / request.param, + FuelType.GAS, + summary.estimated_balance_point, ) else: natural_gas_usage = None weather_station_short_name = summary.local_weather_station[:4] - temperature_data = load_temperature_data(weather_station_short_name) + temperature_data = load_temperature_data( + ROOT_DIR / "temperature-data.csv", weather_station_short_name + ) example = Example( summary=summary, diff --git a/rules-engine/tests/test_rules_engine/test_parser.py b/rules-engine/tests/test_rules_engine/test_parser.py index 43fa3401..6cbdedfa 100644 --- a/rules-engine/tests/test_rules_engine/test_parser.py +++ b/rules-engine/tests/test_rules_engine/test_parser.py @@ -8,19 +8,18 @@ ROOT_DIR = pathlib.Path(__file__).parent / "cases" / "examples" -# TODO: Make sure that the tests pass because they're all broken because -# of refactoring elsewhere in the codebase. - def _read_gas_bill_eversource() -> str: """Read a test natural gas bill from a test Eversource CSV""" - with open(ROOT_DIR / "feldman" / "natural-gas-eversource.csv") as f: + with open(ROOT_DIR / "natural_gas" / "feldman" / "natural-gas-eversource.csv") as f: return f.read() def _read_gas_bill_national_grid() -> str: """Read a test natural gas bill from a test National Grid CSV""" - with open(ROOT_DIR / "quateman" / "natural-gas-national-grid.csv") as f: + with open( + ROOT_DIR / "natural_gas" / "quateman" / "natural-gas-national-grid.csv" + ) as f: return f.read() diff --git a/rules-engine/tests/test_rules_engine/test_utils.py b/rules-engine/tests/test_rules_engine/test_utils.py new file mode 100644 index 00000000..d4a10db9 --- /dev/null +++ b/rules-engine/tests/test_rules_engine/test_utils.py @@ -0,0 +1,182 @@ +import csv +from datetime import date, datetime, timedelta +from pathlib import Path +from typing import Any, Sequence + +from rules_engine.pydantic_models import ( + FuelType, + NaturalGasBillingInput, + NaturalGasBillingRecordInput, + OilPropaneBillingInput, + OilPropaneBillingRecordInput, + SummaryInput, + SummaryOutput, + TemperatureInput, +) + + +class Summary(SummaryInput, SummaryOutput): + """ + Holds summary.json information alongside a string referring to a + local weather station. + """ + + local_weather_station: str + + +# Extend NG Billing Record Input to capture whole home heat loss input from example data +class NaturalGasBillingRecordExampleInput(NaturalGasBillingRecordInput): + """ + whole_home_heat_loss_rate is added to this class solely because of testing needs + and must be included, and this class must be used instead of NaturalGasBillingRecordInput, + which would otherwise intuitively be used, and which is used in production. + """ + + whole_home_heat_loss_rate: float + + +# Then overload NG Billing Input to contain new NG Billing Record Example Input subclass +class NaturalGasBillingExampleInput(NaturalGasBillingInput): + """ + This class exists to contain a list of NaturalGasBillingRecordExampleInput, which + must be used for testing purposes rather than NaturalGasBillingInput, which would + otherwise intuitively used, and which is used in production. + """ + + records: Sequence[NaturalGasBillingRecordExampleInput] + + +class OilPropaneBillingRecordExampleInput(OilPropaneBillingRecordInput): + """ + whole_home_heat_loss_rate is added to this class solely because of testing needs + and must be included, and this class must be used instead of OilPropaneBillingRecordInput, + which would otherwise intuitively be used, and which is used in production. + """ + + whole_home_heat_loss_rate: float + + +class OilPropaneBillingExampleInput(OilPropaneBillingInput): + """ + This class exists to contain a list of OilPropaneBillingRecordExampleInput, which + must be used for testing purposes rather than OilPropaneBillingInput, which would + otherwise intuitively used, and which is used in production. + """ + + records: Sequence[OilPropaneBillingRecordExampleInput] + + +def load_fuel_billing_example_input( + folder: Path, fuel_type: FuelType, estimated_balance_point: float +) -> NaturalGasBillingExampleInput | OilPropaneBillingExampleInput: + """ + Loads a NaturalGasBillingExampleInput or + OilPropaneBillingExampleInput from an appropriate csv. + + Arguments: + folder - the path to the file + fuel_type - GAS or OIL, the latter of which refers to propane + too + estimated_balance_point - TODO: Document what this argument is. + """ + + file_name = None + match fuel_type: + case FuelType.GAS: + file_name = "natural-gas.csv" + case FuelType.OIL | FuelType.PROPANE: + file_name = "oil-propane.csv" + case _: + raise ValueError("Unsupported fuel type.") + + records: list[Any] = [] + with open(folder / file_name) as f: + 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) + + # Choose the correct billing period heat loss (aka "ua") + # column based on the estimated balance point provided in + # SummaryOutput + ua_column_name = None + # First we will look for an exact match to the value of + # the estimated balance point + for column_name in row: + if ( + "ua_at_" in column_name + and str(estimated_balance_point) in column_name + ): + ua_column_name = column_name + break + # If we don't find that exact match, we round the balance + # point up to find our match. + # It's possible that with further updates to summary data + # in xls and regen csv files, we wouldn't have this case. + if ua_column_name == None: + ua_column_name = ( + "ua_at_" + str(int(round(estimated_balance_point, 0))) + "f" + ) + ua = ( + row[ua_column_name].replace(",", "").strip() + ) # Remove commas and whitespace to cleanup the data + if bool(ua): + whole_home_heat_loss_rate = float(ua) + else: + whole_home_heat_loss_rate = 0 + + if fuel_type == FuelType.GAS: + natural_gas_item = NaturalGasBillingRecordExampleInput( + period_start_date=_parse_date(row["start_date"]), + period_end_date=_parse_date(row["end_date"]), + usage_therms=row["usage"], + inclusion_override=inclusion_override, + whole_home_heat_loss_rate=whole_home_heat_loss_rate, + ) + records.append(natural_gas_item) + elif fuel_type == FuelType.OIL: + if i == 0: + preceding_delivery_date = _parse_date( + row["start_date"] + ) - timedelta(days=1) + + oil_propane_item = OilPropaneBillingRecordExampleInput( + period_end_date=_parse_date(row["end_date"]), + gallons=row["usage"], + inclusion_override=inclusion_override, + whole_home_heat_loss_rate=whole_home_heat_loss_rate, + ) + records.append(oil_propane_item) + else: + raise ValueError("Unsupported fuel type.") + + if fuel_type == FuelType.GAS: + return NaturalGasBillingExampleInput(records=records) + elif fuel_type == FuelType.OIL: + return OilPropaneBillingExampleInput( + records=records, preceding_delivery_date=preceding_delivery_date + ) + else: + raise ValueError("Unsupported fuel type.") + + +def _parse_date(value: str) -> date: + return datetime.strptime(value.split(maxsplit=1)[0], "%Y-%m-%d").date() + + +def load_temperature_data(path: Path, weather_station: str) -> TemperatureInput: + with open(path, encoding="utf-8-sig") as f: + reader = csv.DictReader(f) + dates = [] + temperatures = [] + + row: Any + for row in reader: + dates.append(datetime.strptime(row["Date"], "%Y-%m-%d").date()) + temperatures.append(row[weather_station]) + + return TemperatureInput(dates=dates, temperatures=temperatures)