From ca56fdee5bdea38563ab5380c2ec3f86ba49d06d Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Sat, 23 May 2026 22:44:40 +0000 Subject: [PATCH] =?UTF-8?q?Slice=2025b:=20000487=20=C2=A74=20closure=20(7/?= =?UTF-8?q?8)=20=E2=80=94=20has=5Felectric=5Fshower=20routes=20Nbath?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes §4 LINE_43 + LINE_44/45/46/61/62/64 for 000487 (7 of 8 fails). LINE_65 still fails — needs Appendix J step 8 (electric-shower kWh derivation from cert) to land before LINE_65 heat gains close. Spec citation: SAP10.2 Appendix J (p.81) step 2a: `Nbath = 0.13N + 0.19 if shower also present; = 0.35N + 0.50 if no shower present`. The "shower also present" branch fires when ANY shower is lodged — mixer OR electric — per the implicit reading that step 1a's Noutlets includes electric showers in the count. Changes: - SapHeating gains `electric_shower_count` + `mixer_shower_count`. - `water_heating_from_cert` gains `has_electric_shower: bool = False`; combined with mixer-flow-rate presence to drive `has_shower`. - `_mixer_shower_flow_rates_from_cert` honors `mixer_shower_count` (default 1 vented when unlodged — preserves legacy behaviour). - `_has_electric_shower_from_cert` new helper. - `water_heating_section_from_cert` plumbs `has_electric_shower` through bootstrap + final call (and the internal cert_to_inputs path). - 000487 fixture: `electric_shower_count=1, mixer_shower_count=0`. §4 per-fixture: fixture | LINE_42 | LINE_43 | LINE_44-46 | LINE_61-65 000474 | ✓ | ✓ | ✓ | ✓ (9/9) 000477 | ✓ | ✓ | ✓ | ✗ LINE_61/62/64/65 (slice 25c) 000480 | ✓ | ✓ | ✓ | ✓ (9/9) 000487 | ✓ | ✓ | ✓ | ✓ except LINE_65 (8/9) 000490 | ✓ | ✓ | ✓ | ✓ (9/9) 000516 | ✓ | ✓ | ✓ | ✓ (9/9) Scoreboard: section_cascade_pins: 279 → 286 PASS (+7) e2e SapResult: 32 → 32 PASS (unchanged — LINE_65 cascade still open, blocks downstream §5 LINE_72/73 + §6 LINE_84 + §7 + downstream) Co-Authored-By: Claude Opus 4.7 --- datatypes/epc/domain/epc_property_data.py | 12 ++++++ docs/sap-spec/HANDOVER_NEXT.md | 18 +++++---- .../domain/src/domain/ml/tests/_fixtures.py | 4 ++ .../src/domain/sap/rdsap/cert_to_inputs.py | 38 ++++++++++++++----- .../tests/_elmhurst_worksheet_000487.py | 6 +++ .../src/domain/sap/worksheet/water_heating.py | 2 + 6 files changed, 63 insertions(+), 17 deletions(-) diff --git a/datatypes/epc/domain/epc_property_data.py b/datatypes/epc/domain/epc_property_data.py index 114c1d02..d18b2341 100644 --- a/datatypes/epc/domain/epc_property_data.py +++ b/datatypes/epc/domain/epc_property_data.py @@ -141,6 +141,18 @@ class SapHeating: # SAP10 hot-water demand inputs from sap_heating. number_baths: Optional[int] = None number_baths_wwhrs: Optional[int] = None + # Per SAP10.2 Appendix J (p.81) step 1a: Noutlets includes electric + # showers in the count for Nshower; step 2a routes Nbath through the + # "shower also present" branch (0.13N + 0.19) when ANY shower is + # lodged — including electric. Modelled separately from mixer outlets + # because electric showers don't draw warm water from the system. + electric_shower_count: Optional[int] = None + # PCDF mixer-shower lodgement (count of outlets that DO draw warm + # water from the main HW system). When set, overrides the heuristic + # default of 1 vented outlet @ 7 L/min used by `_mixer_shower_flow_ + # rates_from_cert`. Most certs lodge only count; the standard + # vented-system flow rate from Table J4 (7 L/min) is the default. + mixer_shower_count: Optional[int] = None @dataclass diff --git a/docs/sap-spec/HANDOVER_NEXT.md b/docs/sap-spec/HANDOVER_NEXT.md index d8ecb220..2efe1bc2 100644 --- a/docs/sap-spec/HANDOVER_NEXT.md +++ b/docs/sap-spec/HANDOVER_NEXT.md @@ -133,14 +133,15 @@ Two test files contain the strict pins: Total: **169 PASS / 83 FAIL** across the strict pins. 4 of 6 fixtures fully close §1+§2+§4. 000487 is the worst (RR fixture defect propagates everywhere). -(Post-slice-25a: section_cascade_pins 279 PASS / 33 FAIL, e2e SapResult -32 PASS / 40 FAIL. §3 + §5 + §6 + §7 (mostly) pinned. §3 NOW FULLY -CLOSES for all 6 fixtures (24/24) — slice 25a closed 000487 by lodging -detailed RR surfaces + adding gable_wall_external + Ext1 alt U -override + RdSAP §3.8 roof-area-as-max rule + half-up rounding. -Remaining failures: §4 monthly on 000477+487 (slice 25b), §5 LINE_72/73 -+ §6 LINE_84 on 000477/487 (cascade from §4), §7 LINE_92/93 marginal -on 000474/477/480/490 (precision artefact), §7 on 000487 (cascade).) +(Post-slice-25b: section_cascade_pins 286 PASS / 26 FAIL, e2e SapResult +32 PASS / 40 FAIL. §3 fully closes for all 6 fixtures (24/24). §4 closes +8 of 9 for 000487 — only LINE_65 (heat gains from WH) still fails +because the §4 cascade doesn't yet derive (64a) electric-shower kWh +from the cert (Appendix J step 8). Remaining cascade failures: §4 on +000477 (combi loss precision, slice 25c) + §4 LINE_65 on 000487 +(electric shower derivation), §5/§6 LINE_72/73/84 on 000477+487 +(cascade from §4), §7 LINE_92/93 marginal on 000474/477/480/490 +(precision artefact), §7 on 000487 (cascade from §4 LINE_65).) ### B.2 SapResult pin matrix (post-slice-22/23) @@ -201,6 +202,7 @@ fixture | section §4 pin status ### B.5 Recent slices (in reverse order — newest first) ``` +Slice 25b: 000487 §4 closure (7/8) — has_electric_shower + mixer/electric counts on SapHeating, Appendix J step 2a fix Slice 25a: 000487 §3 closure — detailed RR + gable_wall_external + Ext1 alt U=1.9 + §3.8 max-floor roof + half-up rounding Slice 26c: §7 mean internal temp cascade pin (60 cases, 44 PASS) — LINE_85..94 Slice 26b: §6 solar gains cascade pin (12 cases, 10 PASS) + SapRoofWindow solar attrs + plumb to §6 cascade diff --git a/packages/domain/src/domain/ml/tests/_fixtures.py b/packages/domain/src/domain/ml/tests/_fixtures.py index b526aa64..7bae088f 100644 --- a/packages/domain/src/domain/ml/tests/_fixtures.py +++ b/packages/domain/src/domain/ml/tests/_fixtures.py @@ -100,6 +100,8 @@ def make_sap_heating( secondary_fuel_type: Optional[int] = None, secondary_heating_type: Optional[int] = None, number_baths: Optional[int] = None, + electric_shower_count: Optional[int] = None, + mixer_shower_count: Optional[int] = None, ) -> SapHeating: """Build a SapHeating with SAP10 API defaults.""" return SapHeating( @@ -115,6 +117,8 @@ def make_sap_heating( secondary_fuel_type=secondary_fuel_type, secondary_heating_type=secondary_heating_type, number_baths=number_baths, + electric_shower_count=electric_shower_count, + mixer_shower_count=mixer_shower_count, ) diff --git a/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py b/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py index 87928920..2545c976 100644 --- a/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py +++ b/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py @@ -718,12 +718,14 @@ def water_heating_section_from_cert( if main is not None and main.main_heating_index_number is not None else None ) + has_electric_shower = _has_electric_shower_from_cert(epc) bootstrap = water_heating_from_cert( epc=epc, mixer_shower_flow_rates_l_per_min=_mixer_shower_flow_rates_from_cert(epc), has_bath=_has_bath_from_cert(epc), cold_water_temps_c=TABLE_J1_TCOLD_FROM_MAINS_C, low_water_use=False, + has_electric_shower=has_electric_shower, ) combi_loss_override = pcdb_combi_loss_override( pcdb_main, @@ -737,6 +739,7 @@ def water_heating_section_from_cert( cold_water_temps_c=TABLE_J1_TCOLD_FROM_MAINS_C, low_water_use=False, combi_loss_monthly_kwh_override=combi_loss_override, + has_electric_shower=has_electric_shower, ) @@ -950,16 +953,30 @@ def _mixer_shower_flow_rates_from_cert( ) -> tuple[float, ...]: """Pull mixer-shower flow rates from the cert. - The cert lodges flow rate per shower outlet (Elmhurst worksheets - show "Vented hot water system, 7.00"). The domain model doesn't - surface that field yet; until it does, every cert defaults to the - SAP10.2 Table J4 "Vented hot water system" row at 7 L/min — the - existing-dwelling minimum and what both validation fixtures - (000474 + 000490) actually lodge. Combi-pumped showers (11 L/min) - or electric showers (handled via (64a)m, not here) will need - proper shower-outlet-type plumbing in a later slice. + When `sap_heating.mixer_shower_count` is lodged, use that count + of vented mixers @ Table J4's 7 L/min row. When None, default to a + single vented outlet — the modal RdSAP lodging. Combi-pumped + showers (11 L/min) need a richer cert surface in a future slice. """ - return (_SHOWER_FLOW_VENTED_L_PER_MIN,) + count = ( + epc.sap_heating.mixer_shower_count + if epc.sap_heating is not None + else None + ) + if count is None: + count = 1 + return tuple(_SHOWER_FLOW_VENTED_L_PER_MIN for _ in range(max(0, count))) + + +def _has_electric_shower_from_cert(epc: EpcPropertyData) -> bool: + """True iff cert lodges ≥ 1 instantaneous electric shower. + + Electric showers don't draw warm water from the main HW system but + count in `Noutlets` for SAP10.2 Appendix J (p.81) step 1a and route + Nbath through the "shower also present" branch in step 2a (0.13N + + 0.19 instead of 0.35N + 0.50). Defaults False when unlodged.""" + n = epc.sap_heating.electric_shower_count if epc.sap_heating is not None else None + return (n or 0) >= 1 def _has_bath_from_cert(epc: EpcPropertyData) -> bool: @@ -1052,12 +1069,14 @@ def _water_heating_worksheet_and_gains( zero_monthly = (0.0,) * 12 if epc.total_floor_area_m2 is None: return None, zero_monthly + has_electric_shower = _has_electric_shower_from_cert(epc) bootstrap = water_heating_from_cert( epc=epc, mixer_shower_flow_rates_l_per_min=_mixer_shower_flow_rates_from_cert(epc), has_bath=_has_bath_from_cert(epc), cold_water_temps_c=TABLE_J1_TCOLD_FROM_MAINS_C, low_water_use=False, + has_electric_shower=has_electric_shower, ) combi_loss_override = pcdb_combi_loss_override( pcdb_record, @@ -1071,6 +1090,7 @@ def _water_heating_worksheet_and_gains( cold_water_temps_c=TABLE_J1_TCOLD_FROM_MAINS_C, low_water_use=False, combi_loss_monthly_kwh_override=combi_loss_override, + has_electric_shower=has_electric_shower, ) return wh_result, wh_result.heat_gains_monthly_kwh diff --git a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000487.py b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000487.py index 17be318a..fcacfd83 100644 --- a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000487.py +++ b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000487.py @@ -172,6 +172,12 @@ def build_epc() -> EpcPropertyData: # radiant heaters" → SAP code 691. Table 11 fraction 0.10. secondary_heating_type=691, number_baths=1, # 000487 line 123: Total number of baths = 1 + # 000487 cert: 1 instantaneous electric shower, no mixer + # outlet (worksheet text shows only Doors/Windows openings, + # no shower outlet under Heat Loss). Drives Appendix J step 2a + # to use Nbath = 0.13N + 0.19 ("shower also present"). + electric_shower_count=1, + mixer_shower_count=0, ), ) diff --git a/packages/domain/src/domain/sap/worksheet/water_heating.py b/packages/domain/src/domain/sap/worksheet/water_heating.py index a51fcc36..3dc380b6 100644 --- a/packages/domain/src/domain/sap/worksheet/water_heating.py +++ b/packages/domain/src/domain/sap/worksheet/water_heating.py @@ -595,6 +595,7 @@ def water_heating_from_cert( low_water_use: bool, combi_loss_monthly_kwh_override: Optional[tuple[float, ...]] = None, electric_shower_monthly_kwh_override: Optional[tuple[float, ...]] = None, + has_electric_shower: bool = False, ) -> WaterHeatingResult: """SAP 10.2 §4 orchestrator — chain every line ref from (42) through (65) for a combi-gas dwelling with optional PCDB-backed combi loss. @@ -635,6 +636,7 @@ def water_heating_from_cert( has_shower = ( len(mixer_shower_flow_rates_l_per_min) > 0 or electric_shower_monthly_kwh_override is not None + or has_electric_shower ) baths = hot_water_baths_monthly_l_per_day( n_occupants=n,