Normalize RdSAP hot-water demand (WWHRS rooms) on the degraded 17.0/19.0 paths

PRD #1385, shower/WWHRS family. The 17.0 and 19.0 API mappers left
sap_heating.number_baths / mixer_shower_count as None, so the water-heating
demand (cert_to_inputs) fell back to defaults instead of the lodged bath/shower
room counts. Derive them from schema.sap_heating.instantaneous_wwhrs, mirroring
the 17.1/18.0/20.0 paths:

  number_baths       = rooms_with_bath_and_or_shower + rooms_with_bath_and_mixer_shower
  mixer_shower_count = rooms_with_mixer_shower_no_bath + rooms_with_bath_and_mixer_shower

Guarded on instantaneous_wwhrs is not None (Optional in the schema), like 17.1.
Counts, same meaning across the reduced-field family.

Verification: new test 2 passed; test_mapper_corpus 6002 passed; SAP-accuracy /
RealCertExpectation regressions pass; pyright unchanged (39 -> 39, pre-existing).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jun-te Kim 2026-07-01 12:52:33 +00:00
parent 9db8b60a54
commit 28b72fc67c
2 changed files with 79 additions and 0 deletions

View file

@ -620,6 +620,22 @@ class EpcPropertyDataMapper:
),
sap_heating=SapHeating(
instantaneous_wwhrs=InstantaneousWwhrs(),
# ADR-0028: derive HW-demand counts from the lodged bath/shower
# ROOM counts, mirroring the 17.1/18.0/20.0 paths. These were
# dropped (left None), so the water-heating demand fell back to
# defaults instead of the lodged bath/mixer-shower counts.
number_baths=(
schema.sap_heating.instantaneous_wwhrs.rooms_with_bath_and_or_shower
+ schema.sap_heating.instantaneous_wwhrs.rooms_with_bath_and_mixer_shower
if schema.sap_heating.instantaneous_wwhrs is not None
else None
),
mixer_shower_count=(
schema.sap_heating.instantaneous_wwhrs.rooms_with_mixer_shower_no_bath
+ schema.sap_heating.instantaneous_wwhrs.rooms_with_bath_and_mixer_shower
if schema.sap_heating.instantaneous_wwhrs is not None
else None
),
main_heating_details=[
MainHeatingDetail(
has_fghrs=d.has_fghrs == "Y",
@ -1602,6 +1618,22 @@ class EpcPropertyDataMapper:
),
sap_heating=SapHeating(
instantaneous_wwhrs=InstantaneousWwhrs(),
# ADR-0028: derive HW-demand counts from the lodged bath/shower
# ROOM counts, mirroring the 17.1/18.0/20.0 paths. These were
# dropped (left None), so the water-heating demand fell back to
# defaults instead of the lodged bath/mixer-shower counts.
number_baths=(
schema.sap_heating.instantaneous_wwhrs.rooms_with_bath_and_or_shower
+ schema.sap_heating.instantaneous_wwhrs.rooms_with_bath_and_mixer_shower
if schema.sap_heating.instantaneous_wwhrs is not None
else None
),
mixer_shower_count=(
schema.sap_heating.instantaneous_wwhrs.rooms_with_mixer_shower_no_bath
+ schema.sap_heating.instantaneous_wwhrs.rooms_with_bath_and_mixer_shower
if schema.sap_heating.instantaneous_wwhrs is not None
else None
),
main_heating_details=[
MainHeatingDetail(
has_fghrs=d.has_fghrs == "Y",

View file

@ -0,0 +1,47 @@
"""Regression: the degraded RdSAP paths (17.0, 19.0) must derive the hot-water
bath/shower demand counts from the lodged WWHRS room counts, like the
reference-complete 17.1/18.0/20.0 paths.
PRD #1385 (mapper normalization), shower/WWHRS family. Both paths left
`sap_heating.number_baths` / `mixer_shower_count` as None, so the water-heating
demand (`cert_to_inputs`) fell back to defaults. The reference derivation:
number_baths = rooms_with_bath_and_or_shower + rooms_with_bath_and_mixer_shower
mixer_shower_count = rooms_with_mixer_shower_no_bath + rooms_with_bath_and_mixer_shower
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
from datatypes.epc.domain.mapper import EpcPropertyDataMapper
# 17.0: rooms bath_or_shower=4, mixer_no_bath=0, bath_and_mixer=4
# → number_baths 8, mixer_shower_count 4.
_FIXTURE_17_0 = Path("tests/fixtures/epc_prediction/W104HL/cert-f96b9b6f471b.json")
# 19.0: rooms bath_or_shower=2, mixer_no_bath=1, bath_and_mixer=1
# → number_baths 3, mixer_shower_count 2.
_FIXTURE_19_0 = Path("tests/fixtures/epc_prediction/LE113PX/cert-ba628e4f9cbf.json")
def _load(path: Path, schema_type: str) -> dict[str, Any]:
data: dict[str, Any] = json.loads(path.read_text())
assert data["schema_type"] == schema_type
return data
def test_17_0_derives_hot_water_demand_from_wwhrs_rooms() -> None:
epc = EpcPropertyDataMapper.from_api_response(
_load(_FIXTURE_17_0, "RdSAP-Schema-17.0")
)
assert epc.sap_heating.number_baths == 8
assert epc.sap_heating.mixer_shower_count == 4
def test_19_0_derives_hot_water_demand_from_wwhrs_rooms() -> None:
epc = EpcPropertyDataMapper.from_api_response(
_load(_FIXTURE_19_0, "RdSAP-Schema-19.0")
)
assert epc.sap_heating.number_baths == 3
assert epc.sap_heating.mixer_shower_count == 2