Model/domain/sap10_calculator/docs/AGENT_GUIDE.md
Khalim Conn-Kowlessar 1382c8c886 docs: add AGENT_GUIDE.md — fresh-start onboarding for the SAP calculator
A single durable doc so agents can pick up the calculator without reading
historical handovers: (1) the accuracy bar for the two input paths
(site-notes 1e-4 vs worksheet; API 1e-4 when a worksheet exists, ±0.5
register fallback otherwise; cross-mapper parity); (2) the per-line-walk
debugging loop incl. comparing site-notes vs API; (3) the tools &
pipeline (Summary PDF → extractor → from_elmhurst_site_notes →
cert_to_inputs → calculate_sap_from_inputs → SapResult, plus the API
from_api_response front-end, section helpers, and where the test vectors
live). Pointer added from SAP_CALCULATOR.md; HANDOVER_* flagged as
point-in-time notes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 21:32:29 +00:00

13 KiB
Raw Permalink Blame History

SAP calculator — agent guide (start here)

This is the canonical onboarding doc for working on the SAP 10.2 / RdSAP 10 calculator. It is meant to get you productive without reading any historical handover. The HANDOVER_*.md files in this directory are point-in-time session notes (useful for the specific residual they chase, ignore otherwise). For deep architecture/API see SAP_CALCULATOR.md.

Three things this doc gives you: (1) the accuracy bar for the two input paths, (2) the debugging loop, (3) the tools & pipeline.


0. The one-paragraph mental model

A cert's data comes in via one of two front-ends — an Elmhurst Summary PDF (site-notes path) or an EPC-register API JSON (API path). Both map to the same typed EpcPropertyData, which feeds a deterministic cascade that reproduces the RdSAP10 engine. Our ground truth is the Elmhurst worksheet PDF (U985 / P960 / dr87) — the per-line (1)..(286) calculation, not the rounded values the EPC register lodges. We pin the cascade against the worksheet to abs = 1e-4 on every line ref.


1. Accuracy expectations — site-notes vs API

The worksheet PDF is always the target. The EPC register's lodged SAP/CO2/PE are rounded and carry Elmhurst's own residual, so matching the lodged values is not the goal — matching the worksheet is.

Path Input When a worksheet PDF exists for the cert API/site-notes-only (no worksheet)
Site-notes Elmhurst Summary PDF → extractor → from_elmhurst_site_notes abs = 1e-4 on continuous SAP and every populated line ref and cost / CO2 / PE n/a (we always have the worksheet for site-notes fixtures)
API register JSON → from_api_response abs = 1e-4 on continuous SAP vs the worksheet (same bar as site-notes — the two paths must converge) ±0.5 SAP vs the lodged register value (fallback only)

Three rules that fall out of this:

  • Cross-mapper parity. For a cert that has both an API JSON and an Elmhurst Summary, the two paths must produce SAP within 1e-4 of each other and of the worksheet. The cascade output (not a structural EPC diff) is the equivalence check. A divergence localises to one mapper.
  • No tolerance widening. A failing 1e-4 pin is a real cascade bug or a fixture defect — diagnose it, don't relax it. No rel=, no xfail, no adaptive ceilings. ΔSAP = 0.07 is not "closed".
  • ±0.5 is a fallback, not a destination. It's only for API-only certs with no worksheet to check against. If you can get a worksheet, the bar is 1e-4.

Two documented, deliberate exceptions to "match the spec literal" live in SAP_CALCULATOR.md §8 ("Elmhurst-mirrored spec divergences") — cases where the BRE-approved Elmhurst engine diverges from the SAP 10.2 text and we mirror the engine. Add a §8 row only with ≥2-cert evidence.


2. The tools & pipeline

2.1 The two PDFs per cert

  • Summary_NNNNNN.pdf — the Elmhurst site notes / input. This is what the assessor lodged: dimensions, fabric, heating system, controls, cylinder, etc. It is the INPUT, equivalent to the API JSON.
  • The worksheet — the ground truth output, every line ref (1)..(286) to 4 d.p. Three families, all the same format:
    • U985-0001-NNNNNN.pdf — the 6 gas-combi conformance fixtures.
    • P960-0001-NNNNNN.pdf — the heating-systems corpus + community heating.
    • dr87-0001-NNNNNN.pdf — the API-paired cohort ("Additional data with api").

2.2 The cascade pipeline (site-notes path)

import subprocess, re
from pathlib import Path
from backend.documents_parser.elmhurst_extractor import ElmhurstSiteNotesExtractor
from datatypes.epc.domain.mapper import EpcPropertyDataMapper
from domain.sap10_calculator.rdsap.cert_to_inputs import (
    cert_to_inputs, cert_to_demand_inputs, local_climate_for_cert,
)
from domain.sap10_calculator.calculator import calculate_sap_from_inputs

