diff --git a/backend/app/db/functions/funding_functions.py b/backend/app/db/functions/funding_functions.py index 3c001266..51dffa21 100644 --- a/backend/app/db/functions/funding_functions.py +++ b/backend/app/db/functions/funding_functions.py @@ -40,6 +40,9 @@ def upload_funding(session: Session, p, plan_id, recommendations_to_upload): part_type = "cavity_wall_insulation" if part_type == "sealing_open_fireplace": part_type = "sealing_fireplace" + if part == "low_energy_lighting": + part_type = "low_energy_lighting_installation" + funding_measures_data.append({ "funding_package_id": funding_package_id, "measure": part_type, diff --git a/backend/engine/engine.py b/backend/engine/engine.py index 6f9cac21..2e1ede79 100644 --- a/backend/engine/engine.py +++ b/backend/engine/engine.py @@ -943,6 +943,7 @@ async def model_engine(body: PlanTriggerRequest): # If we have a solution that meets the upgrade target, we select that one optimal_solution = solutions[solutions["meets_upgrade_target"]].iloc[0] else: + # Pick the cheapest optimal_solution = solutions.iloc[0] # This is the list of measures that we will recommend @@ -950,7 +951,8 @@ async def model_engine(body: PlanTriggerRequest): funded_measures = optimal_solution["items"] if scheme != "none" else [] solution = optimal_solution["items"] + optimal_solution["unfunded_items"] # This is the total amount of funding that the project will produce (including uplifts) (£) - project_funding = optimal_solution["full_project_funding"] + project_funding = optimal_solution["full_project_funding"] if scheme == "eco4" else \ + optimal_solution["partial_project_funding"] # This is the total amount of funding associated to the uplift (£) total_uplift = optimal_solution["total_uplift"] # This is the funding scheme selected diff --git a/recommendations/Costs.py b/recommendations/Costs.py index fccc2fc8..33d7b061 100644 --- a/recommendations/Costs.py +++ b/recommendations/Costs.py @@ -1,5 +1,8 @@ import numpy as np from recommendations.county_to_region import county_to_region_map +from utils.logger import setup_logger + +logger = setup_logger() # This data comes from SPONs 2023 regional_labour_variations = [ @@ -224,7 +227,9 @@ class Costs: }.get(self.property.data["local-authority-label"].lower(), None) if self.region is None: - raise ValueError("Region not found in county map") + logger.warning("No region found for county %s, defaulting to South East England", + self.property.data["county"]) + self.region = "South East England" self.labour_adjustment_factor = [ x["Adjustment_Factor"] for x in self.regional_labour_variations if diff --git a/recommendations/county_to_region.py b/recommendations/county_to_region.py index b6b74ee4..35e1852d 100644 --- a/recommendations/county_to_region.py +++ b/recommendations/county_to_region.py @@ -110,7 +110,9 @@ county_to_region_map = { 'West Oxfordshire': 'South East England', 'West Sussex': 'South East England', 'Winchester': 'South East England', 'Windsor and Maidenhead': 'South East England', 'Woking': 'South East England', 'Wokingham': 'South East England', 'Worthing': 'South East England', 'Wycombe': 'South East England', - 'Bath and North East Somerset': 'South West England', 'Bournemouth': 'South West England', + 'Bath and North East Somerset': 'South West England', + 'Bournemouth': 'South West England', + 'Bournemouth, Christchurch and Poole': 'South West England', 'Bristol': 'South West England', 'Cheltenham': 'South West England', 'Christchurch': 'South West England', 'City of Bristol': 'South West England', diff --git a/recommendations/optimiser/funding_optimiser.py b/recommendations/optimiser/funding_optimiser.py index 7150c93c..84ad4dbc 100644 --- a/recommendations/optimiser/funding_optimiser.py +++ b/recommendations/optimiser/funding_optimiser.py @@ -21,7 +21,9 @@ from backend.Funding import Funding logger = setup_logger() # measures we DO NOT treat as fundable in the ECO4 'funded' pass -_ECO4_EXCLUDE_TYPES = {"secondary_heating", "extension_cavity_wall_insulation", "sealing_open_fireplace"} +_ECO4_EXCLUDE_TYPES = { + "secondary_heating", "extension_cavity_wall_insulation", "sealing_open_fireplace", "low_energy_lighting" +} def _path_scheme(path_spec): @@ -329,25 +331,31 @@ def optimise_with_funding_paths(p, input_measures, housing_type, funding: Fundin if picked is None: continue + scheme = _path_scheme(path_spec) + total_cost = fixed_cost + sub_cost total_gain = fixed_gain + sub_gain - total_picks = fixed_items + picked + + unfunded_picked = [] + if scheme == "gbis": + # The fixed items are fundded, everything else is unfunded + total_picks = fixed_items + unfunded_picked = picked + else: + total_picks = fixed_items + picked if housing_type == "Private": - if not _prs_solution_ok(total_picks, p, funding): + if not _prs_solution_ok(total_picks, p, funding) and scheme == "eco4": logger.error( "Found a solution that does not meet the PRS requirements: %s - this shouldn't be happening", total_picks ) continue - scheme = _path_scheme(path_spec) - - unfunded_picked = [] if total_gain - target_gain < -0.1: # In this case, we have a funded package that does not meet the target gain, so we look at the remaining # measures and see if we can include them - picked_types = {opt["type"] for opt in total_picks} + picked_types = {opt["type"] for opt in total_picks + unfunded_picked} # We find the indexes of the picked types picked_group_index = {} @@ -371,11 +379,13 @@ def optimise_with_funding_paths(p, input_measures, housing_type, funding: Fundin if remaining: # If we have remaining measures we can optimise, we run them down an unfunded route - unfunded_picked, unfunded_cost, unfunded_gain = run_optimizer( + unfunded_picked_remaining, unfunded_cost, unfunded_gain = run_optimizer( remaining, budget - total_cost if budget is not None else None, sub_target_gain=target_gain - total_gain if target_gain is not None else None ) + if unfunded_picked_remaining is not None: + unfunded_picked += unfunded_picked_remaining total_cost += unfunded_cost total_gain += unfunded_gain @@ -417,13 +427,6 @@ def optimise_with_funding_paths(p, input_measures, housing_type, funding: Fundin axis=1 ) - for _, x in solutions.iterrows(): - funding._calculate_full_project_abs( - floor_area_band=x["floor_area_band"], - starting_sap_band=x["starting_band"], - ending_sap_band=x["ending_band"], - ) - rate = funding.get_eco4_abs_rate(is_cavity=p.walls["is_cavity_wall"]) solutions["full_project_funding"] = solutions["project_score"] * rate # if the scheme is not ECO4, we set the funding to 0 with iloc @@ -803,6 +806,7 @@ def make_funding_paths(p, input_measures, housing_type, funding: Funding): :param p: The property object containing details about the property, including main heating and controls. :param input_measures: :param housing_type: + :param funding: The funding object that provides methods to check eligibility and calculate funding. :return: """ # We handle the case of minimum insulation requirements. Whenever we have a heating system recommendation, @@ -862,25 +866,27 @@ def make_funding_paths(p, input_measures, housing_type, funding: Funding): return funding_paths, input_measures_innovation if housing_type == "Private": + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # EWI or IWI # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # 1) The package must include EWI or IWI if the property is private rental sector - # We check if we have any EWI or IWI measures available - ewi_or_iwi = [{"OR": []}] - reference_measures = [] - # If we have EWI we add it in - if _find_measure(input_measures, "external_wall_insulation"): - ewi_or_iwi[0]["OR"].append("external_wall_insulation") - reference_measures.append("ewi") + # We check if we have any EWI or IWI measures available - only for EPC E or below + if p.data["current-energy-rating"] not in ["E", "F", "G"]: + ewi_or_iwi = [{"OR": []}] + reference_measures = [] + # If we have EWI we add it in + if _find_measure(input_measures, "external_wall_insulation"): + ewi_or_iwi[0]["OR"].append("external_wall_insulation") + reference_measures.append("ewi") - if _find_measure(input_measures, "internal_wall_insulation"): - ewi_or_iwi[0]["OR"].append("internal_wall_insulation") - reference_measures.append("iwi") + if _find_measure(input_measures, "internal_wall_insulation"): + ewi_or_iwi[0]["OR"].append("internal_wall_insulation") + reference_measures.append("iwi") - if ewi_or_iwi[0]["OR"]: - ewi_or_iwi[0]["reference"] = "+".join(reference_measures) + ":eco4" - funding_paths.append(ewi_or_iwi) + if ewi_or_iwi[0]["OR"]: + ewi_or_iwi[0]["reference"] = "+".join(reference_measures) + ":eco4" + funding_paths.append(ewi_or_iwi) funding_paths = _make_solar_heating_funding_paths( p, input_measures, funding_paths, remaining_insulation_type, housing_type, funding