test: reflect ASHP winning more often after the Vaillant swap

The efficient Vaillant aroTHERM plus 5 kW (ADR-0025) now raises SAP even on gas
dwellings, so the Optimiser selects the ASHP bundle far more often -- a deliberate
product shift (user-confirmed). Updated the integration/harness tests:
- ARA multi-measure: ASHP is additive to the kept fabric package -> add to set.
- ARA listed_uprn: is_listed blocks walls AND ASHP (blocks_internal); the gate is
  now observable via ASHP (selected unrestricted, blocked listed).
- report three-measures: ASHP + solid-floor displace the fabric stack; assert
  their triggers (property_type, main_heating_category / floor).
- console hhr + solid_wall coverage tests: assert the measure is OFFERED as a
  candidate (the wiring/eligibility intent), since ASHP now out-competes them in
  selection; also assert the optimised package leads with ASHP.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-06 21:49:12 +00:00
parent b089bde1e6
commit 037daa98ef
3 changed files with 56 additions and 34 deletions

View file

@ -10,6 +10,8 @@ from datatypes.epc.domain.epc import Epc
from datatypes.epc.domain.epc_property_data import EpcPropertyData
from domain.geospatial.planning_restrictions import PlanningRestrictions
from domain.modelling.contingencies import contingency_rate
from domain.modelling.generators.heating_recommendation import recommend_heating
from domain.modelling.generators.solid_wall_recommendation import recommend_solid_wall
from harness.console import DEFAULT_CATALOGUE, run_modelling, run_one
from repositories.product.product_json_repository import ProductJsonRepository
from tests.domain.modelling._elmhurst_recommendation import (
@ -86,13 +88,21 @@ def test_run_modelling_recommends_solid_wall_insulation_for_solid_brick() -> Non
"solid_brick_ewi_001431_before.pdf"
)
# Act — Modelling only, no database.
plan = run_modelling(epc, goal_band="C", print_table=False)
# Assert — the solid-wall generator is wired in and OFFERS a solid-wall
# Option for this dwelling. Whether the Optimiser then selects it depends on
# the package: the efficient ASHP bundle (ADR-0025) now often reaches the
# band first, so we assert the Option is OFFERED (the wiring/eligibility
# intent) rather than selected.
recommendation = recommend_solid_wall(epc, ProductJsonRepository(DEFAULT_CATALOGUE))
assert recommendation is not None
assert {o.measure_type for o in recommendation.options} & {
"external_wall_insulation",
"internal_wall_insulation",
}
# Assert — the solid-wall generator is wired into the candidate pool, so a
# solid-wall Option reaches the optimised package.
measure_types = {measure.measure_type for measure in plan.measures}
assert measure_types & {"external_wall_insulation", "internal_wall_insulation"}
# And Modelling still produces a plan end to end (now ASHP-led).
plan = run_modelling(epc, goal_band="C", print_table=False)
assert len(plan.measures) >= 1
def test_run_modelling_listed_building_yields_no_wall_insulation() -> None:
@ -195,12 +205,19 @@ def test_run_modelling_recommends_hhr_storage_for_an_electric_dwelling() -> None
# heating generator is wired into the candidate pool (ADR-0024).
epc: EpcPropertyData = _electric_storage_lit_epc()
# Act — Modelling only, no database.
plan = run_modelling(epc, goal_band="C", print_table=False)
# Assert — the heating generator is wired in and OFFERS the HHR storage
# bundle for an electric dwelling. The Optimiser now selects the ASHP bundle
# instead — the efficient Vaillant (ADR-0025) beats resistance storage on SAP
# — so HHR is offered-but-not-selected; assert it is OFFERED to preserve the
# wiring check, and that the optimised package leads with ASHP.
recommendation = recommend_heating(epc, ProductJsonRepository(DEFAULT_CATALOGUE))
assert recommendation is not None
assert "high_heat_retention_storage_heaters" in {
o.measure_type for o in recommendation.options
}
# Assert — the HHR storage bundle reaches the optimised package.
measure_types = {measure.measure_type for measure in plan.measures}
assert "high_heat_retention_storage_heaters" in measure_types
plan = run_modelling(epc, goal_band="C", print_table=False)
assert "air_source_heat_pump" in {m.measure_type for m in plan.measures}
def test_sample_catalogue_prices_every_generator_measure_type() -> None:

View file

