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

318 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 _estimate_suspended_floor_u_value(
self, floor_area, number_of_rooms, insulation_thickness, wall_type, region, age_band
):
"""
Estimate the u-value of a suspended floor, based on RdSap methodology
Default U-value for UNINSULATED suspended floor, based on RdSAP methodology
https://files.bregroup.com/bre-co-uk-file-library-copy/filelibrary/SAP/2012/RdSAP-9.93/RdSAP_2012_9.93.pdf
w = wall thickness, where these estimates are based on the RD SAP methodology, as in table S3
A = floor area
Exposed perimeter = P
soil type clas thermal conductivity lambda_g = 1.5 W/mK
Rsi = 0.17m^2K/W
Rse = 0.04m^2K/W
Rf = 0.001 * d_ins / 0.035 where d_ins is the insulation thickness in mm
height above external ground h = 0.3m
average wind speed at 10m height v=5m/s
wind sheilding factor fw = 0.05
vantilation factor E = 0.003 m^2/m
U-value of walls to underfloor space Uw = 1.5 W/m^2K
# Calulations for suspended ground floors, example for 5 bedroom house with permiter estimated at
44.36214602563767
1) dg = w + lambda_g x (Rsi + Rse) = 0.5 + 1.5 * (0.17 + 0.04) = 0.615
2) B = 2 * A/P = 2 * 123.0 / 44.36214602563767 = 5.545268253204708
3) Ug = 2 * lambda_g * log(pi * B/dg + 1)/(pi * B + dg) =
2 * 1.5 * log(3.141592653589793 * 5.545268253204708/0.615 + 1) / (3.141592653589793 * 5.545268253204708
+ 0.615) = 0.5619604457160708
4) Ux = (2 * h * Uw /B) + (1450 * E * v * fw/B) = (2 * 0.3 * 1.5 / 5.545268253204708) + (1450 * 0.003 * 5 *
0.05/5.545268253204708) = 0.35841367978030436
5) U = 1/ (2 * Rsi + Rf + 1/(Ug + Ux)) = 1 / (2 * 0.17 + 0 + 1/(0.5619604457160708 + 0.35841367978030436)) =
0.701
"""
age_band_letter = [x for x in age_band_data if x[region] == age_band][0]["age_band"]
defaults = {
# We need width in meters
"w": [x[age_band_letter] for x in default_wall_thickness if x["type"] == wall_type][0] / 1000,
"lambda_g": 1.5,
"Rsi": 0.17,
"Rse": 0.04,
"Rf": 0.001 * insulation_thickness / 0.035,
"h": 0.3,
"v": 5,
"fw": 0.05,
"E": 0.003,
"Uw": 1.5,
}
dg = defaults["w"] + defaults["lambda_g"] * (defaults["Rsi"] + defaults["Rse"])
# P is the exposed perimeter, which we estimate as we not have this data
if number_of_rooms <= 2:
p = estimate_perimeter_2_rooms(floor_area=floor_area)
else:
p = estimate_perimeter(floor_area=floor_area, num_rooms=number_of_rooms)
b = 2 * floor_area / p
u_g = 2 * defaults["lambda_g"] * math.log(math.pi * b / dg + 1) / (math.pi * b + dg)
u_x = (2 * defaults["h"] * defaults["Uw"] / b) + (1450 * defaults["E"] * defaults["v"] * defaults["fw"] / b)
# This is the final estimated U-value
u = 1 / (2 * defaults["Rsi"] + defaults["Rf"] + 1 / (u_g + u_x))
return u
def _estimate_solid_floor_u_value(self):
"""
1. dt =w + g × (Rsi + Rf + Rse)
2. B = 2 × A/P
3. if dt < B, U = 2 × g × ln( × B/dt + 1)/( × B + dt)
4. if dt >= B, U = g / (0.457 × B + dt)
:return:
"""
# TODO: complete
insulation_thickness = 0
age_band_letter = "E"
wall_type = "cavity"
floor_area = 37.26
perimeter = 16.2
# age_band_letter = [x for x in age_band_data if x[region] == age_band][0]["age_band"]
defaults = {
# We need width in meters
"w": [x[age_band_letter] for x in default_wall_thickness if x["type"] == wall_type][0] / 1000,
"lambda_g": 1.5,
"Rsi": 0.17,
"Rse": 0.04,
"Rf": 0.001 * insulation_thickness / 0.035,
"h": 0.3,
"v": 5,
"fw": 0.05,
"E": 0.003,
"Uw": 1.5,
}
dt = defaults["w"] + defaults["lambda_g"] * (defaults["Rsi"] + defaults["Rse"] + defaults["Rf"])
# perimeter = self._estimate_perimeter(floor_area, number_of_rooms)
B = 2 * floor_area / perimeter
if dt < B:
U = 2 * defaults["lambda_g"] * math.log(math.pi * B / dt + 1) / (math.pi * B + dt)
else:
print("implement me")
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