mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
created rcommendation utils, floor recommendations wip
This commit is contained in:
parent
a565a35e9a
commit
8976acefdf
3 changed files with 185 additions and 95 deletions
|
|
@ -3,6 +3,47 @@ from model_data.BaseUtility import BaseUtility
|
||||||
from model_data.Property import Property
|
from model_data.Property import Property
|
||||||
from model_data.analysis.UvalueEstimations import UvalueEstimations
|
from model_data.analysis.UvalueEstimations import UvalueEstimations
|
||||||
from model_data.rdsap_tables import default_wall_thickness, age_band_data
|
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):
|
class FloorRecommendations(BaseUtility):
|
||||||
|
|
@ -103,6 +144,7 @@ class FloorRecommendations(BaseUtility):
|
||||||
def recommend(self):
|
def recommend(self):
|
||||||
is_suspended = self.property.floor["is_suspended"]
|
is_suspended = self.property.floor["is_suspended"]
|
||||||
insulation_thickness = self.property.floor["insulation_thickness"]
|
insulation_thickness = self.property.floor["insulation_thickness"]
|
||||||
|
|
||||||
# Check which floor the property is on
|
# Check which floor the property is on
|
||||||
|
|
||||||
self.property.year_built
|
self.property.year_built
|
||||||
|
|
@ -116,6 +158,15 @@ class FloorRecommendations(BaseUtility):
|
||||||
return
|
return
|
||||||
|
|
||||||
if is_suspended:
|
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":
|
if insulation_thickness == "none":
|
||||||
|
|
||||||
region_str, age_band = self.property.data["construction-age-band"].split(":")
|
region_str, age_band = self.property.data["construction-age-band"].split(":")
|
||||||
|
|
@ -123,16 +174,18 @@ class FloorRecommendations(BaseUtility):
|
||||||
age_band = age_band.strip()
|
age_band = age_band.strip()
|
||||||
region = self.REGION_LOOKUP[region_str]
|
region = self.REGION_LOOKUP[region_str]
|
||||||
|
|
||||||
uvalue = self._estimate_suspended_floor_u_value(
|
u_value = self._estimate_suspended_floor_u_value(
|
||||||
floor_area=float(self.property.data["total-floor-area"]),
|
floor_area=total_floor_area / num_floors,
|
||||||
number_of_rooms=float(self.property.data["number-habitable-rooms"]),
|
number_of_rooms=number_of_rooms / num_floors,
|
||||||
insulation_thickness=0,
|
insulation_thickness=0,
|
||||||
wall_type='solid brick',
|
wall_type='solid brick',
|
||||||
region=region,
|
region=region,
|
||||||
age_band=age_band,
|
age_band=age_band,
|
||||||
)
|
)
|
||||||
else:
|
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):
|
def _get_floors_uvalue_estimate(self):
|
||||||
|
|
||||||
|
|
@ -181,3 +234,40 @@ class FloorRecommendations(BaseUtility):
|
||||||
# average
|
# average
|
||||||
|
|
||||||
return u_value_estimate["median_thermal_transmittance"].mean()
|
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
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
import re
|
|
||||||
import itertools
|
import itertools
|
||||||
import math
|
import math
|
||||||
|
|
||||||
from model_data.Property import Property
|
from model_data.Property import Property
|
||||||
from model_data.ConservationAreaClient import ConservationAreaClient
|
|
||||||
from model_data.analysis.UvalueEstimations import UvalueEstimations
|
from model_data.analysis.UvalueEstimations import UvalueEstimations
|
||||||
from model_data.BaseUtility import BaseUtility
|
from model_data.BaseUtility import BaseUtility
|
||||||
import pandas as pd
|
|
||||||
from copy import deepcopy
|
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 = [
|
external_wall_insulation_parts = [
|
||||||
{
|
{
|
||||||
|
|
@ -293,38 +292,6 @@ class WallRecommendations(BaseUtility):
|
||||||
|
|
||||||
raise NotImplementedError("Not implemented yet")
|
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):
|
def find_insulation(self, u_value):
|
||||||
"""
|
"""
|
||||||
This function contains the logic for finding potential insulation measures for a property, depending
|
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 part in ewi_parts + iwi_parts:
|
||||||
|
|
||||||
for depth in part["depths"]:
|
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
|
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
|
# 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
|
# 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
|
# 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
|
continue
|
||||||
|
|
||||||
# We allow a small tolerance for error so we don't discount the recommendation entirely
|
# 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 ewi_part in ewi_parts:
|
||||||
for iwi_part in iwi_parts:
|
for iwi_part in iwi_parts:
|
||||||
for ewi_depth, iwi_depth in itertools.product(ewi_part["depths"], iwi_part["depths"]):
|
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"])
|
ewi_part_u_value = 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"])
|
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
|
# 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
|
# 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)
|
combined_new_u_value = round(combined_new_u_value, 2)
|
||||||
|
|
||||||
if combined_new_u_value < self.DIMINISHING_RETURNS_U_VALUE:
|
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,
|
**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
|
@staticmethod
|
||||||
def rvalue_per_mm(total_r_value: float, thickness_mm: float) -> float:
|
def rvalue_per_mm(total_r_value: float, thickness_mm: float) -> float:
|
||||||
"""Return R-value per mm.
|
"""Return R-value per mm.
|
||||||
|
|
@ -527,25 +466,6 @@ class WallRecommendations(BaseUtility):
|
||||||
"""
|
"""
|
||||||
return total_r_value / thickness_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)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def thermal_conductivity_to_r_value_per_mm(thermal_conductivity: float) -> float:
|
def thermal_conductivity_to_r_value_per_mm(thermal_conductivity: float) -> float:
|
||||||
"""Convert thermal conductivity to R-value per mm.
|
"""Convert thermal conductivity to R-value per mm.
|
||||||
|
|
|
||||||
80
model_data/recommendations/recommendation_utils.py
Normal file
80
model_data/recommendations/recommendation_utils.py
Normal 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
|
||||||
Loading…
Add table
Reference in a new issue