Merge pull request #252 from Hestia-Homes/roof-recommendations

Roof recommendations
This commit is contained in:
KhalimCK 2023-11-17 14:04:26 +00:00 committed by GitHub
commit d8c0fec7b3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 1401 additions and 95 deletions

2
.idea/Model.iml generated
View file

@ -7,7 +7,7 @@
<sourceFolder url="file://$MODULE_DIR$/open_uprn" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/recommendations" isTestSource="false" />
</content>
<orderEntry type="jdk" jdkName="Python 3.10 (backend)" jdkType="Python SDK" />
<orderEntry type="jdk" jdkName="Python 3.10 (model_data)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyNamespacePackagesService">

5
.idea/misc.xml generated
View file

@ -1,6 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (backend)" project-jdk-type="Python SDK" />
<component name="Black">
<option name="sdkName" value="Python 3.10 (backend)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (model_data)" project-jdk-type="Python SDK" />
<component name="PythonCompatibilityInspectionAdvertiser">
<option name="version" value="3" />
</component>

View file

@ -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
@ -597,9 +604,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):

View file

@ -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):

View file

@ -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
@ -129,20 +130,38 @@ 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.load(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)
# 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 = []
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 = FloorRecommendations(property_instance=p, materials=materials_by_type["floor"])
floor_recommender.recommend()
if floor_recommender.recommendations:
@ -150,15 +169,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 +193,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:

View file

@ -15,8 +15,9 @@ 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"]
}
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'
@ -174,25 +201,8 @@ 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")
# 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

View file

@ -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

View file

