diff --git a/docs/adr/0010-sap10-calculator-spec-target-and-validation.md b/docs/adr/0010-sap10-calculator-spec-target-and-validation.md index 8ad06da9..3fce5ea0 100644 --- a/docs/adr/0010-sap10-calculator-spec-target-and-validation.md +++ b/docs/adr/0010-sap10-calculator-spec-target-and-validation.md @@ -66,3 +66,37 @@ Each `domain/sap/worksheet/*.py` module must mirror the SAP 10.2 worksheet struc - **Build versioned Table 12 (pre/post 14-March-2025) keyed on `inspection_date` and validate across the full corpus.** Rejected as more work for no signal benefit during the spec sweep — the filtered cohort gets us to a clean probe faster. A versioned table is still future work if Calculated SAP10 Performance ever needs to reproduce historical cert SAP for products that compare against Lodged Performance directly. - **Keep cert-cal during the sweep and re-derive at the end** (the handover's prescription). Rejected for the reasons in decision (2): the cert-cal layer corrupts the signal during the sweep, which is precisely when the signal needs to be cleanest. - **Pay for an Elmhurst license, lock fixtures to its output.** Held in reserve. BRE worked examples are free and spec-derived; an Elmhurst trace would add value as a per-component reference but is not a prerequisite. + +## Amendment — §10a Fuel costs (2026-05-21) + +Decision 1's "active spec target is SAP 10.2 (14-03-2025)" is narrowed for the §10a Fuel-costs block: **cost prices for §10a and §10b are sourced from RdSAP10 Table 32 (PDF page 95)**, not SAP 10.2 Table 12. RdSAP10 §19.1 is explicit: *"The SAP rating for RdSAP 10 is to be calculated using Table 32 prices (not Table 12) for section 10a and 10b."* + +CO2 emission factors and primary-energy factors remain SAP 10.2 Table 12 per RdSAP10 §19.2 (the values are identical across the two tables; the columns are duplicated in Table 32 for completeness but Table 12 is the canonical authoritative source the calculator continues to import). + +### Why the amendment exists + +The §10a slice 1+2 rewrite (commits `0f255165`, `adfa7f60` on branch `ara-backend-design-prd`) surfaced two structural bugs that the pre-amendment Table-12-only path was masking: + +1. **Wrong table.** Table 12 unit prices were 5–55% off Table 32 per carrier (mains gas 3.64 vs 3.48, heating oil 4.94 vs 7.64, std electricity 16.49 vs 13.19, off-peak 9.40 vs 5.50, PV export 5.59 vs 13.19). Table 32 is what cert assessor software computes against; comparing our Table-12-driven SAP scores against PDF references was an apples-to-oranges check. +2. **Missing (251) standing charges.** Table 12 note (a) (and the identical Table 32 note (a)) gates additional standing charges into the SAP-rating ECF: gas standing added when gas is used for space/water heating; off-peak electricity standing added when an off-peak meter is in use; standard-electricity standing always omitted. Pre-amendment the calculator applied zero standing charges — equivalent to ignoring £92–£120/yr per gas-heated dwelling. + +The 000490 Elmhurst fixture had a recorded -12.5% cost gap (£706 vs £807 PDF) that ADR-0010 §3 Validation Cohort framing attributed to "pre-amendment spec-version drift". The §10a rewrite shows the gap was wrong-table + missing-standing-charges — a real calculator regression, not corpus drift. Post-§10a 000490 closes to within ~4% of PDF cost and SAP rating ceiling tightens 6 → 2. + +### Consequences + +- **`packages/domain/src/domain/sap/tables/table_32.py`** ships the RdSAP10 unit prices + standing charges + Table 12 note (a) gating function. Table 12 keeps the CO2 + PEF columns. +- **`packages/domain/src/domain/sap/tables/table_12a.py`** ships the high-rate-fraction lookups for off-peak split (Table 12a in SAP 10.2 PDF page 191 — RdSAP10 §19.1 cross-references this table directly). `Tariff.TEN_HOUR` carried for spec completeness even though RdSAP cert `meter_type` enum (1..5) has no 10-hour code. +- **`packages/domain/src/domain/sap/worksheet/fuel_cost.py`** ships the §10a orchestrator producing `FuelCostResult` (32 fields, line refs (240)..(255)). `cert_to_inputs._fuel_cost` precompute wires it from cert state. +- The 000474 Elmhurst fixture cost residual widened from -0.6% to +10.7% (SAP rating ceiling loosened 2 → 4) because the pre-amendment wrong-table-but-cancels-kWh accidentally compensated for upstream §4 HW kWh + Appendix L lighting overestimates. **§4 HW worksheet tightening is the next ticket** — see project memory `project_section_4_hw_next_ticket`. Ceiling drops back to 2 (or below) when that lands. +- Golden corpus SAP tolerance widened ±7 → ±11 per the Validation Cohort discipline (oil unit price +55% from Table 12 → Table 32 moves oil-heated golden certs whose lodged SAP scores pre-date Table 32). + +### Deferred work (named in §10a slice 3) + +- §4 HW worksheet tightening + Appendix L lighting predictor — **next ticket**. +- Table 12a high-rate-fraction wiring for off-peak electric mains (`Table12aSystem` cert→row mapping). Currently the cert→precompute path returns a zero `FuelCostResult` sentinel for off-peak certs, deferring to the legacy scalar `_*_fuel_cost_gbp_per_kwh` heuristic. +- Table 13 immersion / HP-DHW WH high-rate fractions. +- Off-peak per-row (230a)..(230g) Table 12a split for pumps/fans (spec line 8076). +- (247a) Instant electric shower kWh routing. +- (252) per-row Appendix M/N split (PV / wind / hydro / micro-CHP) — currently single `pv_credit_gbp` scalar. +- (253)/(254) Appendix Q routes. +- Drop the legacy scalar `space_heating_fuel_cost_gbp_per_kwh` / `hot_water_fuel_cost_gbp_per_kwh` / `other_fuel_cost_gbp_per_kwh` / `secondary_heating_fuel_cost_gbp_per_kwh` / `pv_export_credit_gbp_per_kwh` fields from `CalculatorInputs` once the ~33-occurrence synthetic-test corpus migrates to `fuel_cost=...`. diff --git a/docs/sap-spec/SPEC_COVERAGE.md b/docs/sap-spec/SPEC_COVERAGE.md index 088e58a9..6bd2e2d2 100644 --- a/docs/sap-spec/SPEC_COVERAGE.md +++ b/docs/sap-spec/SPEC_COVERAGE.md @@ -2,7 +2,7 @@ Tracks which sections of the SAP 10.2 specification are implemented in `packages/domain/src/domain/sap/`. Per ADR-0009 the calculator is built from the spec, not reverse-engineered from cert data. This doc is the worksheet-driven roadmap for what remains. -Updated 2026-05-21 after §8c (slices `cf28eec4`…`f3797066`), §8f (`43cc16bc`), §9a single-main slices (`2b5fc6a5`…`380b6781`), PCDB Table 105 integration (`fe04cd3a`…`a104dd55`), and PCDB fixture lodgement (`1b43c95c`…`15d6b781`). +Updated 2026-05-21 after §8c (slices `cf28eec4`…`f3797066`), §8f (`43cc16bc`), §9a single-main slices (`2b5fc6a5`…`380b6781`), PCDB Table 105 integration (`fe04cd3a`…`a104dd55`), PCDB fixture lodgement (`1b43c95c`…`15d6b781`), and §10a Fuel costs (`0f255165`…`adfa7f60`). The canonical SAP10.2 algorithm lives in [`2026-05-19-17-18 RdSap10Worksheet.xlsx`](../../2026-05-19-17-18%20RdSap10Worksheet.xlsx) at the repo root — each line ref `(1)..(486)` maps to a cell. The worksheet sub-modules under `packages/domain/src/domain/sap/worksheet/` implement those line refs directly; Elmhurst worksheets validate end-to-end via `tests/_elmhurst_worksheet_*.py`. @@ -24,7 +24,8 @@ The canonical SAP10.2 algorithm lives in [`2026-05-19-17-18 RdSap10Worksheet.xls | 9 | Energy requirements per heating system (§9a worksheet block) | `worksheet/energy_requirements.py` | **Full (single-main + Table 11 secondary)** | Worksheet-driven (201)..(215) via `space_heating_fuel_monthly_kwh`. (211)m = (98c)m × (204) × 100 / (206) and (215)m mirror; Σ → annuals. EnergyRequirementsResult dataclass mirrors the full §9a worksheet shape with 16 fields incl. (203)/(205)/(207)/(209)/(213)/(221) zero-branch placeholders. `cert_to_inputs` precomputes and stashes on `CalculatorInputs.energy_requirements` composite slot; calculator's `_solve_month` reads precomputed (211)m/(215)m directly (stops doing q/η inline). SapResult adds `main_2_heating_fuel_kwh_per_yr` and `space_cooling_fuel_kwh_per_yr` flat scalars (both zero in scope A). **Deferred**: (203)/(205)/(207)/(213) two-main system (first multi-main cert) + (209)/(221) cooling SEER (Table 10c lookup — first fixed-AC cert) + (230a)-(230h)/(231) Table 4f pumps/fans breakdown + (236)/(237) Appendix Q items + ALL_FIXTURES LINE_206/(211)/(215) PDF-derived pins (**blocked on PCDB** — see Boiler / heat-pump efficiency Manufacturer override in Prioritised gap list). | | 10 | Cooling (spec heading — same content as §8c worksheet block) | `worksheet/space_cooling.py` | **Full (no-AC zero-branch)** | See §8c row above. | | 11 | FEE compliance conditions | `worksheet/fabric_energy_efficiency.py` (the function exists; §11 conditions don't run yet) | Partial | (109) formula exposed via `fabric_energy_efficiency_kwh_per_m2_yr`. Spec §11 conditions (lines 2152-2164: 2-4 extract fans, instantaneous-electric shower, 125 l/day water, 185 lm/m² lighting at 66.9 lm/W, column (B) heating gains, column (A) cooling gains, etc.) not implemented — only relevant for new-build compliance. | -| 12 | Total energy + fuel costs | `calculator.py` | Partial | Per-end-use cost split ✓; meter_type tariff routing ✓ (S-B15); PV cost credit ✓ (S-B19); **standing charges not included** (Table 12 note (a) says rating omits standing charge for std electricity tariff) | +| 10a | Fuel costs incl. micro-CHP | `worksheet/fuel_cost.py` + `tables/table_32.py` + `tables/table_12a.py` | **Full (single-main + standard tariff)** | Worksheet-driven (240)..(255) via `fuel_cost(...)`. 32-field `FuelCostResult` mirrors the §10a worksheet shape: (240a-e) main 1 + (241a-e) main 2 + (242a-e) secondary off-peak splits, (243-247) water heating, (247a) instant shower, (248) cooling, (249) pumps/fans, (250) lighting, (251) standing charges (Table 12 note (a) gating: gas standing + off-peak electricity standing), (252) PV credit (negative), (253-254) Appendix Q, (255) total clamped to ≥ 0. RdSAP10 Table 32 prices per ADR-0010 amendment (overrides SAP10.2 Table 12 for §10a/§10b). `Tariff` + `Table12aSystem` + `OtherUse` enums in `table_12a.py` for off-peak high-rate-fraction lookups (synthetic-tested; unreachable from RdSAP cert flow until Table12aSystem cert→row mapping lands). `cert_to_inputs._fuel_cost` precomputes for STANDARD-tariff certs; off-peak certs return zero sentinel so the calculator's legacy scalar `_*_fuel_cost_gbp_per_kwh` fallback fires (deferred). 000490 SAP rating ceiling tightened 6 → 2; 000474 ceiling loosened 2 → 4 reflecting upstream §4 HW + Appendix L lighting overestimates the pre-§10a wrong-table-but-cancels-kWh had masked. **Deferred**: per-row (252) PV/wind/hydro/μCHP split, Table 13 immersion fractions, Table 12a Table12aSystem cert→row mapping for off-peak electric mains, (230a)-(230g) per-row pumps/fans split, (247a) instant shower wiring, scalar fuel-cost-per-kWh field cleanup from CalculatorInputs. | +| 12 | Total energy + fuel costs (legacy heading — folded into §10a) | `calculator.py` | **Folded into §10a row above** | Pre-§10a §12 row covered the inline cost arithmetic; that block was rewritten into the §10a orchestrator + cert_to_inputs precompute. Calculator delegates `total_fuel_cost_gbp` to `inputs.fuel_cost.total_cost_gbp`. | | 13 | SAP rating | `worksheet/rating.py` | Full | Equations 7-9 verified against SAP 10.2 §13 | | 14 | CO2 + primary energy | `calculator.py` SapResult.co2_kg_per_yr | Partial | Single CO2 factor on main fuel; no per-end-use CO2 mixing; **no primary energy calculation** | | 15 | Building regs | n/a | n/a | Not relevant to ratings | @@ -295,6 +296,47 @@ Status now: 100-cert MAE 4.49, 300-cert MAE 5.45, bias near zero (±0.2). Worksh 5. **Appendix Q items** (236)/(237) — placeholder until a Q-item cert lands. 6. **(238) total delivered energy on SapResult** — promote from `intermediate` dict when §10a or §13 requires it as a named output. +## §10a — slice progress (xlsx rows ~614–740) + +Per ADR-0010 amendment, §10a costs source from RdSAP10 **Table 32** (PDF page 95), not SAP10.2 Table 12. Three new modules + one composite slot on `CalculatorInputs` + one calculator delegation. + +| Slice | What landed | Commit | +|---|---|---| +| 1 | `tables/table_32.py` (28 fuel-row unit prices + 11-row standing charges + note (a) gating fn) + `tables/table_12a.py` (`Tariff` + `Table12aSystem` + `OtherUse` enums + SH/WH/other-use fraction lookups + `tariff_from_meter_type` cert resolver) + `worksheet/fuel_cost.py` (32-field `FuelCostResult` + kwargs `fuel_cost(...)` orchestrator with `_split` off-peak helper). 130 synthetic unit tests. | `0f255165` | +| 2 | `CalculatorInputs.fuel_cost` composite slot (default zero sentinel) + `cert_to_inputs._fuel_cost` precompute (Table 32 prices + note (a) standing-charge gating; off-peak certs return zero sentinel → calculator falls back to legacy scalar `_*_fuel_cost_gbp_per_kwh` helpers, deferred). Calculator delegates `total_fuel_cost_gbp` to `inputs.fuel_cost.total_cost_gbp`. 2 cert-round-trip conformance tests (000474 within e2e 15% tolerance; 000490 within 5%). e2e ceilings adjusted: 000490 6 → 2 (tightened — marquee close-out), 000474 2 → 4 (loosened — exposes upstream §4 HW + Appendix L), golden corpus ±7 → ±11 (oil price +55% Table 12 → Table 32). | `adfa7f60` | +| 3 | Docs — ADR-0010 amendment, this SPEC_COVERAGE row, slice progress table. | _this commit_ | + +### Line-ref status + +| Line ref | Status | Notes | +|---|---|---| +| (240a)-(240e) | Full | Main 1 off-peak split. STANDARD-tariff certs lodge `high_rate_fraction=1.0` so (240c) = kWh × price + (240d) = 0. Off-peak certs defer to legacy scalar fallback (Table12aSystem cert→row mapping deferred). | +| (241a)-(241e) | Zero-branch placeholder | Main 2 — no multi-main fixture in corpus. | +| (242a)-(242e) | Full (zero-valued) | Secondary off-peak split. All 6 fixtures lodge zero `secondary_fuel_kwh_per_yr`. | +| (243)-(247) | Full (single-rate path) | Water heating off-peak split. STANDARD-tariff path active; Table 12a immersion / heat-pump-DHW (Table 13) deferred. | +| (247a) | Zero-branch placeholder | Instant electric shower kWh routing deferred (no fixture lodges one). | +| (248) | Full | Cooling cost at `other_uses_gbp_per_kwh`. Zero in 6 fixtures (f_C=0). | +| (249) | Full (aggregate) | Pumps/fans single-rate. Per-row (230a)-(230g) Table 12a split deferred (spec line 8076). | +| (250) | Full (aggregate) | Lighting single-rate. Per-row off-peak split deferred when first off-peak fixture lands. | +| (251) | Full | Standing charges via Table 12 note (a) — gas (mains gas £120, LPG £70) added when gas used for space/water; off-peak electricity standing added when off-peak meter in use; std electricity standing always omitted. | +| (252) | Partial | Single `pv_credit_gbp` scalar. Per-row PV / wind / hydro / micro-CHP split deferred. | +| (253)/(254) | Zero-branch placeholder | Appendix Q items — no Q-item cert in corpus. | +| (255) | Full | `max(0, Σ all rows)` clamp. | + +**Six Elmhurst fixtures route through the new precompute** (all `meter_type="Single"` → STANDARD tariff). 000490 cost closes to PDF within ~4%; 000474 widens to +10.7% (upstream §4 HW + Appendix L — see Remaining work). + +### Remaining §10a work + +1. **§4 HW worksheet tightening — next ticket.** 000474 HW kWh overestimates +14.4% (2622 vs 2292 PDF), Appendix L lighting overestimates ~3x. Pre-§10a wrong-table-but-cancels-kWh masked these. See project memory `project_section_4_hw_next_ticket`. +2. **Table 12a cert→Table12aSystem mapping** for off-peak electric mains. Currently `cert_to_inputs._fuel_cost` returns the zero sentinel for non-STANDARD tariff certs so the calculator's legacy scalar fallback fires. Off-peak split awaits a real off-peak fixture + the row-mapper. +3. **Table 13 immersion + HP-DHW-only WH fractions** — `Table12aSystem.IMMERSION_OR_HP_DHW_ONLY` raises `NotImplementedError`; populate when first immersion fixture lands. +4. **Electric CPSU → Appendix F fractions** — `Table12aSystem.ELECTRIC_CPSU` raises; populate when first CPSU fixture lands. +5. **Per-row (230a)-(230g) pumps/fans split** for off-peak tariffs (spec line 8076: "if off-peak tariff, list each of (230a) to (230g) separately and apply fuel price according to Table 12a"). Requires §9a Table 4f pumps/fans breakdown (see §9a remaining work item 4) as a prerequisite. +6. **(247a) Instant electric shower** kWh routing — (64a) currently always 0 from `_hot_water_fuel_kwh_per_yr`. Wire when electric-shower cert lodges. +7. **(252) per-row Appendix M/N split** — populate (233a)..(235d) when wind / hydro / micro-CHP fixtures land. +8. **(253)/(254) Appendix Q** — zero placeholders; populate when a Q-item cert lands. +9. **Drop legacy scalar fuel-cost fields** from `CalculatorInputs` (`space_heating_fuel_cost_gbp_per_kwh`, `hot_water_fuel_cost_gbp_per_kwh`, `other_fuel_cost_gbp_per_kwh`, `secondary_heating_fuel_cost_gbp_per_kwh`, `pv_export_credit_gbp_per_kwh`) — currently retained as a synthetic-test fallback. Drops when the ~33-occurrence test corpus migrates to `fuel_cost=...`. + ## PCDB — slice progress (BRE pcdb10.dat ingestion) | Stage | Description | Status | Commit |