diff --git a/backend/SearchEpc.py b/backend/SearchEpc.py index 60999e94..82899a81 100644 --- a/backend/SearchEpc.py +++ b/backend/SearchEpc.py @@ -156,6 +156,7 @@ class SearchEpc: size=None, property_type=None, fast=False, + heating_system: [str, None] = None ): """ Address lines 1 and postcode are mandatory fields. The other address lines are optional @@ -180,6 +181,9 @@ class SearchEpc: self.house_number = self.get_house_number(self.address1) self.numeric_house_number = self.extract_numeric_housenumber_part(self.house_number) + # property attributes + self.heating_system = heating_system + self.max_retries = max_retries if max_retries is not None else self.MAX_RETRIES self.client = EpcClient(auth_token=auth_token) @@ -571,7 +575,8 @@ class SearchEpc: lmks_to_drop: list[str] | None = None, built_form: str = "", property_type: str = "", - exclude_old: bool = False + exclude_old: bool = False, + heating_system: [str, None] = None ): """ Fetches and processes EPC data for a given initial postcode, applying successive trimming @@ -591,6 +596,7 @@ class SearchEpc: :param built_form: The 'built-form' value to be used for filtering the EPC data. :param property_type: The 'property-type' value to be used for filtering the EPC data. :param exclude_old: Flag to exclude EPC data older than 10 years. + :param heating_system: Optional heating system type for additional filtering. :return: """ @@ -703,6 +709,11 @@ class SearchEpc: epc_data["property-type"] == estimation_property_type) ] + if heating_system is not None: + epc_data = epc_data[ + epc_data["mainheat-description"] == heating_system + ] + if not epc_data.empty: return epc_data # Return the filtered data if it's not empty @@ -712,7 +723,7 @@ class SearchEpc: # If loop finishes without a valid response, raise an exception raise Exception("Unable to find postcode data after trimming - investigate me") - def estimate_epc(self, property_type, built_form, lmks_to_drop=None, exclude_old=False): + def estimate_epc(self, property_type, built_form, lmks_to_drop=None, exclude_old=False, heating_system=None): """ For a property that does not have an EPC, we retrieve the EPC data for the closest properties and estimate the EPC for the property in question. @@ -726,6 +737,8 @@ class SearchEpc: :param lmks_to_drop: This is a list of LMK keys that should be dropped from the estimation process. This is used as an override for testing, to drop EPCs for the property we are testing :param exclude_old: Used to drop any expired EPCs (more than 10 years old) + :param heating_system: The heating system of the property we are estimating, if known. Will aim to filter EPCs + to matching heating systems :return: """ @@ -736,7 +749,8 @@ class SearchEpc: lmks_to_drop=lmks_to_drop, built_form=built_form, property_type=property_type, - exclude_old=exclude_old + exclude_old=exclude_old, + heating_system=heating_system ) # Check if it's a new build EPC. A property that doesn't have an EPC is not going to be a new build @@ -906,7 +920,8 @@ class SearchEpc: # We can try and estimate estimated_epc = self.estimate_epc( property_type=self.ordnance_survey_client.property_type, - built_form=self.ordnance_survey_client.built_form + built_form=self.ordnance_survey_client.built_form, + heating_system=self.heating_system ) self.newest_epc = estimated_epc self.older_epcs = [] diff --git a/backend/engine/engine.py b/backend/engine/engine.py index dafcf01e..1a1e75b8 100644 --- a/backend/engine/engine.py +++ b/backend/engine/engine.py @@ -3,6 +3,7 @@ import json from copy import deepcopy from datetime import datetime +from sqlalchemy import Nullable from tqdm import tqdm import pandas as pd import numpy as np @@ -59,7 +60,7 @@ from recommendations.recommendation_utils import convert_thickness_to_numeric, g logger = setup_logger() BATCH_SIZE = 5 -SCORING_BATCH_SIZE = 100 +SCORING_BATCH_SIZE = 300 def extract_portfolio_aggregation_data( @@ -373,6 +374,24 @@ def get_funding_data(): return project_scores_matrix, partial_project_scores_matrix, whlg_eligible_postcodes +def parse_heating_system(config): + """ + Helper function to extract a heating system, which can be used to estimate EPC. This is a very limited, + placeholder function to cover some initial immediate cases. + :return: + """ + + ll_heating = config.get("landlord_heating_system", None) + if not ll_heating: + return None + + if ll_heating == "electric storage heaters": + # Return with the same format at the EPC + return "Electric storage heaters" + + return None + + async def model_engine(body: PlanTriggerRequest): logger.info("Model Engine triggered with body: %s", json.loads(body.model_dump_json())) @@ -502,8 +521,8 @@ async def model_engine(body: PlanTriggerRequest): address1 = config.get("domna_full_address", None) address1 = str(int(address1)) if isinstance(address1, float) else str(address1) - full_address = config["domna_full_address"] if body.file_format == "domna_asset_list" else None + heating_system = parse_heating_system(config) epc_searcher = SearchEpc( address1=address1, @@ -511,7 +530,8 @@ async def model_engine(body: PlanTriggerRequest): uprn=uprn, auth_token=get_settings().EPC_AUTH_TOKEN, os_api_key="", - full_address=full_address + full_address=full_address, + heating_system=heating_system ) epc_searcher.ordnance_survey_client.built_form = config.get("built_form", None) epc_searcher.ordnance_survey_client.property_type = config.get("property_type", None) diff --git a/recommendations/optimiser/funding_optimiser.py b/recommendations/optimiser/funding_optimiser.py index 4ac96f00..417363cd 100644 --- a/recommendations/optimiser/funding_optimiser.py +++ b/recommendations/optimiser/funding_optimiser.py @@ -9,6 +9,7 @@ In the future, we will adapt this into a class-based structure to allow for more from copy import deepcopy import pandas as pd +import numpy as np from backend.app.plan.schemas import ( WALL_INSULATION_MEASURES, ROOF_INSULATION_MEASURES, ECO4_ELIGIBILE_FABRIC_MEASURES @@ -401,9 +402,7 @@ def optimise_with_funding_paths( # If we have a budget, we need to ensure the subproblem respects it so we remove the fixed cost (which # may already be over budget) and the fixed gain (which may not be achievable) - if fixed_gain > target_gain: - picked, sub_cost, sub_gain = ([], 0.0, 0.0) - elif fixed_gain <= target_gain and not sub_measures: + if (fixed_gain > target_gain) or (fixed_gain <= target_gain and not sub_measures): picked, sub_cost, sub_gain = ([], 0.0, 0.0) else: picked, sub_cost, sub_gain = run_optimizer( @@ -412,8 +411,9 @@ def optimise_with_funding_paths( sub_target_gain=target_gain - fixed_gain if target_gain is not None else None ) - if picked is None: - continue + # if picked is None: + # # If we have something in sub_measures, then we have a partial solution, just not enough to + # continue scheme = _path_scheme(path_spec)