From aa3b1e172d586d81422d45a4bbd6a3baa3ecd8ef Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 20 Sep 2023 14:23:46 +0100 Subject: [PATCH] implementing u-value population wip --- model_data/cleaner_app.py | 233 ++++++++++++++++++++++++ recommendations/FloorRecommendations.py | 69 ++++++- recommendations/rdsap_tables.py | 6 +- 3 files changed, 301 insertions(+), 7 deletions(-) diff --git a/model_data/cleaner_app.py b/model_data/cleaner_app.py index 1ccb6238..3bfac57a 100644 --- a/model_data/cleaner_app.py +++ b/model_data/cleaner_app.py @@ -77,6 +77,239 @@ def app(): if len(descriptions) != len(set(descriptions)): raise ValueError("Duplicated descriptions found, check me") + # Finally, we attach u-values to the descriptions for walls, roofs and floors + + wall_types = [ + "Stone: granite or whinstone as built", + "Stone: sandstone or limestone as built", + "Solid brick as built", + "Stone/solid brick with 50 mm external or internal insulation", + "Stone/solid brick with 100 mm external or internal insulation", + "Stone/solid brick with 150 mm external or internal insulation", + "Stone/solid brick with 200 mm external or internal insulation", + "Cob as built", + "Cob with 50 mm external or internal insulation", + "Cob with 100 mm external or internal insulation", + "Cob with 150 mm external or internal insulation", + "Cob with 200 mm external or internal insulation", + "Cavity as built", + "Unfilled cavity with 50 mm external or internal insulation", + "Unfilled cavity with 100 mm external or internal insulation", + "Unfilled cavity with 150 mm external or internal insulation", + "Unfilled cavity with 200 mm external or internal insulation", + "Filled cavity", + "Filled cavity with 50 mm external or internal insulation", + "Filled cavity with 100 mm external or internal insulation", + "Filled cavity with 150 mm external or internal insulation", + "Filled cavity with 200 mm external or internal insulation", + "Timber frame as built", + "Timber frame with internal insulation", + "System build as built", + "System build with 50 mm external or internal insulation", + "System build with 100 mm external or internal insulation", + "System build with 150 mm external or internal insulation", + "System build with 200 mm external or internal insulation", + ] + + u_values = [ + ["a", "a", "a", "a", "1.7b", "1.0", "0.6", "0.60", "0.45", "0.35", "0.30", "0.28"], + ["a", "a", "a", "a", "1.7b", "1.0", "0.6", "0.60", "0.45", "0.35", "0.30", "0.28"], + ["1.7", "1.7", "1.7", "1.7", "1.7", "1.0", "0.60", "0.60", "0.45", "0.35", "0.30", "0.28"], + ["0.55", "0.55", "0.55", "0.55", "0.55", "0.45", "0.35", "0.35", "0.30", "0.25", "0.21", "0.21"], + ["0.32", "0.32", "0.32", "0.32", "0.32", "0.28", "0.24", "0.24", "0.21", "0.19", "0.17", "0.16"], + ["0.23", "0.23", "0.23", "0.23", "0.23", "0.21", "0.18", "0.18", "0.17", "0.15", "0.14", "0.14"], + ["0.18", "0.18", "0.18", "0.18", "0.18", "0.17", "0.15", "0.15", "0.14", "0.13", "0.12", "0.12"], + ["0.80", "0.80", "0.80", "0.80", "0.80", "0.80", "0.60", "0.60", "0.45", "0.35", "0.30", "0.28"], + ["0.40", "0.40", "0.40", "0.40", "0.40", "0.40", "0.35", "0.35", "0.30", "0.25", "0.21", "0.21"], + ["0.26", "0.26", "0.26", "0.26", "0.26", "0.26", "0.24", "0.24", "0.21", "0.19", "0.17", "0.16"], + ["0.20", "0.20", "0.20", "0.20", "0.20", "0.20", "0.18", "0.18", "0.17", "0.15", "0.14", "0.14"], + ["0.16", "0.16", "0.16", "0.16", "0.16", "0.16", "0.15", "0.15", "0.14", "0.13", "0.12", "0.12"], + ["1.5", "1.5", "1.5", "1.5", "1.5", "1.0", "0.60", "0.60", "0.45", "0.35", "0.30", "0.28"], + ["0.53", "0.53", "0.53", "0.53", "0.53", "0.45", "0.35", "0.35", "0.30", "0.25", "0.21", "0.21"], + ["0.32", "0.32", "0.32", "0.32", "0.32", "0.30", "0.24", "0.24", "0.21", "0.19", "0.17", "0.16"], + ["0.23", "0.23", "0.23", "0.23", "0.23", "0.21", "0.18", "0.18", "0.17", "0.15", "0.14", "0.14"], + ["0.18", "0.18", "0.18", "0.18", "0.18", "0.17", "0.15", "0.15", "0.14", "0.13", "0.12", "0.12"], + ["0.7", "0.7", "0.7", "0.7", "0.7", "0.40", "0.35", "0.35", "0.45", "0.35", "0.30", "0.28"], + ["0.37", "0.37", "0.37", "0.37", "0.37", "0.27", "0.25", "0.25", "0.25", "0.25", "0.21", "0.21"], + ["0.25", "0.25", "0.25", "0.25", "0.25", "0.20", "0.19", "0.19", "0.19", "0.19", "0.17", "0.16"], + ["0.19", "0.19", "0.19", "0.19", "0.19", "0.16", "0.15", "0.15", "0.15", "0.15", "0.14", "0.14"], + ["0.16", "0.16", "0.16", "0.16", "0.16", "0.13", "0.13", "0.13", "0.13", "0.13", "0.12", "0.12"], + ["2.5", "1.9", "1.9", "1.0", "0.80", "0.45", "0.40", "0.40", "0.40", "0.35", "0.30", "0.28"], + ["0.60", "0.55", "0.55", "0.40", "0.40", "0.40", "0.40", "0.40", "0.40", "0.35", "0.30", "0.28"], + ["2.0", "2.0", "2.0", "2.0", "1.7", "1.0", "0.60", "0.60", "0.45", "0.35", "0.30", "0.28"], + ["0.60", "0.60", "0.60", "0.60", "0.55", "0.45", "0.35", "0.35", "0.30", "0.25", "0.21", "0.21"], + ["0.35", "0.35", "0.35", "0.35", "0.35", "0.32", "0.24", "0.24", "0.21", "0.19", "0.17", "0.16"], + ["0.25", "0.25", "0.25", "0.25", "0.25", "0.21", "0.18", "0.18", "0.17", "0.15", "0.14", "0.14"], + ["0.18", "0.18", "0.18", "0.18", "0.18", "0.17", "0.15", "0.15", "0.14", "0.13", "0.12", "0.12"], + ] + + age_bands = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L"] + + wall_uvalues = [] + for i, wall_type in enumerate(wall_types): + row = {"Wall_type": wall_type} + for j, age_band in enumerate(age_bands): + row[age_band] = u_values[i][j] + wall_uvalues.append(row) + + parkhome_wall_uvalues = [ + {"Wall_type": "Park home as built", "F": "1.7", "G": "1.2", "I": "0.7", "K": "0.6"}, + {"Wall_type": "Park home with additional insulation", } + ] + + wall_uvalues.extend(parkhome_wall_uvalues) + + wall_uvalues_df = pd.DataFrame(wall_uvalues) + + # This maps the descriptions in the EPC data to the descriptions in the table + epc_wall_description_map = { + ############################ + # Cavity wall mappings + ############################ + "Cavity wall, as built, partial insulation": "Filled cavity", + "Cavity wall, filled cavity": "Filled cavity", + "Cavity wall, as built, no insulation": "Cavity as built", + "Cavity wall, as built, insulated": "Unfilled cavity with 100 mm external or internal insulation", + "Cavity wall, with external insulation": "Unfilled cavity with 100 mm external or internal insulation", + "Cavity wall,": "Cavity as built", # General case of cavity wall without further details + "Cavity wall, filled cavity and external insulation": + "Filled cavity with 100 mm external or internal insulation", + "Cavity wall, filled cavity and internal insulation": + "Filled cavity with 100 mm external or internal insulation", + "Cavity wall, with internal insulation": "Unfilled cavity with 100 mm external or internal insulation", + + ############################ + # Solid brick wall mappings + ############################ + "Solid brick, as built, no insulation": "Solid brick as built", + "Solid brick, with internal insulation": "Stone/solid brick with 100 mm external or internal insulation", + "Solid brick, as built, insulated": "Stone/solid brick with 100 mm external or internal insulation", + "Solid brick, with external insulation": "Stone/solid brick with 100 mm external or internal insulation", + "Solid brick, as built, partial insulation": "Stone/solid brick with 50 mm external or internal insulation", + + ############################ + # Timber frame wall mappings + ############################ + # These mappings are perhaps the most dubious due to the lack of timber options in the RdSAP table + "Timber frame, as built, insulated": "Timber frame with internal insulation", + "Timber frame, with additional insulation": "Timber frame with internal insulation", + "Timber frame, as built, partial insulation": "Timber frame as built", + "Timber frame, as built, no insulation": "Timber frame as built", + "Timber frame, with external insulation": "Timber frame with internal insulation", + + ############################ + # Sandstone/limestones wall mappings + ############################ + "Sandstone or limestone, as built, no insulation": "Stone: sandstone or limestone as built", + "Sandstone or limestone, with internal insulation": + "Stone/solid brick with 100 mm external or internal insulation", + "Sandstone or limestone, as built, partial insulation": "Stone/solid brick with 50 mm external or internal " + "insulation", + "Sandstone, as built, no insulation": "Stone: sandstone or limestone as built", + "Sandstone or limestone, as built, insulated": "Stone/solid brick with 100 mm external or internal" + "insulation", + "Sandstone, as built, insulated": "Stone/solid brick with 100 mm external or internal insulation", + "Sandstone, with internal insulation": "Stone/solid brick with 100 mm external or internal insulation", + "Sandstone or limestone, with external insulation": "Stone/solid brick with 100 mm external or internal " + "insulation", + "Sandstone, with external insulation": "Stone/solid brick with 100 mm external or internal insulation", + "Sandstone, as built, partial insulation": "Stone/solid brick with 50 mm external or internal insulation", + + ############################ + # Granite/whinstone wall mappings + ############################ + "Granite or whinstone, as built, no insulation": "Stone: granite or whinstone as built", + "Granite or whinstone, with internal insulation": "Stone/solid brick with 100 mm external or internal " + "insulation", + "Granite or whinstone, as built, partial insulation": "Stone/solid brick with 50 mm external or internal " + "insulation", + "Granite or whinstone, as built, insulated": "Stone/solid brick with 100 mm external or internal " + "insulation", + "Granite or whinstone, with external insulation": "Stone/solid brick with 100 mm external or internal " + "insulation", + + ############################ + # System built wall mappings + ############################ + "System built, as built, no insulation": "System build as built", + "System built, as built, partial insulation": "System build with 50 mm external or internal insulation", + "System built, with internal insulation": "System build with 100 mm external or internal insulation", + "System built, with external insulation": "System build with 100 mm external or internal insulation", + "System built, as built, insulated": "System build with 100 mm external or internal insulation", + + ############################ + # Cob wall mappings + ############################ + "Cob, as built": "Cob as built", + "Cob, with external insulation": "Cob with 100 mm external or internal insulation", + "Cob, with internal insulation": "Cob with 100 mm external or internal insulation", + + ############################ + # Park home mappings + ############################ + "Park home wall, as built": "Park home as built", + "Park home wall, with external insulation": "Park home with additional insulation", + "Park home wall, with internal insulation": "Park home with additional insulation", + } + + from recommendations.rdsap_tables import default_wall_thickness + + def apply_formula_s_5_1_1(is_granite_or_whinstone, is_sandstone_or_limestone, age_band): + """ + As the u-value table in https://bregroup.com/wp-content/uploads/2019/09/RdSAP_2012_9.94-20-09-2019.pdf + on page 19, certain u-values as indicated by an "a", should be populated using a formula as defined in section + S.5.1.1 + :param wall_type: + :return: + """ + + stone_wall_thickness = [x for x in default_wall_thickness if x["type"] == "stone"][0] + + thickness = stone_wall_thickness["J_K_L"] if age_band in ["J", "L", "L"] else stone_wall_thickness[age_band] + + if is_granite_or_whinstone: + return 3.3 - 0.002 * thickness + + if is_sandstone_or_limestone: + return 3 - 0.002 * thickness + + for wall in cleaned_data["walls-description"]: + if wall["thermal_transmittance"]: + continue + + description = wall["clean_description"] + # Remove (assumed) + description = description.replace("(assumed)", "").rstrip() + + mapped_description = epc_wall_description_map[description] + # Get the u-value + for ab in age_bands: + mapped_value = wall_uvalues_df[wall_uvalues_df["Wall_type"] == mapped_description][ab].values[0] + if mapped_value == "a": + # The rdSap documentation indicateswe should use a formula to calculate the u-value + uvalue = float( + apply_formula_s_5_1_1( + is_granite_or_whinstone=wall["is_granite_or_whinstone"], + is_sandstone_or_limestone=wall["is_sandstone_or_limestone"], + age_band=ab + ) + ) + elif "b" in mapped_value: + potential_uvalue = float(mapped_value.replace("b", "")) + formula_uvalue = float(apply_formula_s_5_1_1( + is_granite_or_whinstone=wall["is_granite_or_whinstone"], + is_sandstone_or_limestone=wall["is_sandstone_or_limestone"], + age_band=ab + )) + uvalue = min(potential_uvalue, formula_uvalue) + else: + uvalue = float(mapped_value) + + df = pd.DataFrame(cleaned_data["walls-description"]) + df = df[pd.isnull(df["thermal_transmittance"])] + + df["clean_description"].values + # We store a singular file however we could store the data under the following file path: # cleaned_epc_data/{component}/{original_description}/cleaned.bson # where component is one of the keys of cleaned_data. If we store it against the original data, this diff --git a/recommendations/FloorRecommendations.py b/recommendations/FloorRecommendations.py index 114a7fe1..fb28fdf4 100644 --- a/recommendations/FloorRecommendations.py +++ b/recommendations/FloorRecommendations.py @@ -66,11 +66,24 @@ class FloorRecommendations(Definitions): # 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) + # Estimate the side length of a square room with the average room size + avg_room_side_length = math.sqrt(avg_room_size) - # Compute the perimeter - perimeter = total_side_length * 4 + # Estimate total side length assuming rooms are lined up in a row + total_side_length = avg_room_side_length * num_rooms + + # Assuming the house is a square (which is a very rough approximation), compute the perimeter + perimeter = math.sqrt(total_side_length) * 4 + + return perimeter + + @staticmethod + def _estimate_perimeter_2_rooms(floor_area): + # Assuming a square layout for the entire floor area to get a first-order approximation of the perimeter + side_length = math.sqrt(floor_area) + + # Calculating the perimeter of the square layout + perimeter = 4 * side_length return perimeter @@ -126,7 +139,10 @@ class FloorRecommendations(Definitions): 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) + if number_of_rooms <= 2: + p = self._estimate_perimeter_2_rooms(floor_area=floor_area) + else: + 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) @@ -135,6 +151,49 @@ class FloorRecommendations(Definitions): return u + def _estimate_solid_floor_u_value(self): + """ + 1. dt =w + g × (Rsi + Rf + Rse) + 2. B = 2 × A/P + 3. if dt < B, U = 2 × g × ln( × B/dt + 1)/( × B + dt) + 4. if dt >= B, U = g / (0.457 × B + dt) + :return: + """ + + # TODO: complete + insulation_thickness = 0 + age_band_letter = "E" + wall_type = "cavity" + floor_area = 37.26 + perimeter = 16.2 + + # 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, + } + + dt = defaults["w"] + defaults["lambda_g"] * (defaults["Rsi"] + defaults["Rse"] + defaults["Rf"]) + + # perimeter = self._estimate_perimeter(floor_area, number_of_rooms) + + B = 2 * floor_area / perimeter + + if dt < B: + U = 2 * defaults["lambda_g"] * math.log(math.pi * B / dt + 1) / (math.pi * B + dt) + else: + print("implement me") + def recommend(self): u_value = self.property.floor["thermal_transmittance"] is_suspended = self.property.floor["is_suspended"] diff --git a/recommendations/rdsap_tables.py b/recommendations/rdsap_tables.py index eeacb935..c9204f34 100644 --- a/recommendations/rdsap_tables.py +++ b/recommendations/rdsap_tables.py @@ -91,6 +91,8 @@ age_band_data = [ }, ] +# As defined in the rdsap documentation on page 9 +# https://bregroup.com/wp-content/uploads/2019/09/RdSAP_2012_9.94-20-09-2019.pdf default_wall_thickness = [ { "type": "stone", "A": 500, "B": 500, "C": 500, "D": 500, "E": 450, "F": 420, "G": 420, "H": 420, @@ -117,7 +119,7 @@ default_wall_thickness = [ "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 + "type": "park home", "A": None, "B": None, "C": None, "D": None, "E": None, "F": 50, "G": 50, + "H": None, "I": 75, "J_K_L": 100 }, ]