Model/recommendations/RoofRecommendations.py
2023-11-13 15:57:51 +09:00

155 lines
6.7 KiB
Python

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"])
# 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"]:
self.recommend_loft_insulation(u_value, insulation_thickness)
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']}"
def recommend_loft_insulation(self, u_value, insulation_thickness):
"""
This method will recommend which insulation materials to use
:param u_value: U-value of the roof before any retrofit measures have been installed
:param insulation_thickness: Existing Insulation thickness of the loft
: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
loft_insulation_materials = [m for m in self.materials if m["type"] == "loft_insulation"]
lowest_selected_u_value = None
recommendations = []
for material in loft_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.floor_area
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": self.make_loft_insulation_description(material, depth),
"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
:param u_value:
:param insulation_thickness:
:return:
"""