From 2fd0fe1c08380eddfd05fcc0fe24d01b0adbc537 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 20 May 2026 12:47:01 +0000 Subject: [PATCH] =?UTF-8?q?=C2=A73=20exact=20conformance:=20non-RR=20LINE?= =?UTF-8?q?=5F31=20+=20LINE=5F36=20match=20Elmhurst=20worksheets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LINE_31 (total external element area) = Σ_parts (gross_wall + roof + floor). Window and door areas cancel in the net-wall expansion, so LINE_31 is independent of the window/door split. This lets us assert the exact Elmhurst worksheet (31) for the two non-RR fixtures (000474, 000490) without needing window-area input data. LINE_36 = y × LINE_31 follows for free. Both 000474 and 000490 use age band B throughout (y = 0.15), giving: 000474: 0.15 × 153.39 = 23.0085 000490: 0.15 × 164.85 = 24.7275 The per-storey-perimeter fix (e6c768c3) was the prerequisite; without it, upper storeys with a smaller perimeter than the ground floor were over-counted (e.g. 000474 Main: 7.07 m ground vs 5.27 m first storey). Co-Authored-By: Claude Sonnet 4.6 --- .../worksheet/tests/test_heat_transmission.py | 53 +++++++++++++++++-- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/packages/domain/src/domain/sap/worksheet/tests/test_heat_transmission.py b/packages/domain/src/domain/sap/worksheet/tests/test_heat_transmission.py index 4960166e..e50969d1 100644 --- a/packages/domain/src/domain/sap/worksheet/tests/test_heat_transmission.py +++ b/packages/domain/src/domain/sap/worksheet/tests/test_heat_transmission.py @@ -1093,6 +1093,51 @@ from domain.sap.worksheet.tests._elmhurst_fixtures import ( # noqa: E402 ALL_FIXTURES as _ELMHURST_FIXTURES, fixture_id as _elmhurst_fixture_id, ) +from domain.sap.worksheet.tests import ( # noqa: E402 + _elmhurst_worksheet_000474 as _w000474, + _elmhurst_worksheet_000490 as _w000490, +) + +_NON_RR_FIXTURES: tuple[ModuleType, ...] = (_w000474, _w000490) + + +@pytest.mark.parametrize("fixture", _NON_RR_FIXTURES, ids=_elmhurst_fixture_id) +def test_section_3_non_rr_line_31_and_36_match_elmhurst_worksheet( + fixture: ModuleType, +) -> None: + """Non-RR fixtures: (31) total external element area and (36) thermal + bridging match the Elmhurst worksheet exactly. + + LINE_31 = Σ_parts (gross_wall + roof + floor). Window and door areas + cancel when expanding the net-wall term: gross_wall − w − d + w + d = + gross_wall. So LINE_31 is independent of window/door apportionment, + meaning we can assert it with zero window area and still hit the exact + worksheet value. LINE_36 = y × LINE_31 follows for free. + + The per-storey-perimeter fix (commit e6c768c3) enabled this: before + that fix, gross_wall used ground_perim × avg_height × count, which + over-counted whenever upper storeys have a smaller perimeter than the + ground floor (e.g. 000474 Main: 7.07 m ground vs 5.27 m first storey). + """ + # Arrange — window/door values are irrelevant for LINE_31; pass zeros + # to make the independence explicit. + epc = fixture.build_epc() + + # Act + result = heat_transmission_from_cert( + epc, + window_total_area_m2=0.0, + window_avg_u_value=None, + door_count=0, + ) + + # Assert + assert result.total_external_element_area_m2 == pytest.approx( + fixture.LINE_31_TOTAL_EXTERNAL_AREA_M2, abs=0.01 + ) + assert result.thermal_bridging_w_per_k == pytest.approx( + fixture.LINE_36_THERMAL_BRIDGING_W_PER_K, abs=0.01 + ) @pytest.mark.parametrize("fixture", _ELMHURST_FIXTURES, ids=_elmhurst_fixture_id) @@ -1139,8 +1184,8 @@ def test_section_3_partial_match_against_elmhurst_worksheet(fixture: ModuleType) assert result.total_w_per_k == pytest.approx( result.fabric_heat_loss_w_per_k + result.thermal_bridging_w_per_k, rel=1e-9 ) - # External-area: RR fixtures still under-count vs worksheet because RR - # sub-areas are not modelled (gap #1). Non-RR fixtures should match - # the worksheet's (31) once the per-storey-perimeter fix lands; for - # now keep the looser non-zero check until RR closes. + # External-area: RR fixtures under-count vs worksheet because RR + # sub-areas (gable/slope/stud-wall/flat-ceiling) are not modelled + # (gap #1). Non-RR fixtures get exact (31) + (36) asserted by the + # dedicated `test_section_3_non_rr_line_31_and_36_match_elmhurst_worksheet`. assert result.total_external_element_area_m2 > 0