created rcommendation utils, floor recommendations wip

This commit is contained in:
Khalim Conn-Kowlessar 2023-06-26 16:01:49 +01:00
parent a565a35e9a
commit 8976acefdf
3 changed files with 185 additions and 95 deletions

View file

@ -3,6 +3,47 @@ from model_data.BaseUtility import BaseUtility
from model_data.Property import Property
from model_data.analysis.UvalueEstimations import UvalueEstimations
from model_data.rdsap_tables import default_wall_thickness, age_band_data
from model_data.recommendations.recommendation_utils import r_value_per_mm_to_u_value, calculate_u_value_uplift, \
is_diminishing_returns
suspended_floor_insulation_parts = [
{
# Example product
# https://www.insulationsuperstore.co.uk/product/recticel-eurothane-general-purpose-pir-insulation-board-2400
# -x-1200-x-100mm.html
# All product types here:
# https://www.insulationsuperstore.co.uk/browse/insulation/brand/recticel/filterby/application/floors.html
"type": "suspended_floor_insulation",
"description": "Rigid Insulation Foam Boards",
"depths": [25, 30, 40, 50, 60, 70, 75, 80, 90, 100, 110, 120, 130, 140, 150],
"depth_unit": "mm",
"cost": None,
"cost_unit": None,
"r_value_per_mm": 0.04545454545454546,
"r_value_unit": "square_meter_kelvin_per_watt",
"thermal_conductivity": 0.022,
"thermal_conductivity_unit": "watt_per_meter_kelvin"
},
{
# Example product
# https://www.insulationsuperstore.co.uk/product/rockwool-rwa45-acoustic-insulation-slab-100mm-2-88m2-pack.html
# All product types here:
# https://www.insulationsuperstore.co.uk/browse/insulation/brand/rockwool/filterby/application/floors
# /material/mineral-wool.html
"type": "suspended_floor_insulation",
"description": "Mineral Wool Floor Insulation",
"depths": [25, 40, 50, 60, 75, 100],
"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"
},
]
parts = suspended_floor_insulation_parts
class FloorRecommendations(BaseUtility):
@ -103,6 +144,7 @@ class FloorRecommendations(BaseUtility):
def recommend(self):
is_suspended = self.property.floor["is_suspended"]
insulation_thickness = self.property.floor["insulation_thickness"]
# Check which floor the property is on
self.property.year_built
@ -116,6 +158,15 @@ class FloorRecommendations(BaseUtility):
return
if is_suspended:
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)
else:
raise NotImplementedError("Implement me")
if insulation_thickness == "none":
region_str, age_band = self.property.data["construction-age-band"].split(":")
@ -123,16 +174,18 @@ class FloorRecommendations(BaseUtility):
age_band = age_band.strip()
region = self.REGION_LOOKUP[region_str]
uvalue = self._estimate_suspended_floor_u_value(
floor_area=float(self.property.data["total-floor-area"]),
number_of_rooms=float(self.property.data["number-habitable-rooms"]),
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='solid brick',
region=region,
age_band=age_band,
)
else:
uvalue = self._get_floors_uvalue_estimate()
u_value = self._get_floors_uvalue_estimate()
# Given the U-value, we recommend underfloor insulation
def _get_floors_uvalue_estimate(self):
@ -181,3 +234,40 @@ class FloorRecommendations(BaseUtility):
# average
return u_value_estimate["median_thermal_transmittance"].mean()
def recommend_suspended_floor_insulation(self, u_value):
"""
This method is taskes with estimating the impact of performing suspended floor insulation
:return:
"""
lowest_selected_u_value = None
for part in parts:
for depth in part["depths"]:
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
@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

View file

@ -1,13 +1,12 @@
import re
import itertools
import math
from model_data.Property import Property
from model_data.ConservationAreaClient import ConservationAreaClient
from model_data.analysis.UvalueEstimations import UvalueEstimations
from model_data.BaseUtility import BaseUtility
import pandas as pd
from copy import deepcopy
from model_data.recommendations.recommendation_utils import r_value_per_mm_to_u_value, calculate_u_value_uplift, \
is_diminishing_returns
external_wall_insulation_parts = [
{
@ -293,38 +292,6 @@ class WallRecommendations(BaseUtility):
raise NotImplementedError("Not implemented yet")
def _is_diminishing_returns(self, new_u_value, lowest_selected_u_value):
"""
What are defines diminishing returns?
1) The new u value is lower than the lowest selected u value
2) The new u value is below the diminishing returns threshold
3) We already have some recommendations so there is no need to
insert another recommendation in
"""
# if we don't have anything selected, lowest_selected_u_value will be missing
if lowest_selected_u_value is None:
if self.recommendations:
raise ValueError("Recommendations should be empty - investigate")
# This means that nothing has been selected yet
# the new u value is less than the threshold, however this MIGHT be the only
# solution and so we consider it
return False
# We should already have recommendations
if not self.recommendations:
raise ValueError("Recommendations should not be empty - investigate")
# We already have a solution that is suitable so we want to make sure that
# any new solutin actually has a higher u-value as it will either be
# 1) cheaper
# 2) thinner with a more efficient material
is_diminishing = (new_u_value < self.DIMINISHING_RETURNS_U_VALUE) and (
new_u_value < lowest_selected_u_value
)
return is_diminishing
def find_insulation(self, u_value):
"""
This function contains the logic for finding potential insulation measures for a property, depending
@ -344,9 +311,9 @@ class WallRecommendations(BaseUtility):
for part in ewi_parts + iwi_parts:
for depth in part["depths"]:
part_u_value = self.r_value_per_mm_to_u_value(depth, part["r_value_per_mm"])
part_u_value = r_value_per_mm_to_u_value(depth, part["r_value_per_mm"])
_, new_u_value = self.calculate_u_value_uplift(u_value, part_u_value)
_, 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
@ -355,7 +322,9 @@ class WallRecommendations(BaseUtility):
# 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 self._is_diminishing_returns(new_u_value, lowest_selected_u_value):
if is_diminishing_returns(
self.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
@ -376,13 +345,13 @@ class WallRecommendations(BaseUtility):
for ewi_part in ewi_parts:
for iwi_part in iwi_parts:
for ewi_depth, iwi_depth in itertools.product(ewi_part["depths"], iwi_part["depths"]):
ewi_part_u_value = self.r_value_per_mm_to_u_value(ewi_depth, ewi_part["r_value_per_mm"])
iwi_part_u_value = self.r_value_per_mm_to_u_value(iwi_depth, iwi_part["r_value_per_mm"])
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 = self.calculate_u_value_uplift(u_value, ewi_part_u_value)
_, 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 = self.calculate_u_value_uplift(ewi_new_u_value, iwi_part_u_value)
_, 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:
@ -479,36 +448,6 @@ class WallRecommendations(BaseUtility):
**recommended_part, "new_u_value": new_u_value,
}
@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.
@ -527,25 +466,6 @@ class WallRecommendations(BaseUtility):
"""
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)
@staticmethod
def thermal_conductivity_to_r_value_per_mm(thermal_conductivity: float) -> float:
"""Convert thermal conductivity to R-value per mm.

View file

@ -0,0 +1,80 @@
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)
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
def is_diminishing_returns(recommendations, new_u_value, lowest_selected_u_value, diminishing_returns_u_value):
"""
What are defines diminishing returns?
1) The new u value is lower than the lowest selected u value
2) The new u value is below the diminishing returns threshold
3) We already have some recommendations so there is no need to
insert another recommendation in
"""
# if we don't have anything selected, lowest_selected_u_value will be missing
if lowest_selected_u_value is None:
if recommendations:
raise ValueError("Recommendations should be empty - investigate")
# This means that nothing has been selected yet
# the new u value is less than the threshold, however this MIGHT be the only
# solution and so we consider it
return False
# We should already have recommendations
if not recommendations:
raise ValueError("Recommendations should not be empty - investigate")
# We already have a solution that is suitable so we want to make sure that
# any new solutin actually has a higher u-value as it will either be
# 1) cheaper
# 2) thinner with a more efficient material
is_diminishing = (new_u_value < diminishing_returns_u_value) and (
new_u_value < lowest_selected_u_value
)
return is_diminishing