added restrictions on heating systems only for ESH, fixed bug in funding solutiosn

This commit is contained in:
Khalim Conn-Kowlessar 2025-11-07 19:41:42 +00:00
parent 3edf5549af
commit 19a766f442
3 changed files with 47 additions and 12 deletions

View file

@ -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 = []

View file

@ -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)

View file

@ -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)