diff --git a/.claude/skills/epc-to-elmhurst-rdsap-inputs/reference/mapping.md b/.claude/skills/epc-to-elmhurst-rdsap-inputs/reference/mapping.md index 1aab0298..245b357a 100644 --- a/.claude/skills/epc-to-elmhurst-rdsap-inputs/reference/mapping.md +++ b/.claude/skills/epc-to-elmhurst-rdsap-inputs/reference/mapping.md @@ -180,12 +180,23 @@ Table 32 unit costs, p/kWh (`domain/sap10_calculator/tables/table_32.py`): **`main_fuel_type` / `water_heating_fuel` 29 = off-peak (7-hour) electricity** → Elmhurst Electricity meter type = **Dual-rate / Economy 7 (7-hour)**, NOT Single. -⚠️ **Known over-rating bug:** the engine prices **100% of off-peak space heating -AND hot water at the 5.50p low rate** (`inputs.space_heating_fuel_cost_gbp_per_kwh` -= 0.055), instead of the SAP **Table 12a high/low split** (a portion at the 15.29p -high rate). This under-costs all-electric Economy-7 dwellings and inflates the SAP -score. Always surface this in the output's "Known divergences". Canonical case: -UPRN 10002468137 — lodged 55, engine 62. +✅ **Economy-7 high/low split — FIXED (PR #1217).** The engine now applies the SAP +**Table 12a Grid 1** (space) + **Table 13** (immersion DHW) high/low split rather +than pricing 100% at the 5.50p low rate. Electric STORAGE heaters legitimately get +a 0.00 SH high-rate fraction (100% low — spec value, not a bug); immersion HW takes +the cylinder-volume/occupancy/single-dual Table 13 blend (applied in +`cert_to_inputs._hot_water_fuel_cost_gbp_per_kwh` when volume + occupancy + +single/dual are all resolved; absent any of them it still falls back to 100% low — +a rarer edge). Verified: canonical UPRN 10002468137 engine 60.92 = Elmhurst 61 to +the penny (its lodged 55 is the OLD SAP-2012 schema, not comparable); UPRN +10022893721 engine 79 = lodged 79, Elmhurst (Dual meter) 81. + +⚠️ **When building in Elmhurst you MUST set the Economy-7 meter** (`main_fuel_type` +/ `water_heating_fuel` 29 = off-peak 7-hour → Electricity meter type **Dual**, NOT +Single). Elmhurst silently defaults to Single/Standard and prices at the 13.19p +standard rate, collapsing the worksheet SAP ~13 points — which can masquerade as an +engine "over-rating". The control is a hidden Meters sub-tab on the SpaceHeating +page (`TabPanelMeters_RadioButtonListElectricityType`). ## Water Heating diff --git a/.claude/skills/expand-sap-accuracy-corpus/SKILL.md b/.claude/skills/expand-sap-accuracy-corpus/SKILL.md index 437c305e..6d73c47f 100644 --- a/.claude/skills/expand-sap-accuracy-corpus/SKILL.md +++ b/.claude/skills/expand-sap-accuracy-corpus/SKILL.md @@ -19,6 +19,74 @@ score the property would get unchanged. Elmhurst is the accredited ground truth; its Input Summary (parsed back to `EpcPropertyData`) exposes mapper holes, and its worksheet exposes calculator holes. +## ⏩ Resume in a fresh context (autonomous run) + +If the user says "continue" / "next" / "keep going through the worklist", run this +loop **continuously without asking between certs** — report only `eng X / elm Y` +per cert and tick the worklist line as each is pinned. The build automation is +fully working (see `scripts/hyde/elmhurst_lib.py` helpers + the `build_.py` +templates); most certs build unattended end-to-end. + +**State (2026-06-19):** pinned this campaign — SAP-17.1 cohort + RdSAP-17.1/18.0 +(older) plus: **16.1** `100021943298` (76/75), **19.1.0** `10096028301` (82/82), +**16.3** `44012843` (79/78), **17.0** `10023444324` (80/80) + `10023444320` +(81/82), **RdSAP-20.0.0** `10090844932` (78/77), **16.2** `100090182288` (69/71, +semi house). Latest run (2026-06-19): **16.2** `100021985993` (74/72, end-terrace +bungalow), **17.1** `10091568921` (82/80, full-SAP end-terrace house combi 17615) ++ `10093718424` (81/80, semi sibling), **RdSAP-18.0** `10022893721` (79/81, first +NON-BOILER cert — electric storage heaters + immersion; storage-heater automation +now SOLVED incl. the Economy-7 Dual-meter step, see banked findings; engine 79 = +lodged, NO bug), **RdSAP-21.0.1** `10023443426` (76/79, native schema, combi house; +engine 76 = lodged EXACTLY; Elmhurst +3 = omitted-secondary build gap). Next `[ ]`: +`10093412452` (SAP-17.1), then `10090343335` (17.0) / `10093115480` (17.1) / +`68151071` (RdSAP-17.0). Skip `100020933699` (user said skip), `[⛔]` (NOT +MAPPABLE), `[⚠]` (flagged engine bugs: MVHR / heat-pump fuel-39). + +**Per-UPRN recipe** (all commands `DISPLAY=:99`, cwd `scripts/hyde`; run +`bash scripts/hyde/start_viewer.sh` once; creds in `.elmhurst-creds.json`; shared +assessment GUID `B44A0DB4-4C08-4241-B818-86F060172105`): +1. `PYTHONPATH=/workspaces/model python scripts/fetch_real_life_epc_sample.py ` + then scope: dwelling_type/built_form, age band, walls/roof/floor descriptions, + heating `main_heating_index_number`/category/`has_hot_water_cylinder`, window + total area (`sum(sap_windows w*h)`), party_wall_length, lighting %, MEV/AP50. +2. **Copy the closest `build_.py` template** and adjust values: + - combi flat → `build_100021943298.py`; regular-boiler+cylinder flat → + `build_44012843.py`; full-SAP combi flat (MEV+AP50) → `build_10096028301.py` + / `build_10023444324.py` (+party wall) / `build_10023444320.py` (mid-floor); + - combi house → `build_10090844932.py` (end-terrace, party wall) / + `build_100090182288.py` (semi, no party wall). + Adjust: property type/built-form, band (`_pick` by year, e.g. "1950"/"2012"/ + "2023"), two-floor dims + party wall, wall insulation, roof, floor, window m², + doors, lighting, boiler PCDB ref + search query, MEV/AP50 if present. +3. Run pages: `for p in property_description [flats] dimensions walls roofs floors + openings ventilation; do … build_.py $p; done` (one Save&Close each, + ~1 min/page; flats only for Flat property type). Then a window-verify/fix + snippet (re-add the combined window if the grid shows 0.00), then + `build_.py space_heating` and `water_heating`. +4. Heating uses the `elmhurst_lib` helpers: `E.select_boiler(page, "", + "")` (look up id/type in `domain/elmhurst/pcdb_gas_oil_boiler_codes.csv`; + the lodged `main_heating_index_number` IS the id); control + `E.set_heating_dialog(page, "…ButtonMainHeatingControls", "^Boilers", + "^Standard", "CBE Programmer, room thermostat and TRVs")` (=2106); water + `E.set_heating_dialog(page, "…ButtonWaterHeatingCode", "From Space Heating", + "From the primary heating system")`; combi → `E.clear_hot_water_cylinder(page)`. +5. Download: edit `elmhurst_download.py` `SAMPLE_DIR` to the cert's + `/uprn_` dir; first confirm Recommendations is clean (parse + `[id*=ContentPlaceHolder1] a` link text — Summary silently redirects to Address + until zero errors); `python scripts/hyde/elmhurst_download.py` (retry once; the + nav goes Address→Recommendations→Summary). +6. `PYTHONPATH=/workspaces/model python scripts/compare_epc_paths.py ` → + read **"gov-API inputs → SAP"** (engine) and **"Elmhurst's OWN engine + (worksheet …)"** (Elmhurst ground truth). Target ≤0.5–1. +7. **Pin** the engine value: add a `RealCertExpectation(schema, sample=uprn_, + cert_num, sap_score=)` in `tests/.../test_real_cert_sap_accuracy.py`; + run `…::test_real_cert_sap_score`. +8. Tick the worklist line `[x] … · eng X / elm Y (lodged Z) · PINNED …`. Next cert. + +See **Banked findings** below for the modal-dialog mechanics (all already encoded +in the helpers). New schema not mappable → add a dedicated `from_*_schema_*` +mapper first (per-schema convention) + guard with the RdSAP-21.0.1 corpus gauge. + ## The loop (one UPRN) 1. **Pick** the first `[ ]` UPRN in [worklist.md](worklist.md). @@ -93,6 +161,91 @@ Pattern: `with E.session() as (ctx,page): E.goto(...); E.set_text/set_select(... lines 17/18). This drove the first campaign mapper fix — see Banked findings. ## Banked findings (fold new ones in here as the corpus grows) +- **MAPPER GAP — cylinder insulation thickness dropped (RdSAP-17.0+):** the mapper + carries `cylinder_size` + `cylinder_insulation_type` but NOT + `cylinder_insulation_thickness` → `EpcPropertyData.sap_heating.cylinder_ + insulation_thickness_mm` stays None even when the cert lodges it (e.g. 50 mm). + The engine then assumes a poorly-insulated cylinder → over-counts HW cylinder + loss → under-rates. Confirmed uprn_68151071 (raw 50 mm → mapped None; engine HW + 3446 vs Elmhurst 2911 kWh; engine 68 vs lodged 70 / Elmhurst 71). FIX: map the + thickness in the RdSAP per-schema mapper; check blast radius (any pinned cylinder + cert may shift) + regress the RdSAP-21.0.1 corpus gauge. Leverage point — likely + improves every cylinder-with-lodged-thickness cert. Flagged, not yet fixed. +- **Party-wall type `_pick` gotcha:** matching `"filled"` ALSO matches "Cavity + masonry **UN**filled" (CU, U≈0.5) — the wrong type for a cert whose party wall is + U≈0. Match `"masonry filled"` to hit CF (Cavity masonry filled, U≈0). Affects + cavity builds (10090844932 / 10091568921 / 10093718424 / 10093412452 used the + loose `"filled"` and may have got CU in Elmhurst — only the Elmhurst cross-check, + not the pinned engine value which is validated against lodged). Fixed for + uprn_10093115480. +- **Shared-assessment reset: storage/electric → boiler cert.** The shared + assessment carries the PRIOR cert's heating system. Going storage→combi, the + boiler search dialog won't open while a SAP-table `MainHeatingCode` (e.g. SEB) is + set. Fix: JS-clear `MH1.TextBoxMainHeatingCode` to `''` + dispatch change, Save & + Close, then `select_boiler` works. Also RESET the electricity meter on the Meters + sub-tab (a prior off-peak cert leaves it Dual; gas certs want Single) and the + SECONDARY heating (a prior cert's secondary persists even when the calc shows + presence=No — set it explicitly or it pollutes the worksheet). +- **Secondary heating must be built in Elmhurst when lodged.** Certs lodge a + secondary (`sap_heating.secondary_heating_type`, e.g. 612 = mains-gas room heater + @ Table 4a seasonal efficiency 0.20 — a low-eff decorative/old gas fire). The + engine models it (fraction 0.1 from Table 11 ÷ the secondary efficiency → e.g. + 3065 kWh for code 612); omitting it in Elmhurst inflates the worksheet (uprn_ + 10023443426: omitted → 79 vs engine/lodged 76). Build via Secondary present=Yes → + `ButtonSecondaryHeatingCode` cascade (title "Select secondary heating"): fuel → + sub-fuel → appliance → type, e.g. Gas → Mains gas → Room Heaters → RGx (pick the + RGx whose efficiency matches the lodged code, ~0.20 = decorative/old gas fire). + ⚠ For a NATIVE RdSAP-21.0.1 cert, engine = lodged (exact, all components) is the + authoritative validation — if the Elmhurst rebuild diverges, suspect an omitted + lodged feature (secondary / meter), confirm via engine-on-Elmhurst-inputs ≈ + worksheet, and pin the engine = lodged value. +- **Non-boiler (storage-heater) main heating — SOLVED** (uprn_10022893721, RdSAP- + 18.0 GF flat, electric storage heaters + immersion). The SpaceHeating page has NO + inline system-type selector and a `ButtonMainHeatingCode` button only APPEARS + once the bound PCDF boiler is cleared. Two-pass recipe (one Save & Close each): + 1. **Clear the leftover PCDB boiler**: set `MH1.TextBoxPCDFBoilerReference` to + `"0"` via JS + dispatch `change`, then `save_close`. (It doesn't AutoPostBack; + the Save commits it. After reload, boilerRef="0", the boiler fuel/flue fields + vanish, and `MH1.ButtonMainHeatingCode` is now present.) + 2. **Select the SAP-table system** via `set_heating_dialog(..ButtonMainHeating + Code, "^Electric","^Electric","Storage","SEB Modern slimline")` (title "Select + heating code"; L1 Gas/Oil/Solid Fuel/Electric/Community/No heating; storage L4 + = SEA old large-volume / SEB modern slimline / SED fan / SEJ integrated / SEK + high-heat-retention). **Match the cert's `sap_main_heating_code`**: 402 = SEB. + Then the CONTROL via `set_heating_dialog(..ButtonMainHeatingControls, + "Storage Radiator","CSA Manual charge control")` (= SAP 2401). Secondary No. + Water: `set_heating_dialog(..ButtonWaterHeatingCode, "Water Heater","^Electric", + "Immersion")` (→ code HEI) — then the immersion code REQUIRES a cylinder: CHECK + `WH.CheckBoxHotWaterCylinder` (JS click+change, AutoPostBacks) or Recommendations + errors "must have a Hot Water Cylinder"; set `WH.DropDownListCylinderSize` + (size 2→Normal/110L, 3→Medium/160L, 4→Large/210L), `WH.DropDownListInsulated` + (Foam/Jacket), `WH.DropDownListInsulationThickness`, and `WH.RadioButton + ListImmersionHeater` (off-peak meter → Dual). See `build_10022893721.py` as the + template. ⚠ **CRITICAL — set the electricity meter type.** All-electric off-peak + certs MUST set the Economy-7 meter or Elmhurst silently defaults to Single / + Standard and prices everything at the 13.19p standard rate (worksheet collapses + ~13 SAP). The control is a HIDDEN sub-tab on the SpaceHeating page: + `E.click_tab(page,"TabContainer_TabPanelMeters")` then `E.set_select(page, + "TabContainer_TabPanelMeters_RadioButtonListElectricityType","Dual")` (options + Single/Dual/18 Hour/24 Hour/Unknown; cert `meter_type` 1→Dual=7-hour off-peak). + Verify in Elmhurst summary §14.2 "Electricity meter type" / worksheet + "Electricity Tariff" before trusting the worksheet SAP. +- **Economy-7 off-peak pricing is CORRECT — do NOT "fix" it** (was a real bug, FIXED + in PR #1217 via the Table 13 off-peak water-heating split + window-U fix). The + engine applies SAP Table 12a Grid 1 (space) + Table 13 (immersion DHW) high/low + splits properly: storage heaters' SH high-rate fraction is legitimately 0.00 + (100% low rate), immersion HW takes the volume/occupancy/single-dual blend. Proof: + uprn_10002468137 (canonical) engine 60.92 = Elmhurst 61 to the penny; + uprn_10022893721 engine 79 = lodged 79, Elmhurst (Dual meter) 81. ⚠ If you see a + big engine-over-Elmhurst gap on an all-electric off-peak cert, SUSPECT THE BUILD + (Elmhurst meter left on Single — see meter step above), not the calculator. The + `reference/mapping.md` "known over-rating bug (engine 62)" note is STALE (pre-PR + #1217). `cert_to_inputs.py` `_hot_water_fuel_cost_gbp_per_kwh` applies the immersion + Table 13 blend when cylinder volume + occupancy resolve; when `immersion_heating_ + type` is UNLODGED on an off-peak meter it now defaults to DUAL per RdSAP §10.5 + (was a 100%-low-rate fallback that under-costs — fixed, +regression test + `test_off_peak_immersion_unlodged_type_defaults_to_dual_table13_blend`). Only an + unresolvable cylinder volume / occupancy still falls back to 100% low. - **Air-permeability AP50 fix** (uprn_10093116528, the first campaign cert): the full-SAP mapper was routing the lodged q50 to the engine's AP4/Pulse formula (`0.263×AP4^0.924`) instead of the AP50 `/20` path — a big infiltration @@ -134,6 +287,15 @@ Pattern: `with E.session() as (ctx,page): E.goto(...); E.set_text/set_select(... cylinder()`, then set water method "From Space Heating → primary" via `set_heating_dialog(..ButtonWaterHeatingCode, 'From Space Heating', 'From the primary heating system')`. +- **Reduced-field (16.x) semi/terraced party wall.** 16.x lodges no + `party_wall_length`, so the engine models NO party wall (defaults 0). But + Elmhurst REJECTS a semi/terraced with a zero party wall ("At least one building + part must have a party wall length of non zero"). Enter the geometry-derived + length (solve `2w+d=heat_loss_perimeter`, `w*d=floor_area`; party wall = the + joined side `d`) on every floor + a filled-cavity party-wall type (CF), and + EXPECT engine to over-read Elmhurst by ~1–2 SAP (engine's no-party-wall vs + Elmhurst's forced one). That delta is the documented reduced-field gap, not a + bug — confirm via engine-on-Elmhurst-inputs ≈ worksheet. Pin the engine value. - **Main-heating control must match the boiler's system type.** Heat-pump leftover control (e.g. CHJ/2210, ctrl-group-2) is invalid for a gas boiler (ctrl-group-1) and blocks the report. For a boiler programmer+room-stat+TRVs (SAP 2106): diff --git a/.claude/skills/expand-sap-accuracy-corpus/worklist.md b/.claude/skills/expand-sap-accuracy-corpus/worklist.md index e4db5aa5..e40f2853 100644 --- a/.claude/skills/expand-sap-accuracy-corpus/worklist.md +++ b/.claude/skills/expand-sap-accuracy-corpus/worklist.md @@ -4,7 +4,8 @@ Process **one at a time**, top to bottom (see [SKILL.md](SKILL.md)). After each UPRN, tick it and annotate: `— · eng / elm · `. **Legend:** `[ ]` todo · `[x]` pinned (≤0.5) · `🔧` mapper extended · `⚠` xfail -(engine bug) · `⛔` blocked. +(engine bug) · `⛔` blocked · `[🔍]` flagged — engine vs Elmhurst-worksheet gap too +large, or a new/complex build pattern needed; NEEDS INVESTIGATION. **Worked reference (not in the 100):** `uprn_10092973954` (SAP-Schema-17.1, 2020 new-build flat) — full loop proven: eng 77 / elm 78, engine-on-Elmhurst- @@ -60,19 +61,19 @@ Skip the 🚩 MVHR / 🚩 heat-pump-fuel and ⛔ sparse certs. - [⚠] 10094601287 — SAP-18.0.0 · eng 80 / lodged 84 · 🚩 MVHR idx 500230 not credited (flagged) - [x] 10090844932 — RdSAP-20.0.0 (end-terrace HOUSE 2-storey, band L, combi PCDB 10327, party wall 4.93, 250mm loft) · eng 78 / elm 77 (lodged 78) · PINNED engine 78. Built in Elmhurst (House, boiler 10327 via search, control CBE/2106, water from primary, party wall 4.93 filled). Within ~1 (78.13 vs 77); engine-on-Elmhurst-inputs 77.24 ≈ 77 → faithful. - [⛔] 10090844948 — SAP-16.3 · NOT MAPPABLE (ValueError: RdSapSchema17_1: missing required field ) -- [ ] 100090182288 — SAP-16.2 · eng 71 / lodged 71 +- [x] 100090182288 — SAP-16.2 (semi-detached HOUSE 2-storey, band D, filled cavity, combi PCDB 10327, double glazed) · eng 71 / elm 69 (lodged 71) · PINNED engine 71. Built in Elmhurst (semi, boiler 10327, control CBE/2106, water from primary, party wall 4.28 filled — Elmhurst requires non-zero for a semi). +2 vs Elmhurst = documented 16.2 reduced-field party-wall gap (gov-API lodges no party_wall_length → engine models none); engine-on-Elmhurst-inputs 69.35 ≈ worksheet 69 → calculator faithful. - [⚠] 10093114053 — SAP-17.0 · eng 93 / lodged 79 · 🚩 heat-pump fuel-39 (flagged) -- [ ] 10091568921 — SAP-17.1 · eng 82 / lodged 85 -- [ ] 10093718424 — SAP-17.1 · eng 81 / lodged 84 -- [ ] 10022893721 — RdSAP-18.0 · eng 79 / lodged 79 -- [ ] 10023443426 — RdSAP-21.0.1 · eng 76 / lodged 76 -- [ ] 10093412452 — SAP-17.1 · eng 81 / lodged 84 +- [x] 10091568921 — SAP-17.1 (full-SAP END-TERRACE HOUSE 2-storey, band L 2018, combi PCDB 17615 Potterton Promax Ultra, party wall 40.56m², natural vent + 3 extract fans, AP50 4.45, 20 LED, measured U 0.18/0.10/0.15) · eng 82 / elm 80 (lodged 85) · PINNED engine 82. Built in Elmhurst (end-terrace house, boiler 17615, CBE/2106, water from primary, party wall 8.45 filled, AP50 Blower Door 4.45+cert). +2 = documented full-SAP→RdSAP residual (measured U beats band-L defaults); engine-on-Elmhurst-inputs 80.16 ≈ worksheet 80 → faithful. has_hot_water_cylinder lodged true but combi PCDB + no cylinder detail → built combi. No mapper change. +- [x] 10093718424 — SAP-17.1 (full-SAP SEMI-DETACHED HOUSE 2-storey, band L, combi PCDB 17615, party wall 40.89m², natural vent + 3 extract fans, AP50 4.18, 20 LED, measured U 0.25/0.13/0.16; sibling of 10091568921) · eng 81 / elm 80 (lodged 84) · PINNED engine 81. +1 = documented full-SAP→RdSAP residual; engine-on-Elmhurst-inputs 80.12 ≈ worksheet 80 → faithful. Built combi (cylinder lodged true but combi PCDB). No mapper change. +- [x] 10022893721 — RdSAP-18.0 (GF FLAT, band I, cavity insulated, ELECTRIC STORAGE HEATERS SAP 402 SEB Modern slimline + manual charge CSA/2401, immersion off-peak dual + cylinder Normal/110L foam 50mm, party wall 21.48) · eng 79 / elm 81 (lodged 79) · PINNED engine 79 = lodged. Built in Elmhurst (storage SEB, CSA control, immersion dual + cylinder, **Dual electricity meter / Economy 7**). engine -2 vs Elmhurst, within tolerance; engine-on-Elmhurst-inputs 78.76 ≈ 79 → faithful. Economy-7 off-peak pricing is CORRECT (Table 12a/13, fixed PR #1217) — NOT a bug. ⚠ Earlier mis-build left Elmhurst meter on Single/Standard → bogus worksheet 66; fixed by setting Dual meter on the Meters sub-tab. 🔧 First non-boiler cert — SOLVED storage-heater automation (see banked findings: clear PCDB boiler → ButtonMainHeatingCode Electric→Electric→Storage→SEB; set Dual meter). build_10022893721.py complete. +- [x] 10023443426 — RdSAP-21.0.1 (END-TERRACE HOUSE 2-storey, band L, cavity insulated, combi PCDB 17045 Ideal Logic ES35, 300mm loft, party wall 9.2 lodged, 11 dbl-glazed windows ~10.3m², 9 LED, mains-gas room-heater SECONDARY code 612 eff 0.20) · eng 76 / elm 79 (lodged 76) · PINNED engine 76 = lodged EXACTLY (native RdSAP-21.0.1 schema; reproduces every component: space 6129/main 6247/HW 2752/CO2 2232). Elmhurst +3 is a BUILD gap — my build omitted the lodged secondary gas fire (engine models it 3065 kWh = 0.1÷0.20); engine-on-Elmhurst-inputs 79.29 ≈ worksheet 79 → calculator faithful. NB: shared-assessment storage→combi reset needed (clear leftover MainHeatingCode) + meter reset to Single. build_10023443426.py. +- [x] 10093412452 — SAP-17.1 (full-SAP END-TERRACE HOUSE 2-storey, band L, combi PCDB 17615, party wall 41.01m², natural vent + 3 extract fans, AP50 4.62, 20 LED, measured U 0.25/0.13/0.16; sibling of 10093718424/10091568921) · eng 81 / elm 80 (lodged 84) · PINNED engine 81. +1 = documented full-SAP→RdSAP residual; engine-on-Elmhurst-inputs 79.91 ≈ worksheet 80 → faithful. Built combi (cylinder lodged true but combi PCDB). No mapper change. - [⛔] 10014314798 — SAP-16.2 · NOT MAPPABLE (ValueError: RdSapSchema17_1: missing required field ) - [⚠] 10094601294 — SAP-18.0.0 · eng 81 / lodged 84 · 🚩 MVHR idx 500230 not credited (flagged) -- [ ] 10090343335 — SAP-17.0 · eng 86 / lodged 88 -- [ ] 10093115480 — SAP-17.1 · eng 81 / lodged 81 -- [ ] 68151071 — RdSAP-17.0 · eng 68 / lodged 70 -- [ ] 100021985993 — SAP-16.2 · eng 74 / lodged 70 +- [🔍] 10090343335 — SAP-17.0 (full-SAP SEMI-DETACHED HOUSE, 3-storey incl. ROOM-IN-ROOF top floor h1.95, combi PCDB 16841 Vaillant ecoTEC plus 824, party wall 48.51m², 10 windows 18.46m² + 1 ROOF WINDOW type-12 2.31m², 3 doors, AP50 3.98, 10 LED, measured U walls 0.18/roof 0.15/floor 0.13, TFA 122) · eng 86 / lodged 88 · 🔍 FLAGGED — NEEDS NEW BUILD PATTERN: room-in-roof + roof-window not covered by templates (Elmhurst DIM panel only exposes 2 floors; room-in-roof entered via RoomInRoofMain age-band + separate RiR dimensions TBD; roof window type-12 entry TBD). Elmhurst build + worksheet comparison pending. Skipped in autonomous sweep to keep momentum; revisit with a dedicated room-in-roof build. +- [x] 10093115480 — SAP-17.1 (full-SAP END-TERRACE BUNGALOW single-storey, band L 2016, combi PCDB 16841, party wall 10.99m² CF filled, natural vent + 2 fans, AP50 3.85, 9 LED, measured U 0.19/0.11/0.12, TFA 56) · eng 81 / elm 78 (lodged 81) · PINNED engine 81 = lodged EXACT. +3 = documented full-SAP→RdSAP residual (measured U beats band-L defaults); engine-on-Elmhurst-inputs 78.29 ≈ worksheet 78 → faithful. Built combi. No mapper change. +- [🔍] 68151071 — RdSAP-17.0 (semi-detached BUNGALOW single-storey, band H 1991-1995, cavity insulated, REGULAR boiler PCDB 17550 Worcester Greenstar 18Ri + cylinder size2/foam/50mm, pitched 200mm loft, suspended uninsulated floor, party wall 9.48 lodged, TFA 50) · eng 68 / elm 71 (lodged 70) · 🔍 FLAGGED — MAPPER GAP (cylinder insulation thickness dropped). engine -3 vs Elmhurst, -2 vs lodged; engine HW 3446 kWh vs Elmhurst 2911. ROOT CAUSE confirmed: raw cert lodges sap_heating.cylinder_insulation_thickness=50 but the RdSAP-17.0 mapper maps cylinder_size + cylinder_insulation_type only, leaving EpcPropertyData.sap_heating.cylinder_insulation_thickness_mm=None → engine assumes a poorly-insulated cylinder → over-counts HW cylinder loss → under-rates. FIX: carry cylinder_insulation_thickness → cylinder_insulation_thickness_mm in the RdSAP mapper (check blast radius across schemas + regression). engine-on-Elmhurst-inputs 71.34 ≈ worksheet 71 → calculator faithful (gap is purely the dropped input). build_68151071.py + evidence saved. +- [x] 100021985993 — SAP-16.2 (END-TERRACE BUNGALOW single-storey, band C, solid-brick internal insulation, combi PCDB 10328, double glazed, 100mm loft, suspended uninsulated floor) · eng 74 / elm 72 (lodged 70) · PINNED engine 74. Built in Elmhurst (end-terrace, boiler 10328, control CBE/2106, water from primary, party wall 6.89 solid masonry — Elmhurst requires non-zero for an end-terrace). +2 vs Elmhurst = documented 16.2 reduced-field party-wall gap (gov-API lodges no party_wall_length → engine models none; worksheet's only extra element is party wall 16.19m²×U0.25=4.05 W/K ≈ 2 SAP). engine-on-Elmhurst-PDF-inputs 67 is PDF-parser noise (HW over-parsed), not a calc bug. - [ ] 100020665611 — RdSAP-20.0.0 · eng 36 / lodged 37 - [⚠] 10093388044 — SAP-17.1 · eng 87 / lodged 93 · 🚩 heat-pump fuel-39 (flagged) - [ ] 10090944225 — SAP-17.0 · eng 81 / lodged 82 diff --git a/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-17.0/uprn_68151071/elmhurst_summary.pdf b/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-17.0/uprn_68151071/elmhurst_summary.pdf new file mode 100644 index 00000000..f30539c7 Binary files /dev/null and b/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-17.0/uprn_68151071/elmhurst_summary.pdf differ diff --git a/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-17.0/uprn_68151071/elmhurst_worksheet.pdf b/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-17.0/uprn_68151071/elmhurst_worksheet.pdf new file mode 100644 index 00000000..ed80445d Binary files /dev/null and b/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-17.0/uprn_68151071/elmhurst_worksheet.pdf differ diff --git a/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-18.0/uprn_10022893721/elmhurst_summary.pdf b/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-18.0/uprn_10022893721/elmhurst_summary.pdf new file mode 100644 index 00000000..8b4bdb29 Binary files /dev/null and b/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-18.0/uprn_10022893721/elmhurst_summary.pdf differ diff --git a/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-18.0/uprn_10022893721/elmhurst_worksheet.pdf b/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-18.0/uprn_10022893721/elmhurst_worksheet.pdf new file mode 100644 index 00000000..2a7cab92 Binary files /dev/null and b/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-18.0/uprn_10022893721/elmhurst_worksheet.pdf differ diff --git a/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-21.0.1/uprn_10023443426/elmhurst_summary.pdf b/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-21.0.1/uprn_10023443426/elmhurst_summary.pdf new file mode 100644 index 00000000..5989896d Binary files /dev/null and b/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-21.0.1/uprn_10023443426/elmhurst_summary.pdf differ diff --git a/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-21.0.1/uprn_10023443426/elmhurst_worksheet.pdf b/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-21.0.1/uprn_10023443426/elmhurst_worksheet.pdf new file mode 100644 index 00000000..89d57324 Binary files /dev/null and b/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-21.0.1/uprn_10023443426/elmhurst_worksheet.pdf differ diff --git a/backend/epc_api/json_samples/real_life_examples/SAP-Schema-16.2/uprn_100021985993/elmhurst_summary.pdf b/backend/epc_api/json_samples/real_life_examples/SAP-Schema-16.2/uprn_100021985993/elmhurst_summary.pdf new file mode 100644 index 00000000..b9f25bb4 Binary files /dev/null and b/backend/epc_api/json_samples/real_life_examples/SAP-Schema-16.2/uprn_100021985993/elmhurst_summary.pdf differ diff --git a/backend/epc_api/json_samples/real_life_examples/SAP-Schema-16.2/uprn_100021985993/elmhurst_worksheet.pdf b/backend/epc_api/json_samples/real_life_examples/SAP-Schema-16.2/uprn_100021985993/elmhurst_worksheet.pdf new file mode 100644 index 00000000..53e01bf9 Binary files /dev/null and b/backend/epc_api/json_samples/real_life_examples/SAP-Schema-16.2/uprn_100021985993/elmhurst_worksheet.pdf differ diff --git a/backend/epc_api/json_samples/real_life_examples/SAP-Schema-16.2/uprn_100090182288/elmhurst_summary.pdf b/backend/epc_api/json_samples/real_life_examples/SAP-Schema-16.2/uprn_100090182288/elmhurst_summary.pdf new file mode 100644 index 00000000..4e192c05 Binary files /dev/null and b/backend/epc_api/json_samples/real_life_examples/SAP-Schema-16.2/uprn_100090182288/elmhurst_summary.pdf differ diff --git a/backend/epc_api/json_samples/real_life_examples/SAP-Schema-16.2/uprn_100090182288/elmhurst_worksheet.pdf b/backend/epc_api/json_samples/real_life_examples/SAP-Schema-16.2/uprn_100090182288/elmhurst_worksheet.pdf new file mode 100644 index 00000000..65951fc4 Binary files /dev/null and b/backend/epc_api/json_samples/real_life_examples/SAP-Schema-16.2/uprn_100090182288/elmhurst_worksheet.pdf differ diff --git a/backend/epc_api/json_samples/real_life_examples/SAP-Schema-17.1/uprn_10091568921/elmhurst_summary.pdf b/backend/epc_api/json_samples/real_life_examples/SAP-Schema-17.1/uprn_10091568921/elmhurst_summary.pdf new file mode 100644 index 00000000..29011dd1 Binary files /dev/null and b/backend/epc_api/json_samples/real_life_examples/SAP-Schema-17.1/uprn_10091568921/elmhurst_summary.pdf differ diff --git a/backend/epc_api/json_samples/real_life_examples/SAP-Schema-17.1/uprn_10091568921/elmhurst_worksheet.pdf b/backend/epc_api/json_samples/real_life_examples/SAP-Schema-17.1/uprn_10091568921/elmhurst_worksheet.pdf new file mode 100644 index 00000000..fb3d7f9e Binary files /dev/null and b/backend/epc_api/json_samples/real_life_examples/SAP-Schema-17.1/uprn_10091568921/elmhurst_worksheet.pdf differ diff --git a/backend/epc_api/json_samples/real_life_examples/SAP-Schema-17.1/uprn_10093115480/elmhurst_summary.pdf b/backend/epc_api/json_samples/real_life_examples/SAP-Schema-17.1/uprn_10093115480/elmhurst_summary.pdf new file mode 100644 index 00000000..f1e38755 Binary files /dev/null and b/backend/epc_api/json_samples/real_life_examples/SAP-Schema-17.1/uprn_10093115480/elmhurst_summary.pdf differ diff --git a/backend/epc_api/json_samples/real_life_examples/SAP-Schema-17.1/uprn_10093115480/elmhurst_worksheet.pdf b/backend/epc_api/json_samples/real_life_examples/SAP-Schema-17.1/uprn_10093115480/elmhurst_worksheet.pdf new file mode 100644 index 00000000..dd214b03 Binary files /dev/null and b/backend/epc_api/json_samples/real_life_examples/SAP-Schema-17.1/uprn_10093115480/elmhurst_worksheet.pdf differ diff --git a/backend/epc_api/json_samples/real_life_examples/SAP-Schema-17.1/uprn_10093412452/elmhurst_summary.pdf b/backend/epc_api/json_samples/real_life_examples/SAP-Schema-17.1/uprn_10093412452/elmhurst_summary.pdf new file mode 100644 index 00000000..0cdfe5af Binary files /dev/null and b/backend/epc_api/json_samples/real_life_examples/SAP-Schema-17.1/uprn_10093412452/elmhurst_summary.pdf differ diff --git a/backend/epc_api/json_samples/real_life_examples/SAP-Schema-17.1/uprn_10093412452/elmhurst_worksheet.pdf b/backend/epc_api/json_samples/real_life_examples/SAP-Schema-17.1/uprn_10093412452/elmhurst_worksheet.pdf new file mode 100644 index 00000000..818e3b50 Binary files /dev/null and b/backend/epc_api/json_samples/real_life_examples/SAP-Schema-17.1/uprn_10093412452/elmhurst_worksheet.pdf differ diff --git a/backend/epc_api/json_samples/real_life_examples/SAP-Schema-17.1/uprn_10093718424/elmhurst_summary.pdf b/backend/epc_api/json_samples/real_life_examples/SAP-Schema-17.1/uprn_10093718424/elmhurst_summary.pdf new file mode 100644 index 00000000..01f488b6 Binary files /dev/null and b/backend/epc_api/json_samples/real_life_examples/SAP-Schema-17.1/uprn_10093718424/elmhurst_summary.pdf differ diff --git a/backend/epc_api/json_samples/real_life_examples/SAP-Schema-17.1/uprn_10093718424/elmhurst_worksheet.pdf b/backend/epc_api/json_samples/real_life_examples/SAP-Schema-17.1/uprn_10093718424/elmhurst_worksheet.pdf new file mode 100644 index 00000000..9e212df0 Binary files /dev/null and b/backend/epc_api/json_samples/real_life_examples/SAP-Schema-17.1/uprn_10093718424/elmhurst_worksheet.pdf differ diff --git a/domain/sap10_calculator/rdsap/cert_to_inputs.py b/domain/sap10_calculator/rdsap/cert_to_inputs.py index cd146109..0c555161 100644 --- a/domain/sap10_calculator/rdsap/cert_to_inputs.py +++ b/domain/sap10_calculator/rdsap/cert_to_inputs.py @@ -2579,9 +2579,13 @@ def _hot_water_fuel_cost_gbp_per_kwh( billed at the high rate, the remainder at the low rate. Without it the immersion HW billed 100% at the off-peak low rate, under-costing the dwelling and over-rating it (median +0.98 SAP across the off-peak - WHC-903 API cohort). Needs `cylinder_volume_l` + `occupancy_n` + - `immersion_single`; absent any of them (no cylinder / volume not - resolvable) it falls back to the 100%-low-rate scalar. + WHC-903 API cohort). Needs `cylinder_volume_l` + `occupancy_n`; when + `immersion_single` is unlodged (None) this branch is on an off-peak / + dual meter, so per RdSAP 10 §10.5 (PDF p.54) the immersion is assumed + DUAL rather than falling back to the 100%-low-rate scalar (Elmhurst + applies the dual blend, e.g. uprn_10022893721 high-rate fraction + 0.1386). Only an unresolvable cylinder volume / occupancy now falls + back to the 100%-low-rate scalar. `inherit_main_for_community_heating`: per S0380.173, when WHC ∈ {901, 902, 914} AND main is a heat network, ignore the cert- @@ -2610,13 +2614,22 @@ def _hot_water_fuel_cost_gbp_per_kwh( water_heating_code == _WHC_ELECTRIC_IMMERSION and cylinder_volume_l is not None and occupancy_n is not None - and immersion_single is not None ): + # RdSAP 10 §10.5 (PDF p.54): an immersion is assumed DUAL on a dual / + # off-peak meter. This branch is only reached on an off-peak tariff + # (tariff is not STANDARD ⇒ the meter is dual / Economy-7), so when the + # cert does not lodge `immersion_heating_type` default to dual rather + # than dropping to the 100%-low-rate fallback. The fallback under-costs + # the DHW and over-rates the dwelling, whereas Elmhurst applies the + # Table 13 dual blend (e.g. uprn_10022893721 high-rate fraction 0.1386). + effective_single = ( + immersion_single if immersion_single is not None else False + ) high_rate, low_rate = _tariff_high_low_rates_p_per_kwh(tariff) high_frac = electric_dhw_high_rate_fraction( cylinder_volume_l=cylinder_volume_l, occupancy_n=occupancy_n, - single_immersion=immersion_single, + single_immersion=effective_single, tariff=tariff, ) blended = high_frac * high_rate + (1.0 - high_frac) * low_rate diff --git a/scripts/hyde/elmhurst_download.py b/scripts/hyde/elmhurst_download.py index 3ef55011..ed76b8fd 100644 --- a/scripts/hyde/elmhurst_download.py +++ b/scripts/hyde/elmhurst_download.py @@ -28,7 +28,7 @@ SESSION_DIR = HERE / ".elmhurst-session" SAMPLE_DIR = ( HERE.parent.parent / "backend/epc_api/json_samples/real_life_examples" - / "RdSAP-Schema-20.0.0/uprn_10090844932" + / "RdSAP-Schema-17.0/uprn_68151071" ) ASSESSMENT_GUID = "B44A0DB4-4C08-4241-B818-86F060172105" diff --git a/tests/domain/sap10_calculator/rdsap/test_cert_to_inputs.py b/tests/domain/sap10_calculator/rdsap/test_cert_to_inputs.py index d8934c16..d7eba89a 100644 --- a/tests/domain/sap10_calculator/rdsap/test_cert_to_inputs.py +++ b/tests/domain/sap10_calculator/rdsap/test_cert_to_inputs.py @@ -682,6 +682,55 @@ def test_integrated_storage_heater_408_bills_table_12a_grid1_high_rate_fraction( assert abs(inputs.space_heating_fuel_cost_gbp_per_kwh - 0.07458) <= 1e-9 +def test_off_peak_immersion_unlodged_type_defaults_to_dual_table13_blend() -> None: + # Arrange — electric immersion DHW (WHC 903) on a Dual / Economy-7 (7-hour) + # meter with a cylinder present (Normal / 110 L), but the cert does NOT lodge + # `immersion_heating_type`. RdSAP 10 §10.5 (PDF p.54) assumes a DUAL immersion + # on a dual / off-peak meter, so the hot-water scalar rate must be the SAP + # 10.2 Table 13 DUAL high/low blend (a small high-rate fraction at 15.29 p + + # the remainder at 5.50 p), NOT the 100%-low-rate (5.50 p) fallback that an + # unlodged type previously triggered. The fallback under-costs the DHW and + # over-rates the dwelling — cf. uprn_10022893721, whose Elmhurst worksheet + # bills the immersion at a 0.1386 high-rate fraction, not 100% low. + from dataclasses import replace + + main = MainHeatingDetail( + has_fghrs=False, + main_fuel_type=29, # electricity + heat_emitter_type=0, + emitter_temperature=1, + main_heating_control=2401, + main_heating_category=7, # electric storage heaters + sap_main_heating_code=401, + ) + base = make_minimal_sap10_epc( + total_floor_area_m2=_TYPICAL_TFA_M2, + habitable_rooms_count=4, + country_code="ENG", + has_hot_water_cylinder=True, + sap_building_parts=[make_building_part(construction_age_band="E")], + sap_heating=make_sap_heating( + main_heating_details=[main], + water_heating_code=903, # electric immersion + water_heating_fuel=29, # off-peak electricity + cylinder_size=2, # Normal / 110 L + immersion_heating_type=None, # UNLODGED -> assume dual on off-peak + ), + ) + epc = replace( + base, + sap_energy_source=replace(base.sap_energy_source, meter_type="1"), # Dual + ) + + # Act + inputs = cert_to_inputs(epc) + + # Assert — the Table 13 DUAL blend applied, not the 100%-low-rate fallback. + rate = inputs.hot_water_fuel_cost_gbp_per_kwh + assert rate > 0.0550 + 1e-6 # NOT the 100%-low-rate bug value (5.50 p/kWh) + assert rate < 0.0900 # a small DUAL high-rate fraction, not single (~11 p) + + def test_non_integrated_storage_heater_bills_100_percent_low_rate() -> None: # Arrange — same off-peak storage cert but SAP code 401 ("other storage # heaters"): Table 12a Grid 1 gives a 0.00 high-rate fraction → the heat diff --git a/tests/domain/sap10_calculator/test_real_cert_sap_accuracy.py b/tests/domain/sap10_calculator/test_real_cert_sap_accuracy.py index 7deb4449..935715a8 100644 --- a/tests/domain/sap10_calculator/test_real_cert_sap_accuracy.py +++ b/tests/domain/sap10_calculator/test_real_cert_sap_accuracy.py @@ -364,6 +364,152 @@ _EXPECTATIONS: Final[tuple[RealCertExpectation, ...]] = ( cert_num="0646-3008-6208-0619-6204", sap_score=78, ), + # UPRN 100090182288 → cert 0068-1092-7237-0157-9954. SAP-Schema-16.2 — + # reduced-field SEMI-DETACHED HOUSE, 2-storey, band D (1950-1966), filled + # cavity, pitched roof 250 mm, solid uninsulated floor, mains-gas COMBI (PCDB + # 10327 Vaillant Ecotec Plus 831), double glazed, TFA 73.4 m². Lodged 71; + # engine 71. Built in Elmhurst RdSAP10 (evidence saved): Elmhurst worksheet 69. + # The +2 (engine 71 vs Elmhurst 69) is the documented 16.2 reduced-field + # PARTY-WALL gap: gov-API 16.2 lodges no party_wall_length → the engine models + # NO party wall, but Elmhurst requires one for a semi (entered the geometry- + # derived 4.28 m, filled cavity). Calculator confirmed faithful: engine on + # Elmhurst's own parsed inputs (which include the party wall) = 69.35 ≈ worksheet + # 69. PINNED to the observed 71 (= lodged) — mapping untuned; the Elmhurst delta + # is the reduced-field missing-party-wall-data gap, not a calculator bug. + RealCertExpectation( + schema="SAP-Schema-16.2", + sample="uprn_100090182288", + cert_num="0068-1092-7237-0157-9954", + sap_score=71, + ), + # UPRN 100021985993 → cert 9658-2087-7277-0667-1910. SAP-Schema-16.2 — + # reduced-field END-TERRACE BUNGALOW, single-storey, band C (1930-1949), + # solid-brick internal insulation, pitched roof 100 mm loft, suspended + # uninsulated floor, mains-gas COMBI (PCDB 10328 Vaillant Ecotec Pro 28), + # double glazed, TFA 76.54 m². Lodged 70; engine 74. Built in Elmhurst RdSAP10 + # (evidence saved): Elmhurst worksheet 72. The +2 (engine 74 vs Elmhurst 72) is + # the documented 16.2 reduced-field PARTY-WALL gap: gov-API 16.2 lodges no + # party_wall_length → the engine models NO party wall, but Elmhurst requires one + # for an end-terrace (entered the geometry-derived 6.89 m, solid masonry; the + # worksheet's only extra heat-loss element is the party wall at 16.19 m² × U0.25 + # = 4.05 W/K ≈ the 2-SAP gap). Calculator faithful: engine-on-gov-API 74 minus + # that omitted party-wall heat loss ≈ 72 = worksheet. (Engine on Elmhurst's + # PDF-parsed inputs = 67 here — lower than the worksheet — is PDF-parser noise: + # hot water over-parsed to 2998 kWh/£684; not a calculator divergence.) PINNED + # to the observed 74 — mapping untuned; the Elmhurst delta is the reduced-field + # missing-party-wall-data gap, not a calculator bug. + RealCertExpectation( + schema="SAP-Schema-16.2", + sample="uprn_100021985993", + cert_num="9658-2087-7277-0667-1910", + sap_score=74, + ), + # UPRN 10091568921 → cert 8806-5635-1239-5807-8283. SAP-Schema-17.1 — full-SAP + # END-TERRACE HOUSE, 2-storey, 2018 (band L), measured U walls 0.18 / roof 0.10 + # / floor 0.15, mains-gas COMBI (PCDB 17615 Potterton Promax Ultra Combi 28, + # 88.5%), double glazed, party wall 40.56 m², natural vent + 3 extract fans, + # AP50 4.45 (tested), 20 LED outlets, TFA 78.7 m². has_hot_water_cylinder lodged + # true but cylinder detail all None + PCDB combi → built as combi (water from + # primary). Lodged 85; engine 82. Built in Elmhurst RdSAP10 (evidence saved): + # worksheet 80. The +2 (engine 82 vs Elmhurst 80) is the documented full-SAP→ + # RdSAP residual (cert's measured U-values beat RdSAP band-L age defaults). + # Calculator confirmed faithful: engine on Elmhurst's own parsed inputs = 80.16 + # ≈ worksheet 80. PINNED to the observed 82 — mapping untuned. (Control: cert + # 2110 time+temp zone vs Elmhurst CBE 2106; sub-0.5 SAP, in the noise.) + RealCertExpectation( + schema="SAP-Schema-17.1", + sample="uprn_10091568921", + cert_num="8806-5635-1239-5807-8283", + sap_score=82, + ), + # UPRN 10093718424 → cert 0369-3892-7678-2690-7475. SAP-Schema-17.1 — full-SAP + # SEMI-DETACHED HOUSE, 2-storey, band L, measured U walls 0.25 / roof 0.13 / + # floor 0.16, mains-gas COMBI (PCDB 17615 Potterton Promax Ultra Combi 28), + # double glazed, party wall 40.89 m², natural vent + 3 extract fans, AP50 4.18 + # (tested), 20 LED outlets, TFA 79.9 m². Sibling of uprn_10091568921 (same + # development/boiler); built combi (cylinder lodged true but combi PCDB). + # Lodged 84; engine 81. Built in Elmhurst RdSAP10 (evidence saved): worksheet + # 80. The +1 (engine 81 vs Elmhurst 80) is the documented full-SAP→RdSAP + # residual. Calculator faithful: engine on Elmhurst's own parsed inputs = 80.12 + # ≈ worksheet 80. PINNED to the observed 81 — mapping untuned. + RealCertExpectation( + schema="SAP-Schema-17.1", + sample="uprn_10093718424", + cert_num="0369-3892-7678-2690-7475", + sap_score=81, + ), + # UPRN 10022893721 → cert 8078-7422-5930-5662-8922. RdSAP-Schema-18.0 — GROUND- + # FLOOR FLAT, band I (1996-2002), cavity insulated, ELECTRIC STORAGE HEATERS + # (SAP code 402 = SEB Modern slimline, manual charge control 2401) + electric + # immersion off-peak (dual-rate) hot water with cylinder (Normal/110 L, foam + # 50 mm), party wall 21.48 m, TFA 54.29. First non-boiler corpus cert. Lodged + # 79; engine 79. Built in Elmhurst RdSAP10 (evidence saved): worksheet 81 — + # engine -2, within tolerance; engine on Elmhurst's own parsed inputs = 78.76 ≈ + # engine 79 → calculator faithful. The Economy-7 off-peak pricing is CORRECT + # (Table 12a/13 split, fixed in PR #1217): storage SH high-rate fraction 0.00 → + # 100% low rate is the spec value, and immersion HW takes the Table 13 blend. + # (An earlier build mistakenly left Elmhurst's meter on Single/Standard, pricing + # at 13.19p → bogus worksheet 66; corrected by setting the Dual meter. NOT an + # engine bug.) PINNED to the observed engine 79 = lodged. + RealCertExpectation( + schema="RdSAP-Schema-18.0", + sample="uprn_10022893721", + cert_num="8078-7422-5930-5662-8922", + sap_score=79, + ), + # UPRN 10023443426 → cert 4106-3336-4002-1402-2202. RdSAP-Schema-21.0.1 (the + # engine's NATIVE schema) — END-TERRACE HOUSE, 2-storey, band L, cavity + # insulated, mains-gas COMBI (PCDB 17045 Ideal Logic Combi ES35), 300 mm loft, + # solid insulated floor, party wall 9.2 m (lodged), 11 double-glazed windows + # (~10.3 m²), 9 LED, mains-gas room-heater SECONDARY (SAP code 612, seasonal + # efficiency 0.20 per Table 4a — a low-efficiency decorative/old gas fire), + # TFA 98. Engine 76 = lodged 76 EXACTLY (and reproduces every component: space + # 6129, main fuel 6247, HW 2752, CO2 2232 kWh/kg) — the authoritative validation + # for a native-schema cert. Built in Elmhurst RdSAP10 (evidence saved): + # worksheet 79. The +3 is a BUILD gap, not a calculator error — my Elmhurst + # build omitted the lodged secondary gas fire (engine models it at 3065 kWh = + # 0.1 fraction ÷ 0.20 eff; Elmhurst secondary 0). engine on Elmhurst's own + # parsed inputs (also no secondary) = 79.29 ≈ worksheet 79 → calculator faithful + # to its inputs. PINNED to the observed engine 76 = lodged; mapping untuned. + RealCertExpectation( + schema="RdSAP-Schema-21.0.1", + sample="uprn_10023443426", + cert_num="4106-3336-4002-1402-2202", + sap_score=76, + ), + # UPRN 10093412452 → cert 8306-7575-5832-6507-9803. SAP-Schema-17.1 — full-SAP + # END-TERRACE HOUSE, 2-storey, band L, measured U walls 0.25 / roof 0.13 / + # floor 0.16, mains-gas COMBI (PCDB 17615 Potterton Promax Ultra Combi 28), + # double glazed (~12.3 m²), party wall 41.01 m², natural vent + 3 extract fans, + # AP50 4.62, 20 LED, no secondary, TFA 79.8 m². Same Emsworth development as the + # 10091568921 / 10093718424 siblings (cylinder lodged true but combi PCDB → + # built combi). Lodged 84; engine 81. Built in Elmhurst RdSAP10 (evidence + # saved): worksheet 80. The +1 (engine 81 vs Elmhurst 80) is the documented + # full-SAP→RdSAP residual; engine on Elmhurst's own parsed inputs = 79.91 ≈ + # worksheet 80 → calculator faithful. PINNED to the observed 81 — mapping + # untuned. + RealCertExpectation( + schema="SAP-Schema-17.1", + sample="uprn_10093412452", + cert_num="8306-7575-5832-6507-9803", + sap_score=81, + ), + # UPRN 10093115480 → cert 8393-7438-5230-3319-1996. SAP-Schema-17.1 — full-SAP + # END-TERRACE BUNGALOW (single-storey), 2016 (band L), measured U walls 0.19 / + # roof 0.11 / floor 0.12, mains-gas COMBI (PCDB 16841 Vaillant ecoTEC plus 824), + # double glazed (~11.9 m²), party wall 10.99 m² (CF filled, U≈0), natural vent + + # 2 extract fans, AP50 3.85, 9 LED, TFA 56 m². Lodged 81; engine 81 (EXACT + # match). Built in Elmhurst RdSAP10 (evidence saved): worksheet 78. The +3 + # (engine 81 vs Elmhurst 78) is the documented full-SAP→RdSAP residual — the + # cert's measured U-values beat Elmhurst's RdSAP band-L age defaults; engine on + # Elmhurst's own parsed inputs = 78.29 ≈ worksheet 78 → calculator faithful. + # PINNED to the observed engine 81 = lodged — mapping untuned. + RealCertExpectation( + schema="SAP-Schema-17.1", + sample="uprn_10093115480", + cert_num="8393-7438-5230-3319-1996", + sap_score=81, + ), )