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(