mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Adding u-value calculations
This commit is contained in:
parent
03451d0bc7
commit
202365ebf2
6 changed files with 541 additions and 190 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue