From e403e2302c2154fc29ae2895341d9738a1502c41 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 18 May 2026 08:38:03 +0000 Subject: [PATCH] =?UTF-8?q?slice=20S-A5c:=20heating=20utilisation=20factor?= =?UTF-8?q?=20=CE=B7=20(SAP=2010.3=20Table=209a)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seventh slice of the SAP10 Calculator Session A (ADR-0009). Ships utilisation_factor(*, total_gains_w, heat_loss_rate_w, time_constant_h) implementing SAP 10.3 Table 9a: a = 1 + τ / 15 γ = G / L if γ > 0 and γ ≠ 1: η = (1 − γ^a) / (1 − γ^(a+1)) if γ = 1: η = a / (a + 1) if heat_loss_rate ≤ 0: η = 1 (dwelling in net surplus) η caps the contribution of internal + solar gains when they outpace the heat-loss rate. The orchestrator computes time_constant_h = TMP / (3.6 × HLP) and passes it in here; that's a future slice. 5 AAA cycles cover: small γ → η ≈ 1, γ = 1 special-case formula, zero/negative heat loss returning η = 1, large γ dropping η well below 0.5, and higher τ (more thermal mass) raising η for the same γ. --- .../tests/test_utilisation_factor.py | 104 ++++++++++++++++++ .../sap/worksheet/utilisation_factor.py | 43 ++++++++ 2 files changed, 147 insertions(+) create mode 100644 packages/domain/src/domain/sap/worksheet/tests/test_utilisation_factor.py create mode 100644 packages/domain/src/domain/sap/worksheet/utilisation_factor.py diff --git a/packages/domain/src/domain/sap/worksheet/tests/test_utilisation_factor.py b/packages/domain/src/domain/sap/worksheet/tests/test_utilisation_factor.py new file mode 100644 index 00000000..59731a75 --- /dev/null +++ b/packages/domain/src/domain/sap/worksheet/tests/test_utilisation_factor.py @@ -0,0 +1,104 @@ +"""Tests for SAP 10.3 Table 9a — heating utilisation factor η. + +Reduces the contribution of internal + solar gains when they exceed the +heat loss rate (a hot dwelling can't bank extra heat). Function of the +gain-to-loss ratio γ and the dwelling's time constant τ. + +Reference: SAP 10.3 specification (13-01-2026) Table 9a (page 184). +""" + +import pytest + +from domain.sap.worksheet.utilisation_factor import utilisation_factor + + +def test_small_gain_loss_ratio_returns_eta_close_to_one() -> None: + # Arrange — Per Table 9a, when γ = G/L is small, η is close to 1 + # (almost all gains usefully offset losses). Hand-computed: + # G = 100 W, L = 1000 W -> γ = 0.1 + # τ = 10 h -> a = 1 + 10/15 ≈ 1.667 + # η = (1 - 0.1^1.667) / (1 - 0.1^2.667) + # = (1 - 0.02147) / (1 - 0.00461) + # ≈ 0.978 / 0.995 + # ≈ 0.983 + + # Act + result = utilisation_factor( + total_gains_w=100.0, + heat_loss_rate_w=1000.0, + time_constant_h=10.0, + ) + + # Assert + assert result == pytest.approx(0.983, abs=0.005) + + +def test_gain_loss_ratio_equal_one_returns_a_over_a_plus_one() -> None: + # Arrange — Per Table 9a the γ = 1 branch: η = a / (a + 1). For τ = 15 h + # this gives a = 2 and η = 2/3 ≈ 0.667. Avoids the 0/0 in the general + # formula at γ = 1. + + # Act + result = utilisation_factor( + total_gains_w=500.0, + heat_loss_rate_w=500.0, + time_constant_h=15.0, + ) + + # Assert + assert result == pytest.approx(2.0 / 3.0, abs=0.005) + + +def test_zero_or_negative_heat_loss_returns_eta_one() -> None: + # Arrange — Table 9a edge case: when L ≤ 0 (the dwelling is in net heat + # surplus over the month), η = 1 by convention so the heating + # requirement Q_heat = 0.024 × (L − η·G) × n_m clamps to zero. + + # Act + zero_loss = utilisation_factor( + total_gains_w=500.0, heat_loss_rate_w=0.0, time_constant_h=10.0, + ) + negative_loss = utilisation_factor( + total_gains_w=500.0, heat_loss_rate_w=-100.0, time_constant_h=10.0, + ) + + # Assert + assert zero_loss == 1.0 + assert negative_loss == 1.0 + + +def test_large_gain_loss_ratio_drops_eta_well_below_half() -> None: + # Arrange — When γ = 3 (gains three times loss rate), η drops to ≈ 0.30 + # for τ = 10 h (a = 1.667). Real-world scenario: well-insulated home + # with strong solar gains in a mild month — most of the gain doesn't + # offset heating because the dwelling overheats. + + # Act + result = utilisation_factor( + total_gains_w=1500.0, + heat_loss_rate_w=500.0, # γ = 3.0 + time_constant_h=10.0, + ) + + # Assert + assert result == pytest.approx(0.296, abs=0.01) + assert result < 0.5 + + +def test_higher_time_constant_increases_eta_for_same_gain_loss_ratio() -> None: + # Arrange — Heavier dwellings (more thermal mass, higher τ) use gains + # better because the building doesn't overheat as quickly. Holding + # γ = 1.5 fixed, doubling τ from 10 h to 40 h should noticeably raise η. + + # Act + light = utilisation_factor( + total_gains_w=750.0, heat_loss_rate_w=500.0, time_constant_h=10.0, + ) + heavy = utilisation_factor( + total_gains_w=750.0, heat_loss_rate_w=500.0, time_constant_h=40.0, + ) + + # Assert + assert heavy > light + assert light == pytest.approx(0.496, abs=0.01) + assert heavy == pytest.approx(0.608, abs=0.01) diff --git a/packages/domain/src/domain/sap/worksheet/utilisation_factor.py b/packages/domain/src/domain/sap/worksheet/utilisation_factor.py new file mode 100644 index 00000000..cb702027 --- /dev/null +++ b/packages/domain/src/domain/sap/worksheet/utilisation_factor.py @@ -0,0 +1,43 @@ +"""SAP 10.3 Table 9a — heating utilisation factor η. + +η reduces the contribution of internal + solar gains when they outpace the +dwelling's heat-loss rate. A well-insulated dwelling with large solar gains +in October can't fully use those gains — it's already warm enough. + +Formula per Table 9a: + + a = 1 + τ / 15 where τ is the dwelling time constant (h) + γ = G / L gain-to-loss ratio (W / W) + if γ > 0 and γ ≠ 1: η = (1 − γ^a) / (1 − γ^(a+1)) + if γ = 1: η = a / (a + 1) + if γ ≤ 0: η = 1 + +The time constant τ = TMP / (3.6 × HLP) comes from the dwelling's thermal +mass parameter and heat-loss parameter; computed by the orchestrator and +passed in here. + +Reference: SAP 10.3 specification (13-01-2026) Table 9a (page 184). +""" + +from __future__ import annotations + + +def utilisation_factor( + *, + total_gains_w: float, + heat_loss_rate_w: float, + time_constant_h: float, +) -> float: + """SAP 10.3 Table 9a heating utilisation factor η. + + γ = total_gains_w / heat_loss_rate_w; η ∈ (0, 1]. When the heat-loss + rate is non-positive (dwelling already balanced or gaining), η = 1 + so the gains are fully credited. + """ + if heat_loss_rate_w <= 0: + return 1.0 + gamma = total_gains_w / heat_loss_rate_w + a = 1.0 + time_constant_h / 15.0 + if gamma == 1.0: + return a / (a + 1.0) + return (1.0 - gamma**a) / (1.0 - gamma ** (a + 1.0))