handling single tariff ashp

This commit is contained in:
Khalim Conn-Kowlessar 2025-08-19 16:17:24 +01:00
parent 7b1b1e0c11
commit 01722a94e2
10 changed files with 90 additions and 55 deletions

2
.idea/Model.iml generated
View file

@ -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="AssetList" jdkType="Python SDK" />
<orderEntry type="jdk" jdkName="Fastapi-backend" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

2
.idea/misc.xml generated
View file

@ -3,7 +3,7 @@
<component name="Black">
<option name="sdkName" value="Python 3.10 (backend)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="AssetList" project-jdk-type="Python SDK" />
<component name="ProjectRootManager" version="2" project-jdk-name="Fastapi-backend" project-jdk-type="Python SDK" />
<component name="PyCharmProfessionalAdvertiser">
<option name="shown" value="true" />
</component>

View file

@ -1041,7 +1041,7 @@ class Funding:
pre_heating_system=pre_heating_system
)
innovation_uplift = pps * measure["uplift"]
innovation_uplift = pps * measure["innovation_rate"]
if self.tenure == "Private":
# We return ECO4 rates

View file

@ -1238,15 +1238,11 @@ class Property:
):
return True
suitable_house = self.data["property-type"] == "House" and self.data["built-form"] in [
"Detached", "Semi-Detached", "End-Terrace",
]
suitable_property_type = (
self.data["property-type"] in ["House", "Bungalow"] and
self.data["built-form"] not in ["Enclosed Mid-Terrace", "Enclosed End-Terrace"]
)
suitable_bungalow = self.data["property-type"] == "Bungalow" and self.data["built-form"] in [
"Detached", "Semi-Detached"
]
suitable_property_type = suitable_house or suitable_bungalow
has_air_source_heat_pump = self.main_heating["has_air_source_heat_pump"]
return suitable_property_type and not has_air_source_heat_pump

View file

@ -548,22 +548,19 @@ class GoogleSolarApi:
"""
is_flat = property_instance.roof["is_flat"]
filtered_segments = []
for i, seg in enumerate(self.roof_segments):
# DO NOT overwrite seg["segmentIndex"]
keep = True
if not is_flat:
if self.NORTH_FACING_AZIMUTH_RANGE[0] <= seg['azimuthDegrees'] <= self.NORTH_FACING_AZIMUTH_RANGE[1]:
keep = False
kept = []
allowed = set()
for i, seg in enumerate(self.roof_segments): # i is the API segmentIndex
if not is_flat and (
self.NORTH_FACING_AZIMUTH_RANGE[0] <= seg['azimuthDegrees'] <= self.NORTH_FACING_AZIMUTH_RANGE[1]):
continue
s = dict(seg)
s["localIndex"] = len(kept) # for charts/UI only
kept.append(s)
allowed.add(i) # this i IS the API segmentIndex
if keep:
seg = dict(**seg) # shallow copy
seg["localIndex"] = i # optional local index as a reference from this loop
filtered_segments.append(seg)
self.roof_segments = filtered_segments
self.allowed_segment_indices = {s["segmentIndex"] for s in self.roof_segments}
self.roof_segments = kept
self.allowed_segment_indices = allowed
@staticmethod
def haversine(lat1, lon1, lat2, lon2):

View file

@ -1,5 +1,6 @@
import ast
import json
from copy import deepcopy
from datetime import datetime
from tqdm import tqdm
@ -547,7 +548,6 @@ async def model_engine(body: PlanTriggerRequest):
epc_searcher.ordnance_survey_client.property_type = config.get("property_type", None)
# For the moment, our OS API access is unavailable, so we skip and interpolate
epc_searcher.find_property(skip_os=True)
# TODO: Placeholder
if epc_searcher.newest_epc.get("estimated") and body.file_format == "domna_asset_list":
epc_searcher.newest_epc["uprn-source"] = epc_searcher.UPRN_SOURCE_SIMULATED
@ -821,11 +821,7 @@ async def model_engine(body: PlanTriggerRequest):
x in property_measure_types for x in assumptions.measures_needing_ventilation
) and not p.has_ventilation
input_measures = optimiser_functions.prepare_input_measures(
measures_to_optimise, body.goal, needs_ventilation
)
if not input_measures[0]:
if not measures_to_optimise:
# Nothing to do, we just reshape the recommendations
recommendations[p.id] = optimiser_functions.flatten_recommendations_with_defaults(
p.id, recommendations, set()
@ -838,18 +834,77 @@ async def model_engine(body: PlanTriggerRequest):
gain = optimiser_functions.calculate_gain(body=body, p=p, fixed_gain=fixed_gain)
from backend.Funding import Funding
from recommendations.optimiser.funding_optimiser import optimise_with_funding_paths
from recommendations.recommendation_utils import convert_thickness_to_numeric, get_wall_u_value
funding = Funding(
tenure=body.housing_type,
project_scores_matrix=project_scores_matrix,
partial_project_scores_matrix=partial_project_scores_matrix,
whlg_eligible_postcodes=whlg_eligible_postcodes,
eco4_social_cavity_abs_rate=13,
eco4_social_solid_abs_rate=17,
eco4_private_cavity_abs_rate=13,
eco4_private_solid_abs_rate=17,
gbis_social_cavity_abs_rate=21,
gbis_social_solid_abs_rate=25,
gbis_private_cavity_abs_rate=21,
gbis_private_solid_abs_rate=28,
)
# When the goal is Increasing EPC, we can run the funding optimiser
if body.goal == "Increasing EPC":
# We insert the innovation uplift
measures_to_optimise_with_uplift = deepcopy(measures_to_optimise)
li_thickness = convert_thickness_to_numeric(
p.roof["insulation_thickness"], p.roof["is_pitched"], p.roof["is_flat"]
)
current_wall_u_value = p.walls["thermal_transmittance"]
if current_wall_u_value is None:
current_wall_u_value = get_wall_u_value(
clean_description=p.walls["clean_description"],
age_band=p.age_band,
is_granite_or_whinstone=p.walls["is_granite_or_whinstone"],
is_sandstone_or_limestone=p.walls["is_sandstone_or_limestone"],
)
for group in measures_to_optimise_with_uplift:
for r in group:
if r["type"] in ["mechanical_ventilation", "low_energy_lighting", "secondary_heating"]:
r["innovation_uplift"] = 0
continue
r["innovation_uplift"] = funding.get_innovation_uplift(
measure=r,
starting_sap=p.data["current-energy-efficiency"],
floor_area=p.floor_area,
is_cavity=p.walls["is_cavity_wall"],
current_wall_uvalue=current_wall_u_value,
is_partial="partial" in p.walls["clean_description"].lower(),
existing_li_thickness=li_thickness,
mainheating=p.main_heating,
main_fuel=p.main_fuel,
mainheat_energy_eff=p.data["mainheat-energy-eff"],
)
input_measures = optimiser_functions.prepare_input_measures(
measures_to_optimise_with_uplift, body.goal, needs_ventilation, funding=True
)
solutions = optimise_with_funding_paths(
p=p,
input_measures=input_measures,
housing_type=body.housing_type,
budget=body.budget,
target_gain=gain,
funding=funding
)
# Given the solutions we select the optimal one
# optimal_solution =
if not body.optimise:
if body.goal != "Increasing EPC":
raise NotImplementedError("Only EPC optimisation is currently supported")

View file

@ -11,7 +11,6 @@ from etl.epc.settings import (
IGNORED_TENURES,
FULLY_GLAZED_DESCRIPTIONS,
AVERAGE_FIXED_FEATURES,
BUILT_FORM_REMAP,
COLUMNS_TO_MERGE_ON,
FIXED_FEATURES,
COLUMNTYPES,
@ -123,7 +122,6 @@ class EPCDataProcessor:
self.confine_data(ignore_step=ignore_step)
self.remap_anomalies()
self.remap_floor_level(ignore_step=ignore_step)
self.remap_build_form()
self.cast_data_column_values_to_lower()
self.standardise_construction_age_band(ignore_step=ignore_step)
self.clean_missing_rooms(ignore_step=ignore_step)
@ -240,13 +238,6 @@ class EPCDataProcessor:
for col in convert_to_lower:
self.data[col] = self.data[col].str.lower()
def remap_build_form(self):
"""
Remap build form to standard values
No Violation mode or newdata modes required
"""
self.data["BUILT_FORM"] = self.data["BUILT_FORM"].replace(BUILT_FORM_REMAP)
def remap_anomalies(self):
"""
Remap anomalies to None

View file

@ -7,7 +7,7 @@ from etl.epc.ValidationConfiguration import (
)
from etl.epc.DataProcessor import EPCDataProcessor
from recommendations.rdsap_tables import england_wales_age_band_lookup, FLOOR_LEVEL_MAP
from etl.epc.settings import DATA_ANOMALY_MATCHES, BUILT_FORM_REMAP
from etl.epc.settings import DATA_ANOMALY_MATCHES
import re
import os
import numpy as np
@ -748,10 +748,6 @@ class EPCRecord:
if not self.prepared_epc:
raise ValueError("EPC Recrod doesn not contain epc data")
self.prepared_epc["built-form"] = BUILT_FORM_REMAP.get(
self.prepared_epc["built-form"], self.prepared_epc["built-form"]
)
if self.prepared_epc["built-form"] in DATA_ANOMALY_MATCHES:
if self.prepared_epc["property-type"] in ["Flat", "Maisonette"]:
self.prepared_epc["built-form"] = "End-Terrace"

View file

@ -526,9 +526,8 @@ class HeatingRecommender:
ashp_descriptions = {
"Time and temperature zone control": (
f"Install two cascaded air source heat pumps, and upgrade heating controls to Smart Thermostats, "
"room sensors and smart radiator valves (time & temperature zone control). Ensure you have an 18 "
"or "
"24 hour tariff"
"room sensors and smart radiator valves (time & temperature zone control). Ensure you have single "
"tariff"
)
}
else:
@ -536,9 +535,8 @@ class HeatingRecommender:
ashp_descriptions = {
"Time and temperature zone control": (
f"Install a {ashp_size}KW air source heat pump, and upgrade heating controls to Smart Thermostats, "
"room sensors and smart radiator valves (time & temperature zone control). Ensure you have an 18 "
"or "
"24 hour tariff"
"room sensors and smart radiator valves (time & temperature zone control). Ensure you have a "
"single tariff"
),
"Programmer, TRVs and bypass": (
f"Install a {ashp_size}KW air source heat pump, with programmer, TRVs and a Bypass valve. Ensure "
@ -560,7 +558,7 @@ class HeatingRecommender:
ashp_costs_with_controls[key] += controls_rec[key]
if controls_rec is None:
description = f"Install a {ashp_size}KW Air source heat pump. Ensure you have an 18 or 24 hour tariff"
description = f"Install a {ashp_size}KW Air source heat pump. Ensure you have a single tariff"
elif already_installed:
description = "The property already has an air source heat pump, no further action needed."
else:
@ -581,9 +579,10 @@ class HeatingRecommender:
f" £7,500 of funding can be claimed from the boiler upgrade scheme"
)
# These are the impacts based on a single tariff with an ashp
simulation_config = {
"mainheat_energy_eff_ending": "Very Good",
"hot_water_energy_eff_ending": "Very Good"
"mainheat_energy_eff_ending": "Good",
"hot_water_energy_eff_ending": "Average"
}
description_simulation = {
"mainheat-description": new_heating_description,

View file

@ -291,5 +291,6 @@ class SolarPvRecommendations:
},
"initial_ac_kwh_per_year": recommendation_config["initial_ac_kwh_per_year"],
"description_simulation": {"photo-supply": roof_coverage_percent},
"innovation_rate": solar_pv_product["innovation_rate"],
}
)