mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
§4 slice 4: hot_water_mixer_showers_monthly_l_per_day (line (42a)m)
Appendix J equations J1–J3. Per-day hot water draw for mixer showers combines the per-day shower count (rising with N, depressed slightly when a bath is also present) with each outlet's flow × 6 min × Table J5 behavioural factor, then multiplied by the cold-water-dependent hot fraction (41 °C delivery vs 52 °C hot supply, Tcold from J1). Multi-outlet handling: N_shower is split across outlets so a dwelling with two identical mixers produces the same (42a)m total as a single outlet — the count only matters when outlets have different flow rates. Instantaneous electric showers belong in (64a)m and must be excluded from the input. Validated against the Elmhurst non-RR fixtures (both 1 vented mixer at 7 L/min, mains Tcold): - 000490 N=2.1468 → Jan V_d,hot = 52.6878 - 000474 N=1.8896 → Jan V_d,hot = 48.9139 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
dad7fbf31f
commit
1dcbdb28e6
2 changed files with 161 additions and 0 deletions
|
|
@ -19,6 +19,7 @@ from domain.sap.worksheet.water_heating import (
|
|||
TABLE_J1_TCOLD_FROM_MAINS_C,
|
||||
assumed_occupancy,
|
||||
hot_water_baths_monthly_l_per_day,
|
||||
hot_water_mixer_showers_monthly_l_per_day,
|
||||
hot_water_other_uses_monthly_l_per_day,
|
||||
)
|
||||
|
||||
|
|
@ -212,6 +213,118 @@ def test_hot_water_baths_low_water_use_reduces_warm_water_by_5pct() -> None:
|
|||
assert l == pytest.approx(n * 0.95, abs=1e-9), f"month {m+1}"
|
||||
|
||||
|
||||
def test_hot_water_mixer_showers_matches_elmhurst_worksheet_000490() -> None:
|
||||
"""SAP10.2 §4 line (42a)m via Appendix J equations J1–J3.
|
||||
|
||||
Bath present so:
|
||||
N_shower = 0.45 × N + 0.65 (J1)
|
||||
V_warm,i[m] = flow × 6 × J5_fbeh[m] (step 1d)
|
||||
V_d,warm,i[m] = V_warm,i × N_shower / N_outlets (J2)
|
||||
V_d,hot,i[m] = V_d,warm,i × (41−Tcold[m])/(52−Tcold[m]) (J3)
|
||||
|
||||
000490: 1 vented mixer outlet at 7 L/min, mains Tcold,
|
||||
N=2.1468, bath present → Jan V_d,hot ≈ 52.69 L/day.
|
||||
"""
|
||||
# Arrange
|
||||
expected = (
|
||||
52.6878, 51.8960, 50.7422, 48.5346, 46.9054, 45.0886,
|
||||
44.0560, 45.2010, 46.4562, 48.4069, 50.6619, 52.4859,
|
||||
)
|
||||
|
||||
# Act
|
||||
monthly = hot_water_mixer_showers_monthly_l_per_day(
|
||||
n_occupants=_w000490.LINE_42_OCCUPANCY,
|
||||
has_bath=True,
|
||||
mixer_shower_flow_rates_l_per_min=(7.0,),
|
||||
cold_water_temps_c=TABLE_J1_TCOLD_FROM_MAINS_C,
|
||||
)
|
||||
|
||||
# Assert
|
||||
for m, (actual, exp) in enumerate(zip(monthly, expected)):
|
||||
assert actual == pytest.approx(exp, abs=1e-3), f"month {m+1}"
|
||||
|
||||
|
||||
def test_hot_water_mixer_showers_matches_elmhurst_worksheet_000474() -> None:
|
||||
"""Same (42a)m formula for 000474 (N=1.8896, 1 vented mixer at 7 L/min)."""
|
||||
# Arrange
|
||||
expected = (
|
||||
48.9139, 48.1788, 47.1076, 45.0582, 43.5457, 41.8591,
|
||||
40.9004, 41.9634, 43.1287, 44.9397, 47.0332, 48.7265,
|
||||
)
|
||||
|
||||
# Act
|
||||
monthly = hot_water_mixer_showers_monthly_l_per_day(
|
||||
n_occupants=_w000474.LINE_42_OCCUPANCY,
|
||||
has_bath=True,
|
||||
mixer_shower_flow_rates_l_per_min=(7.0,),
|
||||
cold_water_temps_c=TABLE_J1_TCOLD_FROM_MAINS_C,
|
||||
)
|
||||
|
||||
# Assert
|
||||
for m, (actual, exp) in enumerate(zip(monthly, expected)):
|
||||
assert actual == pytest.approx(exp, abs=1e-3), f"month {m+1}"
|
||||
|
||||
|
||||
def test_hot_water_mixer_showers_zero_when_no_outlets() -> None:
|
||||
"""Appendix J J1 third branch: no shower outlets → N_shower=0 → (42a)m
|
||||
is all zeros regardless of N."""
|
||||
# Arrange / Act
|
||||
monthly = hot_water_mixer_showers_monthly_l_per_day(
|
||||
n_occupants=3.0,
|
||||
has_bath=True,
|
||||
mixer_shower_flow_rates_l_per_min=(),
|
||||
cold_water_temps_c=TABLE_J1_TCOLD_FROM_MAINS_C,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert all(v == pytest.approx(0.0, abs=1e-9) for v in monthly)
|
||||
|
||||
|
||||
def test_hot_water_mixer_showers_no_bath_present_uses_higher_n_shower_branch() -> None:
|
||||
"""J1 second branch: with no bath the per-day shower count rises from
|
||||
0.45N+0.65 to 0.58N+0.83 (higher slope and intercept). For N=2 the
|
||||
ratio is (0.58×2+0.83)/(0.45×2+0.65) = 1.99/1.55 = 1.284."""
|
||||
# Arrange / Act
|
||||
with_bath = 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,
|
||||
)
|
||||
no_bath = hot_water_mixer_showers_monthly_l_per_day(
|
||||
n_occupants=2.0, has_bath=False,
|
||||
mixer_shower_flow_rates_l_per_min=(8.0,),
|
||||
cold_water_temps_c=TABLE_J1_TCOLD_FROM_MAINS_C,
|
||||
)
|
||||
|
||||
# Assert — V_d,hot scales linearly with N_shower; ratio is identical
|
||||
# for every month and equals (1.99/1.55).
|
||||
expected_ratio = (0.58 * 2.0 + 0.83) / (0.45 * 2.0 + 0.65)
|
||||
for m, (b, nb) in enumerate(zip(with_bath, no_bath)):
|
||||
assert nb == pytest.approx(b * expected_ratio, abs=1e-6), f"month {m+1}"
|
||||
|
||||
|
||||
def test_hot_water_mixer_showers_two_outlets_distributes_shower_count_evenly() -> None:
|
||||
"""V_d,warm,i = V_warm,i × N_shower / N_outlets — adding a second
|
||||
identical outlet halves the per-outlet daily warm water, but two
|
||||
outlets summed equals one outlet's contribution. Net (42a)m is
|
||||
unchanged for identical flow rates."""
|
||||
# Arrange / Act
|
||||
single = 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,
|
||||
)
|
||||
twin = hot_water_mixer_showers_monthly_l_per_day(
|
||||
n_occupants=2.0, has_bath=True,
|
||||
mixer_shower_flow_rates_l_per_min=(8.0, 8.0),
|
||||
cold_water_temps_c=TABLE_J1_TCOLD_FROM_MAINS_C,
|
||||
)
|
||||
|
||||
# Assert
|
||||
for m, (s, t) in enumerate(zip(single, twin)):
|
||||
assert t == pytest.approx(s, abs=1e-9), f"month {m+1}"
|
||||
|
||||
|
||||
def test_assumed_occupancy_floor_at_n_eq_1_for_small_dwellings() -> None:
|
||||
"""Appendix J piecewise definition: TFA ≤ 13.9 m² → N=1 exactly. A
|
||||
tiny studio flat at the boundary is the most common trigger."""
|
||||
|
|
|
|||
|
|
@ -61,6 +61,9 @@ _BATH_VOLUME_L: Final[float] = 73.0
|
|||
# Appendix J equation J8 / J3 mixing temperatures.
|
||||
_HOT_DELIVERY_TEMPERATURE_C: Final[float] = 52.0
|
||||
_WARM_BATH_TEMPERATURE_C: Final[float] = 42.0
|
||||
_WARM_SHOWER_TEMPERATURE_C: Final[float] = 41.0
|
||||
# Appendix J step 1d: assumed shower duration in minutes per event.
|
||||
_SHOWER_DURATION_MIN: Final[float] = 6.0
|
||||
|
||||
|
||||
def assumed_occupancy(total_floor_area_m2: float) -> float:
|
||||
|
|
@ -77,6 +80,51 @@ def assumed_occupancy(total_floor_area_m2: float) -> float:
|
|||
return 1.0 + 1.76 * (1.0 - exp(-0.000349 * x * x)) + 0.0013 * x
|
||||
|
||||
|
||||
def hot_water_mixer_showers_monthly_l_per_day(
|
||||
*,
|
||||
n_occupants: float,
|
||||
has_bath: bool,
|
||||
mixer_shower_flow_rates_l_per_min: tuple[float, ...],
|
||||
cold_water_temps_c: tuple[float, ...],
|
||||
) -> tuple[float, ...]:
|
||||
"""SAP 10.2 §4 line (42a)m via Appendix J equations J1, J2, J3.
|
||||
|
||||
Mixer showers draw from the main hot water system (cylinder or combi)
|
||||
and mix with cold to a 41 °C delivery. The per-day shower count is
|
||||
|
||||
N_shower = 0.45 × N + 0.65 (bath present)
|
||||
= 0.58 × N + 0.83 (no bath)
|
||||
= 0 (no shower outlets)
|
||||
|
||||
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.
|
||||
|
||||
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 = len(mixer_shower_flow_rates_l_per_min)
|
||||
if n_outlets == 0:
|
||||
return tuple(0.0 for _ in range(12))
|
||||
if has_bath:
|
||||
n_shower = 0.45 * n_occupants + 0.65
|
||||
else:
|
||||
n_shower = 0.58 * n_occupants + 0.83
|
||||
monthly: list[float] = []
|
||||
for fbeh, tcold in zip(TABLE_J5_BEHAVIOURAL_FACTOR, cold_water_temps_c):
|
||||
f_hot = (_WARM_SHOWER_TEMPERATURE_C - tcold) / (
|
||||
_HOT_DELIVERY_TEMPERATURE_C - tcold
|
||||
)
|
||||
v_hot_total = 0.0
|
||||
for flow in mixer_shower_flow_rates_l_per_min:
|
||||
v_warm_per_outlet = flow * _SHOWER_DURATION_MIN * fbeh
|
||||
v_d_warm = v_warm_per_outlet * n_shower / n_outlets
|
||||
v_hot_total += v_d_warm * f_hot
|
||||
monthly.append(v_hot_total)
|
||||
return tuple(monthly)
|
||||
|
||||
|
||||
def hot_water_baths_monthly_l_per_day(
|
||||
*,
|
||||
n_occupants: float,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue