mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
267 lines
12 KiB
Python
267 lines
12 KiB
Python
import math
|
|
from typing import List
|
|
|
|
import pandas as pd
|
|
|
|
from BaseUtility import Definitions
|
|
from datatypes.enums import QuantityUnits
|
|
from backend.app.plan.schemas import MEASURE_MAP
|
|
from backend.Property import Property
|
|
from recommendations.recommendation_utils import (
|
|
r_value_per_mm_to_u_value, calculate_u_value_uplift, is_diminishing_returns, update_lowest_selected_u_value,
|
|
get_recommended_part, get_floor_u_value, override_costs, check_simulation_difference
|
|
)
|
|
from recommendations.Costs import Costs
|
|
from etl.epc_clean.epc_attributes.FloorAttributes import FloorAttributes
|
|
|
|
|
|
class FloorRecommendations(Definitions):
|
|
# part L building regulations indicate that any rennovations on an existing property's walls should
|
|
# achieve a U-value of no higher than 0.3
|
|
BUILDING_REGULATIONS_PART_L_MAX_U_VALUE = 0.25
|
|
# We don't recommend measures that are too low because it becomes expensive, therefore we aim to avoid
|
|
# diminishing returns. This value should be verified with Osmosis (TODO)
|
|
DIMINISHING_RETURNS_U_VALUE = 0.2
|
|
|
|
REGION_LOOKUP = {
|
|
"England and Wales": "England_Wales",
|
|
}
|
|
|
|
PART_L_YEAR_CUTOFF = 2002
|
|
|
|
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.suspended_floor_insulation_materials = [
|
|
part for part in materials if part["type"] == "suspended_floor_insulation"
|
|
]
|
|
|
|
self.suspended_floor_non_insulation_materials = [
|
|
part for part in materials if part["type"] in [
|
|
"suspended_floor_demolition", "suspended_floor_redecoration", "suspended_floor_vapour_barrier"
|
|
]
|
|
]
|
|
|
|
# For solid floor, we don't use materials that are too thick
|
|
self.solid_floor_insulation_materials = [
|
|
part for part in materials if part["type"] == "solid_floor_insulation" if float(part["depth"]) <= 75
|
|
]
|
|
|
|
self.solid_floor_non_insulation_materials = [
|
|
part for part in materials if part["type"] in [
|
|
"solid_floor_demolition", "solid_floor_preparation", "solid_floor_vapour_barrier",
|
|
"solid_floor_redecoration"
|
|
]
|
|
]
|
|
|
|
def recommend(self, phase=0, measures=None):
|
|
|
|
measures = MEASURE_MAP["floor_insulation"] if measures is None else measures
|
|
|
|
# If we have no measures or none of the measures are relevant, we can't recommend anything
|
|
if not measures or not any(x in measures for x in MEASURE_MAP["floor_insulation"]):
|
|
return
|
|
|
|
u_value = self.property.floor["thermal_transmittance"]
|
|
property_type = self.property.data["property-type"]
|
|
floor_area = self.property.insulation_floor_area
|
|
|
|
if self.property.floor["another_property_below"] | (self.property.floor["insulation_thickness"] in [
|
|
"average", "above average"
|
|
]):
|
|
# If there's another property below, it's likely impractical to recommend a floor upgrade,
|
|
# or if the floor is already insualted
|
|
return
|
|
|
|
# If the property is a flat that isn't at ground level, it's likely impractical to recommend a floor upgrade
|
|
if (self.property.floor_level != 0) and (property_type == "Flat") and (
|
|
self.property.floor["another_property_below"]
|
|
):
|
|
return
|
|
|
|
# If the property is a new build flat, we won't recommend floor upgrades
|
|
if len(self.property.full_sap_epc) and (property_type == "Flat"):
|
|
return
|
|
|
|
if u_value:
|
|
|
|
# In this case where we have the u-value of a floor, we likely don't have any other information about it
|
|
# so there is no recommendation that we can practically make
|
|
if (
|
|
self.property.floor["is_suspended"] or
|
|
self.property.floor["is_to_unheated_space"] or
|
|
self.property.floor["is_to_external_air"] or
|
|
self.property.floor["is_solid"]
|
|
):
|
|
raise ValueError("This should not be possible")
|
|
return
|
|
|
|
if u_value is None:
|
|
u_value = get_floor_u_value(
|
|
floor_type=self.property.floor_type,
|
|
area=floor_area,
|
|
perimeter=self.property.perimeter,
|
|
age_band=self.property.age_band,
|
|
insulation_thickness=self.property.floor["insulation_thickness"],
|
|
wall_type=self.property.wall_type
|
|
)
|
|
|
|
self.estimated_u_value = u_value
|
|
|
|
if u_value < self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE:
|
|
return
|
|
|
|
if (
|
|
self.property.floor["is_suspended"] or
|
|
self.property.floor["is_to_unheated_space"] or
|
|
self.property.floor["is_to_external_air"]
|
|
) and "suspended_floor_insulation" in measures:
|
|
# Given the U-value, we recommend underfloor insulation
|
|
self.recommend_floor_insulation(
|
|
phase=phase,
|
|
u_value=u_value,
|
|
insulation_materials=self.suspended_floor_insulation_materials,
|
|
non_insulation_materials=self.suspended_floor_non_insulation_materials
|
|
)
|
|
return
|
|
|
|
if self.property.floor["is_solid"] and "solid_floor_insulation" in measures:
|
|
# Given the U-value, we recommend solid floor insulation options which are usually solid foam
|
|
self.recommend_floor_insulation(
|
|
u_value=u_value,
|
|
insulation_materials=self.solid_floor_insulation_materials,
|
|
non_insulation_materials=self.solid_floor_non_insulation_materials,
|
|
phase=phase
|
|
)
|
|
return
|
|
|
|
raise NotImplementedError("Implement me!")
|
|
|
|
@staticmethod
|
|
def _make_floor_description(material):
|
|
|
|
if material["type"] == "suspended_floor_insulation":
|
|
return (f"Install {int(material['depth'])}{material['depth_unit']} {material['description']} insulation in "
|
|
f"suspended floor")
|
|
|
|
if material["type"] == "solid_floor_insulation":
|
|
return (f"Install {int(material['depth'])}{material['depth_unit']} {material['description']} insulation on "
|
|
f"solid floor")
|
|
|
|
if material["type"] == "exposed_floor_insulation":
|
|
return (f"Install {int(material['depth'])}{material['depth_unit']} {material['description']} insulation in "
|
|
f"exposed floor")
|
|
|
|
raise ValueError("Invalid material type - implement me!")
|
|
|
|
def recommend_floor_insulation(self, u_value, insulation_materials, non_insulation_materials, phase):
|
|
"""
|
|
This method is tasked with estimating the impact of performing suspended floor insulation
|
|
:return:
|
|
"""
|
|
|
|
insulation_materials = pd.DataFrame(insulation_materials)
|
|
|
|
non_invasive_recs = next(
|
|
(r for r in self.property.non_invasive_recommendations if
|
|
r["type"] == insulation_materials["type"].values[0]), {}
|
|
)
|
|
|
|
lowest_selected_u_value = None
|
|
for _, insulation_material_group in insulation_materials.groupby("description"):
|
|
|
|
for _, material in insulation_material_group.iterrows():
|
|
|
|
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 is_diminishing_returns(
|
|
self.recommendations, new_u_value, lowest_selected_u_value, self.DIMINISHING_RETURNS_U_VALUE
|
|
):
|
|
continue
|
|
|
|
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)
|
|
|
|
if material["type"] == "suspended_floor_insulation":
|
|
cost_result = self.costs.suspended_floor_insulation(
|
|
insulation_floor_area=self.property.insulation_floor_area,
|
|
material=material.to_dict(),
|
|
non_insulation_materials=non_insulation_materials
|
|
)
|
|
|
|
already_installed = "suspended_floor_insulation" in self.property.already_installed
|
|
if already_installed:
|
|
cost_result = override_costs(cost_result)
|
|
|
|
new_description = "Suspended, insulated"
|
|
|
|
elif material["type"] == "solid_floor_insulation":
|
|
cost_result = self.costs.solid_floor_insulation(
|
|
insulation_floor_area=self.property.insulation_floor_area,
|
|
material=material.to_dict(),
|
|
non_insulation_materials=non_insulation_materials
|
|
)
|
|
|
|
already_installed = "solid_floor_insulation" in self.property.already_installed
|
|
if already_installed:
|
|
cost_result = override_costs(cost_result)
|
|
|
|
new_description = "Solid, insulated"
|
|
else:
|
|
raise NotImplementedError("Implement me!")
|
|
|
|
sap_points = non_invasive_recs.get("sap_points", None)
|
|
survey = non_invasive_recs.get("survey", False)
|
|
|
|
floor_ending_config = FloorAttributes(new_description).process()
|
|
floor_simulation_config = check_simulation_difference(
|
|
new_config=floor_ending_config, old_config=self.property.floor, prefix="floor_"
|
|
)
|
|
|
|
simulation_config = {
|
|
**floor_simulation_config,
|
|
# We don't simulate the impact using this U-value, but rather the average because this
|
|
# variable is way too volatile. Will likely be removed from the model
|
|
"floor_thermal_transmittance_ending": 0.685593,
|
|
}
|
|
|
|
self.recommendations.append(
|
|
{
|
|
"phase": phase,
|
|
"parts": [
|
|
get_recommended_part(
|
|
part=material.to_dict(),
|
|
quantity=self.property.insulation_floor_area,
|
|
quantity_unit=QuantityUnits.m2.value,
|
|
cost_result=cost_result
|
|
),
|
|
],
|
|
"type": material["type"],
|
|
"measure_type": material["type"], # This is distinct between suspended and solid floor
|
|
"description": self._make_floor_description(material),
|
|
"starting_u_value": u_value,
|
|
"new_u_value": new_u_value,
|
|
"sap_points": sap_points,
|
|
"survey": survey,
|
|
"already_installed": already_installed,
|
|
"simulation_config": simulation_config,
|
|
"description_simulation": {
|
|
"floor-description": "Solid, insulated" if
|
|
material["type"] == "solid_floor_insulation"
|
|
else "Suspended, insulated"
|
|
},
|
|
**cost_result
|
|
}
|
|
)
|