import math 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 class FloorRecommendations(BaseUtility): # 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", } def __init__(self, property_instance: Property, uvalue_estimates: UvalueEstimations): self.property = property_instance self.uvalue_estimates = uvalue_estimates # 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 = [] @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): 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 self.property.data["floor-energy-eff"] self.property.data["floor-env-eff"] # TODO: We neeed to know if the property is ground floor or not if self.property.floor["another_property_below"]: # If there's another property below, it's likely impractical to recommend a floor upgrade return if is_suspended: 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] 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"]), insulation_thickness=0, wall_type='solid brick', region=region, age_band=age_band, ) else: uvalue = self._get_floors_uvalue_estimate() def _get_floors_uvalue_estimate(self): """ Wrapper function which contains the methodology to extract a property's walls u-value estimate when we don't have a true value and if we can't base our assumption off of the material :return: """ total_floor_area_group_decile = self.uvalue_estimates.classify_decile_newvalues( decile_boundaries=self.uvalue_estimates.floors_decile_data["decile_boundaries"], decile_labels=self.uvalue_estimates.floors_decile_data["decile_labels"], new_values=[float(self.property.data["total-floor-area"])], )[0] u_value_estimate = self.uvalue_estimates.floors[ (self.uvalue_estimates.floors["local-authority"] == self.property.data["local-authority"]) & (self.uvalue_estimates.floors["property-type"] == self.property.data["property-type"]) & (self.uvalue_estimates.floors["built-form"] == self.property.data["built-form"]) & (self.uvalue_estimates.floors["floor-energy-eff"] == self.property.data["floor-energy-eff"]) & (self.uvalue_estimates.floors["floor-env-eff"] == self.property.data["floor-env-eff"]) & (self.uvalue_estimates.floors["total-floor-area_group"] == total_floor_area_group_decile) ] if u_value_estimate.empty: raise ValueError("No U-value estimate found for the given property") # Because of how spuriously populated the data is for number-habitable-rooms and number-heated-rooms, # we will try and filter on these to see if we get a result habitable_rooms_filter = ( self.uvalue_estimates.walls["number-habitable-rooms"] == self.property.data["number-habitable-rooms"] ) if any(habitable_rooms_filter): u_value_estimate = u_value_estimate[habitable_rooms_filter] heated_rooms_filter = ( self.uvalue_estimates.walls["number-heated-rooms"] == self.property.data["number-heated-rooms"] ) if any(heated_rooms_filter): u_value_estimate = u_value_estimate[heated_rooms_filter] # It's possible for us to have multiple rows if we didn't do a habitable/heated rooms filter so we # average return u_value_estimate["median_thermal_transmittance"].mean()