Slice S0380.11: resolve zero-shower lodgings to count=0 (closes cert 2225)

Cert 2225-3062-8205-2856-7204 lodges **zero showers** in its Summary
§1x Baths and Showers block. The Summary mapper at
`mapper.py:3536-3537` predicated the shower-count assignment on
`has_electric_shower`: for cohort certs with no electric shower the
counts collapsed to None — but cert 2225 has no showers at all, and
the cascade's None-handling defaults to 1 mixer shower (over-counting
HW kWh by ~66 against the worksheet (64)/(216) target).

Same disposition the API path received in slice 102f-prep.8 (commit
1d5183c6, "API mapper resolves shower_outlets=None → 0 mixers") —
extending it to the Summary mapper.

Scope-limited fix: zero-shower lodgings resolve to **explicit 0**
counts (not None) so the cascade does not default-assume a mixer.
Non-zero shower lodgings keep their existing convention (None for
non-electric → cascade derives count from `shower_outlets`) so the 5
boiler-cohort hand-built parity tests
(`test_from_elmhurst_site_notes_matches_hand_built_*`) stay GREEN.

Forcing function: cert 2225 first-attempt Summary SAP closes from
Δ -0.3079 to Δ **+0.0441** — within the ±0.07 ASHP-cohort spec floor.

Cohort closure status (5 of 7 ASHP certs now at spec floor):
  cert  Δ vs worksheet  spec floor?
  0380  +0.0594         ✓
  0350  +0.0458         ✓
  2225  +0.0441         ✓  ← this slice
  2636  +0.4873         ✗  (cantilever + alt-wall; next slice)
  3800  +0.0442         ✓
  9285  +0.0502         ✓
  9418  +2.5973         ✗  (Daikin EDLQ05CAV3, distinct PCDB)

Added two tests:
- `test_summary_2225_no_showers_lodged_resolves_to_zero_counts` —
  unit-level pin that no-shower lodgings produce explicit 0 counts.
- `test_summary_2225_full_chain_sap_within_spec_floor_of_worksheet`
  — Layer-4 chain test at ±0.07.

Pyright net-zero on both edited files (mapper.py 32 baseline).

Regression suite: 682 pass + 10 fail (handover baseline 669 + 10 +
13 new GREEN tests across S0380.2..S0380.11). The 5 boiler hand-
built parity tests confirmed still GREEN — the refinement
deliberately preserves their convention by only flipping the zero-
shower case.

Spec refs:
- Slice 102f-prep.8 (commit 1d5183c6) — API-path precedent.
- SAP 10.2 Appendix J — shower energy accounting (electric vs mixer
  routing); mixer showers draw from the HW system and contribute to
  HW kWh; electric showers are §J line 64a (separate energy stream).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-27 21:04:55 +00:00 committed by Jun-te Kim
parent 11e0279dce
commit 29cfdf6461
2 changed files with 68 additions and 2 deletions

View file