# 1. Summary PDF -> per-page text (pdftotext -layout, one string per page)
def summary_pdf_to_pages(pdf: Path) -> list[str]:
    n = int(re.search(r"Pages:\s+(\d+)",
            subprocess.run(["pdfinfo", str(pdf)], capture_output=True, text=True).stdout).group(1))
    pages = []
    for i in range(1, n + 1):
        layout = subprocess.run(
            ["pdftotext", "-layout", "-f", str(i), "-l", str(i), str(pdf), "-"],
            capture_output=True, text=True).stdout
        pages.append("\n".join(
            tok for line in layout.splitlines() for tok in re.split(r"\s{2,}", line.strip()) if tok))
    return pages

pages      = summary_pdf_to_pages(Path("sap worksheets/.../Summary_NNNNNN.pdf"))
site_notes = ElmhurstSiteNotesExtractor(pages).extract()         # -> ElmhurstSiteNotes
epc        = EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes)  # -> EpcPropertyData

# 2. Two cascades. RATING = SAP/EI rating (UK-avg climate, region 0).
#    DEMAND = Current Carbon / Current PE / Fuel Bill (postcode climate, PCDB Table 172).
rating = calculate_sap_from_inputs(cert_to_inputs(epc))
demand = calculate_sap_from_inputs(cert_to_demand_inputs(epc))   # climate = local_climate_for_cert(epc)

rating.sap_score_continuous   # un-rounded SAP — pin THIS, not the integer
rating.total_fuel_cost_gbp
rating.co2_kg_per_yr
demand.primary_energy_kwh_per_yr

Shortcut: Sap10Calculator().calculate(epc) runs the rating cascade (cert_to_inputscalculate_sap_from_inputs) in one call.

2.3 The API path

Identical from EpcPropertyData onward — only the front-end changes:

import json
data = json.loads(Path("tests/domain/sap10_calculator/rdsap/fixtures/golden/<cert>.json").read_text())
epc  = EpcPropertyDataMapper.from_api_response(data)   # -> EpcPropertyData
# ... same cert_to_inputs / calculate_sap_from_inputs as above

2.4 Section helpers — intermediate line refs

Every worksheet section has a <section>_section_from_cert(epc) helper returning a typed result with the line-ref values. Use these to inspect where a residual originates without running the whole cascade (postcode_climate= selects rating vs demand):

from domain.sap10_calculator.rdsap.cert_to_inputs import (
    water_heating_section_from_cert,        # §4  (42)..(65)m
    heat_transmission_section_from_cert,    # §3  (26)..(37)
    internal_gains_section_from_cert,       # §5  (66)..(73)
    mean_internal_temperature_section_from_cert,  # §7 (85)..(94)
    space_heating_section_from_cert,        # §8  (95)..(99)
    fuel_cost_section_from_cert,            # §10a (240)..(255)
    environmental_section_from_cert,        # §12  (261)..(274)
    primary_energy_section_from_cert,       # §13a (275)..(286)
)
wh = water_heating_section_from_cert(epc)
wh.energy_content_monthly_kwh   # (45)m ;  wh.output_kwh_per_yr  # (62)/(64)

(Full table of helpers + line refs is in SAP_CALCULATOR.md §1.3.)

2.5 Reading the worksheet from the shell

# Dump a worksheet line ref (e.g. (217)m water-heater monthly efficiency):
pdftotext -layout "sap worksheets/.../P960-0001-NNNNNN.pdf" - | grep -nE "\(217\)|\(62\)|\(210\)"
# Read a Summary input field (controls, cylinder, fuel):
pdftotext -layout "sap worksheets/.../Summary_NNNNNN.pdf" - | grep -niE "cylinder|control|interlock|fuel"

2.6 Where the test vectors live

Set Location What
6 U985 conformance fixtures tests/domain/sap10_calculator/worksheet/_elmhurst_worksheet_NNNNNN.py (+ Summary PDFs in backend/documents_parser/tests/fixtures/) Gas-combi certs, every line ref transcribed as LINE_* / DEMAND_LINE_* constants. Pinned in worksheet/test_section_cascade_pins.py + worksheet/test_e2e_elmhurst_sap_score.py.
Heating-systems corpus sap worksheets/heating systems examples/<variant>/ (Summary + P960) 41 variants of one property with only the heating system changed → any residual is attributable to the heating subsystem. Pinned in backend/documents_parser/tests/test_heating_systems_corpus.py.
API golden fixtures tests/domain/sap10_calculator/rdsap/fixtures/golden/<cert>.json Register JSON for the API path.
API + worksheet pairs sap worksheets/Additional data with api/<cert>/ (Summary + dr87) Certs that have BOTH an API JSON and a worksheet → cross-mapper parity checks.

