Model/recommendations/FloorRecommendations.py
2023-09-21 15:44:09 +01:00

211 lines
8.5 KiB
Python

import math
from typing import List
from model_data.BaseUtility import Definitions
from datatypes.enums import QuantityUnits
from backend.Property import Property
from recommendations.rdsap_tables import default_wall_thickness, age_band_data
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_uvalue_estimate, estimate_perimeter, estimate_perimeter_2_rooms
)
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
# TODO: This is a placeholder methodology which isn't particularly scalable as more
# unusual floor descriptions are introduced
FLOOR_LEVELS = {
"Ground": 0,
# We don't know what floor level, we just make sure it's not 0
"mid floor": 1,
"4th": 4,
# We set
"00": 0,
"3rd": 3
}
def __init__(
self,
property_instance: Property,
uvalue_estimates: List,
total_floor_area_group_decile: str,
materials: List,
):
self.property = property_instance
self.uvalue_estimates = uvalue_estimates
self.total_floor_area_group_decile = total_floor_area_group_decile
# 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
self.suspended_floor_insulation_parts = [
part for part in self.materials if part["type"] == "suspended_floor_insulation"
]
self.solid_floor_insulation_parts = [
part for part in self.materials if part["type"] == "solid_floor_insulation"
]
def recommend(self):
u_value = self.property.floor["thermal_transmittance"]
is_suspended = self.property.floor["is_suspended"]
insulation_thickness = self.property.floor["insulation_thickness"]
is_solid = self.property.floor["is_solid"]
floor_level = (
self.FLOOR_LEVELS[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"]
year_built = self.property.year_built
if self.property.floor["another_property_below"]:
# If there's another property below, it's likely impractical to recommend a floor upgrade
return
# If the property is a flat that isn't at ground level, it's likely impractical to recommend a floor upgrade
if (floor_level != 0) and (property_type == "Flat"):
return
if u_value:
if self.property.data["property-type"] != "House":
raise NotImplementedError("Implement me")
# 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
# For these methods, we need to know the additional details about the property
if self.property.walls["is_solid_brick"]:
wall_type = "solid brick"
else:
raise NotImplementedError("Implement me")
total_floor_area = float(self.property.data["total-floor-area"])
number_of_rooms = float(self.property.data["number-habitable-rooms"])
if self.property.data["property-type"] == "House":
num_floors = self._estimate_floors(total_floor_area, number_of_rooms)
elif self.property.data["property-type"] == "Flat":
num_floors = 1
else:
raise NotImplementedError("Implement me")
if insulation_thickness == "none":
region_str, age_band = self.property.data["construction-age-band"].split(":")
region_str = region_str.strip()
age_band = age_band.strip()
region = self.REGION_LOOKUP[region_str]
u_value = self._estimate_suspended_floor_u_value(
floor_area=total_floor_area / num_floors,
number_of_rooms=number_of_rooms / num_floors,
insulation_thickness=0,
wall_type=wall_type,
region=region,
age_band=age_band,
)
else:
u_value = get_uvalue_estimate(
uvalue_estimates=self.uvalue_estimates,
property=self.property,
total_floor_area_group_decile=self.total_floor_area_group_decile
)
self.estimated_u_value = u_value
if is_suspended:
# Given the U-value, we recommend underfloor insulation
self.recommend_floor_insulation(u_value=u_value, parts=self.suspended_floor_insulation_parts)
if 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)
@staticmethod
def _make_floor_description(part, depth):
return f"Install {depth}{part['depth_unit']} {part['description']} insulation"
def recommend_floor_insulation(self, u_value, parts):
"""
This method is tasked with estimating the impact of performing suspended floor insulation
:return:
"""
lowest_selected_u_value = None
for part in parts:
for depth, cost_per_unit in zip(part["depths"], part["cost"]):
part_u_value = r_value_per_mm_to_u_value(depth, part["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)
estimated_cost = cost_per_unit * self.property.floor_area
self.recommendations.append(
{
"parts": [
get_recommended_part(
part=part,
selected_depth=depth,
quantity=self.property.floor_area,
quantity_unit=QuantityUnits.m2.value,
selected_total_cost=estimated_cost
),
],
"type": "floor_insulation",
"description": self._make_floor_description(part, depth),
"starting_u_value": u_value,
"new_u_value": new_u_value,
"sap_points": None,
"cost": estimated_cost,
}
)
@staticmethod
def _estimate_floors(floor_area, num_rooms):
"""
Simple utility funciton, which assuming a 15m squared room, estimates the number of floors in a property
:param floor_area: Gross floor area of a property
:param num_rooms: Number of rooms in a property
:return: Number of floors in a property
"""
# Estimate total room area
total_room_area = num_rooms * 15
# Estimate the number of floors
floors = floor_area / total_room_area
# Round up to the nearest whole number
floors = round(floors)
return floors