From aff678e8ebb30d931af3be585dab87adebd517cb Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 20 May 2026 15:27:03 +0000 Subject: [PATCH] =?UTF-8?q?=C2=A74=20slice=201:=20assumed=5Foccupancy=20(w?= =?UTF-8?q?orksheet=20line=20(42),=20Appendix=20J)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First slice of the §4 worksheet-driven rewrite (xlsx rows 207-304). New module `domain/sap/worksheet/water_heating.py` lands the line-ref mapped functions; subsequent slices append below. `assumed_occupancy(tfa)` implements the SAP10.2 Appendix J Table 1b piecewise formula. Validated against: - canonical xlsx worked example (TFA Q23 → N U209) - Elmhurst U985-0001-000474 (TFA 56.79 → N 1.8896) - Elmhurst U985-0001-000490 (TFA 66.06 → N 2.1468) - boundary case TFA ≤ 13.9 (N=1 floor) The legacy `domain.ml.demand._default_occupants_sap_j` mirror stays in place until the §4 worksheet rewrite is complete; both sources will be reconciled in a later slice once dependent callers move over. Co-Authored-By: Claude Opus 4.7 --- .../tests/_elmhurst_worksheet_000474.py | 3 + .../tests/_elmhurst_worksheet_000490.py | 3 + .../sap/worksheet/tests/test_water_heating.py | 69 +++++++++++++++++++ .../src/domain/sap/worksheet/water_heating.py | 44 ++++++++++++ 4 files changed, 119 insertions(+) create mode 100644 packages/domain/src/domain/sap/worksheet/tests/test_water_heating.py create mode 100644 packages/domain/src/domain/sap/worksheet/water_heating.py diff --git a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000474.py b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000474.py index a605fda3..c4d0b534 100644 --- a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000474.py +++ b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000474.py @@ -181,3 +181,6 @@ LINE_37_TOTAL_FABRIC_HEAT_LOSS_W_PER_K: float = 232.1169 WINDOW_TOTAL_AREA_M2: float = 11.72 WINDOW_AVG_RAW_U_VALUE: float = 2.37 DOOR_COUNT: int = 2 # cascade default 1.85 m²/door → 3.70 m² matches worksheet + +# §4 Water heating energy requirements +LINE_42_OCCUPANCY: float = 1.8896 diff --git a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000490.py b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000490.py index 07d54757..11bf5f27 100644 --- a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000490.py +++ b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000490.py @@ -169,3 +169,6 @@ LINE_37_TOTAL_FABRIC_HEAT_LOSS_W_PER_K: float = 236.6211 WINDOW_TOTAL_AREA_M2: float = 9.03 WINDOW_AVG_RAW_U_VALUE: float = 2.8 DOOR_COUNT: int = 2 # cascade default 1.85 m²/door → 3.70 m² matches worksheet + +# §4 Water heating energy requirements +LINE_42_OCCUPANCY: float = 2.1468 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 new file mode 100644 index 00000000..4a10da7e --- /dev/null +++ b/packages/domain/src/domain/sap/worksheet/tests/test_water_heating.py @@ -0,0 +1,69 @@ +"""Tests for SAP 10.2 §4 — water heating energy requirements. + +Worksheet line refs land in `domain.sap.worksheet.water_heating`. Each +test asserts a single line-ref output against the canonical xlsx worked +example and/or Elmhurst conformance fixtures. + +Reference: SAP 10.2 specification §4 + Appendix J; canonical xlsx rows +207–304 (sheet `NonRegionalWeather`). +""" + +import pytest + +from domain.sap.worksheet.tests import ( + _elmhurst_worksheet_000474 as _w000474, + _elmhurst_worksheet_000490 as _w000490, +) +from domain.sap.worksheet.tests._xlsx_loader import load_cells +from domain.sap.worksheet.water_heating import assumed_occupancy + + +def test_assumed_occupancy_matches_canonical_xlsx_worked_example() -> None: + """SAP10.2 §4 line (42), Appendix J Table 1b. Canonical xlsx cell U209 + holds the worked-example occupancy for TFA=158.99 (Q23). The formula: + + N = 1 + 1.76 × (1 − exp(−0.000349 × (TFA − 13.9)²)) + + 0.0013 × (TFA − 13.9) for TFA > 13.9 + N = 1 for TFA ≤ 13.9 + + must reproduce 2.9475 W (matching the worksheet to 4 d.p.). + """ + # Arrange — TFA + expected N from the canonical worksheet. + cells = load_cells("NonRegionalWeather", ("Q23", "U209")) + tfa = cells["Q23"] + expected_n = cells["U209"] + + # Act + n = assumed_occupancy(tfa) + + # Assert + assert n == pytest.approx(expected_n, abs=1e-4) + + +def test_assumed_occupancy_matches_elmhurst_worksheet_000474() -> None: + """Mid-terrace TFA=56.79 m² → N=1.8896 per Elmhurst U985-0001-000474 + line (42).""" + # Arrange / Act + n = assumed_occupancy(_w000474.LINE_4_TFA_M2) + + # Assert + assert n == pytest.approx(_w000474.LINE_42_OCCUPANCY, abs=1e-4) + + +def test_assumed_occupancy_matches_elmhurst_worksheet_000490() -> None: + """End-terrace TFA=66.06 m² → N=2.1468 per Elmhurst U985-0001-000490 + line (42).""" + # Arrange / Act + n = assumed_occupancy(_w000490.LINE_4_TFA_M2) + + # Assert + assert n == pytest.approx(_w000490.LINE_42_OCCUPANCY, abs=1e-4) + + +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.""" + # Arrange / Act / Assert + assert assumed_occupancy(13.9) == pytest.approx(1.0, abs=1e-9) + assert assumed_occupancy(10.0) == pytest.approx(1.0, abs=1e-9) + assert assumed_occupancy(0.0) == pytest.approx(1.0, abs=1e-9) diff --git a/packages/domain/src/domain/sap/worksheet/water_heating.py b/packages/domain/src/domain/sap/worksheet/water_heating.py new file mode 100644 index 00000000..d3cf5a7b --- /dev/null +++ b/packages/domain/src/domain/sap/worksheet/water_heating.py @@ -0,0 +1,44 @@ +"""SAP 10.2 §4 — water heating energy requirements. + +Worksheet line refs (xlsx rows 207-304, sheet `NonRegionalWeather`): + (42) assumed occupancy N from Appendix J Table 1b + (42a)m monthly hot water usage for mixer showers + (42b)m monthly hot water usage for baths + (42c)m monthly hot water usage for other uses + (43) annual average hot water usage (litres/day) + (44)m daily hot water usage by month + (45)m energy content of monthly hot water demand + (46)m distribution loss = 0.15 × (45)m + (47)–(56) storage volume + water storage / HIU loss + (57) dedicated solar storage adjustment + (58)–(61) primary loss + combi loss + (62)m total monthly water heat requirement + (63a)–(63d) WWHRS / PV-diverter / Solar / FGHRS reductions + (64)m output from water heater + (65)m heat gains from water heating + +Reference: SAP 10.2 specification §4 (pages 22-31) + Appendix J (pages +84-90); canonical xlsx worked example at the repo root. +""" + +from __future__ import annotations + +from math import exp +from typing import Final + + +_OCCUPANCY_TFA_FLOOR_M2: Final[float] = 13.9 + + +def assumed_occupancy(total_floor_area_m2: float) -> float: + """SAP 10.2 §4 line (42) / Appendix J Table 1b. + + Piecewise occupancy by total floor area: + TFA ≤ 13.9 m² : N = 1 + TFA > 13.9 m² : N = 1 + 1.76 × (1 − exp(−0.000349 × (TFA − 13.9)²)) + + 0.0013 × (TFA − 13.9) + """ + if total_floor_area_m2 <= _OCCUPANCY_TFA_FLOOR_M2: + 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