mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
501 lines
21 KiB
Python
501 lines
21 KiB
Python
import itertools
|
|
import math
|
|
|
|
from datatypes.enums import QuantityUnits
|
|
from backend.Property import Property
|
|
from model_data.BaseUtility import Definitions
|
|
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
|
|
)
|
|
|
|
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, 150, 200],
|
|
"depth_unit": "mm",
|
|
"cost": None,
|
|
"cost_unit": None,
|
|
"r_value_per_mm": 0.0278,
|
|
"r_value_unit": "square_meter_kelvin_per_watt",
|
|
"thermal_conductivity": 0.036,
|
|
"thermal_conductivity_unit": "watt_per_meter_kelvin"
|
|
},
|
|
{
|
|
# Example product
|
|
# https://www.insulationking.co.uk/products/polystyrene-eps70?variant=44156186558759
|
|
"type": "external_wall_insulation",
|
|
"description": "Expanded Polystyrene External Wall Insulation",
|
|
"depths": [25, 50, 100, 125],
|
|
"depth_unit": "mm",
|
|
"cost": None,
|
|
"cost_unit": None,
|
|
"r_value_per_mm": 0.02703,
|
|
"r_value_unit": "square_meter_kelvin_per_watt",
|
|
"thermal_conductivity": 0.037,
|
|
"thermal_conductivity_unit": "watt_per_meter_kelvin"
|
|
},
|
|
{
|
|
# Example product
|
|
# https://www.insulationshop.co/20mm_kooltherm_k5_external_wall_kingspan.html
|
|
"type": "external_wall_insulation",
|
|
"description": "Phenolic Foam External Wall Insulation",
|
|
"depths": [20, 50, 100],
|
|
"depth_unit": "mm",
|
|
"cost": None,
|
|
"cost_unit": None,
|
|
"r_value_per_mm": 0.043478260869565216,
|
|
"r_value_unit": "square_meter_kelvin_per_watt",
|
|
"thermal_conductivity": 0.023,
|
|
"thermal_conductivity_unit": "watt_per_meter_kelvin"
|
|
|
|
},
|
|
{
|
|
"type": "external_wall_insulation",
|
|
"description": "Polyisocyanurate/Polyurethane Foam External Wall Insulation",
|
|
"depths": [],
|
|
"depth_unit": "mm",
|
|
"cost": None,
|
|
"cost_unit": None,
|
|
"r_value_per_mm": None,
|
|
"r_value_unit": "square_meter_kelvin_per_watt",
|
|
"thermal_conductivity": None,
|
|
"thermal_conductivity_unit": "watt_per_meter_kelvin"
|
|
},
|
|
{
|
|
# Example product
|
|
# https://www.mikewye.co.uk/product/steico-duo-dry/
|
|
"type": "external_wall_insulation",
|
|
"description": "Wood Fiber External Wall Insulation",
|
|
"depths": [40, 60],
|
|
"depth_unit": "mm",
|
|
"cost": None,
|
|
"cost_unit": None,
|
|
"r_value_per_mm": 0.023255813953488375,
|
|
"r_value_unit": "square_meter_kelvin_per_watt",
|
|
"thermal_conductivity": 0.043,
|
|
"thermal_conductivity_unit": "watt_per_meter_kelvin"
|
|
},
|
|
{
|
|
# Example product
|
|
# https://www.thermablok.co.uk/site/wp-content/uploads/2022/09/Thermablok-Aerogel-Insulation-Blanket-TDS-AIS
|
|
# -and-Steel-Related-Details.pdf
|
|
"type": "external_wall_insulation",
|
|
"description": "Aerogel External Wall Insulation",
|
|
"depths": [10, 20, 30, 40, 50, 60, 70],
|
|
"depth_unit": "mm",
|
|
"cost": None,
|
|
"cost_unit": None,
|
|
"r_value_per_mm": 0.06666666666666667,
|
|
"r_value_unit": "square_meter_kelvin_per_watt",
|
|
"thermal_conductivity": 0.015,
|
|
"thermal_conductivity_unit": "watt_per_meter_kelvin"
|
|
},
|
|
{
|
|
"type": "external_wall_insulation",
|
|
"description": "Vacuum Insulation Panels External Wall Insulation",
|
|
"depths": [45, 60],
|
|
"depth_unit": "mm",
|
|
"cost": None,
|
|
"cost_unit": None,
|
|
"r_value_per_mm": 0.16666666666666666,
|
|
"r_value_unit": "square_meter_kelvin_per_watt",
|
|
"thermal_conductivity": 0.006,
|
|
"thermal_conductivity_unit": "watt_per_meter_kelvin"
|
|
}
|
|
]
|
|
|
|
internal_wall_insulation_parts = [
|
|
{
|
|
# Example product
|
|
# https://www.insulationshop.co/25mm_polystyrene_insulation_eps_70jablite.html
|
|
"type": "internal_wall_insulation",
|
|
"description": "Rigid Insulation Boards Internal Wall Insulation",
|
|
"depths": [25, 40, 50, 75, 100],
|
|
"depth_unit": "mm",
|
|
"cost": None,
|
|
"cost_unit": None,
|
|
"r_value_per_mm": 0.026315789473684213,
|
|
"r_value_unit": "square_meter_kelvin_per_watt",
|
|
"thermal_conductivity": 0.038,
|
|
"thermal_conductivity_unit": "watt_per_meter_kelvin"
|
|
},
|
|
{
|
|
# Example product
|
|
# https://www.rockwool.com/siteassets/rw-uk/downloads/datasheets/flexi.pdf
|
|
"type": "internal_wall_insulation",
|
|
"description": "Mineral Wool Internal Wall Insulation",
|
|
"depths": [140],
|
|
"depth_unit": "mm",
|
|
"cost": None,
|
|
"cost_unit": None,
|
|
"r_value_per_mm": 0.02857142857142857,
|
|
"r_value_unit": "square_meter_kelvin_per_watt",
|
|
"thermal_conductivity": 0.035,
|
|
"thermal_conductivity_unit": "watt_per_meter_kelvin"
|
|
},
|
|
{
|
|
# Example product
|
|
# https://www.kingspan.com/gb/en/products/insulation-boards/wall-insulation-boards/kooltherm-k118-insulated
|
|
# -plasterboard/
|
|
"type": "internal_wall_insulation",
|
|
"description": "Insulated Plasterboard Internal Wall Insulation",
|
|
"depths": [25, 80],
|
|
"depth_unit": "mm",
|
|
"cost": None,
|
|
"cost_unit": None,
|
|
"r_value_per_mm": 0.02857142857142857,
|
|
"r_value_unit": "square_meter_kelvin_per_watt",
|
|
"thermal_conductivity": 0.019,
|
|
"thermal_conductivity_unit": "watt_per_meter_kelvin"
|
|
},
|
|
{
|
|
"type": "internal_wall_insulation",
|
|
"description": "Reflective Internal Wall Insulation",
|
|
"depths": [],
|
|
"depth_unit": "mm",
|
|
"cost": None,
|
|
"cost_unit": None,
|
|
"r_value_per_mm": None,
|
|
"r_value_unit": "square_meter_kelvin_per_watt",
|
|
"thermal_conductivity": None,
|
|
"thermal_conductivity_unit": "watt_per_meter_kelvin"
|
|
},
|
|
{
|
|
# Example product
|
|
# https://www.insulationsuperstore.co.uk/product/vacutherm-vacupor-nt-b2-vacuum-insulated-panel-1m-x-600mm-x
|
|
# -30mm.html
|
|
"type": "internal_wall_insulation",
|
|
"description": "Vacuum Insulation Panels Wall Insulation",
|
|
"depths": [20, 30],
|
|
"depth_unit": "mm",
|
|
"cost": None,
|
|
"cost_unit": None,
|
|
"r_value_per_mm": 0.125,
|
|
"r_value_unit": "square_meter_kelvin_per_watt",
|
|
"thermal_conductivity": 0.008,
|
|
"thermal_conductivity_unit": "watt_per_meter_kelvin"
|
|
},
|
|
]
|
|
|
|
wall_parts = external_wall_insulation_parts + internal_wall_insulation_parts
|
|
|
|
|
|
class WallRecommendations(Definitions):
|
|
YEAR_WALLS_BUILT_WITH_INSULATION = 1990
|
|
# After 1930, Solid brick walls became less populate and instead, cavity walls became a
|
|
# more popular choice
|
|
YEARS_CAVITY_WALLS_BEGAN = 1930
|
|
U_VALUE_UNIT = 'w/m-¦k'
|
|
|
|
# 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.3
|
|
# 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.25
|
|
|
|
# Part L regulations indicate that any new build should have walls that achieve a u-value of no higher
|
|
# than 0.18.
|
|
BUILDING_REGULATIONS_PART_L_NEW_BUILD_MAX_U_VALUE = 0.18
|
|
# 0.15 is an often cited diminishing returns value for new builds
|
|
NEW_BUILD_DIMINISHING_RETURNS_U_VALUE = 0.15
|
|
|
|
# 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
|
|
|
|
# TODO: Review this value against RdSAP
|
|
# Page 19 of rdsap here:
|
|
# https://files.bregroup.com/bre-co-uk-file-library-copy/filelibrary/SAP/2012/RdSAP-9.93/RdSAP_2012_9.93.pdf
|
|
# provides default U-values for solid brick walls depending on age band
|
|
DEFAULT_U_VALUES = {
|
|
"solid_brick": 2,
|
|
}
|
|
|
|
def __init__(self, property_instance: Property, uvalue_estimates, total_floor_area_group_decile, materials=None):
|
|
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 = []
|
|
|
|
if materials:
|
|
self.materials = materials
|
|
else:
|
|
self.materials = wall_parts
|
|
|
|
@property
|
|
def ewi_valid(self):
|
|
"""
|
|
This method check available data, to determine if a property is suitable for external wall insulation
|
|
"""
|
|
|
|
# Current logic: If the property is in a conservation area or a flat, it is not suitable for EWI
|
|
if (self.property.in_conservation_area in ["in_conversation_area"]) or \
|
|
(self.property.data["property-type"].lower() == "flat"):
|
|
return False
|
|
|
|
return True
|
|
|
|
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"]
|
|
insulation_thickness = self.property.walls["insulation_thickness"]
|
|
|
|
# We check if the wall is already insulated and if so, we exit
|
|
if insulation_thickness in ["average", "above average"]:
|
|
return
|
|
|
|
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")
|
|
|
|
# TODO: It's worth thinking about this logic because depending on when properties were built,
|
|
# they're likely to be of a certain standard. E.g. properties built within a certain time
|
|
# period are likely to have cavity walls
|
|
|
|
# We can't detect it's a cavity wall, but it was built after 1990 so likely built with insulation already
|
|
# + it already has a U-value WORSE than the building regulations, so we recommend either internal or
|
|
# external wall insulation
|
|
if (not is_cavity_wall) and (self.property.year_built >= self.YEAR_WALLS_BUILT_WITH_INSULATION) and (
|
|
u_value >= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE
|
|
):
|
|
# Recommend insulation
|
|
self.find_insulation(u_value)
|
|
return
|
|
|
|
# We can't detect it's a cavity wall, but it was built after 1990 so likely built with insulation already
|
|
# + it already has a U-value better than the building regulations, so we don't need to recommend anything
|
|
if (not is_cavity_wall) and (self.property.year_built >= self.YEAR_WALLS_BUILT_WITH_INSULATION) and (
|
|
u_value <= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE
|
|
):
|
|
# Recommend nothing
|
|
return
|
|
|
|
raise NotImplementedError("Not implemented yet")
|
|
|
|
if is_solid_brick:
|
|
|
|
if insulation_thickness == "none":
|
|
# This is an estimated figure based on industry standards
|
|
u_value = self.DEFAULT_U_VALUES["solid_brick"]
|
|
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 u_value >= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE:
|
|
self.find_insulation(u_value)
|
|
return
|
|
|
|
# If the u-value is within regulations, we don't do anything
|
|
return
|
|
|
|
raise NotImplementedError("Not implemented yet")
|
|
|
|
def _find_insulation(self, parts, u_value):
|
|
lowest_selected_u_value = None
|
|
recommendations = []
|
|
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 I have a lowest U value and my new u value is higher than that but lower than the
|
|
# diminishing returns threshold, it can be considered
|
|
|
|
# If I have a lowest U value and my new u value is lower than the lowest value, it's
|
|
# further into the diminishing returns threshold and can shouldn't be
|
|
|
|
if is_diminishing_returns(
|
|
recommendations, new_u_value, lowest_selected_u_value, self.DIMINISHING_RETURNS_U_VALUE
|
|
):
|
|
continue
|
|
|
|
# We allow a small tolerance for error so we don't discount the recommendation entirely
|
|
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.insulation_wall_area
|
|
|
|
recommendations.append(
|
|
{
|
|
"parts": [
|
|
get_recommended_part(
|
|
part=part,
|
|
selected_depth=depth,
|
|
quantity=self.property.insulation_wall_area,
|
|
quantity_unit=QuantityUnits.m2.value,
|
|
selected_total_cost=estimated_cost
|
|
)
|
|
],
|
|
"type": "wall_insulation",
|
|
"description": "Install " + self._make_description(part, depth),
|
|
"starting_u_value": u_value,
|
|
"new_u_value": new_u_value,
|
|
"sap_points": None,
|
|
"cost": estimated_cost,
|
|
}
|
|
)
|
|
|
|
return recommendations
|
|
|
|
def find_insulation(self, u_value):
|
|
"""
|
|
This function contains the logic for finding potential insulation measures for a property, depending
|
|
on the parts available and whether the property can have external wall insulation installed
|
|
:return:
|
|
"""
|
|
|
|
ewi_parts = [
|
|
part for part in self.materials if part["type"] == "external_wall_insulation"
|
|
] if self.ewi_valid else []
|
|
|
|
iwi_parts = [part for part in self.materials if part["type"] == "internal_wall_insulation"]
|
|
|
|
# Recommend external and internal wall insulation separately
|
|
# Since external and internal wall insulation are sufficiently different,
|
|
# we separate the logic for for recommending them, therefore we don't
|
|
# consider diminishing returns between the two
|
|
|
|
ewi_recommendations = self._find_insulation(ewi_parts, u_value)
|
|
iwi_recommendations = self._find_insulation(iwi_parts, u_value)
|
|
|
|
self.recommendations += ewi_recommendations + iwi_recommendations
|
|
|
|
# We also can recommend both internal and external wall insulation together
|
|
# By looping through ewi first, if there is nothing there, that ensures not combinations are tested
|
|
for ewi_part in ewi_parts:
|
|
for iwi_part in iwi_parts:
|
|
for (ewi_depth, ewi_cost_per_unit), (iwi_depth, iwi_cost_per_unit) in itertools.product(
|
|
zip(ewi_part["depths"], ewi_part["cost"]),
|
|
zip(iwi_part["depths"], iwi_part["cost"])
|
|
):
|
|
ewi_part_u_value = r_value_per_mm_to_u_value(ewi_depth, ewi_part["r_value_per_mm"])
|
|
iwi_part_u_value = r_value_per_mm_to_u_value(iwi_depth, iwi_part["r_value_per_mm"])
|
|
|
|
# First calculate the new U-value after applying external wall insulation
|
|
_, ewi_new_u_value = calculate_u_value_uplift(u_value, ewi_part_u_value)
|
|
# Then calculate the new U-value after applying internal wall insulation
|
|
_, combined_new_u_value = calculate_u_value_uplift(ewi_new_u_value, iwi_part_u_value)
|
|
combined_new_u_value = round(combined_new_u_value, 2)
|
|
|
|
if combined_new_u_value < self.DIMINISHING_RETURNS_U_VALUE:
|
|
# We don't recommend an overkill solution
|
|
continue
|
|
|
|
# Check if the combined new U-value meets the requirement
|
|
if combined_new_u_value - self.U_VALUE_ERROR <= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE:
|
|
# Here you might want to define a way to add both recommendations together.
|
|
# For now, I'm adding them as separate items in the list
|
|
ewi_esimtated_cost = ewi_cost_per_unit * self.property.insulation_wall_area
|
|
iwi_esimtated_cost = iwi_cost_per_unit * self.property.insulation_wall_area
|
|
|
|
recommendation = {
|
|
"parts": [
|
|
get_recommended_part(
|
|
part=ewi_part,
|
|
selected_depth=ewi_depth,
|
|
quantity=self.property.insulation_wall_area,
|
|
quantity_unit=QuantityUnits.m2.value,
|
|
selected_total_cost=ewi_esimtated_cost
|
|
),
|
|
get_recommended_part(
|
|
part=iwi_part,
|
|
selected_depth=iwi_depth,
|
|
quantity=self.property.insulation_wall_area,
|
|
quantity_unit=QuantityUnits.m2.value,
|
|
selected_total_cost=iwi_esimtated_cost
|
|
)
|
|
],
|
|
"type": "wall_insulation",
|
|
"description": (
|
|
"Install " + self._make_description(ewi_part, ewi_depth) + " and " +
|
|
self._make_description(iwi_part, iwi_depth)
|
|
),
|
|
"starting_u_value": u_value,
|
|
"new_u_value": combined_new_u_value,
|
|
"sap_points": None,
|
|
"cost": ewi_esimtated_cost + iwi_esimtated_cost,
|
|
}
|
|
self.recommendations.append(recommendation)
|
|
|
|
self.prune_diminishing_recommendations()
|
|
|
|
@staticmethod
|
|
def _make_description(part, depth):
|
|
return f"{depth}{part['depth_unit']} {part['description']}"
|
|
|
|
def prune_diminishing_recommendations(self):
|
|
# For any recommendations, if we have at least 1 reommendation that does not exhibit diminishing returns
|
|
# we trim all others that are beyond the diminishing returns threshold
|
|
|
|
# We first check if we have any recommendations that are not diminishing returns
|
|
not_diminishing_return = [
|
|
rec for rec in self.recommendations if rec["new_u_value"] >= self.DIMINISHING_RETURNS_U_VALUE
|
|
]
|
|
if not_diminishing_return:
|
|
self.recommendations = [
|
|
rec for rec in self.recommendations if rec["new_u_value"] >= self.DIMINISHING_RETURNS_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 thermal_conductivity_to_r_value_per_mm(thermal_conductivity: float) -> float:
|
|
"""Convert thermal conductivity to R-value per mm.
|
|
|
|
Parameters
|
|
----------
|
|
thermal_conductivity : float
|
|
Thermal conductivity (in W/mK).
|
|
|
|
Returns
|
|
-------
|
|
float
|
|
R-value per mm.
|
|
"""
|
|
# Calculate R-value in m²K/W for 1 meter of the material
|
|
r_value_per_meter = 1 / thermal_conductivity
|
|
|
|
# Convert R-value to R-value per mm
|
|
r_value_per_mm = r_value_per_meter / 1000
|
|
|
|
return r_value_per_mm
|