diff --git a/backend/Funding.py b/backend/Funding.py index 18e77713..3c30ba27 100644 --- a/backend/Funding.py +++ b/backend/Funding.py @@ -1,6 +1,8 @@ from enum import Enum from typing import List +import pandas as pd + from backend.app.plan.schemas import HousingType, WALL_INSULATION_MEASURES, ROOF_INSULATION_MEASURES, MEASURE_MAP @@ -38,11 +40,6 @@ class Funding: self.floor_area_band = None self.project_scores_matrix = project_scores_matrix self.partial_project_scores_matrix = partial_project_scores_matrix - # Filter on the starting band and floor area so we only do this once - self.partial_project_scores_matrix = self.partial_project_scores_matrix[ - (self.partial_project_scores_matrix["Total Floor Area Band"] == self.floor_area_band) & - (self.partial_project_scores_matrix["Starting Band"] == self.starting_sap_band) - ] self.whlg_eligible_postcodes = whlg_eligible_postcodes @@ -408,16 +405,16 @@ class Funding: ]: return 'Electric Storage Heaters Responsiveness >0.2' - if mainheating["has_room_heaters"] and main_fuel["fuel_tye"] == "lpg": + if mainheating["has_room_heaters"] and main_fuel["fuel_type"] == "lpg": return 'Bottled LPG Room Heaters' - if mainheating["has_room_heaters"] and main_fuel["fuel_tye"] == "electricity": + if mainheating["has_room_heaters"] and main_fuel["fuel_type"] == "electricity": return 'Electric Room Heaters' - if mainheating["has_room_heaters"] and main_fuel["fuel_tye"] == "mains gas": + if mainheating["has_room_heaters"] and main_fuel["fuel_type"] == "mains gas": return 'Gas Room Heaters' - if mainheating["has_room_heaters"] and main_fuel["fuel_tye"] in [ + if mainheating["has_room_heaters"] and main_fuel["fuel_type"] in [ "dual fuel appliance mineral and wood", "manufactured smokeless fuel" ]: return 'Solid Fossil Room Heaters' @@ -430,6 +427,7 @@ class Funding: mainheating: dict, main_fuel: dict, mainheat_energy_eff: str, + filtered_pps_matrix: pd.DataFrame, current_wall_uvalue: float = None, is_partial: bool = False, existing_li_thickness: float = None, @@ -438,7 +436,7 @@ class Funding: """ Calculate the partial project ABS score for a single measure. """ - df = self.partial_project_scores_matrix + # Filter on the starting band and floor area so we only do this once if measure_type == "internal_wall_insulation": if current_wall_uvalue is None: @@ -446,7 +444,7 @@ class Funding: starting_str, ending_str = self.get_starting_ending_uvalues(current_wall_uvalue) measure_code = f"IWI_solid_{starting_str}_{ending_str}" - pps = df[df["Measure_Type"] == measure_code] + pps = filtered_pps_matrix[filtered_pps_matrix["Measure_Type"] == measure_code] if pps.shape[0] != 1: raise ValueError(f"Invalid IWI category: {measure_code}") @@ -458,7 +456,7 @@ class Funding: starting_str, ending_str = self.get_starting_ending_uvalues(current_wall_uvalue) measure_code = f"EWI_solid_{starting_str}_{ending_str}" - pps = df[df["Measure_Type"] == measure_code] + pps = filtered_pps_matrix[filtered_pps_matrix["Measure_Type"] == measure_code] if pps.shape[0] != 1: raise ValueError(f"Invalid EWI category: {measure_code}") @@ -466,7 +464,7 @@ class Funding: if measure_type == "cavity_wall_insulation": measure_code = "CWI_partial_fill" if is_partial else "CWI_0.033" - pps = df[df["Measure_Type"] == measure_code] + pps = filtered_pps_matrix[filtered_pps_matrix["Measure_Type"] == measure_code] if pps.shape[0] != 1: raise ValueError(f"Invalid CWI category: {measure_code}") @@ -477,14 +475,14 @@ class Funding: raise ValueError("existing_li_thickness is required for LI") measure_code = "LI_lessequal100" if existing_li_thickness <= 100 else "LI_greater100" - pps = df[df["Measure_Type"] == measure_code] + pps = filtered_pps_matrix[filtered_pps_matrix["Measure_Type"] == measure_code] if pps.shape[0] != 1: raise ValueError(f"Invalid LI category: {measure_code}") return pps.squeeze()["Cost Savings"] if measure_type == "flat_roof_insulation": - pps = df[df["Measure_Type"] == "FRI"] + pps = filtered_pps_matrix[filtered_pps_matrix["Measure_Type"] == "FRI"] if pps.shape[0] != 1: raise ValueError("Invalid FRI category") return pps.squeeze()["Cost Savings"] @@ -493,36 +491,38 @@ class Funding: # Use the more conservative score (unin is usually lower) # code = "RIRI_res_unin" if not is_roof_insulated else "RIRI_res_in" code = "RIRI_res_unin" - pps = df[df["Measure_Type"] == code] + pps = filtered_pps_matrix[filtered_pps_matrix["Measure_Type"] == code] if pps.shape[0] != 1: raise ValueError(f"Invalid RIRI category: {code}") return pps.squeeze()["Cost Savings"] if measure_type == "suspended_floor_insulation": - pps = df[df["Measure_Type"] == "UFI"] + pps = filtered_pps_matrix[filtered_pps_matrix["Measure_Type"] == "UFI"] if pps.shape[0] != 1: raise ValueError("Invalid UFI category") return pps.squeeze()["Cost Savings"] if measure_type == "solid_floor_insulation": - pps = df[df["Measure_Type"] == "SFI"] + pps = filtered_pps_matrix[filtered_pps_matrix["Measure_Type"] == "SFI"] if pps.shape[0] != 1: raise ValueError("Invalid SFI category") return pps.squeeze()["Cost Savings"] if measure_type == "solar_pv": pre_heating_system = self._map_to_pre_main_heating(mainheating, main_fuel, mainheat_energy_eff) - solar_pps_df = df[ - (df["Measure_Type"] == "Solar_PV") & (df["Pre_Main_Heating_Source"] == pre_heating_system) + solar_pps_df = filtered_pps_matrix[ + (filtered_pps_matrix["Measure_Type"] == "Solar_PV") & + (filtered_pps_matrix["Pre_Main_Heating_Source"] == pre_heating_system) ] return solar_pps_df.squeeze()["Cost Savings"] if measure_type == "air_source_heat_pump": pre_heating_system = self._map_to_pre_main_heating(mainheating, main_fuel, mainheat_energy_eff) - pps = df[ - (df["Pre_Main_Heating_Source"] == pre_heating_system) & - (df["Post_Main_Heating_Source"] == "Air to Water ASHP") & - (df["Measure_Type"] == "B_Upgrade_nopreHCs") # We assume we'll be making a heating system upgrade + pps = filtered_pps_matrix[ + (filtered_pps_matrix["Pre_Main_Heating_Source"] == pre_heating_system) & + (filtered_pps_matrix["Post_Main_Heating_Source"] == "Air to Water ASHP") & + (filtered_pps_matrix["Measure_Type"] == "B_Upgrade_nopreHCs") + # We assume we'll be making a heating system upgrade ] if pps.shape[0] != 1: @@ -763,6 +763,11 @@ class Funding: self.ending_sap_band = self.get_sap_band(ending_sap) self.floor_area_band = self.get_floor_area_band(floor_area) + filtered_pps_matrix = self.partial_project_scores_matrix[ + (self.partial_project_scores_matrix["Total Floor Area Band"] == self.floor_area_band) & + (self.partial_project_scores_matrix["Starting Band"] == self.starting_sap_band) + ].copy() + if self.tenure == "Private": # ECO4 PRS self.eco4_prs_eligibility(starting_sap, ending_sap, measure_types, has_solar, solar_eligible) @@ -774,7 +779,6 @@ class Funding: self.eco4_funding = self.full_project_abs * ( self.private_cavity_abs_rate if is_cavity else self.private_solid_abs_rate) - elif self.tenure == "Social": # ECO4 Social self.eco4_sh_eligibility( @@ -791,6 +795,11 @@ class Funding: # We calculate uplift innovation, where required project_uplifts = [] for i, measure in enumerate(measure_types): + if not innovation_flags[i]: + # Capture 0 innovation uplift for debugging + project_uplifts.append(0) + continue + pps = self.calculate_partial_project_abs( measure_type=measure, mainheating=mainheating, @@ -799,6 +808,7 @@ class Funding: current_wall_uvalue=current_wall_uvalue, is_partial=is_partial, existing_li_thickness=existing_li_thickness, + filtered_pps_matrix=filtered_pps_matrix, ) project_uplifts.append(pps * uplifts[i]) total_uplift = sum(project_uplifts) @@ -817,6 +827,7 @@ class Funding: current_wall_uvalue=current_wall_uvalue, is_partial=is_partial, existing_li_thickness=existing_li_thickness, + filtered_pps_matrix=filtered_pps_matrix ) diff --git a/backend/tests/test_funding.py b/backend/tests/test_funding.py index 7758de5f..860dd120 100644 --- a/backend/tests/test_funding.py +++ b/backend/tests/test_funding.py @@ -994,8 +994,11 @@ def test_uplift( measures = [ {"type": "solar_pv", "is_innovation": True, "uplift": 0.45}, {"type": "internal_wall_insulation", "is_innovation": False, "uplift": 0}, + {"type": "cavity_wall_insulation", "is_innovation": False, "uplift": 0}, + {"type": "external_wall_insulation", "is_innovation": False, "uplift": 0}, {"type": "loft_insulation", "is_innovation": False, "uplift": 0}, {"type": "air_source_heat_pump", "is_innovation": False, "uplift": 0}, + {"type": "double_glazing", "is_innovation": False, "uplift": 0}, {"type": "cavity_wall_insulation", "is_innovation": True, "uplift": 0.25}, {"type": "high_heat_retention_storage_heaters", "is_innovation": False, "uplift": 0}, ] @@ -1030,6 +1033,23 @@ for _, x in tqdm(epc_df.iterrows(), total=len(epc_df)): tenure="Social" ) + self = funding + measures = measures + starting_sap = 33 + ending_sap = 69 + floor_area = 71 + mainheat_description = x["MAINHEAT_DESCRIPTION"] + heating_control_description = x["MAINHEATCONT_DESCRIPTION"] + is_cavity = True + current_wall_uvalue = 2 + is_partial = False + existing_li_thickness = 0 + has_wall_insulation_recommendation = True + has_roof_insulation_recommendation = True + mainheating = h + main_fuel = f + mainheat_energy_eff = mainheat_energy_eff + funding.check_funding( measures=measures, starting_sap=33,