added in basic process for sloping ceiling

This commit is contained in:
Khalim Conn-Kowlessar 2026-01-26 17:49:56 +00:00
parent e8b7a569ff
commit 64eb2e2f20
3 changed files with 143 additions and 11 deletions

View file

@ -9,7 +9,9 @@ TYPICAL_MEASURE_TYPES = [
]
WALL_INSULATION_MEASURES = ["internal_wall_insulation", "external_wall_insulation", "cavity_wall_insulation"]
ROOF_INSULATION_MEASURES = ["loft_insulation", "flat_roof_insulation", "room_roof_insulation"]
ROOF_INSULATION_MEASURES = [
"loft_insulation", "flat_roof_insulation", "room_roof_insulation", "sloping_ceiling_insulation"
]
# Both all and roof insulaiton measures are eligible for ECO4. These are the remaining fabric and heating measures
# This is based on th measures we have recommendations for
@ -31,7 +33,7 @@ SPECIFIC_MEASURES = (
INSULATION_MEASURES = [
"internal_wall_insulation", "external_wall_insulation", "cavity_wall_insulation",
"loft_insulation", "flat_roof_insulation", "room_roof_insulation",
"loft_insulation", "flat_roof_insulation", "room_roof_insulation", "sloping_ceiling_insulation",
"suspended_floor_insulation", "solid_floor_insulation",
]
@ -46,7 +48,9 @@ MEASURE_MAP = {
"wall_insulation": [
"internal_wall_insulation", "external_wall_insulation", "cavity_wall_insulation",
],
"roof_insulation": ["loft_insulation", "flat_roof_insulation", "room_roof_insulation"],
"roof_insulation": [
"loft_insulation", "flat_roof_insulation", "room_roof_insulation", "sloping_ceiling_insulation"
],
"floor_insulation": ["suspended_floor_insulation", "solid_floor_insulation"],
"heating": ["boiler_upgrade", "high_heat_retention_storage_heaters", "air_source_heat_pump"],
"windows": ["double_glazing", "secondary_glazing"],

View file

@ -1,4 +1,6 @@
from typing import Mapping, Any
import numpy as np
from recommendations.county_to_region import county_to_region_map
from utils.logger import setup_logger
from backend.ml_models.AnnualBillSavings import AnnualBillSavings
@ -166,7 +168,8 @@ class Costs:
"room_roof_insulation": 0.26,
"heater_removal": 0.1,
"sealing_open_fireplace": 0.1,
"mechanical_ventilation": 0.26
"mechanical_ventilation": 0.26,
"sloping_ceiling_insulation": 0.26 # Similar to IWI so using the same contingency
}
# Preliminaries are a percentage of the total cost of the work and covers the cost of site-specific costs
@ -935,3 +938,66 @@ class Costs:
"labour_hours": 80,
"labour_days": 10,
}
@staticmethod
def _estimate_number_of_days_for_sloping_ceiling(insulation_roof_area: float) -> float:
"""
Estimate labour days required to insulate an existing sloping ceiling.
Heuristic model based on retrofit guidance (Checkatrade, The Green Age)
and analogy with internal wall insulation.
Assumptions:
- ~30 of sloping ceiling takes ~4 working days
- Small jobs still require multiple days (setup, stripping, reboarding)
- Larger areas benefit from economies of scale, but not linearly
:param insulation_roof_area: of sloping ceiling to be insulated
"""
base_days = 4
base_area = 30 # m2 reference case
labour_exponent = 0.85
min_days = 2
labour_days = max(
min_days,
base_days * (insulation_roof_area / base_area) ** labour_exponent
)
return labour_days
@classmethod
def sloping_ceiling_insulation(cls, insulation_roof_area: float) -> Mapping[str, Any]:
"""
This costing for this is based on Checkatrade desktop research, since we are yet to receive installer quotes.
:param insulation_roof_area: Area of the sloping ceiling to be insulated
:return:
"""
################
# Assumptions
################
# Sources:
# https://www.checkatrade.com/blog/cost-guides/vaulted-ceiling-cost/
# https://www.thegreenage.co.uk/can-i-insulate-my-sloping-ceiling/
# These assumptions last updated 21/02/2026
insulation_cost_per_m2 = 52 # The actual install process is quite similar to IWI
labour_rate = 250 # per day
contingency_rate = cls.CONTINGENCIES["sloping_ceiling_insulation"]
labour_days = cls._estimate_number_of_days_for_sloping_ceiling(insulation_roof_area)
labour_hours = labour_days * 8
total = (insulation_cost_per_m2 * insulation_roof_area) + (labour_rate * labour_days)
# Assume VAT included in the total => total is 120% of subtotal
vat = total - (total / 1.2)
return {
"total": total,
"contingency": total * contingency_rate,
"contingency_rate": contingency_rate,
"vat": vat,
"labour_hours": labour_hours,
"labour_days": labour_days,
}

