mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
318 lines
13 KiB
Python
318 lines
13 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 _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
|