mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
slice S-A5e: monthly space-heating requirement (SAP 10.3 Table 9c step 10)
Ninth slice of the SAP10 Calculator Session A (ADR-0009). Ships
monthly_heat_requirement_kwh implementing the Table 9c step-10 formula:
L_m = H × (T_i,m − T_e,m) (W)
Q_heat,m = 0.024 × (L_m − η_m × G_m) × n_m (kWh)
with the table's clamp: Q_heat is set to 0 when negative or below 1 kWh
per month (summer months and well-insulated dwellings in shoulder
months).
The orchestrator (S-A6) iterates utilisation factor + mean internal
temperature until they converge before calling this function.
5 AAA cycles cover: typical-winter-month hand-computed worked example,
summer month with gains exceeding losses clamping to 0, gains-scaling
direction check, external-temperature direction check, and the sub-1-kWh
clamp per the Table 9c note.
This commit is contained in:
parent
8c21b399c6
commit
c0afe3592f
2 changed files with 159 additions and 0 deletions
45
packages/domain/src/domain/sap/worksheet/space_heating.py
Normal file
45
packages/domain/src/domain/sap/worksheet/space_heating.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
"""SAP 10.3 Table 9c step 10 — monthly space-heating requirement.
|
||||
|
||||
Final step of the heating worksheet: the heat the heating system has to
|
||||
deliver to maintain the mean internal temperature, given the loss rate to
|
||||
the outside and the gains the dwelling has already accumulated.
|
||||
|
||||
L_m = H × (T_i,m − T_e,m) (W)
|
||||
Q_heat,m = 0.024 × (L_m − η_m × G_m) × n_m (kWh)
|
||||
|
||||
If Q_heat would be negative or below 1 kWh in any month, set it to 0 per
|
||||
the Table 9c clamp.
|
||||
|
||||
Reference: SAP 10.3 specification (13-01-2026) Table 9c (page 185).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Final
|
||||
|
||||
|
||||
_MIN_KWH_PER_MONTH: Final[float] = 1.0
|
||||
_WH_TO_KWH_PER_DAY: Final[float] = 0.024 # 24 h / 1000
|
||||
|
||||
|
||||
def monthly_heat_requirement_kwh(
|
||||
*,
|
||||
heat_transfer_coefficient_w_per_k: float,
|
||||
internal_temperature_c: float,
|
||||
external_temperature_c: float,
|
||||
utilisation_factor: float,
|
||||
total_gains_w: float,
|
||||
days_in_month: int,
|
||||
) -> float:
|
||||
"""SAP 10.3 Table 9c step 10. Returns delivered kWh required for the
|
||||
month; clamps to 0 when below 1 kWh or negative."""
|
||||
loss_rate_w = heat_transfer_coefficient_w_per_k * (
|
||||
internal_temperature_c - external_temperature_c
|
||||
)
|
||||
useful_loss_w = loss_rate_w - utilisation_factor * total_gains_w
|
||||
if useful_loss_w <= 0:
|
||||
return 0.0
|
||||
q_heat = _WH_TO_KWH_PER_DAY * useful_loss_w * days_in_month
|
||||
if q_heat < _MIN_KWH_PER_MONTH:
|
||||
return 0.0
|
||||
return q_heat
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
"""Tests for SAP 10.3 Table 9c step 10 — monthly space-heating requirement.
|
||||
|
||||
Final step of the heating worksheet: convert the monthly loss rate minus
|
||||
utilised gains into a delivered-kWh figure. Clamped to zero if it would be
|
||||
negative or below 1 kWh/month per the Table 9c note.
|
||||
|
||||
Reference: SAP 10.3 specification (13-01-2026) Table 9c (page 185).
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from domain.sap.worksheet.space_heating import monthly_heat_requirement_kwh
|
||||
|
||||
|
||||
def test_typical_winter_month_returns_positive_kwh() -> None:
|
||||
# Arrange — Mid-mass dwelling, January conditions.
|
||||
# H = 200 W/K, T_i = 18 °C, T_e = 5 °C, G = 300 W, η = 0.9, n_m = 31.
|
||||
# L = 200 × (18 − 5) = 2600 W
|
||||
# useful_loss = L − η·G = 2600 − 0.9 × 300 = 2330 W
|
||||
# Q_heat = 0.024 × 2330 × 31 ≈ 1733.5 kWh.
|
||||
|
||||
# Act
|
||||
result = monthly_heat_requirement_kwh(
|
||||
heat_transfer_coefficient_w_per_k=200.0,
|
||||
internal_temperature_c=18.0,
|
||||
external_temperature_c=5.0,
|
||||
utilisation_factor=0.9,
|
||||
total_gains_w=300.0,
|
||||
days_in_month=31,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result == pytest.approx(1733.5, abs=5.0)
|
||||
|
||||
|
||||
def test_summer_month_with_gains_above_losses_clamps_to_zero() -> None:
|
||||
# Arrange — July: T_i ≈ 21 °C indoor, T_e ≈ 17 °C, modest loss rate but
|
||||
# huge solar + internal gains exceed it. Loss = 200 × 4 = 800 W, η·G = 0.5 × 2000 = 1000 W.
|
||||
# useful_loss = -200 W → Q_heat = 0 by the Table 9c clamp.
|
||||
|
||||
# Act
|
||||
result = monthly_heat_requirement_kwh(
|
||||
heat_transfer_coefficient_w_per_k=200.0,
|
||||
internal_temperature_c=21.0,
|
||||
external_temperature_c=17.0,
|
||||
utilisation_factor=0.5,
|
||||
total_gains_w=2000.0,
|
||||
days_in_month=31,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result == 0.0
|
||||
|
||||
|
||||
def test_more_gains_reduce_monthly_heat_requirement() -> None:
|
||||
# Arrange — Direction check: higher internal+solar gains for the same
|
||||
# losses must reduce the heating requirement linearly via the η·G term.
|
||||
|
||||
# Act
|
||||
low_gains = monthly_heat_requirement_kwh(
|
||||
heat_transfer_coefficient_w_per_k=200.0,
|
||||
internal_temperature_c=18.0, external_temperature_c=5.0,
|
||||
utilisation_factor=0.9, total_gains_w=100.0, days_in_month=31,
|
||||
)
|
||||
high_gains = monthly_heat_requirement_kwh(
|
||||
heat_transfer_coefficient_w_per_k=200.0,
|
||||
internal_temperature_c=18.0, external_temperature_c=5.0,
|
||||
utilisation_factor=0.9, total_gains_w=600.0, days_in_month=31,
|
||||
)
|
||||
|
||||
# Assert — drop = 0.9 × (600 − 100) × 0.024 × 31 = 334.8 kWh.
|
||||
assert low_gains - high_gains == pytest.approx(334.8, abs=2.0)
|
||||
|
||||
|
||||
def test_warmer_external_temperature_reduces_monthly_heat_requirement() -> None:
|
||||
# Arrange — Direction check: same inputs but warmer outdoor temp drops
|
||||
# loss rate proportionally. ΔL = 200 × ΔT × 0.024 × 31 = 148.8 kWh per °C.
|
||||
|
||||
# Act
|
||||
cold = monthly_heat_requirement_kwh(
|
||||
heat_transfer_coefficient_w_per_k=200.0,
|
||||
internal_temperature_c=18.0, external_temperature_c=0.0,
|
||||
utilisation_factor=0.9, total_gains_w=300.0, days_in_month=31,
|
||||
)
|
||||
mild = monthly_heat_requirement_kwh(
|
||||
heat_transfer_coefficient_w_per_k=200.0,
|
||||
internal_temperature_c=18.0, external_temperature_c=10.0,
|
||||
utilisation_factor=0.9, total_gains_w=300.0, days_in_month=31,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert cold > mild
|
||||
assert (cold - mild) == pytest.approx(10 * 200 * 0.024 * 31, abs=2.0)
|
||||
|
||||
|
||||
def test_sub_one_kwh_requirement_clamps_to_zero_per_table_9c_note() -> None:
|
||||
# Arrange — Table 9c clamp: "Set Q_heat to 0 if negative or less than
|
||||
# 1 kWh." Engineer a barely-positive useful loss so the unclamped result
|
||||
# falls below 1 kWh. With H=10, ΔT=0.1, gains=0, days=30:
|
||||
# useful_loss = 1 W
|
||||
# Q_heat = 0.024 × 1 × 30 = 0.72 kWh -> clamped to 0.
|
||||
|
||||
# Act
|
||||
result = monthly_heat_requirement_kwh(
|
||||
heat_transfer_coefficient_w_per_k=10.0,
|
||||
internal_temperature_c=18.0,
|
||||
external_temperature_c=17.9,
|
||||
utilisation_factor=0.9,
|
||||
total_gains_w=0.0,
|
||||
days_in_month=30,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result == 0.0
|
||||
Loading…
Add table
Reference in a new issue