mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
470 lines
20 KiB
Python
470 lines
20 KiB
Python
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
|
|
from recommendations.recommendation_utils import is_diminishing_returns
|
|
|
|
# with open(
|
|
# os.path.abspath(os.path.dirname(__file__)) + "/recommendations/tests/test_data/input_properties.pkl", "rb"
|
|
# ) as f:
|
|
# input_properties = pickle.load(f)
|
|
#
|
|
# with open(
|
|
# os.path.abspath(os.path.dirname(__file__)) + "/recommendations/tests/test_data/uvalue_estimates.pkl", "rb"
|
|
# ) as f:
|
|
# uvalue_estimates = pickle.load(f)
|
|
|
|
|
|
external_wall_insulation_parts = [
|
|
{
|
|
# Example product
|
|
# https://insulationgo.co.uk/100mm-rockwool-external-wall-insulation-dual-density-slabs-a1-non-combustible
|
|
# -slab-ewi-render-fire/
|
|
"type": "external_wall_insulation",
|
|
"description": "Mineral Wool External Wall Insulation",
|
|
"depths": [30, 50, 70, 80, 90, 100, 150, 200],
|
|
"depth_unit": "mm",
|
|
"cost": [30, 50, 70, 80, 90, 100, 150, 200],
|
|
"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"
|
|
},
|
|
{
|
|
# Example product
|
|
# https://www.insulationking.co.uk/products/polystyrene-eps70?variant=44156186558759
|
|
"type": "external_wall_insulation",
|
|
"description": "Expanded Polystyrene External Wall Insulation",
|
|
"depths": [25, 50, 100, 125],
|
|
"depth_unit": "mm",
|
|
"cost": [25, 50, 100, 125],
|
|
"cost_unit": "gbp_sq_meter",
|
|
"r_value_per_mm": 0.02703,
|
|
"r_value_unit": "square_meter_kelvin_per_watt",
|
|
"thermal_conductivity": 0.037,
|
|
"thermal_conductivity_unit": "watt_per_meter_kelvin"
|
|
},
|
|
{
|
|
# Example product
|
|
# https://www.insulationshop.co/20mm_kooltherm_k5_external_wall_kingspan.html
|
|
"type": "external_wall_insulation",
|
|
"description": "Phenolic Foam External Wall Insulation",
|
|
"depths": [20, 50, 100],
|
|
"depth_unit": "mm",
|
|
"cost": [20, 50, 100],
|
|
"cost_unit": "gbp_sq_meter",
|
|
"r_value_per_mm": 0.043478260869565216,
|
|
"r_value_unit": "square_meter_kelvin_per_watt",
|
|
"thermal_conductivity": 0.023,
|
|
"thermal_conductivity_unit": "watt_per_meter_kelvin"
|
|
|
|
},
|
|
{
|
|
"type": "external_wall_insulation",
|
|
"description": "Polyisocyanurate/Polyurethane Foam External Wall Insulation",
|
|
"depths": [],
|
|
"depth_unit": "mm",
|
|
"cost": [],
|
|
"cost_unit": "gbp_sq_meter",
|
|
"r_value_per_mm": None,
|
|
"r_value_unit": "square_meter_kelvin_per_watt",
|
|
"thermal_conductivity": None,
|
|
"thermal_conductivity_unit": "watt_per_meter_kelvin"
|
|
},
|
|
{
|
|
# Example product
|
|
# https://www.mikewye.co.uk/product/steico-duo-dry/
|
|
"type": "external_wall_insulation",
|
|
"description": "Wood Fiber External Wall Insulation",
|
|
"depths": [40, 60],
|
|
"depth_unit": "mm",
|
|
"cost": [40, 60],
|
|
"cost_unit": "gbp_sq_meter",
|
|
"r_value_per_mm": 0.023255813953488375,
|
|
"r_value_unit": "square_meter_kelvin_per_watt",
|
|
"thermal_conductivity": 0.043,
|
|
"thermal_conductivity_unit": "watt_per_meter_kelvin"
|
|
},
|
|
{
|
|
# Example product
|
|
# https://www.thermablok.co.uk/site/wp-content/uploads/2022/09/Thermablok-Aerogel-Insulation-Blanket-TDS-AIS
|
|
# -and-Steel-Related-Details.pdf
|
|
"type": "external_wall_insulation",
|
|
"description": "Aerogel External Wall Insulation",
|
|
"depths": [10, 20, 30, 40, 50, 60, 70],
|
|
"depth_unit": "mm",
|
|
"cost": [10, 20, 30, 40, 50, 60, 70],
|
|
"cost_unit": "gbp_sq_meter",
|
|
"r_value_per_mm": 0.06666666666666667,
|
|
"r_value_unit": "square_meter_kelvin_per_watt",
|
|
"thermal_conductivity": 0.015,
|
|
"thermal_conductivity_unit": "watt_per_meter_kelvin"
|
|
},
|
|
{
|
|
"type": "external_wall_insulation",
|
|
"description": "Vacuum Insulation Panels External Wall Insulation",
|
|
"depths": [45, 60],
|
|
"depth_unit": "mm",
|
|
"cost": [45, 60],
|
|
"cost_unit": "gbp_sq_meter",
|
|
"r_value_per_mm": 0.16666666666666666,
|
|
"r_value_unit": "square_meter_kelvin_per_watt",
|
|
"thermal_conductivity": 0.006,
|
|
"thermal_conductivity_unit": "watt_per_meter_kelvin"
|
|
}
|
|
]
|
|
|
|
internal_wall_insulation_parts = [
|
|
{
|
|
# Example product
|
|
# https://www.insulationshop.co/25mm_polystyrene_insulation_eps_70jablite.html
|
|
"type": "internal_wall_insulation",
|
|
"description": "Rigid Insulation Boards Internal Wall Insulation",
|
|
"depths": [25, 40, 50, 75, 100],
|
|
"depth_unit": "mm",
|
|
"cost": [25, 40, 50, 75, 100],
|
|
"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"
|
|
},
|
|
{
|
|
# Example product
|
|
# https://www.rockwool.com/siteassets/rw-uk/downloads/datasheets/flexi.pdf
|
|
"type": "internal_wall_insulation",
|
|
"description": "Mineral Wool Internal Wall Insulation",
|
|
"depths": [140],
|
|
"depth_unit": "mm",
|
|
"cost": [140],
|
|
"cost_unit": "gbp_sq_meter",
|
|
"r_value_per_mm": 0.02857142857142857,
|
|
"r_value_unit": "square_meter_kelvin_per_watt",
|
|
"thermal_conductivity": 0.035,
|
|
"thermal_conductivity_unit": "watt_per_meter_kelvin"
|
|
},
|
|
{
|
|
# Example product
|
|
# https://www.kingspan.com/gb/en/products/insulation-boards/wall-insulation-boards/kooltherm-k118-insulated
|
|
# -plasterboard/
|
|
"type": "internal_wall_insulation",
|
|
"description": "Insulated Plasterboard Internal Wall Insulation",
|
|
"depths": [25, 80],
|
|
"depth_unit": "mm",
|
|
"cost": [25, 80],
|
|
"cost_unit": "gbp_sq_meter",
|
|
"r_value_per_mm": 0.02857142857142857,
|
|
"r_value_unit": "square_meter_kelvin_per_watt",
|
|
"thermal_conductivity": 0.019,
|
|
"thermal_conductivity_unit": "watt_per_meter_kelvin"
|
|
},
|
|
{
|
|
"type": "internal_wall_insulation",
|
|
"description": "Reflective Internal Wall Insulation",
|
|
"depths": [],
|
|
"depth_unit": "mm",
|
|
"cost": [],
|
|
"cost_unit": "gbp_sq_meter",
|
|
"r_value_per_mm": None,
|
|
"r_value_unit": "square_meter_kelvin_per_watt",
|
|
"thermal_conductivity": None,
|
|
"thermal_conductivity_unit": "watt_per_meter_kelvin"
|
|
},
|
|
{
|
|
# Example product
|
|
# https://www.insulationsuperstore.co.uk/product/vacutherm-vacupor-nt-b2-vacuum-insulated-panel-1m-x-600mm-x
|
|
# -30mm.html
|
|
"type": "internal_wall_insulation",
|
|
"description": "Vacuum Insulation Panels Wall Insulation",
|
|
"depths": [20, 30],
|
|
"depth_unit": "mm",
|
|
"cost": [20, 30],
|
|
"cost_unit": "gbp_sq_meter",
|
|
"r_value_per_mm": 0.125,
|
|
"r_value_unit": "square_meter_kelvin_per_watt",
|
|
"thermal_conductivity": 0.008,
|
|
"thermal_conductivity_unit": "watt_per_meter_kelvin"
|
|
},
|
|
]
|
|
|
|
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:
|
|
|
|
@pytest.fixture
|
|
def input_properties(self):
|
|
with open(
|
|
os.path.abspath(os.path.dirname(__file__)) + "/test_data/input_properties.pkl", "rb"
|
|
) as f:
|
|
return pickle.load(f)
|
|
|
|
@pytest.fixture
|
|
def mock_wall_rec_instance(self):
|
|
# Creating a mock instance of WallRecommendations with the necessary attributes
|
|
property_mock = Mock()
|
|
property_mock.full_sap_epc = {"lodgement-date": "2000-01-01"} # or any date you want
|
|
property_mock.data = {"construction-age-band": "1950"} # or any other data that fits your tests
|
|
|
|
mock_wall_rec_instance = WallRecommendations(
|
|
property_mock, materials=wall_parts
|
|
)
|
|
return mock_wall_rec_instance
|
|
|
|
def test_init(self, input_properties):
|
|
obj = WallRecommendations(
|
|
property_instance=input_properties[0],
|
|
materials=wall_parts
|
|
)
|
|
assert obj
|
|
assert obj.property
|
|
|
|
def test_uvalue_0_16(self, input_properties):
|
|
"""
|
|
This tests the wall description Average thermal transmittance 0.16 W/m-¦K
|
|
The important data for this recommendation is:
|
|
- u value of 0.16
|
|
- property built in 2014
|
|
Since epc built after 1990 are typically built with insulation and this property
|
|
already has really good insulation, we do NOT recommend any measures for this property
|
|
"""
|
|
input_properties[0].year_built = 2014
|
|
recommender = WallRecommendations(
|
|
property_instance=input_properties[0],
|
|
materials=wall_parts
|
|
)
|
|
assert recommender.property.walls["original_description"] == "Average thermal transmittance 0.16 W/m-¦K"
|
|
recommender.recommend()
|
|
# This should be empty
|
|
assert recommender.recommendations == []
|
|
|
|
def test_solid_brick_no_insulation(self, input_properties):
|
|
"""
|
|
This tests a property with a wall description of Solid brick, as built, no insulation (assumed)
|
|
The property was built in 1930, right on the threshold for when cavity walls were introduced
|
|
However, we're told this property is solid brik so we assume no cavity.
|
|
We're also told that it has no insulation, so we will recommend internal/external wall insulation
|
|
|
|
This property is not in a conservation area, however it's a flat so we don't recommend external wall insulation
|
|
"""
|
|
input_properties[1].year_built = 1930
|
|
input_properties[1].insulation_wall_area = 100
|
|
input_properties[1].walls["clean_description"] = "Solid brick, as built, no insulation"
|
|
input_properties[1].walls["is_sandstone_or_limestone"] = False
|
|
input_properties[1].age_band = "A"
|
|
|
|
recommender = WallRecommendations(
|
|
property_instance=input_properties[1],
|
|
materials=wall_parts
|
|
)
|
|
assert recommender.property.walls["original_description"] == "Solid brick, as built, no insulation (assumed)"
|
|
assert not recommender.ewi_valid
|
|
assert recommender.property.in_conservation_area == "not_in_conservation_area"
|
|
assert recommender.property.data["property-type"] == "Flat"
|
|
|
|
recommender.recommend()
|
|
# This should result in some recommendations, all of which should be internal insulation
|
|
assert recommender.recommendations
|
|
|
|
rec_types = {part["type"] for rec in recommender.recommendations for part in rec["parts"]}
|
|
assert rec_types == {"internal_wall_insulation"}
|
|
|
|
# Check the recommendations provide a u value below the minimum
|
|
assert all(
|
|
rec["new_u_value"] < WallRecommendations.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE for rec in
|
|
recommender.recommendations
|
|
)
|
|
|
|
def test_solid_brick_insulation(self, input_properties):
|
|
"""
|
|
This tests a property with a wall description of Solid brick, as built, insulation (assumed)
|
|
The property was built in 1991, after cavity walls were introduced
|
|
However, we're told this property is solid brik so we assume no cavity.
|
|
We're also told that it has some insulation already
|
|
|
|
Since the walls are already insulated, we don't recommend further measures
|
|
|
|
This property is not in a conservation area, however it's a flat so we don't recommend external wall insulation
|
|
"""
|
|
|
|
input_properties[6].year_built = 1991
|
|
recommender = WallRecommendations(
|
|
property_instance=input_properties[6],
|
|
materials=wall_parts
|
|
)
|
|
|
|
assert recommender.property.walls["original_description"] == "Solid brick, as built, insulated (assumed)"
|
|
assert not recommender.ewi_valid
|
|
assert recommender.property.in_conservation_area == "not_in_conservation_area"
|
|
assert recommender.property.data["property-type"] == "Flat"
|
|
assert recommender.estimated_u_value is None
|
|
|
|
recommender.recommend()
|
|
|
|
assert not recommender.recommendations
|
|
assert not recommender.estimated_u_value
|
|
|
|
def test_is_diminishing_returns_no_recommendations(self):
|
|
# We have no recommendations but the new u value is below the diminishing returns threshold
|
|
# however since there are no recommendaions yet, we should include it and say
|
|
# it is NOT diminishing
|
|
|
|
new_u_value = 0.24
|
|
assert new_u_value < WallRecommendations.DIMINISHING_RETURNS_U_VALUE
|
|
|
|
assert not is_diminishing_returns([], new_u_value, None, WallRecommendations.DIMINISHING_RETURNS_U_VALUE)
|
|
|
|
def test_is_diminishing_returns_diminishing(self):
|
|
# We have a recommendation already and the new u value falls below the diminishing returns
|
|
# threshold and is also lower than the lowest selected u value, so we should say this IS
|
|
# diminishing returns
|
|
|
|
new_u_value = 0.2
|
|
lowest_selected_u_value = 0.23
|
|
assert new_u_value < WallRecommendations.DIMINISHING_RETURNS_U_VALUE
|
|
|
|
assert is_diminishing_returns([Mock()], new_u_value, lowest_selected_u_value,
|
|
WallRecommendations.DIMINISHING_RETURNS_U_VALUE)
|
|
|
|
def test_is_diminishing_returns_is_diminishing(self):
|
|
# We have a recommendation already and the new u value falls below the diminishing returns
|
|
# threshold and it's lower than the lowest selected u value, so we should say this IS DIMINISHIN
|
|
|
|
new_u_value = 0.24
|
|
lowest_selected_u_value = 0.26
|
|
|
|
assert new_u_value < WallRecommendations.DIMINISHING_RETURNS_U_VALUE
|
|
|
|
assert is_diminishing_returns([Mock()], new_u_value, lowest_selected_u_value,
|
|
WallRecommendations.DIMINISHING_RETURNS_U_VALUE)
|
|
|
|
def test_is_diminishing_returns_not_diminishing(self):
|
|
# We have a recommendation already and the new u value falls below the diminishing returns
|
|
# threshold however it's higher than the lowest selected u value, so we should say this is NOT
|
|
# diminishing returns
|
|
|
|
new_u_value = 0.24
|
|
lowest_selected_u_value = 0.22
|
|
|
|
assert new_u_value < WallRecommendations.DIMINISHING_RETURNS_U_VALUE
|
|
|
|
assert not is_diminishing_returns([Mock()], new_u_value, lowest_selected_u_value,
|
|
WallRecommendations.DIMINISHING_RETURNS_U_VALUE)
|
|
|
|
def test_is_diminishing_returns_error_in_recommendations(self, mock_wall_rec_instance):
|
|
# Testing case where there's an error in recommendations
|
|
mock_wall_rec_instance.recommendations = []
|
|
with pytest.raises(ValueError):
|
|
is_diminishing_returns([], 0.2, 0.24, 0.25)
|
|
|
|
|
|
class TestWallRecommendationsBase:
|
|
@pytest.fixture
|
|
def property_mock(self):
|
|
property_mock = MagicMock(spec=Property)
|
|
property_mock.full_sap_epc = {"lodgement-date": "1999-12-31"}
|
|
property_mock.in_conservation_area = "not_in_conservation_area"
|
|
return property_mock
|
|
|
|
@pytest.fixture
|
|
def wall_recommendations_instance(self, property_mock):
|
|
wall_recommendations_instance = WallRecommendations(
|
|
property_mock, materials=wall_parts
|
|
)
|
|
return wall_recommendations_instance
|
|
|
|
def test_ewi_valid_in_conservation_area(self, wall_recommendations_instance):
|
|
wall_recommendations_instance.property.in_conservation_area = "in_conversation_area"
|
|
assert wall_recommendations_instance.ewi_valid is False
|
|
|
|
def test_ewi_valid_is_flat(self, wall_recommendations_instance):
|
|
wall_recommendations_instance.property.data = {"property-type": "flat"}
|
|
assert wall_recommendations_instance.ewi_valid is False
|
|
|
|
def test_ewi_valid_not_in_conservation_area_and_not_flat(self, wall_recommendations_instance):
|
|
wall_recommendations_instance.property.in_conservation_area = "not_in_conversation_area"
|
|
wall_recommendations_instance.property.data = {"property-type": "house"}
|
|
assert wall_recommendations_instance.ewi_valid is True
|
|
|
|
def test_recommend_without_u_value(self, wall_recommendations_instance):
|
|
wall_recommendations_instance.property.walls = {
|
|
"thermal_transmittance": None,
|
|
"is_solid_brick": False,
|
|
"is_cavity_wall": False,
|
|
"insulation_thickness": "none",
|
|
"clean_description": "Solid brick, as built, no insulation",
|
|
"is_granite_or_whinstone": False,
|
|
"is_sandstone_or_limestone": False,
|
|
}
|
|
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)
|