diff --git a/.idea/Model.iml b/.idea/Model.iml index b9459684..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 5914e57c..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 301be1ae..0689d33b 100644 --- a/backend/Funding.py +++ b/backend/Funding.py @@ -7,6 +7,7 @@ from backend.app.plan.schemas import HousingType, WALL_INSULATION_MEASURES, ROOF class EligibilityCaveats(Enum): + EPC_RATING = "epc_rating" # EPC requirements not met TENANT_ON_BENEFITS_OR_LOW_INCOME = "tenant_on_benefits_or_low_income" INNOVATION_REQUIRED = "innovation_required" SOLAR_NEEDS_HEATING = "solar_needs_heating" @@ -26,19 +27,27 @@ class Funding: def __init__( self, tenure: str, # 'Private' or 'Social' - social_cavity_abs_rate: float, - social_solid_abs_rate: float, - private_cavity_abs_rate: float, - private_solid_abs_rate: float, + eco4_social_cavity_abs_rate: float, + eco4_social_solid_abs_rate: float, + eco4_private_cavity_abs_rate: float, + eco4_private_solid_abs_rate: float, + gbis_social_cavity_abs_rate: float, + gbis_social_solid_abs_rate: float, + gbis_private_cavity_abs_rate: float, + gbis_private_solid_abs_rate: float, project_scores_matrix, partial_project_scores_matrix, whlg_eligible_postcodes ): self.tenure = tenure - self.social_cavity_abs_rate = social_cavity_abs_rate - self.social_solid_abs_rate = social_solid_abs_rate - self.private_cavity_abs_rate = private_cavity_abs_rate - self.private_solid_abs_rate = private_solid_abs_rate + self.eco4_social_cavity_abs_rate = eco4_social_cavity_abs_rate + self.eco4_social_solid_abs_rate = eco4_social_solid_abs_rate + self.eco4_private_cavity_abs_rate = eco4_private_cavity_abs_rate + self.eco4_private_solid_abs_rate = eco4_private_solid_abs_rate + self.gbis_social_cavity_abs_rate = gbis_social_cavity_abs_rate + self.gbis_social_solid_abs_rate = gbis_social_solid_abs_rate + self.gbis_private_cavity_abs_rate = gbis_private_cavity_abs_rate + self.gbis_private_solid_abs_rate = gbis_private_solid_abs_rate self.starting_sap_band = None self.ending_sap_band = None @@ -55,6 +64,7 @@ class Funding: # Funding calculation variables self.full_project_abs = None + self.gbis_funding = None self.eco4_funding = None self.eco4_uplift = 0 @@ -141,7 +151,7 @@ class Funding: if not meets_epc or not meets_upgrade_target: self.eco4_eligible = False - self.eco4_eligibility_caveats = [] + self.eco4_eligibility_caveats.append(EligibilityCaveats.EPC_RATING) return if has_solar and not solar_eligible: @@ -587,6 +597,22 @@ class Funding: raise ValueError("something went wrong, more than one pps for ashp") return pps.squeeze()["Cost Savings"] + if measure_type == "high_heat_retention_storage_heater": + pps_data = filtered_pps_matrix[ + filtered_pps_matrix["Post_Main_Heating_Source"] == "High Heat Retention Storage Heaters" + ] + # Not every heating upgrade, that ends at HHRSH, will have a PPS. E.g. a gas boiler to HHRSH upgrade + # doesn't have a PPS + if pre_heating_system in pps_data["Pre_Main_Heating_Source"].values: + pps = pps_data[ + pps_data["Pre_Main_Heating_Source"] == pre_heating_system + ] + if pps.shape[0] != 1: + raise ValueError("something went wrong, more than one pps for HHRSH") + return pps.squeeze()["Cost Savings"] + + return 0 + raise ValueError(f"Invalid measure type for partial project ABS calculation: {measure_type}") # ----------------------- @@ -752,6 +778,40 @@ class Funding: return True + def calc_innovation_uplift( + self, + measure_types, + innovation_flags, + uplifts, + filtered_pps_matrix, + pre_heating_system, + mainheating, + main_fuel, + mainheat_energy_eff, + current_wall_uvalue, + is_partial, + existing_li_thickness, + ): + """Wrapper fundgion to calculate the innovation uplift for a project.""" + project_uplifts = [] + for i, measure in enumerate(measure_types): + if not innovation_flags[i]: + project_uplifts.append(0) + continue + pps = self.calculate_partial_project_abs( + measure_type=measure, + mainheating=mainheating, + main_fuel=main_fuel, + mainheat_energy_eff=mainheat_energy_eff, + current_wall_uvalue=current_wall_uvalue, + is_partial=is_partial, + existing_li_thickness=existing_li_thickness, + filtered_pps_matrix=filtered_pps_matrix, + pre_heating_system=pre_heating_system + ) + project_uplifts.append(pps * uplifts[i]) + return sum(project_uplifts) + def check_funding( self, measures: List[dict], @@ -835,12 +895,43 @@ class Funding: self.gbis_prs_eligibility(starting_sap, council_tax_band or "", measure_types) if self.eco4_eligible: + # Calculate the full project ABS for ECO4 self.full_project_abs = self.calculate_full_project_abs() + + self.eco4_uplift = self.calc_innovation_uplift( + measure_types=measure_types, + innovation_flags=innovation_flags, + uplifts=uplifts, + filtered_pps_matrix=filtered_pps_matrix, + pre_heating_system=pre_heating_system, + mainheating=mainheating, + main_fuel=main_fuel, + mainheat_energy_eff=mainheat_energy_eff, + current_wall_uvalue=current_wall_uvalue, + is_partial=is_partial, + existing_li_thickness=existing_li_thickness, + ) + + self.full_project_abs += self.eco4_uplift self.eco4_funding = self.full_project_abs * ( - self.private_cavity_abs_rate if is_cavity else self.private_solid_abs_rate) + self.eco4_social_cavity_abs_rate if is_cavity else self.eco4_social_solid_abs_rate + ) if self.gbis_eligible: - raise NotImplementedError("FIX ME") + self.partial_project_abs = self.calculate_partial_project_abs( + measure_type=measure_types[0], + mainheating=mainheating, + main_fuel=main_fuel, + mainheat_energy_eff=mainheat_energy_eff, + current_wall_uvalue=current_wall_uvalue, + is_partial=is_partial, + existing_li_thickness=existing_li_thickness, + filtered_pps_matrix=filtered_pps_matrix, + pre_heating_system=pre_heating_system + ) + self.gbis_funding = self.partial_project_abs * ( + self.gbis_private_cavity_abs_rate if is_cavity else self.gbis_private_solid_abs_rate + ) elif self.tenure == "Social": # ECO4 Social @@ -855,30 +946,23 @@ class Funding: # Calculate the full project ABS for ECO4 self.full_project_abs = self.calculate_full_project_abs() - # 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 + self.eco4_uplift = self.calc_innovation_uplift( + measure_types=measure_types, + innovation_flags=innovation_flags, + uplifts=uplifts, + filtered_pps_matrix=filtered_pps_matrix, + pre_heating_system=pre_heating_system, + mainheating=mainheating, + main_fuel=main_fuel, + mainheat_energy_eff=mainheat_energy_eff, + current_wall_uvalue=current_wall_uvalue, + is_partial=is_partial, + existing_li_thickness=existing_li_thickness, + ) - pps = self.calculate_partial_project_abs( - measure_type=measure, - mainheating=mainheating, - main_fuel=main_fuel, - mainheat_energy_eff=mainheat_energy_eff, - current_wall_uvalue=current_wall_uvalue, - is_partial=is_partial, - existing_li_thickness=existing_li_thickness, - filtered_pps_matrix=filtered_pps_matrix, - pre_heating_system=pre_heating_system - ) - project_uplifts.append(pps * uplifts[i]) - self.eco4_uplift = sum(project_uplifts) self.full_project_abs += self.eco4_uplift self.eco4_funding = self.full_project_abs * ( - self.social_cavity_abs_rate if is_cavity else self.social_solid_abs_rate + self.eco4_social_cavity_abs_rate if is_cavity else self.eco4_social_solid_abs_rate ) if self.gbis_eligible: @@ -894,6 +978,9 @@ class Funding: filtered_pps_matrix=filtered_pps_matrix, pre_heating_system=pre_heating_system ) + self.gbis_funding = self.partial_project_abs * ( + self.gbis_social_cavity_abs_rate if is_cavity else self.gbis_social_solid_abs_rate + ) else: diff --git a/backend/tests/test_data/innovation_measure_fixtures.py b/backend/tests/test_data/innovation_measure_fixtures.py index db1ed4ed..886421c4 100644 --- a/backend/tests/test_data/innovation_measure_fixtures.py +++ b/backend/tests/test_data/innovation_measure_fixtures.py @@ -1,10 +1,10 @@ -from backend.Funding import Funding, EligibilityCaveats +from backend.Funding import EligibilityCaveats innovation_scenarios = [ # 1) Innovation PV, non-eligible heating system in place, EPC D - not eligible { "description": "Innovation PV, non-eligible heating system in place, EPC D", - "measures": [{"type": "solar_pv", "is_innovation": True}], + "measures": [{"type": "solar_pv", "is_innovation": True, "uplift": 0.45}], "starting_sap": 60, "mainheat_description": "Electric storage heaters", "heating_control_description": "Manual charge control", @@ -16,7 +16,7 @@ innovation_scenarios = [ # 2) Innovation PV, eligible heating system in place, EPC D - eligible { "description": "Innovation PV, eligible heating system in place, EPC D", - "measures": [{"type": "solar_pv", "is_innovation": True}], + "measures": [{"type": "solar_pv", "is_innovation": True, "uplift": 0.45}], "starting_sap": 60, "mainheat_description": "Air source heat pump, radiators", "heating_control_description": "Programmer, room thermostat and TRVs", @@ -29,8 +29,8 @@ innovation_scenarios = [ { "description": "Innovation PV + HHRSH upgrade, EPC E", "measures": [ - {"type": "solar_pv", "is_innovation": True}, - {"type": "high_heat_retention_storage_heater", "is_innovation": True} + {"type": "solar_pv", "is_innovation": True, "uplift": 0.45}, + {"type": "high_heat_retention_storage_heater", "is_innovation": True, "uplift": 0.1} ], "starting_sap": 50, "mainheat_description": "Electric storage heaters", @@ -44,8 +44,8 @@ innovation_scenarios = [ { "description": "Innovation PV + HHRSH upgrade, EPC E", "measures": [ - {"type": "solar_pv", "is_innovation": True}, - {"type": "high_heat_retention_storage_heater", "is_innovation": True} + {"type": "solar_pv", "is_innovation": True, "uplift": 0.45}, + {"type": "high_heat_retention_storage_heater", "is_innovation": True, "uplift": 0.1} ], "starting_sap": 50, "mainheat_description": "Electric storage heaters", @@ -58,7 +58,7 @@ innovation_scenarios = [ # 5) Innovation PV, needs wall insulation, no wall insulation measure - not eligible { "description": "Innovation PV, wall insulation recommended, but not installed", - "measures": [{"type": "solar_pv", "is_innovation": True}], + "measures": [{"type": "solar_pv", "is_innovation": True, "uplift": 0.45}], "starting_sap": 60, "mainheat_description": "Air source heat pump, radiators", "heating_control_description": "Programmer, room thermostat and TRVs", @@ -71,8 +71,8 @@ innovation_scenarios = [ { "description": "Innovation PV, wall insulation recommended and installed", "measures": [ - {"type": "solar_pv", "is_innovation": True}, - {"type": "internal_wall_insulation", "is_innovation": False} + {"type": "solar_pv", "is_innovation": True, "uplift": 0.45}, + {"type": "internal_wall_insulation", "is_innovation": False, "uplift": 0.25} ], "starting_sap": 60, "mainheat_description": "Air source heat pump, radiators", @@ -85,7 +85,7 @@ innovation_scenarios = [ # 7) Innovation PV, needs roof insulation, no roof insulation measure - not eligible { "description": "Innovation PV, roof insulation recommended, not installed", - "measures": [{"type": "solar_pv", "is_innovation": True}], + "measures": [{"type": "solar_pv", "is_innovation": True, "uplift": 0.45}], "starting_sap": 60, "mainheat_description": "Air source heat pump, radiators", "heating_control_description": "Programmer, room thermostat and TRVs", @@ -98,8 +98,8 @@ innovation_scenarios = [ { "description": "Innovation PV, roof insulation recommended and installed", "measures": [ - {"type": "solar_pv", "is_innovation": True}, - {"type": "loft_insulation", "is_innovation": False} + {"type": "solar_pv", "is_innovation": True, "uplift": 0.45}, + {"type": "loft_insulation", "is_innovation": False, "uplift": 0} ], "starting_sap": 60, "mainheat_description": "Air source heat pump, radiators", @@ -112,7 +112,7 @@ innovation_scenarios = [ # 9) Innovation PV, needs both roof + wall insulation, no insulation - not eligible { "description": "Innovation PV, both insulations recommended, none installed", - "measures": [{"type": "solar_pv", "is_innovation": True}], + "measures": [{"type": "solar_pv", "is_innovation": True, "uplift": 0.45}], "starting_sap": 60, "mainheat_description": "Air source heat pump, radiators", "heating_control_description": "Programmer, room thermostat and TRVs", @@ -125,8 +125,8 @@ innovation_scenarios = [ { "description": "Innovation PV, both insulations recommended, only wall done", "measures": [ - {"type": "solar_pv", "is_innovation": True}, - {"type": "internal_wall_insulation", "is_innovation": False} + {"type": "solar_pv", "is_innovation": True, "uplift": 0.45}, + {"type": "internal_wall_insulation", "is_innovation": False, "uplift": 0.25} ], "starting_sap": 60, "mainheat_description": "Air source heat pump, radiators", @@ -140,8 +140,8 @@ innovation_scenarios = [ { "description": "Innovation PV, both insulations recommended, only roof done", "measures": [ - {"type": "solar_pv", "is_innovation": True}, - {"type": "loft_insulation", "is_innovation": False} + {"type": "solar_pv", "is_innovation": True, "uplift": 0.45}, + {"type": "loft_insulation", "is_innovation": False, "uplift": 0} ], "starting_sap": 60, "mainheat_description": "Air source heat pump, radiators", @@ -155,9 +155,9 @@ innovation_scenarios = [ { "description": "Innovation PV, both insulations recommended and installed", "measures": [ - {"type": "solar_pv", "is_innovation": True}, - {"type": "internal_wall_insulation", "is_innovation": False}, - {"type": "loft_insulation", "is_innovation": False} + {"type": "solar_pv", "is_innovation": True, "uplift": 0.45}, + {"type": "internal_wall_insulation", "is_innovation": False, "uplift": 0.25}, + {"type": "loft_insulation", "is_innovation": False, "uplift": 0} ], "starting_sap": 60, "mainheat_description": "Air source heat pump, radiators", diff --git a/backend/tests/test_funding.py b/backend/tests/test_funding.py index 01da1e6c..59d65a28 100644 --- a/backend/tests/test_funding.py +++ b/backend/tests/test_funding.py @@ -69,7 +69,9 @@ def mock_mainheating(): 'has_electricaire': False, 'has_assumed_for_most_rooms': False, 'has_underfloor_heating': False, "has_electric_heat_pumps": False, - "has_micro-cogeneration": False + "has_micro-cogeneration": False, + 'has_mineral_and_wood': False, + "has_dual_fuel_appliance": False } @@ -80,7 +82,7 @@ def mock_main_fuel(): 'electricity', 'tariff_type': 'unspecified tariff', 'is_community': False, 'no_individual_heating_or_community_network': False, - 'complex_fuel_type': None + 'complex_fuel_type': None, } @@ -93,15 +95,22 @@ def mock_mainheat_energy_eff(): ### PRIVATE RENTED SECTOR (PRS) ### ------------------------- -def test_eco4_prs_eligible_with_swi(mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes): +def test_eco4_prs_eligible_with_swi( + mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel, + mock_mainheat_energy_eff +): funding = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, - social_cavity_abs_rate=13.5, - social_solid_abs_rate=17, - private_cavity_abs_rate=13.5, - private_solid_abs_rate=17, + eco4_social_cavity_abs_rate=13.5, + eco4_social_solid_abs_rate=17, + eco4_private_cavity_abs_rate=13.5, + eco4_private_solid_abs_rate=17, + gbis_social_cavity_abs_rate=21, + gbis_social_solid_abs_rate=25, + gbis_private_cavity_abs_rate=22, + gbis_private_solid_abs_rate=28, tenure="Private", ) @@ -111,7 +120,7 @@ def test_eco4_prs_eligible_with_swi(mock_project_scores_matrix, mock_partial_sco # 3) is getting a solid was measure # so it's eligible for ECO4 - measures = [{"type": "internal_wall_insulation", "is_innovation": False}] + measures = [{"type": "internal_wall_insulation", "is_innovation": False, "uplift": 0}] funding.check_funding( measures=measures, starting_sap=50, # EPC E @@ -123,27 +132,37 @@ def test_eco4_prs_eligible_with_swi(mock_project_scores_matrix, mock_partial_sco council_tax_band="B", is_partial=False, existing_li_thickness=0, - current_wall_uvalue=2 + current_wall_uvalue=2, + mainheating=mock_mainheating, + main_fuel=mock_main_fuel, + mainheat_energy_eff=mock_mainheat_energy_eff ) assert funding.eco4_eligible assert EligibilityCaveats.TENANT_ON_BENEFITS_OR_LOW_INCOME in funding.eco4_eligibility_caveats -def test_eco4_prs_not_eligible_high_epc(mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes): +def test_eco4_prs_not_eligible_high_epc( + mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel, + mock_mainheat_energy_eff +): """Should NOT be eligible if EPC is too high (C or above).""" funding = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, - social_cavity_abs_rate=13.5, - social_solid_abs_rate=17, - private_cavity_abs_rate=13.5, - private_solid_abs_rate=17, + eco4_social_cavity_abs_rate=13.5, + eco4_social_solid_abs_rate=17, + eco4_private_cavity_abs_rate=13.5, + eco4_private_solid_abs_rate=17, + gbis_social_cavity_abs_rate=21, + gbis_social_solid_abs_rate=25, + gbis_private_cavity_abs_rate=22, + gbis_private_solid_abs_rate=28, tenure="Private", ) - measures = [{"type": "internal_wall_insulation", "is_innovation": False}] + measures = [{"type": "internal_wall_insulation", "is_innovation": False, "uplift": 0}] funding.check_funding( measures=measures, starting_sap=72, # EPC C (too high) @@ -156,25 +175,35 @@ def test_eco4_prs_not_eligible_high_epc(mock_project_scores_matrix, mock_partial is_partial=False, existing_li_thickness=0, current_wall_uvalue=2, + mainheating=mock_mainheating, + main_fuel=mock_main_fuel, + mainheat_energy_eff=mock_mainheat_energy_eff ) assert not funding.eco4_eligible -def test_gbis_prs_general_eligibility(mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes): +def test_gbis_prs_general_eligibility( + mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel, + mock_mainheat_energy_eff +): """PRS EPC D–G & council tax band A–D should trigger GBIS general route.""" funding = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, - social_cavity_abs_rate=13.5, - social_solid_abs_rate=17, - private_cavity_abs_rate=13.5, - private_solid_abs_rate=17, + eco4_social_cavity_abs_rate=13.5, + eco4_social_solid_abs_rate=17, + eco4_private_cavity_abs_rate=13.5, + eco4_private_solid_abs_rate=17, + gbis_social_cavity_abs_rate=21, + gbis_social_solid_abs_rate=25, + gbis_private_cavity_abs_rate=22, + gbis_private_solid_abs_rate=28, tenure="Private", ) - measures = [{"type": "internal_wall_insulation", "is_innovation": False}] + measures = [{"type": "internal_wall_insulation", "is_innovation": False, "uplift": 0}] funding.check_funding( measures=measures, starting_sap=65, # EPC D @@ -187,25 +216,35 @@ def test_gbis_prs_general_eligibility(mock_project_scores_matrix, mock_partial_s is_partial=False, existing_li_thickness=0, current_wall_uvalue=2, + mainheating=mock_mainheating, + main_fuel=mock_main_fuel, + mainheat_energy_eff=mock_mainheat_energy_eff ) assert funding.gbis_eligible -def test_gbis_prs_low_income_caveat(mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes): +def test_gbis_prs_low_income_caveat( + mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel, + mock_mainheat_energy_eff +): """PRS EPC D–G should flag low-income caveat when low-income route is used.""" funding = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, - social_cavity_abs_rate=13.5, - social_solid_abs_rate=17, - private_cavity_abs_rate=13.5, - private_solid_abs_rate=17, + eco4_social_cavity_abs_rate=13.5, + eco4_social_solid_abs_rate=17, + eco4_private_cavity_abs_rate=13.5, + eco4_private_solid_abs_rate=17, + gbis_social_cavity_abs_rate=21, + gbis_social_solid_abs_rate=25, + gbis_private_cavity_abs_rate=22, + gbis_private_solid_abs_rate=28, tenure="Private", ) - measures = [{"type": "cavity_wall_insulation", "is_innovation": False}] + measures = [{"type": "cavity_wall_insulation", "is_innovation": False, "uplift": 0}] funding.check_funding( measures=measures, starting_sap=60, # EPC D @@ -218,6 +257,9 @@ def test_gbis_prs_low_income_caveat(mock_project_scores_matrix, mock_partial_sco is_partial=False, existing_li_thickness=0, current_wall_uvalue=2, + mainheating=mock_mainheating, + main_fuel=mock_main_fuel, + mainheat_energy_eff=mock_mainheat_energy_eff ) assert funding.gbis_eligible @@ -228,20 +270,27 @@ def test_gbis_prs_low_income_caveat(mock_project_scores_matrix, mock_partial_sco ### SOCIAL HOUSING ### ------------------------- -def test_eco4_sh_epc_e_eligible(mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes): +def test_eco4_sh_epc_e_eligible( + mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel, + mock_mainheat_energy_eff +): """EPC E social housing should be ECO4 eligible without innovation.""" funding = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, - social_cavity_abs_rate=13.5, - social_solid_abs_rate=17, - private_cavity_abs_rate=13.5, - private_solid_abs_rate=17, + eco4_social_cavity_abs_rate=13.5, + eco4_social_solid_abs_rate=17, + eco4_private_cavity_abs_rate=13.5, + eco4_private_solid_abs_rate=17, + gbis_social_cavity_abs_rate=21, + gbis_social_solid_abs_rate=25, + gbis_private_cavity_abs_rate=22, + gbis_private_solid_abs_rate=28, tenure="Social", ) - measures = [{"type": "internal_wall_insulation", "is_innovation": False}] + measures = [{"type": "internal_wall_insulation", "is_innovation": False, "uplift": 0}] funding.check_funding( measures=measures, starting_sap=50, # EPC E @@ -253,25 +302,35 @@ def test_eco4_sh_epc_e_eligible(mock_project_scores_matrix, mock_partial_scores_ current_wall_uvalue=2, is_partial=False, existing_li_thickness=0, + mainheating=mock_mainheating, + main_fuel=mock_main_fuel, + mainheat_energy_eff=mock_mainheat_energy_eff ) assert funding.eco4_eligible -def test_eco4_sh_epc_d_requires_innovation(mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes): +def test_eco4_sh_epc_d_requires_innovation( + mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel, + mock_mainheat_energy_eff +): """EPC D social housing should require an innovation measure.""" funding = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, - social_cavity_abs_rate=13.5, - social_solid_abs_rate=17, - private_cavity_abs_rate=13.5, - private_solid_abs_rate=17, + eco4_social_cavity_abs_rate=13.5, + eco4_social_solid_abs_rate=17, + eco4_private_cavity_abs_rate=13.5, + eco4_private_solid_abs_rate=17, + gbis_social_cavity_abs_rate=21, + gbis_social_solid_abs_rate=25, + gbis_private_cavity_abs_rate=22, + gbis_private_solid_abs_rate=28, tenure="Social", ) - measures = [{"type": "internal_wall_insulation", "is_innovation": False}] + measures = [{"type": "internal_wall_insulation", "is_innovation": False, "uplift": 0}] funding.check_funding( measures=measures, starting_sap=60, # EPC D @@ -283,6 +342,9 @@ def test_eco4_sh_epc_d_requires_innovation(mock_project_scores_matrix, mock_part current_wall_uvalue=2, is_partial=False, existing_li_thickness=0, + mainheating=mock_mainheating, + main_fuel=mock_main_fuel, + mainheat_energy_eff=mock_mainheat_energy_eff ) assert not funding.eco4_eligible @@ -293,13 +355,17 @@ def test_eco4_sh_epc_d_requires_innovation(mock_project_scores_matrix, mock_part project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, - social_cavity_abs_rate=13.5, - social_solid_abs_rate=17, - private_cavity_abs_rate=13.5, - private_solid_abs_rate=17, + eco4_social_cavity_abs_rate=13.5, + eco4_social_solid_abs_rate=17, + eco4_private_cavity_abs_rate=13.5, + eco4_private_solid_abs_rate=17, + gbis_social_cavity_abs_rate=21, + gbis_social_solid_abs_rate=25, + gbis_private_cavity_abs_rate=22, + gbis_private_solid_abs_rate=28, tenure="Social", ) - measures2 = [{"type": "internal_wall_insulation", "is_innovation": True}] + measures2 = [{"type": "internal_wall_insulation", "is_innovation": True, "uplift": 0.25}] funding2.check_funding( measures=measures2, starting_sap=60, # EPC D @@ -311,6 +377,9 @@ def test_eco4_sh_epc_d_requires_innovation(mock_project_scores_matrix, mock_part current_wall_uvalue=2, is_partial=False, existing_li_thickness=0, + mainheating=mock_mainheating, + main_fuel=mock_main_fuel, + mainheat_energy_eff=mock_mainheat_energy_eff ) assert funding2.eco4_eligible @@ -324,13 +393,17 @@ def test_eco4_sh_epc_d_requires_innovation(mock_project_scores_matrix, mock_part project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, - social_cavity_abs_rate=13.5, - social_solid_abs_rate=17, - private_cavity_abs_rate=13.5, - private_solid_abs_rate=17, + eco4_social_cavity_abs_rate=13.5, + eco4_social_solid_abs_rate=17, + eco4_private_cavity_abs_rate=13.5, + eco4_private_solid_abs_rate=17, + gbis_social_cavity_abs_rate=21, + gbis_social_solid_abs_rate=25, + gbis_private_cavity_abs_rate=22, + gbis_private_solid_abs_rate=28, tenure="Social", ) - measures3 = [{"type": "solar_pv", "is_innovation": True}] + measures3 = [{"type": "solar_pv", "is_innovation": True, "uplift": 0.45}] funding3.check_funding( measures=measures3, starting_sap=60, # EPC D @@ -342,6 +415,9 @@ def test_eco4_sh_epc_d_requires_innovation(mock_project_scores_matrix, mock_part current_wall_uvalue=2, is_partial=False, existing_li_thickness=0, + mainheating=mock_mainheating, + main_fuel=mock_main_fuel, + mainheat_energy_eff=mock_mainheat_energy_eff ) assert not funding3.eco4_eligible @@ -352,14 +428,18 @@ def test_eco4_sh_epc_d_requires_innovation(mock_project_scores_matrix, mock_part project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, - social_cavity_abs_rate=13.5, - social_solid_abs_rate=17, - private_cavity_abs_rate=13.5, - private_solid_abs_rate=17, + eco4_social_cavity_abs_rate=13.5, + eco4_social_solid_abs_rate=17, + eco4_private_cavity_abs_rate=13.5, + eco4_private_solid_abs_rate=17, + gbis_social_cavity_abs_rate=21, + gbis_social_solid_abs_rate=25, + gbis_private_cavity_abs_rate=22, + gbis_private_solid_abs_rate=28, tenure="Social", ) - measures4 = [{"type": "solar_pv", "is_innovation": True}] + measures4 = [{"type": "solar_pv", "is_innovation": True, "uplift": 0.45}, ] funding4.check_funding( measures=measures4, starting_sap=60, # EPC D @@ -371,6 +451,9 @@ def test_eco4_sh_epc_d_requires_innovation(mock_project_scores_matrix, mock_part current_wall_uvalue=2, is_partial=False, existing_li_thickness=0, + mainheating=mock_mainheating, + main_fuel=mock_main_fuel, + mainheat_energy_eff=mock_mainheat_energy_eff ) assert funding4.eco4_eligible @@ -381,16 +464,20 @@ def test_eco4_sh_epc_d_requires_innovation(mock_project_scores_matrix, mock_part project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, - social_cavity_abs_rate=13.5, - social_solid_abs_rate=17, - private_cavity_abs_rate=13.5, - private_solid_abs_rate=17, + eco4_social_cavity_abs_rate=13.5, + eco4_social_solid_abs_rate=17, + eco4_private_cavity_abs_rate=13.5, + eco4_private_solid_abs_rate=17, + gbis_social_cavity_abs_rate=21, + gbis_social_solid_abs_rate=25, + gbis_private_cavity_abs_rate=22, + gbis_private_solid_abs_rate=28, tenure="Social", ) measures5 = [ - {"type": "solar_pv", "is_innovation": True}, - {"type": "high_heat_retention_storage_heater", "is_innovation": False} + {"type": "solar_pv", "is_innovation": True, "uplift": 0.45}, + {"type": "high_heat_retention_storage_heater", "is_innovation": False, "uplift": 0} ] funding5.check_funding( measures=measures5, @@ -403,6 +490,9 @@ def test_eco4_sh_epc_d_requires_innovation(mock_project_scores_matrix, mock_part current_wall_uvalue=2, is_partial=False, existing_li_thickness=0, + mainheating=mock_mainheating, + main_fuel=mock_main_fuel, + mainheat_energy_eff=mock_mainheat_energy_eff ) assert not funding5.eco4_eligible @@ -414,15 +504,19 @@ def test_eco4_sh_epc_d_requires_innovation(mock_project_scores_matrix, mock_part project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, - social_cavity_abs_rate=13.5, - social_solid_abs_rate=17, - private_cavity_abs_rate=13.5, - private_solid_abs_rate=17, + eco4_social_cavity_abs_rate=13.5, + eco4_social_solid_abs_rate=17, + eco4_private_cavity_abs_rate=13.5, + eco4_private_solid_abs_rate=17, + gbis_social_cavity_abs_rate=21, + gbis_social_solid_abs_rate=25, + gbis_private_cavity_abs_rate=22, + gbis_private_solid_abs_rate=28, tenure="Social", ) measures6 = [ - {"type": "solar_pv", "is_innovation": True}, + {"type": "solar_pv", "is_innovation": True, "uplift": 0.45}, ] funding6.check_funding( measures=measures6, @@ -437,6 +531,9 @@ def test_eco4_sh_epc_d_requires_innovation(mock_project_scores_matrix, mock_part current_wall_uvalue=2, is_partial=False, existing_li_thickness=0, + mainheating=mock_mainheating, + main_fuel=mock_main_fuel, + mainheat_energy_eff=mock_mainheat_energy_eff ) assert not funding6.eco4_eligible @@ -448,16 +545,20 @@ def test_eco4_sh_epc_d_requires_innovation(mock_project_scores_matrix, mock_part project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, - social_cavity_abs_rate=13.5, - social_solid_abs_rate=17, - private_cavity_abs_rate=13.5, - private_solid_abs_rate=17, + eco4_social_cavity_abs_rate=13.5, + eco4_social_solid_abs_rate=17, + eco4_private_cavity_abs_rate=13.5, + eco4_private_solid_abs_rate=17, + gbis_social_cavity_abs_rate=21, + gbis_social_solid_abs_rate=25, + gbis_private_cavity_abs_rate=22, + gbis_private_solid_abs_rate=28, tenure="Social", ) measures7 = [ - {"type": "solar_pv", "is_innovation": True}, - {"type": "cavity_wall_insulation", "is_innovation": False}, - {"type": "loft_insulation", "is_innovation": False} + {"type": "solar_pv", "is_innovation": True, "uplift": 0.45}, + {"type": "cavity_wall_insulation", "is_innovation": False, "uplift": 0.25}, + {"type": "loft_insulation", "is_innovation": False, "uplift": 0} ] funding7.check_funding( measures=measures7, @@ -470,25 +571,35 @@ def test_eco4_sh_epc_d_requires_innovation(mock_project_scores_matrix, mock_part current_wall_uvalue=2, is_partial=False, existing_li_thickness=0, + mainheating=mock_mainheating, + main_fuel=mock_main_fuel, + mainheat_energy_eff=mock_mainheat_energy_eff ) assert funding7.eco4_eligible assert not funding7.eco4_eligibility_caveats -def test_eco4_sh_solar_pv_requires_heating(mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes): +def test_eco4_sh_solar_pv_requires_heating( + mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel, + mock_mainheat_energy_eff +): """Solar PV as innovation measure requires ASHP or HHRSH.""" funding = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, - social_cavity_abs_rate=13.5, - social_solid_abs_rate=17, - private_cavity_abs_rate=13.5, - private_solid_abs_rate=17, + eco4_social_cavity_abs_rate=13.5, + eco4_social_solid_abs_rate=17, + eco4_private_cavity_abs_rate=13.5, + eco4_private_solid_abs_rate=17, + gbis_social_cavity_abs_rate=21, + gbis_social_solid_abs_rate=25, + gbis_private_cavity_abs_rate=22, + gbis_private_solid_abs_rate=28, tenure="Social", ) - measures = [{"type": "solar_pv", "is_innovation": True}] + measures = [{"type": "solar_pv", "is_innovation": True, "uplift": 0.45}] funding.check_funding( measures=measures, starting_sap=60, # EPC D @@ -500,29 +611,38 @@ def test_eco4_sh_solar_pv_requires_heating(mock_project_scores_matrix, mock_part current_wall_uvalue=2, is_partial=False, existing_li_thickness=0, + mainheating=mock_mainheating, + main_fuel=mock_main_fuel, + mainheat_energy_eff=mock_mainheat_energy_eff ) assert not funding.eco4_eligible assert EligibilityCaveats.SOLAR_NEEDS_HEATING in funding.eco4_eligibility_caveats -def test_eco4_sh_solar_pv_with_heating_is_ok(mock_project_scores_matrix, mock_partial_scores_matrix, - mock_whlg_postcodes): +def test_eco4_sh_solar_pv_with_heating_is_ok( + mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel, + mock_mainheat_energy_eff +): """Solar PV innovation with ASHP should pass EPC D innovation rule.""" funding = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, - social_cavity_abs_rate=13.5, - social_solid_abs_rate=17, - private_cavity_abs_rate=13.5, - private_solid_abs_rate=17, + eco4_social_cavity_abs_rate=13.5, + eco4_social_solid_abs_rate=17, + eco4_private_cavity_abs_rate=13.5, + eco4_private_solid_abs_rate=17, + gbis_social_cavity_abs_rate=21, + gbis_social_solid_abs_rate=25, + gbis_private_cavity_abs_rate=22, + gbis_private_solid_abs_rate=28, tenure="Social", ) measures = [ - {"type": "solar_pv", "is_innovation": True}, - {"type": "air_source_heat_pump", "is_innovation": False} + {"type": "solar_pv", "is_innovation": True, "uplift": 0.45}, + {"type": "air_source_heat_pump", "is_innovation": False, "uplift": 0} ] funding.check_funding( measures=measures, @@ -535,27 +655,36 @@ def test_eco4_sh_solar_pv_with_heating_is_ok(mock_project_scores_matrix, mock_pa current_wall_uvalue=2, is_partial=False, existing_li_thickness=0, + mainheating=mock_mainheating, + main_fuel=mock_main_fuel, + mainheat_energy_eff=mock_mainheat_energy_eff ) assert not funding.eco4_eligible assert EligibilityCaveats.INNOVATION_REQUIRED in funding.eco4_eligibility_caveats -def test_eco4_upgrade_requirement_e_to_c_pass(mock_project_scores_matrix, mock_partial_scores_matrix, - mock_whlg_postcodes): +def test_eco4_upgrade_requirement_e_to_c_pass( + mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel, + mock_mainheat_energy_eff +): """EPC E upgraded to C should pass ECO4 upgrade rule.""" funding = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, - social_cavity_abs_rate=13.5, - social_solid_abs_rate=17, - private_cavity_abs_rate=13.5, - private_solid_abs_rate=17, + eco4_social_cavity_abs_rate=13.5, + eco4_social_solid_abs_rate=17, + eco4_private_cavity_abs_rate=13.5, + eco4_private_solid_abs_rate=17, + gbis_social_cavity_abs_rate=21, + gbis_social_solid_abs_rate=25, + gbis_private_cavity_abs_rate=22, + gbis_private_solid_abs_rate=28, tenure="Private", ) - measures = [{"type": "internal_wall_insulation", "is_innovation": False}] + measures = [{"type": "internal_wall_insulation", "is_innovation": False, "uplift": 0}] # E (SAP 50) → C (SAP 70) meets upgrade rule funding.check_funding( @@ -570,26 +699,35 @@ def test_eco4_upgrade_requirement_e_to_c_pass(mock_project_scores_matrix, mock_p current_wall_uvalue=2, is_partial=False, existing_li_thickness=0, + mainheating=mock_mainheating, + main_fuel=mock_main_fuel, + mainheat_energy_eff=mock_mainheat_energy_eff ) assert funding.eco4_eligible -def test_eco4_upgrade_requirement_e_to_d_fail(mock_project_scores_matrix, mock_partial_scores_matrix, - mock_whlg_postcodes): +def test_eco4_upgrade_requirement_e_to_d_fail( + mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel, + mock_mainheat_energy_eff +): """EPC E upgraded to D should FAIL ECO4 upgrade rule (needs to hit C).""" funding = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, - social_cavity_abs_rate=13.5, - social_solid_abs_rate=17, - private_cavity_abs_rate=13.5, - private_solid_abs_rate=17, + eco4_social_cavity_abs_rate=13.5, + eco4_social_solid_abs_rate=17, + eco4_private_cavity_abs_rate=13.5, + eco4_private_solid_abs_rate=17, + gbis_social_cavity_abs_rate=21, + gbis_social_solid_abs_rate=25, + gbis_private_cavity_abs_rate=22, + gbis_private_solid_abs_rate=28, tenure="Private", ) - measures = [{"type": "internal_wall_insulation", "is_innovation": False}] + measures = [{"type": "internal_wall_insulation", "is_innovation": False, "uplift": 0}] # E (SAP 50) → D (SAP 65) does NOT meet ECO4 upgrade rule funding.check_funding( @@ -604,26 +742,35 @@ def test_eco4_upgrade_requirement_e_to_d_fail(mock_project_scores_matrix, mock_p current_wall_uvalue=2, is_partial=False, existing_li_thickness=0, + mainheating=mock_mainheating, + main_fuel=mock_main_fuel, + mainheat_energy_eff=mock_mainheat_energy_eff ) assert not funding.eco4_eligible -def test_eco4_upgrade_requirement_f_to_d_pass(mock_project_scores_matrix, mock_partial_scores_matrix, - mock_whlg_postcodes): +def test_eco4_upgrade_requirement_f_to_d_pass( + mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel, + mock_mainheat_energy_eff +): """EPC F upgraded to D should pass ECO4 upgrade rule.""" funding = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, - social_cavity_abs_rate=13.5, - social_solid_abs_rate=17, - private_cavity_abs_rate=13.5, - private_solid_abs_rate=17, + eco4_social_cavity_abs_rate=13.5, + eco4_social_solid_abs_rate=17, + eco4_private_cavity_abs_rate=13.5, + eco4_private_solid_abs_rate=17, + gbis_social_cavity_abs_rate=21, + gbis_social_solid_abs_rate=25, + gbis_private_cavity_abs_rate=22, + gbis_private_solid_abs_rate=28, tenure="Private", ) - measures = [{"type": "internal_wall_insulation", "is_innovation": False}] + measures = [{"type": "internal_wall_insulation", "is_innovation": False, "uplift": 0}] # F (SAP 35) → D (SAP 60) is OK for ECO4 funding.check_funding( @@ -638,26 +785,35 @@ def test_eco4_upgrade_requirement_f_to_d_pass(mock_project_scores_matrix, mock_p current_wall_uvalue=2, is_partial=False, existing_li_thickness=0, + mainheating=mock_mainheating, + main_fuel=mock_main_fuel, + mainheat_energy_eff=mock_mainheat_energy_eff ) assert funding.eco4_eligible -def test_eco4_upgrade_requirement_f_to_e_fail(mock_project_scores_matrix, mock_partial_scores_matrix, - mock_whlg_postcodes): +def test_eco4_upgrade_requirement_f_to_e_fail( + mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel, + mock_mainheat_energy_eff +): """EPC F upgraded only to E should FAIL ECO4 upgrade rule (needs to hit at least D).""" funding = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, - social_cavity_abs_rate=13.5, - social_solid_abs_rate=17, - private_cavity_abs_rate=13.5, - private_solid_abs_rate=17, + eco4_social_cavity_abs_rate=13.5, + eco4_social_solid_abs_rate=17, + eco4_private_cavity_abs_rate=13.5, + eco4_private_solid_abs_rate=17, + gbis_social_cavity_abs_rate=21, + gbis_social_solid_abs_rate=25, + gbis_private_cavity_abs_rate=22, + gbis_private_solid_abs_rate=28, tenure="Private", ) - measures = [{"type": "internal_wall_insulation", "is_innovation": False}] + measures = [{"type": "internal_wall_insulation", "is_innovation": False, "uplift": 0}] # F (SAP 35) → E (SAP 50) does NOT meet ECO4 rule funding.check_funding( @@ -672,6 +828,9 @@ def test_eco4_upgrade_requirement_f_to_e_fail(mock_project_scores_matrix, mock_p current_wall_uvalue=2, is_partial=False, existing_li_thickness=0, + mainheating=mock_mainheating, + main_fuel=mock_main_fuel, + mainheat_energy_eff=mock_mainheat_energy_eff ) assert not funding.eco4_eligible @@ -680,21 +839,27 @@ def test_eco4_upgrade_requirement_f_to_e_fail(mock_project_scores_matrix, mock_p ### ------------------------- ### INNOVATION PRODUCTS ### ------------------------- -def test_epc_d_social_no_innovation_no_heating(mock_project_scores_matrix, mock_partial_scores_matrix, - mock_whlg_postcodes): +def test_epc_d_social_no_innovation_no_heating( + mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel, + mock_mainheat_energy_eff +): funding = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, - social_cavity_abs_rate=13.5, - social_solid_abs_rate=17, - private_cavity_abs_rate=13.5, - private_solid_abs_rate=17, + eco4_social_cavity_abs_rate=13.5, + eco4_social_solid_abs_rate=17, + eco4_private_cavity_abs_rate=13.5, + eco4_private_solid_abs_rate=17, + gbis_social_cavity_abs_rate=21, + gbis_social_solid_abs_rate=25, + gbis_private_cavity_abs_rate=22, + gbis_private_solid_abs_rate=28, tenure="Social" ) measures = [ - {"type": "solar_pv", "is_innovation": True} + {"type": "solar_pv", "is_innovation": True, "uplift": 0.45} ] funding.check_funding( @@ -709,32 +874,41 @@ def test_epc_d_social_no_innovation_no_heating(mock_project_scores_matrix, mock_ has_roof_insulation_recommendation=False, current_wall_uvalue=2, is_partial=False, - existing_li_thickness=0 + existing_li_thickness=0, + mainheating=mock_mainheating, + main_fuel=mock_main_fuel, + mainheat_energy_eff=mock_mainheat_energy_eff ) assert not funding.eco4_eligible assert EligibilityCaveats.SOLAR_NEEDS_HEATING in funding.eco4_eligibility_caveats -def test_epc_d_social_with_heating_and_insulation(mock_project_scores_matrix, mock_partial_scores_matrix, - mock_whlg_postcodes): +def test_epc_d_social_with_heating_and_insulation( + mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel, + mock_mainheat_energy_eff +): funding = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, - social_cavity_abs_rate=13.5, - social_solid_abs_rate=17, - private_cavity_abs_rate=13.5, - private_solid_abs_rate=17, + eco4_social_cavity_abs_rate=13.5, + eco4_social_solid_abs_rate=17, + eco4_private_cavity_abs_rate=13.5, + eco4_private_solid_abs_rate=17, + gbis_social_cavity_abs_rate=21, + gbis_social_solid_abs_rate=25, + gbis_private_cavity_abs_rate=22, + gbis_private_solid_abs_rate=28, tenure="Social" ) # Should NOT be eligible as the ASHP is not an innovation measure measures = [ - {"type": "solar_pv", "is_innovation": True}, - {"type": "internal_wall_insulation", "is_innovation": False}, - {"type": "loft_insulation", "is_innovation": False}, - {"type": "air_source_heat_pump", "is_innovation": False} + {"type": "solar_pv", "is_innovation": True, "uplift": 0.45}, + {"type": "internal_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} ] funding.check_funding( @@ -749,7 +923,10 @@ def test_epc_d_social_with_heating_and_insulation(mock_project_scores_matrix, mo has_roof_insulation_recommendation=True, current_wall_uvalue=2, is_partial=False, - existing_li_thickness=0 + existing_li_thickness=0, + mainheating=mock_mainheating, + main_fuel=mock_main_fuel, + mainheat_energy_eff=mock_mainheat_energy_eff ) assert not funding.eco4_eligible @@ -757,24 +934,29 @@ def test_epc_d_social_with_heating_and_insulation(mock_project_scores_matrix, mo def test_epc_d_social_solar_with_only_minimum_insulation_should_fail( - mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes + mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel, + mock_mainheat_energy_eff ): funding = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, - social_cavity_abs_rate=13.5, - social_solid_abs_rate=17, - private_cavity_abs_rate=13.5, - private_solid_abs_rate=17, + eco4_social_cavity_abs_rate=13.5, + eco4_social_solid_abs_rate=17, + eco4_private_cavity_abs_rate=13.5, + eco4_private_solid_abs_rate=17, + gbis_social_cavity_abs_rate=21, + gbis_social_solid_abs_rate=25, + gbis_private_cavity_abs_rate=22, + gbis_private_solid_abs_rate=28, tenure="Social" ) # Solar PV innovation with insulation, but no heating system upgrade => not eligible measures = [ - {"type": "solar_pv", "is_innovation": True}, - {"type": "internal_wall_insulation", "is_innovation": False}, - {"type": "loft_insulation", "is_innovation": False} + {"type": "solar_pv", "is_innovation": True, "uplift": 0.45}, + {"type": "internal_wall_insulation", "is_innovation": False, "uplift": 0}, + {"type": "loft_insulation", "is_innovation": False, "uplift": 0} ] funding.check_funding( @@ -789,7 +971,10 @@ def test_epc_d_social_solar_with_only_minimum_insulation_should_fail( has_roof_insulation_recommendation=True, current_wall_uvalue=2, is_partial=False, - existing_li_thickness=0 + existing_li_thickness=0, + mainheating=mock_mainheating, + main_fuel=mock_main_fuel, + mainheat_energy_eff=mock_mainheat_energy_eff ) assert not funding.eco4_eligible @@ -797,23 +982,28 @@ def test_epc_d_social_solar_with_only_minimum_insulation_should_fail( def test_epc_d_social_solar_with_ashp_and_no_insulation_should_fail( - mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes + mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel, + mock_mainheat_energy_eff ): funding = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, - social_cavity_abs_rate=13.5, - social_solid_abs_rate=17, - private_cavity_abs_rate=13.5, - private_solid_abs_rate=17, + eco4_social_cavity_abs_rate=13.5, + eco4_social_solid_abs_rate=17, + eco4_private_cavity_abs_rate=13.5, + eco4_private_solid_abs_rate=17, + gbis_social_cavity_abs_rate=21, + gbis_social_solid_abs_rate=25, + gbis_private_cavity_abs_rate=22, + gbis_private_solid_abs_rate=28, tenure="Social" ) # Solar PV innovation with heating, but no insulation when insulation is recommended => not eligible measures = [ - {"type": "solar_pv", "is_innovation": True}, - {"type": "air_source_heat_pump", "is_innovation": False} + {"type": "solar_pv", "is_innovation": True, "uplift": 0.45}, + {"type": "air_source_heat_pump", "is_innovation": False, "uplift": 0} ] funding.check_funding( @@ -828,7 +1018,10 @@ def test_epc_d_social_solar_with_ashp_and_no_insulation_should_fail( has_roof_insulation_recommendation=True, current_wall_uvalue=2, is_partial=False, - existing_li_thickness=0 + existing_li_thickness=0, + mainheating=mock_mainheating, + main_fuel=mock_main_fuel, + mainheat_energy_eff=mock_mainheat_energy_eff ) assert not funding.eco4_eligible @@ -836,26 +1029,31 @@ def test_epc_d_social_solar_with_ashp_and_no_insulation_should_fail( def test_epc_d_social_solar_with_heating_and_minimum_insulation_should_pass( - mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes + mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel, + mock_mainheat_energy_eff ): funding = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, - social_cavity_abs_rate=13.5, - social_solid_abs_rate=17, - private_cavity_abs_rate=13.5, - private_solid_abs_rate=17, + eco4_social_cavity_abs_rate=13.5, + eco4_social_solid_abs_rate=17, + eco4_private_cavity_abs_rate=13.5, + eco4_private_solid_abs_rate=17, + gbis_social_cavity_abs_rate=21, + gbis_social_solid_abs_rate=25, + gbis_private_cavity_abs_rate=22, + gbis_private_solid_abs_rate=28, tenure="Social" ) # Innovation solar + insulation measures + eligible heating upgrade = not valid because the heat pump isn;t # an innovation measure measures = [ - {"type": "solar_pv", "is_innovation": True}, - {"type": "internal_wall_insulation", "is_innovation": False}, - {"type": "loft_insulation", "is_innovation": False}, - {"type": "air_source_heat_pump", "is_innovation": False} + {"type": "solar_pv", "is_innovation": True, "uplift": 0.45}, + {"type": "internal_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} ] funding.check_funding( @@ -870,7 +1068,10 @@ def test_epc_d_social_solar_with_heating_and_minimum_insulation_should_pass( has_roof_insulation_recommendation=True, current_wall_uvalue=2, is_partial=False, - existing_li_thickness=0 + existing_li_thickness=0, + mainheating=mock_mainheating, + main_fuel=mock_main_fuel, + mainheat_energy_eff=mock_mainheat_energy_eff ) assert not funding.eco4_eligible @@ -880,20 +1081,24 @@ def test_epc_d_social_solar_with_heating_and_minimum_insulation_should_pass( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, - social_cavity_abs_rate=13.5, - social_solid_abs_rate=17, - private_cavity_abs_rate=13.5, - private_solid_abs_rate=17, + eco4_social_cavity_abs_rate=13.5, + eco4_social_solid_abs_rate=17, + eco4_private_cavity_abs_rate=13.5, + eco4_private_solid_abs_rate=17, + gbis_social_cavity_abs_rate=21, + gbis_social_solid_abs_rate=25, + gbis_private_cavity_abs_rate=22, + gbis_private_solid_abs_rate=28, tenure="Social" ) # Innovation solar + insulation measures + eligible heating upgrade = should be valid because the # heat pump is an innovation measure measures2 = [ - {"type": "solar_pv", "is_innovation": True}, - {"type": "internal_wall_insulation", "is_innovation": False}, - {"type": "loft_insulation", "is_innovation": False}, - {"type": "air_source_heat_pump", "is_innovation": True} + {"type": "solar_pv", "is_innovation": True, "uplift": 0.45}, + {"type": "internal_wall_insulation", "is_innovation": False, "uplift": 0}, + {"type": "loft_insulation", "is_innovation": False, "uplift": 0}, + {"type": "air_source_heat_pump", "is_innovation": True, "uplift": 0.25} ] funding2.check_funding( @@ -908,7 +1113,10 @@ def test_epc_d_social_solar_with_heating_and_minimum_insulation_should_pass( has_roof_insulation_recommendation=True, current_wall_uvalue=2, is_partial=False, - existing_li_thickness=0 + existing_li_thickness=0, + mainheating=mock_mainheating, + main_fuel=mock_main_fuel, + mainheat_energy_eff=mock_mainheat_energy_eff ) assert funding2.eco4_eligible @@ -920,16 +1128,23 @@ def test_custom_eco4_scenarios( scenario, mock_project_scores_matrix, mock_partial_scores_matrix, - mock_whlg_postcodes + mock_whlg_postcodes, + mock_mainheating, + mock_main_fuel, + mock_mainheat_energy_eff ): funding = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, - social_cavity_abs_rate=13.5, - social_solid_abs_rate=17, - private_cavity_abs_rate=13.5, - private_solid_abs_rate=17, + eco4_social_cavity_abs_rate=13.5, + eco4_social_solid_abs_rate=17, + eco4_private_cavity_abs_rate=13.5, + eco4_private_solid_abs_rate=17, + gbis_social_cavity_abs_rate=21, + gbis_social_solid_abs_rate=25, + gbis_private_cavity_abs_rate=22, + gbis_private_solid_abs_rate=28, tenure="Social" ) @@ -945,7 +1160,10 @@ def test_custom_eco4_scenarios( is_partial=False, existing_li_thickness=0, has_wall_insulation_recommendation=scenario.get("has_wall_insulation_recommendation", False), - has_roof_insulation_recommendation=scenario.get("has_roof_insulation_recommendation", False) + has_roof_insulation_recommendation=scenario.get("has_roof_insulation_recommendation", False), + mainheating=mock_mainheating, + main_fuel=mock_main_fuel, + mainheat_energy_eff=mock_mainheat_energy_eff ) assert funding.eco4_eligible == scenario["expected_eligibility"], f"Failed: {scenario['description']}" @@ -971,10 +1189,14 @@ def test_uplift( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, - social_cavity_abs_rate=13.5, - social_solid_abs_rate=17, - private_cavity_abs_rate=13.5, - private_solid_abs_rate=17, + eco4_social_cavity_abs_rate=13.5, + eco4_social_solid_abs_rate=17, + eco4_private_cavity_abs_rate=13.5, + eco4_private_solid_abs_rate=17, + gbis_social_cavity_abs_rate=21, + gbis_social_solid_abs_rate=25, + gbis_private_cavity_abs_rate=22, + gbis_private_solid_abs_rate=28, tenure="Social" ) @@ -1006,18 +1228,23 @@ def test_uplift( mainheat_energy_eff=mock_mainheat_energy_eff, ) - assert funding.eco4_funding == 123 - assert funding.eco4_uplift == 456 + assert funding.eco4_funding == 5302.3949999999995 + assert funding.full_project_abs == 392.77 # is 280 + the 112.77 innovation uplift + assert funding.eco4_uplift == 112.77 def _dummy_funding(): # Matrices/whlg are unused by _map_to_pre_main_heating; pass harmless placeholders return Funding( tenure="Social", - social_cavity_abs_rate=0.0, - social_solid_abs_rate=0.0, - private_cavity_abs_rate=0.0, - private_solid_abs_rate=0.0, + eco4_social_cavity_abs_rate=13.5, + eco4_social_solid_abs_rate=17, + eco4_private_cavity_abs_rate=13.5, + eco4_private_solid_abs_rate=17, + gbis_social_cavity_abs_rate=21, + gbis_social_solid_abs_rate=25, + gbis_private_cavity_abs_rate=22, + gbis_private_solid_abs_rate=28, project_scores_matrix=None, partial_project_scores_matrix=None, whlg_eligible_postcodes=set(), @@ -1042,9 +1269,6 @@ def test_map_to_pre_main_heating(scenario): "expected"], f"Failed: {scenario['description']} -> {result} (expected {scenario['expected']})" -# TODO: Add innovation uplift to private -raise ValueError("TODO: ADD INNOVATION TO PRIVATE") - # Large scale testing for various measures # measures = [ # {"type": "solar_pv", "is_innovation": True, "uplift": 0.45}, @@ -1057,3 +1281,115 @@ raise ValueError("TODO: ADD INNOVATION TO PRIVATE") # {"type": "cavity_wall_insulation", "is_innovation": True, "uplift": 0.25}, # {"type": "high_heat_retention_storage_heaters", "is_innovation": False, "uplift": 0}, # ] + + +### ------------------------- +### PRIVATE (PRS/Owner) — Innovation uplift behaviour +### ------------------------- + +def test_private_epc_e_solar_needs_heating( + mock_project_scores_matrix, + mock_partial_scores_matrix, + mock_whlg_postcodes, + mock_mainheating, + mock_main_fuel, + mock_mainheat_energy_eff +): + """EPC D private: Solar PV as innovation requires eligible low-carbon heating.""" + funding = Funding( + project_scores_matrix=mock_project_scores_matrix, + partial_project_scores_matrix=mock_partial_scores_matrix, + whlg_eligible_postcodes=mock_whlg_postcodes, + eco4_social_cavity_abs_rate=13.5, + eco4_social_solid_abs_rate=17, + eco4_private_cavity_abs_rate=13.5, + eco4_private_solid_abs_rate=17, + gbis_social_cavity_abs_rate=21, + gbis_social_solid_abs_rate=25, + gbis_private_cavity_abs_rate=22, + gbis_private_solid_abs_rate=28, + tenure="Private", + ) + + measures = [{"type": "solar_pv", "is_innovation": True, "uplift": 0.45}] + funding.check_funding( + measures=measures, + starting_sap=54, # EPC E - eligible for private on EPC + ending_sap=69, + floor_area=80, + mainheat_description="Boiler and radiators, mains gas", # not eligible for solar innovation + heating_control_description="Programmer, room thermostat and TRVs", + is_cavity=True, + current_wall_uvalue=2, + is_partial=False, + existing_li_thickness=0, + has_wall_insulation_recommendation=False, + has_roof_insulation_recommendation=False, + mainheating=mock_mainheating, + main_fuel=mock_main_fuel, + mainheat_energy_eff=mock_mainheat_energy_eff, + council_tax_band="B", + ) + + assert not funding.eco4_eligible + assert EligibilityCaveats.SOLAR_NEEDS_HEATING in funding.eco4_eligibility_caveats + + +def test_private_epc_e_solar_with_heating_and_minimum_insulation_produces_uplift( + mock_project_scores_matrix, + mock_partial_scores_matrix, + mock_whlg_postcodes, + mock_mainheating, + mock_main_fuel, + mock_mainheat_energy_eff +): + """EPC E private: Solar PV innovation + eligible heating + required insulation -> eligible and uplift > 0.""" + funding = Funding( + project_scores_matrix=mock_project_scores_matrix, + partial_project_scores_matrix=mock_partial_scores_matrix, + whlg_eligible_postcodes=mock_whlg_postcodes, + eco4_social_cavity_abs_rate=13.5, + eco4_social_solid_abs_rate=17, + eco4_private_cavity_abs_rate=13.5, + eco4_private_solid_abs_rate=17, + gbis_social_cavity_abs_rate=21, + gbis_social_solid_abs_rate=25, + gbis_private_cavity_abs_rate=22, + gbis_private_solid_abs_rate=28, + tenure="Private", + ) + + measures = [ + {"type": "solar_pv", "is_innovation": True, "uplift": 0.45}, + {"type": "air_source_heat_pump", "is_innovation": False, "uplift": 0}, + {"type": "cavity_wall_insulation", "is_innovation": False, "uplift": 0}, + {"type": "loft_insulation", "is_innovation": False, "uplift": 0}, + ] + + funding.check_funding( + measures=measures, + starting_sap=54, # EPC E + ending_sap=69, + floor_area=80, + mainheat_description="Air source heat pump, radiators", # eligible low-carbon heating present + heating_control_description="Programmer, room thermostat and TRVs", + 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=mock_mainheating, + main_fuel=mock_main_fuel, + mainheat_energy_eff=mock_mainheat_energy_eff, + council_tax_band="B", + ) + + assert funding.eco4_eligible + assert EligibilityCaveats.INNOVATION_REQUIRED not in funding.eco4_eligibility_caveats + assert EligibilityCaveats.SOLAR_NEEDS_HEATING not in funding.eco4_eligibility_caveats + # We don't pin an exact numeric value (depends on score matrices), + # but innovation uplift should be positive when solar PV has an uplift. + assert funding.eco4_uplift and funding.eco4_uplift > 0 + # And total funding should include that uplift + assert funding.eco4_funding and funding.eco4_funding > 0 diff --git a/etl/epc_clean/app.py b/etl/epc_clean/app.py index ff8fc95a..1f320a9b 100644 --- a/etl/epc_clean/app.py +++ b/etl/epc_clean/app.py @@ -40,28 +40,35 @@ def app(): cleaned_data = {} epc_directories = [entry for entry in EPC_DIRECTORY.iterdir() if entry.is_dir()] + errors = [] for directory in tqdm(epc_directories): - data = pd.read_csv(directory / "certificates.csv", low_memory=False) - # Rename the columns to the same format as the api returns - data.columns = [c.replace("_", "-").lower() for c in data.columns] - # Take just date before the date threshold - data = data[data["lodgement-date"] >= "2011-01-01"] + try: + data = pd.read_csv(directory / "certificates.csv", low_memory=False) + # Rename the columns to the same format as the api returns + data.columns = [c.replace("_", "-").lower() for c in data.columns] + # Take just date before the date threshold + data = data[data["lodgement-date"] >= "2011-01-01"] - # Convert to list of dictioaries as returned by the api - data = data.to_dict("records") + # Convert to list of dictioaries as returned by the api + data = data.to_dict("records") - # Incorporate input data into cleaning - cleaner = EpcClean(data) + # Incorporate input data into cleaning + cleaner = EpcClean(data) - cleaner.clean() - # Extended cleaned_data - for k, data in cleaner.cleaned.items(): - if k not in cleaned_data: - cleaned_data[k] = data - else: - existing_descriptions = [x["original_description"] for x in cleaned_data[k]] - new_data = [x for x in data if x["original_description"] not in existing_descriptions] - cleaned_data[k].extend(new_data) + cleaner.clean() + # Extended cleaned_data + for k, data in cleaner.cleaned.items(): + if k not in cleaned_data: + cleaned_data[k] = data + else: + existing_descriptions = [x["original_description"] for x in cleaned_data[k]] + new_data = [x for x in data if x["original_description"] not in existing_descriptions] + cleaned_data[k].extend(new_data) + except Exception as e: + errors.append(directory) + + if errors: + raise ValueError("We have errors") # Basic check to make sure all descriptions are unique for _, cleaned in cleaned_data.items(): @@ -75,7 +82,6 @@ def app(): # data being read in will be extremely small, meaning quicker load times. We'll begin by storing as a single # file and monitor usage patterns to see if it makes sense to split the data up - # TODO: Copy the existing cleaned to an archive location, in case we wish to roll back easily cleaned_historic = read_from_s3( s3_file_name="cleaned_epc_data/cleaned.bson", bucket_name=f"retrofit-data-{ENVIRONMENT}" diff --git a/etl/epc_clean/epc_attributes/FloorAttributes.py b/etl/epc_clean/epc_attributes/FloorAttributes.py index bba33424..6def93f0 100644 --- a/etl/epc_clean/epc_attributes/FloorAttributes.py +++ b/etl/epc_clean/epc_attributes/FloorAttributes.py @@ -34,6 +34,8 @@ class FloorAttributes(Definitions): "i ofod heb ei wresogi, dim inswleiddio (rhagdybiaeth)": "to unheated space, no insulation (assumed)", "i ofod heb ei wresogi, heb ei inswleiddio (rhagdybiaeth)": "to unheated space, no insulation (assumed)", "i ofod heb ei wresogi, dim inswleiddio": "to unheated space, no insulation", + "igçör awyr y tu allan, wedigçöi inswleiddio (rhagdybiaeth)": "to external air, insulated (assumed)", + "crog, inswleiddio cyfyngedig (rhagdybiaeth)": "suspended, limited insulation (assumed)" } def __init__(self, description: str): diff --git a/etl/epc_clean/epc_attributes/HotWaterAttributes.py b/etl/epc_clean/epc_attributes/HotWaterAttributes.py index 76b4e6fa..1ea743fc 100644 --- a/etl/epc_clean/epc_attributes/HotWaterAttributes.py +++ b/etl/epc_clean/epc_attributes/HotWaterAttributes.py @@ -130,6 +130,7 @@ class HotWaterAttributes(Definitions): "o r brif system, gydag ynni r haul, dim thermostat ar y silindr": "from main system, plus solar, no cylinder " "thermostat", "o r brif system, gydag ynni r haul": "from main system, plus solar", + "pwmp gwres": "heat pump" } NODATA_DESCRIPTIONS = [ diff --git a/etl/epc_clean/epc_attributes/LightingAttributes.py b/etl/epc_clean/epc_attributes/LightingAttributes.py index 08275446..712c6daa 100644 --- a/etl/epc_clean/epc_attributes/LightingAttributes.py +++ b/etl/epc_clean/epc_attributes/LightingAttributes.py @@ -12,6 +12,7 @@ class LightingAttributes(Definitions): "goleuadau ynni-isel ym mhob un o'r mannau gosod": 'Low energy lighting in all fixed outlets', "effeithlonrwydd goleuo da": 'good lighting efficiency', "effeithlonrwydd goleuo is na'r cyfartaledd": 'below average lighting efficiency', + "effeithlonrwydd goleuo rhagorol": "excellent lighting efficiency" } OBSERVED_ERRORS = [] diff --git a/etl/epc_clean/epc_attributes/MainheatAttributes.py b/etl/epc_clean/epc_attributes/MainheatAttributes.py index 85860bbf..312fa9fe 100644 --- a/etl/epc_clean/epc_attributes/MainheatAttributes.py +++ b/etl/epc_clean/epc_attributes/MainheatAttributes.py @@ -92,7 +92,9 @@ class MainHeatAttributes(Definitions): "gas-fired heat pumps, electric": "air source heat pump, electric", "radiator heating, heat from boilers - gas": "boiler and radiators, mains gas", "heat pump, warm air, mains gas": "air source heat pump, warm air, mains gas", - "air sourceheat pump, radiators, electric": "air source heat pump, radiators, electric" + "air sourceheat pump, radiators, electric": "air source heat pump, radiators, electric", + "bwyler gyda rheiddiaduron a gwres dan y llawr, nwy prif gyflenwad": "Boiler and radiators, mains gas, " + "Boiler and underfloor heating, mains gas", } edge_case_result = {} diff --git a/etl/epc_clean/epc_attributes/MainheatControlAttributes.py b/etl/epc_clean/epc_attributes/MainheatControlAttributes.py index 3b97e02a..997865d3 100644 --- a/etl/epc_clean/epc_attributes/MainheatControlAttributes.py +++ b/etl/epc_clean/epc_attributes/MainheatControlAttributes.py @@ -75,6 +75,7 @@ class MainheatControlAttributes(Definitions): TO_REMAP = { "celect control": 'celect-type control', "celect controls": 'celect-type control', + "celect type controls": 'celect-type control', "trv's, program & flow switch": 'trvs, programmer & flow switch', 'appliance thermostat': 'appliance thermostats', } diff --git a/etl/epc_clean/tests/test_data/test_mainheat_attributes_cases.py b/etl/epc_clean/tests/test_data/test_mainheat_attributes_cases.py index 64478b5f..45994b1d 100644 --- a/etl/epc_clean/tests/test_data/test_mainheat_attributes_cases.py +++ b/etl/epc_clean/tests/test_data/test_mainheat_attributes_cases.py @@ -864,7 +864,7 @@ mainheat_cases = [ 'has_wood_pellets': False, 'has_anthracite': False, 'has_dual_fuel_mineral_and_wood': True, 'has_smokeless_fuel': False, 'has_lpg': False, 'has_assumed': False, 'has_electricaire': False, 'has_assumed_for_most_rooms': False, 'has_underfloor_heating': False, "has_electric_heat_pumps": False, - "has_micro-cogeneration": False}, + "has_micro-cogeneration": False, 'has_mineral_and_wood': True}, {'original_description': 'Room heaters, electric', 'has_radiators': False, 'has_fan_coil_units': False, 'has_pipes_in_screed_above_insulation': False, 'has_pipes_in_insulated_timber_floor': False, 'has_pipes_in_concrete_slab': False, 'has_boiler': False, 'has_air_source_heat_pump': False, @@ -1455,8 +1455,7 @@ mainheat_cases = [ 'has_coal': False, 'has_oil': False, 'has_wood_pellets': False, 'has_anthracite': False, 'has_dual_fuel_mineral_and_wood': True, 'has_smokeless_fuel': False, 'has_lpg': False, 'has_assumed': False, 'has_electricaire': False, 'has_assumed_for_most_rooms': False, 'has_underfloor_heating': False, - "has_electric_heat_pumps": False, - "has_micro-cogeneration": False}, + "has_electric_heat_pumps": False, "has_micro-cogeneration": False, "has_mineral_and_wood": True}, {'original_description': 'Bwyler a rheiddiaduron, dau danwydd (mwynau a choed)', 'has_radiators': True, 'has_fan_coil_units': False, 'has_pipes_in_screed_above_insulation': False, 'has_pipes_in_insulated_timber_floor': False, @@ -1468,8 +1467,8 @@ mainheat_cases = [ 'has_coal': False, 'has_oil': False, 'has_wood_pellets': False, 'has_anthracite': False, 'has_dual_fuel_mineral_and_wood': True, 'has_smokeless_fuel': False, 'has_lpg': False, 'has_assumed': False, 'has_electricaire': False, 'has_assumed_for_most_rooms': False, 'has_underfloor_heating': False, - "has_electric_heat_pumps": False, - "has_micro-cogeneration": False}, + "has_electric_heat_pumps": False, "has_micro-cogeneration": False, "has_mineral_and_wood": True + }, {'original_description': 'Pwmp gwres syGÇÖn tarddu yn y ddaear, dan y llawr, trydan', 'has_radiators': False, 'has_fan_coil_units': False, 'has_pipes_in_screed_above_insulation': False, 'has_pipes_in_insulated_timber_floor': False, 'has_pipes_in_concrete_slab': False, 'has_boiler': False, @@ -1541,7 +1540,7 @@ mainheat_cases = [ 'has_wood_pellets': False, 'has_anthracite': False, 'has_dual_fuel_mineral_and_wood': True, 'has_smokeless_fuel': False, 'has_lpg': False, 'has_assumed': False, 'has_electricaire': False, 'has_assumed_for_most_rooms': False, 'has_underfloor_heating': False, "has_electric_heat_pumps": False, - "has_micro-cogeneration": False}, + "has_micro-cogeneration": False, "has_mineral_and_wood": True}, {'original_description': 'Room heaters, wood pellets', 'has_radiators': False, 'has_fan_coil_units': False, 'has_pipes_in_screed_above_insulation': False, 'has_pipes_in_insulated_timber_floor': False, 'has_pipes_in_concrete_slab': False, 'has_boiler': False, 'has_air_source_heat_pump': False, diff --git a/recommendations/tests/test_heating_recommendations.py b/recommendations/tests/test_heating_recommendations.py index 039801e1..b18839aa 100644 --- a/recommendations/tests/test_heating_recommendations.py +++ b/recommendations/tests/test_heating_recommendations.py @@ -51,6 +51,12 @@ class TestHeatingRecommendations: :return: """ + # We patch an old version of cleaned which is missing some attributes for 'mainheat-description' + for x in cleaned['mainheat-description']: + x["has_hot-water-only"] = False + x["has_mineral_and_wood"] = False + x["has_dual_fuel_appliance"] = False + epc_records = {"original_epc": test_case["epc"].copy(), "full_sap_epc": {}, "old_data": []} epc_record = EPCRecord(