From 15e48c216596c417640635162e63c7860f6ebf99 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 13 Nov 2023 20:58:00 +0900 Subject: [PATCH] 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"