diff --git a/model_data/analysis/UvalueEstimations.py b/model_data/analysis/UvalueEstimations.py index 551e60b2..ff39ae7b 100644 --- a/model_data/analysis/UvalueEstimations.py +++ b/model_data/analysis/UvalueEstimations.py @@ -15,6 +15,7 @@ class UvalueEstimations: self.walls_decile_data = {} self.roofs = None self.floors = None + self.floors_decile_data = {} def get_estimates(self, cleaner: EpcClean): """ @@ -150,6 +151,10 @@ class UvalueEstimations: ] self.floors = u_value_summary + self.floors_decile_data = { + "decile_labels": decile_labels, + "decile_boundaries": decile_boundaries + } @staticmethod def classify_into_deciles(df: pd.DataFrame, column: str) -> (pd.DataFrame, list, list): diff --git a/model_data/app.py b/model_data/app.py index b804745b..0a8a6aaa 100644 --- a/model_data/app.py +++ b/model_data/app.py @@ -128,6 +128,13 @@ def handler(): floors_df["address1"].values[2] floors_df["original_description"].values[2] + df = pd.DataFrame( + [ + x.data for x in input_properties + ] + ) + df["property-type"].unique() + from model_data.recommendations.FloorRecommendations import FloorRecommendations self = FloorRecommendations(property_instance=input_properties[2], uvalue_estimates=uvalue_estimates) diff --git a/model_data/rdsap_tables.py b/model_data/rdsap_tables.py new file mode 100644 index 00000000..eeacb935 --- /dev/null +++ b/model_data/rdsap_tables.py @@ -0,0 +1,123 @@ +""" +This script contains standard tables which are defined in rdsap. The most recent version of sap/rdsap is +based on the 2012 version, however the government is currently working on releasing a new version, and there +we will need to re-visit this +""" + +age_band_data = [ + { + "age_band": "A", + "England_Wales": "before 1900", + "Scotland": "before 1919", + "Northern_Ireland": "before 1919", + "Park_home_UK": None + }, + { + "age_band": "B", + "England_Wales": "1900-1929", + "Scotland": "1919-1929", + "Northern_Ireland": "1919-1929", + "Park_home_UK": None + }, + { + "age_band": "C", + "England_Wales": "1930-1949", + "Scotland": "1930-1949", + "Northern_Ireland": "1930-1949", + "Park_home_UK": None + }, + { + "age_band": "D", + "England_Wales": "1950-1966", + "Scotland": "1950-1964", + "Northern_Ireland": "1950-1973", + "Park_home_UK": None + }, + { + "age_band": "E", + "England_Wales": "1967-1975", + "Scotland": "1965-1975", + "Northern_Ireland": "1974-1977", + "Park_home_UK": None + }, + { + "age_band": "F", + "England_Wales": "1976-1982", + "Scotland": "1976-1983", + "Northern_Ireland": "1978-1985", + "Park_home_UK": "before 1983" + }, + { + "age_band": "G", + "England_Wales": "1983-1990", + "Scotland": "1984-1991", + "Northern_Ireland": "1986-1991", + "Park_home_UK": "1983-1995" + }, + { + "age_band": "H", + "England_Wales": "1991-1995", + "Scotland": "1992-1998", + "Northern_Ireland": "1992-1999", + "Park_home_UK": None + }, + { + "age_band": "I", + "England_Wales": "1996-2002", + "Scotland": "1999-2002", + "Northern_Ireland": "2000-2006", + "Park_home_UK": "1996-2005" + }, + { + "age_band": "J", + "England_Wales": "2003-2006", + "Scotland": "2003-2007", + "Northern_Ireland": None, + "Park_home_UK": None + }, + { + "age_band": "K", + "England_Wales": "2007-2011", + "Scotland": "2008-2011", + "Northern_Ireland": "2007-2013", + "Park_home_UK": "2006 onwards" + }, + { + "age_band": "L", + "England_Wales": "2012 onwards", + "Scotland": "2012 onwards", + "Northern_Ireland": "2014 onwards", + "Park_home_UK": None + }, +] + +default_wall_thickness = [ + { + "type": "stone", "A": 500, "B": 500, "C": 500, "D": 500, "E": 450, "F": 420, "G": 420, "H": 420, + "I": 450, "J_K_L": 450 + }, + { + "type": "solid brick", "A": 220, "B": 220, "C": 220, "D": 220, "E": 240, "F": 250, "G": 270, "H": 270, + "I": 300, "J_K_L": 300 + }, + { + "type": "cavity", "A": 250, "B": 250, "C": 250, "D": 250, "E": 250, "F": 260, "G": 270, "H": 270, + "I": 300, "J_K_L": 300 + }, + { + "type": "timber frame", "A": 150, "B": 150, "C": 150, "D": 250, "E": 270, "F": 270, "G": 270, "H": 270, + "I": 300, "J_K_L": 300 + }, + { + "type": "cob", "A": 540, "B": 540, "C": 540, "D": 540, "E": 540, "F": 540, "G": 560, "H": 560, "I": 590, + "J_K_L": 590 + }, + { + "type": "system build", "A": 250, "B": 250, "C": 250, "D": 250, "E": 250, "F": 300, "G": 300, "H": 300, + "I": 300, "J_K_L": 300 + }, + { + "type": "park home", "A": None, "B": None, "C": None, "D": None, "E": None, "F": 50, "G": None, + "H": None, "I": 50, "J_K_L": 100 + }, +] diff --git a/model_data/recommendations/FloorRecommendations.py b/model_data/recommendations/FloorRecommendations.py index a9ee8387..a54175da 100644 --- a/model_data/recommendations/FloorRecommendations.py +++ b/model_data/recommendations/FloorRecommendations.py @@ -1,6 +1,8 @@ +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): @@ -11,6 +13,10 @@ class FloorRecommendations(BaseUtility): # 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 @@ -20,9 +26,85 @@ class FloorRecommendations(BaseUtility): # 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"] @@ -35,10 +117,67 @@ class FloorRecommendations(BaseUtility): if is_suspended: if insulation_thickness == "none": - uvalue = None - else: - uvalue = self.uvalue_estimates.get_estimate( - component="floor", - description="", - thickness=insulation_thickness + + 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()