mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Merge pull request #252 from Hestia-Homes/roof-recommendations
Roof recommendations
This commit is contained in:
commit
d8c0fec7b3
18 changed files with 1401 additions and 95 deletions
2
.idea/Model.iml
generated
2
.idea/Model.iml
generated
|
|
@ -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
5
.idea/misc.xml
generated
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
),
|
||||
|
|
|
|||
282
recommendations/RoofRecommendations.py
Normal file
282
recommendations/RoofRecommendations.py
Normal 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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
########################################################################################################################
|
||||
#
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
58
recommendations/tests/test_fireplace_recommendations.py
Normal file
58
recommendations/tests/test_fireplace_recommendations.py
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
429
recommendations/tests/test_roof_recommendations.py
Normal file
429
recommendations/tests/test_roof_recommendations.py
Normal 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"
|
||||
Loading…
Add table
Reference in a new issue