mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
§4 slice 3: hot_water_baths_monthly_l_per_day (line (42b)m)
Appendix J equations J6, J7, J8. Daily hot water for bath fills depends
on N, presence of bath and/or shower, and monthly Tcold:
N_bath = 0 if no bath but a shower exists
= 0.13×N + 0.19 if bath + shower
= 0.35×N + 0.50 otherwise
V_d,bath[m] = N_bath × 73 × J5_fbeh[m] × (42−Tcold[m])/(52−Tcold[m])
Tables J1 (mains + header tank Tcold) and J5 (behavioural factor) are
exported as module constants for reuse by (42a)m showers next.
Validated against the Elmhurst non-RR fixtures, both with bath + shower
and "Cold Water Source: From mains":
- 000490 N=2.1468 → Jan V_d,bath = 27.3868
- 000474 N=1.8896 → Jan V_d,bath = 25.4345
Also covers the zero-bath branch and the 5% low-water-use reduction.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
5cc68ab3fd
commit
dad7fbf31f
2 changed files with 155 additions and 0 deletions
|
|
@ -16,7 +16,9 @@ from domain.sap.worksheet.tests import (
|
|||
)
|
||||
from domain.sap.worksheet.tests._xlsx_loader import load_cells
|
||||
from domain.sap.worksheet.water_heating import (
|
||||
TABLE_J1_TCOLD_FROM_MAINS_C,
|
||||
assumed_occupancy,
|
||||
hot_water_baths_monthly_l_per_day,
|
||||
hot_water_other_uses_monthly_l_per_day,
|
||||
)
|
||||
|
||||
|
|
@ -122,6 +124,94 @@ def test_hot_water_other_uses_low_water_use_target_reduces_by_5pct() -> None:
|
|||
assert l == pytest.approx(n * 0.95, abs=1e-9), f"month {m+1}"
|
||||
|
||||
|
||||
def test_hot_water_baths_matches_elmhurst_worksheet_000490() -> None:
|
||||
"""SAP10.2 §4 line (42b)m via Appendix J equations J6, J7, J8.
|
||||
|
||||
Bath + shower present (typical home), N=2.1468:
|
||||
N_bath = 0.13 × N + 0.19 = 0.4691 baths/day
|
||||
V_d,warm,bath[m] = N_bath × 73 × J5_fbeh[m]
|
||||
V_d,bath[m] = V_d,warm,bath[m] × (42 − Tcold[m]) / (52 − Tcold[m])
|
||||
|
||||
The cold-water table is Tcold from mains (Table J1) — the 000490
|
||||
cert lodges "Cold Water Source = From mains".
|
||||
"""
|
||||
# Arrange
|
||||
expected = (
|
||||
27.3868, 26.9801, 26.4073, 25.3512, 24.5605, 23.6836,
|
||||
23.2100, 23.7787, 24.3980, 25.3363, 26.4141, 27.2942,
|
||||
)
|
||||
|
||||
# Act
|
||||
monthly = hot_water_baths_monthly_l_per_day(
|
||||
n_occupants=_w000490.LINE_42_OCCUPANCY,
|
||||
has_bath=True,
|
||||
has_shower=True,
|
||||
cold_water_temps_c=TABLE_J1_TCOLD_FROM_MAINS_C,
|
||||
low_water_use=False,
|
||||
)
|
||||
|
||||
# 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_baths_matches_elmhurst_worksheet_000474() -> None:
|
||||
"""Same (42b)m formulas for 000474 (N=1.8896, bath + shower)."""
|
||||
# Arrange
|
||||
expected = (
|
||||
25.4345, 25.0567, 24.5248, 23.5440, 22.8096, 21.9953,
|
||||
21.5554, 22.0836, 22.6587, 23.5301, 24.5311, 25.3485,
|
||||
)
|
||||
|
||||
# Act
|
||||
monthly = hot_water_baths_monthly_l_per_day(
|
||||
n_occupants=_w000474.LINE_42_OCCUPANCY,
|
||||
has_bath=True,
|
||||
has_shower=True,
|
||||
cold_water_temps_c=TABLE_J1_TCOLD_FROM_MAINS_C,
|
||||
low_water_use=False,
|
||||
)
|
||||
|
||||
# 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_baths_zero_when_no_bath_present_and_a_shower_is() -> None:
|
||||
"""Appendix J J6 first branch: N_bath = 0 when there's no bath but a
|
||||
shower is present, regardless of N."""
|
||||
# Arrange / Act
|
||||
monthly = hot_water_baths_monthly_l_per_day(
|
||||
n_occupants=3.0,
|
||||
has_bath=False,
|
||||
has_shower=True,
|
||||
cold_water_temps_c=TABLE_J1_TCOLD_FROM_MAINS_C,
|
||||
low_water_use=False,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert all(v == pytest.approx(0.0, abs=1e-9) for v in monthly)
|
||||
|
||||
|
||||
def test_hot_water_baths_low_water_use_reduces_warm_water_by_5pct() -> None:
|
||||
"""J7 footnote: ≤125 L/person/day target reduces V_d,warm,bath by 5%.
|
||||
Since V_d,bath is linear in V_d,warm,bath, the monthly array shrinks
|
||||
by exactly 5% too."""
|
||||
# Arrange / Act
|
||||
normal = hot_water_baths_monthly_l_per_day(
|
||||
n_occupants=2.0, has_bath=True, has_shower=True,
|
||||
cold_water_temps_c=TABLE_J1_TCOLD_FROM_MAINS_C, low_water_use=False,
|
||||
)
|
||||
lwu = hot_water_baths_monthly_l_per_day(
|
||||
n_occupants=2.0, has_bath=True, has_shower=True,
|
||||
cold_water_temps_c=TABLE_J1_TCOLD_FROM_MAINS_C, low_water_use=True,
|
||||
)
|
||||
|
||||
# Assert
|
||||
for m, (n, l) in enumerate(zip(normal, lwu)):
|
||||
assert l == pytest.approx(n * 0.95, 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."""
|
||||
|
|
|
|||
|
|
@ -39,6 +39,29 @@ _TABLE_J2_MONTHLY_FACTORS: Final[tuple[float, ...]] = (
|
|||
# designed to ≤125 L/person/day total water use.
|
||||
_LOW_WATER_USE_REDUCTION: Final[float] = 0.05
|
||||
|
||||
# Table J1 — cold-water inlet temperatures (°C). Two columns per the spec:
|
||||
# from a header tank (cooler) vs from the mains. The cert lodges which
|
||||
# applies; both populations need spec-exact monthly arrays.
|
||||
TABLE_J1_TCOLD_FROM_HEADER_TANK_C: Final[tuple[float, ...]] = (
|
||||
11.1, 11.3, 12.3, 14.5, 16.2, 18.8, 21.3, 19.3, 18.7, 16.2, 13.2, 11.2,
|
||||
)
|
||||
TABLE_J1_TCOLD_FROM_MAINS_C: Final[tuple[float, ...]] = (
|
||||
8.0, 8.2, 9.3, 12.7, 14.6, 16.7, 18.4, 17.6, 16.6, 14.3, 11.1, 8.5,
|
||||
)
|
||||
|
||||
# Table J5 — behavioural variation factor for showers AND baths. Used by
|
||||
# (42a)m showers (Appendix J step 1d) and (42b)m baths (step 2b) alike.
|
||||
TABLE_J5_BEHAVIOURAL_FACTOR: Final[tuple[float, ...]] = (
|
||||
1.035, 1.021, 1.007, 0.993, 0.979, 0.965,
|
||||
0.965, 0.979, 0.993, 1.007, 1.021, 1.035,
|
||||
)
|
||||
|
||||
# Appendix J equation J7 constants.
|
||||
_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
|
||||
|
||||
|
||||
def assumed_occupancy(total_floor_area_m2: float) -> float:
|
||||
"""SAP 10.2 §4 line (42) / Appendix J Table 1b.
|
||||
|
|
@ -54,6 +77,48 @@ 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_baths_monthly_l_per_day(
|
||||
*,
|
||||
n_occupants: float,
|
||||
has_bath: bool,
|
||||
has_shower: bool,
|
||||
cold_water_temps_c: tuple[float, ...],
|
||||
low_water_use: bool,
|
||||
) -> tuple[float, ...]:
|
||||
"""SAP 10.2 §4 line (42b)m via Appendix J equations J6, J7, J8.
|
||||
|
||||
Per-day hot water for bath fills across the 12 months. Per J6:
|
||||
|
||||
N_bath = 0 if no bath (but a shower is present)
|
||||
= 0.13 × N + 0.19 if shower is also present
|
||||
= 0.35 × N + 0.50 if no shower present, or no bath
|
||||
and no shower
|
||||
|
||||
Each bath fills 73 L of warm water at 42 °C; the hot fraction depends
|
||||
on monthly cold-water temperature (Table J1, either header tank or
|
||||
mains depending on the cert). `low_water_use` knocks 5% off the
|
||||
warm-water term per the J7 footnote.
|
||||
|
||||
`cold_water_temps_c` must be a 12-tuple of monthly Tcold values —
|
||||
pass `TABLE_J1_TCOLD_FROM_MAINS_C` for the common case.
|
||||
"""
|
||||
if not has_bath and has_shower:
|
||||
return tuple(0.0 for _ in range(12))
|
||||
if has_bath and has_shower:
|
||||
n_bath = 0.13 * n_occupants + 0.19
|
||||
else:
|
||||
n_bath = 0.35 * n_occupants + 0.50
|
||||
lwu_factor = 1.0 - _LOW_WATER_USE_REDUCTION if low_water_use else 1.0
|
||||
monthly: list[float] = []
|
||||
for fbeh, tcold in zip(TABLE_J5_BEHAVIOURAL_FACTOR, cold_water_temps_c):
|
||||
v_warm = n_bath * _BATH_VOLUME_L * fbeh * lwu_factor
|
||||
f_hot = (_WARM_BATH_TEMPERATURE_C - tcold) / (
|
||||
_HOT_DELIVERY_TEMPERATURE_C - tcold
|
||||
)
|
||||
monthly.append(v_warm * f_hot)
|
||||
return tuple(monthly)
|
||||
|
||||
|
||||
def hot_water_other_uses_monthly_l_per_day(
|
||||
*, n_occupants: float, low_water_use: bool
|
||||
) -> tuple[float, ...]:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue