mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
refactoring roof recommendations logic
This commit is contained in:
parent
f4f2ffb599
commit
0ad3f09902
5 changed files with 199 additions and 38 deletions
|
|
@ -60,7 +60,7 @@ def app():
|
|||
"""
|
||||
|
||||
data_folder = "/Users/khalimconn-kowlessar/Documents/hestia/Customers/Hackney"
|
||||
data_filename = "Domna SHF Wave 3.xlsx"
|
||||
data_filename = "Domna SHF Wave 3 (3).xlsx"
|
||||
sheet_name = "Domna Wave 3"
|
||||
postcode_column = 'Postcode'
|
||||
address1_column = "Address 1"
|
||||
|
|
@ -68,11 +68,11 @@ def app():
|
|||
fulladdress_column = None
|
||||
address_cols_to_concat = ["Address 1"]
|
||||
missing_postcodes_method = None
|
||||
landlord_year_built = None
|
||||
landlord_year_built = "Construction Years"
|
||||
landlord_os_uprn = "UPRN"
|
||||
landlord_property_type = None
|
||||
landlord_built_form = None
|
||||
landlord_wall_construction = None
|
||||
landlord_property_type = "Type"
|
||||
landlord_built_form = "Attachment"
|
||||
landlord_wall_construction = "Wall type"
|
||||
landlord_roof_construction = None
|
||||
landlord_heating_system = None
|
||||
landlord_existing_pv = None
|
||||
|
|
|
|||
|
|
@ -665,7 +665,7 @@ class RetrieveFindMyEpc:
|
|||
],
|
||||
"Change heating to gas condensing boiler": ["boiler_upgrade"],
|
||||
"Fan assisted storage heaters and dual immersion cylinder": ["high_heat_retention_storage_heaters"],
|
||||
"Flat roof or sloping ceiling insulation": ["flat_roof_insulation"],
|
||||
"Flat roof or sloping ceiling insulation": ["flat_roof_insulation", "sloping_ceiling_insulation"],
|
||||
"Heating controls (room thermostat)": [
|
||||
"roomstat_programmer_trvs", "time_temperature_zone_control"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -160,6 +160,13 @@ class Costs:
|
|||
"low_energy_lighting": 0.26,
|
||||
"high_heat_retention_storage_heaters": 0.1,
|
||||
"windows_glazing": 0.15,
|
||||
"boiler_upgrade": 0.26,
|
||||
"time_and_temperature_zone_control": 0.1,
|
||||
"roomstat_programmer_trvs": 0.1,
|
||||
"room_roof_insulation": 0.26,
|
||||
"heater_removal": 0.1,
|
||||
"sealing_open_fireplace": 0.1,
|
||||
"mechanical_ventilation": 0.26
|
||||
}
|
||||
|
||||
# Preliminaries are a percentage of the total cost of the work and covers the cost of site-specific costs
|
||||
|
|
@ -664,10 +671,12 @@ class Costs:
|
|||
subtotal_before_vat = total_cost / (1 + self.VAT_RATE)
|
||||
vat = total_cost - subtotal_before_vat
|
||||
|
||||
contingency_rate = self.CONTINGENCIES["roomstat_programmer_trvs"]
|
||||
|
||||
return {
|
||||
"total": total_cost,
|
||||
"contingency": total_cost * self.CONTINGENCY,
|
||||
"contingency_rate": self.CONTINGENCY,
|
||||
"contingency": total_cost * contingency_rate,
|
||||
"contingency_rate": contingency_rate,
|
||||
"subtotal": subtotal_before_vat,
|
||||
"vat": vat,
|
||||
"labour_hours": labour_hours,
|
||||
|
|
@ -698,10 +707,12 @@ class Costs:
|
|||
|
||||
labour_days = np.ceil(labour_hours / 8)
|
||||
|
||||
contingency_rate = self.CONTINGENCIES["time_and_temperature_zone_control"]
|
||||
|
||||
return {
|
||||
"total": total_cost,
|
||||
"contingency": total_cost * self.CONTINGENCY,
|
||||
"contingency_rate": self.CONTINGENCY,
|
||||
"contingency": total_cost * contingency_rate,
|
||||
"contingency_rate": contingency_rate,
|
||||
"subtotal": subtotal_before_vat,
|
||||
"vat": vat,
|
||||
"labour_hours": labour_hours,
|
||||
|
|
@ -752,10 +763,12 @@ class Costs:
|
|||
subtotal_before_vat = removal_cost
|
||||
total_cost = subtotal_before_vat + vat
|
||||
|
||||
contingency_rate = self.CONTINGENCIES["heater_removal"]
|
||||
|
||||
return {
|
||||
"total": total_cost,
|
||||
"contingency": total_cost * self.CONTINGENCY,
|
||||
"contingency_rate": self.CONTINGENCY,
|
||||
"contingency": total_cost * contingency_rate,
|
||||
"contingency_rate": contingency_rate,
|
||||
"subtotal": subtotal_before_vat,
|
||||
"vat": vat,
|
||||
"labour_hours": removal_labour_hours,
|
||||
|
|
@ -858,10 +871,12 @@ class Costs:
|
|||
subtotal_before_vat += system_change_cost_before_vat
|
||||
vat += system_change_vat
|
||||
|
||||
contingency_rate = self.CONTINGENCIES["boiler_upgrade"]
|
||||
|
||||
return {
|
||||
"total": total_cost,
|
||||
"contingency": total_cost * self.CONTINGENCY,
|
||||
"contingency_rate": self.CONTINGENCY,
|
||||
"contingency": total_cost * contingency_rate,
|
||||
"contingency_rate": contingency_rate,
|
||||
"subtotal": subtotal_before_vat,
|
||||
"vat": vat,
|
||||
"labour_hours": labour_hours,
|
||||
|
|
|
|||
|
|
@ -137,41 +137,127 @@ class RoofRecommendations:
|
|||
"""
|
||||
pass
|
||||
|
||||
def recommend(self, phase, measures=None, default_u_values=False):
|
||||
@staticmethod
|
||||
def is_sloping_ceiling_appropriate(
|
||||
is_pitched: bool, is_loft: bool, is_assumed: bool, has_sloping_ceiling_recommendation: bool,
|
||||
primary_roof_is_sloped: bool
|
||||
) -> bool:
|
||||
"""
|
||||
|
||||
:param is_pitched: Boolean - indicates whether or not the roof is pitched
|
||||
:param is_loft: Boolean - indicates whether or not the roof is described as a loft
|
||||
:param is_assumed: Boolean - indiates if the assessment of the roof is assumed or actually confirmed
|
||||
:param has_sloping_ceiling_recommendation: Boolean - indicates if the property has a sloping ceiling
|
||||
recommendation
|
||||
:param primary_roof_is_sloped: Boolean - indicates if the primary room is described a sloped (as opposed to
|
||||
an extension)
|
||||
:return:
|
||||
"""
|
||||
# We need to check:
|
||||
# 1) If the property has a pitched roof
|
||||
# 2) Does it have a recommendation for sloping ceiling
|
||||
# 3) Is the insulation status NOT assumed
|
||||
# 4) Is there a sloping ceiling recommendation (this may relate to the primary or secondary roof)
|
||||
|
||||
# If we have a loft primary roof and sloping cei
|
||||
|
||||
# The property is pitched, not a loft, not assumed and has a sloping ceiling rec
|
||||
if (is_pitched and not is_loft and not is_assumed and has_sloping_ceiling_recommendation and
|
||||
primary_roof_is_sloped):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def is_loft_insulation_appropriate(
|
||||
non_invasive_recommendations, measures, is_pitched, is_at_rafters, rir_over_loft
|
||||
) -> bool:
|
||||
"""
|
||||
Determine if loft insulation is appropriate
|
||||
:param non_invasive_recommendations: List - list of non-invasive recommendations
|
||||
:param measures: List - list of measures
|
||||
:param is_pitched: Boolean - indicates whether or not the roof is pitched
|
||||
:param is_at_rafters: Boolean - indicates whether or not the loft insulation is at rafters
|
||||
:param rir_over_loft: Boolean - indicates whether or not there we should be doing RIR insulation
|
||||
:return:
|
||||
"""
|
||||
|
||||
has_li_in_measures = "loft_insulation" in measures
|
||||
has_li_non_invasive_recommendation = any(
|
||||
x["type"] == "loft_insulation" for x in non_invasive_recommendations
|
||||
)
|
||||
|
||||
return has_li_non_invasive_recommendation or (
|
||||
is_pitched and has_li_in_measures and not is_at_rafters
|
||||
) and not rir_over_loft
|
||||
|
||||
@staticmethod
|
||||
def is_flat_roof_insulation_appropriate(
|
||||
is_flat: bool, measures: List, non_invasive_recommendations: List
|
||||
) -> bool:
|
||||
"""
|
||||
Determine if flat roof insulation is appropriate
|
||||
:param is_flat: Boolean - indicates whether or not the roof is flat
|
||||
:param measures: List - list of measures
|
||||
:param non_invasive_recommendations: List - list of non-invasive recommendations
|
||||
:return:
|
||||
"""
|
||||
|
||||
flat_roof_in_measures = "flat_roof_insulation" in measures
|
||||
flat_roof_non_invasive_rec = has_li_non_invasive_recommendation = any(
|
||||
x["type"] == "flat_roof_insulation" for x in non_invasive_recommendations
|
||||
)
|
||||
|
||||
return (is_flat and flat_roof_in_measures) or flat_roof_non_invasive_rec
|
||||
|
||||
def _does_roof_need_recommendation(self, measures: List | None = None, u_value: float | None = None):
|
||||
"""
|
||||
Utility function to recommend which contains the logic to determine whether the roof needs a recommendation
|
||||
:return:
|
||||
"""
|
||||
# If there is a property above, nothing can be done
|
||||
if self.property.roof["has_dwelling_above"]:
|
||||
return
|
||||
return False
|
||||
|
||||
measures = MEASURE_MAP["roof_insulation"] if measures is None else measures
|
||||
|
||||
u_value = self.property.roof["thermal_transmittance"]
|
||||
|
||||
# If we have a flat roof but we don't have flat roof as a measure, we exit
|
||||
# If we have a flat roof but not flat roof insulation recommendation
|
||||
if self.property.roof["is_flat"] and "flat_roof_insulation" not in measures:
|
||||
return
|
||||
return False
|
||||
|
||||
# We check if the roof is already insulated and if so, we exit
|
||||
|
||||
# Building regulations part L recommend installing at least 270mm of insulation, however generally we
|
||||
# experience diminishing returns in terms of SAP once we go beyond around 150mm of insulation
|
||||
# This only holds true for pitched roofs.
|
||||
# Logic to check if we have an already insulated loft
|
||||
if self.is_loft_already_insulated(measures):
|
||||
return
|
||||
return False
|
||||
|
||||
# Logic to check if we have an insulated flat roof
|
||||
if (self.insulation_thickness >= self.MINIMUM_FLAT_ROOF_ISULATION_MM) and self.property.roof["is_flat"]:
|
||||
return
|
||||
return False
|
||||
|
||||
# Logic to check if we have an already insulated room in roof
|
||||
if self.is_room_roof_insulated_or_unsuitable(measures):
|
||||
return
|
||||
return False
|
||||
|
||||
if self.property.roof["is_thatched"]:
|
||||
return
|
||||
return False
|
||||
|
||||
# If we have a u-value and we don't have a non-invasive recommendation, we can't recommend anything
|
||||
if (u_value is not None) and not any(
|
||||
x in MEASURE_MAP["roof_insulation"] for x in [r["type"] for r in self.property.non_invasive_recommendations]
|
||||
):
|
||||
# We don't have enough information to provide a recommendation
|
||||
return False
|
||||
|
||||
def recommend(self, phase: int, measures: List | None = None, default_u_values: bool = False):
|
||||
"""
|
||||
Main method to recommend roof insulation measures
|
||||
:param phase: Integer - phase of the recommendation, determines the order in which recommendations are
|
||||
applied to the property
|
||||
:param measures: List - list of measures to consider for recommendation
|
||||
:param default_u_values: Boolean - whether or not to use default u-values for the recommendations
|
||||
:return:
|
||||
"""
|
||||
|
||||
measures = MEASURE_MAP["roof_insulation"] if measures is None else measures
|
||||
u_value = self.property.roof["thermal_transmittance"]
|
||||
property_needs_roof_recommendation = self._does_roof_need_recommendation(measures, u_value)
|
||||
|
||||
if not property_needs_roof_recommendation:
|
||||
return
|
||||
|
||||
u_value = get_roof_u_value(
|
||||
|
|
@ -200,17 +286,37 @@ class RoofRecommendations:
|
|||
# 1) We have an uninsulated loft (assumed)
|
||||
# 2) We have a non-intrusive recommendation for room in roof insulation
|
||||
|
||||
is_pitched = self.property.roof["is_pitched"]
|
||||
is_loft = self.property.roof["is_loft"]
|
||||
is_assumed = self.property.roof["is_assumed"]
|
||||
is_at_rafters = self.property.roof["is_at_rafters"]
|
||||
has_sloping_ceiling_recommendation = any(
|
||||
x["type"] == "sloping_ceiling_insulation" for x in non_invasive_recommendations
|
||||
)
|
||||
primary_roof_is_sloped = False # TODO
|
||||
|
||||
rir_over_loft = (
|
||||
self.property.roof["is_pitched"] and
|
||||
is_pitched and
|
||||
self.property.roof["insulation_thickness"] == "none" and
|
||||
"room_in_roof_insulation" in [x["type"] for x in non_invasive_recommendations]
|
||||
)
|
||||
|
||||
needs_sloping_ceiling = self.is_sloping_ceiling_appropriate(
|
||||
is_pitched=is_pitched, is_loft=is_loft, is_assumed=is_assumed,
|
||||
has_sloping_ceiling_recommendation=has_sloping_ceiling_recommendation,
|
||||
primary_roof_is_sloped=primary_roof_is_sloped
|
||||
)
|
||||
|
||||
needs_loft_insulation = self.is_loft_insulation_appropriate(
|
||||
non_invasive_recommendations=non_invasive_recommendations, measures=measures,
|
||||
is_pitched=is_pitched, is_at_rafters=is_at_rafters, rir_over_loft=rir_over_loft
|
||||
)
|
||||
|
||||
##################################################
|
||||
# ~~~~~ Loft Insulation Recommendation Logic ~~~~~
|
||||
##################################################
|
||||
# We firstly handle non-intrusive recommendations, which may override the normal roof insulation recommendations
|
||||
if ("loft_insulation" in [x["type"] for x in non_invasive_recommendations]) or (
|
||||
self.property.roof["is_pitched"] and "loft_insulation" in measures and
|
||||
not self.property.roof["is_at_rafters"]
|
||||
) and not rir_over_loft:
|
||||
if needs_loft_insulation:
|
||||
self.recommend_roof_insulation(
|
||||
u_value=u_value,
|
||||
insulation_thickness=self.insulation_thickness,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from backend.Property import Property
|
|||
from recommendations.RoofRecommendations import RoofRecommendations
|
||||
from recommendations.tests.test_data.materials import materials
|
||||
from etl.epc.Record import EPCRecord
|
||||
import pytest
|
||||
|
||||
|
||||
class TestRoofRecommendations:
|
||||
|
|
@ -402,3 +403,42 @@ class TestRoofRecommendations:
|
|||
roof_recommender14.recommend(phase=0)
|
||||
|
||||
assert not roof_recommender14.recommendations
|
||||
|
||||
# ~~~~~~~~~~~~ Sloping Ceiling Insulation ~~~~~~~~~~~~
|
||||
@pytest.mark.parameterize("roof",
|
||||
[
|
||||
(
|
||||
# For this example, the roof is pitched, without insulation and the description
|
||||
# isn't assumed
|
||||
{'original_description': 'Pitched, no insulation', 'thermal_transmittance': None,
|
||||
'thermal_transmittance_unit': None,
|
||||
'is_pitched': True, 'is_roof_room': False, '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': 'none'}
|
||||
)
|
||||
]
|
||||
)
|
||||
def test_sloping_ceiling_valid(self, roof):
|
||||
# All conditions are met and therefore we should produce a sloping ceiling recommendation
|
||||
assert RoofRecommendations.is_sloping_ceiling_appropriate(
|
||||
is_pitched=True, is_loft=False, is_assumed=False, has_sloping_ceiling_recommendation=True
|
||||
)
|
||||
|
||||
# One condition not met - we cannot verify
|
||||
assert not RoofRecommendations.is_sloping_ceiling_appropriate(
|
||||
is_pitched=True, is_loft=True, is_assumed=False, has_sloping_ceiling_recommendation=True
|
||||
)
|
||||
assert not RoofRecommendations.is_sloping_ceiling_appropriate(
|
||||
is_pitched=False, is_loft=False, is_assumed=False, has_sloping_ceiling_recommendation=True,
|
||||
primary_roof_is_sloped=True
|
||||
)
|
||||
assert not RoofRecommendations.is_sloping_ceiling_appropriate(
|
||||
is_pitched=True, is_loft=False, is_assumed=True, has_sloping_ceiling_recommendation=True,
|
||||
primary_roof_is_sloped=True
|
||||
)
|
||||
assert not RoofRecommendations.is_sloping_ceiling_appropriate(
|
||||
is_pitched=True, is_loft=False, is_assumed=True, has_sloping_ceiling_recommendation=True,
|
||||
primary_roof_is_sloped=True
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue