Merge branch 'main' of https://github.com/Hestia-Homes/Model into renewables-recommendations

This commit is contained in:
Khalim Conn-Kowlessar 2024-10-08 15:33:02 +01:00
commit 968be2d3d1
9 changed files with 203 additions and 81 deletions

View file

@ -566,8 +566,6 @@ class Property:
if not self.data: if not self.data:
raise ValueError("Property does not contain data") raise ValueError("Property does not contain data")
self.set_basic_property_dimensions()
for description, attribute in cleaned.items(): for description, attribute in cleaned.items():
if self.data[description] in self.DATA_ANOMALY_MATCHES: if self.data[description] in self.DATA_ANOMALY_MATCHES:
@ -615,6 +613,7 @@ class Property:
setattr(self, self.ATTRIBUTE_MAP[description], attributes[0]) setattr(self, self.ATTRIBUTE_MAP[description], attributes[0])
self.set_basic_property_dimensions()
self.set_wall_type() self.set_wall_type()
self.set_floor_type() self.set_floor_type()
self.set_floor_level() self.set_floor_level()
@ -629,15 +628,6 @@ class Property:
""" """
self.solar_panel_configuration = solar_panel_configuration self.solar_panel_configuration = solar_panel_configuration
if not self.roof["is_flat"]:
default_roof_area = estimate_pitched_roof_area(
floor_area=self.insulation_floor_area,
)
else:
default_roof_area = self.insulation_floor_area
self.roof_area = default_roof_area
def set_current_energy(self, kwh_client, kwh_predictions): def set_current_energy(self, kwh_client, kwh_predictions):
""" """
Given what we know about the property now, estimates the current energy consumption using the UCL paper Given what we know about the property now, estimates the current energy consumption using the UCL paper
@ -972,6 +962,13 @@ class Property:
self.floor_area / self.number_of_floors self.floor_area / self.number_of_floors
) )
if not self.roof["is_flat"]:
self.roof_area = estimate_pitched_roof_area(
floor_area=self.insulation_floor_area,
)
else:
self.roof_area = self.insulation_floor_area
def set_floor_level(self): def set_floor_level(self):
self.floor_level = ( self.floor_level = (
FLOOR_LEVEL_MAP[self.data["floor-level"]] FLOOR_LEVEL_MAP[self.data["floor-level"]]

View file

@ -3,25 +3,24 @@ import itertools
import pandas as pd import pandas as pd
from etl.epc.Record import EPCRecord from etl.epc.Record import EPCRecord
from etl.bill_savings.KwhData import KwhData
from backend.SearchEpc import SearchEpc from backend.SearchEpc import SearchEpc
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from backend.app.config import get_settings from backend.app.config import get_settings, get_prediction_buckets
from backend.app.db.connection import db_engine from backend.app.db.connection import db_engine
from backend.app.db.functions.materials_functions import get_materials from backend.app.db.functions.materials_functions import get_materials
from backend.ml_models.api import ModelApi
from backend.app.plan.utils import get_cleaned from backend.app.plan.utils import get_cleaned
from backend.Property import Property from backend.Property import Property
from etl.solar.SolarPhotoSupply import SolarPhotoSupply
from recommendations.Recommendations import Recommendations from recommendations.Recommendations import Recommendations
from utils.logger import setup_logger from utils.logger import setup_logger
from utils.s3 import read_dataframe_from_s3_parquet, save_dataframe_to_s3_parquet from utils.s3 import read_dataframe_from_s3_parquet, save_dataframe_to_s3_parquet
from datetime import datetime
now = datetime.now().strftime("%d-%m-%Y-%H-%M-%S") now = datetime.now().strftime("%d-%m-%Y-%H-%M-%S")
logger = setup_logger() logger = setup_logger()
@ -41,21 +40,16 @@ cleaning_data = read_dataframe_from_s3_parquet(
materials = get_materials(session) materials = get_materials(session)
cleaned = get_cleaned() cleaned = get_cleaned()
# TODO: THIS IS A TEMPORARY FIX
new_walls_description_mapping = pd.DataFrame(cleaned["walls-description"])
new_walls_description_mapping.loc[
~new_walls_description_mapping["thermal_transmittance_unit"].isnull(),
"thermal_transmittance_unit",
] = "w/m-¦k"
cleaned["walls-description"] = new_walls_description_mapping.to_dict(orient="records")
uprn_filenames = read_dataframe_from_s3_parquet( uprn_filenames = read_dataframe_from_s3_parquet(
bucket_name=get_settings().DATA_BUCKET, file_key="spatial/filename_meta.parquet" bucket_name=get_settings().DATA_BUCKET, file_key="spatial/filename_meta.parquet"
) )
photo_supply_lookup, floor_area_decile_thresholds = SolarPhotoSupply.load(
bucket=get_settings().DATA_BUCKET kwh_client = KwhData(bucket="retrofit-data-dev", read_consumption_data=False)
kwh_client.retail_price_comparison = pd.DataFrame(
[{"Date": datetime.today().strftime("%Y-%m-%d"),
'Average standard variable tariff (Large legacy suppliers)': 1}]
) )
kwh_client.retail_price_comparison["Date"] = pd.to_datetime(kwh_client.retail_price_comparison["Date"])
scenario_properties = [ scenario_properties = [
{ {
@ -66,13 +60,13 @@ scenario_properties = [
[ [
["internal_wall_insulation"], ["internal_wall_insulation"],
"11", "11",
{"walls_insulation_thickness_ending": "average"}, {},
[0], [0],
], ],
[ [
["external_wall_insulation"], ["external_wall_insulation"],
"10", "10",
{"walls_insulation_thickness_ending": "average"}, {},
[0], [0],
], ],
[["solar", "windows"], "15", {"photo_supply_ending": 50}, [0, 1]], [["solar", "windows"], "15", {"photo_supply_ending": 50}, [0, 1]],
@ -85,8 +79,8 @@ scenario_properties = [
"measures": [ "measures": [
[ [
["cavity_wall_insulation", "loft_insulation"], ["cavity_wall_insulation", "loft_insulation"],
"15", "11",
{"walls_insulation_thickness_ending": "average"}, {},
[0, 1], [0, 1],
], ],
], ],
@ -98,8 +92,8 @@ scenario_properties = [
"measures": [ "measures": [
[ [
["cavity_wall_insulation", "loft_insulation"], ["cavity_wall_insulation", "loft_insulation"],
"15", "10",
{"walls_insulation_thickness_ending": "average"}, {},
[0, 1], [0, 1],
], ],
], ],
@ -111,8 +105,8 @@ scenario_properties = [
"measures": [ "measures": [
[ [
["cavity_wall_insulation", "loft_insulation"], ["cavity_wall_insulation", "loft_insulation"],
"15", "11",
{"walls_insulation_thickness_ending": "average"}, {},
[0, 1], [0, 1],
], ],
], ],
@ -124,15 +118,14 @@ scenario_properties = [
"measures": [ "measures": [
[ [
["cavity_wall_insulation", "loft_insulation"], ["cavity_wall_insulation", "loft_insulation"],
"15", "10",
{"walls_insulation_thickness_ending": "average"}, {},
[0, 1], [0, 1],
], ],
], ],
}, },
] ]
recommendations_scoring_data = [] recommendations_scoring_data = []
for scenario_property in scenario_properties: for scenario_property in scenario_properties:
@ -173,9 +166,19 @@ for scenario_property in scenario_properties:
) )
p.get_spatial_data(uprn_filenames) p.get_spatial_data(uprn_filenames)
p.get_components(cleaned, photo_supply_lookup, floor_area_decile_thresholds)
recommender = Recommendations(property_instance=p, materials=materials) kwh_predictions = {
"heating_kwh_predictions": pd.DataFrame([{"id": p.uprn, "predictions": 12000}]),
"hotwater_kwh_predictions": pd.DataFrame([{"id": p.uprn, "predictions": 3000}]),
}
p.set_features(cleaned, kwh_client, kwh_predictions)
p.solar_panel_configuration = {
"panel_performance": pd.DataFrame(
[{"panneled_roof_area": 34, "n_panels": 10, "array_wattage": 4000, "initial_ac_kwh_per_year": 3800}]
)
}
recommender = Recommendations(property_instance=p, materials=materials, default_u_values=True)
property_recommendations = recommender.recommend() property_recommendations = recommender.recommend()
wall_recommendations = recommender.wall_recomender.recommendations wall_recommendations = recommender.wall_recomender.recommendations
@ -277,20 +280,21 @@ recommendations_scoring_data.insert(0, "impact", impact_col)
id_col = recommendations_scoring_data.pop("id") id_col = recommendations_scoring_data.pop("id")
recommendations_scoring_data.insert(0, "id", id_col) recommendations_scoring_data.insert(0, "id", id_col)
from backend.ml_models.api import ModelApi model_api = ModelApi(
portfolio_id="generate-scenarios-data", timestamp=created_at, prediction_buckets=get_prediction_buckets()
model_api = ModelApi(portfolio_id="generate-scenarios-data", timestamp=created_at)
all_predictions = model_api.predict_all(
df=recommendations_scoring_data,
bucket=get_settings().DATA_BUCKET,
prediction_buckets={
"sap_change_predictions": get_settings().SAP_PREDICTIONS_BUCKET,
"heat_demand_predictions": get_settings().HEAT_PREDICTIONS_BUCKET,
"carbon_change_predictions": get_settings().CARBON_PREDICTIONS_BUCKET,
},
) )
all_predictions = model_api.predict_all(df=recommendations_scoring_data, bucket=get_settings().DATA_BUCKET)
sap_impact = pd.concat(
[
all_predictions["sap_change_predictions"],
recommendations_scoring_data[["uprn", "sap_starting", "impact"]],
],
axis=1
)
sap_impact["predicted_impact"] = sap_impact["predictions"] - sap_impact["sap_starting"]
save_dataframe_to_s3_parquet( save_dataframe_to_s3_parquet(
recommendations_scoring_data, recommendations_scoring_data,
"retrofit-data-dev", "retrofit-data-dev",

View file

@ -34,6 +34,7 @@ class Recommendations:
materials: List, materials: List,
exclusions: List[str] = None, exclusions: List[str] = None,
inclusions: List[str] = None, inclusions: List[str] = None,
default_u_values: bool = False,
): ):
""" """
:param property_instance: Instance of the Property class, for the home associated to property_id :param property_instance: Instance of the Property class, for the home associated to property_id
@ -42,12 +43,14 @@ class Recommendations:
None, meaning no exclusions to be applied None, meaning no exclusions to be applied
:param inclusions: List of specific measures of measure types to include. Defaulted to None, meaning all :param inclusions: List of specific measures of measure types to include. Defaulted to None, meaning all
measures are included measures are included
:param default_u_values: Boolean, if True, the recommendations will use the default u-values for the property
""" """
self.property_instance = property_instance self.property_instance = property_instance
self.materials = materials self.materials = materials
self.exclusions = exclusions if exclusions else [] self.exclusions = exclusions if exclusions else []
self.inclusions = inclusions if inclusions else [] self.inclusions = inclusions if inclusions else []
self.default_u_values = default_u_values
self.all_specific_measures = SPECIFIC_MEASURES self.all_specific_measures = SPECIFIC_MEASURES
self.all_non_invase_measures = NON_INVASIVE_SPECIFIC_MEASURES self.all_non_invase_measures = NON_INVASIVE_SPECIFIC_MEASURES
@ -120,12 +123,12 @@ class Recommendations:
non_invasive_recommendation_types = [r["type"] for r in self.property_instance.non_invasive_recommendations] non_invasive_recommendation_types = [r["type"] for r in self.property_instance.non_invasive_recommendations]
# Building Fabric # Building Fabric
self.wall_recomender.recommend(phase=phase, measures=measures) self.wall_recomender.recommend(phase=phase, measures=measures, default_u_values=self.default_u_values)
if self.wall_recomender.recommendations: if self.wall_recomender.recommendations:
property_recommendations.append(self.wall_recomender.recommendations) property_recommendations.append(self.wall_recomender.recommendations)
phase += 1 phase += 1
self.roof_recommender.recommend(phase=phase, measures=measures) self.roof_recommender.recommend(phase=phase, measures=measures, default_u_values=self.default_u_values)
if self.roof_recommender.recommendations: if self.roof_recommender.recommendations:
property_recommendations.append(self.roof_recommender.recommendations) property_recommendations.append(self.roof_recommender.recommendations)
phase += 1 phase += 1

View file

@ -108,7 +108,7 @@ class RoofRecommendations:
return full_insulated_room_roof or room_roof_insulated_at_rafters return full_insulated_room_roof or room_roof_insulated_at_rafters
def recommend(self, phase, measures=None): def recommend(self, phase, measures=None, default_u_values=False):
if self.property.roof["has_dwelling_above"]: if self.property.roof["has_dwelling_above"]:
return return
@ -171,7 +171,8 @@ class RoofRecommendations:
insulation_thickness=self.insulation_thickness, insulation_thickness=self.insulation_thickness,
phase=phase, phase=phase,
is_flat=False, is_flat=False,
is_pitched=True is_pitched=True,
default_u_values=default_u_values
) )
return return
@ -184,7 +185,8 @@ class RoofRecommendations:
insulation_thickness=0, insulation_thickness=0,
phase=phase, phase=phase,
is_flat=True, is_flat=True,
is_pitched=False is_pitched=False,
default_u_values=default_u_values
) )
return return
@ -193,7 +195,7 @@ class RoofRecommendations:
if self.property.roof["is_roof_room"] and ("room_roof_insulation" in measures) or ( if self.property.roof["is_roof_room"] and ("room_roof_insulation" in measures) or (
"room_roof_insulation" in [x["type"] for x in non_invasive_recommendations] "room_roof_insulation" in [x["type"] for x in non_invasive_recommendations]
): ):
self.recommend_room_roof_insulation(u_value, phase) self.recommend_room_roof_insulation(u_value, phase, default_u_values)
return return
raise NotImplementedError("Implement me") raise NotImplementedError("Implement me")
@ -215,7 +217,7 @@ class RoofRecommendations:
raise ValueError("Invalid material type") raise ValueError("Invalid material type")
def recommend_roof_insulation( def recommend_roof_insulation(
self, u_value, insulation_thickness, phase, is_pitched, is_flat self, u_value, insulation_thickness, phase, is_pitched, is_flat, default_u_values
): ):
""" """
@ -241,6 +243,7 @@ class RoofRecommendations:
:param phase: Phase of the recommendation :param phase: Phase of the recommendation
:param is_pitched: Is the roof pitched :param is_pitched: Is the roof pitched
:param is_flat: Is the roof flat :param is_flat: Is the roof flat
:param default_u_values: Use default u-values
:return: :return:
""" """
@ -266,7 +269,6 @@ class RoofRecommendations:
recommendations = [] recommendations = []
for _, insulation_material_group in insulation_materials.groupby("description"): for _, insulation_material_group in insulation_materials.groupby("description"):
for _, material in insulation_material_group.iterrows(): for _, material in insulation_material_group.iterrows():
# We make sure we hit a depth of 270mm. We should factor in any existing insulation if the # We make sure we hit a depth of 270mm. We should factor in any existing insulation if the
# loft is already partially insulated. # loft is already partially insulated.
# Note: This requirement is only for loft insulation # Note: This requirement is only for loft insulation
@ -305,7 +307,9 @@ class RoofRecommendations:
cost_result = override_costs(cost_result) cost_result = override_costs(cost_result)
if material["type"] == "loft_insulation": if material["type"] == "loft_insulation":
new_thickness = insulation_thickness + material["depth"] # We take the new thickness as just the thickness of the insulation, to be conservative
# and assume that any existing insulation will be replaced
new_thickness = material["depth"]
# This is based on the values we have in the training data # This is based on the values we have in the training data
valid_numeric_values = [ valid_numeric_values = [
@ -330,7 +334,7 @@ class RoofRecommendations:
valid_numeric_values, key=lambda x: abs(x - proposed_depth) valid_numeric_values, key=lambda x: abs(x - proposed_depth)
) )
if proposed_depth >= 270: if proposed_depth >= 300:
new_efficiency = "Very Good" new_efficiency = "Very Good"
else: else:
if self.property.data["walls-energy-eff"] not in ["Good", "Very Good"]: if self.property.data["walls-energy-eff"] not in ["Good", "Very Good"]:
@ -340,9 +344,35 @@ class RoofRecommendations:
new_description = f"Pitched, {int(proposed_depth)}mm loft insulation" new_description = f"Pitched, {int(proposed_depth)}mm loft insulation"
if default_u_values:
# We update the u-value with the default if we're using default u-values
new_u_value = get_roof_u_value(
insulation_thickness=str(int(new_thickness)),
has_dwelling_above=self.property.roof["has_dwelling_above"],
is_loft=self.property.roof["is_loft"],
is_roof_room=self.property.roof["is_roof_room"],
is_thatched=self.property.roof["is_thatched"],
age_band=self.property.age_band,
is_flat=self.property.roof["is_flat"],
is_pitched=self.property.roof["is_pitched"],
is_at_rafters=self.property.roof["is_at_rafters"],
)
elif material["type"] == "flat_roof_insulation": elif material["type"] == "flat_roof_insulation":
new_description = "Flat, insulated" new_description = "Flat, insulated"
new_efficiency = "Good" new_efficiency = "Good"
if default_u_values:
new_u_value = get_roof_u_value(
insulation_thickness="100",
has_dwelling_above=self.property.roof["has_dwelling_above"],
is_loft=self.property.roof["is_loft"],
is_roof_room=self.property.roof["is_roof_room"],
is_thatched=self.property.roof["is_thatched"],
age_band=self.property.age_band,
is_flat=self.property.roof["is_flat"],
is_pitched=self.property.roof["is_pitched"],
is_at_rafters=self.property.roof["is_at_rafters"],
)
else: else:
raise ValueError("Invalid material type") raise ValueError("Invalid material type")

View file

@ -177,9 +177,7 @@ class SolarPvRecommendations:
if self.property.roof["is_flat"]: if self.property.roof["is_flat"]:
roof_area = self.property.insulation_floor_area roof_area = self.property.insulation_floor_area
else: else:
roof_area = estimate_pitched_roof_area( roof_area = estimate_pitched_roof_area(floor_area=self.property.insulation_floor_area, )
floor_area=self.property.insulation_floor_area, floor_height=self.property.data["floor-height"]
)
solar_configurations = pd.DataFrame( solar_configurations = pd.DataFrame(
[ [
{ {

View file

@ -179,7 +179,7 @@ class WallRecommendations(Definitions):
return ewi_recommendations return ewi_recommendations
def recommend(self, phase=0, measures=None): def recommend(self, phase=0, measures=None, default_u_values=False):
# if building built after 1990 + we're able to identify U-value + # if building built after 1990 + we're able to identify U-value +
# U-value less than 0.18 and if in or close to a conversation area, # U-value less than 0.18 and if in or close to a conversation area,
# recommend internal wall insulation as a possible measure # recommend internal wall insulation as a possible measure
@ -255,19 +255,19 @@ class WallRecommendations(Definitions):
if (is_cavity_wall and "cavity_wall_insulation" in measures) or "cavity_extract_and_refill" in measures: if (is_cavity_wall and "cavity_wall_insulation" in measures) or "cavity_extract_and_refill" in measures:
if u_value >= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE: if u_value >= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE:
# Test filling cavity # Test filling cavity
self.find_cavity_insulation(u_value, insulation_thickness, phase, measures) self.find_cavity_insulation(u_value, insulation_thickness, phase, measures, default_u_values)
return return
# Remaining wall types are treated with IWI or EWI # Remaining wall types are treated with IWI or EWI
if (u_value >= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE) and self.is_suitable_for_solid_insulation(): if (u_value >= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE) and self.is_suitable_for_solid_insulation():
self.find_insulation(u_value, phase, measures=measures) self.find_insulation(u_value, phase, measures=measures, default_u_values=default_u_values)
return return
# If the u-value is within regulations, we don't do anything # If the u-value is within regulations, we don't do anything
return return
def find_cavity_insulation(self, u_value, insulation_thickness, phase, measures): def find_cavity_insulation(self, u_value, insulation_thickness, phase, measures, default_u_values):
""" """
This method tests different materials to fill the cavity wall, determining which This method tests different materials to fill the cavity wall, determining which
material will give us the best U-value. material will give us the best U-value.
@ -289,6 +289,7 @@ class WallRecommendations(Definitions):
filled cavity wall filled cavity wall
:param phase: The phase of the recommendation :param phase: The phase of the recommendation
:param measures: The measures we're considering :param measures: The measures we're considering
:param default_u_values: If we should use default u values
""" """
insulation_materials = pd.DataFrame(self.cavity_wall_insulation_materials) insulation_materials = pd.DataFrame(self.cavity_wall_insulation_materials)
@ -344,7 +345,15 @@ class WallRecommendations(Definitions):
description = self._make_description(material) description = self._make_description(material)
# updated the new u-value with the best possible our installers have # updated the new u-value with the best possible our installers have
new_u_value = max(0.31, new_u_value) if default_u_values:
new_u_value = get_wall_u_value(
clean_description="Cavity wall, filled cavity",
age_band="G",
is_granite_or_whinstone=self.property.walls["is_granite_or_whinstone"],
is_sandstone_or_limestone=self.property.walls["is_sandstone_or_limestone"],
)
else:
new_u_value = max(0.31, new_u_value)
wall_ending_config = WallAttributes("Cavity wall, filled cavity").process() wall_ending_config = WallAttributes("Cavity wall, filled cavity").process()
@ -359,7 +368,7 @@ class WallRecommendations(Definitions):
simulation_config = { simulation_config = {
**simulation_config, **simulation_config,
**walls_simulation_config, **walls_simulation_config,
"walls_thermal_transmittance_ending": new_u_value, "walls_thermal_transmittance_ending": new_u_value if not default_u_values else 0.7,
} }
recommendations.append( recommendations.append(
@ -439,7 +448,7 @@ class WallRecommendations(Definitions):
return simulation_config return simulation_config
def _find_insulation(self, u_value, insulation_materials, phase): def _find_insulation(self, u_value, insulation_materials, phase, default_u_values):
lowest_selected_u_value = None lowest_selected_u_value = None
recommendations = [] recommendations = []
@ -534,6 +543,15 @@ class WallRecommendations(Definitions):
"walls_thermal_transmittance_ending": new_u_value "walls_thermal_transmittance_ending": new_u_value
} }
if default_u_values:
# If we're using default U-values, we overwrite new_u_value
new_u_value = get_wall_u_value(
clean_description=new_description,
age_band=self.property.age_band,
is_granite_or_whinstone=self.property.walls["is_granite_or_whinstone"],
is_sandstone_or_limestone=self.property.walls["is_sandstone_or_limestone"],
)
recommendations.append( recommendations.append(
{ {
"phase": phase, "phase": phase,
@ -564,7 +582,7 @@ class WallRecommendations(Definitions):
return recommendations return recommendations
def find_insulation(self, u_value, phase, measures): def find_insulation(self, u_value, phase, measures, default_u_values):
""" """
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
on the parts available and whether the property can have external wall insulation installed on the parts available and whether the property can have external wall insulation installed
@ -584,6 +602,7 @@ class WallRecommendations(Definitions):
self.external_wall_insulation_materials self.external_wall_insulation_materials
), ),
phase=phase, phase=phase,
default_u_values=default_u_values
) )
iwi_recommendations = [] iwi_recommendations = []
@ -592,6 +611,7 @@ class WallRecommendations(Definitions):
u_value=u_value, u_value=u_value,
insulation_materials=pd.DataFrame(self.internal_wall_insulation_materials), insulation_materials=pd.DataFrame(self.internal_wall_insulation_materials),
phase=phase, phase=phase,
default_u_values=default_u_values
) )
self.recommendations += ewi_recommendations + iwi_recommendations self.recommendations += ewi_recommendations + iwi_recommendations

View file

@ -340,6 +340,7 @@ s9_list = [
s10_list = [ s10_list = [
{ {
"Age_band": "A, B, C, D", "Age_band": "A, B, C, D",
"Insulation_Thickness": "none",
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 2.3, "Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 2.3,
"Pitched_slates_or_tiles_insulation_at_rafters": 2.3, "Pitched_slates_or_tiles_insulation_at_rafters": 2.3,
"Flat_roof": 2.3, "Flat_roof": 2.3,
@ -350,6 +351,7 @@ s10_list = [
}, },
{ {
"Age_band": "E", "Age_band": "E",
"Insulation_Thickness": 12,
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 1.5, "Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 1.5,
"Pitched_slates_or_tiles_insulation_at_rafters": 1.5, "Pitched_slates_or_tiles_insulation_at_rafters": 1.5,
"Flat_roof": 1.5, "Flat_roof": 1.5,
@ -360,6 +362,7 @@ s10_list = [
}, },
{ {
"Age_band": "F", "Age_band": "F",
"Insulation_Thickness": 50,
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.68, "Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.68,
"Pitched_slates_or_tiles_insulation_at_rafters": 0.68, "Pitched_slates_or_tiles_insulation_at_rafters": 0.68,
"Flat_roof": 0.68, "Flat_roof": 0.68,
@ -370,6 +373,7 @@ s10_list = [
}, },
{ {
"Age_band": "G", "Age_band": "G",
"Insulation_Thickness": 100,
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.40, "Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.40,
"Pitched_slates_or_tiles_insulation_at_rafters": 0.40, "Pitched_slates_or_tiles_insulation_at_rafters": 0.40,
"Flat_roof": 0.40, "Flat_roof": 0.40,
@ -380,6 +384,7 @@ s10_list = [
}, },
{ {
"Age_band": "H", "Age_band": "H",
"Insulation_Thickness": 150,
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.30, "Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.30,
"Pitched_slates_or_tiles_insulation_at_rafters": 0.35, "Pitched_slates_or_tiles_insulation_at_rafters": 0.35,
"Flat_roof": 0.35, "Flat_roof": 0.35,
@ -390,6 +395,7 @@ s10_list = [
}, },
{ {
"Age_band": "I", "Age_band": "I",
"Insulation_Thickness": 150,
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.26, "Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.26,
"Pitched_slates_or_tiles_insulation_at_rafters": 0.35, "Pitched_slates_or_tiles_insulation_at_rafters": 0.35,
"Flat_roof": 0.35, "Flat_roof": 0.35,
@ -400,6 +406,7 @@ s10_list = [
}, },
{ {
"Age_band": "J", "Age_band": "J",
"Insulation_Thickness": 270,
"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, "Pitched_slates_or_tiles_insulation_at_rafters": 0.20,
"Flat_roof": 0.25, "Flat_roof": 0.25,
@ -410,6 +417,7 @@ s10_list = [
}, },
{ {
"Age_band": "K", "Age_band": "K",
"Insulation_Thickness": 270,
"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, "Pitched_slates_or_tiles_insulation_at_rafters": 0.20,
"Flat_roof": 0.25, "Flat_roof": 0.25,
@ -420,6 +428,7 @@ s10_list = [
}, },
{ {
"Age_band": "L", "Age_band": "L",
"Insulation_Thickness": 270,
"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, "Pitched_slates_or_tiles_insulation_at_rafters": 0.18,
"Flat_roof": 0.18, "Flat_roof": 0.18,

View file

@ -239,12 +239,7 @@ def get_wall_u_value(
return float(mapped_value) return float(mapped_value)
def get_u_value_from_s9( def extract_thickness(thickness, is_roof_room, is_at_rafters, is_loft, is_flat):
thickness, s9, is_loft, is_roof_room, is_thatched, is_at_rafters
):
"""Get the U-value from table S9 based on the insulation thickness."""
# If the roof as pitched & insulated at the rafters, it's a room roof
if is_roof_room or is_at_rafters: if is_roof_room or is_at_rafters:
# TODO: We get None instead of a string none, this should be fixed # TODO: We get None instead of a string none, this should be fixed
if thickness is None: if thickness is None:
@ -258,19 +253,40 @@ def get_u_value_from_s9(
} }
thickness = thickness_map[thickness] thickness = thickness_map[thickness]
if is_flat:
try:
thickness = int(thickness)
return thickness
except ValueError:
# If thickness is not a valid number (could be a string or None), return None
return None
if thickness in ["below average", "average", "above average", "none", None] or ( if thickness in ["below average", "average", "above average", "none", None] or (
not is_loft and not is_roof_room and not is_at_rafters not is_loft and not is_roof_room and not is_at_rafters
): ):
return None return None
elif thickness.endswith("+"): elif thickness.endswith("+"):
thickness = int(thickness[:-1]) thickness = int(thickness[:-1])
return thickness
else: else:
try: try:
thickness = int(thickness) thickness = int(thickness)
return thickness
except ValueError: except ValueError:
# If thickness is not a valid number (could be a string or None), return None # If thickness is not a valid number (could be a string or None), return None
return None return None
def get_u_value_from_s9(
thickness, s9, is_loft, is_roof_room, is_thatched, is_at_rafters
):
"""Get the U-value from table S9 based on the insulation thickness."""
if thickness in ["below average", "average", "above average", "none", None, "0", 0] or (
not is_loft and not is_roof_room and not is_at_rafters
):
return None
# Determine the column to refer based on the roof type # Determine the column to refer based on the roof type
column = ( column = (
"Thatched_roof_U_value_W_m2K" "Thatched_roof_U_value_W_m2K"
@ -323,6 +339,14 @@ def get_roof_u_value(
if has_dwelling_above: if has_dwelling_above:
return 0.0 return 0.0
thickness = extract_thickness(
thickness=insulation_thickness,
is_roof_room=is_roof_room,
is_at_rafters=is_at_rafters,
is_loft=is_loft,
is_flat=is_flat
)
# Step 1: Try to get the U-value from table S9 based on the insulation thickness # Step 1: Try to get the U-value from table S9 based on the insulation thickness
# The conditions for using table S9 are: # The conditions for using table S9 are:
# - The insulation thickness is known # - The insulation thickness is known
@ -330,7 +354,7 @@ def get_roof_u_value(
# The criteria for using this table is predominately defined by insulation around joists which is predominately # The criteria for using this table is predominately defined by insulation around joists which is predominately
# a feature of lofts and roof rooms # a feature of lofts and roof rooms
u_value = get_u_value_from_s9( u_value = get_u_value_from_s9(
thickness=insulation_thickness, thickness=thickness,
s9=s9, s9=s9,
is_loft=is_loft, is_loft=is_loft,
is_roof_room=is_roof_room, is_roof_room=is_roof_room,
@ -363,9 +387,34 @@ def get_roof_u_value(
column = "Pitched_slates_or_tiles_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 # 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] if is_flat and thickness is not None:
u_value = s10.loc[
(s10["Insulation_Thickness"] == thickness) |
s10["Age_band"].str.contains(age_band),
column
].values.min()
else:
u_value = s10.loc[s10["Age_band"].str.contains(age_band), column].values[0]
return float(u_value) u_value = float(u_value)
# As per the documentation here: https://bregroup.com/documents/d/bre-group/rdsap_2012_9-94-20-09-2019
# Table s.10
# "The value from the table applies for unknown and as built. If the roof is known to have more insulation than
# would normally be expected for the age band, either observed or on the basis of documentary evidence, use the
# lower of the value in the table and:
# 50 mm insulation 0.68
# 100 mm insulation: 0.40
# 150 mm or more insulation: 0.30"
if thickness is not None:
if thickness == 50:
u_value = min(u_value, 0.68)
if thickness == 100:
u_value = min(u_value, 0.40)
if thickness >= 150:
u_value = min(u_value, 0.30)
return u_value
def estimate_number_of_floors(property_type): def estimate_number_of_floors(property_type):

View file

@ -6,6 +6,7 @@ from recommendations import recommendation_utils
from datatypes.enums import QuantityUnits from datatypes.enums import QuantityUnits
from recommendations.tests.test_data.wall_uvalue_test_cases import wall_uvalue_test_cases from recommendations.tests.test_data.wall_uvalue_test_cases import wall_uvalue_test_cases
from recommendations.tests.test_data.floor_uvalue_test_cases import floor_uvalue_test_cases from recommendations.tests.test_data.floor_uvalue_test_cases import floor_uvalue_test_cases
from recommendations.tests.test_data.roof_uvalue_test_cases import roof_uvalue_test_cases
class TestRecommendationUtils: class TestRecommendationUtils:
@ -222,6 +223,17 @@ class TestRecommendationUtils:
u_value = recommendation_utils.get_roof_u_value(**inputs) u_value = recommendation_utils.get_roof_u_value(**inputs)
assert u_value == 0.0, f"Expected 0.0, but got {u_value}" assert u_value == 0.0, f"Expected 0.0, but got {u_value}"
@pytest.mark.parametrize(
"test_case",
roof_uvalue_test_cases
)
def test_roof_uvalues(self, test_case):
expected_uvalue = test_case["uvalue"]
inputs = test_case.copy()
del inputs["uvalue"]
uvalue = recommendation_utils.get_roof_u_value(**inputs)
assert expected_uvalue == uvalue, f"Expected u value {expected_uvalue}, recieved {uvalue}"
@pytest.mark.parametrize( @pytest.mark.parametrize(
"test_case", "test_case",
wall_uvalue_test_cases wall_uvalue_test_cases