mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Merge branch 'main' of https://github.com/Hestia-Homes/Model into renewables-recommendations
This commit is contained in:
commit
968be2d3d1
9 changed files with 203 additions and 81 deletions
|
|
@ -566,8 +566,6 @@ class Property:
|
|||
if not self.data:
|
||||
raise ValueError("Property does not contain data")
|
||||
|
||||
self.set_basic_property_dimensions()
|
||||
|
||||
for description, attribute in cleaned.items():
|
||||
|
||||
if self.data[description] in self.DATA_ANOMALY_MATCHES:
|
||||
|
|
@ -615,6 +613,7 @@ class Property:
|
|||
|
||||
setattr(self, self.ATTRIBUTE_MAP[description], attributes[0])
|
||||
|
||||
self.set_basic_property_dimensions()
|
||||
self.set_wall_type()
|
||||
self.set_floor_type()
|
||||
self.set_floor_level()
|
||||
|
|
@ -629,15 +628,6 @@ class Property:
|
|||
"""
|
||||
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):
|
||||
"""
|
||||
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
|
||||
)
|
||||
|
||||
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):
|
||||
self.floor_level = (
|
||||
FLOOR_LEVEL_MAP[self.data["floor-level"]]
|
||||
|
|
|
|||
|
|
@ -3,25 +3,24 @@ import itertools
|
|||
|
||||
import pandas as pd
|
||||
from etl.epc.Record import EPCRecord
|
||||
from etl.bill_savings.KwhData import KwhData
|
||||
from backend.SearchEpc import SearchEpc
|
||||
|
||||
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.functions.materials_functions import get_materials
|
||||
from backend.ml_models.api import ModelApi
|
||||
|
||||
from backend.app.plan.utils import get_cleaned
|
||||
|
||||
from backend.Property import Property
|
||||
from etl.solar.SolarPhotoSupply import SolarPhotoSupply
|
||||
|
||||
from recommendations.Recommendations import Recommendations
|
||||
from utils.logger import setup_logger
|
||||
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")
|
||||
|
||||
logger = setup_logger()
|
||||
|
|
@ -41,21 +40,16 @@ cleaning_data = read_dataframe_from_s3_parquet(
|
|||
materials = get_materials(session)
|
||||
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(
|
||||
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 = [
|
||||
{
|
||||
|
|
@ -66,13 +60,13 @@ scenario_properties = [
|
|||
[
|
||||
["internal_wall_insulation"],
|
||||
"11",
|
||||
{"walls_insulation_thickness_ending": "average"},
|
||||
{},
|
||||
[0],
|
||||
],
|
||||
[
|
||||
["external_wall_insulation"],
|
||||
"10",
|
||||
{"walls_insulation_thickness_ending": "average"},
|
||||
{},
|
||||
[0],
|
||||
],
|
||||
[["solar", "windows"], "15", {"photo_supply_ending": 50}, [0, 1]],
|
||||
|
|
@ -85,8 +79,8 @@ scenario_properties = [
|
|||
"measures": [
|
||||
[
|
||||
["cavity_wall_insulation", "loft_insulation"],
|
||||
"15",
|
||||
{"walls_insulation_thickness_ending": "average"},
|
||||
"11",
|
||||
{},
|
||||
[0, 1],
|
||||
],
|
||||
],
|
||||
|
|
@ -98,8 +92,8 @@ scenario_properties = [
|
|||
"measures": [
|
||||
[
|
||||
["cavity_wall_insulation", "loft_insulation"],
|
||||
"15",
|
||||
{"walls_insulation_thickness_ending": "average"},
|
||||
"10",
|
||||
{},
|
||||
[0, 1],
|
||||
],
|
||||
],
|
||||
|
|
@ -111,8 +105,8 @@ scenario_properties = [
|
|||
"measures": [
|
||||
[
|
||||
["cavity_wall_insulation", "loft_insulation"],
|
||||
"15",
|
||||
{"walls_insulation_thickness_ending": "average"},
|
||||
"11",
|
||||
{},
|
||||
[0, 1],
|
||||
],
|
||||
],
|
||||
|
|
@ -124,15 +118,14 @@ scenario_properties = [
|
|||
"measures": [
|
||||
[
|
||||
["cavity_wall_insulation", "loft_insulation"],
|
||||
"15",
|
||||
{"walls_insulation_thickness_ending": "average"},
|
||||
"10",
|
||||
{},
|
||||
[0, 1],
|
||||
],
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
recommendations_scoring_data = []
|
||||
|
||||
for scenario_property in scenario_properties:
|
||||
|
|
@ -173,9 +166,19 @@ for scenario_property in scenario_properties:
|
|||
)
|
||||
|
||||
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()
|
||||
|
||||
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")
|
||||
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)
|
||||
|
||||
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,
|
||||
},
|
||||
model_api = ModelApi(
|
||||
portfolio_id="generate-scenarios-data", timestamp=created_at, prediction_buckets=get_prediction_buckets()
|
||||
)
|
||||
|
||||
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(
|
||||
recommendations_scoring_data,
|
||||
"retrofit-data-dev",
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ class Recommendations:
|
|||
materials: List,
|
||||
exclusions: 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
|
||||
|
|
@ -42,12 +43,14 @@ class Recommendations:
|
|||
None, meaning no exclusions to be applied
|
||||
:param inclusions: List of specific measures of measure types to include. Defaulted to None, meaning all
|
||||
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.materials = materials
|
||||
self.exclusions = exclusions if exclusions else []
|
||||
self.inclusions = inclusions if inclusions else []
|
||||
self.default_u_values = default_u_values
|
||||
|
||||
self.all_specific_measures = 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]
|
||||
|
||||
# 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:
|
||||
property_recommendations.append(self.wall_recomender.recommendations)
|
||||
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:
|
||||
property_recommendations.append(self.roof_recommender.recommendations)
|
||||
phase += 1
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ class RoofRecommendations:
|
|||
|
||||
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"]:
|
||||
return
|
||||
|
|
@ -171,7 +171,8 @@ class RoofRecommendations:
|
|||
insulation_thickness=self.insulation_thickness,
|
||||
phase=phase,
|
||||
is_flat=False,
|
||||
is_pitched=True
|
||||
is_pitched=True,
|
||||
default_u_values=default_u_values
|
||||
)
|
||||
return
|
||||
|
||||
|
|
@ -184,7 +185,8 @@ class RoofRecommendations:
|
|||
insulation_thickness=0,
|
||||
phase=phase,
|
||||
is_flat=True,
|
||||
is_pitched=False
|
||||
is_pitched=False,
|
||||
default_u_values=default_u_values
|
||||
)
|
||||
return
|
||||
|
||||
|
|
@ -193,7 +195,7 @@ class RoofRecommendations:
|
|||
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]
|
||||
):
|
||||
self.recommend_room_roof_insulation(u_value, phase)
|
||||
self.recommend_room_roof_insulation(u_value, phase, default_u_values)
|
||||
return
|
||||
|
||||
raise NotImplementedError("Implement me")
|
||||
|
|
@ -215,7 +217,7 @@ class RoofRecommendations:
|
|||
raise ValueError("Invalid material type")
|
||||
|
||||
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 is_pitched: Is the roof pitched
|
||||
:param is_flat: Is the roof flat
|
||||
:param default_u_values: Use default u-values
|
||||
:return:
|
||||
"""
|
||||
|
||||
|
|
@ -266,7 +269,6 @@ class RoofRecommendations:
|
|||
recommendations = []
|
||||
for _, insulation_material_group in insulation_materials.groupby("description"):
|
||||
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
|
||||
# loft is already partially insulated.
|
||||
# Note: This requirement is only for loft insulation
|
||||
|
|
@ -305,7 +307,9 @@ class RoofRecommendations:
|
|||
cost_result = override_costs(cost_result)
|
||||
|
||||
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
|
||||
valid_numeric_values = [
|
||||
|
|
@ -330,7 +334,7 @@ class RoofRecommendations:
|
|||
valid_numeric_values, key=lambda x: abs(x - proposed_depth)
|
||||
)
|
||||
|
||||
if proposed_depth >= 270:
|
||||
if proposed_depth >= 300:
|
||||
new_efficiency = "Very Good"
|
||||
else:
|
||||
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"
|
||||
|
||||
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":
|
||||
new_description = "Flat, insulated"
|
||||
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:
|
||||
raise ValueError("Invalid material type")
|
||||
|
||||
|
|
|
|||
|
|
@ -177,9 +177,7 @@ class SolarPvRecommendations:
|
|||
if self.property.roof["is_flat"]:
|
||||
roof_area = self.property.insulation_floor_area
|
||||
else:
|
||||
roof_area = estimate_pitched_roof_area(
|
||||
floor_area=self.property.insulation_floor_area, floor_height=self.property.data["floor-height"]
|
||||
)
|
||||
roof_area = estimate_pitched_roof_area(floor_area=self.property.insulation_floor_area, )
|
||||
solar_configurations = pd.DataFrame(
|
||||
[
|
||||
{
|
||||
|
|
|
|||
|
|
@ -179,7 +179,7 @@ class WallRecommendations(Definitions):
|
|||
|
||||
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 +
|
||||
# U-value less than 0.18 and if in or close to a conversation area,
|
||||
# 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 u_value >= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE:
|
||||
# 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
|
||||
|
||||
# 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():
|
||||
self.find_insulation(u_value, phase, measures=measures)
|
||||
self.find_insulation(u_value, phase, measures=measures, default_u_values=default_u_values)
|
||||
return
|
||||
|
||||
# If the u-value is within regulations, we don't do anything
|
||||
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
|
||||
material will give us the best U-value.
|
||||
|
|
@ -289,6 +289,7 @@ class WallRecommendations(Definitions):
|
|||
filled cavity wall
|
||||
:param phase: The phase of the recommendation
|
||||
: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)
|
||||
|
|
@ -344,7 +345,15 @@ class WallRecommendations(Definitions):
|
|||
description = self._make_description(material)
|
||||
|
||||
# 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()
|
||||
|
||||
|
|
@ -359,7 +368,7 @@ class WallRecommendations(Definitions):
|
|||
simulation_config = {
|
||||
**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(
|
||||
|
|
@ -439,7 +448,7 @@ class WallRecommendations(Definitions):
|
|||
|
||||
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
|
||||
recommendations = []
|
||||
|
|
@ -534,6 +543,15 @@ class WallRecommendations(Definitions):
|
|||
"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(
|
||||
{
|
||||
"phase": phase,
|
||||
|
|
@ -564,7 +582,7 @@ class WallRecommendations(Definitions):
|
|||
|
||||
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
|
||||
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
|
||||
),
|
||||
phase=phase,
|
||||
default_u_values=default_u_values
|
||||
)
|
||||
|
||||
iwi_recommendations = []
|
||||
|
|
@ -592,6 +611,7 @@ class WallRecommendations(Definitions):
|
|||
u_value=u_value,
|
||||
insulation_materials=pd.DataFrame(self.internal_wall_insulation_materials),
|
||||
phase=phase,
|
||||
default_u_values=default_u_values
|
||||
)
|
||||
|
||||
self.recommendations += ewi_recommendations + iwi_recommendations
|
||||
|
|
|
|||
|
|
@ -340,6 +340,7 @@ s9_list = [
|
|||
s10_list = [
|
||||
{
|
||||
"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_at_rafters": 2.3,
|
||||
"Flat_roof": 2.3,
|
||||
|
|
@ -350,6 +351,7 @@ s10_list = [
|
|||
},
|
||||
{
|
||||
"Age_band": "E",
|
||||
"Insulation_Thickness": 12,
|
||||
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 1.5,
|
||||
"Pitched_slates_or_tiles_insulation_at_rafters": 1.5,
|
||||
"Flat_roof": 1.5,
|
||||
|
|
@ -360,6 +362,7 @@ s10_list = [
|
|||
},
|
||||
{
|
||||
"Age_band": "F",
|
||||
"Insulation_Thickness": 50,
|
||||
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.68,
|
||||
"Pitched_slates_or_tiles_insulation_at_rafters": 0.68,
|
||||
"Flat_roof": 0.68,
|
||||
|
|
@ -370,6 +373,7 @@ s10_list = [
|
|||
},
|
||||
{
|
||||
"Age_band": "G",
|
||||
"Insulation_Thickness": 100,
|
||||
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.40,
|
||||
"Pitched_slates_or_tiles_insulation_at_rafters": 0.40,
|
||||
"Flat_roof": 0.40,
|
||||
|
|
@ -380,6 +384,7 @@ s10_list = [
|
|||
},
|
||||
{
|
||||
"Age_band": "H",
|
||||
"Insulation_Thickness": 150,
|
||||
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.30,
|
||||
"Pitched_slates_or_tiles_insulation_at_rafters": 0.35,
|
||||
"Flat_roof": 0.35,
|
||||
|
|
@ -390,6 +395,7 @@ s10_list = [
|
|||
},
|
||||
{
|
||||
"Age_band": "I",
|
||||
"Insulation_Thickness": 150,
|
||||
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.26,
|
||||
"Pitched_slates_or_tiles_insulation_at_rafters": 0.35,
|
||||
"Flat_roof": 0.35,
|
||||
|
|
@ -400,6 +406,7 @@ s10_list = [
|
|||
},
|
||||
{
|
||||
"Age_band": "J",
|
||||
"Insulation_Thickness": 270,
|
||||
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.16,
|
||||
"Pitched_slates_or_tiles_insulation_at_rafters": 0.20,
|
||||
"Flat_roof": 0.25,
|
||||
|
|
@ -410,6 +417,7 @@ s10_list = [
|
|||
},
|
||||
{
|
||||
"Age_band": "K",
|
||||
"Insulation_Thickness": 270,
|
||||
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.16,
|
||||
"Pitched_slates_or_tiles_insulation_at_rafters": 0.20,
|
||||
"Flat_roof": 0.25,
|
||||
|
|
@ -420,6 +428,7 @@ s10_list = [
|
|||
},
|
||||
{
|
||||
"Age_band": "L",
|
||||
"Insulation_Thickness": 270,
|
||||
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.16,
|
||||
"Pitched_slates_or_tiles_insulation_at_rafters": 0.18,
|
||||
"Flat_roof": 0.18,
|
||||
|
|
|
|||
|
|
@ -239,12 +239,7 @@ def get_wall_u_value(
|
|||
return float(mapped_value)
|
||||
|
||||
|
||||
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 the roof as pitched & insulated at the rafters, it's a room roof
|
||||
def extract_thickness(thickness, is_roof_room, is_at_rafters, is_loft, is_flat):
|
||||
if is_roof_room or is_at_rafters:
|
||||
# TODO: We get None instead of a string none, this should be fixed
|
||||
if thickness is None:
|
||||
|
|
@ -258,19 +253,40 @@ def get_u_value_from_s9(
|
|||
}
|
||||
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 (
|
||||
not is_loft and not is_roof_room and not is_at_rafters
|
||||
):
|
||||
return None
|
||||
elif thickness.endswith("+"):
|
||||
thickness = int(thickness[:-1])
|
||||
return thickness
|
||||
else:
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
column = (
|
||||
"Thatched_roof_U_value_W_m2K"
|
||||
|
|
@ -323,6 +339,14 @@ def get_roof_u_value(
|
|||
if has_dwelling_above:
|
||||
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
|
||||
# The conditions for using table S9 are:
|
||||
# - 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
|
||||
# a feature of lofts and roof rooms
|
||||
u_value = get_u_value_from_s9(
|
||||
thickness=insulation_thickness,
|
||||
thickness=thickness,
|
||||
s9=s9,
|
||||
is_loft=is_loft,
|
||||
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"
|
||||
|
||||
# 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):
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from recommendations import recommendation_utils
|
|||
from datatypes.enums import QuantityUnits
|
||||
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.roof_uvalue_test_cases import roof_uvalue_test_cases
|
||||
|
||||
|
||||
class TestRecommendationUtils:
|
||||
|
|
@ -222,6 +223,17 @@ class TestRecommendationUtils:
|
|||
u_value = recommendation_utils.get_roof_u_value(**inputs)
|
||||
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(
|
||||
"test_case",
|
||||
wall_uvalue_test_cases
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue