From 01722a94e24720141077897472c3853347961dd3 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 19 Aug 2025 16:17:24 +0100 Subject: [PATCH] handling single tariff ashp --- .idea/Model.iml | 2 +- .idea/misc.xml | 2 +- backend/Funding.py | 2 +- backend/Property.py | 12 ++-- backend/apis/GoogleSolarApi.py | 27 ++++----- backend/engine/engine.py | 67 +++++++++++++++++++++-- etl/epc/DataProcessor.py | 9 --- etl/epc/Record.py | 6 +- recommendations/HeatingRecommender.py | 17 +++--- recommendations/SolarPvRecommendations.py | 1 + 10 files changed, 90 insertions(+), 55 deletions(-) diff --git a/.idea/Model.iml b/.idea/Model.iml index 09f2e496..c6561970 100644 --- a/.idea/Model.iml +++ b/.idea/Model.iml @@ -7,7 +7,7 @@ - + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index fb10c6b0..50cad4ca 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,7 +3,7 @@ - + diff --git a/backend/Funding.py b/backend/Funding.py index 4a198bf9..4609e3d4 100644 --- a/backend/Funding.py +++ b/backend/Funding.py @@ -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 diff --git a/backend/Property.py b/backend/Property.py index 66ca84e3..d6f43b8a 100644 --- a/backend/Property.py +++ b/backend/Property.py @@ -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 diff --git a/backend/apis/GoogleSolarApi.py b/backend/apis/GoogleSolarApi.py index 9d136b22..9073b307 100644 --- a/backend/apis/GoogleSolarApi.py +++ b/backend/apis/GoogleSolarApi.py @@ -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): diff --git a/backend/engine/engine.py b/backend/engine/engine.py index 026b0405..d97d96ab 100644 --- a/backend/engine/engine.py +++ b/backend/engine/engine.py @@ -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") diff --git a/etl/epc/DataProcessor.py b/etl/epc/DataProcessor.py index 9655cf77..f5fc3582 100644 --- a/etl/epc/DataProcessor.py +++ b/etl/epc/DataProcessor.py @@ -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 diff --git a/etl/epc/Record.py b/etl/epc/Record.py index b8950757..8e6be5d0 100644 --- a/etl/epc/Record.py +++ b/etl/epc/Record.py @@ -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" diff --git a/recommendations/HeatingRecommender.py b/recommendations/HeatingRecommender.py index e7e008d5..d2bccbcc 100644 --- a/recommendations/HeatingRecommender.py +++ b/recommendations/HeatingRecommender.py @@ -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, diff --git a/recommendations/SolarPvRecommendations.py b/recommendations/SolarPvRecommendations.py index 1a9b47ac..f21b7bf3 100644 --- a/recommendations/SolarPvRecommendations.py +++ b/recommendations/SolarPvRecommendations.py @@ -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"], } )