diff --git a/domain/sap10_calculator/docs/HANDOVER_POST_S0380_200.md b/domain/sap10_calculator/docs/HANDOVER_POST_S0380_200.md new file mode 100644 index 00000000..f3ad76cc --- /dev/null +++ b/domain/sap10_calculator/docs/HANDOVER_POST_S0380_200.md @@ -0,0 +1,154 @@ +# Handover — post S0380.200 (dual-main split done; boiler-interlock −5pp OPEN) + +Point-in-time note. Start from [`AGENT_GUIDE.md`](AGENT_GUIDE.md) for methodology, +accuracy bar, and pipeline — this records *what this session did* and *what is open*. + +- **Branch:** `feature/per-cert-mapper-validation` +- **HEAD:** `8ae978a6` (S0380.200) +- **Baseline:** `2355 passed, 1 skipped, 0 failed`. Verify with the AGENT_GUIDE §4 suite command. + +--- + +## What this session shipped (S0380.196–200) + +The through-line: **golden certs 6035 and 0240 were both closed to SAP-exact** +by finding real API-mapper bugs (not "lodged divergence"), each confirmed +against a user-generated Elmhurst worksheet ("simulated case 5/6"). + +| Slice | What | Spec | +|---|---|---| +| **.196** | API Simplified Type 1 room-in-roof: `room_in_roof_type_1` gables (length-only, no height) weren't deducted from the A_RR shell → whole shell billed as roof at U_RR=2.30 (+52 W/K). Route them through `detailed_surfaces` (gable area = L × 2.45 default RR storey height). **6035 SAP −2→+0 exact**, PE +19.16→+1.84. | RdSAP 10 §3.9.1(e) p.21; Table 4 p.22 | +| **.197** | Promoted "simulated case 5" (detached sandstone RR) to e2e fixture (`001431_case5`, 11 pins @1e-4). Fixed sandstone wall label `"SS"`→2 (`_ELMHURST_WALL_CODE_TO_SAP10`) + `_parse_thickness_mm` for "400+ mm" roof insulation (trailing `+` was dropped → u_roof fell to age default). | — | +| **.198** | **API `window_wall_type=4` → roof window.** These are roof-of-room rooflights; the mapper flattened them into `sap_windows` (vertical, (27), U=2.0) instead of `sap_roof_windows` ((27a), inclined U=2.30 + 45° solar). The inclined solar dominates → **0240 SAP −1→+0 exact**, PE +3.91→+1.95; 6035 PE +1.84→+1.37. Discriminator is `wall_type==4` NOT `window_type==2` (0390/7536 lodge window_type=2 on main walls). | SAP 10.2 §3 (27a); Table 6e Note 2 | +| **.199** | Site-notes mirror of .198: extractor parses "Roof of Room" window rows (`_parse_window_from_anchors`); `_is_elmhurst_roof_window` location branch; `_ELMHURST_ROOF_WINDOW_U_BY_GLAZING["Double between 2002 and 2021"]=2.30`. Case 6 pinned on §3 windows (`test_section_3_roof_windows_case6_match_pdf`): (27)=22.7408, (27a)=13.0375 exact. | RdSAP 10 §3.7 | +| **.200** | **SAP 10.2 §9a two-main-heating split** (203)/(204)/(205)/(207)/(213). The cascade lumped a 2-main dwelling into one system. Now `space_heating_fuel_monthly_kwh` splits demand (204) to sys1 @ (206) + (205) to sys2 @ (207); `_solve_month` sums main_1+main_2; `_main_heating_detail_efficiency` (new, the per-detail core of `_main_heating_efficiency`) gives each system its own efficiency. Site-notes: `_map_elmhurst_main_heating_2` inherits Main 1's fuel when §14.1 omits Fuel Type. Cost/CO2/PE main_2 paths were already wired. 0240 unchanged (identical Eq-D1 systems). | SAP 10.2 §9a | + +Two new e2e fixtures: `001431_case5` (full SapResult, S0380.197) and +`001431_case6` (§3 windows only, S0380.199 — see why below). Source PDFs +tracked under `sap worksheets/golden fixture debugging/simulated case {5,6}/`; +Summaries mirrored to `backend/documents_parser/tests/fixtures/Summary_001431_case{5,6}.pdf`. + +--- + +## OPEN (the priority) — boiler-interlock −5pp efficiency adjustment, per main system + +**Goal:** a RdSAP-10/SAP-10.2 **spec-accurate** implementation of the boiler +interlock efficiency adjustment, applied **per main heating system**, done in +the established pattern of this domain (per-line walk → cite spec → TDD → +re-pin). This is the last gap blocking full closure of simulated **case 6**, +and it will also re-pin golden **0240**. + +### The evidence (simulated case 6) + +`sap worksheets/golden fixture debugging/simulated case 6/` — detached, dual +**oil** boiler (both SAP code **127**, base seasonal eff **84%**), radiators 51% +(control **2106**) + underfloor 49% (control **2110**). Its P960 worksheet: + +| line | worksheet | meaning | +|---|---|---| +| (206) main sys-1 eff | **79.0** | 84 − **5pp** | +| (207) main sys-2 eff | **84.0** | base, no penalty | +| (216) water-heater eff | **72.0** | also penalised (DHW leg of the −5pp) | +| "Temperature adjustment" | 0.0000 | **flow temp has NO effect** — this is NOT a flow-temperature feature | + +Summary §14 lodges it explicitly: system 1 **"Boiler Interlock: No"**, system 2 +**"Boiler Interlock: Yes"**. The 84→79 is the SAP 10.2 **Table 4c(2)** "no boiler +interlock" −5pp **Space + DHW** adjustment (same mechanism as the AGENT_GUIDE +"oil 6" worked example, S0380.177 — but that one fired off control 2101). + +### Why control 2106 (which HAS a room thermostat) is "no interlock" + +Per RdSAP 10 boiler-interlock rules (find + cite the exact §; the existing +`_NO_INTERLOCK_CONTROLS = {2101, 2102}` block in `cert_to_inputs.py` ~line 1238 +quotes "RdSAP 10 §3 p.57: boiler interlock is assumed present if there is a room +thermostat and [time control], AND — when there is a hot-water cylinder — a +cylinder thermostat; otherwise not interlocked"): system 1 serves the **DHW +cylinder**, the cylinder is present (`Hot Water Cylinder Present: Yes`) but +**`Cylinder Thermostat: No`** → interlock **not** present → −5pp, *despite* the +room thermostat. System 2 (underfloor, separate part, no cylinder interaction) +keeps interlock via its zone control → no penalty. + +So the determination is **not** "control ∈ {2101,2102}". It is, per system: +`interlock present` ⇔ (room thermostat present, from the control code) AND +(time/programmer control) AND (cylinder absent OR cylinder thermostat present). +The current cascade only catches the "no room thermostat" path (2101/2102); it +misses the "room thermostat present but no cylinder thermostat" path that 2106 +hits here. + +### This single root cause explains BOTH remaining case-6 deltas + +- space heating: sys-1 eff 79 not 84 → main fuel cascade 14925 vs ws **14736.96** +- hot water: the −5pp DHW leg → cascade HW 4824 vs ws **4902.86** (lower cascade + fuel ⇒ cascade eff too high ⇒ missing the penalty) + +### 0240 will shift — and that is correct (apply the spec uniformly) + +Golden **0240** has the SAME controls (sys1 2106 / sys2 2110) AND the same +`cylinder_thermostat = "N"` with a cylinder present. So the spec-correct rule +applies the −5pp to 0240's system 1 too. 0240 is currently SAP-exact (continuous +72.55) **without** the penalty — that is an offsetting coincidence (it's API-only, +±0.5 bar, no worksheet). Per [[feedback-software-no-special-handling]] + +[[feedback-spec-floor-skepticism]]: implement the spec rule, let 0240 shift, and +**re-pin** it with a documented note. Expect 0240 continuous SAP to drop ~0.3–0.5 +(may take the integer 73→72; if so the golden `expected_sap_resid` moves −1 and +that is the new truth). Measure precisely and re-pin PE/CO2 too. + +### Where to implement (per-line walk first, then TDD) + +1. **Interlock determination.** Add a per-`MainHeatingDetail` helper, e.g. + `_boiler_interlock_present(main, epc) -> bool`, encoding the RdSAP 10 rule + above (room thermostat from control code + cylinder-thermostat gate when a + cylinder is present). `epc.sap_heating.cylinder_thermostat` ("Y"/"N") and + `cylinder_size`/`hot_water_cylinder_present` are the cylinder signals. The + site-notes path already lodges `cylinder_thermostat` (mapper.py ~5183, string + "Y"/"N"); the API path lodges it on `sap_heating.cylinder_thermostat` (0240 = + "N"). +2. **Apply Table 4c(2) −5pp per system.** The existing −5pp lives near the + `_NO_INTERLOCK_CONTROLS` block — find how it currently adjusts the seasonal + efficiency for 2101/2102 and generalise it to fire on + `not _boiler_interlock_present(main, epc)`, applied inside + `_main_heating_detail_efficiency` so **each** main system gets its own + adjustment (sys1 −5, sys2 0). Confirm the DHW leg (water-heater efficiency + (216)) is penalised too — the §4 water-heating cascade reads + `_main_heating_efficiency`; verify the −5pp flows there (case 6 (216)=72 + is the check). +3. **Verify combi vs regular rows of Table 4c(2).** The "no interlock" −5pp has a + combi row (Space −5 / DHW 0) and a regular-boiler row (Space −5 / DHW −5); + the DHW leg is gated on cylinder presence. Case 6 is a regular oil boiler with + a cylinder → DHW −5 applies (hence (216)=72). Read the table; don't assume. + +### Validation target + +After the fix, **promote case 6 to a full SapResult e2e fixture** (it's currently +§3-windows-only because the lumped efficiency made (211)/(219)/(231) non- +comparable). Case 6 worksheet Block-1 pin grid (P960-0001-001431): +- SAP 72 (258), continuous **71.6597**, ECF **2.0316** (257) +- total fuel cost **1162.5374** (255), CO2 **5953.6679** (272) +- (211) main sys-1 fuel **7741.6458**, (213) main sys-2 fuel **6995.3106** + (SapResult.main_heating_fuel_kwh_per_yr should be the sum **14736.9564**) +- hot water **4902.8601** (219), lighting **357.6571** (232) +- pumps/fans **356.0** (231) — **see the SECOND open item below** + +### SECONDARY open item — dual-system auxiliary pumps (Table 4f) + +Case 6 (231) = **356** = (230c) central-heating pump 156 + (230d) oil-boiler pump +200. Cascade gives **241**. Two boilers → two pump contributions per Table 4f +(note c: "where there are two main heating systems include two figures from this +table" — same note already used for the 0240 oil-pump in S0380.148). Needs the +per-system pump aggregation. Smaller than the interlock fix; do it after, then +case 6's (231) pin closes and the full e2e fixture lands. + +--- + +## Process notes +- One slice = one commit, spec citation (page + line) in the message, + `Co-Authored-By: Claude Opus 4.8 ` trailer. +- AAA tests (`# Arrange/# Act/# Assert`), `abs(x-y) <= tol` (not `pytest.approx`). +- New code passes `pyright` strict, 0 errors. (mapper.py + cert_to_inputs.py each + carry **32 pre-existing** errors — don't add to them; check with a `git stash` + baseline comparison.) +- The Elmhurst worksheet is ground truth at abs=1e-4. 0240 is API-only (±0.5 + fallback) — case 6 is its worksheet-backed proxy for the heating archetype, but + differs from 0240 on the boiler SAP code (127 vs 0240's 130 condensing combi), + so pin case 6 to ITS OWN worksheet, not 0240's register. +- Suite command + section/e2e harness layout: AGENT_GUIDE §2.6 + §4.