Elmhurst 000474: §3 LINE_33 + LINE_37 close exactly

Closes the second non-RR Elmhurst worksheet (mid-terrace, 3 parts).
LINE_33 (209.1084) and LINE_37 (232.1169) reproduce to 0.1 W/K.

Cert inputs lodged on the fixture:
  - Ext1 SapFloorDimension(is_exposed_floor=True) — Table 20 route
  - Ext2 ground floor (tiny 1.35 m², P=3.30) stays on Table 19 fn 1
    suspended-timber default for age B (cascade → U≈1.25, worksheet 1.25)
  - door_count=2 → 3.70 m² total door area
  - WINDOW_TOTAL_AREA_M2=11.72 split across two glazing types
    (Type 1: 6.22 m² post-2002 raw U=2.0, Type 2: 5.50 m² pre-2002 raw
    U=2.8). Area-weighted aggregate raw U=2.37 reproduces the worksheet's
    25.37 W/K through the curtain-resistance transform.

Non-RR §3 scope closed:
  - LINE_31  exact (existing test)
  - LINE_33  exact ← this slice + the 000490 slice
  - LINE_36  exact (existing test, y × LINE_31)
  - LINE_37  exact ← this slice + the 000490 slice

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-20 14:14:08 +00:00
parent 4479fc69ac
commit cf244762d5
2 changed files with 46 additions and 0 deletions

View file

@ -64,11 +64,14 @@ def build_epc() -> EpcPropertyData:
wall_thickness_measured=False,
party_wall_construction=0,
sap_floor_dimensions=[
# Ext1 hangs off the main from the first storey upward — its
# lowest dimension is an exposed timber floor (Table 20).
SapFloorDimension(
room_height_m=2.81, # lowest of ext, internal
total_floor_area_m2=15.04,
party_wall_length_m=3.56, heat_loss_perimeter_m=8.46,
floor=0,
is_exposed_floor=True,
),
SapFloorDimension(
room_height_m=3.13, # = 2.88 + 0.25
@ -163,3 +166,18 @@ LINE_31_TOTAL_EXTERNAL_AREA_M2: float = 153.3900
LINE_33_FABRIC_HEAT_LOSS_W_PER_K: float = 209.1084
LINE_36_THERMAL_BRIDGING_W_PER_K: float = 23.0085
LINE_37_TOTAL_FABRIC_HEAT_LOSS_W_PER_K: float = 232.1169
# §3 windows + doors — values from the Elmhurst worksheet §3 table.
# Two window types share the dwelling:
# Type 1 (post-2002 double-glazed, PVC, g=0.72): 6.22 m² at raw U=2.0,
# U_eff=1.8519
# Type 2 (pre-2002 double-glazed, PVC, g=0.76): 5.50 m² at raw U=2.8,
# U_eff=2.5180
# Worksheet windows_w_per_k = 25.3675. Aggregating to a single area-
# weighted raw U with the curtain-resistance transform applied reproduces
# the total to better than 0.05 W/K:
# U_eff_target = 25.3675 / 11.72 = 2.1645
# raw U = 1/(1/2.1645 - 0.04) ≈ 2.37
WINDOW_TOTAL_AREA_M2: float = 11.72
WINDOW_AVG_RAW_U_VALUE: float = 2.37
DOOR_COUNT: int = 2 # cascade default 1.85 m²/door → 3.70 m² matches worksheet

View file

@ -1179,6 +1179,34 @@ def test_section_3_non_rr_line_31_and_36_match_elmhurst_worksheet(
)
def test_section_3_line_33_and_line_37_match_elmhurst_worksheet_000474() -> None:
"""Full §3 fabric heat loss for Elmhurst U985-0001-000474. Mid-terrace
with 3 building parts (Main + 2 extensions). Ext1 is the exposed-timber
upper floor (same shape as 000490 Ext1). Two window types share an
area-weighted raw U-value when aggregated (post-2002 raw 2.0 mixed
with pre-2002 raw 2.8); reproducing the worksheet's per-type sum of
25.37 W/K to 0.1 W/K is sufficient given the curtain-resistance
transform is non-linear."""
# Arrange
epc = _w000474.build_epc()
# Act
result = heat_transmission_from_cert(
epc,
window_total_area_m2=_w000474.WINDOW_TOTAL_AREA_M2,
window_avg_u_value=_w000474.WINDOW_AVG_RAW_U_VALUE,
door_count=_w000474.DOOR_COUNT,
)
# Assert
assert result.fabric_heat_loss_w_per_k == pytest.approx(
_w000474.LINE_33_FABRIC_HEAT_LOSS_W_PER_K, abs=0.1
)
assert result.total_w_per_k == pytest.approx(
_w000474.LINE_37_TOTAL_FABRIC_HEAT_LOSS_W_PER_K, abs=0.1
)
def test_section_3_line_33_and_line_37_match_elmhurst_worksheet_000490() -> None:
"""Full §3 fabric heat loss for Elmhurst U985-0001-000490. Once the
suspended/exposed floor routes are wired and the cert lodges the