import itertools import math from datatypes.enums import QuantityUnits from backend.Property import Property from model_data.BaseUtility import Definitions from recommendations.recommendation_utils import ( r_value_per_mm_to_u_value, calculate_u_value_uplift, is_diminishing_returns, update_lowest_selected_u_value, get_recommended_part, get_uvalue_estimate, estimate_sap_points ) external_wall_insulation_parts = [ { # Example product # https://insulationgo.co.uk/100mm-rockwool-external-wall-insulation-dual-density-slabs-a1-non-combustible # -slab-ewi-render-fire/ "type": "external_wall_insulation", "description": "Mineral Wool External Wall Insulation", "depths": [30, 50, 70, 80, 90, 100, 150, 200], "depth_unit": "mm", "cost": None, "cost_unit": None, "r_value_per_mm": 0.0278, "r_value_unit": "square_meter_kelvin_per_watt", "thermal_conductivity": 0.036, "thermal_conductivity_unit": "watt_per_meter_kelvin" }, { # Example product # https://www.insulationking.co.uk/products/polystyrene-eps70?variant=44156186558759 "type": "external_wall_insulation", "description": "Expanded Polystyrene External Wall Insulation", "depths": [25, 50, 100, 125], "depth_unit": "mm", "cost": None, "cost_unit": None, "r_value_per_mm": 0.02703, "r_value_unit": "square_meter_kelvin_per_watt", "thermal_conductivity": 0.037, "thermal_conductivity_unit": "watt_per_meter_kelvin" }, { # Example product # https://www.insulationshop.co/20mm_kooltherm_k5_external_wall_kingspan.html "type": "external_wall_insulation", "description": "Phenolic Foam External Wall Insulation", "depths": [20, 50, 100], "depth_unit": "mm", "cost": None, "cost_unit": None, "r_value_per_mm": 0.043478260869565216, "r_value_unit": "square_meter_kelvin_per_watt", "thermal_conductivity": 0.023, "thermal_conductivity_unit": "watt_per_meter_kelvin" }, { "type": "external_wall_insulation", "description": "Polyisocyanurate/Polyurethane Foam External Wall Insulation", "depths": [], "depth_unit": "mm", "cost": None, "cost_unit": None, "r_value_per_mm": None, "r_value_unit": "square_meter_kelvin_per_watt", "thermal_conductivity": None, "thermal_conductivity_unit": "watt_per_meter_kelvin" }, { # Example product # https://www.mikewye.co.uk/product/steico-duo-dry/ "type": "external_wall_insulation", "description": "Wood Fiber External Wall Insulation", "depths": [40, 60], "depth_unit": "mm", "cost": None, "cost_unit": None, "r_value_per_mm": 0.023255813953488375, "r_value_unit": "square_meter_kelvin_per_watt", "thermal_conductivity": 0.043, "thermal_conductivity_unit": "watt_per_meter_kelvin" }, { # Example product # https://www.thermablok.co.uk/site/wp-content/uploads/2022/09/Thermablok-Aerogel-Insulation-Blanket-TDS-AIS # -and-Steel-Related-Details.pdf "type": "external_wall_insulation", "description": "Aerogel External Wall Insulation", "depths": [10, 20, 30, 40, 50, 60, 70], "depth_unit": "mm", "cost": None, "cost_unit": None, "r_value_per_mm": 0.06666666666666667, "r_value_unit": "square_meter_kelvin_per_watt", "thermal_conductivity": 0.015, "thermal_conductivity_unit": "watt_per_meter_kelvin" }, { "type": "external_wall_insulation", "description": "Vacuum Insulation Panels External Wall Insulation", "depths": [45, 60], "depth_unit": "mm", "cost": None, "cost_unit": None, "r_value_per_mm": 0.16666666666666666, "r_value_unit": "square_meter_kelvin_per_watt", "thermal_conductivity": 0.006, "thermal_conductivity_unit": "watt_per_meter_kelvin" } ] internal_wall_insulation_parts = [ { # Example product # https://www.insulationshop.co/25mm_polystyrene_insulation_eps_70jablite.html "type": "internal_wall_insulation", "description": "Rigid Insulation Boards Internal Wall Insulation", "depths": [25, 40, 50, 75, 100], "depth_unit": "mm", "cost": None, "cost_unit": None, "r_value_per_mm": 0.026315789473684213, "r_value_unit": "square_meter_kelvin_per_watt", "thermal_conductivity": 0.038, "thermal_conductivity_unit": "watt_per_meter_kelvin" }, { # Example product # https://www.rockwool.com/siteassets/rw-uk/downloads/datasheets/flexi.pdf "type": "internal_wall_insulation", "description": "Mineral Wool Internal Wall Insulation", "depths": [140], "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" }, { # Example product # https://www.kingspan.com/gb/en/products/insulation-boards/wall-insulation-boards/kooltherm-k118-insulated # -plasterboard/ "type": "internal_wall_insulation", "description": "Insulated Plasterboard Internal Wall Insulation", "depths": [25, 80], "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.019, "thermal_conductivity_unit": "watt_per_meter_kelvin" }, { "type": "internal_wall_insulation", "description": "Reflective Internal Wall Insulation", "depths": [], "depth_unit": "mm", "cost": None, "cost_unit": None, "r_value_per_mm": None, "r_value_unit": "square_meter_kelvin_per_watt", "thermal_conductivity": None, "thermal_conductivity_unit": "watt_per_meter_kelvin" }, { # Example product # https://www.insulationsuperstore.co.uk/product/vacutherm-vacupor-nt-b2-vacuum-insulated-panel-1m-x-600mm-x # -30mm.html "type": "internal_wall_insulation", "description": "Vacuum Insulation Panels Wall Insulation", "depths": [20, 30], "depth_unit": "mm", "cost": None, "cost_unit": None, "r_value_per_mm": 0.125, "r_value_unit": "square_meter_kelvin_per_watt", "thermal_conductivity": 0.008, "thermal_conductivity_unit": "watt_per_meter_kelvin" }, ] wall_parts = external_wall_insulation_parts + internal_wall_insulation_parts class WallRecommendations(Definitions): YEAR_WALLS_BUILT_WITH_INSULATION = 1990 # After 1930, Solid brick walls became less populate and instead, cavity walls became a # more popular choice YEARS_CAVITY_WALLS_BEGAN = 1930 U_VALUE_UNIT = 'w/m-¦k' # part L building regulations indicate that any rennovations on an existing property's walls should # achieve a U-value of no higher than 0.3 BUILDING_REGULATIONS_PART_L_MAX_U_VALUE = 0.3 # We don't recommend measures that are too low because it becomes expensive, therefore we aim to avoid # diminishing returns. This value should be verified with Osmosis (TODO) DIMINISHING_RETURNS_U_VALUE = 0.25 # Part L regulations indicate that any new build should have walls that achieve a u-value of no higher # than 0.18. BUILDING_REGULATIONS_PART_L_NEW_BUILD_MAX_U_VALUE = 0.18 # 0.15 is an often cited diminishing returns value for new builds NEW_BUILD_DIMINISHING_RETURNS_U_VALUE = 0.15 # Add some error so that if, for example, a new part we recommend provides a u-value of 0.19, # we still consider it as an option U_VALUE_ERROR = 0.01 # TODO: Review this value against RdSAP # Page 19 of rdsap here: # https://files.bregroup.com/bre-co-uk-file-library-copy/filelibrary/SAP/2012/RdSAP-9.93/RdSAP_2012_9.93.pdf # provides default U-values for solid brick walls depending on age band DEFAULT_U_VALUES = { "solid_brick": 2, } def __init__(self, property_instance: Property, uvalue_estimates, total_floor_area_group_decile, materials=None): self.property = property_instance self.uvalue_estimates = uvalue_estimates self.total_floor_area_group_decile = total_floor_area_group_decile # For audit purposes, when estimating u values we'll store it self.estimated_u_value = None # Will contains a list of recommended measures self.recommendations = [] if materials: self.materials = materials else: self.materials = wall_parts @property def ewi_valid(self): """ This method check available data, to determine if a property is suitable for external wall insulation """ # Current logic: If the property is in a conservation area or a flat, it is not suitable for EWI if (self.property.in_conservation_area in ["in_conversation_area"]) or \ (self.property.data["property-type"].lower() == "flat"): return False return True def recommend(self): # if building built after 1990 + we're able to identify U-value + # U-value less than 0.18 and if in or close to a conversation area, # recommend internal wall insulation as a possible measure u_value = self.property.walls["thermal_transmittance"] is_cavity_wall = self.property.walls["is_cavity_wall"] is_solid_brick = self.property.walls["is_solid_brick"] insulation_thickness = self.property.walls["insulation_thickness"] if u_value: if self.property.walls["thermal_transmittance_unit"] != self.U_VALUE_UNIT: raise NotImplementedError("Haven't handled the case of other u value units yet") # TODO: It's worth thinking about this logic because depending on when properties were built, # they're likely to be of a certain standard. E.g. properties built within a certain time # period are likely to have cavity walls # We can't detect it's a cavity wall, but it was built after 1990 so likely built with insulation already # + it already has a U-value WORSE than the building regulations, so we recommend either internal or # external wall insulation if (not is_cavity_wall) and (self.property.year_built >= self.YEAR_WALLS_BUILT_WITH_INSULATION) and ( u_value >= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE ): # Recommend insulation self.find_insulation(u_value) return # We can't detect it's a cavity wall, but it was built after 1990 so likely built with insulation already # + it already has a U-value better than the building regulations, so we don't need to recommend anything if (not is_cavity_wall) and (self.property.year_built >= self.YEAR_WALLS_BUILT_WITH_INSULATION) and ( u_value <= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE ): # Recommend nothing return raise NotImplementedError("Not implemented yet") if is_solid_brick: if insulation_thickness == "none": # This is an estimated figure based on industry standards u_value = self.DEFAULT_U_VALUES["solid_brick"] else: u_value = get_uvalue_estimate( uvalue_estimates=self.uvalue_estimates, property=self.property, total_floor_area_group_decile=self.total_floor_area_group_decile ) self.estimated_u_value = u_value if u_value >= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE: self.find_insulation(u_value) return # If the u-value is within regulations, we don't do anything return raise NotImplementedError("Not implemented yet") def _find_insulation(self, parts, u_value): lowest_selected_u_value = None recommendations = [] for part in parts: for depth, cost_per_unit in zip(part["depths"], part["cost"]): 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 I have a lowest U value and my new u value is higher than that but lower than the # diminishing returns threshold, it can be considered # 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 is_diminishing_returns( 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 if new_u_value <= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE: lowest_selected_u_value = update_lowest_selected_u_value(lowest_selected_u_value, new_u_value) estimated_cost = cost_per_unit * self.property.insulation_wall_area recommendations.append( { "parts": [ get_recommended_part( part=part, selected_depth=depth, quantity=self.property.insulation_wall_area, quantity_unit=QuantityUnits.m2.value, selected_total_cost=estimated_cost ) ], "type": "wall_insulation", "description": "Install " + self._make_description(part, depth), "starting_u_value": u_value, "new_u_value": new_u_value, "sap_points": estimate_sap_points(), "cost": estimated_cost, } ) return recommendations def find_insulation(self, u_value): """ This function contains the logic for finding potential insulation measures for a property, depending on the parts available and whether the property can have external wall insulation installed :return: """ ewi_parts = [ part for part in self.materials if part["type"] == "external_wall_insulation" ] if self.ewi_valid else [] iwi_parts = [part for part in self.materials if part["type"] == "internal_wall_insulation"] # Recommend external and internal wall insulation separately # Since external and internal wall insulation are sufficiently different, # we separate the logic for for recommending them, therefore we don't # consider diminishing returns between the two ewi_recommendations = self._find_insulation(ewi_parts, u_value) iwi_recommendations = self._find_insulation(iwi_parts, u_value) self.recommendations += ewi_recommendations + iwi_recommendations # We also can recommend both internal and external wall insulation together # By looping through ewi first, if there is nothing there, that ensures not combinations are tested for ewi_part in ewi_parts: for iwi_part in iwi_parts: for (ewi_depth, ewi_cost_per_unit), (iwi_depth, iwi_cost_per_unit) in itertools.product( zip(ewi_part["depths"], ewi_part["cost"]), zip(iwi_part["depths"], iwi_part["cost"]) ): 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 = 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 = 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: # We don't recommend an overkill solution continue # Check if the combined new U-value meets the requirement if combined_new_u_value - self.U_VALUE_ERROR <= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE: # Here you might want to define a way to add both recommendations together. # For now, I'm adding them as separate items in the list ewi_esimtated_cost = ewi_cost_per_unit * self.property.insulation_wall_area iwi_esimtated_cost = iwi_cost_per_unit * self.property.insulation_wall_area recommendation = { "parts": [ get_recommended_part( part=ewi_part, selected_depth=ewi_depth, quantity=self.property.insulation_wall_area, quantity_unit=QuantityUnits.m2.value, selected_total_cost=ewi_esimtated_cost ), get_recommended_part( part=iwi_part, selected_depth=iwi_depth, quantity=self.property.insulation_wall_area, quantity_unit=QuantityUnits.m2.value, selected_total_cost=iwi_esimtated_cost ) ], "type": "wall_insulation", "description": ( "Install " + self._make_description(ewi_part, ewi_depth) + " and " + self._make_description(iwi_part, iwi_depth) ), "starting_u_value": u_value, "new_u_value": combined_new_u_value, "sap_points": estimate_sap_points(), "cost": ewi_esimtated_cost + iwi_esimtated_cost, } self.recommendations.append(recommendation) self.prune_diminishing_recommendations() @staticmethod def _make_description(part, depth): return f"{depth}{part['depth_unit']} {part['description']}" def prune_diminishing_recommendations(self): # For any recommendations, if we have at least 1 reommendation that does not exhibit diminishing returns # we trim all others that are beyond the diminishing returns threshold # We first check if we have any recommendations that are not diminishing returns not_diminishing_return = [ rec for rec in self.recommendations if rec["new_u_value"] >= self.DIMINISHING_RETURNS_U_VALUE ] if not_diminishing_return: self.recommendations = [ rec for rec in self.recommendations if rec["new_u_value"] >= self.DIMINISHING_RETURNS_U_VALUE ] @staticmethod def rvalue_per_mm(total_r_value: float, thickness_mm: float) -> float: """Return R-value per mm. Parameters ---------- total_r_value : float Total R-value (in m2K/W). thickness_mm : float Thickness of the material in mm. Returns ------- float R-value per mm. """ return total_r_value / thickness_mm @staticmethod def thermal_conductivity_to_r_value_per_mm(thermal_conductivity: float) -> float: """Convert thermal conductivity to R-value per mm. Parameters ---------- thermal_conductivity : float Thermal conductivity (in W/mK). Returns ------- float R-value per mm. """ # Calculate R-value in m²K/W for 1 meter of the material r_value_per_meter = 1 / thermal_conductivity # Convert R-value to R-value per mm r_value_per_mm = r_value_per_meter / 1000 return r_value_per_mm