Skip to content

Commit

Permalink
Added winter cusp month logic per convo with Steve Breit.
Browse files Browse the repository at this point in the history
  • Loading branch information
eriksynn committed Aug 28, 2024
1 parent da1543e commit 61bd1c8
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 33 deletions.
78 changes: 57 additions & 21 deletions rules-engine/src/rules_engine/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def get_outputs_oil_propane(
period_start_date=start_date,
period_end_date=input_val.period_end_date,
usage=input_val.gallons,
inclusion_override=input_val.inclusion_override,
inclusion_override=bool(input_val.inclusion_override),
)
)
last_date = input_val.period_end_date
Expand All @@ -72,7 +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,
inclusion_override=input_val.inclusion_override,
inclusion_override=bool(input_val.inclusion_override),
)
)

Expand Down Expand Up @@ -145,6 +145,7 @@ def get_outputs_normalized(
usage=billing_period.input.usage,
inclusion_override=billing_period.input.inclusion_override,
analysis_type=billing_period.analysis_type,
winter_cusp_month=billing_period.winter_cusp_month,
eliminated_as_outlier=billing_period.eliminated_as_outlier,
whole_home_heat_loss_rate=billing_period.ua,
)
Expand All @@ -171,6 +172,7 @@ def convert_to_intermediate_billing_periods(
# Build a list of lists of temperatures, where each list of temperatures contains all the temperatures
# in the corresponding billing period
intermediate_billing_periods = []
winter_cusp_month = False

for billing_period in billing_periods:
# the HEAT Excel sheet is inclusive of the temperatures that fall on both the start and end dates
Expand All @@ -183,7 +185,7 @@ def convert_to_intermediate_billing_periods(
)

if fuel_type == FuelType.GAS:
analysis_type = _date_to_analysis_type_natural_gas(
analysis_type, winter_cusp_month = _date_to_analysis_type_natural_gas(
billing_period.period_end_date
)
elif fuel_type == FuelType.OIL or fuel_type == FuelType.PROPANE:
Expand All @@ -198,6 +200,7 @@ def convert_to_intermediate_billing_periods(
avg_temps=temperature_input.temperatures[start_idx:end_idx],
usage=billing_period.usage,
analysis_type=analysis_type,
winter_cusp_month=winter_cusp_month,
inclusion_override=billing_period.inclusion_override,
)
intermediate_billing_periods.append(intermediate_billing_period)
Expand All @@ -222,25 +225,41 @@ def _date_to_analysis_type_oil_propane(
return AnalysisType.ALLOWED_HEATING_USAGE


def _date_to_analysis_type_natural_gas(d: date) -> AnalysisType:
def _date_to_analysis_type_natural_gas(d: date) -> tuple[AnalysisType, bool]:
"""
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,
2: AnalysisType.ALLOWED_HEATING_USAGE,
3: AnalysisType.ALLOWED_HEATING_USAGE,
4: AnalysisType.NOT_ALLOWED_IN_CALCULATIONS,
4: AnalysisType.ALLOWED_HEATING_USAGE,
5: AnalysisType.NOT_ALLOWED_IN_CALCULATIONS,
6: AnalysisType.NOT_ALLOWED_IN_CALCULATIONS,
7: AnalysisType.ALLOWED_NON_HEATING_USAGE,
8: AnalysisType.ALLOWED_NON_HEATING_USAGE,
9: AnalysisType.ALLOWED_NON_HEATING_USAGE,
10: AnalysisType.NOT_ALLOWED_IN_CALCULATIONS,
11: AnalysisType.NOT_ALLOWED_IN_CALCULATIONS,
11: AnalysisType.ALLOWED_HEATING_USAGE,
12: AnalysisType.ALLOWED_HEATING_USAGE,
}
return months[d.month]

# Determine if a winter month is on the cusp per the "months" table
# Different regions of the country will have different tables, thus this algorithm
_analysis_type = months[d.month]
_winter_cusp_month = False

if _analysis_type == AnalysisType.ALLOWED_HEATING_USAGE:
try:
if (
months[d.month + 1] == AnalysisType.NOT_ALLOWED_IN_CALCULATIONS
or months[d.month - 1] == AnalysisType.NOT_ALLOWED_IN_CALCULATIONS
):
_winter_cusp_month = True
except KeyError:
pass # months 1 and 12 are not cusp months; so cusp is False; keep going

return (_analysis_type, _winter_cusp_month)


def hdd(avg_temp: float, balance_point: float) -> float:
Expand Down Expand Up @@ -397,26 +416,41 @@ def _initialize_billing_periods(self, billing_periods: list[BillingPeriod]) -> N
for billing_period in billing_periods:
billing_period.set_initial_balance_point(self.balance_point)

"""
The UI depicts billing period usage as several distinctive icons on the left hand column of the screen; "analysis_type"
For winter "cusp" months, for example April and November in MA, the UI will show those rows grayed out; "winter_cusp_month"
The user has the ability to "include" those cusp months in calculations by checking a box on far right; "inclusion_override"
The user may also choose to "exclude" any other "allowed" month by checking a box on the far right; "inclusion_override"
The following code implements this algorithm and adds bills accordingly to winter, summer, or shoulder (i.e. excluded) lists
"""

_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
# Only bills deemed ALLOWED by the AnalysisType algorithm can be included/excluded by the user
if (
_analysis_type == AnalysisType.ALLOWED_HEATING_USAGE
or _analysis_type == AnalysisType.ALLOWED_NON_HEATING_USAGE
):
if billing_period.inclusion_override:
# The user has requested we override an inclusion algorithm decision
if billing_period.winter_cusp_month == True:
# This bill is on the cusp of winter; the user has requested we include it
_analysis_type = AnalysisType.ALLOWED_HEATING_USAGE
else:
# The user has requested we exclude this bill from our calculations
_analysis_type = AnalysisType.NOT_ALLOWED_IN_CALCULATIONS
else:
# In this case we assume the intent is to exclude this bill from summer calculations
_analysis_type = AnalysisType.NOT_ALLOWED_IN_CALCULATIONS
# The user has chosen to not override our automatic calculations, even for a winter cusp month
if billing_period.winter_cusp_month == True:
_analysis_type = AnalysisType.NOT_ALLOWED_IN_CALCULATIONS

# Assign the bill to the appropriate list for winter or summer calculations
if _analysis_type == AnalysisType.ALLOWED_HEATING_USAGE:
self.bills_winter.append(billing_period)
elif _analysis_type == AnalysisType.NOT_ALLOWED_IN_CALCULATIONS:
self.bills_shoulder.append(billing_period)
else:
elif _analysis_type == AnalysisType.ALLOWED_NON_HEATING_USAGE:
self.bills_summer.append(billing_period)
else: # the rest are excluded from calculations
self.bills_shoulder.append(billing_period)

self._calculate_avg_summer_usage()
self._calculate_avg_non_heating_usage()
Expand Down Expand Up @@ -636,12 +670,14 @@ def __init__(
avg_temps: list[float],
usage: float,
analysis_type: AnalysisType,
winter_cusp_month: bool,
inclusion_override: bool,
) -> None:
self.input = input
self.avg_temps = avg_temps
self.usage = usage
self.analysis_type = analysis_type
self.winter_cusp_month = winter_cusp_month
self.inclusion_override = inclusion_override
self.eliminated_as_outlier = False
self.ua = None
Expand All @@ -653,7 +689,7 @@ def set_initial_balance_point(self, balance_point: float) -> None:
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}"
return f"{self.input}, {self.ua}, {self.eliminated_as_outlier}, {self.days}, {self.avg_temps}, {self.usage}, {self.analysis_type}, {self.winter_cusp_month}"

def __repr__(self) -> str:
return self.__str__()
20 changes: 11 additions & 9 deletions rules-engine/src/rules_engine/pydantic_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,19 @@ class AnalysisType(Enum):
'Inclusion' in calculations is now determined by
the date_to_analysis_type calculation and the inclusion_override variable
TODO: determine if the following logic has actually been implemented...
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
)
# winter months - allowed in heating usage calculations
ALLOWED_HEATING_USAGE = 1
# summer months - allowed in non-heating usage calculations
ALLOWED_NON_HEATING_USAGE = -1
# shoulder months - these fall outside reasonable bounds
NOT_ALLOWED_IN_CALCULATIONS = 0


class FuelType(Enum):
Expand Down Expand Up @@ -144,7 +145,7 @@ class NormalizedBillingPeriodRecordBase(BaseModel):
"""
Base class for a normalized billing period record.
Holds data as fields.
Holds data inputs provided by the UI as fields.
"""

model_config = ConfigDict(validate_assignment=True)
Expand All @@ -159,12 +160,13 @@ class NormalizedBillingPeriodRecord(NormalizedBillingPeriodRecordBase):
"""
Derived class for holding a normalized billing period record.
Holds data.
Holds generated data that is calculated by the rules engine.
"""

model_config = ConfigDict(validate_assignment=True)

analysis_type: AnalysisType = Field(frozen=True)
winter_cusp_month: bool = Field(frozen=True)
eliminated_as_outlier: bool = Field(frozen=True)
whole_home_heat_loss_rate: Optional[float] = Field(frozen=True)

Expand Down
Loading

0 comments on commit 61bd1c8

Please sign in to comment.