mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
§4 slice 5: lines (43) annual avg + (44)m monthly total
Two thin wrappers landing the aggregation step: (44)m = (42a)m + (42b)m + (42c)m Appendix J equation J13 (43) = V_d,shower,ave + V_d,bath,ave + V_d,other,ave J12 A subtle spec point caught here: (43) is the SUM OF THE COMPONENT ANNUAL AVERAGES (per the J12 text), not the days-weighted mean of (44)m. The two are arithmetically different because Table J2's days-weighted mean is 0.99973 rather than 1.0 — the "other uses" term contributes its unmodulated baseline (9.8N+14), and only the showers + baths terms get the days-weighted reduction. Spec-following the J12 wording matches the Elmhurst (43) values to 1e-3 L/day on both fixtures. annual_average_hot_water_other_uses_l_per_day exposes V_d,other,ave annual_average_hot_water_l_per_day composes the J12 sum total_hot_water_monthly_l_per_day J13 (44)m sum LINE_43 + LINE_44_M lodged on 000474 and 000490 fixtures. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
1dcbdb28e6
commit
702b1c6ce6
4 changed files with 151 additions and 0 deletions
|
|
@ -184,3 +184,8 @@ DOOR_COUNT: int = 2 # cascade default 1.85 m²/door → 3.70 m² matches worksh
|
|||
|
||||
# §4 Water heating energy requirements
|
||||
LINE_42_OCCUPANCY: float = 1.8896
|
||||
LINE_43_ANNUAL_AVG_HW_USAGE_L_PER_DAY: float = 101.1966
|
||||
LINE_44_M_DAILY_HW_USAGE_L: tuple[float, ...] = (
|
||||
110.1180, 107.7045, 104.8007, 100.4697, 96.9221, 93.1204,
|
||||
91.7218, 94.6138, 97.6550, 101.6380, 106.0332, 109.8446,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -172,3 +172,8 @@ DOOR_COUNT: int = 2 # cascade default 1.85 m²/door → 3.70 m² matches worksh
|
|||
|
||||
# §4 Water heating energy requirements
|
||||
LINE_42_OCCUPANCY: float = 2.1468
|
||||
LINE_43_ANNUAL_AVG_HW_USAGE_L_PER_DAY: float = 109.0070
|
||||
LINE_44_M_DAILY_HW_USAGE_L: tuple[float, ...] = (
|
||||
118.6172, 116.0171, 112.8890, 108.2238, 104.4023, 100.3071,
|
||||
98.8008, 101.9162, 105.1922, 109.4827, 114.2171, 118.3228,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,10 +17,13 @@ from domain.sap.worksheet.tests import (
|
|||
from domain.sap.worksheet.tests._xlsx_loader import load_cells
|
||||
from domain.sap.worksheet.water_heating import (
|
||||
TABLE_J1_TCOLD_FROM_MAINS_C,
|
||||
annual_average_hot_water_l_per_day,
|
||||
annual_average_hot_water_other_uses_l_per_day,
|
||||
assumed_occupancy,
|
||||
hot_water_baths_monthly_l_per_day,
|
||||
hot_water_mixer_showers_monthly_l_per_day,
|
||||
hot_water_other_uses_monthly_l_per_day,
|
||||
total_hot_water_monthly_l_per_day,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -325,6 +328,78 @@ def test_hot_water_mixer_showers_two_outlets_distributes_shower_count_evenly() -
|
|||
assert t == pytest.approx(s, abs=1e-9), f"month {m+1}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("fixture", (_w000474, _w000490), ids=("000474", "000490"))
|
||||
def test_total_hot_water_monthly_matches_elmhurst_line_44(fixture) -> None: # type: ignore[no-untyped-def]
|
||||
"""SAP10.2 §4 line (44)m via Appendix J equation J13:
|
||||
V_d,m = V_d,shower[m] + V_d,bath[m] + V_d,other[m]
|
||||
|
||||
A pure sum — each component already validated independently — but the
|
||||
end-to-end product is what callers compare against the worksheet."""
|
||||
# Arrange
|
||||
showers = hot_water_mixer_showers_monthly_l_per_day(
|
||||
n_occupants=fixture.LINE_42_OCCUPANCY,
|
||||
has_bath=True,
|
||||
mixer_shower_flow_rates_l_per_min=(7.0,),
|
||||
cold_water_temps_c=TABLE_J1_TCOLD_FROM_MAINS_C,
|
||||
)
|
||||
baths = hot_water_baths_monthly_l_per_day(
|
||||
n_occupants=fixture.LINE_42_OCCUPANCY, has_bath=True, has_shower=True,
|
||||
cold_water_temps_c=TABLE_J1_TCOLD_FROM_MAINS_C, low_water_use=False,
|
||||
)
|
||||
other = hot_water_other_uses_monthly_l_per_day(
|
||||
n_occupants=fixture.LINE_42_OCCUPANCY, low_water_use=False,
|
||||
)
|
||||
|
||||
# Act
|
||||
monthly = total_hot_water_monthly_l_per_day(
|
||||
showers=showers, baths=baths, other_uses=other
|
||||
)
|
||||
|
||||
# Assert
|
||||
for m, (actual, exp) in enumerate(zip(monthly, fixture.LINE_44_M_DAILY_HW_USAGE_L)):
|
||||
assert actual == pytest.approx(exp, abs=1e-3), f"month {m+1}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("fixture", (_w000474, _w000490), ids=("000474", "000490"))
|
||||
def test_annual_average_hot_water_matches_elmhurst_line_43(fixture) -> None: # type: ignore[no-untyped-def]
|
||||
"""SAP10.2 §4 line (43) via Appendix J equation J12 (also (44)m days-
|
||||
weighted form):
|
||||
V_d,ave = Σ V_d,m × n_m / 365
|
||||
|
||||
Feed the live computed (44)m (chained from (42a)+(42b)+(42c)) so
|
||||
no rounding noise from the fixture's 4-d.p. display values leaks
|
||||
into the days-weighted sum.
|
||||
"""
|
||||
# Arrange — same (42a)m + (42b)m inputs as the worksheet used; "other"
|
||||
# contributes its unmodulated annual baseline V_d,other,ave (9.8N+14),
|
||||
# not the days-weighted mean of (42c)m.
|
||||
showers = hot_water_mixer_showers_monthly_l_per_day(
|
||||
n_occupants=fixture.LINE_42_OCCUPANCY,
|
||||
has_bath=True,
|
||||
mixer_shower_flow_rates_l_per_min=(7.0,),
|
||||
cold_water_temps_c=TABLE_J1_TCOLD_FROM_MAINS_C,
|
||||
)
|
||||
baths = hot_water_baths_monthly_l_per_day(
|
||||
n_occupants=fixture.LINE_42_OCCUPANCY, has_bath=True, has_shower=True,
|
||||
cold_water_temps_c=TABLE_J1_TCOLD_FROM_MAINS_C, low_water_use=False,
|
||||
)
|
||||
other_avg = annual_average_hot_water_other_uses_l_per_day(
|
||||
n_occupants=fixture.LINE_42_OCCUPANCY, low_water_use=False,
|
||||
)
|
||||
|
||||
# Act
|
||||
annual_avg = annual_average_hot_water_l_per_day(
|
||||
showers_monthly=showers,
|
||||
baths_monthly=baths,
|
||||
other_uses_annual_avg=other_avg,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert annual_avg == pytest.approx(
|
||||
fixture.LINE_43_ANNUAL_AVG_HW_USAGE_L_PER_DAY, abs=1e-3
|
||||
)
|
||||
|
||||
|
||||
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."""
|
||||
|
|
|
|||
|
|
@ -65,6 +65,13 @@ _WARM_SHOWER_TEMPERATURE_C: Final[float] = 41.0
|
|||
# Appendix J step 1d: assumed shower duration in minutes per event.
|
||||
_SHOWER_DURATION_MIN: Final[float] = 6.0
|
||||
|
||||
# Days per month (non-leap year) — used by Appendix J J4 / J9 / J12 to
|
||||
# turn monthly daily-rate arrays into days-weighted annual averages.
|
||||
_DAYS_IN_MONTH: Final[tuple[int, ...]] = (
|
||||
31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31,
|
||||
)
|
||||
_DAYS_IN_YEAR: Final[int] = sum(_DAYS_IN_MONTH)
|
||||
|
||||
|
||||
def assumed_occupancy(total_floor_area_m2: float) -> float:
|
||||
"""SAP 10.2 §4 line (42) / Appendix J Table 1b.
|
||||
|
|
@ -167,6 +174,65 @@ def hot_water_baths_monthly_l_per_day(
|
|||
return tuple(monthly)
|
||||
|
||||
|
||||
def total_hot_water_monthly_l_per_day(
|
||||
*,
|
||||
showers: tuple[float, ...],
|
||||
baths: tuple[float, ...],
|
||||
other_uses: tuple[float, ...],
|
||||
) -> tuple[float, ...]:
|
||||
"""SAP 10.2 §4 line (44)m via Appendix J equation J13:
|
||||
V_d,m = V_d,shower[m] + V_d,bath[m] + V_d,other[m]
|
||||
|
||||
A pure element-wise sum of the three monthly demand streams. All
|
||||
three inputs must be 12-tuples — caller is responsible for ensuring
|
||||
they were computed against the same Tcold table.
|
||||
"""
|
||||
return tuple(s + b + o for s, b, o in zip(showers, baths, other_uses))
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def annual_average_hot_water_other_uses_l_per_day(
|
||||
*, n_occupants: float, low_water_use: bool
|
||||
) -> float:
|
||||
"""SAP 10.2 §4 — V_d,other,ave per Appendix J step 3a: 9.8 × N + 14,
|
||||
less 5% for the low-water-use target. The annual average is computed
|
||||
BEFORE Table J2 monthly modulation, so it's the unmodulated baseline
|
||||
value (the days-weighted mean of (42c)m would drift slightly off
|
||||
because Table J2 doesn't days-average to exactly 1)."""
|
||||
annual = 9.8 * n_occupants + 14.0
|
||||
if low_water_use:
|
||||
annual *= 1.0 - _LOW_WATER_USE_REDUCTION
|
||||
return annual
|
||||
|
||||
|
||||
def annual_average_hot_water_l_per_day(
|
||||
*,
|
||||
showers_monthly: tuple[float, ...],
|
||||
baths_monthly: tuple[float, ...],
|
||||
other_uses_annual_avg: float,
|
||||
) -> float:
|
||||
"""SAP 10.2 §4 line (43) via Appendix J equation J12:
|
||||
V_d,ave = V_d,shower,ave + V_d,bath,ave + V_d,other,ave
|
||||
|
||||
Per the spec text after J12, (43) is the sum of the three component
|
||||
annual averages — NOT the days-weighted average of (44)m. The
|
||||
distinction only matters for "other uses": its monthly array (42c)m
|
||||
is the unmodulated annual baseline times Table J2 factors, and the
|
||||
days-weighted average of those factors is 0.9997 (not exactly 1.0),
|
||||
so taking the days-weighted mean of (42c)m would drift slightly low.
|
||||
Showers and baths only have annual averages via J4 / J9.
|
||||
"""
|
||||
return (
|
||||
_days_weighted_average(showers_monthly)
|
||||
+ _days_weighted_average(baths_monthly)
|
||||
+ other_uses_annual_avg
|
||||
)
|
||||
|
||||
|
||||
def hot_water_other_uses_monthly_l_per_day(
|
||||
*, n_occupants: float, low_water_use: bool
|
||||
) -> tuple[float, ...]:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue