diff --git a/rules-engine/src/rules_engine/engine.py b/rules-engine/src/rules_engine/engine.py index 0f9fa9a4..3bc8b6bb 100644 --- a/rules-engine/src/rules_engine/engine.py +++ b/rules-engine/src/rules_engine/engine.py @@ -89,9 +89,8 @@ def get_outputs_normalized( home = Home( summary_input=summary_input, billing_periods=intermediate_billing_periods, + dhw_input=dhw_input, initial_balance_point=initial_balance_point, - has_boiler_for_dhw=dhw_input is not None, - same_fuel_dhw_heating=dhw_input is not None, ) home.calculate() @@ -277,6 +276,30 @@ def get_maximum_heat_load( return (design_set_point - design_temp) * ua +def calculate_dhw_usage(dhw_input: DhwInput, heating_system_efficiency: float) -> float: + """ + Calculate non-heating usage with oil or propane + """ + if dhw_input.estimated_water_heating_efficiency is not None: + heating_system_efficiency = dhw_input.estimated_water_heating_efficiency + + stand_by_losses = Constants.DEFAULT_STAND_BY_LOSSES + if dhw_input.stand_by_losses is not None: + stand_by_losses = dhw_input.stand_by_losses + + daily_fuel_oil_use_for_dhw = ( + dhw_input.number_of_occupants + * Constants.DAILY_DHW_CONSUMPTION_PER_OCCUPANT + * Constants.WATER_WEIGHT + * (Constants.LEAVING_WATER_TEMPERATURE - Constants.ENTERING_WATER_TEMPERATURE) + * Constants.SPECIFIC_HEAT_OF_WATER + / Constants.FUEL_OIL_BTU_PER_GAL + / (heating_system_efficiency * (1 - stand_by_losses)) + ) + + return daily_fuel_oil_use_for_dhw + + class Home: """ Defines attributes and methods for calculating home heat metrics. @@ -291,16 +314,14 @@ def __init__( self, summary_input: SummaryInput, billing_periods: List[BillingPeriod], + dhw_input: Optional[DhwInput], initial_balance_point: float = 60, - has_boiler_for_dhw: bool = False, - same_fuel_dhw_heating: bool = False, ): self.fuel_type = summary_input.fuel_type self.heat_sys_efficiency = summary_input.heating_system_efficiency self.thermostat_set_point = summary_input.thermostat_set_point self.balance_point = initial_balance_point - self.has_boiler_for_dhw = has_boiler_for_dhw - self.same_fuel_dhw_heating = same_fuel_dhw_heating + self.dhw_input = dhw_input self._initialize_billing_periods(billing_periods) def _initialize_billing_periods(self, billing_periods: List[BillingPeriod]) -> None: @@ -335,19 +356,6 @@ def _calculate_avg_summer_usage(self) -> None: else: self.avg_summer_usage = 0 - def _calculate_boiler_usage(self, fuel_multiplier: float) -> float: - """ - Calculate boiler usage with oil or propane - Args: - fuel_multiplier: a constant that's determined by the fuel - type - """ - - # self.num_occupants: the number of occupants in Home - # self.water_heat_efficiency: a number indicating how efficient the heating system is - - return 0 * fuel_multiplier - def _calculate_avg_non_heating_usage(self) -> None: """ Calculate avg non heating usage for this home @@ -355,11 +363,11 @@ def _calculate_avg_non_heating_usage(self) -> None: if self.fuel_type == FuelType.GAS: self.avg_non_heating_usage = self.avg_summer_usage - elif self.has_boiler_for_dhw and self.same_fuel_dhw_heating: - 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) + elif self.dhw_input is not None and self.fuel_type == FuelType.OIL: + # TODO: support non-heating usage for Propane in addition to fuel oil + self.avg_non_heating_usage = calculate_dhw_usage( + self.dhw_input, self.heat_sys_efficiency + ) else: self.avg_non_heating_usage = 0 diff --git a/rules-engine/src/rules_engine/pydantic_models.py b/rules-engine/src/rules_engine/pydantic_models.py index 3b36de42..d1f08702 100644 --- a/rules-engine/src/rules_engine/pydantic_models.py +++ b/rules-engine/src/rules_engine/pydantic_models.py @@ -55,8 +55,8 @@ class DhwInput(BaseModel): """From DHW (Domestic Hot Water) Tab""" number_of_occupants: int = Field(description="DHW!B4") - estimated_water_heating_efficiency: float = Field(description="DHW!B5") - stand_by_losses: float = Field(description="DHW!B6") + estimated_water_heating_efficiency: Optional[float] = Field(description="DHW!B5") + stand_by_losses: Optional[float] = Field(description="DHW!B6") class OilPropaneBillingRecordInput(BaseModel): @@ -144,4 +144,11 @@ class BalancePointGraph(BaseModel): @dataclass class Constants: BALANCE_POINT_SENSITIVITY: float = 0.5 - DESIGN_SET_POINT: float = 70 + DESIGN_SET_POINT: float = 70 # deg. F + DAILY_DHW_CONSUMPTION_PER_OCCUPANT: float = 15.78 # Gal/day/person + WATER_WEIGHT: float = 8.33 # lbs/gal + ENTERING_WATER_TEMPERATURE: float = 55 # deg. F + LEAVING_WATER_TEMPERATURE: float = 125 # deg. F + SPECIFIC_HEAT_OF_WATER: float = 1.00 # BTU/lbs-deg. F + DEFAULT_STAND_BY_LOSSES: float = 0.05 # + FUEL_OIL_BTU_PER_GAL: float = 139000 diff --git a/rules-engine/tests/test_rules_engine/test_engine.py b/rules-engine/tests/test_rules_engine/test_engine.py index 6e516276..723bff83 100644 --- a/rules-engine/tests/test_rules_engine/test_engine.py +++ b/rules-engine/tests/test_rules_engine/test_engine.py @@ -210,6 +210,7 @@ def test_bp_ua_estimates(sample_summary_inputs, sample_billing_periods): home = engine.Home( sample_summary_inputs, sample_billing_periods, + dhw_input=None, initial_balance_point=58, ) @@ -229,6 +230,7 @@ def test_bp_ua_with_outlier(sample_summary_inputs, sample_billing_periods_with_o home = engine.Home( sample_summary_inputs, sample_billing_periods_with_outlier, + dhw_input=None, initial_balance_point=58, ) @@ -284,3 +286,62 @@ def test_get_outputs_normalized( assert summary_output.standard_deviation_of_heat_loss_rate == approx( 0.0463, abs=0.01 ) + + +@pytest.mark.parametrize( + "sample_dhw_inputs, summary_input_heating_system_efficiency, expected_fuel_oil_usage", + [ + ( + DhwInput( + number_of_occupants=2, + estimated_water_heating_efficiency=None, + stand_by_losses=None, + ), + 0.80, + 0.17, + ), + ( + DhwInput( + number_of_occupants=2, + estimated_water_heating_efficiency=0.8, + stand_by_losses=None, + ), + 0.85, + 0.17, + ), + ( + DhwInput( + number_of_occupants=4, + estimated_water_heating_efficiency=0.8, + stand_by_losses=None, + ), + 0.84, + 0.35, + ), + ( + DhwInput( + number_of_occupants=5, + estimated_water_heating_efficiency=0.8, + stand_by_losses=None, + ), + 0.83, + 0.43, + ), + ( + DhwInput( + number_of_occupants=5, + estimated_water_heating_efficiency=0.8, + stand_by_losses=0.10, + ), + 0.82, + 0.46, + ), + ], +) +def test_calculate_dhw_usage( + sample_dhw_inputs, summary_input_heating_system_efficiency, expected_fuel_oil_usage +): + fuel_oil_usage = engine.calculate_dhw_usage( + sample_dhw_inputs, summary_input_heating_system_efficiency + ) + assert fuel_oil_usage == approx(expected_fuel_oil_usage, abs=0.01)