diff --git a/domain/sap10_calculator/docs/HANDOVER_CERT_0380_MIT_CASCADE.md b/domain/sap10_calculator/docs/HANDOVER_CERT_0380_MIT_CASCADE.md new file mode 100644 index 00000000..9c1635b5 --- /dev/null +++ b/domain/sap10_calculator/docs/HANDOVER_CERT_0380_MIT_CASCADE.md @@ -0,0 +1,208 @@ +# Handover — cert 0380 §N3.5 MIT cascade landed; PSR-formula residual + Layer 4 chain test deferred + +Branch `feature/per-cert-mapper-validation`. Picks up from +[`HANDOVER_CERT_0380_HW_CASCADE.md`](HANDOVER_CERT_0380_HW_CASCADE.md) +after a `/tdd` session shipped slices 102f-prep.1 through 102f-prep.6, +closing the §7 MIT cascade against worksheet (92) at 1e-3 per month +and dropping cert 0380's SAP residual from **+0.5999 → +0.0594** vs +worksheet 88.5104. + +## What landed this session (commits on branch) + +| Slice | Commit | What it did | +|---|---|---| +| **102f-prep.1** | 7adb6c79 | PCDB Table 362 `heating_duration_code` field. Format-465 position 48 holds "24" / "16" / "9" / "V"; cohort always "V" per SAP 10.2 footnote 48 (PDF p.105). | +| **102f-prep.2** | a6ef1987 | SAP 10.2 Table N5 PSR interpolation (PDF p.107) for variable-duration N24,9 / N16,9 annual totals. Clamps PSR ≤ 0.2 / ≥ 1.2 per spec. | +| **102f-prep.3** | 4e07991f | Cold-first day-allocation algorithm (Jan, Dec, Feb, Mar, Nov, Apr, Oct, May). N24,9 filled first, then N16,9 occupies remaining month days. | +| **102f-prep.4** | c341eba9 | SAP 10.2 Equation N5 blending leaf — `T = [N24,9 × Th + N16,9 × T_uni + (Nm − N16,9 − N24,9) × T_bi] / Nm`. | +| **102f-prep.5** | 2be79056 | Wire `extended_heating_days_per_month` kwarg through `mean_internal_temperature_monthly` + `cert_to_inputs`. HP-gated; non-HP certs identical. MIT 12-tuple lands at 1e-2 vs worksheet (92). | +| **102f-prep.6** | 80e528e5 | Gate §5 central-heating pump gains for HP certs per Table 4f (cert 0380's worksheet line 70 = 0.0 every month). MIT tightens to 1e-3. | + +## Cumulative state at session end + +Cert 0380 (Mitsubishi PUZ-WM50VHA, PCDB 104568, semi-detached +bungalow, age D, TFA 60.43 m², PSR ≈ 1.43): + +| Metric | Cascade | Worksheet target | Δ | +|---|---|---|---| +| MIT 12-tuple | matches | line (92) | **abs < 1e-3 per month** ✓ | +| (37) total fabric heat loss W/K | 96.0889 | 96.0889 | exact | +| (62) annual HW demand kWh/yr | 1502.16 | 1502.16 | exact at 1e-4 ✓ | +| (56)m Jan storage loss kWh/month | 36.9530 | 36.9530 | exact ✓ | +| (59)m Jan primary loss kWh/month | 43.3132 | 43.3132 | exact ✓ | +| useful space heating kWh/yr | 5351.85 | 5349.73 | +2.12 (0.04%) | +| HW kWh/yr | 878.05 | 877.97 | +0.08 | +| main_heating_efficiency (COP_space) | 2.2348 | 2.2305 | +0.0043 (0.2%) | +| **SAP continuous** | **88.5698** | **88.5104** | **+0.0594** | + +## Remaining +0.0594 SAP residual — root cause: PSR-formula divergence + +The cascade computes PSR per the spec PDF p.105 line 5956 ("the +dwelling's heat loss coefficient, worksheet (39), is multiplied by a +temperature difference of 24.2 K to provide the dwelling design heat +loss"): + +``` +PSR_cascade = max_output_kw × 1000 / (HLC_annual_avg × 24.2 K) + = 4390 / (127.1578 × 24.2) + = 1.4266 +``` + +Worksheet (206) η_space = **223.0480** back-solves to **PSR ≈ 1.4321** +(linear-interpolated from PCDB record 104568's η_space groups at PSR +1.2 = 253.9 and PSR 1.5 = 229.2). The 0.4% PSR drift propagates to a +0.2% η_space drift (cascade 223.48 vs worksheet 223.05), then to a +0.04 SAP drift via the (211) main-fuel cascade. η_water is far less +sensitive (1.5× slope vs η_space's 11×), so (217) lands at 1e-2 vs +worksheet 171.0746. + +The cascade's PSR formula is **spec-correct** — no other source in +SAP 10.2 or RdSAP 10 specifies a different formulation. Candidate +hypotheses (none confirmed): + +1. **PCDB max_output field** — Position 47 = 4.39 kW is "output power + at -4.7°C ambient" per the BRE web entry. The spec says "maximum + nominal output of the package" which may refer to a different + rating point. Try output @ 35°C flow temperature (PCDB position + may differ); 5.0 kW (nameplate) over-shoots significantly so + that's not it. +2. **Effective (39) for design** — Worksheet (39) annual avg lands at + 127.1578 W/K exactly; the spec says use this. But Elmhurst may + compute a heating-season-only or peak-month-weighted value. +3. **ΔT** — Spec is unambiguous at 24.2 K; older SAP versions (pre + Mar 2025 revision) may have used a slightly different value. + Worksheet was lodged against SAP 10.2 Feb 2022, before the most + recent spec revision. +4. **Rounding inside Elmhurst** — Worksheet might pre-round one of + max_output, HLC, or PSR to a different precision than the cascade. + +### Investigation pointers for next session + +- Pull the BRE web entry for PCDB 104568 (https://www.ncm-pcdb.org.uk/sap/pcdbdetails.jsp?type=362&id=104568) + and inspect labelled fields for an alternative output rating — + Output @ 35°C / Output @ 55°C / Heat output at design temp. +- Cross-check cert 0350 / 2225 / 2636 / 3800 / 9285 (same PCDB + 104568) for the same η_space (206) drift sign + magnitude — if + consistent ~0.4%, the bias is in the PSR formula, not cert- + specific. If variable, look for a per-cert-shape factor. +- Cert 9418 uses Daikin PCDB 102421 with a different output rating + — if its η_space drift differs, that's a strong signal the + max_output_kw field interpretation is the issue. +- Check `domain/sap10_calculator/tables/pcdb/data/pcdb10.dat` raw + row for 104568 (already inspected — fields 47 = 4.39 only obvious + output candidate). + +## Remaining slices (recommended next session) + +### 1. Close the PSR-formula divergence (BLOCKING for slice 102f) + +Until the +0.045 SAP residual closes, slice 102f's `assert abs(sap - +88.5104) < 1e-4` cannot pass. Investigation strategies above. Expect +a small spec correction or PCDB field reinterpretation to drop η_space +by 0.4% (worksheet alignment), closing both per-spec metrics +simultaneously. + +### 2. Slice 102f: Layer 4 chain test cert 0380 API at 1e-4 + +Once PSR closes: + +```python +def test_api_0380_full_chain_sap_matches_worksheet_pdf_exactly() -> None: + # Arrange + doc = json.loads(_API_0380_JSON.read_text()) + epc = EpcPropertyDataMapper.from_api_response(doc) + # Act + result = calculate_sap_from_inputs(cert_to_inputs(epc, prices=SAP_10_2_SPEC_PRICES)) + # Assert + assert abs(result.sap_score_continuous - 88.5104) < 1e-4 +``` + +### 3. Cohort closure: 6 remaining ASHP certs + +After cert 0380 closes: + +| Cert | PCDB | cylinder_size | Volume | Expected close | +|---|---|---|---|---| +| 0350-2968-2650-2796-5255 | 104568 | 3 | 160 L | 1 slice | +| 2225-3062-8205-2856-7204 | 104568 | 3 | 160 L | 1 slice | +| 2636-0525-2600-0401-2296 | 104568 | 3 | 160 L | 1 slice | +| 3800-8515-0922-3398-3563 | 104568 | 3 | 160 L | 1 slice | +| 9285-3062-0205-7766-7200 | 104568 | 3 | 160 L | 1 slice | +| 9418-3062-8205-3566-7200 | 102421 | 4 | 210 L | 1-2 slices | + +## Test baselines you should see + +```bash +PYTHONPATH=/workspaces/model python -m pytest \ + backend/documents_parser/tests/test_summary_pdf_mapper_chain.py \ + backend/documents_parser/tests/test_elmhurst_extractor.py \ + backend/documents_parser/tests/test_elmhurst_end_to_end.py \ + domain/sap10_calculator/worksheet/tests/test_e2e_elmhurst_sap_score.py \ + domain/sap10_calculator/worksheet/tests/test_water_heating.py \ + domain/sap10_calculator/worksheet/tests/test_mean_internal_temperature.py \ + domain/sap10_calculator/rdsap/tests/test_cert_to_inputs.py \ + domain/sap10_calculator/rdsap/tests/test_golden_fixtures.py \ + domain/sap10_calculator/tests/test_pcdb_table_362_lookup.py \ + domain/sap10_ml/tests/test_rdsap_uvalues.py \ + datatypes/epc/schema/tests/test_schema_loading.py \ + --no-cov -q +``` + +Expected: **651 pass + 10 pre-existing fails (9 cert 001479 + 1 FEE)**. +Closed certs 001479, 0330, 9501 remain GREEN on Layer 4 1e-4 chain gates. + +Probe state at HEAD: + +```bash +PYTHONPATH=/workspaces/model python -c " +import json +from pathlib import Path +from datatypes.epc.domain.mapper import EpcPropertyDataMapper +from domain.sap10_calculator.rdsap.cert_to_inputs import cert_to_inputs, SAP_10_2_SPEC_PRICES +from domain.sap10_calculator.calculator import calculate_sap_from_inputs +doc = json.loads(Path('/workspaces/model/domain/sap10_calculator/rdsap/tests/fixtures/golden/0380-2471-3250-2596-8761.json').read_text()) +epc = EpcPropertyDataMapper.from_api_response(doc) +inputs = cert_to_inputs(epc, prices=SAP_10_2_SPEC_PRICES) +result = calculate_sap_from_inputs(inputs) +print(f'SAP: {result.sap_score_continuous:.4f} Δ: {result.sap_score_continuous-88.5104:+.4f}') +print(f'main_eff: {inputs.main_heating_efficiency:.4f}') +ws_92 = (18.9539, 18.0081, 18.3466, 18.8491, 19.3582, 19.8174, 20.0288, 20.0064, 19.6975, 19.0702, 18.3966, 18.1573) +mit_drift = max(abs(c - w) for c, w in zip(inputs.mean_internal_temp_monthly_c, ws_92)) +print(f'max MIT drift vs worksheet (92): {mit_drift:.5f}')" +``` + +You should see: + +``` +SAP: 88.5698 Δ: +0.0594 +main_eff: 2.2348 +max MIT drift vs worksheet (92): 0.00091 +``` + +## Pyright baselines (net-zero per slice) + +- `datatypes/epc/domain/mapper.py`: 32 +- `domain/sap10_calculator/worksheet/water_heating.py`: 1 +- `domain/sap10_calculator/worksheet/heat_transmission.py`: 13 +- `domain/sap10_calculator/worksheet/mean_internal_temperature.py`: 0 +- `domain/sap10_calculator/worksheet/internal_gains.py`: 4 (was 5; this session dropped one) +- `domain/sap10_calculator/rdsap/cert_to_inputs.py`: 35 +- `domain/sap10_calculator/tables/pcdb/parser.py`: 0 +- `domain/sap10_ml/rdsap_uvalues.py`: 1 (pre-existing) +- `datatypes/epc/domain/epc_property_data.py`: 1 (pre-existing) + +## Conventions (preserved) + +- One slice = one commit; stage by name. +- AAA test convention: literal `# Arrange / # Act / # Assert` headers. +- `abs(diff) <= tol` (NOT `pytest.approx`). +- 1e-4 worksheet tolerance for end-state pins (Layer 4 chain tests); + intermediate slice tests may use 1e-2 to 1e-3 absorbing known + drifts documented in commit messages. +- Spec citation in commit messages (RdSAP 10 / SAP 10.2 page or line ref). +- Pyright net-zero per file. + +Good luck closing the PSR residual. The MIT cascade itself is now +spec-faithful through Equation N5; the final +0.06 SAP drift is a +single bug in the PSR computation (one input — max_output, HLC, or +ΔT — differs from worksheet convention).