mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
fix(water-heating): count electric showers in Noutlets for mixer demand (App J)
The mixer-shower hot-water demand (worksheet 42a) divided N_shower by the count of MIXER outlets only. But SAP 10.2 Appendix J step 1a is explicit: "Establish how many shower outlets are present in the dwelling, Noutlets (including in the count any instantaneous electric showers)" — and the electric-shower step (64a) uses that same Noutlets from step 1a. So a dwelling with both a mixer and an electric shower assigned the FULL N_shower to the mixer system AND billed the electric shower on top of it, double- counting shower demand → over-counted main HW → under-rated the dwelling. Fix: thread the electric-shower count into the mixer demand so the denominator is the total outlet count (mixer + electric), iterating the warm-water draw over the mixer outlets only (per step 1e). shower_types=1,2 cohort: -0.37 median -> +0.28 (crossed zero); API gauge 68.4% -> 69.0% within-0.5. Golden cert 0300-2747 (1 mixer + 1 electric) re-pinned: PE +0.93 -> -0.10, CO2 +0.25 -> +0.15 (both toward zero, confirming the double-count). Worksheet harness 47/47, 0 divergers (the Elmhurst fixtures have no electric showers). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
4fb9b853dc
commit
5317175dd3
3 changed files with 52 additions and 8 deletions
|
|
@ -132,6 +132,7 @@ def hot_water_mixer_showers_monthly_l_per_day(
|
|||
has_bath: bool,
|
||||
mixer_shower_flow_rates_l_per_min: tuple[float, ...],
|
||||
cold_water_temps_c: tuple[float, ...],
|
||||
n_electric_showers: int = 0,
|
||||
) -> tuple[float, ...]:
|
||||
"""SAP 10.2 §4 line (42a)m via Appendix J equations J1, J2, J3.
|
||||
|
||||
|
|
@ -145,14 +146,23 @@ def hot_water_mixer_showers_monthly_l_per_day(
|
|||
Each outlet's warm-water draw for a month is the flow rate × 6 min ×
|
||||
Table J5 fbeh. The hot fraction is (41 − Tcold[m])/(52 − Tcold[m]).
|
||||
Per-outlet daily warm water is scaled by N_shower / N_outlets, then
|
||||
summed across outlets to give (42a)m.
|
||||
summed across the MIXER outlets to give (42a)m.
|
||||
|
||||
Instantaneous electric showers belong to worksheet (64a)m, not (42a)m
|
||||
— those should be excluded from `mixer_shower_flow_rates_l_per_min`.
|
||||
N_outlets is the dwelling's TOTAL shower-outlet count: SAP 10.2
|
||||
Appendix J step 1a is explicit that the count INCLUDES "any
|
||||
instantaneous electric showers" (which then bill their own energy via
|
||||
(64a)m). So `mixer_shower_flow_rates_l_per_min` lists only the mixer
|
||||
outlets (iterated for the warm-water draw), but `n_electric_showers`
|
||||
must be added to the denominator — otherwise a dwelling with both a
|
||||
mixer and an electric shower assigns the FULL N_shower to the mixer
|
||||
system AND bills the electric shower on top, double-counting shower
|
||||
demand (over-counts main HW → under-rates the dwelling).
|
||||
"""
|
||||
n_outlets = len(mixer_shower_flow_rates_l_per_min)
|
||||
if n_outlets == 0:
|
||||
n_mixer_outlets = len(mixer_shower_flow_rates_l_per_min)
|
||||
if n_mixer_outlets == 0:
|
||||
return tuple(0.0 for _ in range(12))
|
||||
# Appendix J step 1a: Noutlets includes instantaneous electric showers.
|
||||
n_outlets = n_mixer_outlets + n_electric_showers
|
||||
if has_bath:
|
||||
n_shower = 0.45 * n_occupants + 0.65
|
||||
else:
|
||||
|
|
@ -894,6 +904,8 @@ def water_heating_from_cert(
|
|||
has_bath=has_bath,
|
||||
mixer_shower_flow_rates_l_per_min=mixer_shower_flow_rates_l_per_min,
|
||||
cold_water_temps_c=cold_water_temps_c,
|
||||
# SAP 10.2 Appendix J step 1a — Noutlets includes electric showers.
|
||||
n_electric_showers=electric_shower_count if has_electric_shower else 0,
|
||||
)
|
||||
has_shower = (
|
||||
len(mixer_shower_flow_rates_l_per_min) > 0
|
||||
|
|
|
|||
|
|
@ -224,8 +224,8 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = (
|
|||
cert_number="0300-2747-7640-2526-2135",
|
||||
actual_sap=78,
|
||||
expected_sap_resid=+0,
|
||||
expected_pe_resid_kwh_per_m2=+0.9264,
|
||||
expected_co2_resid_tonnes_per_yr=+0.2495,
|
||||
expected_pe_resid_kwh_per_m2=-0.1033,
|
||||
expected_co2_resid_tonnes_per_yr=+0.1488,
|
||||
notes=(
|
||||
"Large semi-detached, TFA 526, age D, gas boiler PCDB-listed "
|
||||
"(no Table 4b code). Cert lodges open_flues_count=1 + "
|
||||
|
|
@ -245,7 +245,11 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = (
|
|||
"CO2 / PE factors through the cert's mains-gas "
|
||||
"`secondary_fuel_type` (mirroring the cost-side Slice 58 "
|
||||
"fix), closing PE +8.28 → +0.93 and CO2 −0.25 → +0.25 — "
|
||||
"second-biggest cohort PE closure to date."
|
||||
"second-biggest cohort PE closure to date. Slice S0380.xx "
|
||||
"(SAP 10.2 Appendix J step 1a — Noutlets includes electric "
|
||||
"showers) halved this cert's mixer-shower HW (1 mixer + 1 "
|
||||
"electric → /2 not /1), removing the double-counted shower "
|
||||
"demand: PE +0.93 → -0.10, CO2 +0.25 → +0.15 (both toward 0)."
|
||||
),
|
||||
),
|
||||
_GoldenExpectation(
|
||||
|
|
|
|||
|
|
@ -344,6 +344,34 @@ def test_hot_water_mixer_showers_two_outlets_distributes_shower_count_evenly() -
|
|||
assert t == pytest.approx(s, abs=1e-9), f"month {m+1}"
|
||||
|
||||
|
||||
def test_hot_water_mixer_showers_counts_electric_showers_in_noutlets() -> None:
|
||||
# Arrange — SAP 10.2 Appendix J step 1a (PDF p.~): "Establish how many
|
||||
# shower outlets are present in the dwelling, Noutlets (INCLUDING in the
|
||||
# count any instantaneous electric showers)". The mixer (42a) demand
|
||||
# divides N_shower by this TOTAL outlet count, so a coexisting electric
|
||||
# shower reduces each mixer outlet's share. Adding one electric shower to
|
||||
# a single-mixer dwelling makes Noutlets=2 → the mixer demand halves
|
||||
# (vs. counting only the mixer outlet, which over-counts main HW and
|
||||
# under-rates the dwelling).
|
||||
mixer_only = hot_water_mixer_showers_monthly_l_per_day(
|
||||
n_occupants=2.0, has_bath=True,
|
||||
mixer_shower_flow_rates_l_per_min=(8.0,),
|
||||
cold_water_temps_c=TABLE_J1_TCOLD_FROM_MAINS_C,
|
||||
)
|
||||
|
||||
# Act — same single mixer outlet, but one electric shower also present.
|
||||
with_electric = hot_water_mixer_showers_monthly_l_per_day(
|
||||
n_occupants=2.0, has_bath=True,
|
||||
mixer_shower_flow_rates_l_per_min=(8.0,),
|
||||
cold_water_temps_c=TABLE_J1_TCOLD_FROM_MAINS_C,
|
||||
n_electric_showers=1,
|
||||
)
|
||||
|
||||
# Assert — N_shower / 2 instead of / 1 → exactly half the mixer demand.
|
||||
for m, (only, both) in enumerate(zip(mixer_only, with_electric)):
|
||||
assert abs(both - only / 2.0) <= 1e-9, f"month {m+1}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("fixture", ALL_FIXTURES, ids=[fixture_id(f) for f in ALL_FIXTURES])
|
||||
def test_total_hot_water_monthly_matches_elmhurst_line_44(fixture: ModuleType) -> None:
|
||||
"""SAP10.2 §4 line (44)m via Appendix J equation J13:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue