diff --git a/packages/domain/src/domain/sap/worksheet/internal_gains.py b/packages/domain/src/domain/sap/worksheet/internal_gains.py index f52f3116..d93f6584 100644 --- a/packages/domain/src/domain/sap/worksheet/internal_gains.py +++ b/packages/domain/src/domain/sap/worksheet/internal_gains.py @@ -30,6 +30,12 @@ from dataclasses import dataclass from math import cos, exp, pi from typing import Final, Optional +_DAYS_PER_YEAR: Final[float] = 365.0 +_APPLIANCES_E_A_COEFF: Final[float] = 207.8 +_APPLIANCES_E_A_EXPONENT: Final[float] = 0.4714 +_APPLIANCES_MONTHLY_AMPLITUDE: Final[float] = 0.157 +_APPLIANCES_MONTHLY_PHASE: Final[float] = 1.78 + _MONTHS_IN_YEAR: Final[int] = 12 _DAYS_PER_MONTH: Final[tuple[int, ...]] = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) @@ -73,6 +79,38 @@ def cooking_monthly_w(*, n_occupants: float) -> tuple[float, ...]: return tuple(gain for _ in range(_MONTHS_IN_YEAR)) +def appliances_monthly_w( + *, + total_floor_area_m2: float, + n_occupants: float, +) -> tuple[float, ...]: + """SAP 10.2 §5 line (68) — appliance gains in watts per month. + + Appendix L equations L13, L14, L16a (Column A, typical gains): + E_A = 207.8 × (TFA × N)^0.4714 [kWh/yr] + E_A,m = E_A × [1 + 0.157 × cos(2π × (m - 1.78) / 12)] × n_m / 365 [kWh/mo] + G_A,m = E_A,m × 1000 / (24 × n_m) [W] + + The cosine peaks ~end-of-January (m=1.78) and troughs ~end-of-July. + All electrical appliance energy stays as internal heat — full 1.0× + conversion. Column B's L16 (0.67×) reduced form is for new-build + DPER/TPER only. + """ + e_a_annual = ( + _APPLIANCES_E_A_COEFF + * (total_floor_area_m2 * n_occupants) ** _APPLIANCES_E_A_EXPONENT + ) + monthly: list[float] = [] + for m_idx, days in enumerate(_DAYS_PER_MONTH): + m = m_idx + 1 + factor = 1.0 + _APPLIANCES_MONTHLY_AMPLITUDE * cos( + 2.0 * pi * (m - _APPLIANCES_MONTHLY_PHASE) / _MONTHS_IN_YEAR + ) + e_a_m_kwh = e_a_annual * factor * days / _DAYS_PER_YEAR + monthly.append(e_a_m_kwh * _KWH_TO_WH / (_HOURS_PER_DAY * days)) + return tuple(monthly) + + def water_heating_gains_monthly_w( *, heat_gains_from_water_heating_monthly_kwh: tuple[float, ...], diff --git a/packages/domain/src/domain/sap/worksheet/tests/test_internal_gains.py b/packages/domain/src/domain/sap/worksheet/tests/test_internal_gains.py index 73a13a8f..a4a4aafb 100644 --- a/packages/domain/src/domain/sap/worksheet/tests/test_internal_gains.py +++ b/packages/domain/src/domain/sap/worksheet/tests/test_internal_gains.py @@ -13,6 +13,7 @@ Table 5a + Appendix L (lighting/appliances/cooking) + Appendix J Table 1b import pytest from domain.sap.worksheet.internal_gains import ( + appliances_monthly_w, cooking_monthly_w, losses_monthly_w, metabolic_monthly_w, @@ -121,3 +122,36 @@ def test_water_heating_gains_bridge_converts_kwh_per_month_to_watts() -> None: assert len(monthly) == 12 for m, (actual, exp) in enumerate(zip(monthly, expected_w)): assert actual == pytest.approx(exp, abs=1e-3), f"month {m+1}" + + +def test_appliances_gains_match_appendix_l13_l14_l16a_for_000490() -> None: + """SAP 10.2 Appendix L equations L13/L14/L16a: + E_A = 207.8 × (TFA × N)^0.4714 [kWh/yr] + E_A,m = E_A × [1 + 0.157 × cos(2π × (m - 1.78) / 12)] × n_m / 365 [kWh/mo] + G_A,m = E_A,m × 1000 / (24 × n_m) [W] + + Verified against U985-0001-000490 worksheet (68)m row. For + TFA=66.06, N=2.1468: + E_A = 207.8 × 141.84^0.4714 ≈ 2147.8 kWh/yr + Jan factor = 1 + 0.157 × cos(2π × -0.78/12) ≈ 1.1441 + E_A,Jan ≈ 208.66 kWh → G_A,Jan ≈ 280.46 W + matches worksheet 280.4965 to ≈0.04 W (display rounding from E_A^0.4714). + + Table 5 Column A applies (rating + cooling); Column B's 0.67× + reduced-gain form (L16) is deferred to a future new-build slice. + """ + # Arrange + tfa = 66.06 + n = 2.1468 + expected_w = ( + 280.4965, 283.4071, 276.0723, 260.4574, 240.7463, 222.2207, + 209.8445, 206.9338, 214.2686, 229.8835, 249.5946, 268.1202, + ) + + # Act + monthly = appliances_monthly_w(total_floor_area_m2=tfa, n_occupants=n) + + # Assert + assert len(monthly) == 12 + for m, (actual, exp) in enumerate(zip(monthly, expected_w)): + assert actual == pytest.approx(exp, abs=5e-2), f"month {m+1}"