mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
1084 lines
39 KiB
Python
1084 lines
39 KiB
Python
import pytest
|
||
import pandas as pd
|
||
from backend.Funding import Funding, EligibilityCaveats
|
||
from backend.tests.test_data.innovation_measure_fixtures import innovation_scenarios
|
||
|
||
|
||
@pytest.fixture
|
||
def mock_project_scores_matrix():
|
||
data = []
|
||
floor_segments = ["0-72", "73-97", "98-199", "200"]
|
||
bands = [
|
||
"Low_G", "High_G", "Low_F", "High_F", "Low_E", "High_E", "Low_D", "High_D", "Low_C", "High_C", "Low_B",
|
||
"High_B", "Low_A", "High_A"
|
||
]
|
||
|
||
cost = 50.0
|
||
for floor in floor_segments:
|
||
for start in bands:
|
||
for finish in bands:
|
||
if start != finish: # skip identical start/finish (no SAP movement)
|
||
data.append({
|
||
"Floor Area Segment": floor,
|
||
"Starting Band": start,
|
||
"Finishing Band": finish,
|
||
"Cost Savings": cost
|
||
})
|
||
cost += 5.0 # increment to create variety
|
||
|
||
return pd.DataFrame(data)
|
||
|
||
|
||
@pytest.fixture
|
||
def mock_partial_scores_matrix():
|
||
df = pd.read_csv("backend/tests/test_data/ECO4_Partial_Project_Scores_Matrix_v6.csv")
|
||
df.columns = ['Measure category', 'Measure_Type', 'Pre_Main_Heating_Source',
|
||
'Post_Main_Heating_Source', 'Total Floor Area Band', 'Starting Band',
|
||
'Average Treatable Factor', 'Cost Savings', 'SAP Savings']
|
||
return df
|
||
|
||
|
||
@pytest.fixture
|
||
def mock_whlg_postcodes():
|
||
return pd.DataFrame([{"Postcode": "ab12cd"}])
|
||
|
||
|
||
### -------------------------
|
||
### PRIVATE RENTED SECTOR (PRS)
|
||
### -------------------------
|
||
|
||
def test_eco4_prs_eligible_with_swi(mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes):
|
||
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,
|
||
tenure="Private",
|
||
)
|
||
|
||
# The property is:
|
||
# 1) private,
|
||
# 2) EPC E
|
||
# 3) is getting a solid was measure
|
||
# so it's eligible for ECO4
|
||
|
||
measures = [{"type": "internal_wall_insulation", "is_innovation": False}]
|
||
funding.check_funding(
|
||
measures=measures,
|
||
starting_sap=50, # EPC E
|
||
ending_sap=69,
|
||
floor_area=80,
|
||
mainheat_description="Boiler and radiators, mains gas",
|
||
heating_control_description="Programmer, room thermostat and TRVs",
|
||
is_cavity=True,
|
||
council_tax_band="B",
|
||
is_partial=False,
|
||
existing_li_thickness=0,
|
||
current_wall_uvalue=2
|
||
)
|
||
|
||
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):
|
||
"""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,
|
||
tenure="Private",
|
||
)
|
||
|
||
measures = [{"type": "internal_wall_insulation", "is_innovation": False}]
|
||
funding.check_funding(
|
||
measures=measures,
|
||
starting_sap=72, # EPC C (too high)
|
||
ending_sap=75,
|
||
floor_area=80,
|
||
mainheat_description="Boiler and radiators, mains gas",
|
||
heating_control_description="Programmer, room thermostat and TRVs",
|
||
is_cavity=True,
|
||
council_tax_band="B",
|
||
is_partial=False,
|
||
existing_li_thickness=0,
|
||
current_wall_uvalue=2,
|
||
)
|
||
|
||
assert not funding.eco4_eligible
|
||
|
||
|
||
def test_gbis_prs_general_eligibility(mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes):
|
||
"""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,
|
||
tenure="Private",
|
||
)
|
||
|
||
measures = [{"type": "internal_wall_insulation", "is_innovation": False}]
|
||
funding.check_funding(
|
||
measures=measures,
|
||
starting_sap=65, # EPC D
|
||
ending_sap=70,
|
||
floor_area=80,
|
||
mainheat_description="Boiler and radiators, mains gas",
|
||
heating_control_description="Programmer, room thermostat and TRVs",
|
||
is_cavity=True,
|
||
council_tax_band="A",
|
||
is_partial=False,
|
||
existing_li_thickness=0,
|
||
current_wall_uvalue=2,
|
||
)
|
||
|
||
assert funding.gbis_eligible
|
||
|
||
|
||
def test_gbis_prs_low_income_caveat(mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes):
|
||
"""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,
|
||
tenure="Private",
|
||
)
|
||
|
||
measures = [{"type": "cavity_wall_insulation", "is_innovation": False}]
|
||
funding.check_funding(
|
||
measures=measures,
|
||
starting_sap=60, # EPC D
|
||
ending_sap=70,
|
||
floor_area=80,
|
||
mainheat_description="Boiler and radiators, mains gas",
|
||
heating_control_description="Programmer, room thermostat and TRVs",
|
||
is_cavity=True,
|
||
council_tax_band="B",
|
||
is_partial=False,
|
||
existing_li_thickness=0,
|
||
current_wall_uvalue=2,
|
||
)
|
||
|
||
assert funding.gbis_eligible
|
||
assert EligibilityCaveats.TENANT_ON_BENEFITS_OR_LOW_INCOME in funding.gbis_eligibility_caveats
|
||
|
||
|
||
### -------------------------
|
||
### SOCIAL HOUSING
|
||
### -------------------------
|
||
|
||
def test_eco4_sh_epc_e_eligible(mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes):
|
||
"""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,
|
||
tenure="Social",
|
||
)
|
||
|
||
measures = [{"type": "internal_wall_insulation", "is_innovation": False}]
|
||
funding.check_funding(
|
||
measures=measures,
|
||
starting_sap=50, # EPC E
|
||
ending_sap=69,
|
||
floor_area=80,
|
||
mainheat_description="Boiler and radiators, mains gas",
|
||
heating_control_description="Programmer, room thermostat and TRVs",
|
||
is_cavity=True,
|
||
current_wall_uvalue=2,
|
||
is_partial=False,
|
||
existing_li_thickness=0,
|
||
)
|
||
|
||
assert funding.eco4_eligible
|
||
|
||
|
||
def test_eco4_sh_epc_d_requires_innovation(mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes):
|
||
"""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,
|
||
tenure="Social",
|
||
)
|
||
|
||
measures = [{"type": "internal_wall_insulation", "is_innovation": False}]
|
||
funding.check_funding(
|
||
measures=measures,
|
||
starting_sap=60, # EPC D
|
||
ending_sap=69,
|
||
floor_area=80,
|
||
mainheat_description="Boiler and radiators, mains gas",
|
||
heating_control_description="Programmer, room thermostat and TRVs",
|
||
is_cavity=True,
|
||
current_wall_uvalue=2,
|
||
is_partial=False,
|
||
existing_li_thickness=0,
|
||
)
|
||
|
||
assert not funding.eco4_eligible
|
||
assert EligibilityCaveats.INNOVATION_REQUIRED in funding.eco4_eligibility_caveats
|
||
|
||
# Test with an innovation measure
|
||
funding2 = 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,
|
||
tenure="Social",
|
||
)
|
||
measures2 = [{"type": "internal_wall_insulation", "is_innovation": True}]
|
||
funding2.check_funding(
|
||
measures=measures2,
|
||
starting_sap=60, # EPC D
|
||
ending_sap=69,
|
||
floor_area=80,
|
||
mainheat_description="Boiler and radiators, mains gas",
|
||
heating_control_description="Programmer, room thermostat and TRVs",
|
||
is_cavity=True,
|
||
current_wall_uvalue=2,
|
||
is_partial=False,
|
||
existing_li_thickness=0,
|
||
)
|
||
|
||
assert funding2.eco4_eligible
|
||
assert not funding2.eco4_eligibility_caveats
|
||
|
||
# Test with innovation solar. If the measure is solar, we need to have an eligible heating system.
|
||
# If we don't have an eligible heating system in place, we need to have one as part of the measure
|
||
# package
|
||
# THIS SHOULD NOT BE ELIGIBLE
|
||
funding3 = 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,
|
||
tenure="Social",
|
||
)
|
||
measures3 = [{"type": "solar_pv", "is_innovation": True}]
|
||
funding3.check_funding(
|
||
measures=measures3,
|
||
starting_sap=60, # EPC D
|
||
ending_sap=69,
|
||
floor_area=80,
|
||
mainheat_description="Boiler and radiators, mains gas",
|
||
heating_control_description="Programmer, room thermostat and TRVs",
|
||
is_cavity=True,
|
||
current_wall_uvalue=2,
|
||
is_partial=False,
|
||
existing_li_thickness=0,
|
||
)
|
||
|
||
assert not funding3.eco4_eligible
|
||
assert EligibilityCaveats.SOLAR_NEEDS_HEATING in funding3.eco4_eligibility_caveats
|
||
|
||
# Test with innovation solar and ASHP. This should be eligible
|
||
funding4 = 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,
|
||
tenure="Social",
|
||
)
|
||
|
||
measures4 = [{"type": "solar_pv", "is_innovation": True}]
|
||
funding4.check_funding(
|
||
measures=measures4,
|
||
starting_sap=60, # EPC D
|
||
ending_sap=69,
|
||
floor_area=80,
|
||
mainheat_description="Air source heat pump, radiators",
|
||
heating_control_description="Programmer, room thermostat and TRVs",
|
||
is_cavity=True,
|
||
current_wall_uvalue=2,
|
||
is_partial=False,
|
||
existing_li_thickness=0,
|
||
)
|
||
|
||
assert funding4.eco4_eligible
|
||
assert not funding4.eco4_eligibility_caveats
|
||
|
||
# Test with innovation solar, a non-eligible heating system but a heating upgrade
|
||
funding5 = 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,
|
||
tenure="Social",
|
||
)
|
||
|
||
measures5 = [
|
||
{"type": "solar_pv", "is_innovation": True},
|
||
{"type": "high_heat_retention_storage_heater", "is_innovation": False}
|
||
]
|
||
funding5.check_funding(
|
||
measures=measures5,
|
||
starting_sap=60, # EPC D
|
||
ending_sap=69,
|
||
floor_area=80,
|
||
mainheat_description="Electric storage heaters",
|
||
heating_control_description="Manual charge control",
|
||
is_cavity=True,
|
||
current_wall_uvalue=2,
|
||
is_partial=False,
|
||
existing_li_thickness=0,
|
||
)
|
||
|
||
assert not funding5.eco4_eligible
|
||
assert EligibilityCaveats.INNOVATION_REQUIRED in funding5.eco4_eligibility_caveats
|
||
|
||
# Test with innovation solar, an eligible heating system but a package that excludes the required
|
||
# fabric upgrades
|
||
funding6 = 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,
|
||
tenure="Social",
|
||
)
|
||
|
||
measures6 = [
|
||
{"type": "solar_pv", "is_innovation": True},
|
||
]
|
||
funding6.check_funding(
|
||
measures=measures6,
|
||
starting_sap=60, # EPC D
|
||
ending_sap=69,
|
||
floor_area=80,
|
||
mainheat_description="Electric storage heaters",
|
||
heating_control_description="controls for high heat retention storage heaters",
|
||
is_cavity=True,
|
||
has_wall_insulation_recommendation=True,
|
||
has_roof_insulation_recommendation=False,
|
||
current_wall_uvalue=2,
|
||
is_partial=False,
|
||
existing_li_thickness=0,
|
||
)
|
||
|
||
assert not funding6.eco4_eligible
|
||
assert EligibilityCaveats.MINIMUM_INSULATION_PRECONDITIONS_NOT_MET in funding6.eco4_eligibility_caveats
|
||
|
||
# Test with innovation solar, an eligible heating system but a package that includes the required
|
||
# fabric upgrades
|
||
funding7 = 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,
|
||
tenure="Social",
|
||
)
|
||
measures7 = [
|
||
{"type": "solar_pv", "is_innovation": True},
|
||
{"type": "cavity_wall_insulation", "is_innovation": False},
|
||
{"type": "loft_insulation", "is_innovation": False}
|
||
]
|
||
funding7.check_funding(
|
||
measures=measures7,
|
||
starting_sap=60, # EPC D
|
||
ending_sap=69,
|
||
floor_area=80,
|
||
mainheat_description="Air source heat pump, radiators",
|
||
heating_control_description="Programmer, room thermostat and TRVs",
|
||
is_cavity=True,
|
||
current_wall_uvalue=2,
|
||
is_partial=False,
|
||
existing_li_thickness=0,
|
||
)
|
||
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):
|
||
"""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,
|
||
tenure="Social",
|
||
)
|
||
|
||
measures = [{"type": "solar_pv", "is_innovation": True}]
|
||
funding.check_funding(
|
||
measures=measures,
|
||
starting_sap=60, # EPC D
|
||
ending_sap=69,
|
||
floor_area=80,
|
||
mainheat_description="Boiler and radiators, mains gas",
|
||
heating_control_description="Programmer, room thermostat and TRVs",
|
||
is_cavity=True,
|
||
current_wall_uvalue=2,
|
||
is_partial=False,
|
||
existing_li_thickness=0,
|
||
)
|
||
|
||
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):
|
||
"""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,
|
||
tenure="Social",
|
||
)
|
||
|
||
measures = [
|
||
{"type": "solar_pv", "is_innovation": True},
|
||
{"type": "air_source_heat_pump", "is_innovation": False}
|
||
]
|
||
funding.check_funding(
|
||
measures=measures,
|
||
starting_sap=60, # EPC D
|
||
ending_sap=69,
|
||
floor_area=80,
|
||
mainheat_description="Boiler and radiators, mains gas",
|
||
heating_control_description="Programmer, room thermostat and TRVs",
|
||
is_cavity=True,
|
||
current_wall_uvalue=2,
|
||
is_partial=False,
|
||
existing_li_thickness=0,
|
||
)
|
||
|
||
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):
|
||
"""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,
|
||
tenure="Private",
|
||
)
|
||
|
||
measures = [{"type": "internal_wall_insulation", "is_innovation": False}]
|
||
|
||
# E (SAP 50) → C (SAP 70) meets upgrade rule
|
||
funding.check_funding(
|
||
measures=measures,
|
||
starting_sap=50,
|
||
ending_sap=70,
|
||
floor_area=80,
|
||
mainheat_description="Boiler and radiators, mains gas",
|
||
heating_control_description="Programmer, room thermostat and TRVs",
|
||
is_cavity=True,
|
||
council_tax_band="B",
|
||
current_wall_uvalue=2,
|
||
is_partial=False,
|
||
existing_li_thickness=0,
|
||
)
|
||
|
||
assert funding.eco4_eligible
|
||
|
||
|
||
def test_eco4_upgrade_requirement_e_to_d_fail(mock_project_scores_matrix, mock_partial_scores_matrix,
|
||
mock_whlg_postcodes):
|
||
"""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,
|
||
tenure="Private",
|
||
)
|
||
|
||
measures = [{"type": "internal_wall_insulation", "is_innovation": False}]
|
||
|
||
# E (SAP 50) → D (SAP 65) does NOT meet ECO4 upgrade rule
|
||
funding.check_funding(
|
||
measures=measures,
|
||
starting_sap=50,
|
||
ending_sap=65,
|
||
floor_area=80,
|
||
mainheat_description="Boiler and radiators, mains gas",
|
||
heating_control_description="Programmer, room thermostat and TRVs",
|
||
is_cavity=True,
|
||
council_tax_band="B",
|
||
current_wall_uvalue=2,
|
||
is_partial=False,
|
||
existing_li_thickness=0,
|
||
)
|
||
|
||
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):
|
||
"""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,
|
||
tenure="Private",
|
||
)
|
||
|
||
measures = [{"type": "internal_wall_insulation", "is_innovation": False}]
|
||
|
||
# F (SAP 35) → D (SAP 60) is OK for ECO4
|
||
funding.check_funding(
|
||
measures=measures,
|
||
starting_sap=35,
|
||
ending_sap=60,
|
||
floor_area=80,
|
||
mainheat_description="Boiler and radiators, mains gas",
|
||
heating_control_description="Programmer, room thermostat and TRVs",
|
||
is_cavity=True,
|
||
council_tax_band="B",
|
||
current_wall_uvalue=2,
|
||
is_partial=False,
|
||
existing_li_thickness=0,
|
||
)
|
||
|
||
assert funding.eco4_eligible
|
||
|
||
|
||
def test_eco4_upgrade_requirement_f_to_e_fail(mock_project_scores_matrix, mock_partial_scores_matrix,
|
||
mock_whlg_postcodes):
|
||
"""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,
|
||
tenure="Private",
|
||
)
|
||
|
||
measures = [{"type": "internal_wall_insulation", "is_innovation": False}]
|
||
|
||
# F (SAP 35) → E (SAP 50) does NOT meet ECO4 rule
|
||
funding.check_funding(
|
||
measures=measures,
|
||
starting_sap=35,
|
||
ending_sap=50,
|
||
floor_area=80,
|
||
mainheat_description="Boiler and radiators, mains gas",
|
||
heating_control_description="Programmer, room thermostat and TRVs",
|
||
is_cavity=True,
|
||
council_tax_band="B",
|
||
current_wall_uvalue=2,
|
||
is_partial=False,
|
||
existing_li_thickness=0,
|
||
)
|
||
|
||
assert not funding.eco4_eligible
|
||
|
||
|
||
### -------------------------
|
||
### INNOVATION PRODUCTS
|
||
### -------------------------
|
||
def test_epc_d_social_no_innovation_no_heating(mock_project_scores_matrix, mock_partial_scores_matrix,
|
||
mock_whlg_postcodes):
|
||
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,
|
||
tenure="Social"
|
||
)
|
||
|
||
measures = [
|
||
{"type": "solar_pv", "is_innovation": True}
|
||
]
|
||
|
||
funding.check_funding(
|
||
measures=measures,
|
||
starting_sap=61,
|
||
ending_sap=69,
|
||
floor_area=80,
|
||
mainheat_description="Electric storage heaters",
|
||
heating_control_description="Manual charge control",
|
||
is_cavity=True,
|
||
has_wall_insulation_recommendation=False,
|
||
has_roof_insulation_recommendation=False,
|
||
current_wall_uvalue=2,
|
||
is_partial=False,
|
||
existing_li_thickness=0
|
||
)
|
||
|
||
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):
|
||
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,
|
||
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}
|
||
]
|
||
|
||
funding.check_funding(
|
||
measures=measures,
|
||
starting_sap=61,
|
||
ending_sap=69,
|
||
floor_area=80,
|
||
mainheat_description="Boiler and radiators, mains gas",
|
||
heating_control_description="Programmer, room thermostat and TRVs",
|
||
is_cavity=True,
|
||
has_wall_insulation_recommendation=True,
|
||
has_roof_insulation_recommendation=True,
|
||
current_wall_uvalue=2,
|
||
is_partial=False,
|
||
existing_li_thickness=0
|
||
)
|
||
|
||
assert not funding.eco4_eligible
|
||
assert EligibilityCaveats.INNOVATION_REQUIRED in funding.eco4_eligibility_caveats
|
||
|
||
|
||
def test_epc_d_social_solar_with_only_minimum_insulation_should_fail(
|
||
mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes
|
||
):
|
||
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,
|
||
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}
|
||
]
|
||
|
||
funding.check_funding(
|
||
measures=measures,
|
||
starting_sap=61,
|
||
ending_sap=69,
|
||
floor_area=80,
|
||
mainheat_description="Electric storage heaters",
|
||
heating_control_description="Manual charge control",
|
||
is_cavity=True,
|
||
has_wall_insulation_recommendation=True,
|
||
has_roof_insulation_recommendation=True,
|
||
current_wall_uvalue=2,
|
||
is_partial=False,
|
||
existing_li_thickness=0
|
||
)
|
||
|
||
assert not funding.eco4_eligible
|
||
assert EligibilityCaveats.SOLAR_NEEDS_HEATING in funding.eco4_eligibility_caveats
|
||
|
||
|
||
def test_epc_d_social_solar_with_ashp_and_no_insulation_should_fail(
|
||
mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes
|
||
):
|
||
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,
|
||
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}
|
||
]
|
||
|
||
funding.check_funding(
|
||
measures=measures,
|
||
starting_sap=61,
|
||
ending_sap=69,
|
||
floor_area=80,
|
||
mainheat_description="Electric storage heaters",
|
||
heating_control_description="Manual charge control",
|
||
is_cavity=True,
|
||
has_wall_insulation_recommendation=True,
|
||
has_roof_insulation_recommendation=True,
|
||
current_wall_uvalue=2,
|
||
is_partial=False,
|
||
existing_li_thickness=0
|
||
)
|
||
|
||
assert not funding.eco4_eligible
|
||
assert EligibilityCaveats.MINIMUM_INSULATION_PRECONDITIONS_NOT_MET in funding.eco4_eligibility_caveats
|
||
|
||
|
||
def test_epc_d_social_solar_with_heating_and_minimum_insulation_should_pass(
|
||
mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes
|
||
):
|
||
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,
|
||
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}
|
||
]
|
||
|
||
funding.check_funding(
|
||
measures=measures,
|
||
starting_sap=61,
|
||
ending_sap=69,
|
||
floor_area=80,
|
||
mainheat_description="Electric storage heaters",
|
||
heating_control_description="Manual charge control",
|
||
is_cavity=True,
|
||
has_wall_insulation_recommendation=True,
|
||
has_roof_insulation_recommendation=True,
|
||
current_wall_uvalue=2,
|
||
is_partial=False,
|
||
existing_li_thickness=0
|
||
)
|
||
|
||
assert not funding.eco4_eligible
|
||
assert EligibilityCaveats.INNOVATION_REQUIRED in funding.eco4_eligibility_caveats
|
||
|
||
funding2 = 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,
|
||
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}
|
||
]
|
||
|
||
funding2.check_funding(
|
||
measures=measures2,
|
||
starting_sap=61,
|
||
ending_sap=69,
|
||
floor_area=80,
|
||
mainheat_description="Electric storage heaters",
|
||
heating_control_description="Manual charge control",
|
||
is_cavity=True,
|
||
has_wall_insulation_recommendation=True,
|
||
has_roof_insulation_recommendation=True,
|
||
current_wall_uvalue=2,
|
||
is_partial=False,
|
||
existing_li_thickness=0
|
||
)
|
||
|
||
assert funding2.eco4_eligible
|
||
assert not funding2.eco4_eligibility_caveats
|
||
|
||
|
||
@pytest.mark.parametrize("scenario", innovation_scenarios)
|
||
def test_custom_eco4_scenarios(
|
||
scenario,
|
||
mock_project_scores_matrix,
|
||
mock_partial_scores_matrix,
|
||
mock_whlg_postcodes
|
||
):
|
||
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,
|
||
tenure="Social"
|
||
)
|
||
|
||
funding.check_funding(
|
||
measures=scenario["measures"],
|
||
starting_sap=scenario["starting_sap"],
|
||
ending_sap=69,
|
||
floor_area=80,
|
||
mainheat_description=scenario["mainheat_description"],
|
||
heating_control_description=scenario["heating_control_description"],
|
||
is_cavity=True,
|
||
current_wall_uvalue=2,
|
||
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)
|
||
)
|
||
|
||
assert funding.eco4_eligible == scenario["expected_eligibility"], f"Failed: {scenario['description']}"
|
||
for caveat in scenario.get("expected_caveats", []):
|
||
assert caveat in funding.eco4_eligibility_caveats, f"Missing caveat in: {scenario['description']}"
|
||
for caveat in funding.eco4_eligibility_caveats:
|
||
assert caveat in scenario.get("expected_caveats", []), f"Unexpected caveat in: {scenario['description']}"
|
||
|
||
|
||
### -------------------------
|
||
### Innovation uplift scenarios
|
||
### -------------------------
|
||
|
||
def test_uplift(
|
||
mock_project_scores_matrix,
|
||
mock_partial_scores_matrix,
|
||
mock_whlg_postcodes
|
||
):
|
||
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,
|
||
tenure="Social"
|
||
)
|
||
|
||
# # TODO: Add a scenario with multiple measures, where some are innovation, some are not and we have
|
||
# TODO: Make sure private works too
|
||
measures = [
|
||
{"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},
|
||
{"type": "cavity_wall_insulation", "is_innovation": False, "uplift": 0.25},
|
||
]
|
||
|
||
mainheating = {
|
||
'original_description': 'Electric storage heaters', '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,
|
||
'has_room_heaters': False, 'has_electric_storage_heaters': True, 'has_warm_air': False,
|
||
'has_electric_underfloor_heating': False, 'has_electric_ceiling_heating': False,
|
||
'has_community_scheme': False,
|
||
'has_ground_source_heat_pump': False, 'has_no_system_present': False,
|
||
'has_portable_electric_heaters': False,
|
||
'has_water_source_heat_pump': False, 'has_electric': True, 'has_mains_gas': False,
|
||
'has_wood_logs': False,
|
||
'has_coal': False, 'has_oil': False, 'has_wood_pellets': False, 'has_anthracite':
|
||
False,
|
||
'has_dual_fuel_mineral_and_wood': False, '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
|
||
}
|
||
main_fuel = {
|
||
'original_description': 'Electricity: electricity, unspecified tariff', 'fuel_type':
|
||
'electricity',
|
||
'tariff_type': 'unspecified tariff', 'is_community': False,
|
||
'no_individual_heating_or_community_network': False,
|
||
'complex_fuel_type': None
|
||
}
|
||
mainheat_energy_eff = "Good"
|
||
|
||
funding.check_funding(
|
||
measures=measures,
|
||
starting_sap=33,
|
||
ending_sap=69,
|
||
floor_area=71,
|
||
mainheat_description="Electic storage heaters",
|
||
heating_control_description="Manual charge control",
|
||
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=mainheating,
|
||
main_fuel=main_fuel,
|
||
mainheat_energy_eff=mainheat_energy_eff,
|
||
)
|
||
|
||
|
||
# Large scale testing for various measures
|
||
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},
|
||
]
|
||
epc_df = pd.read_csv(
|
||
"/Users/khalimconn-kowlessar/Downloads/domestic-E08000025-Birmingham/certificates.csv"
|
||
)
|
||
from tqdm import tqdm
|
||
from etl.epc_clean.epc_attributes.MainheatAttributes import MainHeatAttributes
|
||
from etl.epc_clean.epc_attributes.MainFuelAttributes import MainFuelAttributes
|
||
|
||
# TODO: Add innovation uplift to private
|
||
|
||
mock_project_scores_matrix = mock_project_scores_matrix()
|
||
mock_whlg_postcodes = mock_whlg_postcodes()
|
||
mock_partial_scores_matrix = mock_partial_scores_matrix()
|
||
|
||
errors = []
|
||
for _, x in tqdm(epc_df.iterrows(), total=len(epc_df)):
|
||
try:
|
||
# inputs
|
||
mainheat_energy_eff = x["MAINHEAT_ENERGY_EFF"]
|
||
heating_cleaner = MainHeatAttributes(description=x["MAINHEAT_DESCRIPTION"])
|
||
fuel_cleaner = MainFuelAttributes(description="" if pd.isnull(x["MAIN_FUEL"]) else x["MAIN_FUEL"])
|
||
|
||
h = heating_cleaner.process()
|
||
f = fuel_cleaner.process()
|
||
|
||
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,
|
||
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,
|
||
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,
|
||
)
|
||
except Exception as e:
|
||
errors.append(x["LMK_KEY"])
|
||
|
||
errored_epcs = epc_df[epc_df["LMK_KEY"].isin(errors)]
|
||
unique_combs = errored_epcs[["MAINHEAT_ENERGY_EFF", "MAINHEAT_DESCRIPTION", "MAIN_FUEL"]].drop_duplicates()
|
||
i = 0
|
||
x = errored_epcs[
|
||
(errored_epcs["MAINHEAT_ENERGY_EFF"] == unique_combs["MAINHEAT_ENERGY_EFF"].values[i]) &
|
||
(errored_epcs["MAINHEAT_DESCRIPTION"] == unique_combs["MAINHEAT_DESCRIPTION"].values[i]) &
|
||
(errored_epcs["MAIN_FUEL"] == unique_combs["MAIN_FUEL"].values[i])
|
||
].head(1).squeeze()
|