diff --git a/.idea/Model.iml b/.idea/Model.iml
index ed9033de..4413bb06 100644
--- a/.idea/Model.iml
+++ b/.idea/Model.iml
@@ -7,7 +7,7 @@
-
+
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 3ab974fc..6f308057 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -3,7 +3,7 @@
-
+
diff --git a/backend/app/plan/router.py b/backend/app/plan/router.py
index 83a57d07..f110b27a 100644
--- a/backend/app/plan/router.py
+++ b/backend/app/plan/router.py
@@ -21,7 +21,7 @@ from backend.app.db.models.portfolio import rating_lookup
from backend.app.dependencies import validate_token
from backend.app.plan.schemas import PlanTriggerRequest
from backend.app.plan.utils import (
- create_recommendation_scoring_data, filter_materials, get_cleaned, insert_temp_recommendation_id
+ create_recommendation_scoring_data, prepare_materials, get_cleaned, insert_temp_recommendation_id
)
from backend.app.utils import epc_to_sap_lower_bound, read_csv_from_s3, read_parquet_from_s3
@@ -114,7 +114,7 @@ async def trigger_plan(body: PlanTriggerRequest):
# the same data
logger.info("Reading in materials and cleaned datasets")
materials = get_materials(session)
- materials_by_type = filter_materials(materials)
+ materials = prepare_materials(materials)
cleaned = get_cleaned()
logger.info("Getting components and epc recommendations")
@@ -126,13 +126,14 @@ async def trigger_plan(body: PlanTriggerRequest):
recommendations_scoring_data = []
for p in input_properties:
+
# Property recommendations
p.get_components(cleaned)
property_recommendations = []
# Floor recommendations
- floor_recommender = FloorRecommendations(property_instance=p, materials=materials_by_type["floor"])
+ floor_recommender = FloorRecommendations(property_instance=p, materials=materials)
floor_recommender.recommend()
if floor_recommender.recommendations:
diff --git a/backend/app/plan/utils.py b/backend/app/plan/utils.py
index e2bf9d86..e3723b24 100644
--- a/backend/app/plan/utils.py
+++ b/backend/app/plan/utils.py
@@ -1,6 +1,5 @@
import pandas as pd
from backend.Property import Property
-from collections import defaultdict
from utils.s3 import read_from_s3
from recommendations.recommendation_utils import get_wall_u_value, get_floor_u_value, get_roof_u_value
@@ -10,22 +9,13 @@ from backend.app.config import get_settings
import msgpack
-def filter_materials(materials):
- materials_by_type = defaultdict(list)
-
- mapping = {
- "walls": ["internal_wall_insulation", "external_wall_insulation", "cavity_wall_insulation"],
- "floor": ["suspended_floor_insulation", "solid_floor_insulation", "exposed_floor_insulation"],
- "ventilation": ["mechanical_ventilation"],
- "roof": ["loft_insulation", "flat_roof_insulation", "room_roof_insulation"]
- }
-
- materials = [row2dict(material) for material in materials]
-
- for component, types in mapping.items():
- materials_by_type[component] = [part for part in materials if part["type"] in types]
-
- return dict(materials_by_type)
+def prepare_materials(materials):
+ """
+ This function will prepare the materials for recommendations
+ :param materials: list of materials, as retrieved from the database
+ :return:
+ """
+ return [row2dict(material) for material in materials]
def insert_temp_recommendation_id(property_recommendations):
@@ -173,7 +163,7 @@ def create_recommendation_scoring_data(
parts = recommendation["parts"]
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_ENERGY_EFF_ENDING"] = "Very Good"
else:
diff --git a/etl/costs/app.py b/etl/costs/app.py
index 0117a66e..1ecbbb5f 100644
--- a/etl/costs/app.py
+++ b/etl/costs/app.py
@@ -1,12 +1,12 @@
import os
import dotenv
-import json
import pandas as pd
import numpy as np
from pathlib import Path
from sqlalchemy.orm import Session
from sqlalchemy import create_engine
from backend.app.db.models.materials import Material
+from recommendations.recommendation_utils import calculate_r_value_per_mm
DATA_DIRECTORY = Path(__file__).parent / "local_data" / "Hestia Materials.xlsx"
# Environment file is at the same level as this file
@@ -90,6 +90,14 @@ def app():
costs["depth"] = costs["depth"].fillna(0)
costs["depth"] = costs["depth"].astype(str)
+ costs["r_value_per_mm"] = costs.apply(
+ lambda row: calculate_r_value_per_mm(float(row["depth"]), row["thermal_conductivity"]), axis=1
+ )
+ costs["r_value_unit"] = "square_meter_kelvin_per_watt"
+
+ for col in ["material_cost", "labour_cost", "labour_hours_per_unit", "plant_cost"]:
+ costs[col] = costs[col].fillna(0)
+
# Push the costs to the database
push_costs_to_db(db_engine, costs)
diff --git a/recommendations/FloorRecommendations.py b/recommendations/FloorRecommendations.py
index 5b194e0d..641272a3 100644
--- a/recommendations/FloorRecommendations.py
+++ b/recommendations/FloorRecommendations.py
@@ -1,5 +1,8 @@
import math
from typing import List
+
+import pandas as pd
+
from BaseUtility import Definitions
from datatypes.enums import QuantityUnits
from backend.Property import Property
@@ -8,6 +11,7 @@ from recommendations.recommendation_utils import (
get_recommended_part, get_floor_u_value
)
from recommendations.rdsap_tables import FLOOR_LEVEL_MAP
+from recommendations.Costs import Costs
class FloorRecommendations(Definitions):
@@ -30,25 +34,41 @@ class FloorRecommendations(Definitions):
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.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.suspended_floor_insulation_materials = [
+ part for part in materials if part["type"] == "suspended_floor_insulation"
]
- self.exposed_floor_insulation_parts = [
- part for part in self.materials if part["type"] == "exposed_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"
+ ]
]
+ self.solid_floor_insulation_materials = [
+ part for part in materials if part["type"] == "solid_floor_insulation"
+ ]
+
+ 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"
+ ]
+ ]
+
+ self.exposed_floor_insulation_materials = [
+ part for part in materials if part["type"] == "exposed_floor_insulation"
+ ]
+
+ # TODO: To be completed
+ self.exposed_floor_non_insulation_materials = []
+
def recommend(self):
u_value = self.property.floor["thermal_transmittance"]
@@ -98,7 +118,11 @@ class FloorRecommendations(Definitions):
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)
+ self.recommend_floor_insulation(
+ 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"]:
@@ -113,20 +137,23 @@ class FloorRecommendations(Definitions):
raise NotImplementedError("Implement me!")
@staticmethod
- def _make_floor_description(part, depth):
- return f"Install {depth}{part['depth_unit']} {part['description']} insulation"
+ def _make_floor_description(material):
+ return f"Install {int(material['depth'])}{material['depth_unit']} {material['description']} insulation"
- def recommend_floor_insulation(self, u_value, parts):
+ def recommend_floor_insulation(self, u_value, insulation_materials, non_insulation_materials):
"""
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"]):
+ insulation_materials = pd.DataFrame(insulation_materials)
- part_u_value = r_value_per_mm_to_u_value(depth, part["r_value_per_mm"])
+ 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
@@ -137,26 +164,37 @@ class FloorRecommendations(Definitions):
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.insulation_floor_area
- estimated_cost = cost_per_unit * quantity
+ 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
+ )
+ 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
+ )
+ else:
+ raise NotImplementedError("Implement me!")
self.recommendations.append(
{
"parts": [
get_recommended_part(
- part=part,
- selected_depth=depth,
- quantity=quantity,
+ part=material.to_dict(),
+ quantity=self.property.insulation_floor_area,
quantity_unit=QuantityUnits.m2.value,
- selected_total_cost=estimated_cost
+ cost_result=cost_result
),
],
"type": "floor_insulation",
- "description": self._make_floor_description(part, depth),
+ "description": self._make_floor_description(material),
"starting_u_value": u_value,
"new_u_value": new_u_value,
"sap_points": None,
- "cost": estimated_cost,
+ **cost_result
}
)
diff --git a/recommendations/recommendation_utils.py b/recommendations/recommendation_utils.py
index 217f313f..5bd77a2a 100644
--- a/recommendations/recommendation_utils.py
+++ b/recommendations/recommendation_utils.py
@@ -109,22 +109,21 @@ def update_lowest_selected_u_value(lowest_selected_u_value, new_u_value):
return lowest_selected_u_value
-def get_recommended_part(part, selected_depth, selected_total_cost, quantity, quantity_unit):
+def get_recommended_part(part, cost_result, quantity, quantity_unit):
"""
Utility function to return a recommended part with the selected depth.
:param part: part to be recommended
- :param selected_depth: depth of the selected part
- :param selected_total_cost: Total cost of the selected part
+ :param cost_result: Total cost of the selected part, as returned by the Cost class
:param quantity: Quantity of the selected part
:param quantity_unit: Unit of the quantity
:return:
"""
recommended_part = deepcopy(part)
- recommended_part["depths"] = [selected_depth]
- recommended_part["estimated_cost"] = selected_total_cost
recommended_part["quantity"] = quantity
recommended_part["quantity_unit"] = quantity_unit
+ recommended_part.update(cost_result)
+
return recommended_part
@@ -563,6 +562,9 @@ def calculate_r_value_per_mm(thickness_mm, thermal_conductivity_w_mK):
:return:
"""
+ if thermal_conductivity_w_mK is None:
+ return None
+
r_value_m2k_w = (thickness_mm / 1000) / thermal_conductivity_w_mK
# Calculate R-value per mm