Model/recommendations/RoofRecommendations.py

338 lines
16 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
)
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
# 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"
]
]
def recommend(self, phase):
if self.property.roof["has_dwelling_above"]:
return
u_value = self.property.roof["thermal_transmittance"]
insulation_thickness = convert_thickness_to_numeric(
self.property.roof["insulation_thickness"],
self.property.roof["is_pitched"],
self.property.roof["is_flat"]
)
# 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 (insulation_thickness > self.MINIMUM_LOFT_ISULATION_MM) and self.property.roof["is_pitched"]:
return
if (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:
# The Roof is already compliant
return
if self.property.roof["is_pitched"] or self.property.roof["is_flat"]:
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_LOFT_ISULATION_MM) 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
)
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
)
else:
raise ValueError("Invalid material type")
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,
**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