import math from typing import List from model_data.BaseUtility import Definitions from datatypes.enums import QuantityUnits from backend.Property import Property from recommendations.rdsap_tables import default_wall_thickness, age_band_data 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 ) 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 data_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 data_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" }, ] solid_floor_insulation_parts = [ { # Example product # https://www.insulationexpress.co.uk/floor-insulation/solid-floor-insulation/k103-100mm # All product data_types here: # https://www.insulationexpress.co.uk/floor-insulation/solid-floor-insulation?brand=7015&p=1 # Example screed https://www.screwfix.com/p/mapei-ultraplan-3240-self-levelling-compound-25kg/4959f "type": "solid_floor_insulation", "description": "Rigid Insulation Foam Boards with floor screed", "depths": [25, 50, 70, 75, 100], "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.052631578947368425, "thermal_conductivity_unit": "watt_per_meter_kelvin" }, ] parts = suspended_floor_insulation_parts + solid_floor_insulation_parts class FloorRecommendations(Definitions): # 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.25 # 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.2 REGION_LOOKUP = { "England and Wales": "England_Wales", } PART_L_YEAR_CUTOFF = 2002 # TODO: This is a placeholder methodology which isn't particularly scalable as more # unusual floor descriptions are introduced FLOOR_LEVELS = { "Ground": 0, # We don't know what floor level, we just make sure it's not 0 "mid floor": 1, "4th": 4, # We set "00": 0, "3rd": 3 } def __init__( self, property_instance: Property, uvalue_estimates: List, total_floor_area_group_decile: str, materials: List = 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 = parts self.suspended_floor_insulation_parts = [ part for part in self.materials if part["type"] == "suspended_floor_insulation" ] self.solid_floor_insulation_parts = [ part for part in self.materials if part["type"] == "solid_floor_insulation" ] @staticmethod def _estimate_perimeter(floor_area, num_rooms): # Compute average room size based on total floor area and number of rooms avg_room_size = floor_area / num_rooms # Estimate total side length for square layout total_side_length = math.sqrt(avg_room_size * num_rooms) # Compute the perimeter perimeter = total_side_length * 4 return perimeter def _estimate_suspended_floor_u_value( self, floor_area, number_of_rooms, insulation_thickness, wall_type, region, age_band ): """ Estimate the u-value of a suspended floor, based on RdSap methodology Default U-value for UNINSULATED suspended floor, based on RdSAP methodology https://files.bregroup.com/bre-co-uk-file-library-copy/filelibrary/SAP/2012/RdSAP-9.93/RdSAP_2012_9.93.pdf w = wall thickness, where these estimates are based on the RD SAP methodology, as in table S3 A = floor area Exposed perimeter = P soil type clas thermal conductivity lambda_g = 1.5 W/mK Rsi = 0.17m^2K/W Rse = 0.04m^2K/W Rf = 0.001 * d_ins / 0.035 where d_ins is the insulation thickness in mm height above external ground h = 0.3m average wind speed at 10m height v=5m/s wind sheilding factor fw = 0.05 vantilation factor E = 0.003 m^2/m U-value of walls to underfloor space Uw = 1.5 W/m^2K # Calulations for suspended ground floors, example for 5 bedroom house with permiter estimated at 44.36214602563767 1) dg = w + lambda_g x (Rsi + Rse) = 0.5 + 1.5 * (0.17 + 0.04) = 0.615 2) B = 2 * A/P = 2 * 123.0 / 44.36214602563767 = 5.545268253204708 3) Ug = 2 * lambda_g * log(pi * B/dg + 1)/(pi * B + dg) = 2 * 1.5 * log(3.141592653589793 * 5.545268253204708/0.615 + 1) / (3.141592653589793 * 5.545268253204708 + 0.615) = 0.5619604457160708 4) Ux = (2 * h * Uw /B) + (1450 * E * v * fw/B) = (2 * 0.3 * 1.5 / 5.545268253204708) + (1450 * 0.003 * 5 * 0.05/5.545268253204708) = 0.35841367978030436 5) U = 1/ (2 * Rsi + Rf + 1/(Ug + Ux)) = 1 / (2 * 0.17 + 0 + 1/(0.5619604457160708 + 0.35841367978030436)) = 0.701 """ age_band_letter = [x for x in age_band_data if x[region] == age_band][0]["age_band"] defaults = { # We need width in meters "w": [x[age_band_letter] for x in default_wall_thickness if x["type"] == wall_type][0] / 1000, "lambda_g": 1.5, "Rsi": 0.17, "Rse": 0.04, "Rf": 0.001 * insulation_thickness / 0.035, "h": 0.3, "v": 5, "fw": 0.05, "E": 0.003, "Uw": 1.5, } dg = defaults["w"] + defaults["lambda_g"] * (defaults["Rsi"] + defaults["Rse"]) # P is the exposed perimeter, which we estimate as we not have this data p = self._estimate_perimeter(floor_area=floor_area, num_rooms=number_of_rooms) b = 2 * floor_area / p u_g = 2 * defaults["lambda_g"] * math.log(math.pi * b / dg + 1) / (math.pi * b + dg) u_x = (2 * defaults["h"] * defaults["Uw"] / b) + (1450 * defaults["E"] * defaults["v"] * defaults["fw"] / b) # This is the final estimated U-value u = 1 / (2 * defaults["Rsi"] + defaults["Rf"] + 1 / (u_g + u_x)) return u def recommend(self): u_value = self.property.floor["thermal_transmittance"] is_suspended = self.property.floor["is_suspended"] insulation_thickness = self.property.floor["insulation_thickness"] is_solid = self.property.floor["is_solid"] floor_level = ( self.FLOOR_LEVELS[self.property.data["floor-level"]] if self.property.data["floor-level"] not in self.DATA_ANOMALY_MATCHES else None ) property_type = self.property.data["property-type"] year_built = self.property.year_built if self.property.floor["another_property_below"]: # If there's another property below, it's likely impractical to recommend a floor upgrade return # If the property is a flat that isn't at ground level, it's likely impractical to recommend a floor upgrade if (floor_level != 0) and (property_type == "Flat"): return if u_value: if self.property.data["property-type"] != "House": raise NotImplementedError("Implement me") # By being built more recently than this, it means that the property was likely build with soild # concrete floors with insulation already if year_built < self.PART_L_YEAR_CUTOFF: raise NotImplementedError("Not investigated this use case") if u_value <= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE: # The floor is already compliant return # For these methods, we need to know the additional details about the property if self.property.walls["is_solid_brick"]: wall_type = "solid brick" else: raise NotImplementedError("Implement me") 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) elif self.property.data["property-type"] == "Flat": num_floors = 1 else: raise NotImplementedError("Implement me") if insulation_thickness == "none": region_str, age_band = self.property.data["construction-age-band"].split(":") region_str = region_str.strip() age_band = age_band.strip() region = self.REGION_LOOKUP[region_str] 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=wall_type, region=region, age_band=age_band, ) 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 is_suspended: # Given the U-value, we recommend underfloor insulation self.recommend_floor_insulation(u_value=u_value, parts=self.suspended_floor_insulation_parts) if is_solid: # Given the U-value, we recommend solid floor insulation options which are usually solid foam self.recommend_floor_insulation(u_value=u_value, parts=self.solid_floor_insulation_parts) @staticmethod def _make_floor_description(part, depth): return f"Install {depth}{part['depth_unit']} {part['description']} insulation" def recommend_floor_insulation(self, u_value, parts): """ This method is tasked with estimating the impact of performing suspended floor insulation :return: """ lowest_selected_u_value = None 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 is_diminishing_returns( self.recommendations, new_u_value, lowest_selected_u_value, self.DIMINISHING_RETURNS_U_VALUE ): continue 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.floor_area self.recommendations.append( { "parts": [ get_recommended_part( part=part, selected_depth=depth, quantity=self.property.floor_area, quantity_unit=QuantityUnits.m2.value, selected_total_cost=estimated_cost ), ], "type": "floor_insulation", "description": self._make_floor_description(part, depth), "starting_u_value": u_value, "new_u_value": new_u_value, "sap_points": estimate_sap_points(), "cost": estimated_cost, } ) @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