From 0851b48807cddbd4515b3b4cd82883adc7b73ae3 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Sun, 28 Jun 2026 21:25:20 +0000 Subject: [PATCH] =?UTF-8?q?fix(elmhurst):=20party-ceiling=20Main=20BP=20wi?= =?UTF-8?q?ndow=20is=20vertical,=20not=20a=20rooflight=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _is_elmhurst_roof_window routed a vertical window (Location "External wall") to sap_roof_windows whenever its building part's roof type was a party ceiling (A "Another dwelling above" / NR "Non-residential space above"). That is right for an EXTENSION (cert 000565 Ext2/Ext4 score on worksheet (27a)) but wrong for the MAIN BP: a Main "A"/"NR" roof is the dwelling's own party ceiling — no external roof — so its windows are vertical. Gate the BP-roof-type rooflight rule to non-Main building parts. On a mid- floor flat (case 53 / cert 000565 re-keyed) the Main External-wall window is now vertical: its area deducts from the wall (was over-counted ~7 W/K) and it prices as a window. Summary-path SAP 74.0 -> 75.17, matching Elmhurst's accredited worksheet (75; space-heat 3408.6 vs 3408.58) — confirming the calculator is correct; the gap was the misclassification. 000565's extension rooflights, the "Roof of Room" branch, and the U>3.0 room-in-roof backstop are unchanged. gov-API path unaffected (it routes roof windows via window_wall_type==4). Co-Authored-By: Claude Opus 4.8 (1M context) --- datatypes/epc/domain/mapper.py | 46 +++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index efacc71b..e0758e3d 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -6229,12 +6229,16 @@ def _elmhurst_orientation_int(orientation: str) -> int: # skylight per the worksheet's (27a) row. _ELMHURST_ROOF_WINDOW_U_THRESHOLD: Final[float] = 3.0 -# RdSAP 10 §8.2 (PDF p.50) — BPs whose roof type is "Another dwelling -# above" (A) or "Non-residential space above" (NR) have their own -# external roof structure with potential rooflights, distinct from a -# pitched-roof dwelling. Windows lodged against these BPs are routed -# to `sap_roof_windows` regardless of their U-value. +# Elmhurst §8 roof types that route an EXTENSION's window to a rooflight: +# "A Another dwelling above" / "NR Non-residential space above". On an +# extension BP these score on the worksheet (27a) Roof Windows line (cert +# 000565 Ext2 [NR] + Ext4 [A]). NOT applied to the Main BP — a Main roof +# of "A"/"NR" means the whole dwelling sits under another dwelling (a party +# ceiling with no external roof), so its windows are vertical (simulated +# case 53 / cert 000565 re-keyed as a mid-floor flat: Main roof "A", +# External-wall window U 2.0 = vertical, Elmhurst Type "Window"). _ELMHURST_BP_ROOF_TYPES_WITH_ROOFLIGHTS: Final[tuple[str, ...]] = ("A ", "NR ") +_ELMHURST_MAIN_BUILDING_PARTS: Final[tuple[str, ...]] = ("Main", "Main Property") def _elmhurst_bp_roof_type( @@ -6245,7 +6249,7 @@ def _elmhurst_bp_roof_type( Returns None when the BP isn't found (single-bp cert with no matching extension).""" bp = w.building_part - if bp in ("Main", "Main Property"): + if bp in _ELMHURST_MAIN_BUILDING_PARTS: return survey.roof.roof_type for ext in survey.extensions: if ext.name == bp: @@ -6279,12 +6283,16 @@ def _is_elmhurst_roof_window( 1. Single-glazed windows are never rooflights — Part L 2006 minimum glazing for any rooflight is double-glazed. - 2. BP roof type ∈ {A, NR} → rooflight — the BP has its own - external roof structure with rooflights (worksheet (30) - External roof + (27a) Roof Windows treatment). - 3. U > 3.0 W/m²K → rooflight — cohort backstop catching old - skylights on pitched roofs (cohort cert 000516 W6). - 4. Otherwise vertical. + 2. Explicit "Roof of Room" location → rooflight. + 3. EXTENSION BP roof type ∈ {A, NR} → rooflight (worksheet (30) + External roof + (27a) Roof Windows treatment). EXCLUDES the Main + BP: a Main roof of "A"/"NR" is a party ceiling (whole dwelling + under another dwelling), so its windows are vertical — simulated + case 53 / cert 000565 mid-floor flat (Main roof "A", External-wall + window U 2.0, Elmhurst Type "Window"). + 4. U > 3.0 W/m²K AND the BP has a room-in-roof → rooflight — cohort + backstop catching old skylights on a sloping ceiling (cert 000516 W6). + 5. Otherwise vertical. """ if w.glazing_type.startswith("Single"): return False @@ -6293,11 +6301,15 @@ def _is_elmhurst_roof_window( # regardless of BP roof type or U-value. if "roof of room" in (w.location or "").lower(): return True - bp_roof_type = _elmhurst_bp_roof_type(w, survey) - if bp_roof_type is not None and bp_roof_type.startswith( - _ELMHURST_BP_ROOF_TYPES_WITH_ROOFLIGHTS - ): - return True + # EXTENSION BP whose roof type is a party ceiling (A / NR) scores its + # windows on (27a) — but never the Main BP (a Main "A"/"NR" roof is the + # dwelling's own party ceiling; case 53). + if w.building_part not in _ELMHURST_MAIN_BUILDING_PARTS: + bp_roof_type = _elmhurst_bp_roof_type(w, survey) + if bp_roof_type is not None and bp_roof_type.startswith( + _ELMHURST_BP_ROOF_TYPES_WITH_ROOFLIGHTS + ): + return True # U > 3.0 backstop, gated on the BP having a room-in-roof. Elmhurst routes a # high-U "Double pre 2002" unit through the worksheet's (27a) Roof Windows # line regardless of its lodged "External wall" location — but ONLY where the