Slice 19a: strict cascade-pin scoreboard for SapResult vs U985 PDFs

Replaces the loose collection of fixture-specific SAP score tests +
parametrized lighting / pumps_fans / secondary spot-checks with a
single strict cascade pin: every SapResult float field vs PDF line
ref at abs=1e-4, every fixture × field pair as its own parametrized
case. 66 cases (11 fields × 6 fixtures); 18 pass, 48 fail.

Why: the Elmhurst corpus is a deterministic test-vector set — input
lodgement, intermediate values per line ref, final SAP outputs all
known to 4 d.p. To replicate SAP 10.2 exactly there is no reason to
accept tolerance >0 on the final outputs. The prior pattern (per-
section unit tests using PDF values as INPUTS, fixture-specific SAP
tests at <=0.5 continuous, fuel-cost tests at rel=0.05 / rel=0.15)
let cascade biases propagate without surfacing as named failures.

Pin matrix:

  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              |  ✓  |  ✓  |  ✓  |  ✓  |  ✓  |  ✓

Each failing test name is the work queue. No tolerance widening, no
xfail — a failing pin is a named calculator bug. Subsequent slices
close them one at a time.

Existing loose-tolerance tests in test_fuel_cost.py (rel=0.15 for
000474 and rel=0.05 for 000490) are subsumed by the new
total_fuel_cost_gbp pin at abs=1e-4 and will be removed in 19b.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-22 22:28:59 +00:00
parent 5e34594d8a
commit 6bfb0614aa

View file

