fabric measures for ECO

This commit is contained in:
Khalim Conn-Kowlessar 2025-08-13 11:20:38 +01:00
parent f182773b4b
commit 27f3f136c4
2 changed files with 75 additions and 11 deletions

View file

@ -11,13 +11,23 @@ TYPICAL_MEASURE_TYPES = [
WALL_INSULATION_MEASURES = ["internal_wall_insulation", "external_wall_insulation", "cavity_wall_insulation"]
ROOF_INSULATION_MEASURES = ["loft_insulation", "flat_roof_insulation", "room_roof_insulation"]
SPECIFIC_MEASURES = WALL_INSULATION_MEASURES + ROOF_INSULATION_MEASURES + [
"suspended_floor_insulation", "solid_floor_insulation",
"boiler_upgrade", "high_heat_retention_storage_heater", "air_source_heat_pump",
"secondary_heating", "solar_pv", "double_glazing", "secondary_glazing",
"ventilation", "low_energy_lighting", "fireplace", "hot_water_tank_insulation",
"cylinder_thermostat"
# Both all and roof insulaiton measures are eligible for ECO4. These are the remaining fabric and heating measures
# This is based on th measures we have recommendations for
ECO4_ELIGIBILE_FABRIC_MEASURES = [
"suspended_floor_insulation", "solid_floor_insulation", "double_glazing", "secondary_glazing"
]
ECO4_ELIGIBLE_HEATING_MEASURES = [
"boiler_upgrade", "high_heat_retention_storage_heater", "air_source_heat_pump", "solar_pv"
]
SPECIFIC_MEASURES = (
WALL_INSULATION_MEASURES + ROOF_INSULATION_MEASURES + ECO4_ELIGIBILE_FABRIC_MEASURES +
ECO4_ELIGIBLE_HEATING_MEASURES + [
"secondary_heating" "ventilation", "low_energy_lighting", "fireplace",
"hot_water_tank_insulation",
"cylinder_thermostat"
]
)
INSULATION_MEASURES = [
"internal_wall_insulation", "external_wall_insulation", "cavity_wall_insulation",

View file

@ -5,6 +5,9 @@ from numpy import nan
import datetime
from copy import deepcopy
from app.plan.schemas import (
WALL_INSULATION_MEASURES, ROOF_INSULATION_MEASURES, ECO4_ELIGIBILE_FABRIC_MEASURES, ECO4_ELIGIBLE_HEATING_MEASURES
)
from recommendations.optimiser.CostOptimiser import CostOptimiser
from recommendations.optimiser.GainOptimiser import GainOptimiser
from backend.Funding import Funding
@ -472,7 +475,7 @@ def _find_measure(input_measures, measure_type):
return False
def _make_generic_eco4_funding_paths(p, input_measures, funding_paths, remaining_insulation_type):
def _make_solar_heating_funding_paths(p, input_measures, funding_paths, remaining_insulation_type):
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Solar PV with existing eligible heating system
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -624,7 +627,7 @@ def make_funding_paths(p, input_measures, tenure):
):
input_gbis_measures_innovation.append([measure])
funding_paths = _make_generic_eco4_funding_paths(
funding_paths = _make_solar_heating_funding_paths(
p, input_measures_innovation, funding_paths, remaining_insulation_type
)
@ -653,7 +656,7 @@ def make_funding_paths(p, input_measures, tenure):
ewi_or_iwi[0]["reference"] = "+".join(reference_measures) + ":eco4"
funding_paths.append(ewi_or_iwi)
funding_paths = _make_generic_eco4_funding_paths(
funding_paths = _make_solar_heating_funding_paths(
p, input_measures, funding_paths, remaining_insulation_type
)
@ -758,6 +761,24 @@ def violates_min_insulation(fixed):
return is_heating and not has_insul
# Treat "type" like "external_wall_insulation+mechanical_ventilation" → "external_wall_insulation"
def _base_type(s: str) -> str:
return s.split("+", 1)[0]
def _filter_measures_by_types(input_measures, allowed_types):
"""
Keep only groups that have 1 allowed option; inside each group keep only allowed options.
"""
allowed_set = set(allowed_types)
filtered = []
for group in input_measures:
kept_opts = [opt for opt in group if _base_type(opt["type"]) in allowed_set]
if kept_opts:
filtered.append(kept_opts)
return filtered
def optimise_with_funding_paths(input_measures, budget=None, target_gain=None, social=False):
"""
run_optimizer(sub_measures, budget, target_gain) -> (picked_options, sub_cost, sub_gain)
@ -765,8 +786,40 @@ def optimise_with_funding_paths(input_measures, budget=None, target_gain=None, s
funding_paths = make_funding_paths(p, input_measures, body.housing_type)
# We now produce a fabric only path for ECO4
# We add in generic insulation funding paths (where there is no fixed measure)
# Heating controls are only eligible if installed as part of a heating upgrade and so we do not include them
# here
allowed_types = WALL_INSULATION_MEASURES + ROOF_INSULATION_MEASURES + ECO4_ELIGIBILE_FABRIC_MEASURES
funding_paths = [{'AND': [], 'reference': 'fabric-only:eco4'}] + funding_paths
solutions = []
for path_spec in funding_paths:
if path_spec["reference"] == "fabric-only:eco4":
sub_measures = _filter_measures_by_types(input_measures, allowed_types)
if not sub_measures:
continue
picked, sub_cost, sub_gain = run_optimizer(
sub_measures,
budget=budget, # no fixed items; budget unchanged
sub_target_gain=target_gain
)
if picked is None:
continue
solutions.append(
{
"fixed_ids": [],
"items": picked,
"total_cost": sub_cost,
"total_gain": sub_gain,
"path": path_spec,
}
)
# 1) expand fixed selections for this path
fixed_selections = expand_funding_path(input_measures, path_spec) if path_spec else [[]]
if not fixed_selections:
@ -1031,8 +1084,9 @@ def run_optimizer(input_measures, budget=None, sub_target_gain=None, allow_slack
else:
if sub_target_gain is None:
raise ValueError("Either budget or target_gain must be provided.")
opt = CostOptimiser(input_measures, min_gain=sub_target_gain)
opt = CostOptimiser(sub_measures, min_gain=sub_target_gain)
opt.setup()
opt.solve()
return opt.solution, opt.solution_cost, opt.solution_gain
cost = sum([x["cost"] for x in opt.solution])
return opt.solution, cost, opt.solution_gain