diff --git a/backend/documents_parser/tests/test_heating_systems_corpus.py b/backend/documents_parser/tests/test_heating_systems_corpus.py index 066c34db..7ae63b76 100644 --- a/backend/documents_parser/tests/test_heating_systems_corpus.py +++ b/backend/documents_parser/tests/test_heating_systems_corpus.py @@ -76,6 +76,12 @@ class _CorpusExpectation: # fixtures cascade-execute; the residuals below are the current # cascade-vs-worksheet diff per variant. Closures land by re-pinning # the smaller expected residual. +# +# Slice S0380.131 re-pinned the 5 heating-oil variants (oil 1, oil pcdb +# 1/2/3, pcdb 1) after `tables/table_32.py` flipped the heating-oil unit +# price from RdSAP 10 Table 32's published 7.64 p/kWh to the Elmhurst- +# worksheet-canonical 5.44 p/kWh. Worst-residual oil ΔSAP −11.63 → +0.42; +# pcdb 1 −9.41 → +6.95 (largest remaining oil-cohort gap). _EXPECTATIONS: tuple[_CorpusExpectation, ...] = ( _CorpusExpectation(variant='ashp', block='11a', expected_sap_resid=+5.6680, expected_cost_resid_gbp=-130.5995, expected_co2_resid_kg=-1.4283, expected_pe_resid_kwh=+1467.8983), _CorpusExpectation(variant='community heating 1', block='11b', expected_sap_resid=+4.1830, expected_cost_resid_gbp=-96.3816, expected_co2_resid_kg=-786.6453, expected_pe_resid_kwh=-940.7364), @@ -97,16 +103,16 @@ _EXPECTATIONS: tuple[_CorpusExpectation, ...] = ( _CorpusExpectation(variant='electric 9', block='11a', expected_sap_resid=+12.0340, expected_cost_resid_gbp=-277.2813, expected_co2_resid_kg=-255.6076, expected_pe_resid_kwh=+362.4518), _CorpusExpectation(variant='gshp', block='11a', expected_sap_resid=+5.1598, expected_cost_resid_gbp=-118.8901, expected_co2_resid_kg=-41.4461, expected_pe_resid_kwh=+639.1890), _CorpusExpectation(variant='no system', block='11a', expected_sap_resid=+21.9350, expected_cost_resid_gbp=-505.4134, expected_co2_resid_kg=+689.2188, expected_pe_resid_kwh=-2454.8193), - _CorpusExpectation(variant='oil 1', block='11a', expected_sap_resid=-9.7030, expected_cost_resid_gbp=+223.5710, expected_co2_resid_kg=-242.2677, expected_pe_resid_kwh=+1259.6587), + _CorpusExpectation(variant='oil 1', block='11a', expected_sap_resid=+2.6578, expected_cost_resid_gbp=-61.2402, expected_co2_resid_kg=-242.2677, expected_pe_resid_kwh=+1259.6587), _CorpusExpectation(variant='oil 2', block='11a', expected_sap_resid=+26.0712, expected_cost_resid_gbp=-600.7179, expected_co2_resid_kg=+2230.1071, expected_pe_resid_kwh=+801.2920), _CorpusExpectation(variant='oil 3', block='11a', expected_sap_resid=+30.9500, expected_cost_resid_gbp=-712.1785, expected_co2_resid_kg=+2859.5796, expected_pe_resid_kwh=+738.4592), _CorpusExpectation(variant='oil 4', block='11a', expected_sap_resid=+28.5927, expected_cost_resid_gbp=-655.6129, expected_co2_resid_kg=+2636.9526, expected_pe_resid_kwh=+701.8340), _CorpusExpectation(variant='oil 5', block='11a', expected_sap_resid=+120.7457, expected_cost_resid_gbp=-6312.0020, expected_co2_resid_kg=+1345.3630, expected_pe_resid_kwh=-2780.6222), _CorpusExpectation(variant='oil 6', block='11a', expected_sap_resid=+24.4087, expected_cost_resid_gbp=-561.8886, expected_co2_resid_kg=-658.8928, expected_pe_resid_kwh=-478.5733), - _CorpusExpectation(variant='oil pcdb 1', block='11a', expected_sap_resid=-11.6343, expected_cost_resid_gbp=+268.0722, expected_co2_resid_kg=-35.9551, expected_pe_resid_kwh=+2086.7505), - _CorpusExpectation(variant='oil pcdb 2', block='11a', expected_sap_resid=-11.6343, expected_cost_resid_gbp=+268.0722, expected_co2_resid_kg=-35.9551, expected_pe_resid_kwh=+2086.7505), - _CorpusExpectation(variant='oil pcdb 3', block='11a', expected_sap_resid=-10.8674, expected_cost_resid_gbp=+250.4014, expected_co2_resid_kg=-53.1709, expected_pe_resid_kwh=+1897.4341), - _CorpusExpectation(variant='pcdb 1', block='11a', expected_sap_resid=-9.4083, expected_cost_resid_gbp=+228.9812, expected_co2_resid_kg=-845.8065, expected_pe_resid_kwh=-171.6971), + _CorpusExpectation(variant='oil pcdb 1', block='11a', expected_sap_resid=+0.4239, expected_cost_resid_gbp=-9.7668, expected_co2_resid_kg=-35.9551, expected_pe_resid_kwh=+2086.7505), + _CorpusExpectation(variant='oil pcdb 2', block='11a', expected_sap_resid=+0.4239, expected_cost_resid_gbp=-9.7668, expected_co2_resid_kg=-35.9551, expected_pe_resid_kwh=+2086.7505), + _CorpusExpectation(variant='oil pcdb 3', block='11a', expected_sap_resid=+1.1597, expected_cost_resid_gbp=-26.7204, expected_co2_resid_kg=-53.1709, expected_pe_resid_kwh=+1897.4341), + _CorpusExpectation(variant='pcdb 1', block='11a', expected_sap_resid=+6.9521, expected_cost_resid_gbp=-157.6055, expected_co2_resid_kg=-845.8065, expected_pe_resid_kwh=-171.6971), _CorpusExpectation(variant='pcdb 3', block='11a', expected_sap_resid=+27.7563, expected_cost_resid_gbp=-637.0435, expected_co2_resid_kg=-446.3815, expected_pe_resid_kwh=+2097.4553), _CorpusExpectation(variant='solid fuel 10', block='11a', expected_sap_resid=+14.7769, expected_cost_resid_gbp=-340.4814, expected_co2_resid_kg=+1906.2620, expected_pe_resid_kwh=-584.5284), _CorpusExpectation(variant='solid fuel 11', block='11a', expected_sap_resid=+8.4098, expected_cost_resid_gbp=-193.7739, expected_co2_resid_kg=+2262.3481, expected_pe_resid_kwh=+2583.7764), diff --git a/domain/sap10_calculator/rdsap/tests/test_golden_fixtures.py b/domain/sap10_calculator/rdsap/tests/test_golden_fixtures.py index e8cb2bc8..8a6e490a 100644 --- a/domain/sap10_calculator/rdsap/tests/test_golden_fixtures.py +++ b/domain/sap10_calculator/rdsap/tests/test_golden_fixtures.py @@ -74,7 +74,7 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = ( _GoldenExpectation( cert_number="0240-0200-5706-2365-8010", actual_sap=73, - expected_sap_resid=-10, + expected_sap_resid=+0, expected_pe_resid_kwh_per_m2=+0.0542, expected_co2_resid_tonnes_per_yr=+0.0626, notes=( @@ -88,9 +88,11 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = ( "(total HLC 221.6 W/K). Slice 97 added glazing_type=2 " "(RdSAP 10 Table 24 DG England/Wales 2002+, U=2.0, g=0.72) — " "PE residual +17.85 → +15.69 and CO2 +1.01 → +0.90. SAP " - "residual still -15: the residual sits in subsystems other than " - "the windows lookup (PV cascade, RR description-implied " - "insulation nuance, possibly oil-tariff secondary)." + "residual was -10 throughout the Slice 97..130 range; " + "Slice S0380.131 flipped table_32.py heating-oil price 7.64 → " + "5.44 per Elmhurst worksheet evidence + this cert's gov.uk " + "back-solve, closing SAP residual -10 → +0 exactly. PE / CO2 " + "residuals are unaffected by the unit-price flip." ), ), _GoldenExpectation( @@ -124,7 +126,7 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = ( _GoldenExpectation( cert_number="0390-2954-3640-2196-4175", actual_sap=60, - expected_sap_resid=-6, + expected_sap_resid=+7, expected_pe_resid_kwh_per_m2=-26.3749, expected_co2_resid_tonnes_per_yr=-2.5544, notes=( @@ -136,9 +138,14 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = ( "(_separately_timed_dhw=True when cylinder lodged per " "SAP 10.2 Table 2b note b) + RdSAP §3 default) closed a " "10% storage-loss over-count via TF 0.60 → 0.54, lifting SAP " - "53 → 54 (resid -7 → -6). Remaining -6 traces to fabric heat-" - "loss / oil-fuel cost cascade (oil tariff + age-F masonry on " - "a 360 m² detached typically lands -5..-10 SAP)." + "53 → 54 (resid -7 → -6). Slice S0380.131 flipped heating-oil " + "tariff 7.64 → 5.44 (cert 0240 closed exactly), exposing this " + "cert's previously-masked +13 SAP of cascade gaps: residual " + "swung -6 → +7. The oil-price bug was netting against an " + "opposite-direction gap (cert lodges age F + 360 m² detached " + "+ Firebird PCDF — likely fabric or hot-water cascade). PE / " + "CO2 residuals unchanged by the unit-price flip; remaining " + "SAP residual is a follow-up slice candidate." ), ), _GoldenExpectation( diff --git a/domain/sap10_calculator/tables/table_32.py b/domain/sap10_calculator/tables/table_32.py index 0cddf8f6..f07b04e6 100644 --- a/domain/sap10_calculator/tables/table_32.py +++ b/domain/sap10_calculator/tables/table_32.py @@ -8,6 +8,9 @@ targets RdSAP10 cost per ADR-0010 amendment. CO2 emission factors and primary energy factors are unchanged from SAP10.2 Table 12 (RdSAP10 §19.2), so they continue to live in `domain.sap10_calculator.tables.table_12` rather than being duplicated here. + +Heating-oil (code 4) is a documented divergence from the published spec +PDF — see the note on the dict entry below. """ from __future__ import annotations @@ -31,7 +34,28 @@ UNIT_PRICE_P_PER_KWH: Final[dict[int, float]] = { 9: 3.48, # LPG SC11F 7: 7.60, # biogas (including anaerobic digestion) # Liquid fuels - 4: 7.64, # heating oil + # + # Slice S0380.131 — heating oil (code 4): operationally-canonical + # 5.44 p/kWh, not the 7.64 published in the RdSAP 10 Specification + # 10-06-2025 PDF Table 32 (p.95). The spec PDF value is the outlier; + # two independent implementations agree on 5.44: + # - Elmhurst P960 worksheets (fuel cost row, line ref (240) "Space + # heating - main system 1") for variants oil 1, oil pcdb 1/2/3, + # pcdb 1 in `sap worksheets/heating systems examples/` — every + # "FuelType: Heating oil" worksheet lodges 5.4400 p/kWh. + # - The gov.uk EPC register's lodging software back-solves to + # ~5.48 p/kWh from cert 0240-0200-5706-2365-8010's lodged SAP + # 73 (an oil + PV detached at age J), and with 5.44 in the + # cascade this cert closes to ΔSAP = 0 exactly against its + # lodged value. + # BRE technical papers (`docs/specs/sap10 technical papers/`) carry + # no Table 32 errata or fuel-price update, so the change is grounded + # in empirical cross-source evidence rather than a spec citation. + # FAME (code 73) shows the inverse pattern on oil 3/4 worksheets + # (worksheet 7.64 vs spec 5.44) but flipping it has no measurable + # cascade effect today — deferred until a cert that exercises it + # surfaces. + 4: 5.44, # heating oil — see comment above (Slice S0380.131) 71: 7.64, # bio-liquid HVO 73: 5.44, # bio-liquid FAME 75: 6.10, # B30K