Model/recommendations/FloorRecommendations.py
2024-04-12 15:31:07 +01:00

232 lines
9.7 KiB
Python

import math
from typing import List
import pandas as pd
from BaseUtility import Definitions
from datatypes.enums import QuantityUnits
from backend.Property import Property
from recommendations.recommendation_utils import (
r_value_per_mm_to_u_value, calculate_u_value_uplift, is_diminishing_returns, update_lowest_selected_u_value,
get_recommended_part, get_floor_u_value, override_costs
)
from recommendations.Costs import Costs
class FloorRecommendations(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
BUILDING_REGULATIONS_PART_L_MAX_U_VALUE = 0.25
# 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.2
REGION_LOOKUP = {
"England and Wales": "England_Wales",
}
PART_L_YEAR_CUTOFF = 2002
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.suspended_floor_insulation_materials = [
part for part in materials if part["type"] == "suspended_floor_insulation"
]
self.suspended_floor_non_insulation_materials = [
part for part in materials if part["type"] in [
"suspended_floor_demolition", "suspended_floor_redecoration", "suspended_floor_vapour_barrier"
]
]
# For solid floor, we don't use materials that are too thick
self.solid_floor_insulation_materials = [
part for part in materials if part["type"] == "solid_floor_insulation" if float(part["depth"]) <= 75
]
self.solid_floor_non_insulation_materials = [
part for part in materials if part["type"] in [
"solid_floor_demolition", "solid_floor_preparation", "solid_floor_vapour_barrier",
"solid_floor_redecoration"
]
]
self.exposed_floor_insulation_materials = [
part for part in materials if part["type"] == "exposed_floor_insulation"
]
# TODO: To be completed
self.exposed_floor_non_insulation_materials = []
def recommend(self, phase=0):
u_value = self.property.floor["thermal_transmittance"]
property_type = self.property.data["property-type"]
floor_area = self.property.insulation_floor_area
year_built = self.property.year_built
if self.property.floor["another_property_below"] | (self.property.floor["insulation_thickness"] in [
"average", "above average"
]):
# If there's another property below, it's likely impractical to recommend a floor upgrade,
# or if the floor is already insualted
return
# If the property is a flat that isn't at ground level, it's likely impractical to recommend a floor upgrade
if (self.property.floor_level != 0) and (property_type == "Flat") and (
self.property.floor["another_property_below"]
):
return
# If the property is a new build flat, we won't recommend floor upgrades
if len(self.property.full_sap_epc) and (property_type == "Flat"):
return
if u_value:
# By being built more recently than this, it means that the property was likely build with soild
# concrete floors with insulation already
if year_built < self.PART_L_YEAR_CUTOFF:
raise NotImplementedError("Not investigated this use case")
if u_value <= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE:
# The floor is already compliant
return
if u_value is None:
u_value = get_floor_u_value(
floor_type=self.property.floor_type,
area=floor_area,
perimeter=self.property.perimeter,
age_band=self.property.age_band,
insulation_thickness=self.property.floor["insulation_thickness"],
wall_type=self.property.wall_type
)
self.estimated_u_value = u_value
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(
phase=phase,
u_value=u_value,
insulation_materials=self.suspended_floor_insulation_materials,
non_insulation_materials=self.suspended_floor_non_insulation_materials
)
return
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,
insulation_materials=self.solid_floor_insulation_materials,
non_insulation_materials=self.solid_floor_non_insulation_materials,
phase=phase
)
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(material):
if material["type"] == "suspended_floor_insulation":
return (f"Install {int(material['depth'])}{material['depth_unit']} {material['description']} insulation in "
f"suspended floor")
if material["type"] == "solid_floor_insulation":
return (f"Install {int(material['depth'])}{material['depth_unit']} {material['description']} insulation on "
f"solid floor")
if material["type"] == "exposed_floor_insulation":
return (f"Install {int(material['depth'])}{material['depth_unit']} {material['description']} insulation in "
f"exposed floor")
raise ValueError("Invalid material type - implement me!")
def recommend_floor_insulation(self, u_value, insulation_materials, non_insulation_materials, phase):
"""
This method is tasked with estimating the impact of performing suspended floor insulation
:return:
"""
insulation_materials = pd.DataFrame(insulation_materials)
lowest_selected_u_value = None
for _, insulation_material_group in insulation_materials.groupby("description"):
for _, material in insulation_material_group.iterrows():
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 is_diminishing_returns(
self.recommendations, new_u_value, lowest_selected_u_value, self.DIMINISHING_RETURNS_U_VALUE
):
continue
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"] == "suspended_floor_insulation":
cost_result = self.costs.suspended_floor_insulation(
insulation_floor_area=self.property.insulation_floor_area,
material=material.to_dict(),
non_insulation_materials=non_insulation_materials
)
is_override = "suspended_floor_insulation" in self.property.override
if is_override:
cost_result = override_costs(cost_result)
elif material["type"] == "solid_floor_insulation":
cost_result = self.costs.solid_floor_insulation(
insulation_floor_area=self.property.insulation_floor_area,
material=material.to_dict(),
non_insulation_materials=non_insulation_materials
)
is_override = "solid_floor_insulation" in self.property.override
if is_override:
cost_result = override_costs(cost_result)
else:
raise NotImplementedError("Implement me!")
self.recommendations.append(
{
"phase": phase,
"parts": [
get_recommended_part(
part=material.to_dict(),
quantity=self.property.insulation_floor_area,
quantity_unit=QuantityUnits.m2.value,
cost_result=cost_result
),
],
"type": material["type"],
"description": self._make_floor_description(material),
"starting_u_value": u_value,
"new_u_value": new_u_value,
"sap_points": None,
**cost_result
}
)