Model/docs/adr/0027-boiler-and-tune-up-costing.md
Khalim Conn-Kowlessar ae7e6a0c42 feat(modelling): composite per-dwelling boiler + tune-up costing (ADR-0027)
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>
2026-06-10 19:41:06 +00:00

7.4 KiB
Raw Blame History

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 incrementalsmart_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_fixesjacket £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, and Products.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 its id, 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.json with 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.