mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice S0380.19: count Elmhurst shower outlets by type (no more hardcoded 1)
Surfaces the lodged shower multiplicity from the Elmhurst Summary §16
on the EPC. Previously `_map_elmhurst_sap_heating` hardcoded:
electric_shower_count = 1 if has_electric_shower else None
mixer_shower_count = 0 if has_electric_shower else None
losing the count for any cert with ≥ 2 outlets. Cert
7800-1501-0922-7127-3563 lodges TWO instantaneous electric showers
("Shower 01" + "Shower 11") but the mapper produced
`electric_shower_count=1`. After this slice:
electric_shower_count = Σ(s for s in showers if s.outlet_type
== "Electric shower")
mixer_shower_count = Σ(s for s in showers if s.outlet_type
!= "Electric shower")
**Cascade SAP effect:** None on cert 7800. Appendix J's eq J16
(`N_ES,per_outlet = N_shower / N_outlets`) and eq J18 (Σ_j E_ES,j)
are symmetric in N_electric_showers when there are no mixer outlets,
so the lodged (64a) kWh and (247a) cost are unchanged. The fix is
correctness-by-construction, not a delta-closer for the negative-band
certs (their +0.69 GBP total-cost gap traces to the gas hot-water
kWh path — separate slice).
**Hand-built fixture updates (5):** the cohort-1 hand-builts at
`domain/sap10_calculator/worksheet/tests/_elmhurst_worksheet_*.py`
previously omitted `electric_shower_count` / `mixer_shower_count`
(implicitly None), which matched the mapper's pre-slice None
sentinel. Updated each to the lodged counts the mapper now surfaces:
000474: 1 mixer → (0, 1)
000477: 1 mixer → (0, 1)
000480: 1 mixer → (0, 1)
000490: 1 mixer → (0, 1)
000516: 1 mixer → (0, 1)
000487 (already at (1, 0) for an electric-shower lodging) unchanged.
Tests:
- `test_summary_7800_two_electric_showers_count_as_two_not_one` —
pins the multi-shower mapping for cert 7800 (Summary_000890.pdf).
- 5 hand-built field-parity tests
(`test_from_elmhurst_site_notes_matches_hand_built_*`) now pass at
the new integer counts instead of None.
Pyright net-zero per file:
- datatypes/epc/domain/mapper.py: 32 (baseline 32)
- backend/documents_parser/tests/test_summary_pdf_mapper_chain.py: 0
Regression baseline: 699 pass + 10 fail (= prior 698 + 10 + 1 new).
Spec refs:
- SAP 10.2 Appendix J §1a — outlet counting drives `N_outlets` used
in eq J6/J7 (mixer shower water draw) and eq J16/J17/J18 (electric
shower energy).
- Cert 7800-1501-0922-7127-3563 Summary §16 "Showers" lodgement.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
57fbf83b1e
commit
1f8a070f66
8 changed files with 60 additions and 24 deletions
BIN
backend/documents_parser/tests/fixtures/Summary_000890.pdf
vendored
Normal file
BIN
backend/documents_parser/tests/fixtures/Summary_000890.pdf
vendored
Normal file
Binary file not shown.
|
|
@ -73,6 +73,7 @@ _SUMMARY_000902_PDF = _FIXTURES / "Summary_000902.pdf" # cert 9418
|
|||
_SUMMARY_000889_PDF = _FIXTURES / "Summary_000889.pdf" # cert 2536 (Normal cylinder)
|
||||
_SUMMARY_000884_PDF = _FIXTURES / "Summary_000884.pdf" # cert 9421 (Normal cylinder)
|
||||
_SUMMARY_000910_PDF = _FIXTURES / "Summary_000910.pdf" # cert 0036 (Flat, party wall U=0)
|
||||
_SUMMARY_000890_PDF = _FIXTURES / "Summary_000890.pdf" # cert 7800 (two electric showers)
|
||||
|
||||
# GOV.UK EPB API JSON for cert 001479 — the API-path counterpart of the
|
||||
# Summary_001479.pdf fixture. Together they drive the API ≡ Summary
|
||||
|
|
@ -945,6 +946,28 @@ def test_summary_mapper_raises_on_unmapped_glazing_type_label() -> None:
|
|||
assert excinfo.value.value == "Quintuple glazed with helium"
|
||||
|
||||
|
||||
def test_summary_7800_two_electric_showers_count_as_two_not_one() -> None:
|
||||
# Arrange — cert 7800-1501-0922-7127-3563's Summary §16 lodges TWO
|
||||
# instantaneous electric showers ("Shower 01" + "Shower 11", both
|
||||
# `outlet_type='Electric shower'`). Pre-Slice S0380.19 the mapper
|
||||
# hardcoded `electric_shower_count = 1 if has_electric_shower else
|
||||
# None`, losing the multiplicity. Cascade-equivalent on this cert:
|
||||
# Appendix J eq J16 (N_ES,per_outlet = N_shower / N_outlets) and
|
||||
# eq J18 (Σ_j E_ES,j) yield the same (64a) value for 1 vs 2 outlets
|
||||
# when there are no mixer outlets, so the SAP delta is unchanged
|
||||
# — but the lodged multiplicity is now surfaced for any future
|
||||
# cascade consumer that needs it.
|
||||
pages = _summary_pdf_to_textract_style_pages(_SUMMARY_000890_PDF)
|
||||
site_notes = ElmhurstSiteNotesExtractor(pages).extract()
|
||||
|
||||
# Act
|
||||
epc = EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes)
|
||||
|
||||
# Assert — both lodged electric showers surface on the EPC.
|
||||
assert epc.sap_heating.electric_shower_count == 2
|
||||
assert epc.sap_heating.mixer_shower_count == 0
|
||||
|
||||
|
||||
def test_summary_0036_flat_unknown_party_wall_routes_to_u_zero() -> None:
|
||||
# Arrange — cert 0036-6325-1100-0063-1226 is a "Flat, Mid-Terrace"
|
||||
# whose Summary lodges party_wall_type='U Unable to determine'.
|
||||
|
|
|
|||
|
|
@ -3564,12 +3564,19 @@ def _map_elmhurst_sap_heating(survey: ElmhurstSiteNotes) -> SapHeating:
|
|||
# Shower-outlet classification: SAP10.2 Appendix J routes electric
|
||||
# showers via §J line 64a (their own kWh stream) and treats mixer
|
||||
# showers as drawing from the HW system. The Summary PDF lodges
|
||||
# outlet_type as 'Electric shower' or 'Non-electric shower' — set
|
||||
# the explicit counts so the cascade doesn't default mixer=1 on
|
||||
# electric-only dwellings (000487).
|
||||
has_electric_shower = any(
|
||||
s.outlet_type == "Electric shower"
|
||||
for s in survey.baths_and_showers.showers
|
||||
# outlet_type as 'Electric shower' or 'Non-electric shower' — count
|
||||
# each outlet by type so the cascade sees the actual lodged number
|
||||
# (cert 7800-1501-0922-7127-3563 has 2 electric showers; the
|
||||
# previous hardcoded `1 if has_electric_shower else None` lost the
|
||||
# multiplicity, and `None` left the cascade defaulting to mixer=1
|
||||
# on electric-only dwellings).
|
||||
electric_shower_count_from_survey = sum(
|
||||
1 for s in survey.baths_and_showers.showers
|
||||
if s.outlet_type == "Electric shower"
|
||||
)
|
||||
mixer_shower_count_from_survey = sum(
|
||||
1 for s in survey.baths_and_showers.showers
|
||||
if s.outlet_type != "Electric shower"
|
||||
)
|
||||
# Water heating fuel: Summary §15 "Water Heating Fuel Type" lodges
|
||||
# the fuel name as a string ("Mains gas", "Electricity", ...). Map
|
||||
|
|
@ -3635,24 +3642,12 @@ def _map_elmhurst_sap_heating(survey: ElmhurstSiteNotes) -> SapHeating:
|
|||
mh.secondary_heating_sap_code,
|
||||
),
|
||||
number_baths=survey.baths_and_showers.number_of_baths,
|
||||
# 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)
|
||||
),
|
||||
# Both counts derived by tallying the lodged shower list. Zero-
|
||||
# shower lodgings resolve to (0, 0) — the API path's slice 102f-
|
||||
# prep.8 disposition for cert 2225. Multi-shower lodgings surface
|
||||
# the lodged multiplicity (cert 7800: 2 electric showers).
|
||||
electric_shower_count=electric_shower_count_from_survey,
|
||||
mixer_shower_count=mixer_shower_count_from_survey,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -221,6 +221,12 @@ def build_epc() -> EpcPropertyData:
|
|||
epc.sap_heating.shower_outlets = ShowerOutlets(
|
||||
shower_outlet=ShowerOutlet(shower_outlet_type="Non-electric shower"),
|
||||
)
|
||||
# Slice S0380.19: Elmhurst mapper now counts shower outlets by
|
||||
# type (electric vs mixer) instead of the previous hardcoded
|
||||
# 0/1/None sentinels. Cohort cert 000474 lodges 1 non-electric
|
||||
# (mixer) outlet → electric=0, mixer=1.
|
||||
epc.sap_heating.electric_shower_count = 0
|
||||
epc.sap_heating.mixer_shower_count = 1
|
||||
# Summary §14 "Heat pump age: Unknown" — surfaced by the Elmhurst
|
||||
# mapper as the str dual-encoding that internal_gains.py reads.
|
||||
# `make_main_heating_detail` doesn't expose the str kwarg, so set
|
||||
|
|
|
|||
|
|
@ -188,6 +188,9 @@ def build_epc() -> EpcPropertyData:
|
|||
epc.sap_heating.shower_outlets = ShowerOutlets(
|
||||
shower_outlet=ShowerOutlet(shower_outlet_type="Non-electric shower"),
|
||||
)
|
||||
# Slice S0380.19: counted shower outlets (was: None/None sentinels).
|
||||
epc.sap_heating.electric_shower_count = 0
|
||||
epc.sap_heating.mixer_shower_count = 1
|
||||
# Summary §14 "Heat pump age: Unknown" — surfaced by the Elmhurst
|
||||
# mapper as the str dual-encoding that internal_gains.py reads.
|
||||
# `make_main_heating_detail` doesn't expose the str kwarg, so set
|
||||
|
|
|
|||
|
|
@ -238,6 +238,9 @@ def build_epc() -> EpcPropertyData:
|
|||
epc.sap_heating.shower_outlets = ShowerOutlets(
|
||||
shower_outlet=ShowerOutlet(shower_outlet_type="Non-electric shower"),
|
||||
)
|
||||
# Slice S0380.19: counted shower outlets (was: None/None sentinels).
|
||||
epc.sap_heating.electric_shower_count = 0
|
||||
epc.sap_heating.mixer_shower_count = 1
|
||||
# Summary §14 "Heat pump age: Unknown" — surfaced by the Elmhurst
|
||||
# mapper as the str dual-encoding that internal_gains.py reads.
|
||||
epc.sap_heating.main_heating_details[0].central_heating_pump_age_str = "Unknown"
|
||||
|
|
|
|||
|
|
@ -190,6 +190,9 @@ def build_epc() -> EpcPropertyData:
|
|||
epc.sap_heating.shower_outlets = ShowerOutlets(
|
||||
shower_outlet=ShowerOutlet(shower_outlet_type="Non-electric shower"),
|
||||
)
|
||||
# Slice S0380.19: counted shower outlets (was: None/None sentinels).
|
||||
epc.sap_heating.electric_shower_count = 0
|
||||
epc.sap_heating.mixer_shower_count = 1
|
||||
epc.sap_heating.main_heating_details[0].central_heating_pump_age_str = "Unknown"
|
||||
return epc
|
||||
|
||||
|
|
|
|||
|
|
@ -214,6 +214,9 @@ def build_epc() -> EpcPropertyData:
|
|||
epc.sap_heating.shower_outlets = ShowerOutlets(
|
||||
shower_outlet=ShowerOutlet(shower_outlet_type="Non-electric shower"),
|
||||
)
|
||||
# Slice S0380.19: counted shower outlets (was: None/None sentinels).
|
||||
epc.sap_heating.electric_shower_count = 0
|
||||
epc.sap_heating.mixer_shower_count = 1
|
||||
epc.sap_heating.main_heating_details[0].central_heating_pump_age_str = "Unknown"
|
||||
return epc
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue