§1-§6 fully close (252/252). §7 closes 52/60 (LINE_92/93 marginal on 4 fixtures). §8-§12 not yet pinned. Handover now reads top-to-bottom with current scoreboard, per-section work queue, spec page reference index, and the section helper map for the new agent to extend. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
21 KiB
Handover — §7 LINE_92/93 + §8–§12 sweep to abs=1e-4 closure
Goal: every line ref of every output for every one of the 6 Elmhurst fixtures pins against the U985 worksheet PDF at abs=1e-4.
Owner: khalim@domna.homes. Branch: ara-backend-design-prd.
Spec PDFs in docs/sap-spec/: SAP 10.2 (14-03-2025), RdSAP 10 (10-06-2025), PCDF.
§A — Hard rules. Internalise before any code.
A.1 What this project IS
This repo replicates the rdSAP calculation engine to bit-level fidelity against 6 known test vectors (the U985 Elmhurst worksheets):
- Inputs:
Summary_NNNNNN.pdf(cert lodgement) for each of 6 fixtures (000474, 000477, 000480, 000487, 000490, 000516). - Intermediate values:
U985-0001-NNNNNN.{pdf,txt}lodges every worksheet line ref (1) through (282+) to 4 decimal places. - Final outputs: SAP rating (continuous + integer), ECF, total fuel cost, CO2, primary energy, per-end-use kWh.
It is a deterministic numerical function with fully-known test vectors.
A.2 The bar: abs=1e-4 on EVERY pin
- The PDF lodges 4 d.p. display precision. abs=1e-4 is the floor of "match what the PDF says".
- NO
rel=…tolerances. - NO
<= 0.5continuous SAP ceilings. - NO
xfailmarkers on cascade pins. - NO "documented widening".
A failing pin is a calculator bug or fixture defect. If you can't close it in this slice, leave it failing — that's the next slice's work.
A.3 Past mistakes — DO NOT REPEAT
- Treating SAP integer Δ=0 as "closed" — that's a weak gate (hides ±0.5 continuous drift). The real gate is per-line-ref abs=1e-4.
- Widening tolerances to make tests green.
- Testing sections in isolation using
fixture.LINE_XPDF values AS INPUTS. The cascade test walkscert_to_inputs(epc), NOT isolated calls. - Missing fixture defects — When a cascade pin fails, audit the fixture against the PDF FIRST. Many lodgements have been incomplete.
- Diagnosing downstream first. Cascade is upstream→downstream (§1 → §2 → §3 → §4 → §5 → §6 → §7 → §8 → §9a → §10a → §11a → §12). A downstream pin failure is meaningless to diagnose until upstream pins close.
If you find yourself about to widen a tolerance, add an xfail, or skip a fixture — stop and ask the user.
A.4 Reporting format — matrix not prose
sec 474 477 480 487 490 516 total
--- ---- ---- ---- ---- ---- ---- -----
§1 2/2 2/2 2/2 2/2 2/2 2/2 12/12
§3 4/4 4/4 4/4 4/4 4/4 4/4 24/24
...
Or numeric residuals when finer granularity helps:
fixture | LINE_92 Δ | LINE_93 Δ
000474 | 0.00013 | 0.00013
000477 | 0.00016 | 0.00016
...
✓ = within abs=1e-4. Use this format instead of prose summaries.
A.5 Workflow rules
- Don't scan >50 lines of spec PDF without checking with the user for the page anchor. The user has the page references and prefers to give them up-front rather than have you fumble through the spec.
- One slice = one commit. AAA test convention (`# Arrange / # Act /
Assert`). Co-Authored-By trailer.
- Don't touch SAP rating constants in
worksheet/rating.py—ENERGY_COST_DEFLATOR=0.42,ECF_LOG_THRESHOLD=3.5,SAP_LOG_COEFF=113.7,SAP_LOG_CONSTANT=117.0. SAP 10.2 per ADR-0010, pinned by 8+ tests. - Don't auto-update unrelated
git statusentries. The pre-existing deletion ofdocs/sap-spec/rdsap-10-specification-2025-06-10.pdfand the untrackeddocs/sap-spec/RdSAP 10 Specification 10-06-2025.pdfare stable; don't touch. - Don't invoke
/ultrareview— user-triggered only. - Terse prose. No filler.
- Delete
_TEMP.pydiagnostic files before commit.
§B — Current state
B.1 Cascade pin scoreboard (per-section)
sec 474 477 480 487 490 516 total
--- ---- ---- ---- ---- ---- ---- -----
§1 2/2 2/2 2/2 2/2 2/2 2/2 12/12 ✓
§2 16/16 16/16 16/16 16/16 16/16 16/16 96/96 ✓
§3 4/4 4/4 4/4 4/4 4/4 4/4 24/24 ✓
§4 9/9 9/9 9/9 9/9 9/9 9/9 54/54 ✓
§5 9/9 9/9 9/9 9/9 9/9 9/9 54/54 ✓
§6 2/2 2/2 2/2 2/2 2/2 2/2 12/12 ✓
§7 8/10 8/10 8/10 10/10 8/10 10/10 52/60
---------- ------------------------------------------------------ -------
total 304/312 (97.4%)
§1–§6 fully close for all 6 fixtures (252/252). Only §7 LINE_92/93 on 4 fixtures (000474/477/480/490) remains in the cascade.
B.2 SapResult pin matrix (e2e)
field | 474 | 477 | 480 | 487 | 490 | 516
-----------------------------------+-----+-----+-----+-----+-----+-----
sap_score (int) | ✓ | ✓ | ✓ | ✗ | ✓ | ✓
sap_score_continuous | ✗ | ✗ | ✗ | ✗ | ✗ | ✓
ecf | ✓ | ✓ | ✓ | ✗ | ✓ | ✓
total_fuel_cost_gbp | ✗ | ✗ | ✗ | ✗ | ✗ | ✗
co2_kg_per_yr | ✗ | ✗ | ✗ | ✗ | ✗ | ✗
space_heating_kwh_per_yr | ✗ | ✗ | ✗ | ✗ | ✗ | ✗
main_heating_fuel_kwh_per_yr | ✗ | ✗ | ✗ | ✗ | ✗ | ✗
secondary_heating_fuel_kwh_per_yr | ✓ | ✗ | ✗ | ✗ | ✗ | ✗
hot_water_kwh_per_yr | ✓ | ✗ | ✓ | ✓ | ✗ | ✗
lighting_kwh_per_yr | ✓ | ✓ | ✓ | ✓ | ✓ | ✓
pumps_fans_kwh_per_yr | ✓ | ✓ | ✓ | ✓ | ✓ | ✓
27 SapResult pin PASS / 39 FAIL. Most downstream fails will close as
§8/§9a/§10a/§11a/§12 land. 000516 sap_score_continuous already
passes — a useful sanity check that the full cascade is consistent
when the upstream sections close.
B.3 Recent slices (in reverse order — newest first)
25d: 000487 §4 LINE_65 closure — derive (64a) electric-shower kWh from cert (App J step 8, p.82)
25c: 000477 §4/§5/§6 closure — SAP10.2 Table 3c (p.162) M+L lower bound 100.0 → 100.2
25b: 000487 §4 LINE_43-64 closure — has_electric_shower + Appendix J step 2a Nbath branch
25a: 000487 §3 full closure — RR detailed surfaces + gable_wall_external + §3.8 max-floor roof + half-up rounding
26c: §7 mean internal temp cascade pin (60 cases, 52 PASS)
26b: §6 solar gains cascade pin + SapRoofWindow solar attrs + plumb to §6 cascade
26: §5 internal gains cascade pin + rooflight daylight plumb
27b: §3 element-area rounding to 2 d.p. per RdSAP10 §15 (p.66)
27: BS EN ISO 13370 floor U rounded to 2 d.p. per RdSAP10 §5.12 (p.46)
24: rooflight (line 27a) — SapRoofWindow datatype + 000516 cascade closure
§C — Work queue (priority order)
C.1 §7 LINE_92/93 marginal residual (8 fails, 4 fixtures)
Per the matrix above. Diff is ~0.00010–0.00016 K per failing case — just
above the 1e-4 threshold. The PDF passes LINE_87 (T_living) and LINE_90
(T_elsewhere) for the same 4 fixtures, but the weighted combination
LINE_92 = (91) × T_living + (1 - (91)) × T_elsewhere drifts.
Hypotheses to test:
- PDF uses rounded T_living/T_elsewhere at some precision higher than 4 d.p. but lower than full float in the weighted sum. The cascade pin on LINE_87/90 passes at abs=1e-4 because both my full-precision and the PDF's higher-precision values round to the same 4-d.p. display.
- PDF rounds LINE_92 to specific d.p. before later use, but the stored value doesn't quite match the in-memory full-precision combo.
- A spec-defined intermediate rounding step in §7 step 9 (RdSAP10 §15 doesn't list MIT in its rounding list — only U-values and areas).
Diagnostic: write a TEMP test that prints my T_living[m], T_elsewhere[m], LINE_91, and computes the weighted sum at several precision levels (4 d.p., 5 d.p., 6 d.p., full). Compare each to the PDF's LINE_92[m]. If 5-d.p. matches the PDF for all 4 fixtures and 12 months, the rule is "round T_living + T_elsewhere to 5 d.p. before combining". Ask the user for the SAP10.2 §7 spec page (likely §9.3 or near, page ~28-32) before applying any new rounding rule.
000516 + 000487 §7 already close at 10/10 — so the artefact isn't universal. Compare their T_living[m] values against the failing fixtures to spot the trigger pattern.
C.2 §8 space heating cascade pin (lines 95–99)
Fixtures lodge:
LINE_95_M_USEFUL_GAINS_W(12-tuple)LINE_97_M_HEAT_LOSS_RATE_W(12-tuple)LINE_98A_M_SPACE_HEATING_KWH(12-tuple)LINE_98C_M_TOTAL_SPACE_HEATING_KWH(12-tuple, same as 98a for current fixtures)LINE_98C_ANNUAL_KWH(scalar)LINE_99_PER_M2_KWH(scalar)
§8 orchestrator: domain.sap.worksheet.space_heating.space_heating_monthly_kwh.
Section helper to add: space_heating_section_from_cert(epc) in
cert_to_inputs.py. Inputs needed: §7 (MIT + η_whole), §1 (TFA, volume),
§2 (effective_monthly_ach), §3 (total HLC), §5+§6 (total gains), climate.
Same composition pattern as mean_internal_temperature_section_from_cert.
Add pin tests at the end of test_section_cascade_pins.py mirroring the
_SECTION_7_MONTHLY_PINS shape.
C.3 §8c space cooling cascade pin (lines 100–108)
All 6 fixtures lodge f_C=0 (no air conditioning), so:
- LINE_103 cooling gains = (0,)×12
- LINE_107 monthly cooling = (0,)×12
- LINE_107 annual = 0
- LINE_108 per m² = 0
LINE_101 utilisation factor collapses to 1.0 (γ ≤ 0 branch); LINE_106
intermittency monthly is the spec default mask. Fixture constants
LINE_101_M_UTILISATION_FACTOR_LOSS = SECTION_8C_ETA_LOSS_ALL_ONE,
LINE_106_M_INTERMITTENCY_FACTOR = SECTION_8C_INTERMITTENCY_MONTHLY,
LINE_107_M_SPACE_COOLING_KWH = SECTION_8C_ALL_ZERO_MONTHLY.
§8c orchestrator: domain.sap.worksheet.space_cooling. Section helper
likely trivial since all inputs collapse to zero.
C.4 §8f Fabric Energy Efficiency (line 109)
Single scalar: LINE_109_FEE_KWH_PER_M2. Per spec, (109) = (98a)/TFA +
(108). For all 6 fixtures (98b) solar space heating = 0, so Σ(98a) =
Σ(98c) → LINE_109 = LINE_99 + LINE_108 = LINE_99 (no AC).
§8f orchestrator: domain.sap.worksheet.fabric_energy_efficiency.
C.5 §9a energy requirements (lines 201, 206–219)
Lodged on fixtures:
- LINE_211 main heating fuel (annual)
- LINE_215 secondary heating fuel (annual)
- LINE_219 hot water fuel (annual)
- Plus LINE_201, 206–208, 213–215 monthly tuples possibly
Already partially exposed on SapResult (main_heating_fuel_kwh_per_yr,
secondary_heating_fuel_kwh_per_yr, hot_water_kwh_per_yr). Pin tests
at the cascade level walk energy_requirements_from_cert (or compose
inside cert_to_inputs).
C.6 §10a fuel costs (lines 240–255)
17+ line refs. Already exposed via SapResult.total_fuel_cost_gbp.
Cascade tests should pin each component (main fuel cost, secondary,
hot water, pumps/fans, lighting, PV credit, standing charges).
§10a orchestrator: domain.sap.worksheet.fuel_cost.fuel_cost.
C.7 §11a SAP rating (lines 256–258)
3 line refs:
- LINE_256 ECF (energy cost factor)
- LINE_257 SAP score continuous
- LINE_258 SAP score integer
Already on SapResult as ecf, sap_score_continuous, sap_score.
e2e pins exist. Add explicit cascade pins for symmetry.
rating.py constants are immutable per ADR-0010 — do not touch.
C.8 §12 environmental (lines 261–282)
CO2 + primary energy + EI rating monthly + annual. Already partly on
SapResult.co2_kg_per_yr. Big section with many line refs.
§D — Workflow toolbox
D.1 Adding a section cascade pin (the standard pattern)
- Find or extract a
<section>_from_cert(epc)helper indomain.sap.rdsap.cert_to_inputs. If it doesn't exist, add one mirroringinternal_gains_section_from_certormean_internal_ temperature_section_from_cert— compose upstream section helpers then call the orchestrator with the result's fields. - Add a
_SECTION_X_PINStuple totest_section_cascade_pins.pymapping("LINE_X_<NAME>", "result_attr_name"). - Add a parametrised test that walks every
(fixture, line_ref)pair and asserts_pin(actual, expected, ...)at abs=1e-4. - Run, see failures, diagnose. Fixture defect or calculator bug — fix in place, no widening.
D.2 Diagnostic pattern
When a pin fails:
- Add a TEMP test file
test_<thing>_diag_TEMP.pythat dumps the per-component breakdown alongside PDF expected values. awk '/^X\. Section/,/^Y\./' "sap worksheets/U985-0001-NNNNNN.txt"to extract the PDF block.- Identify the drift source — fixture defect (audit fixture first) or calc bug.
- Fix. Re-run the pin.
- Delete the TEMP file before committing.
D.3 Spec page references already in hand
RdSAP 10 (10-06-2025):
§3.1 precision rule p.16
§3.6 wall area p.19
§3.7.1 window area p.20
§3.8 roof area (max-floor) p.20
§3.9 RR simplified p.21
§3.10 RR detailed p.21
Table 4 (RR gable walls) p.22
§5.12 floor U + Table 19 p.46
§5.13 + Table 20 exposed floor p.47
§5.17 + Table 23 basement p.48
§5.18 curtain wall p.48
Table 24 (window U) p.50 (Standard | Roof window cols)
§15 rounding rules p.66
Table 11 (secondary fraction) p.188
Table 12 (fuel/CO2/PEF) p.189
Table 12a (standing/off-peak) p.191
SAP 10.2 (14-03-2025):
Appendix J §2a Nbath p.81
Appendix J §8 electric shower p.82
Table J4 (shower flow/power) p.83
Table J5 (behavioural fbeh) p.83
Table 3a (HW combi keep-hot) p.160
Table 3b (HW combi profile M) p.161
Table 3c (HW combi M+L / M+S) p.162
For new pages ask the user. Spec PDFs are big.
D.4 Spec-grounded patterns we've discovered
- RdSAP §15 rounding: U-values + element gross areas to 2 d.p. —
apply at the BOUNDARY between RdSAP input and SAP calculator. See
heat_transmission.pyfor the pattern (_round_half_up). - Half-up rounding, not banker's: Python's
round(17.125, 2) = 17.12but SAP wants 17.13. The_round_half_uphelper inheat_transmission.pyis the right utility — reuse it for any new §15 boundary you cross. - §3.8 roof area = MAX of floor areas across levels, not the top floor area. Bites when an extension's footprint steps back.
- Assessor-lodged U overrides cascade: cert PDFs lodge measured U
for some walls/gables. The
u_valuefield onSapRoomInRoofSurfaceandSapAlternativeWallhonours this. When extending to new surface types, follow the same pattern.
D.5 Section helper map (cert→inputs cascade entry points)
domain.sap.rdsap.cert_to_inputs
dimensions_from_cert(epc) §1 → Dimensions
ventilation_from_cert(epc) §2 → VentilationResult
heat_transmission_section_from_cert(epc) §3 → HeatTransmission
water_heating_section_from_cert(epc) §4 → WaterHeatingResult
internal_gains_section_from_cert(epc) §5 → InternalGainsResult
solar_gains_section_from_cert(epc) §6 → SolarGainsResult
mean_internal_temperature_section_from_cert(epc) §7 → MeanInternalTemperatureResult
-- next to add --
space_heating_section_from_cert(epc) §8 → SpaceHeatingResult
space_cooling_section_from_cert(epc) §8c → SpaceCoolingResult
fabric_energy_efficiency_from_cert(epc) §8f → float (kWh/m²)
energy_requirements_section_from_cert(epc) §9a → EnergyRequirementsResult
fuel_cost_section_from_cert(epc) §10a → FuelCostResult
sap_rating_section_from_cert(epc) §11a → (ecf, sap_continuous, sap_int)
environmental_section_from_cert(epc) §12 → EnvironmentalResult
D.6 Hard rules summary card
| do | don't |
|---|---|
pytest.approx(..., abs=1e-4) |
rel=… |
| Audit fixture against PDF first | Diagnose downstream first |
| Leave failing pins, fix one at a time | Widen tolerance / add xfail |
| Quote PDF page when asking for spec | Scan >50 lines of PDF without asking |
[[reference-style]] cross-links in memory |
Bare prose references |
Use _round_half_up, not Python round |
Banker's rounding at §15 boundaries |
Delete _TEMP.py before commit |
Commit diagnostic scripts |
§E — File map
docs/sap-spec/
sap-10-2-full-specification-2025-03-14.pdf SAP 10.2 spec
RdSAP 10 Specification 10-06-2025.pdf RdSAP 10 spec
HANDOVER_NEXT.md this file
pcdb_table_105_gas_oil_boilers.jsonl PCDB combi records
sap worksheets/ U985 + Summary PDFs
packages/domain/src/domain/sap/calculator.py Top-level SAP10.2 orchestrator
packages/domain/src/domain/sap/rdsap/cert_to_inputs.py Cert→CalculatorInputs + section helpers
packages/domain/src/domain/sap/tables/table_12.py Table 12 (price/CO2/PEF)
packages/domain/src/domain/sap/tables/table_12a.py Off-peak high-rate fraction
packages/domain/src/domain/sap/tables/table_32.py RdSAP10 Table 32 (cost prices)
packages/domain/src/domain/sap/worksheet/
dimensions.py §1
ventilation.py §2 + VentilationResult
heat_transmission.py §3 + HeatTransmission + _round_half_up helper
water_heating.py §4 + WaterHeatingResult + electric_shower_monthly_kwh
internal_gains.py §5 + InternalGainsResult
solar_gains.py §6 + SolarGainsResult + RoofWindowInput
mean_internal_temperature.py §7 + MeanInternalTemperatureResult
space_heating.py §8 + SpaceHeatingResult
space_cooling.py §8c
fabric_energy_efficiency.py §8f
energy_requirements.py §9a + EnergyRequirementsResult
fuel_cost.py §10a + FuelCostResult
rating.py §11/§13 SAP rating equations (DO NOT TOUCH constants)
packages/domain/src/domain/sap/worksheet/tests/
test_section_cascade_pins.py Strict per-section line-ref pins (THE work)
test_e2e_elmhurst_sap_score.py SapResult-field pins
_elmhurst_worksheet_NNNNNN.py The 6 fixture modules (1 per fixture)
_elmhurst_fixtures.py ALL_FIXTURES registry
test_*.py Legacy per-section isolation tests
datatypes/epc/domain/epc_property_data.py
SapBuildingPart + sap_room_in_roof
SapRoomInRoof + detailed_surfaces
SapRoomInRoofSurface + u_value override, kind enum:
"slope" | "flat_ceiling" | "stud_wall" |
"gable_wall" | "gable_wall_external"
SapAlternativeWall + u_value override
SapRoofWindow area + u_value_raw + orientation +
pitch_deg + g_perpendicular + frame_factor
SapHeating + electric_shower_count, mixer_shower_count,
number_baths
§F — Definitely do NOT
- Do not widen any tolerance.
- Do not add xfail to cascade pins.
- Do not "investigate later" by widening — fix it or leave it failing.
- Do not assume the calculator is wrong before auditing the fixture.
- Do not touch
rating.pyconstants. - Do not scan unread spec PDF pages without asking the user.
- Do not invoke
/ultrareview. - Do not auto-update unrelated
git statusitems. - Do not use Python
round()at a §15 boundary — use_round_half_up.
§G — Quick orient
# Run the full cascade scoreboard
python -m pytest \
packages/domain/src/domain/sap/worksheet/tests/test_section_cascade_pins.py \
packages/domain/src/domain/sap/worksheet/tests/test_e2e_elmhurst_sap_score.py \
--no-header --no-cov --tb=no -q
# Run §7 only
python -m pytest packages/domain/src/domain/sap/worksheet/tests/test_section_cascade_pins.py \
-k "section_7" --no-cov --tb=no -q
# Per-fixture residual diffs for a section
python -m pytest packages/domain/src/domain/sap/worksheet/tests/test_section_cascade_pins.py \
-k "section_7 and 000474" --no-cov --tb=line
# Single SapResult pin numeric diff
python -m pytest \
"packages/domain/src/domain/sap/worksheet/tests/test_e2e_elmhurst_sap_score.py::test_sap_result_pin[000477-space_heating_kwh_per_yr]" \
--no-cov 2>&1 | grep AssertionError
# Extract a PDF §X block for a fixture
awk '/^X\. Section/,/^Y\./' "sap worksheets/U985-0001-NNNNNN.txt"
# Wider regression check
python -m pytest packages/domain/src/domain/sap/worksheet/tests/ \
packages/domain/src/domain/sap/tests/ packages/domain/src/domain/ml/ \
--no-header --no-cov --tb=no -q | tail -5
End of handover. Read §A again before starting.