From 14918994124bdf67126e5aa6373be0e57db2d197 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 2 Jun 2026 09:48:37 +0000 Subject: [PATCH] =?UTF-8?q?Slice=20S0380.166:=20Elmhurst=20"Bulk=20LPG"=20?= =?UTF-8?q?label=20=E2=86=92=20API=20code=2027=20(mapper=20unblock)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the single missing dict entry that lets cert `pcdb 3` cascade: `_ELMHURST_MAIN_FUEL_TO_SAP10["Bulk LPG"] = 27` API code 27 = "LPG (not community)" — routes via: - `API_FUEL_TO_TABLE_12[27] = 2` (SAP 10.2 Table 12 bulk LPG: £62 standing, 6.74 p/kWh, 0.241 CO2, 1.141 PE; spec PDF p.189) - `API_FUEL_TO_TABLE_32[27] = 2` (RdSAP 10 Table 32 bulk LPG: £70 standing, 7.60 p/kWh; spec PDF p.95) Pre-slice the mapper produced `main_fuel_type=''` for any Elmhurst fixture lodging "Bulk LPG" as fuel type, so the cascade strict-raised `MissingMainFuelType` per S0380.132. The legacy `"LPG bulk"` label (different word order) maps to API code 6 = wood logs — a pre-existing oddity unexercised by any live fixture; left untouched per [[feedback-bigger-slices-for-uniform-work]] (different label, different fix). Cascade closure `pcdb 3` (Vokera Linea LPG combi 83.10 %, PCDB index 8262, no cylinder, 18-hour tariff) — EXACT on first try across all 4 metrics: cascade SAP_c = 49.2953 worksheet = 49.2953 Δ = +0.0000 cascade cost = £1165.81 worksheet = £1165.81 Δ = +0.0000 cascade CO2 = 3367.95 worksheet = 3367.95 Δ = +0.0000 cascade PE = 13936.60 worksheet = 13936.60 Δ = +0.0000 Closure on first try because the cascade was already fully wired for the gas/oil/LPG path; the Elmhurst label was the only gap. Moves pcdb 3 out of `_BLOCKED_BY_MISSING_MAIN_FUEL_TYPE` into `_EXPECTATIONS` at ±0.0000. Blocked tier now: 15 variants (community heating × 5, electric storage 11-14, no system, oil 2-6). Tests: - test_elmhurst_main_fuel_to_sap10_maps_bulk_lpg_to_api_code_27 - corpus pin: pcdb 3 expected residuals = ±0.0000 on all 4 metrics 912 pass / 0 fail; pyright net-zero 43 → 43. Co-Authored-By: Claude Opus 4.7 --- .../tests/test_heating_systems_corpus.py | 12 ++++++- datatypes/epc/domain/mapper.py | 9 +++++ .../rdsap/tests/test_cert_to_inputs.py | 33 +++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/backend/documents_parser/tests/test_heating_systems_corpus.py b/backend/documents_parser/tests/test_heating_systems_corpus.py index 0c02f665..ad1ba32a 100644 --- a/backend/documents_parser/tests/test_heating_systems_corpus.py +++ b/backend/documents_parser/tests/test_heating_systems_corpus.py @@ -438,6 +438,15 @@ _EXPECTATIONS: tuple[_CorpusExpectation, ...] = ( _CorpusExpectation(variant='solid fuel 9', block='11a', expected_sap_resid=-0.0000, expected_cost_resid_gbp=-0.0000, expected_co2_resid_kg=-0.0000, expected_pe_resid_kwh=+0.0000), _CorpusExpectation(variant='solid fuel 10', block='11a', expected_sap_resid=+0.0000, expected_cost_resid_gbp=-0.0000, expected_co2_resid_kg=-0.0000, expected_pe_resid_kwh=-0.0000), _CorpusExpectation(variant='solid fuel 11', block='11a', expected_sap_resid=+0.0000, expected_cost_resid_gbp=+0.0000, expected_co2_resid_kg=-0.0000, expected_pe_resid_kwh=-0.0000), + # Slice S0380.166 unblocked `pcdb 3` (PCDB 8262 Vokera Linea LPG combi + # 83.10 %, Bulk LPG fuel, no cylinder, 18-hour tariff) by adding + # `"Bulk LPG": 27` to `_ELMHURST_MAIN_FUEL_TO_SAP10` (API code 27 + # = "LPG (not community)" → Table 32 / Table 12 code 2 = bulk LPG). + # Pre-slice the cascade raised `MissingMainFuelType` because the + # mapper produced `main_fuel_type=''`. Post-slice all 4 metrics + # EXACT on first try — the cascade was fully wired for the gas/oil/ + # LPG path; only the Elmhurst label mapping was missing. + _CorpusExpectation(variant='pcdb 3', block='11a', expected_sap_resid=+0.0000, expected_cost_resid_gbp=+0.0000, expected_co2_resid_kg=+0.0000, expected_pe_resid_kwh=+0.0000), ) @@ -472,10 +481,11 @@ _BLOCKED_BY_MISSING_MAIN_FUEL_TYPE: tuple[str, ...] = ( 'oil 4', 'oil 5', 'oil 6', - 'pcdb 3', # Slice S0380.133 unblocked all 10 solid-fuel variants via the # §14.0 EES-code-driven fuel derivation; they now appear in # `_EXPECTATIONS` above with their post-derivation residual pins. + # Slice S0380.166 unblocked `pcdb 3` via `"Bulk LPG": 27` in the + # Elmhurst label dict; it now lives in `_EXPECTATIONS` at ±0.0000. ) diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index 937f8e62..eafcd5ee 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -3823,6 +3823,15 @@ _ELMHURST_MAIN_FUEL_TO_SAP10: Dict[str, int] = { # main_fuel row for "oil (not community)", which routes via # `API_FUEL_TO_TABLE_32` → Table 32 code 4 for cost / CO2 / PE. "Heating oil": 28, + # Elmhurst Summary §14.0 / §15.0 lodging form for SAP 10.2 Table 12 + # bulk LPG (£62 standing, 6.74 p/kWh, 0.241 kg CO2/kWh, 1.141 PE). + # 27 = epc_codes.csv main_fuel row for "LPG (not community)", which + # routes via `API_FUEL_TO_TABLE_32` / `API_FUEL_TO_TABLE_12` → fuel + # code 2 (bulk LPG) for cost / CO2 / PE. Distinct from the legacy + # "LPG bulk" label above (API code 6 = "wood logs" — same pre- + # existing oddity as "Oil" → 8; both labels are unused by any live + # fixture). Live form on Elmhurst worksheets is "Bulk LPG". + "Bulk LPG": 27, "Coal": 11, "Electricity": 30, "Electricity (off-peak 7hr)": 33, diff --git a/domain/sap10_calculator/rdsap/tests/test_cert_to_inputs.py b/domain/sap10_calculator/rdsap/tests/test_cert_to_inputs.py index a85e1edb..5631d3aa 100644 --- a/domain/sap10_calculator/rdsap/tests/test_cert_to_inputs.py +++ b/domain/sap10_calculator/rdsap/tests/test_cert_to_inputs.py @@ -1845,6 +1845,39 @@ def test_section_12_4_4_summer_immersion_applies_to_back_boiler_combos() -> None ) is False +def test_elmhurst_main_fuel_to_sap10_maps_bulk_lpg_to_api_code_27() -> None: + # Arrange — Elmhurst Summary §14.0 / §15.0 lodges "Bulk LPG" as the + # fuel type for PCDB LPG-combi certs (corpus variant pcdb 3 lodges + # PCDB index 8262 = Vokera Linea LPG, 18-hour tariff). Pre-slice + # `_ELMHURST_MAIN_FUEL_TO_SAP10` had no entry for "Bulk LPG" so the + # mapper produced `main_fuel_type=''` and the cascade strict-raised + # `MissingMainFuelType` per S0380.132. + # + # SAP 10.2 Table 12 (PDF p.189) bulk LPG = fuel code 2 (£62 standing, + # 6.74 p/kWh, 0.241 CO2, 1.141 PE). RdSAP10 Table 32 bulk LPG = + # £70 standing, 7.60 p/kWh. The cascade routes both via API code 27 + # ("LPG (not community)") through `API_FUEL_TO_TABLE_32[27] = 2` + # and `API_FUEL_TO_TABLE_12[27] = 2`. + # + # The legacy "LPG bulk" label (different word order) maps to API + # code 6 = wood logs in the same dict — a pre-existing oddity + # unexercised by any live fixture. Left untouched here per + # [[feedback-bigger-slices-for-uniform-work]] (different label, + # different fix). + + from datatypes.epc.domain.mapper import ( + _ELMHURST_MAIN_FUEL_TO_SAP10, # pyright: ignore[reportPrivateUsage] + _elmhurst_main_fuel_int, # pyright: ignore[reportPrivateUsage] + ) + + # Act + code = _elmhurst_main_fuel_int("Bulk LPG") + + # Assert + assert code == 27 + assert _ELMHURST_MAIN_FUEL_TO_SAP10["Bulk LPG"] == 27 + + def test_apply_water_efficiency_applies_interlock_penalty_after_equation_d1() -> None: # Arrange — SAP 10.2 §9.4.11 (PDF p.30) "Boiler interlock": "The # efficiency of gas and liquid fuel boilers for both space and water