mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
588 lines
27 KiB
Python
588 lines
27 KiB
Python
import math
|
|
import pandas as pd
|
|
from backend.Property import Property
|
|
from backend.app.plan.schemas import MEASURE_MAP
|
|
from typing import List
|
|
from datatypes.enums import QuantityUnits
|
|
from recommendations.recommendation_utils import (
|
|
get_roof_u_value, r_value_per_mm_to_u_value, calculate_u_value_uplift, is_diminishing_returns,
|
|
update_lowest_selected_u_value, get_recommended_part, convert_thickness_to_numeric, override_costs,
|
|
check_simulation_difference
|
|
)
|
|
from recommendations.Costs import Costs
|
|
from etl.epc_clean.epc_attributes.RoofAttributes import RoofAttributes
|
|
|
|
|
|
class RoofRecommendations:
|
|
# part L building regulations indicate that any rennovations on an existing property's roof should
|
|
# achieve a U-value of no higher than 0.16
|
|
# This can be seen in table 4.3 in building regulations part L:
|
|
# https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/1133079
|
|
# /Approved_Document_L__Conservation_of_fuel_and_power__Volume_1_Dwellings__2021_edition_incorporating_2023_amendments.pdf
|
|
BUILDING_REGULATIONS_PART_L_MAX_U_VALUE = 0.16
|
|
|
|
DIMINISHING_RETURNS_U_VALUE = 0.14
|
|
|
|
# It is recommended that lofts should have at least 270mm of insulation. If the property has more than 200mm of
|
|
# loft insulation in place already, we do not recommend anything for the moment
|
|
MINIMUM_LOFT_ISULATION_MM = 200
|
|
MINIMUM_RECOMMENDED_LOFT_INSULATION = 280
|
|
# Flat roof should have at least 100mm of insulation
|
|
MINIMUM_FLAT_ROOF_ISULATION_MM = 100
|
|
|
|
def __init__(
|
|
self,
|
|
property_instance: Property,
|
|
materials: List
|
|
):
|
|
self.property = property_instance
|
|
self.costs = Costs(self.property)
|
|
# For audit purposes, when estimating u values we'll store it
|
|
self.estimated_u_value = None
|
|
|
|
# Will contains a list of recommended measures
|
|
self.recommendations = []
|
|
|
|
self.loft_insulation_materials = [
|
|
part for part in materials if (part["type"] == "loft_insulation") and (part["is_installer_quote"])
|
|
]
|
|
|
|
# We don't have proper installer quotes for flat roof insulation
|
|
self.flat_roof_insulation_materials = [
|
|
part for part in materials if part["type"] == "flat_roof_insulation"
|
|
]
|
|
|
|
self.room_roof_insulation_materials = [
|
|
part for part in materials if part["type"] == "room_roof_insulation"
|
|
]
|
|
|
|
# Extract the insulation thickness from the roof, which is used throughout this method
|
|
self.insulation_thickness = convert_thickness_to_numeric(
|
|
self.property.roof["insulation_thickness"],
|
|
self.property.roof["is_pitched"],
|
|
self.property.roof["is_flat"]
|
|
)
|
|
|
|
@classmethod
|
|
def get_loft_insulation_sap_limit(cls, roof_energy_eff, existing_thickness):
|
|
"""
|
|
Get the SAP limit for loft insulation
|
|
:param roof_energy_eff:
|
|
:return:
|
|
"""
|
|
|
|
if str(existing_thickness).isdigit():
|
|
if float(existing_thickness) >= 250:
|
|
return 0
|
|
|
|
if roof_energy_eff in ["Good", "Very Good"]:
|
|
return 1
|
|
|
|
return None
|
|
|
|
def is_loft_already_insulated(self, measures):
|
|
"""
|
|
Check if the loft is already insulated
|
|
"""
|
|
|
|
# If we have a non-invasive recommendation for the loft insulation, we can assume that the loft is not insulated
|
|
if "loft_insulation" in measures:
|
|
return False
|
|
|
|
return (self.insulation_thickness > self.MINIMUM_LOFT_ISULATION_MM) and self.property.roof["is_pitched"]
|
|
|
|
def is_room_roof_insulated_or_unsuitable(self, measures):
|
|
|
|
"""
|
|
Check if the room roof is already insulated
|
|
"""
|
|
|
|
# If the roof is a room roof room roof is not included in the measures, we deem the recommendation unsuitable
|
|
unsuitable = "room_roof_insulation" not in measures and self.property.roof["is_roof_room"]
|
|
if unsuitable:
|
|
return True
|
|
|
|
full_insulated_room_roof = (
|
|
self.property.roof["is_roof_room"] and
|
|
self.property.roof["insulation_thickness"] in ["average", "above_average"]
|
|
)
|
|
|
|
room_roof_insulated_at_rafters = (
|
|
self.property.roof["is_pitched"] and
|
|
self.property.roof["is_at_rafters"] and
|
|
self.property.roof["insulation_thickness"] in ["average", "above_average"]
|
|
)
|
|
|
|
has_non_invasive_recommendation = any(
|
|
x["type"] == "room_roof_insulation" for x in self.property.non_invasive_recommendations
|
|
)
|
|
|
|
return (full_insulated_room_roof or room_roof_insulated_at_rafters) and not has_non_invasive_recommendation
|
|
|
|
def recommend(self, phase, measures=None, default_u_values=False):
|
|
|
|
if self.property.roof["has_dwelling_above"]:
|
|
return
|
|
|
|
measures = MEASURE_MAP["roof_insulation"] if measures is None else measures
|
|
|
|
u_value = self.property.roof["thermal_transmittance"]
|
|
|
|
# If we have a flat roof but we don't have flat roof as a measure, we exit
|
|
if self.property.roof["is_flat"] and "flat_roof_insulation" not in measures:
|
|
return
|
|
|
|
# We check if the roof is already insulated and if so, we exit
|
|
|
|
# Building regulations part L recommend installing at least 270mm of insulation, however generally we
|
|
# experience diminishing returns in terms of SAP once we go beyond around 150mm of insulation
|
|
# This only holds true for pitched roofs.
|
|
if self.is_loft_already_insulated(measures):
|
|
return
|
|
|
|
if (self.insulation_thickness >= self.MINIMUM_FLAT_ROOF_ISULATION_MM) and self.property.roof["is_flat"]:
|
|
return
|
|
|
|
if self.is_room_roof_insulated_or_unsuitable(measures):
|
|
return
|
|
|
|
if self.property.roof["is_thatched"]:
|
|
return
|
|
|
|
# If we have a u-value and we don't have a non-invasive recommendation, we can't recommend anything
|
|
if u_value and not any(
|
|
x in MEASURE_MAP["roof_insulation"] for x in [r["type"] for r in self.property.non_invasive_recommendations]
|
|
):
|
|
# We don't have enough information to provide a recommendation
|
|
return
|
|
|
|
u_value = get_roof_u_value(
|
|
insulation_thickness=self.property.roof["insulation_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"],
|
|
)
|
|
|
|
self.estimated_u_value = u_value
|
|
if (u_value <= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE) or all(
|
|
m not in measures for m in MEASURE_MAP["roof_insulation"]
|
|
):
|
|
# The Roof is already compliant
|
|
return
|
|
|
|
non_invasive_recommendations = self.property.non_invasive_recommendations
|
|
|
|
# We check a specific condition - which will imply loft insulation isn't appropriate but room in roof
|
|
# insulation is
|
|
# 1) We have an uninsulated loft (assumed)
|
|
# 2) We have a non-intrusive recommendation for room in roof insulation
|
|
|
|
rir_over_loft = (
|
|
self.property.roof["is_pitched"] and
|
|
self.property.roof["insulation_thickness"] == "none" and
|
|
"room_in_roof_insulation" in [x["type"] for x in non_invasive_recommendations]
|
|
)
|
|
|
|
# We firstly handle non-intrusive recommendations, which may override the normal roof insulation recommendations
|
|
if ("loft_insulation" in [x["type"] for x in non_invasive_recommendations]) or (
|
|
self.property.roof["is_pitched"] and "loft_insulation" in measures and
|
|
not self.property.roof["is_at_rafters"]
|
|
) and not rir_over_loft:
|
|
self.recommend_roof_insulation(
|
|
u_value=u_value,
|
|
insulation_thickness=self.insulation_thickness,
|
|
phase=phase,
|
|
is_flat=False,
|
|
is_pitched=True,
|
|
default_u_values=default_u_values
|
|
)
|
|
return
|
|
|
|
if (
|
|
(self.property.roof["is_flat"] and "flat_roof_insulation" in measures) or
|
|
"flat_roof_insulation" in [x["type"] for x in non_invasive_recommendations]
|
|
):
|
|
self.recommend_roof_insulation(
|
|
u_value=u_value,
|
|
insulation_thickness=0,
|
|
phase=phase,
|
|
is_flat=True,
|
|
is_pitched=False,
|
|
default_u_values=default_u_values
|
|
)
|
|
return
|
|
|
|
# There are cases where the property might have a room roof as the second roof, but we have a recommendation for
|
|
# it, so we allow this override
|
|
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] or
|
|
rir_over_loft
|
|
):
|
|
self.recommend_room_roof_insulation(u_value, phase, default_u_values)
|
|
return
|
|
|
|
raise NotImplementedError("Implement me")
|
|
|
|
@staticmethod
|
|
def make_roof_insulation_description(material):
|
|
if material["type"] == "loft_insulation":
|
|
return f"Install {int(material['depth'])}{material['depth_unit']} of {material['description']} in your loft"
|
|
|
|
if material["type"] == "flat_roof_insulation":
|
|
return (
|
|
f"Insulate the home's flat roof with {int(material['depth'])}{material['depth_unit']} of "
|
|
f"{material['description']}"
|
|
)
|
|
if material["type"] == "room_roof_insulation":
|
|
return (f"Insulate your room roof with {int(material['depth'])}{material['depth_unit']} of "
|
|
f"{material['description']}")
|
|
|
|
raise ValueError("Invalid material type")
|
|
|
|
def recommend_roof_insulation(
|
|
self, u_value, insulation_thickness, phase, is_pitched, is_flat, default_u_values
|
|
):
|
|
|
|
"""
|
|
This method will recommend which insulation materials to use
|
|
This function handles both the case of loft insulation and flat roof insulation
|
|
|
|
We also follow advide provided in this article on the Energy Saving Trust website, providing
|
|
high level guidance around roof insulation:
|
|
https://energysavingtrust.org.uk/advice/roof-and-loft-insulation/
|
|
|
|
The process roughly looks like the following:
|
|
- Remove the Existing Weatherproof Layer: If the roof is being replaced, remove the old weatherproof layer to
|
|
expose the timber roof surface.
|
|
- Install Insulation Boards: Lay the rigid insulation boards directly on the timber roof surface.
|
|
Ensure the boards fit tightly together to prevent thermal bridging (heat loss through the gaps).
|
|
- Add a Vapour Control Layer (VCL): This is crucial to prevent moisture from entering the insulation layer,
|
|
which can lead to dampness and rot. The VCL is placed over the insulation.
|
|
- Install a New Weatherproof Layer: On top of the insulation and VCL, install a new weatherproof layer. This
|
|
could be traditional roofing materials like bitumen-based felt, rubber membranes like EPDM, or fiberglass.
|
|
|
|
:param u_value: U-value of the roof before any retrofit measures have been installed
|
|
:param insulation_thickness: Existing Insulation thickness of the loft
|
|
: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:
|
|
"""
|
|
|
|
# With loft insulation, 100mm goes between the joists and the rest is rolled on top
|
|
# Therefore the price is 100mm + whatever thickness is rolled on top, rolled at a 90 degree angle
|
|
# from the base layer
|
|
|
|
if is_pitched:
|
|
insulation_materials = self.loft_insulation_materials
|
|
measure_type = "loft_insulation"
|
|
elif is_flat:
|
|
insulation_materials = self.flat_roof_insulation_materials
|
|
measure_type = "flat_roof_insulation"
|
|
else:
|
|
raise ValueError("Roof is not pitched or flat")
|
|
|
|
if not insulation_materials:
|
|
raise ValueError("No roof insulation materials found")
|
|
|
|
insulation_materials = pd.DataFrame(insulation_materials)
|
|
|
|
non_invasive_recommendations = next(
|
|
(r for r in self.property.non_invasive_recommendations if
|
|
r["type"] == insulation_materials["type"].values[0]), {}
|
|
)
|
|
|
|
lowest_selected_u_value = None
|
|
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
|
|
if (
|
|
material["depth"] < self.MINIMUM_RECOMMENDED_LOFT_INSULATION
|
|
) and is_pitched:
|
|
continue
|
|
|
|
part_u_value = r_value_per_mm_to_u_value(material["depth"], material["r_value_per_mm"])
|
|
|
|
_, new_u_value = calculate_u_value_uplift(u_value, part_u_value)
|
|
new_u_value = math.ceil(new_u_value * 100.0) / 100.0
|
|
|
|
# If I have a lowest U value and my new u value is higher than that but lower than the
|
|
# diminishing returns threshold, it can be considered
|
|
|
|
# If I have a lowest U value and my new u value is lower than the lowest value, it's
|
|
# further into the diminishing returns threshold and can shouldn't be
|
|
|
|
if is_diminishing_returns(
|
|
recommendations, new_u_value, lowest_selected_u_value, self.DIMINISHING_RETURNS_U_VALUE
|
|
):
|
|
continue
|
|
|
|
# We allow a small tolerance for error so we don't discount the recommendation entirely
|
|
if new_u_value <= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE:
|
|
|
|
lowest_selected_u_value = update_lowest_selected_u_value(lowest_selected_u_value, new_u_value)
|
|
|
|
cost_result = self.costs.loft_and_flat_insulation(
|
|
floor_area=self.property.insulation_floor_area,
|
|
material=material
|
|
)
|
|
|
|
already_installed = material["type"] in self.property.already_installed
|
|
if already_installed:
|
|
cost_result = override_costs(cost_result)
|
|
|
|
if material["type"] == "loft_insulation":
|
|
# 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 = [
|
|
12,
|
|
25,
|
|
50,
|
|
75,
|
|
100,
|
|
150,
|
|
200,
|
|
250,
|
|
270,
|
|
300,
|
|
350,
|
|
400,
|
|
]
|
|
|
|
proposed_depth = new_thickness
|
|
if (new_thickness not in valid_numeric_values) and material["type"] == "loft_insulation":
|
|
# Take the nearest value for scoring
|
|
proposed_depth = min(
|
|
valid_numeric_values, key=lambda x: abs(x - proposed_depth)
|
|
)
|
|
|
|
if proposed_depth >= 300:
|
|
new_efficiency = "Very Good"
|
|
else:
|
|
if self.property.data["roof-energy-eff"] not in ["Good", "Very Good"]:
|
|
new_efficiency = "Good"
|
|
else:
|
|
new_efficiency = "Very Good"
|
|
|
|
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")
|
|
|
|
roof_ending_config = RoofAttributes(new_description).process()
|
|
roof_simulation_config = check_simulation_difference(
|
|
new_config=roof_ending_config, old_config=self.property.roof, prefix="roof_"
|
|
)
|
|
|
|
simulation_config = {
|
|
**roof_simulation_config,
|
|
"roof_thermal_transmittance_ending": new_u_value,
|
|
"roof_energy_eff_ending": new_efficiency
|
|
}
|
|
|
|
recommendations.append(
|
|
{
|
|
"phase": phase,
|
|
"parts": [
|
|
get_recommended_part(
|
|
part=material.to_dict(),
|
|
quantity=self.property.insulation_wall_area,
|
|
quantity_unit=QuantityUnits.m2.value,
|
|
cost_result=cost_result
|
|
)
|
|
],
|
|
"type": material["type"],
|
|
"measure_type": measure_type,
|
|
"description": self.make_roof_insulation_description(material),
|
|
"starting_u_value": u_value,
|
|
"new_u_value": new_u_value,
|
|
"sap_points": non_invasive_recommendations.get("sap_points", 0),
|
|
"already_installed": already_installed,
|
|
"simulation_config": simulation_config,
|
|
"description_simulation": {
|
|
"roof-description": new_description,
|
|
"roof-energy-eff": new_efficiency
|
|
},
|
|
**cost_result,
|
|
"survey": non_invasive_recommendations.get("survey", False),
|
|
"innovation_rate": material.to_dict()["innovation_rate"]
|
|
}
|
|
)
|
|
|
|
self.recommendations = recommendations
|
|
|
|
def recommend_room_roof_insulation(self, u_value, phase, default_u_values):
|
|
"""
|
|
This method recommends room in roof insulation for properties that have been identified
|
|
to possess a room in roof.
|
|
|
|
Because we currently have limited data about the construction of the roof, we make the following
|
|
assumptions:
|
|
1) The room in roof has a sloped roof.
|
|
We will make some basic estimations about the area of the roof given the floor area and the height of the
|
|
floors
|
|
2) Insulation of external walls is covered by the wall recommendation class
|
|
3) We assume a "Gable" roof type
|
|
|
|
Further, we recommend internal roof insulation for the room in roof
|
|
|
|
The following document contains details around best practices for insulating a room in roof
|
|
https://assets.publishing.service.gov.uk/media/61d727d18fa8f50594b59305/retrofit-room-in-roof-insulation-best
|
|
-practice.pdf
|
|
Of particular interest are the following:
|
|
|
|
We also follow advide provided in this article on the Energy Saving Trust website, providing
|
|
high level guidance around roof insulation:
|
|
https://energysavingtrust.org.uk/advice/roof-and-loft-insulation/
|
|
|
|
To insulate a warm loft, the following advice is given
|
|
"An alternative way to insulate your loft is to fit rigid insulation boards between and over the rafters.
|
|
Rafters are the sloping timbers that make up the roof itself."
|
|
|
|
To then insulate a room roof, the following recommendation is provided:
|
|
"If you want to use your loft as a living space, or it is already being used as a living space,
|
|
then you need to make sure that all the walls and ceilings between a heated room and an unheated space
|
|
are insulated.
|
|
|
|
- Sloping ceilings can be insulated in the same way as for a warm roof,
|
|
but with a layer of plasterboard on the inside of the insulation.
|
|
- Vertical walls can be insulated in the same way.
|
|
- Flat ceilings can be insulated like a standard loft.
|
|
|
|
:param u_value: Current u-value of the roof
|
|
:param phase: Phase of the recommendation
|
|
:param default_u_values: Use default u-values
|
|
:return:
|
|
"""
|
|
|
|
# We have a list of materials that can be used for room roof insulation
|
|
# We will iterate over these materials and recommend them based on the current u-value of the roof
|
|
# and the cost of the materials
|
|
|
|
rir_non_invasive_recommendation = next(
|
|
(x for x in self.property.non_invasive_recommendations if x["type"] == "room_in_roof_insulation"), {}
|
|
)
|
|
|
|
insulation_materials = pd.DataFrame(self.room_roof_insulation_materials)
|
|
|
|
# lowest_selected_u_value = None
|
|
recommendations = []
|
|
for _, material_group in insulation_materials.groupby("description"):
|
|
for material in material_group.itertuples():
|
|
|
|
part_u_value = r_value_per_mm_to_u_value(material.depth, material.r_value_per_mm)
|
|
|
|
_, new_u_value = calculate_u_value_uplift(u_value, part_u_value)
|
|
new_u_value = math.ceil(new_u_value * 100.0) / 100.0
|
|
|
|
# We allow a small tolerance for error so we don't discount the recommendation entirely
|
|
|
|
estimated_cost = (
|
|
material.total_cost * self.property.insulation_floor_area if
|
|
rir_non_invasive_recommendation.get("cost") is None else
|
|
rir_non_invasive_recommendation.get("cost")
|
|
)
|
|
|
|
# Could also be Roof room(s), ceiling insulated
|
|
new_descriptin = "Roof room(s), insulated"
|
|
roof_ending_config = RoofAttributes(new_descriptin).process()
|
|
roof_simulation_config = check_simulation_difference(
|
|
new_config=roof_ending_config, old_config=self.property.roof, prefix="roof_"
|
|
)
|
|
if self.property.data["roof-energy-eff"] in ["Very Poor", "Poor"]:
|
|
new_efficiency = "Average"
|
|
else:
|
|
new_efficiency = self.property.data["roof-energy-eff"]
|
|
|
|
if default_u_values:
|
|
new_u_value = get_roof_u_value(
|
|
insulation_thickness="average",
|
|
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"],
|
|
)
|
|
|
|
simulation_config = {
|
|
**roof_simulation_config,
|
|
"roof_thermal_transmittance_ending": new_u_value,
|
|
"roof_energy_eff_ending": new_efficiency
|
|
}
|
|
|
|
already_installed = "flat_roof_insulation" in self.property.already_installed
|
|
cost_result = {
|
|
"total": estimated_cost,
|
|
"labour_hours": 80,
|
|
"labour_days": 5,
|
|
}
|
|
if already_installed:
|
|
cost_result = override_costs(cost_result)
|
|
|
|
recommendations.append(
|
|
{
|
|
"phase": phase,
|
|
"parts": [],
|
|
"type": "room_roof_insulation",
|
|
"measure_type": "room_roof_insulation",
|
|
"description": "Insulate room in roof at rafters and re-decorate",
|
|
"starting_u_value": u_value,
|
|
"new_u_value": new_u_value,
|
|
"sap_points": rir_non_invasive_recommendation.get("sap_points", None),
|
|
"simulation_config": simulation_config,
|
|
"description_simulation": {
|
|
"roof-description": new_descriptin,
|
|
"roof-energy-eff": new_efficiency
|
|
},
|
|
**cost_result,
|
|
"already_installed": already_installed,
|
|
"survey": rir_non_invasive_recommendation.get("survey", None),
|
|
"innovation_rate": material.innovation_rate
|
|
}
|
|
)
|
|
|
|
self.recommendations = recommendations
|