Adding u-value calculations

This commit is contained in:
Khalim Conn-Kowlessar 2023-09-21 15:14:10 +01:00
parent 03451d0bc7
commit 202365ebf2
6 changed files with 541 additions and 190 deletions

View file

@ -77,113 +77,6 @@ def app():
if len(descriptions) != len(set(descriptions)):
raise ValueError("Duplicated descriptions found, check me")
# Finally, we attach u-values to the descriptions for walls, roofs and floors
df = pd.DataFrame(cleaned_data["roof-description"])
df = df[pd.isnull(df["thermal_transmittance"])]
def get_u_value_from_s9(thickness, s9, is_loft, is_roof_room, is_thatched):
"""Get the U-value from table S9 based on the insulation thickness."""
if thickness in ["below average", "average", "above average", "none", None] or (
not is_loft and not is_roof_room
):
return None
elif thickness.endswith("+"):
thickness = int(thickness[:-1])
else:
try:
thickness = int(thickness)
except ValueError:
# If thickness is not a valid number (could be a string or None), return None
return None
# Determine the column to refer based on the roof type
column = 'Thatched_roof_U_value_W_m2K' if is_thatched else 'Slates_or_tiles_U_value_W_m2K'
# Get the correct U-value based on the insulation thickness
return s9[s9['Insulation_thickness_mm'] >= thickness][column].iloc[0]
def get_roof_u_value(description_dict, age_band, s9, s10):
"""
Determine the U-value for a roof based on the description dictionary and age band.
We use table s9 is the insulation thickness was measured, otherwise we use table s10.
Parameters:
description_dict (dict): Dictionary containing the details of the roof description.
age_band (str): The age band of the property.
s9 (pd.DataFrame): The DataFrame representing table S9.
s10 (pd.DataFrame): The DataFrame representing table S10.
Returns:
float: The determined U-value.
"""
# If there is a dwelling above, the U-value is 0
if description_dict['has_dwelling_above']:
return 0.0
# Step 1: Try to get the U-value from table S9 based on the insulation thickness
u_value = get_u_value_from_s9(
thickness=description_dict['insulation_thickness'],
s9=s9,
is_loft=description_dict['is_loft'],
is_roof_room=description_dict['is_roof_room'],
is_thatched=description_dict['is_thatched']
)
if u_value is not None:
return u_value
# Step 2: If the U-value could not be determined from table S9, use table S10
# Define the columns to be used based on the description details
if description_dict['is_flat']:
column = 'Flat_roof'
elif description_dict['is_thatched']:
if description_dict['is_roof_room']:
column = 'Thatched_roof_room_in_roof'
else:
column = 'Thatched_roof'
elif description_dict['is_roof_room']:
column = 'Room_in_roof_slates_or_tiles'
elif description_dict['is_pitched']:
if description_dict['is_at_rafters']:
column = 'Pitched_slates_or_tiles_insulation_at_rafters'
else:
column = 'Pitched_slates_or_tiles_insulation_between_joists_or_unknown'
else:
# Default to pitched roof with insulation between joists or unknown
column = 'Pitched_slates_or_tiles_insulation_between_joists_or_unknown'
# Get the U-value from table S10 based on the age band and the determined column
u_value = s10.loc[s10['Age_band'].str.contains(age_band), column].values[0]
return u_value
from recommendations.rdsap_tables import age_bands
z = pd.DataFrame(cleaned_data["roof-description"])
z = z[pd.isnull(z["thermal_transmittance"])]
z["insulation_thickness"].value_counts()
z[z["insulation_thickness"] == "above average"]
z.head(30).to_dict("records")
for i, roof in enumerate(cleaned_data["roof-description"]):
if roof["thermal_transmittance"] is not None or "Average thermal transmittance" in roof["clean_description"]:
continue
for ab in age_bands:
value = float(
get_roof_u_value(
description_dict=roof,
age_band=ab,
s9=table_s9,
s10=table_s10
)
)
# We store a singular file however we could store the data under the following file path:
# cleaned_epc_data/{component}/{original_description}/cleaned.bson
# where component is one of the keys of cleaned_data. If we store it against the original data, this

View file

