diff --git a/backend/app/plan/router.py b/backend/app/plan/router.py index 8d8ffe2d..fffc604e 100644 --- a/backend/app/plan/router.py +++ b/backend/app/plan/router.py @@ -147,7 +147,7 @@ async def trigger_plan(body: PlanTriggerRequest): property_recommendations.append(wall_recomender.recommendations) # Roof recommendations - roof_recommender = RoofRecommendations(property_instance=p, materials=materials_by_type["roof"]) + roof_recommender = RoofRecommendations(property_instance=p, materials=materials) roof_recommender.recommend() if roof_recommender.recommendations: @@ -156,7 +156,7 @@ async def trigger_plan(body: PlanTriggerRequest): # Ventilation recommendations ventilation_recomender = VentilationRecommendations( property_instance=p, - materials=materials_by_type["ventilation"] + materials=[part for part in materials if part["type"] == "mechanical_ventilation"] ) ventilation_recomender.recommend() diff --git a/backend/app/plan/utils.py b/backend/app/plan/utils.py index b73ba874..20b5db5b 100644 --- a/backend/app/plan/utils.py +++ b/backend/app/plan/utils.py @@ -154,7 +154,7 @@ def create_recommendation_scoring_data( if len(parts) != 1: raise ValueError("More than one part for roof insulation - investiage me") - scoring_dict["roof_insulation_thickness_ENDING"] = str(parts[0]["depths"][0]) + scoring_dict["roof_insulation_thickness_ENDING"] = str(int(parts[0]["depth"])) scoring_dict["ROOF_ENERGY_EFF_ENDING"] = "Very Good" else: # Fill missing roof u-values - this fill is not based on recommended upgrades diff --git a/recommendations/Costs.py b/recommendations/Costs.py index c1c9b42e..a96e1215 100644 --- a/recommendations/Costs.py +++ b/recommendations/Costs.py @@ -113,7 +113,7 @@ class Costs: total_cost = subtotal_before_vat + vat_cost - labour_hours = material["labour_hours"] * wall_area + labour_hours = material["labour_hours_per_unit"] * wall_area return { "total": total_cost, @@ -151,7 +151,7 @@ class Costs: total_cost = subtotal_before_vat + vat_cost - labour_hours = material["labour_hours"] * floor_area + labour_hours = material["labour_hours_per_unit"] * floor_area return { "total": total_cost, diff --git a/recommendations/FireplaceRecommendations.py b/recommendations/FireplaceRecommendations.py index 3e82b9d1..9524c75a 100644 --- a/recommendations/FireplaceRecommendations.py +++ b/recommendations/FireplaceRecommendations.py @@ -43,6 +43,6 @@ class FireplaceRecommendations(Definitions): "starting_u_value": None, "new_u_value": None, "sap_points": None, - "cost": estimated_cost, + "total": estimated_cost, } ] diff --git a/recommendations/FloorRecommendations.py b/recommendations/FloorRecommendations.py index 641272a3..96b1356c 100644 --- a/recommendations/FloorRecommendations.py +++ b/recommendations/FloorRecommendations.py @@ -127,7 +127,11 @@ class FloorRecommendations(Definitions): 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) + self.recommend_floor_insulation( + u_value=u_value, + insulation_materials=self.solid_floor_insulation_materials, + non_insulation_materials=self.solid_floor_non_insulation_materials + ) return if self.property.floor["is_to_unheated_space"] or self.property.floor["is_to_external_air"]: diff --git a/recommendations/RoofRecommendations.py b/recommendations/RoofRecommendations.py index 4f96f629..1bee1e8e 100644 --- a/recommendations/RoofRecommendations.py +++ b/recommendations/RoofRecommendations.py @@ -1,4 +1,5 @@ import math +import pandas as pd from backend.Property import Property from typing import List from datatypes.enums import QuantityUnits @@ -6,6 +7,7 @@ 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 ) +from recommendations.Costs import Costs class RoofRecommendations: @@ -27,13 +29,17 @@ class RoofRecommendations: 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.materials = materials + self.loft_insulation_materials = [ + part for part in materials if part["type"] == "loft_insulation" + ] + self.loft_non_insulation_materials = [] def recommend(self): @@ -58,7 +64,7 @@ class RoofRecommendations: # If we have a u-value already, need to implement this if u_value: if u_value <= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE: - # The floor is already compliant + # The Roof is already compliant return if self.property.data["transaction-type"] == "new dwelling": @@ -66,6 +72,10 @@ class RoofRecommendations: raise NotImplementedError("Implement me") u_value = get_roof_u_value(**{**self.property.roof, "age_band": self.property.age_band}) + self.estimated_u_value = u_value + if u_value <= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE: + # The Roof is already compliant + return if self.property.roof["is_pitched"] or self.property.roof["is_flat"]: self.recommend_roof_insulation(u_value, insulation_thickness, self.property.roof) @@ -78,18 +88,21 @@ class RoofRecommendations: raise NotImplementedError("Implement me") @staticmethod - def make_loft_insulation_description(material, depth): - return f"Install {depth}{material['depth_unit']} of {material['description']} in your loft" + def make_loft_insulation_description(material): + return f"Install {int(material['depth'])}{material['depth_unit']} of {material['description']} in your loft" @staticmethod def make_room_roof_insulation_description(material, depth): return f"Insulate your room roof with {depth}{material['depth_unit']} of {material['description']}" @staticmethod - def make_flat_roof_insulation_description(material, depth): - return f"Insulate the home's flat roof with {depth}{material['depth_unit']} of {material['description']}" + def make_flat_roof_insulation_description(material): + return (f"Insulate the home's flat roof " + f"with {int(material['depth'])}{material['depth_unit']} of {material['description']}") - def recommend_roof_insulation(self, u_value, insulation_thickness, roof): + def recommend_roof_insulation( + self, u_value, insulation_thickness, roof + ): """ This method will recommend which insulation materials to use @@ -120,28 +133,31 @@ class RoofRecommendations: # from the base layer if roof["is_pitched"]: - materials = [m for m in self.materials if m["type"] == "loft_insulation"] + insulation_materials = self.loft_insulation_materials + non_insulation_materials = self.loft_non_insulation_materials elif roof["is_flat"]: - materials = [m for m in self.materials if m["type"] == "flat_roof_insulation"] + raise ValueError("UPDATE ME") else: raise ValueError("Roof is not pitched or flat") - if not materials: + if not insulation_materials: raise ValueError("No roof insulation materials found") + insulation_materials = pd.DataFrame(insulation_materials) + lowest_selected_u_value = None recommendations = [] - for material in materials: + for _, insulation_material_group in insulation_materials.groupby("description"): - for depth, cost_per_unit in zip(material["depths"], material["cost"]): + 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 ((depth + insulation_thickness) < self.MINIMUM_LOFT_ISULATION_MM) and roof["is_pitched"]: + if ((material["depth"] + insulation_thickness) < self.MINIMUM_LOFT_ISULATION_MM) and roof["is_pitched"]: continue - part_u_value = r_value_per_mm_to_u_value(depth, material["r_value_per_mm"]) + 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 @@ -161,23 +177,26 @@ class RoofRecommendations: 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) - # TODO: We should use the floor area divided by the number of floors to get the area of the roof - estimated_cost = cost_per_unit * self.property.floor_area - - if roof["is_pitched"]: - description = self.make_loft_insulation_description(material, depth) + if material["type"] == "loft_insulation": + cost_result = self.costs.loft_insulation( + floor_area=self.property.insulation_floor_area, + material=material + ) + description = self.make_loft_insulation_description(material) + elif material["type"] == "flat_roof_insulation": + description = self.make_flat_roof_insulation_description(material) + raise ValueError("COMPLETE ME") else: - description = self.make_flat_roof_insulation_description(material, depth) + raise ValueError("Invalid material type") recommendations.append( { "parts": [ get_recommended_part( - part=material, - selected_depth=depth, + part=material.to_dict(), quantity=self.property.insulation_wall_area, quantity_unit=QuantityUnits.m2.value, - selected_total_cost=estimated_cost + cost_result=cost_result ) ], "type": "roof_insulation", @@ -185,7 +204,7 @@ class RoofRecommendations: "starting_u_value": u_value, "new_u_value": new_u_value, "sap_points": None, - "cost": estimated_cost, + **cost_result } ) diff --git a/recommendations/VentilationRecommendations.py b/recommendations/VentilationRecommendations.py index a639905b..a0b188f7 100644 --- a/recommendations/VentilationRecommendations.py +++ b/recommendations/VentilationRecommendations.py @@ -65,6 +65,6 @@ class VentilationRecommendations(Definitions): "starting_u_value": None, "new_u_value": None, "sap_points": None, - "cost": estimated_cost, + "total": estimated_cost, } ] diff --git a/recommendations/WallRecommendations.py b/recommendations/WallRecommendations.py index 4595ef22..acc74ead 100644 --- a/recommendations/WallRecommendations.py +++ b/recommendations/WallRecommendations.py @@ -180,7 +180,7 @@ class WallRecommendations(Definitions): filled cavity wall """ - cavity_wall_fills = [m for m in self.materials if m["type"] == "cavity_wall_insulation"] + insulation_materials = pd.DataFrame(self.cavity_wall_insulation_materials) cavity_width = 75 if insulation_thickness == "below average": cavity_width = cavity_width * (1 - PARTIALLY_FILLED_PERCENTAGE_ASSUMPTION) @@ -188,8 +188,9 @@ class WallRecommendations(Definitions): # Test the different fill options lowest_selected_u_value = None recommendations = [] - for part in cavity_wall_fills: - part_u_value = r_value_per_mm_to_u_value(cavity_width, part["r_value_per_mm"]) + for _, material in insulation_materials.iterrows(): + + part_u_value = r_value_per_mm_to_u_value(cavity_width, 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 @@ -202,25 +203,27 @@ class WallRecommendations(Definitions): if new_u_value <= self.BUILDING_REGULATIONS_PART_L_CAVITY_WALL_MAX_U_VALUE: lowest_selected_u_value = update_lowest_selected_u_value(lowest_selected_u_value, new_u_value) - estimated_cost = part["cost"] * self.property.insulation_wall_area + cost_result = self.costs.cavity_wall_insulation( + wall_area=self.property.insulation_wall_area, + material=material.to_dict(), + ) recommendations.append( { "parts": [ get_recommended_part( - part=part, - selected_depth=None, + part=material.to_dict(), quantity=self.property.insulation_wall_area, quantity_unit=QuantityUnits.m2.value, - selected_total_cost=estimated_cost + cost_result=cost_result ) ], "type": "wall_insulation", - "description": f"Fill cavity with {part['description']}", + "description": f"Fill cavity with {material['description']}", "starting_u_value": u_value, "new_u_value": new_u_value, "sap_points": None, - "cost": estimated_cost, + **cost_result } )