diff --git a/recommendations/Recommendations.py b/recommendations/Recommendations.py index ac7635c5..684b0915 100644 --- a/recommendations/Recommendations.py +++ b/recommendations/Recommendations.py @@ -128,7 +128,7 @@ class Recommendations: property_recommendations.append(self.wall_recomender.recommendations) phase += 1 - self.roof_recommender.recommend(phase=phase, measures=measures) + self.roof_recommender.recommend(phase=phase, measures=measures, default_u_values=self.default_u_values) if self.roof_recommender.recommendations: property_recommendations.append(self.roof_recommender.recommendations) phase += 1 diff --git a/recommendations/RoofRecommendations.py b/recommendations/RoofRecommendations.py index 3e266cee..e8a43db0 100644 --- a/recommendations/RoofRecommendations.py +++ b/recommendations/RoofRecommendations.py @@ -108,7 +108,7 @@ class RoofRecommendations: return full_insulated_room_roof or room_roof_insulated_at_rafters - def recommend(self, phase, measures=None): + def recommend(self, phase, measures=None, default_u_values=False): if self.property.roof["has_dwelling_above"]: return @@ -171,7 +171,8 @@ class RoofRecommendations: insulation_thickness=self.insulation_thickness, phase=phase, is_flat=False, - is_pitched=True + is_pitched=True, + default_u_values=default_u_values ) return @@ -184,7 +185,8 @@ class RoofRecommendations: insulation_thickness=0, phase=phase, is_flat=True, - is_pitched=False + is_pitched=False, + default_u_values=default_u_values ) return @@ -193,7 +195,7 @@ class RoofRecommendations: if self.property.roof["is_roof_room"] and ("room_roof_insulation" in measures) or ( "room_roof_insulation" in [x["type"] for x in non_invasive_recommendations] ): - self.recommend_room_roof_insulation(u_value, phase) + self.recommend_room_roof_insulation(u_value, phase, default_u_values) return raise NotImplementedError("Implement me") @@ -215,7 +217,7 @@ class RoofRecommendations: raise ValueError("Invalid material type") def recommend_roof_insulation( - self, u_value, insulation_thickness, phase, is_pitched, is_flat + self, u_value, insulation_thickness, phase, is_pitched, is_flat, default_u_values ): """ @@ -241,6 +243,7 @@ class RoofRecommendations: :param phase: Phase of the recommendation :param is_pitched: Is the roof pitched :param is_flat: Is the roof flat + :param default_u_values: Use default u-values :return: """ @@ -266,7 +269,6 @@ class RoofRecommendations: recommendations = [] for _, insulation_material_group in insulation_materials.groupby("description"): for _, material in insulation_material_group.iterrows(): - # We make sure we hit a depth of 270mm. We should factor in any existing insulation if the # loft is already partially insulated. # Note: This requirement is only for loft insulation @@ -340,9 +342,35 @@ class RoofRecommendations: new_description = f"Pitched, {int(proposed_depth)}mm loft insulation" + if default_u_values: + # We update the u-value with the default if we're using default u-values + new_u_value = get_roof_u_value( + insulation_thickness=str(int(new_thickness)), + has_dwelling_above=self.property.roof["has_dwelling_above"], + is_loft=self.property.roof["is_loft"], + is_roof_room=self.property.roof["is_roof_room"], + is_thatched=self.property.roof["is_thatched"], + age_band=self.property.age_band, + is_flat=self.property.roof["is_flat"], + is_pitched=self.property.roof["is_pitched"], + is_at_rafters=self.property.roof["is_at_rafters"], + ) + elif material["type"] == "flat_roof_insulation": new_description = "Flat, insulated" new_efficiency = "Good" + if default_u_values: + new_u_value = get_roof_u_value( + insulation_thickness="100", + has_dwelling_above=self.property.roof["has_dwelling_above"], + is_loft=self.property.roof["is_loft"], + is_roof_room=self.property.roof["is_roof_room"], + is_thatched=self.property.roof["is_thatched"], + age_band=self.property.age_band, + is_flat=self.property.roof["is_flat"], + is_pitched=self.property.roof["is_pitched"], + is_at_rafters=self.property.roof["is_at_rafters"], + ) else: raise ValueError("Invalid material type") diff --git a/recommendations/WallRecommendations.py b/recommendations/WallRecommendations.py index 28e35584..358547dc 100644 --- a/recommendations/WallRecommendations.py +++ b/recommendations/WallRecommendations.py @@ -544,6 +544,7 @@ class WallRecommendations(Definitions): } if default_u_values: + # If we're using default U-values, we overwrite new_u_value new_u_value = get_wall_u_value( clean_description=new_description, age_band=self.property.age_band, diff --git a/recommendations/rdsap_tables.py b/recommendations/rdsap_tables.py index 5110764b..16c7d26e 100644 --- a/recommendations/rdsap_tables.py +++ b/recommendations/rdsap_tables.py @@ -340,6 +340,7 @@ s9_list = [ s10_list = [ { "Age_band": "A, B, C, D", + "Insulation_Thickness": "none", "Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 2.3, "Pitched_slates_or_tiles_insulation_at_rafters": 2.3, "Flat_roof": 2.3, @@ -350,6 +351,7 @@ s10_list = [ }, { "Age_band": "E", + "Insulation_Thickness": 12, "Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 1.5, "Pitched_slates_or_tiles_insulation_at_rafters": 1.5, "Flat_roof": 1.5, @@ -360,6 +362,7 @@ s10_list = [ }, { "Age_band": "F", + "Insulation_Thickness": 50, "Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.68, "Pitched_slates_or_tiles_insulation_at_rafters": 0.68, "Flat_roof": 0.68, @@ -370,6 +373,7 @@ s10_list = [ }, { "Age_band": "G", + "Insulation_Thickness": 100, "Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.40, "Pitched_slates_or_tiles_insulation_at_rafters": 0.40, "Flat_roof": 0.40, @@ -380,6 +384,7 @@ s10_list = [ }, { "Age_band": "H", + "Insulation_Thickness": 150, "Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.30, "Pitched_slates_or_tiles_insulation_at_rafters": 0.35, "Flat_roof": 0.35, @@ -390,6 +395,7 @@ s10_list = [ }, { "Age_band": "I", + "Insulation_Thickness": 150, "Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.26, "Pitched_slates_or_tiles_insulation_at_rafters": 0.35, "Flat_roof": 0.35, @@ -400,6 +406,7 @@ s10_list = [ }, { "Age_band": "J", + "Insulation_Thickness": 270, "Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.16, "Pitched_slates_or_tiles_insulation_at_rafters": 0.20, "Flat_roof": 0.25, @@ -410,6 +417,7 @@ s10_list = [ }, { "Age_band": "K", + "Insulation_Thickness": 270, "Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.16, "Pitched_slates_or_tiles_insulation_at_rafters": 0.20, "Flat_roof": 0.25, @@ -420,6 +428,7 @@ s10_list = [ }, { "Age_band": "L", + "Insulation_Thickness": 270, "Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.16, "Pitched_slates_or_tiles_insulation_at_rafters": 0.18, "Flat_roof": 0.18, diff --git a/recommendations/recommendation_utils.py b/recommendations/recommendation_utils.py index dcdd9c06..8ddfe1ef 100644 --- a/recommendations/recommendation_utils.py +++ b/recommendations/recommendation_utils.py @@ -239,12 +239,7 @@ def get_wall_u_value( return float(mapped_value) -def get_u_value_from_s9( - thickness, s9, is_loft, is_roof_room, is_thatched, is_at_rafters -): - """Get the U-value from table S9 based on the insulation thickness.""" - - # If the roof as pitched & insulated at the rafters, it's a room roof +def extract_thickness(thickness, is_roof_room, is_at_rafters, is_loft, is_flat): if is_roof_room or is_at_rafters: # TODO: We get None instead of a string none, this should be fixed if thickness is None: @@ -258,19 +253,40 @@ def get_u_value_from_s9( } thickness = thickness_map[thickness] + if is_flat: + try: + thickness = int(thickness) + return thickness + except ValueError: + # If thickness is not a valid number (could be a string or None), return None + return None + if thickness in ["below average", "average", "above average", "none", None] or ( not is_loft and not is_roof_room and not is_at_rafters ): return None elif thickness.endswith("+"): thickness = int(thickness[:-1]) + return thickness else: try: thickness = int(thickness) + return thickness except ValueError: # If thickness is not a valid number (could be a string or None), return None return None + +def get_u_value_from_s9( + thickness, s9, is_loft, is_roof_room, is_thatched, is_at_rafters +): + """Get the U-value from table S9 based on the insulation thickness.""" + + if thickness in ["below average", "average", "above average", "none", None, "0", 0] or ( + not is_loft and not is_roof_room and not is_at_rafters + ): + return None + # Determine the column to refer based on the roof type column = ( "Thatched_roof_U_value_W_m2K" @@ -323,6 +339,14 @@ def get_roof_u_value( if has_dwelling_above: return 0.0 + thickness = extract_thickness( + thickness=insulation_thickness, + is_roof_room=is_roof_room, + is_at_rafters=is_at_rafters, + is_loft=is_loft, + is_flat=is_flat + ) + # Step 1: Try to get the U-value from table S9 based on the insulation thickness # The conditions for using table S9 are: # - The insulation thickness is known @@ -330,7 +354,7 @@ def get_roof_u_value( # The criteria for using this table is predominately defined by insulation around joists which is predominately # a feature of lofts and roof rooms u_value = get_u_value_from_s9( - thickness=insulation_thickness, + thickness=thickness, s9=s9, is_loft=is_loft, is_roof_room=is_roof_room, @@ -363,9 +387,34 @@ def get_roof_u_value( column = "Pitched_slates_or_tiles_insulation_between_joists_or_unknown" # Get the U-value from table S10 based on the age band and the determined column - u_value = s10.loc[s10["Age_band"].str.contains(age_band), column].values[0] + if is_flat and thickness is not None: + u_value = s10.loc[ + (s10["Insulation_Thickness"] == thickness) | + s10["Age_band"].str.contains(age_band), + column + ].values.min() + else: + u_value = s10.loc[s10["Age_band"].str.contains(age_band), column].values[0] - return float(u_value) + u_value = float(u_value) + + # As per the documentation here: https://bregroup.com/documents/d/bre-group/rdsap_2012_9-94-20-09-2019 + # Table s.10 + # "The value from the table applies for unknown and as built. If the roof is known to have more insulation than + # would normally be expected for the age band, either observed or on the basis of documentary evidence, use the + # lower of the value in the table and: + # 50 mm insulation 0.68 + # 100 mm insulation: 0.40 + # 150 mm or more insulation: 0.30" + if thickness is not None: + if thickness == 50: + u_value = min(u_value, 0.68) + if thickness == 100: + u_value = min(u_value, 0.40) + if thickness >= 150: + u_value = min(u_value, 0.30) + + return u_value def estimate_number_of_floors(property_type): diff --git a/recommendations/tests/test_recommendation_utils.py b/recommendations/tests/test_recommendation_utils.py index 24ea6482..b445a798 100644 --- a/recommendations/tests/test_recommendation_utils.py +++ b/recommendations/tests/test_recommendation_utils.py @@ -6,6 +6,7 @@ from recommendations import recommendation_utils from datatypes.enums import QuantityUnits from recommendations.tests.test_data.wall_uvalue_test_cases import wall_uvalue_test_cases from recommendations.tests.test_data.floor_uvalue_test_cases import floor_uvalue_test_cases +from recommendations.tests.test_data.roof_uvalue_test_cases import roof_uvalue_test_cases class TestRecommendationUtils: @@ -222,6 +223,26 @@ class TestRecommendationUtils: u_value = recommendation_utils.get_roof_u_value(**inputs) assert u_value == 0.0, f"Expected 0.0, but got {u_value}" + @pytest.mark.parametrize( + "test_case", + roof_uvalue_test_cases + ) + def test_roof_uvalues(self, test_case): + expected_uvalue = test_case["uvalue"] + inputs = test_case.copy() + del inputs["uvalue"] + # insulation_thickness = inputs["insulation_thickness"] + # has_dwelling_above = inputs["has_dwelling_above"] + # is_loft = inputs["is_loft"] + # is_roof_room = inputs["is_roof_room"] + # is_thatched = inputs["is_thatched"] + # age_band = inputs["age_band"] + # is_flat = inputs["is_flat"] + # is_pitched = inputs["is_pitched"] + # is_at_rafters = inputs["is_at_rafters"] + uvalue = recommendation_utils.get_roof_u_value(**inputs) + assert expected_uvalue == uvalue, f"Expected u value {expected_uvalue}, recieved {uvalue}" + @pytest.mark.parametrize( "test_case", wall_uvalue_test_cases