Replace the flat placeholder scalars (boiler £3000; tune-up £500/£900) with a per-dwelling composite cost, mirroring the ASHP architecture (ADR-0025): a `HeatingRates` table (data, `heating_rates.json`), typed `BoilerCostInputs` / `TuneUpCostInputs`, pure `Products.boiler_bundle_cost` / `tune_up_cost`, and modelling-layer interpreters that read the dwelling into those inputs. The cost mirrors the Simulation Overlay component-for-component, sharing the controls + cylinder pricing across both options: - tune-up (standard) = standard controls + cylinder fixes - tune-up (zone) = zone controls + cylinder fixes - boiler upgrade = £3200 all-in + standard controls (only when the upgrade fired a controls change) + cylinder fixes Standard controls are priced INCREMENTALLY — only the parts missing to reach SAP 2106 (programmer £120 / room thermostat £150 / TRV £35×radiators), read from a Table 4e Group-1 feature map so a dwelling that already has a room thermostat + TRVs is only charged the programmer. Zone controls are a full smart kit (hub £205 + smart TRV £50×radiators) — the smart TRV is itself the room sensor, so there is no separate per-room sensor line. Cylinder fixes: jacket £50 (when under-insulated) + thermostat £150 (when absent). The boiler is a like-for-like wet swap (no radiators/flue/pipework — eligibility already requires an existing wet boiler), so those dead-code extras are not modelled. Figures are research-validated 2025/26 UK installed costs (legacy Costs.py lineage); fully-loaded totals with one contingency on top (Model B, not the legacy VAT/preliminaries engine). Contingency: boiler 0.26; tune-ups 0.10 (was a 0.15 placeholder). ADR-0027 records the design; CONTEXT.md's Heating Eligibility entry updated to cover the partial boiler/tune-up family + composed cost. Products cost pins (delta<=1e-9) + interpreter tests + generator composite-cost assertions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
7.4 KiB
Boiler-Upgrade & System-Tune-Up Costing — Composite, Component-Mirrored
The boiler-upgrade and system-tune-up Options (the Heating & Hot Water expansion to ADR-0024) currently carry flat placeholder catalogue scalars (boiler £3000; tune-up £500/£900). Like the ASHP bundle (ADR-0025), their real cost varies per dwelling — a tune-up that only fits TRVs on a dwelling that already has a programmer + room thermostat costs a fraction of a full controls-from-scratch job, and the Optimiser picks least-cost-to-band, so a flat number mis-ranks them against each other and against ASHP/HHRSH. We therefore compose each cost per dwelling, mirroring ADR-0025's architecture and reusing the (research-validated) legacy recommendations/Costs.py figures as the rate source.
Decision
The cost mirrors the Simulation Overlay component-for-component. The overlay is the source of truth for what is installed; the cost prices exactly those components, using the same conditional predicates the overlay uses to decide what to write. This guarantees cost↔overlay consistency — we never charge for a thermostat the overlay didn't add, nor omit controls the boiler did upgrade — and keeps "one Option = one composite Plan line" honest.
tune-up (standard) = standard_controls + cylinder_fixes
tune-up (zone) = zone_controls + cylinder_fixes
boiler upgrade = boiler(£3200 all-in)
+ standard_controls (only when the boiler fired a controls upgrade)
+ cylinder_fixes (only when a cylinder is present)
standard_controls and cylinder_fixes are shared between boiler and tune-up.
- standard_controls = incremental — price only the parts missing to reach SAP code 2106, read from a Table 4e Group-1 →
(has_programmer, has_room_thermostat, has_TRVs)feature map:programmer £120+room_thermostat £150+TRV £35 × radiators, each charged only when absent. (A 7-radiator dwelling from "no controls" = £515, matching the Energy Saving Trust's ~£550 quoted figure; the £35 TRV is the marginal in-bundle rate, since the drain-down labour is shared once.) - zone_controls = full smart-zone kit, not incremental —
smart_thermostat hub £205+smart_TRV £50 × radiators. A smart zone system replaces whatever's there, and the smart TRV is itself the per-room sensor — so there is no separate per-room temperature-sensor line (the legacy double-counted it; corrected after research). - cylinder_fixes —
jacket £50(when under-insulated) +cylinder_thermostat £150(when absent), each conditional, and only when a cylinder exists. - boiler = £3200 all-in (condensing gas boiler, flue + labour included). No system-change extras (radiators / separate flue / pipework): boiler-upgrade eligibility already requires an existing wet boiler (SAP code 101-141 / 151-161, electric 191-196 excluded, mains gas present), so every upgrade is a like-for-like swap that reuses the existing wet distribution — the dry→wet conversion lines can never fire under the gate, so they are not modelled.
Products owns the catalogue math; the modelling layer owns the dwelling interpretation (ADR-0025 unchanged). New Products.boiler_bundle_cost(BoilerCostInputs) and tune_up_cost(TuneUpCostInputs) sum the applicable lines from a HeatingRates table (data, loaded from heating_rates.json) into a Cost, staying free of EpcPropertyData / Sap10Calculator. The modelling-layer interpreters read the dwelling (radiator count via the existing _radiator_count proxy; existing control features from the SAP control code; cylinder insulation/thermostat state) into those typed inputs. Per-radiator items (TRVs, smart TRVs) scale on _radiator_count; everything else is fixed per dwelling.
Fully-loaded totals, separate contingency (Model B — the ADR-0025 shape, not the legacy VAT/preliminaries engine). The legacy per-item £ figures are reused as fully-loaded rates and summed; one contingency_rate is applied on top (boiler 0.26; both tune-ups 0.10, per legacy Costs.CONTINGENCIES). The legacy's separate VAT-on-labour / preliminaries arithmetic is not reproduced — the cost exists for Optimiser ranking, where those scale near-uniformly and don't change the order, and the per-item figures are themselves estimates, so sub-£100 tax precision is false fidelity.
Rate table (8 lines, research-validated 2025/26 UK installed figures):
| line | £ | driver |
|---|---|---|
| programmer | 120 | fixed |
| room_thermostat | 150 | fixed |
| trv_per_radiator | 35 | per radiator |
| zone_hub (smart thermostat) | 205 | fixed |
| smart_trv_per_radiator | 50 | per radiator |
| cylinder_thermostat | 150 | fixed |
| cylinder_jacket | 50 | fixed |
| boiler | 3200 | fixed |
Considered options
- Replicate the legacy VAT/labour/preliminaries arithmetic exactly. Rejected: re-introduces a tax engine ADR-0025 deliberately avoided; false precision over rough estimates; ranking is insensitive to near-uniform tax/preliminaries.
- Flat catalogue scalar (the placeholder). Rejected: a tune-up's cost varies ~£200–£900 with what's already fitted and the radiator count; a flat number mis-ranks it against the boiler upgrade and ASHP.
- Price controls as a flat job (no per-radiator term). Rejected after research: TRVs and smart TRVs are genuinely per-radiator; a flat job over- or under-charges with dwelling size, and the per-radiator marginal rate is what makes the bundle sum match the EST reference.
- Keep the legacy zone-control build-up (per-room sensor + per-radiator smart TRV). Rejected after research: the smart TRV is the room sensor in real multi-zone systems (Tado/Wiser/evohome); the separate sensor line double-counts.
- Keep the dry→wet system-change extras for robustness. Rejected: dead code under the eligibility gate (existing wet boiler required); ADR-0025 likewise declined to price extras the data path can't reach.
- Boiler cost stays boiler-only; controls/cylinder priced as separate measures. Rejected: they're folded into the one Option's overlay, so pricing them separately would split one Plan line and risk double-charging against a tune-up.
Consequences
- New
HeatingRates+heating_rates.json,BoilerCostInputs/TuneUpCostInputs, andProducts.boiler_bundle_cost/tune_up_cost; the boiler/tune-up Options swap their flat scalar for the composite (the catalogue row is still read for itsid, as ASHP does). Contingencies for the two tune-up types drop 0.15 → 0.10 to match the legacy reference. - A new Table 4e Group-1 control-feature map lives in the modelling interpreter — the single place that reads "what controls does this dwelling already have" from a SAP code. An unrecognised/absent control code defaults to "no parts present" (charge the full standard kit) — conservative, and the standard option is only offered when the control is improvable anyway.
- The figures are research-validated installed UK estimates, not a contractor rate sheet (unlike the ASHP Southern Housing lines). When a real boiler/controls rate sheet arrives it replaces
heating_rates.jsonwith no code change — the rates are data. - Cost↔overlay consistency is structural: both read the same cylinder/control predicates, so they cannot drift (e.g. the overlay adding a thermostat the cost forgot).
- All dwelling reads (radiator count, existing control parts, cylinder state) are whole-dwelling proxies standing in for a survey, documented at each call site.