§5 slice 4: (72) water_heating_gains_monthly_w — bridge from §4 (65)m

Pure unit conversion: G_WH,m = 1000 × (65)m / (n_m × 24). The §4
heat_gains_from_water_heating_monthly_kwh output already encodes the
25%/80% spec-recovery factors for delivered-heat vs pipe-side losses;
this bridge just lands the kWh/month into watts for the §5 sum.

Verified against Elmhurst U985-0001-000490 (72)m row — exact to 4 d.p.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-20 18:04:15 +00:00
parent 984a5b18d6
commit a4d6321f21
2 changed files with 54 additions and 0 deletions

View file

@ -32,6 +32,9 @@ from typing import Final, Optional
_MONTHS_IN_YEAR: Final[int] = 12
_DAYS_PER_MONTH: Final[tuple[int, ...]] = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
_HOURS_PER_DAY: Final[float] = 24.0
_KWH_TO_WH: Final[float] = 1000.0
_METABOLIC_GAIN_W_PER_OCCUPANT_COL_A: Final[float] = 60.0
_LOSSES_W_PER_OCCUPANT_COL_A: Final[float] = -40.0
_COOKING_W_BASE_COL_A: Final[float] = 35.0
@ -70,6 +73,23 @@ def cooking_monthly_w(*, n_occupants: float) -> tuple[float, ...]:
return tuple(gain for _ in range(_MONTHS_IN_YEAR))
def water_heating_gains_monthly_w(
*,
heat_gains_from_water_heating_monthly_kwh: tuple[float, ...],
) -> tuple[float, ...]:
"""SAP 10.2 §5 line (72) — water-heating contribution to internal gains.
Table 5 row "Water heating": G_WH,m = 1000 × (65)m / (n_m × 24). Pure
unit conversion from §4 line (65)m (kWh/month) to watts. (65)m itself
already encodes the 25%/80% spec-recovery factors for delivered-heat
vs pipe-side losses (see §4 heat_gains_from_water_heating_monthly_kwh).
"""
return tuple(
kwh * _KWH_TO_WH / (days * _HOURS_PER_DAY)
for kwh, days in zip(heat_gains_from_water_heating_monthly_kwh, _DAYS_PER_MONTH)
)
_METABOLIC_W_PER_OCCUPANT: Final[float] = 60.0

View file

@ -16,6 +16,7 @@ from domain.sap.worksheet.internal_gains import (
cooking_monthly_w,
losses_monthly_w,
metabolic_monthly_w,
water_heating_gains_monthly_w,
)
@ -87,3 +88,36 @@ def test_cooking_gains_match_table_5_col_a_formula() -> None:
assert len(monthly) == 12
for m, value in enumerate(monthly):
assert value == pytest.approx(35.0 + 7.0 * n, abs=1e-9), f"month {m+1}"
def test_water_heating_gains_bridge_converts_kwh_per_month_to_watts() -> None:
"""SAP 10.2 §5 line (72) — water-heating contribution to internal gains.
Table 5 row "Water heating": G_WH,m = 1000 × (65)m / (n_m × 24) where
(65)m is the §4 line "Heat gains from water heating" in kWh/month
and n_m is days in month m. Pure unit conversion: kWh/month W.
Verified against the Elmhurst U985-0001-000490 worksheet:
(65) Jan = 75.2034 kWh/month
(72) Jan = 75.2034 × 1000 / (24 × 31) = 101.0798 W
matches the worksheet (72)m row to 4 d.p.
"""
# Arrange — 000490's full LINE_65_M tuple feeds the bridge.
heat_gains_kwh = (
75.2034, 66.4381, 70.4305, 61.5896, 59.4711, 53.3391,
52.4705, 54.6991, 55.4582, 62.1383, 66.4342, 74.3403,
)
expected_w = (
101.0798, 98.8663, 94.6647, 85.5412, 79.9343, 74.0821,
70.5249, 73.5203, 77.0253, 83.5192, 92.2698, 99.9197,
)
# Act
monthly = water_heating_gains_monthly_w(
heat_gains_from_water_heating_monthly_kwh=heat_gains_kwh,
)
# Assert
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}"