mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
docs: handover for golden-cert mapper/cascade bugs (roof S0380.210 + community fuel collision)
Records post-S0380.209 state: 0240 verdict (true SAP 72, register 73 = unpreserved 2013+ pump, proven 0=Unknown via 13 pairs), and three open threads — roof Ext1 "insulated (assumed)" U over-count (needs case 11 worksheet), community fuel-code collision (API 18-25 vs Table-12 biomass 18-25; cert 9390 CO2 6x low; needs 9390 worksheet), and 0390 +7 demand-side gap. Plus the audit table of all 5 non-zero-SAP golden certs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
844fc22f67
commit
58ff7d8881
1 changed files with 180 additions and 0 deletions
180
domain/sap10_calculator/docs/HANDOVER_MAPPER_BUGS.md
Normal file
180
domain/sap10_calculator/docs/HANDOVER_MAPPER_BUGS.md
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
# Handover — golden-cert mapper/cascade bugs (post-0240 wall fix)
|
||||
|
||||
Point-in-time note. Start from [`AGENT_GUIDE.md`](AGENT_GUIDE.md) for methodology,
|
||||
the 1e-4 bar, the per-line debugging loop, and the suite command. This records the
|
||||
state after closing the 0240 investigation and fixing the first of several
|
||||
API-mapper/cascade bugs the audit surfaced.
|
||||
|
||||
- **Branch:** `feature/per-cert-mapper-validation`
|
||||
- **HEAD:** `844fc22f` (S0380.209). Confirm with `git rev-parse HEAD`.
|
||||
- **Baseline:** `2383 passed, 1 skipped, 0 failed` (AGENT_GUIDE §4 suite command).
|
||||
- **Next slice number:** **S0380.210**.
|
||||
|
||||
---
|
||||
|
||||
## What this session shipped
|
||||
|
||||
| slice | what |
|
||||
|---|---|
|
||||
| **S0380.208** | Promoted **simulated case 7** (combi swap of case 6) to an e2e fixture. PROVED the condensing-oil-combi (SAP code 130, no cylinder, combi instantaneous DHW, Eq D1, Table 3a keep-hot) path is **exact at 1e-4** with zero calculator changes → exonerated the heating as the source of 0240's residual. |
|
||||
| **S0380.209** | Fixed the **API-path wall U** "as built, insulated (assumed)" bug — routes to the as-built age-band row, not the 50 mm retrofit bucket. New `_described_as_retrofit_insulated` in `heat_transmission.py`. Worksheet-validated by case 9 (sandstone J → 0.35) + case 10 (solid brick J → 0.35). Re-pinned 0240 PE +1.8687 → +5.5044, CO2 +0.0907 → +0.2757 (SAP integer 72 unchanged). |
|
||||
|
||||
Both also carry a memory: [[project_case7_combi_exonerated]], [[project_as_built_insulated_assumed_bug]].
|
||||
|
||||
---
|
||||
|
||||
## The 0240 verdict — RESOLVED, not closable to 73
|
||||
|
||||
**0240's true SAP is 72**, proven three independent ways:
|
||||
- Elmhurst **case 8** (pump=Unknown, 0240's actual lodged value) → worksheet SAP **72**.
|
||||
- Elmhurst **case 9** (correct sandstone wall 0.35 + sloping roof 0.25) → SAP **72**.
|
||||
- Our cascade with **both** the wall (done) and roof (pending) bugs fixed → SAP cont **72.31**.
|
||||
|
||||
The register's **73 requires a "2013 or later" circulation pump (41 kWh)** — Elmhurst
|
||||
**case 7** with that pump = 73. But 0240's API lodges `central_heating_pump_age=0`
|
||||
= **Unknown** → 115 kWh. The encoding was **proven from 13 API+Summary pairs**:
|
||||
`0`="Unknown" (9 pairs), `2`="2013 or later" (4 pairs). The open-data export did not
|
||||
preserve the pump age that produced the lodged 73. **It is genuinely unreachable from
|
||||
the JSON.** Do not chase 0240 to 73; re-pin to its correct 72 once the roof lands.
|
||||
|
||||
`pump_age` enum (verified): `0`=Unknown→115, `1`=Pre-2013→165, `2`=2013+→41
|
||||
(`_TABLE_4F_CIRCULATION_PUMP_KWH_BY_AGE` in `cert_to_inputs.py`).
|
||||
|
||||
---
|
||||
|
||||
## THE METHODOLOGY LESSON FROM THIS SESSION (read this)
|
||||
|
||||
The 0240 baseline (cont 72.39) was **two offsetting bugs**: a wall U **under**-count
|
||||
(0.25 vs 0.35, less loss) masking an Ext1 roof U **over**-count (0.68 vs ~0.25, more
|
||||
loss). Fixing one alone moves the residual the "wrong" way — fix both
|
||||
([[feedback_software_no_special_handling]]). And: **Elmhurst is the arbiter, not the
|
||||
spec text** — twice this session a confident spec/first-principles read was wrong and
|
||||
a generated worksheet settled it (`NI`=not-indicated not "none"; pump `0`=Unknown).
|
||||
**Generate a worksheet rather than guess a U-value or factor.** The user generates
|
||||
Elmhurst worksheets readily (cases 7–10 done) — ask for one.
|
||||
|
||||
---
|
||||
|
||||
## OPEN THREAD 1 — Roof fix (S0380.210), needs **case 11**
|
||||
|
||||
Same root cause as the wall (`project_as_built_insulated_assumed_bug`): the API mapper
|
||||
populates `epc.roofs` with "Pitched, insulated (assumed)"; `u_roof`
|
||||
([`rdsap_uvalues.py:708`](../../sap10_ml/rdsap_uvalues.py)) routes
|
||||
`insulation_thickness_mm==0 and _described_as_insulated(description)` → **0.68**
|
||||
(retrofit 50 mm). 0240's Ext1 is `roof_construction=5` "vaulted ceiling", `NI` thickness,
|
||||
band J → cascade **0.68**, should be a Table 18 sloping-ceiling value.
|
||||
|
||||
**Why it's harder than the wall:**
|
||||
- Just swapping to `_described_as_retrofit_insulated` makes thickness-0 fall to the
|
||||
Table 16 ladder → **2.30** (uninsulated). `NI`/unknown must route to the age-band
|
||||
default, not 0 mm.
|
||||
- `u_roof` receives the **joined all-roofs description** (can't tell which call is the
|
||||
vaulted Ext1) and **no construction type** — needs the per-BP sloping/vaulted signal
|
||||
threaded through (heat_transmission already computes `is_flat_roof`; add an
|
||||
`is_sloping_ceiling`/vaulted flag similarly).
|
||||
- **Code-5 "vaulted" is not recognised as a sloping ceiling** — only code-8
|
||||
"sloping ceiling" is (`_api_resolve_sloping_ceiling_thickness` gates on `==8`;
|
||||
`heat_transmission` keys the cos(30°) area factor on the "sloping ceiling" substring).
|
||||
- **Value ambiguity (needs case 11):** Table 18 (RdSAP 10 p.45) band J gives
|
||||
col (1) joists/unknown **0.16**, col (2) **at rafters 0.20**, col (3) flat /
|
||||
"sloping ceiling + unheated space above" (footnote b) **0.25**. Case 9 (code-8
|
||||
sloping, Unknown) → Elmhurst **0.25**, but that may not equal code-5 vaulted (could
|
||||
be 0.20). All three give 0240 integer 72 — it's a PE-pin precision question.
|
||||
|
||||
**Generate case 11:** a **vaulted Ext1 roof (`roof_construction` vaulted), insulation
|
||||
Unknown/NI, band J**; report worksheet **`(30)` U** (0.20 or 0.25?). Then implement the
|
||||
roof fix to match, re-pin 0240 (both bugs fixed → cont ≈ 72.31, integer 72).
|
||||
|
||||
Simulated worksheet `(30)` decompositions seen so far: case 9 Ext1 sloping-Unknown =
|
||||
0.25; case 7/8/10 roofs are all 400 mm loft (col 1, 0.11) so they do NOT exercise the
|
||||
sloping path.
|
||||
|
||||
---
|
||||
|
||||
## OPEN THREAD 2 — Community fuel-code collision (cert 9390), needs a **9390 worksheet**
|
||||
|
||||
Cert **9390-2722-3520**: SAP +4 (calc 71 / lodged 67), PE ≈ matches (204 vs 205), but
|
||||
**CO2 0.44 t vs lodged 2.8 t**. Community scheme (`sap_main_heating_code=301`,
|
||||
`main_fuel_type=20`).
|
||||
|
||||
**Root cause:** `co2_factor_kg_per_kwh` / `unit_price_p_per_kwh` /
|
||||
`primary_energy_factor` in `tables/table_12.py` + `table_32.py` use an
|
||||
"accept-either-API-or-Table-12-code" lookup that checks the **Table-12 code first**:
|
||||
|
||||
```python
|
||||
if fuel_code in CO2_KG_PER_KWH: # API 20 IS ALSO a T12 code (wood logs, 0.028)
|
||||
return CO2_KG_PER_KWH[fuel_code] # ← returns 0.028, never translates
|
||||
translated = API_FUEL_TO_TABLE_12.get(fuel_code) # 20 → 51 (community gas, 0.21) — unreached
|
||||
```
|
||||
|
||||
API community fuels **18–25** (`API_FUEL_TO_TABLE_12`: 18→75,19→76,20→51,21→52,22→53,
|
||||
23→55,24→54,25→41) all **collide** with Table-12 codes 18–25 (biomass), so they silently
|
||||
get the biomass factor. The heat-network CO2 path
|
||||
(`cert_to_inputs.py` ~L2801-2819: `_co2_factor_kg_per_kwh(main) * scaling`) thus emits
|
||||
`0.028 × 1.25 (1/0.80 heat-source-eff) = 0.035` instead of `0.21 × 1.25`.
|
||||
|
||||
**Why the fix needs care (don't just translate):** a naive translate of the heat-network
|
||||
fuel via `API_FUEL_TO_TABLE_32` corrects CO2 (0.44 → 3.03 t, ≈ lodged 2.8) but
|
||||
**over-corrects PE** (204 → 219.9 vs lodged 205) and **doesn't move the SAP +4** (cost
|
||||
is a *separate* community issue — price/standing-charge/heat-source-eff scaling). The
|
||||
community scheme's declared PE/CO2/cost factors must be pinned against a **9390 Elmhurst
|
||||
worksheet** before committing. This touches the community-heating corpus (the "touchy"
|
||||
S0380.180-184 area — see [[project_heating_systems_corpus]]); run the heating-systems
|
||||
corpus test after any change.
|
||||
|
||||
The right long-term shape: translate API→Table-12 **explicitly** at the known-API-code
|
||||
boundary instead of the ambiguous T12-first "accept-either" — but verify it against the
|
||||
whole cohort.
|
||||
|
||||
---
|
||||
|
||||
## OPEN THREAD 3 — Cert 0390 +7 (demand-side), no worksheet needed to diagnose
|
||||
|
||||
**0390-2954-3640**: SAP +7 (calc 67 / lodged 60), PE 137 vs **165** (−28/m²), CO2 12.3 vs
|
||||
15. Large **360 m² age-F** detached. The boiler is **correctly** resolved (PCDB index
|
||||
**9005** = "Firebird S", 86.4% winter — a real modern oil-boiler retrofit; not a bug).
|
||||
The gap is a **demand-side under-count** (~−28 PE/m² = thousands of kWh). Suspects:
|
||||
cavity wall "as built, **partial** insulation (assumed)" routing; age-F roof "insulated
|
||||
(assumed)"; floor (solid, no insulation) 81 W/K; ventilation/TB. Walk the demand
|
||||
cascade (§2.4 helpers + the lodged register subsystem ratings) to localise. This is a
|
||||
known long-standing gap (see the cert's `notes:` in `test_golden_fixtures.py`).
|
||||
|
||||
---
|
||||
|
||||
## Full audit — all golden certs with non-zero SAP residual
|
||||
|
||||
| cert | SAP resid | diagnosis |
|
||||
|---|---|---|
|
||||
| 0390-2954-3640 | +7 | demand-side under-count (Thread 3); boiler eff correct |
|
||||
| 9390-2722-3520 | +4 | community fuel-code collision → CO2 6× low (Thread 2) |
|
||||
| 0240-0200-5706 | −1 | NOT a bug — unpreserved 2013+ pump; true SAP 72 |
|
||||
| 2130-1033-4050 | +1 | minor fabric precision (multi-part solid-brick wall); low value |
|
||||
| 7536-3827-0600 | +1 | minor fabric precision (multi-bp D/L/F cavity); low value |
|
||||
|
||||
All others pin at residual 0.
|
||||
|
||||
---
|
||||
|
||||
## Worksheets
|
||||
|
||||
- **Available** (user-generated, `sap worksheets/golden fixture debugging/simulated case N/`):
|
||||
case 7 (combi), case 8 (unknown pump), case 9 (sandstone wall + sloping roof),
|
||||
case 10 (solid-brick wall). Case 7's Summary is the only one mirrored into tracked
|
||||
fixtures (`backend/.../Summary_001431_case7.pdf`, used by the e2e pin).
|
||||
- **Needed:** **case 11** (vaulted Ext1 roof, NI, band J — Thread 1) and a **9390**
|
||||
worksheet (community PE/CO2/cost — Thread 2).
|
||||
|
||||
## Pointers
|
||||
- Golden pins + slice history: `tests/domain/sap10_calculator/rdsap/test_golden_fixtures.py`.
|
||||
- Wall fix: `domain/sap10_calculator/worksheet/heat_transmission.py`
|
||||
(`_described_as_retrofit_insulated`, the `wall_ins_present` gate) +
|
||||
`tests/.../worksheet/test_heat_transmission.py`.
|
||||
- Roof code: `domain/sap10_ml/rdsap_uvalues.py` `u_roof` (L708 + Table 18 dicts L637-668).
|
||||
- Community/heat-network CO2/PE/cost: `cert_to_inputs.py` ~L2749-2880, L1837
|
||||
(`_main_fuel_code`), `tables/table_12.py` + `table_32.py` (`co2_factor_kg_per_kwh`,
|
||||
`API_FUEL_TO_TABLE_12/32`).
|
||||
- Process: one slice = one commit, spec citation (page+line), AAA tests, `abs(x-y)<=tol`
|
||||
not `pytest.approx`, `Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>`.
|
||||
SAP 10.2 only. No tolerance widening. mapper.py + cert_to_inputs.py each carry 32
|
||||
pre-existing pyright errors; heat_transmission.py + rdsap_uvalues.py carry their own —
|
||||
baseline-compare with `git stash` for net-zero NEW.
|
||||
Loading…
Add table
Reference in a new issue