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