§8c slice 2: 6-fixture ALL_FIXTURES conformance (all-zero) with shared template constants

Shared SECTION_8C_ALL_ZERO_MONTHLY / SECTION_8C_ETA_LOSS_ALL_ONE / SECTION_8C_INTERMITTENCY_MONTHLY constants live in _elmhurst_fixtures.py; each of the 6 fixtures references them via plain attributes plus SECTION_8C_COOLED_AREA_FRACTION = 0.0 and the per-line LINE_103/106/107/108 + LINE_107_ANNUAL_KWH pins.

(100), (102), (104) values depend on H × (24−T_e) per fixture and are not pinned here — the algebra is exercised by the synthetic-positive leaf/orchestrator tests in slice 1. First cooling-enabled cert will need a fixture pinning those lines; deferred per Q10 grilling decision.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-21 07:54:55 +00:00
parent cf28eec44d
commit 3b9fa936f0
8 changed files with 210 additions and 0 deletions

View file

@ -12,6 +12,18 @@ this directory and append it here.
from types import ModuleType
# §8c shared template constants — every Elmhurst fixture has
# `has_fixed_air_conditioning=False` so f_C = 0 and (107), (108) collapse
# to zero. (101) η_loss = 1.0 every month because γ = G/L = 0 when cooling
# gains are zero. (106) carries the spec Table 10b f_intermittent = 0.25
# value only in the Jun-Aug inclusion window per the worksheet form.
SECTION_8C_ALL_ZERO_MONTHLY: tuple[float, ...] = (0.0,) * 12
SECTION_8C_ETA_LOSS_ALL_ONE: tuple[float, ...] = (1.0,) * 12
SECTION_8C_INTERMITTENCY_MONTHLY: tuple[float, ...] = (
0.0, 0.0, 0.0, 0.0, 0.0, 0.25, 0.25, 0.25, 0.0, 0.0, 0.0, 0.0,
)
from domain.sap.worksheet.tests import (
_elmhurst_worksheet_000474 as w000474,
_elmhurst_worksheet_000477 as w000477,

View file

@ -33,6 +33,12 @@ from domain.sap.worksheet.solar_gains import RoofWindowInput, RooflightInput
from domain.sap.worksheet.ventilation import MechanicalVentilationKind
from domain.sap.worksheet.water_heating import TABLE_J1_TCOLD_FROM_MAINS_C
from domain.sap.worksheet.tests._elmhurst_fixtures import (
SECTION_8C_ALL_ZERO_MONTHLY,
SECTION_8C_ETA_LOSS_ALL_ONE,
SECTION_8C_INTERMITTENCY_MONTHLY,
)
_WC_CAVITY = 4
@ -365,3 +371,20 @@ LINE_98A_M_SPACE_HEATING_KWH: tuple[float, ...] = (
LINE_98C_M_TOTAL_SPACE_HEATING_KWH: tuple[float, ...] = LINE_98A_M_SPACE_HEATING_KWH
LINE_98C_ANNUAL_KWH: float = 10612.8595
LINE_99_PER_M2_KWH: float = 186.879
# ============================================================================
# §8c Space cooling requirement — expected outputs
# ============================================================================
# `has_fixed_air_conditioning=False` for this cert; cert_to_inputs sets f_C=0
# and cooling gains = (0,)*12 so (107) and (108) collapse to zero. (101) η_loss
# collapses to 1.0 every month because γ = G/L = 0 → spec γ≤0 branch. (100),
# (102), (104) values depend on H × (24 T_e) per fixture so they are not
# asserted in the §8c ALL_FIXTURES test (covered by the synthetic-positive
# test in `test_space_cooling.py`).
SECTION_8C_COOLED_AREA_FRACTION: float = 0.0
LINE_101_M_UTILISATION_FACTOR_LOSS = SECTION_8C_ETA_LOSS_ALL_ONE
LINE_103_M_COOLING_GAINS_W = SECTION_8C_ALL_ZERO_MONTHLY
LINE_106_M_INTERMITTENCY_FACTOR = SECTION_8C_INTERMITTENCY_MONTHLY
LINE_107_M_SPACE_COOLING_KWH = SECTION_8C_ALL_ZERO_MONTHLY
LINE_107_ANNUAL_KWH: float = 0.0
LINE_108_PER_M2_KWH: float = 0.0

View file

@ -30,6 +30,12 @@ from domain.sap.worksheet.solar_gains import RoofWindowInput, RooflightInput
from domain.sap.worksheet.ventilation import MechanicalVentilationKind
from domain.sap.worksheet.water_heating import TABLE_J1_TCOLD_FROM_MAINS_C
from domain.sap.worksheet.tests._elmhurst_fixtures import (
SECTION_8C_ALL_ZERO_MONTHLY,
SECTION_8C_ETA_LOSS_ALL_ONE,
SECTION_8C_INTERMITTENCY_MONTHLY,
)
_WC_CAVITY = 4
@ -300,3 +306,20 @@ LINE_98A_M_SPACE_HEATING_KWH: tuple[float, ...] = (
LINE_98C_M_TOTAL_SPACE_HEATING_KWH: tuple[float, ...] = LINE_98A_M_SPACE_HEATING_KWH
LINE_98C_ANNUAL_KWH: float = 10111.2020
LINE_99_PER_M2_KWH: float = 130.3326
# ============================================================================
# §8c Space cooling requirement — expected outputs
# ============================================================================
# `has_fixed_air_conditioning=False` for this cert; cert_to_inputs sets f_C=0
# and cooling gains = (0,)*12 so (107) and (108) collapse to zero. (101) η_loss
# collapses to 1.0 every month because γ = G/L = 0 → spec γ≤0 branch. (100),
# (102), (104) values depend on H × (24 T_e) per fixture so they are not
# asserted in the §8c ALL_FIXTURES test (covered by the synthetic-positive
# test in `test_space_cooling.py`).
SECTION_8C_COOLED_AREA_FRACTION: float = 0.0
LINE_101_M_UTILISATION_FACTOR_LOSS = SECTION_8C_ETA_LOSS_ALL_ONE
LINE_103_M_COOLING_GAINS_W = SECTION_8C_ALL_ZERO_MONTHLY
LINE_106_M_INTERMITTENCY_FACTOR = SECTION_8C_INTERMITTENCY_MONTHLY
LINE_107_M_SPACE_COOLING_KWH = SECTION_8C_ALL_ZERO_MONTHLY
LINE_107_ANNUAL_KWH: float = 0.0
LINE_108_PER_M2_KWH: float = 0.0

View file

@ -31,6 +31,12 @@ from domain.sap.worksheet.solar_gains import RoofWindowInput, RooflightInput
from domain.sap.worksheet.ventilation import MechanicalVentilationKind
from domain.sap.worksheet.water_heating import TABLE_J1_TCOLD_FROM_MAINS_C
from domain.sap.worksheet.tests._elmhurst_fixtures import (
SECTION_8C_ALL_ZERO_MONTHLY,
SECTION_8C_ETA_LOSS_ALL_ONE,
SECTION_8C_INTERMITTENCY_MONTHLY,
)
_WC_CAVITY = 4
@ -329,3 +335,20 @@ LINE_98A_M_SPACE_HEATING_KWH: tuple[float, ...] = (
LINE_98C_M_TOTAL_SPACE_HEATING_KWH: tuple[float, ...] = LINE_98A_M_SPACE_HEATING_KWH
LINE_98C_ANNUAL_KWH: float = 12398.5783
LINE_99_PER_M2_KWH: float = 146.8852
# ============================================================================
# §8c Space cooling requirement — expected outputs
# ============================================================================
# `has_fixed_air_conditioning=False` for this cert; cert_to_inputs sets f_C=0
# and cooling gains = (0,)*12 so (107) and (108) collapse to zero. (101) η_loss
# collapses to 1.0 every month because γ = G/L = 0 → spec γ≤0 branch. (100),
# (102), (104) values depend on H × (24 T_e) per fixture so they are not
# asserted in the §8c ALL_FIXTURES test (covered by the synthetic-positive
# test in `test_space_cooling.py`).
SECTION_8C_COOLED_AREA_FRACTION: float = 0.0
LINE_101_M_UTILISATION_FACTOR_LOSS = SECTION_8C_ETA_LOSS_ALL_ONE
LINE_103_M_COOLING_GAINS_W = SECTION_8C_ALL_ZERO_MONTHLY
LINE_106_M_INTERMITTENCY_FACTOR = SECTION_8C_INTERMITTENCY_MONTHLY
LINE_107_M_SPACE_COOLING_KWH = SECTION_8C_ALL_ZERO_MONTHLY
LINE_107_ANNUAL_KWH: float = 0.0
LINE_108_PER_M2_KWH: float = 0.0

View file

@ -112,6 +112,12 @@ def build_epc() -> EpcPropertyData:
# ============================================================================
from domain.sap.worksheet.ventilation import MechanicalVentilationKind
from domain.sap.worksheet.tests._elmhurst_fixtures import (
SECTION_8C_ALL_ZERO_MONTHLY,
SECTION_8C_ETA_LOSS_ALL_ONE,
SECTION_8C_INTERMITTENCY_MONTHLY,
)
INTERMITTENT_FANS: int = 1
HAS_SUSPENDED_TIMBER_FLOOR: bool = True
SUSPENDED_TIMBER_FLOOR_SEALED: bool = False
@ -346,3 +352,20 @@ LINE_98A_M_SPACE_HEATING_KWH: tuple[float, ...] = (
LINE_98C_M_TOTAL_SPACE_HEATING_KWH: tuple[float, ...] = LINE_98A_M_SPACE_HEATING_KWH
LINE_98C_ANNUAL_KWH: float = 10834.7778
LINE_99_PER_M2_KWH: float = 132.828
# ============================================================================
# §8c Space cooling requirement — expected outputs
# ============================================================================
# `has_fixed_air_conditioning=False` for this cert; cert_to_inputs sets f_C=0
# and cooling gains = (0,)*12 so (107) and (108) collapse to zero. (101) η_loss
# collapses to 1.0 every month because γ = G/L = 0 → spec γ≤0 branch. (100),
# (102), (104) values depend on H × (24 T_e) per fixture so they are not
# asserted in the §8c ALL_FIXTURES test (covered by the synthetic-positive
# test in `test_space_cooling.py`).
SECTION_8C_COOLED_AREA_FRACTION: float = 0.0
LINE_101_M_UTILISATION_FACTOR_LOSS = SECTION_8C_ETA_LOSS_ALL_ONE
LINE_103_M_COOLING_GAINS_W = SECTION_8C_ALL_ZERO_MONTHLY
LINE_106_M_INTERMITTENCY_FACTOR = SECTION_8C_INTERMITTENCY_MONTHLY
LINE_107_M_SPACE_COOLING_KWH = SECTION_8C_ALL_ZERO_MONTHLY
LINE_107_ANNUAL_KWH: float = 0.0
LINE_108_PER_M2_KWH: float = 0.0

View file

@ -35,6 +35,12 @@ from domain.sap.worksheet.solar_gains import RoofWindowInput, RooflightInput
from domain.sap.worksheet.ventilation import MechanicalVentilationKind
from domain.sap.worksheet.water_heating import TABLE_J1_TCOLD_FROM_MAINS_C
from domain.sap.worksheet.tests._elmhurst_fixtures import (
SECTION_8C_ALL_ZERO_MONTHLY,
SECTION_8C_ETA_LOSS_ALL_ONE,
SECTION_8C_INTERMITTENCY_MONTHLY,
)
_WC_CAVITY = 4
@ -341,3 +347,20 @@ LINE_98A_M_SPACE_HEATING_KWH: tuple[float, ...] = (
LINE_98C_M_TOTAL_SPACE_HEATING_KWH: tuple[float, ...] = LINE_98A_M_SPACE_HEATING_KWH
LINE_98C_ANNUAL_KWH: float = 11183.2752
LINE_99_PER_M2_KWH: float = 169.2897
# ============================================================================
# §8c Space cooling requirement — expected outputs
# ============================================================================
# `has_fixed_air_conditioning=False` for this cert; cert_to_inputs sets f_C=0
# and cooling gains = (0,)*12 so (107) and (108) collapse to zero. (101) η_loss
# collapses to 1.0 every month because γ = G/L = 0 → spec γ≤0 branch. (100),
# (102), (104) values depend on H × (24 T_e) per fixture so they are not
# asserted in the §8c ALL_FIXTURES test (covered by the synthetic-positive
# test in `test_space_cooling.py`).
SECTION_8C_COOLED_AREA_FRACTION: float = 0.0
LINE_101_M_UTILISATION_FACTOR_LOSS = SECTION_8C_ETA_LOSS_ALL_ONE
LINE_103_M_COOLING_GAINS_W = SECTION_8C_ALL_ZERO_MONTHLY
LINE_106_M_INTERMITTENCY_FACTOR = SECTION_8C_INTERMITTENCY_MONTHLY
LINE_107_M_SPACE_COOLING_KWH = SECTION_8C_ALL_ZERO_MONTHLY
LINE_107_ANNUAL_KWH: float = 0.0
LINE_108_PER_M2_KWH: float = 0.0

View file

@ -36,6 +36,12 @@ from domain.sap.worksheet.solar_gains import Orientation, RoofWindowInput, Roofl
from domain.sap.worksheet.ventilation import MechanicalVentilationKind
from domain.sap.worksheet.water_heating import TABLE_J1_TCOLD_FROM_MAINS_C
from domain.sap.worksheet.tests._elmhurst_fixtures import (
SECTION_8C_ALL_ZERO_MONTHLY,
SECTION_8C_ETA_LOSS_ALL_ONE,
SECTION_8C_INTERMITTENCY_MONTHLY,
)
_WC_CAVITY = 4
_WC_SOLID_BRICK = 3 # party walls — RdSAP10 maps to U=0.0 (solid masonry)
@ -313,3 +319,20 @@ LINE_98A_M_SPACE_HEATING_KWH: tuple[float, ...] = (
LINE_98C_M_TOTAL_SPACE_HEATING_KWH: tuple[float, ...] = LINE_98A_M_SPACE_HEATING_KWH
LINE_98C_ANNUAL_KWH: float = 12410.3170
LINE_99_PER_M2_KWH: float = 137.0700
# ============================================================================
# §8c Space cooling requirement — expected outputs
# ============================================================================
# `has_fixed_air_conditioning=False` for this cert; cert_to_inputs sets f_C=0
# and cooling gains = (0,)*12 so (107) and (108) collapse to zero. (101) η_loss
# collapses to 1.0 every month because γ = G/L = 0 → spec γ≤0 branch. (100),
# (102), (104) values depend on H × (24 T_e) per fixture so they are not
# asserted in the §8c ALL_FIXTURES test (covered by the synthetic-positive
# test in `test_space_cooling.py`).
SECTION_8C_COOLED_AREA_FRACTION: float = 0.0
LINE_101_M_UTILISATION_FACTOR_LOSS = SECTION_8C_ETA_LOSS_ALL_ONE
LINE_103_M_COOLING_GAINS_W = SECTION_8C_ALL_ZERO_MONTHLY
LINE_106_M_INTERMITTENCY_FACTOR = SECTION_8C_INTERMITTENCY_MONTHLY
LINE_107_M_SPACE_COOLING_KWH = SECTION_8C_ALL_ZERO_MONTHLY
LINE_107_ANNUAL_KWH: float = 0.0
LINE_108_PER_M2_KWH: float = 0.0

View file

@ -5,12 +5,16 @@ Reference: SAP 10.2 specification (14-03-2025) Tables 10a/10b (page 186).
from __future__ import annotations
from types import ModuleType
import pytest
from domain.sap.climate.appendix_u import external_temperature_c
from domain.sap.worksheet.space_cooling import (
space_cooling_monthly_kwh,
utilisation_factor_loss,
)
from domain.sap.worksheet.tests._elmhurst_fixtures import ALL_FIXTURES, fixture_id
_FULLY_INACTIVE_GAINS_WINTER_TE_C: float = -10.0
@ -18,6 +22,12 @@ _JULY_TE_C: float = 22.0
_JULY_GAINS_W: float = 200.0
_CONSTANT_H_W_PER_K: float = 100.0
_TMP_FOR_A_EQUALS_THREE: float = 108.0 # → τ=30 → a=3 → γ=1 closed-form η=0.75
_UK_AVG_EXT_TEMP_C: tuple[float, ...] = tuple(
external_temperature_c(0, m) for m in range(1, 13)
)
_VENTILATION_HLC_COEFF_W_PER_M3_K: float = 0.33 # SAP10.2 (38)
_DEFAULT_THERMAL_MASS_PARAMETER_KJ_PER_M2_K: float = 250.0
_TABLE_10B_INTERMITTENCY_FACTOR: float = 0.25
def test_utilisation_factor_loss_returns_one_for_negative_gamma() -> None:
@ -202,3 +212,53 @@ def test_space_cooling_monthly_kwh_collapses_to_zero_when_f_cool_zero() -> None:
assert result.space_cooling_monthly_kwh == (0.0,) * 12
assert result.space_cooling_per_m2_kwh == 0.0
assert result.cooled_area_fraction == 0.0
def _section_8c_inputs(fixture: ModuleType) -> dict[str, object]:
"""Build space_cooling_monthly_kwh kwargs from §1-§3 fixture pins.
Mirrors cert_to_inputs flow at slice 3: H from (37) + 0.33·V·(25)m, T_e
from Appendix U region 0, cooling gains = (0,)*12 (no AC), f_C = 0,
TMP default 250 kJ/m²K."""
return {
"monthly_heat_transfer_coefficient_w_per_k": tuple(
fixture.LINE_37_TOTAL_FABRIC_HEAT_LOSS_W_PER_K
+ _VENTILATION_HLC_COEFF_W_PER_M3_K
* fixture.LINE_5_VOLUME_M3
* fixture.LINE_25_EFFECTIVE_ACH[m]
for m in range(12)
),
"monthly_external_temperature_c": _UK_AVG_EXT_TEMP_C,
"monthly_total_gains_w": (0.0,) * 12,
"total_floor_area_m2": fixture.LINE_4_TFA_M2,
"thermal_mass_parameter_kj_per_m2_k": _DEFAULT_THERMAL_MASS_PARAMETER_KJ_PER_M2_K,
"cooled_area_fraction": fixture.SECTION_8C_COOLED_AREA_FRACTION,
"intermittency_factor": _TABLE_10B_INTERMITTENCY_FACTOR,
}
@pytest.mark.parametrize("fixture", ALL_FIXTURES, ids=[fixture_id(f) for f in ALL_FIXTURES])
def test_space_cooling_monthly_kwh_matches_elmhurst_worksheet_all_fixtures(
fixture: ModuleType,
) -> None:
"""End-to-end §8c orchestrator against every Elmhurst fixture. Each
fixture has `has_fixed_air_conditioning=False` so f_C=0 collapses (107)
and (108) to zero. (101) η_loss = 1.0 every month (γ=0 from zero cooling
gains). (100), (102), (104) depend on H × (24T_e) per fixture and are
not asserted here covered by the synthetic-positive leaf/orchestrator
tests above. The first cooling-enabled cert lands a separate fixture
pinning (100)..(104) per its T_e profile (deferred see SPEC_COVERAGE
§8c row)."""
# Arrange
inputs = _section_8c_inputs(fixture)
# Act
result = space_cooling_monthly_kwh(**inputs) # type: ignore[arg-type]
# Assert
assert result.cooled_area_fraction == fixture.SECTION_8C_COOLED_AREA_FRACTION
assert result.utilisation_factor_loss_monthly == fixture.LINE_101_M_UTILISATION_FACTOR_LOSS
assert result.cooling_gains_monthly_w == fixture.LINE_103_M_COOLING_GAINS_W
assert result.intermittency_factor_monthly == fixture.LINE_106_M_INTERMITTENCY_FACTOR
assert result.space_cooling_monthly_kwh == fixture.LINE_107_M_SPACE_COOLING_KWH
assert result.space_cooling_kwh_per_yr == fixture.LINE_107_ANNUAL_KWH
assert result.space_cooling_per_m2_kwh == fixture.LINE_108_PER_M2_KWH