fix(elmhurst): party-ceiling Main BP window is vertical, not a rooflight 🟩

_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) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-28 21:25:20 +00:00
parent 23245a085c
commit 0851b48807

View file

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