View file

@ -324,10 +324,11 @@ class RoofRecommendations:
)
self.estimated_u_value = u_value
# The Roof is already compliant - in this case, the u-value is beyond the requirements for
# Building Regs Part L and so we don't recommend anything
if (u_value <= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE) or all(
m not in measures for m in MEASURE_MAP["roof_insulation"]
):
# The Roof is already compliant
return
non_invasive_recommendations = self.property.non_invasive_recommendations
@ -381,14 +382,12 @@ class RoofRecommendations:
has_room_roof_recommendation=has_room_roof_recommendation
)
##################################################
################################################################
# ~~~~~ Loft Insulation Recommendation Logic ~~~~~
##################################################
# We firstly handle non-intrusive recommendations, which may override the normal roof insulation recommendations
################################################################
if needs_loft_insulation:
self.recommend_roof_insulation(
u_value=u_value,
insulation_thickness=self.insulation_thickness,
phase=phase,
is_flat=False,
is_pitched=True,
@ -396,10 +395,12 @@ class RoofRecommendations:
)
return
################################################################
# ~~~~~ Flat Roof Insulation Recommendation Logic ~~~~~
################################################################
if needs_flat_roof_insulation:
self.recommend_roof_insulation(
u_value=u_value,
insulation_thickness=0,
phase=phase,
is_flat=True,
is_pitched=False,
@ -407,12 +408,21 @@ class RoofRecommendations:
)
return
################################################################
# ~~~~~ Room Roof Insulation Recommendation Logic ~~~~~
################################################################
# There are cases where the property might have a room roof as the second roof, but we have a recommendation for
# it, so we allow this override
if needs_rir_insulation:
self.recommend_room_roof_insulation(u_value, phase, default_u_values)
return
####################################################################################################
# ~~~~~ Sloping Ceiling Insulation Recommendation Logic ~~~~~
####################################################################################################
if needs_sloping_ceiling:
self.recommend_sloping_ceiling()
raise NotImplementedError("Implement me")
@staticmethod
@ -432,7 +442,7 @@ class RoofRecommendations:
raise ValueError("Invalid material type")
def recommend_roof_insulation(
self, u_value, insulation_thickness, phase, is_pitched, is_flat, default_u_values
self, u_value, phase, is_pitched, is_flat, default_u_values
):
"""
@ -773,3 +783,55 @@ class RoofRecommendations:
)
self.recommendations = recommendations
def recommend_sloping_ceiling(self, phase: int, u_value, sloping_ceiling_recommendation: dict = None):
"""
Recommend insulation for a sloping ceiling
Since we don't have any materials from installers for this specific recommendation, we
do not iterate through any materials. Instead, we provide a single recommendation, we estimated
prices based on desk research.
:return:
"""
new_description = "Pitched, insulated"
new_efficiency = "Good"
roof_ending_config = RoofAttributes(new_description).process()
roof_simulation_config = check_simulation_difference(
new_config=roof_ending_config, old_config=self.property.roof, prefix="roof_"
)
# We pull out new u-values, based on 75mm of insulation, with u-values defined from Elmhurst
new_u_value = 0.5 # This doesn't change, regardless of starting u-value
simulation_config = {
**roof_simulation_config,
"roof_thermal_transmittance_ending": new_u_value,
"roof_energy_eff_ending": new_efficiency
}
cost_result = self.costs.sloping_ceiling_insulation(
roof_area=self.property.roof_area # For a pitched roof, this is the pitched roof area
)
self.recommendations = [
{
"phase": phase,
"parts": [],
"type": "sloping_ceiling_insulation",
"measure_type": "sloping_ceiling_insulation",
"description": "Insulate sloping ceilings at the rafters and re-decorate",
"starting_u_value": u_value,
"new_u_value": None,
"sap_points": sloping_ceiling_recommendation.get("sap_points", None),
"simulation_config": simulation_config,
"description_simulation": {
"roof-description": new_description,
"roof-energy-eff": new_efficiency
},
**cost_result,
"already_installed": "sloping_ceiling_insulation" in self.property.already_installed,
"survey": sloping_ceiling_recommendation.get("survey", None),
"innovation_rate": 0
}
]