Skip to content

Commit

Permalink
Refactor Home class in engine.py.
Browse files Browse the repository at this point in the history
  • Loading branch information
jkwan2011 committed Oct 25, 2023
1 parent 1ca7e1d commit 54a8fcb
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 35 deletions.
99 changes: 79 additions & 20 deletions rules-engine/src/rules_engine/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@

import numpy as np
from pydantic import BaseModel, Field
from rules_engine.pydantic_models import SummaryInput, DhwInput, NaturalGasBillingInput, SummaryOutput, SensitivityGraph
from rules_engine.pydantic_models import SummaryInput, DhwInput, NaturalGasBillingInput, SummaryOutput, BalancePointGraph

def getOutputsNaturalGas(summaryInput: SummaryInput, dhwInput: Optional[DhwInput], naturalGasBillingInput: NaturalGasBillingInput)->(SummaryOutput, SensitivityGraph):
""""""
def getOutputsNaturalGas(summaryInput: SummaryInput, dhwInput: Optional[DhwInput], naturalGasBillingInput: NaturalGasBillingInput)->(SummaryOutput, BalancePointGraph):

# home = Home(summaryInput, naturalGasBillingInput, dhwInput, naturalGasBillingInput)
# home.calculate()
# return(home.summaryOutput, home.balancePointGraph)

pass

Expand Down Expand Up @@ -123,6 +126,9 @@ def __init__(
self,
fuel_type: FuelType,
heat_sys_efficiency: float,
temps: List[List[float]],
usages: List[float],
inclusion_codes: List[int],
initial_balance_point: float = 60,
thermostat_set_point: float = 68,
has_boiler_for_dhw: bool = False,
Expand All @@ -134,22 +140,57 @@ def __init__(
self.thermostat_set_point = thermostat_set_point
self.has_boiler_for_dhw = has_boiler_for_dhw
self.same_fuel_dhw_heating = same_fuel_dhw_heating
self._initialize_billing_periods(temps, usages, inclusion_codes)

def initialize_billing_periods(
def _initialize_billing_periods(
self, temps: List[List[float]], usages: List[float], inclusion_codes: List[int]
) -> None:
"""
Eventually, this method should categorize the billing periods by
season and calculate avg_non_heating_usage based on that. For
now, we just pass in winter-only heating periods and manually
define non-heating
TODO
"""
# assume for now that temps and usages have the same number of elements

self.bills_winter = []
self.bills_summer = []
self.bills_shoulder = []

# winter months 1; summer months -1; shoulder months 0
for i in range(len(usages)):
if inclusion_codes[i] == 1:
self.bills_winter.append(
BillingPeriod(temps[i], usages[i], self, inclusion_codes[i])
)
elif inclusion_codes[i] == -1:
self.bills_summer.append(
BillingPeriod(temps[i], usages[i], self, inclusion_codes[i])
)
else:
self.bills_shoulder.append(
BillingPeriod(temps[i], usages[i], self, inclusion_codes[i])
)

self._calculate_avg_summer_usage()
self._calculate_avg_non_heating_usage()
for bill in self.bills_winter:
bill.initialize_ua()


def _initialize_billing_periods_reworked(
self, billingperiods: NaturalGasBillingInput
) -> None:
"""
TODO
"""
# assume for now that temps and usages have the same number of elements

self.bills_winter = []
self.bills_summer = []
self.bills_shoulder = []

ngb_start_date = billingperiods.period_start_date
ngbs = billingperiods.records


# winter months 1; summer months -1; shoulder months 0
for i in range(len(usages)):
if inclusion_codes[i] == 1:
Expand All @@ -165,12 +206,13 @@ def initialize_billing_periods(
BillingPeriod(temps[i], usages[i], self, inclusion_codes[i])
)

self.calculate_avg_summer_usage()
self.calculate_avg_non_heating_usage()
self._calculate_avg_summer_usage()
self._calculate_avg_non_heating_usage()
for bill in self.bills_winter:
bill.initialize_ua()

def calculate_avg_summer_usage(self) -> None:

def _calculate_avg_summer_usage(self) -> None:
"""
Calculate average daily summer usage
"""
Expand All @@ -181,7 +223,7 @@ def calculate_avg_summer_usage(self) -> None:
else:
self.avg_summer_usage = 0

def calculate_boiler_usage(self, fuel_multiplier: float) -> float:
def _calculate_boiler_usage(self, fuel_multiplier: float) -> float:
"""
Calculate boiler usage with oil or propane
Args:
Expand All @@ -204,7 +246,7 @@ def calculate_boiler_usage(self, fuel_multiplier: float) -> float:
would be a property of the Home.
"""

def calculate_avg_non_heating_usage(self) -> None:
def _calculate_avg_non_heating_usage(self) -> None:
"""
Calculate avg non heating usage for this Home
Args:
Expand All @@ -217,11 +259,11 @@ def calculate_avg_non_heating_usage(self) -> None:
fuel_multiplier = 1 # default multiplier, for oil, placeholder number
if self.fuel_type == FuelType.PROPANE:
fuel_multiplier = 2 # a placeholder number
self.avg_non_heating_usage = self.calculate_boiler_usage(fuel_multiplier)
self.avg_non_heating_usage = self._calculate_boiler_usage(fuel_multiplier)
else:
self.avg_non_heating_usage = 0

def calculate_balance_point_and_ua(
def _calculate_balance_point_and_ua(
self,
initial_balance_point_sensitivity: float = 2,
stdev_pct_max: float = 0.10,
Expand All @@ -236,7 +278,7 @@ def calculate_balance_point_and_ua(
self.uas = [bp.ua for bp in self.bills_winter]
self.avg_ua = sts.mean(self.uas)
self.stdev_pct = sts.pstdev(self.uas) / self.avg_ua
self.refine_balance_point(initial_balance_point_sensitivity)
self._refine_balance_point(initial_balance_point_sensitivity)

while self.stdev_pct > stdev_pct_max:
biggest_outlier_idx = np.argmax(
Expand All @@ -258,14 +300,17 @@ def calculate_balance_point_and_ua(
else:
self.uas, self.avg_ua, self.stdev_pct = uas_i, avg_ua_i, stdev_pct_i

self.refine_balance_point(next_balance_point_sensitivity)
self._refine_balance_point(next_balance_point_sensitivity)

def calculate_balance_point_and_ua_customizable(
def _calculate_balance_point_and_ua_customizable(
self,
bps_to_remove: List[BillingPeriod],
balance_point_sensitivity: float = 2,
) -> None:
"""
QUESTIONABLE if this is still needed when frontend only sends datapoints selected by user,
so _calculate_balance_point_and_ua() should suffice
Calculates the estimated balance point and UA coefficient for
the home based on user input
Expand All @@ -282,9 +327,9 @@ def calculate_balance_point_and_ua_customizable(
self.stdev_pct = sts.pstdev(self.uas) / self.avg_ua

self.bills_winter = customized_bills
self.refine_balance_point(balance_point_sensitivity)
self._refine_balance_point(balance_point_sensitivity)

def refine_balance_point(self, balance_point_sensitivity: float) -> None:
def _refine_balance_point(self, balance_point_sensitivity: float) -> None:
"""
Tries different balance points plus or minus a given number
of degrees, choosing whichever one minimizes the standard
Expand Down Expand Up @@ -326,6 +371,20 @@ def refine_balance_point(self, balance_point_sensitivity: float) -> None:
if len(directions_to_check) == 2:
directions_to_check.pop(-1)

def calculate(self,
initial_balance_point_sensitivity: float = 2,
stdev_pct_max: float = 0.10,
max_stdev_pct_diff: float = 0.01,
next_balance_point_sensitivity: float = 0.5):
"""
For this Home, calculates avg non heating usage and then the estimated balance point
and UA coefficient for the home, removing UA outliers based on a normalized standard
deviation threshold.
"""
self._calculate_avg_non_heating_usage()
self._calculate_balance_point_and_ua(initial_balance_point_sensitivity,
stdev_pct_max, max_stdev_pct_diff, next_balance_point_sensitivity)


class BillingPeriod:
def __init__(
Expand Down
5 changes: 3 additions & 2 deletions rules-engine/src/rules_engine/pydantic_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,14 @@ class OilPropaneBillingInput(BaseModel):
"""From Oil-Propane tab"""
period_end_date: date = Field(description="Oil-Propane!B")
gallons: float = Field(description="Oil-Propane!C")
exclude: bool


class NaturalGasBillingRecordInput(BaseModel):
"""From Natural Gas tab. A single row of the Billing input table."""
period_end_date: date = Field(description="Natural Gas!B")
usage_therms: float = Field(description="Natural Gas!D")

exclude: bool

class NaturalGasBillingInput(BaseModel):
"""From Natural Gas tab. Container for holding all rows of the billing input table."""
Expand All @@ -57,7 +58,7 @@ class SummaryOutput(BaseModel):
average_indoor_temperature: float = Field(description="Summary!B24")
difference_between_ti_and_tbp: float = Field(description="Summary!B25")
design_temperature: float = Field(description="Summary!B26")
whole_home_heat_loss_rate: float = sField(description="Summary!B27") # UA = heat loss rate
whole_home_heat_loss_rate: float = Field(description="Summary!B27") # UA = heat loss rate
standard_deviation_of_heat_loss_rate: float = Field(description="Summary!B28")
average_heat_load: float = Field(description="Summary!B29")
maximum_heat_load: float = Field(description="Summary!B30")
Expand Down
35 changes: 22 additions & 13 deletions rules-engine/tests/test_rules_engine/test_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,21 +39,24 @@ def test_average_indoor_temp():


def test_bp_ua_estimates():
home = engine.Home(
engine.FuelType.GAS, heat_sys_efficiency=0.88, initial_balance_point=58
)

daily_temps_lists = [
[28, 29, 30, 29],
[32, 35, 35, 38],
[41, 43, 42, 42],
[72, 71, 70, 69],
]

usages = [50, 45, 30, 0.96]
inclusion_codes = [1, 1, 1, -1]
home.initialize_billing_periods(daily_temps_lists, usages, inclusion_codes)
home.calculate_avg_non_heating_usage()
home.calculate_balance_point_and_ua()
heat_sys_efficiency = 0.88
home = engine.Home(
engine.FuelType.GAS,heat_sys_efficiency,daily_temps_lists, usages, inclusion_codes, initial_balance_point=58
)

#home.initialize_billing_periods(daily_temps_lists, usages, inclusion_codes)
#home.calculate_avg_non_heating_usage()
#home.calculate_balance_point_and_ua()
home.calculate()

ua_1, ua_2, ua_3 = [bill.ua for bill in home.bills_winter]

Expand All @@ -66,9 +69,8 @@ def test_bp_ua_estimates():


def test_bp_ua_with_outlier():
home = engine.Home(
engine.FuelType.GAS, heat_sys_efficiency=0.88, initial_balance_point=58
)


daily_temps_lists = [
[41.7, 41.6, 32, 25.4],
[28, 29, 30, 29],
Expand All @@ -78,9 +80,16 @@ def test_bp_ua_with_outlier():
]
usages = [60, 50, 45, 30, 0.96]
inclusion_codes = [1, 1, 1, 1, -1]
home.initialize_billing_periods(daily_temps_lists, usages, inclusion_codes)
home.calculate_avg_non_heating_usage()
home.calculate_balance_point_and_ua()
heat_sys_efficiency=0.88
home = engine.Home(
engine.FuelType.GAS,heat_sys_efficiency,daily_temps_lists, usages, inclusion_codes, initial_balance_point=58
)

# home.initialize_billing_periods(daily_temps_lists, usages, inclusion_codes)
# home.calculate_avg_non_heating_usage()
# home.calculate_balance_point_and_ua()
home.calculate()

ua_1, ua_2, ua_3 = [bill.ua for bill in home.bills_winter]

assert home.balance_point == 60
Expand Down

0 comments on commit 54a8fcb

Please sign in to comment.