§4 slice 2: hot_water_other_uses_monthly_l_per_day (line (42c)m)

Appendix J equation J11 — daily hot water use for non-shower / non-bath
purposes (sinks, dishwashers, etc.) is annual-avg V_d,other,ave = 9.8 ×
N + 14, modulated month-by-month by the Table J2 monthly factors and
reduced by 5% when the dwelling meets the 125 L/person/day water-use
target.

Validated against both Elmhurst non-RR fixtures to better than 1e-3 L:
  - 000490 N=2.1468 → V_d,other,ave ≈ 35.04, Jan = 38.5426
  - 000474 N=1.8896 → V_d,other,ave ≈ 32.52, Jan = 35.7697

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-20 15:43:56 +00:00
parent aff678e8eb
commit 5cc68ab3fd
2 changed files with 93 additions and 1 deletions

View file

@ -15,7 +15,10 @@ from domain.sap.worksheet.tests import (
_elmhurst_worksheet_000490 as _w000490, _elmhurst_worksheet_000490 as _w000490,
) )
from domain.sap.worksheet.tests._xlsx_loader import load_cells from domain.sap.worksheet.tests._xlsx_loader import load_cells
from domain.sap.worksheet.water_heating import assumed_occupancy from domain.sap.worksheet.water_heating import (
assumed_occupancy,
hot_water_other_uses_monthly_l_per_day,
)
def test_assumed_occupancy_matches_canonical_xlsx_worked_example() -> None: def test_assumed_occupancy_matches_canonical_xlsx_worked_example() -> None:
@ -60,6 +63,65 @@ def test_assumed_occupancy_matches_elmhurst_worksheet_000490() -> None:
assert n == pytest.approx(_w000490.LINE_42_OCCUPANCY, abs=1e-4) assert n == pytest.approx(_w000490.LINE_42_OCCUPANCY, abs=1e-4)
def test_hot_water_other_uses_matches_elmhurst_worksheet_000490() -> None:
"""SAP10.2 §4 line (42c)m via Appendix J equation J11:
V_d,other,ave = 9.8 × N + 14
V_d,other[m] = V_d,other,ave × J2_monthly_factor[m]
For 000490 (N=2.1468) the worksheet (42c)m values follow the Table J2
monthly factor pattern of (1.10, 1.06, 1.02, 0.98, 0.94, 0.90, 0.90,
0.94, 0.98, 1.02, 1.06, 1.10) applied to V_d,other,ave 35.04 L/day.
"""
# Arrange — expected values copied from the Elmhurst worksheet
expected = (
38.5426, 37.1411, 35.7395, 34.3380, 32.9364, 31.5349,
31.5349, 32.9364, 34.3380, 35.7395, 37.1411, 38.5426,
)
# Act
monthly = hot_water_other_uses_monthly_l_per_day(
n_occupants=_w000490.LINE_42_OCCUPANCY,
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_other_uses_matches_elmhurst_worksheet_000474() -> None:
"""Same (42c)m formula for 000474 (N=1.8896): V_d,other,ave ≈ 32.52
L/day; Jan = 32.52 × 1.10 35.77."""
# Arrange
expected = (
35.7697, 34.4690, 33.1682, 31.8675, 30.5668, 29.2661,
29.2661, 30.5668, 31.8675, 33.1682, 34.4690, 35.7697,
)
# Act
monthly = hot_water_other_uses_monthly_l_per_day(
n_occupants=_w000474.LINE_42_OCCUPANCY,
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_other_uses_low_water_use_target_reduces_by_5pct() -> None:
"""Appendix J J11 footnote: dwellings designed for ≤125 L/person/day
total water use get the V_d,other,ave reduced by 5%. Monthly factors
apply after the reduction."""
# Arrange / Act
normal = hot_water_other_uses_monthly_l_per_day(n_occupants=2.0, low_water_use=False)
lwu = hot_water_other_uses_monthly_l_per_day(n_occupants=2.0, 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: 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."""

View file

@ -29,6 +29,16 @@ from typing import Final
_OCCUPANCY_TFA_FLOOR_M2: Final[float] = 13.9 _OCCUPANCY_TFA_FLOOR_M2: Final[float] = 13.9
# Table J2 — monthly factors for hot water use (also used by Appendix J
# equation J11 for "other uses"). Symmetric about the year midpoint.
_TABLE_J2_MONTHLY_FACTORS: Final[tuple[float, ...]] = (
1.10, 1.06, 1.02, 0.98, 0.94, 0.90, 0.90, 0.94, 0.98, 1.02, 1.06, 1.10,
)
# Appendix J J11 footnote: -5% reduction in V_d,other,ave for dwellings
# designed to ≤125 L/person/day total water use.
_LOW_WATER_USE_REDUCTION: Final[float] = 0.05
def assumed_occupancy(total_floor_area_m2: float) -> float: def assumed_occupancy(total_floor_area_m2: float) -> float:
"""SAP 10.2 §4 line (42) / Appendix J Table 1b. """SAP 10.2 §4 line (42) / Appendix J Table 1b.
@ -42,3 +52,23 @@ def assumed_occupancy(total_floor_area_m2: float) -> float:
return 1.0 return 1.0
x = total_floor_area_m2 - _OCCUPANCY_TFA_FLOOR_M2 x = total_floor_area_m2 - _OCCUPANCY_TFA_FLOOR_M2
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_other_uses_monthly_l_per_day(
*, n_occupants: float, low_water_use: bool
) -> tuple[float, ...]:
"""SAP 10.2 §4 line (42c)m via Appendix J equation J11.
Annual-average daily hot water use for "other purposes" (i.e. not
showers, not baths sinks, dishwashers, etc.):
V_d,other,ave = 9.8 × N + 14
reduced by 5% if `low_water_use` is True (dwelling designed for 125
L/person/day total water use). The monthly array applies Table J2's
factor sequence so each entry is daily L for that month.
"""
annual_average = 9.8 * n_occupants + 14.0
if low_water_use:
annual_average *= 1.0 - _LOW_WATER_USE_REDUCTION
return tuple(annual_average * f for f in _TABLE_J2_MONTHLY_FACTORS)