Model/tests/domain/modelling
Jun-te Kim f5fbeb8e7e Fix HHRSH storage heaters priced at peak rate (engine 48 vs Elmhurst 71)
The high-heat-retention storage-heater (HHRSH) recommendation was computed
as LOWERING SAP and wrongly rejected by the Optimiser, leaving electric-
room-heater flats/maisonettes stuck at band E with no sensible plan.

Root cause: `_HHR_STORAGE_OVERLAY` sets `sap_main_heating_code=409` (HHR
storage) + `meter_type="Dual"` but left `main_heating_category` at the
pre-existing value (10 = electric room heaters). The SAP 10.2 Table 12a
space-heating high-rate-fraction resolver classifies by category, so it
misread the off-peak storage heating as direct-acting electric (1.00
high-rate) and priced 100% at the 7-hour peak rate (15.29p) instead of the
storage off-peak split (0.00, ~5.5p).

Verified against accredited Elmhurst RdSAP-10 on UPRN 100020942571
(8 Booth Close, SE28 8BW): engine HHRSH SAP 48 (peak-priced, cost £1087)
vs Elmhurst 71 (cost ~£611); with the fix the engine gives 73 / £576.

Two-part fix (defence in depth, so no future overlay/mapper can
reintroduce it):
- `_HHR_STORAGE_OVERLAY` now sets `main_heating_category=7`, keeping the
  overlaid EPC internally consistent.
- `_table_12a_system_for_main` now classifies electric storage heaters by
  their Table 4a SAP code (401-409) as well as `category==7`, checked
  before the room-heater branch, so a specific storage code is
  authoritative over a stale/inconsistent category.

Tests: new regression `test_storage_heater_classified_by_sap_code_over_stale_category`;
updated the HHRSH overlay pin. Full accuracy corpus still passes (no regressions).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 11:24:03 +00:00
..
fixtures feat(modelling): system tune-up options (standard + zone controls) 2026-06-10 10:20:46 +00:00
__init__.py feat(modelling): Overlay Applicator folds EpcSimulation onto EpcPropertyData 2026-06-02 22:13:51 +00:00
_elmhurst_recommendation.py test(modelling): Elmhurst before/after cascade pin for cavity wall (#1154) 2026-06-03 09:36:53 +00:00
test_ashp_cost_inputs.py rename mains_gas to gas_connection_available 2026-07-01 10:50:58 +00:00
test_considered_measures.py feat(modelling): drive measure scoping from the Scenario's exclusions 2026-06-16 15:26:25 +00:00
test_elmhurst_cascade_pins.py modelling: glazing overlay models draught-proofing + frame-factor re-lodge 2026-06-24 08:25:08 +00:00
test_floor_recommendation.py refactor(modelling): group domain/modelling into generators/scoring/optimisation 2026-06-03 13:48:36 +00:00
test_glazing_recommendation.py modelling: glazing overlay models draught-proofing + frame-factor re-lodge 2026-06-24 08:25:08 +00:00
test_heating_cost_inputs.py feat(modelling): composite per-dwelling boiler + tune-up costing (ADR-0027) 2026-06-10 19:41:06 +00:00
test_heating_recommendation.py Fix HHRSH storage heaters priced at peak rate (engine 48 vs Elmhurst 71) 2026-07-02 11:24:03 +00:00
test_lighting_recommendation.py feat(modelling): recommend_lighting upgrades all non-LED bulbs to LED 2026-06-05 12:23:27 +00:00
test_measure_dependency.py refactor(modelling): ventilation_dependency delegates to the generator + wraps 2026-06-03 14:04:17 +00:00
test_measure_type.py feat(modelling): secondary-heating-removal generator + MeasureType (ADR-0028) 2026-06-11 13:35:14 +00:00
test_optimiser.py refactor(modelling): type measure_type fields as MeasureType 2026-06-08 20:13:31 +00:00
test_overlay_applicator.py Rename SapEnergySource.mains_gas to gas_connection_available 🟩 2026-07-01 10:59:21 +00:00
test_package_scorer.py feat(modelling): Score carries the scored SapResult for billing 2026-06-03 17:20:45 +00:00
test_plan.py refactor(modelling): type measure_type fields as MeasureType 2026-06-08 20:13:31 +00:00
test_plan_valuation.py feat(modelling): wire Valuation Uplift onto the Plan 2026-06-04 08:59:04 +00:00
test_products.py feat(modelling): composite per-dwelling boiler + tune-up costing (ADR-0027) 2026-06-10 19:41:06 +00:00
test_roof_recommendation.py feat(modelling): room-in-roof safety guard defers the roof generator 2026-06-05 10:06:08 +00:00
test_scenario.py feat(modelling): drive measure scoping from the Scenario's exclusions 2026-06-16 15:26:25 +00:00
test_scoring.py refactor(modelling): type measure_type fields as MeasureType 2026-06-08 20:13:31 +00:00
test_secondary_heating_recommendation.py feat(modelling): secondary-heating-removal generator + MeasureType (ADR-0028) 2026-06-11 13:35:14 +00:00
test_solar_config_selection.py Harden Dwelling-Roof Cap on real data: positional segments, ground-floor basis 🟩 2026-06-26 12:24:52 +00:00
test_solar_overshading.py Treat solar block without array sizing as no-solar (fix KeyError) 2026-06-24 10:22:06 +00:00
test_solar_potential.py Harden Dwelling-Roof Cap on real data: positional segments, ground-floor basis 🟩 2026-06-26 12:24:52 +00:00
test_solar_products.py feat(modelling): Products.solar_bundle_cost + committed solar rate sheet 2026-06-08 12:10:27 +00:00
test_solar_recommendation.py fix(pv): credit PV only when connected to the dwelling's meter 🟩 2026-06-29 13:40:51 +00:00
test_valuation.py feat(modelling): ValuationUplift domain class (percentage-primary) 2026-06-04 08:33:19 +00:00
test_ventilation_recommendation.py feat(modelling): ventilation Recommendation Generator (detect + price) 2026-06-03 14:01:14 +00:00
test_wall_recommendation.py feat(modelling): explicit park-home guard in the solid-wall generator 2026-06-05 10:49:50 +00:00
test_window_extraction_001431.py fix(extractor): capture all 17 openable §11 windows on cert 001431 2026-06-05 14:33:25 +00:00