mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +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.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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
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