mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
415 lines
19 KiB
Python
415 lines
19 KiB
Python
import math
|
|
import pandas as pd
|
|
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, override_costs
|
|
)
|
|
from recommendations.Costs import Costs
|
|
|
|
|
|
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. If the property has more than 200mm of
|
|
# loft insulation in place already, we do not recommend anything for the moment
|
|
MINIMUM_LOFT_ISULATION_MM = 200
|
|
MINIMUM_RECOMMENDED_LOFT_INSULATION = 280
|
|
# Flat roof should have at least 100mm of insulation
|
|
MINIMUM_FLAT_ROOF_ISULATION_MM = 100
|
|
|
|
def __init__(
|
|
self,
|
|
property_instance: Property,
|
|
materials: List
|
|
):
|
|
self.property = property_instance
|
|
self.costs = Costs(self.property)
|
|
# 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.loft_insulation_materials = [
|
|
part for part in materials if part["type"] == "loft_insulation"
|
|
]
|
|
self.loft_non_insulation_materials = []
|
|
|
|
self.flat_roof_insulation_materials = [
|
|
part for part in materials if part["type"] == "flat_roof_insulation"
|
|
]
|
|
|
|
self.flat_roof_non_insulation_materials = [
|
|
part for part in materials if part["type"] in [
|
|
"flat_roof_preparation", "flat_roof_vapour_barrier", "flat_roof_waterproofing"
|
|
]
|
|
]
|
|
|
|
# Extract the insulation thickness from the roof, which is used throughout this method
|
|
self.insulation_thickness = convert_thickness_to_numeric(
|
|
self.property.roof["insulation_thickness"],
|
|
self.property.roof["is_pitched"],
|
|
self.property.roof["is_flat"]
|
|
)
|
|
|
|
def mds_loft_insulation(self, phase):
|
|
"""
|
|
For usages within the mds report
|
|
:param phase:
|
|
:return:
|
|
"""
|
|
self.recommendations = []
|
|
|
|
u_value = get_roof_u_value(**{**self.property.roof, "age_band": self.property.age_band})
|
|
|
|
self.recommend_roof_insulation(u_value, self.insulation_thickness, self.property.roof, phase)
|
|
|
|
return self.recommendations
|
|
|
|
def is_loft_already_insulated(self):
|
|
"""
|
|
Check if the loft is already insulated
|
|
"""
|
|
|
|
# If we have a non-invasive recommendation for the loft insulation, we can assume that the loft is not insulated
|
|
if "loft_insulation" in self.property.non_invasive_recommendations:
|
|
return False
|
|
|
|
return (self.insulation_thickness > self.MINIMUM_LOFT_ISULATION_MM) and self.property.roof["is_pitched"]
|
|
|
|
def recommend(self, phase):
|
|
|
|
if self.property.roof["has_dwelling_above"]:
|
|
return
|
|
|
|
u_value = self.property.roof["thermal_transmittance"]
|
|
|
|
# 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
|
|
# This only holds true for pitched roofs.
|
|
if self.is_loft_already_insulated():
|
|
return
|
|
|
|
if (self.insulation_thickness >= self.MINIMUM_FLAT_ROOF_ISULATION_MM) and self.property.roof["is_flat"]:
|
|
return
|
|
|
|
if self.property.roof["is_roof_room"]:
|
|
raise ValueError("Update convert_thickness_to_numeric for room roof and implement")
|
|
|
|
# If we have a u-value already, need to implement this
|
|
if u_value:
|
|
if u_value <= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE:
|
|
# The Roof is already compliant
|
|
return
|
|
|
|
if self.property.data["transaction-type"] == "new dwelling":
|
|
return
|
|
raise NotImplementedError("Implement me")
|
|
|
|
u_value = get_roof_u_value(**{**self.property.roof, "age_band": self.property.age_band})
|
|
|
|
self.estimated_u_value = u_value
|
|
if (u_value <= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE) and (
|
|
"loft_insulation" not in self.property.non_invasive_recommendations
|
|
):
|
|
# The Roof is already compliant
|
|
return
|
|
|
|
if self.property.roof["is_pitched"] or self.property.roof["is_flat"]:
|
|
insulation_thickness = (
|
|
0 if "loft_insulation" not in self.property.non_invasive_recommendations else self.insulation_thickness
|
|
)
|
|
self.recommend_roof_insulation(u_value, insulation_thickness, self.property.roof, phase)
|
|
return
|
|
|
|
if self.property.roof["is_roof_room"]:
|
|
self.recommend_room_roof_insulation(u_value, phase)
|
|
return
|
|
|
|
raise NotImplementedError("Implement me")
|
|
|
|
@staticmethod
|
|
def make_roof_insulation_description(material):
|
|
if material["type"] == "loft_insulation":
|
|
return f"Install {int(material['depth'])}{material['depth_unit']} of {material['description']} in your loft"
|
|
|
|
if material["type"] == "flat_roof_insulation":
|
|
return (
|
|
f"Insulate the home's flat roof with {int(material['depth'])}{material['depth_unit']} of "
|
|
f"{material['description']}"
|
|
)
|
|
if material["type"] == "room_roof_insulation":
|
|
return (f"Insulate your room roof with {int(material['depth'])}{material['depth_unit']} of "
|
|
f"{material['description']}")
|
|
|
|
raise ValueError("Invalid material type")
|
|
|
|
def recommend_roof_insulation(
|
|
self, u_value, insulation_thickness, roof, phase
|
|
):
|
|
|
|
"""
|
|
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"]:
|
|
insulation_materials = self.loft_insulation_materials
|
|
non_insulation_materials = self.loft_non_insulation_materials
|
|
elif roof["is_flat"]:
|
|
insulation_materials = self.flat_roof_insulation_materials
|
|
non_insulation_materials = self.flat_roof_non_insulation_materials
|
|
else:
|
|
raise ValueError("Roof is not pitched or flat")
|
|
|
|
if not insulation_materials:
|
|
raise ValueError("No roof insulation materials found")
|
|
|
|
insulation_materials = pd.DataFrame(insulation_materials)
|
|
|
|
lowest_selected_u_value = None
|
|
recommendations = []
|
|
for _, insulation_material_group in insulation_materials.groupby("description"):
|
|
|
|
for _, material in insulation_material_group.iterrows():
|
|
|
|
# We make sure we hit a depth of 270mm. We should factor in any existing insulation if the
|
|
# loft is already partially insulated.
|
|
# Note: This requirement is only for loft insulation
|
|
if (
|
|
(material["depth"] + insulation_thickness) < self.MINIMUM_RECOMMENDED_LOFT_INSULATION
|
|
) and roof["is_pitched"]:
|
|
continue
|
|
|
|
part_u_value = r_value_per_mm_to_u_value(material["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)
|
|
|
|
if material["type"] == "loft_insulation":
|
|
cost_result = self.costs.loft_insulation(
|
|
floor_area=self.property.insulation_floor_area,
|
|
material=material
|
|
)
|
|
already_installed = "loft_insulation" in self.property.already_installed
|
|
if already_installed:
|
|
cost_result = override_costs(cost_result)
|
|
new_thickness = insulation_thickness + material["depth"]
|
|
elif material["type"] == "flat_roof_insulation":
|
|
cost_result = self.costs.flat_roof_insulation(
|
|
floor_area=self.property.insulation_floor_area,
|
|
material=material,
|
|
non_insulation_materials=non_insulation_materials
|
|
)
|
|
already_installed = "flat_roof_insulation" in self.property.already_installed
|
|
if already_installed:
|
|
cost_result = override_costs(cost_result)
|
|
new_thickness = None
|
|
else:
|
|
raise ValueError("Invalid material type")
|
|
|
|
# This is based on the values we have in the training data
|
|
valid_numeric_values = [
|
|
12,
|
|
25,
|
|
50,
|
|
75,
|
|
100,
|
|
150,
|
|
200,
|
|
250,
|
|
270,
|
|
300,
|
|
350,
|
|
400,
|
|
]
|
|
|
|
proposed_depth = new_thickness
|
|
if new_thickness not in valid_numeric_values:
|
|
# Take the nearest value for scoring
|
|
proposed_depth = min(
|
|
valid_numeric_values, key=lambda x: abs(x - proposed_depth)
|
|
)
|
|
|
|
if proposed_depth >= 270:
|
|
new_efficiency = "Very Good"
|
|
else:
|
|
if self.property.data["walls-energy-eff"] not in ["Good", "Very Good"]:
|
|
new_efficiency = "Good"
|
|
|
|
recommendations.append(
|
|
{
|
|
"phase": phase,
|
|
"parts": [
|
|
get_recommended_part(
|
|
part=material.to_dict(),
|
|
quantity=self.property.insulation_wall_area,
|
|
quantity_unit=QuantityUnits.m2.value,
|
|
cost_result=cost_result
|
|
)
|
|
],
|
|
"type": material["type"],
|
|
"description": self.make_roof_insulation_description(material),
|
|
"starting_u_value": u_value,
|
|
"new_u_value": new_u_value,
|
|
"sap_points": None,
|
|
"already_installed": already_installed,
|
|
"new_thickness": new_thickness,
|
|
"description_simulation": {
|
|
"roof-description": f"Pitched, {int(proposed_depth)}mm loft insulation",
|
|
"roof-energy-eff": new_efficiency
|
|
},
|
|
**cost_result
|
|
}
|
|
)
|
|
|
|
self.recommendations = recommendations
|
|
|
|
def recommend_room_roof_insulation(self, u_value, phase):
|
|
"""
|
|
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
|
|
: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"]):
|
|
|
|
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(
|
|
{
|
|
"phase": phase,
|
|
"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": "room_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
|