Slice S0380.166: Elmhurst "Bulk LPG" label → API code 27 (mapper unblock)

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 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-02 09:48:37 +00:00 committed by Jun-te Kim
parent 549c0b2a39
commit 1491899412
3 changed files with 53 additions and 1 deletions

View file

@ -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.
)

View file

@ -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,

View file

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