Model/recommendations/recommendation_utils.py
2023-09-20 15:16:20 +01:00

232 lines
8.4 KiB
Python

from copy import deepcopy
from backend.Property import Property
from statistics import mean
from recommendations.rdsap_tables import epc_wall_description_map, wall_uvalues_df, default_wall_thickness
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
def update_lowest_selected_u_value(lowest_selected_u_value, new_u_value):
"""
Utility funciton which holds the logic for how we update the lowest selected u value
:param lowest_selected_u_value: current lowest selected u value, initialised as None
:param new_u_value: new u value to compare against
:return:
"""
if lowest_selected_u_value is None:
lowest_selected_u_value = new_u_value
if new_u_value <= lowest_selected_u_value:
lowest_selected_u_value = new_u_value
return lowest_selected_u_value
def get_recommended_part(part, selected_depth, selected_total_cost, quantity, quantity_unit):
"""
Utility function to return a recommended part with the selected depth.
:param part: part to be recommended
:param selected_depth: depth of the selected part
:param selected_total_cost: Total cost of the selected part
:param quantity: Quantity of the selected part
:param quantity_unit: Unit of the quantity
:return:
"""
recommended_part = deepcopy(part)
recommended_part["depths"] = [selected_depth]
recommended_part["estimated_cost"] = selected_total_cost
recommended_part["quantity"] = quantity
recommended_part["quantity_unit"] = quantity_unit
return recommended_part
def get_uvalue_estimate(uvalue_estimates, property: Property, total_floor_area_group_decile):
"""
Wrapper function which contains the methodology to extract a property's walls u-value estimate
when we don't have a true value and if we can't base our assumption off of the material
:return:
"""
if not uvalue_estimates:
raise ValueError("No U-value estimate found for the given property - investigate")
# We try and filter on total_floor_area_group_decile
floor_area_filter = [
x for x in uvalue_estimates if
x["total-floor-area_group"] == total_floor_area_group_decile
]
if not floor_area_filter:
# Take a mean of all the u-value estimates
return mean(
[x["median_thermal_transmittance"] for x in uvalue_estimates if x["median_thermal_transmittance"]]
)
# Because of how spuriously populated the data is for number-habitable-rooms and number-heated-rooms,
# we will try and filter on these to see if we get a result
habitable_rooms_filer = [
x for x in floor_area_filter if
x["number-habitable-rooms"] == property.data["number-habitable-rooms"]
]
if not habitable_rooms_filer:
# Take a mean of all the u-value estimates
return mean(
[x["median_thermal_transmittance"] for x in floor_area_filter if x["median_thermal_transmittance"]]
)
# Try perform a filter on heated rooms
heated_rooms_filter = [
x for x in habitable_rooms_filer if
x["number-heated-rooms"] == property.data["number-heated-rooms"]
]
if not heated_rooms_filter:
# Take a mean of all the u-value estimates
return mean(
[x["median_thermal_transmittance"] for x in habitable_rooms_filer if x["median_thermal_transmittance"]]
)
return mean(
[x["median_thermal_transmittance"] for x in heated_rooms_filter if x["median_thermal_transmittance"]]
)
def apply_formula_s_5_1_1(is_granite_or_whinstone, is_sandstone_or_limestone, age_band):
"""
As the u-value table in https://bregroup.com/wp-content/uploads/2019/09/RdSAP_2012_9.94-20-09-2019.pdf
on page 19, certain u-values as indicated by an "a", should be populated using a formula as defined in section
S.5.1.1
"""
stone_wall_thickness = [x for x in default_wall_thickness if x["type"] == "stone"][0]
thickness = stone_wall_thickness["J_K_L"] if age_band in ["J", "L", "L"] else stone_wall_thickness[age_band]
if is_granite_or_whinstone:
return 3.3 - 0.002 * thickness
if is_sandstone_or_limestone:
return 3 - 0.002 * thickness
raise ValueError("This should only be called when is_granite_or_whinstone or is_sandstone_or_limestone is True")
def get_wall_u_value(clean_description, age_band, is_granite_or_whinstone, is_sandstone_or_limestone):
"""
Given some features about a wall, this function will query the wall u-value table and return the u-value
:param clean_description: Cleaned up description of the wall from the EPC data
:param age_band: age band of the property from the EPC data
:param is_granite_or_whinstone: Boolean indicating if the wall is made of granite or whinstone
:param is_sandstone_or_limestone: Boolean indicating if the wall is made of sandstone or limestone
:return:
"""
mapped_description = epc_wall_description_map[clean_description]
mapped_value = wall_uvalues_df[wall_uvalues_df["Wall_type"] == mapped_description][age_band].values[0]
if mapped_value == "a":
# The rdSap documentation indicateswe should use a formula to calculate the u-value
return float(
apply_formula_s_5_1_1(
is_granite_or_whinstone=is_granite_or_whinstone,
is_sandstone_or_limestone=is_sandstone_or_limestone,
age_band=age_band
)
)
if "b" in mapped_value:
potential_uvalue = float(mapped_value.replace("b", ""))
formula_uvalue = float(apply_formula_s_5_1_1(
is_granite_or_whinstone=is_granite_or_whinstone,
is_sandstone_or_limestone=is_sandstone_or_limestone,
age_band=age_band
))
return min(potential_uvalue, formula_uvalue)
if mapped_value == "s1.1.2":
return None
return float(mapped_value)