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 if self.property.floor.get("no_data", False): return u_value = self.property.floor["thermal_transmittance"] property_type = self.property.epc_record.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 # In this case, we have no recommendation to make. E.g., if we have a solid floor property # but solid floor insulation has been excluded as a measure, we get here return @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(), ) 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(), ) 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, "innovation_rate": material["innovation_rate"], } )