@ -15,6 +15,8 @@ from model_data.simulation_system.core.Settings import (
)
from model_data.simulation_system.core.DataProcessor import DataProcessor
from utils.s3 import save_dataframe_to_s3_parquet, read_from_s3, read_dataframe_from_s3_parquet
from recommendations.rdsap_tables import england_wales_age_band_lookup
from recommendations.recommendation_utils import get_wall_u_value, get_roof_u_value
DATA_DIRECTORY = Path(__file__).parent / "model_data" / "simulation_system" / "data" / "all-domestic-certificates"
@ -48,17 +50,11 @@ def process_and_prune_desriptions(df, cleaned_lookup):
:return:
"""
# TODO: In a future iteration, we can test using the binary features and the insulation thickness
# estimates, we well as estimated U-values
# TODO: If we integrate u values, we can probably remove insulation thickness
# TODO: Add in main fuel
cols_to_drop = {
"walls": [
'original_description', 'clean_description', 'thermal_transmittance_unit',
'original_description_ENDING', 'clean_description_ENDING',
# We need to cleaned descriptions for pulling out u-values
'original_description', 'thermal_transmittance_unit',
'original_description_ENDING',
'thermal_transmittance_unit_ENDING',
'is_cavity_wall_ENDING', 'is_filled_cavity_ENDING',
'is_solid_brick_ENDING', 'is_system_built_ENDING',
@ -112,19 +108,23 @@ def process_and_prune_desriptions(df, cleaned_lookup):
component_upper = component_upper.replace("-", "_")
cleaned_key = "main-fuel" if component == "main-fuel" else f"{component}-description"
left_on = (
left_on_starting = (
f"{component_upper}_STARTING" if component == "main-fuel" else f"{component_upper}_DESCRIPTION_STARTING"
)
left_on_ending = (
f"{component_upper}_ENDING" if component == "main-fuel" else f"{component_upper}_DESCRIPTION_ENDING"
)
df = df.merge(
pd.DataFrame(cleaned_lookup[cleaned_key]),
how="left",
left_on=left_on,
left_on=left_on_starting,
right_on="original_description",
).merge(
pd.DataFrame(cleaned_lookup[cleaned_key]),
how="left",
left_on=left_on,
left_on=left_on_ending,
right_on="original_description",
suffixes=("", "_ENDING")
)
@ -163,14 +163,14 @@ def process_and_prune_desriptions(df, cleaned_lookup):
# Drop original cols
original_cols = [
f"{component_upper}_DESCRIPTION_STARTING", f"{component_upper}_DESCRIPTION_ENDING"
] if component != "main-fuel" else [
f"{component_upper}_STARTING", f"{component_upper}_ENDING"
]
df = df.drop(
columns=cols_to_drop[component] + original_cols
)
df = df.drop(columns=cols_to_drop[component] + original_cols)
# If we have an insulation_thickness column, rename it
if "insulation_thickness" in cleaned_lookup[f"{component}-description"][0]:
if "insulation_thickness" in cleaned_lookup[cleaned_key][0]:
df = df.rename(
columns={
"insulation_thickness": f"{component}_insulation_thickness",
@ -178,7 +178,7 @@ def process_and_prune_desriptions(df, cleaned_lookup):
}
)
# If we have thermal transmittance, rename it
if "thermal_transmittance" in cleaned_lookup[f"{component}-description"][0]:
if "thermal_transmittance" in cleaned_lookup[cleaned_key][0]:
df = df.rename(
columns={
"thermal_transmittance": f"{component}_thermal_transmittance",
@ -187,7 +187,7 @@ def process_and_prune_desriptions(df, cleaned_lookup):
)
# If we have tarrif, rename it
if "tariff_type" in cleaned_lookup[f"{component}-description"][0]:
if "tariff_type" in cleaned_lookup[cleaned_key][0]:
df = df.rename(
columns={
"tariff_type": f"{component}_tariff_type",
@ -195,10 +195,109 @@ def process_and_prune_desriptions(df, cleaned_lookup):
}
)
# We need the walls descriptions so we rename them to distinguish them
if component == "walls":
df = df.rename(
columns={
"clean_description": f"{component}_clean_description",
"clean_description_ENDING": f"{component}_clean_description_ENDING",
}
)
# We don't need any lighting specific cleaning, we just drop the original description as we use
# LOW_ENERGY_LIGHTING_STARTING, LOW_ENERGY_LIGHTING_ENDING
df = df.drop(columns=["LOW_ENERGY_LIGHTING_STARTING", "LOW_ENERGY_LIGHTING_ENDING"])
df = df.drop(columns=["LIGHTING_DESCRIPTION_STARTING", "LIGHTING_DESCRIPTION_ENDING"])
return df
def make_uvalues(df):
df["row_index"] = df.index
uvalues = []
for _, x in df.iterrows():
uprn = x["UPRN"]
row_index = x["row_index"]
# ~~~~~~~~~~~~~~~~~~
# Walls
# ~~~~~~~~~~~~~~~~~~
starting_wall_uvalue = x["walls_thermal_transmittance"]
if pd.isnull(starting_wall_uvalue):
starting_wall_uvalue = get_wall_u_value(
clean_description=x["walls_clean_description"],
age_band=england_wales_age_band_lookup[x["CONSTRUCTION_AGE_BAND"]],
is_granite_or_whinstone=x["is_granite_or_whinstone"],
is_sandstone_or_limestone=x["is_sandstone_or_limestone"],
)
ending_wall_uvalue = x["walls_thermal_transmittance_ENDING"]
if pd.isnull(ending_wall_uvalue):
if x["walls_clean_description"] != x["walls_clean_description_ENDING"]:
ending_wall_uvalue = get_wall_u_value(
clean_description=x["walls_clean_description_ENDING"],
age_band=england_wales_age_band_lookup[x["CONSTRUCTION_AGE_BAND"]],
is_granite_or_whinstone=x["is_granite_or_whinstone"],
is_sandstone_or_limestone=x["is_sandstone_or_limestone"],
)
else:
ending_wall_uvalue = starting_wall_uvalue
# ~~~~~~~~~~~~~~~~~~
# Roof
# ~~~~~~~~~~~~~~~~~~
starting_roof_uvalue = x["roof_thermal_transmittance"]
if pd.isnull(starting_roof_uvalue):
starting_roof_uvalue = get_roof_u_value(
insulation_thickness=x["roof_insulation_thickness"],
has_dwelling_above=x["has_dwelling_above"],
is_loft=x["is_loft"],
is_roof_room=x["is_roof_room"],
is_thatched=x["is_thatched"],
is_flat=x["is_flat"],
is_pitched=x["is_pitched"],
is_at_rafters=x["is_at_rafters"],
age_band=england_wales_age_band_lookup[x["CONSTRUCTION_AGE_BAND"]]
)
ending_roof_uvalue = x["roof_thermal_transmittance_ENDING"]
if pd.isnull(ending_roof_uvalue):
ending_roof_uvalue = get_roof_u_value(
insulation_thickness=x["roof_insulation_thickness_ENDING"],
has_dwelling_above=x["has_dwelling_above"],
is_loft=x["is_loft"],
is_roof_room=x["is_roof_room"],
is_thatched=x["is_thatched"],
is_flat=x["is_flat"],
is_pitched=x["is_pitched"],
is_at_rafters=x["is_at_rafters"],
age_band=england_wales_age_band_lookup[x["CONSTRUCTION_AGE_BAND"]]
)
# ~~~~~~~~~~~~~~~~~~
# Floor
# ~~~~~~~~~~~~~~~~~~
uvalues.append(
{
"UPRN": uprn,
"row_index": row_index,
"starting_wall_uvalue": starting_wall_uvalue,
"ending_wall_uvalue": ending_wall_uvalue,
"starting_roof_uvalue": starting_roof_uvalue,
"ending_roof_uvalue": ending_roof_uvalue,
}
)
uvalues = pd.DataFrame(uvalues)
df = df.merge(
uvalues, how="left", on=["UPRN", "row_index"]
).drop(columns="row_index")
return df
@ -332,10 +431,6 @@ def app():
pd.to_datetime(data_by_urpn_df["LODGEMENT_DATE_ENDING"]) - pd.to_datetime(EARLIEST_EPC_DATE)
).dt.days
# TODO: We need to pre-process the data. For instance, rather than using static for roofs, walls and
# floors, we may want to use the U-value. We may also want to handle the (assumed) tags
# within descriptions
# We look for key building fabric features that have changed from one EPC to the next.
# if, for example, we see that a home has gone from being a cavity wall to a solid wall, we
# remove this record, as it indicates that the quality of the EPC conducted in the first instance
@ -347,6 +442,25 @@ def app():
data_by_urpn_df = process_and_prune_desriptions(data_by_urpn_df, cleaned_lookup)
# Apply u-values
########################
# Walls
########################
for col in ["walls_clean_description", "walls_clean_description_ENDING"]:
data_by_urpn_df[col] = data_by_urpn_df[col].str.replace("(assumed)", "").str.rstrip()
data_by_urpn_df = make_uvalues(data_by_urpn_df).drop(
columns=["walls_clean_description", "walls_clean_description_ENDING"]
)
get_wall_u_value(clean_description=)
if pd.isnull(data_by_urpn_df).sum().sum():
raise ValueError("Null values found in dataset after process_and_prune_desriptions")

View file

@ -6,7 +6,7 @@ from backend.Property import Property
from recommendations.rdsap_tables import default_wall_thickness, age_band_data
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
get_recommended_part, get_uvalue_estimate, estimate_perimeter, estimate_perimeter_2_rooms
)
@ -61,32 +61,6 @@ class FloorRecommendations(Definitions):
part for part in self.materials if part["type"] == "solid_floor_insulation"
]
@staticmethod
def _estimate_perimeter(floor_area, num_rooms):
# Compute average room size based on total floor area and number of rooms
avg_room_size = floor_area / num_rooms
# Estimate the side length of a square room with the average room size
avg_room_side_length = math.sqrt(avg_room_size)
# Estimate total side length assuming rooms are lined up in a row
total_side_length = avg_room_side_length * num_rooms
# Assuming the house is a square (which is a very rough approximation), compute the perimeter
perimeter = math.sqrt(total_side_length) * 4
return perimeter
@staticmethod
def _estimate_perimeter_2_rooms(floor_area):
# Assuming a square layout for the entire floor area to get a first-order approximation of the perimeter
side_length = math.sqrt(floor_area)
# Calculating the perimeter of the square layout
perimeter = 4 * side_length
return perimeter
def _estimate_suspended_floor_u_value(
self, floor_area, number_of_rooms, insulation_thickness, wall_type, region, age_band
):
@ -140,9 +114,9 @@ class FloorRecommendations(Definitions):
# P is the exposed perimeter, which we estimate as we not have this data
if number_of_rooms <= 2:
p = self._estimate_perimeter_2_rooms(floor_area=floor_area)
p = estimate_perimeter_2_rooms(floor_area=floor_area)
else:
p = self._estimate_perimeter(floor_area=floor_area, num_rooms=number_of_rooms)
p = estimate_perimeter(floor_area=floor_area, num_rooms=number_of_rooms)
b = 2 * floor_area / p
u_g = 2 * defaults["lambda_g"] * math.log(math.pi * b / dg + 1) / (math.pi * b + dg)
u_x = (2 * defaults["h"] * defaults["Uw"] / b) + (1450 * defaults["E"] * defaults["v"] * defaults["fw"] / b)

View file

@ -92,6 +92,10 @@ age_band_data = [
},
]
england_wales_age_band_lookup = {
f"England and Wales: %s" % x["England_Wales"]: x["age_band"] for x in age_band_data
}
########################################################################################################################
# As defined in the rdsap documentation on page 9
# https://bregroup.com/wp-content/uploads/2019/09/RdSAP_2012_9.94-20-09-2019.pdf
@ -336,39 +340,39 @@ s9_list = [
s10_list = [
{
"Age_band": "A, B, C, D",
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": "2.3",
"Pitched_slates_or_tiles_insulation_at_rafters": "2.3",
"Flat_roof": "2.3",
"Room_in_roof_slates_or_tiles": "2.3",
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 2.3,
"Pitched_slates_or_tiles_insulation_at_rafters": 2.3,
"Flat_roof": 2.3,
"Room_in_roof_slates_or_tiles": 2.3,
"Thatched_roof": 0.35,
"Thatched_roof_room_in_roof": 0.25,
"Park_home": None
},
{
"Age_band": "E",
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": "1.5",
"Pitched_slates_or_tiles_insulation_at_rafters": "1.5",
"Flat_roof": "1.5",
"Room_in_roof_slates_or_tiles": "1.5",
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 1.5,
"Pitched_slates_or_tiles_insulation_at_rafters": 1.5,
"Flat_roof": 1.5,
"Room_in_roof_slates_or_tiles": 1.5,
"Thatched_roof": 0.35,
"Thatched_roof_room_in_roof": 0.25,
"Park_home": None
},
{
"Age_band": "F",
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": "0.68",
"Pitched_slates_or_tiles_insulation_at_rafters": "0.68",
"Flat_roof": "0.68",
"Room_in_roof_slates_or_tiles": "0.80",
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.68,
"Pitched_slates_or_tiles_insulation_at_rafters": 0.68,
"Flat_roof": 0.68,
"Room_in_roof_slates_or_tiles": 0.80,
"Thatched_roof": 0.35,
"Thatched_roof_room_in_roof": 0.25,
"Park_home": 1.7
},
{
"Age_band": "G",
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": "0.40",
"Pitched_slates_or_tiles_insulation_at_rafters": "0.40",
"Flat_roof": "0.40",
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.40,
"Pitched_slates_or_tiles_insulation_at_rafters": 0.40,
"Flat_roof": 0.40,
"Room_in_roof_slates_or_tiles": "0.50",
"Thatched_roof": 0.35,
"Thatched_roof_room_in_roof": 0.25,
@ -376,27 +380,27 @@ s10_list = [
},
{
"Age_band": "H",
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": "0.30",
"Pitched_slates_or_tiles_insulation_at_rafters": "0.35",
"Flat_roof": "0.35",
"Room_in_roof_slates_or_tiles": "0.35",
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.30,
"Pitched_slates_or_tiles_insulation_at_rafters": 0.35,
"Flat_roof": 0.35,
"Room_in_roof_slates_or_tiles": 0.35,
"Thatched_roof": 0.35,
"Thatched_roof_room_in_roof": 0.25,
"Park_home": None
},
{
"Age_band": "I",
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": "0.26",
"Pitched_slates_or_tiles_insulation_at_rafters": "0.35",
"Flat_roof": "0.35",
"Room_in_roof_slates_or_tiles": "0.35",
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.26,
"Pitched_slates_or_tiles_insulation_at_rafters": 0.35,
"Flat_roof": 0.35,
"Room_in_roof_slates_or_tiles": 0.35,
"Thatched_roof": 0.35,
"Thatched_roof_room_in_roof": 0.25,
"Park_home": 0.35
},
{
"Age_band": "J",
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": "0.16",
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.16,
"Pitched_slates_or_tiles_insulation_at_rafters": 0.20,
"Flat_roof": 0.25,
"Room_in_roof_slates_or_tiles": 0.30,
@ -406,17 +410,17 @@ s10_list = [
},
{
"Age_band": "K",
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": "0.16",
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.16,
"Pitched_slates_or_tiles_insulation_at_rafters": 0.20,
"Flat_roof": "0.25",
"Room_in_roof_slates_or_tiles": "0.25",
"Thatched_roof": "0.25",
"Thatched_roof_room_in_roof": "0.25",
"Flat_roof": 0.25,
"Room_in_roof_slates_or_tiles": 0.25,
"Thatched_roof": 0.25,
"Thatched_roof_room_in_roof": 0.25,
"Park_home": 0.30
},
{
"Age_band": "L",
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": "0.16",
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.16,
"Pitched_slates_or_tiles_insulation_at_rafters": 0.18,
"Flat_roof": 0.18,
"Room_in_roof_slates_or_tiles": 0.18,

View file

@ -1,7 +1,10 @@
import math
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
from recommendations.rdsap_tables import (
epc_wall_description_map, wall_uvalues_df, default_wall_thickness, table_s9 as s9, table_s10 as s10
)
def r_value_per_mm_to_u_value(depth_mm: int, r_value_per_mm: float):
@ -230,3 +233,193 @@ def get_wall_u_value(clean_description, age_band, is_granite_or_whinstone, is_sa
return None
return float(mapped_value)
def get_u_value_from_s9(thickness, s9, is_loft, is_roof_room, is_thatched):
"""Get the U-value from table S9 based on the insulation thickness."""
if thickness in ["below average", "average", "above average", "none", None] or (
not is_loft and not is_roof_room
):
return None
elif thickness.endswith("+"):
thickness = int(thickness[:-1])
else:
try:
thickness = int(thickness)
except ValueError:
# If thickness is not a valid number (could be a string or None), return None
return None
# Determine the column to refer based on the roof type
column = 'Thatched_roof_U_value_W_m2K' if is_thatched else 'Slates_or_tiles_U_value_W_m2K'
# Get the correct U-value based on the insulation thickness
return s9[s9['Insulation_thickness_mm'] >= thickness][column].iloc[0]
def get_roof_u_value(
insulation_thickness,
has_dwelling_above,
is_loft,
is_roof_room,
is_thatched,
age_band,
is_flat,
is_pitched,
is_at_rafters,
**kwargs
):
"""
Determine the U-value for a roof based on the description dictionary and age band.
We use table s9 is the insulation thickness was measured, otherwise we use table s10.
The methodology for this process can be found in page 23 of the BRE rdsap 2012 document found here:
https://bregroup.com/wp-content/uploads/2019/09/RdSAP_2012_9.94-20-09-2019.pdf
Parameters:
insulation_thickness (str): contains description of the insulation thickness - may be missing
has_dwelling_above (bool): Indicates if there is a property above
is_loft (bool): Indicates if ther oof has a loft
is_roof_room (bool): Indicates if there is a room in roof
is_thatched (bool): Indicates if the roof is thatched
is_flat (bool): Indicates if the roof is flat
is_pitched (bool): Indicates if the roof is pitched
is_at_rafters (bool): Indicates if there is insulation at the rafters of the roof
age_band (str): The age band of the property.
s9 (pd.DataFrame): The DataFrame representing table S9.
s10 (pd.DataFrame): The DataFrame representing table S10.
Returns:
float: The determined U-value.
"""
# If there is a dwelling above, the U-value is 0
if has_dwelling_above:
return 0.0
# Step 1: Try to get the U-value from table S9 based on the insulation thickness
# The conditions for using table S9 are:
# - The insulation thickness is known
# - The roof is either a loft or a roof room
# The criteria for using this table is predominately defined by insulation around joists which is predominately
# a feature of lofts and roof rooms
u_value = get_u_value_from_s9(
thickness=insulation_thickness,
s9=s9,
is_loft=is_loft,
is_roof_room=is_roof_room,
is_thatched=is_thatched,
)
if u_value is not None:
return u_value
# Step 2: If the U-value could not be determined from table S9, use table S10
# Define the columns to be used based on the description details
if is_flat:
column = 'Flat_roof'
elif is_thatched:
if is_roof_room:
column = 'Thatched_roof_room_in_roof'
else:
column = 'Thatched_roof'
elif is_roof_room:
column = 'Room_in_roof_slates_or_tiles'
elif is_pitched:
if is_at_rafters:
column = 'Pitched_slates_or_tiles_insulation_at_rafters'
else:
column = 'Pitched_slates_or_tiles_insulation_between_joists_or_unknown'
else:
# Default to pitched roof with insulation between joists or unknown
column = 'Pitched_slates_or_tiles_insulation_between_joists_or_unknown'
# Get the U-value from table S10 based on the age band and the determined column
u_value = s10.loc[s10['Age_band'].str.contains(age_band), column].values[0]
return u_value
def estimate_perimeter(floor_area, num_rooms):
# Compute average room size based on total floor area and number of rooms
avg_room_size = floor_area / num_rooms
# Estimate the side length of a square room with the average room size
avg_room_side_length = math.sqrt(avg_room_size)
# Estimate total side length assuming rooms are lined up in a row
total_side_length = avg_room_side_length * num_rooms
# Assuming the house is a square (which is a very rough approximation), compute the perimeter
perimeter = math.sqrt(total_side_length) * 4
return perimeter
def estimate_perimeter_2_rooms(floor_area):
# Assuming a square layout for the entire floor area to get a first-order approximation of the perimeter
side_length = math.sqrt(floor_area)
# Calculating the perimeter of the square layout
perimeter = 4 * side_length
return perimeter
import math
def calculate_floor_u_value(floor_type, area, perimeter, wall_thickness, insulation_thickness=None):
# Define constants
lambda_g = 1.5 # thermal conductivity of soil in W/m·K
Rsi = 0.17 # in m²K/W
Rse = 0.04 # in m²K/W
lambda_ins = 0.035 # thermal conductivity of floor insulation in W/m·K
# Calculate Rf for insulated floors
if insulation_thickness is not None:
Rf = 0.001 * insulation_thickness / lambda_ins
else:
Rf = 0
# Calculate B
B = 2 * area / perimeter
if floor_type == 'solid':
# Calculate dt
dt = wall_thickness + lambda_g * (Rsi + Rf + Rse)
# Calculate U value based on dt and B
if dt < B:
U = 2 * lambda_g * math.log(math.pi * B / dt + 1) / (math.pi * B + dt)
else:
U = lambda_g / (0.457 * B + dt)
elif floor_type == 'suspended':
# Define additional constants for suspended floors
h = 0.3 # height above external ground level in meters
v = 5 # average wind speed at 10 m height in m/s
fw = 0.05 # wind shielding factor
epsilon = 0.003 # ventilation openings per m exposed perimeter in m²/m
Uw = 1.5 # U-value of walls to underfloor space in W/m²K
# Calculate dg
dg = wall_thickness + lambda_g * (Rsi + Rse)
# Calculate Ug and Ux
Ug = 2 * lambda_g * math.log(math.pi * B / dg + 1) / (math.pi * B + dg)
Ux = (2 * h * Uw / B) + (1450 * epsilon * v * fw / B)
# Calculate final U value for suspended floors
if insulation_thickness is not None:
Rf += 0.2 # adding thermal resistance of floor deck
else:
Rf = 0.2 # thermal resistance of uninsulated floor deck
U = 1 / (2 * Rsi + Rf + 1 / (Ug + Ux))
else:
raise ValueError("Invalid floor type. Acceptable values are 'solid' or 'suspended'.")
return round(U, 2) # rounding U value to two decimal places

View file

@ -75,3 +75,176 @@ class TestRecommendationUtils:
with pytest.raises(KeyError):
recommendation_utils.get_uvalue_estimate(uvalue_estimates_missing_key, property_mock, "Decile 1")
def test_get_roof_u_value(self):
# Test case 1: Insulation thickness is known and is_loft is True
description_dict = {
'insulation_thickness': '50',
'is_loft': True,
'is_roof_room': False,
'is_thatched': False,
'has_dwelling_above': False,
'is_flat': False,
'is_pitched': True,
'is_at_rafters': False,
}
for age_band in ["A", "B", "C", "D"]:
assert recommendation_utils.get_roof_u_value(description_dict, age_band) == 0.68
def test_get_roof_u_value_case_2(self):
description_dict = {
'original_description': 'Pitched, 400+ mm insulation at joists',
'clean_description': 'Pitched, 400+ mm insulation at joists',
'thermal_transmittance': None,
'thermal_transmittance_unit': None,
'is_pitched': True,
'is_roof_room': False,
'is_loft': False,
'is_flat': False,
'is_thatched': False,
'is_at_rafters': False,
'is_assumed': False,
'has_dwelling_above': False,
'is_valid': True,
'insulation_thickness': '400+'
}
age_band = "J"
u_value = recommendation_utils.get_roof_u_value(description_dict, age_band)
assert u_value == 0.16, f"Expected 0.16, but got {u_value}"
def test_get_roof_u_value_case_3(self):
description_dict = {
'original_description': 'Room-in-roof, 200 mm insulation at rafters',
'clean_description': 'Room-in-roof, 200 mm insulation at rafters',
'thermal_transmittance': None,
'thermal_transmittance_unit': None,
'is_pitched': False,
'is_roof_room': True,
'is_loft': False,
'is_flat': False,
'is_thatched': False,
'is_at_rafters': True,
'is_assumed': False,
'has_dwelling_above': False,
'is_valid': True,
'insulation_thickness': '200'
}
age_band = "J"
u_value = recommendation_utils.get_roof_u_value(description_dict, age_band)
assert u_value == 0.21, f"Expected 0.21, but got {u_value}"
def test_get_roof_u_value_case_4(self):
description_dict = {
'original_description': 'Pitched, below average insulation',
'clean_description': 'Pitched, below average insulation',
'thermal_transmittance': None,
'thermal_transmittance_unit': None,
'is_pitched': True,
'is_roof_room': False,
'is_loft': False,
'is_flat': False,
'is_thatched': False,
'is_at_rafters': False,
'is_assumed': False,
'has_dwelling_above': False,
'is_valid': True,
'insulation_thickness': 'below average'
}
age_band = "E"
u_value = recommendation_utils.get_roof_u_value(description_dict, age_band)
assert u_value == 1.5, f"Expected 1.5, but got {u_value}"
def test_get_roof_u_value_case_5():
# Test case where insulation thickness is exactly specified
description_dict = {
'original_description': 'Pitched, 100mm insulation',
'clean_description': 'Pitched, 100mm insulation',
'thermal_transmittance': None,
'thermal_transmittance_unit': None,
'is_pitched': True,
'is_roof_room': False,
'is_loft': False,
'is_flat': False,
'is_thatched': False,
'is_at_rafters': False,
'is_assumed': False,
'has_dwelling_above': False,
'is_valid': True,
'insulation_thickness': '100'
}
age_band = "G"
u_value = get_roof_u_value(description_dict, age_band, table_s9, table_s10)
assert u_value == 0.40, f"Expected 0.40, but got {u_value}"
def test_get_roof_u_value_case_6():
# Test case for a thatched roof
description_dict = {
'original_description': 'Thatched, 75mm insulation',
'clean_description': 'Thatched, 75mm insulation',
'thermal_transmittance': None,
'thermal_transmittance_unit': None,
'is_pitched': False,
'is_roof_room': False,
'is_loft': False,
'is_flat': False,
'is_thatched': True,
'is_at_rafters': False,
'is_assumed': False,
'has_dwelling_above': False,
'is_valid': True,
'insulation_thickness': '75'
}
age_band = "H"
u_value = get_roof_u_value(description_dict, age_band, table_s9, table_s10)
assert u_value == 0.22, f"Expected 0.22, but got {u_value}"
def test_get_roof_u_value_case_7():
# Test case where the roof has a room in it
description_dict = {
'original_description': 'Pitched, room-in-roof, 100mm insulation',
'clean_description': 'Pitched, room-in-roof, 100mm insulation',
'thermal_transmittance': None,
'thermal_transmittance_unit': None,
'is_pitched': True,
'is_roof_room': True,
'is_loft': False,
'is_flat': False,
'is_thatched': False,
'is_at_rafters': False,
'is_assumed': False,
'has_dwelling_above': False,
'is_valid': True,
'insulation_thickness': '100'
}
age_band = "J"
u_value = get_roof_u_value(description_dict, age_band, table_s9, table_s10)
assert u_value == 0.30, f"Expected 0.30, but got {u_value}"
def test_get_roof_u_value_case_8():
# Test case where there is a dwelling above the roof, U-value should be 0
description_dict = {
'original_description': 'Pitched, 100mm insulation',
'clean_description': 'Pitched, 100mm insulation',
'thermal_transmittance': None,
'thermal_transmittance_unit': None,
'is_pitched': True,
'is_roof_room': False,
'is_loft': False,
'is_flat': False,
'is_thatched': False,
'is_at_rafters': False,
'is_assumed': False,
'has_dwelling_above': True,
'is_valid': True,
'insulation_thickness': '100'
}
age_band = "J"
u_value = get_roof_u_value(description_dict, age_band, table_s9, table_s10)
assert u_value == 0.0, f"Expected 0.0, but got {u_value}"