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)