import math from typing import List from BaseUtility import Definitions from datatypes.enums import QuantityUnits 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 ) from recommendations.rdsap_tables import FLOOR_LEVEL_MAP 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 # 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.materials = materials self.suspended_floor_insulation_parts = [ part for part in self.materials if part["type"] == "suspended_floor_insulation" ] self.solid_floor_insulation_parts = [ part for part in self.materials if part["type"] == "solid_floor_insulation" ] self.exposed_floor_insulation_parts = [ part for part in self.materials if part["type"] == "exposed_floor_insulation" ] def recommend(self): u_value = self.property.floor["thermal_transmittance"] floor_level = ( FLOOR_LEVEL_MAP[self.property.data["floor-level"]] if self.property.data["floor-level"] not in self.DATA_ANOMALY_MATCHES else None ) property_type = self.property.data["property-type"] floor_area = self.property.floor_area / self.property.number_of_floors year_built = self.property.year_built 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 (floor_level != 0) and (property_type == "Flat"): return if u_value: # By being built more recently than this, it means that the property was likely build with soild # concrete floors with insulation already if year_built < self.PART_L_YEAR_CUTOFF: raise NotImplementedError("Not investigated this use case") if u_value <= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE: # The floor is already compliant return 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"]: # Given the U-value, we recommend underfloor insulation self.recommend_floor_insulation(u_value=u_value, parts=self.suspended_floor_insulation_parts) return if self.property.floor["is_solid"]: # Given the U-value, we recommend solid floor insulation options which are usually solid foam self.recommend_floor_insulation(u_value=u_value, parts=self.solid_floor_insulation_parts) return if self.property.floor["is_to_unheated_space"] or self.property.floor["is_to_external_air"]: self.recommend_floor_insulation(u_value=u_value, parts=self.exposed_floor_insulation_parts) return raise NotImplementedError("Implement me!") @staticmethod def _make_floor_description(part, depth): return f"Install {depth}{part['depth_unit']} {part['description']} insulation" def recommend_floor_insulation(self, u_value, parts): """ This method is tasked with estimating the impact of performing suspended floor insulation :return: """ lowest_selected_u_value = None for part in parts: for depth, cost_per_unit in zip(part["depths"], part["cost"]): part_u_value = r_value_per_mm_to_u_value(depth, part["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) quantity = self.property.floor_area / self.property.number_of_floors estimated_cost = cost_per_unit * quantity self.recommendations.append( { "parts": [ get_recommended_part( part=part, selected_depth=depth, quantity=quantity, quantity_unit=QuantityUnits.m2.value, selected_total_cost=estimated_cost ), ], "type": "floor_insulation", "description": self._make_floor_description(part, depth), "starting_u_value": u_value, "new_u_value": new_u_value, "sap_points": None, "cost": estimated_cost, } )