mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
§4 slice 8: line (64)m output from water heater
(64)m = max(0, (62)m + (63a)m + (63b)m + (63c)m + (63d)m) The four (63 a-d) inputs are WWHRS, PV-diverter, solar HW and FGHRS contributions — entered as negative quantities so the formula uses +, not −. The max-clamp guards "if (64)m < 0 then set to 0" per the spec worksheet text: a renewable-heavy summer can't show negative delivered heat. Both Elmhurst non-RR fixtures lodge zero for all four (no WWHRS, no PV diverter, no solar, no FGHRS), so (64)m = (62)m for every month. Validated end-to-end on both with abs=1e-3 kWh tolerance. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
bfba610b70
commit
feef819814
4 changed files with 104 additions and 0 deletions
|
|
@ -197,3 +197,21 @@ LINE_46_M_DISTRIBUTION_LOSS_KWH: tuple[float, ...] = (
|
|||
26.1600, 23.0055, 24.1612, 20.6307, 19.5714, 17.1754,
|
||||
16.6394, 17.5728, 18.0628, 20.6883, 22.6596, 25.7985,
|
||||
)
|
||||
LINE_56_M_STORAGE_LOSS_KWH: tuple[float, ...] = (0.0,) * 12 # combi, no cylinder
|
||||
LINE_57_M_SOLAR_STORAGE_KWH: tuple[float, ...] = (0.0,) * 12 # no solar HW
|
||||
LINE_59_M_PRIMARY_LOSS_KWH: tuple[float, ...] = (0.0,) * 12 # combi, no primary circuit
|
||||
# (61)m combi loss values from worksheet — derived from Table 3b (PCDB-
|
||||
# tested boiler). Not yet computable by our cascade; lodged for assertion.
|
||||
LINE_61_M_COMBI_LOSS_KWH: tuple[float, ...] = (
|
||||
28.7238, 25.9337, 28.6905, 27.7191, 28.6040, 27.6419,
|
||||
28.5422, 28.5649, 27.6693, 28.6326, 27.7530, 28.7178,
|
||||
)
|
||||
LINE_62_M_TOTAL_WH_KWH: tuple[float, ...] = (
|
||||
203.1238, 179.3035, 189.7652, 165.2572, 159.0798, 142.1443,
|
||||
139.4718, 145.7166, 148.0876, 166.5545, 178.8167, 200.7079,
|
||||
)
|
||||
LINE_63A_M_WWHRS_KWH: tuple[float, ...] = (0.0,) * 12
|
||||
LINE_63B_M_PV_DIVERTER_KWH: tuple[float, ...] = (0.0,) * 12
|
||||
LINE_63C_M_SOLAR_KWH: tuple[float, ...] = (0.0,) * 12
|
||||
LINE_63D_M_FGHRS_KWH: tuple[float, ...] = (0.0,) * 12
|
||||
LINE_64_M_OUTPUT_FROM_WH_KWH: tuple[float, ...] = LINE_62_M_TOTAL_WH_KWH
|
||||
|
|
|
|||
|
|
@ -196,3 +196,8 @@ LINE_62_M_TOTAL_WH_KWH: tuple[float, ...] = (
|
|||
238.8196, 211.2342, 224.4651, 197.4680, 191.5045, 172.6544,
|
||||
170.4499, 177.1525, 179.0276, 199.5259, 212.0383, 236.2237,
|
||||
)
|
||||
LINE_63A_M_WWHRS_KWH: tuple[float, ...] = (0.0,) * 12 # no WWHRS
|
||||
LINE_63B_M_PV_DIVERTER_KWH: tuple[float, ...] = (0.0,) * 12 # no PV diverter
|
||||
LINE_63C_M_SOLAR_KWH: tuple[float, ...] = (0.0,) * 12 # no solar HW
|
||||
LINE_63D_M_FGHRS_KWH: tuple[float, ...] = (0.0,) * 12 # no FGHRS
|
||||
LINE_64_M_OUTPUT_FROM_WH_KWH: tuple[float, ...] = LINE_62_M_TOTAL_WH_KWH # no reductions
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ from domain.sap.worksheet.water_heating import (
|
|||
hot_water_baths_monthly_l_per_day,
|
||||
hot_water_mixer_showers_monthly_l_per_day,
|
||||
hot_water_other_uses_monthly_l_per_day,
|
||||
output_from_water_heater_monthly_kwh,
|
||||
total_hot_water_monthly_l_per_day,
|
||||
total_water_heating_demand_monthly_kwh,
|
||||
)
|
||||
|
|
@ -503,6 +504,56 @@ def test_total_water_heating_demand_matches_elmhurst_line_62_for_000490() -> Non
|
|||
assert actual == pytest.approx(exp, abs=1e-3), f"month {m+1}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("fixture", (_w000474, _w000490), ids=("000474", "000490"))
|
||||
def test_output_from_water_heater_matches_elmhurst_line_64(fixture) -> None: # type: ignore[no-untyped-def]
|
||||
"""SAP10.2 §4 line (64)m:
|
||||
(64)m = max(0, (62)m + (63a)m + (63b)m + (63c)m + (63d)m)
|
||||
|
||||
The (63 a-d) WWHRS / PV-diverter / Solar / FGHRS inputs are entered
|
||||
as negative quantities (heat displaced FROM the water heater). The
|
||||
max-clamp guards against the worksheet text "if (64)m < 0 then set
|
||||
to 0" — a renewable-heavy system would otherwise show negative
|
||||
delivered heat for the warmest months.
|
||||
|
||||
Both fixtures have all four inputs zero (no WWHRS, no PV diverter,
|
||||
no solar HW, no FGHRS), so (64)m = (62)m for every month.
|
||||
"""
|
||||
# Arrange / Act
|
||||
monthly = output_from_water_heater_monthly_kwh(
|
||||
total_demand_monthly_kwh=fixture.LINE_62_M_TOTAL_WH_KWH,
|
||||
wwhrs_monthly_kwh=fixture.LINE_63A_M_WWHRS_KWH,
|
||||
pv_diverter_monthly_kwh=fixture.LINE_63B_M_PV_DIVERTER_KWH,
|
||||
solar_monthly_kwh=fixture.LINE_63C_M_SOLAR_KWH,
|
||||
fghrs_monthly_kwh=fixture.LINE_63D_M_FGHRS_KWH,
|
||||
)
|
||||
|
||||
# Assert
|
||||
for m, (actual, exp) in enumerate(zip(monthly, fixture.LINE_64_M_OUTPUT_FROM_WH_KWH)):
|
||||
assert actual == pytest.approx(exp, abs=1e-3), f"month {m+1}"
|
||||
|
||||
|
||||
def test_output_from_water_heater_clamps_to_zero_when_renewables_exceed_demand() -> None:
|
||||
"""(64)m floor at zero per spec: a solar HW system that contributes
|
||||
more in July than the dwelling demands shouldn't show negative
|
||||
delivered heat for that month."""
|
||||
# Arrange — 100 kWh demand, -150 kWh solar input (overshoots)
|
||||
demand = tuple(100.0 for _ in range(12))
|
||||
solar = tuple(-150.0 for _ in range(12))
|
||||
zero = tuple(0.0 for _ in range(12))
|
||||
|
||||
# Act
|
||||
monthly = output_from_water_heater_monthly_kwh(
|
||||
total_demand_monthly_kwh=demand,
|
||||
wwhrs_monthly_kwh=zero,
|
||||
pv_diverter_monthly_kwh=zero,
|
||||
solar_monthly_kwh=solar,
|
||||
fghrs_monthly_kwh=zero,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert all(v == pytest.approx(0.0, abs=1e-9) for v in monthly)
|
||||
|
||||
|
||||
def test_assumed_occupancy_floor_at_n_eq_1_for_small_dwellings() -> None:
|
||||
"""Appendix J piecewise definition: TFA ≤ 13.9 m² → N=1 exactly. A
|
||||
tiny studio flat at the boundary is the most common trigger."""
|
||||
|
|
|
|||
|
|
@ -272,6 +272,36 @@ def total_water_heating_demand_monthly_kwh(
|
|||
)
|
||||
|
||||
|
||||
def output_from_water_heater_monthly_kwh(
|
||||
*,
|
||||
total_demand_monthly_kwh: tuple[float, ...],
|
||||
wwhrs_monthly_kwh: tuple[float, ...],
|
||||
pv_diverter_monthly_kwh: tuple[float, ...],
|
||||
solar_monthly_kwh: tuple[float, ...],
|
||||
fghrs_monthly_kwh: tuple[float, ...],
|
||||
) -> tuple[float, ...]:
|
||||
"""SAP 10.2 §4 line (64)m:
|
||||
(64)m = max(0, (62)m + (63a)m + (63b)m + (63c)m + (63d)m)
|
||||
|
||||
Output from the water heater after subtracting renewable / heat-
|
||||
recovery contributions. The four reduction inputs are entered as
|
||||
negative quantities (heat displaced FROM the boiler/cylinder), so
|
||||
the formula uses + not -. Spec note "if (64)m < 0 then set to 0"
|
||||
floors the result month-by-month — a renewable-heavy system can't
|
||||
show negative delivered heat for the warmest months.
|
||||
"""
|
||||
return tuple(
|
||||
max(0.0, t + w + pv + s + f)
|
||||
for t, w, pv, s, f in zip(
|
||||
total_demand_monthly_kwh,
|
||||
wwhrs_monthly_kwh,
|
||||
pv_diverter_monthly_kwh,
|
||||
solar_monthly_kwh,
|
||||
fghrs_monthly_kwh,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _days_weighted_average(monthly: tuple[float, ...]) -> float:
|
||||
"""Σ value[m] × n_m / 365 — used by Appendix J equations J4 and J9."""
|
||||
return sum(v * d for v, d in zip(monthly, _DAYS_IN_MONTH)) / _DAYS_IN_YEAR
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue