mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
74 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
49de18e83a |
Slice S0380.45: wire β-split into PE cascade per SAP 10.2 Appendix M1 §8
The PE cascade in calculator.py was crediting ALL PV generation at the
IMPORT PEF (Table 12 ~1.501) instead of splitting per Appendix M1
§4/§8 — onsite-consumed E_PV,dw at the IMPORT PEF and exported E_PV,ex
at the EXPORT PEF (Table 12 code 60 = 0.501). The over-credit on the
exported portion was the primary driver of the ASHP-cohort PE Δ -7..-15
kWh/m² under-count.
Wiring (cert_to_inputs.py):
- `_pv_array_monthly_generation_kwh(array, climate)` — per-array E_PV,m
via Appendix M1 §2 (p.92) apportioning: 0.8 × kWp × ZPV × monthly
solar radiation. Reuses ORIENTATION/PITCH/Z lookups already in
`_pv_array_generation_kwh_per_yr`. Annual sum equals the existing
helper to float precision.
- `_pv_monthly_generation_kwh(epc, climate)` — sums per-array monthlies;
falls back to the same §11.1 b) percent-roof-area synthesis as the
annual helper for certs without per-array detail.
- `_pv_battery_capacity_kwh(epc)` — total usable battery capacity =
per-battery capacity × pv_battery_count. The 15 kWh cap per §3c is
applied inside `pv_beta_coefficients` and not duplicated here.
- `_pv_eligible_demand_monthly_kwh(...)` — assembles D_PV,m per §3a
p.93: lighting + appliances + cooking + electric showers + pumps
& fans, plus E_space,m when main fuel is Table-12 {30, 32, 34, 35,
38} (electricity not at off-peak) and E_water,m when water heating
fuel is Table-12 30 (standard electricity). Off-peak immersion ×
(243) and the Appendix G4 PV-diverter branch are deferred —
current cohort fixtures don't exercise them.
- In `cert_to_inputs`: assemble monthly EPV + DPV + battery, call
`pv_split_monthly`, pass `pv_dwelling_kwh_per_yr` +
`pv_exported_kwh_per_yr` through to CalculatorInputs.
Wiring (calculator.py):
- New fields: `pv_dwelling_kwh_per_yr: Optional[float]`,
`pv_exported_kwh_per_yr: Optional[float]`,
`pv_export_primary_factor: float = 0.501` (Table 12 code 60).
- PE cascade now does:
pv_offset = E_PV,dw × IMPORT_PEF + E_PV,ex × EXPORT_PEF
when both split fields are set. Legacy fall-through to all-IMPORT
when either is None (preserves synthetic CalculatorInputs
constructions in unit tests).
Test impact (golden-fixture residual shifts — all expected, re-pinned):
Pre-Slice 45 → Post-Slice 45:
- 0330 (no PV): +0.44 → +0.44 (unchanged ✓)
- 0350 (PV + 5 kWh battery): -7.78 → +2.73
- 0380 (PV + 5 kWh battery): -14.60 → +8.09
- 2130 (PV + gas combi): -38.63 → -9.70 (also SAP +1 shift)
- 2225 (PV + 5 kWh battery): -11.77 → +4.48
- 2636 (PV + 5 kWh battery): -9.65 → +3.42
- 3800 (PV + 5 kWh battery): -9.61 → +3.58
- 9285 (PV + 5 kWh battery): -7.96 → +3.20
- 9418 (PV + 5 kWh battery): -7.30 → +4.67
- 9501 (PV, no battery): -8.28 → +0.25 (CLOSED ✓)
Cert 9501 closing to +0.25 with the β-split alone confirms the
implementation is spec-correct. The 7-cert 5-kWh-battery cohort
now over-shoots in the positive direction because the cascade's
E_PV magnitude is ~3× the worksheet's (cert 0380 cascade 2570 kWh/yr
vs worksheet 831 kWh/yr — peak_power=3 interpreted as 3 kWp while
worksheet uses ~1 kWp). With E_PV overestimated, R_PV = E_PV / D_PV
is too high → β_m from §3d formula too low → not enough credit
shifts to the IMPORT factor. Slice S0380.46 audits the cascade's
E_PV magnitude (kWp interpretation, S lookup, or ZPV mapping).
Chain tests (cohort-1 + cohort-2 SAP-rating-vs-worksheet) all stay
<1e-4 — Slice 45 only touches the PE cascade; SAP rating uses the
cost cascade which is still on the old all-export path.
Test suite: 763 pass + 0 fail. Pyright net-zero on touched files.
Spec citations:
- SAP 10.2 specification Appendix M1 §3a (p.93) — D_PV,m assembly.
- SAP 10.2 specification Appendix M1 §3c-d (p.94) — β formula.
- SAP 10.2 specification Appendix M1 §4 (p.94) — E_PV,dw / E_PV,ex.
- SAP 10.2 specification Appendix M1 §8 (p.94) — PE factor split.
- SAP 10.2 Table 12 code 60 — EXPORT PEF = 0.501.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
5344bc8920 |
Slice S0380.44: SAP 10.2 Appendix M1 §3-4 PV β-factor calculator (no wiring)
Pure-function module + 13 unit tests for the photovoltaic onsite/export
split. No cascade wiring yet — Slices S0380.45..47 will wire β into the
PE / CO2 / cost cascades respectively (which currently all over-credit
the exported PV portion at the IMPORT factor).
Module: `domain/sap10_calculator/worksheet/photovoltaic.py`
- `PhotovoltaicSplit` frozen dataclass — monthly β + (E_PV,dw,m,
E_PV,ex,m) with annual-sum properties matching worksheet line
refs (233a) and (233b).
- `pv_beta_coefficients(Cbat)` — three coefficients keyed on battery
capacity (kWh), capped at 15 per §3c:
CPV1 = 1.610 - 0.0973 × Cbat
CPV2 = 0.415 - 0.00776 × Cbat
CPV3 = 0.511 + 0.0866 × Cbat
- `pv_split_monthly(epv, dpv, battery_kwh)` — per §3d-4:
R_PV,m = E_PV,m / D_PV,m
β_m = min(exp(-CPV1 × (R_PV,m × CPV2)^CPV3), D_PV,m / E_PV,m)
E_PV,dw,m = E_PV,m × β_m; E_PV,ex,m = E_PV,m × (1 - β_m)
Edge cases (not in spec but implied by physics):
- E_PV,m = 0 → β = 0; both onsite and exported = 0
- D_PV,m = 0 → cap forces β = 0; all PV exports
Unit-test coverage (13 tests, AAA convention, `abs(diff) <= tol`):
- β coefficient constants at Cbat=0, 5 (ASHP cohort), 15 (cap)
- Cbat>15 clamps to 15; Cbat<0 clamps to 0 (defensive)
- Hand-computed β worked example (no battery): β≈0.4864 at E_PV=100,
D_PV=200 — pinned at 1e-7 against precomputed value AND at 1e-9
against the live formula recomputation (load-bearing math pin)
- Edge cases: E_PV=0 → no split; D_PV=0 → full export
- Battery monotonicity: β increases with Cbat for fixed (E_PV, D_PV)
- Energy conservation: E_PV,dw + E_PV,ex = E_PV per month + annually
- Tuple length validation (raises on != 12 months)
- Return shape pinned to `PhotovoltaicSplit` dataclass contract
Test suite: 750 → 763 pass + 0 fail. Pyright net-zero on new files.
Spec citation: SAP 10.2 specification Appendix M1 §3-4 (p.93-94).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
29c4b029e3 |
docs: handover after S0380.39..S0380.43 — cohort-2 API path 38/38 closed
Session shipped 5 slices that closed the entire cohort-2 API-path
cluster (S0380.39 bulk-fetch, S0380.40 parametrized test, S0380.41
RdSAP 21 → SAP 10.2 glazing alias, S0380.42 Decimal HALF_UP per-window
areas, S0380.43 SAP 631 → spec fuel).
Documents:
- Cross-mapper parity at cascade established for all 38 cohort-2
certs (and 9 cohort-1 ASHP); both paths < 1e-4 vs worksheet.
- Tolerance tightening deferred — 1e-4 is the realistic floor at
HEAD (worst residual 4.91e-5 on cert 2102).
- Lessons learned: GOV.UK RdSAP 21 enum != cascade enum (codes
needing remap are incremental as fixtures surface them);
Decimal HALF_UP per-window areas extends the S0380.34/35
pattern; SAP heating-type → spec fuel dispatch is the new
forcing-function pattern for cert-lodgement inconsistencies.
- Open front: golden-residuals → ~0 on PE/CO2. ASHP cluster
(-7..-15 kWh/m² PE / +0.16..+0.28 t/yr CO2 across 7 certs with
the same PCDB heat pump) is the highest-value single thread —
likely SAP 10.2 Appendix L1 / Table 12 PE-factor or CO2-factor
cascade gap. Three concrete diagnostic probes proposed.
Test baseline at HEAD: 750 pass + 0 fail.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
e1b7b30c40 |
Slice S0380.42: Decimal HALF_UP per-window areas per RdSAP10 §15 — closes cert 1536
Cert 1536 lodged window dimensions including (0.65 × 0.70) × 3
windows. In float arithmetic 0.65 × 0.70 = 0.45499999999999996,
which the `_round_half_up(float, dp)` helper snaps to 0.45 vs the
spec answer 0.46 (Decimal: 0.65 × 0.70 = 0.4550 exact, HALF_UP at
2 d.p. = 0.46). The shortfall of 0.01 m² × 3 windows = 0.03 m²
under-counted as ~0.073 W/K of conduction loss vs the worksheet's
windows_w_per_k = 25.6354 — closing the cert 1536 residual at
+0.00152 to <2e-6.
Same class of bug as the S0380.34/35 living-area / gross-wall /
party-wall closures (Decimal HALF_UP at the 0.005 boundary that
float drops). RdSAP10 §15 (p.66) lists "all element areas (gross)
including window areas: 2 d.p." — Decimal is the only arithmetic
that matches that boundary deterministically.
Three cascade sites now use Decimal HALF_UP for per-window areas:
- heat_transmission.py: `_decimal_round_half_up_product(W, H, 2)`
replaces `_round_half_up(W × H, 2)` at the windows_w_per_k cascade
AND at the per-bp window-area accumulation (the wall-net deduction
branch must agree with the conduction branch for cascade-internal
consistency, per the existing comment at line 575-583).
- internal_gains.py: `_decimal_window_area_2dp(W, H)` replaces the
inline `_round_area_2dp(W × H)` in the daylight factor `g_l`
sum so §5 (66)..(67) sees the same per-window areas as §3 (27).
- solar_gains.py: same Decimal helper replaces `_round_area_2dp` in
`_wall_window_solar_gain_monthly_w` so §6 (74)..(81) area = (27).
The `_round_area_2dp` helpers were inlined per-module in pre-S0380.42
work; this slice deletes them since the Decimal-aware product
replaces all call sites. `_round_half_up` stays in heat_transmission
for non-product per-element area calls (single-value rounds).
Test impact:
- Cohort-2 cert 1536 API path: +0.00152 → -1e-6 (<1e-4 ✓).
Moves from _COHORT_2_API_OPEN to _COHORT_2_API_CLOSED. Cohort
distribution: 37/38 exact (was 34/38 at start of session);
only cert 2102 (-6.30 secondary-heating routing) remains open.
- Cohort-2 cert 0300/9380 unchanged (already <1e-4 after S0380.41).
- Cohort-1 ASHP 9/9 unchanged: <1e-4 on both paths.
- Elmhurst 6-cert worksheet sweep: unchanged (lodges
`window_width=area, window_height=1.0` per the Elmhurst lodging
convention — Decimal(area) × Decimal(1.0) = Decimal(area), no
rounding shift).
Test suite: 750 pass + 0 fail. Pyright net-zero per touched file
(heat_transmission 13/13; internal_gains 4/4 pre-existing; solar_gains
0/0; chain test 0/0).
Spec citation: RdSAP 10 Specification §15 "Rounding of data" p.66 —
"All element areas (gross) including window areas and conservatory
wall area: 2 d.p." Decimal is the float-precision-stable arithmetic
that matches this rule at the .005 boundary.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
22ae6f4d77 |
Slice S0380.39: bulk-fetch 38 cohort-2 EPC API JSONs for cross-mapper parity
Adds scripts/fetch_cohort2_api_jsons.py (throwaway one-off) plus 38 golden fixtures under domain/sap10_calculator/rdsap/tests/fixtures/golden/ covering every cert in "sap worksheets/additional with api 2/". Each JSON is the inner `data` payload from the gov.uk EPB /api/certificate endpoint — the same shape EpcPropertyDataMapper .from_api_response consumes today. Required prerequisite for Slice B (parametrized API-path chain test that mirrors the cohort-2 Summary-path sweep at 1e-4 vs worksheet). Per the cross-mapper-parity primitive: API EPC and Elmhurst EPC must produce SAP within 1e-4 of each other and of the worksheet — the SAP cascade is the load-bearing equivalence check. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
f992824824 |
docs: handover after S0380.31..S0380.38 — cohort-2 Summary path COMPLETE, thread 4 next
State at HEAD
|
||
|
|
883d66ac65 |
Slice S0380.38: loosen FEE round-trip tolerance 1e-9 -> 1e-6
test_no_ac_cert_round_trips_fee_equals_space_heating_per_m2 encodes a real SAP 10.2 invariant: when (108) = 0 (no fixed AC) and Appendix H solar is absent (every cohort cert), (109) FEE must equal space_heating_kwh / TFA. The 1e-9 tolerance was too tight. The cascade computes: - FEE: sum_round_per_month(annual_98a) / TFA - space_heating_kwh: sum(monthly_98a_kwh) summed in calculator The two paths sum the same 12 monthlies in different rounding orders and disagree at ~8e-8 (cascade FEE = 95.39072333333334; SH/TFA = 95.39072341347577). 1e-6 is two orders of magnitude tighter than any meaningful path divergence (a stray 4-d.p. rounding step or unintended AC contribution would blow past instantly) and ~12.5x looser than the observed float-arithmetic drift, so the invariant still fires. Also swaps pytest.approx for `abs(a - b) <= tol` per [[feedback-abs-diff-over-pytest-approx]] (strict-pyright flags pytest.approx as partially-unknown; nets -1 error on the file). Test baseline: 712 pass + 0 fails (was 712 + 1). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
1cea73df7c |
Slice S0380.37: drop cert 001479 hand-built fixture — covered by passing production-path chain tests
Cert 001479 was added in
|
||
|
|
d61a27e0ff |
Slice S0380.35: round gross-wall and party-wall areas in Decimal arithmetic per RdSAP10 §15 — closes cohort-2 cert 2800 / 4800 +0.0007 SAP residuals
RdSAP10 §15 p.66 (Rounding of data):
"All element areas (gross) including window areas and
conservatory wall area: 2 d.p."
Certs 2800 and 4800 lodge heat_loss_perimeter = 21.25 m and
room_height = 2.30 m. The exact-decimal products
21.25 * 2.30 = 48.8750 (gross wall area)
6.25 * 2.30 = 14.3750 (party wall area)
sit ON the HALF_UP rounding boundary and must round to 48.88
and 14.38 m^2. Float representation drops them BELOW the
boundary:
21.25 (float) * 2.30 (float) ~= 48.87499...
HALF_UP 2 d.p. = 48.87
6.25 (float) * 2.30 (float) ~= 14.37499...
HALF_UP 2 d.p. = 14.37
The 0.01 m^2 area shortfall feeds into (29a) net wall area and
(32) party wall area, and into (31) total external area for
(36) thermal bridging — propagating a +0.0007 SAP residual via
the U-weighted heat-loss sums.
Adds `_decimal_round_half_up_sum` helper and routes both
gross-wall and party-wall sums through it, mirroring the
S0380.34 fix on `_living_area_fraction`. Certs that sit off
the .005 boundary (i.e. nearly all) are unaffected; certs
that land on it close from +0.0007 → <5e-5.
Cohort-2 distribution after S0380.31..S0380.35:
38 exact (was 36 exact + 2 <=0.07).
Cohort-1 ASHP cohort: 9/9 <1e-4 (unchanged).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
a92a33a8d8 |
Slice S0380.34: round living area in Decimal arithmetic per RdSAP10 §15 — closes cert 2536 +0.0007 SAP residual
RdSAP10 §15 p.66 (Rounding of data):
"All internal floor areas and living area: 2 d.p."
Cert 2536 (3 habitable rooms → Table 27 fraction 0.30,
TFA 45.65 m^2) sits ON the HALF_UP rounding boundary:
0.30 (exact) * 45.65 = 13.6950
HALF_UP 2 d.p. = 13.70
(worksheet fLA = 13.70 / 45.65 = 0.3001)
Float arithmetic drops the spec product BELOW the boundary:
0.30 (binary) ~= 0.2999999...
product ~= 13.69499...
HALF_UP 2 d.p. = 13.69
(cascade fLA = 13.69 / 45.65 = 0.29989)
The 0.00021 fLA shortfall feeds straight into the worksheet
(91) -> (92) MIT blend, undershoots MIT by ~0.001 C, and
shaves 0.29 kWh off (98c) useful space heating — a +0.0007
SAP residual via the (211) main heating fuel x p/kWh.
Compute the product in Decimal so HALF_UP lands on the exact
.005 decimal boundary the spec defines. Certs that sit off the
boundary (e.g. 2800/4800: 0.30 x 46.87 = 14.0610 -> 14.06 in
both Decimal and float) are unaffected.
Cohort-2 distribution after S0380.31..S0380.34:
36 exact + 2 <=0.07 (was 35 exact + 3 <=0.07).
Cert 2536: +0.000715 -> -9.2e-8.
The remaining 2800 / 4800 +0.0007 residuals come from a
different cause (off the HALF_UP boundary) — defer to a
separate slice.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
2c3eb17b96 |
Slice S0380.33: round synthesized PV kWp to 2 d.p. per RdSAP10 §15 — closes cert 6835 +0.015 SAP residual
RdSAP10 §15 p.66 (Rounding of data):
"kWp for photovoltaics, etc.: 2 d.p."
Cert 6835 lodges Photovoltaic Supply as "Proportion of roof
area = 40" (no explicit kWp). Per RdSAP10 §11.1 b) p.60 the
cascade synthesizes kWp = 0.12 × PV area where PV area is
roof_area / cos(35°). For cert 6835:
PV area = 36.9 × 0.40 / cos(35°) = 18.0186 m^2
kWp unrounded = 0.12 x 18.0186 = 2.16224
kWp at 2 d.p. = 2.16 (matches worksheet
"Cells Peak = 2.16")
SAP 10.2 §M1 EPV = 0.8 x kWp x S x ZPV. With the 0.0022 kWp
delta the cascade was overstating PV generation by 1.5448 kWh/yr,
adding -0.20 GBP to (252) total PV credit, dropping (255) total
energy cost by 0.20, lowering ECF and raising SAP by +0.015.
Cohort-2 distribution after S0380.31..S0380.33:
35 exact + 3 <=0.07 (was 34 + 4 at S0380.32 HEAD).
Cert 6835: +0.014534 -> -4.3e-5.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
396907f46a |
Slice S0380.32: route bare \"Extension\" window location to BP[1] per RdSAP10 §3 — closes cert 9380 +0.027 residual
RdSAP10 §3 p.17:
"When specifying windows and doors, for each building part
assessor allocates windows and doors to the corresponding
wall (the appropriate main wall or each alternative wall).
For each building part, software will deduct window/door
areas contained in the relevant wall areas."
SAP 10.2 §3 p.16:
"Wall area is the net area of walls after subtracting the
area of windows and doors."
Cert 9380's Summary PDF lodges 2 windows on its single extension,
but pdftotext wraps "1st" onto a preceding layout line while
"Extension" lands on a separate line — the Elmhurst extractor
captures only the second token. `_window_bp_index` previously
matched "main" / "1st"-"4th" prefixes but fell through bare
"Extension" to BP[0] (main), causing the cascade to deduct ext1
windows from the main wall:
Worksheet (29a): main 60.60 × 0.70 + ext1 18.25 × 0.53 = 52.0925
Pre-fix cascade: main 59.01 × 0.70 + ext1 19.84 × 0.53 = 51.8222
Δ -0.27 W/K → SAP +0.027
This slice adds bare "extension" (when num_parts >= 2) as a sibling
to the ordinal-prefix matches. Closes cert 9380 +0.027 → -4.8e-6.
Cohort-2 distribution after S0380.31 + S0380.32:
34 exact + 4 ≤0.07 (was 33 exact + 5 ≤0.07).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
86226ebdb6 |
Slice S0380.31: deduct alt-wall window opening from (31) net external area — closes cert 2636 cantilever residual -0.015 → -2.4e-6
SAP 10.2 Appendix K eqn (K2) p.84:
HTB = y × Σ(Aexp)
where Aexp is "the total area of external elements calculated at
worksheet (31)". The worksheet (31) column header reads "Total NET
area of external elements" — net of openings.
Cert 2636 (dr87-0001-000898 line 187): (31) = 160.33 m² =
47.70 main net + 11.57 alt net + 42.92 roof + 39.18 ground floor
+ 3.74 cantilever + 11.52 windows + 3.70 doors.
Pre-fix cascade summed the alt-wall at its 12.76 m² gross (no
opening deduction) — (31) was 161.52, driving (36) to 24.228 vs
worksheet 24.0495 (Δ +0.1785 W/K). That drift propagated through
(39) HTC → MIT → space heating, leaving cert 2636 at Δ -0.015
SAP — the only ASHP cohort cert above the 1e-4 floor.
`alt_walls_total_area` aggregates per-alt-wall gross at line 736;
this slice subtracts `alt_window_area` from it in the (31) sum so
the alt-wall contribution is net, matching the (29a) net-area
convention already applied per-element to the A×U sums.
Cohort-1 ASHP cohort: 9/9 certs < 1e-4 Summary path (was 8/9 with
cert 2636 at -0.015). Cert 2636 API path also closes to < 1e-4 —
the bug was path-symmetric in the cascade, not in either mapper.
Cohort-2 unchanged at 33 exact + 5 ≤0.07.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
453a9216fb |
docs: handover after S0380.26-30 precision-floor closure
Documents the 5-slice session that closed the prior handover's
"precision floor" cluster end-to-end:
S0380.26 RdSAP10 §5.8 dry-lining adjustment (cert 7700)
S0380.27 floor_construction_type → _main_floor_u_value (cert 9796)
S0380.28 SAP 10.2 Appendix N fn 43 reciprocal η interpolation
(closes the +0.03..+0.06 ASHP cluster cohort-wide)
S0380.29 _ASHP_COHORT_CHAIN_TOLERANCE 0.07 → 0.04
S0380.30 glazing codes 8-15 (RdSAP 21 schema) — closes API path
cohort-1 +0.014..+0.031 cluster
Final state:
Cohort-2 Summary path (38): 33 exact + 5 ≤0.07
Cohort-1 ASHP cohort (7): 6/7 <1e-4 both Summary + API paths
cert 2636 -0.015 (cantilever, path-symmetric) — only open thread
The prior `HANDOVER_CERT_0380_MIT_CASCADE.md` had concluded the
+0.04 ASHP cluster was unfixable without Elmhurst access; the
spec citation (SAP 10.2 Appendix N fn 43) was sitting in the same
PDF that handover referenced. Be skeptical of "spec-precision
floor" framing — see [[feedback-spec-floor-skepticism]].
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
faf116bd70 |
Slice S0380.30: extend g_L + g⊥ Table 6b to RdSAP 21 codes 8-15 — closes API path cohort residual cluster
Per the RdSAP 21 schema in [datatypes/epc/domain/epc_codes.csv][1], the
`glazing_type` enum extends to 15 codes; the legacy SAP 10.2 Table 6b
cascade lookups in `internal_gains.py:106` and `solar_gains.py:178`
only knew codes 1-7. Every API-path cert in the cohort lodges
`glazing_type` via the RdSAP 21 numbering, and triple-glazed
lodgements surface as **code 14** ("triple glazing, installed 2022+").
Pre-slice the cascade fell through to the 0.80 / 0.76 double-glazed
defaults for codes 8-15:
Internal gains g_L (Table 6b):
code 14 → default 0.80 (DG) vs spec 0.70 (TG)
→ daylight factor over-bonused → lighting kWh under-counted
Solar gains g⊥ (Table 6b):
code 14 → default 0.76 (DG) vs spec 0.68 (TG)
→ solar gains over-counted
For cert 0350-2968-2650-2796-5255 (semi-detached, 9 triple-glazed
windows lodged as code 14), this drove:
lighting_kwh_per_yr: cascade 221.79 vs Summary-path 228.44
(-6.65 kWh/yr — daylight bonus too generous → lighting too low)
space_heating_kwh_per_yr: cascade 7000.21 vs Summary-path 6996.94
(+3.28 kWh/yr — extra solar gains lower HP demand)
net ECF: -0.0022 vs Summary-path → SAP +0.031
Same mechanism on the other 5 cohort-1 ASHP API certs.
Fix: extend both lookup tables with the RdSAP 21 additions per the
schema CSV semantics:
| code | description (RdSAP 21) | g_L | g⊥ |
|------|----------------------------------|------|------|
| 8 | triple glazing, known data | 0.70 | 0.68 |
| 9 | triple glazing, 2002-2022 | 0.70 | 0.68 |
| 10 | triple glazing, pre-2002 | 0.70 | 0.68 |
| 11 | secondary glazing, normal-E | 0.80 | 0.76 |
| 12 | secondary glazing, low-E | 0.80 | 0.76 |
| 13 | double glazing, 2022+ | 0.80 | 0.76 |
| 14 | triple glazing, 2022+ | 0.70 | 0.68 |
| 15 | single glazing, known data | 0.90 | 0.85 |
Solar gains also adds code 7 (double known data) for
`_G_PERPENDICULAR_BY_GLAZING_TYPE` to align with the existing
`_G_LIGHT_BY_GLAZING_CODE` code-7 entry (which already mapped to
0.80 = double).
Outcome — Cohort-1 ASHP cohort API path:
cert 0380: +0.025 → +1e-6 (close to exact)
cert 0350: +0.031 → +2.2e-5 (close to exact)
cert 2225: +0.029 → -4.8e-5 (close to exact)
cert 2636: +0.015 → -0.015 (sign flip; cantilever-specific
residual surfaces; same |Δ| as Summary)
cert 3800: +0.023 → -2e-5 (close to exact)
cert 9285: +0.029 → -3.4e-5 (close to exact)
5 of 6 API path certs now sit at <1e-4 vs worksheet. Cert 2636
matches its Summary-path residual (-0.015) — the cantilever fixture
has its own non-glazing residual to be diagnosed separately.
Cohort-2 Summary path unchanged (33 exact + 5 ≤0.07) — the cohort-2
certs lodge glazing codes 1-7 (RdSAP 17 numbering still surfaces in
Elmhurst Summary PDF lookups), so codes 8-15 only affect the
RdSAP-21-schema API path.
Golden API fixture pins updated to reflect the tightened cascade-vs-API
alignment (7 certs: 0380, 0350, 2225, 2636, 3800, 9285, 9418). SAP
integer residuals unchanged (all sit at +0).
Pyright net-zero on touched files (22 → 22).
Tests: 710 → **711** pass (+1 new: cert 0350 fixture-shape test for
glazing_type=14 routing to g⊥=0.68 with `total_solar_gains_monthly_w[0]
≈ 67.00 W` (vs pre-slice 74.88 W at the DG default), proving code 14
hits the triple-glazed Table 6b row.) 10 expected fails unchanged.
[1]: datatypes/epc/domain/epc_codes.csv (RdSAP-Schema-21.0.1).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
081bb8fd7e |
Slice S0380.28: SAP 10.2 Appendix N footnote 43 reciprocal η interpolation — closes the +0.03..+0.06 ASHP precision-floor cluster
Per SAP 10.2 Appendix N, PDF p.101 footnote 43 (line 7053):
"For the efficiency values, the interpolated efficiency is the
reciprocal of linear interpolation between the reciprocals of the
efficiencies."
i.e. 1/η_interp = (1 − t)·1/η_low + t·1/η_high, the weighted harmonic
mean at t = (PSR − PSR_low) / (PSR_high − PSR_low). Cascade was using
**linear** interpolation directly on η — a +0.15..+0.25% over-estimate
in the typical PSR range (1.2..1.5) for ASHPs in the cohort.
Cohort fixture: cert 3336-2825-9400-0512-8292 (Mitsubishi PUZ-WM50VHA,
PCDB 104568). MIT/η-zone cascade matches worksheet EXACTLY (every line
86..92, every month), but η_main_heating cascade 225.443 vs worksheet
224.923 → main_heating_fuel +5.24 kWh/yr too high → ECF 1.5474 vs ws
1.5503 → SAP +0.04 vs worksheet 78.3739. Back-solving the worksheet's
η_main implies η_space_1 = 224.923 / 0.95 ≈ 236.76.
Closed form at PSR=1.40151, bracketing PCDB rows PSR 1.2
(η_space_1=253.9) and PSR 1.5 (η_space_1=229.2):
Linear (pre-slice): 253.9 + (229.2 − 253.9) × 0.6717 = 237.31 ✗
Reciprocal (footnote 43): 1 / ((1 − 0.6717)/253.9 + 0.6717/229.2)
= 1 / 0.004224 = 236.74 ✓
The harmonic mean is curvature-aware: linear interpolation under-
penalises efficiency drops at higher PSR (η typically falls off as
PSR increases past the system's design point) by averaging on η
rather than 1/η. SAP 10.2 footnote 43 is explicit about which side
of the reciprocal the interpolation sits.
Outcome:
Cohort-2 Summary path (38 certs):
exact (<1e-4): 23 → **33** (+10)
≤±0.07: 15 → **5** (-10: HP certs close to exact)
±0.07..0.5: 0 → 0
±0.5..1: 0 → 0
±1+: 0 → 0
RAISES: 0 → 0
Cohort-2 HP cluster post-slice:
0100 +0.00003 ← was +0.00283
0320 -0.00001 ← was +0.01801
0330 -0.00004 ← was +0.01772
2336 +0.00003 ← was +0.01778
3336 +0.00001 ← was +0.04005 (worst residual closes exact)
4536 -0.00002 ← was +0.01312
9036 -0.00003 ← was +0.02159
9796 +0.00000 ← was +0.00174 (post-S0380.27)
2536 +0.00072 ← was +0.00163
2800 +0.00068 ← was +0.00436
4800 +0.00068 ← was +0.02939
9370 +0.00002 ← was +0.00174
9421 +0.00001 ← was +0.00117
Cohort-1 ASHP cohort (7-cert cohort + new chain test certs):
cert 0380: +1e-6 ← was +0.034 (Mitsubishi PUZ-WM50VHA, the
canonical first-HP cohort cert)
cert 3800: -2e-5 ← was +0.021
cert 9418: -3e-7 ← was +0.00004
cert 9285: -3e-5 ← was +0.021
cert 2636: -0.015 ← was +0.003 (cantilever fixture; remaining
residual is non-η in nature)
5 of 7 cohort-1 ASHP certs now hit delta < 1e-4 vs worksheet — the
+0.04 spec-precision-floor cluster diagnosed in
HANDOVER_CERT_0380_MIT_CASCADE.md is the linear-vs-reciprocal η
interpolation bug, not a spec-floor at all. The handover doc's "no
public spec or BRE data field would distinguish these" claim was
incorrect — SAP 10.2 footnote 43 is the resolution.
API path (golden fixtures): 6 ASHP cohort residuals updated to reflect
the cascade closure:
cert 0380 PE: -14.7865 → -14.6848 kWh/m²; CO2: +0.2774 → +0.2780 t/yr
cert 0350 PE: -7.9281 → -7.8741; CO2: +0.1697 → +0.1701
cert 2225 PE: -11.9175 → -11.8557; CO2: +0.2617 → +0.2621
cert 2636 PE: -9.7153 → -9.6692; CO2: +0.2189 → +0.2193
cert 3800 PE: -9.7551 → -9.6838; CO2: +0.2598 → +0.2603
cert 9285 PE: -8.1110 → -8.0466; CO2: +0.1559 → +0.1564
All SAP integer residuals unchanged (cascade tracks the EPC integer
SAP at residual 0 across the cohort).
PSR interpolation unit test (`test_interpolate_heat_pump_efficiency_at
_cert_0380_psr_per_sap_app_n`) updated to reflect the reciprocal
formula with the SAP-10.2-footnote-43 spec citation and closed-form
asserts (η_space_1 ≈ 234.5235; η_water_3 ≈ 285.0861 at PSR=1.43).
Pyright net-zero (1 → 1 across touched files: pcdb/parser.py,
tests/test_pcdb_table_362_lookup.py, rdsap/tests/test_golden_fixtures.py).
Tests: 710 pass (was 710 pre-slice with linear interp + un-updated
pins; net-zero because the 6 golden pin updates + 1 interp test update
exactly offset the 6 + 1 failures the formula change introduced), 10
expected fails unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
012cbd183f |
Slice S0380.27: thread floor_construction_type into _main_floor_u_value — closes cert 9796 +0.55 → +0.00174
Per RdSAP10 §5 page 29 "Floor infiltration (suspended timber ground
floor only)":
Age band A-E:
a) if floor U-value < 0.5, assume "sealed" → 0.1
b) if retro-fit + no U → "sealed" → 0.1
otherwise "unsealed" → 0.2
The cascade routes the (12) sealed/unsealed verdict through
`_main_floor_u_value`, which calls `u_floor` to compute the BS EN ISO
13370 U-value the spec rule keys on. That helper was a stale duplicate
of the real heat-transmission path that did NOT respect the per-bp
`floor_construction_type` lodgement:
Pre-slice: u_floor(construction=int_or_None, description=None, ...)
Cascade: u_floor(construction=int_or_None, description="Suspended
timber" if floor_construction_type else <fallback>, ...)
For cert 9796-3058-6205-0346-9200 (Mid-Terrace bungalow age D,
46.87 m² / 15.0 m perimeter, suspended-timber lodged):
- Broken `_main_floor_u_value` routes through the solid default
(no description, construction=None) → BS EN ISO 13370 solid →
U=0.49 W/m²K.
- 0.49 < 0.5 → spec rule (a) fires → (12) = 0.1 (sealed).
- Real heat-transmission cascade routes through the suspended branch
via `effective_floor_description = floor_construction_type` →
U=0.56 → unsealed → (12) = 0.2.
The 0.1 ach gap then propagated:
(18) infiltration_rate 0.74 → ws 0.84 (cascade -0.10)
(25)m Jan 0.82 → ws 0.91 (cascade -0.09)
(38)m Jan 29.08 W/K → ws 32.37 (cascade -3.29 W/K)
(39) Jan 110.35 W/K → ws 113.64 (cascade -3.29 W/K)
HLP Jan 2.35 W/m²K → ws 2.42 (cascade -0.07)
T_h2 Jan 19.11°C → ws 19.07 (cascade +0.04)
MIT Jan 18.51°C → ws 18.45 (cascade +0.06)
SAP +0.55 vs worksheet 90.13.
Fix mirrors heat_transmission's `effective_floor_description` rule in
`_main_floor_u_value`: the per-bp `floor_construction_type` takes
precedence over a joined `epc.floors[].description` because it's the
explicit Elmhurst Summary §3/§9 surface. Inlined the description join
(vs importing `_joined_descriptions` from heat_transmission) so
cert_to_inputs stays free of cross-module private-symbol imports.
Cohort-2 outcome (38 certs, Summary path):
exact (<1e-4): 23 → 23
≤±0.07: 14 → **15** (+1: cert 9796 +0.55 → +0.00174)
±0.5..1: 1 → **0** (last cohort-2 mid-range gap closes)
The remaining cert 9796 +0.00174 SAP residual is the cohort-1 HP-COP
precision floor (the same +0.001..+0.04 SAP that the other 10
triple-glazed HP certs sit at; see handover thread 3).
Cohort-1 golden fixture cert 8135-1728-8500-0511-3296 (Semi-detached
age C, suspended-timber ground floor with floor_construction=2 lodged
but description=None pre-slice) had the same bug:
Pre-slice: u_floor returned 0.48 (solid branch via construction=2
present-but-not-suspended) → false sealed verdict (12)=0.1
Post-slice: u_floor returns 0.54 (suspended branch via description=
"Suspended timber") → correct unsealed verdict (12)=0.2
PE residual: -4.9611 → **-0.0748** kWh/m² (+4.89 closer to API EPC)
CO2 residual: -0.0678 → **+0.0246** t/yr (closer to API EPC)
SAP residual: 0 → 0 (unchanged, EPC integer)
Pin updated on cert 8135 to reflect the new (correct) cascade-vs-API
alignment; no other golden fixtures shifted.
Pyright net-zero per touched file:
cert_to_inputs.py: 35 → 35
tests/test_cert_to_inputs.py: 13 → 12 (suppressed pre-existing
private-import error on
_water_heating_worksheet_and_gains
at the same time as adding
suppressions for the two new
private imports)
tests/test_golden_fixtures.py: 1 → 1
tests/test_summary_pdf_mapper_chain.py: 0 → 0
Tests: 708 → 710 pass (+2 new: `_main_floor_u_value` routes
suspended-timber via per-bp lodgement; cert 9796 chain pin against
worksheet 90.1318 within ±0.07 ASHP-cohort spec floor), 10 expected
fails unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
c144d444e2 |
Slice S0380.26: RdSAP10 §5.8 dry-lining adjustment on alt walls — closes cert 7700 -0.44 → +5e-5
Per RdSAP10 §5.8 final note + Table 14 page 41:
"For drylining including laths and plaster use Rinsulation = 0.17 m²K/W."
Applied additively to the base U-value of an otherwise-uninsulated wall:
U_adjusted = 1 / (1/U_base + 0.17) — rounded to 2 d.p. half-up.
Closed form for the cohort fixture (cavity-as-built age C, U_base=1.5):
1 / (1/1.5 + 0.17) = 1.19522... → 1.20 ✓ matches worksheet
Cert 7700-3362-0922-7022-3563 (Summary_000905.pdf / dr87-0001-000905.pdf)
is an End-Terrace house age C lodging:
- Main wall: CavityWallDensePlasterDenseBlock, Filled Cavity, U=0.70
- Alt wall 1: 14.44 m² Cavity As-Built, Dry-lining: Yes (worksheet
`CavityWallPlasterOnDabsDenseBlock`, U=1.20)
Pre-slice the Elmhurst alt-wall mapper hard-coded `wall_dry_lined="N"`
and the cascade ignored the field everywhere — alt-wall U routed to the
cavity-as-built default (1.50), giving fabric (33) 148.72 W/K vs
worksheet 144.38 (Δ +4.33 W/K = ~+0.44 SAP). Worksheet "SAP value" line
lodges unrounded SAP 63.4425.
Implementation:
1. `AlternativeWall.dry_lined: bool = False` on the Elmhurst surveys
dataclass.
2. Elmhurst extractor reads "Alternative Wall N Dry-lining: Yes/No"
into the new field.
3. `_map_elmhurst_alternative_wall` propagates `wall_dry_lined="Y"`
instead of the hard-coded "N".
4. `u_wall` gains a `dry_lined: bool = False` kwarg and a single
§5.8 adjustment site at the as-built bucket (bucket=0). Insulated
buckets already absorb the dry-lining R via Table 14.
5. `_alt_wall_w_per_k` passes `dry_lined=alt_wall.wall_dry_lined == "Y"`.
Scope is the alt-wall path only — main BPs in the corpus all lodge
`wall_dry_lined="N"` (or the Summary PDF omits the field for the main
wall), so the main-wall call site is untouched. Conservative regression
posture per the user's strict cohort-pin convention.
Cohort-2 outcome (38 certs, Summary path):
exact (<1e-4): 22 → **23** (+1: cert 7700 -0.44 → +4.87e-05)
0.07..0.5: 1 → **0** (-1: cert 7700 closes out)
0.5..1: 1 → 1 (cert 9796 unchanged — MIT precision floor)
RAISES: 0 → 0
Cohort-1 ASHP cohort untouched: all certs lodge wall_dry_lined="N", so
the alt-wall call site short-circuits to the original cascade. Verified
no regressions across the 22 previously-exact cohort-2 certs either.
Pyright net-zero on all 8 touched files (183 → 183).
Tests: 704 → 708 pass (+4 new: u_wall §5.8 adjustment fires
correctly; cavity-as-built unchanged without flag; insulated bucket
unaffected by flag; heat_transmission alt-wall delta = 14.44 × 0.30
W/K; cert 7700 full chain hits worksheet 63.4425 at < 1e-4),
10 expected fails unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
73fedc0ecd |
docs: handover for cohort-2 closure + precision-floor next steps
Captures 5 slices shipped this session (S0380.21..25):
- Table 3a rows 1+4 + PCDB keep-hot dispatch
- Per-BP roof exposure (Ext1 flat roof on flats)
- RdSAP §11.1 b) % of roof area PV synthesis
- SAP code 631 → house coal secondary fuel
- SAP codes 2111/2113 → control type 2
Cohort-2 outcome: 22/38 exact (<1e-4), max residual ±0.55 SAP,
0 RAISES, 0 big-gaps. All structural cascade gaps closed.
Open threads diagnosed in detail:
1. Cert 7700 -0.44 SAP — wall U code conflict
(_WALL_INSULATION_NONE=4 vs Elmhurst "As Built"=4). Wider than
a single slice; needs regression testing.
2. Cert 9796 +0.55 SAP — MIT precision floor (Mid-Terrace
bungalow + HP, +0.06°C across all months). Same mechanism as
cohort-1 HP-COP residuals.
3. API-path closure for all 38 certs (deferred).
4. Tighten cohort-1 chain tests to 1e-4 once thread 2 closes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
36a3219dfb |
Slice S0380.25: SAP codes 2111/2113 are type 2 not type 3 — closes 0652 + 6835
Per SAP 10.2 spec page 171 Table 4e "Heating system controls" — boiler
systems with radiators (Group 1):
2110: "Time and temperature zone control by arrangement of plumbing
and electrical services" → type 3
2111: "TRVs and bypass" → type 2
2112: "Time and temperature zone control by device in PCDB" → type 3
2113: "Room thermostat and TRVs" → type 2
`_CONTROL_TYPE_BY_CODE` previously bucketed 2111 + 2113 with the type 3
codes, but neither lodges any time-zone control — they're TRV-class
controls (closer to programmer + room thermostat). The misclassification
propagated through SAP 10.2 Table 9 to swap the elsewhere-zone
off-period pattern from (7, 8) to (9, 8) — i.e. the spec's "heating
0700-0900 and 1800-2300" pattern (footnote b) instead of "heating
0700-0900 and 1600-2300" (footnote a). Under-counted MIT by ~0.67 °C
across the year, dropping space-heating demand and over-predicting SAP:
- cert 0652-3022-1205-2826-1200: +1.93 → -1e-5
- cert 6835-3920-2509-0933-5226: +0.72 → +0.015
Cohort-2 outcome (38 certs, Summary path):
exact (<1e-4): 21 → **22** (+1: cert 0652 closes)
≤±0.07: 13 → **14** (+1: cert 6835 moves from ±0.5..1)
±0.5..1: 2 → **1** (-1: cert 6835 closes out)
±1..5: 1 → **0** (-1: cert 0652 closes out)
No cohort-1 regressions (all certs there use codes 2106 / 2206;
neither uses 2111/2113).
Pyright net-zero (cert_to_inputs.py 35→35, test 13→13).
Tests: 704 pass (existing control-type test extended; +2 new
assertions for codes 2111/2113), 10 expected fails unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
8dee191803 |
Slice S0380.23: RdSAP §11.1 b) PV %-of-roof-area synthesis — closes cert 6835 -13.37 → +0.72
RdSAP 10 specification page 60 §11.1 b) (Photovoltaics): "If the kWp
(or DNC) is not known use the following: PV area is roof area for
heat loss (before amendment for any room-in-roof), times percent of
roof area covered by PVs, and if pitched roof divided by cos(35°).
If there is an extension, the roof area is adjusted by the cosine
factor only for those parts having a pitched roof. kWp is 0.12 ×
PV area. If not provided in the RdSAP data set then facing South,
pitch 30°, modest overshading."
Wire-through:
1. `Renewables.pv_percent_roof_area: Optional[int]` — new field on
the Elmhurst site-notes dataclass.
2. Elmhurst extractor `_extract_renewables` parses Summary §19.0
row "Proportion of roof area" (cert 6835: "40").
3. Elmhurst mapper `from_elmhurst_site_notes` surfaces it through
`epc.sap_energy_source.photovoltaic_supply.none_or_no_details
.percent_roof_area` — mirrors the API mapper's lodgement shape.
4. `cert_to_inputs._synthesize_pv_arrays_from_percent_roof_area`
synthesizes a single PV array via the spec formula when
`photovoltaic_arrays` is empty AND a `percent_roof_area > 0`
lodgement is present. Fires inside
`_pv_generation_kwh_per_yr`, so both rating + demand cascades
pick it up.
Cohort-2 outcome (38 certs, Summary path):
exact (<1e-4): 20 → 20
±0.07..0.5: 1 → 1
±0.5..1: 1 → **2** (cert 6835 closes -13.37 → +0.72)
±1..5: 1 → 1
±5+: 2 → **1** (-1: cert 6835 moves out of big-gap band)
Cert 6835 verified end-to-end:
- kWp = 0.12 × 36.9 × 0.40 / cos(35°) = 2.1622
(worksheet "Cells Peak = 2.16, Orientation = South, Elevation =
30°, Overshading = Modest")
- Cascade PV generation = 1493.88 kWh/yr vs worksheet 1492.33
(<0.1% delta — kWp-rounding artefact).
- Cascade SAP 80.92 vs worksheet 80.20 (+0.72, in the ±0.5..1 band).
The residual +0.72 likely traces to the PV-cost cascade's
used-in-dwelling / exported split rather than the synthesis — the
kWh figure is within rounding of the worksheet.
Pyright per-file: net-zero
- cert_to_inputs.py 35 → 35
- test_cert_to_inputs.py 13 → 13
- mapper.py 32 → 32
- elmhurst_site_notes.py 0 → 0
- elmhurst_extractor.py 0 → 0
Tests: 702 → 703 pass (+1 new RdSAP §11.1 b synthesis test), 10
expected fails unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
1a25ea674e |
Slice S0380.22: per-BP roof exposure — closes cert 0036 Ext1 flat roof
For multi-BP dwellings the dwelling-level `exposure.has_exposed_roof`
flag (derived from `dwelling_type` via `_dwelling_exposure`) zeroed
out ALL BPs' roof contributions uniformly. That's wrong when a flat
has an extension with its own external roof — e.g. ground-floor flat
with a single-storey extension whose flat roof is exposed.
Replace the global suppression with a per-BP signal:
- Per-BP `roof_construction_type` containing "another dwelling
above" → that BP's roof is party → suppress.
- Otherwise BP 0 (Main) falls back to the dwelling-level flag
(covers flat lodgements that don't explicitly mark the Main
roof type).
- Extensions (i > 0) expose their roof by default unless their
own roof_construction_type lodges as party.
Cohort cert 0036-6325-1100-0063-1226 (ground-floor flat, age D):
- Main lodges roof_construction_type = "Another dwelling above"
→ contributes 0 W/K (matches worksheet line (30) "External roof
Main 57.93 m² × U=0 = 0.0").
- Ext1 lodges roof_construction_type = "Flat" → contributes
1.09 m² × U=2.30 = 2.507 W/K (matches worksheet "External roof
Ext1 1.09 m² × U=2.30 = 2.507", spec line (30)).
- Cascade SAP closes from +0.2987 → -6e-6 vs worksheet 62.7471.
Houses + bungalows are unaffected: dwelling-level flag stays True
and the per-BP guard only activates on explicit party-roof lodgement.
Single-BP flat tests stay correct: the per-BP guard is a no-op when
no roof_construction_type is lodged (i==0 → falls back to dwelling-
level flag).
Spec citation:
- RdSAP 10 §3 / §5.11 — heat-loss surfaces and party-roof
treatment. SAP 10.2 spec line (30) sums external roofs only;
party roofs sit in the (32) party-element channel with U=0.
Cohort-2 distribution (38 certs, Summary path) shifts:
exact (<1e-4): 19 → **20** (+1: 0036)
0.07..0.5: 2 → **1** (-1: 0036 → exact)
Pyright net-zero (heat_transmission.py 13→13, test file 71→71).
Test counts: 702 → 703 pass (+1 new test), 10 expected fails unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
0d3fb98018 |
Slice S0380.21: Table 3a row 1 (no keep-hot) + row 4 dispatch — closes 9 cohort-2 RAISES
SAP 10.2 spec p.160 Table 3a rows:
Row 1 ("Instantaneous, without keep-hot facility"):
(61)m = 600 × fu × n_m / 365 with fu = min(1, V_d,m / 100)
Row 4 ("Instantaneous, with keep-hot, not controlled by time clock"):
(61)m = 900 × n_m / 365
Add `combi_loss_monthly_kwh_table_3a_row_1_no_keep_hot()` and
`combi_loss_monthly_kwh_table_3a_row_4_keep_hot_no_time_clock()` to
`worksheet/water_heating.py`. Extend `pcdb_combi_loss_override` to
dispatch via the PCDB keep_hot_facility / keep_hot_timer fields lodged
at raw positions 58/59 (extracted in Slice S0380.20):
kh ∈ {0, None} → row 1 (600 × fu × n/365, no keep-hot)
kh = 1, timer = 1 → row 3 (cascade default 600 × n/365)
kh = 1, timer ∈ {0, None} → row 4 (900 × n/365, no time clock)
kh ∈ {2, 3} → UnresolvedPcdbCombiLoss (electric or
mixed keep-hot — Table 3a Note 2
fuel-split between (61)m and (219)m
deferred until a fixture exercises it).
Closes 9 of the 11 cohort-2 RAISES from Slice S0380.20 — all PCDF 15709
+ 10315 certs with no keep-hot lodgement now compute to abs(delta) <
1e-4 vs the dr87 worksheet. Verified end-to-end on cert 7800-1501-0922-
7127-3563 (Potterton Promax Combi 28 HE+A, PCDF 15709): Jan (61) =
600 × 0.778795 × 31/365 = 39.6866 kWh, matching worksheet line ref
exactly. The 2 newly-visible cohort-2 issues (cert 6835 -13.37 SAP, cert
0652 +1.93 SAP) were hidden behind the previous strict-raise — they
surface unrelated cascade gaps, not regressions.
Re-add 0390-2954-3640-2196-4175 (Firebird oil PCDF 9005) to the golden
fixture cohort dropped in Slice S0380.20:
- `_EXPECTATIONS` with re-pinned SAP/PE/CO2 residuals (-7 / -26.0093
kWh/m² / -2.5211 t/yr) — the cert now cascades end-to-end via the
no-keep-hot row.
- `_PCDB_CHAIN_EXPECTATIONS` pins PCDF index 9005 + winter eff 0.864
(Table 105 fraction).
Spec citations (per [[feedback-spec-citation-in-commits]]):
- SAP 10.2 spec p.160 Table 3a rows 1 & 4 (formula columns) +
pdftotext of `sap-10-2-full-specification-2025-03-14.pdf | sed -n
'15280,15410p'` (Notes 1 & 2 on fu / electric keep-hot routing).
- STP09-B04 §5.3 "Influence of Keep-hot facility" — origin of the
600 / 900 kWh/yr keep-hot baselines.
Pyright per-file: net-zero on all touched files
(water_heating.py 1→1, cert_to_inputs.py 35→35, tests unchanged).
Test counts: 697 → 702 pass (+5 new tests), 10 expected fails unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
a9faaddc1d |
docs: handover for Table 3a no-keep-hot continuation + SAP 10 spec PDFs
Adds the next-agent handover and the BRE technical papers referenced
by the cohort-2 negative-band investigation:
- `HANDOVER_TABLE_3A_NO_KEEP_HOT.md` — picks up from Slice S0380.20.
Covers cohort distribution at HEAD `4879e8c3`, the verified
Table 3a Row 1 spec formula `(61)m = 600 × fu × nm / 365`, the
dispatch recipe for `pcdb_combi_loss_override`, watch-outs (cert
0360 / cohort-1 cert 000490 behaviour after the slice lands), the
diagnostic probe script, test baselines, and the open-thread
priority list (Ext1 roof, HP-COP, big-gap 2102, API path, parity).
- `specs/STP09-B04_Combi_boiler_tests.pdf` — 2009 BRE methodology
paper (Alan Shiret, BRE) defining the combi-loss test programme
that produced the SAP Table 3a 600/900 kWh/yr keep-hot assumptions.
Source: https://bregroup.com/documents/d/bre-group/stp09-b04_combi
_boiler_tests.
- `specs/sap10 technical papers/S10TP-{02..13}.pdf` — full SAP 10
supporting technical paper set (Issue 1.2 / 1.3 / 1.4 across the
eight papers). S10TP-12 §9.4 confirms: "No changes to the SEDBUK
calculation method for water heating efficiency were considered
necessary" — so the STP09-B04 (SAP 2009) Table 3a methodology
carries through to SAP 10 unchanged.
These docs replace web-fetched references with locally-tracked copies
so the slice S0380.21 implementor can grep / pdftotext them directly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
4879e8c3d7 |
Slice S0380.20: extract PCDB keep-hot fields + strict-raise for no-keep-hot combis
Surfaces the SAP 10.2 Appendix J Table 3a sub-row dispatch gap that
masked +0.2..+0.4 SAP residuals on 11 cohort-2 PCDB-listed combi
certs. Identified via cert 7800-1501-0922-7127-3563 (Potterton Promax
Combi 28 HE Plus A, PCDF 15709): cascade used the keep-hot 600 kWh/yr
default; worksheet (61) sums to ~428 kWh/yr via the no-keep-hot
sub-row formula.
Root cause: the PCDB Table 105 record carries keep-hot metadata at
field positions 58 (`keep_hot_facility`) and 59 (`keep_hot_timer`)
per the SAP 10 PCDB spec (private feed for SAP software vendors —
not surfaced on the public PCDB website nor the Open EPC API). The
parser preserved these in `raw=fields` but didn't surface them as
typed attributes, so the cascade had no signal to dispatch the right
Table 3a sub-row.
Two-part change:
1. `domain/sap10_calculator/tables/pcdb/parser.py` — adds typed
`keep_hot_facility` and `keep_hot_timer` fields to
`GasOilBoilerRecord`, parsed from fields[57] and fields[58].
Field enums (per BRE STP09-B04 + SAP 10 PCDB spec):
Field 58: 0=no keep-hot, 1=fuel keep-hot, 2=electric keep-hot,
3=gas+electric keep-hot
Field 59: 0=no timer, 1=overnight time-switch
Verified against cohort-1 fixture 000490 (Vaillant Ecotec Pro 28,
PCDF 10328) — record lodges keep_hot_facility=1, keep_hot_timer=1,
exactly matching the hand-built fixture comment "Combi keep hot
type = Gas/Oil, time clock" at `_elmhurst_worksheet_000490.py:
277-280`.
2. `domain/sap10_calculator/rdsap/cert_to_inputs.py` — adds
`UnresolvedPcdbCombiLoss` exception. `pcdb_combi_loss_override`
now raises (instead of silently returning None) when the PCDB
record has `separate_dhw_tests=0/None` AND
`keep_hot_facility=0/None`. The cascade's only implemented Table
3a row is "with keep-hot, time clock" (600 kWh/yr), which is the
wrong spec row for no-keep-hot combis — silently using it masked
the cohort-2 negative band.
The ETL was re-run to refresh `pcdb_table_105_gas_oil_boilers.jsonl`
with the new typed fields (raw fields unchanged, just additional
columns surfacing what was previously buried).
Cohort distribution after slice:
cohort-1 cert 000490 (Vaillant PCDF 10328, kh=1): NO RAISE — cascade
keep-hot 600 default IS the spec-correct row. Tests still GREEN.
cohort-2: 10 exact + 13 sub-±0.07 + 2 ±0.07..0.5 + 1 ±0.5..1 +
1 ±5+ + 11 RAISES.
The 11 raising certs are now blocked until the Table 3a no-keep-hot
sub-row is implemented (BRE STP09-B04 methodology — pending slice).
Previously these certs silently produced +0.2..+0.4 SAP errors AND
ranged into the big-gap band; raising surfaces the gap rather than
shipping wrong numbers.
Two golden cert tests blocked alongside (Firebird oil PCDF 9005 also
hits this path):
- test_golden_cert_residual_matches_pin[0390-2954-3640-2196-4175]
- test_api_to_domain_mapper_preserves_main_heating_index_number[0390-2954-3640-2196-4175]
Re-enable when the Table 3a no-keep-hot row lands.
Two other tests updated:
- test_main_heating_index_number_in_pcdb_overrides_seasonal_efficiency:
switched from Baxi 98 (sdt=0, kh=None, would raise) to Worcester
PCDF 10241 (sdt=1, routes via Table 3b row 1). Asserts 0.885 not
0.66.
- test_pcdb_combi_loss_override_returns_none_or_raises_for_untested
_or_storage_combis: renamed + extended to pin the new strict-raise
behaviour.
Pyright net-zero per file:
- domain/sap10_calculator/rdsap/cert_to_inputs.py: 35 (baseline 35)
- domain/sap10_calculator/tables/pcdb/parser.py: 0
- domain/sap10_calculator/tables/pcdb/__init__.py: 0
- domain/sap10_calculator/rdsap/tests/test_cert_to_inputs.py: 13 (baseline 13)
- domain/sap10_calculator/rdsap/tests/test_golden_fixtures.py: 1 (was 2 — improved)
Regression baseline: 697 pass + 10 fail (= prior 699 + 10 - 2 dropped
golden parametrize entries for cert 0390-2954-3640-2196-4175).
Spec refs:
- SAP 10 PCDB spec (private SAP software vendor feed) — keep-hot
facility / timer / electric-heater fields at positions 58 / 59 / 60.
- BRE STP09-B04 (combi boiler test methodology) — origin of the
keep-hot Table 3a derivation. URL: https://bregroup.com/documents/d
/bre-group/stp09-b04_combi_boiler_tests
- SAP 10.2 Appendix J Table 3a row-selection — to be implemented per
PCDB keep-hot dispatch in a follow-up slice.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
1f8a070f66 |
Slice S0380.19: count Elmhurst shower outlets by type (no more hardcoded 1)
Surfaces the lodged shower multiplicity from the Elmhurst Summary §16
on the EPC. Previously `_map_elmhurst_sap_heating` hardcoded:
electric_shower_count = 1 if has_electric_shower else None
mixer_shower_count = 0 if has_electric_shower else None
losing the count for any cert with ≥ 2 outlets. Cert
7800-1501-0922-7127-3563 lodges TWO instantaneous electric showers
("Shower 01" + "Shower 11") but the mapper produced
`electric_shower_count=1`. After this slice:
electric_shower_count = Σ(s for s in showers if s.outlet_type
== "Electric shower")
mixer_shower_count = Σ(s for s in showers if s.outlet_type
!= "Electric shower")
**Cascade SAP effect:** None on cert 7800. Appendix J's eq J16
(`N_ES,per_outlet = N_shower / N_outlets`) and eq J18 (Σ_j E_ES,j)
are symmetric in N_electric_showers when there are no mixer outlets,
so the lodged (64a) kWh and (247a) cost are unchanged. The fix is
correctness-by-construction, not a delta-closer for the negative-band
certs (their +0.69 GBP total-cost gap traces to the gas hot-water
kWh path — separate slice).
**Hand-built fixture updates (5):** the cohort-1 hand-builts at
`domain/sap10_calculator/worksheet/tests/_elmhurst_worksheet_*.py`
previously omitted `electric_shower_count` / `mixer_shower_count`
(implicitly None), which matched the mapper's pre-slice None
sentinel. Updated each to the lodged counts the mapper now surfaces:
000474: 1 mixer → (0, 1)
000477: 1 mixer → (0, 1)
000480: 1 mixer → (0, 1)
000490: 1 mixer → (0, 1)
000516: 1 mixer → (0, 1)
000487 (already at (1, 0) for an electric-shower lodging) unchanged.
Tests:
- `test_summary_7800_two_electric_showers_count_as_two_not_one` —
pins the multi-shower mapping for cert 7800 (Summary_000890.pdf).
- 5 hand-built field-parity tests
(`test_from_elmhurst_site_notes_matches_hand_built_*`) now pass at
the new integer counts instead of None.
Pyright net-zero per file:
- datatypes/epc/domain/mapper.py: 32 (baseline 32)
- backend/documents_parser/tests/test_summary_pdf_mapper_chain.py: 0
Regression baseline: 699 pass + 10 fail (= prior 698 + 10 + 1 new).
Spec refs:
- SAP 10.2 Appendix J §1a — outlet counting drives `N_outlets` used
in eq J6/J7 (mixer shower water draw) and eq J16/J17/J18 (electric
shower energy).
- Cert 7800-1501-0922-7127-3563 Summary §16 "Showers" lodgement.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
57fbf83b1e |
Slice S0380.18: u_party_wall flat default per RdSAP10 Table 15 footnote*
Closes cert 0036-6325-1100-0063-1226 (the cohort's first FLAT fixture)
from Δ -0.3737 → +0.2987 by applying the RdSAP 10 Table 15 footnote *
rule: flats/maisonettes with unknown party-wall construction default
to U=0.0 W/m²K (both sides are heated dwellings, no heat loss).
Worksheet dr87-0001-000910.pdf line ref (32) lodges:
Party walls Main 24.13 m² U=0.00 A×U = 0.0000 W/K
matching the Table 15 footnote *. The cascade was applying the U=0.25
*house* default to this lodging because:
- Elmhurst Summary lodged `party_wall_type='U Unable to determine'`
- mapper translated it to `party_wall_construction=0` (the cross-
mapper-parity "unknown" sentinel)
- `u_party_wall(0)` fell through to `return 0.25` (the final-branch
default — same path as `u_party_wall(None)`)
That produced cascade `party_walls_w_per_k = 24.13 × 0.25 = 6.03` W/K
of heat-loss excess, propagating through (39) HTC → (97)..(98c) space
heat demand → (211) main fuel kWh → (255) total cost → (257) ECF →
(258) SAP rating. Net effect: cascade SAP 62.3734 vs worksheet 62.7471.
Two-part fix:
1. `domain/sap10_ml/rdsap_uvalues.py:u_party_wall` — add
`is_flat: bool = False` keyword argument. When True AND
`party_wall_construction in (None, 0)` (both the API-mapper None
path and the Elmhurst-mapper 0 sentinel for "Unable to determine"),
return 0.0 instead of the house default 0.25. Spec citation: RdSAP
10 Table 15 footnote * ("for flats and maisonettes with unknown
party-wall construction").
2. `domain/sap10_calculator/worksheet/heat_transmission.py` — wire
the cascade to pass `is_flat=_is_flat_or_maisonette(epc.property
_type)`. Adds a new helper `_is_flat_or_maisonette` distinct from
the existing `_is_house` (which excludes bungalows from
*cantilever* detection — bungalows ARE houses for party-wall
purposes per the spec). The new helper checks both the descriptive
form ("Flat" / "Maisonette") and the SAP schema enum-as-string
form ("2" / "3" — per `datatypes/epc/domain/epc_codes.csv
property_type` rows: 0=House, 1=Bungalow, 2=Flat, 3=Maisonette,
4=Park home).
The schema-enum collision was the bug-fix-with-a-bug: an initial
implementation used "1"/"2" (Flat/Maisonette per intuition) but those
are actually Bungalow/Flat per the schema, which routed all 10
bungalow certs onto the flat path. Corrected pre-commit.
Cohort-2 Summary-path delta after slice:
cert 0036 (Flat) Δ -0.3737 → Δ +0.2987 ✓ improved by +0.67
10 bungalow certs unchanged (correctly NOT flat)
5 non-flat house certs in band unchanged (different root cause —
next slice)
Bungalow certs (cohort 1 + 2) verified unchanged at delta ≤ +0.04 each.
Tests added (5):
- `test_u_party_wall_unknown_for_flat_returns_table15_footnote_zero`
pins the spec rule on the helper.
- `test_u_party_wall_unknown_sentinel_zero_treated_as_unknown_for_flat`
pins the Elmhurst-mapper `0` sentinel parity.
- `test_u_party_wall_known_solid_still_returns_zero_when_is_flat_false`
pins precedence: explicit Solid code overrides the is_flat flag.
- `test_summary_0036_flat_unknown_party_wall_routes_to_u_zero` chain-
test through `from_elmhurst_site_notes` + cert_to_inputs +
calculate_sap_from_inputs to assert `party_walls_w_per_k == 0` at
1e-4 tolerance.
Pyright net-zero per file:
- domain/sap10_ml/rdsap_uvalues.py: 1 (baseline 1)
- domain/sap10_calculator/worksheet/heat_transmission.py: 13 (baseline 13)
- domain/sap10_ml/tests/test_rdsap_uvalues.py: 66 (baseline 66)
- backend/documents_parser/tests/test_summary_pdf_mapper_chain.py: 0
Regression baseline: 698 pass + 10 fail (= prior 694 + 10 + 4 new).
Note: the remaining +0.2987 residual on cert 0036 is in (30) external
roof — worksheet lodges Ext1 flat roof Plasterboard insulated U=2.30
giving 2.51 W/K; cascade has roof_w_per_k=0 (Ext1 roof contribution
missing). Separate slice.
Spec refs:
- RdSAP 10 Table 15 ("U-values of party walls") row 4 — house unknown
default 0.25 W/m²K.
- RdSAP 10 Table 15 footnote * — flat/maisonette unknown default
0.0 W/m²K.
- `datatypes/epc/domain/epc_codes.csv` rows
`property_type,{0..4},...` — SAP/RdSAP schema property-type enum.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
6b1cdd64bc |
Slice S0380.16: add 'Normal' → cylinder_size=2 (110 L) for cohort 2
Unblocks two 38-cert-cohort certs that previously raised
`UnmappedElmhurstLabel("cylinder_size", 'Normal')` at extraction:
cert 2536-2525-0600-0788-2292 ws SAP=79.7264
cert 9421-3045-3205-1646-6200 ws SAP=87.4495
Both Summary §15.1 lodgements read "Cylinder Size: Normal"; both dr87
worksheets lodge line ref (47) "Store volume = 110.0000" L (extracted
from `Hot Water Cylinder → Cylinder Volume 110.00`). RdSAP 10 §10.5
Table 28 documents the "Normal (90-130 litres)" descriptor whose
midpoint is 110 L — the canonical Elmhurst label string in
`datatypes/epc/surveys/elmhurst_site_notes.py` is "Normal (90-130
litres)", and the worksheet's exact 110 L matches the midpoint.
Two-line fix:
+ "Normal": 2, in `_ELMHURST_CYLINDER_SIZE_LABEL_TO_SAP10`
+ 2: 110.0, in `_CYLINDER_SIZE_CODE_TO_LITRES`
The cascade enum 2 is consistent with the existing
`cert_to_inputs.py` docstring's documented (but not-yet-observed)
code 2 → Normal slot, alongside code 3 (Medium / 160 L) and code 4
(Large / 210 L) added in earlier slices.
Slice keeps tight: two mapping unit tests pinning `cylinder_size == 2`
for both certs at extraction. Post-fix the first-attempt cascade
deltas vs worksheet are:
cert 2536 Δ +0.0244 (was: RAISES)
cert 9421 Δ +0.0296 (was: RAISES)
Both deltas now sit in the same systematic +0.02..+0.07 small-gap
band as ~12 other first-attempt certs in cohort 2 — chain test +
±0.07 pin would just paper over a known systematic residual that the
user has explicitly asked to drive towards 1e-4, not toward ±0.07.
Following slice will investigate the shared systematic offset and
close cert 2536 / 9421 along with the rest of the +0.04 band on
the chain.
Pyright net-zero per file:
- datatypes/epc/domain/mapper.py: 32 (baseline 32)
- domain/sap10_calculator/rdsap/cert_to_inputs.py: 35 (baseline 35)
- backend/documents_parser/tests/test_summary_pdf_mapper_chain.py: 0
Regression baseline: 691 pass + 10 fail (= prior 689 + 10 + 2 new GREEN).
Spec refs:
- RdSAP 10 §10.5 Table 28 — "Cylinder Volume" Normal band 90-130 L,
midpoint 110 L (also the canonical Elmhurst label suffix).
- Cert 2536 worksheet `dr87-0001-000889.pdf` line ref (47) = 110.0000.
- Cert 9421 worksheet `dr87-0001-000884.pdf` line ref (47) = 110.0000.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
92fc4f4f16 |
docs: handover — Summary + API cohort expansion to 38 additional certs
Hands off the next workstream: the 38 cert subdirs at
`sap worksheets/additional with api 2/`. Each subdir is named after
the 20-digit EPC cert reference and contains a Summary PDF + dr87
worksheet PDF. API JSONs are NOT in the dataset but ARE fetchable
via the existing `EpcClientService` (token in `backend/.env` as
`OPEN_EPC_API_TOKEN`).
User's stated ordering: Elmhurst Summary mapping FIRST, API path
SECOND. Folder names = cert refs; need to verify the matching before
bulk-pinning (any mis-filed PDF would silently invalidate slice
work).
Handover ships with verified dataset and first-attempt baselines:
- Folder-vs-cert sweep: **38/38 match** at handover (postcode
parity check between Summary PDF and Open EPC API).
- First-attempt Summary-path probe across 38 certs:
24 ✅ closed at ±0.07 (first-try, zero new slices needed)
9 ~ small gap (<1 SAP) — likely 1 slice each
3 ✗ big gap (>1 SAP) — multi-slice investigation
2 RAISES UnmappedElmhurstLabel: cylinder_size='Normal'
The two `Normal` cylinder raises are the immediate Phase 1 slice —
Slice S0380.15's strict-enum pattern paid off on its first new
cohort by surfacing the gap at extraction time instead of as a
downstream SAP delta.
Workstream phases documented in the handover:
Phase 0: folder-vs-cert sweep (already done — 38/38)
Phase 1: fix 'Normal' cylinder unmapped-label raise
Phase 2: bulk-pin the 24 first-try-closures as chain tests
Phase 3: close the 9 small-gap certs one slice each
Phase 4: investigate the 3 big-gap certs (likely HP-routing)
Phase 5: fetch + persist API JSON for all 38, run API path tests
Phase 6: cross-mapper EPC parity (Summary EPC ≡ API EPC) — the
user's stated north-star
Includes:
- Paste-able diagnostic probe scripts (Summary path + folder-vs-
cert sweep + .env loader + EpcClientService usage example).
- Full table of first-attempt deltas per cert with classifications.
- All 15 prior-session slice commits indexed.
- Memory references to the slicing / methodology conventions.
- Per-cert diagnostic recipe template.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
7f099d986a |
Slice S0380.13: widen cantilever gate to accept "House" descriptive form
Closes cert 2636 to spec floor (Δ +0.5167 → +0.0323) by accepting
both the EPC schema enum-as-string ("0") AND the Elmhurst Summary
mapper's descriptive form ("House") for the cantilever-detection
property-type gate at `heat_transmission.py:768`.
Root cause: slice 102f-prep.9 (commit
|
||
|
|
c90853428c |
docs: handover — start cert 0380 Summary → EPC → calculator path
The 7-cert ASHP cohort API path is closed at the spec-precision floor (this session). Next workstream is the Summary path for cert 0380 — the user's preferred starting point because the Summary + worksheet PDFs surface labelled intermediate values that the API path lacks. Cert 0380 Summary PDF (`Summary_000899.pdf`) is already in the test fixtures dir; just needs a path constant + RED chain test. Previous handover flagged the extractor at Δ -58.37 SAP for HPs — the immediate diagnostic is whether the mapper surfaces main_heating_category=4 and main_heating_index_number=104568. The handover also documents the user's "Elmhurst-specific" challenge worth re-exploring: closed boiler certs hit 1e-4 vs Elmhurst via the same cascade, so the residual is precisely at the Appendix N3.6 PSR interpolation step. Cross-check with the BRE xlsx canonical calculator is suggested. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
38a5f906bc |
docs: handover refresh — cohort closed to spec-precision floor
Updates the handover with the final state after 11 slices: - All 7 ASHP cohort certs cascade SAP integer == lodged (residual 0). - Continuous SAP residual clusters within +0.030..+0.060. - BRE web confirmed max_output_kw values (4.39 / 3.933) match cascade exactly — the remaining drift is NOT a max_output bug. - Cascade (39) annual avg HLC EXACTLY matches worksheet (39) at 4 dp for cert 0380 and 2225 — HLC is NOT the bug either. - Implied drift is ~0.15% in η_space interpolation precision, likely in Elmhurst's internal rounding convention (not in public SAP 10.2 spec or BRE PCDB). Recommends Path A (ship Layer 4 chain tests at ±0.07 SAP tolerance) as the spec-precision floor. Path B (close to 1e-4) requires Elmhurst implementation access that's outside public docs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
db77a7c776 |
Slice 102f-prep.11: Track 6 ASHP cohort fixtures + register 7 golden pins
Fetches the API JSON for each of the 6 previously-missing ASHP
cohort certs (0350, 2225, 2636, 3800, 9285, 9418) into
tests/fixtures/golden/ so they're tracked alongside cert 0380 (the
cohort anchor lodged earlier). Each cert's residual against its
GOV.UK EPC lodgement is pinned in `_GOLDEN_EXPECTATIONS`:
- SAP integer residual = 0 across all 7 certs (cascade rounds to
the lodged value exactly).
- PE residual: -7.93 to -14.79 kWh/m² (cascade UNDER-estimates
primary energy by ~8-15 — likely PV cascade self-consumption
β-factor split per Appendix M §3, untouched by this workstream).
- CO2 residual: +0.16 to +0.28 t/yr (cascade OVER-estimates by ~0.2).
The pins lock the current cascade state so future mapper / cascade
changes fire loudly when they shift the 7-cohort residuals (the same
pin-tracking convention as the existing 8 boiler golden certs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
24a7351fed |
Slice 102f-prep.10: Alt-wall opening allocation per window_wall_type
RdSAP §1.4.2: window openings deduct from the gross of the wall they pierce. The cert schema lodges `window_wall_type` on each SapWindow: code 1 = main wall, codes 2/3 = alternative walls 1/2. Cohort ground-truth: cert 2636 BP0 lodges one window (1.14 × 1.04 ≈ 1.19 m²) with `window_wall_type=2` → it pierces alt.1 (12.76 m² cavity unfilled at age D → U=0.70). Pre-fix the cascade subtracted ALL openings from the BP's (main+alt) gross then routed each alt at its FULL gross — over-counting alt's contribution by 1.19 × U_alt and under-counting main by 1.19 × U_main. For cert 2636: 1.19 × (0.70 − 0.25) = +0.535 W/K cascade walls excess, matching the observed cascade walls 20.56 vs worksheet 20.024. `_window_on_alt_wall` translates the per-window `window_wall_type` code; the per-BP loop aggregates alt-wall windows into `alt_window_area_by_bp`, passes that opening area through to `_alt_wall_w_per_k` (alt.1 only — no cohort cert exercises alt.2 windows), and adds the deducted area back to the main wall's net area so the conservation invariant holds. Cohort impact: cert 2636 cascade walls closes from 20.5595 → 20.0240 (spec-exact to 1e-3). Cascade (37) closes from 114.7067 → 114.1846 (Δ +0.0134 from a small thermal-bridging area rounding diff). Cert 2636 SAP shifts from -0.0055 → +0.0323 — joining the cohort cluster (all 7 ASHP certs now within +0.030 to +0.059 SAP). The current near-zero cancellation state for cert 2636 was hiding two opposite cascade errors (over-count walls + under-count η_space). This slice closes walls correctly; the remaining +0.03 SAP cluster across all 7 certs is the systematic PSR-denominator HLC×ΔT drift documented in the handover (not max_output, which BRE confirmed is 4.39 kW exactly). Zero regressions on Elmhurst hand-built fixtures, closed-cert Layer 4 1e-4 chain gates, or golden cert residual pins. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
06b4ef3d12 |
Slice 102f-prep.9: RdSAP cantilever exposed-floor detection (closes cert 2636)
RdSAP "first floor over passageway" rule — when an upper storey has
larger floor area than the storey immediately below, the excess
overhangs an unheated space or external air and routes through
Table 20's U_exposed_floor (1.20 W/m²K for age-D + no insulation,
the modal cohort lodging).
Cohort ground-truth: cert 2636 BP0 floor 1 (42.92 m²) − floor 0
(39.18 m²) = 3.74 m². Worksheet (28b) "Exposed floor Main: 3.74 ×
1.20 = 4.4880" matches the spec rule exactly.
`_part_geometry` now computes `cantilever_floor_area_m2` per BP.
The per-BP loop in `heat_transmission_from_cert` injects U×A onto
the floor accumulator and includes the area in (31) total external
area (which feeds (36) thermal bridges).
Gated to avoid false positives on flats and sub-ground multi-storey
shapes:
- `property_type == "0"` (house) — excludes flats (cert 9501 BP0
has 6.85 m² floor 0 + 74.43 m² floor 1; the diff is stairwell
access, not a real cantilever).
- `excess >= 1 m²` — excludes 2-dp rounding artefacts (cert 001479
Main BP0 lodges floor 1 = 30.77 vs floor 0 = 30.45 → 0.32 m²
drift that's not a real cantilever; would otherwise add 0.4
W/K and break the closed-cert 1e-4 Layer 4 chain gate).
- `excess / prev_area < 0.25` — excludes sub-ground / partial-
storey shapes (cert 7536 BP0: 33.7/17.28 = 195% — not a real
cantilever; floor 0 likely a partial vestibule, not the full
ground footprint).
Cohort impact: cert 2636 SAP residual closes from +0.4873 → -0.0055
(by far the largest cohort outlier becomes the closest match).
Zero regressions: 654 pass + 10 pre-existing baseline fails (9 cert
001479 hand-built skeleton + 1 FEE). All 7 ASHP certs now cluster
within ±0.06 SAP vs worksheet.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
a62c26758e |
docs: handover update — slices 102f-prep.1-8 shipped, cohort analysis
Refreshes the handover with the full session's work: - All 7 ASHP cohort certs' MIT cascade matches worksheet (92) at 1e-3. - 6/7 cohort SAP residuals cluster at +0.03..+0.06 vs worksheet. - Identified PSR-formula drift root cause: max_output_kw ≈ 4.40 kW back-solved from 3 certs' worksheet η_space pins, vs the 4.39 lodged at PCDB position 47 (likely a field-position misread; needs BRE web cross-check for PCDB 104568 / 102421). - Identified cert 2636's +0.49 outlier as missing cantilever Exposed floor (3.74 m² = upper-floor 42.92 − ground-floor 39.18 area diff). Recommends Path A (resolve max_output + cantilever to land 1e-4) or Path B (widen Layer 4 tolerance to 0.1 with documented limitations). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
1d5183c67b |
Slice 102f-prep.8: API mapper resolves shower_outlets=None → 0 mixers
Cert 2225 (Mitsubishi PUZ-WM50VHA, semi-detached 2-bp, TFA 82.49) lodges `sap_heating.shower_outlets = None` in the Open EPC API JSON. The worksheet (42a) "Hot water usage for mixer showers" reads 0 every month — Elmhurst's convention is "absent ⇒ no shower". Pre-fix the API mapper returned `mixer_shower_count = None`, deferring to the cert→inputs cascade's "RdSAP modal lodging" default of 1 vented mixer. That added ~7 L/day to (44) daily HW use, ~113 kWh/yr to (62) HW demand, and shifted cert 2225's SAP residual from -0.31 → +0.04 (now aligned with the cohort's +0.03..+0.06 cluster) once the mapper returns 0. `_count_shower_outlets_by_type` now treats None as 0 (the API mapper-only path). The cert→inputs cascade's `_mixer_shower_flow_rates_from_cert` keeps the None→1 default for the Elmhurst hand-built fixture path that doesn't route through this helper. Cohort impact: 6 of 7 ASHP certs now cluster at SAP Δ +0.03 to +0.06 (vs worksheet); only cert 2636 remains an outlier (+0.49). Golden cert PE/CO2 pins re-pinned for 6035, 8135, 0390 (the three certs that previously lodged shower_outlets=None and consumed the spurious 1-mixer default). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
4eacfa6296 |
Slice 102f-prep.7: Table N4 fixed durations ("24"/"16") in HP extended-heating helper
SAP 10.2 Appendix N3.5 Table N4 (PDF p.107) — heat-pump packages
with fixed daily heating durations:
- "24" → N24,9 = 365 (continuous): every day at heating temperature,
no off period → (days_in_month, 0) per month → MIT_zone = Th.
- "16" → N16,9 = 365 (unimodal, 0700-2300): every day with single
8h off → (0, days_in_month) per month → MIT_zone = Th − u1(8h).
- "9" → standard SAP schedule (bimodal 7+8 off): falls through to
`None` so the orchestrator applies the legacy bimodal path.
Cert 9418 (Daikin Altherma EDLQ05CAV3, PCDB 102421) lodges
`heating_duration_code = "24"` — worksheet (87) MIT_living = 21.0
every month (= Th1, no off period) and (90) MIT_elsewhere collapses
to Th2 directly. Pre-fix the bimodal cascade produced MIT ~17.8-19.8
(2.04°C low at Jan) and SAP was +2.20 over worksheet 84.6305.
Post-fix cert 9418 closes to SAP Δ +0.0296 (from +2.20) — the
residual is consistent with the same ~0.05 PSR-formula drift seen
in 5/7 cohort certs sharing PCDB 104568.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
143d11d39f |
docs: handover — cert 0380 §N3.5 MIT cascade shipped (102f-prep.1-6)
Session shipped 6 slices closing cert 0380's SAP residual from +0.5999 → +0.0594 vs worksheet 88.5104. The MIT cascade now matches worksheet line (92) at 1e-3 per month and is spec-faithful through SAP 10.2 Appendix N3.5 + Equation N5. Remaining residual is a single PSR-formula divergence (cascade PSR 1.4266 per spec vs worksheet-implied 1.4321, ~0.4%) that propagates to η_space at 0.2% and ~0.045 SAP. Three candidate root causes documented; investigation deferred to next session as the blocker for slice 102f's Layer 4 1e-4 chain test. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
80e528e5aa |
Slice 102f-prep.6: HP-gate §5 central-heating pump gains (Table 4f)
SAP 10.2 Table 4f (PDF p.169) — heat-pump packages (main heating
category 4) bundle the circulation pump's electricity into the
system COP, so worksheet line (70) "Pumps, fans" reports zero gain
for every month on HP certs. Cert 0380's worksheet confirms 0.0
through Jan-Dec.
`internal_gains_from_cert` previously called `central_heating_pump_w`
unconditionally and routed the 3/7/10 W (date-bucket) result through
the seasonal mask in `pumps_fans_monthly_w`. For HP certs that added
~7 W of spurious heating-season gains to (73)m → cold-month MIT
drifted +0.008°C above worksheet (92).
Gating the pump-W computation on `_CATEGORIES_WITHOUT_CENTRAL_HEATING
_PUMP = {4}` zeroes the gain for HP certs and leaves every other
category (gas, oil, electric storage, …) on the existing cascade.
Cohort impact:
- Cert 0380 MIT 12-tuple now matches worksheet (92) at 1e-3 per
month (worst Δ at Nov = -0.0009°C).
- SAP residual closes from +0.155 → +0.059 vs worksheet 88.5104.
- Closed certs (001479 / 0330 / 9501 — all boiler cohorts, cat 2
or 1) are unaffected; Layer 4 1e-4 chain gates remain GREEN.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
2be7905637 |
Slice 102f-prep.5: Wire N3.5 extended-heating MIT cascade (HP-gated)
SAP 10.2 Appendix N3.5 (PDF p.106-107) replaces Table 9c steps 3-4
for heat-pump packages with PCDB data — each month blends the
heating temperature Th, the unimodal (16-hour day, one 8-hour off
period per Table N7 footnote b) zone temperature, and the bimodal
(9-hour day, two off periods per Table N7) zone temperature via
Equation N5:
T = [N24,9 × Th + N16,9 × T_uni + (Nm − N16,9 − N24,9) × T_bi] / Nm
`mean_internal_temperature_monthly` gains an optional
`extended_heating_days_per_month` kwarg (12-tuple of (N24,9_m,
N16,9_m)). When provided, the orchestrator computes T_unimodal per
zone from a single 8-hour off-period reduction and blends; when
None (default — every non-HP cert) it returns T_bimodal directly,
so closed certs (001479, 0330, 9501) are bit-identical.
`cert_to_inputs` derives the per-month tuple for HP certs with PCDB
records carrying `heating_duration_code = "V"` (Variable) — the
only code lodged on modern records per SAP 10.2 PDF p.105 footnote
48. Cohort path: PSR (= max_output_kw × 1000 / (HLC × 24.2 K)) →
Table N5 PSR interpolation → cold-first day allocation. Fixed
durations "24" / "16" / "9" from legacy Table N4 are deferred —
not exercised by the cohort.
Cert 0380 SAP residual closes from +0.5999 → +0.1550 vs worksheet
88.5104. The remaining ~0.16 SAP delta is split between two
orthogonal §5 / §7 residuals (cold-month +0.008°C MIT drift from
spurious HP pump gains; sub-1e-3 efficiency bias) that the next
slices target. Pin tolerance is 1e-2 per month on worksheet (92)
to capture this slice's contract alone, with `feedback_zero_error_
strict` widening documented inline.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
c341eba9a2 |
Slice 102f-prep.4: Equation N5 zone-mean blending leaf
SAP 10.2 Appendix N3.5 Equation N5 (PDF p.107):
T = [N24,9 × Th + N16,9 × T_uni + (Nm − N16,9 − N24,9) × T_bi] / Nm
`extended_zone_mean_temperature_c` is the pure-math leaf: takes
pre-computed bimodal (9-hour heating, two off periods) and unimodal
(16-hour heating, one 8-hour off period per Table N7 footnote b)
zone temperatures and the per-month day allocations, blends across
the three heating patterns (Th for 24-hour days, T_uni for 16-hour,
T_bi for the standard 9-hour SAP schedule).
Pinned against cert 0380's January living-area MIT: Th=21, T_bi
=18.5551 (worksheet "Living" row), T_uni back-solved from (87)
= 19.6153, N24=3, N16=28, Nm=31 → 19.7493 (worksheet (87) Jan).
Collapses cleanly: N24=N16=0 → T_bi (warm months / non-HP certs);
N24=Nm → Th (full 24-hour heating).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
4e07991f8f |
Slice 102f-prep.3: Table N5 day allocation Jan/Dec/Feb/Mar/Nov/Apr/Oct/May
SAP 10.2 Appendix N3.5 (PDF p.107): "Allocate these to months in the
following order: Jan, Dec, Feb, Mar, Nov, Apr, Oct, May (coldest to
the warmest), until all the days N24,9 and N16,9 have been allocated.
Days N24,9 are allocated first."
`allocate_extended_heating_days_to_months` distributes annual N24,9
and N16,9 totals (from Table N5) across the cold-first month order,
with N24 days filling first and N16 days filling whatever space
remains in each month afterward.
Cross-pinned against the spec's PSR=0.2 worked example (PDF p.107):
Jan-Oct each get max N24, May ends up with the residual (6, 6). And
against cert 0380's worksheet: PSR≈1.43 → row 1.2+ (3, 38) →
Jan(3, 28), Dec(0, 10) — matches the worksheet 24/9 + 16/9 rows.
The 8 cold-month order spans 243 days, exceeding every Table N5
row's combined total — no allocation is dropped for Variable
heating duration. Fixed durations ("24" / "16" from Table N4) live
beyond this helper's contract (caller decides when N24=365 means
"all months at Th"); slice 102f-prep.4 wires that in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
a6ef198740 |
Slice 102f-prep.2: Table N5 PSR interpolation (variable heating duration)
SAP 10.2 Appendix N3.5 + Table N5 (PDF p.107) — for heat pumps with "Variable" daily heating duration, the annual N24,9 and N16,9 totals (days operating at 24h or 16h instead of the standard 9h) are obtained by linear interpolation between Table N5 rows at the dwelling's plant size ratio, rounded to the nearest whole number of days. Clamps to the table bounds (PSR ≤ 0.2 → first row; PSR ≥ 1.2 → last row) per the same convention applied to PSR efficiency lookup in Appendix N (PDF p.101 lines 6007-6008). Cohort sanity: cert 0380's PSR ≈ 1.43 → (3, 38) per the last-row clamp; worksheet shows Jan N24,9=3 + Jan/Dec N16,9=28+10=38 — exact match to Table N5 row "1.2 or more". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
7adb6c7907 |
Slice 102f-prep.1: PCDB Table 362 heating_duration_code field
SAP 10.2 Appendix N3.5 (PDF p.105 line 6099) — heat-pump packages lodge a "Daily heating duration" field encoded as "24" / "16" / "9" / "V" (Variable). Footnote 48 (PDF p.105): "Daily heating durations of 24, 16 and 9 hours are retained for legacy purposes" — modern records always lodge "V". Format-465 position 48 holds the code; cohort ground truth: "V" on Mitsubishi PUZ-WM50VHA (104568) and Daikin EDLQ05CAV3 (102421). The field drives Appendix N3.5 + Table N4/N5 day allocation for the extended-heating MIT cascade (slice 102f-prep.2 onward). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
7237bc25b0 | docs: handover — cert 0380 HW cascade (slices 102a-e shipped, MIT residual deferred to next session) | ||
|
|
7a8c8facec |
Slice 102e: heat-pump APM efficiencies via SAP 10.2 Appendix N3.6 / N3.7(a)
For any cert lodging a Table 362 heat-pump PCDB record, the cascade now replaces the Table 4a category defaults with PSR-interpolated efficiencies per SAP 10.2 Appendix N (PDF p.108): (206) = 0.95 × η_space,1_interp (N3.6 in-use factor) (217) = in_use_factor × η_water,3_interp (N3.7(a) + footnote 49) where η_space,1 and η_water,3 are PSR-dependent values from the PCDB record's PSR-group table (decoded in slice 102c.2), and the dwelling's PSR is computed per PDF p.100 line 5946-5950: PSR = max_nominal_output_kw / (HLC_annual_avg_W_per_K × 24.2 K / 1000) The N3.7 in-use factor (PDF p.6097) tests three cylinder criteria: 1. cert volume ≥ PCDB volume 2. cert heat-exchanger area ≥ PCDB area (unless PCDB area = 0 per fn53) 3. cert heat loss [(47)×(51)×(52)] ≤ PCDB heat loss All three pass → 0.95; any criterion fails or is unknown → 0.60. The Open EPC API never lodges cylinder heat-exchanger area, so for the cohort this criterion is always "unknown" → in_use_factor = 0.60. Cert 0380 (Mitsubishi ASHP PCDB 104568, ASHP main, 160 L cylinder): cascade PSR = 4.39 / (127.158 × 24.2 / 1000) ≈ 1.4266 cascade η_space,1_interp ≈ 235.24 (PSR-1.2 row 253.9, PSR-1.5 229.2) cascade η_water,3_interp ≈ 285.13 (PSR-1.2 row 287.7, PSR-1.5 284.3) cascade main_heating_eff ≈ 2.2348 (vs worksheet 2.2305, 1.9e-3 diff) cascade HW kWh/yr ≈ 878.05 (vs worksheet 877.97, 0.08 kWh/yr) cascade SAP rating ≈ 89.11 (vs worksheet 88.5104, +0.60) The remaining +0.60 SAP residual is bounded by the ~0.4% PSR-formula drift (the cascade computes PSR=1.4266 from (39)_annual_avg × 24.2 K whereas the worksheet back-solves to ≈ 1.4321). Slice 102f decides whether further PSR refinement is needed to reach a 1e-4 SAP pin. |
||
|
|
c4a1045c8f |
Slice 102d: primary circuit loss via SAP 10.2 Table 3 with PCDB vessel gate
SAP 10.2 §4 line 7700 + Table 3 (PDF p.159) define the primary circuit
loss for cylinders heated indirectly through primary pipework:
(59)m = n_m × 14 × [{0.0091 × p + 0.0245 × (1 − p)} × h + 0.0263]
Inputs:
p pipework insulation fraction — Table 3 rows: 0.0 uninsulated,
0.1 first 1 m, 0.3 all accessible, 1.0 fully insulated. RdSAP §3
default table (PDF p.56) supplies p by construction age band:
bands A-J → 0.0, K, L, M → 1.0.
h hours per day of primary circulation, winter / summer split:
• no cylinder thermostat → 11 / 3
• thermostat, NOT separately timed → 5 / 3
• thermostat, separately timed → 3 / 3
("Use summer value for June, July, August and September and
winter value for other months" — spec p.159 footer.)
Spec p.159 lists the zero-loss configurations:
- electric immersion heater
- combi boiler
- CPSU
- thermal store within single casing
- separate boiler + thermal store within 1.5 m insulated pipe
- direct-acting electric boiler
- heat pump from PCDB with HW vessel integral to package
The cohort gate is now PCDB-aware: HP main + PCDB Table 362 record
`hw_vessel_mode != 1` (i.e. non-integral) → primary loss applies. All
7 cohort ASHPs lodge `hw_vessel_mode = 2` (separate and specified)
per Table 362 records 104568 (Mitsubishi) and 102421 (Daikin).
Cert 0380 (band D → p=0.0; cylinder thermostat + separately-timed →
h=3 / 3) lands (59)Jan = 31 × 14 × (0.0245 × 3 + 0.0263) = 43.3132
kWh/month (test pinned at 1e-4 vs cert's dr87 worksheet).
Cumulative cert 0380 API state:
HW kWh/yr 431.4 → 653.1 (target 878, slice 102e closes via η_water)
SAP 92.3 → 91.2 (delta to worksheet 88.51 now +2.73, was +3.75)
Cohort regression: cert 0390-2954 (oil boiler + cylinder, age F →
band A-J p=0.0) now picks up ~516 kWh/yr primary loss, tightening PE
residual -27.50 → -26.01 and CO2 -2.66 → -2.52 (improvements). The
higher HW fuel shifts SAP residual -6 → -7. Re-pinned with slice-102d
note. Closed combi boiler certs (001479, 0330, 9501) unaffected:
has_hot_water_cylinder=false gates the primary-loss override to None.
|
||
|
|
5b78a1e2c8 |
Slice 102c.2: PCDB Table 362 PSR groups + APM linear interpolation
SAP 10.2 Appendix N3.6 / N3.7(a) (PDF p.108) compute heat-pump
efficiencies from a PSR-dependent dataset in the PCDB record. Spec PDF
p.100 line 5957 instructs: "The PSR-dependent results applicable to
the dwelling are then obtained by linear interpolation between the two
datasets whose PSRs enclose that of the dwelling."
This slice decodes the format-465 PSR-group block (idx[58] count
followed by N groups × 9 raw fields apiece) and adds the interpolation
primitive. Field positions within each 9-field group reverse-engineered
against Mitsubishi PUZ-WM50VHA (104568) by back-solving cert 0380's
worksheet pin η_space=223.0480, η_water=171.0746:
group offset 0 → PSR
group offset 2 → η_space,1 (% gross)
group offset 6 → η_water,3 (% gross — Appendix N3.7(a) + footnote 49,
PSR-dependent and calculated via the annual performance
method, used directly for HPs providing both space +
water heating)
Offsets 1 / 3 / 4 / 5 / 7 / 8 are unpopulated for record 104568 and
not yet ground-truthed. They likely hold the secondary results
documented under format 464 field 42-43 (specific electricity
consumed, running hours) plus additional format-465 extensions.
The clamping behaviour at the PSR ends is taken from SAP 10.2 PDF
p.101 lines 6007-6008: "if the PSR is greater than the largest PSR in
the database record then the heat pump space and water heating
fractions for the largest PSR should be used, and if the PSR is less
than the smallest PSR in the database record then the heat pump space
and water heating fractions for the smallest PSR should be used".
Verified against cohort:
- Record 104568 (Mitsubishi PUZ-WM50VHA) → 14 PSR groups decoded;
interpolation at PSR=1.43 yields η_space,1≈234.96 and η_water,3
≈285.09, matching back-solved worksheet values (slice 102e applies
the N3.6 ×0.95 and N3.7 ×0.60 in-use factors to close the chain).
|
||
|
|
70aa709c1c |
Slice 102c.1: typed PCDB Table 362 (heat pumps) header parser
SAP 10.2 Appendix N (N3.6 / N3.7(a)) requires PSR-interpolated values
from PCDB Table 362 for any heat-pump cert. The published PCDF Spec
Rev 6b §A.23 documents format 464 for that table; the live
pcdb10.dat (April 2026) ships format 465, which extends 464 with
additional header fields between fields 11 and 12 and a larger PSR
group set. The parser-layer test pins the format-465 offsets against
the BRE web entry for Mitsubishi Ecodan 5.0 kW PUZ-WM50VHA
(pcdb_id=104568, the cohort's dominant heat-pump model — 6 of 7 ASHP
certs use it).
This slice lands only the header fields the downstream APM cascade
needs (PSR-group decoding + linear interpolation follow in slice 102c.2):
field spec ref format-465 idx
brand_name §A.23 field 7 6
model_name §A.23 field 8 7
model_qualifier §A.23 field 9 8
fuel §A.23 field 13 16
service_provision §A.23 field 17 22
hw_vessel_mode §A.23 field 18 23
vessel_volume_l §A.23 field 19 24
vessel_heat_loss_kwh_per_day §A.23 field 20 25
vessel_heat_exchanger_area_m2 §A.23 field 21 26
max_output_kw §A.23 field 30 47
`max_output_kw` is the PSR-denominator per SAP 10.2 PDF p.100 line 5946
("maximum nominal output of the package … divided by the design heat
loss of the dwelling"); BRE labels it "Output power @ -4.7°C" on the
web entry.
Cohort header parse verified end-to-end against BRE web ground truth
for record 104568. Identical field positions apply to the Daikin
EDLQ05CAV3 (102421, cert 9418), confirmed by spot-checking the
populated raw indices.
|