From 8976acefdf9378f111fad0beadaf8ffd11640c5f Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 26 Jun 2023 16:01:49 +0100 Subject: [PATCH] created rcommendation utils, floor recommendations wip --- .../recommendations/FloorRecommendations.py | 98 ++++++++++++++++- .../recommendations/WallRecommendations.py | 102 ++---------------- .../recommendations/recommendation_utils.py | 80 ++++++++++++++ 3 files changed, 185 insertions(+), 95 deletions(-) create mode 100644 model_data/recommendations/recommendation_utils.py diff --git a/model_data/recommendations/FloorRecommendations.py b/model_data/recommendations/FloorRecommendations.py index a54175da..0604daa4 100644 --- a/model_data/recommendations/FloorRecommendations.py +++ b/model_data/recommendations/FloorRecommendations.py @@ -3,6 +3,47 @@ from model_data.BaseUtility import BaseUtility from model_data.Property import Property from model_data.analysis.UvalueEstimations import UvalueEstimations from model_data.rdsap_tables import default_wall_thickness, age_band_data +from model_data.recommendations.recommendation_utils import r_value_per_mm_to_u_value, calculate_u_value_uplift, \ + is_diminishing_returns + +suspended_floor_insulation_parts = [ + { + # Example product + # https://www.insulationsuperstore.co.uk/product/recticel-eurothane-general-purpose-pir-insulation-board-2400 + # -x-1200-x-100mm.html + # All product types here: + # https://www.insulationsuperstore.co.uk/browse/insulation/brand/recticel/filterby/application/floors.html + "type": "suspended_floor_insulation", + "description": "Rigid Insulation Foam Boards", + "depths": [25, 30, 40, 50, 60, 70, 75, 80, 90, 100, 110, 120, 130, 140, 150], + "depth_unit": "mm", + "cost": None, + "cost_unit": None, + "r_value_per_mm": 0.04545454545454546, + "r_value_unit": "square_meter_kelvin_per_watt", + "thermal_conductivity": 0.022, + "thermal_conductivity_unit": "watt_per_meter_kelvin" + }, + { + # Example product + # https://www.insulationsuperstore.co.uk/product/rockwool-rwa45-acoustic-insulation-slab-100mm-2-88m2-pack.html + # All product types here: + # https://www.insulationsuperstore.co.uk/browse/insulation/brand/rockwool/filterby/application/floors + # /material/mineral-wool.html + "type": "suspended_floor_insulation", + "description": "Mineral Wool Floor Insulation", + "depths": [25, 40, 50, 60, 75, 100], + "depth_unit": "mm", + "cost": None, + "cost_unit": None, + "r_value_per_mm": 0.02857142857142857, + "r_value_unit": "square_meter_kelvin_per_watt", + "thermal_conductivity": 0.035, + "thermal_conductivity_unit": "watt_per_meter_kelvin" + }, +] + +parts = suspended_floor_insulation_parts class FloorRecommendations(BaseUtility): @@ -103,6 +144,7 @@ class FloorRecommendations(BaseUtility): def recommend(self): is_suspended = self.property.floor["is_suspended"] insulation_thickness = self.property.floor["insulation_thickness"] + # Check which floor the property is on self.property.year_built @@ -116,6 +158,15 @@ class FloorRecommendations(BaseUtility): return if is_suspended: + + total_floor_area = float(self.property.data["total-floor-area"]) + number_of_rooms = float(self.property.data["number-habitable-rooms"]) + + if self.property.data["property-type"] == "House": + num_floors = self._estimate_floors(total_floor_area, number_of_rooms) + else: + raise NotImplementedError("Implement me") + if insulation_thickness == "none": region_str, age_band = self.property.data["construction-age-band"].split(":") @@ -123,16 +174,18 @@ class FloorRecommendations(BaseUtility): age_band = age_band.strip() region = self.REGION_LOOKUP[region_str] - uvalue = self._estimate_suspended_floor_u_value( - floor_area=float(self.property.data["total-floor-area"]), - number_of_rooms=float(self.property.data["number-habitable-rooms"]), + u_value = self._estimate_suspended_floor_u_value( + floor_area=total_floor_area / num_floors, + number_of_rooms=number_of_rooms / num_floors, insulation_thickness=0, wall_type='solid brick', region=region, age_band=age_band, ) else: - uvalue = self._get_floors_uvalue_estimate() + u_value = self._get_floors_uvalue_estimate() + + # Given the U-value, we recommend underfloor insulation def _get_floors_uvalue_estimate(self): @@ -181,3 +234,40 @@ class FloorRecommendations(BaseUtility): # average return u_value_estimate["median_thermal_transmittance"].mean() + + def recommend_suspended_floor_insulation(self, u_value): + """ + This method is taskes with estimating the impact of performing suspended floor insulation + :return: + """ + + lowest_selected_u_value = None + for part in parts: + for depth in part["depths"]: + part_u_value = r_value_per_mm_to_u_value(depth, part["r_value_per_mm"]) + _, new_u_value = calculate_u_value_uplift(u_value, part_u_value) + new_u_value = math.ceil(new_u_value * 100.0) / 100.0 + + if is_diminishing_returns( + self.recommendations, new_u_value, lowest_selected_u_value, self.DIMINISHING_RETURNS_U_VALUE + ): + continue + + @staticmethod + def _estimate_floors(floor_area, num_rooms): + """ + Simple utility funciton, which assuming a 15m squared room, estimates the number of floors in a property + :param floor_area: Gross floor area of a property + :param num_rooms: Number of rooms in a property + :return: Number of floors in a property + """ + # Estimate total room area + total_room_area = num_rooms * 15 + + # Estimate the number of floors + floors = floor_area / total_room_area + + # Round up to the nearest whole number + floors = round(floors) + + return floors diff --git a/model_data/recommendations/WallRecommendations.py b/model_data/recommendations/WallRecommendations.py index e1c05f5c..b87e7184 100644 --- a/model_data/recommendations/WallRecommendations.py +++ b/model_data/recommendations/WallRecommendations.py @@ -1,13 +1,12 @@ -import re import itertools import math from model_data.Property import Property -from model_data.ConservationAreaClient import ConservationAreaClient from model_data.analysis.UvalueEstimations import UvalueEstimations from model_data.BaseUtility import BaseUtility -import pandas as pd from copy import deepcopy +from model_data.recommendations.recommendation_utils import r_value_per_mm_to_u_value, calculate_u_value_uplift, \ + is_diminishing_returns external_wall_insulation_parts = [ { @@ -293,38 +292,6 @@ class WallRecommendations(BaseUtility): raise NotImplementedError("Not implemented yet") - def _is_diminishing_returns(self, new_u_value, lowest_selected_u_value): - """ - What are defines diminishing returns? - 1) The new u value is lower than the lowest selected u value - 2) The new u value is below the diminishing returns threshold - 3) We already have some recommendations so there is no need to - insert another recommendation in - """ - - # if we don't have anything selected, lowest_selected_u_value will be missing - if lowest_selected_u_value is None: - if self.recommendations: - raise ValueError("Recommendations should be empty - investigate") - # This means that nothing has been selected yet - # the new u value is less than the threshold, however this MIGHT be the only - # solution and so we consider it - return False - - # We should already have recommendations - if not self.recommendations: - raise ValueError("Recommendations should not be empty - investigate") - - # We already have a solution that is suitable so we want to make sure that - # any new solutin actually has a higher u-value as it will either be - # 1) cheaper - # 2) thinner with a more efficient material - is_diminishing = (new_u_value < self.DIMINISHING_RETURNS_U_VALUE) and ( - new_u_value < lowest_selected_u_value - ) - - return is_diminishing - def find_insulation(self, u_value): """ This function contains the logic for finding potential insulation measures for a property, depending @@ -344,9 +311,9 @@ class WallRecommendations(BaseUtility): for part in ewi_parts + iwi_parts: for depth in part["depths"]: - part_u_value = self.r_value_per_mm_to_u_value(depth, part["r_value_per_mm"]) + part_u_value = r_value_per_mm_to_u_value(depth, part["r_value_per_mm"]) - _, new_u_value = self.calculate_u_value_uplift(u_value, part_u_value) + _, new_u_value = calculate_u_value_uplift(u_value, part_u_value) new_u_value = math.ceil(new_u_value * 100.0) / 100.0 # If I have a lowest U value and my new u value is higher than that but lower than the @@ -355,7 +322,9 @@ class WallRecommendations(BaseUtility): # If I have a lowest U value and my new u value is lower than the lowest value, it's # further into the diminishing returns threshold and can shouldn't be - if self._is_diminishing_returns(new_u_value, lowest_selected_u_value): + if is_diminishing_returns( + self.recommendations, new_u_value, lowest_selected_u_value, self.DIMINISHING_RETURNS_U_VALUE + ): continue # We allow a small tolerance for error so we don't discount the recommendation entirely @@ -376,13 +345,13 @@ class WallRecommendations(BaseUtility): for ewi_part in ewi_parts: for iwi_part in iwi_parts: for ewi_depth, iwi_depth in itertools.product(ewi_part["depths"], iwi_part["depths"]): - ewi_part_u_value = self.r_value_per_mm_to_u_value(ewi_depth, ewi_part["r_value_per_mm"]) - iwi_part_u_value = self.r_value_per_mm_to_u_value(iwi_depth, iwi_part["r_value_per_mm"]) + ewi_part_u_value = r_value_per_mm_to_u_value(ewi_depth, ewi_part["r_value_per_mm"]) + iwi_part_u_value = r_value_per_mm_to_u_value(iwi_depth, iwi_part["r_value_per_mm"]) # First calculate the new U-value after applying external wall insulation - _, ewi_new_u_value = self.calculate_u_value_uplift(u_value, ewi_part_u_value) + _, ewi_new_u_value = calculate_u_value_uplift(u_value, ewi_part_u_value) # Then calculate the new U-value after applying internal wall insulation - _, combined_new_u_value = self.calculate_u_value_uplift(ewi_new_u_value, iwi_part_u_value) + _, combined_new_u_value = calculate_u_value_uplift(ewi_new_u_value, iwi_part_u_value) combined_new_u_value = round(combined_new_u_value, 2) if combined_new_u_value < self.DIMINISHING_RETURNS_U_VALUE: @@ -479,36 +448,6 @@ class WallRecommendations(BaseUtility): **recommended_part, "new_u_value": new_u_value, } - @staticmethod - def calculate_u_value_uplift(u_value, insulation_u_value): - """ - Calculates the U-value uplift (improvement) when applying internal wall insulation to a wall. - - - :param u_value: Float, Starting U-value of the wall (without insulation) in W/m²K. - :param insulation_u_value: Float, U-value of the internal wall insulation in W/m²K. - - Returns: - float: U-value uplift (improvement) achieved by applying internal wall insulation in W/m²K. - - Raises: - ZeroDivisionError: If either u_value or iwi_u_value is zero. - - Notes: - This function assumes 100% coverage of the internal wall insulation and does not account for other factors - such as thermal bridging or the specific configuration of the wall. - """ - - inverse_u_value = 1 / u_value - inverse_insulation_u_value = 1 / insulation_u_value - - inverse_u_total = inverse_u_value + inverse_insulation_u_value - new_u_value = 1 / inverse_u_total - - u_value_uplift = u_value - new_u_value - - return u_value_uplift, new_u_value - @staticmethod def rvalue_per_mm(total_r_value: float, thickness_mm: float) -> float: """Return R-value per mm. @@ -527,25 +466,6 @@ class WallRecommendations(BaseUtility): """ return total_r_value / thickness_mm - @staticmethod - def r_value_per_mm_to_u_value(depth_mm: int, r_value_per_mm: float): - """ - Converts R-value per mm to U-value in W/m²K. - - Parameters - ---------- - depth_mm : int - Depth of the material in mm. - r_value_per_mm : float - R-value per mm. - - Returns - ------- - float - U-value in W/m²K. - """ - return 1 / (depth_mm * r_value_per_mm) - @staticmethod def thermal_conductivity_to_r_value_per_mm(thermal_conductivity: float) -> float: """Convert thermal conductivity to R-value per mm. diff --git a/model_data/recommendations/recommendation_utils.py b/model_data/recommendations/recommendation_utils.py new file mode 100644 index 00000000..d8b51e6c --- /dev/null +++ b/model_data/recommendations/recommendation_utils.py @@ -0,0 +1,80 @@ +def r_value_per_mm_to_u_value(depth_mm: int, r_value_per_mm: float): + """ + Converts R-value per mm to U-value in W/m²K. + + Parameters + ---------- + depth_mm : int + Depth of the material in mm. + r_value_per_mm : float + R-value per mm. + + Returns + ------- + float + U-value in W/m²K. + """ + return 1 / (depth_mm * r_value_per_mm) + + +def calculate_u_value_uplift(u_value, insulation_u_value): + """ + Calculates the U-value uplift (improvement) when applying internal wall insulation to a wall. + + + :param u_value: Float, Starting U-value of the wall (without insulation) in W/m²K. + :param insulation_u_value: Float, U-value of the internal wall insulation in W/m²K. + + Returns: + float: U-value uplift (improvement) achieved by applying internal wall insulation in W/m²K. + + Raises: + ZeroDivisionError: If either u_value or iwi_u_value is zero. + + Notes: + This function assumes 100% coverage of the internal wall insulation and does not account for other factors + such as thermal bridging or the specific configuration of the wall. + """ + + inverse_u_value = 1 / u_value + inverse_insulation_u_value = 1 / insulation_u_value + + inverse_u_total = inverse_u_value + inverse_insulation_u_value + new_u_value = 1 / inverse_u_total + + u_value_uplift = u_value - new_u_value + + return u_value_uplift, new_u_value + + +def is_diminishing_returns(recommendations, new_u_value, lowest_selected_u_value, diminishing_returns_u_value): + """ + What are defines diminishing returns? + 1) The new u value is lower than the lowest selected u value + 2) The new u value is below the diminishing returns threshold + 3) We already have some recommendations so there is no need to + insert another recommendation in + """ + + # if we don't have anything selected, lowest_selected_u_value will be missing + if lowest_selected_u_value is None: + if recommendations: + raise ValueError("Recommendations should be empty - investigate") + # This means that nothing has been selected yet + # the new u value is less than the threshold, however this MIGHT be the only + # solution and so we consider it + return False + + # We should already have recommendations + if not recommendations: + raise ValueError("Recommendations should not be empty - investigate") + + # We already have a solution that is suitable so we want to make sure that + # any new solutin actually has a higher u-value as it will either be + # 1) cheaper + # 2) thinner with a more efficient material + is_diminishing = (new_u_value < diminishing_returns_u_value) and ( + new_u_value < lowest_selected_u_value + ) + + return is_diminishing