mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +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,
|
TABLE_J1_TCOLD_FROM_MAINS_C,
|
||||||
assumed_occupancy,
|
assumed_occupancy,
|
||||||
hot_water_baths_monthly_l_per_day,
|
hot_water_baths_monthly_l_per_day,
|
||||||
|
hot_water_mixer_showers_monthly_l_per_day,
|
||||||
hot_water_other_uses_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}"
|
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:
|
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
|
"""Appendix J piecewise definition: TFA ≤ 13.9 m² → N=1 exactly. A
|
||||||
tiny studio flat at the boundary is the most common trigger."""
|
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.
|
# Appendix J equation J8 / J3 mixing temperatures.
|
||||||
_HOT_DELIVERY_TEMPERATURE_C: Final[float] = 52.0
|
_HOT_DELIVERY_TEMPERATURE_C: Final[float] = 52.0
|
||||||
_WARM_BATH_TEMPERATURE_C: Final[float] = 42.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:
|
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
|
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(
|
def hot_water_baths_monthly_l_per_day(
|
||||||
*,
|
*,
|
||||||
n_occupants: float,
|
n_occupants: float,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue