diff --git a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_fixtures.py b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_fixtures.py index 049686ac..45d8d538 100644 --- a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_fixtures.py +++ b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_fixtures.py @@ -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, diff --git a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000474.py b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000474.py index 4e7c9423..82115035 100644 --- a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000474.py +++ b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000474.py @@ -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 diff --git a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000477.py b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000477.py index e5aa8391..c4f969d6 100644 --- a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000477.py +++ b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000477.py @@ -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 diff --git a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000480.py b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000480.py index 5400b58d..4253496c 100644 --- a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000480.py +++ b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000480.py @@ -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 diff --git a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000487.py b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000487.py index 15095955..35899130 100644 --- a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000487.py +++ b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000487.py @@ -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 diff --git a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000490.py b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000490.py index fb5ef2b4..ede28f76 100644 --- a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000490.py +++ b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000490.py @@ -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 diff --git a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000516.py b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000516.py index 94d4a490..a098360a 100644 --- a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000516.py +++ b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000516.py @@ -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 diff --git a/packages/domain/src/domain/sap/worksheet/tests/test_space_cooling.py b/packages/domain/src/domain/sap/worksheet/tests/test_space_cooling.py index fbc38446..f1fbc261 100644 --- a/packages/domain/src/domain/sap/worksheet/tests/test_space_cooling.py +++ b/packages/domain/src/domain/sap/worksheet/tests/test_space_cooling.py @@ -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 × (24−T_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