mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-27 23:35:01 +00:00
Map PasHub water-heating fields for cylinder dwellings
`_map_sap_heating` set cylinder geometry/insulation but left `water_heating_code` / `water_heating_fuel` / `immersion_heating_type` None (and immersion as a raw label). For cylinder dwellings that loses the HW-system typing the calculator's water cascade keys off. Resolve them at the mapper boundary, gated on cylinder presence (combi dwellings keep the None default so HW inherits the main system, unchanged): - `water_heating_code` from the `system` label — "From main heating 1" -> 901, "Electric immersion" -> 903, community -> 950; strict-raises an unknown label. - `water_heating_fuel` = standard electricity (30) for the electric-immersion WHC 903; None otherwise (901 inherits the main fuel). - `immersion_heating_type` via the Elmhurst map (Single -> 2), cylinder-gated. Combi-dominated cohort so the accuracy impact is negligible by design (Tier 3): within-0.5 and MAE unchanged (12.9% / 2.56), harness still 206-xfail with no new hard failures. Closes #1565 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f7e6bf1184
commit
597ca51dd9
2 changed files with 120 additions and 2 deletions
|
|
@ -6071,6 +6071,20 @@ def _map_sap_heating(
|
|||
)
|
||||
)
|
||||
|
||||
# Water heating is only typed for cylinder dwellings; a combi leaves the WHC
|
||||
# None so HW inherits the main system (issue #1565).
|
||||
cylinder_present = heating.water_heating.cylinder_size != "No Cylinder"
|
||||
water_heating_code = (
|
||||
_pashub_water_heating_code(heating.water_heating.system)
|
||||
if cylinder_present
|
||||
else None
|
||||
)
|
||||
water_heating_fuel = (
|
||||
_PASHUB_STANDARD_ELECTRICITY_FUEL
|
||||
if water_heating_code == _PASHUB_WHC_ELECTRIC_IMMERSION
|
||||
else None
|
||||
)
|
||||
|
||||
return SapHeating(
|
||||
instantaneous_wwhrs=InstantaneousWwhrs(),
|
||||
main_heating_details=[
|
||||
|
|
@ -6092,12 +6106,16 @@ def _map_sap_heating(
|
|||
shower_outlets=shower_outlets,
|
||||
cylinder_size=(
|
||||
heating.water_heating.cylinder_size
|
||||
if heating.water_heating.cylinder_size != "No Cylinder"
|
||||
if cylinder_present
|
||||
else None
|
||||
),
|
||||
cylinder_insulation_type=heating.water_heating.insulation_type,
|
||||
cylinder_insulation_thickness_mm=heating.water_heating.insulation_thickness_mm,
|
||||
immersion_heating_type=heating.water_heating.immersion_type,
|
||||
water_heating_code=water_heating_code,
|
||||
water_heating_fuel=water_heating_fuel,
|
||||
immersion_heating_type=_pashub_immersion_type_code(
|
||||
heating.water_heating.immersion_type, cylinder_present
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -7259,6 +7277,48 @@ def _pashub_glazing_type_int(label: Optional[str]) -> Union[int, str]:
|
|||
return _PASHUB_GLAZING_TYPE_TO_SAP10[key]
|
||||
|
||||
|
||||
# PasHub water-heating `system` label → SAP10 water-heating code (WHC) that the
|
||||
# calculator's water cascade keys off (cert_to_inputs: `_WHC_FROM_MAIN_HEATING`
|
||||
# 901 inherits the main fuel; `_WHC_ELECTRIC_IMMERSION` 903; heat-network water
|
||||
# 950). Only lodged for cylinder dwellings — combi dwellings leave it None and HW
|
||||
# inherits from the main system (issue #1565).
|
||||
_PASHUB_WATER_HEATING_SYSTEM_TO_SAP10: Dict[str, int] = {
|
||||
"From main heating 1": 901,
|
||||
"Electric immersion": 903,
|
||||
"Hot water only community scheme - boilers": 950,
|
||||
}
|
||||
_PASHUB_WHC_ELECTRIC_IMMERSION: Final[int] = 903
|
||||
_PASHUB_STANDARD_ELECTRICITY_FUEL: Final[int] = 30 # Table 32 standard electricity
|
||||
|
||||
|
||||
def _pashub_water_heating_code(system_label: Optional[str]) -> Optional[int]:
|
||||
"""Resolve a PasHub water-heating `system` label to its SAP10 WHC code. Blank
|
||||
is "no lodging" (None); an unmapped label strict-raises `UnmappedPasHubLabel`.
|
||||
Call only for cylinder dwellings (#1565)."""
|
||||
if system_label is None or not system_label.strip():
|
||||
return None
|
||||
key = system_label.strip()
|
||||
if key not in _PASHUB_WATER_HEATING_SYSTEM_TO_SAP10:
|
||||
raise UnmappedPasHubLabel("water heating system", system_label)
|
||||
return _PASHUB_WATER_HEATING_SYSTEM_TO_SAP10[key]
|
||||
|
||||
|
||||
def _pashub_immersion_type_code(
|
||||
immersion_label: Optional[str], cylinder_present: bool
|
||||
) -> Optional[int]:
|
||||
"""Resolve a PasHub `immersion_type` label to the SAP10 `immersion_heating_type`
|
||||
code, reusing the Elmhurst map (1 = dual, 2 = single, RdSAP 10 §10.5). None
|
||||
when no cylinder or no immersion lodged; unmapped label strict-raises."""
|
||||
if not cylinder_present or immersion_label is None:
|
||||
return None
|
||||
key = immersion_label.strip()
|
||||
if key in ("", "None"):
|
||||
return None
|
||||
if key not in _ELMHURST_IMMERSION_TYPE_LABEL_TO_SAP10:
|
||||
raise UnmappedPasHubLabel("immersion type", immersion_label)
|
||||
return _ELMHURST_IMMERSION_TYPE_LABEL_TO_SAP10[key]
|
||||
|
||||
|
||||
def _resolve_elmhurst_underfloor_subtype(
|
||||
main_floor: ElmhurstFloorDetails,
|
||||
main_age_band: str,
|
||||
|
|
|
|||
|
|
@ -188,6 +188,63 @@ class TestGlazingTypeCoding:
|
|||
EpcPropertyDataMapper.from_site_notes(survey)
|
||||
|
||||
|
||||
class TestWaterHeatingCoding:
|
||||
"""`from_site_notes` must int-code the PAS Hub water-heating fields for
|
||||
**cylinder** dwellings — `water_heating_code` (WHC), `water_heating_fuel`,
|
||||
`immersion_heating_type` — not leave them None/raw. Combi dwellings (no
|
||||
cylinder) keep the None default (HW inherits from the main system), so they
|
||||
are untouched (issue #1565). example1 is a cylinder dwelling.
|
||||
"""
|
||||
|
||||
def _survey(self, **water: object) -> PasHubRdSapSiteNotes:
|
||||
data = load("pashub_rdsap_site_notes_example1.json")
|
||||
data["heating_and_hot_water"]["water_heating"].update(water)
|
||||
return from_dict(PasHubRdSapSiteNotes, data)
|
||||
|
||||
def test_cylinder_from_main_maps_whc_901(self) -> None:
|
||||
result = EpcPropertyDataMapper.from_site_notes(
|
||||
self._survey(system="From main heating 1", cylinder_size="Normal (90-130 litres)")
|
||||
)
|
||||
assert result.sap_heating.water_heating_code == 901
|
||||
assert result.sap_heating.water_heating_fuel is None
|
||||
|
||||
def test_electric_immersion_maps_903_electric_fuel_and_single_immersion(self) -> None:
|
||||
result = EpcPropertyDataMapper.from_site_notes(
|
||||
self._survey(
|
||||
system="Electric immersion",
|
||||
cylinder_size="Normal (90-130 litres)",
|
||||
immersion_type="Single",
|
||||
)
|
||||
)
|
||||
assert result.sap_heating.water_heating_code == 903
|
||||
assert result.sap_heating.water_heating_fuel == 30 # standard electricity
|
||||
assert result.sap_heating.immersion_heating_type == 2 # single
|
||||
|
||||
def test_community_scheme_maps_950(self) -> None:
|
||||
result = EpcPropertyDataMapper.from_site_notes(
|
||||
self._survey(
|
||||
system="Hot water only community scheme - boilers",
|
||||
cylinder_size="Normal (90-130 litres)",
|
||||
)
|
||||
)
|
||||
assert result.sap_heating.water_heating_code == 950
|
||||
|
||||
def test_combi_leaves_whc_none(self) -> None:
|
||||
result = EpcPropertyDataMapper.from_site_notes(
|
||||
self._survey(system="From main heating 1", cylinder_size="No Cylinder")
|
||||
)
|
||||
assert result.sap_heating.water_heating_code is None
|
||||
|
||||
def test_unknown_cylinder_system_strict_raises(self) -> None:
|
||||
with pytest.raises(UnmappedPasHubLabel):
|
||||
EpcPropertyDataMapper.from_site_notes(
|
||||
self._survey(
|
||||
system="Perpetual motion boiler",
|
||||
cylinder_size="Normal (90-130 litres)",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class TestFromSiteNotesExample1:
|
||||
"""
|
||||
Fixture: pashub_rdsap_site_notes_example1.json
|
||||
|
|
@ -546,6 +603,7 @@ class TestFromSiteNotesExample1:
|
|||
cylinder_size="Normal (90-130 litres)",
|
||||
cylinder_insulation_type="Factory fitted",
|
||||
cylinder_insulation_thickness_mm=12,
|
||||
water_heating_code=901, # cylinder + "From main heating 1" → WHC 901
|
||||
shower_outlets=ShowerOutlets(
|
||||
shower_outlet=ShowerOutlet(
|
||||
shower_outlet_type="Non-Electric Shower"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue