From 6bfb0614aa6ff50cad1327a19e920ed4eba482ce Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Fri, 22 May 2026 22:28:59 +0000 Subject: [PATCH] Slice 19a: strict cascade-pin scoreboard for SapResult vs U985 PDFs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../tests/test_e2e_elmhurst_sap_score.py | 445 ++++++------------ 1 file changed, 144 insertions(+), 301 deletions(-) diff --git a/packages/domain/src/domain/sap/worksheet/tests/test_e2e_elmhurst_sap_score.py b/packages/domain/src/domain/sap/worksheet/tests/test_e2e_elmhurst_sap_score.py index f3e2ecf5..9393ab1f 100644 --- a/packages/domain/src/domain/sap/worksheet/tests/test_e2e_elmhurst_sap_score.py +++ b/packages/domain/src/domain/sap/worksheet/tests/test_e2e_elmhurst_sap_score.py @@ -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.` 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 cert→inputs 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 - )