3. The debugging loop

When a cert's SAP/cost/CO2/PE is off, never guess a fix — walk it.

  1. Reproduce & decompose. Build the epc (extractor+mapper, or a fixture's build_epc()), run both cascades, and see which of the four outputs drifts. Cost/CO2/PE drift with the same sign as energy; isolate the carrier.
  2. Find the section. Walk the four metrics back to a worksheet section: SAP off but cost EXACT often means a demand/gains issue; cost off but energy EXACT means a price/factor issue; CO2/PE off but cost EXACT means a factor issue. Use the §2.4 section helpers to get the cascade's intermediate line refs.
  3. Per-line compare vs the worksheet. pdftotext -layout the worksheet and compare the cascade's (45)/(56)/(62)/(210)/(217)m/... line-by-line against the PDF. The first diverging line ref is the bug.
  4. Localise to a layer.
    • cascade value present in worksheet but cascade has 0 / wrong → calculator gap (a spec rule not wired, or a dispatch gate).
    • the input field the worksheet used isn't in epcmapper (mis-mapped) or extractor (didn't capture the Summary field). Audit the Summary PDF for the field first — many lodgements are incomplete and the fixture, not the calculator, is wrong.
  5. Cite the spec. Find the SAP 10.2 / RdSAP 10 rule (page + line) that produces the worksheet's number. Confirm the worksheet matches the spec literal; if it diverges, it's a candidate §8 Elmhurst-mirror (needs ≥2-cert evidence). SAP 10.2 only — never 10.3.
  6. Cross-check vs API (when available). If the cert has an API JSON too, run from_api_response through the same cascade. If the API path matches the worksheet but the site-notes path doesn't (or vice-versa), the bug is in that mapper, not the calculator. If both diverge identically, it's the calculator/cascade.
  7. Fix one cause, re-pin smaller. TDD: one failing AAA test → one impl → re-pin the (now smaller) residual. A spec-correct fix often exposes the next residual that an offsetting bug was masking — that's the next slice, not a regression. Don't conflate main_heating_category (often None on Elmhurst Table 4b boilers) with sap_main_heating_code.

Worked shape (real example: oil 6)

Residual +3.05 SAP. (1) HW + space both off. (2) §4 HW efficiency. (3) worksheet (210) space eff = 75 but Table 4b code 126 = 80; (217)m summer = 63 = 685 → a 5pp penalty. (4) the Summary lodges control 2101 ("no thermostatic control of room temperature") → no room thermostat → P960 header "Boiler Interlock: No". (5) RdSAP 10 §3 + SAP 10.2 Table 4c(2): no room thermostat ⇒ not interlocked ⇒ 5pp Space+DHW. Fix the no_interlock gate → space+HW fuel EXACT, residual collapses to a single exposed pump cause (Table 4f footnote a) ×1.3) → next slice. Two slices, fully closed.


4. Run the suite

PYTHONPATH=/workspaces/model python -m pytest \
    tests/domain/sap10_calculator/ \
    backend/documents_parser/tests/ \
    --no-cov -q -p no:cacheprovider

Conformance pins only:

PYTHONPATH=/workspaces/model python -m pytest \
    tests/domain/sap10_calculator/worksheet/test_section_cascade_pins.py \
    tests/domain/sap10_calculator/worksheet/test_e2e_elmhurst_sap_score.py \
    backend/documents_parser/tests/test_heating_systems_corpus.py \
    --no-cov -q

Notes:

  • load_cells tests pin against the gitignored *.xlsx reference worksheet at repo root; they skip when it's absent (CI), run locally when present.
  • All new code passes pyright strict, zero errors. Tests use literal # Arrange / # Act / # Assert headers and abs(x - y) <= tol (not pytest.approx, which strict-pyright flags).
  • Commit one slice per change, with the spec citation in the message.

5. Spec PDFs on disk

domain/sap10_calculator/docs/specs/
  sap-10-2-full-specification-2025-03-14.pdf   # SAP 10.2 (the methodology)
  RdSAP 10 Specification 10-06-2025.pdf         # RdSAP 10 (the reduced-data rules)
  pcdb10.dat / pcdb_table_*.jsonl               # PCDB (boilers, HPs, postcode weather)

Pages worth bookmarking: SAP 10.2 §7 MIT (p.28-32), Table 4b boiler eff (p.168), Table 4c efficiency adjustments (p.169), Table 4e controls (p.171-174), Table 4f auxiliary energy (p.175), Table 12 factors (p.191), Appendix U region tables (p.124-127). RdSAP 10 §10 water heating (p.54-56, incl. §10.7 no-water-heating default), Table 28/29 cylinder defaults, Table 32 prices (p.95).