mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
§8 slice 1: space_heating_monthly_kwh orchestrator + summer clamp + SpaceHeatingResult
Adds the §8 orchestrator producing (95)..(99) line refs for all 12 months. Composes the existing monthly_heat_requirement_kwh leaf with the spec inclusion rule (Table 9c step 10 final clause): "Include the heating requirement for each month from October to May (disregarding June to September)" Jun..Sep are zeroed regardless of computed value, on top of the per-month value clamp (< 1 kWh / negative). SpaceHeatingResult exposes (95) useful gains, (97) heat loss rate, (98a) space heating requirement, (98b) solar space heating (always 0 — Appendix H deferred), (98c) total, Σ(98c) annual + (99) per-m². All length-12 tuples + 2 scalars. Driven by Elmhurst 000490 (98c) annual = 11183.2752 kWh to abs=5e-3 kWh. Without the summer clamp the current calculator over-predicts annual by +1575 kWh (+14%) on this fixture; the clamp closes the gap to spec. Slice 3 wires CalculatorInputs.space_heating_monthly_kwh + cert_to_inputs; calculator stops calling monthly_heat_requirement_kwh inline. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
eec8fb6f4f
commit
9113f30aa8
2 changed files with 150 additions and 1 deletions
|
|
@ -15,11 +15,17 @@ Reference: SAP 10.3 specification (13-01-2026) Table 9c (page 185).
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Final
|
||||
|
||||
|
||||
_MIN_KWH_PER_MONTH: Final[float] = 1.0
|
||||
_WH_TO_KWH_PER_DAY: Final[float] = 0.024 # 24 h / 1000
|
||||
_DAYS_IN_MONTH: Final[tuple[int, ...]] = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
|
||||
# SAP10.2 Table 9c step 10: "Include the heating requirement for each month
|
||||
# from October to May (disregarding June to September)." Set Q_heat to zero
|
||||
# in Jun..Sep regardless of computed value. Indices 5..8 inclusive (zero-based).
|
||||
_SUMMER_MONTH_INDICES: Final[frozenset[int]] = frozenset({5, 6, 7, 8})
|
||||
|
||||
|
||||
def monthly_heat_requirement_kwh(
|
||||
|
|
@ -43,3 +49,89 @@ def monthly_heat_requirement_kwh(
|
|||
if q_heat < _MIN_KWH_PER_MONTH:
|
||||
return 0.0
|
||||
return q_heat
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SpaceHeatingResult:
|
||||
"""SAP 10.2 §8 worksheet line refs (95)..(99).
|
||||
|
||||
Returned by `space_heating_monthly_kwh`. Downstream calculator consumes
|
||||
`total_space_heating_monthly_kwh` (98c) directly to drive fuel-cost +
|
||||
rating chains; per-line tuples are exposed for worksheet conformance +
|
||||
audit. Field names mirror the SAP 10.2 line refs.
|
||||
"""
|
||||
|
||||
useful_gains_monthly_w: tuple[float, ...] # (95)
|
||||
heat_loss_rate_monthly_w: tuple[float, ...] # (97)
|
||||
space_heating_requirement_monthly_kwh: tuple[float, ...] # (98a)
|
||||
solar_space_heating_monthly_kwh: tuple[float, ...] # (98b)
|
||||
total_space_heating_monthly_kwh: tuple[float, ...] # (98c)
|
||||
total_space_heating_kwh_per_yr: float # Σ(98c)
|
||||
space_heating_per_m2_kwh: float # (99)
|
||||
|
||||
|
||||
def space_heating_monthly_kwh(
|
||||
*,
|
||||
monthly_heat_transfer_coefficient_w_per_k: tuple[float, ...],
|
||||
monthly_internal_temperature_c: tuple[float, ...],
|
||||
monthly_external_temperature_c: tuple[float, ...],
|
||||
monthly_utilisation_factor: tuple[float, ...],
|
||||
monthly_total_gains_w: tuple[float, ...],
|
||||
total_floor_area_m2: float,
|
||||
) -> SpaceHeatingResult:
|
||||
"""SAP 10.2 §8 orchestrator — produce (95)..(99) line refs for all months.
|
||||
|
||||
Composes the existing single-month leaf with the spec inclusion rule:
|
||||
Jun..Sep are zeroed (Table 9c step 10) regardless of computed value,
|
||||
on top of the per-month value clamp (< 1 kWh or negative).
|
||||
|
||||
Solar space heating (98b) — Appendix H — is always 0 in this slice; no
|
||||
Elmhurst fixture lodges a solar space heating system. (98c) = (98a) for
|
||||
the current corpus.
|
||||
|
||||
Inputs are length-12 Jan..Dec tuples. `total_floor_area_m2` only drives
|
||||
the (99) per-m² aggregate; everything else is per-month physics.
|
||||
"""
|
||||
useful_gains: list[float] = []
|
||||
heat_loss_rate: list[float] = []
|
||||
q_heat_98a: list[float] = []
|
||||
q_solar_98b: list[float] = []
|
||||
q_total_98c: list[float] = []
|
||||
|
||||
for m in range(12):
|
||||
h = monthly_heat_transfer_coefficient_w_per_k[m]
|
||||
t_i = monthly_internal_temperature_c[m]
|
||||
t_e = monthly_external_temperature_c[m]
|
||||
eta = monthly_utilisation_factor[m]
|
||||
gains = monthly_total_gains_w[m]
|
||||
|
||||
useful_gains.append(eta * gains)
|
||||
heat_loss_rate.append(h * (t_i - t_e))
|
||||
|
||||
q98a = monthly_heat_requirement_kwh(
|
||||
heat_transfer_coefficient_w_per_k=h,
|
||||
internal_temperature_c=t_i,
|
||||
external_temperature_c=t_e,
|
||||
utilisation_factor=eta,
|
||||
total_gains_w=gains,
|
||||
days_in_month=_DAYS_IN_MONTH[m],
|
||||
)
|
||||
# Spec inclusion rule: Jun..Sep do not contribute regardless of value.
|
||||
if m in _SUMMER_MONTH_INDICES:
|
||||
q98a = 0.0
|
||||
q_heat_98a.append(q98a)
|
||||
q_solar_98b.append(0.0)
|
||||
q_total_98c.append(q98a)
|
||||
|
||||
annual_98c = sum(q_total_98c)
|
||||
per_m2_99 = annual_98c / total_floor_area_m2 if total_floor_area_m2 > 0 else 0.0
|
||||
|
||||
return SpaceHeatingResult(
|
||||
useful_gains_monthly_w=tuple(useful_gains),
|
||||
heat_loss_rate_monthly_w=tuple(heat_loss_rate),
|
||||
space_heating_requirement_monthly_kwh=tuple(q_heat_98a),
|
||||
solar_space_heating_monthly_kwh=tuple(q_solar_98b),
|
||||
total_space_heating_monthly_kwh=tuple(q_total_98c),
|
||||
total_space_heating_kwh_per_yr=annual_98c,
|
||||
space_heating_per_m2_kwh=per_m2_99,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,64 @@ 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
|
||||
from domain.sap.worksheet.space_heating import (
|
||||
SpaceHeatingResult,
|
||||
monthly_heat_requirement_kwh,
|
||||
space_heating_monthly_kwh,
|
||||
)
|
||||
|
||||
|
||||
# Worksheet U985-0001-000490 (UK-avg weather, region 0) inputs for §8.
|
||||
# (93)m, (94)m from §7; (84)m gains = §5 (73) + §6 (83); (39)m HTC pinned
|
||||
# from PDF; (96)m ext temp from Appendix U Table U1 region 0; TFA pinned.
|
||||
_W000490_HTC_W_PER_K: tuple[float, ...] = (
|
||||
289.6265, 288.8665, 288.1216, 284.6229, 283.9683, 280.9211,
|
||||
280.9211, 280.3568, 282.0949, 283.9683, 285.2926, 286.6770,
|
||||
)
|
||||
_W000490_T_INT_C: tuple[float, ...] = (
|
||||
15.1899, 15.4380, 15.9663, 16.7359, 17.5529, 18.3114,
|
||||
18.7103, 18.6666, 18.1104, 17.0915, 16.0323, 15.1662,
|
||||
)
|
||||
_W000490_T_EXT_C: tuple[float, ...] = (
|
||||
4.3, 4.9, 6.5, 8.9, 11.7, 14.6, 16.6, 16.4, 14.1, 10.6, 7.1, 4.2,
|
||||
)
|
||||
_W000490_ETA_WHOLE: tuple[float, ...] = (
|
||||
0.9735, 0.9659, 0.9524, 0.9261, 0.8752, 0.7738,
|
||||
0.6027, 0.6474, 0.8348, 0.9303, 0.9644, 0.9759,
|
||||
)
|
||||
_W000490_TOTAL_GAINS_W: tuple[float, ...] = (
|
||||
595.2863, 661.0571, 716.2901, 763.4028, 790.9684, 764.1081,
|
||||
732.0878, 691.6074, 654.2542, 610.6983, 573.2833, 568.9492,
|
||||
)
|
||||
_W000490_TFA_M2: float = 66.06
|
||||
# (98c) annual from PDF — Σ Jan..May + Σ Oct..Dec, Jun..Sep clamped to 0
|
||||
# per SAP10.2 Table 9c step 10 inclusion rule.
|
||||
_W000490_ANNUAL_98C_KWH: float = 11183.2752
|
||||
|
||||
|
||||
def test_space_heating_monthly_kwh_reproduces_000490_annual_with_summer_clamp() -> None:
|
||||
# Arrange — Elmhurst 000490 §7 outputs + §3/§5/§6 inputs above. Expected
|
||||
# annual (98c) = 11183.2752 kWh. Spec inclusion rule (Table 9c step 10):
|
||||
# June-September contribute zero regardless of computed value.
|
||||
|
||||
# Act
|
||||
result = space_heating_monthly_kwh(
|
||||
monthly_heat_transfer_coefficient_w_per_k=_W000490_HTC_W_PER_K,
|
||||
monthly_internal_temperature_c=_W000490_T_INT_C,
|
||||
monthly_external_temperature_c=_W000490_T_EXT_C,
|
||||
monthly_utilisation_factor=_W000490_ETA_WHOLE,
|
||||
monthly_total_gains_w=_W000490_TOTAL_GAINS_W,
|
||||
total_floor_area_m2=_W000490_TFA_M2,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert isinstance(result, SpaceHeatingResult)
|
||||
assert result.total_space_heating_kwh_per_yr == pytest.approx(
|
||||
_W000490_ANNUAL_98C_KWH, abs=5e-3
|
||||
)
|
||||
# Summer-clamp invariant — Jun..Sep months always zero per spec.
|
||||
for m in range(5, 9): # indices 5..8 = Jun..Sep
|
||||
assert result.total_space_heating_monthly_kwh[m] == 0.0, f"(98c) month {m+1}"
|
||||
|
||||
|
||||
def test_typical_winter_month_returns_positive_kwh() -> None:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue