mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice 102f-prep.3: Table N5 day allocation Jan/Dec/Feb/Mar/Nov/Apr/Oct/May
SAP 10.2 Appendix N3.5 (PDF p.107): "Allocate these to months in the
following order: Jan, Dec, Feb, Mar, Nov, Apr, Oct, May (coldest to
the warmest), until all the days N24,9 and N16,9 have been allocated.
Days N24,9 are allocated first."
`allocate_extended_heating_days_to_months` distributes annual N24,9
and N16,9 totals (from Table N5) across the cold-first month order,
with N24 days filling first and N16 days filling whatever space
remains in each month afterward.
Cross-pinned against the spec's PSR=0.2 worked example (PDF p.107):
Jan-Oct each get max N24, May ends up with the residual (6, 6). And
against cert 0380's worksheet: PSR≈1.43 → row 1.2+ (3, 38) →
Jan(3, 28), Dec(0, 10) — matches the worksheet 24/9 + 16/9 rows.
The 8 cold-month order spans 243 days, exceeding every Table N5
row's combined total — no allocation is dropped for Variable
heating duration. Fixed durations ("24" / "16" from Table N4) live
beyond this helper's contract (caller decides when N24=365 means
"all months at Th"); slice 102f-prep.4 wires that in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
a6ef198740
commit
4e07991f8f
2 changed files with 148 additions and 0 deletions
|
|
@ -53,6 +53,15 @@ _TABLE_N5_VARIABLE_HEATING_DAYS: Final[tuple[tuple[float, int, int], ...]] = (
|
|||
(1.2, 3, 38),
|
||||
)
|
||||
|
||||
# SAP 10.2 Table 1a — calendar days per month (non-leap), used for
|
||||
# Equation N5 + the N24,9 / N16,9 day allocation algorithm (PDF p.107).
|
||||
_DAYS_IN_MONTH: Final[tuple[int, ...]] = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
|
||||
# SAP 10.2 PDF p.107 — month indices (0-based Jan..Dec) in cold-to-warm
|
||||
# order: Jan, Dec, Feb, Mar, Nov, Apr, Oct, May. The remaining four
|
||||
# months (Jun, Jul, Aug, Sep) never receive any allocation — for cohort
|
||||
# PSRs the year totals never exceed the sum of these eight cold months.
|
||||
_TABLE_N5_ALLOCATION_ORDER: Final[tuple[int, ...]] = (0, 11, 1, 2, 10, 3, 9, 4)
|
||||
|
||||
|
||||
def extended_heating_days_from_psr_variable(*, psr: float) -> tuple[int, int]:
|
||||
"""SAP 10.2 Appendix N3.5 + Table N5 (PDF p.107) — for heat-pump
|
||||
|
|
@ -92,6 +101,51 @@ def extended_heating_days_from_psr_variable(*, psr: float) -> tuple[int, int]:
|
|||
raise AssertionError("PSR bracket not found despite range check")
|
||||
|
||||
|
||||
def allocate_extended_heating_days_to_months(
|
||||
*,
|
||||
n24_9_year: int,
|
||||
n16_9_year: int,
|
||||
) -> tuple[tuple[int, int], ...]:
|
||||
"""SAP 10.2 Appendix N3.5 (PDF p.107) — distribute the annual N24,9
|
||||
and N16,9 day counts to months following the spec's allocation
|
||||
order ("Jan, Dec, Feb, Mar, Nov, Apr, Oct, May (coldest to the
|
||||
warmest)"). N24,9 days are filled first across the order, then
|
||||
N16,9 days fill the days not yet claimed by N24,9.
|
||||
|
||||
Returns a length-12 tuple of `(N24,9_m, N16,9_m)` Jan..Dec. The
|
||||
summer months (Jun, Jul, Aug, Sep) always return (0, 0) for the
|
||||
PSR/duration cases this codebase handles — they're outside the
|
||||
allocation order.
|
||||
"""
|
||||
n24_remaining = n24_9_year
|
||||
n16_remaining = n16_9_year
|
||||
allocations: list[tuple[int, int]] = [(0, 0)] * 12
|
||||
|
||||
# Sweep 1 — N24,9: fill each cold month up to its day count.
|
||||
for m_idx in _TABLE_N5_ALLOCATION_ORDER:
|
||||
if n24_remaining <= 0:
|
||||
break
|
||||
month_days = _DAYS_IN_MONTH[m_idx]
|
||||
take = min(n24_remaining, month_days)
|
||||
allocations[m_idx] = (take, 0)
|
||||
n24_remaining -= take
|
||||
|
||||
# Sweep 2 — N16,9: fill remaining month space (month_days − N24,m).
|
||||
for m_idx in _TABLE_N5_ALLOCATION_ORDER:
|
||||
if n16_remaining <= 0:
|
||||
break
|
||||
month_days = _DAYS_IN_MONTH[m_idx]
|
||||
n24_m, _ = allocations[m_idx]
|
||||
space = month_days - n24_m
|
||||
if space <= 0:
|
||||
continue
|
||||
take = min(n16_remaining, space)
|
||||
allocations[m_idx] = (n24_m, take)
|
||||
n16_remaining -= take
|
||||
|
||||
return tuple(allocations)
|
||||
|
||||
|
||||
def elsewhere_heating_temperature_c(
|
||||
*,
|
||||
heat_loss_parameter: float,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import pytest
|
|||
from domain.sap10_calculator.climate.appendix_u import external_temperature_c
|
||||
from domain.sap10_calculator.worksheet.mean_internal_temperature import (
|
||||
MeanInternalTemperatureResult,
|
||||
allocate_extended_heating_days_to_months,
|
||||
elsewhere_heating_temperature_c,
|
||||
extended_heating_days_from_psr_variable,
|
||||
mean_internal_temperature_monthly,
|
||||
|
|
@ -358,3 +359,96 @@ def test_extended_heating_days_from_psr_variable_rounds_to_nearest_day() -> None
|
|||
assert n24_9 == 118
|
||||
assert n16_9 == 61
|
||||
|
||||
|
||||
def test_allocate_extended_heating_days_matches_spec_psr_0_2_example() -> None:
|
||||
"""SAP 10.2 PDF p.107 worked example — Variable heating duration
|
||||
at PSR 0.2 (N24,9 = 218, N16,9 = 6):
|
||||
|
||||
January: N24,9,m=1 = 31. All days in January have been allocated
|
||||
so N16,9,m=1 = 0. Remaining N24,9 = 187, N16,9 = 6.
|
||||
… continued for Dec, Feb, Mar, Nov, Apr, Oct after which
|
||||
remaining N24,9 = 6 and N16,9 = 6.
|
||||
For May: N24,9,m=5 = 6 and N16,9,m=5 = 6.
|
||||
|
||||
Allocation order is Jan, Dec, Feb, Mar, Nov, Apr, Oct, May (coldest
|
||||
to warmest); N24,9 days are allocated first.
|
||||
"""
|
||||
# Arrange / Act
|
||||
monthly = allocate_extended_heating_days_to_months(n24_9_year=218, n16_9_year=6)
|
||||
|
||||
# Assert — Jan/Dec/Feb/Mar/Nov/Apr/Oct fill with N24,9 up to month days.
|
||||
assert monthly[0] == (31, 0) # Jan: 31 N24
|
||||
assert monthly[11] == (31, 0) # Dec: 31 N24
|
||||
assert monthly[1] == (28, 0) # Feb: 28 N24
|
||||
assert monthly[2] == (31, 0) # Mar: 31 N24
|
||||
assert monthly[10] == (30, 0) # Nov: 30 N24
|
||||
assert monthly[3] == (30, 0) # Apr: 30 N24
|
||||
assert monthly[9] == (31, 0) # Oct: 31 N24 (sum so far = 212)
|
||||
# May: remaining N24 = 218 - 212 = 6; remaining N16 = 6.
|
||||
assert monthly[4] == (6, 6)
|
||||
# All other months (Jun, Jul, Aug, Sep) get nothing — summer.
|
||||
assert monthly[5] == (0, 0)
|
||||
assert monthly[6] == (0, 0)
|
||||
assert monthly[7] == (0, 0)
|
||||
assert monthly[8] == (0, 0)
|
||||
# Year-total invariant
|
||||
assert sum(n24 for n24, _ in monthly) == 218
|
||||
assert sum(n16 for _, n16 in monthly) == 6
|
||||
|
||||
|
||||
def test_allocate_extended_heating_days_matches_cert_0380_worksheet() -> None:
|
||||
"""Cert 0380 (Mitsubishi PUZ-WM50VHA, PSR ≈ 1.43) lands on Table N5
|
||||
row "1.2 or more": (N24,9, N16,9) = (3, 38). Worksheet for cert
|
||||
0380 shows:
|
||||
Jan row "24/9" = 3, row "16/9" = 28
|
||||
Dec row "24/9" = 0, row "16/9" = 10
|
||||
|
||||
The N24 (=3) fits in Jan; N16 then fills Jan's remaining 31-3 = 28
|
||||
days, and the final 10 N16 days land in Dec (next-coldest in the
|
||||
Table N5 allocation order).
|
||||
"""
|
||||
# Arrange / Act
|
||||
monthly = allocate_extended_heating_days_to_months(n24_9_year=3, n16_9_year=38)
|
||||
|
||||
# Assert
|
||||
assert monthly[0] == (3, 28) # Jan: 3 N24 + 28 N16
|
||||
assert monthly[11] == (0, 10) # Dec: 0 N24 + 10 N16 (remaining after Jan)
|
||||
# All other months see no extended heating.
|
||||
for m in range(1, 11):
|
||||
assert monthly[m] == (0, 0), f"month {m+1} should be (0, 0)"
|
||||
assert sum(n24 for n24, _ in monthly) == 3
|
||||
assert sum(n16 for _, n16 in monthly) == 38
|
||||
|
||||
|
||||
def test_allocate_extended_heating_days_zero_is_all_zero() -> None:
|
||||
"""A "9"-hour heating duration package (or any case where N24,9 =
|
||||
N16,9 = 0) collapses to the standard SAP heating schedule — every
|
||||
month is (0, 0)."""
|
||||
# Arrange / Act
|
||||
monthly = allocate_extended_heating_days_to_months(n24_9_year=0, n16_9_year=0)
|
||||
|
||||
# Assert
|
||||
assert monthly == ((0, 0),) * 12
|
||||
|
||||
|
||||
def test_allocate_extended_heating_days_variable_year_totals_are_preserved() -> None:
|
||||
"""The helper's invariant for the Variable case: every input
|
||||
(N24,9, N16,9) day from Table N5 must land in some cold month
|
||||
(Jan, Dec, Feb, Mar, Nov, Apr, Oct, May). The eight cold months
|
||||
hold 31+31+28+31+30+30+31+31 = 243 days, larger than every Table
|
||||
N5 row's combined total, so no allocation is dropped.
|
||||
|
||||
Pin the year totals at the largest Table N5 row sum (PSR 0.2 →
|
||||
218 + 6 = 224 days) and at a row with non-trivial N16,9 (PSR 0.5
|
||||
→ 128 + 56 = 184).
|
||||
"""
|
||||
# Arrange / Act
|
||||
psr_02 = allocate_extended_heating_days_to_months(n24_9_year=218, n16_9_year=6)
|
||||
psr_05 = allocate_extended_heating_days_to_months(n24_9_year=128, n16_9_year=56)
|
||||
|
||||
# Assert — totals preserved.
|
||||
assert sum(n24 for n24, _ in psr_02) == 218
|
||||
assert sum(n16 for _, n16 in psr_02) == 6
|
||||
assert sum(n24 for n24, _ in psr_05) == 128
|
||||
assert sum(n16 for _, n16 in psr_05) == 56
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue