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