SAP 10.2 Table 4c(3) (PDF p.169) "Factor for controls and charging method"
multiplies a heat network's heat requirement by 1.05-1.10 for FLAT-RATE
charging (note d: household pays a fixed amount regardless of heat used, so
no incentive to economise), and by 1.0 for charging linked to use. The
worksheet folds it into the heat-network requirement alongside the Table 12c
distribution loss factor:
(307) space = (98c) x (302) x (305) x (306)
(310) DHW = (64) x (305a) x (306)
Our cascade applied (306) DLF but never (305)/(305a), so every flat-rate
community-heating cert under-counted demand -> over-rated SAP.
Folded the factor into the 1/DLF efficiency override at the space-heating
(206) and DHW (water-inherits-from-main) sites. Space column adds +0.05 for
no thermostatic control (2301/2302); DHW column is 1.05 flat-rate / 1.0
linked-to-use.
Corpus (RdSAP-21.0.1, 1000 certs): community cluster median +0.32 -> -0.19,
within-0.5 38% -> 62% (control 2307 +0.83 -> -0.19; 2306 unchanged at factor
1.0 as spec requires). Overall gauge 65.0% -> 65.9%, MAE 1.174 -> 1.160.
Ratcheted the corpus-test floor 0.62 -> 0.63 / MAE ceiling 1.25 -> 1.22.
Also records (corpus-test comment + scripts/decompose_co2_pe_error.py) the
disproof of the prior "CO2/PE +5% is a factor/scope bug" lead: factors are
spec-exact, scope identical, and the bias is per-cert demand fidelity
(corr(SAP-err, PE-diff) = -0.54), not a one-slice factor fix.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a committed integration test driving the full API path — raw gov-EPC
response → from_api_response → cert_to_inputs → calculate_sap_from_inputs —
across all 1000 certs in the in-repo RdSAP-21.0.1 corpus, and pins the
aggregate accuracy of our continuous SAP (plus CO2 and primary energy)
against each cert's lodged figures. Mirrors scripts/eval_api_sap_accuracy.py
but runs in CI off the committed corpus (~2s, no /tmp sample needed).
Scoped to RdSAP-21.0.1 — the SAP 10.2-era schema whose lodged rating uses the
same methodology we compute (a fair target). Pre-SAP10 schemas (17.x-20.0.0)
lodge SAP 2012 ratings and are out of scope (guarded for mapping only by
test_mapper_corpus.py).
Current: SAP within-0.5 = 65.0%, MAE = 1.174 (tight floor/ceiling — the
optimised gauge). CO2 MAE = 0.27 t/yr (bias +0.17) and PE MAE = 14.6
kWh/m2/yr (bias +8.9) are reported + loosely guarded: cost is well-calibrated
but CO2/PE both run ~+5-10% high (uniform across fuels — a systematic
CO2/PE-factor or scope gap, not yet investigated). Thresholds ratchet as
slices tighten each metric.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The mixer-shower hot-water demand (worksheet 42a) divided N_shower by the
count of MIXER outlets only. But SAP 10.2 Appendix J step 1a is explicit:
"Establish how many shower outlets are present in the dwelling, Noutlets
(including in the count any instantaneous electric showers)" — and the
electric-shower step (64a) uses that same Noutlets from step 1a. So a
dwelling with both a mixer and an electric shower assigned the FULL N_shower
to the mixer system AND billed the electric shower on top of it, double-
counting shower demand → over-counted main HW → under-rated the dwelling.
Fix: thread the electric-shower count into the mixer demand so the
denominator is the total outlet count (mixer + electric), iterating the
warm-water draw over the mixer outlets only (per step 1e).
shower_types=1,2 cohort: -0.37 median -> +0.28 (crossed zero); API gauge
68.4% -> 69.0% within-0.5. Golden cert 0300-2747 (1 mixer + 1 electric)
re-pinned: PE +0.93 -> -0.10, CO2 +0.25 -> +0.15 (both toward zero,
confirming the double-count). Worksheet harness 47/47, 0 divergers (the
Elmhurst fixtures have no electric showers).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The no-PCDB MEV fan-electricity path fed the SAP 10.2 Table 4g default SFP
(0.8 W/(l/s)) directly as SFPav. But Table 4g note 3 (PDF p.176) is explicit:
the default SFP values "are to be multiplied by the appropriate in-use factor
for default data from the PCDB" — PCDB Table 329 system_type 10 ("default
data, used when SFP is taken from Table 4g rather than the PCDB"), IUF 2.5
(duct-agnostic per note 2). Table 4h, which previously held these factors, is
retired ("no longer used – data now stored in the PCDB").
Omitting the IUF under-billed the index-less MEV fan electricity by 2.5x
(SFPav 0.8 instead of 0.8 x 2.5 = 2.0), so cost was too low and the cohort
over-rated. This is distinct from the with-index path, which already applies
the tested-product system_type-2 "no scheme" IUF (~1.45) per fan.
Index-less gas-house MEV cohort: +1.37 median -> -0.18 (12% -> 92% within 0.5),
no overshoot — the missing IUF was exactly the over-rate. API gauge 67.7% ->
68.4% within-0.5 (mean|err| 0.992 -> 0.986, signed +0.031 -> +0.006).
Worksheet harness 47/47, 0 divergers (Summary-path MEV certs carry a PCDB
index or are natural, so unaffected).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Promote RdSAP-Schema-17.0 into SUPPORTED so all 1000 corpus certs are held to
the strict parse+map guard. Drop the now-redundant cert[0] tracer (subsumed by
the parametrised bucket); keep the reduced-field synthesis behavioural test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the 17.0 synthesis seam over the shared _synthesise_reduced_field_windows
core (inherited 20.0.0 coefficients, ND glazing -> DG-modal default 2, per
ADR-0028). 17.0 glazed_type codes (1-4,7) are a subset of the verified 1-8
space. The 10 rich certs use lodged window_area directly; the windowless 990
synthesise a 4-way N/E/S/W split.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dispatch RdSAP-Schema-17.0 through from_api_response, parse-fix the schema
(data-driven required->optional, validated against the 1000-cert 17.0 corpus
per ADR-0028 — incl. SapHeating.cylinder_insulation_type and the
has_hot_water_cylinder / has_fixed_air_conditioning / has_heated_separate_
conservatory flags), and port the defensive mapper reads (dwelling_type
str/dict/number, photovoltaic_supply guard, sap_floor_dimensions guard). All
1000 corpus certs now parse and map.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reusable per-schema profiler: glazed_area band mix, Validation Cohort size,
observed-vs-predicted band glazing/floor ratio, and the ND/str sentinels that
drive schema widening. Regenerates the ADR-0028 transfer-check table from any
harvested corpus.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Promote RdSAP-Schema-19.0 into SUPPORTED so all 1000 corpus certs are held to
the strict parse+map guard. Drop the now-redundant cert[0] tracer (subsumed by
the parametrised bucket); keep the reduced-field synthesis behavioural test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the 19.0 synthesis seam over the shared _synthesise_reduced_field_windows
core (inherited 20.0.0 coefficients, ND glazing -> DG-modal default 2, per
ADR-0028). 19.0 glazed_type codes (1-4,6,7) are a subset of the verified 1-8
space. The 6 rich certs use lodged window_area directly; the windowless 994
synthesise a 4-way N/E/S/W split.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dispatch RdSAP-Schema-19.0 through from_api_response, parse-fix the schema
(data-driven required->optional, validated against the 1000-cert 19.0 corpus
per ADR-0028), and port 18.0's defensive mapper reads (dwelling_type str/dict/
number, photovoltaic_supply guard, sap_room_in_roof Measurement coercion).
All 1000 corpus certs now parse and map.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Orchestrator runs recommend_secondary_heating_removal; report._triggers_for
explains it via the lodged secondary_heating_type; harness catalogue + ARA seed
price it. Re-pins the golden/integration plans it shifts: it is a cheap (\£250)
SAP lever, so on gas-main certs lodging an electric secondary (691) it displaces
the \£12k ASHP (0330, 0036) or joins the all-beneficial-measures package (000490,
where its marginal SAP is 0 under the category-4 ASHP but the heater is still
physically removed). Consistent with the optimiser's existing kitchen-sink
package behaviour.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two cascade tests on the worksheet-pinned 001431 build_epc() (the user's
before/after Summary PDFs trip the documented 001431 window-extraction bug, so
the repo's sanctioned 001431 baseline is used instead):
- electric-storage main (code 402) + secondary 691: removal reproduces the
secondary-removed cert at delta 0 — RdSAP §A.2.2 re-forces a default secondary,
matching the user's F35→F35 example;
- gas combi main (code 104) + secondary 691: removal strictly raises SAP
(74.22→77.61) — the Table 11 fraction reallocates to the cheaper main.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Records the four load-bearing design decisions from the grill-with-docs session:
standalone co-selectable rec; eligibility = lodged-only (no effectiveness gate,
electric-storage §A.2.2 no-op is the Optimiser's call); dedicated clearing
SecondaryHeatingOverlay; flat per-dwelling cost (a lodged secondary is fixed per
RdSAP, so a real decommission job, not room-scaled).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The gov-EPC API surfaces the assessor's RdSAP-assessed per-element U-values
as `roof_u_value` / `wall_u_value` / `floor_u_value` on each building part.
These were undeclared on the RdSAP 21.0.0/21.0.1 schemas, so `from_dict`
silently dropped them, and `heat_transmission` re-derived each U from the §5.6
/§5.7/§5.11 construction-default cascade. The gov OPEN data routinely redacts
the backing insulation thickness, so that re-derivation mis-bills an insulated
element as uninsulated.
RdSAP 10 §5.1: a known element U-value (documentary evidence / the lodged
RdSAP output) is used directly in place of the construction-default cascade.
Per [[project_per_cert_mapper_validation_state]] the gov API carries RdSAP
OUTPUT, so the lodged U reproduces the official's element heat loss exactly.
Worst case in the 2026 sample: cert 7921-0052-0940-5007-0663, an age-C
"Pitched, sloping ceiling" (rc=8) top-floor flat lodging roof_u_value=0.2 with
no thickness. The cascade returned the uninsulated 2.30 W/m²K → SAP 56.9 vs
lodged 80 (-23.09, the single largest error in the sample). The roof override
alone recovers ~15 SAP; the wall override (lodged 0.34 vs cascade) closes the
rest of this cohort.
Override applies to the MAIN wall only (alt-wall sub-areas keep their own
per-area U) and the part's floor=0. Fires only when the rare field is present
(9 of 909 computed certs), so the Summary path — which never lodges these
API fields — is untouched.
API gauge: 67.1% → 67.7% within-0.5, mean|err| 1.024 → 0.992.
Worksheet harness: 47/47, 0 divergers (unchanged).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Flat per-dwelling decommission price (sample_catalogue \£250) + 0.25 contingency
(covers unknown heater count / hard-wired-vs-plugged / repaint extent). The JSON
repo joins the contingency from config, proven by the new repo test. No composite
Products machinery — a lodged secondary is one roughly-fixed job, not room-scaled.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
recommend_secondary_heating_removal offers one standalone Option that clears the
lodged secondary system. Eligibility is purely physical (offer iff
sap_heating.secondary_heating_type is set) — no effectiveness gate, since a
lodged secondary is a fixed emitter per RdSAP (portables are ignored), and the
electric-storage §A.2.2 no-op is the Optimiser's call (ADR-0028 decisions 1-2).
Priced at a flat per-dwelling decommission cost, not room-scaled.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ADR-0028's deferred extraction, triggered by 17.1 as the second instance: the
inherited 20.0.0 coefficients (0.148 + band multipliers + 4-way split) now live
in one `_synthesise_reduced_field_windows` core. The 20.0.0 / 18.0 / 17.1 seams
keep their own names (so each can diverge) but collapse to glazing-type
resolution (cascade + that spec's ND handling) plus a call to the core.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rich certs (14/1000) use lodged window_area; the windowless majority synthesise
glazing from the glazed_area band via _synthesise_17_1_sap_windows (own seam,
inherited 20.0.0 coefficients); lighting/ventilation/hot-water mirror 18.0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The first overlay surface that sets fields to *absent* rather than to a
target state: _fold_secondary_heating clears sap_heating.secondary_heating_type
+ secondary_fuel_type, so the calculator's Table 11 secondary-fraction split
(SAP 10.2 §9a) routes 100% of space heating to the main. On an electric-storage
main RdSAP §A.2.2 re-forces a default secondary, making removal a no-op there —
left to the Optimiser to de-select (ADR-0028 decisions 2-3).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Schema-fix delivered in the prior slice; this promotes the 1000-cert corpus
into the strict guard (matches 18.0/20.0.0).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The §5.16 Table 22 thermal-mass-parameter (TMP) "always low-mass" set was
{timber 5, cob 7, park home 8}. But wall_construction code 8 is OVERLOADED by
the same gov-API/calc code-space divergence as the wall-U fix: the Summary
path's "PH" mapping uses 8 for park home, while the gov-EPC API enum uses 8
for SYSTEM BUILD (Summary system build = code 6). So every API system-built
cert was mis-rated as low-mass 100 kJ/m²K instead of masonry 250 (Table 22
lists system build as masonry — PDF p.48, line "System build 250...").
A too-low TMP shortens the §7 time constant tau = Cm/(3.6·H), over-cutting
the temperature reduction so mean internal temperature is UNDER-stated →
space-heating demand under-stated → SAP over-rated. This was the cause of the
uninsulated system-built over-rate cluster (n=9 gas-boiler certs at signed
+2.39 vs cavity +0.43 / solid-brick +0.08 at the same bands — a system-built-
specific anomaly with a spec-correct wall U).
Fix: drop 8 from the always-low set and gate it on `property_type` — code 8 is
the low-mass park-home value only when the dwelling really is a park home,
otherwise it is gov-API system build and keeps masonry 250. Disambiguated by
the same `property_type == "park home"` signal used elsewhere in the cascade.
Worksheet harness UNAFFECTED (47/47, 0 divergers): the Summary path uses code
6 for system build and code 8 only for genuine park homes (which stay
low-mass via the property_type gate). API gauge 65.3% -> 67.1% within-0.5
(mean|err| 1.059 -> 1.024, signed +0.050 -> -0.002). The uninsulated
system-built cluster collapses +2.82 -> +0.28 signed (0/11 -> 7/11 within
0.5). 2 AAA tests (parametrised code-8 system-built -> 250; park-home
property -> 100). pyright net-zero.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Documents the inherit-and-validate decision for 18.0: reuse 20.0.0's 0.148 +
band multipliers (the corpus can't self-fit — 958/1000 band-1 with no measured
band-1 windows), validated against 18.0's own band-4 rich certs (0.223 obs vs
0.148 x 1.51 pred). References ADR-0027 one-way (keeps the accepted ADR immutable).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>