From 1f078af7db869276c0b7ac71265ea0693c775007 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 20 May 2026 22:35:12 +0000 Subject: [PATCH] =?UTF-8?q?=C2=A78=20slice=202:=206=20Elmhurst=20fixtures?= =?UTF-8?q?=20conform=20on=20(95)..(99)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds LINE_95_M_USEFUL_GAINS_W, LINE_97_M_HEAT_LOSS_RATE_W, LINE_98A_M_SPACE_HEATING_KWH, LINE_98C_M_TOTAL_SPACE_HEATING_KWH, LINE_98C_ANNUAL_KWH, LINE_99_PER_M2_KWH to each _elmhurst_worksheet_*.py fixture, plus an ALL_FIXTURES-parametrised end-to-end test. Tolerances vary by line ref per §5's per-line precedent: - (95) η × G → 5e-2 W per month - (97) H × ΔT → 5e-2 W per month - (98a)/(98c) → 1e-1 kWh per month - ∑(98c) annual → 1e-1 kWh - (99) per-m² → 5e-3 kWh Looser than §6/§7's flat 5e-3 W budget because §8 inputs (LINE_93, LINE_94, LINE_84) carry 4-d.p. display rounding from upstream worksheets, and §8's 0.024·31·(L−ηG) amplifies that rounding into the per-month kWh band. The orchestrator computes in full precision; tolerances reflect the fixture-pin precision floor, not physics error. Co-Authored-By: Claude Opus 4.7 --- .../tests/_elmhurst_worksheet_000474.py | 19 +++++ .../tests/_elmhurst_worksheet_000477.py | 19 +++++ .../tests/_elmhurst_worksheet_000480.py | 19 +++++ .../tests/_elmhurst_worksheet_000487.py | 19 +++++ .../tests/_elmhurst_worksheet_000490.py | 19 +++++ .../tests/_elmhurst_worksheet_000516.py | 19 +++++ .../sap/worksheet/tests/test_space_heating.py | 84 +++++++++++++++++++ 7 files changed, 198 insertions(+) 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 7f3b21cd..4e7c9423 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 @@ -346,3 +346,22 @@ LINE_94_M_UTILISATION_WHOLE: tuple[float, ...] = ( 0.9720, 0.9633, 0.9455, 0.9098, 0.8457, 0.7330, 0.5633, 0.6168, 0.8187, 0.9256, 0.9627, 0.9745, ) + +# ============================================================================ +# §8 Space heating requirement — expected outputs +# ============================================================================ +LINE_95_M_USEFUL_GAINS_W: tuple[float, ...] = ( + 506.0790, 567.8843, 634.7414, 693.9770, 699.4312, 595.3705, + 436.3012, 437.1801, 518.5588, 513.1261, 484.1045, 483.5787, +) +LINE_97_M_HEAT_LOSS_RATE_W: tuple[float, ...] = ( + 2956.1218, 2856.8816, 2570.5049, 2119.4574, 1594.2286, 1008.8825, + 577.3275, 616.0265, 1080.3274, 1738.5871, 2392.7150, 2948.0890, +) +LINE_98A_M_SPACE_HEATING_KWH: tuple[float, ...] = ( + 1822.8319, 1538.2062, 1440.2081, 1026.3458, 665.7293, 0.0, + 0.0, 0.0, 0.0, 911.7430, 1374.1995, 1833.5957, +) +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 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 ebece32f..e5aa8391 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 @@ -281,3 +281,22 @@ LINE_94_M_UTILISATION_WHOLE: tuple[float, ...] = ( 0.9833, 0.9781, 0.9667, 0.9426, 0.8931, 0.7917, 0.6257, 0.6735, 0.8617, 0.9507, 0.9774, 0.9852, ) + +# ============================================================================ +# §8 Space heating requirement — expected outputs +# ============================================================================ +LINE_95_M_USEFUL_GAINS_W: tuple[float, ...] = ( + 590.8300, 645.1621, 698.8688, 740.0958, 732.9898, 628.3945, + 474.8614, 479.1612, 568.4900, 575.2972, 558.7756, 566.6199, +) +LINE_97_M_HEAT_LOSS_RATE_W: tuple[float, ...] = ( + 2959.4740, 2848.9539, 2553.0985, 2078.0015, 1561.3993, 993.9559, + 595.9047, 632.0681, 1074.1362, 1720.1111, 2368.4759, 2924.2562, +) +LINE_98A_M_SPACE_HEATING_KWH: tuple[float, ...] = ( + 1762.2712, 1480.9481, 1379.5469, 963.2921, 616.3366, 0.0, + 0.0, 0.0, 0.0, 851.7415, 1302.9842, 1754.0814, +) +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 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 7c94897b..5400b58d 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 @@ -310,3 +310,22 @@ LINE_94_M_UTILISATION_WHOLE: tuple[float, ...] = ( 0.9830, 0.9785, 0.9687, 0.9467, 0.9000, 0.8039, 0.6441, 0.6959, 0.8745, 0.9534, 0.9772, 0.9847, ) + +# ============================================================================ +# §8 Space heating requirement — expected outputs +# ============================================================================ +LINE_95_M_USEFUL_GAINS_W: tuple[float, ...] = ( + 624.7214, 673.4519, 721.4221, 767.3929, 774.6171, 677.1408, + 516.2266, 513.9498, 591.7998, 597.6119, 588.5443, 600.6212, +) +LINE_97_M_HEAT_LOSS_RATE_W: tuple[float, ...] = ( + 3470.4829, 3343.3744, 2995.0906, 2453.9817, 1839.7089, 1170.0720, + 687.2651, 730.6558, 1258.5840, 2031.0834, 2804.7962, 3458.8981, +) +LINE_98A_M_SPACE_HEATING_KWH: tuple[float, ...] = ( + 2117.2466, 1794.1879, 1691.6094, 1214.3439, 792.4283, 0.0, + 0.0, 0.0, 0.0, 1066.5028, 1595.7014, 2126.5580, +) +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 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 d37eb6e2..15095955 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 @@ -327,3 +327,22 @@ LINE_94_M_UTILISATION_WHOLE: tuple[float, ...] = ( 0.9813, 0.9748, 0.9651, 0.9481, 0.9139, 0.8354, 0.6865, 0.7123, 0.8636, 0.9441, 0.9737, 0.9835, ) + +# ============================================================================ +# §8 Space heating requirement — expected outputs +# ============================================================================ +LINE_95_M_USEFUL_GAINS_W: tuple[float, ...] = ( + 658.8087, 728.6249, 757.9288, 747.8850, 703.1606, 604.2371, + 481.2153, 495.1643, 607.8162, 650.0718, 633.5798, 629.6995, +) +LINE_97_M_HEAT_LOSS_RATE_W: tuple[float, ...] = ( + 3164.2298, 3053.3605, 2735.3722, 2227.2636, 1667.5142, 1068.2279, + 651.0154, 692.3750, 1169.8152, 1866.6927, 2554.8108, 3138.0600, +) +LINE_98A_M_SPACE_HEATING_KWH: tuple[float, ...] = ( + 1864.0333, 1562.2224, 1471.2179, 1065.1526, 717.4791, 0.0, + 0.0, 0.0, 0.0, 905.1659, 1383.2864, 1866.2202, +) +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 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 cf9bf68a..fb5ef2b4 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 @@ -322,3 +322,22 @@ LINE_94_M_UTILISATION_WHOLE: tuple[float, ...] = ( 0.9735, 0.9659, 0.9524, 0.9261, 0.8752, 0.7738, 0.6027, 0.6474, 0.8348, 0.9303, 0.9644, 0.9759, ) + +# ============================================================================ +# §8 Space heating requirement — expected outputs +# ============================================================================ +LINE_95_M_USEFUL_GAINS_W: tuple[float, ...] = ( + 579.5188, 638.5351, 682.1664, 707.0047, 692.2499, 591.2711, + 441.2650, 447.7583, 546.1761, 568.1093, 552.8782, 555.2620, +) +LINE_97_M_HEAT_LOSS_RATE_W: tuple[float, ...] = ( + 3153.9991, 3044.0808, 2727.4470, 2230.2770, 1662.0453, 1042.5972, + 592.8375, 635.4462, 1131.3062, 1843.3753, 2548.3231, 3143.7625, +) +LINE_98A_M_SPACE_HEATING_KWH: tuple[float, ...] = ( + 1915.4133, 1616.5267, 1521.6888, 1096.7561, 721.5278, 0.0, + 0.0, 0.0, 0.0, 948.7979, 1436.7203, 1925.8443, +) +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 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 e5d3d745..94d4a490 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 @@ -294,3 +294,22 @@ LINE_94_M_UTILISATION_WHOLE: tuple[float, ...] = ( 0.9841, 0.9791, 0.9693, 0.9484, 0.9040, 0.8120, 0.6598, 0.7062, 0.8755, 0.9540, 0.9783, 0.9858, ) + +# ============================================================================ +# §8 Space heating requirement — expected outputs +# ============================================================================ +LINE_95_M_USEFUL_GAINS_W: tuple[float, ...] = ( + 674.6949, 736.2594, 787.5631, 827.0143, 824.6046, 719.4386, + 557.6630, 557.3181, 645.0463, 654.9073, 639.2232, 646.7565, +) +LINE_97_M_HEAT_LOSS_RATE_W: tuple[float, ...] = ( + 3539.0517, 3411.2290, 3059.3047, 2504.6246, 1886.7214, 1213.3943, + 735.3648, 778.4417, 1307.1906, 2082.2459, 2857.3815, 3515.5362, +) +LINE_98A_M_SPACE_HEATING_KWH: tuple[float, ...] = ( + 2131.0814, 1797.5796, 1690.1757, 1207.8794, 790.2149, 0.0, + 0.0, 0.0, 0.0, 1061.9399, 1597.0740, 2134.3721, +) +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 diff --git a/packages/domain/src/domain/sap/worksheet/tests/test_space_heating.py b/packages/domain/src/domain/sap/worksheet/tests/test_space_heating.py index 983129f3..edf70840 100644 --- a/packages/domain/src/domain/sap/worksheet/tests/test_space_heating.py +++ b/packages/domain/src/domain/sap/worksheet/tests/test_space_heating.py @@ -7,13 +7,23 @@ negative or below 1 kWh/month per the Table 9c note. Reference: SAP 10.3 specification (13-01-2026) Table 9c (page 185). """ +from types import ModuleType + import pytest +from domain.sap.climate.appendix_u import external_temperature_c from domain.sap.worksheet.space_heating import ( SpaceHeatingResult, monthly_heat_requirement_kwh, space_heating_monthly_kwh, ) +from domain.sap.worksheet.tests._elmhurst_fixtures import ALL_FIXTURES, fixture_id + + +_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) # Worksheet U985-0001-000490 (UK-avg weather, region 0) inputs for §8. @@ -169,3 +179,77 @@ def test_sub_one_kwh_requirement_clamps_to_zero_per_table_9c_note() -> None: # Assert assert result == 0.0 + + +def _section_8_inputs(fixture: ModuleType) -> dict[str, object]: + """Build space_heating_monthly_kwh kwargs from §1-§7 fixture pins. + Mirrors cert_to_inputs flow at slice 3: (84)m total gains = LINE_84, + (39)m HTC = LINE_37 + 0.33·V·LINE_25_M, T_internal from §7 LINE_93, + η_whole from §7 LINE_94, ext from Appendix U region 0.""" + 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_internal_temperature_c": fixture.LINE_93_M_ADJUSTED_MIT_C, + "monthly_external_temperature_c": _UK_AVG_EXT_TEMP_C, + "monthly_utilisation_factor": fixture.LINE_94_M_UTILISATION_WHOLE, + "monthly_total_gains_w": fixture.LINE_84_M_TOTAL_GAINS_W, + "total_floor_area_m2": fixture.LINE_4_TFA_M2, + } + + +@pytest.mark.parametrize("fixture", ALL_FIXTURES, ids=[fixture_id(f) for f in ALL_FIXTURES]) +def test_space_heating_monthly_kwh_matches_elmhurst_worksheet_all_fixtures( + fixture: ModuleType, +) -> None: + """End-to-end §8 orchestrator against every Elmhurst conformance fixture. + + Each fixture pins LINE_95_M (useful gains), LINE_97_M (heat loss rate), + LINE_98A_M / LINE_98C_M (space heating req incl. summer clamp), + LINE_98C_ANNUAL_KWH, LINE_99_PER_M2_KWH. Inputs flow from §1-§7 pins + via `_section_8_inputs(fixture)` — same shape as cert_to_inputs at + slice 3. + + Asserts each per-month line ref. Tolerances vary by line ref because + upstream fixture pins (LINE_93, LINE_94, LINE_84) are 4-d.p. display- + rounded and the products / sums in §8 accumulate that rounding: + - (95) η × G → 5e-2 W per month + - (97) H × ΔT → 5e-2 W per month + - (98a)/(98c) → 1e-1 kWh per month (driven by (97)−(95) propagation + through 0.024·31 days amplifier ~×0.7; mixed-glazing + fixtures 000474/000477/000487 compound worst) + - ∑(98c) annual → 1e-1 kWh (12-month sum amplifies) + - (99) per-m² → 5e-3 kWh (annual÷TFA brings the digit back) + Same precedent as §5's (68) 5e-2 W tolerance — display-rounding floor, + not physics imprecision (the orchestrator computes in full precision). + """ + # Arrange + inputs = _section_8_inputs(fixture) + + # Act + result = space_heating_monthly_kwh(**inputs) # type: ignore[arg-type] + + # Assert + assert result.total_space_heating_kwh_per_yr == pytest.approx( + fixture.LINE_98C_ANNUAL_KWH, abs=1e-1 + ) + assert result.space_heating_per_m2_kwh == pytest.approx( + fixture.LINE_99_PER_M2_KWH, abs=5e-3 + ) + for m in range(12): + assert result.useful_gains_monthly_w[m] == pytest.approx( + fixture.LINE_95_M_USEFUL_GAINS_W[m], abs=5e-2 + ), f"(95) month {m+1}" + assert result.heat_loss_rate_monthly_w[m] == pytest.approx( + fixture.LINE_97_M_HEAT_LOSS_RATE_W[m], abs=5e-2 + ), f"(97) month {m+1}" + assert result.space_heating_requirement_monthly_kwh[m] == pytest.approx( + fixture.LINE_98A_M_SPACE_HEATING_KWH[m], abs=1e-1 + ), f"(98a) month {m+1}" + assert result.total_space_heating_monthly_kwh[m] == pytest.approx( + fixture.LINE_98C_M_TOTAL_SPACE_HEATING_KWH[m], abs=1e-1 + ), f"(98c) month {m+1}"