@ -1,31 +1,38 @@
"""End-to-end SAP score validation against the Elmhurst worksheet outputs.
"""End-to-end cascade pins against the Elmhurst U985 worksheet PDFs.
For each non-RR Elmhurst fixture, run the full calculator chain
EpcPropertyData cert_to_inputs calculate_sap_from_inputs
and compare the resulting SAP score against the Elmhurst worksheet's
SAP rating (line 258).
The Elmhurst corpus is a deterministic test-vector set: each cert has
input lodgement (the Summary_NNNNNN PDF), intermediate values per
line ref (the U985-0001-NNNNNN PDF), and final SAP outputs (the
rating section). To replicate the SAP 10.2 engine exactly we pin
every SAP-result field against the PDF at `abs=1e-4` for every
fixture in the cohort.
These tests pin the current end-to-end gap so subsequent slices that
shrink it (worksheet §4 wired into cert_to_inputs, PCDB Table 3b combi
loss, etc.) show up as tolerance tightening rather than silent drift.
Each pin is its own test case via pytest parametrize so failures are
named like `test_sap_result_pin[000487-main_heating_fuel_kwh_per_yr]`
making the work queue legible.
Reference: Elmhurst U985-0001-000474.pdf and U985-0001-000490.pdf
(supplied by the user; not stored in repo).
Per `[[feedback-e2e-validation-philosophy]]` + `[[feedback-continuous-
sap-tolerance]]`: tolerances are NOT widened to mask drift. A failing
pin is a named calculator bug to fix, not a tolerance to relax.
Reference: SAP 10.2 specification (14-03-2025).
"""
from dataclasses import dataclass
from dataclasses import dataclass, fields
from types import ModuleType
from typing import Final
import pytest
from types import ModuleType
from domain.sap.calculator import Sap10Calculator
from domain.sap.rdsap.cert_to_inputs import cert_to_inputs
from domain.sap.worksheet.tests import (
_elmhurst_worksheet_000474 as _w000474,
_elmhurst_worksheet_000477 as _w000477,
_elmhurst_worksheet_000480 as _w000480,
_elmhurst_worksheet_000487 as _w000487,
_elmhurst_worksheet_000490 as _w000490,
_elmhurst_worksheet_000516 as _w000516,
)
from domain.sap.worksheet.tests._elmhurst_fixtures import (
ALL_FIXTURES as _ELMHURST_FIXTURES,
@ -33,301 +40,158 @@ from domain.sap.worksheet.tests._elmhurst_fixtures import (
)
# Absolute tolerance for every float field — matches the PDF's 4 d.p.
# display precision. Anything closer is sub-spec resolution; anything
# looser is a drift gap.
_FLOAT_PIN_ABS: Final[float] = 1e-4
@dataclass(frozen=True)
class ElmhurstExpectedSap:
"""Headline figures from the Elmhurst worksheet's SAP rating section
(xlsx rows around line refs 240 / 255 / 257 / 258 / 261)."""
sap_rating: int # (258) integer
sap_score_continuous: float # (258) un-rounded
space_heating_kwh: float # (98c) annual
hot_water_kwh: float # (219) annual fuel
total_energy_cost_gbp: float # (255)
ecf: float # (257)
_ELMHURST_000490_EXPECTED: Final[ElmhurstExpectedSap] = ElmhurstExpectedSap(
sap_rating=57,
sap_score_continuous=57.3979,
space_heating_kwh=11183.2751,
hot_water_kwh=2850.5701,
total_energy_cost_gbp=807.5421,
ecf=3.0539,
)
_ELMHURST_000477_EXPECTED: Final[ElmhurstExpectedSap] = ElmhurstExpectedSap(
sap_rating=65,
sap_score_continuous=65.0050,
space_heating_kwh=10111.2019,
hot_water_kwh=2116.0365,
total_energy_cost_gbp=732.1396,
ecf=2.5086,
)
_ELMHURST_000474_EXPECTED: Final[ElmhurstExpectedSap] = ElmhurstExpectedSap(
sap_rating=62,
sap_score_continuous=62.2584,
space_heating_kwh=10612.8595,
hot_water_kwh=2291.7784,
total_energy_cost_gbp=655.6949,
ecf=2.7055,
)
def test_elmhurst_000477_end_to_end_sap_score_matches_pdf() -> None:
"""Cohort closure pin for 000477. Mid-terrace combi-gas with PCDF
Vaillant ecoTEC sustain 24 (index 18118) + Electricity Electric
Panel secondary heater (SAP code 691). PDF SAP rating 65."""
# Arrange
epc = _w000477.build_epc()
# Act
result = Sap10Calculator().calculate(epc)
# Assert — integer match (the rdsap engine integration gate).
delta = abs(result.sap_score - _ELMHURST_000477_EXPECTED.sap_rating)
assert delta == 0, (
f"SAP rating delta {delta} — expected 0 (integer match with PDF). "
f"Actual={result.sap_score}, expected={_ELMHURST_000477_EXPECTED.sap_rating}."
)
continuous_delta = abs(
result.sap_score_continuous - _ELMHURST_000477_EXPECTED.sap_score_continuous
)
assert continuous_delta <= 0.5, (
f"Continuous SAP delta {continuous_delta:.2f} exceeds ceiling 0.5"
)
def test_elmhurst_000490_end_to_end_sap_score_currently_within_3_points() -> None:
"""Mid-terrace combi-gas dwelling with time-clock keep-hot. After the
PCDB Table 105 integration the fixture lodges `main_heating_index_
number=10328` (Vaillant Ecotec Pro 28kW, winter eff 88.2%, summer
79.6%) per the PDF's "PCDF boiler reference: 10328 Vaillant Ecotec
Pro 88.20%" lodgement. Cert path now resolves efficiency via the
spec-faithful PCDB precedence rather than the Table 4a category-2
default (80%).
Post-PCDB residuals:
| metric | actual | PDF | delta |
| --------------- | -------- | --------- | ----- |
| space heating | 11467.18 | 11183.275 | +2.5% |
| hot water fuel | 3028.27 | 2850.570 | +6.2% |
| main heating fuel | 13001.3| 13003.85 | -0.02%|
| total fuel cost | £706.23 | £807.54 | 12.5%|
| SAP rating | 63 | 57 | +6 |
The PCDB efficiency override closes the main-heating-fuel gap to <0.1%
(matches PDF), and the HW kWh gap narrows from +8.4% +6.2%. The
total fuel cost diverges from -6.3% -12.5% and the SAP score moves
+3 +6 because the lodged cert pre-dates the 14-March-2025 SAP10.2
amendment which lowered gas unit prices ~13% (per ADR-0010 §3
Validation Cohort: only certs lodged 2025-07-01 are spec-comparable
on cost / SAP rating). The fuel-kWh tightening is the spec-faithful
direction; the cost / SAP residuals are documented spec-version
drift, not a calculator regression.
Ceiling raised 3 6 (SAP integer) and 3.0 6.0 (continuous) to
reflect the post-PCDB current state. **§10a slice 2 tightening:**
ceiling dropped 6 2 after the cost-side rewrite (Table 32 prices
+ Table 12 note (a) standing-charge gating per ADR-0010 amendment)
landed. The "spec-version drift" framing in the handover turned out
to be wrong-table + missing-standing-charges a real calculator
regression, not a corpus issue. **§4 HW slice 2 update:** ceiling
raised 2 3 because the Equation D1 monthly cascade closes the HW
kWh gap (3028 2847 = 0.1% of PDF 2851), which slightly *reduces*
cost (£776 £770) and pushes SAP score from 59 60 further
from the spec-version-drifted PDF SAP 57. The HW kWh closure is
the spec-faithful direction; the +3 SAP delta is the ADR-0010 §3
Validation Cohort filter at work. Tightens further when Tables
D1/D2/D3 Ecodesign + Appendix N adjustments land.
class FixtureCascadePins:
"""PDF-extracted expected values for every top-level `SapResult`
field. Each value is the canonical line ref from the U985 worksheet
(page references in the field comments). Field names mirror
`SapResult` exactly so the parametrized test can read both via
`getattr` without per-field plumbing.
"""
# Arrange
epc = _w000490.build_epc()
# Act
result = Sap10Calculator().calculate(epc)
# Assert — full cohort residual hunt closed: Appendix L lighting +
# secondary heating cascade + ventilation cert lodgements + Table 4f
# pumps_fans + SAP 10.2 rating constants. 000490 now hits SAP integer
# delta=0 (continuous ~0.002 under PDF).
delta = abs(result.sap_score - _ELMHURST_000490_EXPECTED.sap_rating)
assert delta == 0, (
f"SAP rating delta {delta} — expected 0 (integer match with PDF). "
f"Actual={result.sap_score}, expected={_ELMHURST_000490_EXPECTED.sap_rating}."
)
continuous_delta = abs(
result.sap_score_continuous - _ELMHURST_000490_EXPECTED.sap_score_continuous
)
assert continuous_delta <= 0.5, (
f"Continuous SAP delta {continuous_delta:.2f} exceeds ceiling 0.5"
)
sap_score: int # (258) integer rating
sap_score_continuous: float # (258) un-rounded
ecf: float # (257) energy cost factor
total_fuel_cost_gbp: float # (255) total energy cost
co2_kg_per_yr: float # (272) total CO2 emissions
space_heating_kwh_per_yr: float # (98c) annual space heat
main_heating_fuel_kwh_per_yr: float # (211) main system 1 fuel
secondary_heating_fuel_kwh_per_yr: float # (215) secondary fuel
hot_water_kwh_per_yr: float # (219) water heating fuel
lighting_kwh_per_yr: float # (232) electricity for lighting
pumps_fans_kwh_per_yr: float # (231) pumps + fans + flue
def test_elmhurst_000474_end_to_end_sap_score_currently_within_3_points() -> None:
"""End-terrace PCDB-tested Vaillant boiler. After the PCDB Table 105
integration the fixture lodges `main_heating_index_number=16839`
(Vaillant ecoTEC pro 28 VUW GB 286/5-3, winter eff 88.7%, summer
87.0%, comparative HW 75.1%) per the PDF's "PCDF boiler reference:
16839 Vaillant ecoTEC pro 28 88.70%" lodgement.
_FIXTURE_PINS: Final[dict[str, FixtureCascadePins]] = {
"000474": FixtureCascadePins(
sap_score=62, sap_score_continuous=62.2584, ecf=2.7055,
total_fuel_cost_gbp=655.6949, co2_kg_per_yr=3036.2933,
space_heating_kwh_per_yr=10612.8595,
main_heating_fuel_kwh_per_yr=11964.8924,
secondary_heating_fuel_kwh_per_yr=0.0,
hot_water_kwh_per_yr=2291.7784,
lighting_kwh_per_yr=139.9452,
pumps_fans_kwh_per_yr=160.0,
),
"000477": FixtureCascadePins(
sap_score=65, sap_score_continuous=65.0057, ecf=2.5086,
total_fuel_cost_gbp=732.1396, co2_kg_per_yr=2807.8621,
space_heating_kwh_per_yr=10111.2019,
main_heating_fuel_kwh_per_yr=10270.9726,
secondary_heating_fuel_kwh_per_yr=1011.1202,
hot_water_kwh_per_yr=2116.0365,
lighting_kwh_per_yr=201.6754,
pumps_fans_kwh_per_yr=160.0,
),
"000480": FixtureCascadePins(
sap_score=61, sap_score_continuous=61.2986, ecf=2.7743,
total_fuel_cost_gbp=854.8139, co2_kg_per_yr=3393.8852,
space_heating_kwh_per_yr=12398.5783,
main_heating_fuel_kwh_per_yr=12580.2936,
secondary_heating_fuel_kwh_per_yr=1239.8578,
hot_water_kwh_per_yr=2423.6393,
lighting_kwh_per_yr=212.5531,
pumps_fans_kwh_per_yr=160.0,
),
"000487": FixtureCascadePins(
sap_score=62, sap_score_continuous=61.6431, ecf=2.7496,
total_fuel_cost_gbp=828.6119, co2_kg_per_yr=2931.4900,
space_heating_kwh_per_yr=10834.7778,
main_heating_fuel_kwh_per_yr=11018.4181,
secondary_heating_fuel_kwh_per_yr=1083.4778,
hot_water_kwh_per_yr=1489.1033,
lighting_kwh_per_yr=227.6861,
pumps_fans_kwh_per_yr=160.0,
),
"000490": FixtureCascadePins(
sap_score=57, sap_score_continuous=57.3979, ecf=3.0539,
total_fuel_cost_gbp=807.5421, co2_kg_per_yr=3213.5359,
space_heating_kwh_per_yr=11183.2751,
main_heating_fuel_kwh_per_yr=11411.5052,
secondary_heating_fuel_kwh_per_yr=1118.3275,
hot_water_kwh_per_yr=2850.5701,
lighting_kwh_per_yr=171.4217,
pumps_fans_kwh_per_yr=160.0,
),
"000516": FixtureCascadePins(
sap_score=63, sap_score_continuous=62.7937, ecf=2.6671,
total_fuel_cost_gbp=860.7162, co2_kg_per_yr=3416.8449,
space_heating_kwh_per_yr=12410.3170,
main_heating_fuel_kwh_per_yr=12606.4169,
secondary_heating_fuel_kwh_per_yr=1241.0317,
hot_water_kwh_per_yr=2493.1900,
lighting_kwh_per_yr=230.8853,
pumps_fans_kwh_per_yr=160.0,
),
}
Post-PCDB residuals nearly closed:
| metric | actual | expected | delta |
| --------------- | ------- | -------- | ----- |
| space heating | 10914.3 | 10612.86 | +2.8% |
| hot water fuel | 2621.7 | 2291.78 | +14.4%|
| total fuel cost | £651.85 | £655.69 | -0.6% |
| SAP rating | 63 | 62 | +1 |
_FIXTURE_MODULES: Final[dict[str, ModuleType]] = {
"000474": _w000474,
"000477": _w000477,
"000480": _w000480,
"000487": _w000487,
"000490": _w000490,
"000516": _w000516,
}
The PCDB summer efficiency override (was 80% 87.0%) closes the HW
fuel gap from +32% to +14.4% the residual is the Appendix J §3b
PCDB combi loss table that our HW cascade still uses Table 3a row
defaults for. The SAP rating sits comfortably within tolerance.
Ceiling dropped 7 2 (SAP integer) and 7.0 2.0 (continuous)
reflecting the post-PCDB current state. **§10a slice 2 update:**
ceiling raised 2 4 because the post-§10a Table 32 + standing-
charge rewrite exposed upstream HW kWh + Appendix L lighting kWh
overestimates that the wrong pre-§10a prices had been masking.
**§4 HW slices 1 + 2 update:** ceiling dropped 4 3 PCDB Table
3b combi-loss override + Equation D1 monthly water-eff cascade
close 000474 HW kWh from 2622 2292 (matches PDF 2292 to 0.1%).
The remaining +9% cost residual and +3 SAP delta are Appendix L
lighting (528 vs ~169 back-derived) a separate ticket per memory
`project_section_4_hw_next_ticket`'s "secondary upstream" note.
"""
# Arrange
epc = _w000474.build_epc()
# Act
result = Sap10Calculator().calculate(epc)
# Assert — Appendix L closure brought 000474 SAP integer to 62 = PDF 62
# (delta = 0 exactly). Continuous delta lands at ~0.09 — well under the
# 0.5 ceiling. Per feedback-e2e-validation-philosophy: integer match
# is the rdsap engine integration gate; this fixture now passes that gate.
delta = abs(result.sap_score - _ELMHURST_000474_EXPECTED.sap_rating)
assert delta == 0, (
f"SAP rating delta {delta} — expected 0 (integer match with PDF). "
f"Actual={result.sap_score}, expected={_ELMHURST_000474_EXPECTED.sap_rating}."
)
continuous_delta = abs(
result.sap_score_continuous - _ELMHURST_000474_EXPECTED.sap_score_continuous
)
assert continuous_delta <= 0.5, (
f"Continuous SAP delta {continuous_delta:.2f} exceeds ceiling 0.5"
)
_PIN_FIELDS: Final[tuple[str, ...]] = tuple(f.name for f in fields(FixtureCascadePins))
@pytest.mark.parametrize(
"fixture, expected_kwh",
[
(_w000474, _w000474.LINE_232_LIGHTING_KWH_PER_YR),
(_w000490, _w000490.LINE_232_LIGHTING_KWH_PER_YR),
],
ids=["000474", "000490"],
"fixture_name,field_name",
[(name, fld) for name in _FIXTURE_PINS for fld in _PIN_FIELDS],
ids=lambda x: x,
)
def test_elmhurst_end_to_end_lighting_kwh_per_yr_matches_u985_worksheet(
fixture: object, expected_kwh: float
) -> None:
"""Component-level e2e validation: `SapResult.lighting_kwh_per_yr`
must match the U985 worksheet's line ref (232) value to 4 d.p. for
each fixture lodged with full Appendix L cert inputs.
def test_sap_result_pin(fixture_name: str, field_name: str) -> None:
"""Strict cascade pin — `SapResult.<field>` matches the U985 PDF's
line ref to abs=1e-4 for every fixture × field combination.
Closes the legacy `predicted_lighting_kwh` heuristic the cost-side
annual kWh is now derived via the same spec-faithful L1-L11 cascade
that drives §5 (67). Per ADR-0010 + the e2e validation philosophy
(memory: feedback-e2e-validation-philosophy) component pins
validate the rdsap engine piece by piece; SAP integer integration
test must hit delta=0 in a later cycle.
Each (fixture, field) pair is its own pytest case so failures
surface as `test_sap_result_pin[000487-main_heating_fuel_kwh_per_yr]`
the work queue is the test list. Per validation philosophy:
no tolerance widening, no xfail; a failing pin is a calculator
bug to fix.
"""
# Arrange
epc = fixture.build_epc() # type: ignore[attr-defined]
pin = _FIXTURE_PINS[fixture_name]
epc = _FIXTURE_MODULES[fixture_name].build_epc()
expected = getattr(pin, field_name)
# Act
result = Sap10Calculator().calculate(epc)
actual = getattr(result, field_name)
# Assert
assert result.lighting_kwh_per_yr == pytest.approx(expected_kwh, abs=1e-4)
@pytest.mark.parametrize(
"fixture, expected_kwh",
[
(_w000474, 160.0),
(_w000490, 160.0),
],
ids=["000474", "000490"],
)
def test_elmhurst_end_to_end_pumps_fans_kwh_matches_u985_worksheet(
fixture: object, expected_kwh: float
) -> None:
"""Component-level pin on `SapResult.pumps_fans_kwh_per_yr` for the
e2e fixtures. PDF (231) for both 000474 + 000490: 115 (central
heating pump (230c)) + 45 (main heating flue fan (230e)) = 160.
Pre-fix `cert_to_inputs` hardcoded 130 kWh/yr via
`_DEFAULT_PUMPS_FANS_KWH_PER_YR`. The shortfall (-30 kWh × elec
price = -£4) was the dominant remaining residual on 000490 after
Appendix L + secondary heating + ventilation closures pushed
continuous SAP +0.38 over PDF integer 58 vs 57.
"""
# Arrange
epc = fixture.build_epc() # type: ignore[attr-defined]
# Act
result = Sap10Calculator().calculate(epc)
# Assert
assert result.pumps_fans_kwh_per_yr == pytest.approx(expected_kwh, abs=1e-3)
def test_elmhurst_000490_end_to_end_secondary_heating_fuel_kwh_matches_u985_worksheet() -> None:
"""Component-level e2e pin on `SapResult.secondary_heating_fuel_kwh_per_yr`
for 000490 cert lodges secondary heating system "Electricity Electric
Panel, convector or radiant heaters" (SAP Code 691, 100% efficiency).
Table 11 fraction 0.10 of total space heat goes to the secondary
system (215) = 1118.3275 kWh.
Closes the next 000490 residual after Appendix L: secondary fuel was
silently 0 because build_epc didn't lodge secondary_heating_type, so
`_secondary_fraction` early-returned 0.0 all useful space heat
routed to main 1 main_fuel_kwh +1357 kWh over PDF, secondary -1118
under PDF. Cost gap was £147 secondary missing minus £47 main
overshoot = -£104 (the dominant residual after Appendix L closure).
"""
# Arrange
epc = _w000490.build_epc()
# Act
result = Sap10Calculator().calculate(epc)
# Assert — useful space heating now matches PDF to ~0.01% (post
# ventilation-cert closures); secondary cascade propagates 1:1 →
# residual ≤ 0.1 kWh.
assert result.secondary_heating_fuel_kwh_per_yr == pytest.approx(
_w000490.LINE_215_SECONDARY_HEATING_FUEL_KWH, abs=0.1
)
if isinstance(expected, int):
assert actual == expected, (
f"{fixture_name}.{field_name}: actual={actual}, expected={expected}"
)
else:
assert actual == pytest.approx(expected, abs=_FLOAT_PIN_ABS), (
f"{fixture_name}.{field_name}: actual={actual}, "
f"expected={expected}, diff={abs(actual - expected):.4f}"
)
@pytest.mark.parametrize("fixture", _ELMHURST_FIXTURES, ids=_elmhurst_fixture_id)
def test_elmhurst_cert_to_inputs_monthly_infiltration_ach_matches_u985_worksheet(
fixture: ModuleType,
) -> None:
"""Component-level pin on `cert_to_inputs(epc).monthly_infiltration_ach`
for every Elmhurst fixture. The certinputs path must produce the same
(25)m effective ACH tuple that the §2 ventilation module test pins
against `LINE_25_EFFECTIVE_ACH`.
"""Cert→inputs (25)m effective ACH pin (existing test, retained).
Pre-fix the cert path under-counted (8) openings (extract_fans_count
defaulted to 0; PDF lodges 1-2) AND over-counted (15) window infil
(percent_draughtproofed defaulted to 0; PDF lodges 75-100). The net
bias on (25)m propagates 1:1 to HLC × ΔT useful space heat main
+ secondary fuel kWh cost / SAP integer.
Once this pin lands the only remaining ventilation-cascade gap is
`sheltered_sides` (not on EPC schema; cert_to_inputs hardcodes 2
addressed in the next cycle).
Each fixture's `monthly_infiltration_ach` from `cert_to_inputs` must
match the U985 worksheet's LINE_25_EFFECTIVE_ACH at abs=1e-3 — the
upstream of `_sap_result_pin`'s heat-loss-rate path. Lives outside
the cascade-pin grid because it asserts an intermediate (cert
inputs) value, not a SAP result field.
"""
# Arrange
epc = fixture.build_epc()
@ -340,24 +204,3 @@ def test_elmhurst_cert_to_inputs_monthly_infiltration_ach_matches_u985_worksheet
assert inputs.monthly_infiltration_ach[m] == pytest.approx(
fixture.LINE_25_EFFECTIVE_ACH[m], abs=1e-3
), f"(25) month {m+1} drift"
def test_elmhurst_000490_end_to_end_kwh_within_15pct() -> None:
"""Per-end-use kWh sanity check for 000490. Closer-fitting than the
SAP score because intermediate values aren't compressed through the
cost-deflator + rating equations."""
# Arrange
epc = _w000490.build_epc()
# Act
result = Sap10Calculator().calculate(epc)
# Assert
exp = _ELMHURST_000490_EXPECTED
assert result.space_heating_kwh_per_yr == pytest.approx(
exp.space_heating_kwh, rel=0.15
)
assert result.hot_water_kwh_per_yr == pytest.approx(exp.hot_water_kwh, rel=0.15)
assert result.total_fuel_cost_gbp == pytest.approx(
exp.total_energy_cost_gbp, rel=0.15
)