debugged funding test

This commit is contained in:
Khalim Conn-Kowlessar 2025-10-28 19:21:53 +00:00
parent 1c94db54ef
commit ef934f6b7c
6 changed files with 756 additions and 607 deletions

View file

@ -418,7 +418,20 @@ class SearchEpc:
address, [", ".join([r["address"]]) for r in rows], score_cutoff=0
)
# Pick the largest score
if best_match1[1] >= best_match2[1]:
if best_match1[1] == best_match2[1]:
# if thery're the same, we'll work under the assumption that the addresses are the same and we'll
# take whichever has the newest EPC
rows_filtered = [
r for r in rows
if (", ".join([r["address"], r["posttown"]]) == best_match1[0]) or
(r["address"] == best_match2[0])
]
rows_filtered = [
r for r in rows_filtered
if r["lodgement-datetime"] == max([x["lodgement-datetime"] for x in rows_filtered])
]
elif best_match1[1] > best_match2[1]:
# Get all of the scores
rows_filtered = [r for r in rows if ", ".join([r["address"], r["posttown"]]) == best_match1[0]]
else:

View file

@ -4,7 +4,7 @@ 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, "uplift": 0.45}],
"measures": [{"type": "solar_pv", "is_innovation": True, "innovation_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, "uplift": 0.45}],
"measures": [{"type": "solar_pv", "is_innovation": True, "innovation_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, "uplift": 0.45},
{"type": "high_heat_retention_storage_heater", "is_innovation": True, "uplift": 0.1}
{"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45},
{"type": "high_heat_retention_storage_heater", "is_innovation": False, "innovation_uplift": 0}
],
"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, "uplift": 0.45},
{"type": "high_heat_retention_storage_heater", "is_innovation": True, "uplift": 0.1}
{"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45},
{"type": "high_heat_retention_storage_heater", "is_innovation": False, "innovation_uplift": 0}
],
"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, "uplift": 0.45}],
"measures": [{"type": "solar_pv", "is_innovation": True, "innovation_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, "uplift": 0.45},
{"type": "internal_wall_insulation", "is_innovation": False, "uplift": 0.25}
{"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45},
{"type": "internal_wall_insulation", "is_innovation": False, "innovation_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, "uplift": 0.45}],
"measures": [{"type": "solar_pv", "is_innovation": True, "innovation_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, "uplift": 0.45},
{"type": "loft_insulation", "is_innovation": False, "uplift": 0}
{"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45},
{"type": "loft_insulation", "is_innovation": False, "innovation_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, "uplift": 0.45}],
"measures": [{"type": "solar_pv", "is_innovation": True, "innovation_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, "uplift": 0.45},
{"type": "internal_wall_insulation", "is_innovation": False, "uplift": 0.25}
{"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45},
{"type": "internal_wall_insulation", "is_innovation": False, "innovation_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, "uplift": 0.45},
{"type": "loft_insulation", "is_innovation": False, "uplift": 0}
{"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45},
{"type": "loft_insulation", "is_innovation": False, "innovation_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, "uplift": 0.45},
{"type": "internal_wall_insulation", "is_innovation": False, "uplift": 0.25},
{"type": "loft_insulation", "is_innovation": False, "uplift": 0}
{"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45},
{"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0.25},
{"type": "loft_insulation", "is_innovation": False, "innovation_uplift": 0}
],
"starting_sap": 60,
"mainheat_description": "Air source heat pump, radiators",

View file

@ -120,7 +120,7 @@ def test_eco4_prs_eligible_with_swi(
# 3) is getting a solid was measure
# so it's eligible for ECO4
measures = [{"type": "internal_wall_insulation", "is_innovation": False, "uplift": 0}]
measures = [{"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0}]
funding.check_funding(
measures=measures,
starting_sap=50, # EPC E
@ -162,7 +162,7 @@ def test_eco4_prs_not_eligible_high_epc(
tenure="Private",
)
measures = [{"type": "internal_wall_insulation", "is_innovation": False, "uplift": 0}]
measures = [{"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0}]
funding.check_funding(
measures=measures,
starting_sap=72, # EPC C (too high)
@ -203,7 +203,7 @@ def test_gbis_prs_general_eligibility(
tenure="Private",
)
measures = [{"type": "internal_wall_insulation", "is_innovation": False, "uplift": 0}]
measures = [{"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0}]
funding.check_funding(
measures=measures,
starting_sap=65, # EPC D
@ -244,7 +244,7 @@ def test_gbis_prs_low_income_caveat(
tenure="Private",
)
measures = [{"type": "cavity_wall_insulation", "is_innovation": False, "uplift": 0}]
measures = [{"type": "cavity_wall_insulation", "is_innovation": False, "innovation_uplift": 0}]
funding.check_funding(
measures=measures,
starting_sap=60, # EPC D
@ -290,7 +290,7 @@ def test_eco4_sh_epc_e_eligible(
tenure="Social",
)
measures = [{"type": "internal_wall_insulation", "is_innovation": False, "uplift": 0}]
measures = [{"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0}]
funding.check_funding(
measures=measures,
starting_sap=50, # EPC E
@ -330,7 +330,7 @@ def test_eco4_sh_epc_d_requires_innovation(
tenure="Social",
)
measures = [{"type": "internal_wall_insulation", "is_innovation": False, "uplift": 0}]
measures = [{"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0}]
funding.check_funding(
measures=measures,
starting_sap=60, # EPC D
@ -365,7 +365,7 @@ def test_eco4_sh_epc_d_requires_innovation(
gbis_private_solid_abs_rate=28,
tenure="Social",
)
measures2 = [{"type": "internal_wall_insulation", "is_innovation": True, "uplift": 0.25}]
measures2 = [{"type": "internal_wall_insulation", "is_innovation": True, "innovation_uplift": 0.25}]
funding2.check_funding(
measures=measures2,
starting_sap=60, # EPC D
@ -403,7 +403,7 @@ def test_eco4_sh_epc_d_requires_innovation(
gbis_private_solid_abs_rate=28,
tenure="Social",
)
measures3 = [{"type": "solar_pv", "is_innovation": True, "uplift": 0.45}]
measures3 = [{"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45}]
funding3.check_funding(
measures=measures3,
starting_sap=60, # EPC D
@ -439,7 +439,7 @@ def test_eco4_sh_epc_d_requires_innovation(
tenure="Social",
)
measures4 = [{"type": "solar_pv", "is_innovation": True, "uplift": 0.45}, ]
measures4 = [{"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45}, ]
funding4.check_funding(
measures=measures4,
starting_sap=60, # EPC D
@ -476,8 +476,8 @@ def test_eco4_sh_epc_d_requires_innovation(
)
measures5 = [
{"type": "solar_pv", "is_innovation": True, "uplift": 0.45},
{"type": "high_heat_retention_storage_heater", "is_innovation": False, "uplift": 0}
{"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45},
{"type": "high_heat_retention_storage_heater", "is_innovation": False, "innovation_uplift": 0}
]
funding5.check_funding(
measures=measures5,
@ -516,7 +516,7 @@ def test_eco4_sh_epc_d_requires_innovation(
)
measures6 = [
{"type": "solar_pv", "is_innovation": True, "uplift": 0.45},
{"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45},
]
funding6.check_funding(
measures=measures6,
@ -556,9 +556,9 @@ def test_eco4_sh_epc_d_requires_innovation(
tenure="Social",
)
measures7 = [
{"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}
{"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45},
{"type": "cavity_wall_insulation", "is_innovation": False, "innovation_uplift": 0.25},
{"type": "loft_insulation", "is_innovation": False, "innovation_uplift": 0}
]
funding7.check_funding(
measures=measures7,
@ -599,7 +599,7 @@ def test_eco4_sh_solar_pv_requires_heating(
tenure="Social",
)
measures = [{"type": "solar_pv", "is_innovation": True, "uplift": 0.45}]
measures = [{"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45}]
funding.check_funding(
measures=measures,
starting_sap=60, # EPC D
@ -641,8 +641,8 @@ def test_eco4_sh_solar_pv_with_heating_is_ok(
)
measures = [
{"type": "solar_pv", "is_innovation": True, "uplift": 0.45},
{"type": "air_source_heat_pump", "is_innovation": False, "uplift": 0}
{"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45},
{"type": "air_source_heat_pump", "is_innovation": False, "innovation_uplift": 0}
]
funding.check_funding(
measures=measures,
@ -684,7 +684,7 @@ def test_eco4_upgrade_requirement_e_to_c_pass(
tenure="Private",
)
measures = [{"type": "internal_wall_insulation", "is_innovation": False, "uplift": 0}]
measures = [{"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0}]
# E (SAP 50) → C (SAP 70) meets upgrade rule
funding.check_funding(
@ -727,7 +727,7 @@ def test_eco4_upgrade_requirement_e_to_d_fail(
tenure="Private",
)
measures = [{"type": "internal_wall_insulation", "is_innovation": False, "uplift": 0}]
measures = [{"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0}]
# E (SAP 50) → D (SAP 65) does NOT meet ECO4 upgrade rule
funding.check_funding(
@ -770,7 +770,7 @@ def test_eco4_upgrade_requirement_f_to_d_pass(
tenure="Private",
)
measures = [{"type": "internal_wall_insulation", "is_innovation": False, "uplift": 0}]
measures = [{"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0}]
# F (SAP 35) → D (SAP 60) is OK for ECO4
funding.check_funding(
@ -813,7 +813,7 @@ def test_eco4_upgrade_requirement_f_to_e_fail(
tenure="Private",
)
measures = [{"type": "internal_wall_insulation", "is_innovation": False, "uplift": 0}]
measures = [{"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0}]
# F (SAP 35) → E (SAP 50) does NOT meet ECO4 rule
funding.check_funding(
@ -859,7 +859,7 @@ def test_epc_d_social_no_innovation_no_heating(
)
measures = [
{"type": "solar_pv", "is_innovation": True, "uplift": 0.45}
{"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45}
]
funding.check_funding(
@ -905,10 +905,10 @@ def test_epc_d_social_with_heating_and_insulation(
# Should NOT be eligible as the ASHP is not an innovation measure
measures = [
{"type": "solar_pv", "is_innovation": True, "uplift": 0.45},
{"type": "internal_wall_insulation", "is_innovation": False, "uplift": 0},
{"type": "loft_insulation", "is_innovation": False, "uplift": 0},
{"type": "air_source_heat_pump", "is_innovation": False, "uplift": 0}
{"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45},
{"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0},
{"type": "loft_insulation", "is_innovation": False, "innovation_uplift": 0},
{"type": "air_source_heat_pump", "is_innovation": False, "innovation_uplift": 0}
]
funding.check_funding(
@ -954,9 +954,9 @@ def test_epc_d_social_solar_with_only_minimum_insulation_should_fail(
# Solar PV innovation with insulation, but no heating system upgrade => not eligible
measures = [
{"type": "solar_pv", "is_innovation": True, "uplift": 0.45},
{"type": "internal_wall_insulation", "is_innovation": False, "uplift": 0},
{"type": "loft_insulation", "is_innovation": False, "uplift": 0}
{"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45},
{"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0},
{"type": "loft_insulation", "is_innovation": False, "innovation_uplift": 0}
]
funding.check_funding(
@ -1002,8 +1002,8 @@ def test_epc_d_social_solar_with_ashp_and_no_insulation_should_fail(
# Solar PV innovation with heating, but no insulation when insulation is recommended => not eligible
measures = [
{"type": "solar_pv", "is_innovation": True, "uplift": 0.45},
{"type": "air_source_heat_pump", "is_innovation": False, "uplift": 0}
{"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45},
{"type": "air_source_heat_pump", "is_innovation": False, "innovation_uplift": 0}
]
funding.check_funding(
@ -1050,10 +1050,10 @@ def test_epc_d_social_solar_with_heating_and_minimum_insulation_should_pass(
# 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, "uplift": 0.45},
{"type": "internal_wall_insulation", "is_innovation": False, "uplift": 0},
{"type": "loft_insulation", "is_innovation": False, "uplift": 0},
{"type": "air_source_heat_pump", "is_innovation": False, "uplift": 0}
{"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45},
{"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0},
{"type": "loft_insulation", "is_innovation": False, "innovation_uplift": 0},
{"type": "air_source_heat_pump", "is_innovation": False, "innovation_uplift": 0}
]
funding.check_funding(
@ -1095,10 +1095,10 @@ def test_epc_d_social_solar_with_heating_and_minimum_insulation_should_pass(
# 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, "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}
{"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45},
{"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0},
{"type": "loft_insulation", "is_innovation": False, "innovation_uplift": 0},
{"type": "air_source_heat_pump", "is_innovation": True, "innovation_uplift": 0.25}
]
funding2.check_funding(
@ -1203,11 +1203,11 @@ def test_uplift(
# # TODO: Add a scenario with multiple measures, where some are innovation, some are not and we have
# TODO: Make sure private works too
measures = [
{"type": "solar_pv", "is_innovation": True, "uplift": 0.45},
{"type": "internal_wall_insulation", "is_innovation": False, "uplift": 0},
{"type": "loft_insulation", "is_innovation": False, "uplift": 0},
{"type": "air_source_heat_pump", "is_innovation": False, "uplift": 0},
{"type": "cavity_wall_insulation", "is_innovation": False, "uplift": 0.25},
{"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45},
{"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0},
{"type": "loft_insulation", "is_innovation": False, "innovation_uplift": 0},
{"type": "air_source_heat_pump", "is_innovation": False, "innovation_uplift": 0},
{"type": "cavity_wall_insulation", "is_innovation": False, "innovation_uplift": 0.25},
]
funding.check_funding(
@ -1229,7 +1229,7 @@ def test_uplift(
)
assert funding.eco4_funding == 5302.3949999999995
assert funding.full_project_abs == 392.77 # is 280 + the 112.77 innovation uplift
assert funding.full_project_abs == 280 # Doesn't include the eco4 uplift
assert funding.eco4_uplift == 112.77
@ -1311,7 +1311,7 @@ def test_private_epc_e_solar_needs_heating(
tenure="Private",
)
measures = [{"type": "solar_pv", "is_innovation": True, "uplift": 0.45}]
measures = [{"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45}]
funding.check_funding(
measures=measures,
starting_sap=54, # EPC E - eligible for private on EPC
@ -1360,10 +1360,10 @@ def test_private_epc_e_solar_with_heating_and_minimum_insulation_produces_uplift
)
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},
{"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45},
{"type": "air_source_heat_pump", "is_innovation": False, "innovation_uplift": 0},
{"type": "cavity_wall_insulation", "is_innovation": False, "innovation_uplift": 0},
{"type": "loft_insulation", "is_innovation": False, "innovation_uplift": 0},
]
funding.check_funding(

File diff suppressed because it is too large Load diff

View file

@ -26,7 +26,7 @@ class TestSearchEpcIntegration:
# Test case 2: Another valid address and postcode
# In this case, the newest EPC, does not have a uprn associated to it. If we did a search by
# uprn, we would get an old EPC
("Flat 8, Hainton House", "DN32 9AQ", 10090082018, True,
("Flat 8, Hainton House", "DN32 9AQ", "", True,
"bd1149a20a73397184f07a9955f872424826e70f4870c058d71be887766ee1f8", 2),
# Test case 3: When we make a request to the API for this property, we get back results for
# flats 1, 2 and 3. We have some logic to handle the response so that we get back flat 1
@ -56,7 +56,6 @@ class TestSearchEpcIntegration:
# We check that we have the correct epc
assert epc_searcher.newest_epc["lmk-key"] == lmk_key
assert epc_searcher.newest_epc["uprn"] == uprn
assert len(epc_searcher.older_epcs) == n_old_epcs
def test_search_housenumber(self):

View file

@ -144,6 +144,15 @@ class DummyProp:
self.has_ventilation = False
self.floor_area = 70.0
self.main_heating_controls = {"clean_description": "time and temperature zone control"}
self.walls = {'original_description': 'Solid brick, as built, no insulation (assumed)',
'thermal_transmittance': None,
'thermal_transmittance_unit': None, 'is_cavity_wall': False, 'is_filled_cavity': False,
'is_solid_brick': True,
'is_system_built': False, 'is_timber_frame': False, 'is_granite_or_whinstone': False,
'is_as_built': True,
'is_cob': False, 'is_assumed': True, 'is_sandstone_or_limestone': False,
'insulation_thickness': 'none',
'external_insulation': False, 'internal_insulation': False}
self.main_heating = {
'original_description': 'Boiler and radiators, mains gas',
@ -230,6 +239,7 @@ def property_recommendations():
'quantity_unit': 'm2', 'total': 19090.810139104888,
'labour_hours': 0.0, 'labour_days': 0.0}],
'type': 'external_wall_insulation', 'measure_type': 'external_wall_insulation',
"innovation_rate": 0,
'description': 'Install 150mm EWI Pro EPS external wall insulation system with Brick '
'Slip finish on external walls',
'starting_u_value': 1.7, 'new_u_value': 0.32, 'already_installed': False,
@ -258,6 +268,7 @@ def property_recommendations():
'quantity_unit': 'm2', 'total': 5694.929118083911, 'labour_hours': 134.37473199973275,
'labour_days': 4.199210374991648}], 'type': 'internal_wall_insulation',
'measure_type': 'internal_wall_insulation',
"innovation_rate": 0,
'description': 'Install 95mm '
'SWIP EcoBatt & '
'Plastered '
@ -314,6 +325,7 @@ def property_recommendations():
'quantity_unit': 'm2', 'total': 645.0, 'labour_hours': 8,
'labour_days': 1}], 'type': 'loft_insulation',
'measure_type': 'loft_insulation',
"innovation_rate": 0,
'description': 'Install 300mm of Knauf Loft Roll 44 glass fibre roll in your loft',
'starting_u_value': 2.3, 'new_u_value': 2.3, 'sap_points': np.float64(2.4),
'already_installed': False,
@ -338,6 +350,7 @@ def property_recommendations():
'plant_cost': 0.0, 'total_cost': 350.0, 'notes': None, 'is_installer_quote': True, 'total': 700.0,
'quantity': 2,
'quantity_unit': 'part'}], 'type': 'mechanical_ventilation', 'measure_type': 'mechanical_ventilation',
"innovation_rate": 0,
'description': 'Install 2 '
'Mechanical '
'Extract '
@ -387,6 +400,7 @@ def property_recommendations():
'labour_hours': 70.08999999999999,
'labour_days': 2.920416666666666}],
'type': 'suspended_floor_insulation', 'measure_type': 'suspended_floor_insulation',
"innovation_rate": 0,
'description': 'Install 75mm Q-bot underfloor insulation insulation in suspended '
'floor',
'starting_u_value': 0.83, 'new_u_value': 0.22, 'sap_points': 2, 'survey': True,
@ -401,6 +415,7 @@ def property_recommendations():
'energy_cost_savings': np.float64(76.04936470588231)}], [
{'phase': 4, 'parts': [], 'type': 'low_energy_lighting',
'measure_type': 'low_energy_lighting',
"innovation_rate": 0,
'description': 'Install low energy lighting in -886 outlets', 'starting_u_value': None,
'new_u_value': None, 'already_installed': False, 'sap_points': 2,
'kwh_savings': -48508.5, 'energy_cost_savings': -12481.237049999998,
@ -413,6 +428,7 @@ def property_recommendations():
'recommendation_id': '5_phase=4', 'efficiency': -1705.5500000000002,
'heat_demand': np.float64(5.099999999999994)}], [
{'type': 'heating', 'phase': 5, 'measure_type': 'time_temperature_zone_control',
"innovation_rate": 0,
'parts': [],
'description': 'Upgrade heating controls to Smart Thermostats, room sensors and '
'smart radiator valves (time & temperature zone control)',
@ -431,6 +447,7 @@ def property_recommendations():
'energy_cost_savings': np.float64(65.29581176470589)}], [
{'phase': 6, 'parts': [], 'type': 'secondary_heating',
'measure_type': 'secondary_heating',
"innovation_rate": 0,
'description': 'Remove the secondary heating system', 'starting_u_value': None,
'new_u_value': None, 'sap_points': np.float64(3.6), 'already_installed': False,
'total': 30.0, 'subtotal': 25.0, 'vat': 5.0, 'labour_hours': 3.0,
@ -443,6 +460,7 @@ def property_recommendations():
'kwh_savings': np.float64(196.29999999999927),
'energy_cost_savings': np.float64(14.61857647058821)}], [
{'phase': 7, 'parts': [], 'type': 'solar_pv', 'measure_type': 'solar_pv',
"innovation_rate": 0,
'description': 'Install a 4.0 kilowatt-peak (kWp) solar panel system.',
'starting_u_value': None, 'new_u_value': None, 'sap_points': np.float64(13.0),
'already_installed': False, 'total': 6013.139999999999, 'subtotal': 5010.95, 'vat': 0,
@ -455,6 +473,7 @@ def property_recommendations():
'kwh_savings': np.float64(2040.8566307499998),
'energy_cost_savings': np.float64(525.1124110919749)},
{'phase': 7, 'parts': [], 'type': 'solar_pv', 'measure_type': 'solar_pv',
"innovation_rate": 0,
'description': 'Install a 4.0 kilowatt-peak (kWp) solar panel system, with a battery.',
'starting_u_value': None, 'new_u_value': None, 'sap_points': np.float64(13.0),
'already_installed': False, 'total': 10537.008, 'subtotal': 8780.84, 'vat': 0,
@ -467,6 +486,7 @@ def property_recommendations():
'kwh_savings': np.float64(2857.1992830499994),
'energy_cost_savings': np.float64(735.1573755287648)},
{'phase': 7, 'parts': [], 'type': 'solar_pv', 'measure_type': 'solar_pv',
"innovation_rate": 0,
'description': 'Install a 3.6 kilowatt-peak (kWp) solar panel system.',
'starting_u_value': None, 'new_u_value': None, 'sap_points': np.float64(12.0),
'already_installed': False, 'total': 5826.491999999999, 'subtotal': 4855.41, 'vat': 0,
@ -478,6 +498,7 @@ def property_recommendations():
'heat_demand': np.float64(83.69999999999999), 'kwh_savings': np.float64(1846.33397),
'energy_cost_savings': np.float64(475.0617304809999)},
{'phase': 7, 'parts': [], 'type': 'solar_pv', 'measure_type': 'solar_pv',
"innovation_rate": 0,
'description': 'Install a 3.6 kilowatt-peak (kWp) solar panel system, with a battery.',
'starting_u_value': None, 'new_u_value': None, 'sap_points': np.float64(12.0),
'already_installed': False, 'total': 10350.359999999999, 'subtotal': 8625.3, 'vat': 0,
@ -489,6 +510,7 @@ def property_recommendations():
'heat_demand': np.float64(83.69999999999999), 'kwh_savings': np.float64(2584.867558),
'energy_cost_savings': np.float64(665.0864226734)},
{'phase': 7, 'parts': [], 'type': 'solar_pv', 'measure_type': 'solar_pv',
"innovation_rate": 0,
'description': 'Install a 3.2 kilowatt-peak (kWp) solar panel system.',
'starting_u_value': None, 'new_u_value': None, 'sap_points': np.float64(11.0),
'already_installed': False, 'total': 5642.604, 'subtotal': 4702.17, 'vat': 0,
@ -500,6 +522,7 @@ def property_recommendations():
'kwh_savings': np.float64(1650.2708274),
'energy_cost_savings': np.float64(424.61468389001993)},
{'phase': 7, 'parts': [], 'type': 'solar_pv', 'measure_type': 'solar_pv',
"innovation_rate": 0,
'description': 'Install a 3.2 kilowatt-peak (kWp) solar panel system, with a battery.',
'starting_u_value': None, 'new_u_value': None, 'sap_points': np.float64(11.0),
'already_installed': False, 'total': 10166.472, 'subtotal': 8472.06, 'vat': 0,
@ -511,6 +534,7 @@ def property_recommendations():
'heat_demand': np.float64(78.3), 'kwh_savings': np.float64(2310.3791583599996),
'energy_cost_savings': np.float64(594.4605574460278)},
{'phase': 7, 'parts': [], 'type': 'solar_pv', 'measure_type': 'solar_pv',
"innovation_rate": 0,
'description': 'Install a 2.8 kilowatt-peak (kWp) solar panel system.',
'starting_u_value': None, 'new_u_value': None, 'sap_points': np.float64(9.0),
'already_installed': False, 'total': 5458.727999999999, 'subtotal': 4548.94, 'vat': 0,
@ -522,6 +546,7 @@ def property_recommendations():
'kwh_savings': np.float64(1453.5933906),
'energy_cost_savings': np.float64(374.00957940138)},
{'phase': 7, 'parts': [], 'type': 'solar_pv', 'measure_type': 'solar_pv',
"innovation_rate": 0,
'description': 'Install a 2.8 kilowatt-peak (kWp) solar panel system, with a battery.',
'starting_u_value': None, 'new_u_value': None, 'sap_points': np.float64(9.0),
'already_installed': False, 'total': 9982.596, 'subtotal': 8318.83, 'vat': 0,
@ -533,6 +558,7 @@ def property_recommendations():
'heat_demand': np.float64(64.0), 'kwh_savings': np.float64(2035.03074684),
'energy_cost_savings': np.float64(523.6134111619319)},
{'phase': 7, 'parts': [], 'type': 'solar_pv', 'measure_type': 'solar_pv',
"innovation_rate": 0,
'description': 'Install a 2.4 kilowatt-peak (kWp) solar panel system.',
'starting_u_value': None, 'new_u_value': None, 'sap_points': np.float64(8.0),
'already_installed': False, 'total': 5274.852, 'subtotal': 4395.71, 'vat': 0,
@ -544,6 +570,7 @@ def property_recommendations():
'kwh_savings': np.float64(1255.12594),
'energy_cost_savings': np.float64(322.94390436199996)},
{'phase': 7, 'parts': [], 'type': 'solar_pv', 'measure_type': 'solar_pv',
"innovation_rate": 0,
'description': 'Install a 2.4 kilowatt-peak (kWp) solar panel system, with a battery.',
'starting_u_value': None, 'new_u_value': None, 'sap_points': np.float64(8.0),
'already_installed': False, 'total': 9798.72, 'subtotal': 8165.6, 'vat': 0,
@ -555,6 +582,7 @@ def property_recommendations():
'heat_demand': np.float64(54.3), 'kwh_savings': np.float64(1757.1763159999998),
'energy_cost_savings': np.float64(452.1214661067999)},
{'phase': 7, 'parts': [], 'type': 'solar_pv', 'measure_type': 'solar_pv',
"innovation_rate": 0,
'description': 'Install a 2.0 kilowatt-peak (kWp) solar panel system.',
'starting_u_value': None, 'new_u_value': None, 'sap_points': np.float64(7.0),
'already_installed': False, 'total': 5090.976, 'subtotal': 4242.48, 'vat': 0,
@ -566,6 +594,7 @@ def property_recommendations():
'kwh_savings': np.float64(1048.341318),
'energy_cost_savings': np.float64(269.7382211214)},
{'phase': 7, 'parts': [], 'type': 'solar_pv', 'measure_type': 'solar_pv',
"innovation_rate": 0,
'description': 'Install a 2.0 kilowatt-peak (kWp) solar panel system, with a battery.',
'starting_u_value': None, 'new_u_value': None, 'sap_points': np.float64(7.0),
'already_installed': False, 'total': 9614.844, 'subtotal': 8012.369999999999, 'vat': 0,
@ -586,10 +615,20 @@ def _attach_costs_and_uplifts(recs, funding, p):
for group in out:
for r in group:
if r["type"] in ["mechanical_ventilation", "low_energy_lighting", "secondary_heating"]:
r["innovation_uplift"] = 0
(
r["partial_project_score"],
r["partial_project_funding"],
r["innovation_uplift"],
r["uplift_project_score"],
) = (
0, 0, 0, 0
)
continue
r["uplift"] = 0.0 # fixed for determinism in test
r["innovation_uplift"] = funding.get_innovation_uplift(
(
r["partial_project_score"], r["partial_project_funding"], r["innovation_uplift"],
r["uplift_project_score"]
) = funding.get_innovation_uplift(
measure=r,
starting_sap=55,
floor_area=70.0,
@ -663,3 +702,100 @@ def test_social_fabric_only_returns_only_fabric_types(p, funding, property_recom
unfunded_rows = solutions[
solutions["path"].apply(lambda x: isinstance(x, dict) and x.get("reference") == "unfunded:all")]
assert not unfunded_rows.empty
def test_private_solid_wall_no_innovation_epc_d(p, funding, mock_project_scores_matrix, mock_partial_scores_matrix):
"""
We have a specific test for this case which was implemented incorrectly originally.
This is an EPC D property and so shouldn't be eligible for ECO4. Instead, only GBIS should be considered.
"""
# Overwrite the data - copied from real example
p2 = deepcopy(p)
p2.data = {
"current-energy-rating": "D",
"current-energy-efficiency": 68,
"mainheat-energy-eff": "Good",
}
p2.walls = {'original_description': 'Sandstone or limestone, as built, no insulation (assumed)',
'clean_description': 'Sandstone or limestone, as built, no insulation', 'thermal_transmittance': None,
'thermal_transmittance_unit': None, 'is_cavity_wall': False, 'is_filled_cavity': False,
'is_solid_brick': False, 'is_system_built': False, 'is_timber_frame': False,
'is_granite_or_whinstone': False, 'is_as_built': True, 'is_cob': False, 'is_assumed': True,
'is_sandstone_or_limestone': True, 'is_park_home': False, 'insulation_thickness': 'none',
'external_insulation': False, 'internal_insulation': False}
funding2 = Funding(
tenure="Private",
project_scores_matrix=mock_project_scores_matrix,
partial_project_scores_matrix=mock_partial_scores_matrix,
whlg_eligible_postcodes=pd.DataFrame([{"Postcode": "ab12cd"}]),
eco4_social_cavity_abs_rate=12.5,
eco4_social_solid_abs_rate=17,
eco4_private_cavity_abs_rate=12.5,
eco4_private_solid_abs_rate=17,
gbis_social_cavity_abs_rate=21,
gbis_social_solid_abs_rate=25,
gbis_private_cavity_abs_rate=21,
gbis_private_solid_abs_rate=28,
)
input_measures = [
[{'id': '0_phase=0', 'cost': np.float64(4441.202499013676), 'gain': np.float64(3.4000000000000057),
'type': 'internal_wall_insulation+mechanical_ventilation', 'innovation_uplift': np.float64(0.0),
'cost_minus_uplift': np.float64(4441.202499013676), 'raw_cost': 3881.2024990136756,
'partial_project_funding': np.float64(2300.1000000000004), 'partial_project_score': np.float64(135.3),
'uplift_project_score': np.float64(0.0)}], [
{'id': '2_phase=2', 'cost': np.float64(2280.0), 'gain': np.float64(0.4), 'type': 'secondary_glazing',
'innovation_uplift': np.float64(0.0), 'cost_minus_uplift': np.float64(2280.0),
'raw_cost': np.float64(2280.0), 'partial_project_funding': np.float64(1421.1999999999998),
'partial_project_score': np.float64(83.6), 'uplift_project_score': np.float64(0.0)}], [
{'id': '3_phase=3', 'cost': np.float64(604.5840000000001), 'gain': np.float64(1.2),
'type': 'time_temperature_zone_control', 'innovation_uplift': np.float64(0.0),
'cost_minus_uplift': np.float64(604.5840000000001), 'raw_cost': 604.5840000000001,
'partial_project_funding': np.float64(702.0999999999999), 'partial_project_score': np.float64(41.3),
'uplift_project_score': np.float64(0.0)}], [
{'id': '4_phase=4', 'cost': 60.0, 'gain': np.float64(0.0), 'type': 'secondary_heating',
'innovation_uplift': 0, 'cost_minus_uplift': 60.0, 'raw_cost': 60.0, 'partial_project_funding': 0,
'partial_project_score': 0, 'uplift_project_score': 0}]
]
solutions = optimise_with_funding_paths(
p=p2,
input_measures=input_measures,
housing_type="Private",
budget=None,
target_gain=1.5,
funding=funding2
)
# 3) basic shape assertions
assert isinstance(solutions, pd.DataFrame)
assert not solutions.empty
# We should have 2 rows
assert solutions.shape[0] == 2
# We should only have None or GBIS
assert set(solutions["scheme"].unique()) == {"none", "gbis"}
meets_upgrade_gbis = solutions[solutions["meets_upgrade_target"] & solutions["is_eligible"]]
assert meets_upgrade_gbis.shape[0] == 1
# Check exact result
assert meets_upgrade_gbis.squeeze().to_dict() == {
'fixed_ids': ['0_phase=0'], 'items': [
{'id': '0_phase=0', 'cost': 3881.2024990136756, 'gain': np.float64(3.4000000000000057),
'type': 'internal_wall_insulation+mechanical_ventilation', 'innovation_uplift': np.float64(0.0),
'cost_minus_uplift': np.float64(4441.202499013676), 'raw_cost': 3881.2024990136756,
'partial_project_funding': np.float64(2300.1000000000004), 'partial_project_score': np.float64(135.3),
'uplift_project_score': np.float64(0.0)}], 'total_cost': 3881.2024990136756,
'total_gain': 3.4000000000000057, 'path': [{'AND': ['internal_wall_insulation+mechanical_ventilation'],
'reference':
'internal_wall_insulation+mechanical_ventilation:gbis'}],
'scheme': 'gbis', 'is_eligible': True, 'unfunded_items': [], 'meets_upgrade_target': True, 'starting_sap': 68,
'floor_area': 70.0, 'ending_sap': 71.4, 'starting_band': 'High_D', 'ending_band': 'Low_C',
'floor_area_band': '0-72', 'project_score': 540.0, 'full_project_funding': 0.0,
'partial_project_funding': 2300.1000000000004, 'partial_project_score': 135.3, 'total_uplift': 0.0,
'total_uplift_score': 0.0
}