@ -77,33 +77,26 @@ def test_each_fired_measure_carries_the_attributes_that_triggered_it() -> None:
# Assert — the Plan ran and every fired measure names its trigger fields.
assert report.plan is not None
assert report.plan_error is None
# The efficient representative heat pump (Vaillant aroTHERM plus 5 kW,
# ADR-0025) raises SAP enough on this gas dwelling that ASHP + solid-floor
# insulation reach the target band on their own, displacing the fabric stack
# the Optimiser used to assemble (ADR-0024 — ASHP is now a strong candidate).
triggers: dict[str, MeasureTrigger] = _triggers_by_measure(report)
assert set(triggers) == {
"cavity_wall_insulation",
"mechanical_ventilation",
"air_source_heat_pump",
"solid_floor_insulation",
"low_energy_lighting",
}
# Cavity fill fired because the main wall is an uninsulated cavity.
assert triggers["cavity_wall_insulation"].triggers == {
"wall_construction": 4,
"wall_insulation_type": 4,
}
# Ventilation fired because the dwelling lodges no mechanical kind.
assert triggers["mechanical_ventilation"].triggers == {
"mechanical_ventilation_kind": None,
# ASHP fired because the dwelling is a non-flat house not already a heat pump
# (eligibility is physical/planning only — ADR-0024).
assert triggers["air_source_heat_pump"].triggers == {
"property_type": "0",
"main_heating_category": 2,
}
# Solid-floor insulation fired off an uninsulated solid ground floor.
assert triggers["solid_floor_insulation"].triggers == {
"floor_insulation_thickness": None,
"floor_construction_type": "Solid",
}
# The LED upgrade fired because the dwelling lodges 10 low-energy-unknown bulbs.
assert triggers["low_energy_lighting"].triggers == {
"incandescent_fixed_lighting_bulbs_count": 0,
"cfl_fixed_lighting_bulbs_count": None,
"low_energy_fixed_lighting_bulbs_count": 10,
}
def test_few_measure_cert_surfaces_only_its_fired_measures_triggers() -> None:

View file

@ -351,6 +351,10 @@ def test_modelling_optimises_and_persists_a_multi_measure_plan(
# The sample EPC lodges 8 low-energy-unknown bulbs, so the LED upgrade is
# a cheap positive-SAP candidate the Optimiser also keeps (ADR-0023).
"low_energy_lighting",
# The efficient representative heat pump (Vaillant aroTHERM plus 5 kW,
# ADR-0025) now raises SAP even on this gas dwelling, so the Optimiser
# also keeps the ASHP bundle in the least-cost-to-band package (ADR-0024).
"air_source_heat_pump",
}
# Each persisted measure carries the catalogue id of the Product it installs
# (the MaterialRow ids seeded above), replacing the retired
@ -641,16 +645,24 @@ def test_listed_uprn_ingested_blocks_solid_wall_insulation_in_modelling(
fuel_rates=FuelRatesStaticFileRepository(),
).run(property_ids=[40, 41], scenario_ids=[7], portfolio_id=1)
# Assert — the listed dwelling gets no wall insulation (both Options blocked);
# the unrestricted dwelling gets one. The only difference between them is the
# planning status Ingestion cached, proving the loop end to end (ADR-0019/0020).
_WALL_TYPES = {"external_wall_insulation", "internal_wall_insulation"}
# Assert — a listed building blocks the fabric-protected measures: both
# solid-wall Options AND the ASHP bundle (all gated on `blocks_internal`,
# ADR-0024). So the listed dwelling gets neither, while the unrestricted one
# gets the ASHP bundle (which the efficient Vaillant now makes the Optimiser
# select — ADR-0025, so walls are no longer needed to reach the band). The
# only difference between them is the planning status Ingestion cached,
# proving the gate end to end (ADR-0019/0020/0024).
_PROTECTED_TYPES = {
"external_wall_insulation",
"internal_wall_insulation",
"air_source_heat_pump",
}
with Session(db_engine) as session:
listed_types = _plan_measure_types(session, property_id=40)
unrestricted_types = _plan_measure_types(session, property_id=41)
assert _WALL_TYPES.isdisjoint(listed_types)
assert _WALL_TYPES & unrestricted_types
assert _PROTECTED_TYPES.isdisjoint(listed_types)
assert "air_source_heat_pump" in unrestricted_types
def _plan_measure_types(session: Session, *, property_id: int) -> set[str]: