Adds §9a as a first-class row (consistent with §8c/§8f sub-section precedent). The §9 row updates from "Partial — single main only, no Table 11 secondary" to "Full (single-main + Table 11 secondary)" with a deferred list naming the four remaining slices: two-main system, cooling SEER, Table 4f pumps/fans breakdown, Appendix Q. The PCDB gap-list entry (item 1) updates to flag §9a ALL_FIXTURES PDF-derived LINE_206/(211)/(215) pinning as blocked. The 88.2% figure that surfaced from a previous agent's notes cannot be verified without PCDB — corrected the narrative accordingly. Per-§9a slice progress table mirrors §8c/§8f structure with line refs (201)..(238), commit shorthands, and a Remaining work list naming six follow-ups (PCDB integration, two-main, cooling SEER, Table 4f, Appendix Q, (238) on SapResult). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
34 KiB
SAP 10.2 / RdSAP 10 Coverage Map
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), and §9a single-main slices (2b5fc6a5…380b6781).
The canonical SAP10.2 algorithm lives in 2026-05-19-17-18 RdSap10Worksheet.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.
Sections §§1–13 (the SAP worksheet)
| § | xlsx rows | Topic | Module | Status | Notes / gaps |
|---|---|---|---|---|---|
| 1 | 1–120 (approx) | Dimensions | worksheet/dimensions.py |
Full | Porches, conservatories, RIR deferred per ADR-0009 |
| 2 | 121–206 (approx) | Ventilation | worksheet/ventilation.py |
Partial | No mechanical ventilation (MVHR/MEV), no wind-shelter factor, no pressure-test override (worksheet lines 17-18), no AP4 override (worksheet line 19) |
| 3 | 121–207 | Heat transmission | worksheet/heat_transmission.py |
Full (non-RR) | LINE_31/33/36/37 exact for both non-RR Elmhurst fixtures (000474, 000490). Suspended-timber + Table 20 exposed-floor routes wired. RR sub-areas (gable/slope/stud-wall) deferred until SapRoomInRoof carries them. Global y-factor (Table R2 per-junction deferred). |
| 4 | 207–304 | Hot water + Appendix J | worksheet/water_heating.py (new) + legacy domain.ml.demand.predicted_hot_water_kwh (calculator still calls this) |
Happy-path done — see slice progress below. Worksheet-driven module lands line refs (42)..(65) for the combi-gas-boiler population (~70% of corpus). Cylinder + solar + renewables branches deferred. Calculator.py not yet wired to new module — next step. | |
| 5 | Internal gains + Appendix L | worksheet/internal_gains.py |
Full | Worksheet-driven (66)..(73), Table 5 Column A, Table 5a 9-row dispatch + heating-season mask, Appendix L L1-L12 with RdSAP §12-1 bulb defaults + Table 6d Z_L (light access factor). Wired into calculator.py via cert_to_inputs. Six Elmhurst fixtures conform end-to-end to ≤0.6% lighting / ≤0.2 W (73). |
|
| 6 | Solar gains + Tables 6b/6c/6d + Appendix U | worksheet/solar_gains.py |
Full | Worksheet-driven (74)..(83). Table 6b g⊥ via manufacturer window_transmission_details first, Table 6b code lookup fallback; Table 6c FF by frame_material substring; Table 6d Z (heating column) by OvershadingCategory; roof windows pitched at RdSAP10 Table 24 default 45°; rooflights horizontal per §U3.2 p128. solar_gains_from_cert wired into cert_to_inputs + calculator.py. Six Elmhurst fixtures conform end-to-end to ≤5e-3 W on (83) + (84). |
|
| 7 | Mean internal temperature | worksheet/mean_internal_temperature.py |
Full | Worksheet-driven (85)..(94) via mean_internal_temperature_monthly. Table 9c steps 1-9 sequential (per-zone η: (86) η_living at Ti=T_h1, (89) η_elsewhere at Ti=T_h2, (94) η_whole at Ti=(93)). Table 9b u-formula consumes weighted R for two-main case 1 (single-main is default). Wired into calculator.py + cert_to_inputs via two new CalculatorInputs fields. Six Elmhurst fixtures conform end-to-end to ≤5e-3 °C on all 9 line tuples + 2 scalars per month (588 assertions). Table 4e adj defaults 0 (cert-side mapping deferred — all 6 fixtures = 0); two-main case 2 (different parts heated separately) deferred. |
|
| 8 | Off-period temperature reduction | inline in mean_internal_temperature.py |
Full | Table 9b implemented | |
| 8 | Space heating requirement | worksheet/space_heating.py |
Full | Worksheet-driven (95)..(99) via space_heating_monthly_kwh. Includes the Table 9c step 10 spec inclusion rule (Jun..Sep zeroed) on top of the < 1 kWh value clamp. (98b) Appendix H solar space heating defaulted to 0 (no Elmhurst fixture lodges a solar space heating system). Wired into calculator.py + cert_to_inputs via CalculatorInputs.space_heating_monthly_kwh. Six Elmhurst fixtures conform end-to-end on (95)/(97)/(98a)/(98c)/(99)/annual @ 5e-2..1e-1 kWh (looser than §6/§7's 5e-3 because LINE_84/93/94 fixture pins are 4-d.p. display-rounded and §8's 0.024·n_m·(L−ηG) amplifies that rounding). |
|
| 8c | Space cooling requirement | worksheet/space_cooling.py |
Full (no-AC zero-branch) | Worksheet-driven (100)..(108) via space_cooling_monthly_kwh. Table 10a η_loss with 8-dp γ rounding + L=0 sentinel; Table 10b Q_cool with Jun-Aug inclusion mask + post-f_C × f_intermittent 1-kWh clamp per spec line 10321. Internal temp hardcoded 24 °C. Wired into calculator.py (MonthlyEntry.space_cool_requirement_kwh + SapResult.space_cooling_kwh_per_yr) + cert_to_inputs via CalculatorInputs.space_cooling_monthly_kwh. Six Elmhurst fixtures all has_fixed_air_conditioning=False → (107), (108) ≡ 0 — ALL_FIXTURES asserts (101)/(103)/(106)/(107)/(108) per fixture; synthetic positive test (γ=1 closed-form) covers the algebra. Deferred: RdSAP cooled-area defaulting rule + cooling_gains_from_cert (drops Table 5a items per spec 10280) + Table 10c SEER → cooling fuel kWh + fuel cost cascade (first non-zero-cooling cert triggers this slice). |
|
| 8f | Fabric Energy Efficiency (line ref (109)) | worksheet/fabric_energy_efficiency.py |
Full (rating-conditions transparency) | Spec line 7898: (109) = (98a) ÷ (4) + (108). fabric_energy_efficiency_kwh_per_m2_yr(...) is a single-scalar free function — no dataclass. Σ(98a) (pre Appendix H solar) added as SpaceHeatingResult.space_heating_requirement_kwh_per_yr so spec literal is honoured; for our corpus (98b)=0 so Σ(98a) = Σ(98c). cert_to_inputs precomputes FEE from local SpaceHeatingResult + SpaceCoolingResult; calculator passes through to SapResult.fabric_energy_efficiency_kwh_per_m2_yr. Six Elmhurst fixtures all (108)=0 → LINE_109 = LINE_99 exactly. §11 compliance conditions (different ventilation / HW / lighting / gains column) are deferred — current FEE is a rating-conditions transparency output, not a §11 compliance figure. Future §11 slice invokes the same function with §11-conditions upstream values. |
|
| 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) | |
| 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 |
Appendices
| Appendix | Topic | Status | Notes |
|---|---|---|---|
| A | Main + secondary heating identification | Not implemented | First main only; Table 11 secondary fraction missing — high-value gap |
| B | Gas/oil boilers + boiler interlock | Partial | Via Table 4b in domain.ml.sap_efficiencies.seasonal_efficiency |
| C | Community heating | Not implemented | Heat network certs handled by Table 4a codes 301-304 (efficiency only) |
| D | PCDB | Stubbed | Per ADR-0009 grill: NoOpPcdbLookup returning None; Session C bundles a CSV PCDB extract |
| E | Heat pumps | Partial | Category fallback to 2.30 for cat=4; no MCS installation factor (×1.39 GSHP); no flow temp adjustment |
| F | Electric CPSU | Not implemented | Rare |
| G | FGHRS / WWHRS / PV-diverter | Not implemented | Rare |
| H | Solar water heating | Partial | Boolean has_solar_water_heating reduces HW by 250 kWh; no actual collector calc |
| J | Hot water demand | Partial | See §4 row above |
| K | Thermal bridging | Partial | Using global y per age band (per ADR-0009 grill: per-junction Table R2 deferred) |
| L | Lighting | Full | Existing-dwelling fallback ✓ |
| M | PV / wind / hydro generation | Partial | PV ✓ (S-B19); wind / hydro / micro-CHP not implemented |
| N | Micro-CHP | Not implemented | Rare |
| P | Electric storage heaters detail | Partial | Identified via codes 401-409; Table 12a high-rate fractions not exact (we use 100% off-peak per cert-calibration heuristic) |
| Q | Special features | Not implemented | One-off energy-use additions; rare |
| R | Reference dwelling | Not implemented | Only needed for compliance, not ratings |
| S | RdSAP procedures | Separate PDF (114pp) | Cert→inputs mapper in domain.sap.rdsap.cert_to_inputs ports key rules; many sections of RdSAP 10 not yet read |
| T | Improvement measures | Not relevant | Recommendation engine, not rating |
| U | Climate data | Full | Tables U1/U2/U3 + solar declination + Table U5 k1-k9 |
Prioritised gap list (by likely MAE impact)
- Boiler / heat-pump efficiency Manufacturer override (PCDB integration) —
MainHeatingDetaillodges the PCDB pointer (main_heating_index_number) but no scalar efficiency. WithNoOpPcdbLookup(ADR-0009 grill outcome #1) still in place,cert_to_inputsfalls back to the SAP10 Table 4a category default (typically 0.80 for gas boilers, SCOP 2.30 for heat pumps) on every cert. Per ADR-0010 §4 this accounts for ~19 SAP points of MAE on heat-pump certs and most per-cert variance on the 78 % of gas-boiler certs lodgingmain_heating_data_source=1(PCDB-typical 0.88–0.94 vs 0.80 default). Directly visible on 000490 e2e:inputs.main_heating_efficiency = 0.80vs an unverified PDF Manufacturer-declared figure that surfaced from a previous agent's notes — the actual PDF value cannot be confirmed without PCDB. Closing requires a real PCDB CSV ingest +PcdbLookupProtocol impl + precedence wiring incert_to_inputs._main_heating_efficiencyand_water_efficiency_with_category_inherit. Promoted to prerequisite under ADR-0010, not a section-sweep slice. §9a ALL_FIXTURES PDF-derived LINE_206/(211)/(215) pinning is blocked on this — until PCDB lands, §9a conformance is at the synthetic + cert-round-trip level only (no PDF cross-check). - Table 11 Secondary heating allocation — most boiler-main certs allocate 10% of space heating to a secondary system (often a less-efficient room heater on a different fuel). We model 0%. Likely +1-2 SAP-point bias on affected certs.
- Wind-shelter factor on infiltration (§2 worksheet lines 19-21) — multiplies infiltration by
1 - 0.075 × sheltered_sides. We have no shelter input; assume 2 sheltered sides default. Net effect on infiltration ACH probably ~10%. - Table 12a high-rate fraction for off-peak dwellings — we currently bill 100% of E7 space heating at the low rate. Real spec says e.g. heat pumps on 7h tariff at 80% high-rate. Affects ~5% of certs.
- Cylinder-loss factor cascade — currently uses simplified buckets in
domain.ml.demand._STORAGE_LOSS_FACTOR. Spec has more precise interpolation rules from cylinder volume + insulation thickness. - Standing charges in cost — Table 12 note (a) gives the rule for when standing charges are included (energy use vs rating). May affect bias.
- Per-junction thermal bridging (Table R2) — only relevant when assessor lodged junction-count data, otherwise global y is the spec answer for RdSAP-driven assessments.
Status now: 100-cert MAE 4.49, 300-cert MAE 5.45, bias near zero (±0.2). Worksheet-driven phase begins with Secondary heating Table 11 as the next slice.
§4 — slice progress (xlsx rows 207–304)
| Line ref | Description | Status | Commit |
|---|---|---|---|
| (42) | Assumed occupancy N from Appendix J | ✅ | aff678e8 |
| (42a)m | Mixer showers monthly | ✅ | 1dcbdb28 |
| (42b)m | Baths monthly | ✅ | dad7fbf3 |
| (42c)m | Other uses monthly | ✅ | 5cc68ab3 |
| (43) | Annual avg | ✅ | 702b1c6c |
| (44)m | Daily monthly total | ✅ | 702b1c6c |
| (45)m | Energy content | ✅ | a3c687f1 |
| (46)m | Distribution loss | ✅ | a3c687f1 |
| (47)–(56) | Storage / HIU loss | Combi zero-branch only; cylinder paths deferred | — |
| (57)m | Dedicated solar storage | Combi zero-branch only; solar HW deferred | — |
| (59)m | Primary loss | Combi zero-branch only; boiler+cylinder deferred | — |
| (61)m | Combi loss | Table 3a row "time-clock keep-hot" only | bfba610b |
| (62)m | Total demand | ✅ | bfba610b |
| (63a-d) | WWHRS/PV/Solar/FGHRS reductions | Zero-only — non-zero paths deferred | feef8198 |
| (64)m | Output from water heater | ✅ (with max-clamp) | feef8198 |
| (64a)m | Electric shower energy | Zero-only — non-zero path deferred | — |
| (65)m | Heat gains | ✅ | 43da3ea0 |
Happy path closes both non-RR Elmhurst fixtures to <1e-3 kWh end-to-end on lines (42)..(65) (000474's PCDB-backed Table 3b combi loss is the one hand-lodged value).
Remaining §4 work
- Orchestrator
water_heating_from_cert(epc, ...)that wires the leaf functions, mirroringheat_transmission_from_certfor §3. - Integrate with
calculator.py— replace the legacydomain.ml.demand.predicted_hot_water_kwhcall. - End-to-end SAP score validation against the worksheets — the real e2e test the user has been asking for.
- Cylinder + solar + renewables paths for full population coverage (cylinders, FGHRS, WWHRS, PV-diverter).
- PCDB-backed Table 3b/3c combi loss for tested boilers (000474 sits here).
§5 — slice progress (xlsx rows 305–332)
| Line ref | Description | Status | Commit |
|---|---|---|---|
| (66)m | Metabolic = 60 × N (Table 5 Col A) | ✅ | 3ec56216 |
| (71)m | Losses = -40 × N | ✅ | 021f43ba |
| (69)m | Cooking = 35 + 7 × N | ✅ | 984a5b18 |
| (72)m | Water heating bridge (65)m × 1000/(24 × n_m) | ✅ | a4d6321f |
| (68)m | Appliances (L13/L14/L16a) | ✅ | 0bc9eac3 |
| (67)m | Lighting (L1-L12 with Z_L from Table 6d) | ✅ | 50fd940a |
| (70)m | Pumps + fans (Table 5a 9-row dispatch) | ✅ | f77229e4 |
| (73)m | Total internal gains + InternalGainsResult | ✅ | 53aba133 |
| — | internal_gains_from_cert orchestrator |
✅ | f81e744b |
| — | 6-fixture ALL_FIXTURES conformance | ✅ | 99e5c2cd |
| — | cert_to_inputs wiring + calculator.py wiring |
✅ | bf6a7e04 |
| — | Rooflight Z_L=1.0 (Table 6d note 2) | ✅ | 380115e2 |
Six Elmhurst fixtures conform end-to-end on §5 to ≤5e-3 W on every line — display-rounding territory across the board.
Remaining §5 work
- Table 5a fan / PIV / HIU branches are implemented as leaf functions but not yet derived from cert inputs in
internal_gains_from_cert. Pump dispatch is wired; non-pump rows always emit 0 W. MVHR / MEV correctly produces zero gain. - Per-window
frame_material/glazing_typestrings — current orchestrator uses cert numeric codes; needs a robust mapping for site-notes-sourced certs. - Rooflight derivation from cert — orchestrator accepts
rooflight_total_area_m2butcert_to_inputsdoesn't yet detect rooflights (defaults to 0). Needs theSapWindow.window_locationenum that's TODO'd in the domain model. - Column B reduced-gain forms (L12a / L16) for new-build DPER/TPER calculations — deferred until we onboard a new-build cert.
§6 — slice progress (xlsx rows 332–371)
| Line ref | Description | Status | Commit |
|---|---|---|---|
| — | Table 6d Z-solar lookup (winter / heating column) | ✅ | da5909de |
| (74)..(81) | Per-orientation solar gain (N..NW) | ✅ | 4b83e702 |
| (82) | Roof windows — RdSAP10 Table 24 pitch 45° default, Z=1.0 | ✅ | 4b83e702 |
| (82a) | Rooflights — SAP10.2 §U3.2 horizontal, Z=1.0 | ✅ | 4b83e702 |
| (83) | Total solar gains 12-tuple | ✅ | 4b83e702 |
| — | RoofWindowInput + RooflightInput + SolarGainsResult |
✅ | 4b83e702 |
| — | solar_gains_from_cert orchestrator |
✅ | 4b83e702 |
| — | Manufacturer window_transmission_details cascade |
✅ | d56fef4d |
| — | 6-fixture ALL_FIXTURES conformance (83) + (84) ≤5e-3 W | ✅ | 377caea2 |
| — | CalculatorInputs.solar_gains_monthly_w + per-month lookup |
✅ | 376cdb6b |
| — | cert_to_inputs swap legacy _solar_gains_w → orchestrator |
✅ | cd2bd9ce |
| — | Delete _solar_gains_w + WindowInput + _window_inputs |
✅ | a0ce45c9 |
Six Elmhurst fixtures conform end-to-end on §6 to ≤5e-3 W on every month of (83) total solar gains and (84) total gains (the latter cross-checks §5 LINE_73 plus §6 LINE_83). 144 monthly assertions across 6 fixtures, all GREEN.
Remaining §6 work
- Rooflight derivation from cert —
cert_to_inputspassesrooflights=()because Elmhurst summaries lodge rooflights aswindow_location = "External wall"; needs theSapWindow.window_locationenum work (also blocking §5 rooflight detection — common ticket). - Roof window detection from cert — same constraint as rooflights; orchestrator-side support is there (
RoofWindowInputwith pitch default), but the cert→inputs mapper has no signal to populate it. - Multi-window-type per orientation — orchestrator sums all windows of one orientation regardless of glazing type, matching the Elmhurst worksheet's per-orientation Σ. The xlsx repeats (74)..(82a) blocks per "window type" for diagnostic display; we collapse to per-orientation totals. Faithful per-type printout would need a richer result dataclass.
- Table 6c metal frame factor — orchestrator ships 0.8 (spec); the deleted legacy lookup had 0.83 (silent bug). Frame-material strings outside
metal/aluminium/wood/pvc/upvc/compositefall through to 0.7 default.
§7 — slice progress (xlsx rows 372–401)
| Line ref | Description | Status | Commit |
|---|---|---|---|
| — | Per-zone η fix + MeanInternalTemperatureResult + mean_internal_temperature_monthly orchestrator |
✅ | fa49d7b9 |
| (85) | T_h1 = 21 °C scalar | ✅ | fa49d7b9 |
| (86)m | η_living utilisation at Ti=T_h1 | ✅ | fa49d7b9 |
| (87)m | MIT living = T_h1 − u1 − u2 | ✅ | fa49d7b9 |
| (88)m | T_h2 elsewhere heating temp (Table 9) | ✅ | fa49d7b9 |
| (89)m | η_elsewhere utilisation at Ti=T_h2 | ✅ | fa49d7b9 |
| (90)m | MIT elsewhere = T_h2 − u1 − u2 | ✅ | fa49d7b9 |
| (91) | f_LA living area fraction | ✅ | fa49d7b9 |
| (92)m | Blended MIT = f_LA·T_1 + (1−f_LA)·T_2 | ✅ | fa49d7b9 |
| (93)m | Adjusted MIT (+ Table 4e adj, default 0) | ✅ | fa49d7b9 |
| (94)m | η_whole utilisation at Ti=(93) — feeds §8 | ✅ | fa49d7b9 |
| — | Two-main case 1 weighted-R via secondary_fraction | ✅ | 13c2c651 |
| — | 6-fixture ALL_FIXTURES conformance (85)..(94) ≤5e-3 | ✅ | ff5d8c70 |
| — | CalculatorInputs + cert_to_inputs wiring; drop η iteration |
✅ | 8ec9da47 |
| — | Delete legacy single-η mean_internal_temperature_c |
✅ | a7f39685 |
Six Elmhurst fixtures conform end-to-end on §7 to ≤5e-3 °C / unitless on every per-zone line ref every month — 588 monthly assertions across 6 fixtures, all GREEN. The pre-rebuild single-η path drifted by 6.6e-3 °C on 000490 Jan (92)m; the per-zone fix closes that gap by ~70×.
Remaining §7 work
- Table 4e control_temperature_adjustment_c cert mapping — orchestrator accepts the parameter but
cert_to_inputshardcodes 0.0 (all 6 Elmhurst fixtures = 0 anyway). Wire when a non-zero corpus emerges. - Two-main case 2 (different parts heated separately) — needs (203) > 1−(91) branch + conditional T_2 averaging + per-system Table 4e adj weighting. Case 1 (both heat whole house) wired with synthetic test; case 2 deferred.
- Auto-detect
secondary_fractionfromepc.sap_heating.main_heating_details— currently always 0 incert_to_inputs. Wire when a multi-main cert lands.
§8 — slice progress (xlsx rows 401–435)
| Line ref | Description | Status | Commit |
|---|---|---|---|
| — | SpaceHeatingResult + space_heating_monthly_kwh orchestrator |
✅ | 9113f30a |
| — | Table 9c step 10 summer clamp (Jun..Sep = 0) | ✅ | 9113f30a |
| (95)m | Useful gains ηG, W | ✅ | 9113f30a |
| (97)m | Heat loss rate L_m = H·(T_int − T_ext), W | ✅ | 9113f30a |
| (98a)m | Space heating requirement per month, kWh | ✅ | 9113f30a |
| (98b)m | Solar space heating (Appendix H) — hardcoded 0 | ⏸ deferred | — |
| (98c)m | Total = (98a) + (98b), kWh | ✅ | 9113f30a |
| Σ(98c) | Annual total kWh | ✅ | 9113f30a |
| (99) | Annual per-m² = Σ(98c) / TFA | ✅ | 9113f30a |
| — | 6-fixture ALL_FIXTURES conformance on (95)..(99) | ✅ | 1f078af7 |
| — | CalculatorInputs.space_heating_monthly_kwh + cert_to_inputs wiring |
✅ | f6ab7626 |
Six Elmhurst fixtures conform end-to-end on §8 to 5e-2..1e-1 kWh on every per-month line ref (looser than §6/§7's 5e-3 because LINE_84/93/94 fixture pins are 4-d.p. display-rounded and §8's 0.024·n_m·(L−ηG) propagates that rounding into the per-month kWh band; the orchestrator computes in full precision).
E2e SAP-score impact: the spec-correct summer clamp removed a ~+14% over-prediction that was previously masking residual upstream §3/§5 precision drift. The 000490 SAP score moved 57 → 60 (target 57; tolerance updated to "within 3 points" with the full delta breakdown in test_elmhurst_000490_end_to_end_sap_score_currently_within_3_points). 000474 / 000490-kWh tests still pass.
Remaining §8 work
- Appendix H solar space heating (98b) — orchestrator emits
solar_space_heating_monthly_kwh = (0,)·12always. No Elmhurst fixture exercises it. Wire when a solar-space-heating cert lands. - 000490 SAP-score gap to 57 (currently 60) — needs upstream §3 (transmission HLC), §5 (internal gains), and the §4 water heating cert path tightened. Likely closes incrementally as the §9–§14 fuel/cost/rating chain is rebuilt + the legacy
predicted_hot_water_kwhis replaced bywater_heating_from_cert.
§8c — slice progress (xlsx rows 435–466)
| Line ref | Description | Status | Commit |
|---|---|---|---|
| — | SpaceCoolingResult + space_cooling_monthly_kwh orchestrator |
✅ | cf28eec4 |
| — | Table 10a utilisation_factor_loss leaf (γ=G/L, 8-dp rounding, L=0 sentinel, γ≤0 / γ=1 / γ>0∧≠1 branches) |
✅ | cf28eec4 |
| (100)m | Heat loss rate L_m = H·(24 − T_e), W (Ti hardcoded 24 °C) | ✅ | cf28eec4 |
| (101)m | η_loss per month, Table 10a | ✅ | cf28eec4 |
| (102)m | Useful loss η·L, W | ✅ | cf28eec4 |
| (103)m | Cooling gains W (Table 5a exclusions deferred — see below) | ✅ pass-through; Table 5a exclusion ⏸ deferred | cf28eec4 |
| (104)m | Q_whole continuous = 0.024·(G − η·L)·n_m, kWh; Jun-Aug only | ✅ | cf28eec4 |
| Σ(104) | Annual Q_whole total kWh | ✅ | cf28eec4 |
| (105) | Cooled-area fraction f_C — hardcoded 0 in cert_to_inputs | ✅ (zero-branch); RdSAP cooled-area defaulting ⏸ deferred | cf28eec4 |
| (106)m | Intermittency factor 0.25, Jun-Aug only | ✅ | cf28eec4 |
| (107)m | Q_cool = (104)·(105)·(106) with negative-or-< 1 kWh clamp per spec 10321 | ✅ | cf28eec4 |
| Σ(107) | Annual Q_cool kWh | ✅ | cf28eec4 |
| (108) | Annual per-m² = Σ(107) / TFA | ✅ | cf28eec4 |
| — | 6-fixture ALL_FIXTURES conformance on (101)/(103)/(106)/(107)/(108) | ✅ | 3b9fa936 |
| — | CalculatorInputs.space_cooling_monthly_kwh + MonthlyEntry.space_cool_requirement_kwh + SapResult.space_cooling_kwh_per_yr + cert_to_inputs wiring |
✅ | f3797066 |
| (105) Table 10c SEER → cooling fuel kWh | ⏸ deferred | — |
Six Elmhurst fixtures conform end-to-end on §8c to exact equality on (101)/(103)/(106)/(107)/(108) — no tolerance needed because every fixture has has_fixed_air_conditioning=False → f_C=0 → (107), (108) ≡ 0 and (101) ≡ 1.0 (γ=0 every month). (100)/(102)/(104) carry computed non-zero values per fixture and are not pinned in the ALL_FIXTURES test; the algebra is exercised by the synthetic-positive leaf/orchestrator tests in test_space_cooling.py (γ=1 closed-form branch with hand-computed 4.65 kWh end-to-end + γ ≤ 0 / γ=1 / γ>0∧≠1 / γ≈1 boundary / L=0 sentinel leaf tests).
E2e SAP-score impact: none. All Elmhurst fixtures lodge has_fixed_air_conditioning=False; cooling contribution to the rating is structurally zero. The wiring is in place so that the first non-zero-cooling cert triggers the (103) Table 5a exclusion + (105) RdSAP cooled-area defaulting slice atomically.
Remaining §8c work
- Table 5a exclusion in cooling gains (103) —
cert_to_inputscurrently passesmonthly_total_gains_w = (0,)*12to the orchestrator. Spec line 10280 requires cooling G to drop Table 5a items (pumps/fans, intermittent appliances) and use column (A) of Table 5 throughout. Needs acooling_gains_from_certhelper mirroringinternal_gains_from_cert+solar_gains_from_cert. Triggered by the first cooling-enabled cert. - RdSAP cooled-area defaulting (105) —
cert_to_inputscurrently passescooled_area_fraction = 0.0always. Forhas_fixed_air_conditioning=Truecerts the RdSAP 10 spec gives a defaulting rule (cooled area not lodged; cert side derives from dwelling type or assumes whole-dwelling). Needs PDF lookup. Triggered by the first cooling-enabled cert. - Table 10c SEER + cooling fuel kWh + fuel cost cascade — Q_cool (107) needs to drive an electricity-fuel-kWh path (Q_cool ÷ SEER) and through Table 12 onto
SapResult.total_fuel_cost_gbp. Currently the cooling kWh sits onMonthlyEntry.space_cool_requirement_kwhbut doesn't propagate into fuel costs or CO2. Triggered by the first cooling-enabled cert. - §8f Fabric Energy Efficiency (109) = (98a)/TFA + (108) — only relevant for new-build compliance (FEE replaces SAP rating in some contexts). Picked up by §8f slice.
§8f — slice progress (xlsx rows 466–470)
| Line ref | Description | Status | Commit |
|---|---|---|---|
| — | fabric_energy_efficiency_kwh_per_m2_yr free function (no dataclass) |
✅ | 43cc16bc |
| — | SpaceHeatingResult.space_heating_requirement_kwh_per_yr (Σ(98a) — pre Appendix H) |
✅ | 43cc16bc |
| (109) | FEE = (98a)/(4) + (108), kWh/m²/yr | ✅ | 43cc16bc |
| — | 6-fixture ALL_FIXTURES conformance (LINE_109 = LINE_99 since (98b)=0 + (108)=0) | ✅ | 43cc16bc |
| — | CalculatorInputs.fabric_energy_efficiency_kwh_per_m2_yr + SapResult.fabric_energy_efficiency_kwh_per_m2_yr + cert_to_inputs wiring |
✅ | 43cc16bc |
| — | §11 compliance conditions (separate worksheet re-run with §11 ventilation / HW / lighting / gains-column assumptions) | ⏸ deferred | — |
Six Elmhurst fixtures conform end-to-end on §8f to ≤5e-3 kWh/m² on LINE_109. The tolerance inherits from the 4-d.p. display-rounding floor of the LINE_98C_ANNUAL_KWH fixture pins (§8 conformance is at 1e-1 kWh on annual, which divides by TFA to ~5e-3 kWh/m²).
E2e SAP-score impact: none. (109) is a transparency output for rating runs — it doesn't feed ECF, fuel costs, or CO2. The wiring lets §11 compliance code consume FEE later via SapResult.fabric_energy_efficiency_kwh_per_m2_yr once §11 conditions are implemented.
Remaining §8f work
- §11 FEE compliance conditions — spec lines 2152-2164 require a separate worksheet run with: natural ventilation + 2-4 extract fans by TFA, instantaneous-electric shower + bath, 125 l/day water-use target, lighting capacity 185 lm/m² at 66.9 lm/W, column (B) of Table 5 for heating gains, column (A) for cooling gains, etc. This is a new-build compliance path; existing-dwelling ratings don't trigger it. Lands as
fabric_energy_efficiency_under_section_11_conditions(...)when the first compliance use case emerges. - Σ(98a) ≠ Σ(98c) regression coverage — when Appendix H solar space heating lands (currently (98b) ≡ 0), Σ(98a) will diverge from Σ(98c). The FEE function consumes Σ(98a) per spec; a new fixture with non-zero (98b) would assert that distinction holds.
§9a — slice progress (xlsx rows 470–614)
| Line ref | Description | Status | Commit |
|---|---|---|---|
| — | space_heating_fuel_monthly_kwh orchestrator + EnergyRequirementsResult dataclass (16 fields, full worksheet shape) |
✅ | 2b5fc6a5 |
| (201) | Secondary heating fraction (Table 11) | ✅ | 2b5fc6a5 |
| (202) | Main heating total fraction = 1 − (201) | ✅ | 2b5fc6a5 |
| (203) | Main 2 of main fraction | ⏸ zero-branch (no two-main cert yet) | — |
| (204) | Main 1 of total fraction = (202) × (1 − (203)) | ✅ | 2b5fc6a5 |
| (205) | Main 2 of total fraction = (202) × (203) | ⏸ zero-branch | — |
| (206) | Main 1 efficiency % | ✅ pass-through; cert source is SAP10 Table 4a category default (PCDB-blocked) | 2b5fc6a5 |
| (207) | Main 2 efficiency % | ⏸ zero-branch | — |
| (208) | Secondary efficiency % | ✅ | 2b5fc6a5 |
| (209) | Cooling SEER (Table 10c) | ⏸ deferred (see §8c remaining work) | — |
| (211)m | Main 1 fuel kWh per month = (98c)m × (204) × 100 / (206) | ✅ | 2b5fc6a5 |
| (211) | Σ(211)m | ✅ | 2b5fc6a5 |
| (213)m | Main 2 fuel kWh per month | ⏸ zero-branch | — |
| (213) | Σ(213)m | ⏸ zero-branch | — |
| (215)m | Secondary fuel kWh per month = (98c)m × (201) × 100 / (208) | ✅ | 2b5fc6a5 |
| (215) | Σ(215)m | ✅ | 2b5fc6a5 |
| (221) | Cooling fuel kWh per yr = (107) ÷ (209) | ⏸ deferred (Table 10c SEER) | — |
| — | CalculatorInputs.energy_requirements composite slot + SapResult.main_2_heating_fuel_kwh_per_yr + SapResult.space_cooling_fuel_kwh_per_yr + _solve_month refactor + cert_to_inputs wiring (atomic) |
✅ | 380b6781 |
| — | 6-fixture ALL_FIXTURES PDF-derived pins on (206)/(211)/(215) | ⏸ blocked on PCDB (see Prioritised gap list item 1) | — |
| (230a)-(230h) | Table 4f pumps/fans breakdown (warm-air fans, oil aux, gas aux, keep-hot, solar pump, WWHRS pump) | ⏸ deferred | — |
| (231) | Σ(230a)-(230h) — Total electricity for pumps/fans/keep-hot | ⏸ deferred — currently inputs.pumps_fans_kwh_per_yr is an opaque scalar |
— |
| (236)/(237) | Appendix Q items energy saved/used | ⏸ deferred (no Q-item cert in corpus) | — |
| (238) | Total delivered energy kWh/yr | Partial — computed in intermediate["delivered_fuel_kwh_per_yr"]; not yet promoted to SapResult field |
— |
Four synthetic tests on the orchestrator cover: single-main no-secondary 80% efficiency, Table 11 secondary fraction split (211)+(215), summer-clamp zero propagation, and scope-A two-main / cooling-fuel placeholders. Cert round-trip test pins inputs.energy_requirements.main_1_fuel_kwh_per_yr == result.main_heating_fuel_kwh_per_yr to float equality — confirms the refactor preserves existing behaviour.
E2e SAP-score impact: zero (refactor only — _solve_month now reads precomputed (211)m/(215)m instead of doing q/η inline). The 000490 +3 SAP-score gap stays parked behind PCDB integration.
Remaining §9a work
- PCDB integration — see Prioritised gap list item 1. Blocks PDF-derived ALL_FIXTURES pinning + closes 000490 e2e gap. ADR-0010 §4 prerequisite.
- Two-main system (203)/(205)/(207)/(213) — populate from
epc.sap_heating.main_heating_details[1]when a multi-main cert appears in fixtures. EnergyRequirementsResult already exposes the fields as zeros; first multi-main slice flips them from placeholders to real values. - Table 10c SEER → (209)/(221) — cooling fuel kWh from (107) ÷ SEER. Same trigger as §8c remaining work (first fixed-AC cert); add Table 10c lookup table + wire
energy_requirements.cooling_fuel_kwh_per_yr+ SapResult.space_cooling_fuel_kwh_per_yr from zero to real value. - Table 4f pumps/fans breakdown (230a)-(230h) → (231) — replace opaque
CalculatorInputs.pumps_fans_kwh_per_yrscalar with per-source sub-lines. Likely a separate sweep slice once aux electricity becomes load-bearing for ranking. - Appendix Q items (236)/(237) — placeholder until a Q-item cert lands.
- (238) total delivered energy on SapResult — promote from
intermediatedict when §10a or §13 requires it as a named output.