From 2db052600349fcaba67ce9db8dfab31645cfcb71 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Sat, 7 Oct 2023 05:18:35 +0100 Subject: [PATCH] added in cavity fill recommendation --- backend/app/plan/temp_script_for_flight.py | 22 +++++- backend/app/plan/utils.py | 16 ++-- recommendations/WallRecommendations.py | 70 ++++++++++++++++- .../tests/test_wall_recommendations.py | 77 +++++++++++++++++-- 4 files changed, 163 insertions(+), 22 deletions(-) diff --git a/backend/app/plan/temp_script_for_flight.py b/backend/app/plan/temp_script_for_flight.py index 8de3ad3c..9170b4c1 100644 --- a/backend/app/plan/temp_script_for_flight.py +++ b/backend/app/plan/temp_script_for_flight.py @@ -56,10 +56,26 @@ plan_input = local_data["plan_input"] uprn_filenames = local_data["uprn_filenames"] local_property_data = local_data["local_property_data"] materials = local_data["materials"] -materials_by_type = local_data["materials_by_type"] +materials_by_type = filter_materials(materials) cleaned = local_data["cleaned"] cleaning_data = local_data["cleaning_data"] +# Need to find some proper materials +materials_by_type["walls"] += [ + {'id': 4, 'type': 'cavity_wall_insulation', 'description': 'Example Material 1', + 'depths': None, + 'depth_unit': None, 'cost': 20, + 'cost_unit': 'gbp_sq_meter', 'r_value_per_mm': 0.0278, 'r_value_unit': 'square_meter_kelvin_per_watt', + 'thermal_conductivity': 0.036, 'thermal_conductivity_unit': 'watt_per_meter_kelvin', + 'link': None, 'created_at': None, 'is_active': True}, + {'id': 10, 'type': "cavity_wall_insulation", 'description': 'Example Material 2', + 'depths': None, 'depth_unit': None, 'cost': 25, 'cost_unit': 'gbp_sq_meter', + 'r_value_per_mm': 0.02631579, 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.038, + 'thermal_conductivity_unit': 'watt_per_meter_kelvin', + 'link': None, + 'created_at': None, 'is_active': True} +] + epc_client = EpcClient(auth_token="NO-TOKEN") input_properties = [] @@ -102,7 +118,7 @@ for p in input_properties: # Floor recommendations floor_recommender = FloorRecommendations( property_instance=p, - materials=materials_by_type["suspended_floor_insulation"] + materials_by_type["solid_floor_insulation"], + materials=materials_by_type["floor"], ) floor_recommender.recommend() @@ -113,7 +129,7 @@ for p in input_properties: wall_recomender = WallRecommendations( property_instance=p, - materials=materials_by_type["external_wall_insulation"] + materials_by_type["internal_wall_insulation"] + materials=materials_by_type["walls"] ) wall_recomender.recommend() diff --git a/backend/app/plan/utils.py b/backend/app/plan/utils.py index af9f1335..c257fd4e 100644 --- a/backend/app/plan/utils.py +++ b/backend/app/plan/utils.py @@ -11,15 +11,17 @@ import msgpack def filter_materials(materials): materials_by_type = defaultdict(list) - for material in materials: - material = row2dict(material) - material_type = material["type"] - materials_by_type[material_type].append(material) + mapping = { + "walls": ["internal_wall_insulation", "external_wall_insulation", "cavity_wall_insulation"], + "floor": ["suspended_floor_insulation", "solid_floor_insulation"] + } - # Optionally, you can convert the defaultdict to a normal dict if desired - materials_by_type = dict(materials_by_type) + materials = [row2dict(material) for material in materials] - return materials_by_type + for component, types in mapping.items(): + materials_by_type[component] = [part for part in materials if part["type"] in types] + + return dict(materials_by_type) def insert_temp_recommendation_id(property_recommendations): diff --git a/recommendations/WallRecommendations.py b/recommendations/WallRecommendations.py index ff73bc68..2fd93166 100644 --- a/recommendations/WallRecommendations.py +++ b/recommendations/WallRecommendations.py @@ -76,9 +76,7 @@ class WallRecommendations(Definitions): # recommend internal wall insulation as a possible measure u_value = self.property.walls["thermal_transmittance"] - is_cavity_wall = self.property.walls["is_cavity_wall"] - is_solid_brick = self.property.walls["is_solid_brick"] insulation_thickness = self.property.walls["insulation_thickness"] # We check if the wall is already insulated and if so, we exit @@ -86,9 +84,9 @@ class WallRecommendations(Definitions): return if u_value: + if self.property.walls["thermal_transmittance_unit"] != self.U_VALUE_UNIT: raise NotImplementedError("Haven't handled the case of other u value units yet") - # We can't detect it's a cavity wall, but it was built after 1990 so likely built with insulation already # + it already has a U-value WORSE than the building regulations, so we recommend either internal or # external wall insulation @@ -117,7 +115,7 @@ class WallRecommendations(Definitions): ) self.estimated_u_value = u_value - if is_solid_brick: + if self.property.walls["is_solid_brick"]: if u_value >= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE: self.find_insulation(u_value) @@ -126,8 +124,72 @@ class WallRecommendations(Definitions): # If the u-value is within regulations, we don't do anything return + if is_cavity_wall: + if u_value >= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE: + # Test filling cavity + self.find_cavity_insulation(u_value) + + if insulation_thickness not in ["none", None]: + raise ValueError("Implement me") + + return + raise NotImplementedError("Not implemented yet") + def find_cavity_insulation(self, u_value): + """ + This method tests different materials to fill the cavity wall, determining which + material will give us the best U-value. + + We check for diminishing returns, however this function does not check for meeting building + part L regulations right now + :param u_value: u_value of the starting wall + :return: + """ + + cavity_wall_fills = [m for m in self.materials if m["type"] == "cavity_wall_insulation"] + # TODO: Check this and also check the methodology + cavity_width = 125 + # Test the different fill options + lowest_selected_u_value = None + recommendations = [] + for part in cavity_wall_fills: + part_u_value = r_value_per_mm_to_u_value(cavity_width, part["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 is_diminishing_returns( + recommendations, new_u_value, lowest_selected_u_value, self.DIMINISHING_RETURNS_U_VALUE + ): + continue + + 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 + + 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 + def _find_insulation(self, parts, u_value): lowest_selected_u_value = None recommendations = [] diff --git a/recommendations/tests/test_wall_recommendations.py b/recommendations/tests/test_wall_recommendations.py index 0e26d9a9..3f44de32 100644 --- a/recommendations/tests/test_wall_recommendations.py +++ b/recommendations/tests/test_wall_recommendations.py @@ -1,6 +1,7 @@ import os import pytest import pickle +import numpy as np from unittest.mock import Mock, MagicMock from recommendations.WallRecommendations import WallRecommendations from backend.Property import Property @@ -190,7 +191,22 @@ internal_wall_insulation_parts = [ }, ] -wall_parts = external_wall_insulation_parts + internal_wall_insulation_parts +cavity_wall_insulation_parts = [ + {'id': 4, 'type': 'cavity_wall_insulation', 'description': 'Example Material 1', + 'depths': None, + 'depth_unit': None, 'cost': 20, + 'cost_unit': 'gbp_sq_meter', 'r_value_per_mm': 0.0278, 'r_value_unit': 'square_meter_kelvin_per_watt', + 'thermal_conductivity': 0.036, 'thermal_conductivity_unit': 'watt_per_meter_kelvin', + 'link': None, 'created_at': None, 'is_active': True}, + {'id': 10, 'type': "cavity_wall_insulation", 'description': 'Example Material 2', + 'depths': None, 'depth_unit': None, 'cost': 25, 'cost_unit': 'gbp_sq_meter', + 'r_value_per_mm': 0.02631579, 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.038, + 'thermal_conductivity_unit': 'watt_per_meter_kelvin', + 'link': None, + 'created_at': None, 'is_active': True} +] + +wall_parts = external_wall_insulation_parts + internal_wall_insulation_parts + cavity_wall_insulation_parts class TestWallRecommendations: @@ -210,19 +226,17 @@ class TestWallRecommendations: property_mock.data = {"construction-age-band": "1950"} # or any other data that fits your tests mock_wall_rec_instance = WallRecommendations( - property_mock, "Decile 1", materials=wall_parts + property_mock, materials=wall_parts ) return mock_wall_rec_instance def test_init(self, input_properties): obj = WallRecommendations( property_instance=input_properties[0], - total_floor_area_group_decile="Decile 1", materials=wall_parts ) assert obj assert obj.property - assert obj.total_floor_area_group_decile == "Decile 1" def test_uvalue_0_16(self, input_properties): """ @@ -236,7 +250,6 @@ class TestWallRecommendations: input_properties[0].year_built = 2014 recommender = WallRecommendations( property_instance=input_properties[0], - total_floor_area_group_decile="Decile 1", materials=wall_parts ) assert recommender.property.walls["original_description"] == "Average thermal transmittance 0.16 W/m-¦K" @@ -261,7 +274,6 @@ class TestWallRecommendations: recommender = WallRecommendations( property_instance=input_properties[1], - total_floor_area_group_decile="Decile 1", materials=wall_parts ) assert recommender.property.walls["original_description"] == "Solid brick, as built, no insulation (assumed)" @@ -297,7 +309,6 @@ class TestWallRecommendations: input_properties[6].year_built = 1991 recommender = WallRecommendations( property_instance=input_properties[6], - total_floor_area_group_decile="Decile 1", materials=wall_parts ) @@ -377,7 +388,7 @@ class TestWallRecommendationsBase: @pytest.fixture def wall_recommendations_instance(self, property_mock): wall_recommendations_instance = WallRecommendations( - property_mock, "Decile 1", materials=wall_parts + property_mock, materials=wall_parts ) return wall_recommendations_instance @@ -407,3 +418,53 @@ class TestWallRecommendationsBase: wall_recommendations_instance.property.age_band = "A" with pytest.raises(NotImplementedError): wall_recommendations_instance.recommend() + + +class TestCavityWallRecommensations: + data = { + 'low-energy-fixed-light-count': '', 'address': '123 Fake Street', + 'floor-height': '', 'construction-age-band': 'England and Wales: 1950-1966', + 'address3': '', 'property-type': 'House', 'local-authority-label': 'Melton', + 'county': 'Leicestershire', 'postcode': 'LE14 2QH', + 'solar-water-heating-flag': 'N', 'constituency': 'E14000909', + 'number-heated-rooms': '5', 'local-authority': 'E07000133', 'built-form': 'End-Terrace', + 'address1': '1, 23 fake', 'total-floor-area': '85.0', 'environment-impact-current': '49', + 'number-habitable-rooms': 3, 'address2': 'Fake', 'posttown': 'IDK', + 'walls-energy-eff': 'Poor', 'current-energy-rating': 'D', + 'transaction-type': 'ECO assessment', 'uprn': '999', 'current-energy-efficiency': '57', + 'lodgement-date': '2019-07-10', 'lmk-key': '999', 'tenure': 'rental (private)', 'floor-level': 'NODATA!', + 'walls-description': 'Cavity wall, as built, no insulation (assumed)', + } + + def test_fill_empty_cavity(self): + input_property = Property(id=1, postcode="F4k3", address1="123 fake street", epc_client=Mock()) + input_property.walls = { + 'original_description': 'Cavity wall, as built, no insulation (assumed)', + 'clean_description': 'Cavity wall, as built, no insulation', + 'thermal_transmittance': None, 'thermal_transmittance_unit': None, + 'is_cavity_wall': True, 'is_filled_cavity': False, 'is_solid_brick': False, + 'is_system_built': False, 'is_timber_frame': False, 'is_granite_or_whinstone': False, + 'is_as_built': True, 'is_cob': False, 'is_assumed': True, + 'is_sandstone_or_limestone': False, 'is_park_home': False, + 'insulation_thickness': 'none', 'external_insulation': False, + 'internal_insulation': False + } + input_property.age_band = "C" + input_property.insulation_wall_area = 50 + + recommender = WallRecommendations( + property_instance=input_property, + materials=cavity_wall_insulation_parts + ) + + assert not recommender.recommendations + + recommender.recommend() + + assert recommender.recommendations + assert recommender.estimated_u_value == 1.5 + assert np.isclose(recommender.recommendations[0]["new_u_value"], 0.25) + assert np.isclose(recommender.recommendations[0]["cost"], 1000) + + assert np.isclose(recommender.recommendations[1]["new_u_value"], 0.26) + assert np.isclose(recommender.recommendations[1]["cost"], 1250)