mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
added insulation precondition unit tests
This commit is contained in:
parent
1aa6371ff5
commit
e2e2e8c71c
5 changed files with 735 additions and 23 deletions
|
|
@ -1,16 +1,14 @@
|
|||
from enum import Enum
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from typing import List
|
||||
|
||||
from backend.app.plan.schemas import HousingType, WALL_INSULATION_MEASURES, ROOF_INSULATION_MEASURES
|
||||
from backend.app.plan.schemas import HousingType, WALL_INSULATION_MEASURES, ROOF_INSULATION_MEASURES, MEASURE_MAP
|
||||
|
||||
|
||||
class EligibilityCaveats(Enum):
|
||||
TENANT_ON_BENEFITS_OR_LOW_INCOME = "tenant_on_benefits_or_low_income"
|
||||
INNOVATION_REQUIRED = "innovation_required"
|
||||
SOLAR_NEEDS_HEATING = "solar_needs_heating"
|
||||
NEEDS_INSULATION_TO_MINIMUM_STANDARDS = "needs_insulation_to_minimum_standards"
|
||||
MINIMUM_INSULATION_PRECONDITIONS_NOT_MET = "minimum_insulation_preconditions_not_met"
|
||||
|
||||
|
||||
class Funding:
|
||||
|
|
@ -97,9 +95,9 @@ class Funding:
|
|||
measures: list of dicts like {"type": "solar_pv", "is_innovation": True}
|
||||
"""
|
||||
measure_types = [m["type"] for m in measures]
|
||||
has_innovation = any(m.get("is_innovation", False) for m in measures)
|
||||
innovation_flags = [m.get("is_innovation", False) for m in measures]
|
||||
innovation_measures = [m["type"] for m in measures if m.get("is_innovation", False)]
|
||||
return measure_types, has_innovation, innovation_measures
|
||||
return measure_types, innovation_flags, innovation_measures
|
||||
|
||||
@staticmethod
|
||||
def _meets_upgrade_target(starting_sap: int, ending_sap: int) -> bool:
|
||||
|
|
@ -205,7 +203,8 @@ class Funding:
|
|||
ending_sap: int,
|
||||
has_innovation: bool,
|
||||
has_solar: bool,
|
||||
solar_eligible: bool
|
||||
solar_eligible: bool,
|
||||
solar_meets_mir: bool,
|
||||
):
|
||||
"""
|
||||
ECO4 Social Housing eligibility.
|
||||
|
|
@ -215,6 +214,11 @@ class Funding:
|
|||
"""
|
||||
if has_solar and not solar_eligible:
|
||||
# The package contins solar PV but it doesn't meet the eligibility requirements
|
||||
self.eco4_eligible = False
|
||||
if not solar_meets_mir:
|
||||
self.eco4_eligibility_caveats.append(EligibilityCaveats.MINIMUM_INSULATION_PRECONDITIONS_NOT_MET)
|
||||
else:
|
||||
self.eco4_eligibility_caveats.append(EligibilityCaveats.SOLAR_NEEDS_HEATING)
|
||||
return
|
||||
|
||||
meets_epc = starting_sap <= 69
|
||||
|
|
@ -234,6 +238,7 @@ class Funding:
|
|||
|
||||
self.eco4_eligible = True
|
||||
self.eco4_eligibility_caveats = []
|
||||
return
|
||||
|
||||
self.eco4_eligible = True
|
||||
self.eco4_eligibility_caveats = []
|
||||
|
|
@ -442,11 +447,14 @@ class Funding:
|
|||
Because of the various pre-requisites for solar, we have a self-contained function to check for
|
||||
eligibility
|
||||
|
||||
Returns a tuple of booleans (has_solar, solar_eligible)
|
||||
Returns a tuple of booleans (has_solar, solar_eligible, meets_mir): corresponding to:
|
||||
- If the package contains solar PV
|
||||
- If the package is eligible for solar
|
||||
- whether the package meets the minimum insulation requirements (MIR)
|
||||
"""
|
||||
|
||||
if "solar_pv" not in measure_types:
|
||||
return False, False
|
||||
return False, False, False
|
||||
|
||||
# 1) We check if there is an eligible heating system in place
|
||||
has_eligibile_heating = self.check_solar_eligible_heating_system(
|
||||
|
|
@ -457,23 +465,116 @@ class Funding:
|
|||
# We check if there is a recommendation for an ASHP or HHRSH
|
||||
if ("air_source_heat_pump" not in measure_types) and (
|
||||
"high_heat_retention_storage_heater" not in measure_types):
|
||||
return True, False
|
||||
return True, False, True
|
||||
|
||||
# 2) We check if there is a wall insulation measure for this property. If so, we make sure
|
||||
# we have a wall insulation recommendation in this package
|
||||
if has_wall_insulation_recommendation:
|
||||
# Make sure we have a wall insulation recommendation
|
||||
if not any(m in measure_types for m in WALL_INSULATION_MEASURES):
|
||||
return True, False
|
||||
return True, False, False
|
||||
|
||||
# 3) We check if there is a roof insulation measure for this property. If so, we make sure
|
||||
# we have a roof insulation recommendation in this package
|
||||
if has_roof_insulation_recommendation:
|
||||
# Make sure we have a roof insulation recommendation
|
||||
if not any(m in measure_types for m in ROOF_INSULATION_MEASURES):
|
||||
return True, False
|
||||
return True, False, False
|
||||
|
||||
return True, True
|
||||
return True, True, True
|
||||
|
||||
@staticmethod
|
||||
def meets_innovation_requirement(
|
||||
starting_sap: int,
|
||||
measures: List[dict],
|
||||
has_solar: bool,
|
||||
solar_meets_mir: bool,
|
||||
) -> bool:
|
||||
"""
|
||||
Determines if the innovation requirement is met for EPC D social housing.
|
||||
|
||||
- All measures must be innovation, unless:
|
||||
- solar is present
|
||||
- solar meets MIR (e.g. enough insulation)
|
||||
- solar is innovation
|
||||
- all other measures are insulation (can be non-innovation)
|
||||
"""
|
||||
|
||||
if not (55 <= starting_sap <= 68):
|
||||
return True # Only EPC D requires innovation check
|
||||
|
||||
# Case 1: solar + MIR met
|
||||
if has_solar and solar_meets_mir:
|
||||
for m in measures:
|
||||
if m["type"] == "solar_pv":
|
||||
if not m.get("is_innovation", False):
|
||||
return False # solar must be innovation
|
||||
elif m["type"] not in WALL_INSULATION_MEASURES + ROOF_INSULATION_MEASURES + [
|
||||
"suspended_floor_insulation", "solid_floor_insulation"
|
||||
]:
|
||||
if not m.get("is_innovation", False):
|
||||
return False # non-insulation, non-innovation = not eligible
|
||||
return True
|
||||
|
||||
# Case 2: No solar or MIR not met — all measures must be innovation
|
||||
return all(m.get("is_innovation", False) for m in measures)
|
||||
|
||||
@staticmethod
|
||||
def has_heating_measure(measure_types: List[str]) -> bool:
|
||||
"""
|
||||
Heating measures include: ASHP, GSHP, FTCH, DHC, HHRSH, other storage heaters, heating controls, solar PV.
|
||||
"""
|
||||
heating_measures = MEASURE_MAP["heating"] + MEASURE_MAP["heating_controls"] + [
|
||||
"first_time_central_heating", "district_heating_connection", "solar_pv"
|
||||
]
|
||||
return any(m in heating_measures for m in measure_types)
|
||||
|
||||
@staticmethod
|
||||
def meets_minimum_insulation_preconditions(
|
||||
starting_sap: int,
|
||||
measure_types: List[str],
|
||||
has_wall_insulation_recommendation: bool,
|
||||
has_roof_insulation_recommendation: bool,
|
||||
has_ftch: bool = False,
|
||||
has_dhc: bool = False,
|
||||
) -> bool:
|
||||
"""
|
||||
Applies ECO4 insulation guidance:
|
||||
|
||||
- **Precondition 1**:
|
||||
- Applies to EPC D homes WITHOUT FTCH or DHC
|
||||
- Must have at least one insulation measure IF any are recommended
|
||||
|
||||
- **Precondition 2**:
|
||||
- Applies to EPC E/F/G or EPC D WITH FTCH or DHC
|
||||
- Must include ALL *recommended* exterior wall and roof insulation (floor is exempt)
|
||||
"""
|
||||
# Normalize insulation types from MEASURE_MAP
|
||||
wall_measures = MEASURE_MAP["wall_insulation"]
|
||||
roof_measures = MEASURE_MAP["roof_insulation"]
|
||||
floor_measures = MEASURE_MAP["floor_insulation"]
|
||||
|
||||
has_any_insulation_recommendation = (
|
||||
has_wall_insulation_recommendation or has_roof_insulation_recommendation
|
||||
# Floor is exempt, so we don't check for a recommendation here
|
||||
)
|
||||
|
||||
# EPC D homes with no FTCH/DHC must include at least one insulation measure
|
||||
if 55 <= starting_sap <= 68 and not has_ftch and not has_dhc:
|
||||
if not has_any_insulation_recommendation:
|
||||
return True
|
||||
return any(m in measure_types for m in wall_measures + roof_measures + floor_measures)
|
||||
|
||||
# EPC EFG or D with FTCH/DHC: all recommended insulation types must be in place
|
||||
if has_wall_insulation_recommendation and not any(m in measure_types for m in wall_measures):
|
||||
return False
|
||||
if has_roof_insulation_recommendation and not any(m in measure_types for m in roof_measures):
|
||||
return False
|
||||
# We treat floors are exempt due to payback periods
|
||||
# if has_floor_insulation_recommendation and not any(m in measure_types for m in floor_measures):
|
||||
# return False
|
||||
|
||||
return True
|
||||
|
||||
def check_funding(
|
||||
self,
|
||||
|
|
@ -501,10 +602,30 @@ class Funding:
|
|||
"""
|
||||
|
||||
# Normalize measures
|
||||
measure_types, has_innovation, innovation_measures = self._split_measures(measures)
|
||||
measure_types, innovation_flags, innovation_measures = self._split_measures(measures)
|
||||
|
||||
# If we have a heating measure, we check if we meet the pre conditions
|
||||
has_ftch = "first_time_central_heating" in measure_types
|
||||
has_dhc = "district_heating_connection" in measure_types
|
||||
has_heating = self.has_heating_measure(measure_types)
|
||||
if has_heating:
|
||||
meets_mir = self.meets_minimum_insulation_preconditions(
|
||||
starting_sap,
|
||||
measure_types,
|
||||
has_wall_insulation_recommendation,
|
||||
has_roof_insulation_recommendation,
|
||||
has_ftch=has_ftch,
|
||||
has_dhc=has_dhc,
|
||||
)
|
||||
if not meets_mir:
|
||||
self.eco4_eligible = False
|
||||
self.eco4_eligibility_caveats.append(
|
||||
EligibilityCaveats.MINIMUM_INSULATION_PRECONDITIONS_NOT_MET
|
||||
)
|
||||
return
|
||||
|
||||
# Determine if we have a solar eligible heating system
|
||||
has_solar, solar_eligible = self.check_solar_eligibility(
|
||||
has_solar, solar_eligible, solar_meets_mir = self.check_solar_eligibility(
|
||||
measure_types,
|
||||
mainheat_description,
|
||||
heating_control_description,
|
||||
|
|
@ -512,6 +633,10 @@ class Funding:
|
|||
has_roof_insulation_recommendation,
|
||||
)
|
||||
|
||||
meets_innovation = self.meets_innovation_requirement(
|
||||
starting_sap, measures, has_solar, solar_meets_mir
|
||||
)
|
||||
|
||||
# Track EPC bands and floor area
|
||||
self.starting_sap_band = self.get_sap_band(starting_sap)
|
||||
self.ending_sap_band = self.get_sap_band(ending_sap)
|
||||
|
|
@ -531,10 +656,12 @@ class Funding:
|
|||
|
||||
elif self.tenure == "Social":
|
||||
# ECO4 Social
|
||||
self.eco4_sh_eligibility(starting_sap, ending_sap, has_innovation, has_solar, solar_eligible)
|
||||
self.eco4_sh_eligibility(
|
||||
starting_sap, ending_sap, meets_innovation, has_solar, solar_eligible, solar_meets_mir
|
||||
)
|
||||
|
||||
# GBIS Social
|
||||
self.gbis_sh_eligibility(starting_sap, measure_types, has_innovation)
|
||||
self.gbis_sh_eligibility(starting_sap, measure_types, meets_innovation)
|
||||
|
||||
if self.eco4_eligible:
|
||||
# Calculate the full project ABS for ECO4
|
||||
|
|
|
|||
|
Can't render this file because it is too large.
|
104
backend/tests/test_data/heating_scenarios.py
Normal file
104
backend/tests/test_data/heating_scenarios.py
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
from backend.Funding import EligibilityCaveats
|
||||
|
||||
heating_scenarios = [
|
||||
{
|
||||
"description": "EPC D with ASHP and no insulation at all — fails precondition 1",
|
||||
"measures": [{"type": "air_source_heat_pump"}],
|
||||
"starting_sap": 60,
|
||||
"mainheat_description": "air source heat pump",
|
||||
"heating_control_description": "roomstat_programmer_trvs",
|
||||
"has_wall_insulation_recommendation": True,
|
||||
"has_roof_insulation_recommendation": False,
|
||||
"expected_eligibility": False,
|
||||
"expected_caveats": [EligibilityCaveats.MINIMUM_INSULATION_PRECONDITIONS_NOT_MET],
|
||||
},
|
||||
{
|
||||
"description": "EPC D with ASHP and no insulation at all — fails precondition 1",
|
||||
"measures": [{"type": "air_source_heat_pump"}],
|
||||
"starting_sap": 60,
|
||||
"mainheat_description": "air source heat pump",
|
||||
"heating_control_description": "roomstat_programmer_trvs",
|
||||
"has_wall_insulation_recommendation": False,
|
||||
"has_roof_insulation_recommendation": False,
|
||||
"expected_eligibility": False,
|
||||
"expected_caveats": [EligibilityCaveats.MINIMUM_INSULATION_PRECONDITIONS_NOT_MET],
|
||||
},
|
||||
{
|
||||
"description": "EPC D with ASHP and floor insulation — passes precondition 1",
|
||||
"measures": [
|
||||
{"type": "air_source_heat_pump"},
|
||||
{"type": "suspended_floor_insulation"}
|
||||
],
|
||||
"starting_sap": 60,
|
||||
"has_wall_insulation_recommendation": False,
|
||||
"has_roof_insulation_recommendation": False,
|
||||
"mainheat_description": "air source heat pump",
|
||||
"heating_control_description": "roomstat_programmer_trvs",
|
||||
"expected_eligibility": True,
|
||||
"expected_caveats": [],
|
||||
},
|
||||
{
|
||||
"description": "EPC E with ASHP and only floor insulation — fails precondition 2 due to missing wall/roof",
|
||||
"measures": [
|
||||
{"type": "air_source_heat_pump"},
|
||||
{"type": "suspended_floor_insulation"}
|
||||
],
|
||||
"starting_sap": 45,
|
||||
"mainheat_description": "air source heat pump",
|
||||
"heating_control_description": "roomstat_programmer_trvs",
|
||||
"has_wall_insulation_recommendation": True,
|
||||
"has_roof_insulation_recommendation": True,
|
||||
"expected_eligibility": False,
|
||||
"expected_caveats": [EligibilityCaveats.MINIMUM_INSULATION_PRECONDITIONS_NOT_MET],
|
||||
},
|
||||
{
|
||||
"description": "EPC E with ASHP and both wall and roof insulation — passes precondition 2",
|
||||
"measures": [
|
||||
{"type": "air_source_heat_pump"},
|
||||
{"type": "external_wall_insulation"},
|
||||
{"type": "loft_insulation"}
|
||||
],
|
||||
"starting_sap": 45,
|
||||
"mainheat_description": "air source heat pump",
|
||||
"heating_control_description": "roomstat_programmer_trvs",
|
||||
"has_wall_insulation_recommendation": True,
|
||||
"has_roof_insulation_recommendation": True,
|
||||
"expected_eligibility": True,
|
||||
"expected_caveats": [],
|
||||
},
|
||||
{
|
||||
"description": "EPC D with FTCH and no insulation — still passes (exempt from precondition 1)",
|
||||
"measures": [{"type": "first_time_central_heating"}],
|
||||
"starting_sap": 60,
|
||||
"mainheat_description": "none",
|
||||
"heating_control_description": "none",
|
||||
"expected_eligibility": True,
|
||||
"expected_caveats": [],
|
||||
},
|
||||
{
|
||||
"description": "EPC E with FTCH and no insulation — fails precondition 2",
|
||||
"measures": [{"type": "first_time_central_heating"}],
|
||||
"starting_sap": 45,
|
||||
"mainheat_description": "none",
|
||||
"heating_control_description": "none",
|
||||
"has_wall_insulation_recommendation": True,
|
||||
"has_roof_insulation_recommendation": True,
|
||||
"expected_eligibility": False,
|
||||
"expected_caveats": [EligibilityCaveats.MINIMUM_INSULATION_PRECONDITIONS_NOT_MET],
|
||||
},
|
||||
{
|
||||
"description": "EPC E with FTCH and wall/roof insulation — passes precondition 2",
|
||||
"measures": [
|
||||
{"type": "first_time_central_heating"},
|
||||
{"type": "external_wall_insulation"},
|
||||
{"type": "loft_insulation"},
|
||||
],
|
||||
"starting_sap": 45,
|
||||
"mainheat_description": "none",
|
||||
"heating_control_description": "none",
|
||||
"has_wall_insulation_recommendation": True,
|
||||
"has_roof_insulation_recommendation": True,
|
||||
"expected_eligibility": True,
|
||||
"expected_caveats": [],
|
||||
},
|
||||
]
|
||||
170
backend/tests/test_data/innovation_measure_fixtures.py
Normal file
170
backend/tests/test_data/innovation_measure_fixtures.py
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
from backend.Funding import Funding, 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}],
|
||||
"starting_sap": 60,
|
||||
"mainheat_description": "Electric storage heaters",
|
||||
"heating_control_description": "Manual charge control",
|
||||
"has_wall_insulation_recommendation": False,
|
||||
"has_roof_insulation_recommendation": False,
|
||||
"expected_eligibility": False,
|
||||
"expected_caveats": [EligibilityCaveats.SOLAR_NEEDS_HEATING],
|
||||
},
|
||||
# 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}],
|
||||
"starting_sap": 60,
|
||||
"mainheat_description": "Air source heat pump, radiators",
|
||||
"heating_control_description": "Programmer, room thermostat and TRVs",
|
||||
"has_wall_insulation_recommendation": False,
|
||||
"has_roof_insulation_recommendation": False,
|
||||
"expected_eligibility": True,
|
||||
"expected_caveats": [],
|
||||
},
|
||||
# 3) Innovation PV, non-eligible heating system, heating upgrade to HHRSH, EPC E - eligible
|
||||
{
|
||||
"description": "Innovation PV + HHRSH upgrade, EPC E",
|
||||
"measures": [
|
||||
{"type": "solar_pv", "is_innovation": True},
|
||||
{"type": "high_heat_retention_storage_heater", "is_innovation": True}
|
||||
],
|
||||
"starting_sap": 50,
|
||||
"mainheat_description": "Electric storage heaters",
|
||||
"heating_control_description": "Manual charge control",
|
||||
"has_wall_insulation_recommendation": False,
|
||||
"has_roof_insulation_recommendation": False,
|
||||
"expected_eligibility": True,
|
||||
"expected_caveats": [],
|
||||
},
|
||||
# 4) Innovation PV + HHRSH upgrade
|
||||
{
|
||||
"description": "Innovation PV + HHRSH upgrade, EPC E",
|
||||
"measures": [
|
||||
{"type": "solar_pv", "is_innovation": True},
|
||||
{"type": "high_heat_retention_storage_heater", "is_innovation": True}
|
||||
],
|
||||
"starting_sap": 50,
|
||||
"mainheat_description": "Electric storage heaters",
|
||||
"heating_control_description": "Manual charge control",
|
||||
"has_wall_insulation_recommendation": False,
|
||||
"has_roof_insulation_recommendation": False,
|
||||
"expected_eligibility": True,
|
||||
"expected_caveats": [],
|
||||
},
|
||||
# 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}],
|
||||
"starting_sap": 60,
|
||||
"mainheat_description": "Air source heat pump, radiators",
|
||||
"heating_control_description": "Programmer, room thermostat and TRVs",
|
||||
"has_wall_insulation_recommendation": True,
|
||||
"has_roof_insulation_recommendation": False,
|
||||
"expected_eligibility": False,
|
||||
"expected_caveats": [EligibilityCaveats.MINIMUM_INSULATION_PRECONDITIONS_NOT_MET],
|
||||
},
|
||||
# 6) Innovation PV, wall insulation recommended and installed - eligible
|
||||
{
|
||||
"description": "Innovation PV, wall insulation recommended and installed",
|
||||
"measures": [
|
||||
{"type": "solar_pv", "is_innovation": True},
|
||||
{"type": "internal_wall_insulation", "is_innovation": False}
|
||||
],
|
||||
"starting_sap": 60,
|
||||
"mainheat_description": "Air source heat pump, radiators",
|
||||
"heating_control_description": "Programmer, room thermostat and TRVs",
|
||||
"has_wall_insulation_recommendation": True,
|
||||
"has_roof_insulation_recommendation": False,
|
||||
"expected_eligibility": True,
|
||||
"expected_caveats": [],
|
||||
},
|
||||
# 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}],
|
||||
"starting_sap": 60,
|
||||
"mainheat_description": "Air source heat pump, radiators",
|
||||
"heating_control_description": "Programmer, room thermostat and TRVs",
|
||||
"has_wall_insulation_recommendation": False,
|
||||
"has_roof_insulation_recommendation": True,
|
||||
"expected_eligibility": False,
|
||||
"expected_caveats": [EligibilityCaveats.MINIMUM_INSULATION_PRECONDITIONS_NOT_MET],
|
||||
},
|
||||
# 8) Innovation PV, roof insulation recommended and installed - eligible
|
||||
{
|
||||
"description": "Innovation PV, roof insulation recommended and installed",
|
||||
"measures": [
|
||||
{"type": "solar_pv", "is_innovation": True},
|
||||
{"type": "loft_insulation", "is_innovation": False}
|
||||
],
|
||||
"starting_sap": 60,
|
||||
"mainheat_description": "Air source heat pump, radiators",
|
||||
"heating_control_description": "Programmer, room thermostat and TRVs",
|
||||
"has_wall_insulation_recommendation": False,
|
||||
"has_roof_insulation_recommendation": True,
|
||||
"expected_eligibility": True,
|
||||
"expected_caveats": [],
|
||||
},
|
||||
# 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}],
|
||||
"starting_sap": 60,
|
||||
"mainheat_description": "Air source heat pump, radiators",
|
||||
"heating_control_description": "Programmer, room thermostat and TRVs",
|
||||
"has_wall_insulation_recommendation": True,
|
||||
"has_roof_insulation_recommendation": True,
|
||||
"expected_eligibility": False,
|
||||
"expected_caveats": [EligibilityCaveats.MINIMUM_INSULATION_PRECONDITIONS_NOT_MET],
|
||||
},
|
||||
# 10) Innovation PV, both recommended, only wall insulation installed - not eligible
|
||||
{
|
||||
"description": "Innovation PV, both insulations recommended, only wall done",
|
||||
"measures": [
|
||||
{"type": "solar_pv", "is_innovation": True},
|
||||
{"type": "internal_wall_insulation", "is_innovation": False}
|
||||
],
|
||||
"starting_sap": 60,
|
||||
"mainheat_description": "Air source heat pump, radiators",
|
||||
"heating_control_description": "Programmer, room thermostat and TRVs",
|
||||
"has_wall_insulation_recommendation": True,
|
||||
"has_roof_insulation_recommendation": True,
|
||||
"expected_eligibility": False,
|
||||
"expected_caveats": [EligibilityCaveats.MINIMUM_INSULATION_PRECONDITIONS_NOT_MET],
|
||||
},
|
||||
# 11) Innovation PV, both recommended, only roof insulation installed - not eligible
|
||||
{
|
||||
"description": "Innovation PV, both insulations recommended, only roof done",
|
||||
"measures": [
|
||||
{"type": "solar_pv", "is_innovation": True},
|
||||
{"type": "loft_insulation", "is_innovation": False}
|
||||
],
|
||||
"starting_sap": 60,
|
||||
"mainheat_description": "Air source heat pump, radiators",
|
||||
"heating_control_description": "Programmer, room thermostat and TRVs",
|
||||
"has_wall_insulation_recommendation": True,
|
||||
"has_roof_insulation_recommendation": True,
|
||||
"expected_eligibility": False,
|
||||
"expected_caveats": [EligibilityCaveats.MINIMUM_INSULATION_PRECONDITIONS_NOT_MET],
|
||||
},
|
||||
# 12) Innovation PV, both recommended, both installed - eligible
|
||||
{
|
||||
"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}
|
||||
],
|
||||
"starting_sap": 60,
|
||||
"mainheat_description": "Air source heat pump, radiators",
|
||||
"heating_control_description": "Programmer, room thermostat and TRVs",
|
||||
"has_wall_insulation_recommendation": True,
|
||||
"has_roof_insulation_recommendation": True,
|
||||
"expected_eligibility": True,
|
||||
"expected_caveats": [],
|
||||
},
|
||||
]
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
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
|
||||
|
|
@ -30,7 +31,7 @@ def mock_project_scores_matrix():
|
|||
|
||||
@pytest.fixture
|
||||
def mock_partial_scores_matrix():
|
||||
df = pd.read_csv("recommendations/tests/test_data/ECO4_Partial_Project_Scores_Matrix_v6.csv")
|
||||
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']
|
||||
|
|
@ -358,8 +359,8 @@ def test_eco4_sh_epc_d_requires_innovation(mock_project_scores_matrix, mock_part
|
|||
existing_li_thickness=0,
|
||||
)
|
||||
|
||||
assert funding5.eco4_eligible
|
||||
assert not funding5.eco4_eligibility_caveats
|
||||
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
|
||||
|
|
@ -393,7 +394,39 @@ def test_eco4_sh_epc_d_requires_innovation(mock_project_scores_matrix, mock_part
|
|||
)
|
||||
|
||||
assert not funding6.eco4_eligible
|
||||
assert not funding6.eco4_eligibility_caveats
|
||||
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):
|
||||
|
|
@ -458,8 +491,8 @@ def test_eco4_sh_solar_pv_with_heating_is_ok(mock_project_scores_matrix, mock_pa
|
|||
existing_li_thickness=0,
|
||||
)
|
||||
|
||||
assert funding.eco4_eligible
|
||||
assert EligibilityCaveats.SOLAR_NEEDS_HEATING not in funding.eco4_eligibility_caveats
|
||||
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,
|
||||
|
|
@ -596,3 +629,281 @@ def test_eco4_upgrade_requirement_f_to_e_fail(mock_project_scores_matrix, mock_p
|
|||
)
|
||||
|
||||
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']}"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue