Resolve the PasHub Cylinder Size label to the SAP10 Table 28 enum 🟩

"Normal (90-130 litres)"->2, "Large (>170 litres)"->4; "No Access" follows
RdSAP 10 Table 28 p.55 (otherwise-branch 110L for a main-fed cylinder;
electric-immersion No Access strict-raises until meter context is plumbed).
The raw band string was int-or-noned by _cylinder_volume_l_from_code, skipping
the Table 28 volume convention (issue #1590 bug 6). Golden block updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-07-14 21:52:32 +00:00
parent 2949460e6a
commit de15bd1ed7
2 changed files with 44 additions and 2 deletions

View file

@ -6128,7 +6128,10 @@ def _map_sap_heating(
secondary_heating_type=heating.secondary_heating.secondary_system,
shower_outlets=shower_outlets,
cylinder_size=(
heating.water_heating.cylinder_size
_pashub_cylinder_size_code(
heating.water_heating.cylinder_size,
heating.water_heating.system,
)
if cylinder_present
else None
),
@ -6172,6 +6175,45 @@ def _pashub_mechanical_ventilation_kind(
return _PASHUB_VENTILATION_TYPE_TO_KIND[ventilation_type]
# PasHub surveyed §Water-Heating "Cylinder Size" band → SAP10 cylinder-size
# enum (Table 28: 2=Normal/110L, 3=Medium/160L, 4=Large/210L). PasHub renders
# the band with its own litre-range suffix (unlike the Elmhurst forms in
# `_ELMHURST_CYLINDER_SIZE_LABEL_TO_SAP10`); the raw string was int-or-noned by
# `_cylinder_volume_l_from_code`, silently skipping the Table 28 volume
# convention (issue #1590 bug 6).
_PASHUB_CYLINDER_SIZE_TO_SAP10: Dict[str, int] = {
"Normal (90-130 litres)": 2,
"Large (>170 litres)": 4,
}
def _pashub_cylinder_size_code(
size_label: Optional[str], system_label: str
) -> Optional[int]:
"""Resolve a PasHub surveyed Cylinder Size band to the SAP10 enum at the
mapper boundary (ADR-0015). "No Access" follows RdSAP 10 Table 28 (p.55):
off-peak dual electric immersion 210 L, solid-fuel boiler 160 L,
otherwise 110 L (code 2) the cohort's case is a cylinder fed from the
gas main. An electric-immersion "No Access" needs the meter-type context
this path doesn't yet plumb, so it strict-raises (loud gap, mirroring the
cohort-scoped control approach) rather than silently taking the 110 L
branch; any other uncovered non-empty label strict-raises too."""
if not size_label or size_label == "No Cylinder":
return None
if size_label == "No Access":
if system_label == "Electric immersion":
raise UnmappedPasHubLabel(
"cylinder size",
"'No Access' on an electric immersion needs meter-type "
"context (RdSAP 10 Table 28 210L branch) — not yet mapped",
)
return 2 # Table 28 "otherwise": 110 L
code = _PASHUB_CYLINDER_SIZE_TO_SAP10.get(size_label)
if code is None:
raise UnmappedPasHubLabel("cylinder size", size_label)
return code
def _map_sap_ventilation(ventilation: Ventilation) -> SapVentilation:
return SapVentilation(
ventilation_type=ventilation.ventilation_type,

View file

@ -1038,7 +1038,7 @@ class TestFromSiteNotesExample1:
)
],
has_fixed_air_conditioning=False,
cylinder_size="Normal (90-130 litres)",
cylinder_size=2, # "Normal (90-130 litres)" → Table 28 code 2
cylinder_insulation_type="Factory fitted",
cylinder_insulation_thickness_mm=12,
water_heating_code=901, # cylinder + "From main heating 1" → WHC 901