refactored the handling of dual heating recommendations and fixing coverage of heating types in property class

This commit is contained in:
Khalim Conn-Kowlessar 2025-11-14 22:20:03 +00:00
parent 3624b34dd0
commit 005c6b844a
3 changed files with 140 additions and 60 deletions

View file

@ -1221,7 +1221,8 @@ class Property:
None: "Natural Gas (Community Scheme)",
"mains gas": "Natural Gas (Community Scheme)",
"biomass": "Smokeless Fuel",
"electricity": "Electricity"
"electricity": "Electricity",
"biogas": "Smokeless Fuel",
}
if self.main_fuel["fuel_type"] in fuel_map: # We assume when None as it's unknown
self.heating_energy_source = fuel_map[self.main_fuel["fuel_type"]]

View file

@ -1,36 +1,36 @@
import ast
import json
# import ast
# import json
from copy import deepcopy
from dataclasses import replace
from datetime import datetime
# from dataclasses import replace
# from datetime import datetime
import random
from tqdm import tqdm
import pandas as pd
# import pandas as pd
import numpy as np
from etl.epc.Record import EPCRecord
from backend.SearchEpc import SearchEpc
from sqlalchemy.exc import IntegrityError, OperationalError
from sqlalchemy.orm import sessionmaker
from starlette.responses import Response
# from backend.SearchEpc import SearchEpc
# from sqlalchemy.exc import IntegrityError, OperationalError
# from sqlalchemy.orm import sessionmaker
# from starlette.responses import Response
from backend.app.config import get_settings, get_prediction_buckets
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,
update_or_create_property_spatial_details
)
from backend.app.db.functions.recommendations_functions import (
create_plan, upload_recommendations, create_scenario
)
from backend.app.db.functions.funding_functions import upload_funding
from backend.app.db.functions.energy_assessment_functions import get_latest_assessment_by_uprn
from backend.app.db.models.portfolio import rating_lookup
# from backend.app.config import get_settings, get_prediction_buckets
# 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,
# update_or_create_property_spatial_details
# )
# from backend.app.db.functions.recommendations_functions import (
# create_plan, upload_recommendations, create_scenario
# )
# from backend.app.db.functions.funding_functions import upload_funding
# from backend.app.db.functions.energy_assessment_functions import get_latest_assessment_by_uprn
# from backend.app.db.models.portfolio import rating_lookup
from backend.app.plan.schemas import PlanTriggerRequest, WALL_INSULATION_MEASURES, ROOF_INSULATION_MEASURES
from backend.app.plan.utils import get_cleaned
from backend.app.utils import sap_to_epc
# from backend.app.plan.utils import get_cleaned
# from backend.app.utils import sap_to_epc
import backend.app.assumptions as assumptions
from backend.ml_models.api import ModelApi
@ -41,13 +41,13 @@ from recommendations.optimiser.CostOptimiser import CostOptimiser
from recommendations.optimiser.GainOptimiser import GainOptimiser
import recommendations.optimiser.optimiser_functions as optimiser_functions
from recommendations.Recommendations import Recommendations
from utils.logger import setup_logger
from utils.s3 import read_dataframe_from_s3_parquet, read_csv_from_s3, read_excel_from_s3
from backend.ml_models.Valuation import PropertyValuation
from etl.bill_savings.KwhData import KwhData
from etl.spatial.OpenUprnClient import OpenUprnClient
from etl.find_my_epc.RetrieveFindMyEpc import RetrieveFindMyEpc
# from utils.logger import setup_logger
# from utils.s3 import read_dataframe_from_s3_parquet, read_csv_from_s3, read_excel_from_s3
# from backend.ml_models.Valuation import PropertyValuation
#
# from etl.bill_savings.KwhData import KwhData
# from etl.spatial.OpenUprnClient import OpenUprnClient
# from etl.find_my_epc.RetrieveFindMyEpc import RetrieveFindMyEpc
from backend.Funding import Funding
from recommendations.optimiser.funding_optimiser import optimise_with_funding_paths
@ -91,7 +91,7 @@ costs_by_floor_area = costs_by_floor_area.groupby("current-energy-efficiency")[
].mean().reset_index()
sample_epc_data = epc_data[pd.to_datetime(epc_data["LODGEMENT_DATE"]) >= "2015-01-01"].drop_duplicates("UPRN").sample(
20000).reset_index(drop=True)
3000).reset_index(drop=True)
# TODO: In Property find_energy_sources, sort out biomass community heating - what fuel type
# TODO: We might be able to remove find_energy_sources entirely and remove estimate_electrical_consumption. It's used

View file

@ -10,6 +10,9 @@ from etl.epc_clean.epc_attributes.MainheatAttributes import MainHeatAttributes
from etl.epc_clean.epc_attributes.HotWaterAttributes import HotWaterAttributes
from etl.epc_clean.epc_attributes.MainFuelAttributes import MainFuelAttributes
from recommendations.HeatingControlRecommender import HeatingControlRecommender
from utils.logger import setup_logger
logger = setup_logger()
class HeatingRecommender:
@ -44,6 +47,22 @@ class HeatingRecommender:
]
}
},
"Boiler and radiators, mains gas, electric underfloor heating": {
"boiler": {
"mainheating_description": "Boiler and radiators, mains gas, electric underfloor heating",
"recommendation_description": "Upgrade the existing boiler to a new, more efficient condensing "
"boiler. ",
"controls_suffix": "Manual charge controls"
},
# These are the heating types we need to produce a dual heating recommendation
"dual": {
"recommendation_description": "Upgrade the existing boiler to a new condensing boiler",
"types": [
# type 1
"boiler_upgrade",
]
}
},
"Portable electric heaters assumed for most rooms, room heaters, electric": {
"hhr": {
"mainheating_description": "Electric storage heaters, radiators",
@ -127,7 +146,7 @@ class HeatingRecommender:
n_trues += 1
if n_trues > 2 or n_trues == 0:
raise Exception("Implement me")
raise NotImplementedError("Implement me, zero or more than two heating systemss")
if n_trues == 1:
return False
@ -917,9 +936,11 @@ class HeatingRecommender:
if self.property.main_heating_controls["clean_description"] != self.high_heat_retention_contols_desc:
if self.dual_heating:
controls_prefix = self.DUAL_HEATING_DESCRIPTIONS[
self.property.main_heating["clean_description"]
]["hhr"]["controls_prefix"]
controls_prefix = self._map_dual_heating_description(
backup_map_to_description="current_controls",
output_type="controls_prefix",
recommendation_type="hhr"
)
if controls_prefix == "current_controls":
description_prefix = self.property.main_heating_controls["clean_description"]
@ -951,9 +972,11 @@ class HeatingRecommender:
# We check if the property has dual heating in place with a boiler and storage heaters
if self.dual_heating:
new_heating_description = self.DUAL_HEATING_DESCRIPTIONS[
self.property.main_heating["clean_description"]
]["hhr"]["mainheating_description"]
new_heating_description = self._map_dual_heating_description(
backup_map_to_description="Electric storage heaters",
output_type="mainheating_description",
recommendation_type="hhr"
)
new_hot_water_description = self.property.hotwater["clean_description"] # We keep the hot water system
else:
new_heating_description = "Electric storage heaters"
@ -1010,10 +1033,12 @@ class HeatingRecommender:
product=hhrsh_product
)
if self.dual_heating:
description = self.DUAL_HEATING_DESCRIPTIONS[
self.property.main_heating["clean_description"]
]["hhr"]["recommendation_description"]
description = self._map_dual_heating_description(
backup_map_to_description="Install high heat retention electric storage heaters with an appropriate "
"off-peak tariff.",
output_type="recommendation_description",
recommendation_type="hhr"
)
else:
description = "Install high heat retention electric storage heaters with an appropriate off-peak tariff."
@ -1102,6 +1127,60 @@ class HeatingRecommender:
return max(num_heated_rooms * 1.5, 6)
def _map_dual_heating_description(
self, backup_map_to_description, output_type, recommendation_type
):
"""
Utility function to handle dual heating systems
:param backup_map_to_description:
:return:
"""
if backup_map_to_description not in [
# Recommendation descriptions - these are the textual descriptions shown in the front end
"Upgrade to a new condensing boiler.",
"Install high heat retention electric storage heaters with an appropriate off-peak tariff.",
# Simulation descriptions - this is the new EPC description we simulate with in the case
# of single heating
"Boiler and radiators, mains gas",
"Electric storage heaters",
# Suffixes allowed
"",
# Controls prefixes
"current_controls"
]:
raise ValueError(f"Invalid backup_map_to_description, given {backup_map_to_description}")
if output_type not in [
"recommendation_description",
"mainheating_description",
"controls_suffix",
"controls_prefix",
]:
raise ValueError(f"Invalid output_type, given {output_type}")
if recommendation_type not in [
"boiler",
]:
raise ValueError(f"Given invalid recommendation type {recommendation_type}")
# "Upgrade to a new condensing boiler."
if self.dual_heating:
# We check if we have a mapped description
if self.property.main_heating["clean_description"] not in self.DUAL_HEATING_DESCRIPTIONS:
logger.warning(
f"We have a dual heating system that hasn't been mapped, defaulting to single "
f"{self.property.main_heating['clean_description']}"
)
return backup_map_to_description
return self.DUAL_HEATING_DESCRIPTIONS[
self.property.main_heating["clean_description"]
][recommendation_type][output_type]
return backup_map_to_description
def recommend_boiler_upgrades(self, phase, system_change, exising_room_heaters):
"""
This boiler recommendation will only recommend a like-for-like upgrade, since changing the system
@ -1137,12 +1216,11 @@ class HeatingRecommender:
if has_inefficient_space_heating or has_inefficient_water:
if self.dual_heating:
description = self.DUAL_HEATING_DESCRIPTIONS[
self.property.main_heating["clean_description"]
]["boiler"]["recommendation_description"]
else:
description = "Upgrade to a new condensing boiler."
description = self._map_dual_heating_description(
backup_map_to_description="Upgrade to a new condensing boiler.",
output_type="recommendation_description",
recommendation_type="boiler"
)
new_heating_eff = (
"Good" if self.property.data["mainheat-energy-eff"] in ["Very Poor", "Poor", "Average"]
@ -1167,13 +1245,12 @@ class HeatingRecommender:
if system_change:
# Installation of a boiler improves the hot water system so we need to reflect this in
# the outcome of the recommendation
if self.dual_heating:
new_heating_description = self.DUAL_HEATING_DESCRIPTIONS[
self.property.main_heating["clean_description"]
]["boiler"]["mainheating_description"]
else:
new_heating_description = "Boiler and radiators, mains gas"
new_heating_description = self._map_dual_heating_description(
backup_map_to_description="Boiler and radiators, mains gas",
output_type="mainheating_description",
recommendation_type="boiler"
)
new_hotwater_description = "From main system"
new_fuel_description = "mains gas (not community)"
@ -1239,9 +1316,11 @@ class HeatingRecommender:
# If the property did not previously have a boiler, we combine
controls_recommender = HeatingControlRecommender(self.property)
if self.dual_heating:
description_suffix = self.DUAL_HEATING_DESCRIPTIONS[
self.property.main_heating["clean_description"]
]["boiler"]["controls_suffix"]
description_suffix = self._map_dual_heating_description(
backup_map_to_description="",
output_type="controls_suffix",
recommendation_type="boiler"
)
else:
description_suffix = ""
controls_recommender.recommend(