mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
slice S-A5c: heating utilisation factor η (SAP 10.3 Table 9a)
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 γ.
This commit is contained in:
parent
57bf7833a9
commit
e403e2302c
2 changed files with 147 additions and 0 deletions
|
|
@ -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)
|
||||
|
|
@ -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))
|
||||
Loading…
Add table
Reference in a new issue