From 2d52d3942a92770188a91f364455d6d5708c5bfe Mon Sep 17 00:00:00 2001 From: Jonathan Kwan Date: Wed, 28 Feb 2024 01:15:10 +0000 Subject: [PATCH 1/3] Implemented calculate_boiler_usage for fuel oil Co-authored-by: thatoldplatitude Co-authored-by: dwindleduck Co-authored-by: Alan Pinkert Co-authored-by: Debajyoti Debnath Co-authored-by: David Egan Co-authored-by: Chad Stoughton --- rules-engine/src/rules_engine/engine.py | 48 +++++++++++-------- .../src/rules_engine/pydantic_models.py | 13 +++-- .../tests/test_rules_engine/test_engine.py | 1 + 3 files changed, 40 insertions(+), 22 deletions(-) diff --git a/rules-engine/src/rules_engine/engine.py b/rules-engine/src/rules_engine/engine.py index 0f9fa9a4..e9af39da 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() @@ -291,16 +290,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,18 +332,33 @@ 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, dhw_input: DhwInput, heating_system_efficiency: float + ) -> float: """ Calculate boiler usage with oil or propane - Args: - fuel_multiplier: a constant that's determined by the fuel - type """ + 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)) + ) - # 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 + return daily_fuel_oil_use_for_dhw def _calculate_avg_non_heating_usage(self) -> None: """ @@ -355,11 +367,9 @@ 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 = self._calculate_boiler_usage(self.dhw_input) 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..30ea98db 100644 --- a/rules-engine/tests/test_rules_engine/test_engine.py +++ b/rules-engine/tests/test_rules_engine/test_engine.py @@ -15,6 +15,7 @@ SummaryInput, SummaryOutput, TemperatureInput, + DhwInput, ) From ae5fc7793f9bf204aebf3ad7153cbc2063b4f951 Mon Sep 17 00:00:00 2001 From: Jonathan Kwan Date: Wed, 28 Feb 2024 01:50:02 +0000 Subject: [PATCH 2/3] Renamed _calculate_boiler_usage to calculate_dhw_usage Added unit test for calculate_dhw_usage --- rules-engine/src/rules_engine/engine.py | 58 +++++++++---------- .../tests/test_rules_engine/test_engine.py | 16 +++++ 2 files changed, 45 insertions(+), 29 deletions(-) diff --git a/rules-engine/src/rules_engine/engine.py b/rules-engine/src/rules_engine/engine.py index e9af39da..475a2bfc 100644 --- a/rules-engine/src/rules_engine/engine.py +++ b/rules-engine/src/rules_engine/engine.py @@ -275,6 +275,34 @@ 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: """ @@ -332,34 +360,6 @@ def _calculate_avg_summer_usage(self) -> None: else: self.avg_summer_usage = 0 - def _calculate_boiler_usage( - self, dhw_input: DhwInput, heating_system_efficiency: float - ) -> float: - """ - Calculate boiler 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 - def _calculate_avg_non_heating_usage(self) -> None: """ Calculate avg non heating usage for this home @@ -369,7 +369,7 @@ def _calculate_avg_non_heating_usage(self) -> None: self.avg_non_heating_usage = self.avg_summer_usage 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 = self._calculate_boiler_usage(self.dhw_input) + self.avg_non_heating_usage = self._calculate_dhw_usage(self.dhw_input) else: self.avg_non_heating_usage = 0 diff --git a/rules-engine/tests/test_rules_engine/test_engine.py b/rules-engine/tests/test_rules_engine/test_engine.py index 30ea98db..edd04e9d 100644 --- a/rules-engine/tests/test_rules_engine/test_engine.py +++ b/rules-engine/tests/test_rules_engine/test_engine.py @@ -211,6 +211,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, ) @@ -230,6 +231,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, ) @@ -285,3 +287,17 @@ 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) From a7626010204b05cc5406b2346a5214972d3e9354 Mon Sep 17 00:00:00 2001 From: Jonathan Kwan Date: Wed, 28 Feb 2024 01:55:06 +0000 Subject: [PATCH 3/3] Fixed static checks --- rules-engine/src/rules_engine/engine.py | 14 ++--- .../tests/test_rules_engine/test_engine.py | 60 ++++++++++++++++--- 2 files changed, 58 insertions(+), 16 deletions(-) diff --git a/rules-engine/src/rules_engine/engine.py b/rules-engine/src/rules_engine/engine.py index 475a2bfc..3bc8b6bb 100644 --- a/rules-engine/src/rules_engine/engine.py +++ b/rules-engine/src/rules_engine/engine.py @@ -275,9 +275,8 @@ def get_maximum_heat_load( """ return (design_set_point - design_temp) * ua -def calculate_dhw_usage( - dhw_input: DhwInput, heating_system_efficiency: float -) -> float: + +def calculate_dhw_usage(dhw_input: DhwInput, heating_system_efficiency: float) -> float: """ Calculate non-heating usage with oil or propane """ @@ -292,10 +291,7 @@ def calculate_dhw_usage( dhw_input.number_of_occupants * Constants.DAILY_DHW_CONSUMPTION_PER_OCCUPANT * Constants.WATER_WEIGHT - * ( - Constants.LEAVING_WATER_TEMPERATURE - - Constants.ENTERING_WATER_TEMPERATURE - ) + * (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)) @@ -369,7 +365,9 @@ def _calculate_avg_non_heating_usage(self) -> None: self.avg_non_heating_usage = self.avg_summer_usage 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 = self._calculate_dhw_usage(self.dhw_input) + 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/tests/test_rules_engine/test_engine.py b/rules-engine/tests/test_rules_engine/test_engine.py index edd04e9d..723bff83 100644 --- a/rules-engine/tests/test_rules_engine/test_engine.py +++ b/rules-engine/tests/test_rules_engine/test_engine.py @@ -15,7 +15,6 @@ SummaryInput, SummaryOutput, TemperatureInput, - DhwInput, ) @@ -288,16 +287,61 @@ def test_get_outputs_normalized( 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), + ( + 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) +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)