import pint import re from model_data.Property import Property import pandas as pd 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], "depth_unit": "mm", "cost": None, "cost_unit": None, # The u-value here is just a placehoder for now and we probably want to have multiple # options for internal wall insulation (e.g. 50mm, 100mm, 150mm), with different material types # and costs "r_value_per_mm": 0.0278, "r_value_unit": "m2K/W" }, { "type": "external_wall_insulation", "description": "Expanded Polystyrene External Wall Insulation", "depths": [], "depth_unit": "mm", "cost": None, # The u-value here is just a placehoder for now and we probably want to have multiple # options for internal wall insulation (e.g. 50mm, 100mm, 150mm), with different material types # and costs "u_value": None }, { "type": "external_wall_insulation", "description": "Phenolic Foam External Wall Insulation", "depths": [], "depth_unit": "mm", "cost": None, # The u-value here is just a placehoder for now and we probably want to have multiple # options for internal wall insulation (e.g. 50mm, 100mm, 150mm), with different material types # and costs "u_value": None }, { "type": "external_wall_insulation", "description": "Polyisocyanurate Foam External Wall Insulation", "depths": [], "depth_unit": "mm", "cost": None, # The u-value here is just a placehoder for now and we probably want to have multiple # options for internal wall insulation (e.g. 50mm, 100mm, 150mm), with different material types # and costs "u_value": None }, { "type": "external_wall_insulation", "description": "Woof Fiber External Wall Insulation", "depths": [], "depth_unit": "mm", "cost": None, # The u-value here is just a placehoder for now and we probably want to have multiple # options for internal wall insulation (e.g. 50mm, 100mm, 150mm), with different material types # and costs "u_value": None }, { "type": "external_wall_insulation", "description": "Aerogel External Wall Insulation", "depths": [], "depth_unit": "mm", "cost": None, # The u-value here is just a placehoder for now and we probably want to have multiple # options for internal wall insulation (e.g. 50mm, 100mm, 150mm), with different material types # and costs "u_value": None }, { "type": "external_wall_insulation", "description": "Vacuum Insulation Panels External Wall Insulation", "depths": [], "depth_unit": "mm", "cost": None, # The u-value here is just a placehoder for now and we probably want to have multiple # options for internal wall insulation (e.g. 50mm, 100mm, 150mm), with different material types # and costs "u_value": None } ] wall_parts = [ { "id": 1, "type": "internal_wall_insulation", "description": "Internal wall insulation", "depth": None, "depth_unit": "mm", "cost": None, # The u-value here is just a placehoder for now and we probably want to have multiple # options for internal wall insulation (e.g. 50mm, 100mm, 150mm), with different material types # and costs "u_value": 0.3 }, ] class WallRecommendations: YEAR_WALLS_BUILT_WITH_INSULATION = 1990 U_VALUE_UNIT = 'w/m-¦k' BUILDING_REGULATIONS_PART_L_MAX_U_VALUE = 0.18 # 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 DEFAULT_U_VALUES = { "solid_brick": 2, } def __init__(self, property_instance: Property): self.property = property_instance self.year_built = self._year_property_was_built() # Will contains a list of recommended measures self.recommendations = [] def _year_property_was_built(self): """ Estimates when the property was built based on as much available data as possible. """ if self.property.full_sap_epc: return pd.to_datetime(self.property.full_sap_epc["lodgement-date"]).year if self.property.data["construction-age-band"]: # Take the upper limit band = [int(x) for x in re.findall(r'\b\d{4}\b', self.property.data["construction-age-band"])] return band[1] raise NotImplementedError("Implement me!") 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"] 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") if (not is_cavity_wall) and (self.year_built >= self.YEAR_WALLS_BUILT_WITH_INSULATION) and ( u_value >= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE ): # Recommend internal wall insulation iwi_parts = [part for part in wall_parts if part["type"] == "internal_wall_insulation"] for part in iwi_parts: _, new_u_value = self.calculate_u_value_uplift(u_value, part["u_value"]) new_u_value = round(new_u_value, 2) # We allow a small tolerance for error so we don't discount the recommendation entirely # if it's close, since this is an estimated new u-value if new_u_value - self.U_VALUE_ERROR <= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE: self.recommendations.append( { **part, "new_u_value": new_u_value, } ) raise NotImplementedError("Not implemented yet") @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. 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 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)