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:
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"]]

View file

@ -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",

View file

@ -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

View file

@ -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")

View file

@ -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(
[
{

View file

@ -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

View file

@ -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,

View file

@ -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):

View file

@ -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