From 13ceb4031d7125aaa7a0039638308276d3edab0f Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Fri, 20 Oct 2023 18:51:49 +1100 Subject: [PATCH 01/21] implementing loft insulation wip --- recommendations/RoofRecommendations.py | 134 ++++++++++++++++++++++++ recommendations/WallRecommendations.py | 49 +++++---- recommendations/recommendation_utils.py | 17 +++ 3 files changed, 179 insertions(+), 21 deletions(-) create mode 100644 recommendations/RoofRecommendations.py diff --git a/recommendations/RoofRecommendations.py b/recommendations/RoofRecommendations.py new file mode 100644 index 00000000..13cf958e --- /dev/null +++ b/recommendations/RoofRecommendations.py @@ -0,0 +1,134 @@ +import math +from backend import Property +from typing import List +from datatypes.enums import QuantityUnits +from recommendations.recommendation_utils import ( + get_roof_u_value, r_value_per_mm_to_u_value, calculate_u_value_uplift, is_diminishing_returns, + update_lowest_selected_u_value, get_recommended_part +) + + +class RoofRecommendations: + # part L building regulations indicate that any rennovations on an existing property's roof should + # achieve a U-value of no higher than 0.16 + # This can be seen in table 4.3 in building regulations part L: + # https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/1133079 + # /Approved_Document_L__Conservation_of_fuel_and_power__Volume_1_Dwellings__2021_edition_incorporating_2023_amendments.pdf + BUILDING_REGULATIONS_PART_L_MAX_U_VALUE = 0.16 + + DIMINISHING_RETURNS_U_VALUE = 0.14 + + def __init__( + self, + property_instance: Property, + materials: List + ): + self.property = property_instance + # 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 = [] + + self.materials = materials + + def recommend(self): + u_value = self.property.roof["thermal_transmittance"] + + insulation_thickness = self.property.walls["insulation_thickness"] + + # We check if the roof is already insulated and if so, we exit + if insulation_thickness in ["average", "above average"]: + return + + # If we have a u-value already, need to implement this + if u_value: + raise NotImplementedError("Implement me") + + u_value = get_roof_u_value(**{**self.property.roof, "age_band": self.property.age_band}) + + # With loft insulation, 100mm goes between the joists and the rest is rolled on top + # Therefore the price is 100mm + whatever thickness is rolled on top, rolled at a 90 degree angle + # from the base layer + materials = [ + { + 'id': 4, + 'type': 'loft_insulation', + 'description': 'Iso Spacesaver Mineral Wool insulation', + 'depths': [270, 300], + 'depth_unit': 'mm', + 'cost': [9, 10], + 'cost_unit': 'gbp_sq_meter', + 'r_value_per_mm': 0.022727272727272728, + 'r_value_unit': 'square_meter_kelvin_per_watt', + 'thermal_conductivity': 0.044, + 'thermal_conductivity_unit': 'watt_per_meter_kelvin', + 'link': "https://flooringwarehousedirect.co.uk/product/isover-spacesaver-roll-100mm-x-1160mm-x-12-18m" + "-14-13m2/", + 'is_active': True + }, + ] + + self.materials = materials + + if self.property.roof["is_pitched"]: + # We recommend loft insulation + self.recommend_loft_insulation(u_value) + return + + def recommend_loft_insulation(self, u_value): + + """ + This method will recommend which insulation materials to use + :return: + """ + + loft_insulation_materials = [m for m in self.materials if m["type"] == "loft_insulation"] + + lowest_selected_u_value = None + recommendations = [] + for material in loft_insulation_materials: + + for depth, cost_per_unit in zip(material["depths"], material["cost"]): + + part_u_value = r_value_per_mm_to_u_value(depth, material["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 I have a lowest U value and my new u value is higher than that but lower than the + # diminishing returns threshold, it can be considered + + # If I have a lowest U value and my new u value is lower than the lowest value, it's + # further into the diminishing returns threshold and can shouldn't be + + if is_diminishing_returns( + recommendations, new_u_value, lowest_selected_u_value, self.DIMINISHING_RETURNS_U_VALUE + ): + continue + + # We allow a small tolerance for error so we don't discount the recommendation entirely + 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.insulation_wall_area + + recommendations.append( + { + "parts": [ + get_recommended_part( + part=material, + selected_depth=depth, + quantity=self.property.insulation_wall_area, + quantity_unit=QuantityUnits.m2.value, + selected_total_cost=estimated_cost + ) + ], + "type": "roof_insulation", + "description": "TODO ", + "starting_u_value": u_value, + "new_u_value": new_u_value, + "sap_points": None, + "cost": estimated_cost, + } + ) diff --git a/recommendations/WallRecommendations.py b/recommendations/WallRecommendations.py index ad2ca861..12085840 100644 --- a/recommendations/WallRecommendations.py +++ b/recommendations/WallRecommendations.py @@ -23,11 +23,17 @@ class WallRecommendations(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 + # This can be seen in table 4.3 in building regulations part L: + # https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/1133079 + # /Approved_Document_L__Conservation_of_fuel_and_power__Volume_1_Dwellings__2021_edition_incorporating_2023_amendments.pdf BUILDING_REGULATIONS_PART_L_MAX_U_VALUE = 0.3 # 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.25 + # Building regulations part L also indicates that cavity wall insulation should result in 0.55 u-value + BUILDING_REGULATIONS_PART_L_CAVITY_WALL_MAX_U_VALUE = 0.55 + # Part L regulations indicate that any new build should have walls that achieve a u-value of no higher # than 0.18. BUILDING_REGULATIONS_PART_L_NEW_BUILD_MAX_U_VALUE = 0.18 @@ -167,29 +173,30 @@ class WallRecommendations(Definitions): ): continue - lowest_selected_u_value = update_lowest_selected_u_value(lowest_selected_u_value, new_u_value) + if new_u_value <= self.BUILDING_REGULATIONS_PART_L_CAVITY_WALL_MAX_U_VALUE: + lowest_selected_u_value = update_lowest_selected_u_value(lowest_selected_u_value, new_u_value) - estimated_cost = part["cost"] * self.property.insulation_wall_area + estimated_cost = part["cost"] * self.property.insulation_wall_area - recommendations.append( - { - "parts": [ - get_recommended_part( - part=part, - selected_depth=None, - quantity=self.property.insulation_wall_area, - quantity_unit=QuantityUnits.m2.value, - selected_total_cost=estimated_cost - ) - ], - "type": "wall_insulation", - "description": f"Fill cavity with {part['description']}", - "starting_u_value": u_value, - "new_u_value": new_u_value, - "sap_points": None, - "cost": estimated_cost, - } - ) + recommendations.append( + { + "parts": [ + get_recommended_part( + part=part, + selected_depth=None, + quantity=self.property.insulation_wall_area, + quantity_unit=QuantityUnits.m2.value, + selected_total_cost=estimated_cost + ) + ], + "type": "wall_insulation", + "description": f"Fill cavity with {part['description']}", + "starting_u_value": u_value, + "new_u_value": new_u_value, + "sap_points": None, + "cost": estimated_cost, + } + ) self.recommendations = recommendations diff --git a/recommendations/recommendation_utils.py b/recommendations/recommendation_utils.py index cd7bb3f8..4f0813ab 100644 --- a/recommendations/recommendation_utils.py +++ b/recommendations/recommendation_utils.py @@ -517,3 +517,20 @@ def estimate_wall_area(num_floors, floor_height, perimeter): total_wall_area = wall_area_one_floor * num_floors return total_wall_area + + +def calculate_r_value_per_mm(thickness_mm, thermal_conductivity_w_mK): + """ + # Calculate R-value (thermal resistance) using the formula: R = thickness / thermal_conductivity + # Note: The thickness should be converted to meters for the units to be consistent. + :param thickness_mm: + :param thermal_conductivity_w_mK: + :return: + """ + + r_value_m2k_w = (thickness_mm / 1000) / thermal_conductivity_w_mK + + # Calculate R-value per mm + r_value_per_mm = r_value_m2k_w / thickness_mm + + return r_value_per_mm From 67e6a555f0611a1d876e726868b50736cf68dd40 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Fri, 20 Oct 2023 19:01:30 +1100 Subject: [PATCH 02/21] almost completed loft insulation --- recommendations/RoofRecommendations.py | 40 +++++++++----------------- 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/recommendations/RoofRecommendations.py b/recommendations/RoofRecommendations.py index 13cf958e..5a2a6bcf 100644 --- a/recommendations/RoofRecommendations.py +++ b/recommendations/RoofRecommendations.py @@ -47,35 +47,17 @@ class RoofRecommendations: u_value = get_roof_u_value(**{**self.property.roof, "age_band": self.property.age_band}) - # With loft insulation, 100mm goes between the joists and the rest is rolled on top - # Therefore the price is 100mm + whatever thickness is rolled on top, rolled at a 90 degree angle - # from the base layer - materials = [ - { - 'id': 4, - 'type': 'loft_insulation', - 'description': 'Iso Spacesaver Mineral Wool insulation', - 'depths': [270, 300], - 'depth_unit': 'mm', - 'cost': [9, 10], - 'cost_unit': 'gbp_sq_meter', - 'r_value_per_mm': 0.022727272727272728, - 'r_value_unit': 'square_meter_kelvin_per_watt', - 'thermal_conductivity': 0.044, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', - 'link': "https://flooringwarehousedirect.co.uk/product/isover-spacesaver-roll-100mm-x-1160mm-x-12-18m" - "-14-13m2/", - 'is_active': True - }, - ] - - self.materials = materials - if self.property.roof["is_pitched"]: # We recommend loft insulation self.recommend_loft_insulation(u_value) return + raise NotImplementedError("Implement me") + + @staticmethod + def make_loft_insulation_description(material, depth): + return f"Install {depth}{material['depth_unit']} of {material['description']}" + def recommend_loft_insulation(self, u_value): """ @@ -83,6 +65,10 @@ class RoofRecommendations: :return: """ + # With loft insulation, 100mm goes between the joists and the rest is rolled on top + # Therefore the price is 100mm + whatever thickness is rolled on top, rolled at a 90 degree angle + # from the base layer + loft_insulation_materials = [m for m in self.materials if m["type"] == "loft_insulation"] lowest_selected_u_value = None @@ -111,7 +97,7 @@ class RoofRecommendations: 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.insulation_wall_area + estimated_cost = cost_per_unit * self.property.floor_area recommendations.append( { @@ -125,10 +111,12 @@ class RoofRecommendations: ) ], "type": "roof_insulation", - "description": "TODO ", + "description": self.make_loft_insulation_description(material, depth), "starting_u_value": u_value, "new_u_value": new_u_value, "sap_points": None, "cost": estimated_cost, } ) + + self.recommendations = recommendations From 193e0137646a9057831a191485dc0758948660e0 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Fri, 20 Oct 2023 19:21:42 +1100 Subject: [PATCH 03/21] added roof insulation to backend - almost complete --- backend/app/db/models/materials.py | 1 + backend/app/plan/router.py | 22 +++++++------- backend/app/plan/utils.py | 46 ++++++++++++++++++------------ 3 files changed, 40 insertions(+), 29 deletions(-) diff --git a/backend/app/db/models/materials.py b/backend/app/db/models/materials.py index cf6dd971..1dc47276 100644 --- a/backend/app/db/models/materials.py +++ b/backend/app/db/models/materials.py @@ -14,6 +14,7 @@ class MaterialType(enum.Enum): internal_wall_insulation = "internal_wall_insulation" cavity_wall_insulation = "cavity_wall_insulation" mechanical_ventilation = "mechanical_ventilation" + loft_insulation = "loft_insulation" class DepthUnit(enum.Enum): diff --git a/backend/app/plan/router.py b/backend/app/plan/router.py index fdbf155d..d5555d0b 100644 --- a/backend/app/plan/router.py +++ b/backend/app/plan/router.py @@ -30,6 +30,7 @@ from backend.Property import Property from etl.epc.DataProcessor import DataProcessor from etl.epc.settings import COLUMNS_TO_MERGE_ON from recommendations.FloorRecommendations import FloorRecommendations +from recommendations.RoofRecommendations import RoofRecommendations from recommendations.VentilationRecommendations import VentilationRecommendations from recommendations.FireplaceRecommendations import FireplaceRecommendations from recommendations.optimiser.CostOptimiser import CostOptimiser @@ -139,10 +140,7 @@ async def trigger_plan(body: PlanTriggerRequest): p.get_components(cleaned) # Floor recommendations - floor_recommender = FloorRecommendations( - property_instance=p, - materials=materials_by_type["floor"], - ) + floor_recommender = FloorRecommendations(property_instance=p, materials=materials_by_type["floor"]) floor_recommender.recommend() if floor_recommender.recommendations: @@ -150,15 +148,19 @@ async def trigger_plan(body: PlanTriggerRequest): # Wall recommendations - wall_recomender = WallRecommendations( - property_instance=p, - materials=materials_by_type["walls"] - ) + wall_recomender = WallRecommendations(property_instance=p, materials=materials_by_type["walls"]) wall_recomender.recommend() if wall_recomender.recommendations: property_recommendations.append(wall_recomender.recommendations) + # Roof recommendations + roof_recommender = RoofRecommendations(property_instance=p, materials=materials_by_type["roof"]) + roof_recommender.recommend() + + if roof_recommender.recommendations: + property_recommendations.append(roof_recommender.recommendations) + # Ventilation recommendations ventilation_recomender = VentilationRecommendations( property_instance=p, @@ -170,9 +172,7 @@ async def trigger_plan(body: PlanTriggerRequest): property_recommendations.append(ventilation_recomender.recommendation) # Fireplace sealing recommendations - fireplace_recommender = FireplaceRecommendations( - property_instance=p - ) + fireplace_recommender = FireplaceRecommendations(property_instance=p) fireplace_recommender.recommend() if fireplace_recommender.recommendation: diff --git a/backend/app/plan/utils.py b/backend/app/plan/utils.py index c06d9293..5a761b2d 100644 --- a/backend/app/plan/utils.py +++ b/backend/app/plan/utils.py @@ -17,6 +17,7 @@ def filter_materials(materials): "walls": ["internal_wall_insulation", "external_wall_insulation", "cavity_wall_insulation"], "floor": ["suspended_floor_insulation", "solid_floor_insulation"], "ventilation": ["mechanical_ventilation"], + "roof": ["loft_insulation"] } materials = [row2dict(material) for material in materials] @@ -145,7 +146,6 @@ def create_recommendation_scoring_data( # Update description to indicate it's insulate if recommendation["type"] == "floor_insulation": - if len(recommendation["parts"]) > 1: raise NotImplementedError("Have more than 1 floor insulation part - handle this case") @@ -167,6 +167,33 @@ def create_recommendation_scoring_data( if scoring_dict["floor_insulation_thickness_ENDING"] is None: scoring_dict["floor_insulation_thickness_ENDING"] = "none" + if recommendation["type"] == "roof_insulation": + scoring_dict["roof_thermal_transmittance_ENDING"] = recommendation["new_u_value"] + + parts = recommendation["parts"] + if len(parts) != 1: + raise ValueError("More than one part for roof insulation - investiage me") + + scoring_dict["roof_insulation_thickness_ENDING"] = str(parts[0]["depths"][0]) + scoring_dict["ROOF_ENERGY_EFF_ENDING"] = "Very Good" + else: + # Fill missing roof u-values - this fill is not based on recommended upgrades + if scoring_dict["roof_thermal_transmittance_ENDING"] is None: + scoring_dict["roof_thermal_transmittance_ENDING"] = get_roof_u_value( + insulation_thickness=property.roof["insulation_thickness"], + has_dwelling_above=property.roof["has_dwelling_above"], + is_loft=property.roof["is_loft"], + is_roof_room=property.roof["is_roof_room"], + is_thatched=property.roof["is_thatched"], + age_band=property.age_band, + is_flat=property.roof["is_flat"], + is_pitched=property.roof["is_pitched"], + is_at_rafters=property.roof["is_at_rafters"], + ) + + if scoring_dict["roof_insulation_thickness_ENDING"] is None: + scoring_dict["roof_insulation_thickness_ENDING"] = "none" + if recommendation["type"] == "mechanical_ventilation": scoring_dict["MECHANICAL_VENTILATION_ENDING"] = 'mechanical, extract only' @@ -178,21 +205,4 @@ def create_recommendation_scoring_data( ]: raise NotImplementedError("Implement me") - # Fill missing roof u-values - this fill is not based on recommended upgrades - if scoring_dict["roof_thermal_transmittance_ENDING"] is None: - scoring_dict["roof_thermal_transmittance_ENDING"] = get_roof_u_value( - insulation_thickness=property.roof["insulation_thickness"], - has_dwelling_above=property.roof["has_dwelling_above"], - is_loft=property.roof["is_loft"], - is_roof_room=property.roof["is_roof_room"], - is_thatched=property.roof["is_thatched"], - age_band=property.age_band, - is_flat=property.roof["is_flat"], - is_pitched=property.roof["is_pitched"], - is_at_rafters=property.roof["is_at_rafters"], - ) - - if scoring_dict["roof_insulation_thickness_ENDING"] is None: - scoring_dict["roof_insulation_thickness_ENDING"] = "none" - return scoring_dict From 9346b82c2e5022640beff6461b6da1f450a75841 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Sat, 21 Oct 2023 20:18:18 +1100 Subject: [PATCH 04/21] allow roof_insulation in create_recommendation_scoring_data --- backend/app/plan/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/app/plan/utils.py b/backend/app/plan/utils.py index 5a761b2d..e056c61c 100644 --- a/backend/app/plan/utils.py +++ b/backend/app/plan/utils.py @@ -201,7 +201,7 @@ def create_recommendation_scoring_data( scoring_dict["NUMBER_OPEN_FIREPLACES_ENDING"] = 0 if recommendation["type"] not in [ - "wall_insulation", "floor_insulation", "mechanical_ventilation", "sealing_open_fireplace" + "wall_insulation", "floor_insulation", "roof_insulation", "mechanical_ventilation", "sealing_open_fireplace", ]: raise NotImplementedError("Implement me") From 57c8788a373f0e96bc87a6b742a183020b614e6a Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 24 Oct 2023 06:23:05 +1100 Subject: [PATCH 05/21] basic unit tests for fireplace recommendations --- backend/app/plan/router.py | 10 ++++ recommendations/RoofRecommendations.py | 11 ++-- recommendations/recommendation_utils.py | 26 +++++++++ .../tests/test_fireplace_recommendations.py | 58 +++++++++++++++++++ .../tests/test_recommendation_utils.py | 11 ++++ .../tests/test_roof_recommendations.py | 39 +++++++++++++ 6 files changed, 151 insertions(+), 4 deletions(-) create mode 100644 recommendations/tests/test_fireplace_recommendations.py create mode 100644 recommendations/tests/test_roof_recommendations.py diff --git a/backend/app/plan/router.py b/backend/app/plan/router.py index d5555d0b..6171ddbf 100644 --- a/backend/app/plan/router.py +++ b/backend/app/plan/router.py @@ -130,6 +130,16 @@ async def trigger_plan(body: PlanTriggerRequest): # with open("new_sap_dataset.pickle", "rb") as f: # new_sap_dataset = pickle.load(f) + # import pickle + # with open("cleaned.pickle", "rb") as f: + # cleaned = pickle.dump(f) + + # with open("sap_dataset.pickle", "rb") as f: + # sap_dataset = pickle.load(f) + + # with open("materials_by_type", "rb") as f: + # materials_by_type = pickle.load(f) + recommendations = {} recommendations_scoring_data = [] diff --git a/recommendations/RoofRecommendations.py b/recommendations/RoofRecommendations.py index 5a2a6bcf..06eab302 100644 --- a/recommendations/RoofRecommendations.py +++ b/recommendations/RoofRecommendations.py @@ -1,10 +1,10 @@ import math -from backend import Property +from backend.Property import Property from typing import List from datatypes.enums import QuantityUnits from recommendations.recommendation_utils import ( get_roof_u_value, r_value_per_mm_to_u_value, calculate_u_value_uplift, is_diminishing_returns, - update_lowest_selected_u_value, get_recommended_part + update_lowest_selected_u_value, get_recommended_part, convert_thickness_to_numeric ) @@ -35,10 +35,13 @@ class RoofRecommendations: def recommend(self): u_value = self.property.roof["thermal_transmittance"] - insulation_thickness = self.property.walls["insulation_thickness"] + insulation_thickness = convert_thickness_to_numeric(self.property.roof["insulation_thickness"]) # We check if the roof is already insulated and if so, we exit - if insulation_thickness in ["average", "above average"]: + + # Building regulations part L recommend installing at least 270mm of insulation, however generally we + # experience diminishing returns in terms of SAP once we go beyond around 150mm of insulation + if insulation_thickness < 270: return # If we have a u-value already, need to implement this diff --git a/recommendations/recommendation_utils.py b/recommendations/recommendation_utils.py index 4f0813ab..67550fc8 100644 --- a/recommendations/recommendation_utils.py +++ b/recommendations/recommendation_utils.py @@ -534,3 +534,29 @@ def calculate_r_value_per_mm(thickness_mm, thermal_conductivity_w_mK): r_value_per_mm = r_value_m2k_w / thickness_mm return r_value_per_mm + + +def convert_thickness_to_numeric(string_thickness): + """ + Roof insulation thickness could be a string like "None", "300mm+" or a numeric string. + This function will convert these strings to a number for easy usage + :param string_thickness: string measure of insulation thickness + :return: integer measure of insulation thickness + """ + + lookup = { + "none": 0, + "below average": 50, + "average": 100, + "above average": 270 + } + + mapped = lookup.get(string_thickness) + + if mapped is not None: + return mapped + + if "+" in string_thickness: + return int(string_thickness.replace("+", "")) + + return int(string_thickness) diff --git a/recommendations/tests/test_fireplace_recommendations.py b/recommendations/tests/test_fireplace_recommendations.py new file mode 100644 index 00000000..a1e0c1c6 --- /dev/null +++ b/recommendations/tests/test_fireplace_recommendations.py @@ -0,0 +1,58 @@ +from backend.Property import Property +from unittest.mock import Mock +from recommendations.FireplaceRecommendations import FireplaceRecommendations + + +class TestFirepaceRecommendations: + + def test_no_fireplaces(self): + property_instance = Property(id=0, address1="fake", postcode="fake", epc_client=Mock()) + property_instance.data = { + "number-open-fireplaces": 0 + } + + recommender = FireplaceRecommendations( + property_instance=property_instance + ) + + assert recommender.recommendation is None + + recommender.recommend() + + assert recommender.recommendation is None + + def test_one_fireplace(self): + property_instance = Property(id=0, address1="fake", postcode="fake", epc_client=Mock()) + property_instance.data = { + "number-open-fireplaces": 1 + } + + recommender = FireplaceRecommendations( + property_instance=property_instance + ) + + assert recommender.recommendation is None + + recommender.recommend() + + assert recommender.recommendation + assert recommender.recommendation[0]["type"] == "sealing_open_fireplace" + assert recommender.recommendation[0]["cost"] == 300 + + def test_multiple_fireplaces(self): + property_instance = Property(id=0, address1="fake", postcode="fake", epc_client=Mock()) + property_instance.data = { + "number-open-fireplaces": 3 + } + + recommender = FireplaceRecommendations( + property_instance=property_instance + ) + + assert recommender.recommendation is None + + recommender.recommend() + + assert recommender.recommendation + assert recommender.recommendation[0]["type"] == "sealing_open_fireplace" + assert recommender.recommendation[0]["cost"] == 900 diff --git a/recommendations/tests/test_recommendation_utils.py b/recommendations/tests/test_recommendation_utils.py index eb1a5024..dc98d946 100644 --- a/recommendations/tests/test_recommendation_utils.py +++ b/recommendations/tests/test_recommendation_utils.py @@ -277,6 +277,17 @@ class TestRecommendationUtils: insulation_thickness=None, ) + def test_convert_thickness_to_numeric(self): + + assert recommendation_utils.convert_thickness_to_numeric("none") == 0 + assert recommendation_utils.convert_thickness_to_numeric("below average") == 50 + assert recommendation_utils.convert_thickness_to_numeric("average") == 100 + assert recommendation_utils.convert_thickness_to_numeric("above average") == 270 + + assert recommendation_utils.convert_thickness_to_numeric("300+") == 300 + assert recommendation_utils.convert_thickness_to_numeric("400+") == 400 + assert recommendation_utils.convert_thickness_to_numeric("270") == 270 + def test_estimate_perimeter_regular_inputs(): assert math.isclose( diff --git a/recommendations/tests/test_roof_recommendations.py b/recommendations/tests/test_roof_recommendations.py new file mode 100644 index 00000000..b5507421 --- /dev/null +++ b/recommendations/tests/test_roof_recommendations.py @@ -0,0 +1,39 @@ +from backend.Property import Property +from unittest.mock import Mock +from recommendations.RoofRecommendations import RoofRecommendations + +loft_insulation_materials = [ + { + 'id': 18, 'type': 'loft_insulation', 'description': 'Iso Spacesaver Mineral Wool insulation', + 'depths': [270, 300], 'depth_unit': 'mm', 'cost': [9, 10], 'cost_unit': 'gbp_sq_meter', + 'r_value_per_mm': 0.022727273, 'r_value_unit': 'square_meter_kelvin_per_watt', + 'thermal_conductivity': 0.044, 'thermal_conductivity_unit': 'watt_per_meter_kelvin', + 'link': 'https://flooringwarehousedirect.co.uk/product/isover-spacesaver-roll-100mm-x-1160mm-x-12-18m-14-13m2/', + 'is_active': True + } +] + + +class TestRoofRecommendations: + + def test_loft_insulation_recommendation_no_insulation(self): + property_instance = Property(id=0, address1="fake", postcode="fake", epc_client=Mock()) + property_instance.age_band = "F" + property_instance.floor_area = 100 + property_instance.roof = { + 'original_description': 'Pitched, no insulation (assumed)', + 'clean_description': 'Pitched, no insulation', + 'thermal_transmittance': None, + 'thermal_transmittance_unit': None, + 'is_pitched': True, 'is_roof_room': False, 'is_loft': False, 'is_flat': False, 'is_thatched': False, + 'is_at_rafters': False, 'is_assumed': True, 'has_dwelling_above': False, 'is_valid': True, + 'insulation_thickness': 'none', 'roof_thermal_transmittance': None, 'roof_insulation_thickness': 'none' + } + + roof_recommender = RoofRecommendations(property_instance=property_instance, materials=loft_insulation_materials) + + assert not roof_recommender.recommendations + + roof_recommender.recommend() + + assert len(roof_recommender.recommendations) From 0a549e6916963a42bba2bfc7410f90e02c9df0fd Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 24 Oct 2023 06:56:29 +1100 Subject: [PATCH 06/21] wrote complete loft insulation recommendations --- backend/app/plan/router.py | 2 +- recommendations/RoofRecommendations.py | 15 +- .../tests/test_roof_recommendations.py | 155 ++++++++++++++++++ 3 files changed, 168 insertions(+), 4 deletions(-) diff --git a/backend/app/plan/router.py b/backend/app/plan/router.py index 6171ddbf..59e6cc32 100644 --- a/backend/app/plan/router.py +++ b/backend/app/plan/router.py @@ -121,7 +121,7 @@ async def trigger_plan(body: PlanTriggerRequest): # TODO: Move this to a class. We probably want a Recommender class which takes the injects the optimisers # in as a dependency and then the optimisers can take the input measures in as part of the setup() method - + # import pickle # with open("input_properties.pickle", "rb") as f: # input_properties = pickle.load(f) diff --git a/recommendations/RoofRecommendations.py b/recommendations/RoofRecommendations.py index 06eab302..d364dea9 100644 --- a/recommendations/RoofRecommendations.py +++ b/recommendations/RoofRecommendations.py @@ -18,6 +18,9 @@ class RoofRecommendations: DIMINISHING_RETURNS_U_VALUE = 0.14 + # It is recommended that lofts should have at least 270mm of insulation + MINIMUM_LOFT_ISULATION_MM = 270 + def __init__( self, property_instance: Property, @@ -41,7 +44,7 @@ class RoofRecommendations: # Building regulations part L recommend installing at least 270mm of insulation, however generally we # experience diminishing returns in terms of SAP once we go beyond around 150mm of insulation - if insulation_thickness < 270: + if insulation_thickness >= self.MINIMUM_LOFT_ISULATION_MM: return # If we have a u-value already, need to implement this @@ -52,7 +55,7 @@ class RoofRecommendations: if self.property.roof["is_pitched"]: # We recommend loft insulation - self.recommend_loft_insulation(u_value) + self.recommend_loft_insulation(u_value, insulation_thickness) return raise NotImplementedError("Implement me") @@ -61,10 +64,12 @@ class RoofRecommendations: def make_loft_insulation_description(material, depth): return f"Install {depth}{material['depth_unit']} of {material['description']}" - def recommend_loft_insulation(self, u_value): + def recommend_loft_insulation(self, u_value, insulation_thickness): """ This method will recommend which insulation materials to use + :param u_value: U-value of the roof before any retrofit measures have been installed + :param insulation_thickness: Existing Insulation thickness of the loft :return: """ @@ -79,6 +84,10 @@ class RoofRecommendations: for material in loft_insulation_materials: for depth, cost_per_unit in zip(material["depths"], material["cost"]): + # We make sure we hit a depth of 270mm. We should factor in any existing insulation if the + # loft is already partially insulated + if (depth + insulation_thickness) < self.MINIMUM_LOFT_ISULATION_MM: + continue part_u_value = r_value_per_mm_to_u_value(depth, material["r_value_per_mm"]) diff --git a/recommendations/tests/test_roof_recommendations.py b/recommendations/tests/test_roof_recommendations.py index b5507421..8243a35b 100644 --- a/recommendations/tests/test_roof_recommendations.py +++ b/recommendations/tests/test_roof_recommendations.py @@ -13,6 +13,28 @@ loft_insulation_materials = [ } ] +loft_insulation_materials_50mm_existing = [ + { + 'id': 18, 'type': 'loft_insulation', 'description': 'Iso Spacesaver Mineral Wool insulation', + 'depths': [220, 210], 'depth_unit': 'mm', 'cost': [9, 10], 'cost_unit': 'gbp_sq_meter', + 'r_value_per_mm': 0.022727273, 'r_value_unit': 'square_meter_kelvin_per_watt', + 'thermal_conductivity': 0.044, 'thermal_conductivity_unit': 'watt_per_meter_kelvin', + 'link': 'https://flooringwarehousedirect.co.uk/product/isover-spacesaver-roll-100mm-x-1160mm-x-12-18m-14-13m2/', + 'is_active': True + } +] + +loft_insulation_materials_150mm_existing = [ + { + 'id': 18, 'type': 'loft_insulation', 'description': 'Iso Spacesaver Mineral Wool insulation', + 'depths': [130, 119], 'depth_unit': 'mm', 'cost': [9, 10], 'cost_unit': 'gbp_sq_meter', + 'r_value_per_mm': 0.022727273, 'r_value_unit': 'square_meter_kelvin_per_watt', + 'thermal_conductivity': 0.044, 'thermal_conductivity_unit': 'watt_per_meter_kelvin', + 'link': 'https://flooringwarehousedirect.co.uk/product/isover-spacesaver-roll-100mm-x-1160mm-x-12-18m-14-13m2/', + 'is_active': True + } +] + class TestRoofRecommendations: @@ -37,3 +59,136 @@ class TestRoofRecommendations: roof_recommender.recommend() assert len(roof_recommender.recommendations) + + def test_loft_insulation_recommendation_50mm_insulation(self): + property_instance2 = Property(id=0, address1="fake", postcode="fake", epc_client=Mock()) + property_instance2.age_band = "F" + property_instance2.floor_area = 100 + property_instance2.roof = { + 'original_description': 'Pitched, 50mm loft insulation (assumed)', + 'clean_description': 'Pitched, 50mm loft insulation', + 'thermal_transmittance': None, + 'thermal_transmittance_unit': None, + 'is_pitched': True, 'is_roof_room': False, 'is_loft': True, 'is_flat': False, 'is_thatched': False, + 'is_at_rafters': False, 'is_assumed': True, 'has_dwelling_above': False, 'is_valid': True, + 'insulation_thickness': '50', 'roof_thermal_transmittance': None, 'roof_insulation_thickness': 'none' + } + + roof_recommender2 = RoofRecommendations( + property_instance=property_instance2, materials=loft_insulation_materials + ) + + assert not roof_recommender2.recommendations + + roof_recommender2.recommend() + + assert len(roof_recommender2.recommendations) == 1 + + assert roof_recommender2.recommendations[0]["cost"] == 900 + assert roof_recommender2.recommendations[0]["new_u_value"] == 0.14 + assert roof_recommender2.recommendations[0]["starting_u_value"] == 0.68 + + property_instance3 = Property(id=0, address1="fake", postcode="fake", epc_client=Mock()) + property_instance3.age_band = "F" + property_instance3.floor_area = 100 + property_instance3.roof = { + 'original_description': 'Pitched, 50mm loft insulation (assumed)', + 'clean_description': 'Pitched, 50mm loft insulation', + 'thermal_transmittance': None, + 'thermal_transmittance_unit': None, + 'is_pitched': True, 'is_roof_room': False, 'is_loft': True, 'is_flat': False, 'is_thatched': False, + 'is_at_rafters': False, 'is_assumed': True, 'has_dwelling_above': False, 'is_valid': True, + 'insulation_thickness': '50', 'roof_thermal_transmittance': None, 'roof_insulation_thickness': 'none' + } + + roof_recommender3 = RoofRecommendations( + property_instance=property_instance3, materials=loft_insulation_materials_50mm_existing + ) + + assert not roof_recommender3.recommendations + + roof_recommender3.recommend() + + # The 220mm insulation should be selected, not the 210 + assert roof_recommender3.recommendations + assert len(roof_recommender3.recommendations) == 1 + assert roof_recommender3.recommendations[0]["parts"][0]["depths"] == [220] + + def test_loft_insulation_recommendation_150mm_insulation(self): + property_instance4 = Property(id=0, address1="fake", postcode="fake", epc_client=Mock()) + property_instance4.age_band = "F" + property_instance4.floor_area = 100 + property_instance4.roof = { + 'original_description': 'Pitched, 150mm loft insulation (assumed)', + 'clean_description': 'Pitched, 150mm loft insulation', + 'thermal_transmittance': None, + 'thermal_transmittance_unit': None, + 'is_pitched': True, 'is_roof_room': False, 'is_loft': True, 'is_flat': False, 'is_thatched': False, + 'is_at_rafters': False, 'is_assumed': True, 'has_dwelling_above': False, 'is_valid': True, + 'insulation_thickness': '150', 'roof_thermal_transmittance': None, 'roof_insulation_thickness': 'none' + } + + roof_recommender4 = RoofRecommendations( + property_instance=property_instance4, materials=loft_insulation_materials + ) + + assert not roof_recommender4.recommendations + + roof_recommender4.recommend() + + assert len(roof_recommender4.recommendations) == 1 + + assert roof_recommender4.recommendations[0]["cost"] == 900 + assert roof_recommender4.recommendations[0]["new_u_value"] == 0.11 + assert roof_recommender4.recommendations[0]["starting_u_value"] == 0.3 + + property_instance5 = Property(id=0, address1="fake", postcode="fake", epc_client=Mock()) + property_instance5.age_band = "F" + property_instance5.floor_area = 100 + property_instance5.roof = { + 'original_description': 'Pitched, 150mm loft insulation (assumed)', + 'clean_description': 'Pitched, 150mm loft insulation', + 'thermal_transmittance': None, + 'thermal_transmittance_unit': None, + 'is_pitched': True, 'is_roof_room': False, 'is_loft': True, 'is_flat': False, 'is_thatched': False, + 'is_at_rafters': False, 'is_assumed': True, 'has_dwelling_above': False, 'is_valid': True, + 'insulation_thickness': '150', 'roof_thermal_transmittance': None, 'roof_insulation_thickness': 'none' + } + + roof_recommender5 = RoofRecommendations( + property_instance=property_instance5, materials=loft_insulation_materials_150mm_existing + ) + + assert not roof_recommender5.recommendations + + roof_recommender5.recommend() + + # The 130mm insulation should be selected, not the 110 + assert roof_recommender5.recommendations + assert len(roof_recommender5.recommendations) == 1 + assert roof_recommender5.recommendations[0]["parts"][0]["depths"] == [130] + + def test_loft_insulation_recommendation_270mm_insulation(self): + # We shouldn't recommend anything in this case + property_instance6 = Property(id=0, address1="fake", postcode="fake", epc_client=Mock()) + property_instance6.age_band = "F" + property_instance6.floor_area = 100 + property_instance6.roof = { + 'original_description': 'Pitched, 270mm loft insulation (assumed)', + 'clean_description': 'Pitched, 270mm loft insulation', + 'thermal_transmittance': None, + 'thermal_transmittance_unit': None, + 'is_pitched': True, 'is_roof_room': False, 'is_loft': True, 'is_flat': False, 'is_thatched': False, + 'is_at_rafters': False, 'is_assumed': True, 'has_dwelling_above': False, 'is_valid': True, + 'insulation_thickness': '270', 'roof_thermal_transmittance': None, 'roof_insulation_thickness': 'none' + } + + roof_recommender6 = RoofRecommendations( + property_instance=property_instance6, materials=loft_insulation_materials + ) + + assert not roof_recommender6.recommendations + + roof_recommender6.recommend() + + assert len(roof_recommender6.recommendations) == 0 From 1228c680a6b9b9905a8d9e4faefed1ef521d7c73 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 24 Oct 2023 15:19:18 +0800 Subject: [PATCH 07/21] implemented unheated space floor u-value --- backend/Property.py | 21 +++++++++++++-- backend/app/plan/router.py | 8 +++--- etl/epc/property_change_app.py | 2 -- recommendations/FloorRecommendations.py | 2 ++ recommendations/rdsap_tables.py | 28 ++++++++++++++++++++ recommendations/recommendation_utils.py | 34 ++++++++++++++++++++++++- 6 files changed, 86 insertions(+), 9 deletions(-) diff --git a/backend/Property.py b/backend/Property.py index 045b6220..b0c6b083 100644 --- a/backend/Property.py +++ b/backend/Property.py @@ -597,9 +597,26 @@ class Property(Definitions): def set_floor_type(self): """ This method sets the floor type of the property, which is used for calculating u-values - :return: + + Section 5.6 of the BRE indicates that + "to simplify data collection no distinction is made in terms of U-value between an exposed floor (to + outside air below) and a semi-exposed floor (to an enclosed but unheated space below) + and the U-values in Table S12 are used. + + Therefore, we treat the exposed floor and suspended floor as the same type of floor, which is used for + calculating u-values """ - self.floor_type = "suspended" if self.floor["is_suspended"] else "solid" + + if self.floor["is_suspended"] | self.floor["another_property_below"]: + self.floor_type = "suspended" + elif self.floor["is_solid"]: + self.floor_type = "solid" + elif self.floor["is_to_unheated_space"] | self.floor["is_to_external_air"]: + self.floor_type = "exposed_floor" + elif self.floor["thermal_transmittance"] is not None: + self.floor_type = "solid" + else: + raise NotImplementedError("Implement this floor type") @staticmethod def _extract_component(component_data, component_rename_cols, component_drop_cols, rename_prefix=None): diff --git a/backend/app/plan/router.py b/backend/app/plan/router.py index 59e6cc32..d3ea3f83 100644 --- a/backend/app/plan/router.py +++ b/backend/app/plan/router.py @@ -121,7 +121,7 @@ async def trigger_plan(body: PlanTriggerRequest): # TODO: Move this to a class. We probably want a Recommender class which takes the injects the optimisers # in as a dependency and then the optimisers can take the input measures in as part of the setup() method - + # import pickle # with open("input_properties.pickle", "rb") as f: # input_properties = pickle.load(f) @@ -132,7 +132,7 @@ async def trigger_plan(body: PlanTriggerRequest): # import pickle # with open("cleaned.pickle", "rb") as f: - # cleaned = pickle.dump(f) + # cleaned = pickle.load(f) # with open("sap_dataset.pickle", "rb") as f: # sap_dataset = pickle.load(f) @@ -144,11 +144,11 @@ async def trigger_plan(body: PlanTriggerRequest): recommendations_scoring_data = [] for p in input_properties: - property_recommendations = [] - # Property recommendations p.get_components(cleaned) + property_recommendations = [] + # Floor recommendations floor_recommender = FloorRecommendations(property_instance=p, materials=materials_by_type["floor"]) floor_recommender.recommend() diff --git a/etl/epc/property_change_app.py b/etl/epc/property_change_app.py index 435b668d..4f49f6da 100644 --- a/etl/epc/property_change_app.py +++ b/etl/epc/property_change_app.py @@ -415,13 +415,11 @@ def app(): all_equal_rows = [] for directory in tqdm(directories): - filepath = directory / "certificates.csv" data_processor = DataProcessor(filepath=filepath) df = data_processor.pre_process() - df[df["WALLS_DESCRIPTION"].str.contains("Cavity")]["WALLS_DESCRIPTION"].unique() cleaning_averages = data_processor.make_cleaning_averages() diff --git a/recommendations/FloorRecommendations.py b/recommendations/FloorRecommendations.py index 35e34648..a20a4fe1 100644 --- a/recommendations/FloorRecommendations.py +++ b/recommendations/FloorRecommendations.py @@ -97,6 +97,8 @@ class FloorRecommendations(Definitions): # 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) + raise NotImplementedError("Implement me!") + @staticmethod def _make_floor_description(part, depth): return f"Install {depth}{part['depth_unit']} {part['description']} insulation" diff --git a/recommendations/rdsap_tables.py b/recommendations/rdsap_tables.py index 0ce139ab..e396f727 100644 --- a/recommendations/rdsap_tables.py +++ b/recommendations/rdsap_tables.py @@ -463,6 +463,34 @@ s11_list = [ table_s11 = pd.DataFrame(s11_list) +######################################################################################################################## +# Table s12 is used for assigning the u-values of floors to unheated spaces or external air +# which can be found on page 26 of the BRE document, section 5.6 +# https://bregroup.com/wp-content/uploads/2019/09/RdSAP_2012_9.94-20-09-2019.pdf +# +# the insulation_{thickness} fields indicate the u-value at that insulation thickness +######################################################################################################################## + +s12_list = [ + {"age_band": "A", "insulation_0": 1.2, "insulation_50": 0.5, "insulation_100": 0.3, "insulation_150": 0.22}, + {"age_band": "B", "insulation_0": 1.2, "insulation_50": 0.5, "insulation_100": 0.3, "insulation_150": 0.22}, + {"age_band": "C", "insulation_0": 1.2, "insulation_50": 0.5, "insulation_100": 0.3, "insulation_150": 0.22}, + {"age_band": "D", "insulation_0": 1.2, "insulation_50": 0.5, "insulation_100": 0.3, "insulation_150": 0.22}, + {"age_band": "E", "insulation_0": 1.2, "insulation_50": 0.5, "insulation_100": 0.3, "insulation_150": 0.22}, + {"age_band": "F", "insulation_0": 1.2, "insulation_50": 0.5, "insulation_100": 0.3, "insulation_150": 0.22}, + {"age_band": "G", "insulation_0": 1.2, "insulation_50": 0.5, "insulation_100": 0.3, "insulation_150": 0.22}, + + {"age_band": "H", "insulation_0": 0.51, "insulation_50": 0.5, "insulation_100": 0.3, "insulation_150": 0.22}, + {"age_band": "I", "insulation_0": 0.51, "insulation_50": 0.5, "insulation_100": 0.3, "insulation_150": 0.22}, + + {"age_band": "J", "insulation_0": 0.25, "insulation_50": 0.25, "insulation_100": 0.25, "insulation_150": 0.22}, + + {"age_band": "K", "insulation_0": 0.22, "insulation_50": 0.22, "insulation_100": 0.22, "insulation_150": 0.22}, + {"age_band": "L", "insulation_0": 0.22, "insulation_50": 0.22, "insulation_100": 0.22, "insulation_150": 0.22}, +] + +table_s12 = pd.DataFrame(s12_list) + ######################################################################################################################## # diff --git a/recommendations/recommendation_utils.py b/recommendations/recommendation_utils.py index 67550fc8..8e67a384 100644 --- a/recommendations/recommendation_utils.py +++ b/recommendations/recommendation_utils.py @@ -5,7 +5,7 @@ import pandas as pd from recommendations.rdsap_tables import ( epc_wall_description_map, wall_uvalues_df, default_wall_thickness, table_s9 as s9, table_s10 as s10, - table_s11 as s11 + table_s11 as s11, table_s12 as s12 ) from recommendations.config import PARTIALLY_FILLED_PERCENTAGE_ASSUMPTION, PARTIAL_CAVITY_DESCRIPTIONS @@ -340,6 +340,32 @@ def estimate_perimeter(floor_area, num_rooms): return perimeter +def get_exposed_floor_uvalue(insulation_thickness_str, age_band): + """ + We implement the methodology as defined in section 5.6 and table S12 of the RdSAP document + :param insulation_thickness_str: + :return: + """ + + if age_band in ["A", "B", "C", "D", "E", "F", "G", "H", "I"]: + # As directed by the documentation, if the insulation thickness is not known, we assume it's + # 50mm for these age bands + if insulation_thickness_str in ["below_average", "average", "above_average"]: + insulation_thickness = 50 + elif insulation_thickness_str in ["none", None]: + insulation_thickness = 0 + elif insulation_thickness_str in ["below_average"]: + insulation_thickness = 50 + elif insulation_thickness_str == "average": + insulation_thickness = 100 + elif insulation_thickness_str == "above_average": + insulation_thickness = 150 + else: + insulation_thickness = int(insulation_thickness_str.replace("mm", "")) + + return s12[s12["age_band"] == age_band][f"insulation_{insulation_thickness}"].values[0] + + def get_floor_u_value(floor_type, area, perimeter, age_band, wall_type, insulation_thickness=None): """ Estimate the u-value of a suspended floor, based on RdSap methodology @@ -372,6 +398,12 @@ def get_floor_u_value(floor_type, area, perimeter, age_band, wall_type, insulati 0.701 """ + if floor_type == "exposed_floor": + # In this case, we extract the u-value from table s12 + # See section 5.6 of the RdSAP document for more details + # https://bregroup.com/wp-content/uploads/2019/09/RdSAP_2012_9.94-20-09-2019.pdf + return get_exposed_floor_uvalue(insulation_thickness, age_band) + # Cleans our regularly inputted insulation thickness for usage in this function insulation_thickness = extract_insulation_thickness(insulation_thickness) From 448c6a377239d33feb1cc8e732eaf10cf5f787ef Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 24 Oct 2023 16:02:08 +0800 Subject: [PATCH 08/21] added exposed floor insulation --- backend/app/plan/utils.py | 2 +- recommendations/FloorRecommendations.py | 14 ++++++++++---- recommendations/recommendation_utils.py | 12 +++++++----- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/backend/app/plan/utils.py b/backend/app/plan/utils.py index e056c61c..71a61be1 100644 --- a/backend/app/plan/utils.py +++ b/backend/app/plan/utils.py @@ -15,7 +15,7 @@ def filter_materials(materials): mapping = { "walls": ["internal_wall_insulation", "external_wall_insulation", "cavity_wall_insulation"], - "floor": ["suspended_floor_insulation", "solid_floor_insulation"], + "floor": ["suspended_floor_insulation", "solid_floor_insulation", "exposed_floor_insulation"], "ventilation": ["mechanical_ventilation"], "roof": ["loft_insulation"] } diff --git a/recommendations/FloorRecommendations.py b/recommendations/FloorRecommendations.py index a20a4fe1..2524d25f 100644 --- a/recommendations/FloorRecommendations.py +++ b/recommendations/FloorRecommendations.py @@ -45,10 +45,13 @@ class FloorRecommendations(Definitions): part for part in self.materials if part["type"] == "solid_floor_insulation" ] + self.exposed_floor_insulation_parts = [ + part for part in self.materials if part["type"] == "exposed_floor_insulation" + ] + def recommend(self): u_value = self.property.floor["thermal_transmittance"] - is_suspended = self.property.floor["is_suspended"] - is_solid = self.property.floor["is_solid"] + floor_level = ( FLOOR_LEVEL_MAP[self.property.data["floor-level"]] if self.property.data["floor-level"] not in self.DATA_ANOMALY_MATCHES else None @@ -89,14 +92,17 @@ class FloorRecommendations(Definitions): ) self.estimated_u_value = u_value - if is_suspended: + if self.property.floor["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: + if self.property.floor["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) + if self.property.floor["is_to_unheated_space"] or self.property.floor["is_to_external_air"]: + self.recommend_floor_insulation(u_value=u_value, parts=self.exposed_floor_insulation_parts) + raise NotImplementedError("Implement me!") @staticmethod diff --git a/recommendations/recommendation_utils.py b/recommendations/recommendation_utils.py index 8e67a384..74910e07 100644 --- a/recommendations/recommendation_utils.py +++ b/recommendations/recommendation_utils.py @@ -347,11 +347,13 @@ def get_exposed_floor_uvalue(insulation_thickness_str, age_band): :return: """ - if age_band in ["A", "B", "C", "D", "E", "F", "G", "H", "I"]: - # As directed by the documentation, if the insulation thickness is not known, we assume it's - # 50mm for these age bands - if insulation_thickness_str in ["below_average", "average", "above_average"]: - insulation_thickness = 50 + unknown_insulation_age_bands = ["A", "B", "C", "D", "E", "F", "G", "H", "I"] + # As directed by the documentation, if the insulation thickness is not known, we assume it's + # 50mm for these age bands + if insulation_thickness_str in ["below_average", "average", "above_average"] and ( + age_band in unknown_insulation_age_bands + ): + insulation_thickness = 50 elif insulation_thickness_str in ["none", None]: insulation_thickness = 0 elif insulation_thickness_str in ["below_average"]: From 0c5ff1153c21a3d1540f821380e75f223ec4fc45 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 24 Oct 2023 16:27:38 +0800 Subject: [PATCH 09/21] unit tests for unheated space floor insulation --- recommendations/FloorRecommendations.py | 14 +- recommendations/recommendation_utils.py | 6 +- .../tests/test_floor_recommendations.py | 142 +++++++++++++++++- 3 files changed, 155 insertions(+), 7 deletions(-) diff --git a/recommendations/FloorRecommendations.py b/recommendations/FloorRecommendations.py index 2524d25f..a78d984e 100644 --- a/recommendations/FloorRecommendations.py +++ b/recommendations/FloorRecommendations.py @@ -58,6 +58,7 @@ class FloorRecommendations(Definitions): ) property_type = self.property.data["property-type"] + floor_area = self.property.floor_area / self.property.number_of_storeys year_built = self.property.year_built if self.property.floor["another_property_below"] | (self.property.floor["insulation_thickness"] in [ @@ -84,7 +85,7 @@ class FloorRecommendations(Definitions): u_value = get_floor_u_value( floor_type=self.property.floor_type, - area=float(self.property.data["total-floor-area"]), + area=floor_area, perimeter=self.property.perimeter, age_band=self.property.age_band, insulation_thickness=self.property.floor["insulation_thickness"], @@ -92,16 +93,22 @@ class FloorRecommendations(Definitions): ) self.estimated_u_value = u_value + if u_value < self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE: + return + if self.property.floor["is_suspended"]: # Given the U-value, we recommend underfloor insulation self.recommend_floor_insulation(u_value=u_value, parts=self.suspended_floor_insulation_parts) + return if self.property.floor["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) + return if self.property.floor["is_to_unheated_space"] or self.property.floor["is_to_external_air"]: self.recommend_floor_insulation(u_value=u_value, parts=self.exposed_floor_insulation_parts) + return raise NotImplementedError("Implement me!") @@ -130,8 +137,9 @@ class FloorRecommendations(Definitions): 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) + quantity = self.property.floor_area / self.property.number_of_storeys - estimated_cost = cost_per_unit * self.property.floor_area + estimated_cost = cost_per_unit * quantity self.recommendations.append( { @@ -139,7 +147,7 @@ class FloorRecommendations(Definitions): get_recommended_part( part=part, selected_depth=depth, - quantity=self.property.floor_area, + quantity=quantity, quantity_unit=QuantityUnits.m2.value, selected_total_cost=estimated_cost ), diff --git a/recommendations/recommendation_utils.py b/recommendations/recommendation_utils.py index 74910e07..9205cec7 100644 --- a/recommendations/recommendation_utils.py +++ b/recommendations/recommendation_utils.py @@ -350,17 +350,17 @@ def get_exposed_floor_uvalue(insulation_thickness_str, age_band): unknown_insulation_age_bands = ["A", "B", "C", "D", "E", "F", "G", "H", "I"] # As directed by the documentation, if the insulation thickness is not known, we assume it's # 50mm for these age bands - if insulation_thickness_str in ["below_average", "average", "above_average"] and ( + if insulation_thickness_str in ["below average", "average", "above average"] and ( age_band in unknown_insulation_age_bands ): insulation_thickness = 50 elif insulation_thickness_str in ["none", None]: insulation_thickness = 0 - elif insulation_thickness_str in ["below_average"]: + elif insulation_thickness_str == "below average": insulation_thickness = 50 elif insulation_thickness_str == "average": insulation_thickness = 100 - elif insulation_thickness_str == "above_average": + elif insulation_thickness_str == "above average": insulation_thickness = 150 else: insulation_thickness = int(insulation_thickness_str.replace("mm", "")) diff --git a/recommendations/tests/test_floor_recommendations.py b/recommendations/tests/test_floor_recommendations.py index f34bbe81..cad5fe24 100644 --- a/recommendations/tests/test_floor_recommendations.py +++ b/recommendations/tests/test_floor_recommendations.py @@ -3,6 +3,7 @@ import pytest import os from unittest.mock import Mock from recommendations.FloorRecommendations import FloorRecommendations +from backend.Property import Property # with open( # os.path.abspath(os.path.dirname(__file__)) + "/recommendations/tests/test_data/input_properties.pkl", "rb" @@ -67,7 +68,23 @@ solid_floor_insulation_parts = [ ] -parts = suspended_floor_insulation_parts + solid_floor_insulation_parts +exposed_floor_insulation_parts = [ + { + "type": "exposed_floor_insulation", + "description": "Rockwool Stone Wool insulation", + "depths": [50, 100, 140], + "depth_unit": "mm", + "cost": [8, 11, 15], + "cost_unit": "gbp_sq_meter", + "r_value_per_mm": 0.026315789473684213, + "r_value_unit": "square_meter_kelvin_per_watt", + "thermal_conductivity": 0.038, + "thermal_conductivity_unit": "watt_per_meter_kelvin", + "link": "https://insulation4less.co.uk/products/rockwool-flexi-slab-all-sizes?variant=33409590853685" + }, +] + +parts = suspended_floor_insulation_parts + solid_floor_insulation_parts + exposed_floor_insulation_parts class TestFloorRecommendations: @@ -119,6 +136,7 @@ class TestFloorRecommendations: input_properties[2].perimeter = 20 input_properties[2].wall_type = "solid brick" input_properties[2].floor_type = "suspended" + input_properties[2].number_of_storeys = 1 recommender = FloorRecommendations( property_instance=input_properties[2], @@ -162,6 +180,7 @@ class TestFloorRecommendations: input_properties[4].perimeter = 50 input_properties[4].wall_type = "solid brick" input_properties[4].floor_type = "solid" + input_properties[4].number_of_storeys = 1 recommender = FloorRecommendations( property_instance=input_properties[4], @@ -193,3 +212,124 @@ class TestFloorRecommendations: assert not recommender.property.floor["is_solid"] assert recommender.estimated_u_value is None assert not recommender.recommendations + + def test_exposed_floor_no_insulation(self): + input_property = Property(id=1, postcode="F4k3 2", address1="223 fake street", epc_client=Mock()) + input_property.floor = { + 'original_description': 'To unheated space, no insulation (assumed)', + 'clean_description': 'To unheated space, no insulation', 'thermal_transmittance': None, + 'thermal_transmittance_unit': None, 'is_assumed': True, 'is_to_unheated_space': True, + 'is_to_external_air': False, 'is_suspended': False, 'is_solid': False, 'another_property_below': False, + 'insulation_thickness': 'none' + } + input_property.age_band = "L" + input_property.set_floor_type() + input_property.data = {"floor-level": 0, "property-type": "House"} + input_property.floor_area = 100 + input_property.number_of_storeys = 1 + + recommender = FloorRecommendations( + property_instance=input_property, + materials=exposed_floor_insulation_parts + ) + + assert not recommender.recommendations + + recommender.recommend() + + # Because of age band L, this should have a u-value of 0.22 to begin with and no recommendation + assert not len(recommender.recommendations) + assert recommender.estimated_u_value == 0.22 + + # Now with an older age band + + input_property2 = Property(id=1, postcode="F4k3 2", address1="223 fake street", epc_client=Mock()) + input_property2.floor = { + 'original_description': 'To unheated space, no insulation (assumed)', + 'clean_description': 'To unheated space, no insulation', 'thermal_transmittance': None, + 'thermal_transmittance_unit': None, 'is_assumed': True, 'is_to_unheated_space': True, + 'is_to_external_air': False, 'is_suspended': False, 'is_solid': False, 'another_property_below': False, + 'insulation_thickness': 'none' + } + input_property2.age_band = "D" + input_property2.set_floor_type() + input_property2.data = {"floor-level": 0, "property-type": "House"} + input_property2.floor_area = 100 + input_property2.number_of_storeys = 1 + + recommender2 = FloorRecommendations( + property_instance=input_property2, + materials=exposed_floor_insulation_parts + ) + + assert not recommender2.recommendations + + recommender2.recommend() + + assert len(recommender2.recommendations) == 1 + + assert recommender2.recommendations[0]["new_u_value"] == 0.23 + assert recommender2.recommendations[0]["starting_u_value"] == 1.2 + assert recommender2.recommendations[0]["cost"] == 1500 + + def test_exposed_floor_below_average_insulated(self): + input_property3 = Property(id=1, postcode="F4k3 2", address1="223 fake street", epc_client=Mock()) + input_property3.floor = { + 'original_description': 'To unheated space, below average insulation (assumed)', + 'clean_description': 'To unheated space, below average insulation', 'thermal_transmittance': None, + 'thermal_transmittance_unit': None, 'is_assumed': True, 'is_to_unheated_space': True, + 'is_to_external_air': False, 'is_suspended': False, 'is_solid': False, 'another_property_below': False, + 'insulation_thickness': 'below average' + } + input_property3.age_band = "C" + input_property3.set_floor_type() + input_property3.data = {"floor-level": 0, "property-type": "House"} + input_property3.floor_area = 100 + input_property3.number_of_storeys = 1 + + recommender3 = FloorRecommendations( + property_instance=input_property3, + materials=exposed_floor_insulation_parts + ) + + assert not recommender3.recommendations + + recommender3.recommend() + + assert recommender3.estimated_u_value == 0.5 + + assert len(recommender3.recommendations) == 1 + + assert recommender3.recommendations[0]["new_u_value"] == 0.22 + assert recommender3.recommendations[0]["starting_u_value"] == 0.5 + assert recommender3.recommendations[0]["cost"] == 1100 + assert recommender3.recommendations[0]["parts"][0]["depths"] == [100] + + # With average insulation, no recommendations + + input_property4 = Property(id=1, postcode="F4k3 2", address1="223 fake street", epc_client=Mock()) + input_property4.floor = { + 'original_description': 'To unheated space, insulated (assumed)', + 'clean_description': 'To unheated space, insulated', 'thermal_transmittance': None, + 'thermal_transmittance_unit': None, 'is_assumed': True, 'is_to_unheated_space': True, + 'is_to_external_air': False, 'is_suspended': False, 'is_solid': False, 'another_property_below': False, + 'insulation_thickness': 'average' + } + input_property4.age_band = "C" + input_property4.set_floor_type() + input_property4.data = {"floor-level": 0, "property-type": "House"} + input_property4.floor_area = 100 + input_property4.number_of_storeys = 1 + + recommender4 = FloorRecommendations( + property_instance=input_property4, + materials=exposed_floor_insulation_parts + ) + + assert not recommender4.recommendations + + recommender4.recommend() + + assert recommender4.estimated_u_value is None + + assert len(recommender4.recommendations) == 0 From 22a0bb1ffd9867334fd888059c28be0f9284e66f Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 24 Oct 2023 17:32:48 +0800 Subject: [PATCH 10/21] insulation of floor to unheated space complete --- backend/app/plan/router.py | 11 +++++++++++ backend/tests/test_sap_model_prep.py | 25 ++++++++++++------------- recommendations/FloorRecommendations.py | 4 ++-- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/backend/app/plan/router.py b/backend/app/plan/router.py index d3ea3f83..23ad4262 100644 --- a/backend/app/plan/router.py +++ b/backend/app/plan/router.py @@ -140,6 +140,17 @@ async def trigger_plan(body: PlanTriggerRequest): # with open("materials_by_type", "rb") as f: # materials_by_type = pickle.load(f) + # materials_by_type["floor"].append( + # {'id': 18, 'type': 'exposed_floor_insulation', 'description': 'Rockwool Stone Wool insulation', + # 'depths': [50, 100, 140], 'depth_unit': 'mm', 'cost': [8, 11, 15], + # 'cost_unit': 'gbp_sq_meter', 'r_value_per_mm': 0.026315789473684213, + # 'r_value_unit': 'square_meter_kelvin_per_watt', + # 'thermal_conductivity': 0.038, 'thermal_conductivity_unit': 'watt_per_meter_kelvin', + # 'link': 'https://insulation4less.co.uk/products/rockwool-flexi-slab-all-sizes?variant=33409590853685', + # 'created_at': datetime(2023, 8, 10, 16, 59, 10, 815531), 'is_active': True} + # + # ) + recommendations = {} recommendations_scoring_data = [] diff --git a/backend/tests/test_sap_model_prep.py b/backend/tests/test_sap_model_prep.py index dc793cad..da38cabf 100644 --- a/backend/tests/test_sap_model_prep.py +++ b/backend/tests/test_sap_model_prep.py @@ -10,23 +10,22 @@ from utils.s3 import read_dataframe_from_s3_parquet from tqdm import tqdm -# Handy code for selecting testin data +# Handy code for selecting testing data # import pickle # -# with open("sap_change_dataset.pickle", "rb") as f: +# with open("sap_dataset.pickle", "rb") as f: # sap_change_dataset = pickle.load(f) # # search_from = sap_change_dataset[ -# (sap_change_dataset["walls_thermal_transmittance_ENDING"] == sap_change_dataset["walls_thermal_transmittance"]) -# ] +# (sap_change_dataset["walls_thermal_transmittance_ENDING"] == sap_change_dataset["walls_thermal_transmittance"]) & +# sap_change_dataset["is_to_unheated_space"] +# ] # search_from = search_from[ # (search_from["roof_thermal_transmittance_ENDING"] == search_from["roof_thermal_transmittance"]) & -# (search_from["floor_thermal_transmittance_ENDING"] == search_from["floor_thermal_transmittance"]) & +# (search_from["floor_thermal_transmittance_ENDING"] != search_from["floor_thermal_transmittance"]) & # (search_from["MECHANICAL_VENTILATION_ENDING"] == search_from["MECHANICAL_VENTILATION_STARTING"]) & # (search_from["SECONDHEAT_DESCRIPTION_ENDING"] == search_from["SECONDHEAT_DESCRIPTION_STARTING"]) & -# (search_from["GLAZED_TYPE_ENDING"] == search_from["GLAZED_TYPE_STARTING"]) & -# (search_from["NUMBER_OPEN_FIREPLACES_STARTING"] > 0) & -# (search_from["NUMBER_OPEN_FIREPLACES_ENDING"] == 0) +# (search_from["GLAZED_TYPE_ENDING"] == search_from["GLAZED_TYPE_STARTING"]) # ] # # # Find a record where the only difference is cavity wall getting filled @@ -54,7 +53,7 @@ from tqdm import tqdm # starting_cols.append(starting_col) # # # We want them to be different -# if c == "NUMBER_OPEN_FIREPLACES_ENDING": +# if c == "floor_thermal_transmittance_ENDING": # if (row[c] == row[starting_col]) | (row[starting_col] != "natural"): # same = False # break @@ -82,11 +81,11 @@ from tqdm import tqdm # # compare = pd.concat([start, end], axis=1) # -# ending_lmk = "bab3983fa167717b8bb4a36ef395046d53937f9b880a45bcc751270d72e5de45" -# starting_lmk = "736b6f4803a11d9e45b49bf98f36eb8a7f357b0dd24f3e7cddef5295518e5bef" +# ending_lmk = "1252008839062019090910572351658131" +# starting_lmk = "1252008819542014122308482236142128" # # client = EpcClient(auth_token=EPC_AUTH_TOKEN) -# result = client.domestic.search(params={"address": "9 Glebe Road, Asfordby Hill", "postcode": "LE14 3QT"}) +# result = client.domestic.search(params={"address": "Flat 14 Charles House, Freemens Way", "postcode": "CT14 9DL"}) # starting_epc = [x for x in result["rows"] if x["lmk-key"] == starting_lmk][0] # ending_epc = [x for x in result["rows"] if x["lmk-key"] == ending_lmk][0] @@ -101,7 +100,7 @@ from tqdm import tqdm # ) as f: # cleaning_data = pickle.load(f) -# TODO: Need to do floors, both suspended and solid +# TODO: Need to do floors, suspended and solid and to unheated space class TestSapModelPrep: diff --git a/recommendations/FloorRecommendations.py b/recommendations/FloorRecommendations.py index a78d984e..bc24b6c3 100644 --- a/recommendations/FloorRecommendations.py +++ b/recommendations/FloorRecommendations.py @@ -58,7 +58,7 @@ class FloorRecommendations(Definitions): ) property_type = self.property.data["property-type"] - floor_area = self.property.floor_area / self.property.number_of_storeys + floor_area = self.property.floor_area / self.property.number_of_floors year_built = self.property.year_built if self.property.floor["another_property_below"] | (self.property.floor["insulation_thickness"] in [ @@ -137,7 +137,7 @@ class FloorRecommendations(Definitions): 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) - quantity = self.property.floor_area / self.property.number_of_storeys + quantity = self.property.floor_area / self.property.number_of_floors estimated_cost = cost_per_unit * quantity From c96fafa70138ca69be4718537e8cb0a8997a6c4e Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 13 Nov 2023 15:57:51 +0900 Subject: [PATCH 11/21] basic implementation of pitched roof area estimate --- backend/Property.py | 9 +++++- recommendations/RoofRecommendations.py | 23 +++++++++++++- recommendations/recommendation_utils.py | 31 +++++++++++++++++++ .../tests/test_roof_recommendations.py | 22 +++++++++++++ 4 files changed, 83 insertions(+), 2 deletions(-) diff --git a/backend/Property.py b/backend/Property.py index b0c6b083..1094e7b2 100644 --- a/backend/Property.py +++ b/backend/Property.py @@ -11,7 +11,9 @@ from utils.s3 import read_dataframe_from_s3_parquet from epc_api.client import EpcClient from BaseUtility import Definitions from recommendations.rdsap_tables import england_wales_age_band_lookup -from recommendations.recommendation_utils import estimate_floors, estimate_perimeter, get_wall_type, estimate_wall_area +from recommendations.recommendation_utils import ( + estimate_floors, estimate_perimeter, get_wall_type, estimate_wall_area, esimtate_pitched_roof_area +) ENVIRONMENT = os.environ.get('ENVIRONMENT', 'dev') EPC_AUTH_TOKEN = os.environ.get('EPC_AUTH_TOKEN') @@ -79,6 +81,7 @@ class Property(Definitions): self.floor_height = None self.insulation_wall_area = None self.floor_area = None + self.pitched_roof_area = None if epc_client: self.epc_client = epc_client @@ -587,6 +590,10 @@ class Property(Definitions): num_floors=self.number_of_floors, floor_height=self.floor_height, perimeter=self.perimeter ) + self.pitched_roof_area = esimtate_pitched_roof_area( + floor_area=self.floor_area / self.number_of_floors, floor_height=self.floor_height + ) + def set_wall_type(self): """ This method sets the wall type of the property, using a simple approach based on the wall description diff --git a/recommendations/RoofRecommendations.py b/recommendations/RoofRecommendations.py index d364dea9..cf7c2a0f 100644 --- a/recommendations/RoofRecommendations.py +++ b/recommendations/RoofRecommendations.py @@ -54,10 +54,13 @@ class RoofRecommendations: u_value = get_roof_u_value(**{**self.property.roof, "age_band": self.property.age_band}) if self.property.roof["is_pitched"]: - # We recommend loft insulation self.recommend_loft_insulation(u_value, insulation_thickness) return + if self.property.roof["is_roof_room"]: + self.recommend_room_roof_insulation(u_value, insulation_thickness) + return + raise NotImplementedError("Implement me") @staticmethod @@ -132,3 +135,21 @@ class RoofRecommendations: ) self.recommendations = recommendations + + def recommend_room_roof_insulation(self, u_value, insulation_thickness): + """ + This method recommends room in roof insulation for properties that have been identified + to possess a room in roof. + + Because we currently have limited data about the construction of the roof, we make the following + assumptions: + 1) The room in roof has a sloped roof. + We will make some basic estimations about the area of the roof given the floor area and the height of the + floors + 2) Insulation of external walls is covered by the wall recommendation class + 3) We assume a "Gable" roof type + + :param u_value: + :param insulation_thickness: + :return: + """ diff --git a/recommendations/recommendation_utils.py b/recommendations/recommendation_utils.py index 9205cec7..954998f4 100644 --- a/recommendations/recommendation_utils.py +++ b/recommendations/recommendation_utils.py @@ -1,6 +1,7 @@ import math from copy import deepcopy +import numpy as np import pandas as pd from recommendations.rdsap_tables import ( @@ -594,3 +595,33 @@ def convert_thickness_to_numeric(string_thickness): return int(string_thickness.replace("+", "")) return int(string_thickness) + + +def esimtate_pitched_roof_area(floor_area: float, floor_height: float) -> float: + """ + This function will estimate the area of a pitched roof, given the floor area below the roof and the floor + height of the property. + + Given limited information about the home, this is a very rough method to estimate the roof area and we + assume the the room is a gable roof. + + We assume a roughly average pitch of 45 degrees + + Note that both floor area and height should be in the same units. E.g. if floor area is meters squared, + floor height should be in meters + + :param floor_area: area of the home's floor + :param floor_height: height of the home's floors + :return: Numerical estimate of the surface area of the top of the pitched roof + """ + + # We estimate the length of the wall by just modelling the house as a square + wall_width = np.sqrt(floor_area) + + # We're modelling the roof as two triangles where we know two of the three sides. + # The floor height makes up one side and half of the wall width makes up the other side + slope = np.sqrt(np.square(wall_width / 2) + np.square(floor_height)) + + area = 2 * (slope * wall_width) + + return area diff --git a/recommendations/tests/test_roof_recommendations.py b/recommendations/tests/test_roof_recommendations.py index 8243a35b..b822f829 100644 --- a/recommendations/tests/test_roof_recommendations.py +++ b/recommendations/tests/test_roof_recommendations.py @@ -192,3 +192,25 @@ class TestRoofRecommendations: roof_recommender6.recommend() assert len(roof_recommender6.recommendations) == 0 + + def test_uninsulated_room_in_roof(self): + property_instance7 = Property(id=0, address1="fake", postcode="fake", epc_client=Mock()) + property_instance7.age_band = "F" + property_instance7.floor_area = 100 + property_instance7.roof = { + 'original_description': 'Roof room(s), no insulation (assumed)', + 'clean_description': 'Roof room(s), no insulation', + 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': False, + 'is_roof_room': True, 'is_loft': False, 'is_flat': False, 'is_thatched': False, 'is_at_rafters': False, + 'is_assumed': True, 'has_dwelling_above': False, 'is_valid': True, 'insulation_thickness': 'none' + } + + property_instance7 + + roof_recommender7 = RoofRecommendations( + property_instance=property_instance7, materials=loft_insulation_materials + ) + + assert not roof_recommender7.recommendations + + roof_recommender7.recommend() From 15e48c216596c417640635162e63c7860f6ebf99 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 13 Nov 2023 20:58:00 +0900 Subject: [PATCH 12/21] wrote unit tests for room roof insulation --- recommendations/RoofRecommendations.py | 96 +++++++++++++- .../tests/test_recommendation_utils.py | 56 +++++++++ .../tests/test_roof_recommendations.py | 118 +++++++++++++++++- 3 files changed, 266 insertions(+), 4 deletions(-) diff --git a/recommendations/RoofRecommendations.py b/recommendations/RoofRecommendations.py index cf7c2a0f..a33a709b 100644 --- a/recommendations/RoofRecommendations.py +++ b/recommendations/RoofRecommendations.py @@ -67,6 +67,10 @@ class RoofRecommendations: def make_loft_insulation_description(material, depth): return f"Install {depth}{material['depth_unit']} of {material['description']}" + @staticmethod + def make_room_roof_insulation(material, depth): + return f"Insulate your room roof with {depth}{material['depth_unit']} of {material['description']}" + def recommend_loft_insulation(self, u_value, insulation_thickness): """ @@ -81,6 +85,8 @@ class RoofRecommendations: # from the base layer loft_insulation_materials = [m for m in self.materials if m["type"] == "loft_insulation"] + if not loft_insulation_materials: + raise ValueError("No loft insulation materials found") lowest_selected_u_value = None recommendations = [] @@ -149,7 +155,93 @@ class RoofRecommendations: 2) Insulation of external walls is covered by the wall recommendation class 3) We assume a "Gable" roof type - :param u_value: - :param insulation_thickness: + Further, we recommend internal roof insulation for the room in roof + + The following document contains details around best practices for insulating a room in roof + https://assets.publishing.service.gov.uk/media/61d727d18fa8f50594b59305/retrofit-room-in-roof-insulation-best + -practice.pdf + Of particular interest are the following: + + We also follow advide provided in this article on the Energy Saving Trust website, providing + high level guidance around roof insulation: + https://energysavingtrust.org.uk/advice/roof-and-loft-insulation/ + + To insulate a warm loft, the following advice is given + "An alternative way to insulate your loft is to fit rigid insulation boards between and over the rafters. + Rafters are the sloping timbers that make up the roof itself." + + To then insulate a room roof, the following recommendation is provided: + "If you want to use your loft as a living space, or it is already being used as a living space, + then you need to make sure that all the walls and ceilings between a heated room and an unheated space + are insulated. + + - Sloping ceilings can be insulated in the same way as for a warm roof, + but with a layer of plasterboard on the inside of the insulation. + - Vertical walls can be insulated in the same way. + - Flat ceilings can be insulated like a standard loft. + " + + :param u_value: Current u-value of the roof + :param insulation_thickness: Current insulation thickness of the roof :return: """ + + roof_roof_insulation_materials = [m for m in self.materials if m["type"] == "room_roof_insulation"] + if not roof_roof_insulation_materials: + raise ValueError("No room in roof insulation materials found") + + if self.property.pitched_roof_area is None: + raise ValueError("pitched_roof_area not included as property attribute") + + lowest_selected_u_value = None + recommendations = [] + for material in roof_roof_insulation_materials: + for depth, cost_per_unit in zip(material["depths"], material["cost"]): + # We make sure we hit a depth of 270mm. We should factor in any existing insulation if the + # loft is already partially insulated + if (depth + insulation_thickness) < self.MINIMUM_LOFT_ISULATION_MM: + continue + + part_u_value = r_value_per_mm_to_u_value(depth, material["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 I have a lowest U value and my new u value is higher than that but lower than the + # diminishing returns threshold, it can be considered + + # If I have a lowest U value and my new u value is lower than the lowest value, it's + # further into the diminishing returns threshold and can shouldn't be + + if is_diminishing_returns( + recommendations, new_u_value, lowest_selected_u_value, self.DIMINISHING_RETURNS_U_VALUE + ): + continue + + # We allow a small tolerance for error so we don't discount the recommendation entirely + 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.pitched_roof_area + + recommendations.append( + { + "parts": [ + get_recommended_part( + part=material, + selected_depth=depth, + quantity=self.property.pitched_roof_area, + quantity_unit=QuantityUnits.m2.value, + selected_total_cost=estimated_cost + ) + ], + "type": "roof_insulation", + "description": self.make_room_roof_insulation(material, depth), + "starting_u_value": u_value, + "new_u_value": new_u_value, + "sap_points": None, + "cost": estimated_cost, + } + ) + + self.recommendations = recommendations diff --git a/recommendations/tests/test_recommendation_utils.py b/recommendations/tests/test_recommendation_utils.py index dc98d946..b0c8a26b 100644 --- a/recommendations/tests/test_recommendation_utils.py +++ b/recommendations/tests/test_recommendation_utils.py @@ -1,3 +1,4 @@ +import numpy as np import pytest import math from unittest.mock import MagicMock @@ -344,3 +345,58 @@ def test_park_home(): assert recommendation_utils.get_floor_u_value( 'suspended', 100, 40, 'A', 'park home', insulation_thickness="20mm" ) == 0 + + +def test_esimtate_pitched_roof_area(): + roof_area1 = recommendation_utils.esimtate_pitched_roof_area( + floor_area=100, floor_height=2 + ) + + assert np.isclose(roof_area1, 107.70329614269008) + + # As the floor height gets bigger, the area should get bigger + roof_area2 = recommendation_utils.esimtate_pitched_roof_area( + floor_area=100, floor_height=3 + ) + + assert np.isclose(roof_area2, 116.61903789690601) + + # As the floor area gets smaller, the area should get smaller + roof_area3 = recommendation_utils.esimtate_pitched_roof_area( + floor_area=100, floor_height=1 + ) + + assert np.isclose(roof_area3, 101.9803902718557) + + # As the floor area decreases, area should decrease + roof_area4 = recommendation_utils.esimtate_pitched_roof_area( + floor_area=50, floor_height=2 + ) + + assert np.isclose(roof_area4, 57.44562646538029) + + # As the floor area increases, area should increase + roof_area5 = recommendation_utils.esimtate_pitched_roof_area( + floor_area=150, floor_height=2 + ) + + assert np.isclose(roof_area5, 157.797338380595) + + zero_roof_area = recommendation_utils.esimtate_pitched_roof_area( + floor_area=0, floor_height=1000 + ) + + assert zero_roof_area == 0 + + # If the floor height zero, we don't have a traingle, it's a flat roof + flat_roof_area = recommendation_utils.esimtate_pitched_roof_area( + floor_area=1000, floor_height=0 + ) + + assert flat_roof_area == 1000 + + zero_roof_area2 = recommendation_utils.esimtate_pitched_roof_area( + floor_area=0, floor_height=0 + ) + + assert zero_roof_area2 == 0 diff --git a/recommendations/tests/test_roof_recommendations.py b/recommendations/tests/test_roof_recommendations.py index b822f829..3f99535c 100644 --- a/recommendations/tests/test_roof_recommendations.py +++ b/recommendations/tests/test_roof_recommendations.py @@ -35,6 +35,19 @@ loft_insulation_materials_150mm_existing = [ } ] +room_roof_insulation_materials = [ + { + 'id': 18, + 'type': 'room_roof_insulation', + 'description': 'Example room roof insulation', + 'depths': [50, 150, 220, 270, 300], 'depth_unit': 'mm', 'cost': [9, 10, 11, 12, 13], + 'cost_unit': 'gbp_sq_meter', + 'r_value_per_mm': 0.022727273, 'r_value_unit': 'square_meter_kelvin_per_watt', + 'thermal_conductivity': 0.044, 'thermal_conductivity_unit': 'watt_per_meter_kelvin', + 'link': None, 'is_active': True + } +] + class TestRoofRecommendations: @@ -205,12 +218,113 @@ class TestRoofRecommendations: 'is_assumed': True, 'has_dwelling_above': False, 'is_valid': True, 'insulation_thickness': 'none' } - property_instance7 + property_instance7.pitched_roof_area = 110 roof_recommender7 = RoofRecommendations( - property_instance=property_instance7, materials=loft_insulation_materials + property_instance=property_instance7, materials=room_roof_insulation_materials ) assert not roof_recommender7.recommendations roof_recommender7.recommend() + + # Even though we have 3 depths, we only end with 1 due to diminishin returns + assert len(roof_recommender7.recommendations) == 1 + + assert roof_recommender7.recommendations[0]["parts"][0]["depths"] == [270] + + assert roof_recommender7.recommendations[0]["new_u_value"] == 0.14 + assert roof_recommender7.recommendations[0]["starting_u_value"] == 0.8 + assert roof_recommender7.recommendations[0]["description"] == \ + "Insulate your room roof with 270mm of Example room roof insulation" + + def test_ceiling_insulated_room_in_roof(self): + property_instance8 = Property(id=8, address1="fake", postcode="fake", epc_client=Mock()) + property_instance8.age_band = "F" + property_instance8.floor_area = 100 + property_instance8.roof = { + 'original_description': 'Roof room(s), ceiling insulated', + 'clean_description': 'Roof room(s), ceiling insulated', + 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': False, + 'is_roof_room': True, 'is_loft': False, 'is_flat': False, 'is_thatched': False, + 'is_at_rafters': False, + 'is_assumed': False, 'has_dwelling_above': False, 'is_valid': True, + 'insulation_thickness': 'average' + } + + property_instance8.pitched_roof_area = 110 + + roof_recommender8 = RoofRecommendations( + property_instance=property_instance8, materials=room_roof_insulation_materials + ) + + assert not roof_recommender8.recommendations + + roof_recommender8.recommend() + + # No recommendations in this case + assert not roof_recommender8.recommendations + + def test_insulated_room_in_roof(self): + property_instance9 = Property(id=9, address1="fake", postcode="fake", epc_client=Mock()) + property_instance9.age_band = "F" + property_instance9.floor_area = 100 + property_instance9.roof = { + 'original_description': 'Roof room(s), insulated (assumed)', + 'clean_description': 'Roof room(s), insulated', + 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': False, + 'is_roof_room': True, 'is_loft': False, 'is_flat': False, 'is_thatched': False, 'is_at_rafters': False, + 'is_assumed': True, 'has_dwelling_above': False, 'is_valid': True, 'insulation_thickness': 'average' + } + + property_instance9.pitched_roof_area = 110 + + roof_recommender9 = RoofRecommendations( + property_instance=property_instance9, materials=room_roof_insulation_materials + ) + + assert not roof_recommender9.recommendations + + roof_recommender9.recommend() + + # No recommendations in this case + assert not roof_recommender9.recommendations + + def test_limited_insulated_room_in_roof(self): + property_instance10 = Property(id=10, address1="fake", postcode="fake", epc_client=Mock()) + property_instance10.age_band = "F" + property_instance10.floor_area = 100 + property_instance10.roof = { + 'original_description': 'Roof room(s), limited insulation (assumed)', + 'clean_description': 'Roof room(s), limited insulation', + 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': False, + 'is_roof_room': True, 'is_loft': False, 'is_flat': False, 'is_thatched': False, 'is_at_rafters': False, + 'is_assumed': True, 'has_dwelling_above': False, 'is_valid': True, + 'insulation_thickness': 'below average' + } + + property_instance10.pitched_roof_area = 110 + + roof_recommender10 = RoofRecommendations( + property_instance=property_instance10, materials=room_roof_insulation_materials + ) + + assert not roof_recommender10.recommendations + + roof_recommender10.recommend() + + assert len(roof_recommender10.recommendations) == 2 + + assert roof_recommender10.recommendations[0]["parts"][0]["depths"] == [220] + assert roof_recommender10.recommendations[1]["parts"][0]["depths"] == [270] + + assert roof_recommender10.recommendations[0]["new_u_value"] == 0.16 + assert roof_recommender10.recommendations[1]["new_u_value"] == 0.14 + + assert roof_recommender10.recommendations[0]["starting_u_value"] == 0.8 + assert roof_recommender10.recommendations[1]["starting_u_value"] == 0.8 + + assert roof_recommender10.recommendations[0]["description"] == \ + "Insulate your room roof with 220mm of Example room roof insulation" + assert roof_recommender10.recommendations[1]["description"] == \ + "Insulate your room roof with 270mm of Example room roof insulation" From 7ae49d35e4feee804a95209b930a33b8f9983e7b Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Fri, 17 Nov 2023 10:43:14 +0000 Subject: [PATCH 13/21] Adding flat roof insulation --- .idea/misc.xml | 3 + recommendations/RoofRecommendations.py | 55 +++++++++++++++---- .../tests/test_roof_recommendations.py | 25 +++++++++ 3 files changed, 73 insertions(+), 10 deletions(-) diff --git a/.idea/misc.xml b/.idea/misc.xml index 3b05c6ac..6f308057 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,5 +1,8 @@ + + diff --git a/.idea/misc.xml b/.idea/misc.xml index 6f308057..1122b380 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,7 +3,7 @@ - + diff --git a/backend/tests/test_sap_model_prep.py b/backend/tests/test_sap_model_prep.py index da38cabf..d1a643ee 100644 --- a/backend/tests/test_sap_model_prep.py +++ b/backend/tests/test_sap_model_prep.py @@ -195,7 +195,7 @@ class TestSapModelPrep: 'is_granite_or_whinstone': False, 'is_as_built': True, 'is_cob': False, 'is_sandstone_or_limestone': False, 'is_park_home': False, 'walls_insulation_thickness': 'none', 'external_insulation': False, 'internal_insulation': False, 'walls_thermal_transmittance_ENDING': 0.7, - 'is_park_home_ENDING': False, 'walls_insulation_thickness_ENDING': 'none', + 'is_park_home_ENDING': False, 'walls_insulation_thickness_ENDING': 'average', 'external_insulation_ENDING': False, 'internal_insulation_ENDING': False, 'floor_thermal_transmittance': 0.64, 'is_to_unheated_space': False, 'is_to_external_air': False, 'is_suspended': True, 'is_solid': False, 'another_property_below': False, @@ -253,7 +253,29 @@ class TestSapModelPrep: 'no_individual_heating_or_community_network': False, 'complex_fuel_type': 'Unknown', 'fuel_type_ENDING': 'oil', 'main-fuel_tariff_type_ENDING': 'Unknown', 'is_community_ENDING': False, 'no_individual_heating_or_community_network_ENDING': False, 'complex_fuel_type_ENDING': 'Unknown', - 'estimated_perimeter_STARTING': 44.77882152472145, 'estimated_perimeter_ENDING': 44.77882152472145 + 'estimated_perimeter_STARTING': 44.77882152472145, 'estimated_perimeter_ENDING': 44.77882152472145, + 'HOT_WATER_ENERGY_EFF_STARTING': "Good", + "FLOOR_ENERGY_EFF_STARTING": "Unknown", + "WINDOWS_ENERGY_EFF_STARTING": "Good", + "WALLS_ENERGY_EFF_STARTING": "Poor", + "SHEATING_ENERGY_EFF_STARTING": "Unknown", + "ROOF_ENERGY_EFF_STARTING": "Very Poor", + "MAINHEAT_ENERGY_EFF_STARTING": "Average", + "MAINHEATC_ENERGY_EFF_STARTING": "Good", + "LIGHTING_ENERGY_EFF_STARTING": "Average", + "POTENTIAL_ENERGY_EFFICIENCY": 64, + "ENVIRONMENT_IMPACT_POTENTIAL": 53, + "ENERGY_CONSUMPTION_POTENTIAL": 177.0, + "CO2_EMISSIONS_POTENTIAL": 5.7, + "HOT_WATER_ENERGY_EFF_ENDING": "Good", + "FLOOR_ENERGY_EFF_ENDING": "Unknown", + "WINDOWS_ENERGY_EFF_ENDING": "Good", + "WALLS_ENERGY_EFF_ENDING": "Good", + "SHEATING_ENERGY_EFF_ENDING": "Unknown", + "ROOF_ENERGY_EFF_ENDING": "Very Poor", + "MAINHEAT_ENERGY_EFF_ENDING": "Average", + "MAINHEATC_ENERGY_EFF_ENDING": "Good", + "LIGHTING_ENERGY_EFF_ENDING": "Average", } home = Property( @@ -321,10 +343,9 @@ class TestSapModelPrep: continue if c == "walls_insulation_thickness_ENDING": - print("Add back in the checks") - continue assert row[c] == "average" - assert test_record[c] == "above average" + assert test_record[c].values[0] == "above average" + continue assert test_record[c].values[0] == row[c] From 4d51410ff30fbef541bc037b684c05eca4ee4589 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Fri, 17 Nov 2023 11:51:39 +0000 Subject: [PATCH 17/21] Fixing sap model process setup --- backend/tests/test_sap_model_prep.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/backend/tests/test_sap_model_prep.py b/backend/tests/test_sap_model_prep.py index d1a643ee..52104eab 100644 --- a/backend/tests/test_sap_model_prep.py +++ b/backend/tests/test_sap_model_prep.py @@ -473,7 +473,29 @@ class TestSapModelPrep: 'fuel_type_ENDING': 'electricity', 'main-fuel_tariff_type_ENDING': 'Unknown', 'is_community_ENDING': False, 'no_individual_heating_or_community_network_ENDING': False, 'complex_fuel_type_ENDING': 'Unknown', 'estimated_perimeter_STARTING': 35.4964786985977, - 'estimated_perimeter_ENDING': 35.4964786985977 + 'estimated_perimeter_ENDING': 35.4964786985977, + 'HOT_WATER_ENERGY_EFF_STARTING': "Very Poor", + "FLOOR_ENERGY_EFF_STARTING": "Unknown", + "WINDOWS_ENERGY_EFF_STARTING": "Average", + "WALLS_ENERGY_EFF_STARTING": "Very Poor", + "SHEATING_ENERGY_EFF_STARTING": "Unknown", + "ROOF_ENERGY_EFF_STARTING": "Unknown", + "MAINHEAT_ENERGY_EFF_STARTING": "Very Poor", + "MAINHEATC_ENERGY_EFF_STARTING": "Good", + "LIGHTING_ENERGY_EFF_STARTING": "Poor", + "POTENTIAL_ENERGY_EFFICIENCY": 71, + "ENVIRONMENT_IMPACT_POTENTIAL": 51, + "ENERGY_CONSUMPTION_POTENTIAL": 307, + "CO2_EMISSIONS_POTENTIAL": 3.6, + 'HOT_WATER_ENERGY_EFF_ENDING': "Very Poor", + "FLOOR_ENERGY_EFF_ENDING": "Unknown", + "WINDOWS_ENERGY_EFF_ENDING": "Average", + "WALLS_ENERGY_EFF_ENDING": "Good", + "SHEATING_ENERGY_EFF_ENDING": "Unknown", + "ROOF_ENERGY_EFF_ENDING": "Unknown", + "MAINHEAT_ENERGY_EFF_ENDING": "Very Poor", + "MAINHEATC_ENERGY_EFF_ENDING": "Good", + "LIGHTING_ENERGY_EFF_ENDING": "Poor", } home2 = Property( From 314ad8719df424a182d514d69953fa428aa1deba Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Fri, 17 Nov 2023 12:12:43 +0000 Subject: [PATCH 18/21] fixing sap model prep unit tests --- backend/tests/test_sap_model_prep.py | 48 ++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/backend/tests/test_sap_model_prep.py b/backend/tests/test_sap_model_prep.py index 52104eab..ab7cd678 100644 --- a/backend/tests/test_sap_model_prep.py +++ b/backend/tests/test_sap_model_prep.py @@ -692,7 +692,29 @@ class TestSapModelPrep: 'no_individual_heating_or_community_network': False, 'complex_fuel_type': 'Unknown', 'fuel_type_ENDING': 'mains gas', 'main-fuel_tariff_type_ENDING': 'Unknown', 'is_community_ENDING': False, 'no_individual_heating_or_community_network_ENDING': False, 'complex_fuel_type_ENDING': 'Unknown', - 'estimated_perimeter_STARTING': 41.634120622393354, 'estimated_perimeter_ENDING': 41.634120622393354 + 'estimated_perimeter_STARTING': 41.634120622393354, 'estimated_perimeter_ENDING': 41.634120622393354, + 'HOT_WATER_ENERGY_EFF_STARTING': "Good", + "FLOOR_ENERGY_EFF_STARTING": "Unknown", + "WINDOWS_ENERGY_EFF_STARTING": "Average", + "WALLS_ENERGY_EFF_STARTING": "Very Poor", + "SHEATING_ENERGY_EFF_STARTING": "Unknown", + "ROOF_ENERGY_EFF_STARTING": "Very Poor", + "MAINHEAT_ENERGY_EFF_STARTING": "Good", + "MAINHEATC_ENERGY_EFF_STARTING": "Average", + "LIGHTING_ENERGY_EFF_STARTING": "Average", + "POTENTIAL_ENERGY_EFFICIENCY": 80, + "ENVIRONMENT_IMPACT_POTENTIAL": 75, + "ENERGY_CONSUMPTION_POTENTIAL": 152, + "CO2_EMISSIONS_POTENTIAL": 2.9, + 'HOT_WATER_ENERGY_EFF_ENDING': "Good", + "FLOOR_ENERGY_EFF_ENDING": "Unknown", + "WINDOWS_ENERGY_EFF_ENDING": "Average", + "WALLS_ENERGY_EFF_ENDING": "Very Poor", + "SHEATING_ENERGY_EFF_ENDING": "Unknown", + "ROOF_ENERGY_EFF_ENDING": "Very Poor", + "MAINHEAT_ENERGY_EFF_ENDING": "Good", + "MAINHEATC_ENERGY_EFF_ENDING": "Average", + "LIGHTING_ENERGY_EFF_ENDING": "Average", } home3 = Property( @@ -878,7 +900,29 @@ class TestSapModelPrep: 'no_individual_heating_or_community_network': False, 'complex_fuel_type': 'Unknown', 'fuel_type_ENDING': 'mains gas', 'main-fuel_tariff_type_ENDING': 'Unknown', 'is_community_ENDING': False, 'no_individual_heating_or_community_network_ENDING': False, 'complex_fuel_type_ENDING': 'Unknown', - 'estimated_perimeter_STARTING': 37.54197650630557, 'estimated_perimeter_ENDING': 37.54197650630557 + 'estimated_perimeter_STARTING': 37.54197650630557, 'estimated_perimeter_ENDING': 37.54197650630557, + 'HOT_WATER_ENERGY_EFF_STARTING': "Good", + "FLOOR_ENERGY_EFF_STARTING": "Unknown", + "WINDOWS_ENERGY_EFF_STARTING": "Average", + "WALLS_ENERGY_EFF_STARTING": "Very Poor", + "SHEATING_ENERGY_EFF_STARTING": "Unknown", + "ROOF_ENERGY_EFF_STARTING": "Good", + "MAINHEAT_ENERGY_EFF_STARTING": "Good", + "MAINHEATC_ENERGY_EFF_STARTING": "Average", + "LIGHTING_ENERGY_EFF_STARTING": "Average", + "POTENTIAL_ENERGY_EFFICIENCY": 78, + "ENVIRONMENT_IMPACT_POTENTIAL": 76, + "ENERGY_CONSUMPTION_POTENTIAL": 153, + "CO2_EMISSIONS_POTENTIAL": 2.4, + 'HOT_WATER_ENERGY_EFF_ENDING': "Good", + "FLOOR_ENERGY_EFF_ENDING": "Unknown", + "WINDOWS_ENERGY_EFF_ENDING": "Average", + "WALLS_ENERGY_EFF_ENDING": "Very Poor", + "SHEATING_ENERGY_EFF_ENDING": "Unknown", + "ROOF_ENERGY_EFF_ENDING": "Good", + "MAINHEAT_ENERGY_EFF_ENDING": "Good", + "MAINHEATC_ENERGY_EFF_ENDING": "Average", + "LIGHTING_ENERGY_EFF_ENDING": "Average", } home4 = Property( From ccc0d8603b81fa8a1d9bc29a5872f775d7322a40 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Fri, 17 Nov 2023 12:16:11 +0000 Subject: [PATCH 19/21] fixed property unit tests --- backend/tests/test_property.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/tests/test_property.py b/backend/tests/test_property.py index d7028bc6..b376db9e 100644 --- a/backend/tests/test_property.py +++ b/backend/tests/test_property.py @@ -227,7 +227,8 @@ class TestProperty: "mainheat-description": [{"original_description": "Main Heating Description"}], "hotwater-description": [{"original_description": "Hot Water Description"}], "lighting-description": [{"original_description": "Good Lighting Efficiency"}], - "floor-description": [{"original_description": "Floor Description", "is_suspended": True}] + "floor-description": [ + {"original_description": "Floor Description", "is_suspended": True, "another_property_below": False}] } return mock_cleaner @@ -317,7 +318,9 @@ class TestProperty: } property_instance.floor = { - "is_suspended": False + "is_suspended": False, + "another_property_below": False, + "is_solid": True } # Assert backup cleaning has been applied From 763b88cd1edc46c7201cf5ea334dfef8c4b256c0 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Fri, 17 Nov 2023 13:55:46 +0000 Subject: [PATCH 20/21] fix tests for convert thickness to numeric --- .../tests/test_recommendation_utils.py | 19 ++++++++++++------- .../tests/test_roof_recommendations.py | 9 ++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/recommendations/tests/test_recommendation_utils.py b/recommendations/tests/test_recommendation_utils.py index b0c8a26b..22280ed5 100644 --- a/recommendations/tests/test_recommendation_utils.py +++ b/recommendations/tests/test_recommendation_utils.py @@ -280,14 +280,19 @@ class TestRecommendationUtils: def test_convert_thickness_to_numeric(self): - assert recommendation_utils.convert_thickness_to_numeric("none") == 0 - assert recommendation_utils.convert_thickness_to_numeric("below average") == 50 - assert recommendation_utils.convert_thickness_to_numeric("average") == 100 - assert recommendation_utils.convert_thickness_to_numeric("above average") == 270 + assert recommendation_utils.convert_thickness_to_numeric("none", True) == 0 + assert recommendation_utils.convert_thickness_to_numeric("below average", True) == 50 + assert recommendation_utils.convert_thickness_to_numeric("average", True) == 100 + assert recommendation_utils.convert_thickness_to_numeric("above average", True) == 270 - assert recommendation_utils.convert_thickness_to_numeric("300+") == 300 - assert recommendation_utils.convert_thickness_to_numeric("400+") == 400 - assert recommendation_utils.convert_thickness_to_numeric("270") == 270 + assert recommendation_utils.convert_thickness_to_numeric("300+", True) == 300 + assert recommendation_utils.convert_thickness_to_numeric("400+", True) == 400 + assert recommendation_utils.convert_thickness_to_numeric("270", True) == 270 + + assert recommendation_utils.convert_thickness_to_numeric("none", False) == 0 + assert recommendation_utils.convert_thickness_to_numeric("below average", False) == 100 + assert recommendation_utils.convert_thickness_to_numeric("average", False) == 270 + assert recommendation_utils.convert_thickness_to_numeric("above average", False) == 270 def test_estimate_perimeter_regular_inputs(): diff --git a/recommendations/tests/test_roof_recommendations.py b/recommendations/tests/test_roof_recommendations.py index a3cc9266..37cc2daf 100644 --- a/recommendations/tests/test_roof_recommendations.py +++ b/recommendations/tests/test_roof_recommendations.py @@ -362,21 +362,16 @@ class TestRoofRecommendations: roof_recommender11.recommend() - assert len(roof_recommender11.recommendations) == 2 + assert len(roof_recommender11.recommendations) == 1 assert roof_recommender11.recommendations[0]["parts"][0]["depths"] == [270] - assert roof_recommender11.recommendations[1]["parts"][0]["depths"] == [300] - assert roof_recommender11.recommendations[0]["new_u_value"] == 0.16 - assert roof_recommender11.recommendations[1]["new_u_value"] == 0.14 + assert roof_recommender11.recommendations[0]["new_u_value"] == 0.11 assert roof_recommender11.recommendations[0]["starting_u_value"] == 2.3 - assert roof_recommender11.recommendations[1]["starting_u_value"] == 2.3 assert roof_recommender11.recommendations[0]["description"] == \ "Insulate the home's flat roof with 270mm of Example flat roof insulation" - assert roof_recommender11.recommendations[1]["description"] == \ - "Insulate the home's flat roof with 300mm of Example flat roof insulation" def test_flat_insulated(self): property_instance12 = Property(id=12, address1="fake", postcode="fake", epc_client=Mock()) From ee0fd1452bd0bd8a53b65ac9f7ca0d9482944b36 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Fri, 17 Nov 2023 14:03:42 +0000 Subject: [PATCH 21/21] fixed unit tests --- .../tests/test_floor_recommendations.py | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/recommendations/tests/test_floor_recommendations.py b/recommendations/tests/test_floor_recommendations.py index cad5fe24..82ba7cf4 100644 --- a/recommendations/tests/test_floor_recommendations.py +++ b/recommendations/tests/test_floor_recommendations.py @@ -115,6 +115,8 @@ class TestFloorRecommendations: assert obj.property def test_other_premises_below(self, input_properties): + input_properties[0].floor_area = 100 + input_properties[0].number_of_floors = 1 recommender = FloorRecommendations( property_instance=input_properties[0], materials=parts @@ -136,7 +138,7 @@ class TestFloorRecommendations: input_properties[2].perimeter = 20 input_properties[2].wall_type = "solid brick" input_properties[2].floor_type = "suspended" - input_properties[2].number_of_storeys = 1 + input_properties[2].number_of_floors = 1 recommender = FloorRecommendations( property_instance=input_properties[2], @@ -145,7 +147,7 @@ class TestFloorRecommendations: assert recommender.estimated_u_value is None recommender.recommend() assert recommender.property.floor["is_suspended"] - assert recommender.estimated_u_value == 0.39 + assert recommender.estimated_u_value == 0.66 assert recommender.recommendations types = {part["type"] for x in recommender.recommendations for part in x["parts"]} @@ -158,6 +160,8 @@ class TestFloorRecommendations: does not need floor insulation :return: """ + input_properties[3].floor_area = 100 + input_properties[3].number_of_floors = 1 recommender = FloorRecommendations( property_instance=input_properties[3], materials=parts @@ -180,7 +184,7 @@ class TestFloorRecommendations: input_properties[4].perimeter = 50 input_properties[4].wall_type = "solid brick" input_properties[4].floor_type = "solid" - input_properties[4].number_of_storeys = 1 + input_properties[4].number_of_floors = 1 recommender = FloorRecommendations( property_instance=input_properties[4], @@ -190,7 +194,7 @@ class TestFloorRecommendations: recommender.recommend() assert not recommender.property.floor["is_suspended"] assert recommender.property.floor["is_solid"] - assert recommender.estimated_u_value == 0.71 + assert recommender.estimated_u_value == 0.73 assert recommender.recommendations types = {part["type"] for x in recommender.recommendations for part in x["parts"]} @@ -202,6 +206,8 @@ class TestFloorRecommendations: This is another description we see when there is a property below """ + input_properties[6].floor_area = 100 + input_properties[6].number_of_floors = 1 recommender = FloorRecommendations( property_instance=input_properties[6], materials=parts @@ -226,7 +232,7 @@ class TestFloorRecommendations: input_property.set_floor_type() input_property.data = {"floor-level": 0, "property-type": "House"} input_property.floor_area = 100 - input_property.number_of_storeys = 1 + input_property.number_of_floors = 1 recommender = FloorRecommendations( property_instance=input_property, @@ -255,7 +261,7 @@ class TestFloorRecommendations: input_property2.set_floor_type() input_property2.data = {"floor-level": 0, "property-type": "House"} input_property2.floor_area = 100 - input_property2.number_of_storeys = 1 + input_property2.number_of_floors = 1 recommender2 = FloorRecommendations( property_instance=input_property2, @@ -285,7 +291,7 @@ class TestFloorRecommendations: input_property3.set_floor_type() input_property3.data = {"floor-level": 0, "property-type": "House"} input_property3.floor_area = 100 - input_property3.number_of_storeys = 1 + input_property3.number_of_floors = 1 recommender3 = FloorRecommendations( property_instance=input_property3, @@ -319,7 +325,7 @@ class TestFloorRecommendations: input_property4.set_floor_type() input_property4.data = {"floor-level": 0, "property-type": "House"} input_property4.floor_area = 100 - input_property4.number_of_storeys = 1 + input_property4.number_of_floors = 1 recommender4 = FloorRecommendations( property_instance=input_property4,