@ -62,6 +62,7 @@ _SUMMARY_000899_PDF = _FIXTURES / "Summary_000899.pdf"
_SUMMARY_000903_PDF = _FIXTURES / "Summary_000903.pdf"
_SUMMARY_000901_PDF = _FIXTURES / "Summary_000901.pdf" # cert 3800
_SUMMARY_000904_PDF = _FIXTURES / "Summary_000904.pdf" # cert 9285
_SUMMARY_000900_PDF = _FIXTURES / "Summary_000900.pdf" # cert 2225
# GOV.UK EPB API JSON for cert 001479 — the API-path counterpart of the
# Summary_001479.pdf fixture. Together they drive the API ≡ Summary
@ -713,6 +714,55 @@ def test_summary_0350_full_chain_sap_within_spec_floor_of_worksheet() -> None:
assert abs(result.sap_score_continuous - worksheet_unrounded_sap) < _ASHP_COHORT_CHAIN_TOLERANCE
def test_summary_2225_no_showers_lodged_resolves_to_zero_counts() -> None:
# Arrange — cert 2225-3062-8205-2856-7204's Summary §1x Baths and
# Showers block lodges 0 baths and ZERO showers (no shower rows at
# all). The Summary mapper's existing logic at
# `mapper.py:3536-3537` predicates the count assignment on
# `has_electric_shower`: when no electric shower is detected the
# counts collapse to None — but cert 2225 has no showers at all,
# not "non-electric showers". The None values then drive the
# cascade's default-1-mixer assumption, over-counting HW kWh.
# Same disposition the API path received in slice 102f-prep.8
# (commit 1d5183c6: "API mapper resolves shower_outlets=None →
# 0 mixers").
pages = _summary_pdf_to_textract_style_pages(_SUMMARY_000900_PDF)
site_notes = ElmhurstSiteNotesExtractor(pages).extract()
# Pre-condition: §1x lodges zero showers (proves the test sees
# the same no-showers fixture the cascade does).
assert len(site_notes.baths_and_showers.showers) == 0
# Act
epc = EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes)
# Assert — zero-shower lodgings resolve to explicit 0 counts (not
# None) so the cascade does not default-assume a mixer.
assert epc.sap_heating.electric_shower_count == 0
assert epc.sap_heating.mixer_shower_count == 0
def test_summary_2225_full_chain_sap_within_spec_floor_of_worksheet() -> None:
# Arrange — cert 2225-3062-8205-2856-7204 (Summary_000900.pdf):
# Mitsubishi PUZ-WM50VHA, single-bp single-array PV (3.28 kWp SE),
# ZERO showers lodged. Worksheet "SAP value" 88.7921. Slice
# S0380.11 closed the zero-shower defaulting bug (None → 0 mixers
# for cohort certs that lodge no showers); cert 2225 was the
# forcing function. Same disposition the API path received in
# slice 102f-prep.8 (commit 1d5183c6).
pages = _summary_pdf_to_textract_style_pages(_SUMMARY_000900_PDF)
site_notes = ElmhurstSiteNotesExtractor(pages).extract()
epc = EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes)
# Act
result = calculate_sap_from_inputs(
cert_to_inputs(epc, prices=SAP_10_2_SPEC_PRICES)
)
# Assert — ±0.07 ASHP-cohort spec-floor tolerance.
worksheet_unrounded_sap = 88.7921
assert abs(result.sap_score_continuous - worksheet_unrounded_sap) < _ASHP_COHORT_CHAIN_TOLERANCE
def test_summary_3800_full_chain_sap_within_spec_floor_of_worksheet() -> None:
# Arrange — cert 3800-8515-0922-3398-3563 (Summary_000901.pdf /
# dr87-0001-000901.pdf) is the third ASHP cohort cert to close on

View file

@ -3533,8 +3533,24 @@ def _map_elmhurst_sap_heating(survey: ElmhurstSiteNotes) -> SapHeating:
mh.secondary_heating_sap_code,
),
number_baths=survey.baths_and_showers.number_of_baths,
electric_shower_count=1 if has_electric_shower else None,
mixer_shower_count=0 if has_electric_shower else None,
# Zero-shower lodgings resolve to explicit 0 (not None) so the
# cascade doesn't default-assume a mixer — same disposition
# the API path received in slice 102f-prep.8 ("API mapper
# resolves shower_outlets=None → 0 mixers") on cohort cert
# 2225. Non-zero shower lodgings keep the hand-built-fixture
# convention (None for non-electric → cascade derives count
# from `shower_outlets` instead) so the boiler-cohort parity
# tests in this file stay GREEN.
electric_shower_count=(
0
if not survey.baths_and_showers.showers
else (1 if has_electric_shower else None)
),
mixer_shower_count=(
0
if not survey.baths_and_showers.showers
else (0 if has_electric_shower else None)
),
)