Skip to content

Commit

Permalink
Added enum for NaturalGasCompany, refactored parser testing to have a…
Browse files Browse the repository at this point in the history
…n entry point

Co-authored-by: Debajyoti Debnath <[email protected]>
Co-authored-by: thatoldplatitude <[email protected]>
Co-authored-by: eriksynn <[email protected]>
Co-authored-by: AdamFinkle <[email protected]>
  • Loading branch information
5 people committed Apr 17, 2024
1 parent a153957 commit f4a9874
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 39 deletions.
33 changes: 16 additions & 17 deletions rules-engine/src/rules_engine/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ def get_outputs_normalized(
default_inclusion_by_calculation = True
if billing_period.analysis_type == AnalysisType.NOT_ALLOWED_IN_CALCULATIONS:
default_inclusion_by_calculation = False

billing_record = NormalizedBillingPeriodRecord(
input=billing_period.input,
analysis_type=billing_period.analysis_type,
Expand All @@ -147,7 +147,7 @@ def get_outputs_normalized(
result = RulesEngineResult(
summary_output=summary_output,
balance_point_graph=balance_point_graph,
billing_records=billing_records
billing_records=billing_records,
)
return result

Expand Down Expand Up @@ -354,37 +354,35 @@ def _initialize_billing_periods(self, billing_periods: List[BillingPeriod]) -> N
self.bills_shoulder = []

# 'Inclusion' in calculations is now determined by two variables:
# billing_period.default_inclusion_by_calculation: bool
# billing_period.default_inclusion_by_calculation: bool
# billing_period.inclusion_override: bool (False by default)
#
#
# Our logic around the AnalysisType can now disallow
# a billing_period from being included or overridden
# by marking it as NOT_ALLOWED_IN_CALCULATIONS
#
#
# Options for billing_period.analysis_type:
# ALLOWED_HEATING_USAGE = 1 # winter months - allowed in heating usage calculations
# ALLOWED_NON_HEATING_USAGE = -1 # summer months - allowed in non-heating usage calculations
# NOT_ALLOWED_IN_CALCULATIONS = 0 # shoulder months that fall outside reasonable bounds
#
# NOT_ALLOWED_IN_CALCULATIONS = 0 # shoulder months that fall outside reasonable bounds
#
# Use HDDs to determine if shoulder months
# are heating or non-heating or not allowed,
# or included or excluded
#
#
# Rough calculations from Steve, this will be ammended:
# IF hdds is within 70% or higher of max, allowed
# less than 25% of max, not allowed
#

#


# IF winter months
# analysis_type = ALLOWED_HEATING_USAGE
# default_inclusion_by_calculation = True
# ELSE IF summer months
# analysis_type = ALLOWED_NON_HEATING_USAGE
# default_inclusion_by_calculation = True
# ELSE IF shoulder months
# IF hdds < 25% || hdds > 70%
# IF hdds < 25% || hdds > 70%
# analysis_type = NOT_ALLOWED_IN_CALCULATIONS
# default_inclusion_by_calculation = False
# IF 25% < hdds < 50%
Expand All @@ -394,15 +392,15 @@ def _initialize_billing_periods(self, billing_periods: List[BillingPeriod]) -> N
# analysis_type = ALLOWED_HEATING_USAGE
# default_inclusion_by_calculation = False



# winter months 1; summer months -1; shoulder months 0
for billing_period in billing_periods:
billing_period.set_initial_balance_point(self.balance_point)

if billing_period.analysis_type == AnalysisType.ALLOWED_HEATING_USAGE:
self.bills_winter.append(billing_period)
elif billing_period.analysis_type == AnalysisType.NOT_ALLOWED_IN_CALCULATIONS:
elif (
billing_period.analysis_type == AnalysisType.NOT_ALLOWED_IN_CALCULATIONS
):
self.bills_shoulder.append(billing_period)
else:
self.bills_summer.append(billing_period)
Expand Down Expand Up @@ -482,7 +480,8 @@ def _calculate_balance_point_and_ua(
stdev_pct_i = sts.pstdev(uas_i) / avg_ua_i
if (
# the outlier has been removed
self.stdev_pct - stdev_pct_i < max_stdev_pct_diff
self.stdev_pct - stdev_pct_i
< max_stdev_pct_diff
): # if it's a small enough change
# add the outlier back in
self.bills_winter.append(
Expand Down Expand Up @@ -625,7 +624,7 @@ def __init__(
self.usage = usage
self.analysis_type = analysis_type
self.eliminated_as_outlier = False

self.days = len(self.avg_temps)

def set_initial_balance_point(self, balance_point: float) -> None:
Expand Down
19 changes: 19 additions & 0 deletions rules-engine/src/rules_engine/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@
import csv
import io
from datetime import datetime, timedelta
from enum import Enum

from .pydantic_models import NaturalGasBillingInput, NaturalGasBillingRecordInput


class NaturalGasCompany(Enum):
EVERSOURCE = 1
NATIONAL_GRID = 2


class _GasBillRowEversource:
"""
Holds data for one row of an Eversource gas bill CSV.
Expand Down Expand Up @@ -51,6 +57,19 @@ def __init__(self, row):
self.usage = row["USAGE"]


def parse_gas_bill(data: str, company: NaturalGasCompany) -> NaturalGasBillingInput:
"""
Parse a natural gas bill from a given natural gas company.
"""
match company:
case NaturalGasCompany.EVERSOURCE:
return parse_gas_bill_eversource(data)
case NaturalGasCompany.NATIONAL_GRID:
return parse_gas_bill_national_grid(data)
case _:
raise ValueError("Wrong CSV format selected: select another format.")


def parse_gas_bill_eversource(data: str) -> NaturalGasBillingInput:
"""
Return a list of gas bill data parsed from an Eversource CSV
Expand Down
25 changes: 17 additions & 8 deletions rules-engine/src/rules_engine/pydantic_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,31 @@


class AnalysisType(Enum):
"""Enum for analysis type.
'Inclusion' in calculations is now determined by
"""
Enum for analysis type.
'Inclusion' in calculations is now determined by
the default_inclusion_by_calculation and inclusion_override variables
Use HDDs to determine if shoulder months
are heating or non-heating or not allowed,
or included or excluded
"""

ALLOWED_HEATING_USAGE = 1 # winter months - allowed in heating usage calculations
ALLOWED_NON_HEATING_USAGE = -1 # summer months - allowed in non-heating usage calculations
NOT_ALLOWED_IN_CALCULATIONS = 0 # shoulder months that fall outside reasonable bounds

ALLOWED_HEATING_USAGE = 1 # winter months - allowed in heating usage calculations
ALLOWED_NON_HEATING_USAGE = (
-1
) # summer months - allowed in non-heating usage calculations
NOT_ALLOWED_IN_CALCULATIONS = (
0 # shoulder months that fall outside reasonable bounds
)


class FuelType(Enum):
"""Enum for fuel types. Values are BTU per usage"""
"""
Enum for fuel types.
Values are BTU per usage
"""

GAS = 100000 # BTU / therm
OIL = 139600 # BTU / gal
Expand Down Expand Up @@ -102,7 +111,7 @@ class NormalizedBillingPeriodRecordInput(BaseModel):
period_start_date: date
period_end_date: date
usage: float
analysis_type_override: Optional[AnalysisType] # for testing only
analysis_type_override: Optional[AnalysisType] # for testing only


class NormalizedBillingPeriodRecord:
Expand Down
20 changes: 15 additions & 5 deletions rules-engine/tests/test_rules_engine/test_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,25 @@ def sample_billing_periods() -> list[engine.BillingPeriod]:
engine.BillingPeriod([28, 29, 30, 29], 50, AnalysisType.ALLOWED_HEATING_USAGE),
engine.BillingPeriod([32, 35, 35, 38], 45, AnalysisType.ALLOWED_HEATING_USAGE),
engine.BillingPeriod([41, 43, 42, 42], 30, AnalysisType.ALLOWED_HEATING_USAGE),
engine.BillingPeriod([72, 71, 70, 69], 0.96, AnalysisType.NOT_ALLOWED_IN_CALCULATIONS),
engine.BillingPeriod(
[72, 71, 70, 69], 0.96, AnalysisType.NOT_ALLOWED_IN_CALCULATIONS
),
]
return billing_periods


@pytest.fixture()
def sample_billing_periods_with_outlier() -> list[engine.BillingPeriod]:
billing_periods = [
engine.BillingPeriod([41.7, 41.6, 32, 25.4], 60, AnalysisType.ALLOWED_HEATING_USAGE),
engine.BillingPeriod(
[41.7, 41.6, 32, 25.4], 60, AnalysisType.ALLOWED_HEATING_USAGE
),
engine.BillingPeriod([28, 29, 30, 29], 50, AnalysisType.ALLOWED_HEATING_USAGE),
engine.BillingPeriod([32, 35, 35, 38], 45, AnalysisType.ALLOWED_HEATING_USAGE),
engine.BillingPeriod([41, 43, 42, 42], 30, AnalysisType.ALLOWED_HEATING_USAGE),
engine.BillingPeriod([72, 71, 70, 69], 0.96, AnalysisType.NOT_ALLOWED_IN_CALCULATIONS),
engine.BillingPeriod(
[72, 71, 70, 69], 0.96, AnalysisType.NOT_ALLOWED_IN_CALCULATIONS
),
]

return billing_periods
Expand Down Expand Up @@ -255,11 +261,15 @@ def test_convert_to_intermediate_billing_periods(
)

expected_results = [
engine.BillingPeriod([41.7, 41.6, 32, 25.4], 60, AnalysisType.ALLOWED_HEATING_USAGE),
engine.BillingPeriod(
[41.7, 41.6, 32, 25.4], 60, AnalysisType.ALLOWED_HEATING_USAGE
),
engine.BillingPeriod([28, 29, 30, 29], 50, AnalysisType.ALLOWED_HEATING_USAGE),
engine.BillingPeriod([32, 35, 35, 38], 45, AnalysisType.ALLOWED_HEATING_USAGE),
engine.BillingPeriod([41, 43, 42, 42], 30, AnalysisType.ALLOWED_HEATING_USAGE),
engine.BillingPeriod([72, 71, 70, 69], 0.96, AnalysisType.NOT_ALLOWED_IN_CALCULATIONS),
engine.BillingPeriod(
[72, 71, 70, 69], 0.96, AnalysisType.NOT_ALLOWED_IN_CALCULATIONS
),
]

for i in range(len(expected_results)):
Expand Down
55 changes: 46 additions & 9 deletions rules-engine/tests/test_rules_engine/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,24 @@

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 test_parse_gas_bill_eversource():

def _read_gas_bill_eversource():
"""Read a test natural gas bill from a test Eversource CSV"""
with open(ROOT_DIR / "feldman" / "natural-gas-eversource.csv") as f:
s = f.read()
return f.read()


def _read_gas_bill_national_grid():
"""Read a test natural gas bill from a test National Grid CSV"""
with open(ROOT_DIR / "quateman" / "natural-gas-national-grid.csv") as f:
return f.read()

result = parser.parse_gas_bill_eversource(s)

def _validate_eversource(result):
"""Validate the result of reading an Eversource CSV."""
assert len(result.records) == 36
for row in result.records:
assert isinstance(row, NaturalGasBillingRecordInput)
Expand All @@ -28,12 +39,8 @@ def test_parse_gas_bill_eversource():
assert second_row.inclusion_override == None


def test_parse_gas_bill_national_grid():
with open(ROOT_DIR / "quateman" / "natural-gas-national-grid.csv") as f:
s = f.read()

result = parser.parse_gas_bill_national_grid(s)

def _validate_national_grid(result):
"""Validate the result of reading a National Grid CSV."""
assert len(result.records) == 25
for row in result.records:
assert isinstance(row, NaturalGasBillingRecordInput)
Expand All @@ -47,3 +54,33 @@ def test_parse_gas_bill_national_grid():
assert isinstance(second_row.usage_therms, float)
assert second_row.usage_therms == 36
assert second_row.inclusion_override == None


def test_parse_gas_bill():
"""
Tests the logic of parse_gas_bill.
"""
_validate_eversource(
parser.parse_gas_bill(
_read_gas_bill_eversource(), parser.NaturalGasCompany.EVERSOURCE
)
)
_validate_national_grid(
parser.parse_gas_bill(
_read_gas_bill_national_grid(), parser.NaturalGasCompany.NATIONAL_GRID
)
)
# TODO: Does not verify that the method crashes when given the wrong
# enum.


def test_parse_gas_bill_eversource():
"""Tests parsing a natural gas bill from Eversource."""
_validate_eversource(parser.parse_gas_bill_eversource(_read_gas_bill_eversource()))


def test_parse_gas_bill_national_grid():
"""Tests parsing a natural gas bill from National Grid."""
_validate_national_grid(
parser.parse_gas_bill_national_grid(_read_gas_bill_national_grid())
)

0 comments on commit f4a9874

Please sign in to comment.