§3 exact conformance: non-RR LINE_31 + LINE_36 match Elmhurst worksheets

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 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-20 12:47:01 +00:00
parent a374bd075e
commit 2fd0fe1c08

View file

@ -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