@ -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:
@ -196,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,
@ -254,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(
@ -322,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]
@ -453,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(
@ -650,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(
@ -836,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(

View file

@ -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()

View file

@ -45,16 +45,20 @@ 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
)
property_type = self.property.data["property-type"]
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 [
@ -81,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"],
@ -89,13 +93,24 @@ class FloorRecommendations(Definitions):
)
self.estimated_u_value = u_value
if is_suspended:
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 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)
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!")
@staticmethod
def _make_floor_description(part, depth):
@ -122,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_floors
estimated_cost = cost_per_unit * self.property.floor_area
estimated_cost = cost_per_unit * quantity
self.recommendations.append(
{
@ -131,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
),

View file

@ -0,0 +1,282 @@
import math
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, convert_thickness_to_numeric
)
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
# It is recommended that lofts should have at least 270mm of insulation
MINIMUM_LOFT_ISULATION_MM = 270
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 = convert_thickness_to_numeric(
self.property.roof["insulation_thickness"],
self.property.roof["is_pitched"]
)
# We check if the roof is already insulated and if so, we exit
# 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 >= self.MINIMUM_LOFT_ISULATION_MM:
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})
if self.property.roof["is_pitched"] or self.property.roof["is_flat"]:
self.recommend_roof_insulation(u_value, insulation_thickness, self.property.roof)
return
if self.property.roof["is_roof_room"]:
self.recommend_room_roof_insulation(u_value, insulation_thickness)
return
raise NotImplementedError("Implement me")
@staticmethod
def make_loft_insulation_description(material, depth):
return f"Install {depth}{material['depth_unit']} of {material['description']} in your loft"
@staticmethod
def make_room_roof_insulation_description(material, depth):
return f"Insulate your room roof with {depth}{material['depth_unit']} of {material['description']}"
@staticmethod
def make_flat_roof_insulation_description(material, depth):
return f"Insulate the home's flat roof with {depth}{material['depth_unit']} of {material['description']}"
def recommend_roof_insulation(self, u_value, insulation_thickness, roof):
"""
This method will recommend which insulation materials to use
This function handles both the case of loft insulation and flat roof insulation
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/
The process roughly looks like the following:
- Remove the Existing Weatherproof Layer: If the roof is being replaced, remove the old weatherproof layer to
expose the timber roof surface.
- Install Insulation Boards: Lay the rigid insulation boards directly on the timber roof surface.
Ensure the boards fit tightly together to prevent thermal bridging (heat loss through the gaps).
- Add a Vapour Control Layer (VCL): This is crucial to prevent moisture from entering the insulation layer,
which can lead to dampness and rot. The VCL is placed over the insulation.
- Install a New Weatherproof Layer: On top of the insulation and VCL, install a new weatherproof layer. This
could be traditional roofing materials like bitumen-based felt, rubber membranes like EPDM, or fiberglass.
:param u_value: U-value of the roof before any retrofit measures have been installed
:param insulation_thickness: Existing Insulation thickness of the loft
:param roof: dictionary describing the make-up of the roof
: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
if roof["is_pitched"]:
materials = [m for m in self.materials if m["type"] == "loft_insulation"]
elif roof["is_flat"]:
materials = [m for m in self.materials if m["type"] == "flat_roof_insulation"]
else:
raise ValueError("Roof is not pitched or flat")
if not materials:
raise ValueError("No roof insulation materials found")
lowest_selected_u_value = None
recommendations = []
for material in 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.floor_area
if roof["is_pitched"]:
description = self.make_loft_insulation_description(material, depth)
else:
description = self.make_flat_roof_insulation_description(material, depth)
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": description,
"starting_u_value": u_value,
"new_u_value": new_u_value,
"sap_points": None,
"cost": estimated_cost,
}
)
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
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_description(material, depth),
"starting_u_value": u_value,
"new_u_value": new_u_value,
"sap_points": None,
"cost": estimated_cost,
}
)
self.recommendations = recommendations

View file

@ -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

View file

@ -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)
########################################################################################################################
#

View file

@ -1,11 +1,12 @@
import math
from copy import deepcopy
import numpy as np
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 +341,34 @@ 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:
"""
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 == "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 +401,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)
@ -517,3 +552,90 @@ 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
def convert_thickness_to_numeric(string_thickness, is_pitched):
"""
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
we handle loft insulation differently to flat roof or room in roof insulation, since for loft insulation,
we are presented with an insulation thickness, whereas for the other forms of roof, we are just told whether or not
the roof is insulated or not.
:param string_thickness: string measure of insulation thickness
:param is_pitched: boolean indicating if the roof is a pitched roof
:return: integer measure of insulation thickness
"""
if is_pitched:
lookup = {
"none": 0,
"below average": 50,
"average": 100,
"above average": 270
}
else:
lookup = {
"none": 0,
"below average": 100,
"average": 270,
"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)
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

View file

@ -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

View file

@ -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:
@ -98,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
@ -119,6 +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_floors = 1
recommender = FloorRecommendations(
property_instance=input_properties[2],
@ -127,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"]}
@ -140,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
@ -162,6 +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_floors = 1
recommender = FloorRecommendations(
property_instance=input_properties[4],
@ -171,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"]}
@ -183,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
@ -193,3 +218,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_floors = 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_floors = 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_floors = 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_floors = 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

View file

@ -1,3 +1,4 @@
import numpy as np
import pytest
import math
from unittest.mock import MagicMock
@ -277,6 +278,22 @@ class TestRecommendationUtils:
insulation_thickness=None,
)
def test_convert_thickness_to_numeric(self):
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+", 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():
assert math.isclose(
@ -333,3 +350,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

View file

@ -0,0 +1,429 @@
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
}
]
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
}
]
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
}
]
flat_roof_insulation_materials = [
{
'id': 18,
'type': 'flat_roof_insulation',
'description': 'Example flat 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.032727273, '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:
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)
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
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.pitched_roof_area = 110
roof_recommender7 = RoofRecommendations(
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"
def test_flat_no_insulation(self):
property_instance11 = Property(id=11, address1="fake", postcode="fake", epc_client=Mock())
property_instance11.age_band = "D"
property_instance11.floor_area = 150
property_instance11.roof = {
'original_description': 'Flat, no insulation (assumed)',
'clean_description': 'Flat, no insulation',
'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': False,
'is_roof_room': False, 'is_loft': False, 'is_flat': True, 'is_thatched': False, 'is_at_rafters': False,
'is_assumed': True, 'has_dwelling_above': False, 'is_valid': True, 'insulation_thickness': 'none'
}
roof_recommender11 = RoofRecommendations(
property_instance=property_instance11, materials=flat_roof_insulation_materials
)
assert not roof_recommender11.recommendations
roof_recommender11.recommend()
assert len(roof_recommender11.recommendations) == 1
assert roof_recommender11.recommendations[0]["parts"][0]["depths"] == [270]
assert roof_recommender11.recommendations[0]["new_u_value"] == 0.11
assert roof_recommender11.recommendations[0]["starting_u_value"] == 2.3
assert roof_recommender11.recommendations[0]["description"] == \
"Insulate the home's flat roof with 270mm of Example flat roof insulation"
def test_flat_insulated(self):
property_instance12 = Property(id=12, address1="fake", postcode="fake", epc_client=Mock())
property_instance12.age_band = "D"
property_instance12.floor_area = 150
property_instance12.roof = {
'original_description': 'Flat, insulated (assumed)',
'clean_description': 'Flat, insulated',
'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': False,
'is_roof_room': False,
'is_loft': False, 'is_flat': True, 'is_thatched': False, 'is_at_rafters': False, 'is_assumed': True,
'has_dwelling_above': False, 'is_valid': True, 'insulation_thickness': 'average'
}
roof_recommender12 = RoofRecommendations(
property_instance=property_instance12, materials=flat_roof_insulation_materials
)
assert not roof_recommender12.recommendations
roof_recommender12.recommend()
assert not roof_recommender12.recommendations
def test_flat_limited_insulation(self):
property_instance13 = Property(id=12, address1="fake", postcode="fake", epc_client=Mock())
property_instance13.age_band = "D"
property_instance13.floor_area = 150
property_instance13.roof = {
'original_description': 'Flat, limited insulation (assumed)',
'clean_description': 'Flat, limited insulation',
'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': False,
'is_roof_room': False,
'is_loft': False, 'is_flat': True, 'is_thatched': False, 'is_at_rafters': False, 'is_assumed': True,
'has_dwelling_above': False, 'is_valid': True, 'insulation_thickness': 'below average'
}
roof_recommender13 = RoofRecommendations(
property_instance=property_instance13, materials=flat_roof_insulation_materials
)
assert not roof_recommender13.recommendations
roof_recommender13.recommend()
assert len(roof_recommender13.recommendations) == 1
assert roof_recommender13.recommendations[0]["parts"][0]["depths"] == [220]
assert roof_recommender13.recommendations[0]["new_u_value"] == 0.14
assert roof_recommender13.recommendations[0]["starting_u_value"] == 2.3
assert roof_recommender13.recommendations[0]["description"] == \
"Insulate the home's flat roof with 220mm of Example flat roof insulation"