From 5cc68ab3fd97abfe04eacbaf3d7de72b3822e299 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 20 May 2026 15:43:56 +0000 Subject: [PATCH] =?UTF-8?q?=C2=A74=20slice=202:=20hot=5Fwater=5Fother=5Fus?= =?UTF-8?q?es=5Fmonthly=5Fl=5Fper=5Fday=20(line=20(42c)m)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../sap/worksheet/tests/test_water_heating.py | 64 ++++++++++++++++++- .../src/domain/sap/worksheet/water_heating.py | 30 +++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/packages/domain/src/domain/sap/worksheet/tests/test_water_heating.py b/packages/domain/src/domain/sap/worksheet/tests/test_water_heating.py index 4a10da7e..5c4816af 100644 --- a/packages/domain/src/domain/sap/worksheet/tests/test_water_heating.py +++ b/packages/domain/src/domain/sap/worksheet/tests/test_water_heating.py @@ -15,7 +15,10 @@ from domain.sap.worksheet.tests import ( _elmhurst_worksheet_000490 as _w000490, ) 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: @@ -60,6 +63,65 @@ def test_assumed_occupancy_matches_elmhurst_worksheet_000490() -> None: 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: """Appendix J piecewise definition: TFA ≤ 13.9 m² → N=1 exactly. A tiny studio flat at the boundary is the most common trigger.""" diff --git a/packages/domain/src/domain/sap/worksheet/water_heating.py b/packages/domain/src/domain/sap/worksheet/water_heating.py index d3cf5a7b..bb780468 100644 --- a/packages/domain/src/domain/sap/worksheet/water_heating.py +++ b/packages/domain/src/domain/sap/worksheet/water_heating.py @@ -29,6 +29,16 @@ from typing import Final _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: """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 x = total_floor_area_m2 - _OCCUPANCY_TFA_FLOOR_M2 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)