debugging funding optimiser for existing gshp - remove ashp and hhrsh recommendations when gshp in place

This commit is contained in:
Khalim Conn-Kowlessar 2025-10-29 10:51:34 +00:00
parent bdc5147663
commit 2aecf27900
4 changed files with 269 additions and 6 deletions

View file

@ -1,11 +1,14 @@
from enum import Enum
from typing import List
import pandas as pd
from utils.logger import setup_logger
from etl.epc_clean.epc_attributes.MainheatAttributes import MainHeatAttributes
from backend.app.plan.schemas import VALID_HOUSING_TYPES, WALL_INSULATION_MEASURES, ROOF_INSULATION_MEASURES, \
MEASURE_MAP
logger = setup_logger(__name__)
class EligibilityCaveats(Enum):
EPC_RATING = "epc_rating" # EPC requirements not met
@ -637,13 +640,25 @@ class Funding:
if self.starting_sap_band in ["Low_C", "High_C", "Low_B", "High_B", "Low_A", "High_A"]:
return 0
pps = filtered_pps_matrix[
(filtered_pps_matrix["Pre_Main_Heating_Source"] == pre_heating_system) &
(filtered_pps_matrix["Post_Main_Heating_Source"] == "Air to Water ASHP") &
(filtered_pps_matrix["Measure_Type"] == "B_Upgrade_nopreHCs")
pps_data = filtered_pps_matrix[
filtered_pps_matrix["Post_Main_Heating_Source"] == "Air to Water ASHP"
]
if pre_heating_system not in pps_data["Pre_Main_Heating_Source"].values:
logger.info(
f"No PPS data for ASHP upgrade from {pre_heating_system}, returning 0"
)
return 0
pps = pps_data[
(pps_data["Pre_Main_Heating_Source"] == pre_heating_system) &
(pps_data["Measure_Type"] == "B_Upgrade_nopreHCs")
# We assume we'll be making a heating system upgrade
]
# Not every pre heating system will result in PPS, e.g. a ground source heat pump to ASHP upgrade
# won't have a PPS.
if pps.shape[0] != 1:
raise ValueError("something went wrong, more than one pps for ashp")
return pps.squeeze()["Cost Savings"]

View file

@ -0,0 +1,163 @@
import enum
import pytz
import datetime
from sqlalchemy import (
Column,
BigInteger,
Text,
DateTime,
Enum,
ForeignKey,
)
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
# -------------------------------------------------------------------
# ENUM DEFINITIONS (equivalent to drizzle pgEnum calls)
# -------------------------------------------------------------------
class InspectionArchetype(enum.Enum):
BUNGALOW = "Bungalow"
FLAT = "Flat"
MAISONETTE = "Maisonette"
HOUSE = "House"
NON_DOMESTIC = "non-domestic"
class InspectionArchetype2(enum.Enum):
DETACHED = "detached"
MID_TERRACE = "mid-terrace"
ENCLOSED_MID_TERRACE = "enclosed mid-terrace"
END_TERRACE = "end-terrace"
ENCLOSED_END_TERRACE = "enclosed end-terrace"
SEMI_DETACHED = "semi-detached"
class InspectionsWallConstruction(enum.Enum):
CAVITY = "cavity"
SOLID = "solid"
SYSTEM_BUILT = "system built"
TIMBER_FRAMED = "timber framed"
STEEL_FRAMED = "steel framed"
RE_WALLED_CAVITY = "re-walled cavity"
MANSARD_PRE_FAB = "mansard pre-fab"
MANSARD_EWI = "mansard ewi"
MANSARD_RE_WALLED = "mansard re-walled"
class InspectionsWallInsulation(enum.Enum):
EMPTY_CAVITY = "empty cavity"
FILLED_AT_BUILD = "filled at build"
PARTIAL = "partial"
RETRO_DRILLED = "retro drilled"
EWI = "ewi"
IWI = "iwi"
SOLID_NON_CAVITY = "solid non-cavity"
SYSTEM_BUILT = "system built"
TIMBER_FRAMED = "timber framed"
STEEL_FRAMED = "steel framed"
class InspectionsInsulationMaterial(enum.Enum):
EMPTY_50_90 = "empty 50-90"
EMPTY_100_PLUS = "empty 100+"
EMPTY_30_40 = "empty 30-40"
EMPTY_LESS_THAN_30 = "empty less than 30"
LOOSE_FIBRE_WOOL = "loose fibre/wool"
EPS_CELO_KING = "eps/celo/king"
FIBRE_BATTS_WITH_CAVITY = "fibre batts - with cavity"
FIBRE_BATTS_NO_CAVITY = "fibre batts - no cavity"
LOOSE_BEAD = "loose bead"
GLUED_BEAD = "glued bead"
FORMALDEHYDE = "formaldehyde"
BUBBLE_WRAP = "bubble wrap"
POLY_CHUNKS = "poly chunks"
class InspectionBorescoped(enum.Enum):
YES = "yes"
NO = "no"
REFUSED = "refused"
class InspectionsRoofOrientation(enum.Enum):
NORTH = "north"
EAST = "east"
SOUTH = "south"
WEST = "west"
NORTH_EAST = "north-east"
NORTH_WEST = "north-west"
SOUTH_EAST = "south-east"
SOUTH_WEST = "south-west"
N_S_SPLIT = "n/s split"
E_W_SPLIT = "e/w split"
NE_SW_SPLIT = "ne/sw split"
NW_SE_SPLIT = "nw/se split"
FLAT_ROOF = "flat roof"
NO_ROOF = "no roof"
ROOF_TOO_SMALL = "roof too small"
ALREADY_HAS_SOLAR_PV = "already has solar pv"
class InspectionsTileHung(enum.Enum):
YES = "yes"
NO = "no"
FIRST_FLOOR_FLATS_TILE_HUNG = "first floor flats are tile hung"
class InspectionsRendered(enum.Enum):
NO_RENDER = "no render"
INSUFFICIENT_DPC_SPACE = "rendered with “insufficient” space between dpc and render"
SUFFICIENT_DPC_SPACE = "rendered with “sufficient” space between dpc and render"
class InspectionsCladding(enum.Enum):
NONE = "none"
SUFFICIENT_SPACE = "cladded with “sufficient space to fill the wall”"
INSUFFICIENT_SPACE = "cladded with “insufficient space to fill the wall”"
class InspectionsAccessIssues(enum.Enum):
SEE_NOTES = "see notes"
DAMP_ISSUES = "damp issues"
FOLIAGE_ON_WALLS = "foliage on walls"
BUSHES_AGAINST_WALL = "bushes against wall"
TREES_AROUND_ABOVE = "trees around/anove property"
HIGH_RISE = "high rise block flats/maisonettes"
CONSERVATORY = "conservatory"
LEAN_TO = "lean-to"
GARAGE = "garage"
EXTENSION = "extension"
DECKING = "decking"
SHED_AGAINST_WALL = "shed against wall"
class InspectionModel(Base):
__tablename__ = "inspections"
id = Column(BigInteger, primary_key=True, autoincrement=True)
property_id = Column(BigInteger, ForeignKey("property.id"), nullable=False)
archetype = Column(Enum(InspectionArchetype), nullable=True)
archetype_2 = Column(Enum(InspectionArchetype2), nullable=True)
wall_construction = Column(Enum(InspectionsWallConstruction), nullable=True)
insulation = Column(Enum(InspectionsWallInsulation), nullable=True)
insulation_material = Column(Enum(InspectionsInsulationMaterial), nullable=True)
borescoped = Column(Enum(InspectionBorescoped), nullable=True)
roof_orientation = Column(Enum(InspectionsRoofOrientation), nullable=True)
tile_hung = Column(Enum(InspectionsTileHung), nullable=True)
rendered = Column(Enum(InspectionsRendered), nullable=True)
cladding = Column(Enum(InspectionsCladding), nullable=True)
access_issues = Column(Enum(InspectionsAccessIssues), nullable=True)
notes = Column(Text)
surveyor_name = Column(Text)
created_at = Column(
DateTime, nullable=False, default=datetime.datetime.now(pytz.utc)
)
uploaded_at = Column(
DateTime, nullable=False, default=datetime.datetime.now(pytz.utc)
)

View file

@ -1393,3 +1393,85 @@ def test_private_epc_e_solar_with_heating_and_minimum_insulation_produces_uplift
assert funding.eco4_uplift and funding.eco4_uplift > 0
# And total funding should include that uplift
assert funding.eco4_funding and funding.eco4_funding > 0
def test_existing_gshp_to_ashp():
r = {'phase': 3, 'parts': [], 'type': 'heating', 'measure_type': 'air_source_heat_pump',
'description': 'Install a 5KW air source heat pump, and upgrade heating controls to Smart Thermostats, '
'room sensors and smart radiator valves (time & temperature zone control). Ensure you have a '
'single tariff',
'starting_u_value': None, 'new_u_value': None, 'sap_points': 7.7, 'already_installed': False,
'simulation_config': {'mainheat_energy_eff_ending': 'Good', 'hot_water_energy_eff_ending': 'Average',
'has_air_source_heat_pump_ending': True, 'has_ground_source_heat_pump_ending': False,
'extra_features_ending': None,
'thermostatic_control_ending': 'time and temperature zone control',
'switch_system_ending': None, 'multiple_room_thermostats_ending': False,
'mainheatc_energy_eff_ending': 'Very Good'},
'description_simulation': {'mainheat-description': 'Air source heat pump, radiators, electric',
'mainheat-energy-eff': 'Good', 'hot-water-energy-eff': 'Average',
'hotwater-description': 'From main system',
'mainheatcont-description': 'Time and temperature zone control',
'mainheatc-energy-eff': 'Very Good'}, 'total': 13188.996000000001,
'contingency': 3145.8150000000005, 'contingency_rate': 0.35, 'vat': 2080.666, 'labour_hours': 44.7,
'labour_days': 6.0, 'innovation_rate': 0, 'recommendation_id': '6_phase=3',
'efficiency': 13188.996000000001, 'co2_equivalent_savings': 0.4999999999999998,
'heat_demand': 53.20000000000002, 'kwh_savings': 801.5000000000005,
'energy_cost_savings': 327.31316785714296
}
funding = Funding(
project_scores_matrix=mock_project_scores_matrix,
partial_project_scores_matrix=mock_partial_scores_matrix,
whlg_eligible_postcodes=mock_whlg_postcodes,
eco4_social_cavity_abs_rate=13.5,
eco4_social_solid_abs_rate=17,
eco4_private_cavity_abs_rate=13.5,
eco4_private_solid_abs_rate=17,
gbis_social_cavity_abs_rate=21,
gbis_social_solid_abs_rate=25,
gbis_private_cavity_abs_rate=22,
gbis_private_solid_abs_rate=28,
tenure="Private",
)
(
pps, ppf, iu, ups
) = funding.get_innovation_uplift(
measure=r,
starting_sap=62,
floor_area=69,
is_cavity=True,
current_wall_uvalue=0.7,
is_partial=False,
existing_li_thickness=200,
mainheating={
'original_description': 'Ground source heat pump, radiators, electric',
'clean_description': 'Ground source heat pump, radiators, electric', 'has_radiators': True,
'has_fan_coil_units': False, 'has_pipes_in_screed_above_insulation': False,
'has_pipes_in_insulated_timber_floor': False, 'has_pipes_in_concrete_slab': False, 'has_boiler': False,
'has_air_source_heat_pump': False, 'has_room_heaters': False, 'has_electric_storage_heaters': False,
'has_warm_air': False, 'has_electric_underfloor_heating': False, 'has_electric_ceiling_heating': False,
'has_community_scheme': False, 'has_ground_source_heat_pump': True, 'has_no_system_present': False,
'has_portable_electric_heaters': False, 'has_water_source_heat_pump': False,
'has_electric_heat_pump': False, 'has_micro-cogeneration': False, 'has_solar_assisted_heat_pump': False,
'has_exhaust_source_heat_pump': False, 'has_community_heat_pump': False, 'has_hot-water-only': False,
'has_electric': True, 'has_mains_gas': False, 'has_wood_logs': False, 'has_coal': False, 'has_oil': False,
'has_wood_pellets': False, 'has_anthracite': False, 'has_dual_fuel_mineral_and_wood': False,
'has_smokeless_fuel': False, 'has_lpg': False, 'has_b30k': False, 'has_mineral_and_wood': False,
'has_dual_fuel_appliance': False, 'has_assumed': False, 'has_electricaire': False,
'has_assumed_for_most_rooms': False, 'has_underfloor_heating': False
},
main_fuel={
'original_description': 'electricity (not community)',
'clean_description': 'Electricity not community', 'fuel_type': 'electricity', 'tariff_type': None,
'is_community': False, 'no_individual_heating_or_community_network': False,
'complex_fuel_type': None
},
mainheat_energy_eff="Poor",
)
# All should be zero
assert pps == 0
assert ppf == 0
assert iu == 0
assert ups == 0

View file

@ -103,6 +103,7 @@ class HeatingRecommender:
self.property.main_heating["has_electric"] or self.property.main_heating["has_electricaire"]
)
self.has_ashp = self.property.main_heating["has_air_source_heat_pump"]
self.has_gshp = self.property.main_heating["has_ground_source_heat_pump"]
self.has_room_heaters = (
self.property.main_heating["has_room_heaters"] or
self.property.main_heating["has_portable_electric_heaters"]
@ -151,8 +152,10 @@ class HeatingRecommender:
"underfloor heating" not in self.property.main_heating["clean_description"]
)
# If the property has a ground source heat pump, or air source heat pump, we don't recommend HHRSH
return (
hhr_suitable and (not ashp_only_heating_recommendation) and not self.has_ashp and
hhr_suitable and (not ashp_only_heating_recommendation) and not self.has_ashp and not self.has_gshp and
("high_heat_retention_storage_heater" in measures)
)
@ -345,7 +348,7 @@ class HeatingRecommender:
if (
self.property.is_ashp_valid(measures=measures) and
non_invasive_ashp_recommendation["suitable"] and
not self.has_ashp
not self.has_ashp and not self.has_gshp
):
self.recommend_air_source_heat_pump(
phase=phase,