Model/docs/sap-spec/HANDOVER_NEXT.md
Khalim Conn-Kowlessar 144f08533f Docs: rewrite HANDOVER_NEXT.md for fresh agent pickup post-slice-25d
§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>
2026-05-23 23:17:43 +00:00

21 KiB
Raw Blame History

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.5 continuous SAP ceilings.
  • NO xfail markers 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

  1. 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.
  2. Widening tolerances to make tests green.
  3. Testing sections in isolation using fixture.LINE_X PDF values AS INPUTS. The cascade test walks cert_to_inputs(epc), NOT isolated calls.
  4. Missing fixture defects — When a cascade pin fails, audit the fixture against the PDF FIRST. Many lodgements have been incomplete.
  5. 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.pyENERGY_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 status entries. The pre-existing deletion of docs/sap-spec/rdsap-10-specification-2025-06-10.pdf and the untracked docs/sap-spec/RdSAP 10 Specification 10-06-2025.pdf are stable; don't touch.
  • Don't invoke /ultrareview — user-triggered only.
  • Terse prose. No filler.
  • Delete _TEMP.py diagnostic 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.000100.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:

  1. 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.
  2. 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.
  3. 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 9599)

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 100108)

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, 206219)

Lodged on fixtures:

  • LINE_211 main heating fuel (annual)
  • LINE_215 secondary heating fuel (annual)
  • LINE_219 hot water fuel (annual)
  • Plus LINE_201, 206208, 213215 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 240255)

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 256258)

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 261282)

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)

  1. Find or extract a <section>_from_cert(epc) helper in domain.sap.rdsap.cert_to_inputs. If it doesn't exist, add one mirroring internal_gains_section_from_cert or mean_internal_ temperature_section_from_cert — compose upstream section helpers then call the orchestrator with the result's fields.
  2. Add a _SECTION_X_PINS tuple to test_section_cascade_pins.py mapping ("LINE_X_<NAME>", "result_attr_name").
  3. Add a parametrised test that walks every (fixture, line_ref) pair and asserts _pin(actual, expected, ...) at abs=1e-4.
  4. Run, see failures, diagnose. Fixture defect or calculator bug — fix in place, no widening.

D.2 Diagnostic pattern

When a pin fails:

  1. Add a TEMP test file test_<thing>_diag_TEMP.py that dumps the per-component breakdown alongside PDF expected values.
  2. awk '/^X\. Section/,/^Y\./' "sap worksheets/U985-0001-NNNNNN.txt" to extract the PDF block.
  3. Identify the drift source — fixture defect (audit fixture first) or calc bug.
  4. Fix. Re-run the pin.
  5. 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.py for the pattern (_round_half_up).
  • Half-up rounding, not banker's: Python's round(17.125, 2) = 17.12 but SAP wants 17.13. The _round_half_up helper in heat_transmission.py is 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_value field on SapRoomInRoofSurface and SapAlternativeWall honours 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.py constants.
  • Do not scan unread spec PDF pages without asking the user.
  • Do not invoke /ultrareview.
  • Do not auto-update unrelated git status items.
  • 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.