Model/model_data/recommendations/WallRecommendations.py
2023-06-20 18:45:08 +01:00

244 lines
8.6 KiB
Python

import pint
import re
from model_data.Property import Property
import pandas as pd
external_wall_insulation_parts = [
{
# Example product
# https://insulationgo.co.uk/100mm-rockwool-external-wall-insulation-dual-density-slabs-a1-non-combustible
# -slab-ewi-render-fire/
"type": "external_wall_insulation",
"description": "Mineral Wool External Wall Insulation",
"depths": [30, 50, 70, 80, 90, 100],
"depth_unit": "mm",
"cost": None,
"cost_unit": None,
# The u-value here is just a placehoder for now and we probably want to have multiple
# options for internal wall insulation (e.g. 50mm, 100mm, 150mm), with different material types
# and costs
"r_value_per_mm": 0.0278,
"r_value_unit": "m2K/W"
},
{
"type": "external_wall_insulation",
"description": "Expanded Polystyrene External Wall Insulation",
"depths": [],
"depth_unit": "mm",
"cost": None,
# The u-value here is just a placehoder for now and we probably want to have multiple
# options for internal wall insulation (e.g. 50mm, 100mm, 150mm), with different material types
# and costs
"u_value": None
},
{
"type": "external_wall_insulation",
"description": "Phenolic Foam External Wall Insulation",
"depths": [],
"depth_unit": "mm",
"cost": None,
# The u-value here is just a placehoder for now and we probably want to have multiple
# options for internal wall insulation (e.g. 50mm, 100mm, 150mm), with different material types
# and costs
"u_value": None
},
{
"type": "external_wall_insulation",
"description": "Polyisocyanurate Foam External Wall Insulation",
"depths": [],
"depth_unit": "mm",
"cost": None,
# The u-value here is just a placehoder for now and we probably want to have multiple
# options for internal wall insulation (e.g. 50mm, 100mm, 150mm), with different material types
# and costs
"u_value": None
},
{
"type": "external_wall_insulation",
"description": "Woof Fiber External Wall Insulation",
"depths": [],
"depth_unit": "mm",
"cost": None,
# The u-value here is just a placehoder for now and we probably want to have multiple
# options for internal wall insulation (e.g. 50mm, 100mm, 150mm), with different material types
# and costs
"u_value": None
},
{
"type": "external_wall_insulation",
"description": "Aerogel External Wall Insulation",
"depths": [],
"depth_unit": "mm",
"cost": None,
# The u-value here is just a placehoder for now and we probably want to have multiple
# options for internal wall insulation (e.g. 50mm, 100mm, 150mm), with different material types
# and costs
"u_value": None
},
{
"type": "external_wall_insulation",
"description": "Vacuum Insulation Panels External Wall Insulation",
"depths": [],
"depth_unit": "mm",
"cost": None,
# The u-value here is just a placehoder for now and we probably want to have multiple
# options for internal wall insulation (e.g. 50mm, 100mm, 150mm), with different material types
# and costs
"u_value": None
}
]
wall_parts = [
{
"id": 1,
"type": "internal_wall_insulation",
"description": "Internal wall insulation",
"depth": None,
"depth_unit": "mm",
"cost": None,
# The u-value here is just a placehoder for now and we probably want to have multiple
# options for internal wall insulation (e.g. 50mm, 100mm, 150mm), with different material types
# and costs
"u_value": 0.3
},
]
class WallRecommendations:
YEAR_WALLS_BUILT_WITH_INSULATION = 1990
U_VALUE_UNIT = 'w/m-¦k'
BUILDING_REGULATIONS_PART_L_MAX_U_VALUE = 0.18
# Add some error so that if, for example, a new part we recommend provides a u-value of 0.19,
# we still consider it as an option
U_VALUE_ERROR = 0.01
DEFAULT_U_VALUES = {
"solid_brick": 2,
}
def __init__(self, property_instance: Property):
self.property = property_instance
self.year_built = self._year_property_was_built()
# Will contains a list of recommended measures
self.recommendations = []
def _year_property_was_built(self):
"""
Estimates when the property was built based on as much available data as possible.
"""
if self.property.full_sap_epc:
return pd.to_datetime(self.property.full_sap_epc["lodgement-date"]).year
if self.property.data["construction-age-band"]:
# Take the upper limit
band = [int(x) for x in re.findall(r'\b\d{4}\b', self.property.data["construction-age-band"])]
return band[1]
raise NotImplementedError("Implement me!")
def recommend(self):
# if building built after 1990 + we're able to identify U-value +
# U-value less than 0.18 and if in or close to a conversation area,
# recommend internal wall insulation as a possible measure
u_value = self.property.walls["thermal_transmittance"]
is_cavity_wall = self.property.walls["is_cavity_wall"]
is_solid_brick = self.property.walls["is_solid_brick"]
if u_value:
if self.property.walls["thermal_transmittance_unit"] != self.U_VALUE_UNIT:
raise NotImplementedError("Haven't handled the case of other u value units yet")
if (not is_cavity_wall) and (self.year_built >= self.YEAR_WALLS_BUILT_WITH_INSULATION) and (
u_value >= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE
):
# Recommend internal wall insulation
iwi_parts = [part for part in wall_parts if part["type"] == "internal_wall_insulation"]
for part in iwi_parts:
_, new_u_value = self.calculate_u_value_uplift(u_value, part["u_value"])
new_u_value = round(new_u_value, 2)
# We allow a small tolerance for error so we don't discount the recommendation entirely
# if it's close, since this is an estimated new u-value
if new_u_value - self.U_VALUE_ERROR <= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE:
self.recommendations.append(
{
**part, "new_u_value": new_u_value,
}
)
raise NotImplementedError("Not implemented yet")
@staticmethod
def calculate_u_value_uplift(u_value, insulation_u_value):
"""
Calculates the U-value uplift (improvement) when applying internal wall insulation to a wall.
:param u_value: Float, Starting U-value of the wall (without insulation) in W/m²K.
:param insulation_u_value: Float, U-value of the internal wall insulation in W/m²K.
Returns:
float: U-value uplift (improvement) achieved by applying internal wall insulation in W/m²K.
Raises:
ZeroDivisionError: If either u_value or iwi_u_value is zero.
Notes:
This function assumes 100% coverage of the internal wall insulation and does not account for other factors
such as thermal bridging or the specific configuration of the wall.
"""
inverse_u_value = 1 / u_value
inverse_insulation_u_value = 1 / insulation_u_value
inverse_u_total = inverse_u_value + inverse_insulation_u_value
new_u_value = 1 / inverse_u_total
u_value_uplift = u_value - new_u_value
return u_value_uplift, new_u_value
@staticmethod
def rvalue_per_mm(total_r_value: float, thickness_mm: float) -> float:
"""Return R-value per mm.
Parameters
----------
total_r_value : float
Total R-value (in m2K/W).
thickness_mm : float
Thickness of the material in mm.
Returns
-------
float
R-value per mm.
"""
return total_r_value / thickness_mm
@staticmethod
def r_value_per_mm_to_u_value(depth_mm: int, r_value_per_mm: float):
"""
Converts R-value per mm to U-value in W/m²K.
Parameters
----------
depth_mm : int
Depth of the material in mm.
r_value_per_mm : float
R-value per mm.
Returns
-------
float
U-value in W/m²K.
"""
return 1 / (depth_mm * r_value_per_mm)