diff --git a/.idea/Model.iml b/.idea/Model.iml
index 4413bb06..b0f9c00d 100644
--- a/.idea/Model.iml
+++ b/.idea/Model.iml
@@ -7,7 +7,7 @@
-
+
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 3b05c6ac..1122b380 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,6 +1,9 @@
-
+
+
+
+
diff --git a/backend/Property.py b/backend/Property.py
index 045b6220..1094e7b2 100644
--- a/backend/Property.py
+++ b/backend/Property.py
@@ -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):
diff --git a/backend/app/db/models/materials.py b/backend/app/db/models/materials.py
index cf6dd971..1dc47276 100644
--- a/backend/app/db/models/materials.py
+++ b/backend/app/db/models/materials.py
@@ -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):
diff --git a/backend/app/plan/router.py b/backend/app/plan/router.py
index fdbf155d..23ad4262 100644
--- a/backend/app/plan/router.py
+++ b/backend/app/plan/router.py
@@ -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:
diff --git a/backend/app/plan/utils.py b/backend/app/plan/utils.py
index c06d9293..71a61be1 100644
--- a/backend/app/plan/utils.py
+++ b/backend/app/plan/utils.py
@@ -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
diff --git a/backend/tests/test_property.py b/backend/tests/test_property.py
index d7028bc6..b376db9e 100644
--- a/backend/tests/test_property.py
+++ b/backend/tests/test_property.py
@@ -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
diff --git a/backend/tests/test_sap_model_prep.py b/backend/tests/test_sap_model_prep.py
index dc793cad..ab7cd678 100644
--- a/backend/tests/test_sap_model_prep.py
+++ b/backend/tests/test_sap_model_prep.py
@@ -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(
diff --git a/etl/epc/property_change_app.py b/etl/epc/property_change_app.py
index 435b668d..4f49f6da 100644
--- a/etl/epc/property_change_app.py
+++ b/etl/epc/property_change_app.py
@@ -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()
diff --git a/recommendations/FloorRecommendations.py b/recommendations/FloorRecommendations.py
index 35e34648..bc24b6c3 100644
--- a/recommendations/FloorRecommendations.py
+++ b/recommendations/FloorRecommendations.py
@@ -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
),
diff --git a/recommendations/RoofRecommendations.py b/recommendations/RoofRecommendations.py
new file mode 100644
index 00000000..283370ac
--- /dev/null
+++ b/recommendations/RoofRecommendations.py
@@ -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
diff --git a/recommendations/WallRecommendations.py b/recommendations/WallRecommendations.py
index ad2ca861..12085840 100644
--- a/recommendations/WallRecommendations.py
+++ b/recommendations/WallRecommendations.py
@@ -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
diff --git a/recommendations/rdsap_tables.py b/recommendations/rdsap_tables.py
index 0ce139ab..e396f727 100644
--- a/recommendations/rdsap_tables.py
+++ b/recommendations/rdsap_tables.py
@@ -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)
+
########################################################################################################################
#
diff --git a/recommendations/recommendation_utils.py b/recommendations/recommendation_utils.py
index cd7bb3f8..13f58fd9 100644
--- a/recommendations/recommendation_utils.py
+++ b/recommendations/recommendation_utils.py
@@ -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
diff --git a/recommendations/tests/test_fireplace_recommendations.py b/recommendations/tests/test_fireplace_recommendations.py
new file mode 100644
index 00000000..a1e0c1c6
--- /dev/null
+++ b/recommendations/tests/test_fireplace_recommendations.py
@@ -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
diff --git a/recommendations/tests/test_floor_recommendations.py b/recommendations/tests/test_floor_recommendations.py
index f34bbe81..82ba7cf4 100644
--- a/recommendations/tests/test_floor_recommendations.py
+++ b/recommendations/tests/test_floor_recommendations.py
@@ -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
diff --git a/recommendations/tests/test_recommendation_utils.py b/recommendations/tests/test_recommendation_utils.py
index eb1a5024..22280ed5 100644
--- a/recommendations/tests/test_recommendation_utils.py
+++ b/recommendations/tests/test_recommendation_utils.py
@@ -1,3 +1,4 @@
+import numpy as np
import pytest
import math
from unittest.mock import MagicMock
@@ -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
diff --git a/recommendations/tests/test_roof_recommendations.py b/recommendations/tests/test_roof_recommendations.py
new file mode 100644
index 00000000..37cc2daf
--- /dev/null
+++ b/recommendations/tests/test_roof_recommendations.py
@@ -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"