mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Merge pull request #259 from Hestia-Homes/integrate-new-models
Integrate new models
This commit is contained in:
commit
2174f9d283
41 changed files with 2772 additions and 1166 deletions
2
.idea/Model.iml
generated
2
.idea/Model.iml
generated
|
|
@ -7,7 +7,7 @@
|
|||
<sourceFolder url="file://$MODULE_DIR$/open_uprn" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/recommendations" isTestSource="false" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Python 3.10 (backend)" jdkType="Python SDK" />
|
||||
<orderEntry type="jdk" jdkName="Python 3.10 (model_data)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="PyNamespacePackagesService">
|
||||
|
|
|
|||
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
|
|
@ -3,7 +3,7 @@
|
|||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.10 (backend)" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (backend)" project-jdk-type="Python SDK" />
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (model_data)" project-jdk-type="Python SDK" />
|
||||
<component name="PythonCompatibilityInspectionAdvertiser">
|
||||
<option name="version" value="3" />
|
||||
</component>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import os
|
|||
import pandas as pd
|
||||
|
||||
from etl.epc.DataProcessor import DataProcessor
|
||||
from etl.epc.settings import POTENTIAL_COLUMNS, EFFICIENCY_FEATURES
|
||||
from etl.epc.settings import POTENTIAL_COLUMNS, EFFICIENCY_FEATURES, BUILT_FORM_REMAP
|
||||
from etl.epc_clean.epc_attributes.all_cleaners import all_cleaner_map
|
||||
from utils.logger import setup_logger
|
||||
from utils.s3 import read_dataframe_from_s3_parquet
|
||||
|
|
@ -45,7 +45,7 @@ class Property(Definitions):
|
|||
windows = None
|
||||
lighting = None
|
||||
|
||||
coordinates = None
|
||||
spatial = None
|
||||
|
||||
def __init__(self, id, postcode, address1, epc_client=None, data=None):
|
||||
self.id = id
|
||||
|
|
@ -83,6 +83,10 @@ class Property(Definitions):
|
|||
self.floor_area = None
|
||||
self.pitched_roof_area = None
|
||||
self.insulation_floor_area = None
|
||||
self.number_lighting_outlets = None
|
||||
|
||||
self.current_adjusted_energy = None
|
||||
self.expected_adjusted_energy = None
|
||||
|
||||
if epc_client:
|
||||
self.epc_client = epc_client
|
||||
|
|
@ -125,13 +129,6 @@ class Property(Definitions):
|
|||
else:
|
||||
self.uprn = int(self.data["uprn"])
|
||||
|
||||
def set_coordinates(self, coordinates):
|
||||
"""
|
||||
This method sets the coordinates of the property, given the open uprn data
|
||||
:param coordinates: dictionary
|
||||
"""
|
||||
self.coordinates = {key.lower(): value for key, value in coordinates.items()}
|
||||
|
||||
def set_energy(self):
|
||||
"""
|
||||
Extracts and formats data about the home's energy and co2 consumption
|
||||
|
|
@ -274,6 +271,9 @@ class Property(Definitions):
|
|||
if not self.data:
|
||||
raise ValueError("Property does not contain data")
|
||||
|
||||
# We need to implement an EPC cleaning process, which we run on the EPC data, immediately after we download
|
||||
# it
|
||||
self.data["built-form"] = BUILT_FORM_REMAP.get(self.data["built-form"], self.data["built-form"])
|
||||
self.set_energy()
|
||||
self.set_ventilation()
|
||||
self.set_solar_pv()
|
||||
|
|
@ -360,6 +360,9 @@ class Property(Definitions):
|
|||
def set_spatial(self, spatial: pd.DataFrame):
|
||||
"""
|
||||
Sets whether the property is in a conservation area given the output of the ConservationAreaClient
|
||||
|
||||
Will store a dictionary, spatial, which is used to populate the property spatial table in the database
|
||||
|
||||
:param spatial: Dataframe, containing the spatial data for the property
|
||||
"""
|
||||
self.in_conservation_area = spatial["conservation_status"].values[0]
|
||||
|
|
@ -369,6 +372,17 @@ class Property(Definitions):
|
|||
if self.in_conservation_area is True | self.is_listed is True | self.is_heritage is True:
|
||||
self.restricted_measures = True
|
||||
|
||||
spatial_dict = spatial.to_dict("records")[0]
|
||||
self.spatial = {
|
||||
"x_coordinate": spatial_dict["X_COORDINATE"],
|
||||
"y_coordinate": spatial_dict["Y_COORDINATE"],
|
||||
"latitude": spatial_dict["LATITUDE"],
|
||||
"longitude": spatial_dict["LONGITUDE"],
|
||||
"conservation_status": spatial_dict["conservation_status"],
|
||||
"is_listed_building": spatial_dict["is_listed_building"],
|
||||
"is_heritage_building": spatial_dict["is_heritage_building"],
|
||||
}
|
||||
|
||||
def set_year_built(self):
|
||||
"""
|
||||
Estimates when the property was built based on as much available data as possible.
|
||||
|
|
@ -461,7 +475,7 @@ class Property(Definitions):
|
|||
"year_built": self.year_built,
|
||||
"tenure": self.data["tenure"],
|
||||
"current_epc_rating": self.data["current-energy-rating"],
|
||||
"current_sap_points": self.data["current-energy-efficiency"]
|
||||
"current_sap_points": self.data["current-energy-efficiency"],
|
||||
}
|
||||
|
||||
property_data = self._clean_upload_data(property_data)
|
||||
|
|
@ -513,6 +527,7 @@ class Property(Definitions):
|
|||
"energy_tariff": self.data["energy-tariff"],
|
||||
"primary_energy_consumption": self.energy["primary_energy_consumption"],
|
||||
"co2_emissions": self.energy["co2_emissions"],
|
||||
"adjusted_energy_consumption": self.current_adjusted_energy,
|
||||
}
|
||||
|
||||
return property_details_epc
|
||||
|
|
@ -703,7 +718,6 @@ class Property(Definitions):
|
|||
'PROPERTY_TYPE',
|
||||
'UPRN',
|
||||
'NUMBER_OPEN_FIREPLACES',
|
||||
'FIXED_LIGHTING_OUTLETS_COUNT',
|
||||
'MULTI_GLAZE_PROPORTION',
|
||||
'MECHANICAL_VENTILATION',
|
||||
'PHOTO_SUPPLY',
|
||||
|
|
@ -752,9 +766,28 @@ class Property(Definitions):
|
|||
"FLOOR_HEIGHT": self.floor_height,
|
||||
"NUMBER_HABITABLE_ROOMS": self.number_of_rooms,
|
||||
"TOTAL_FLOOR_AREA": self.floor_area,
|
||||
"FIXED_LIGHTING_OUTLETS_COUNT": self.number_lighting_outlets,
|
||||
**epc_raw_data,
|
||||
"BUILT_FORM": built_form,
|
||||
"POSTCODE": self.data["postcode"],
|
||||
}
|
||||
|
||||
return property_data
|
||||
|
||||
def set_number_lighting_outlets(self, cleaned_property_data):
|
||||
"""
|
||||
Extracts and cleans the estimated number of lighting outlets
|
||||
:return:
|
||||
"""
|
||||
|
||||
if self.data["fixed-lighting-outlets-count"] == "":
|
||||
self.number_lighting_outlets = round(cleaned_property_data["FIXED_LIGHTING_OUTLETS_COUNT"].values[0])
|
||||
else:
|
||||
self.number_lighting_outlets = float(self.data["fixed-lighting-outlets-count"])
|
||||
|
||||
def set_adjusted_energy(self, current_adjusted_energy, expected_adjusted_energy):
|
||||
"""
|
||||
Stores these values for usage later
|
||||
"""
|
||||
self.current_adjusted_energy = current_adjusted_energy
|
||||
self.expected_adjusted_energy = expected_adjusted_energy
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@ class Settings(BaseSettings):
|
|||
SECRET_KEY: str
|
||||
ENVIRONMENT: str
|
||||
DATA_BUCKET: str
|
||||
PREDICTIONS_BUCKET: str
|
||||
SAP_PREDICTIONS_BUCKET: str
|
||||
CARBON_PREDICTIONS_BUCKET: str
|
||||
HEAT_PREDICTIONS_BUCKET: str
|
||||
PLAN_TRIGGER_BUCKET: str
|
||||
EPC_AUTH_TOKEN: str
|
||||
DB_HOST: str
|
||||
|
|
|
|||
|
|
@ -3,15 +3,17 @@ from backend.app.db.models.recommendations import Plan, PlanRecommendations, Rec
|
|||
from backend.app.db.models.portfolio import Portfolio
|
||||
|
||||
|
||||
def aggregate_portfolio_recommendations(session, portfolio_id: int):
|
||||
def aggregate_portfolio_recommendations(
|
||||
session, portfolio_id: int, total_valuation_increase: float, labour_days: float
|
||||
):
|
||||
# Aggregate multiple fields
|
||||
aggregates = (
|
||||
session.query(
|
||||
func.sum(Recommendation.estimated_cost).label("cost"),
|
||||
func.sum(Recommendation.total_work_hours).label("total_work_hours"),
|
||||
# For future usage we will aggregate multiple fields in this step
|
||||
# func.sum(Recommendation.heat_demand).label("total_heat_demand"),
|
||||
# func.sum(Recommendation.energy_savings).label("total_energy_savings")
|
||||
func.sum(Recommendation.heat_demand).label("energy_savings"),
|
||||
func.sum(Recommendation.co2_equivalent_savings).label("co2_equivalent_savings"),
|
||||
func.sum(Recommendation.energy_cost_savings).label("energy_cost_savings"),
|
||||
)
|
||||
.join(PlanRecommendations, PlanRecommendations.recommendation_id == Recommendation.id)
|
||||
.join(Plan, Plan.id == PlanRecommendations.plan_id)
|
||||
|
|
@ -22,8 +24,9 @@ def aggregate_portfolio_recommendations(session, portfolio_id: int):
|
|||
aggregates_dict = {
|
||||
"cost": aggregates.cost or 0,
|
||||
"total_work_hours": aggregates.total_work_hours or 0,
|
||||
# "total_heat_demand": aggregates.total_heat_demand or 0,
|
||||
# "total_energy_savings": aggregates.total_energy_savings or 0
|
||||
"energy_savings": aggregates.energy_savings or 0,
|
||||
"co2_equivalent_savings": aggregates.co2_equivalent_savings or 0,
|
||||
"energy_cost_savings": aggregates.energy_cost_savings or 0,
|
||||
}
|
||||
|
||||
# Get the portfolio and update the fields
|
||||
|
|
@ -32,6 +35,10 @@ def aggregate_portfolio_recommendations(session, portfolio_id: int):
|
|||
for key, value in aggregates_dict.items():
|
||||
setattr(portfolio, key, value)
|
||||
|
||||
# Insert total valuation increase and labour days
|
||||
portfolio.property_valuation_increase = total_valuation_increase
|
||||
portfolio.labour_days = labour_days
|
||||
|
||||
# Merge the updated portfolio back into the session
|
||||
session.merge(portfolio)
|
||||
session.flush()
|
||||
|
|
|
|||
|
|
@ -3,13 +3,15 @@
|
|||
###
|
||||
import datetime
|
||||
import pytz
|
||||
from sqlalchemy.orm import Session
|
||||
from backend.app.db.models.portfolio import (
|
||||
PropertyModel, PropertyCreationStatus, PortfolioStatus, PropertyTargetsModel, PropertyDetailsEpcModel
|
||||
PropertyModel, PropertyCreationStatus, PortfolioStatus, PropertyTargetsModel, PropertyDetailsEpcModel,
|
||||
PropertyDetailsSpatial
|
||||
)
|
||||
from sqlalchemy.orm.exc import NoResultFound
|
||||
|
||||
|
||||
def create_property(session, portfolio_id: int, address: str, postcode: str) -> (int, bool):
|
||||
def create_property(session: Session, portfolio_id: int, address: str, postcode: str) -> (int, bool):
|
||||
"""
|
||||
This function will create a record for the property in the database if it does not exist.
|
||||
If it does exist, it will just update the updated_at field.
|
||||
|
|
@ -55,7 +57,9 @@ def create_property(session, portfolio_id: int, address: str, postcode: str) ->
|
|||
return new_property.id, True
|
||||
|
||||
|
||||
def create_property_targets(session, property_id: int, portfolio_id: int, epc_target=None, heat_demand_target=None):
|
||||
def create_property_targets(
|
||||
session: Session, property_id: int, portfolio_id: int, epc_target=None, heat_demand_target=None
|
||||
):
|
||||
"""
|
||||
This function will create a record for the property targets in the database if it does not exist.
|
||||
:param session: The database session
|
||||
|
|
@ -78,7 +82,9 @@ def create_property_targets(session, property_id: int, portfolio_id: int, epc_ta
|
|||
return True
|
||||
|
||||
|
||||
def update_property_data(session, property_id: int, portfolio_id: int, property_data: dict):
|
||||
def update_property_data(
|
||||
session: Session, property_id: int, portfolio_id: int, property_data: dict
|
||||
):
|
||||
now = datetime.datetime.now(pytz.utc)
|
||||
|
||||
try:
|
||||
|
|
@ -103,7 +109,9 @@ def update_property_data(session, property_id: int, portfolio_id: int, property_
|
|||
return True
|
||||
|
||||
|
||||
def create_property_details_epc(session, property_details_epc: dict):
|
||||
def create_property_details_epc(
|
||||
session: Session, property_details_epc: dict
|
||||
):
|
||||
"""
|
||||
This function will create or update a record for the property details EPC in the database.
|
||||
:param session: The database session
|
||||
|
|
@ -128,3 +136,36 @@ def create_property_details_epc(session, property_details_epc: dict):
|
|||
session.flush()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def update_or_create_property_spatial_details(session: Session, uprn: int, property_details_spatial: dict):
|
||||
"""
|
||||
Update an existing property details record or create a new one based on the UPRN.
|
||||
|
||||
:param session: The SQLAlchemy session for database interaction.
|
||||
:param uprn: The unique property reference number (UPRN) of the property.
|
||||
:param property_details_spatial: A dictionary containing the spatial property details to store or update.
|
||||
:return: True if the operation is successful, otherwise raises an exception.
|
||||
"""
|
||||
|
||||
try:
|
||||
# Attempt to fetch the existing property details
|
||||
existing_property_details = session.query(PropertyDetailsSpatial).filter_by(
|
||||
uprn=uprn
|
||||
).one()
|
||||
|
||||
# Update the fields with the data in property_details
|
||||
for key, value in property_details_spatial.items():
|
||||
setattr(existing_property_details, key, value)
|
||||
|
||||
# Merge the updated property details back into the session and flush
|
||||
session.merge(existing_property_details)
|
||||
session.flush()
|
||||
|
||||
except NoResultFound:
|
||||
# Create a new record if not found
|
||||
new_property_details = PropertyDetailsSpatial(uprn=uprn, **property_details_spatial)
|
||||
session.add(new_property_details)
|
||||
session.flush()
|
||||
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -80,7 +80,11 @@ def upload_recommendations(session: Session, recommendations_to_upload, property
|
|||
"starting_u_value": rec.get("starting_u_value"),
|
||||
"new_u_value": rec.get("new_u_value"),
|
||||
"sap_points": rec["sap_points"],
|
||||
"heat_demand": rec["heat_demand"],
|
||||
"co2_equivalent_savings": rec["co2_equivalent_savings"],
|
||||
"total_work_hours": rec["labour_hours"],
|
||||
"energy_cost_savings": rec["energy_cost_savings"],
|
||||
"labour_days": rec["labour_days"]
|
||||
}
|
||||
for rec in recommendations_to_upload
|
||||
]
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ class MaterialType(enum.Enum):
|
|||
ewi_wall_demolition = "ewi_wall_demolition"
|
||||
ewi_wall_preparation = "ewi_wall_preparation"
|
||||
ewi_wall_redecoration = "ewi_wall_redecoration"
|
||||
low_energy_lighting_installation = "low_energy_lighting_installation"
|
||||
|
||||
|
||||
class DepthUnit(enum.Enum):
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ class Portfolio(Base):
|
|||
property_valuation_increase = Column(Float) # Unit is always £ so we don't need to store the unit for the moment
|
||||
rental_yield_increase = Column(Float) # Unit is always £ so we don't need to store the unit for the moment
|
||||
total_work_hours = Column(Float)
|
||||
labour_days = Column(Float)
|
||||
created_at = Column(DateTime, nullable=False, default=datetime.datetime.now(pytz.utc))
|
||||
updated_at = Column(DateTime, nullable=False, default=datetime.datetime.now(pytz.utc))
|
||||
|
||||
|
|
@ -151,6 +152,20 @@ class PropertyDetailsEpcModel(Base):
|
|||
energy_tariff = Column(Text)
|
||||
primary_energy_consumption = Column(Float)
|
||||
co2_emissions = Column(Float)
|
||||
adjusted_energy_consumption = Column(Float)
|
||||
|
||||
|
||||
class PropertyDetailsSpatial(Base):
|
||||
__tablename__ = "property_details_spatial"
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
uprn = Column(Integer, nullable=False)
|
||||
x_coordinate = Column(Float)
|
||||
y_coordinate = Column(Float)
|
||||
latitude = Column(Float)
|
||||
longitude = Column(Float)
|
||||
conservation_status = Column(Boolean)
|
||||
is_listed_building = Column(Boolean)
|
||||
is_heritage_building = Column(Boolean)
|
||||
|
||||
|
||||
class PropertyDetailsMeter(Base):
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ class Recommendation(Base):
|
|||
property_valuation_increase = Column(Float)
|
||||
rental_yield_increase = Column(Float)
|
||||
total_work_hours = Column(Float)
|
||||
labour_days = Column(Float)
|
||||
|
||||
|
||||
class RecommendationMaterials(Base):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from datetime import datetime
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from epc_api.client import EpcClient
|
||||
from fastapi import APIRouter, Depends
|
||||
|
|
@ -12,7 +13,8 @@ from backend.app.db.connection import db_engine
|
|||
from backend.app.db.functions.materials_functions import get_materials
|
||||
from backend.app.db.functions.portfolio_functions import aggregate_portfolio_recommendations
|
||||
from backend.app.db.functions.property_functions import (
|
||||
create_property, create_property_details_epc, create_property_targets, update_property_data
|
||||
create_property, create_property_details_epc, create_property_targets, update_property_data,
|
||||
update_or_create_property_spatial_details
|
||||
)
|
||||
from backend.app.db.functions.recommendations_functions import (
|
||||
create_plan, create_plan_recommendations, upload_recommendations
|
||||
|
|
@ -20,25 +22,21 @@ from backend.app.db.functions.recommendations_functions import (
|
|||
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, get_cleaned, insert_temp_recommendation_id
|
||||
)
|
||||
from backend.app.utils import epc_to_sap_lower_bound, read_csv_from_s3, read_parquet_from_s3
|
||||
from backend.app.plan.utils import create_recommendation_scoring_data, get_cleaned
|
||||
from backend.app.utils import epc_to_sap_lower_bound, read_csv_from_s3, read_parquet_from_s3, sap_to_epc
|
||||
|
||||
from backend.ml_models.sap_change_model.api import SAPChangeModelAPI
|
||||
from backend.ml_models.api import ModelApi
|
||||
from backend.Property import Property
|
||||
from etl.epc.DataProcessor import DataProcessor
|
||||
from etl.epc.settings import COLUMNS_TO_MERGE_ON
|
||||
from recommendations.FloorRecommendations import FloorRecommendations
|
||||
from recommendations.RoofRecommendations import RoofRecommendations
|
||||
from recommendations.VentilationRecommendations import VentilationRecommendations
|
||||
from recommendations.FireplaceRecommendations import FireplaceRecommendations
|
||||
from recommendations.optimiser.CostOptimiser import CostOptimiser
|
||||
from recommendations.optimiser.GainOptimiser import GainOptimiser
|
||||
from recommendations.optimiser.optimiser_functions import prepare_input_measures
|
||||
from recommendations.WallRecommendations import WallRecommendations
|
||||
from recommendations.Recommendations import Recommendations
|
||||
from utils.logger import setup_logger
|
||||
from utils.s3 import read_dataframe_from_s3_parquet
|
||||
from backend.ml_models.Valuation import PropertyValuation
|
||||
from backend.ml_models.AnnualBillSavings import AnnualBillSavings
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
|
|
@ -83,6 +81,7 @@ async def trigger_plan(body: PlanTriggerRequest):
|
|||
if not is_new:
|
||||
continue
|
||||
# TODO: Need to add heat demand target
|
||||
|
||||
create_property_targets(
|
||||
session,
|
||||
property_id=property_id,
|
||||
|
|
@ -123,55 +122,23 @@ async def trigger_plan(body: PlanTriggerRequest):
|
|||
|
||||
recommendations = {}
|
||||
recommendations_scoring_data = []
|
||||
property_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)
|
||||
floor_recommender.recommend()
|
||||
|
||||
if floor_recommender.recommendations:
|
||||
property_recommendations.append(floor_recommender.recommendations)
|
||||
|
||||
# Wall recommendations
|
||||
|
||||
wall_recomender = WallRecommendations(property_instance=p, materials=materials)
|
||||
wall_recomender.recommend()
|
||||
|
||||
if wall_recomender.recommendations:
|
||||
property_recommendations.append(wall_recomender.recommendations)
|
||||
|
||||
# Roof recommendations
|
||||
roof_recommender = RoofRecommendations(property_instance=p, materials=materials)
|
||||
roof_recommender.recommend()
|
||||
|
||||
if roof_recommender.recommendations:
|
||||
property_recommendations.append(roof_recommender.recommendations)
|
||||
|
||||
# Ventilation recommendations
|
||||
ventilation_recomender = VentilationRecommendations(
|
||||
property_instance=p,
|
||||
materials=[part for part in materials if part["type"] == "mechanical_ventilation"]
|
||||
# This is temp - this should happen after scoring
|
||||
cleaned_property_data = DataProcessor.apply_averages_cleaning(
|
||||
data_to_clean=pd.DataFrame([dict(**p.get_model_data(), LOCAL_AUTHORITY=p.data["local-authority"])]),
|
||||
cleaning_data=cleaning_data,
|
||||
cols_to_merge_on=['PROPERTY_TYPE', 'BUILT_FORM', 'CONSTRUCTION_AGE_BAND', 'LOCAL_AUTHORITY'],
|
||||
)
|
||||
ventilation_recomender.recommend()
|
||||
p.set_number_lighting_outlets(cleaned_property_data)
|
||||
|
||||
if ventilation_recomender.recommendation:
|
||||
property_recommendations.append(ventilation_recomender.recommendation)
|
||||
|
||||
# Fireplace sealing recommendations
|
||||
fireplace_recommender = FireplaceRecommendations(property_instance=p)
|
||||
fireplace_recommender.recommend()
|
||||
|
||||
if fireplace_recommender.recommendation:
|
||||
property_recommendations.append(fireplace_recommender.recommendation)
|
||||
|
||||
# We insert temporary ids into the recommendations which is important for the optimiser later
|
||||
property_recommendations = insert_temp_recommendation_id(property_recommendations)
|
||||
recommender = Recommendations(property_instance=p, materials=materials)
|
||||
property_recommendations = recommender.recommend()
|
||||
|
||||
if not property_recommendations:
|
||||
continue
|
||||
|
|
@ -194,6 +161,12 @@ async def trigger_plan(body: PlanTriggerRequest):
|
|||
# We update the ending record with the recommended updates and we set lodgement date to today
|
||||
ending_epc_data["DAYS_TO_ENDING"] = data_processor.calculate_days_to(created_at)
|
||||
|
||||
property_scoring_data[p.id] = {
|
||||
"starting_epc_data": starting_epc_data,
|
||||
"ending_epc_data": ending_epc_data,
|
||||
"fixed_data": fixed_data
|
||||
}
|
||||
|
||||
for recommendations_by_type in property_recommendations:
|
||||
for i, rec in enumerate(recommendations_by_type):
|
||||
scoring_dict = create_recommendation_scoring_data(
|
||||
|
|
@ -234,55 +207,42 @@ async def trigger_plan(body: PlanTriggerRequest):
|
|||
|
||||
recommendations_scoring_data = DataProcessor.clean_efficiency_variables(recommendations_scoring_data)
|
||||
|
||||
sap_change_model_api = SAPChangeModelAPI(portfolio_id=body.portfolio_id, timestamp=created_at)
|
||||
file_location = sap_change_model_api.upload_scoring_data(
|
||||
df=recommendations_scoring_data, bucket=get_settings().DATA_BUCKET
|
||||
model_api = ModelApi(portfolio_id=body.portfolio_id, timestamp=created_at)
|
||||
all_predictions = model_api.predict_all(
|
||||
df=recommendations_scoring_data,
|
||||
bucket=get_settings().DATA_BUCKET,
|
||||
prediction_buckets={
|
||||
"sap_change_predictions": get_settings().SAP_PREDICTIONS_BUCKET,
|
||||
"heat_demand_predictions": get_settings().HEAT_PREDICTIONS_BUCKET,
|
||||
"carbon_change_predictions": get_settings().CARBON_PREDICTIONS_BUCKET
|
||||
}
|
||||
)
|
||||
response = sap_change_model_api.predict(
|
||||
file_location="s3://{DATA_BUCKET}/".format(DATA_BUCKET=get_settings().DATA_BUCKET) + file_location,
|
||||
)
|
||||
|
||||
# Retrieve the predictions
|
||||
predictions = pd.DataFrame(
|
||||
read_parquet_from_s3(
|
||||
bucket_name=get_settings().PREDICTIONS_BUCKET,
|
||||
file_key=response["storage_filepath"].split(get_settings().PREDICTIONS_BUCKET + "/")[1]
|
||||
)
|
||||
)
|
||||
|
||||
predictions["predictions"] = predictions["predictions"].astype(float).round(1)
|
||||
predictions[['property_id', 'recommendation_id']] = predictions['id'].str.split('+', expand=True)
|
||||
|
||||
# Insert the predictions into the recommendations and run the optimiser
|
||||
logger.info("Optimising recommendations")
|
||||
for property_id in recommendations.keys():
|
||||
|
||||
property = [p for p in input_properties if p.id == property_id][0]
|
||||
property_predictions = predictions[predictions["property_id"] == str(property_id)]
|
||||
property_instance = [p for p in input_properties if p.id == property_id][0]
|
||||
|
||||
for recommendations_by_type in recommendations[property_id]:
|
||||
for rec in recommendations_by_type:
|
||||
new_sap = property_predictions[property_predictions["recommendation_id"] == str(
|
||||
rec["recommendation_id"]
|
||||
)]["predictions"].values[0]
|
||||
recommendations_with_impact = Recommendations.calculate_recommendation_impact(
|
||||
property_instance=property_instance,
|
||||
all_predictions=all_predictions,
|
||||
recommendations=recommendations
|
||||
)
|
||||
|
||||
rec["sap_points"] = new_sap - float(property.data["current-energy-efficiency"])
|
||||
|
||||
if rec["sap_points"] is None:
|
||||
raise ValueError("Sap points missing")
|
||||
|
||||
input_measures = prepare_input_measures(recommendations[property_id], body.goal)
|
||||
input_measures = prepare_input_measures(recommendations_with_impact, body.goal)
|
||||
|
||||
if body.budget:
|
||||
optimiser = GainOptimiser(input_measures, max_cost=body.budget)
|
||||
else:
|
||||
# The minimum gain is the minimum number of SAP points required to get to the target SAP band
|
||||
current_sap_points = int(property.data["current-energy-efficiency"])
|
||||
current_sap_points = int(property_instance.data["current-energy-efficiency"])
|
||||
target_sap_points = epc_to_sap_lower_bound(body.goal_value)
|
||||
|
||||
# If the gain is negative, the optimiser will return an empty solution
|
||||
optimiser = CostOptimiser(
|
||||
input_measures, min_gain=target_sap_points - current_sap_points
|
||||
input_measures,
|
||||
min_gain=CostOptimiser.calculate_sap_gain_with_slack(target_sap_points - current_sap_points)
|
||||
)
|
||||
|
||||
optimiser.setup()
|
||||
|
|
@ -291,13 +251,28 @@ async def trigger_plan(body: PlanTriggerRequest):
|
|||
|
||||
selected_recommendations = {r["id"] for r in solution}
|
||||
|
||||
# If wall ventilation is selected, we also include mechanical ventilation as a best practice measure
|
||||
if any(x in [r["type"] for r in solution] for x in [
|
||||
"internal_wall_insulation", "external_wall_insulation", "cavity_wall_insulation"
|
||||
]):
|
||||
ventilation_rec = [
|
||||
r for r in recommendations_with_impact if r[0]["type"] == "mechanical_ventilation"
|
||||
][0]
|
||||
|
||||
selected_recommendations = set(
|
||||
list(selected_recommendations) + [ventilation_rec[0]["recommendation_id"]]
|
||||
)
|
||||
|
||||
# We check if the selected recommendation is wall ventilation and if so, we make sure
|
||||
# mechanical ventilation is selected
|
||||
|
||||
# We'll use the set of selected recommendations to filter the recommendations to upload
|
||||
final_recommendations = [
|
||||
[
|
||||
{**rec, "default": True if rec["recommendation_id"] in selected_recommendations else False}
|
||||
for rec in recommendations_by_type
|
||||
]
|
||||
for recommendations_by_type in recommendations[property_id]
|
||||
for recommendations_by_type in recommendations_with_impact
|
||||
]
|
||||
|
||||
# We'll also unlist the recommendations so they're a bit easier to handle from here onwards
|
||||
|
|
@ -306,11 +281,209 @@ async def trigger_plan(body: PlanTriggerRequest):
|
|||
]
|
||||
recommendations[property_id] = final_recommendations
|
||||
|
||||
# This is a temporary step, to estimate the impact of the measured on heat demand and carbon
|
||||
# TODO: This needs to be cleaned up, if it happens to be kept
|
||||
combined_recommendations_scoring_data = []
|
||||
representative_recs = {}
|
||||
for property_id, property_recommendations in recommendations.items():
|
||||
default_recommendations = [r for r in property_recommendations if r["default"]]
|
||||
default_types = {x["type"] for x in default_recommendations}
|
||||
|
||||
# Missing types
|
||||
missing_types = list(set([r["type"] for r in property_recommendations if r["type"] not in default_types]))
|
||||
# We might have a missing type as one of the solid wall options because for a solid wall, you might
|
||||
# have ewi or iwi but only one of them will be a default
|
||||
if ("internal_wall_insulation" in default_types) or ("external_wall_insaultion" in default_types):
|
||||
missing_types = [
|
||||
t for t in missing_types if t not in ["internal_wall_insulation", "external_wall_insulation"]
|
||||
]
|
||||
|
||||
if missing_types:
|
||||
for missed_type in missing_types:
|
||||
missed = [r for r in property_recommendations if r["type"] == missed_type]
|
||||
min_cost = min([r["total"] for r in missed])
|
||||
# Grab a representative, based on cheapest cost
|
||||
|
||||
representative_rec = [r for r in property_recommendations if np.isclose(r["total"], min_cost)]
|
||||
default_recommendations.append(representative_rec[0])
|
||||
|
||||
representative_recs[property_id] = default_recommendations
|
||||
|
||||
property_instance = [p for p in input_properties if p.id == property_id][0]
|
||||
|
||||
property_scoring_datasets = property_scoring_data[property_id]
|
||||
starting_epc_data = property_scoring_datasets["starting_epc_data"].copy()
|
||||
ending_epc_data = property_scoring_datasets["ending_epc_data"].copy()
|
||||
fixed_data = property_scoring_datasets["fixed_data"].copy()
|
||||
|
||||
scoring_dict = {}
|
||||
for rec in default_recommendations:
|
||||
scoring_dict = create_recommendation_scoring_data(
|
||||
property=property_instance,
|
||||
recommendation=rec,
|
||||
starting_epc_data=starting_epc_data,
|
||||
ending_epc_data=ending_epc_data,
|
||||
fixed_data=fixed_data,
|
||||
)
|
||||
# At each iteration, we want to update the ending_epc_data, so in the end, ending_epc_data contains
|
||||
# all of the updates
|
||||
for k in scoring_dict.keys():
|
||||
if k in ending_epc_data.columns:
|
||||
ending_epc_data[k] = scoring_dict[k]
|
||||
|
||||
combined_recommendations_scoring_data.append(scoring_dict)
|
||||
|
||||
# PERFORM SAME STEPS AGAIN - TODO: TO BE REMOVED
|
||||
combined_recommendations_scoring_data = pd.DataFrame(combined_recommendations_scoring_data)
|
||||
|
||||
# Perform the same cleaning as in the model - first clean number of room variables though
|
||||
combined_recommendations_scoring_data = DataProcessor.apply_averages_cleaning(
|
||||
data_to_clean=combined_recommendations_scoring_data,
|
||||
cleaning_data=cleaning_data,
|
||||
cols_to_merge_on=['PROPERTY_TYPE', 'BUILT_FORM', 'CONSTRUCTION_AGE_BAND', 'LOCAL_AUTHORITY'],
|
||||
colnames=["NUMBER_HABITABLE_ROOMS", "NUMBER_HEATED_ROOMS"],
|
||||
)
|
||||
|
||||
combined_recommendations_scoring_data = DataProcessor.apply_averages_cleaning(
|
||||
data_to_clean=combined_recommendations_scoring_data,
|
||||
cleaning_data=cleaning_data,
|
||||
cols_to_merge_on=COLUMNS_TO_MERGE_ON + ["LOCAL_AUTHORITY"],
|
||||
).drop(columns=["LOCAL_AUTHORITY"])
|
||||
|
||||
combined_recommendations_scoring_data = DataProcessor.clean_missings_after_description_process(
|
||||
combined_recommendations_scoring_data,
|
||||
ignore_cols=[
|
||||
c for c in combined_recommendations_scoring_data.columns if ("thermal_transmittance" in c) or (
|
||||
"insulation_thickness" in c) or ("ENERGY_EFF" in c)
|
||||
]
|
||||
)
|
||||
|
||||
combined_recommendations_scoring_data = DataProcessor.clean_efficiency_variables(
|
||||
combined_recommendations_scoring_data
|
||||
)
|
||||
|
||||
model_api = ModelApi(portfolio_id=body.portfolio_id, timestamp=created_at)
|
||||
all_combined_predictions = model_api.predict_all(
|
||||
df=combined_recommendations_scoring_data,
|
||||
bucket=get_settings().DATA_BUCKET,
|
||||
prediction_buckets={
|
||||
"sap_change_predictions": get_settings().SAP_PREDICTIONS_BUCKET,
|
||||
"heat_demand_predictions": get_settings().HEAT_PREDICTIONS_BUCKET,
|
||||
"carbon_change_predictions": get_settings().CARBON_PREDICTIONS_BUCKET
|
||||
}
|
||||
)
|
||||
|
||||
# We update the carbon and heat demand predictions
|
||||
for property_id, property_recommendations in recommendations.items():
|
||||
combined_heat_demand = all_combined_predictions["heat_demand_predictions"]
|
||||
combined_heat_demand = combined_heat_demand[combined_heat_demand["property_id"] == str(property_id)]
|
||||
|
||||
combined_carbon = all_combined_predictions["carbon_change_predictions"]
|
||||
combined_carbon = combined_carbon[combined_carbon["property_id"] == str(property_id)]
|
||||
|
||||
property_instance = [p for p in input_properties if p.id == property_id][0]
|
||||
|
||||
carbon_change = float(
|
||||
property_instance.data["co2-emissions-current"]
|
||||
) - combined_carbon["predictions"].values[0]
|
||||
|
||||
starting_heat_demand = (
|
||||
float(property_instance.data["energy-consumption-current"]) * property_instance.floor_area
|
||||
)
|
||||
expected_heat_demand = starting_heat_demand - (
|
||||
combined_heat_demand["predictions"].values[0] * property_instance.floor_area
|
||||
)
|
||||
|
||||
# We don't want to adjust the heat demand for mechanical ventilation so we add it back on
|
||||
|
||||
# We adjust the heat demand figures to align to the UCL paper
|
||||
current_adjusted_energy = AnnualBillSavings.adjust_energy_to_metered(
|
||||
epc_energy_consumption=starting_heat_demand,
|
||||
current_epc_rating=property_instance.data["current-energy-rating"],
|
||||
)
|
||||
|
||||
# We sum up the SAP points of the default recommendations and calculate a new EPC category. This
|
||||
# category is then used to produce adjusted energy figures
|
||||
total_sap_points = sum([x["sap_points"] for x in representative_recs[property_id]])
|
||||
expected_epc = sap_to_epc(float(property_instance.data["current-energy-efficiency"]) + total_sap_points)
|
||||
|
||||
expected_adjusted_energy = AnnualBillSavings.adjust_energy_to_metered(
|
||||
epc_energy_consumption=expected_heat_demand,
|
||||
current_epc_rating=expected_epc,
|
||||
)
|
||||
|
||||
heat_demand_change = (
|
||||
current_adjusted_energy - expected_adjusted_energy
|
||||
)
|
||||
|
||||
# update the recommendations
|
||||
# We need to totals for the representative recommendations
|
||||
representative_rec_data = [
|
||||
{
|
||||
"recommendation_id": r["recommendation_id"],
|
||||
"co2_equivalent_savings": r.get("co2_equivalent_savings"),
|
||||
"heat_demand": r.get("heat_demand"),
|
||||
"type": r["type"]
|
||||
} for r
|
||||
in representative_recs[property_id]
|
||||
]
|
||||
representative_rec_data = pd.DataFrame(representative_rec_data)
|
||||
# Convert co2 and heat demand to proportions of their column sums
|
||||
representative_rec_data["co2_equivalent_savings_percent"] = (
|
||||
representative_rec_data["co2_equivalent_savings"] /
|
||||
representative_rec_data["co2_equivalent_savings"].sum()
|
||||
)
|
||||
|
||||
representative_rec_data["heat_demand_percent"] = (
|
||||
representative_rec_data["heat_demand"] / representative_rec_data["heat_demand"].sum()
|
||||
)
|
||||
|
||||
# We'll use the proportions to update the carbon and heat demand
|
||||
representative_rec_data["co2_equivalent_savings"] = (
|
||||
carbon_change * representative_rec_data["co2_equivalent_savings_percent"]
|
||||
)
|
||||
|
||||
representative_rec_data["heat_demand"] = (
|
||||
heat_demand_change * representative_rec_data["heat_demand_percent"]
|
||||
)
|
||||
|
||||
# Finally, insert these values into the final recommendations
|
||||
for rec in property_recommendations:
|
||||
if rec["type"] in ["external_wall_insulation", "internal_wall_insulation"]:
|
||||
change_data = representative_rec_data[
|
||||
representative_rec_data["type"].isin(["external_wall_insulation", "internal_wall_insulation"])
|
||||
]
|
||||
else:
|
||||
change_data = representative_rec_data[representative_rec_data["type"] == rec["type"]]
|
||||
|
||||
if rec["type"] == "mechanical_ventilation":
|
||||
rec["co2_equivalent_savings"] = 0
|
||||
rec["heat_demand"] = 0
|
||||
rec["energy_cost_savings"] = 0
|
||||
else:
|
||||
rec["co2_equivalent_savings"] = change_data["co2_equivalent_savings"].values[0]
|
||||
rec["heat_demand"] = change_data["heat_demand"].values[0]
|
||||
rec["energy_cost_savings"] = AnnualBillSavings.estimate(rec["heat_demand"])
|
||||
|
||||
# Update recommendations
|
||||
recommendations[property_id] = property_recommendations
|
||||
|
||||
# For expected adjust energy, we don't include mechanical ventilation so we'll add it back on
|
||||
expected_adjusted_energy = expected_adjusted_energy + representative_rec_data[
|
||||
representative_rec_data["type"] == "mechanical_ventilation"
|
||||
]["heat_demand"].values[0]
|
||||
|
||||
property_instance.set_adjusted_energy(
|
||||
current_adjusted_energy=current_adjusted_energy,
|
||||
expected_adjusted_energy=expected_adjusted_energy
|
||||
)
|
||||
|
||||
# 1) the property data
|
||||
# 2) the property details (epc)
|
||||
# 3) the recommendations
|
||||
|
||||
logger.info("Uploading recommendations to the database")
|
||||
property_valuation_increases = []
|
||||
session.commit()
|
||||
for i in range(0, len(input_properties), BATCH_SIZE):
|
||||
try:
|
||||
|
|
@ -324,6 +497,8 @@ async def trigger_plan(body: PlanTriggerRequest):
|
|||
)
|
||||
create_property_details_epc(session, property_details_epc)
|
||||
|
||||
update_or_create_property_spatial_details(session, p.uprn, p.spatial)
|
||||
|
||||
# TODO: TEMP
|
||||
if p.data["uprn"] == "":
|
||||
print("Get rid of me!")
|
||||
|
|
@ -350,6 +525,16 @@ async def trigger_plan(body: PlanTriggerRequest):
|
|||
session, plan_id=new_plan_id, recommendation_ids=uploaded_recommendation_ids
|
||||
)
|
||||
|
||||
# Get defaults
|
||||
default_recommendations = [r for r in recommendations_to_upload if r["default"]]
|
||||
total_sap_points = sum([r["sap_points"] for r in default_recommendations])
|
||||
new_sap_points = float(p.data["current-energy-efficiency"]) + total_sap_points
|
||||
new_epc = sap_to_epc(new_sap_points)
|
||||
|
||||
property_valuation_increases.append(
|
||||
PropertyValuation.estimate(property_instance=p, target_epc=new_epc)
|
||||
)
|
||||
|
||||
# Commit the session after each batch
|
||||
session.commit()
|
||||
|
||||
|
|
@ -365,7 +550,18 @@ async def trigger_plan(body: PlanTriggerRequest):
|
|||
# way to do this, but it's the simplest and will be a process that we can re-use since when we change a
|
||||
# recommendation from being default to not default, we'll need to re-run this process to re-calculate the
|
||||
# the portfolion level impact
|
||||
aggregate_portfolio_recommendations(session, portfolio_id=body.portfolio_id)
|
||||
|
||||
total_valuation_increase = sum(property_valuation_increases)
|
||||
labour_days = round(max(
|
||||
[sum(r["labour_days"] for r in rec_group if r["default"]) for p_id, rec_group in recommendations.items()]
|
||||
))
|
||||
|
||||
aggregate_portfolio_recommendations(
|
||||
session,
|
||||
portfolio_id=body.portfolio_id,
|
||||
total_valuation_increase=total_valuation_increase,
|
||||
labour_days=labour_days
|
||||
)
|
||||
|
||||
# Commit final changes
|
||||
session.commit()
|
||||
|
|
|
|||
|
|
@ -8,25 +8,6 @@ from backend.app.config import get_settings
|
|||
import msgpack
|
||||
|
||||
|
||||
def insert_temp_recommendation_id(property_recommendations):
|
||||
"""
|
||||
Creates a temporary recommendation id which is needed for
|
||||
filtering recommendations between default and no, after the optimiser has been
|
||||
run
|
||||
:param property_recommendations: nested list of recommendations, grouped by data_types
|
||||
:return: Updated recommendations_to_upload, where where recommendation has a "recommendation_id"
|
||||
integer inserted
|
||||
"""
|
||||
idx = 0
|
||||
|
||||
for recs in property_recommendations:
|
||||
for rec in recs:
|
||||
rec["recommendation_id"] = idx
|
||||
idx += 1
|
||||
|
||||
return property_recommendations
|
||||
|
||||
|
||||
def get_cleaned():
|
||||
"""
|
||||
This function will retrieve the cleaned dataset from s3 which has the cleaned
|
||||
|
|
@ -106,7 +87,7 @@ def create_recommendation_scoring_data(
|
|||
scoring_dict[col] = "none"
|
||||
|
||||
# We update the description to indicate it's insulated
|
||||
if recommendation["type"] == "wall_insulation":
|
||||
if recommendation["type"] in ["internal_wall_insulation", "external_wall_insulation", "cavity_wall_insulation"]:
|
||||
# The upgrade made here is to the u-value of the walls and the description of the
|
||||
# insulation thickness
|
||||
scoring_dict["walls_thermal_transmittance_ENDING"] = recommendation["new_u_value"]
|
||||
|
|
@ -125,7 +106,7 @@ def create_recommendation_scoring_data(
|
|||
scoring_dict["walls_insulation_thickness_ENDING"] = "none"
|
||||
|
||||
# Update description to indicate it's insulate
|
||||
if recommendation["type"] == "floor_insulation":
|
||||
if recommendation["type"] in ["solid_floor_insulation", "suspended_floor_insulation", "exposed_floor_insulation"]:
|
||||
if len(recommendation["parts"]) > 1:
|
||||
raise NotImplementedError("Have more than 1 floor insulation part - handle this case")
|
||||
|
||||
|
|
@ -147,14 +128,24 @@ def create_recommendation_scoring_data(
|
|||
if scoring_dict["floor_insulation_thickness_ENDING"] is None:
|
||||
scoring_dict["floor_insulation_thickness_ENDING"] = "none"
|
||||
|
||||
if recommendation["type"] == "roof_insulation":
|
||||
if recommendation["type"] in ["loft_insulation", "room_roof_insulation", "flat_roof_insulation"]:
|
||||
scoring_dict["roof_thermal_transmittance_ENDING"] = recommendation["new_u_value"]
|
||||
|
||||
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(int(parts[0]["depth"]))
|
||||
# This is based on the values we have in the training data
|
||||
valid_numeric_values = [
|
||||
12, 25, 50, 75, 100, 150, 200, 250, 270, 300, 350, 400
|
||||
]
|
||||
|
||||
proposed_depth = int(parts[0]["depth"])
|
||||
if proposed_depth not in valid_numeric_values:
|
||||
# Take the nearest value for scoring
|
||||
proposed_depth = min(valid_numeric_values, key=lambda x: abs(x - proposed_depth))
|
||||
|
||||
scoring_dict["roof_insulation_thickness_ENDING"] = str(proposed_depth)
|
||||
scoring_dict["ROOF_ENERGY_EFF_ENDING"] = "Very Good"
|
||||
else:
|
||||
# Fill missing roof u-values - this fill is not based on recommended upgrades
|
||||
|
|
@ -180,8 +171,15 @@ def create_recommendation_scoring_data(
|
|||
if recommendation["type"] == "sealing_open_fireplace":
|
||||
scoring_dict["NUMBER_OPEN_FIREPLACES_ENDING"] = 0
|
||||
|
||||
if recommendation["type"] == "low_energy_lighting":
|
||||
scoring_dict["LOW_ENERGY_LIGHTING_ENDING"] = 100
|
||||
scoring_dict["LIGHTING_ENERGY_EFF_STARTING"] = "Very Good"
|
||||
|
||||
if recommendation["type"] not in [
|
||||
"wall_insulation", "floor_insulation", "roof_insulation", "mechanical_ventilation", "sealing_open_fireplace",
|
||||
"mechanical_ventilation", "sealing_open_fireplace", "low_energy_lighting",
|
||||
"internal_wall_insulation", "external_wall_insulation", "cavity_wall_insulation",
|
||||
"loft_insulation", "room_roof_insulation", "flat_roof_insulation",
|
||||
"solid_floor_insulation", "suspended_floor_insulation", "exposed_floor_insulation"
|
||||
]:
|
||||
raise NotImplementedError("Implement me")
|
||||
|
||||
|
|
|
|||
|
|
@ -69,10 +69,10 @@ def generate_api_key():
|
|||
return api_key
|
||||
|
||||
|
||||
def sap_to_epc(sap_points: int):
|
||||
def sap_to_epc(sap_points: int | float):
|
||||
"""
|
||||
Simple utility function to convert SAP points to EPC rating.
|
||||
:param sapPoints: numerical value of SAP points, typically between 0 and 100
|
||||
:param sap_points: numerical value of SAP points, typically between 0 and 100
|
||||
:return:
|
||||
"""
|
||||
|
||||
|
|
|
|||
72
backend/ml_models/AnnualBillSavings.py
Normal file
72
backend/ml_models/AnnualBillSavings.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
class AnnualBillSavings:
|
||||
"""
|
||||
This is a simple class which will estimate the annual bill savings, based on the kwh savings.
|
||||
This class uses data from Ofgem, including their price caps, to provide us with an estimate for
|
||||
1KWH of energy.
|
||||
"""
|
||||
|
||||
# These gas an electricity consumption figures are based off of figures presented by Ofgem
|
||||
# https://www.ofgem.gov.uk/information-consumers/energy-advice-households/average-gas-and-electricity-use-explained
|
||||
AVERAGE_ELECTRICITY_CONSUMPTION = 2700
|
||||
AVERAGE_GAS_CONSUMPTION = 11500
|
||||
|
||||
# Latest price cap figures from Ofgem are for January 2024
|
||||
# https://www.ofgem.gov.uk/publications/changes-energy-price-cap-1-january-2024
|
||||
ELECTRICITY_PRICE_CAP = 0.29
|
||||
GAS_PRICE_CAP = 0.07
|
||||
|
||||
# This is a weighted mean of the price caps, using the consumption figures above as weights
|
||||
PRICE_FACTOR = 0.11183098591549295
|
||||
|
||||
@classmethod
|
||||
def estimate(cls, kwh: float):
|
||||
"""
|
||||
Estimate the annual bill savings based on the kwh savings
|
||||
:param kwh: The kwh savings
|
||||
:return: An estimate for annual bill savings
|
||||
"""
|
||||
return cls.PRICE_FACTOR * kwh
|
||||
|
||||
@classmethod
|
||||
def adjust_energy_to_metered(cls, epc_energy_consumption, current_epc_rating):
|
||||
"""
|
||||
The over-prediction of energy use by EPCs in Great Britain: A comparison
|
||||
of EPC-modelled and metered primary energy use intensity
|
||||
|
||||
Which can be found here: https://www.sciencedirect.com/science/article/pii/S0378778823002542
|
||||
We implement the results on page 10
|
||||
|
||||
:return:
|
||||
"""
|
||||
|
||||
gradients = {
|
||||
"A": -0.1,
|
||||
"B": -0.1,
|
||||
"C": -0.43,
|
||||
"D": -0.52,
|
||||
"E": -0.7,
|
||||
"F": -0.76,
|
||||
"G": -0.76
|
||||
}
|
||||
|
||||
intercepts = {
|
||||
"A": 28,
|
||||
"B": 28,
|
||||
"C": 97,
|
||||
"D": 119,
|
||||
"E": 160,
|
||||
"F": 157,
|
||||
"G": 157
|
||||
}
|
||||
|
||||
gradient = gradients[current_epc_rating]
|
||||
intercept = intercepts[current_epc_rating]
|
||||
|
||||
# This should be negative
|
||||
consumption_difference = gradient * epc_energy_consumption + intercept
|
||||
if consumption_difference > 0:
|
||||
raise ValueError("consumption_difference should be negative")
|
||||
|
||||
adjusted_consumption = (epc_energy_consumption + consumption_difference)
|
||||
|
||||
return adjusted_consumption
|
||||
22
backend/ml_models/Valuation.py
Normal file
22
backend/ml_models/Valuation.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
class PropertyValuation:
|
||||
"""
|
||||
This is a placeholder class for the property valuation model
|
||||
"""
|
||||
|
||||
UPRN_VALUE_LOOKUP = {
|
||||
15038202: {"current_value": 202000, "increase_percentage": 0.05725},
|
||||
37024763: {"current_value": 213000, "increase_percentage": 0.025},
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def estimate(cls, property_instance, target_epc):
|
||||
data = cls.UPRN_VALUE_LOOKUP.get(property_instance.uprn)
|
||||
|
||||
if not data:
|
||||
raise ValueError("Have not implemented valuation for this property")
|
||||
|
||||
new_valuation = (1 + data["increase_percentage"]) * data["current_value"]
|
||||
|
||||
increase = round(new_valuation - data["current_value"], 2)
|
||||
|
||||
return increase
|
||||
139
backend/ml_models/api.py
Normal file
139
backend/ml_models/api.py
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
import pandas as pd
|
||||
import requests
|
||||
from requests.exceptions import RequestException
|
||||
from utils.logger import setup_logger
|
||||
from utils.s3 import save_dataframe_to_s3_parquet
|
||||
from backend.app.utils import read_parquet_from_s3
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
|
||||
class ModelApi:
|
||||
MODEL_PREFIXES = [
|
||||
"sap_change_predictions",
|
||||
"heat_demand_predictions",
|
||||
"carbon_change_predictions"
|
||||
]
|
||||
|
||||
MODEL_URLS = {
|
||||
"sap_change_predictions": "sapmodel",
|
||||
"heat_demand_predictions": "heatmodel",
|
||||
"carbon_change_predictions": "carbonmodel"
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
portfolio_id,
|
||||
timestamp,
|
||||
base_url="https://api.dev.hestia.homes",
|
||||
):
|
||||
"""
|
||||
This class handles the communication with the Model APIs. These models include SAP change, heat demain change
|
||||
and carbon change
|
||||
|
||||
property_id (int, optional): :
|
||||
:param portfolio_id: The portfolio ID to be passed in the request payload. Defaults to 4.
|
||||
:param timestamp: The creation timestamp to be passed in the request payload. Defaults to None.
|
||||
:param base_url:
|
||||
"""
|
||||
self.base_url = base_url
|
||||
self.portfolio_id = portfolio_id
|
||||
self.timestamp = timestamp
|
||||
|
||||
def upload_scoring_data(self, df: pd.DataFrame, bucket: str, model_prefix: str) -> str:
|
||||
"""
|
||||
The sap model api needs a scoring data that is sitting in s3 to use as a dataset to score on
|
||||
This method allows the user to upload a table as a parquet file. This method will return the file
|
||||
location, which can be used as the file location in the predict() method
|
||||
|
||||
:param df: Pandas dataframe with scoring data to be uploaded to s3
|
||||
:param bucket: Name of the bucket in s3 to upload to
|
||||
:param model_prefix: The model prefix to be used in the file location
|
||||
:return:
|
||||
"""
|
||||
|
||||
if model_prefix not in self.MODEL_PREFIXES:
|
||||
raise ValueError(f"Model prefix specified is not in {self.MODEL_PREFIXES}")
|
||||
|
||||
# Store parquet file in s3 for scoring
|
||||
file_location = f"{model_prefix}/{self.portfolio_id}/{self.timestamp}.parquet"
|
||||
|
||||
logger.info("Storing scoring data to s3")
|
||||
save_dataframe_to_s3_parquet(
|
||||
df=df,
|
||||
bucket_name=bucket,
|
||||
file_key=file_location
|
||||
)
|
||||
|
||||
return file_location
|
||||
|
||||
def predict(self, file_location, model_prefix: str):
|
||||
"""Makes a POST request to the SAP Change Model API with the provided parameters.
|
||||
|
||||
Args:
|
||||
file_location (str): The file location to be passed in the request payload.
|
||||
model_prefix (str): The model prefix to be used in the request URL.
|
||||
|
||||
Returns:
|
||||
dict: The API response as a dictionary if the request was successful, None otherwise.
|
||||
"""
|
||||
logger.info(f"Making request to {model_prefix} change api")
|
||||
url = f"{self.base_url}/{self.MODEL_URLS[model_prefix]}/predict"
|
||||
payload = {
|
||||
"file_location": file_location,
|
||||
"property_id": "", # This should get removed
|
||||
"portfolio_id": self.portfolio_id,
|
||||
"created_at": self.timestamp
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(url, json=payload, headers={"Content-Type": "application/json"}, timeout=120)
|
||||
|
||||
# Check if the response status code is 2xx (success)
|
||||
response.raise_for_status()
|
||||
|
||||
# Return the JSON response as a Python dictionary
|
||||
return response.json()
|
||||
except RequestException as e:
|
||||
logger.error(f"An error occurred: {e}")
|
||||
# In case of an error, you might want to return None or raise the exception
|
||||
# depending on how you want to handle errors in your application
|
||||
return None
|
||||
|
||||
def predict_all(self, df, bucket, prediction_buckets) -> dict:
|
||||
|
||||
"""
|
||||
For each model prefix, this method will upload the scoring data to s3 and then make a request to the
|
||||
model api to generate predictions. The predictions will be stored in the predictions bucket.
|
||||
This method will then fetch the stored predictions and format them, returning all of the predictions as
|
||||
a dictionary of panaas dataframes
|
||||
:param df: Pandas dataframe with scoring data to be uploaded to s3
|
||||
:param bucket: Name of the bucket in s3 to upload to
|
||||
:param prediction_buckets: Dictionary containing the prediction buckets for each model prefix
|
||||
:return:
|
||||
"""
|
||||
|
||||
predictions = {}
|
||||
for model_prefix in self.MODEL_PREFIXES:
|
||||
logger.info(f"Scoring for model prefix: {model_prefix}")
|
||||
file_location = self.upload_scoring_data(df, bucket, model_prefix)
|
||||
response = self.predict(
|
||||
"s3://{DATA_BUCKET}/".format(DATA_BUCKET=bucket) + file_location, model_prefix
|
||||
)
|
||||
|
||||
predictions_bucket = prediction_buckets[model_prefix]
|
||||
|
||||
# Retrieve the predictions
|
||||
predictions_df = pd.DataFrame(
|
||||
read_parquet_from_s3(
|
||||
bucket_name=predictions_bucket,
|
||||
file_key=response["storage_filepath"].split(predictions_bucket + "/")[1]
|
||||
)
|
||||
)
|
||||
|
||||
predictions_df["predictions"] = predictions_df["predictions"].astype(float).round(1)
|
||||
predictions_df[['property_id', 'recommendation_id']] = predictions_df['id'].str.split('+', expand=True)
|
||||
|
||||
predictions[model_prefix] = predictions_df
|
||||
|
||||
return predictions
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
import pandas as pd
|
||||
import requests
|
||||
from requests.exceptions import RequestException
|
||||
from utils.logger import setup_logger
|
||||
from utils.s3 import save_dataframe_to_s3_parquet
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
|
||||
class SAPChangeModelAPI:
|
||||
def __init__(
|
||||
self,
|
||||
portfolio_id,
|
||||
timestamp,
|
||||
base_url="https://api.dev.hestia.homes",
|
||||
):
|
||||
"""
|
||||
property_id (int, optional): :
|
||||
:param portfolio_id: The portfolio ID to be passed in the request payload. Defaults to 4.
|
||||
:param timestamp: The creation timestamp to be passed in the request payload. Defaults to None.
|
||||
:param base_url:
|
||||
"""
|
||||
self.base_url = base_url
|
||||
self.portfolio_id = portfolio_id
|
||||
self.timestamp = timestamp
|
||||
|
||||
def upload_scoring_data(self, df: pd.DataFrame, bucket: str) -> str:
|
||||
"""
|
||||
The sap model api needs a scoring data that is sitting in s3 to use as a dataset to score on
|
||||
This method allows the user to upload a table as a parquet file. This method will return the file
|
||||
location, which can be used as the file location in the predict() method
|
||||
|
||||
:param df: Pandas dataframe with scoring data to be uploaded to s3
|
||||
:param bucket: Name of the bucket in s3 to upload to
|
||||
:return:
|
||||
"""
|
||||
|
||||
# Store parquet file in s3 for scoring
|
||||
file_location = "sap_change_predictions/{portfolio_id}/{timestamp}.parquet".format(
|
||||
portfolio_id=self.portfolio_id,
|
||||
timestamp=self.timestamp
|
||||
)
|
||||
|
||||
logger.info("Storing scoring data to s3")
|
||||
save_dataframe_to_s3_parquet(
|
||||
df=df,
|
||||
bucket_name=bucket,
|
||||
file_key=file_location
|
||||
)
|
||||
|
||||
return file_location
|
||||
|
||||
def predict(self, file_location):
|
||||
"""Makes a POST request to the SAP Change Model API with the provided parameters.
|
||||
|
||||
Args:
|
||||
file_location (str): The file location to be passed in the request payload.
|
||||
|
||||
Returns:
|
||||
dict: The API response as a dictionary if the request was successful, None otherwise.
|
||||
"""
|
||||
logger.info("Making request to sap change api")
|
||||
url = f"{self.base_url}/sapmodel/predict"
|
||||
payload = {
|
||||
"file_location": file_location,
|
||||
"property_id": "", # This should get removed
|
||||
"portfolio_id": self.portfolio_id,
|
||||
"created_at": self.timestamp
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(url, json=payload, headers={"Content-Type": "application/json"}, timeout=120)
|
||||
|
||||
# Check if the response status code is 2xx (success)
|
||||
response.raise_for_status()
|
||||
|
||||
# Return the JSON response as a Python dictionary
|
||||
return response.json()
|
||||
except RequestException as e:
|
||||
logger.error(f"An error occurred: {e}")
|
||||
# In case of an error, you might want to return None or raise the exception
|
||||
# depending on how you want to handle errors in your application
|
||||
return None
|
||||
|
|
@ -12,6 +12,7 @@ mock_epc_response = {
|
|||
"uprn": 1,
|
||||
"number-habitable-rooms": 5,
|
||||
"property-type": "House",
|
||||
"built-form": "Detached",
|
||||
"inspection-date": "2023-06-01",
|
||||
'lodgement-datetime': '2023-06-01 20:29:01',
|
||||
"some-other-key": "some-value",
|
||||
|
|
@ -42,6 +43,7 @@ mock_epc_response = {
|
|||
"uprn": 2,
|
||||
"number-habitable-rooms": 5,
|
||||
"property-type": "House",
|
||||
"built-form": "Detached",
|
||||
"inspection-date": "2023-05-01",
|
||||
'lodgement-datetime': '2023-05-01 20:29:01',
|
||||
"some-other-key": "some-other-value",
|
||||
|
|
|
|||
|
|
@ -204,9 +204,9 @@ class TestSapModelPrep:
|
|||
'external_insulation': False, 'internal_insulation': False, 'walls_thermal_transmittance_ENDING': 0.7,
|
||||
'is_park_home_ENDING': False, 'walls_insulation_thickness_ENDING': 'average',
|
||||
'external_insulation_ENDING': False, 'internal_insulation_ENDING': False,
|
||||
'floor_thermal_transmittance': 0.64, 'is_to_unheated_space': False, 'is_to_external_air': False,
|
||||
'floor_thermal_transmittance': 0.52, 'is_to_unheated_space': False, 'is_to_external_air': False,
|
||||
'is_suspended': True, 'is_solid': False, 'another_property_below': False,
|
||||
'floor_insulation_thickness': 'none', 'floor_thermal_transmittance_ENDING': 0.64,
|
||||
'floor_insulation_thickness': 'none', 'floor_thermal_transmittance_ENDING': 0.52,
|
||||
'floor_insulation_thickness_ENDING': 'none', 'roof_thermal_transmittance': 1.5, 'is_pitched': True,
|
||||
'is_roof_room': False, 'is_loft': False, 'is_flat': False, 'is_thatched': False, 'is_at_rafters': False,
|
||||
'has_dwelling_above': False, 'roof_insulation_thickness': 'below average',
|
||||
|
|
@ -260,7 +260,7 @@ class TestSapModelPrep:
|
|||
'no_individual_heating_or_community_network': False, 'complex_fuel_type': 'Unknown',
|
||||
'fuel_type_ENDING': 'oil', 'main-fuel_tariff_type_ENDING': 'Unknown', 'is_community_ENDING': False,
|
||||
'no_individual_heating_or_community_network_ENDING': False, 'complex_fuel_type_ENDING': 'Unknown',
|
||||
'estimated_perimeter_STARTING': 44.77882152472145, 'estimated_perimeter_ENDING': 44.77882152472145,
|
||||
'estimated_perimeter_STARTING': 30.531014675946444, 'estimated_perimeter_ENDING': 30.531014675946444,
|
||||
'HOT_WATER_ENERGY_EFF_STARTING': "Good",
|
||||
"FLOOR_ENERGY_EFF_STARTING": "Unknown",
|
||||
"WINDOWS_ENERGY_EFF_STARTING": "Good",
|
||||
|
|
@ -310,7 +310,7 @@ class TestSapModelPrep:
|
|||
recommendation = {
|
||||
"recommendation_id": 0,
|
||||
"new_u_value": 0.7,
|
||||
"type": "wall_insulation"
|
||||
"type": "cavity_wall_insulation"
|
||||
}
|
||||
|
||||
test_record = create_recommendation_scoring_data(
|
||||
|
|
@ -356,7 +356,7 @@ class TestSapModelPrep:
|
|||
|
||||
assert test_record[c].values[0] == row[c]
|
||||
|
||||
def test_solid_wall_insulation(self, cleaned, cleaning_data):
|
||||
def test_internal_wall_insulation(self, cleaned, cleaning_data):
|
||||
|
||||
starting_epc2 = {
|
||||
'low-energy-fixed-light-count': '2', 'address': 'FLAT 12, WAREHOUSE W, 3 WESTERN GATEWAY',
|
||||
|
|
@ -513,6 +513,7 @@ class TestSapModelPrep:
|
|||
data=starting_epc2
|
||||
)
|
||||
home2.get_components(cleaned)
|
||||
home2.set_number_lighting_outlets(None)
|
||||
|
||||
data_processor2 = DataProcessor(None, newdata=True)
|
||||
data_processor2.insert_data(pd.DataFrame([home2.get_model_data()]))
|
||||
|
|
@ -530,7 +531,7 @@ class TestSapModelPrep:
|
|||
recommendation2 = {
|
||||
"recommendation_id": 0,
|
||||
"new_u_value": 0.21,
|
||||
"type": "wall_insulation"
|
||||
"type": "internal_wall_insulation"
|
||||
}
|
||||
|
||||
test_record2 = create_recommendation_scoring_data(
|
||||
|
|
@ -644,9 +645,9 @@ class TestSapModelPrep:
|
|||
'is_park_home': False, 'walls_insulation_thickness': 'none', 'external_insulation': False,
|
||||
'internal_insulation': False, 'walls_thermal_transmittance_ENDING': 2.0, 'is_park_home_ENDING': False,
|
||||
'walls_insulation_thickness_ENDING': 'none', 'external_insulation_ENDING': False,
|
||||
'internal_insulation_ENDING': False, 'floor_thermal_transmittance': 0.62, 'is_to_unheated_space': False,
|
||||
'internal_insulation_ENDING': False, 'floor_thermal_transmittance': 0.51, 'is_to_unheated_space': False,
|
||||
'is_to_external_air': False, 'is_suspended': True, 'is_solid': False, 'another_property_below': False,
|
||||
'floor_insulation_thickness': 'none', 'floor_thermal_transmittance_ENDING': 0.62,
|
||||
'floor_insulation_thickness': 'none', 'floor_thermal_transmittance_ENDING': 0.51,
|
||||
'floor_insulation_thickness_ENDING': 'none', 'roof_thermal_transmittance': 2.3, 'is_pitched': True,
|
||||
'is_roof_room': False, 'is_loft': False, 'is_flat': False, 'is_thatched': False, 'is_at_rafters': False,
|
||||
'has_dwelling_above': False, 'roof_insulation_thickness': 'none', 'roof_thermal_transmittance_ENDING': 2.3,
|
||||
|
|
@ -699,7 +700,7 @@ class TestSapModelPrep:
|
|||
'no_individual_heating_or_community_network': False, 'complex_fuel_type': 'Unknown',
|
||||
'fuel_type_ENDING': 'mains gas', 'main-fuel_tariff_type_ENDING': 'Unknown', 'is_community_ENDING': False,
|
||||
'no_individual_heating_or_community_network_ENDING': False, 'complex_fuel_type_ENDING': 'Unknown',
|
||||
'estimated_perimeter_STARTING': 41.634120622393354, 'estimated_perimeter_ENDING': 41.634120622393354,
|
||||
'estimated_perimeter_STARTING': 30.06908711617298, 'estimated_perimeter_ENDING': 30.06908711617298,
|
||||
'HOT_WATER_ENERGY_EFF_STARTING': "Good",
|
||||
"FLOOR_ENERGY_EFF_STARTING": "Unknown",
|
||||
"WINDOWS_ENERGY_EFF_STARTING": "Average",
|
||||
|
|
@ -732,6 +733,7 @@ class TestSapModelPrep:
|
|||
data=starting_epc3
|
||||
)
|
||||
home3.get_components(cleaned)
|
||||
home3.set_number_lighting_outlets(None)
|
||||
|
||||
data_processor3 = DataProcessor(None, newdata=True)
|
||||
data_processor3.insert_data(pd.DataFrame([home3.get_model_data()]))
|
||||
|
|
@ -851,9 +853,9 @@ class TestSapModelPrep:
|
|||
'is_park_home': False, 'walls_insulation_thickness': 'none', 'external_insulation': False,
|
||||
'internal_insulation': False, 'walls_thermal_transmittance_ENDING': 1.7, 'is_park_home_ENDING': False,
|
||||
'walls_insulation_thickness_ENDING': 'none', 'external_insulation_ENDING': False,
|
||||
'internal_insulation_ENDING': False, 'floor_thermal_transmittance': 0.66, 'is_to_unheated_space': False,
|
||||
'internal_insulation_ENDING': False, 'floor_thermal_transmittance': 0.53, 'is_to_unheated_space': False,
|
||||
'is_to_external_air': False, 'is_suspended': False, 'is_solid': True, 'another_property_below': False,
|
||||
'floor_insulation_thickness': 'none', 'floor_thermal_transmittance_ENDING': 0.66,
|
||||
'floor_insulation_thickness': 'none', 'floor_thermal_transmittance_ENDING': 0.53,
|
||||
'floor_insulation_thickness_ENDING': 'none', 'roof_thermal_transmittance': 0.21, 'is_pitched': True,
|
||||
'is_roof_room': False, 'is_loft': True, 'is_flat': False, 'is_thatched': False, 'is_at_rafters': False,
|
||||
'has_dwelling_above': False, 'roof_insulation_thickness': '200', 'roof_thermal_transmittance_ENDING': 0.21,
|
||||
|
|
@ -907,7 +909,7 @@ class TestSapModelPrep:
|
|||
'no_individual_heating_or_community_network': False, 'complex_fuel_type': 'Unknown',
|
||||
'fuel_type_ENDING': 'mains gas', 'main-fuel_tariff_type_ENDING': 'Unknown', 'is_community_ENDING': False,
|
||||
'no_individual_heating_or_community_network_ENDING': False, 'complex_fuel_type_ENDING': 'Unknown',
|
||||
'estimated_perimeter_STARTING': 37.54197650630557, 'estimated_perimeter_ENDING': 37.54197650630557,
|
||||
'estimated_perimeter_STARTING': 27.113649698998472, 'estimated_perimeter_ENDING': 27.113649698998472,
|
||||
'HOT_WATER_ENERGY_EFF_STARTING': "Good",
|
||||
"FLOOR_ENERGY_EFF_STARTING": "Unknown",
|
||||
"WINDOWS_ENERGY_EFF_STARTING": "Average",
|
||||
|
|
@ -940,6 +942,7 @@ class TestSapModelPrep:
|
|||
data=starting_epc4
|
||||
)
|
||||
home4.get_components(cleaned)
|
||||
home4.set_number_lighting_outlets(None)
|
||||
|
||||
data_processor4 = DataProcessor(None, newdata=True)
|
||||
data_processor4.insert_data(pd.DataFrame([home4.get_model_data()]))
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ def app():
|
|||
suspended_floor_costs = pd.read_excel(DATA_DIRECTORY, sheet_name="suspended_floor_insulation", header=0)
|
||||
solid_floor_costs = pd.read_excel(DATA_DIRECTORY, sheet_name="solid_floor_insulation", header=0)
|
||||
ewi_costs = pd.read_excel(DATA_DIRECTORY, sheet_name="external_wall_insulation", header=0)
|
||||
lel_costs = pd.read_excel(DATA_DIRECTORY, sheet_name="low_energy_lighting", header=0)
|
||||
|
||||
# Form a single table to be uploaded
|
||||
costs = pd.concat(
|
||||
|
|
@ -83,6 +84,7 @@ def app():
|
|||
suspended_floor_costs,
|
||||
solid_floor_costs,
|
||||
ewi_costs,
|
||||
lel_costs
|
||||
]
|
||||
)
|
||||
|
||||
|
|
|
|||
79
etl/testing_data/birmingham_pilot.py
Normal file
79
etl/testing_data/birmingham_pilot.py
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
"""
|
||||
This script will create an input csv for the recommendation engine and upload it to S3, which can be used for
|
||||
testing
|
||||
"""
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from epc_api.client import EpcClient
|
||||
from utils.s3 import save_csv_to_s3
|
||||
|
||||
FILE_SIZE = 5
|
||||
EPC_AUTH_TOKEN = os.getenv("EPC_AUTH_TOKEN", None)
|
||||
USER_ID = 8
|
||||
PORTFOLIO_ID = 54
|
||||
|
||||
|
||||
def app():
|
||||
# For this dataset, we want 3 properties, all hourses. A mid-terrace, and end-terrace and a semi-detached
|
||||
|
||||
epc_client = EpcClient(auth_token=EPC_AUTH_TOKEN)
|
||||
|
||||
# Birmingham has a Local Authority Code of E08000025
|
||||
|
||||
# Let's take an EPC D property
|
||||
example_1_reponse = epc_client.domestic.search(
|
||||
params={
|
||||
"local-authority": "E08000025",
|
||||
"property-type": "house",
|
||||
}
|
||||
)
|
||||
|
||||
g_data = epc_client.domestic.search(params={"energy-band": "g"}, size=n_g)
|
||||
f_data = epc_client.domestic.search(params={"energy-band": "f"}, size=n_f)
|
||||
e_data = epc_client.domestic.search(params={"energy-band": "e"}, size=n_e)
|
||||
d_data = epc_client.domestic.search(params={"energy-band": "d"}, size=n_d)
|
||||
c_data = epc_client.domestic.search(params={"energy-band": "c"}, size=n_c)
|
||||
b_data = epc_client.domestic.search(params={"energy-band": "b"}, size=n_b)
|
||||
a_data = epc_client.domestic.search(params={"energy-band": "a"}, size=n_a)
|
||||
|
||||
# Combine the final data
|
||||
final_data = (
|
||||
g_data["rows"] + f_data["rows"] + e_data["rows"] + d_data["rows"] + c_data["rows"] + b_data["rows"]
|
||||
+ a_data["rows"]
|
||||
)
|
||||
|
||||
# TODO: We also take homes with just a specific type of wall
|
||||
|
||||
final_data = [
|
||||
x for x in final_data if ("cavity wall" in x["walls-description"].lower()) or (
|
||||
"solid brick" in x["walls-description"].lower()
|
||||
) or ("average thermal transmittance" in x["walls-description"].lower())
|
||||
]
|
||||
|
||||
# TODO: For the moment, don't use park homes
|
||||
final_csv_data = pd.DataFrame(
|
||||
[{"address": x["address"], "postcode": x["postcode"], "Notes": None} for x
|
||||
in final_data if
|
||||
x["property-type"] not in ["Park home"]]
|
||||
)
|
||||
|
||||
final_csv_data = pd.concat([starting_csv, final_csv_data]).reset_index(drop=True)
|
||||
|
||||
# Store the data in s3
|
||||
filename = f"{USER_ID}/{PORTFOLIO_ID}/test_inputs.csv"
|
||||
save_csv_to_s3(
|
||||
dataframe=final_csv_data,
|
||||
bucket_name="retrofit-plan-inputs-dev",
|
||||
file_name=filename
|
||||
)
|
||||
|
||||
body = {
|
||||
"portfolio_id": str(PORTFOLIO_ID),
|
||||
"housing_type": "Social",
|
||||
"goal": "Increase EPC",
|
||||
"goal_value": "B",
|
||||
"trigger_file_path": filename
|
||||
}
|
||||
print(body)
|
||||
|
|
@ -1,27 +1,23 @@
|
|||
import numpy as np
|
||||
from recommendations.county_to_region import county_to_region_map
|
||||
|
||||
# This data comes from SPONs
|
||||
# This data comes from SPONs 2023
|
||||
regional_labour_variations = [
|
||||
{"Region": "Outer London (Spon’s 2023)", "Adjustment_Factor": 1.00},
|
||||
{"Region": "Outer London", "Adjustment_Factor": 1.00},
|
||||
{"Region": "Inner London", "Adjustment_Factor": 1.05},
|
||||
{"Region": "South East", "Adjustment_Factor": 0.96},
|
||||
{"Region": "South West", "Adjustment_Factor": 0.90},
|
||||
{"Region": "South East England", "Adjustment_Factor": 0.96},
|
||||
{"Region": "South West England", "Adjustment_Factor": 0.90},
|
||||
{"Region": "East of England", "Adjustment_Factor": 0.93},
|
||||
{"Region": "East Midlands", "Adjustment_Factor": 0.88},
|
||||
{"Region": "West Midlands", "Adjustment_Factor": 0.87},
|
||||
{"Region": "North East", "Adjustment_Factor": 0.83},
|
||||
{"Region": "North West", "Adjustment_Factor": 0.88},
|
||||
{"Region": "Yorkshire and Humberside", "Adjustment_Factor": 0.86},
|
||||
{"Region": "North East England", "Adjustment_Factor": 0.83},
|
||||
{"Region": "North West England", "Adjustment_Factor": 0.88},
|
||||
{"Region": "Yorkshire and the Humber", "Adjustment_Factor": 0.86},
|
||||
{"Region": "Wales", "Adjustment_Factor": 0.88},
|
||||
{"Region": "Scotland", "Adjustment_Factor": 0.88},
|
||||
{"Region": "Northern Ireland", "Adjustment_Factor": 0.76}
|
||||
]
|
||||
|
||||
county_map = {
|
||||
"Northamptonshire": "East Midlands",
|
||||
"Hampshire": "South East",
|
||||
}
|
||||
|
||||
|
||||
class Costs:
|
||||
"""
|
||||
|
|
@ -40,8 +36,12 @@ class Costs:
|
|||
# We assume a conservative 10% contingency for all works which is a rate defined by SPONs
|
||||
CONTINGENCY = 0.1
|
||||
|
||||
# We use a higher contingency rate for internal wall insulation because of the potential for issues with moving
|
||||
# fittings and trimming doors, as well as scope for damage to the existing wall during preparation.
|
||||
IWI_CONTINGENCY = 0.15
|
||||
|
||||
# Where there is more uncertainty, a higher contingency rate is used
|
||||
HIGH_RISK_CONTINGENCY = 0.15
|
||||
HIGH_RISK_CONTINGENCY = 0.2
|
||||
# When there is less uncertainty, a lower contingency rate is used
|
||||
LOW_RISK_CONTINGENCY = 0.05
|
||||
|
||||
|
|
@ -54,11 +54,11 @@ class Costs:
|
|||
# have a preliminaries of 12-14% so we use 12% as the median for the preliminaries rate.
|
||||
# For External wall insulation (EWI), we use 15% as the preliminaries rate if we think the property might
|
||||
# need scaffolding, otherwise we use 12%. This is to account for any site preparation that might be required
|
||||
EWI_NO_SCAFFOLDING_PRELIMINARIES = 0.12
|
||||
EWI_SCAFFOLDING_PRELIMINARIES = 0.15
|
||||
EWI_NO_SCAFFOLDING_PRELIMINARIES = 0.15
|
||||
EWI_SCAFFOLDING_PRELIMINARIES = 0.20
|
||||
|
||||
VAT_RATE = 0.2
|
||||
PROFIT_MARGIN = 0.15
|
||||
PROFIT_MARGIN = 0.2
|
||||
|
||||
def __init__(self, property_instance):
|
||||
"""
|
||||
|
|
@ -71,13 +71,16 @@ class Costs:
|
|||
self.property = property_instance
|
||||
self.regional_labour_variations = regional_labour_variations
|
||||
|
||||
self.county = county_map.get(self.property.data["county"], None)
|
||||
if self.county is None:
|
||||
raise ValueError("County not found in county map")
|
||||
self.region = county_to_region_map.get(self.property.data["county"], None)
|
||||
if self.region is None:
|
||||
# Try and grab using the local-authority-label
|
||||
self.region = county_to_region_map.get(self.property.data["local-authority-label"], None)
|
||||
if self.region is None:
|
||||
raise ValueError("Region not found in county map")
|
||||
|
||||
self.labour_adjustment_factor = [
|
||||
x["Adjustment_Factor"] for x in self.regional_labour_variations if
|
||||
x["Region"] == self.county
|
||||
x["Region"] == self.region
|
||||
][0]
|
||||
|
||||
if not self.labour_adjustment_factor:
|
||||
|
|
@ -115,6 +118,9 @@ class Costs:
|
|||
|
||||
labour_hours = material["labour_hours_per_unit"] * wall_area
|
||||
|
||||
# Assume a team of 2
|
||||
labour_days = (labour_hours / 8) / 2
|
||||
|
||||
return {
|
||||
"total": total_cost,
|
||||
"subtotal": subtotal_before_vat,
|
||||
|
|
@ -124,7 +130,8 @@ class Costs:
|
|||
"material": base_material_cost,
|
||||
"profit": profit_cost,
|
||||
"labour_hours": labour_hours,
|
||||
"labour_cost": labour_cost
|
||||
"labour_cost": labour_cost,
|
||||
"labour_days": labour_days
|
||||
}
|
||||
|
||||
def loft_insulation(self, floor_area, material):
|
||||
|
|
@ -153,6 +160,9 @@ class Costs:
|
|||
|
||||
labour_hours = material["labour_hours_per_unit"] * floor_area
|
||||
|
||||
# Assume a team of 1 person
|
||||
labour_days = labour_hours / 8
|
||||
|
||||
return {
|
||||
"total": total_cost,
|
||||
"subtotal": subtotal_before_vat,
|
||||
|
|
@ -162,7 +172,8 @@ class Costs:
|
|||
"material": base_material_cost,
|
||||
"profit": profit_cost,
|
||||
"labour_hours": labour_hours,
|
||||
"labour_cost": labour_cost
|
||||
"labour_cost": labour_cost,
|
||||
"labour_days": labour_days
|
||||
}
|
||||
|
||||
def internal_wall_insulation(self, wall_area, material, non_insulation_materials):
|
||||
|
|
@ -224,8 +235,7 @@ class Costs:
|
|||
|
||||
subtotal_before_profit = labour_costs + materials_costs + demolition_plant_costs
|
||||
|
||||
# We use high risk contingency for iwi
|
||||
contingency_cost = subtotal_before_profit * self.HIGH_RISK_CONTINGENCY
|
||||
contingency_cost = subtotal_before_profit * self.IWI_CONTINGENCY
|
||||
preliminaries_cost = subtotal_before_profit * self.PRELIMINARIES
|
||||
profit_cost = subtotal_before_profit * self.PROFIT_MARGIN
|
||||
|
||||
|
|
@ -569,3 +579,51 @@ class Costs:
|
|||
"labour_days": labour_days,
|
||||
"labour_cost": labour_costs
|
||||
}
|
||||
|
||||
def low_energy_lighting(self, number_of_lights, number_current_lel_lights, material):
|
||||
|
||||
"""
|
||||
Calculates the total cost for low energy lighting based on material and labor costs,
|
||||
including contingency, preliminaries, profit, and VAT.
|
||||
|
||||
:param number_of_lights: Int, number of light
|
||||
:param number_current_lel_lights: Int, number of low energy lights currently installed in the home
|
||||
:material: Dict, material data containing costs of fittings
|
||||
"""
|
||||
|
||||
# If there are no lights fitted in the property, we increase the contingency in case there are potential wiring
|
||||
# blockers
|
||||
if number_current_lel_lights == 0:
|
||||
contingency = self.HIGH_RISK_CONTINGENCY
|
||||
else:
|
||||
contingency = self.CONTINGENCY
|
||||
|
||||
material_cost = material["material_cost"] * number_of_lights
|
||||
labour_cost = material["labour_cost"] * number_of_lights * self.labour_adjustment_factor
|
||||
|
||||
subtotal_before_profit = material_cost + labour_cost
|
||||
|
||||
contingency_cost = subtotal_before_profit * contingency
|
||||
preliminaries_cost = subtotal_before_profit * self.PRELIMINARIES
|
||||
profit_cost = subtotal_before_profit * self.PROFIT_MARGIN
|
||||
|
||||
subtotal_before_vat = subtotal_before_profit + contingency_cost + preliminaries_cost + profit_cost
|
||||
vat_cost = subtotal_before_vat * self.VAT_RATE
|
||||
total_cost = subtotal_before_vat + vat_cost
|
||||
|
||||
labour_hours = material["labour_hours_per_unit"] * number_of_lights
|
||||
# Assume a single electrician installing
|
||||
labour_days = (labour_hours / 8)
|
||||
|
||||
return {
|
||||
"total": total_cost,
|
||||
"subtotal": subtotal_before_vat,
|
||||
"vat": vat_cost,
|
||||
"contingency": contingency_cost,
|
||||
"preliminaries": preliminaries_cost,
|
||||
"material": material_cost,
|
||||
"profit": profit_cost,
|
||||
"labour_hours": labour_hours,
|
||||
"labour_days": labour_days,
|
||||
"labour_cost": labour_cost
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ class FireplaceRecommendations(Definitions):
|
|||
"sap_points": None,
|
||||
"total": estimated_cost,
|
||||
# Take a very basic estimate of 6 hours, multipled by the number of open fireplaces to seal
|
||||
"labour_hours": 6 * number_open_fireplaces
|
||||
"labour_hours": 6 * number_open_fireplaces,
|
||||
"labour_days": 6 * number_open_fireplaces / 8, # Assume 8 hour day
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -51,8 +51,9 @@ class FloorRecommendations(Definitions):
|
|||
]
|
||||
]
|
||||
|
||||
# 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"
|
||||
part for part in materials if part["type"] == "solid_floor_insulation" if float(part["depth"]) <= 75
|
||||
]
|
||||
|
||||
self.solid_floor_non_insulation_materials = [
|
||||
|
|
@ -142,7 +143,20 @@ class FloorRecommendations(Definitions):
|
|||
|
||||
@staticmethod
|
||||
def _make_floor_description(material):
|
||||
return f"Install {int(material['depth'])}{material['depth_unit']} {material['description']} insulation"
|
||||
|
||||
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):
|
||||
"""
|
||||
|
|
@ -194,7 +208,7 @@ class FloorRecommendations(Definitions):
|
|||
cost_result=cost_result
|
||||
),
|
||||
],
|
||||
"type": "floor_insulation",
|
||||
"type": material["type"],
|
||||
"description": self._make_floor_description(material),
|
||||
"starting_u_value": u_value,
|
||||
"new_u_value": new_u_value,
|
||||
|
|
|
|||
73
recommendations/LightingRecommendations.py
Normal file
73
recommendations/LightingRecommendations.py
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
from backend.Property import Property
|
||||
from typing import List
|
||||
from recommendations.Costs import Costs
|
||||
|
||||
|
||||
class LightingRecommendations:
|
||||
|
||||
def __init__(self, property_instance: Property, materials: List):
|
||||
"""
|
||||
:param property_instance: Instance of the Property class, for the home associated to property_id
|
||||
:param materials: List of materials to be used in the recommendations
|
||||
"""
|
||||
|
||||
self.property = property_instance
|
||||
self.costs = Costs(self.property)
|
||||
|
||||
material = [
|
||||
material for material in materials if material["type"] == "low_energy_lighting_installation"
|
||||
]
|
||||
if len(material) != 1:
|
||||
raise ValueError("Incorrect number of low energy lighting materials specified")
|
||||
|
||||
self.material = material[0]
|
||||
self.recommendation = []
|
||||
|
||||
def recommend(self):
|
||||
"""
|
||||
This method will check if there are any lighting fittings that aren't low energy.
|
||||
|
||||
If there are, the will recommend fitting the rest of the outlets with low energy lighting fittings
|
||||
:return:
|
||||
"""
|
||||
|
||||
if self.property.lighting["low_energy_proportion"] == 100:
|
||||
return
|
||||
|
||||
number_lighting_outlets = self.property.number_lighting_outlets
|
||||
|
||||
# Number non lel outlets
|
||||
number_non_lel_outlets = number_lighting_outlets - (
|
||||
self.property.lighting["low_energy_proportion"] * number_lighting_outlets
|
||||
)
|
||||
|
||||
number_non_lel_outlets = round(number_non_lel_outlets)
|
||||
|
||||
if number_non_lel_outlets == 0:
|
||||
return
|
||||
|
||||
# Get the cost of the fittings
|
||||
cost_result = self.costs.low_energy_lighting(
|
||||
number_of_lights=number_non_lel_outlets,
|
||||
number_current_lel_lights=number_lighting_outlets - number_non_lel_outlets,
|
||||
material=self.material
|
||||
)
|
||||
|
||||
if number_non_lel_outlets == 1:
|
||||
description = "Install low energy lighting in 1 remaining outlet"
|
||||
else:
|
||||
description = "Install low energy lighting in %s outlets" % str(number_non_lel_outlets)
|
||||
|
||||
self.recommendation = [
|
||||
{
|
||||
"parts": [],
|
||||
"type": "low_energy_lighting",
|
||||
"description": description,
|
||||
"starting_u_value": None,
|
||||
"new_u_value": None,
|
||||
# For SAP points, we use the fact that lighting is usually worth 2 points and we scale this to
|
||||
# the proportion of lights that will be set to low energy
|
||||
"sap_points": round(2 * (number_non_lel_outlets / number_lighting_outlets), 2),
|
||||
**cost_result
|
||||
}
|
||||
]
|
||||
163
recommendations/Recommendations.py
Normal file
163
recommendations/Recommendations.py
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
from backend.Property import Property
|
||||
from typing import List
|
||||
from recommendations.FloorRecommendations import FloorRecommendations
|
||||
from recommendations.WallRecommendations import WallRecommendations
|
||||
from recommendations.RoofRecommendations import RoofRecommendations
|
||||
from recommendations.VentilationRecommendations import VentilationRecommendations
|
||||
from recommendations.FireplaceRecommendations import FireplaceRecommendations
|
||||
from recommendations.LightingRecommendations import LightingRecommendations
|
||||
from backend.ml_models.AnnualBillSavings import AnnualBillSavings
|
||||
|
||||
|
||||
class Recommendations:
|
||||
"""
|
||||
High level recommendations class, which sits above the measure specific recommendation classes
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
property_instance: Property,
|
||||
materials: List
|
||||
):
|
||||
"""
|
||||
:param property_instance: Instance of the Property class, for the home associated to property_id
|
||||
:param materials: List of materials to be used in the recommendations
|
||||
"""
|
||||
|
||||
self.property_instance = property_instance
|
||||
self.materials = materials
|
||||
|
||||
self.floor_recommender = FloorRecommendations(property_instance=property_instance, materials=materials)
|
||||
self.wall_recomender = WallRecommendations(property_instance=property_instance, materials=materials)
|
||||
self.roof_recommender = RoofRecommendations(property_instance=property_instance, materials=materials)
|
||||
self.ventilation_recomender = VentilationRecommendations(
|
||||
property_instance=property_instance, materials=materials
|
||||
)
|
||||
self.fireplace_recommender = FireplaceRecommendations(property_instance=property_instance)
|
||||
self.lighting_recommender = LightingRecommendations(property_instance=property_instance, materials=materials)
|
||||
|
||||
def recommend(self):
|
||||
|
||||
"""
|
||||
This method runs the recommendations for the individual measures and then appends them to a list for output
|
||||
:return:
|
||||
"""
|
||||
|
||||
property_recommendations = []
|
||||
|
||||
# Floor recommendations
|
||||
self.floor_recommender.recommend()
|
||||
if self.floor_recommender.recommendations:
|
||||
property_recommendations.append(self.floor_recommender.recommendations)
|
||||
|
||||
# Wall recommendations
|
||||
self.wall_recomender.recommend()
|
||||
if self.wall_recomender.recommendations:
|
||||
property_recommendations.append(self.wall_recomender.recommendations)
|
||||
|
||||
# Roof recommendations
|
||||
self.roof_recommender.recommend()
|
||||
if self.roof_recommender.recommendations:
|
||||
property_recommendations.append(self.roof_recommender.recommendations)
|
||||
|
||||
# Ventilation recommendations
|
||||
self.ventilation_recomender.recommend()
|
||||
if self.ventilation_recomender.recommendation:
|
||||
property_recommendations.append(self.ventilation_recomender.recommendation)
|
||||
|
||||
# Fireplace sealing recommendations
|
||||
self.fireplace_recommender.recommend()
|
||||
if self.fireplace_recommender.recommendation:
|
||||
property_recommendations.append(self.fireplace_recommender.recommendation)
|
||||
|
||||
# Lighting recommendations
|
||||
self.lighting_recommender.recommend()
|
||||
if self.lighting_recommender.recommendation:
|
||||
property_recommendations.append(self.lighting_recommender.recommendation)
|
||||
|
||||
# We insert temporary ids into the recommendations which is important for the optimiser later
|
||||
property_recommendations = self.insert_temp_recommendation_id(property_recommendations)
|
||||
|
||||
return property_recommendations
|
||||
|
||||
@staticmethod
|
||||
def insert_temp_recommendation_id(property_recommendations):
|
||||
"""
|
||||
Creates a temporary recommendation id which is needed for
|
||||
filtering recommendations between default and no, after the optimiser has been
|
||||
run
|
||||
:param property_recommendations: nested list of recommendations, grouped by data_types
|
||||
:return: Updated recommendations_to_upload, where where recommendation has a "recommendation_id"
|
||||
integer inserted
|
||||
"""
|
||||
idx = 0
|
||||
|
||||
for recs in property_recommendations:
|
||||
for rec in recs:
|
||||
rec["recommendation_id"] = idx
|
||||
idx += 1
|
||||
|
||||
return property_recommendations
|
||||
|
||||
@classmethod
|
||||
def calculate_recommendation_impact(cls, property_instance, all_predictions, recommendations):
|
||||
|
||||
"""
|
||||
Given predictions from the model apis, with method will update the recommendations with the predicted
|
||||
impact of the recommendation on the property
|
||||
|
||||
:param property_instance: Instance of the Property class, for the home associated to property_id
|
||||
:param all_predictions: dictionary of predictions from the model apis
|
||||
:param recommendations: dictionary of recommendations for the property
|
||||
:return:
|
||||
"""
|
||||
|
||||
property_sap_predictions = all_predictions["sap_change_predictions"][
|
||||
all_predictions["sap_change_predictions"]["property_id"] == str(property_instance.id)
|
||||
]
|
||||
property_heat_predictions = all_predictions["heat_demand_predictions"][
|
||||
all_predictions["heat_demand_predictions"]["property_id"] == str(property_instance.id)
|
||||
]
|
||||
property_carbon_predictions = all_predictions["carbon_change_predictions"][
|
||||
all_predictions["carbon_change_predictions"]["property_id"] == str(property_instance.id)
|
||||
]
|
||||
|
||||
property_recommendations = recommendations[property_instance.id].copy()
|
||||
|
||||
for recommendations_by_type in property_recommendations:
|
||||
for rec in recommendations_by_type:
|
||||
|
||||
new_heat_demand = property_heat_predictions[property_heat_predictions["recommendation_id"] == str(
|
||||
rec["recommendation_id"]
|
||||
)]["predictions"].values[0]
|
||||
|
||||
new_carbon = property_carbon_predictions[property_carbon_predictions["recommendation_id"] == str(
|
||||
rec["recommendation_id"]
|
||||
)]["predictions"].values[0]
|
||||
|
||||
# We don't use the model for low energy lighting at the moment
|
||||
if rec["type"] != "low_energy_lighting":
|
||||
new_sap = property_sap_predictions[property_sap_predictions["recommendation_id"] == str(
|
||||
rec["recommendation_id"]
|
||||
)]["predictions"].values[0]
|
||||
rec["sap_points"] = new_sap - float(property_instance.data["current-energy-efficiency"])
|
||||
|
||||
if rec["type"] == "mechanical_ventilation":
|
||||
# For the moment, we cap the number of SAP points that can be achieved by ventilation at 2
|
||||
rec["sap_points"] = min(rec["sap_points"], VentilationRecommendations.SAP_LIMIT)
|
||||
|
||||
rec["co2_equivalent_savings"] = float(property_instance.data["co2-emissions-current"]) - new_carbon
|
||||
|
||||
# Energy consumption current is per meter squared, so we need to multiply by the floor area to get
|
||||
# an absolute figure for the home
|
||||
rec["heat_demand"] = (
|
||||
(float(property_instance.data["energy-consumption-current"]) - new_heat_demand
|
||||
) * property_instance.floor_area)
|
||||
|
||||
rec["energy_cost_savings"] = AnnualBillSavings.estimate(rec["heat_demand"])
|
||||
|
||||
if (rec["sap_points"] is None) and (rec["co2_equivalent_savings"] is None) or (
|
||||
rec["heat_demand"] is None) or (rec["energy_cost_savings"] is None):
|
||||
raise ValueError("sap points, co2 or heat demand is missing")
|
||||
|
||||
return property_recommendations
|
||||
|
|
@ -88,17 +88,20 @@ class RoofRecommendations:
|
|||
raise NotImplementedError("Implement me")
|
||||
|
||||
@staticmethod
|
||||
def make_loft_insulation_description(material):
|
||||
return f"Install {int(material['depth'])}{material['depth_unit']} of {material['description']} in your loft"
|
||||
def make_roof_insulation_description(material):
|
||||
if material["type"] == "loft_insulation":
|
||||
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']}"
|
||||
if material["type"] == "flat_roof_insulation":
|
||||
return (
|
||||
f"Insulate the home's flat roof with {int(material['depth'])}{material['depth_unit']} of "
|
||||
f"{material['description']}"
|
||||
)
|
||||
if material["type"] == "room_roof_insulation":
|
||||
return (f"Insulate your room roof with {int(material['depth'])}{material['depth_unit']} of "
|
||||
f"{material['description']}")
|
||||
|
||||
@staticmethod
|
||||
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']}")
|
||||
raise ValueError("Invalid material type")
|
||||
|
||||
def recommend_roof_insulation(
|
||||
self, u_value, insulation_thickness, roof
|
||||
|
|
@ -182,9 +185,7 @@ class RoofRecommendations:
|
|||
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:
|
||||
raise ValueError("Invalid material type")
|
||||
|
|
@ -199,8 +200,8 @@ class RoofRecommendations:
|
|||
cost_result=cost_result
|
||||
)
|
||||
],
|
||||
"type": "roof_insulation",
|
||||
"description": description,
|
||||
"type": material["type"],
|
||||
"description": self.make_roof_insulation_description(material),
|
||||
"starting_u_value": u_value,
|
||||
"new_u_value": new_u_value,
|
||||
"sap_points": None,
|
||||
|
|
@ -297,7 +298,7 @@ class RoofRecommendations:
|
|||
selected_total_cost=estimated_cost
|
||||
)
|
||||
],
|
||||
"type": "roof_insulation",
|
||||
"type": "room_roof_insulation",
|
||||
"description": self.make_room_roof_insulation_description(material, depth),
|
||||
"starting_u_value": u_value,
|
||||
"new_u_value": new_u_value,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ class VentilationRecommendations(Definitions):
|
|||
'mechanical, supply and extract'
|
||||
]
|
||||
|
||||
# We introduce a SAP limit, to prevent over-predicting the SAP impact of mechanical ventilation
|
||||
SAP_LIMIT = 2
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
property_instance: Property,
|
||||
|
|
@ -24,7 +27,7 @@ class VentilationRecommendations(Definitions):
|
|||
|
||||
self.has_ventilaion = None
|
||||
self.recommendation = None
|
||||
self.materials = materials
|
||||
self.materials = [part for part in materials if part["type"] == "mechanical_ventilation"]
|
||||
|
||||
def identify_ventilation(self):
|
||||
self.has_ventilaion = self.property.data["mechanical-ventilation"] in self.VENTILATION_DESCRIPTIONS
|
||||
|
|
@ -67,6 +70,7 @@ class VentilationRecommendations(Definitions):
|
|||
"sap_points": None,
|
||||
"total": estimated_cost,
|
||||
# We use a very simple and rough estimate of 4 hours per unit
|
||||
"labour_hours": 4 * n_units
|
||||
"labour_hours": 4 * n_units,
|
||||
"labour_days": 4 * n_units / 8.0 # Assume 8 hour day
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -218,8 +218,8 @@ class WallRecommendations(Definitions):
|
|||
cost_result=cost_result
|
||||
)
|
||||
],
|
||||
"type": "wall_insulation",
|
||||
"description": f"Fill cavity with {material['description']}",
|
||||
"type": "cavity_wall_insulation",
|
||||
"description": self._make_description(material),
|
||||
"starting_u_value": u_value,
|
||||
"new_u_value": new_u_value,
|
||||
"sap_points": None,
|
||||
|
|
@ -282,8 +282,8 @@ class WallRecommendations(Definitions):
|
|||
cost_result=cost_result
|
||||
)
|
||||
],
|
||||
"type": "wall_insulation",
|
||||
"description": "Install " + self._make_description(material),
|
||||
"type": material["type"],
|
||||
"description": self._make_description(material),
|
||||
"starting_u_value": u_value,
|
||||
"new_u_value": new_u_value,
|
||||
"sap_points": None,
|
||||
|
|
@ -303,7 +303,7 @@ class WallRecommendations(Definitions):
|
|||
# Recommend external and internal wall insulation separately
|
||||
# Since external and internal wall insulation are sufficiently different,
|
||||
# we separate the logic for for recommending them, therefore we don't
|
||||
# consider diminishing returns between the two
|
||||
# consider diminishing returns between the two as they are considered to be separate measures
|
||||
|
||||
ewi_recommendations = []
|
||||
if self.ewi_valid:
|
||||
|
|
@ -321,24 +321,20 @@ class WallRecommendations(Definitions):
|
|||
|
||||
self.recommendations += ewi_recommendations + iwi_recommendations
|
||||
|
||||
self.prune_diminishing_recommendations()
|
||||
|
||||
@staticmethod
|
||||
def _make_description(material):
|
||||
return f"{int(material['depth'])}{material['depth_unit']} {material['description']}"
|
||||
if material["type"] == "internal_wall_insulation":
|
||||
return (f"Install {int(material['depth'])}{material['depth_unit']} {material['description']} on internal "
|
||||
f"walls")
|
||||
|
||||
def prune_diminishing_recommendations(self):
|
||||
# For any recommendations, if we have at least 1 reommendation that does not exhibit diminishing returns
|
||||
# we trim all others that are beyond the diminishing returns threshold
|
||||
if material["type"] == "external_wall_insulation":
|
||||
return (f"Install {int(material['depth'])}{material['depth_unit']} {material['description']} on external "
|
||||
f"walls")
|
||||
|
||||
# We first check if we have any recommendations that are not diminishing returns
|
||||
not_diminishing_return = [
|
||||
rec for rec in self.recommendations if rec["new_u_value"] >= self.DIMINISHING_RETURNS_U_VALUE
|
||||
]
|
||||
if not_diminishing_return:
|
||||
self.recommendations = [
|
||||
rec for rec in self.recommendations if rec["new_u_value"] >= self.DIMINISHING_RETURNS_U_VALUE
|
||||
]
|
||||
if material["type"] == "cavity_wall_insulation":
|
||||
return f"Fill cavity with {material['description']}"
|
||||
|
||||
raise ValueError("Invalid material type")
|
||||
|
||||
@staticmethod
|
||||
def rvalue_per_mm(total_r_value: float, thickness_mm: float) -> float:
|
||||
|
|
|
|||
179
recommendations/county_to_region.py
Normal file
179
recommendations/county_to_region.py
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
# This map was found here:
|
||||
# https://gist.github.com/radiac/d91d2ed1b971c03d49e9b7bd85e23f1c#file-uk-counties-to-regions-csv
|
||||
county_to_region_map = {
|
||||
'Guernsey': 'Crown Dependencies', 'IOM': 'Crown Dependencies', 'Jersey': 'Crown Dependencies',
|
||||
'North East Derbyshire': 'East Midlands', 'Amber Valley': 'East Midlands', 'Ashfield': 'East Midlands',
|
||||
'Bassetlaw': 'East Midlands', 'Blaby': 'East Midlands', 'Bolsover': 'East Midlands', 'Boston': 'East Midlands',
|
||||
'Broxtowe': 'East Midlands', 'Charnwood': 'East Midlands', 'Chesterfield': 'East Midlands',
|
||||
'Corby': 'East Midlands', 'Daventry': 'East Midlands', 'Derby': 'East Midlands', 'Derbyshire': 'East Midlands',
|
||||
'Derbyshire Dales': 'East Midlands', 'East Lindsey': 'East Midlands', 'East Northamptonshire': 'East Midlands',
|
||||
'Erewash': 'East Midlands', 'Gedling': 'East Midlands', 'Harborough': 'East Midlands', 'High Peak': 'East Midlands',
|
||||
'Hinckley and Bosworth': 'East Midlands', 'Kettering': 'East Midlands', 'Leicester': 'East Midlands',
|
||||
'Leicestershire': 'East Midlands', 'Lincoln': 'East Midlands', 'Lincolnshire': 'Yorkshire and the Humber',
|
||||
'Mansfield': 'East Midlands', 'Melton': 'East Midlands', 'Newark and Sherwood': 'East Midlands',
|
||||
'North Kesteven': 'East Midlands', 'North West Leicestershire': 'East Midlands', 'Northampton': 'East Midlands',
|
||||
'Northamptonshire': 'East Midlands', 'Nottingham': 'East Midlands', 'Nottinghamshire': 'East Midlands',
|
||||
'Oadby and Wigston': 'East Midlands', 'Rushcliffe': 'East Midlands', 'Rutland': 'East Midlands',
|
||||
'South Derbyshire': 'East Midlands', 'South Holland': 'East Midlands', 'South Kesteven': 'East Midlands',
|
||||
'South Northamptonshire': 'East Midlands', 'Wellingborough': 'East Midlands', 'West Lindsey': 'East Midlands',
|
||||
'Babergh': 'East of England', 'Basildon': 'East of England', 'Bedford': 'East of England',
|
||||
'Bedford Borough': 'East of England', 'Bedfordshire': 'East of England', 'Braintree': 'East of England',
|
||||
'Breckland': 'East of England', 'Brentwood': 'East of England', 'Broadland': 'East of England',
|
||||
'Broxbourne': 'East of England', 'Cambridge': 'East of England', 'Cambridgeshire': 'East of England',
|
||||
'Castle Point': 'East of England', 'Central Bedfordshire': 'East of England', 'Chelmsford': 'East of England',
|
||||
'Colchester': 'East of England', 'Dacorum': 'East of England', 'East Cambridgeshire': 'East of England',
|
||||
'East Hertfordshire': 'East of England', 'Epping Forest': 'East of England', 'Essex': 'East of England',
|
||||
'Fenland': 'East of England', 'Forest Heath': 'East of England', 'Great Yarmouth': 'East of England',
|
||||
'Harlow': 'East of England', 'Hertfordshire': 'East of England', 'Hertsmere': 'East of England',
|
||||
'Huntingdonshire': 'East of England', 'Ipswich': 'East of England',
|
||||
"King's Lynn and West Norfolk": 'East of England', 'Luton': 'East of England', 'Maldon': 'East of England',
|
||||
'Mid Suffolk': 'East of England', 'Norfolk': 'East of England', 'North Hertfordshire': 'East of England',
|
||||
'North Norfolk': 'East of England', 'Norwich': 'East of England', 'Peterborough': 'East of England',
|
||||
'Rochford': 'East of England', 'South Cambridgeshire': 'East of England', 'South Norfolk': 'East of England',
|
||||
'Southend-on-Sea': 'East of England', 'St Albans': 'East of England', 'St. Edmundsbury': 'East of England',
|
||||
'Stevenage': 'East of England', 'Suffolk': 'East of England', 'Suffolk Coastal': 'East of England',
|
||||
'Tendring': 'East of England', 'Three Rivers': 'East of England', 'Thurrock': 'East of England',
|
||||
'Uttlesford': 'East of England', 'Watford': 'East of England', 'Waveney': 'East of England',
|
||||
'Welwyn Hatfield': 'East of England',
|
||||
'County Durham': 'North East England',
|
||||
'Darlington': 'North East England', 'Durham': 'North East England', 'Gateshead': 'North East England',
|
||||
'Hartlepool': 'North East England', 'Middlesbrough': 'North East England',
|
||||
'Newcastle Upon Tyne': 'North East England', 'North Tyneside': 'North East England',
|
||||
'North Yorkshire': 'Yorkshire and the Humber', 'Northumberland': 'North East England',
|
||||
'Redcar and Cleveland': 'North East England', 'South Tyneside': 'North East England',
|
||||
'Stockton-on-Tees': 'North East England', 'Sunderland': 'North East England', 'Tyne and Wear': 'North East England',
|
||||
'Allerdale': 'North West England', 'Barrow-in-Furness': 'North West England',
|
||||
'Blackburn with Darwen': 'North West England', 'Blackpool': 'North West England', 'Bolton': 'North West England',
|
||||
'Burnley': 'North West England', 'Bury': 'North West England', 'Carlisle': 'North West England',
|
||||
'Cheshire': 'North West England', 'Cheshire East': 'North West England',
|
||||
'Cheshire West and Chester': 'North West England', 'Chorley': 'North West England',
|
||||
'Copeland': 'North West England', 'Cumbria': 'North West England', 'Eden': 'North West England',
|
||||
'Fylde': 'North West England', 'Greater Manchester': 'North West England', 'Halton': 'North West England',
|
||||
'Hyndburn': 'North West England', 'Knowsley': 'North West England', 'Lancashire': 'North West England',
|
||||
'Lancaster': 'North West England', 'Liverpool': 'North West England', 'Manchester': 'North West England',
|
||||
'Merseyside': 'North West England', 'Oldham': 'North West England', 'Pendle': 'North West England',
|
||||
'Preston': 'North West England', 'Ribble Valley': 'North West England', 'Rochdale': 'North West England',
|
||||
'Rossendale': 'North West England', 'Salford': 'North West England', 'Sefton': 'North West England',
|
||||
'South Lakeland': 'North West England', 'South Ribble': 'North West England', 'St Helens': 'North West England',
|
||||
'St. Helens': 'North West England', 'Stockport': 'North West England', 'Tameside': 'North West England',
|
||||
'Trafford': 'North West England', 'Warrington': 'North West England', 'West Lancashire': 'North West England',
|
||||
'Wigan': 'North West England', 'Wirral': 'North West England', 'Wyre': 'North West England',
|
||||
'Antrim': 'Northern Ireland', 'Ards': 'Northern Ireland', 'Armagh': 'Northern Ireland',
|
||||
'Ballymena': 'Northern Ireland', 'Ballymoney': 'Northern Ireland', 'Banbridge': 'Northern Ireland',
|
||||
'Belfast': 'Northern Ireland', 'Carrickfergus': 'Northern Ireland', 'Castlereagh': 'Northern Ireland',
|
||||
'Coleraine': 'Northern Ireland', 'Cookstown': 'Northern Ireland', 'County Armagh': 'Northern Ireland',
|
||||
'County Fermanagh': 'Northern Ireland', 'Craigavon': 'Northern Ireland', 'Derry': 'Northern Ireland',
|
||||
'Down': 'Northern Ireland', 'Dungannon': 'Northern Ireland', 'Fermanagh': 'Northern Ireland',
|
||||
'Larne': 'Northern Ireland', 'Limavady': 'Northern Ireland', 'Lisburn': 'Northern Ireland',
|
||||
'Magherafelt': 'Northern Ireland', 'Moyle': 'Northern Ireland', 'Newry and Mourne': 'Northern Ireland',
|
||||
'Newtownabbey': 'Northern Ireland', 'North Down': 'Northern Ireland', 'Omagh': 'Northern Ireland',
|
||||
'South Tyrone': 'Northern Ireland', 'Strabane': 'Northern Ireland', 'Aberdeen City': 'Scotland',
|
||||
'Aberdeenshire': 'Scotland', 'Angus': 'Scotland', 'Argyll and Bute': 'Scotland', 'Argyllshire': 'Scotland',
|
||||
'Ayrshire': 'Scotland', 'Banffshire': 'Scotland', 'Berwickshire': 'Scotland', 'Bute': 'Scotland',
|
||||
'Caithness': 'Scotland', 'City of Edinburgh': 'Scotland', 'Clackmannanshire': 'Scotland',
|
||||
'Dumfries and Galloway': 'Scotland', 'Dumfriesshire': 'Scotland', 'Dunbartonshire': 'Scotland',
|
||||
'Dundee City': 'Scotland', 'East Ayrshire': 'Scotland', 'East Dunbartonshire': 'Scotland',
|
||||
'East Lothian': 'Scotland', 'East Renfrewshire': 'Scotland', 'Edinburgh City': 'Scotland',
|
||||
'Eilean Siar': 'Scotland', 'Falkirk': 'Scotland', 'Fife': 'Scotland', 'Glasgow City': 'Scotland',
|
||||
'Highland': 'Scotland', 'Inverclyde': 'Scotland', 'Inverness-shire': 'Scotland', 'Kincardineshire': 'Scotland',
|
||||
'Kinross-shire': 'Scotland', 'Kirkcudbrightshire': 'Scotland', 'Lanarkshire': 'Scotland', 'Midlothian': 'Scotland',
|
||||
'Moray': 'Scotland', 'Nairnshire': 'Scotland', 'North Ayrshire': 'Scotland', 'North Lanarkshire': 'Scotland',
|
||||
'Orkney': 'Scotland', 'Orkney Islands': 'Scotland', 'Peeblesshire': 'Scotland', 'Perth and Kinross': 'Scotland',
|
||||
'Perthshire': 'Scotland', 'Renfrewshire': 'Scotland', 'Ross and Cromarty': 'Scotland', 'Roxburghshire': 'Scotland',
|
||||
'Selkirkshire': 'Scotland', 'Shetland Islands': 'Scotland', 'South Ayrshire': 'Scotland',
|
||||
'South Lanarkshire': 'Scotland', 'Stirling': 'Scotland', 'Stirlingshire': 'Scotland', 'Sutherland': 'Scotland',
|
||||
'The Scottish Borders': 'Scotland', 'West Ayrshire': 'Scotland', 'West Dunbartonshire': 'Scotland',
|
||||
'West Lothian': 'Scotland', 'Wigtownshire': 'Scotland', 'Zetland': 'Scotland', 'Adur': 'South East England',
|
||||
'Arun': 'South East England', 'Ashford': 'South East England', 'Aylesbury Vale': 'South East England',
|
||||
'Basingstoke and Deane': 'South East England', 'Berkshire': 'South East England',
|
||||
'Bracknell Forest': 'South East England', 'Brighton and Hove': 'South East England',
|
||||
'Buckinghamshire': 'South East England', 'Canterbury': 'South East England', 'Cherwell': 'South East England',
|
||||
'Chichester': 'South East England', 'Chiltern': 'South East England', 'Crawley': 'South East England',
|
||||
'Dartford': 'South East England', 'Dover': 'South East England', 'East Hampshire': 'South East England',
|
||||
'East Sussex': 'South East England', 'Eastbourne': 'South East England', 'Eastleigh': 'South East England',
|
||||
'Elmbridge': 'South East England', 'Epsom and Ewell': 'South East England', 'Fareham': 'South East England',
|
||||
'Gosport': 'South East England', 'Gravesham': 'South East England', 'Guildford': 'South East England',
|
||||
'Hampshire': 'South East England', 'Hart': 'South East England', 'Hastings': 'South East England',
|
||||
'Havant': 'South East England', 'Horsham': 'South East England', 'Isle of Wight': 'South East England',
|
||||
'Kent': 'South East England', 'Lewes': 'South East England', 'Maidstone': 'South East England',
|
||||
'Medway': 'South East England', 'Mid Sussex': 'South East England', 'Milton Keynes': 'South East England',
|
||||
'Mole Valley': 'South East England', 'New Forest': 'South East England', 'Oxford': 'South East England',
|
||||
'Oxfordshire': 'South East England', 'Portsmouth': 'South East England', 'Reading': 'South East England',
|
||||
'Reigate and Banstead': 'South East England', 'Rother': 'South East England', 'Runnymede': 'South East England',
|
||||
'Rushmoor': 'South East England', 'Sevenoaks': 'South East England', 'Shepway': 'South East England',
|
||||
'Slough': 'South East England', 'South Bucks': 'South East England', 'South Oxfordshire': 'South East England',
|
||||
'Southampton': 'South East England', 'Spelthorne': 'South East England', 'Surrey': 'South East England',
|
||||
'Surrey Heath': 'South East England', 'Swale': 'South East England', 'Tandridge': 'South East England',
|
||||
'Test Valley': 'South East England', 'Thanet': 'South East England', 'Tonbridge and Malling': 'South East England',
|
||||
'Tunbridge Wells': 'South East England', 'Vale of White Horse': 'South East England',
|
||||
'Waverley': 'South East England', 'Wealden': 'South East England', 'West Berkshire': 'South East England',
|
||||
'West Oxfordshire': 'South East England', 'West Sussex': 'South East England', 'Winchester': 'South East England',
|
||||
'Windsor and Maidenhead': 'South East England', 'Woking': 'South East England', 'Wokingham': 'South East England',
|
||||
'Worthing': 'South East England', 'Wycombe': 'South East England',
|
||||
'Bath and North East Somerset': 'South West England', 'Bournemouth': 'South West England',
|
||||
'Bristol': 'South West England', 'Cheltenham': 'South West England', 'Christchurch': 'South West England',
|
||||
'City of Bristol': 'South West England', 'Cornwall': 'South West England', 'Cotswold': 'South West England',
|
||||
'Devon': 'South West England', 'Dorset': 'South West England', 'East Devon': 'South West England',
|
||||
'East Dorset': 'South West England', 'Exeter': 'South West England', 'Forest of Dean': 'South West England',
|
||||
'Gloucester': 'South West England', 'Gloucestershire': 'South West England',
|
||||
'Isles of Scilly': 'South West England', 'Mendip': 'South West England', 'Mid Devon': 'South West England',
|
||||
'North Devon': 'South West England', 'North Dorset': 'South West England', 'North Somerset': 'South West England',
|
||||
'Plymouth': 'South West England', 'Poole': 'South West England', 'Purbeck': 'South West England',
|
||||
'Sedgemoor': 'South West England', 'Somerset': 'South West England', 'South Gloucestershire': 'South West England',
|
||||
'South Hams': 'South West England', 'South Somerset': 'South West England', 'Stroud': 'South West England',
|
||||
'Swindon': 'South West England', 'Taunton Deane': 'South West England', 'Teignbridge': 'South West England',
|
||||
'Tewkesbury': 'South West England', 'Torbay': 'South West England', 'Torridge': 'South West England',
|
||||
'West Devon': 'South West England', 'West Dorset': 'South West England', 'West Somerset': 'South West England',
|
||||
'Weymouth and Portland': 'South West England', 'Wiltshire': 'South West England', 'Aberdare': 'Wales',
|
||||
'Bargoed': 'Wales', 'Barry': 'Wales', 'Blaenau Gwent': 'Wales', 'Bridgend': 'Wales', 'Caerphilly': 'Wales',
|
||||
'Cardiff': 'Wales', 'Carmarthenshire': 'Wales', 'Ceredigion': 'Wales', 'Conwy': 'Wales', 'Cowbridge': 'Wales',
|
||||
'Denbighshire': 'Wales', 'Dinas Powys': 'Wales', 'Ferndale': 'Wales', 'Flintshire': 'Wales', 'Gwynedd': 'Wales',
|
||||
'Hengoed': 'Wales', 'Isle of Anglesey': 'Wales', 'Llantwit Major': 'Wales', 'Maesteg': 'Wales',
|
||||
'Merthyr Tydfil': 'Wales', 'Monmouthshire': 'Wales', 'Mountain Ash': 'Wales', 'Neath Port Talbot': 'Wales',
|
||||
'Newport': 'Wales', 'Pembrokeshire': 'Wales', 'Penarth': 'Wales', 'Pentre': 'Wales', 'Pontyclun': 'Wales',
|
||||
'Pontypridd': 'Wales', 'Porth': 'Wales', 'Porthcawl': 'Wales', 'Powys': 'Wales', 'Rhondda Cynon Taff': 'Wales',
|
||||
'Rhoose': 'Wales', 'Sully': 'Wales', 'Swansea': 'Wales', 'The Vale of Glamorgan': 'Wales', 'Tonypandy': 'Wales',
|
||||
'Torfaen': 'Wales', 'Treharris': 'Wales', 'Treorchy': 'Wales', 'Wrexham': 'Wales', 'Birmingham': 'West Midlands',
|
||||
'Bromsgrove': 'West Midlands', 'Cannock Chase': 'West Midlands', 'Coventry': 'West Midlands',
|
||||
'Dudley': 'West Midlands', 'East Staffordshire': 'West Midlands', 'Herefordshire': 'West Midlands',
|
||||
'Lichfield': 'West Midlands', 'Malvern Hills': 'West Midlands', 'Newcastle-under-Lyme': 'West Midlands',
|
||||
'North Warwickshire': 'West Midlands', 'Nuneaton and Bedworth': 'West Midlands', 'Redditch': 'West Midlands',
|
||||
'Rugby': 'West Midlands', 'Sandwell': 'West Midlands', 'Shropshire': 'West Midlands', 'Solihull': 'West Midlands',
|
||||
'South Staffordshire': 'West Midlands', 'Stafford': 'West Midlands', 'Staffordshire': 'West Midlands',
|
||||
'Staffordshire Moorlands': 'West Midlands', 'Stoke-on-Trent': 'West Midlands', 'Stratford-on-Avon': 'West Midlands',
|
||||
'Tamworth': 'West Midlands', 'Telford and Wrekin': 'West Midlands', 'Walsall': 'West Midlands',
|
||||
'Warwick': 'West Midlands', 'Warwickshire': 'West Midlands', 'West Midlands': 'West Midlands',
|
||||
'Wolverhampton': 'West Midlands', 'Worcester': 'West Midlands', 'Worcestershire': 'West Midlands',
|
||||
'Wychavon': 'West Midlands', 'Wyre Forest': 'West Midlands', 'Barnsley': 'Yorkshire and the Humber',
|
||||
'Bradford': 'Yorkshire and the Humber', 'Calderdale': 'Yorkshire and the Humber',
|
||||
'City of Kingston-upon-Hull': 'Yorkshire and the Humber', 'Craven': 'Yorkshire and the Humber',
|
||||
'Doncaster': 'Yorkshire and the Humber', 'East Riding of Yorkshire': 'Yorkshire and the Humber',
|
||||
'Hambleton': 'Yorkshire and the Humber', 'Harrogate': 'Yorkshire and the Humber',
|
||||
'Kingston upon Hull': 'Yorkshire and the Humber', 'Kirklees': 'Yorkshire and the Humber',
|
||||
'Leeds': 'Yorkshire and the Humber', 'North East Lincolnshire': 'Yorkshire and the Humber',
|
||||
'North Lincolnshire': 'Yorkshire and the Humber', 'Richmondshire': 'Yorkshire and the Humber',
|
||||
'Rotherham': 'Yorkshire and the Humber', 'Ryedale': 'Yorkshire and the Humber',
|
||||
'Scarborough': 'Yorkshire and the Humber', 'Selby': 'Yorkshire and the Humber',
|
||||
'Sheffield': 'Yorkshire and the Humber', 'South Yorkshire': 'Yorkshire and the Humber',
|
||||
'Wakefield': 'Yorkshire and the Humber', 'West Yorkshire': 'Yorkshire and the Humber',
|
||||
'York': 'Yorkshire and the Humber',
|
||||
|
||||
# Additional mappings requried, based on what we find in the EPC database
|
||||
'Greater London Authority': 'Inner London',
|
||||
# We have a bunch of inner London local authority mappings, which can be used if the county is not found
|
||||
'Barking and Dagenham': 'Inner London', 'Barnet': 'Inner London', 'Bexley': 'Inner London',
|
||||
'Brent': 'Inner London', 'Bromley': 'Inner London', 'Camden': 'Inner London', 'City of London': 'Inner London',
|
||||
'City of Westminster': 'Inner London', 'Croydon': 'Inner London', 'Ealing': 'Inner London',
|
||||
'Enfield': 'Inner London',
|
||||
'Greater London': 'Inner London', 'Greenwich': 'Inner London', 'Hackney': 'Inner London',
|
||||
'Hammersmith and Fulham': 'Inner London',
|
||||
'Haringey': 'Inner London', 'Harrow': 'Inner London', 'Havering': 'Inner London', 'Hillingdon': 'Inner London',
|
||||
'Hounslow': 'Inner London',
|
||||
'Islington': 'Inner London', 'Kensington and Chelsea': 'Inner London', 'Kingston upon Thames': 'Inner London',
|
||||
'Lambeth': 'Inner London',
|
||||
'Lewisham': 'Inner London', 'Merton': 'Inner London', 'Newham': 'Inner London', 'Redbridge': 'Inner London',
|
||||
'Richmond': 'Inner London',
|
||||
'Southwark': 'Inner London', 'Sutton': 'Inner London', 'Tower Hamlets': 'Inner London',
|
||||
'Waltham Forest': 'Inner London',
|
||||
'Wandsworth': 'Inner London', 'Westminster': 'Inner London',
|
||||
}
|
||||
|
|
@ -9,6 +9,9 @@ class CostOptimiser:
|
|||
This class is used to minimise cost, given a constrained minimum gain
|
||||
"""
|
||||
|
||||
# We add an optional buffer to the minimum gain to allow for slack in the optimisation
|
||||
BUFFER = 0.2
|
||||
|
||||
def __init__(self, components, min_gain):
|
||||
self.components = components
|
||||
self.min_gain = min_gain
|
||||
|
|
@ -20,6 +23,20 @@ class CostOptimiser:
|
|||
self.solution_cost = None
|
||||
self.solution_gain = None
|
||||
|
||||
@classmethod
|
||||
def calculate_sap_gain_with_slack(cls, min_gain: int | float):
|
||||
"""
|
||||
Adds a small amount of buffer to the minimum gain, to account for possible error in SAP predictions
|
||||
:param min_gain: Numerical value for the minimum gain
|
||||
:return:
|
||||
"""
|
||||
if min_gain <= 5:
|
||||
return min_gain + 0.5
|
||||
elif min_gain <= 20:
|
||||
return min_gain + 1.5
|
||||
else:
|
||||
return min_gain + 2
|
||||
|
||||
def setup(self):
|
||||
# Initialize Model
|
||||
self.m = Model("knapsack")
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class TestCosts:
|
|||
"prime_cost": 5.17,
|
||||
"material_cost": 5.62,
|
||||
"labour_cost": 1.125,
|
||||
"labour_hours": 0.065
|
||||
"labour_hours_per_unit": 0.065,
|
||||
}
|
||||
|
||||
cwi_results = costs.cavity_wall_insulation(
|
||||
|
|
@ -27,10 +27,12 @@ class TestCosts:
|
|||
material=cwi_material,
|
||||
)
|
||||
|
||||
assert cwi_results == {'total': 1027.0280465530302, 'subtotal': 855.8567054608585, 'vat': 171.1713410921717,
|
||||
'contingency': 63.396792997100626, 'preliminaries': 63.396792997100626,
|
||||
'material': 539.0166061175574, 'profit': 95.09518949565093,
|
||||
'labour_hours': 6.234177828761786, 'labour_cost': 94.95132385344874}
|
||||
assert cwi_results == {
|
||||
'total': 1065.0661223512907, 'subtotal': 887.5551019594088, 'vat': 177.51102039188177,
|
||||
'contingency': 63.396792997100626, 'preliminaries': 63.396792997100626, 'material': 539.0166061175574,
|
||||
'profit': 126.79358599420125, 'labour_hours': 6.234177828761786, 'labour_cost': 94.95132385344874,
|
||||
'labour_days': 0.38963611429761164
|
||||
}
|
||||
|
||||
def test_loft_insulation(self):
|
||||
mock_property = Mock()
|
||||
|
|
@ -46,7 +48,7 @@ class TestCosts:
|
|||
"prime_cost": None,
|
||||
"material_cost": 5.91938,
|
||||
"labour_cost": 1.96,
|
||||
"labour_hours": 0.11
|
||||
"labour_hours_per_unit": 0.11
|
||||
}
|
||||
|
||||
loft_results = costs.loft_insulation(
|
||||
|
|
@ -54,10 +56,11 @@ class TestCosts:
|
|||
material=loft_material,
|
||||
)
|
||||
|
||||
assert loft_results == {'total': 414.8496486, 'subtotal': 345.70804050000004, 'vat': 69.14160810000001,
|
||||
'contingency': 25.608003000000004, 'preliminaries': 25.608003000000004,
|
||||
'material': 198.29923000000002, 'profit': 38.4120045, 'labour_hours': 3.685,
|
||||
'labour_cost': 57.7808}
|
||||
assert loft_results == {
|
||||
'total': 430.21445040000003, 'subtotal': 358.512042, 'vat': 71.70240840000001,
|
||||
'contingency': 25.608003000000004, 'preliminaries': 25.608003000000004, 'material': 198.29923000000002,
|
||||
'profit': 51.21600600000001, 'labour_hours': 3.685, 'labour_cost': 57.7808, 'labour_days': 0.460625
|
||||
}
|
||||
|
||||
def test_internal_wall_insulation(self):
|
||||
mock_property = Mock()
|
||||
|
|
@ -171,11 +174,14 @@ class TestCosts:
|
|||
non_insulation_materials=iwi_non_insulation_materials
|
||||
)
|
||||
|
||||
assert iwi_results == {'total': 6421.5484411659245, 'subtotal': 5351.29036763827, 'vat': 1070.258073527654,
|
||||
'contingency': 573.3525393898148, 'preliminaries': 382.2350262598765,
|
||||
'material': 1747.488000615996, 'profit': 573.3525393898148,
|
||||
'labour_hours': 88.23759388401297, 'labour_days': 2.757424808875405,
|
||||
'labour_cost': 1927.1602026551818}
|
||||
assert iwi_results == {
|
||||
'total': 6650.889456921851, 'subtotal': 5542.407880768209, 'vat': 1108.4815761536418,
|
||||
'contingency': 573.3525393898148, 'preliminaries': 382.2350262598765,
|
||||
'material': 1747.488000615996,
|
||||
'profit': 764.470052519753, 'labour_hours': 88.23759388401297,
|
||||
'labour_days': 2.757424808875405,
|
||||
'labour_cost': 1927.1602026551818
|
||||
}
|
||||
|
||||
def test_suspended_floor_insulation(self):
|
||||
mock_property = Mock()
|
||||
|
|
@ -185,16 +191,18 @@ class TestCosts:
|
|||
|
||||
costs = Costs(mock_property)
|
||||
|
||||
sus_floor_material = {'type': 'suspended_floor_insulation', 'description': 'Thermafleece CosyWool Roll',
|
||||
'depth': 140.0,
|
||||
'depth_unit': 'mm', 'cost_unit': 'gbp_per_m2', 'thermal_conductivity': 0.039,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'prime_material_cost': 0,
|
||||
'material_cost': 11.68, 'labour_cost': 1.78, 'labour_hours_per_unit': 0.1,
|
||||
'plant_cost': 0,
|
||||
'total_cost': 13.46, 'link': 'SPONs',
|
||||
'Notes': 'Spons did not contain labour costs so we use values for similar insulations. '
|
||||
'We use the '
|
||||
'same values as in Crown loft roll 44, since it is also an insulation roll'}
|
||||
sus_floor_material = {
|
||||
'type': 'suspended_floor_insulation', 'description': 'Thermafleece CosyWool Roll',
|
||||
'depth': 140.0,
|
||||
'depth_unit': 'mm', 'cost_unit': 'gbp_per_m2', 'thermal_conductivity': 0.039,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'prime_material_cost': 0,
|
||||
'material_cost': 11.68, 'labour_cost': 1.78, 'labour_hours_per_unit': 0.1,
|
||||
'plant_cost': 0,
|
||||
'total_cost': 13.46, 'link': 'SPONs',
|
||||
'Notes': 'Spons did not contain labour costs so we use values for similar insulations. '
|
||||
'We use the '
|
||||
'same values as in Crown loft roll 44, since it is also an insulation roll'
|
||||
}
|
||||
|
||||
sus_floor_non_insulation_materials = [
|
||||
{'type': 'suspended_floor_demolition', 'description': 'Removal of carpet and underfelt', 'depth': 0,
|
||||
|
|
@ -231,9 +239,8 @@ class TestCosts:
|
|||
)
|
||||
|
||||
assert sus_floor_results == {
|
||||
'total': 3003.366924, 'subtotal': 2502.80577, 'vat': 500.561154,
|
||||
'contingency': 185.39302, 'preliminaries': 185.39302, 'material': 483.405,
|
||||
'profit': 278.08952999999997, 'labour_hours': 54.940000000000005,
|
||||
'total': 3114.6027360000003, 'subtotal': 2595.50228, 'vat': 519.100456, 'contingency': 185.39302,
|
||||
'preliminaries': 185.39302, 'material': 483.405, 'profit': 370.78604, 'labour_hours': 54.940000000000005,
|
||||
'labour_days': 2.289166666666667, 'labour_cost': 1370.5252
|
||||
}
|
||||
|
||||
|
|
@ -263,28 +270,29 @@ class TestCosts:
|
|||
'description': 'clean surface of concrete to receive new damp-proof membrane', 'depth': 0,
|
||||
'depth_unit': 0, 'cost_unit': 0, 'thermal_conductivity': 0, 'thermal_conductivity_unit': 0,
|
||||
'prime_material_cost': 0, 'material_cost': 0, 'labour_cost': 4.36, 'labour_hours_per_unit': 0.14,
|
||||
'plant_cost': 0, 'total_cost': 4.36, 'link': 0, 'Notes': 0}, {'type': 'solid_floor_preparation',
|
||||
'description': 'Clean out crack to '
|
||||
'form a 20mm×20mm '
|
||||
'groove and fill with '
|
||||
'cement: mortar mixed '
|
||||
'with bonding agent',
|
||||
'depth': 0, 'depth_unit': 0,
|
||||
'cost_unit': 0,
|
||||
'thermal_conductivity': 0,
|
||||
'thermal_conductivity_unit': 0,
|
||||
'prime_material_cost': 0,
|
||||
'material_cost': 6.91,
|
||||
'labour_cost': 18.99,
|
||||
'labour_hours_per_unit': 0.61,
|
||||
'plant_cost': 0.16,
|
||||
'total_cost': 26.06, 'link': 0,
|
||||
'Notes': 'This step is the '
|
||||
'assessment and repair of '
|
||||
'any damage to the concrete '
|
||||
'floor such as filling '
|
||||
'cracks or levelling uneven '
|
||||
'areas'},
|
||||
'plant_cost': 0, 'total_cost': 4.36, 'link': 0, 'Notes': 0}, {
|
||||
'type': 'solid_floor_preparation',
|
||||
'description': 'Clean out crack to '
|
||||
'form a 20mm×20mm '
|
||||
'groove and fill with '
|
||||
'cement: mortar mixed '
|
||||
'with bonding agent',
|
||||
'depth': 0, 'depth_unit': 0,
|
||||
'cost_unit': 0,
|
||||
'thermal_conductivity': 0,
|
||||
'thermal_conductivity_unit': 0,
|
||||
'prime_material_cost': 0,
|
||||
'material_cost': 6.91,
|
||||
'labour_cost': 18.99,
|
||||
'labour_hours_per_unit': 0.61,
|
||||
'plant_cost': 0.16,
|
||||
'total_cost': 26.06, 'link': 0,
|
||||
'Notes': 'This step is the '
|
||||
'assessment and repair of '
|
||||
'any damage to the concrete '
|
||||
'floor such as filling '
|
||||
'cracks or levelling uneven '
|
||||
'areas'},
|
||||
{'type': 'solid_floor_vapour_barrier', 'description': 'Visqueen High Performance Vapour Barrier',
|
||||
'depth': 0, 'depth_unit': 0, 'cost_unit': 0, 'thermal_conductivity': 0,
|
||||
'thermal_conductivity_unit': 0, 'prime_material_cost': 0.58, 'material_cost': 1.21, 'labour_cost': 0.48,
|
||||
|
|
@ -316,8 +324,8 @@ class TestCosts:
|
|||
)
|
||||
|
||||
assert sol_floor_results == {
|
||||
'total': 3962.021952, 'subtotal': 3301.68496, 'vat': 660.336992, 'contingency': 353.75196,
|
||||
'preliminaries': 235.83464, 'material': 1006.3399999999999, 'profit': 353.75196, 'labour_hours': 57.285,
|
||||
'total': 4245.023520000001, 'subtotal': 3537.5196, 'vat': 707.5039200000001, 'contingency': 471.66928,
|
||||
'preliminaries': 235.83464, 'material': 1006.3399999999999, 'profit': 471.66928, 'labour_hours': 57.285,
|
||||
'labour_days': 2.386875, 'labour_cost': 1346.6464
|
||||
}
|
||||
|
||||
|
|
@ -331,11 +339,13 @@ class TestCosts:
|
|||
|
||||
costs = Costs(mock_property)
|
||||
|
||||
ewi_material = {'type': 'external_wall_insulation', 'description': 'Ecotherm Eco-Versal PIR Insulation Board',
|
||||
'depth': 150.0, 'depth_unit': 'mm', 'cost_unit': 'gbp_per_m2', 'thermal_conductivity': 0.022,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'prime_material_cost': 23.53,
|
||||
'material_cost': 34.62, 'labour_cost': 33.06, 'labour_hours_per_unit': 1.4, 'plant_cost': 0,
|
||||
'total_cost': 67.68, 'link': 'SPONs', 'Notes': 0}
|
||||
ewi_material = {
|
||||
'type': 'external_wall_insulation', 'description': 'Ecotherm Eco-Versal PIR Insulation Board',
|
||||
'depth': 150.0, 'depth_unit': 'mm', 'cost_unit': 'gbp_per_m2', 'thermal_conductivity': 0.022,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'prime_material_cost': 23.53,
|
||||
'material_cost': 34.62, 'labour_cost': 33.06, 'labour_hours_per_unit': 1.4, 'plant_cost': 0,
|
||||
'total_cost': 67.68, 'link': 'SPONs', 'Notes': 0
|
||||
}
|
||||
ewi_non_insulation_materials = [
|
||||
{'type': 'ewi_wall_demolition',
|
||||
'description': 'Solid & Dry Lined walls: Hack of wall finishes with chipping '
|
||||
|
|
@ -403,9 +413,8 @@ class TestCosts:
|
|||
)
|
||||
|
||||
assert ewi_results == {
|
||||
'total': 13590.909723215433, 'subtotal': 11325.758102679527, 'vat': 2265.1516205359053,
|
||||
'contingency': 808.9827216199662, 'preliminaries': 1213.4740824299492,
|
||||
'material': 4020.565147410677, 'profit': 1213.4740824299492,
|
||||
'labour_hours': 187.02533486285358, 'labour_days': 5.8445417144641745,
|
||||
'total': 14561.688989159393, 'subtotal': 12134.740824299493, 'vat': 2426.948164859899,
|
||||
'contingency': 808.9827216199662, 'preliminaries': 1617.9654432399325, 'material': 4020.565147410677,
|
||||
'profit': 1617.9654432399325, 'labour_hours': 187.02533486285358, 'labour_days': 5.8445417144641745,
|
||||
'labour_cost': 3921.5600094613983
|
||||
}
|
||||
|
|
|
|||
835
recommendations/tests/test_data/materials.py
Normal file
835
recommendations/tests/test_data/materials.py
Normal file
|
|
@ -0,0 +1,835 @@
|
|||
import datetime
|
||||
|
||||
materials = [
|
||||
{'id': 17, 'type': 'mechanical_ventilation', 'description': 'Mechanical Extract Ventilation', 'depth': None,
|
||||
'depth_unit': None, 'cost': 500, 'cost_unit': 'gbp_per_unit', 'r_value_per_mm': None, 'r_value_unit': None,
|
||||
'thermal_conductivity': None, 'thermal_conductivity_unit': None, 'link': None,
|
||||
'created_at': datetime.datetime(2023, 10, 18, 16, 39, 9, 827188), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': None, 'labour_cost': None, 'labour_hours_per_unit': None, 'plant_cost': None, 'total_cost': None,
|
||||
'notes': None},
|
||||
{'id': 1109, 'type': 'cavity_wall_insulation', 'description': 'Expanded Polystyrene Beads cavity wall insulation',
|
||||
'depth': 75.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.030303031,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.033,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin',
|
||||
'link': 'https://www.styrene.co.uk/downloads/Datasheets/Stylite_Cavity_Loose_Fill_Insulation_Datasheet_v20211.pdf',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 18.875, 'labour_cost': 1.125, 'labour_hours_per_unit': 0.065, 'plant_cost': 0.0,
|
||||
'total_cost': 20.0,
|
||||
'notes': "It is hard to find materials online. To price this, we've used this article: "
|
||||
"https://www.greenmatch.co.uk/blog/cavity-wall-insulation-cost It puts EPS beads at around £22 per "
|
||||
"meter squared, blowing wool insulation at £18 per meter squared and Polyurethane Foam at £26 per meter "
|
||||
"squared, when taking the most pessimistic prices. These rates have been used to adjust the price of "
|
||||
"the mineral wool insulation to give us the other forms of insulation"},
|
||||
{'id': 1110, 'type': 'cavity_wall_insulation', 'description': 'Injected Polyurthane Foam cavity wall insulation',
|
||||
'depth': 75.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.030303031,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.033,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin',
|
||||
'link': 'https://www.foaminstall.co.uk/wp-content/uploads/2017/04/Lapolla-Cavity-Fill-BBA-certificate-sheet1.pdf',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 22.875, 'labour_cost': 1.125, 'labour_hours_per_unit': 0.065, 'plant_cost': 0.0,
|
||||
'total_cost': 24.0, 'notes': None},
|
||||
{'id': 1111, 'type': 'loft_insulation', 'description': 'Crown Loft Roll 44 glass fibre roll', 'depth': 100.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.022727273,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.044,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 2.03,
|
||||
'material_cost': 2.1, 'labour_cost': 1.56, 'labour_hours_per_unit': 0.09, 'plant_cost': 0.0, 'total_cost': 3.66,
|
||||
'notes': None},
|
||||
{'id': 1112, 'type': 'loft_insulation', 'description': 'Crown Loft Roll 44 glass fibre roll', 'depth': 150.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.022727273,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.044,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 3.06,
|
||||
'material_cost': 3.16, 'labour_cost': 1.78, 'labour_hours_per_unit': 0.1, 'plant_cost': 0.0, 'total_cost': 4.94,
|
||||
'notes': None},
|
||||
{'id': 1113, 'type': 'loft_insulation', 'description': 'Crown Loft Roll 44 glass fibre roll', 'depth': 170.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.022727273,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.044,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin',
|
||||
'link': 'https://insulation4less.co.uk/products/knauf-170mm-combi-cut?variant=31671561257013&dfw_tracker=77750'
|
||||
'-31671561257013&utm_source=google&utm_medium=shopping&utm_campaign=shoptimised&gad_source=1&gclid'
|
||||
'=CjwKCAiAx_GqBhBQEiwAlDNAZi1LiTWKVn0W1vktOYAPPQU3hss5Tq2qNn6GNhodCQoRD_tvqCLdxhoCKnIQAvD_BwE',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 3.81938, 'labour_cost': 1.71304, 'labour_hours_per_unit': 0.11, 'plant_cost': 0.0,
|
||||
'total_cost': 5.53242,
|
||||
'notes': "We don't have a 170mm in SPONs so the material cost is based on the fact that the 170mm insulation is "
|
||||
"87.4% of the cost of the 200mm insulation"},
|
||||
{'id': 1114, 'type': 'loft_insulation', 'description': 'Crown Loft Roll 44 glass fibre roll', 'depth': 200.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.022727273,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.044,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 4.25,
|
||||
'material_cost': 4.37, 'labour_cost': 1.96, 'labour_hours_per_unit': 0.11, 'plant_cost': 0.0, 'total_cost': 6.33,
|
||||
'notes': None},
|
||||
{'id': 1115, 'type': 'loft_insulation', 'description': 'Crown Loft Roll 44 glass fibre roll', 'depth': 270.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.022727273,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.044,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 5.91938, 'labour_cost': 1.96, 'labour_hours_per_unit': 0.11, 'plant_cost': 0.0,
|
||||
'total_cost': 7.87938, 'notes': 'This is the 100mm product + the 170mm product'},
|
||||
{'id': 1116, 'type': 'loft_insulation', 'description': 'Crown Loft Roll 44 glass fibre roll', 'depth': 300.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.022727273,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.044,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 6.47, 'labour_cost': 1.96, 'labour_hours_per_unit': 0.11, 'plant_cost': 0.0, 'total_cost': 8.43,
|
||||
'notes': 'This is the 100mm product + the 200mm product'},
|
||||
{'id': 1117, 'type': 'loft_insulation', 'description': 'Isover Mineral Wool Modular Roll', 'depth': 100.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.023255814,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.043,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 1.99,
|
||||
'material_cost': 2.05, 'labour_cost': 1.6, 'labour_hours_per_unit': 0.09, 'plant_cost': 0.0, 'total_cost': 3.65,
|
||||
'notes': None},
|
||||
{'id': 1118, 'type': 'loft_insulation', 'description': 'Isover Mineral Wool Modular Roll', 'depth': 150.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.023255814,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.043,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 2.96,
|
||||
'material_cost': 3.05, 'labour_cost': 1.78, 'labour_hours_per_unit': 0.1, 'plant_cost': 0.0, 'total_cost': 4.83,
|
||||
'notes': None},
|
||||
{'id': 1119, 'type': 'loft_insulation', 'description': 'Isover Mineral Wool Modular Roll', 'depth': 170.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.023255814,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.043,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin',
|
||||
'link': 'https://flooringwarehousedirect.co.uk/product/isover-spacesaver-roll-170mm-x-1160mm-x-7-03m-8-15m2/',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 3.8706238, 'labour_cost': 2.281361, 'labour_hours_per_unit': 0.12816635, 'plant_cost': 0.0,
|
||||
'total_cost': 6.1519847,
|
||||
'notes': "We don't have a 170mm in SPONs so the material cost is based on the fact that the 170mm insulation is "
|
||||
"85.4% of the cost of the 200mm insulation"},
|
||||
{'id': 1120, 'type': 'loft_insulation', 'description': 'Isover Mineral Wool Modular Roll', 'depth': 200.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.023255814,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.043,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 4.4,
|
||||
'material_cost': 4.53, 'labour_cost': 2.67, 'labour_hours_per_unit': 0.15, 'plant_cost': 0.0, 'total_cost': 7.2,
|
||||
'notes': None},
|
||||
{'id': 1121, 'type': 'loft_insulation', 'description': 'Isover Mineral Wool Modular Roll', 'depth': 270.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.023255814,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.043,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 5.920624, 'labour_cost': 2.67, 'labour_hours_per_unit': 0.15, 'plant_cost': 0.0,
|
||||
'total_cost': 8.590624, 'notes': 'This is the 100mm product + the 170mm product'},
|
||||
{'id': 1122, 'type': 'loft_insulation', 'description': 'Isover Mineral Wool Modular Roll', 'depth': 300.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.023255814,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.043,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 6.58, 'labour_cost': 2.67, 'labour_hours_per_unit': 0.15, 'plant_cost': 0.0, 'total_cost': 9.25,
|
||||
'notes': 'This is the 100mm product + the 200mm product'},
|
||||
{'id': 1123, 'type': 'loft_insulation', 'description': 'Isover Acoustic Partition Roll', 'depth': 100.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.023255814,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.043,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 5.93,
|
||||
'material_cost': 6.4, 'labour_cost': 2.67, 'labour_hours_per_unit': 0.15, 'plant_cost': 0.0, 'total_cost': 9.07,
|
||||
'notes': 'This provides acoustic insulation as well'},
|
||||
{'id': 1124, 'type': 'loft_insulation', 'description': 'Isover Acoustic Partition Roll', 'depth': 300.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.023255814,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.043,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 17.79,
|
||||
'material_cost': 19.2, 'labour_cost': 2.67, 'labour_hours_per_unit': 0.15, 'plant_cost': 0.0, 'total_cost': 21.87,
|
||||
'notes': 'This provides acoustic insulation as well'},
|
||||
{'id': 1125, 'type': 'loft_insulation', 'description': 'Thermafleece EcoRoll Insulation', 'depth': 300.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.025641026,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.039,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 24.78, 'labour_cost': 2.67, 'labour_hours_per_unit': 0.15, 'plant_cost': 0.0, 'total_cost': 27.45,
|
||||
'notes': 'This material is based on installing 3 layers of the 100mm product'},
|
||||
{'id': 1126, 'type': 'loft_insulation', 'description': 'Thermafleece EcoRoll Insulation', 'depth': 280.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.025641026,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.039,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 23.36, 'labour_cost': 3.12, 'labour_hours_per_unit': 0.18, 'plant_cost': 0.0, 'total_cost': 26.48,
|
||||
'notes': 'This material is based on installed 2 layers of the 140mm product'},
|
||||
{'id': 1127, 'type': 'iwi_wall_demolition',
|
||||
'description': 'Solid & Dry Lined walls: Hack of wall finishes with chipping hammer; plaster to walls.',
|
||||
'depth': 0.0, 'depth_unit': None, 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': None,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, 'thermal_conductivity_unit': None,
|
||||
'link': 'SPONs', 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True,
|
||||
'prime_material_cost': None, 'material_cost': 0.0, 'labour_cost': 10.27, 'labour_hours_per_unit': 0.33,
|
||||
'plant_cost': 1.28, 'total_cost': 11.55, 'notes': None}, {'id': 1128, 'type': 'iwi_wall_demolition',
|
||||
'description': 'Stud walls: Remove wall linings '
|
||||
'including battening behind; '
|
||||
'plasterboard and skim',
|
||||
'depth': 0.0, 'depth_unit': None, 'cost': None,
|
||||
'cost_unit': 'gbp_per_m2', 'r_value_per_mm': None,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt',
|
||||
'thermal_conductivity': None,
|
||||
'thermal_conductivity_unit': None, 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12,
|
||||
244907),
|
||||
'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 0.0, 'labour_cost': 6.23,
|
||||
'labour_hours_per_unit': 0.2, 'plant_cost': 1.25,
|
||||
'total_cost': 7.48, 'notes': None},
|
||||
{'id': 1129, 'type': 'iwi_wall_demolition',
|
||||
'description': 'Lathe and Plaster walls: Remove wall linings including battening behind; wood lath and plaster',
|
||||
'depth': 0.0, 'depth_unit': None, 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': None,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, 'thermal_conductivity_unit': None,
|
||||
'link': 'SPONs', 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True,
|
||||
'prime_material_cost': None, 'material_cost': 0.0, 'labour_cost': 6.85, 'labour_hours_per_unit': 0.22,
|
||||
'plant_cost': 2.09, 'total_cost': 8.94, 'notes': None},
|
||||
{'id': 1130, 'type': 'internal_wall_insulation', 'description': 'Foamglas Grade F Wall Insulation Slabs',
|
||||
'depth': 60.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.02631579,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.038,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 41.69,
|
||||
'material_cost': 53.33, 'labour_cost': 29.52, 'labour_hours_per_unit': 1.25, 'plant_cost': 0.0,
|
||||
'total_cost': 82.85, 'notes': None},
|
||||
{'id': 1131, 'type': 'internal_wall_insulation', 'description': 'Foamglas Grade F Wall Insulation Slabs',
|
||||
'depth': 100.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.02631579,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.038,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 86.86,
|
||||
'material_cost': 99.85, 'labour_cost': 29.52, 'labour_hours_per_unit': 1.25, 'plant_cost': 0.0,
|
||||
'total_cost': 129.37, 'notes': None},
|
||||
{'id': 1132, 'type': 'internal_wall_insulation', 'description': 'Foamglas Grade F Wall Insulation Slabs',
|
||||
'depth': 150.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.02631579,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.038,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True,
|
||||
'prime_material_cost': 130.29, 'material_cost': 144.58, 'labour_cost': 29.52, 'labour_hours_per_unit': 1.25,
|
||||
'plant_cost': 0.0, 'total_cost': 174.1, 'notes': None},
|
||||
{'id': 1133, 'type': 'internal_wall_insulation', 'description': 'Ecotherm Eco-Versal PIR Insulation Board',
|
||||
'depth': 30.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 6.16,
|
||||
'material_cost': 16.73, 'labour_cost': 28.34, 'labour_hours_per_unit': 1.2, 'plant_cost': 0.0, 'total_cost': 45.07,
|
||||
'notes': None},
|
||||
{'id': 1134, 'type': 'internal_wall_insulation', 'description': 'Ecotherm Eco-Versal PIR Insulation Board',
|
||||
'depth': 50.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 8.46,
|
||||
'material_cost': 19.1, 'labour_cost': 28.34, 'labour_hours_per_unit': 1.2, 'plant_cost': 0.0, 'total_cost': 47.44,
|
||||
'notes': None},
|
||||
{'id': 1135, 'type': 'internal_wall_insulation', 'description': 'Ecotherm Eco-Versal PIR Insulation Board',
|
||||
'depth': 100.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 15.12,
|
||||
'material_cost': 25.96, 'labour_cost': 30.7, 'labour_hours_per_unit': 1.3, 'plant_cost': 0.0, 'total_cost': 56.66,
|
||||
'notes': None},
|
||||
{'id': 1136, 'type': 'internal_wall_insulation', 'description': 'Kingspan Kooltherm K18 insulated plasterboard',
|
||||
'depth': 37.5, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.04761905,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.021,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 26.86, 'labour_cost': 5.21, 'labour_hours_per_unit': 0.23, 'plant_cost': 0.0, 'total_cost': 32.07,
|
||||
'notes': None},
|
||||
{'id': 1137, 'type': 'internal_wall_insulation', 'description': 'Kingspan Kooltherm K18 insulated plasterboard',
|
||||
'depth': 42.5, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.04761905,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.021,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 17.37, 'labour_cost': 5.21, 'labour_hours_per_unit': 0.23, 'plant_cost': 0.0, 'total_cost': 22.58,
|
||||
'notes': None},
|
||||
{'id': 1138, 'type': 'internal_wall_insulation', 'description': 'Kingspan Kooltherm K18 insulated plasterboard',
|
||||
'depth': 52.5, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.04761905,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.021,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 21.74, 'labour_cost': 5.79, 'labour_hours_per_unit': 0.25, 'plant_cost': 0.0, 'total_cost': 27.53,
|
||||
'notes': None},
|
||||
{'id': 1139, 'type': 'internal_wall_insulation', 'description': 'Kingspan Kooltherm K18 insulated plasterboard',
|
||||
'depth': 62.5, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.04761905,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.021,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 19.3, 'labour_cost': 5.79, 'labour_hours_per_unit': 0.25, 'plant_cost': 0.0, 'total_cost': 25.09,
|
||||
'notes': None},
|
||||
{'id': 1140, 'type': 'internal_wall_insulation', 'description': 'Kingspan Kooltherm K18 insulated plasterboard',
|
||||
'depth': 72.5, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.04761905,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.021,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 23.15, 'labour_cost': 5.79, 'labour_hours_per_unit': 0.25, 'plant_cost': 0.0, 'total_cost': 28.94,
|
||||
'notes': None},
|
||||
{'id': 1141, 'type': 'iwi_vapour_barrier', 'description': 'Visqueen High Performance Vapour Barrier', 'depth': 0.0,
|
||||
'depth_unit': None, 'cost': None, 'cost_unit': None, 'r_value_per_mm': None,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, 'thermal_conductivity_unit': None,
|
||||
'link': 'SPONs', 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True,
|
||||
'prime_material_cost': 0.58, 'material_cost': 1.21, 'labour_cost': 0.48, 'labour_hours_per_unit': 0.02,
|
||||
'plant_cost': 0.0, 'total_cost': 1.69, 'notes': None}, {'id': 1142, 'type': 'iwi_redecoration',
|
||||
'description': 'Plaster; one coat Thistle board finish '
|
||||
'or other equal; steel trowelled; 3 mm '
|
||||
'thick work to walls or ceilings; one '
|
||||
'coat; to plasterboard base; over 600mm '
|
||||
'wide',
|
||||
'depth': 0.0, 'depth_unit': None, 'cost': None,
|
||||
'cost_unit': None, 'r_value_per_mm': None,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt',
|
||||
'thermal_conductivity': None,
|
||||
'thermal_conductivity_unit': None, 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12,
|
||||
244907), 'is_active': True,
|
||||
'prime_material_cost': None, 'material_cost': 0.06,
|
||||
'labour_cost': 6.58, 'labour_hours_per_unit': 0.25,
|
||||
'plant_cost': 0.0, 'total_cost': 6.64, 'notes': None},
|
||||
{'id': 1143, 'type': 'iwi_redecoration',
|
||||
'description': 'Two coats emulsion paint on plaster, over 40mm girth; 3.5m - 5m high', 'depth': 0.0,
|
||||
'depth_unit': None, 'cost': None, 'cost_unit': None, 'r_value_per_mm': None,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, 'thermal_conductivity_unit': None,
|
||||
'link': 'SPONs', 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True,
|
||||
'prime_material_cost': None, 'material_cost': 0.41, 'labour_cost': 3.93, 'labour_hours_per_unit': 0.21,
|
||||
'plant_cost': 0.0, 'total_cost': 4.34, 'notes': None}, {'id': 1144, 'type': 'iwi_redecoration',
|
||||
'description': 'Fitting existing softwood skirting or '
|
||||
'architrave to new frames; 150mm high',
|
||||
'depth': 0.0, 'depth_unit': None, 'cost': None,
|
||||
'cost_unit': None, 'r_value_per_mm': None,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt',
|
||||
'thermal_conductivity': None,
|
||||
'thermal_conductivity_unit': None, 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12,
|
||||
244907), 'is_active': True,
|
||||
'prime_material_cost': None, 'material_cost': 0.01,
|
||||
'labour_cost': 4.87, 'labour_hours_per_unit': 0.12,
|
||||
'plant_cost': 0.0, 'total_cost': 4.88, 'notes': None},
|
||||
{'id': 1145, 'type': 'suspended_floor_demolition', 'description': 'Removal of carpet and underfelt', 'depth': 0.0,
|
||||
'depth_unit': None, 'cost': None, 'cost_unit': None, 'r_value_per_mm': None,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, 'thermal_conductivity_unit': None,
|
||||
'link': 'SPONs', 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True,
|
||||
'prime_material_cost': None, 'material_cost': 0.0, 'labour_cost': 3.32, 'labour_hours_per_unit': 0.11,
|
||||
'plant_cost': 0.0, 'total_cost': 3.32,
|
||||
'notes': 'We ignore the plant cost that is in SPONs because we assume the carpet is not scrapped and therefore '
|
||||
'there is no need for a skip'},
|
||||
{'id': 1146, 'type': 'suspended_floor_demolition',
|
||||
'description': 'Remove boarding; withdraw nails; set aside for reuse; ground level', 'depth': 0.0,
|
||||
'depth_unit': None, 'cost': None, 'cost_unit': None, 'r_value_per_mm': None,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, 'thermal_conductivity_unit': None,
|
||||
'link': 'SPONs', 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True,
|
||||
'prime_material_cost': None, 'material_cost': 0.0, 'labour_cost': 9.34, 'labour_hours_per_unit': 0.3,
|
||||
'plant_cost': 0.0, 'total_cost': 9.34, 'notes': None},
|
||||
{'id': 1147, 'type': 'suspended_floor_vapour_barrier', 'description': 'Visqueen High Performance Vapour Barrier',
|
||||
'depth': 0.0, 'depth_unit': None, 'cost': None, 'cost_unit': None, 'r_value_per_mm': None,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, 'thermal_conductivity_unit': None,
|
||||
'link': 'SPONs', 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True,
|
||||
'prime_material_cost': 0.58, 'material_cost': 1.21, 'labour_cost': 0.48, 'labour_hours_per_unit': 0.02,
|
||||
'plant_cost': 0.0, 'total_cost': 1.69, 'notes': None},
|
||||
{'id': 1148, 'type': 'suspended_floor_insulation', 'description': 'Thermafleece CosyWool Roll', 'depth': 50.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.025641026,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.039,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 4.24, 'labour_cost': 1.56, 'labour_hours_per_unit': 0.09, 'plant_cost': 0.0, 'total_cost': 5.8,
|
||||
'notes': 'Spons did not contain labour costs so we use values for similar insulations. We use the same values as '
|
||||
'in Crown loft roll 44, since it is also an insulation roll'},
|
||||
{'id': 1149, 'type': 'suspended_floor_insulation', 'description': 'Thermafleece CosyWool Roll', 'depth': 75.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.025641026,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.039,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 6.31, 'labour_cost': 1.56, 'labour_hours_per_unit': 0.09, 'plant_cost': 0.0, 'total_cost': 7.87,
|
||||
'notes': 'Spons did not contain labour costs so we use values for similar insulations. We use the same values as '
|
||||
'in Crown loft roll 44, since it is also an insulation roll'},
|
||||
{'id': 1150, 'type': 'suspended_floor_insulation', 'description': 'Thermafleece CosyWool Roll', 'depth': 100.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.025641026,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.039,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 8.26, 'labour_cost': 1.56, 'labour_hours_per_unit': 0.1, 'plant_cost': 0.0, 'total_cost': 9.82,
|
||||
'notes': 'Spons did not contain labour costs so we use values for similar insulations. We use the same values as '
|
||||
'in Crown loft roll 44, since it is also an insulation roll'},
|
||||
{'id': 1151, 'type': 'suspended_floor_insulation', 'description': 'Thermafleece CosyWool Roll', 'depth': 140.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.025641026,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.039,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 11.68, 'labour_cost': 1.78, 'labour_hours_per_unit': 0.1, 'plant_cost': 0.0, 'total_cost': 13.46,
|
||||
'notes': 'Spons did not contain labour costs so we use values for similar insulations. We use the same values as '
|
||||
'in Crown loft roll 44, since it is also an insulation roll'},
|
||||
{'id': 1152, 'type': 'suspended_floor_insulation',
|
||||
'description': 'Thermafleece TF35 high density wool insulating batts', 'depth': 50.0, 'depth_unit': 'mm',
|
||||
'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.028571429,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.035,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 6.63, 'labour_cost': 1.56, 'labour_hours_per_unit': 0.09, 'plant_cost': 0.0, 'total_cost': 8.19,
|
||||
'notes': 'Spons did not contain labour costs so we use values for similar insulations. We use the same values as '
|
||||
'in Crown loft roll 44, since it is also an insulation roll'},
|
||||
{'id': 1153, 'type': 'suspended_floor_insulation',
|
||||
'description': 'Thermafleece TF35 high density wool insulating batts', 'depth': 75.0, 'depth_unit': 'mm',
|
||||
'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.028571429,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.035,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 10.31, 'labour_cost': 1.56, 'labour_hours_per_unit': 0.09, 'plant_cost': 0.0, 'total_cost': 11.87,
|
||||
'notes': 'Spons did not contain labour costs so we use values for similar insulations. We use the same values as '
|
||||
'in Crown loft roll 44, since it is also an insulation roll'},
|
||||
{'id': 1154, 'type': 'suspended_floor_insulation',
|
||||
'description': 'Ecotherm Eco-Versal General Purpose Insulation Board', 'depth': 30.0, 'depth_unit': 'mm',
|
||||
'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 6.16,
|
||||
'material_cost': 16.73, 'labour_cost': 28.34, 'labour_hours_per_unit': 1.2, 'plant_cost': 0.0, 'total_cost': 45.07,
|
||||
'notes': None}, {'id': 1155, 'type': 'suspended_floor_insulation',
|
||||
'description': 'Ecotherm Eco-Versal General Purpose Insulation Board', 'depth': 50.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True,
|
||||
'prime_material_cost': 8.46, 'material_cost': 19.1, 'labour_cost': 28.34,
|
||||
'labour_hours_per_unit': 1.2, 'plant_cost': 0.0, 'total_cost': 47.44, 'notes': None},
|
||||
{'id': 1156, 'type': 'suspended_floor_insulation',
|
||||
'description': 'Ecotherm Eco-Versal General Purpose Insulation Board', 'depth': 100.0, 'depth_unit': 'mm',
|
||||
'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 15.12,
|
||||
'material_cost': 25.96, 'labour_cost': 30.7, 'labour_hours_per_unit': 1.3, 'plant_cost': 0.0, 'total_cost': 56.66,
|
||||
'notes': None}, {'id': 1157, 'type': 'suspended_floor_insulation',
|
||||
'description': 'Ecotherm Eco-Versal General Purpose Insulation Board', 'depth': 150.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True,
|
||||
'prime_material_cost': 23.53, 'material_cost': 34.62, 'labour_cost': 33.06,
|
||||
'labour_hours_per_unit': 1.4, 'plant_cost': 0.0, 'total_cost': 67.68, 'notes': None},
|
||||
{'id': 1158, 'type': 'suspended_floor_insulation', 'description': 'Crown Loft Roll 44 glass fibre roll',
|
||||
'depth': 100.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_unit', 'r_value_per_mm': 0.022727273,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.044,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 2.03,
|
||||
'material_cost': 2.1, 'labour_cost': 1.56, 'labour_hours_per_unit': 0.09, 'plant_cost': 0.0, 'total_cost': 3.66,
|
||||
'notes': None},
|
||||
{'id': 1159, 'type': 'suspended_floor_insulation', 'description': 'Crown Loft Roll 44 glass fibre roll',
|
||||
'depth': 150.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_unit', 'r_value_per_mm': 0.022727273,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.044,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 3.06,
|
||||
'material_cost': 3.16, 'labour_cost': 1.78, 'labour_hours_per_unit': 0.1, 'plant_cost': 0.0, 'total_cost': 4.94,
|
||||
'notes': None},
|
||||
{'id': 1160, 'type': 'suspended_floor_insulation', 'description': 'Crown Loft Roll 44 glass fibre roll',
|
||||
'depth': 200.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_unit', 'r_value_per_mm': 0.022727273,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.044,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 4.25,
|
||||
'material_cost': 4.37, 'labour_cost': 1.96, 'labour_hours_per_unit': 0.11, 'plant_cost': 0.0, 'total_cost': 6.33,
|
||||
'notes': None},
|
||||
{'id': 1161, 'type': 'suspended_floor_insulation', 'description': 'Isover Mineral Wool Modular Roll',
|
||||
'depth': 100.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_unit', 'r_value_per_mm': 0.023255814,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.043,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 1.99,
|
||||
'material_cost': 2.05, 'labour_cost': 1.6, 'labour_hours_per_unit': 0.09, 'plant_cost': 0.0, 'total_cost': 3.65,
|
||||
'notes': None},
|
||||
{'id': 1162, 'type': 'suspended_floor_insulation', 'description': 'Isover Mineral Wool Modular Roll',
|
||||
'depth': 150.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_unit', 'r_value_per_mm': 0.023255814,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.043,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 2.96,
|
||||
'material_cost': 3.05, 'labour_cost': 1.78, 'labour_hours_per_unit': 0.1, 'plant_cost': 0.0, 'total_cost': 4.83,
|
||||
'notes': None},
|
||||
{'id': 1163, 'type': 'suspended_floor_insulation', 'description': 'Isover Mineral Wool Modular Roll',
|
||||
'depth': 200.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_unit', 'r_value_per_mm': 0.023255814,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.043,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 4.4,
|
||||
'material_cost': 4.53, 'labour_cost': 2.67, 'labour_hours_per_unit': 0.15, 'plant_cost': 0.0, 'total_cost': 7.2,
|
||||
'notes': None},
|
||||
{'id': 1164, 'type': 'suspended_floor_insulation', 'description': 'Isover Acoustic Partition Roll', 'depth': 25.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_unit', 'r_value_per_mm': 0.025641026,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.039,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 1.67,
|
||||
'material_cost': 2.01, 'labour_cost': 1.43, 'labour_hours_per_unit': 0.08, 'plant_cost': 0.0, 'total_cost': 3.44,
|
||||
'notes': None},
|
||||
{'id': 1165, 'type': 'suspended_floor_insulation', 'description': 'Isover Acoustic Partition Roll', 'depth': 50.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_unit', 'r_value_per_mm': 0.025641026,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.039,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 2.74,
|
||||
'material_cost': 3.11, 'labour_cost': 1.6, 'labour_hours_per_unit': 0.09, 'plant_cost': 0.0, 'total_cost': 4.71,
|
||||
'notes': None},
|
||||
{'id': 1166, 'type': 'suspended_floor_insulation', 'description': 'Isover Acoustic Partition Roll', 'depth': 75.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_unit', 'r_value_per_mm': 0.023255814,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.043,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 4.57,
|
||||
'material_cost': 5.01, 'labour_cost': 1.78, 'labour_hours_per_unit': 0.1, 'plant_cost': 0.0, 'total_cost': 6.79,
|
||||
'notes': None},
|
||||
{'id': 1167, 'type': 'suspended_floor_insulation', 'description': 'Isover Acoustic Partition Roll', 'depth': 100.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_unit', 'r_value_per_mm': 0.023255814,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.043,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 5.93,
|
||||
'material_cost': 6.4, 'labour_cost': 2.67, 'labour_hours_per_unit': 0.15, 'plant_cost': 0.0, 'total_cost': 9.07,
|
||||
'notes': None},
|
||||
{'id': 1168, 'type': 'suspended_floor_insulation', 'description': 'Kay-Cel Expanded Polystyrene Board',
|
||||
'depth': 25.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.030303031,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.033,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 3.88, 'labour_cost': 3.24, 'labour_hours_per_unit': 0.14, 'plant_cost': 0.0, 'total_cost': 7.12,
|
||||
'notes': None},
|
||||
{'id': 1169, 'type': 'suspended_floor_insulation', 'description': 'Kay-Cel Expanded Polystyrene Board',
|
||||
'depth': 50.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.030303031,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.033,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 6.62, 'labour_cost': 3.71, 'labour_hours_per_unit': 0.16, 'plant_cost': 0.0, 'total_cost': 10.33,
|
||||
'notes': None},
|
||||
{'id': 1170, 'type': 'suspended_floor_insulation', 'description': 'Kay-Cel Expanded Polystyrene Board',
|
||||
'depth': 75.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.030303031,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.033,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 9.3, 'labour_cost': 4.17, 'labour_hours_per_unit': 0.18, 'plant_cost': 0.0, 'total_cost': 13.47,
|
||||
'notes': None},
|
||||
{'id': 1171, 'type': 'suspended_floor_insulation', 'description': 'Kay-Cel Expanded Polystyrene Board',
|
||||
'depth': 100.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.030303031,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.033,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 12.02, 'labour_cost': 4.4, 'labour_hours_per_unit': 0.19, 'plant_cost': 0.0, 'total_cost': 16.42,
|
||||
'notes': None}, {'id': 1172, 'type': 'suspended_floor_insulation',
|
||||
'description': 'Kingspan Thermafloor TF70 High Performance Rigid Floor Insulation', 'depth': 50.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True,
|
||||
'prime_material_cost': None, 'material_cost': 10.36, 'labour_cost': 4.06,
|
||||
'labour_hours_per_unit': 0.18, 'plant_cost': 0.0, 'total_cost': 14.42, 'notes': None},
|
||||
{'id': 1173, 'type': 'suspended_floor_insulation',
|
||||
'description': 'Kingspan Thermafloor TF70 High Performance Rigid Floor Insulation', 'depth': 75.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 15.35, 'labour_cost': 4.06, 'labour_hours_per_unit': 0.18, 'plant_cost': 0.0, 'total_cost': 19.41,
|
||||
'notes': None}, {'id': 1174, 'type': 'suspended_floor_insulation',
|
||||
'description': 'Kingspan Thermafloor TF70 High Performance Rigid Floor Insulation',
|
||||
'depth': 100.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2',
|
||||
'r_value_per_mm': 0.045454547, 'r_value_unit': 'square_meter_kelvin_per_watt',
|
||||
'thermal_conductivity': 0.022, 'thermal_conductivity_unit': 'watt_per_meter_kelvin',
|
||||
'link': 'SPONs', 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907),
|
||||
'is_active': True, 'prime_material_cost': None, 'material_cost': 19.17, 'labour_cost': 4.06,
|
||||
'labour_hours_per_unit': 0.18, 'plant_cost': 0.0, 'total_cost': 23.23, 'notes': None},
|
||||
{'id': 1175, 'type': 'suspended_floor_insulation',
|
||||
'description': 'Kingspan Thermafloor TF70 High Performance Rigid Floor Insulation', 'depth': 125.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 26.59, 'labour_cost': 4.06, 'labour_hours_per_unit': 0.18, 'plant_cost': 0.0, 'total_cost': 30.65,
|
||||
'notes': None}, {'id': 1176, 'type': 'suspended_floor_insulation',
|
||||
'description': 'Kingspan Thermafloor TF70 High Performance Rigid Floor Insulation',
|
||||
'depth': 150.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2',
|
||||
'r_value_per_mm': 0.045454547, 'r_value_unit': 'square_meter_kelvin_per_watt',
|
||||
'thermal_conductivity': 0.022, 'thermal_conductivity_unit': 'watt_per_meter_kelvin',
|
||||
'link': 'SPONs', 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907),
|
||||
'is_active': True, 'prime_material_cost': None, 'material_cost': 31.13, 'labour_cost': 4.64,
|
||||
'labour_hours_per_unit': 0.2, 'plant_cost': 0.0, 'total_cost': 35.77, 'notes': None},
|
||||
{'id': 1177, 'type': 'suspended_floor_redecoration', 'description': 'refix floorboards previously set aside',
|
||||
'depth': 0.0, 'depth_unit': None, 'cost': None, 'cost_unit': None, 'r_value_per_mm': None,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, 'thermal_conductivity_unit': None,
|
||||
'link': 'SPONs', 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True,
|
||||
'prime_material_cost': None, 'material_cost': 1.54, 'labour_cost': 24.98, 'labour_hours_per_unit': 0.74,
|
||||
'plant_cost': 0.0, 'total_cost': 26.52, 'notes': None},
|
||||
{'id': 1178, 'type': 'suspended_floor_redecoration', 'description': 'Fitting carpet', 'depth': 0.0,
|
||||
'depth_unit': None, 'cost': None, 'cost_unit': None, 'r_value_per_mm': None,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, 'thermal_conductivity_unit': None,
|
||||
'link': 'SPONs', 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True,
|
||||
'prime_material_cost': None, 'material_cost': 0.0, 'labour_cost': 6.59, 'labour_hours_per_unit': 0.37,
|
||||
'plant_cost': 0.0, 'total_cost': 6.59,
|
||||
'notes': 'SPONs does not have data on re-fitting the carpet so we use the data in Fitted carpeting; Gradus woven '
|
||||
'polypropylene tufted loop\n\n as a baseline. We assume re-use of carpets, therefore we need just '
|
||||
'labour rates'},
|
||||
{'id': 1179, 'type': 'solid_floor_demolition', 'description': 'Removal of carpet and underfelt', 'depth': 0.0,
|
||||
'depth_unit': None, 'cost': None, 'cost_unit': None, 'r_value_per_mm': None,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, 'thermal_conductivity_unit': None,
|
||||
'link': 'SPONs', 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True,
|
||||
'prime_material_cost': None, 'material_cost': 0.0, 'labour_cost': 3.32, 'labour_hours_per_unit': 0.11,
|
||||
'plant_cost': 0.0, 'total_cost': 3.32,
|
||||
'notes': 'We ignore the plant cost that is in SPONs because we assume the carpet is not scrapped and therefore '
|
||||
'there is no need for a skip'},
|
||||
{'id': 1180, 'type': 'solid_floor_preparation',
|
||||
'description': 'clean surface of concrete to receive new damp-proof membrane', 'depth': 0.0, 'depth_unit': None,
|
||||
'cost': None, 'cost_unit': None, 'r_value_per_mm': None, 'r_value_unit': 'square_meter_kelvin_per_watt',
|
||||
'thermal_conductivity': None, 'thermal_conductivity_unit': None, 'link': None,
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 0.0, 'labour_cost': 4.36, 'labour_hours_per_unit': 0.14, 'plant_cost': 0.0, 'total_cost': 4.36,
|
||||
'notes': None}, {'id': 1181, 'type': 'solid_floor_preparation',
|
||||
'description': 'Clean out crack to form a 20mm×20mm groove and fill with cement: mortar mixed '
|
||||
'with bonding agent',
|
||||
'depth': 0.0, 'depth_unit': None, 'cost': None, 'cost_unit': None, 'r_value_per_mm': None,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None,
|
||||
'thermal_conductivity_unit': None, 'link': None,
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True,
|
||||
'prime_material_cost': None, 'material_cost': 6.91, 'labour_cost': 18.99,
|
||||
'labour_hours_per_unit': 0.61, 'plant_cost': 0.16, 'total_cost': 26.06,
|
||||
'notes': 'This step is the assessment and repair of any damage to the concrete floor such as '
|
||||
'filling cracks or levelling uneven areas'},
|
||||
{'id': 1182, 'type': 'solid_floor_vapour_barrier', 'description': 'Visqueen High Performance Vapour Barrier',
|
||||
'depth': 0.0, 'depth_unit': None, 'cost': None, 'cost_unit': None, 'r_value_per_mm': None,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, 'thermal_conductivity_unit': None,
|
||||
'link': 'SPONs', 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True,
|
||||
'prime_material_cost': 0.58, 'material_cost': 1.21, 'labour_cost': 0.48, 'labour_hours_per_unit': 0.02,
|
||||
'plant_cost': 0.0, 'total_cost': 1.69, 'notes': None},
|
||||
{'id': 1183, 'type': 'solid_floor_insulation', 'description': 'Kay-Cel Expanded Polystyrene Board', 'depth': 25.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.030303031,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.033,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 3.88, 'labour_cost': 3.24, 'labour_hours_per_unit': 0.14, 'plant_cost': 0.0, 'total_cost': 7.12,
|
||||
'notes': None},
|
||||
{'id': 1184, 'type': 'solid_floor_insulation', 'description': 'Kay-Cel Expanded Polystyrene Board', 'depth': 50.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.030303031,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.033,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 6.62, 'labour_cost': 3.71, 'labour_hours_per_unit': 0.16, 'plant_cost': 0.0, 'total_cost': 10.33,
|
||||
'notes': None},
|
||||
{'id': 1185, 'type': 'solid_floor_insulation', 'description': 'Kay-Cel Expanded Polystyrene Board', 'depth': 75.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.030303031,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.033,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 9.3, 'labour_cost': 4.17, 'labour_hours_per_unit': 0.18, 'plant_cost': 0.0, 'total_cost': 13.47,
|
||||
'notes': None}, {'id': 1186, 'type': 'solid_floor_insulation',
|
||||
'description': 'Kingspan Thermafloor TF70 High Performance Rigid Floor Insulation', 'depth': 50.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True,
|
||||
'prime_material_cost': None, 'material_cost': 10.36, 'labour_cost': 4.06,
|
||||
'labour_hours_per_unit': 0.18, 'plant_cost': 0.0, 'total_cost': 14.42, 'notes': None},
|
||||
{'id': 1187, 'type': 'solid_floor_insulation',
|
||||
'description': 'Kingspan Thermafloor TF70 High Performance Rigid Floor Insulation', 'depth': 75.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 15.35, 'labour_cost': 4.06, 'labour_hours_per_unit': 0.18, 'plant_cost': 0.0, 'total_cost': 19.41,
|
||||
'notes': None}, {'id': 1188, 'type': 'solid_floor_insulation',
|
||||
'description': 'Ecotherm Eco-Versal General Purpose Insulation Board', 'depth': 30.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True,
|
||||
'prime_material_cost': 6.16, 'material_cost': 16.73, 'labour_cost': 28.34,
|
||||
'labour_hours_per_unit': 1.2, 'plant_cost': 0.0, 'total_cost': 45.07, 'notes': None},
|
||||
{'id': 1189, 'type': 'solid_floor_insulation',
|
||||
'description': 'Ecotherm Eco-Versal General Purpose Insulation Board', 'depth': 50.0, 'depth_unit': 'mm',
|
||||
'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 8.46,
|
||||
'material_cost': 19.1, 'labour_cost': 28.34, 'labour_hours_per_unit': 1.2, 'plant_cost': 0.0, 'total_cost': 47.44,
|
||||
'notes': None}, {'id': 1190, 'type': 'solid_floor_insulation',
|
||||
'description': 'Ecotherm Eco-Versal General Purpose Insulation Board', 'depth': 60.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin',
|
||||
'link': 'https://londonbuildingsupplies.co.uk/products/60mm--ecotherm-eco-versal-general'
|
||||
'-purpose-pir-insulation-board---2.4m-x-1.2m-x-60mm.html',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True,
|
||||
'prime_material_cost': None, 'material_cost': 24.081198, 'labour_cost': 28.34,
|
||||
'labour_hours_per_unit': 1.2, 'plant_cost': 0.0, 'total_cost': 52.421196,
|
||||
'notes': "This material isn't in SPONs but checking online, is around 92% of the cost of the "
|
||||
"100mm"},
|
||||
{'id': 1191, 'type': 'solid_floor_insulation',
|
||||
'description': 'Ecotherm Eco-Versal General Purpose Insulation Board', 'depth': 70.0, 'depth_unit': 'mm',
|
||||
'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin',
|
||||
'link': 'https://londonbuildingsupplies.co.uk/products/70mm--ecotherm-eco-versal-general-purpose-pir-insulation'
|
||||
'-board---2.4m-x-1.2m-x-70mm.html',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 27.089088, 'labour_cost': 28.34, 'labour_hours_per_unit': 1.2, 'plant_cost': 0.0,
|
||||
'total_cost': 55.42909,
|
||||
'notes': "This material isn't in SPONs but checking online, is around 104% of the cost of the 100mm (more "
|
||||
"expensive than 100mm)"},
|
||||
{'id': 1192, 'type': 'solid_floor_insulation',
|
||||
'description': 'Ecotherm Eco-Versal General Purpose Insulation Board', 'depth': 100.0, 'depth_unit': 'mm',
|
||||
'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 15.12,
|
||||
'material_cost': 25.96, 'labour_cost': 30.7, 'labour_hours_per_unit': 1.3, 'plant_cost': 0.0, 'total_cost': 56.66,
|
||||
'notes': None},
|
||||
{'id': 1193, 'type': 'solid_floor_insulation', 'description': 'Ravatherm XPS X 500 SL Polystyrene Foam',
|
||||
'depth': 50.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.032258064,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.031,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 11.07, 'labour_cost': 10.66, 'labour_hours_per_unit': 0.46, 'plant_cost': 0.0,
|
||||
'total_cost': 21.73,
|
||||
'notes': "In Spons, the thermal conductivity is 0.033 however the datasheet indicates it's 0.32: "
|
||||
"https://ravagobuildingsolutions.com/uk/wp-content/uploads/sites/30/2022/08/ravatherm-xps-x-500-sl-tds"
|
||||
"-version-1-20210901.pdf"},
|
||||
{'id': 1194, 'type': 'solid_floor_insulation', 'description': 'Ravatherm XPS X 500 SL Polystyrene Foam',
|
||||
'depth': 75.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.03125,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.032,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 16.28, 'labour_cost': 10.66, 'labour_hours_per_unit': 0.46, 'plant_cost': 0.0,
|
||||
'total_cost': 26.94, 'notes': None}, {'id': 1195, 'type': 'solid_floor_redecoration',
|
||||
'description': 'Screeded beds; protection to compressible formwork '
|
||||
'exceeding 600mm wide',
|
||||
'depth': 0.0, 'depth_unit': None, 'cost': None, 'cost_unit': None,
|
||||
'r_value_per_mm': None, 'r_value_unit': 'square_meter_kelvin_per_watt',
|
||||
'thermal_conductivity': None, 'thermal_conductivity_unit': None,
|
||||
'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907),
|
||||
'is_active': True, 'prime_material_cost': 9.6, 'material_cost': 9.89,
|
||||
'labour_cost': 2.67, 'labour_hours_per_unit': 0.15, 'plant_cost': 0.0,
|
||||
'total_cost': 12.56,
|
||||
'notes': 'This is the screed layer, placed on top of the insulation'},
|
||||
{'id': 1196, 'type': 'solid_floor_redecoration', 'description': 'Fitting carpet', 'depth': 0.0, 'depth_unit': None,
|
||||
'cost': None, 'cost_unit': None, 'r_value_per_mm': None, 'r_value_unit': 'square_meter_kelvin_per_watt',
|
||||
'thermal_conductivity': None, 'thermal_conductivity_unit': None, 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 0.0, 'labour_cost': 6.59, 'labour_hours_per_unit': 0.37, 'plant_cost': 0.0, 'total_cost': 6.59,
|
||||
'notes': 'SPONs does not have data on re-fitting the carpet so we use the data in Fitted carpeting; Gradus woven '
|
||||
'polypropylene tufted loop\n\n as a baseline. We assume re-use of carpets, therefore we need just '
|
||||
'labour rates'},
|
||||
{'id': 1197, 'type': 'solid_floor_redecoration',
|
||||
'description': 'Fitting existing softwood skirting or architrave to new frames; 150mm high', 'depth': 0.0,
|
||||
'depth_unit': None, 'cost': None, 'cost_unit': None, 'r_value_per_mm': None,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, 'thermal_conductivity_unit': None,
|
||||
'link': 'SPONs', 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True,
|
||||
'prime_material_cost': None, 'material_cost': 0.01, 'labour_cost': 4.87, 'labour_hours_per_unit': 0.12,
|
||||
'plant_cost': 0.0, 'total_cost': 4.88, 'notes': None}, {'id': 1198, 'type': 'ewi_wall_demolition',
|
||||
'description': 'Solid & Dry Lined walls: Hack of wall '
|
||||
'finishes with chipping hammer; plaster '
|
||||
'to walls.',
|
||||
'depth': 0.0, 'depth_unit': None, 'cost': None,
|
||||
'cost_unit': None, 'r_value_per_mm': None,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt',
|
||||
'thermal_conductivity': None,
|
||||
'thermal_conductivity_unit': None, 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12,
|
||||
244907), 'is_active': True,
|
||||
'prime_material_cost': None, 'material_cost': 0.0,
|
||||
'labour_cost': 10.27, 'labour_hours_per_unit': 0.33,
|
||||
'plant_cost': 1.28, 'total_cost': 11.55, 'notes': None},
|
||||
{'id': 1199, 'type': 'ewi_wall_demolition',
|
||||
'description': 'Stud walls: Remove wall linings including battening behind; plasterboard and skim', 'depth': 0.0,
|
||||
'depth_unit': None, 'cost': None, 'cost_unit': None, 'r_value_per_mm': None,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, 'thermal_conductivity_unit': None,
|
||||
'link': 'SPONs', 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True,
|
||||
'prime_material_cost': None, 'material_cost': 0.0, 'labour_cost': 6.23, 'labour_hours_per_unit': 0.2,
|
||||
'plant_cost': 1.25, 'total_cost': 7.48, 'notes': None}, {'id': 1200, 'type': 'ewi_wall_demolition',
|
||||
'description': 'Lathe and Plaster walls: Remove wall '
|
||||
'linings including battening behind; '
|
||||
'wood lath and plaster',
|
||||
'depth': 0.0, 'depth_unit': None, 'cost': None,
|
||||
'cost_unit': None, 'r_value_per_mm': None,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt',
|
||||
'thermal_conductivity': None,
|
||||
'thermal_conductivity_unit': None, 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12,
|
||||
244907),
|
||||
'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 0.0, 'labour_cost': 6.85,
|
||||
'labour_hours_per_unit': 0.22, 'plant_cost': 2.09,
|
||||
'total_cost': 8.94, 'notes': None},
|
||||
{'id': 1201, 'type': 'ewi_wall_preparation',
|
||||
'description': 'Clean and prepare surfaces, one coat Keim dilution, one coat primer and two coats of Keim Ecosil '
|
||||
'paint; Brick or block walls; over 300 mm girth',
|
||||
'depth': 0.0, 'depth_unit': None, 'cost': None, 'cost_unit': None, 'r_value_per_mm': None,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, 'thermal_conductivity_unit': None,
|
||||
'link': 'SPONs', 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True,
|
||||
'prime_material_cost': None, 'material_cost': 7.3, 'labour_cost': 5.62, 'labour_hours_per_unit': 0.3,
|
||||
'plant_cost': 0.0, 'total_cost': 12.92,
|
||||
'notes': 'This work covers the preparation and priming of the wall before insulating'},
|
||||
{'id': 1202, 'type': 'external_wall_insulation', 'description': 'Ecotherm Eco-Versal PIR Insulation Board',
|
||||
'depth': 30.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 6.16,
|
||||
'material_cost': 16.73, 'labour_cost': 28.34, 'labour_hours_per_unit': 1.2, 'plant_cost': 0.0, 'total_cost': 45.07,
|
||||
'notes': None},
|
||||
{'id': 1203, 'type': 'external_wall_insulation', 'description': 'Ecotherm Eco-Versal PIR Insulation Board',
|
||||
'depth': 50.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 8.46,
|
||||
'material_cost': 19.1, 'labour_cost': 28.34, 'labour_hours_per_unit': 1.2, 'plant_cost': 0.0, 'total_cost': 47.44,
|
||||
'notes': None},
|
||||
{'id': 1204, 'type': 'external_wall_insulation', 'description': 'Ecotherm Eco-Versal PIR Insulation Board',
|
||||
'depth': 100.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 15.12,
|
||||
'material_cost': 25.96, 'labour_cost': 30.7, 'labour_hours_per_unit': 1.3, 'plant_cost': 0.0, 'total_cost': 56.66,
|
||||
'notes': None},
|
||||
{'id': 1205, 'type': 'external_wall_insulation', 'description': 'Ecotherm Eco-Versal PIR Insulation Board',
|
||||
'depth': 150.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 23.53,
|
||||
'material_cost': 34.62, 'labour_cost': 33.06, 'labour_hours_per_unit': 1.4, 'plant_cost': 0.0, 'total_cost': 67.68,
|
||||
'notes': None},
|
||||
{'id': 1206, 'type': 'external_wall_insulation', 'description': 'Foamglas Grade F Wall Insulation Slabs',
|
||||
'depth': 60.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.02631579,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.038,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 41.69,
|
||||
'material_cost': 53.33, 'labour_cost': 29.52, 'labour_hours_per_unit': 1.25, 'plant_cost': 0.0,
|
||||
'total_cost': 82.85, 'notes': None},
|
||||
{'id': 1207, 'type': 'external_wall_insulation', 'description': 'Foamglas Grade F Wall Insulation Slabs',
|
||||
'depth': 100.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.02631579,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.038,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 86.86,
|
||||
'material_cost': 99.85, 'labour_cost': 29.52, 'labour_hours_per_unit': 1.25, 'plant_cost': 0.0,
|
||||
'total_cost': 129.37, 'notes': None},
|
||||
{'id': 1208, 'type': 'external_wall_insulation', 'description': 'Foamglas Grade F Wall Insulation Slabs',
|
||||
'depth': 150.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.02631579,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.038,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True,
|
||||
'prime_material_cost': 130.29, 'material_cost': 144.58, 'labour_cost': 29.52, 'labour_hours_per_unit': 1.25,
|
||||
'plant_cost': 0.0, 'total_cost': 174.1, 'notes': None}, {'id': 1209, 'type': 'ewi_wall_redecoration',
|
||||
'description': 'EPS insulation fixed with adhesive to '
|
||||
'SFS structure (measured separately) '
|
||||
'with horizontal PVC intermediate track '
|
||||
'and vertical T-spines; with glassfibre '
|
||||
'mesh reinforcement embedded in Sto '
|
||||
'Armat Classic Basecoat Render and '
|
||||
'Stolit K 1.5 Decorative Topcoat Render '
|
||||
'(white)',
|
||||
'depth': 0.0, 'depth_unit': None, 'cost': None,
|
||||
'cost_unit': None, 'r_value_per_mm': None,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt',
|
||||
'thermal_conductivity': None,
|
||||
'thermal_conductivity_unit': None, 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12,
|
||||
244907),
|
||||
'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 0.0, 'labour_cost': 0.0,
|
||||
'labour_hours_per_unit': 0.0, 'plant_cost': 0.0,
|
||||
'total_cost': 69.94,
|
||||
'notes': 'This material in SPONs is for 70mm EPS '
|
||||
'insulation, which comes in at a cost of 99.17 '
|
||||
'per meter square. This includes the cost of '
|
||||
'insulation. To get the costing for just the '
|
||||
'works and not the insulation, we subtract the '
|
||||
'cost of EPS insulation, using Ravathem 75mm '
|
||||
'insulation as an example, which costs £29.23 '
|
||||
'per meter square, giving us the cost of the '
|
||||
'remaining works without insulation. This '
|
||||
'material gives us a cost for basecoat, '
|
||||
'mesh application and a render finish'},
|
||||
{'id': 1210, 'type': 'low_energy_lighting_installation', 'description': 'Installation of fittings and cost of bub',
|
||||
'depth': 0.0, 'depth_unit': None, 'cost': None, 'cost_unit': 'gbp_per_unit', 'r_value_per_mm': None,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, 'thermal_conductivity_unit': None,
|
||||
'link': 'https://www.checkatrade.com/blog/cost-guides/cost-install-downlights/ '
|
||||
'https://www.hamuch.com/cost/led-spot-light#:~:text=It%20costs%20an%20average%20of,'
|
||||
'will%20drive%20up%20the%20cost.',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 20.0, 'labour_cost': 46.0, 'labour_hours_per_unit': 0.8, 'plant_cost': 0.0, 'total_cost': 66.0,
|
||||
'notes': 'We estimate the unit economics from the checkatrade article. We assume that the average job consists '
|
||||
'of installing 6 lights based on the hamuch article. We use the median value of 400 for a job of 6 '
|
||||
'lights'}]
|
||||
|
|
@ -37,7 +37,7 @@ class TestFirepaceRecommendations:
|
|||
|
||||
assert recommender.recommendation
|
||||
assert recommender.recommendation[0]["type"] == "sealing_open_fireplace"
|
||||
assert recommender.recommendation[0]["cost"] == 300
|
||||
assert recommender.recommendation[0]["total"] == 300
|
||||
|
||||
def test_multiple_fireplaces(self):
|
||||
property_instance = Property(id=0, address1="fake", postcode="fake", epc_client=Mock())
|
||||
|
|
@ -55,4 +55,4 @@ class TestFirepaceRecommendations:
|
|||
|
||||
assert recommender.recommendation
|
||||
assert recommender.recommendation[0]["type"] == "sealing_open_fireplace"
|
||||
assert recommender.recommendation[0]["cost"] == 900
|
||||
assert recommender.recommendation[0]["total"] == 900
|
||||
|
|
|
|||
|
|
@ -3,90 +3,15 @@ import pytest
|
|||
import os
|
||||
from unittest.mock import Mock
|
||||
from recommendations.FloorRecommendations import FloorRecommendations
|
||||
from recommendations.tests.test_data.materials import materials
|
||||
from backend.Property import Property
|
||||
|
||||
|
||||
# with open(
|
||||
# os.path.abspath(os.path.dirname(__file__)) + "/recommendations/tests/test_data/input_properties.pkl", "rb"
|
||||
# ) as f:
|
||||
# input_properties = pickle.load(f)
|
||||
|
||||
suspended_floor_insulation_parts = [
|
||||
{
|
||||
# Example product
|
||||
# https://www.insulationsuperstore.co.uk/product/recticel-eurothane-general-purpose-pir-insulation-board-2400
|
||||
# -x-1200-x-100mm.html
|
||||
# All product data_types here:
|
||||
# https://www.insulationsuperstore.co.uk/browse/insulation/brand/recticel/filterby/application/floors.html
|
||||
"type": "suspended_floor_insulation",
|
||||
"description": "Rigid Insulation Foam Boards",
|
||||
"depths": [25, 30, 40, 50, 60, 70, 75, 80, 90, 100, 110, 120, 130, 140, 150],
|
||||
"depth_unit": "mm",
|
||||
"cost": [25, 30, 40, 50, 60, 70, 75, 80, 90, 100, 110, 120, 130, 140, 150],
|
||||
"cost_unit": "gbp_sq_meter",
|
||||
"r_value_per_mm": 0.04545454545454546,
|
||||
"r_value_unit": "square_meter_kelvin_per_watt",
|
||||
"thermal_conductivity": 0.022,
|
||||
"thermal_conductivity_unit": "watt_per_meter_kelvin"
|
||||
},
|
||||
{
|
||||
# Example product
|
||||
# https://www.insulationsuperstore.co.uk/product/rockwool-rwa45-acoustic-insulation-slab-100mm-2-88m2-pack.html
|
||||
# All product data_types here:
|
||||
# https://www.insulationsuperstore.co.uk/browse/insulation/brand/rockwool/filterby/application/floors
|
||||
# /material/mineral-wool.html
|
||||
"type": "suspended_floor_insulation",
|
||||
"description": "Mineral Wool Floor Insulation",
|
||||
"depths": [25, 40, 50, 60, 75, 100],
|
||||
"depth_unit": "mm",
|
||||
"cost": [25, 40, 50, 60, 75, 100],
|
||||
"cost_unit": "gbp_sq_meter",
|
||||
"r_value_per_mm": 0.02857142857142857,
|
||||
"r_value_unit": "square_meter_kelvin_per_watt",
|
||||
"thermal_conductivity": 0.035,
|
||||
"thermal_conductivity_unit": "watt_per_meter_kelvin"
|
||||
},
|
||||
]
|
||||
|
||||
solid_floor_insulation_parts = [
|
||||
{
|
||||
# Example product
|
||||
# https://www.insulationexpress.co.uk/floor-insulation/solid-floor-insulation/k103-100mm
|
||||
# All product data_types here:
|
||||
# https://www.insulationexpress.co.uk/floor-insulation/solid-floor-insulation?brand=7015&p=1
|
||||
# Example screed https://www.screwfix.com/p/mapei-ultraplan-3240-self-levelling-compound-25kg/4959f
|
||||
"type": "solid_floor_insulation",
|
||||
"description": "Rigid Insulation Foam Boards with floor screed",
|
||||
"depths": [25, 50, 70, 75, 100],
|
||||
"depth_unit": "mm",
|
||||
"cost": [25, 40, 50, 60, 75, 100],
|
||||
"cost_unit": "gbp_sq_meter",
|
||||
"r_value_per_mm": 0.04545454545454546,
|
||||
"r_value_unit": "square_meter_kelvin_per_watt",
|
||||
"thermal_conductivity": 0.052631578947368425,
|
||||
"thermal_conductivity_unit": "watt_per_meter_kelvin"
|
||||
},
|
||||
|
||||
]
|
||||
|
||||
exposed_floor_insulation_parts = [
|
||||
{
|
||||
"type": "exposed_floor_insulation",
|
||||
"description": "Rockwool Stone Wool insulation",
|
||||
"depths": [50, 100, 140],
|
||||
"depth_unit": "mm",
|
||||
"cost": [8, 11, 15],
|
||||
"cost_unit": "gbp_sq_meter",
|
||||
"r_value_per_mm": 0.026315789473684213,
|
||||
"r_value_unit": "square_meter_kelvin_per_watt",
|
||||
"thermal_conductivity": 0.038,
|
||||
"thermal_conductivity_unit": "watt_per_meter_kelvin",
|
||||
"link": "https://insulation4less.co.uk/products/rockwool-flexi-slab-all-sizes?variant=33409590853685"
|
||||
},
|
||||
]
|
||||
|
||||
parts = suspended_floor_insulation_parts + solid_floor_insulation_parts + exposed_floor_insulation_parts
|
||||
|
||||
|
||||
class TestFloorRecommendations:
|
||||
|
||||
@pytest.fixture
|
||||
|
|
@ -100,26 +25,29 @@ class TestFloorRecommendations:
|
|||
def mock_floor_rec_instance(self):
|
||||
# Creating a mock instance of WallRecommendations with the necessary attributes
|
||||
property_mock = Mock()
|
||||
property_mock.full_sap_epc = {"lodgement-date": "2000-01-01"} # or any date you want
|
||||
property_mock.data = {"construction-age-band": "1950"} # or any other data that fits your tests
|
||||
property_mock.full_sap_epc = {"lodgement-date": "2000-01-01"}
|
||||
property_mock.data = {"county": "York"}
|
||||
|
||||
mock_wall_rec_instance = FloorRecommendations(property_mock, parts)
|
||||
mock_wall_rec_instance = FloorRecommendations(property_mock, materials)
|
||||
return mock_wall_rec_instance
|
||||
|
||||
def test_init(self, input_properties):
|
||||
input_properties[0].insulation_floor_area = 50
|
||||
input_properties[0].insulation_wall_area = 90
|
||||
obj = FloorRecommendations(
|
||||
property_instance=input_properties[0],
|
||||
materials=parts
|
||||
materials=materials
|
||||
)
|
||||
assert obj
|
||||
assert obj.property
|
||||
|
||||
def test_other_premises_below(self, input_properties):
|
||||
input_properties[0].floor_area = 100
|
||||
input_properties[0].insulation_floor_area = 100
|
||||
input_properties[0].insulation_wall_area = 999
|
||||
input_properties[0].number_of_floors = 1
|
||||
recommender = FloorRecommendations(
|
||||
property_instance=input_properties[0],
|
||||
materials=parts
|
||||
materials=materials
|
||||
)
|
||||
recommender.recommend()
|
||||
assert recommender.property.floor["another_property_below"]
|
||||
|
|
@ -132,7 +60,8 @@ class TestFloorRecommendations:
|
|||
:return:
|
||||
"""
|
||||
|
||||
input_properties[2].floor_area = 50
|
||||
input_properties[2].insulation_floor_area = 50
|
||||
input_properties[2].insulation_wall_area = 50
|
||||
input_properties[2].walls["is_park_home"] = False
|
||||
input_properties[2].age_band = "A"
|
||||
input_properties[2].perimeter = 20
|
||||
|
|
@ -140,10 +69,7 @@ class TestFloorRecommendations:
|
|||
input_properties[2].floor_type = "suspended"
|
||||
input_properties[2].number_of_floors = 1
|
||||
|
||||
recommender = FloorRecommendations(
|
||||
property_instance=input_properties[2],
|
||||
materials=parts
|
||||
)
|
||||
recommender = FloorRecommendations(property_instance=input_properties[2], materials=materials)
|
||||
assert recommender.estimated_u_value is None
|
||||
recommender.recommend()
|
||||
assert recommender.property.floor["is_suspended"]
|
||||
|
|
@ -154,18 +80,20 @@ class TestFloorRecommendations:
|
|||
|
||||
assert types == {"suspended_floor_insulation"}
|
||||
|
||||
assert len(recommender.recommendations) == 6
|
||||
assert recommender.recommendations[0]["total"] == 4596.858
|
||||
assert recommender.recommendations[0]["new_u_value"] == 0.21
|
||||
|
||||
def test_uvalue_0_12(self, input_properties):
|
||||
"""
|
||||
This is a home that doesn't have a property below but it's highly performant already and therefore
|
||||
does not need floor insulation
|
||||
:return:
|
||||
"""
|
||||
input_properties[3].floor_area = 100
|
||||
input_properties[3].insulation_floor_area = 100
|
||||
input_properties[3].insulation_wall_area = 100
|
||||
input_properties[3].number_of_floors = 1
|
||||
recommender = FloorRecommendations(
|
||||
property_instance=input_properties[3],
|
||||
materials=parts
|
||||
)
|
||||
recommender = FloorRecommendations(property_instance=input_properties[3], materials=materials)
|
||||
assert recommender.estimated_u_value is None
|
||||
recommender.recommend()
|
||||
assert not recommender.property.floor["is_suspended"]
|
||||
|
|
@ -178,7 +106,8 @@ class TestFloorRecommendations:
|
|||
:return:
|
||||
"""
|
||||
|
||||
input_properties[4].floor_area = 100
|
||||
input_properties[4].insulation_floor_area = 100
|
||||
input_properties[4].insulation_wall_area = 100
|
||||
input_properties[4].walls["is_park_home"] = False
|
||||
input_properties[4].age_band = "B"
|
||||
input_properties[4].perimeter = 50
|
||||
|
|
@ -186,10 +115,9 @@ class TestFloorRecommendations:
|
|||
input_properties[4].floor_type = "solid"
|
||||
input_properties[4].number_of_floors = 1
|
||||
|
||||
recommender = FloorRecommendations(
|
||||
property_instance=input_properties[4],
|
||||
materials=parts
|
||||
)
|
||||
# In this case, we have no county, so in this case, it should yse the local-authority-label if possible
|
||||
input_properties[4].data["county"] = ""
|
||||
recommender = FloorRecommendations(property_instance=input_properties[4], materials=materials)
|
||||
assert recommender.estimated_u_value is None
|
||||
recommender.recommend()
|
||||
assert not recommender.property.floor["is_suspended"]
|
||||
|
|
@ -201,17 +129,22 @@ class TestFloorRecommendations:
|
|||
|
||||
assert types == {"solid_floor_insulation"}
|
||||
|
||||
assert len(recommender.recommendations) == 3
|
||||
assert recommender.recommendations[2]["total"] == 14604.660000000002
|
||||
assert recommender.recommendations[2]["new_u_value"] == 0.21
|
||||
assert recommender.recommendations[2]["parts"][0]["depth"] == 75
|
||||
assert recommender.recommendations[2]["parts"][0]["depth"] == 75
|
||||
|
||||
def test_another_dwelling_below(self, input_properties):
|
||||
"""
|
||||
This is another description we see when there is a property below
|
||||
"""
|
||||
|
||||
input_properties[6].floor_area = 100
|
||||
input_properties[6].insulation_floor_area = 100
|
||||
input_properties[6].insulation_wall_area = 1
|
||||
|
||||
input_properties[6].number_of_floors = 1
|
||||
recommender = FloorRecommendations(
|
||||
property_instance=input_properties[6],
|
||||
materials=parts
|
||||
)
|
||||
recommender = FloorRecommendations(property_instance=input_properties[6], materials=materials)
|
||||
assert recommender.estimated_u_value is None
|
||||
recommender.recommend()
|
||||
assert not recommender.property.floor["is_suspended"]
|
||||
|
|
@ -219,123 +152,123 @@ class TestFloorRecommendations:
|
|||
assert recommender.estimated_u_value is None
|
||||
assert not recommender.recommendations
|
||||
|
||||
def test_exposed_floor_no_insulation(self):
|
||||
input_property = Property(id=1, postcode="F4k3 2", address1="223 fake street", epc_client=Mock())
|
||||
input_property.floor = {
|
||||
'original_description': 'To unheated space, no insulation (assumed)',
|
||||
'clean_description': 'To unheated space, no insulation', 'thermal_transmittance': None,
|
||||
'thermal_transmittance_unit': None, 'is_assumed': True, 'is_to_unheated_space': True,
|
||||
'is_to_external_air': False, 'is_suspended': False, 'is_solid': False, 'another_property_below': False,
|
||||
'insulation_thickness': 'none'
|
||||
}
|
||||
input_property.age_band = "L"
|
||||
input_property.set_floor_type()
|
||||
input_property.data = {"floor-level": 0, "property-type": "House"}
|
||||
input_property.floor_area = 100
|
||||
input_property.number_of_floors = 1
|
||||
|
||||
recommender = FloorRecommendations(
|
||||
property_instance=input_property,
|
||||
materials=exposed_floor_insulation_parts
|
||||
)
|
||||
|
||||
assert not recommender.recommendations
|
||||
|
||||
recommender.recommend()
|
||||
|
||||
# Because of age band L, this should have a u-value of 0.22 to begin with and no recommendation
|
||||
assert not len(recommender.recommendations)
|
||||
assert recommender.estimated_u_value == 0.22
|
||||
|
||||
# Now with an older age band
|
||||
|
||||
input_property2 = Property(id=1, postcode="F4k3 2", address1="223 fake street", epc_client=Mock())
|
||||
input_property2.floor = {
|
||||
'original_description': 'To unheated space, no insulation (assumed)',
|
||||
'clean_description': 'To unheated space, no insulation', 'thermal_transmittance': None,
|
||||
'thermal_transmittance_unit': None, 'is_assumed': True, 'is_to_unheated_space': True,
|
||||
'is_to_external_air': False, 'is_suspended': False, 'is_solid': False, 'another_property_below': False,
|
||||
'insulation_thickness': 'none'
|
||||
}
|
||||
input_property2.age_band = "D"
|
||||
input_property2.set_floor_type()
|
||||
input_property2.data = {"floor-level": 0, "property-type": "House"}
|
||||
input_property2.floor_area = 100
|
||||
input_property2.number_of_floors = 1
|
||||
|
||||
recommender2 = FloorRecommendations(
|
||||
property_instance=input_property2,
|
||||
materials=exposed_floor_insulation_parts
|
||||
)
|
||||
|
||||
assert not recommender2.recommendations
|
||||
|
||||
recommender2.recommend()
|
||||
|
||||
assert len(recommender2.recommendations) == 1
|
||||
|
||||
assert recommender2.recommendations[0]["new_u_value"] == 0.23
|
||||
assert recommender2.recommendations[0]["starting_u_value"] == 1.2
|
||||
assert recommender2.recommendations[0]["cost"] == 1500
|
||||
|
||||
def test_exposed_floor_below_average_insulated(self):
|
||||
input_property3 = Property(id=1, postcode="F4k3 2", address1="223 fake street", epc_client=Mock())
|
||||
input_property3.floor = {
|
||||
'original_description': 'To unheated space, below average insulation (assumed)',
|
||||
'clean_description': 'To unheated space, below average insulation', 'thermal_transmittance': None,
|
||||
'thermal_transmittance_unit': None, 'is_assumed': True, 'is_to_unheated_space': True,
|
||||
'is_to_external_air': False, 'is_suspended': False, 'is_solid': False, 'another_property_below': False,
|
||||
'insulation_thickness': 'below average'
|
||||
}
|
||||
input_property3.age_band = "C"
|
||||
input_property3.set_floor_type()
|
||||
input_property3.data = {"floor-level": 0, "property-type": "House"}
|
||||
input_property3.floor_area = 100
|
||||
input_property3.number_of_floors = 1
|
||||
|
||||
recommender3 = FloorRecommendations(
|
||||
property_instance=input_property3,
|
||||
materials=exposed_floor_insulation_parts
|
||||
)
|
||||
|
||||
assert not recommender3.recommendations
|
||||
|
||||
recommender3.recommend()
|
||||
|
||||
assert recommender3.estimated_u_value == 0.5
|
||||
|
||||
assert len(recommender3.recommendations) == 1
|
||||
|
||||
assert recommender3.recommendations[0]["new_u_value"] == 0.22
|
||||
assert recommender3.recommendations[0]["starting_u_value"] == 0.5
|
||||
assert recommender3.recommendations[0]["cost"] == 1100
|
||||
assert recommender3.recommendations[0]["parts"][0]["depths"] == [100]
|
||||
|
||||
# With average insulation, no recommendations
|
||||
|
||||
input_property4 = Property(id=1, postcode="F4k3 2", address1="223 fake street", epc_client=Mock())
|
||||
input_property4.floor = {
|
||||
'original_description': 'To unheated space, insulated (assumed)',
|
||||
'clean_description': 'To unheated space, insulated', 'thermal_transmittance': None,
|
||||
'thermal_transmittance_unit': None, 'is_assumed': True, 'is_to_unheated_space': True,
|
||||
'is_to_external_air': False, 'is_suspended': False, 'is_solid': False, 'another_property_below': False,
|
||||
'insulation_thickness': 'average'
|
||||
}
|
||||
input_property4.age_band = "C"
|
||||
input_property4.set_floor_type()
|
||||
input_property4.data = {"floor-level": 0, "property-type": "House"}
|
||||
input_property4.floor_area = 100
|
||||
input_property4.number_of_floors = 1
|
||||
|
||||
recommender4 = FloorRecommendations(
|
||||
property_instance=input_property4,
|
||||
materials=exposed_floor_insulation_parts
|
||||
)
|
||||
|
||||
assert not recommender4.recommendations
|
||||
|
||||
recommender4.recommend()
|
||||
|
||||
assert recommender4.estimated_u_value is None
|
||||
|
||||
assert len(recommender4.recommendations) == 0
|
||||
# def test_exposed_floor_no_insulation(self):
|
||||
# input_property = Property(id=1, postcode="F4k3 2", address1="223 fake street", epc_client=Mock())
|
||||
# input_property.floor = {
|
||||
# 'original_description': 'To unheated space, no insulation (assumed)',
|
||||
# 'clean_description': 'To unheated space, no insulation', 'thermal_transmittance': None,
|
||||
# 'thermal_transmittance_unit': None, 'is_assumed': True, 'is_to_unheated_space': True,
|
||||
# 'is_to_external_air': False, 'is_suspended': False, 'is_solid': False, 'another_property_below': False,
|
||||
# 'insulation_thickness': 'none'
|
||||
# }
|
||||
# input_property.age_band = "L"
|
||||
# input_property.set_floor_type()
|
||||
# input_property.data = {"floor-level": 0, "property-type": "House"}
|
||||
# input_property.floor_area = 100
|
||||
# input_property.number_of_floors = 1
|
||||
#
|
||||
# recommender = FloorRecommendations(
|
||||
# property_instance=input_property,
|
||||
# materials=materials
|
||||
# )
|
||||
#
|
||||
# assert not recommender.recommendations
|
||||
#
|
||||
# recommender.recommend()
|
||||
#
|
||||
# # Because of age band L, this should have a u-value of 0.22 to begin with and no recommendation
|
||||
# assert not len(recommender.recommendations)
|
||||
# assert recommender.estimated_u_value == 0.22
|
||||
#
|
||||
# # Now with an older age band
|
||||
#
|
||||
# input_property2 = Property(id=1, postcode="F4k3 2", address1="223 fake street", epc_client=Mock())
|
||||
# input_property2.floor = {
|
||||
# 'original_description': 'To unheated space, no insulation (assumed)',
|
||||
# 'clean_description': 'To unheated space, no insulation', 'thermal_transmittance': None,
|
||||
# 'thermal_transmittance_unit': None, 'is_assumed': True, 'is_to_unheated_space': True,
|
||||
# 'is_to_external_air': False, 'is_suspended': False, 'is_solid': False, 'another_property_below': False,
|
||||
# 'insulation_thickness': 'none'
|
||||
# }
|
||||
# input_property2.age_band = "D"
|
||||
# input_property2.set_floor_type()
|
||||
# input_property2.data = {"floor-level": 0, "property-type": "House"}
|
||||
# input_property2.floor_area = 100
|
||||
# input_property2.number_of_floors = 1
|
||||
#
|
||||
# recommender2 = FloorRecommendations(
|
||||
# property_instance=input_property2,
|
||||
# materials=materials
|
||||
# )
|
||||
#
|
||||
# assert not recommender2.recommendations
|
||||
#
|
||||
# recommender2.recommend()
|
||||
#
|
||||
# assert len(recommender2.recommendations) == 1
|
||||
#
|
||||
# assert recommender2.recommendations[0]["new_u_value"] == 0.23
|
||||
# assert recommender2.recommendations[0]["starting_u_value"] == 1.2
|
||||
# assert recommender2.recommendations[0]["cost"] == 1500
|
||||
#
|
||||
# def test_exposed_floor_below_average_insulated(self):
|
||||
# input_property3 = Property(id=1, postcode="F4k3 2", address1="223 fake street", epc_client=Mock())
|
||||
# input_property3.floor = {
|
||||
# 'original_description': 'To unheated space, below average insulation (assumed)',
|
||||
# 'clean_description': 'To unheated space, below average insulation', 'thermal_transmittance': None,
|
||||
# 'thermal_transmittance_unit': None, 'is_assumed': True, 'is_to_unheated_space': True,
|
||||
# 'is_to_external_air': False, 'is_suspended': False, 'is_solid': False, 'another_property_below': False,
|
||||
# 'insulation_thickness': 'below average'
|
||||
# }
|
||||
# input_property3.age_band = "C"
|
||||
# input_property3.set_floor_type()
|
||||
# input_property3.data = {"floor-level": 0, "property-type": "House"}
|
||||
# input_property3.floor_area = 100
|
||||
# input_property3.number_of_floors = 1
|
||||
#
|
||||
# recommender3 = FloorRecommendations(
|
||||
# property_instance=input_property3,
|
||||
# materials=materials
|
||||
# )
|
||||
#
|
||||
# assert not recommender3.recommendations
|
||||
#
|
||||
# recommender3.recommend()
|
||||
#
|
||||
# assert recommender3.estimated_u_value == 0.5
|
||||
#
|
||||
# assert len(recommender3.recommendations) == 1
|
||||
#
|
||||
# assert recommender3.recommendations[0]["new_u_value"] == 0.22
|
||||
# assert recommender3.recommendations[0]["starting_u_value"] == 0.5
|
||||
# assert recommender3.recommendations[0]["cost"] == 1100
|
||||
# assert recommender3.recommendations[0]["parts"][0]["depths"] == [100]
|
||||
#
|
||||
# # With average insulation, no recommendations
|
||||
#
|
||||
# input_property4 = Property(id=1, postcode="F4k3 2", address1="223 fake street", epc_client=Mock())
|
||||
# input_property4.floor = {
|
||||
# 'original_description': 'To unheated space, insulated (assumed)',
|
||||
# 'clean_description': 'To unheated space, insulated', 'thermal_transmittance': None,
|
||||
# 'thermal_transmittance_unit': None, 'is_assumed': True, 'is_to_unheated_space': True,
|
||||
# 'is_to_external_air': False, 'is_suspended': False, 'is_solid': False, 'another_property_below': False,
|
||||
# 'insulation_thickness': 'average'
|
||||
# }
|
||||
# input_property4.age_band = "C"
|
||||
# input_property4.set_floor_type()
|
||||
# input_property4.data = {"floor-level": 0, "property-type": "House"}
|
||||
# input_property4.floor_area = 100
|
||||
# input_property4.number_of_floors = 1
|
||||
#
|
||||
# recommender4 = FloorRecommendations(
|
||||
# property_instance=input_property4,
|
||||
# materials=materials
|
||||
# )
|
||||
#
|
||||
# assert not recommender4.recommendations
|
||||
#
|
||||
# recommender4.recommend()
|
||||
#
|
||||
# assert recommender4.estimated_u_value is None
|
||||
#
|
||||
# assert len(recommender4.recommendations) == 0
|
||||
|
|
|
|||
47
recommendations/tests/test_lighting_recommendations.py
Normal file
47
recommendations/tests/test_lighting_recommendations.py
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import pytest
|
||||
from unittest.mock import Mock
|
||||
from backend.Property import Property
|
||||
from recommendations.LightingRecommendations import LightingRecommendations
|
||||
|
||||
from recommendations.tests.test_data.materials import materials
|
||||
|
||||
|
||||
class TestLightingRecommendations:
|
||||
|
||||
def test_init_invalid_materials(self):
|
||||
input_property0 = Property(id=1, postcode="F4k3 6", address1="623 fake street", epc_client=Mock())
|
||||
input_property0.lighting = {"low_energy_proportion": 0}
|
||||
input_property0.data = {"county": "Greater London Authority"}
|
||||
# Test for invalid materials
|
||||
with pytest.raises(ValueError):
|
||||
LightingRecommendations(input_property0, [])
|
||||
|
||||
def test_recommend_no_action_needed(self):
|
||||
# Case where no recommendation is needed
|
||||
input_property1 = Property(id=1, postcode="F4k3 6", address1="623 fake street", epc_client=Mock())
|
||||
input_property1.lighting = {"low_energy_proportion": 100}
|
||||
input_property1.data = {"county": "Greater London Authority"}
|
||||
|
||||
lr = LightingRecommendations(input_property1, materials)
|
||||
lr.recommend()
|
||||
assert lr.recommendation == []
|
||||
|
||||
def test_recommend_action_needed(self):
|
||||
# Case where recommendation is needed
|
||||
input_property1 = Property(id=1, postcode="F4k3 6", address1="623 fake street", epc_client=Mock())
|
||||
input_property1.lighting = {"low_energy_proportion": 100}
|
||||
input_property1.data = {"county": "Greater London Authority"}
|
||||
input_property1.lighting = {"low_energy_proportion": 0.80}
|
||||
input_property1.number_lighting_outlets = 20
|
||||
|
||||
lr = LightingRecommendations(input_property1, materials)
|
||||
lr.recommend()
|
||||
assert len(lr.recommendation) == 1
|
||||
|
||||
assert lr.recommendation == [
|
||||
{'parts': [], 'type': 'low_energy_lighting', 'description': 'Install low energy lighting in 4 outlets',
|
||||
'starting_u_value': None, 'new_u_value': None, 'sap_points': 0.4, 'total': 458.976, 'subtotal': 382.48,
|
||||
'vat': 76.49600000000001, 'contingency': 27.320000000000007, 'preliminaries': 27.320000000000007,
|
||||
'material': 80.0, 'profit': 54.640000000000015, 'labour_hours': 3.2, 'labour_days': 0.4,
|
||||
'labour_cost': 193.20000000000002}
|
||||
]
|
||||
|
|
@ -42,10 +42,12 @@ class TestRecommendationUtils:
|
|||
assert recommendation_utils.update_lowest_selected_u_value(1, 0.5) == 0.5
|
||||
|
||||
def test_get_recommended_part(self):
|
||||
part = {'depths': [1, 2, 3]}
|
||||
part = {'description': "some insulation material"}
|
||||
|
||||
assert recommendation_utils.get_recommended_part(
|
||||
part=part, selected_depth=1, selected_total_cost=50, quantity=99, quantity_unit="m2"
|
||||
) == {'depths': [1], 'estimated_cost': 50, 'quantity': 99, 'quantity_unit': QuantityUnits.m2.value}
|
||||
part=part, cost_result={"cost_result": 123}, quantity=99, quantity_unit="m2"
|
||||
) == {'description': "some insulation material", 'quantity': 99, 'quantity_unit': QuantityUnits.m2.value,
|
||||
"cost_result": 123}
|
||||
|
||||
def test_get_roof_u_value(self):
|
||||
# Test case 1: Insulation thickness is known and is_loft is True
|
||||
|
|
|
|||
|
|
@ -1,65 +1,7 @@
|
|||
from backend.Property import Property
|
||||
from unittest.mock import Mock
|
||||
from recommendations.RoofRecommendations import RoofRecommendations
|
||||
|
||||
loft_insulation_materials = [
|
||||
{
|
||||
'id': 18, 'type': 'loft_insulation', 'description': 'Iso Spacesaver Mineral Wool insulation',
|
||||
'depths': [270, 300], 'depth_unit': 'mm', 'cost': [9, 10], 'cost_unit': 'gbp_sq_meter',
|
||||
'r_value_per_mm': 0.022727273, 'r_value_unit': 'square_meter_kelvin_per_watt',
|
||||
'thermal_conductivity': 0.044, 'thermal_conductivity_unit': 'watt_per_meter_kelvin',
|
||||
'link': 'https://flooringwarehousedirect.co.uk/product/isover-spacesaver-roll-100mm-x-1160mm-x-12-18m-14-13m2/',
|
||||
'is_active': True
|
||||
}
|
||||
]
|
||||
|
||||
loft_insulation_materials_50mm_existing = [
|
||||
{
|
||||
'id': 18, 'type': 'loft_insulation', 'description': 'Iso Spacesaver Mineral Wool insulation',
|
||||
'depths': [220, 210], 'depth_unit': 'mm', 'cost': [9, 10], 'cost_unit': 'gbp_sq_meter',
|
||||
'r_value_per_mm': 0.022727273, 'r_value_unit': 'square_meter_kelvin_per_watt',
|
||||
'thermal_conductivity': 0.044, 'thermal_conductivity_unit': 'watt_per_meter_kelvin',
|
||||
'link': 'https://flooringwarehousedirect.co.uk/product/isover-spacesaver-roll-100mm-x-1160mm-x-12-18m-14-13m2/',
|
||||
'is_active': True
|
||||
}
|
||||
]
|
||||
|
||||
loft_insulation_materials_150mm_existing = [
|
||||
{
|
||||
'id': 18, 'type': 'loft_insulation', 'description': 'Iso Spacesaver Mineral Wool insulation',
|
||||
'depths': [130, 119], 'depth_unit': 'mm', 'cost': [9, 10], 'cost_unit': 'gbp_sq_meter',
|
||||
'r_value_per_mm': 0.022727273, 'r_value_unit': 'square_meter_kelvin_per_watt',
|
||||
'thermal_conductivity': 0.044, 'thermal_conductivity_unit': 'watt_per_meter_kelvin',
|
||||
'link': 'https://flooringwarehousedirect.co.uk/product/isover-spacesaver-roll-100mm-x-1160mm-x-12-18m-14-13m2/',
|
||||
'is_active': True
|
||||
}
|
||||
]
|
||||
|
||||
room_roof_insulation_materials = [
|
||||
{
|
||||
'id': 18,
|
||||
'type': 'room_roof_insulation',
|
||||
'description': 'Example room roof insulation',
|
||||
'depths': [50, 150, 220, 270, 300], 'depth_unit': 'mm', 'cost': [9, 10, 11, 12, 13],
|
||||
'cost_unit': 'gbp_sq_meter',
|
||||
'r_value_per_mm': 0.022727273, 'r_value_unit': 'square_meter_kelvin_per_watt',
|
||||
'thermal_conductivity': 0.044, 'thermal_conductivity_unit': 'watt_per_meter_kelvin',
|
||||
'link': None, 'is_active': True
|
||||
}
|
||||
]
|
||||
|
||||
flat_roof_insulation_materials = [
|
||||
{
|
||||
'id': 18,
|
||||
'type': 'flat_roof_insulation',
|
||||
'description': 'Example flat roof insulation',
|
||||
'depths': [50, 150, 220, 270, 300], 'depth_unit': 'mm', 'cost': [9, 10, 11, 12, 13],
|
||||
'cost_unit': 'gbp_sq_meter',
|
||||
'r_value_per_mm': 0.032727273, 'r_value_unit': 'square_meter_kelvin_per_watt',
|
||||
'thermal_conductivity': 0.044, 'thermal_conductivity_unit': 'watt_per_meter_kelvin',
|
||||
'link': None, 'is_active': True
|
||||
}
|
||||
]
|
||||
from recommendations.tests.test_data.materials import materials
|
||||
|
||||
|
||||
class TestRoofRecommendations:
|
||||
|
|
@ -67,7 +9,7 @@ class TestRoofRecommendations:
|
|||
def test_loft_insulation_recommendation_no_insulation(self):
|
||||
property_instance = Property(id=0, address1="fake", postcode="fake", epc_client=Mock())
|
||||
property_instance.age_band = "F"
|
||||
property_instance.floor_area = 100
|
||||
property_instance.insulation_floor_area = 100
|
||||
property_instance.roof = {
|
||||
'original_description': 'Pitched, no insulation (assumed)',
|
||||
'clean_description': 'Pitched, no insulation',
|
||||
|
|
@ -77,8 +19,11 @@ class TestRoofRecommendations:
|
|||
'is_at_rafters': False, 'is_assumed': True, 'has_dwelling_above': False, 'is_valid': True,
|
||||
'insulation_thickness': 'none', 'roof_thermal_transmittance': None, 'roof_insulation_thickness': 'none'
|
||||
}
|
||||
property_instance.data = {
|
||||
"county": "Cambridgeshire",
|
||||
}
|
||||
|
||||
roof_recommender = RoofRecommendations(property_instance=property_instance, materials=loft_insulation_materials)
|
||||
roof_recommender = RoofRecommendations(property_instance=property_instance, materials=materials)
|
||||
|
||||
assert not roof_recommender.recommendations
|
||||
|
||||
|
|
@ -89,7 +34,7 @@ class TestRoofRecommendations:
|
|||
def test_loft_insulation_recommendation_50mm_insulation(self):
|
||||
property_instance2 = Property(id=0, address1="fake", postcode="fake", epc_client=Mock())
|
||||
property_instance2.age_band = "F"
|
||||
property_instance2.floor_area = 100
|
||||
property_instance2.insulation_floor_area = 100
|
||||
property_instance2.roof = {
|
||||
'original_description': 'Pitched, 50mm loft insulation (assumed)',
|
||||
'clean_description': 'Pitched, 50mm loft insulation',
|
||||
|
|
@ -99,10 +44,9 @@ class TestRoofRecommendations:
|
|||
'is_at_rafters': False, 'is_assumed': True, 'has_dwelling_above': False, 'is_valid': True,
|
||||
'insulation_thickness': '50', 'roof_thermal_transmittance': None, 'roof_insulation_thickness': 'none'
|
||||
}
|
||||
property_instance2.data = {"county": "Kent"}
|
||||
|
||||
roof_recommender2 = RoofRecommendations(
|
||||
property_instance=property_instance2, materials=loft_insulation_materials
|
||||
)
|
||||
roof_recommender2 = RoofRecommendations(property_instance=property_instance2, materials=materials)
|
||||
|
||||
assert not roof_recommender2.recommendations
|
||||
|
||||
|
|
@ -110,13 +54,13 @@ class TestRoofRecommendations:
|
|||
|
||||
assert len(roof_recommender2.recommendations) == 1
|
||||
|
||||
assert roof_recommender2.recommendations[0]["cost"] == 900
|
||||
assert roof_recommender2.recommendations[0]["total"] == 1310.56464
|
||||
assert roof_recommender2.recommendations[0]["new_u_value"] == 0.14
|
||||
assert roof_recommender2.recommendations[0]["starting_u_value"] == 0.68
|
||||
|
||||
property_instance3 = Property(id=0, address1="fake", postcode="fake", epc_client=Mock())
|
||||
property_instance3.age_band = "F"
|
||||
property_instance3.floor_area = 100
|
||||
property_instance3.insulation_floor_area = 100
|
||||
property_instance3.roof = {
|
||||
'original_description': 'Pitched, 50mm loft insulation (assumed)',
|
||||
'clean_description': 'Pitched, 50mm loft insulation',
|
||||
|
|
@ -126,24 +70,22 @@ class TestRoofRecommendations:
|
|||
'is_at_rafters': False, 'is_assumed': True, 'has_dwelling_above': False, 'is_valid': True,
|
||||
'insulation_thickness': '50', 'roof_thermal_transmittance': None, 'roof_insulation_thickness': 'none'
|
||||
}
|
||||
property_instance3.data = {"county": "Greater London Authority"}
|
||||
|
||||
roof_recommender3 = RoofRecommendations(
|
||||
property_instance=property_instance3, materials=loft_insulation_materials_50mm_existing
|
||||
)
|
||||
roof_recommender3 = RoofRecommendations(property_instance=property_instance3, materials=materials)
|
||||
|
||||
assert not roof_recommender3.recommendations
|
||||
|
||||
roof_recommender3.recommend()
|
||||
|
||||
# The 220mm insulation should be selected, not the 210
|
||||
assert roof_recommender3.recommendations
|
||||
assert len(roof_recommender3.recommendations) == 1
|
||||
assert roof_recommender3.recommendations[0]["parts"][0]["depths"] == [220]
|
||||
assert roof_recommender3.recommendations[0]["parts"][0]["depth"] == 270
|
||||
|
||||
def test_loft_insulation_recommendation_150mm_insulation(self):
|
||||
property_instance4 = Property(id=0, address1="fake", postcode="fake", epc_client=Mock())
|
||||
property_instance4.age_band = "F"
|
||||
property_instance4.floor_area = 100
|
||||
property_instance4.insulation_floor_area = 100
|
||||
property_instance4.roof = {
|
||||
'original_description': 'Pitched, 150mm loft insulation (assumed)',
|
||||
'clean_description': 'Pitched, 150mm loft insulation',
|
||||
|
|
@ -153,24 +95,24 @@ class TestRoofRecommendations:
|
|||
'is_at_rafters': False, 'is_assumed': True, 'has_dwelling_above': False, 'is_valid': True,
|
||||
'insulation_thickness': '150', 'roof_thermal_transmittance': None, 'roof_insulation_thickness': 'none'
|
||||
}
|
||||
property_instance4.data = {"county": "North East Lincolnshire"}
|
||||
|
||||
roof_recommender4 = RoofRecommendations(
|
||||
property_instance=property_instance4, materials=loft_insulation_materials
|
||||
)
|
||||
roof_recommender4 = RoofRecommendations(property_instance=property_instance4, materials=materials)
|
||||
|
||||
assert not roof_recommender4.recommendations
|
||||
|
||||
roof_recommender4.recommend()
|
||||
|
||||
assert len(roof_recommender4.recommendations) == 1
|
||||
assert len(roof_recommender4.recommendations) == 4
|
||||
|
||||
assert roof_recommender4.recommendations[0]["cost"] == 900
|
||||
assert roof_recommender4.recommendations[0]["new_u_value"] == 0.11
|
||||
assert roof_recommender4.recommendations[0]["total"] == 788.0544
|
||||
assert roof_recommender4.recommendations[0]["new_u_value"] == 0.15
|
||||
assert roof_recommender4.recommendations[0]["starting_u_value"] == 0.3
|
||||
assert roof_recommender4.recommendations[0]["parts"][0]["depth"] == 150
|
||||
|
||||
property_instance5 = Property(id=0, address1="fake", postcode="fake", epc_client=Mock())
|
||||
property_instance5.age_band = "F"
|
||||
property_instance5.floor_area = 100
|
||||
property_instance5.insulation_floor_area = 100
|
||||
property_instance5.roof = {
|
||||
'original_description': 'Pitched, 150mm loft insulation (assumed)',
|
||||
'clean_description': 'Pitched, 150mm loft insulation',
|
||||
|
|
@ -180,25 +122,24 @@ class TestRoofRecommendations:
|
|||
'is_at_rafters': False, 'is_assumed': True, 'has_dwelling_above': False, 'is_valid': True,
|
||||
'insulation_thickness': '150', 'roof_thermal_transmittance': None, 'roof_insulation_thickness': 'none'
|
||||
}
|
||||
property_instance5.data = {"county": "Somerset"}
|
||||
|
||||
roof_recommender5 = RoofRecommendations(
|
||||
property_instance=property_instance5, materials=loft_insulation_materials_150mm_existing
|
||||
)
|
||||
roof_recommender5 = RoofRecommendations(property_instance=property_instance5, materials=materials)
|
||||
|
||||
assert not roof_recommender5.recommendations
|
||||
|
||||
roof_recommender5.recommend()
|
||||
|
||||
# The 130mm insulation should be selected, not the 110
|
||||
# The 150mm insulation should be selected, since there it already 150mm
|
||||
assert roof_recommender5.recommendations
|
||||
assert len(roof_recommender5.recommendations) == 1
|
||||
assert roof_recommender5.recommendations[0]["parts"][0]["depths"] == [130]
|
||||
assert len(roof_recommender5.recommendations) == 4
|
||||
assert roof_recommender5.recommendations[0]["parts"][0]["depth"] == 150
|
||||
|
||||
def test_loft_insulation_recommendation_270mm_insulation(self):
|
||||
# We shouldn't recommend anything in this case
|
||||
property_instance6 = Property(id=0, address1="fake", postcode="fake", epc_client=Mock())
|
||||
property_instance6.age_band = "F"
|
||||
property_instance6.floor_area = 100
|
||||
property_instance6.insulation_floor_area = 100
|
||||
property_instance6.roof = {
|
||||
'original_description': 'Pitched, 270mm loft insulation (assumed)',
|
||||
'clean_description': 'Pitched, 270mm loft insulation',
|
||||
|
|
@ -208,10 +149,9 @@ class TestRoofRecommendations:
|
|||
'is_at_rafters': False, 'is_assumed': True, 'has_dwelling_above': False, 'is_valid': True,
|
||||
'insulation_thickness': '270', 'roof_thermal_transmittance': None, 'roof_insulation_thickness': 'none'
|
||||
}
|
||||
property_instance6.data = {"county": "Portsmouth"}
|
||||
|
||||
roof_recommender6 = RoofRecommendations(
|
||||
property_instance=property_instance6, materials=loft_insulation_materials
|
||||
)
|
||||
roof_recommender6 = RoofRecommendations(property_instance=property_instance6, materials=materials)
|
||||
|
||||
assert not roof_recommender6.recommendations
|
||||
|
||||
|
|
@ -219,219 +159,211 @@ class TestRoofRecommendations:
|
|||
|
||||
assert len(roof_recommender6.recommendations) == 0
|
||||
|
||||
def test_uninsulated_room_in_roof(self):
|
||||
property_instance7 = Property(id=0, address1="fake", postcode="fake", epc_client=Mock())
|
||||
property_instance7.age_band = "F"
|
||||
property_instance7.floor_area = 100
|
||||
property_instance7.roof = {
|
||||
'original_description': 'Roof room(s), no insulation (assumed)',
|
||||
'clean_description': 'Roof room(s), no insulation',
|
||||
'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': False,
|
||||
'is_roof_room': True, 'is_loft': False, 'is_flat': False, 'is_thatched': False, 'is_at_rafters': False,
|
||||
'is_assumed': True, 'has_dwelling_above': False, 'is_valid': True, 'insulation_thickness': 'none'
|
||||
}
|
||||
|
||||
property_instance7.pitched_roof_area = 110
|
||||
|
||||
roof_recommender7 = RoofRecommendations(
|
||||
property_instance=property_instance7, materials=room_roof_insulation_materials
|
||||
)
|
||||
|
||||
assert not roof_recommender7.recommendations
|
||||
|
||||
roof_recommender7.recommend()
|
||||
|
||||
# Even though we have 3 depths, we only end with 1 due to diminishin returns
|
||||
assert len(roof_recommender7.recommendations) == 1
|
||||
|
||||
assert roof_recommender7.recommendations[0]["parts"][0]["depths"] == [270]
|
||||
|
||||
assert roof_recommender7.recommendations[0]["new_u_value"] == 0.14
|
||||
assert roof_recommender7.recommendations[0]["starting_u_value"] == 0.8
|
||||
assert roof_recommender7.recommendations[0]["description"] == \
|
||||
"Insulate your room roof with 270mm of Example room roof insulation"
|
||||
|
||||
def test_ceiling_insulated_room_in_roof(self):
|
||||
property_instance8 = Property(id=8, address1="fake", postcode="fake", epc_client=Mock())
|
||||
property_instance8.age_band = "F"
|
||||
property_instance8.floor_area = 100
|
||||
property_instance8.roof = {
|
||||
'original_description': 'Roof room(s), ceiling insulated',
|
||||
'clean_description': 'Roof room(s), ceiling insulated',
|
||||
'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': False,
|
||||
'is_roof_room': True, 'is_loft': False, 'is_flat': False, 'is_thatched': False,
|
||||
'is_at_rafters': False,
|
||||
'is_assumed': False, 'has_dwelling_above': False, 'is_valid': True,
|
||||
'insulation_thickness': 'average'
|
||||
}
|
||||
|
||||
property_instance8.pitched_roof_area = 110
|
||||
|
||||
roof_recommender8 = RoofRecommendations(
|
||||
property_instance=property_instance8, materials=room_roof_insulation_materials
|
||||
)
|
||||
|
||||
assert not roof_recommender8.recommendations
|
||||
|
||||
roof_recommender8.recommend()
|
||||
|
||||
# No recommendations in this case
|
||||
assert not roof_recommender8.recommendations
|
||||
|
||||
def test_insulated_room_in_roof(self):
|
||||
property_instance9 = Property(id=9, address1="fake", postcode="fake", epc_client=Mock())
|
||||
property_instance9.age_band = "F"
|
||||
property_instance9.floor_area = 100
|
||||
property_instance9.roof = {
|
||||
'original_description': 'Roof room(s), insulated (assumed)',
|
||||
'clean_description': 'Roof room(s), insulated',
|
||||
'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': False,
|
||||
'is_roof_room': True, 'is_loft': False, 'is_flat': False, 'is_thatched': False, 'is_at_rafters': False,
|
||||
'is_assumed': True, 'has_dwelling_above': False, 'is_valid': True, 'insulation_thickness': 'average'
|
||||
}
|
||||
|
||||
property_instance9.pitched_roof_area = 110
|
||||
|
||||
roof_recommender9 = RoofRecommendations(
|
||||
property_instance=property_instance9, materials=room_roof_insulation_materials
|
||||
)
|
||||
|
||||
assert not roof_recommender9.recommendations
|
||||
|
||||
roof_recommender9.recommend()
|
||||
|
||||
# No recommendations in this case
|
||||
assert not roof_recommender9.recommendations
|
||||
|
||||
def test_limited_insulated_room_in_roof(self):
|
||||
property_instance10 = Property(id=10, address1="fake", postcode="fake", epc_client=Mock())
|
||||
property_instance10.age_band = "F"
|
||||
property_instance10.floor_area = 100
|
||||
property_instance10.roof = {
|
||||
'original_description': 'Roof room(s), limited insulation (assumed)',
|
||||
'clean_description': 'Roof room(s), limited insulation',
|
||||
'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': False,
|
||||
'is_roof_room': True, 'is_loft': False, 'is_flat': False, 'is_thatched': False, 'is_at_rafters': False,
|
||||
'is_assumed': True, 'has_dwelling_above': False, 'is_valid': True,
|
||||
'insulation_thickness': 'below average'
|
||||
}
|
||||
|
||||
property_instance10.pitched_roof_area = 110
|
||||
|
||||
roof_recommender10 = RoofRecommendations(
|
||||
property_instance=property_instance10, materials=room_roof_insulation_materials
|
||||
)
|
||||
|
||||
assert not roof_recommender10.recommendations
|
||||
|
||||
roof_recommender10.recommend()
|
||||
|
||||
assert len(roof_recommender10.recommendations) == 2
|
||||
|
||||
assert roof_recommender10.recommendations[0]["parts"][0]["depths"] == [220]
|
||||
assert roof_recommender10.recommendations[1]["parts"][0]["depths"] == [270]
|
||||
|
||||
assert roof_recommender10.recommendations[0]["new_u_value"] == 0.16
|
||||
assert roof_recommender10.recommendations[1]["new_u_value"] == 0.14
|
||||
|
||||
assert roof_recommender10.recommendations[0]["starting_u_value"] == 0.8
|
||||
assert roof_recommender10.recommendations[1]["starting_u_value"] == 0.8
|
||||
|
||||
assert roof_recommender10.recommendations[0]["description"] == \
|
||||
"Insulate your room roof with 220mm of Example room roof insulation"
|
||||
assert roof_recommender10.recommendations[1]["description"] == \
|
||||
"Insulate your room roof with 270mm of Example room roof insulation"
|
||||
|
||||
def test_flat_no_insulation(self):
|
||||
property_instance11 = Property(id=11, address1="fake", postcode="fake", epc_client=Mock())
|
||||
property_instance11.age_band = "D"
|
||||
property_instance11.floor_area = 150
|
||||
property_instance11.roof = {
|
||||
'original_description': 'Flat, no insulation (assumed)',
|
||||
'clean_description': 'Flat, no insulation',
|
||||
'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': False,
|
||||
'is_roof_room': False, 'is_loft': False, 'is_flat': True, 'is_thatched': False, 'is_at_rafters': False,
|
||||
'is_assumed': True, 'has_dwelling_above': False, 'is_valid': True, 'insulation_thickness': 'none'
|
||||
}
|
||||
|
||||
roof_recommender11 = RoofRecommendations(
|
||||
property_instance=property_instance11, materials=flat_roof_insulation_materials
|
||||
)
|
||||
|
||||
assert not roof_recommender11.recommendations
|
||||
|
||||
roof_recommender11.recommend()
|
||||
|
||||
assert len(roof_recommender11.recommendations) == 1
|
||||
|
||||
assert roof_recommender11.recommendations[0]["parts"][0]["depths"] == [270]
|
||||
|
||||
assert roof_recommender11.recommendations[0]["new_u_value"] == 0.11
|
||||
|
||||
assert roof_recommender11.recommendations[0]["starting_u_value"] == 2.3
|
||||
|
||||
assert roof_recommender11.recommendations[0]["description"] == \
|
||||
"Insulate the home's flat roof with 270mm of Example flat roof insulation"
|
||||
|
||||
def test_flat_insulated(self):
|
||||
property_instance12 = Property(id=12, address1="fake", postcode="fake", epc_client=Mock())
|
||||
property_instance12.age_band = "D"
|
||||
property_instance12.floor_area = 150
|
||||
property_instance12.roof = {
|
||||
'original_description': 'Flat, insulated (assumed)',
|
||||
'clean_description': 'Flat, insulated',
|
||||
'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': False,
|
||||
'is_roof_room': False,
|
||||
'is_loft': False, 'is_flat': True, 'is_thatched': False, 'is_at_rafters': False, 'is_assumed': True,
|
||||
'has_dwelling_above': False, 'is_valid': True, 'insulation_thickness': 'average'
|
||||
}
|
||||
|
||||
roof_recommender12 = RoofRecommendations(
|
||||
property_instance=property_instance12, materials=flat_roof_insulation_materials
|
||||
)
|
||||
|
||||
assert not roof_recommender12.recommendations
|
||||
|
||||
roof_recommender12.recommend()
|
||||
|
||||
assert not roof_recommender12.recommendations
|
||||
|
||||
def test_flat_limited_insulation(self):
|
||||
property_instance13 = Property(id=12, address1="fake", postcode="fake", epc_client=Mock())
|
||||
property_instance13.age_band = "D"
|
||||
property_instance13.floor_area = 150
|
||||
property_instance13.roof = {
|
||||
'original_description': 'Flat, limited insulation (assumed)',
|
||||
'clean_description': 'Flat, limited insulation',
|
||||
'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': False,
|
||||
'is_roof_room': False,
|
||||
'is_loft': False, 'is_flat': True, 'is_thatched': False, 'is_at_rafters': False, 'is_assumed': True,
|
||||
'has_dwelling_above': False, 'is_valid': True, 'insulation_thickness': 'below average'
|
||||
}
|
||||
|
||||
roof_recommender13 = RoofRecommendations(
|
||||
property_instance=property_instance13, materials=flat_roof_insulation_materials
|
||||
)
|
||||
|
||||
assert not roof_recommender13.recommendations
|
||||
|
||||
roof_recommender13.recommend()
|
||||
|
||||
assert len(roof_recommender13.recommendations) == 1
|
||||
|
||||
assert roof_recommender13.recommendations[0]["parts"][0]["depths"] == [220]
|
||||
|
||||
assert roof_recommender13.recommendations[0]["new_u_value"] == 0.14
|
||||
|
||||
assert roof_recommender13.recommendations[0]["starting_u_value"] == 2.3
|
||||
|
||||
assert roof_recommender13.recommendations[0]["description"] == \
|
||||
"Insulate the home's flat roof with 220mm of Example flat roof insulation"
|
||||
# def test_uninsulated_room_in_roof(self):
|
||||
# property_instance7 = Property(id=0, address1="fake", postcode="fake", epc_client=Mock())
|
||||
# property_instance7.age_band = "F"
|
||||
# property_instance7.insulation_floor_area = 100
|
||||
# property_instance7.roof = {
|
||||
# 'original_description': 'Roof room(s), no insulation (assumed)',
|
||||
# 'clean_description': 'Roof room(s), no insulation',
|
||||
# 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': False,
|
||||
# 'is_roof_room': True, 'is_loft': False, 'is_flat': False, 'is_thatched': False, 'is_at_rafters': False,
|
||||
# 'is_assumed': True, 'has_dwelling_above': False, 'is_valid': True, 'insulation_thickness': 'none'
|
||||
# }
|
||||
#
|
||||
# property_instance7.pitched_roof_area = 110
|
||||
# property_instance7.data = {"county": "Southampton"}
|
||||
#
|
||||
# roof_recommender7 = RoofRecommendations(property_instance=property_instance7, materials=materials)
|
||||
#
|
||||
# assert not roof_recommender7.recommendations
|
||||
#
|
||||
# roof_recommender7.recommend()
|
||||
#
|
||||
# # Even though we have 3 depths, we only end with 1 due to diminishin returns
|
||||
# assert len(roof_recommender7.recommendations) == 1
|
||||
#
|
||||
# assert roof_recommender7.recommendations[0]["parts"][0]["depths"] == [270]
|
||||
#
|
||||
# assert roof_recommender7.recommendations[0]["new_u_value"] == 0.14
|
||||
# assert roof_recommender7.recommendations[0]["starting_u_value"] == 0.8
|
||||
# assert roof_recommender7.recommendations[0]["description"] == \
|
||||
# "Insulate your room roof with 270mm of Example room roof insulation"
|
||||
#
|
||||
# def test_ceiling_insulated_room_in_roof(self):
|
||||
# property_instance8 = Property(id=8, address1="fake", postcode="fake", epc_client=Mock())
|
||||
# property_instance8.age_band = "F"
|
||||
# property_instance8.insulation_floor_area = 100
|
||||
# property_instance8.roof = {
|
||||
# 'original_description': 'Roof room(s), ceiling insulated',
|
||||
# 'clean_description': 'Roof room(s), ceiling insulated',
|
||||
# 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': False,
|
||||
# 'is_roof_room': True, 'is_loft': False, 'is_flat': False, 'is_thatched': False,
|
||||
# 'is_at_rafters': False,
|
||||
# 'is_assumed': False, 'has_dwelling_above': False, 'is_valid': True,
|
||||
# 'insulation_thickness': 'average'
|
||||
# }
|
||||
#
|
||||
# property_instance8.pitched_roof_area = 110
|
||||
#
|
||||
# roof_recommender8 = RoofRecommendations(property_instance=property_instance8, materials=materials)
|
||||
#
|
||||
# assert not roof_recommender8.recommendations
|
||||
#
|
||||
# roof_recommender8.recommend()
|
||||
#
|
||||
# # No recommendations in this case
|
||||
# assert not roof_recommender8.recommendations
|
||||
#
|
||||
# def test_insulated_room_in_roof(self):
|
||||
# property_instance9 = Property(id=9, address1="fake", postcode="fake", epc_client=Mock())
|
||||
# property_instance9.age_band = "F"
|
||||
# property_instance9.insulation_floor_area = 100
|
||||
# property_instance9.roof = {
|
||||
# 'original_description': 'Roof room(s), insulated (assumed)',
|
||||
# 'clean_description': 'Roof room(s), insulated',
|
||||
# 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': False,
|
||||
# 'is_roof_room': True, 'is_loft': False, 'is_flat': False, 'is_thatched': False, 'is_at_rafters': False,
|
||||
# 'is_assumed': True, 'has_dwelling_above': False, 'is_valid': True, 'insulation_thickness': 'average'
|
||||
# }
|
||||
#
|
||||
# property_instance9.pitched_roof_area = 110
|
||||
# property_instance9.data = {"county": "Rutland"}
|
||||
#
|
||||
# roof_recommender9 = RoofRecommendations(property_instance=property_instance9, materials=materials)
|
||||
#
|
||||
# assert not roof_recommender9.recommendations
|
||||
#
|
||||
# roof_recommender9.recommend()
|
||||
#
|
||||
# # No recommendations in this case
|
||||
# assert not roof_recommender9.recommendations
|
||||
#
|
||||
# def test_limited_insulated_room_in_roof(self):
|
||||
# property_instance10 = Property(id=10, address1="fake", postcode="fake", epc_client=Mock())
|
||||
# property_instance10.age_band = "F"
|
||||
# property_instance10.insulation_floor_area = 100
|
||||
# property_instance10.roof = {
|
||||
# 'original_description': 'Roof room(s), limited insulation (assumed)',
|
||||
# 'clean_description': 'Roof room(s), limited insulation',
|
||||
# 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': False,
|
||||
# 'is_roof_room': True, 'is_loft': False, 'is_flat': False, 'is_thatched': False, 'is_at_rafters': False,
|
||||
# 'is_assumed': True, 'has_dwelling_above': False, 'is_valid': True,
|
||||
# 'insulation_thickness': 'below average'
|
||||
# }
|
||||
#
|
||||
# property_instance10.pitched_roof_area = 110
|
||||
# property_instance10.data = {"county": "Westmorland"}
|
||||
#
|
||||
# roof_recommender10 = RoofRecommendations(property_instance=property_instance10, materials=materials)
|
||||
#
|
||||
# assert not roof_recommender10.recommendations
|
||||
#
|
||||
# roof_recommender10.recommend()
|
||||
#
|
||||
# assert len(roof_recommender10.recommendations) == 2
|
||||
#
|
||||
# assert roof_recommender10.recommendations[0]["parts"][0]["depths"] == [220]
|
||||
# assert roof_recommender10.recommendations[1]["parts"][0]["depths"] == [270]
|
||||
#
|
||||
# assert roof_recommender10.recommendations[0]["new_u_value"] == 0.16
|
||||
# assert roof_recommender10.recommendations[1]["new_u_value"] == 0.14
|
||||
#
|
||||
# assert roof_recommender10.recommendations[0]["starting_u_value"] == 0.8
|
||||
# assert roof_recommender10.recommendations[1]["starting_u_value"] == 0.8
|
||||
#
|
||||
# assert roof_recommender10.recommendations[0]["description"] == \
|
||||
# "Insulate your room roof with 220mm of Example room roof insulation"
|
||||
# assert roof_recommender10.recommendations[1]["description"] == \
|
||||
# "Insulate your room roof with 270mm of Example room roof insulation"
|
||||
#
|
||||
# def test_flat_no_insulation(self):
|
||||
# property_instance11 = Property(id=11, address1="fake", postcode="fake", epc_client=Mock())
|
||||
# property_instance11.age_band = "D"
|
||||
# property_instance11.insulation_floor_area = 150
|
||||
# property_instance11.roof = {
|
||||
# 'original_description': 'Flat, no insulation (assumed)',
|
||||
# 'clean_description': 'Flat, no insulation',
|
||||
# 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': False,
|
||||
# 'is_roof_room': False, 'is_loft': False, 'is_flat': True, 'is_thatched': False, 'is_at_rafters': False,
|
||||
# 'is_assumed': True, 'has_dwelling_above': False, 'is_valid': True, 'insulation_thickness': 'none'
|
||||
# }
|
||||
# property_instance11.data = {"county": "Swindon"}
|
||||
#
|
||||
# roof_recommender11 = RoofRecommendations(property_instance=property_instance11, materials=materials)
|
||||
#
|
||||
# assert not roof_recommender11.recommendations
|
||||
#
|
||||
# roof_recommender11.recommend()
|
||||
#
|
||||
# assert len(roof_recommender11.recommendations) == 1
|
||||
#
|
||||
# assert roof_recommender11.recommendations[0]["parts"][0]["depths"] == [270]
|
||||
#
|
||||
# assert roof_recommender11.recommendations[0]["new_u_value"] == 0.11
|
||||
#
|
||||
# assert roof_recommender11.recommendations[0]["starting_u_value"] == 2.3
|
||||
#
|
||||
# assert roof_recommender11.recommendations[0]["description"] == \
|
||||
# "Insulate the home's flat roof with 270mm of Example flat roof insulation"
|
||||
#
|
||||
# def test_flat_insulated(self):
|
||||
# property_instance12 = Property(id=12, address1="fake", postcode="fake", epc_client=Mock())
|
||||
# property_instance12.age_band = "D"
|
||||
# property_instance12.insulation_floor_area = 150
|
||||
# property_instance12.roof = {
|
||||
# 'original_description': 'Flat, insulated (assumed)',
|
||||
# 'clean_description': 'Flat, insulated',
|
||||
# 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': False,
|
||||
# 'is_roof_room': False,
|
||||
# 'is_loft': False, 'is_flat': True, 'is_thatched': False, 'is_at_rafters': False, 'is_assumed': True,
|
||||
# 'has_dwelling_above': False, 'is_valid': True, 'insulation_thickness': 'average'
|
||||
# }
|
||||
# property_instance12.data = {"county": "Thurrock"}
|
||||
#
|
||||
# roof_recommender12 = RoofRecommendations(property_instance=property_instance12, materials=materials)
|
||||
#
|
||||
# assert not roof_recommender12.recommendations
|
||||
#
|
||||
# roof_recommender12.recommend()
|
||||
#
|
||||
# assert not roof_recommender12.recommendations
|
||||
#
|
||||
# def test_flat_limited_insulation(self):
|
||||
# property_instance13 = Property(id=12, address1="fake", postcode="fake", epc_client=Mock())
|
||||
# property_instance13.age_band = "D"
|
||||
# property_instance13.insulation_floor_area = 150
|
||||
# property_instance13.roof = {
|
||||
# 'original_description': 'Flat, limited insulation (assumed)',
|
||||
# 'clean_description': 'Flat, limited insulation',
|
||||
# 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': False,
|
||||
# 'is_roof_room': False,
|
||||
# 'is_loft': False, 'is_flat': True, 'is_thatched': False, 'is_at_rafters': False, 'is_assumed': True,
|
||||
# 'has_dwelling_above': False, 'is_valid': True, 'insulation_thickness': 'below average'
|
||||
# }
|
||||
# property_instance13.data = {"county": "Tyne and Wear"}
|
||||
#
|
||||
# roof_recommender13 = RoofRecommendations(property_instance=property_instance13, materials=materials)
|
||||
#
|
||||
# assert not roof_recommender13.recommendations
|
||||
#
|
||||
# roof_recommender13.recommend()
|
||||
#
|
||||
# assert len(roof_recommender13.recommendations) == 1
|
||||
#
|
||||
# assert roof_recommender13.recommendations[0]["parts"][0]["depths"] == [220]
|
||||
#
|
||||
# assert roof_recommender13.recommendations[0]["new_u_value"] == 0.14
|
||||
#
|
||||
# assert roof_recommender13.recommendations[0]["starting_u_value"] == 2.3
|
||||
#
|
||||
# assert roof_recommender13.recommendations[0]["description"] == \
|
||||
# "Insulate the home's flat roof with 220mm of Example flat roof insulation"
|
||||
|
||||
def test_property_above(self):
|
||||
property_instance14 = Property(id=0, address1="fake", postcode="fake", epc_client=Mock())
|
||||
property_instance14.age_band = "F"
|
||||
property_instance14.floor_area = 100
|
||||
property_instance14.insulation_floor_area = 100
|
||||
property_instance14.roof = {
|
||||
'original_description': '(other premises above)',
|
||||
'clean_description': '(other premises above)', 'thermal_transmittance': 0,
|
||||
|
|
@ -440,10 +372,9 @@ class TestRoofRecommendations:
|
|||
'is_assumed': False, 'has_dwelling_above': True, 'is_valid': True,
|
||||
'insulation_thickness': None
|
||||
}
|
||||
property_instance14.data = {"county": "Suffolk"}
|
||||
|
||||
roof_recommender14 = RoofRecommendations(
|
||||
property_instance=property_instance14, materials=loft_insulation_materials
|
||||
)
|
||||
roof_recommender14 = RoofRecommendations(property_instance=property_instance14, materials=materials)
|
||||
|
||||
assert not roof_recommender14.recommendations
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,7 @@
|
|||
from backend.Property import Property
|
||||
from unittest.mock import Mock
|
||||
from recommendations.VentilationRecommendations import VentilationRecommendations
|
||||
|
||||
ventilation_materials = [
|
||||
{
|
||||
'id': 17, 'type': 'mechanical_ventilation', 'description': 'Mechanical Extract Ventilation',
|
||||
'depths': None, 'depth_unit': None, 'cost': 500, 'cost_unit': 'gbp_per_unit', 'r_value_per_mm': None,
|
||||
'r_value_unit': None, 'thermal_conductivity': None, 'thermal_conductivity_unit': None,
|
||||
'link': None, 'is_active': True, 'estimated_cost': 1000, 'quantity': 2, 'quantity_unit': None
|
||||
}
|
||||
]
|
||||
from recommendations.tests.test_data.materials import materials
|
||||
|
||||
|
||||
class TestVentilationRecommendations:
|
||||
|
|
@ -20,7 +12,7 @@ class TestVentilationRecommendations:
|
|||
|
||||
recommender = VentilationRecommendations(
|
||||
property_instance=input_property1,
|
||||
materials=ventilation_materials
|
||||
materials=materials
|
||||
)
|
||||
|
||||
assert not recommender.recommendation
|
||||
|
|
@ -29,7 +21,7 @@ class TestVentilationRecommendations:
|
|||
|
||||
assert len(recommender.recommendation) == 1
|
||||
|
||||
assert recommender.recommendation[0]["cost"] == 1000
|
||||
assert recommender.recommendation[0]["total"] == 1000
|
||||
assert recommender.recommendation[0]["type"] == "mechanical_ventilation"
|
||||
assert len(recommender.recommendation[0]["parts"]) == 1
|
||||
assert recommender.recommendation[0]["parts"][0]["description"] == 'Mechanical Extract Ventilation'
|
||||
|
|
@ -41,7 +33,7 @@ class TestVentilationRecommendations:
|
|||
|
||||
recommender2 = VentilationRecommendations(
|
||||
property_instance=input_property2,
|
||||
materials=ventilation_materials
|
||||
materials=materials
|
||||
)
|
||||
|
||||
assert not recommender2.recommendation
|
||||
|
|
@ -50,7 +42,7 @@ class TestVentilationRecommendations:
|
|||
|
||||
assert len(recommender2.recommendation) == 1
|
||||
|
||||
assert recommender2.recommendation[0]["cost"] == 1000
|
||||
assert recommender2.recommendation[0]["total"] == 1000
|
||||
assert recommender2.recommendation[0]["type"] == "mechanical_ventilation"
|
||||
assert len(recommender2.recommendation[0]["parts"]) == 1
|
||||
assert recommender2.recommendation[0]["parts"][0]["description"] == 'Mechanical Extract Ventilation'
|
||||
|
|
@ -62,7 +54,7 @@ class TestVentilationRecommendations:
|
|||
|
||||
recommender3 = VentilationRecommendations(
|
||||
property_instance=input_property3,
|
||||
materials=ventilation_materials
|
||||
materials=materials
|
||||
)
|
||||
|
||||
assert not recommender3.recommendation
|
||||
|
|
@ -71,7 +63,7 @@ class TestVentilationRecommendations:
|
|||
|
||||
assert len(recommender3.recommendation) == 1
|
||||
|
||||
assert recommender3.recommendation[0]["cost"] == 1000
|
||||
assert recommender3.recommendation[0]["total"] == 1000
|
||||
assert recommender3.recommendation[0]["type"] == "mechanical_ventilation"
|
||||
assert len(recommender3.recommendation[0]["parts"]) == 1
|
||||
assert recommender3.recommendation[0]["parts"][0]["description"] == 'Mechanical Extract Ventilation'
|
||||
|
|
@ -83,7 +75,7 @@ class TestVentilationRecommendations:
|
|||
|
||||
recommender4 = VentilationRecommendations(
|
||||
property_instance=input_property4,
|
||||
materials=ventilation_materials
|
||||
materials=materials
|
||||
)
|
||||
|
||||
assert not recommender4.recommendation
|
||||
|
|
@ -99,7 +91,7 @@ class TestVentilationRecommendations:
|
|||
|
||||
recommender5 = VentilationRecommendations(
|
||||
property_instance=input_property5,
|
||||
materials=ventilation_materials
|
||||
materials=materials
|
||||
)
|
||||
|
||||
assert not recommender5.recommendation
|
||||
|
|
|
|||
|
|
@ -6,202 +6,14 @@ from unittest.mock import Mock, MagicMock
|
|||
from recommendations.WallRecommendations import WallRecommendations
|
||||
from backend.Property import Property
|
||||
from recommendations.recommendation_utils import is_diminishing_returns
|
||||
from recommendations.tests.test_data.materials import materials
|
||||
|
||||
|
||||
# with open(
|
||||
# os.path.abspath(os.path.dirname(__file__)) + "/recommendations/tests/test_data/input_properties.pkl", "rb"
|
||||
# ) as f:
|
||||
# input_properties = pickle.load(f)
|
||||
|
||||
external_wall_insulation_parts = [
|
||||
{
|
||||
# Example product
|
||||
# https://insulationgo.co.uk/100mm-rockwool-external-wall-insulation-dual-density-slabs-a1-non-combustible
|
||||
# -slab-ewi-render-fire/
|
||||
"type": "external_wall_insulation",
|
||||
"description": "Mineral Wool External Wall Insulation",
|
||||
"depths": [30, 50, 70, 80, 90, 100, 150, 200],
|
||||
"depth_unit": "mm",
|
||||
"cost": [30, 50, 70, 80, 90, 100, 150, 200],
|
||||
"cost_unit": "gbp_sq_meter",
|
||||
"r_value_per_mm": 0.0278,
|
||||
"r_value_unit": "square_meter_kelvin_per_watt",
|
||||
"thermal_conductivity": 0.036,
|
||||
"thermal_conductivity_unit": "watt_per_meter_kelvin"
|
||||
},
|
||||
{
|
||||
# Example product
|
||||
# https://www.insulationking.co.uk/products/polystyrene-eps70?variant=44156186558759
|
||||
"type": "external_wall_insulation",
|
||||
"description": "Expanded Polystyrene External Wall Insulation",
|
||||
"depths": [25, 50, 100, 125],
|
||||
"depth_unit": "mm",
|
||||
"cost": [25, 50, 100, 125],
|
||||
"cost_unit": "gbp_sq_meter",
|
||||
"r_value_per_mm": 0.02703,
|
||||
"r_value_unit": "square_meter_kelvin_per_watt",
|
||||
"thermal_conductivity": 0.037,
|
||||
"thermal_conductivity_unit": "watt_per_meter_kelvin"
|
||||
},
|
||||
{
|
||||
# Example product
|
||||
# https://www.insulationshop.co/20mm_kooltherm_k5_external_wall_kingspan.html
|
||||
"type": "external_wall_insulation",
|
||||
"description": "Phenolic Foam External Wall Insulation",
|
||||
"depths": [20, 50, 100],
|
||||
"depth_unit": "mm",
|
||||
"cost": [20, 50, 100],
|
||||
"cost_unit": "gbp_sq_meter",
|
||||
"r_value_per_mm": 0.043478260869565216,
|
||||
"r_value_unit": "square_meter_kelvin_per_watt",
|
||||
"thermal_conductivity": 0.023,
|
||||
"thermal_conductivity_unit": "watt_per_meter_kelvin"
|
||||
|
||||
},
|
||||
{
|
||||
"type": "external_wall_insulation",
|
||||
"description": "Polyisocyanurate/Polyurethane Foam External Wall Insulation",
|
||||
"depths": [],
|
||||
"depth_unit": "mm",
|
||||
"cost": [],
|
||||
"cost_unit": "gbp_sq_meter",
|
||||
"r_value_per_mm": None,
|
||||
"r_value_unit": "square_meter_kelvin_per_watt",
|
||||
"thermal_conductivity": None,
|
||||
"thermal_conductivity_unit": "watt_per_meter_kelvin"
|
||||
},
|
||||
{
|
||||
# Example product
|
||||
# https://www.mikewye.co.uk/product/steico-duo-dry/
|
||||
"type": "external_wall_insulation",
|
||||
"description": "Wood Fiber External Wall Insulation",
|
||||
"depths": [40, 60],
|
||||
"depth_unit": "mm",
|
||||
"cost": [40, 60],
|
||||
"cost_unit": "gbp_sq_meter",
|
||||
"r_value_per_mm": 0.023255813953488375,
|
||||
"r_value_unit": "square_meter_kelvin_per_watt",
|
||||
"thermal_conductivity": 0.043,
|
||||
"thermal_conductivity_unit": "watt_per_meter_kelvin"
|
||||
},
|
||||
{
|
||||
# Example product
|
||||
# https://www.thermablok.co.uk/site/wp-content/uploads/2022/09/Thermablok-Aerogel-Insulation-Blanket-TDS-AIS
|
||||
# -and-Steel-Related-Details.pdf
|
||||
"type": "external_wall_insulation",
|
||||
"description": "Aerogel External Wall Insulation",
|
||||
"depths": [10, 20, 30, 40, 50, 60, 70],
|
||||
"depth_unit": "mm",
|
||||
"cost": [10, 20, 30, 40, 50, 60, 70],
|
||||
"cost_unit": "gbp_sq_meter",
|
||||
"r_value_per_mm": 0.06666666666666667,
|
||||
"r_value_unit": "square_meter_kelvin_per_watt",
|
||||
"thermal_conductivity": 0.015,
|
||||
"thermal_conductivity_unit": "watt_per_meter_kelvin"
|
||||
},
|
||||
{
|
||||
"type": "external_wall_insulation",
|
||||
"description": "Vacuum Insulation Panels External Wall Insulation",
|
||||
"depths": [45, 60],
|
||||
"depth_unit": "mm",
|
||||
"cost": [45, 60],
|
||||
"cost_unit": "gbp_sq_meter",
|
||||
"r_value_per_mm": 0.16666666666666666,
|
||||
"r_value_unit": "square_meter_kelvin_per_watt",
|
||||
"thermal_conductivity": 0.006,
|
||||
"thermal_conductivity_unit": "watt_per_meter_kelvin"
|
||||
}
|
||||
]
|
||||
|
||||
internal_wall_insulation_parts = [
|
||||
{
|
||||
# Example product
|
||||
# https://www.insulationshop.co/25mm_polystyrene_insulation_eps_70jablite.html
|
||||
"type": "internal_wall_insulation",
|
||||
"description": "Rigid Insulation Boards Internal Wall Insulation",
|
||||
"depths": [25, 40, 50, 75, 100],
|
||||
"depth_unit": "mm",
|
||||
"cost": [25, 40, 50, 75, 100],
|
||||
"cost_unit": "gbp_sq_meter",
|
||||
"r_value_per_mm": 0.026315789473684213,
|
||||
"r_value_unit": "square_meter_kelvin_per_watt",
|
||||
"thermal_conductivity": 0.038,
|
||||
"thermal_conductivity_unit": "watt_per_meter_kelvin"
|
||||
},
|
||||
{
|
||||
# Example product
|
||||
# https://www.rockwool.com/siteassets/rw-uk/downloads/datasheets/flexi.pdf
|
||||
"type": "internal_wall_insulation",
|
||||
"description": "Mineral Wool Internal Wall Insulation",
|
||||
"depths": [140],
|
||||
"depth_unit": "mm",
|
||||
"cost": [140],
|
||||
"cost_unit": "gbp_sq_meter",
|
||||
"r_value_per_mm": 0.02857142857142857,
|
||||
"r_value_unit": "square_meter_kelvin_per_watt",
|
||||
"thermal_conductivity": 0.035,
|
||||
"thermal_conductivity_unit": "watt_per_meter_kelvin"
|
||||
},
|
||||
{
|
||||
# Example product
|
||||
# https://www.kingspan.com/gb/en/products/insulation-boards/wall-insulation-boards/kooltherm-k118-insulated
|
||||
# -plasterboard/
|
||||
"type": "internal_wall_insulation",
|
||||
"description": "Insulated Plasterboard Internal Wall Insulation",
|
||||
"depths": [25, 80],
|
||||
"depth_unit": "mm",
|
||||
"cost": [25, 80],
|
||||
"cost_unit": "gbp_sq_meter",
|
||||
"r_value_per_mm": 0.02857142857142857,
|
||||
"r_value_unit": "square_meter_kelvin_per_watt",
|
||||
"thermal_conductivity": 0.019,
|
||||
"thermal_conductivity_unit": "watt_per_meter_kelvin"
|
||||
},
|
||||
{
|
||||
"type": "internal_wall_insulation",
|
||||
"description": "Reflective Internal Wall Insulation",
|
||||
"depths": [],
|
||||
"depth_unit": "mm",
|
||||
"cost": [],
|
||||
"cost_unit": "gbp_sq_meter",
|
||||
"r_value_per_mm": None,
|
||||
"r_value_unit": "square_meter_kelvin_per_watt",
|
||||
"thermal_conductivity": None,
|
||||
"thermal_conductivity_unit": "watt_per_meter_kelvin"
|
||||
},
|
||||
{
|
||||
# Example product
|
||||
# https://www.insulationsuperstore.co.uk/product/vacutherm-vacupor-nt-b2-vacuum-insulated-panel-1m-x-600mm-x
|
||||
# -30mm.html
|
||||
"type": "internal_wall_insulation",
|
||||
"description": "Vacuum Insulation Panels Wall Insulation",
|
||||
"depths": [20, 30],
|
||||
"depth_unit": "mm",
|
||||
"cost": [20, 30],
|
||||
"cost_unit": "gbp_sq_meter",
|
||||
"r_value_per_mm": 0.125,
|
||||
"r_value_unit": "square_meter_kelvin_per_watt",
|
||||
"thermal_conductivity": 0.008,
|
||||
"thermal_conductivity_unit": "watt_per_meter_kelvin"
|
||||
},
|
||||
]
|
||||
|
||||
cavity_wall_insulation_parts = [
|
||||
{'id': 4, 'type': 'cavity_wall_insulation', 'description': 'Example Material 1',
|
||||
'depths': None,
|
||||
'depth_unit': None, 'cost': 20,
|
||||
'cost_unit': 'gbp_sq_meter', 'r_value_per_mm': 0.0278, 'r_value_unit': 'square_meter_kelvin_per_watt',
|
||||
'thermal_conductivity': 0.036, 'thermal_conductivity_unit': 'watt_per_meter_kelvin',
|
||||
'link': None, 'created_at': None, 'is_active': True},
|
||||
{'id': 10, 'type': "cavity_wall_insulation", 'description': 'Example Material 2',
|
||||
'depths': None, 'depth_unit': None, 'cost': 25, 'cost_unit': 'gbp_sq_meter',
|
||||
'r_value_per_mm': 0.02631579, 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.038,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin',
|
||||
'link': None,
|
||||
'created_at': None, 'is_active': True}
|
||||
]
|
||||
|
||||
wall_parts = external_wall_insulation_parts + internal_wall_insulation_parts + cavity_wall_insulation_parts
|
||||
|
||||
|
||||
class TestWallRecommendations:
|
||||
|
||||
|
|
@ -217,17 +29,20 @@ class TestWallRecommendations:
|
|||
# Creating a mock instance of WallRecommendations with the necessary attributes
|
||||
property_mock = Mock()
|
||||
property_mock.full_sap_epc = {"lodgement-date": "2000-01-01"} # or any date you want
|
||||
property_mock.data = {"construction-age-band": "1950"} # or any other data that fits your tests
|
||||
property_mock.data = {"construction-age-band": "1950",
|
||||
"county": "Derbyshire"} # or any other data that fits your tests
|
||||
|
||||
mock_wall_rec_instance = WallRecommendations(
|
||||
property_mock, materials=wall_parts
|
||||
property_mock, materials=materials
|
||||
)
|
||||
return mock_wall_rec_instance
|
||||
|
||||
def test_init(self, input_properties):
|
||||
input_properties[0].insulation_wall_area = 100
|
||||
|
||||
obj = WallRecommendations(
|
||||
property_instance=input_properties[0],
|
||||
materials=wall_parts
|
||||
materials=materials
|
||||
)
|
||||
assert obj
|
||||
assert obj.property
|
||||
|
|
@ -244,10 +59,11 @@ class TestWallRecommendations:
|
|||
input_properties[0].year_built = 2014
|
||||
input_properties[0].in_conservation_area = None
|
||||
input_properties[0].restricted_measures = False
|
||||
input_properties[0].insulation_wall_area = 100
|
||||
|
||||
recommender = WallRecommendations(
|
||||
property_instance=input_properties[0],
|
||||
materials=wall_parts
|
||||
materials=materials
|
||||
)
|
||||
assert recommender.property.walls["original_description"] == "Average thermal transmittance 0.16 W/m-¦K"
|
||||
recommender.recommend()
|
||||
|
|
@ -272,7 +88,7 @@ class TestWallRecommendations:
|
|||
|
||||
recommender = WallRecommendations(
|
||||
property_instance=input_properties[1],
|
||||
materials=wall_parts
|
||||
materials=materials
|
||||
)
|
||||
assert recommender.property.walls["original_description"] == "Solid brick, as built, no insulation (assumed)"
|
||||
assert not recommender.ewi_valid
|
||||
|
|
@ -306,9 +122,11 @@ class TestWallRecommendations:
|
|||
|
||||
input_properties[6].year_built = 1991
|
||||
input_properties[6].restricted_measures = False
|
||||
input_properties[6].insulation_wall_area = 100
|
||||
|
||||
recommender = WallRecommendations(
|
||||
property_instance=input_properties[6],
|
||||
materials=wall_parts
|
||||
materials=materials
|
||||
)
|
||||
|
||||
assert recommender.property.walls["original_description"] == "Solid brick, as built, insulated (assumed)"
|
||||
|
|
@ -383,12 +201,14 @@ class TestWallRecommendationsBase:
|
|||
property_mock.full_sap_epc = {"lodgement-date": "1999-12-31"}
|
||||
property_mock.in_conservation_area = "not_in_conservation_area"
|
||||
property_mock.restricted_measures = False
|
||||
property_mock.insulation_wall_area = 100
|
||||
property_mock.data = {"county": "Derbyshire"}
|
||||
return property_mock
|
||||
|
||||
@pytest.fixture
|
||||
def wall_recommendations_instance(self, property_mock):
|
||||
wall_recommendations_instance = WallRecommendations(
|
||||
property_mock, materials=wall_parts
|
||||
property_mock, materials=materials
|
||||
)
|
||||
return wall_recommendations_instance
|
||||
|
||||
|
|
@ -425,10 +245,11 @@ class TestCavityWallRecommensations:
|
|||
}
|
||||
input_property.age_band = "C"
|
||||
input_property.insulation_wall_area = 50
|
||||
input_property.data = {"county": "Derbyshire"}
|
||||
|
||||
recommender = WallRecommendations(
|
||||
property_instance=input_property,
|
||||
materials=cavity_wall_insulation_parts
|
||||
materials=materials
|
||||
)
|
||||
|
||||
assert not recommender.recommendations
|
||||
|
|
@ -437,11 +258,11 @@ class TestCavityWallRecommensations:
|
|||
|
||||
assert recommender.recommendations
|
||||
assert recommender.estimated_u_value == 1.5
|
||||
assert np.isclose(recommender.recommendations[0]["new_u_value"], 0.37)
|
||||
assert np.isclose(recommender.recommendations[0]["cost"], 1000)
|
||||
assert np.isclose(recommender.recommendations[0]["new_u_value"], 0.35)
|
||||
assert np.isclose(recommender.recommendations[0]["total"], 1668.6600000000003)
|
||||
|
||||
assert np.isclose(recommender.recommendations[1]["new_u_value"], 0.38)
|
||||
assert np.isclose(recommender.recommendations[1]["cost"], 1250)
|
||||
assert np.isclose(recommender.recommendations[1]["new_u_value"], 0.35)
|
||||
assert np.isclose(recommender.recommendations[1]["total"], 2004.6600000000003)
|
||||
|
||||
def test_fill_partial_filled_cavity(self):
|
||||
input_property = Property(id=1, postcode="F4k3", address1="123 fake street", epc_client=Mock())
|
||||
|
|
@ -458,10 +279,11 @@ class TestCavityWallRecommensations:
|
|||
}
|
||||
input_property.age_band = "C"
|
||||
input_property.insulation_wall_area = 50
|
||||
input_property.data = {"county": "County Durham"}
|
||||
|
||||
recommender = WallRecommendations(
|
||||
property_instance=input_property,
|
||||
materials=cavity_wall_insulation_parts
|
||||
materials=materials
|
||||
)
|
||||
|
||||
assert not recommender.recommendations
|
||||
|
|
@ -470,11 +292,11 @@ class TestCavityWallRecommensations:
|
|||
|
||||
assert recommender.recommendations
|
||||
assert recommender.estimated_u_value == 1.3
|
||||
assert np.isclose(recommender.recommendations[0]["new_u_value"], 0.43)
|
||||
assert np.isclose(recommender.recommendations[0]["cost"], 1000)
|
||||
assert np.isclose(recommender.recommendations[0]["new_u_value"], 0.41)
|
||||
assert np.isclose(recommender.recommendations[0]["total"], 1663.9350000000002)
|
||||
|
||||
assert np.isclose(recommender.recommendations[1]["new_u_value"], 0.45)
|
||||
assert np.isclose(recommender.recommendations[1]["cost"], 1250)
|
||||
assert np.isclose(recommender.recommendations[1]["new_u_value"], 0.41)
|
||||
assert np.isclose(recommender.recommendations[1]["total"], 1999.9350000000002)
|
||||
|
||||
def test_system_built_wall(self):
|
||||
input_property2 = Property(id=1, postcode="F4k3 2", address1="223 fake street", epc_client=Mock())
|
||||
|
|
@ -492,13 +314,13 @@ class TestCavityWallRecommensations:
|
|||
input_property2.age_band = "F"
|
||||
input_property2.insulation_wall_area = 120
|
||||
input_property2.restricted_measures = False
|
||||
input_property2.data = {"property-type": "house"}
|
||||
input_property2.data = {"property-type": "House", "county": "Derbyshire", "built-form": "Detached"}
|
||||
|
||||
assert input_property2.walls["is_system_built"]
|
||||
|
||||
recommender2 = WallRecommendations(
|
||||
property_instance=input_property2,
|
||||
materials=internal_wall_insulation_parts + external_wall_insulation_parts
|
||||
materials=materials
|
||||
)
|
||||
|
||||
assert not recommender2.recommendations
|
||||
|
|
@ -506,22 +328,22 @@ class TestCavityWallRecommensations:
|
|||
recommender2.recommend()
|
||||
|
||||
assert recommender2.recommendations
|
||||
assert len(recommender2.recommendations) == 6
|
||||
assert len(recommender2.recommendations) == 9
|
||||
assert recommender2.estimated_u_value == 1
|
||||
assert np.isclose(recommender2.recommendations[0]["new_u_value"], 0.29)
|
||||
assert np.isclose(recommender2.recommendations[0]["cost"], 10800)
|
||||
assert np.isclose(recommender2.recommendations[0]["new_u_value"], 0.19)
|
||||
assert np.isclose(recommender2.recommendations[0]["total"], 15899.9616)
|
||||
assert recommender2.recommendations[0]["parts"][0]["type"] == "external_wall_insulation"
|
||||
assert recommender2.recommendations[0]["parts"][0]["depths"] == [90]
|
||||
assert recommender2.recommendations[0]["parts"][0]["depth"] == 100
|
||||
|
||||
assert np.isclose(recommender2.recommendations[5]["new_u_value"], 0.29)
|
||||
assert np.isclose(recommender2.recommendations[5]["cost"], 2400)
|
||||
assert recommender2.recommendations[5]["parts"][0]["type"] == "internal_wall_insulation"
|
||||
assert recommender2.recommendations[5]["parts"][0]["depths"] == [20]
|
||||
assert np.isclose(recommender2.recommendations[8]["new_u_value"], 0.23)
|
||||
assert np.isclose(recommender2.recommendations[8]["total"], 10916.3424)
|
||||
assert recommender2.recommendations[8]["parts"][0]["type"] == "internal_wall_insulation"
|
||||
assert recommender2.recommendations[8]["parts"][0]["depth"] == 72.5
|
||||
|
||||
assert np.isclose(recommender2.recommendations[3]["new_u_value"], 0.28)
|
||||
assert np.isclose(recommender2.recommendations[3]["cost"], 4800)
|
||||
assert recommender2.recommendations[3]["parts"][0]["type"] == "external_wall_insulation"
|
||||
assert recommender2.recommendations[3]["parts"][0]["depths"] == [40]
|
||||
assert np.isclose(recommender2.recommendations[6]["new_u_value"], 0.29)
|
||||
assert np.isclose(recommender2.recommendations[6]["total"], 10621.934399999998)
|
||||
assert recommender2.recommendations[6]["parts"][0]["type"] == "internal_wall_insulation"
|
||||
assert recommender2.recommendations[6]["parts"][0]["depth"] == 52.5
|
||||
|
||||
def test_timber_frame_wall(self):
|
||||
input_property3 = Property(id=1, postcode="F4k3 2", address1="223 fake street", epc_client=Mock())
|
||||
|
|
@ -539,13 +361,13 @@ class TestCavityWallRecommensations:
|
|||
input_property3.age_band = "B"
|
||||
input_property3.insulation_wall_area = 99
|
||||
input_property3.restricted_measures = False
|
||||
input_property3.data = {"property-type": "house"}
|
||||
input_property3.data = {"property-type": "House", "county": "Derbyshire", "built-form": "Semi-Detached"}
|
||||
|
||||
assert input_property3.walls["is_timber_frame"]
|
||||
|
||||
recommender3 = WallRecommendations(
|
||||
property_instance=input_property3,
|
||||
materials=internal_wall_insulation_parts + external_wall_insulation_parts
|
||||
materials=materials
|
||||
)
|
||||
|
||||
assert not recommender3.recommendations
|
||||
|
|
@ -553,17 +375,17 @@ class TestCavityWallRecommensations:
|
|||
recommender3.recommend()
|
||||
|
||||
assert recommender3.recommendations
|
||||
assert len(recommender3.recommendations) == 2
|
||||
assert len(recommender3.recommendations) == 6
|
||||
assert recommender3.estimated_u_value == 1.9
|
||||
assert np.isclose(recommender3.recommendations[0]["new_u_value"], 0.26)
|
||||
assert np.isclose(recommender3.recommendations[0]["cost"], 12375)
|
||||
assert np.isclose(recommender3.recommendations[0]["new_u_value"], 0.2)
|
||||
assert np.isclose(recommender3.recommendations[0]["total"], 13117.46832)
|
||||
assert recommender3.recommendations[0]["parts"][0]["type"] == "external_wall_insulation"
|
||||
assert recommender3.recommendations[0]["parts"][0]["depths"] == [125]
|
||||
assert recommender3.recommendations[0]["parts"][0]["depth"] == 100.0
|
||||
|
||||
assert np.isclose(recommender3.recommendations[1]["new_u_value"], 0.26)
|
||||
assert np.isclose(recommender3.recommendations[1]["cost"], 4950)
|
||||
assert np.isclose(recommender3.recommendations[1]["new_u_value"], 0.23)
|
||||
assert np.isclose(recommender3.recommendations[1]["total"], 34070.50944)
|
||||
assert recommender3.recommendations[1]["parts"][0]["type"] == "external_wall_insulation"
|
||||
assert recommender3.recommendations[1]["parts"][0]["depths"] == [50]
|
||||
assert recommender3.recommendations[1]["parts"][0]["depth"] == 150.0
|
||||
|
||||
def test_granite_or_whinstone_wall(self):
|
||||
input_property4 = Property(id=1, postcode="F4k3 2", address1="223 fake street", epc_client=Mock())
|
||||
|
|
@ -581,13 +403,13 @@ class TestCavityWallRecommensations:
|
|||
input_property4.age_band = "A"
|
||||
input_property4.insulation_wall_area = 223
|
||||
input_property4.restricted_measures = False
|
||||
input_property4.data = {"property-type": "Bungalow"}
|
||||
input_property4.data = {"property-type": "Bungalow", "county": "Derbyshire", "built-form": "Detached"}
|
||||
|
||||
assert input_property4.walls["is_granite_or_whinstone"]
|
||||
|
||||
recommender4 = WallRecommendations(
|
||||
property_instance=input_property4,
|
||||
materials=internal_wall_insulation_parts + external_wall_insulation_parts
|
||||
materials=materials
|
||||
)
|
||||
|
||||
assert not recommender4.recommendations
|
||||
|
|
@ -595,17 +417,17 @@ class TestCavityWallRecommensations:
|
|||
recommender4.recommend()
|
||||
|
||||
assert recommender4.recommendations
|
||||
assert len(recommender4.recommendations) == 2
|
||||
assert len(recommender4.recommendations) == 6
|
||||
assert recommender4.estimated_u_value == 2.3
|
||||
assert np.isclose(recommender4.recommendations[0]["new_u_value"], 0.27)
|
||||
assert np.isclose(recommender4.recommendations[0]["cost"], 27875)
|
||||
assert np.isclose(recommender4.recommendations[0]["new_u_value"], 0.21)
|
||||
assert np.isclose(recommender4.recommendations[0]["total"], 28562.514352)
|
||||
assert recommender4.recommendations[0]["parts"][0]["type"] == "external_wall_insulation"
|
||||
assert recommender4.recommendations[0]["parts"][0]["depths"] == [125]
|
||||
assert recommender4.recommendations[0]["parts"][0]["depth"] == 100
|
||||
|
||||
assert np.isclose(recommender4.recommendations[1]["new_u_value"], 0.27)
|
||||
assert np.isclose(recommender4.recommendations[1]["cost"], 11150)
|
||||
assert np.isclose(recommender4.recommendations[1]["new_u_value"], 0.23)
|
||||
assert np.isclose(recommender4.recommendations[1]["total"], 74186.52678400002)
|
||||
assert recommender4.recommendations[1]["parts"][0]["type"] == "external_wall_insulation"
|
||||
assert recommender4.recommendations[1]["parts"][0]["depths"] == [50]
|
||||
assert recommender4.recommendations[1]["parts"][0]["depth"] == 150
|
||||
|
||||
def test_cob_wall(self):
|
||||
input_property5 = Property(id=1, postcode="F4k3 2", address1="223 fake street", epc_client=Mock())
|
||||
|
|
@ -623,13 +445,13 @@ class TestCavityWallRecommensations:
|
|||
input_property5.age_band = "E"
|
||||
input_property5.insulation_wall_area = 77
|
||||
input_property5.restricted_measures = False
|
||||
input_property5.data = {"property-type": "Bungalow"}
|
||||
input_property5.data = {"property-type": "Bungalow", "county": "Derbyshire", "built-form": "Detached"}
|
||||
|
||||
assert input_property5.walls["is_cob"]
|
||||
|
||||
recommender5 = WallRecommendations(
|
||||
property_instance=input_property5,
|
||||
materials=internal_wall_insulation_parts + external_wall_insulation_parts
|
||||
materials=materials
|
||||
)
|
||||
|
||||
assert not recommender5.recommendations
|
||||
|
|
@ -637,22 +459,17 @@ class TestCavityWallRecommensations:
|
|||
recommender5.recommend()
|
||||
|
||||
assert recommender5.recommendations
|
||||
assert len(recommender5.recommendations) == 9
|
||||
assert len(recommender5.recommendations) == 5
|
||||
assert recommender5.estimated_u_value == 0.8
|
||||
assert np.isclose(recommender5.recommendations[0]["new_u_value"], 0.29)
|
||||
assert np.isclose(recommender5.recommendations[0]["cost"], 6160)
|
||||
assert np.isclose(recommender5.recommendations[0]["total"], 8665.040384000002)
|
||||
assert recommender5.recommendations[0]["parts"][0]["type"] == "external_wall_insulation"
|
||||
assert recommender5.recommendations[0]["parts"][0]["depths"] == [80]
|
||||
assert recommender5.recommendations[0]["parts"][0]["depth"] == 50
|
||||
|
||||
assert np.isclose(recommender5.recommendations[3]["new_u_value"], 0.26)
|
||||
assert np.isclose(recommender5.recommendations[3]["cost"], 7700)
|
||||
assert recommender5.recommendations[3]["parts"][0]["type"] == "external_wall_insulation"
|
||||
assert recommender5.recommendations[3]["parts"][0]["depths"] == [100]
|
||||
|
||||
assert np.isclose(recommender5.recommendations[6]["new_u_value"], 0.26)
|
||||
assert np.isclose(recommender5.recommendations[6]["cost"], 7700)
|
||||
assert recommender5.recommendations[6]["parts"][0]["type"] == "internal_wall_insulation"
|
||||
assert recommender5.recommendations[6]["parts"][0]["depths"] == [100]
|
||||
assert np.isclose(recommender5.recommendations[3]["total"], 20078.742992)
|
||||
assert recommender5.recommendations[3]["parts"][0]["type"] == "internal_wall_insulation"
|
||||
assert recommender5.recommendations[3]["parts"][0]["depth"] == 100
|
||||
|
||||
def test_sandstone_or_limestone_wall(self):
|
||||
input_property6 = Property(id=1, postcode="F4k3 6", address1="623 fake street", epc_client=Mock())
|
||||
|
|
@ -670,13 +487,13 @@ class TestCavityWallRecommensations:
|
|||
input_property6.age_band = "F"
|
||||
input_property6.insulation_wall_area = 350
|
||||
input_property6.restricted_measures = False
|
||||
input_property6.data = {"property-type": "House"}
|
||||
input_property6.data = {"property-type": "House", "county": "Derbyshire", "built-form": "Mid-Terrace"}
|
||||
|
||||
assert input_property6.walls["is_sandstone_or_limestone"]
|
||||
|
||||
recommender6 = WallRecommendations(
|
||||
property_instance=input_property6,
|
||||
materials=internal_wall_insulation_parts + external_wall_insulation_parts
|
||||
materials=materials
|
||||
)
|
||||
|
||||
assert not recommender6.recommendations
|
||||
|
|
@ -684,19 +501,19 @@ class TestCavityWallRecommensations:
|
|||
recommender6.recommend()
|
||||
|
||||
assert recommender6.recommendations
|
||||
assert len(recommender6.recommendations) == 6
|
||||
assert len(recommender6.recommendations) == 9
|
||||
assert recommender6.estimated_u_value == 1
|
||||
assert np.isclose(recommender6.recommendations[0]["new_u_value"], 0.29)
|
||||
assert np.isclose(recommender6.recommendations[0]["cost"], 31500)
|
||||
assert np.isclose(recommender6.recommendations[0]["new_u_value"], 0.19)
|
||||
assert np.isclose(recommender6.recommendations[0]["total"], 44829.0584)
|
||||
assert recommender6.recommendations[0]["parts"][0]["type"] == "external_wall_insulation"
|
||||
assert recommender6.recommendations[0]["parts"][0]["depths"] == [90]
|
||||
assert recommender6.recommendations[0]["parts"][0]["depth"] == 100
|
||||
|
||||
assert np.isclose(recommender6.recommendations[2]["new_u_value"], 0.28)
|
||||
assert np.isclose(recommender6.recommendations[2]["cost"], 35000)
|
||||
assert np.isclose(recommender6.recommendations[2]["new_u_value"], 0.21)
|
||||
assert np.isclose(recommender6.recommendations[2]["total"], 116436.25280000002)
|
||||
assert recommender6.recommendations[2]["parts"][0]["type"] == "external_wall_insulation"
|
||||
assert recommender6.recommendations[2]["parts"][0]["depths"] == [100]
|
||||
assert recommender6.recommendations[2]["parts"][0]["depth"] == 150
|
||||
|
||||
assert np.isclose(recommender6.recommendations[4]["new_u_value"], 0.28)
|
||||
assert np.isclose(recommender6.recommendations[4]["cost"], 35000)
|
||||
assert np.isclose(recommender6.recommendations[4]["total"], 91267.0136)
|
||||
assert recommender6.recommendations[4]["parts"][0]["type"] == "internal_wall_insulation"
|
||||
assert recommender6.recommendations[4]["parts"][0]["depths"] == [100]
|
||||
assert recommender6.recommendations[4]["parts"][0]["depth"] == 100
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue