mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
S0380.203: RdSAP 10 §3.7 — "Roof of Room" rooflights deduct from the RR residual
A rooflight deducts from the gross area of the roof element it pierces (RdSAP 10 §3.7, PDF p.19). A "Roof of Room" rooflight (window_wall_type=4 / site-notes "Roof of Room") sits on the room-in-roof sloped ceiling, so its area must deduct from the §3.10.1 RR residual roof — not the flat / loft external roof. The cascade deducted every rooflight from the regular roof (heat_ transmission line 814). Simulated case 6's worksheet is the first worksheet evidence for "Roof of Room" rooflight billing: "Roof room Main remaining area" net 55.54 = gross 61.73 − 6.19 rooflights (U_RR=0.30), while "External roof Main" 14.52 carries no opening. New `_bp_rr_roof_absorbs_rooflight` routes the rooflight area to the RR roof (simplified A_RR_final or detailed §3.10.1 residual) ONLY when the BP's RR contributes such a shell AND lodges no explicit roof surface (slope / flat_ceiling / stud_wall). Case 6 roof (30) 20.2284 → 19.0523 EXACT; demand gap +153 → +61 kWh/yr. Preserved: certs 000565 (Ext2 stud walls) and 000516 (slopes) lodge explicit roof surfaces → rooflight keeps deducting from the regular roof (their 1e-4 worksheet pins hold). Simplified Type 1 RR is excluded too. Re-pin (uniform spec application per [[feedback-software-no-special- handling]] + worksheet-is-truth): API certs 6035 and 0240 are detailed-RR gables-only like case 6 (no worksheet of their own for rooflights), so their "Roof of Room" rooflights now deduct from the RR residual too. This SUPERSEDES the unvalidated S0380.198 "deduct from loft" assumption. - 6035: roof 78.0648 → 73.9176; the previously-"unexplained" +1.37 PE residual COLLAPSES to -0.14 (CO2 -0.0004 → -0.0362; SAP exact 70) — strong corroboration the rooflight-on-RR treatment is correct. - 0240: PE +2.5812 → +2.1519, CO2 +0.1269 → +0.1051 (SAP 72 unchanged). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
3581513b7e
commit
a42e03529c
4 changed files with 113 additions and 16 deletions
|
|
@ -450,6 +450,45 @@ def _part_geometry(part: SapBuildingPart) -> dict[str, float]:
|
|||
}
|
||||
|
||||
|
||||
_RR_ROOF_LODGEMENT_KINDS: Final[frozenset[str]] = frozenset(
|
||||
{"slope", "flat_ceiling", "stud_wall"}
|
||||
)
|
||||
|
||||
|
||||
def _bp_rr_roof_absorbs_rooflight(
|
||||
part: SapBuildingPart, geom: dict[str, Any]
|
||||
) -> bool:
|
||||
"""Whether a rooflight on this building part pierces the room-in-roof
|
||||
sloped ceiling (so it deducts from the RR roof contribution) rather
|
||||
than a flat external roof.
|
||||
|
||||
True ONLY for a Detailed RR (§3.10) lodging wall surfaces but no roof
|
||||
surfaces (gable / common / connected, no slope / flat_ceiling /
|
||||
stud_wall): the §3.10.1 residual roof fires and the rooflight deducts
|
||||
from it (simulated case 6: the 6 "Roof of Room" rooflights deduct from
|
||||
"Roof room Main remaining" net 55.54 = gross 61.73 − 6.19).
|
||||
|
||||
False otherwise:
|
||||
- Simplified Type 1/2 RR (geom A_RR > 0, certs 6035 / 0240): the
|
||||
rooflight pierces the regular loft roof at U_roof, NOT the A_RR
|
||||
shell — its area deducts from `roof_area` (the test
|
||||
`test_6035_api_room_in_roof_gables_deduct_from_roof` pins this).
|
||||
- Detailed RR lodging explicit roof surfaces (cert 000565 Ext2 stud
|
||||
walls / 000516 slopes): the rooflight pierces the regular roof.
|
||||
Both keep the pre-S0380.203 §3.7 "deduct from the host roof" behaviour.
|
||||
"""
|
||||
if geom["rr_simplified_a_rr_m2"] > 0:
|
||||
return False
|
||||
rir = part.sap_room_in_roof
|
||||
if rir is None or not rir.detailed_surfaces:
|
||||
return False
|
||||
if float(rir.floor_area) <= 0.0:
|
||||
return False
|
||||
return not any(
|
||||
s.kind in _RR_ROOF_LODGEMENT_KINDS for s in rir.detailed_surfaces
|
||||
)
|
||||
|
||||
|
||||
def heat_transmission_from_cert(
|
||||
epc: EpcPropertyData,
|
||||
*,
|
||||
|
|
@ -811,7 +850,23 @@ def heat_transmission_from_cert(
|
|||
if "sloping ceiling" in roof_type:
|
||||
top_floor_area = top_floor_area / _COS_30_DEG
|
||||
gross_roof_area = _round_half_up(top_floor_area, _AREA_ROUND_DP)
|
||||
roof_area = max(0.0, gross_roof_area - rw_area_part)
|
||||
# RdSAP 10 §3.7 — a rooflight deducts from the gross roof of the
|
||||
# element it physically pierces. A "Roof of Room" rooflight sits on
|
||||
# the room-in-roof sloped ceiling (the §3.9/§3.10 A_RR shell), not a
|
||||
# flat external roof, so its area deducts from the RR roof
|
||||
# contribution (simplified A_RR_final or the §3.10.1 detailed
|
||||
# residual) rather than `roof_area` — but ONLY when the BP's RR
|
||||
# actually contributes such a shell/residual. Where the BP lodges
|
||||
# explicit roof surfaces (cert 000565 Ext2 stud walls / 000516
|
||||
# slopes), the rooflight pierces those (the regular roof) and
|
||||
# deducts there per §3.7 (current behaviour). Simulated case 6
|
||||
# worksheet: "Roof room Main remaining area" net 55.54 = gross 61.73
|
||||
# − 6.19 rooflights, while "External roof Main" 14.52 carries no
|
||||
# opening.
|
||||
rw_area_on_rr = (
|
||||
rw_area_part if _bp_rr_roof_absorbs_rooflight(part, geom) else 0.0
|
||||
)
|
||||
roof_area = max(0.0, gross_roof_area - (rw_area_part - rw_area_on_rr))
|
||||
floor_area_total = _round_half_up(
|
||||
geom["ground_floor_area_m2"] if exposure.has_exposed_floor else 0.0,
|
||||
_AREA_ROUND_DP,
|
||||
|
|
@ -868,7 +923,9 @@ def heat_transmission_from_cert(
|
|||
rir = part.sap_room_in_roof
|
||||
assert rir is not None # rr_a_rr > 0 ⇒ rir present per _part_geometry
|
||||
walls += uw * (rr_common + rr_gable)
|
||||
a_rr_final = max(0.0, rr_a_rr - rr_common - rr_gable)
|
||||
# Deduct any "Roof of Room" rooflights piercing the RR shell
|
||||
# (see `rw_area_on_rr` rationale at the gross-roof block).
|
||||
a_rr_final = max(0.0, rr_a_rr - rr_common - rr_gable - rw_area_on_rr)
|
||||
u_rr = u_rr_default_all_elements(
|
||||
country=country, age_band=rir.construction_age_band,
|
||||
)
|
||||
|
|
@ -1003,7 +1060,13 @@ def heat_transmission_from_cert(
|
|||
a_rr_shell = _round_half_up(
|
||||
12.5 * sqrt(rr_floor_for_a_rr / 1.5), _AREA_ROUND_DP,
|
||||
)
|
||||
residual_area = max(0.0, a_rr_shell - rr_walls_in_a_rr_area)
|
||||
# Deduct any "Roof of Room" rooflights piercing the RR
|
||||
# residual (see `rw_area_on_rr` rationale at the gross-roof
|
||||
# block) — case 6: 93.09 shell − 31.36 gables − 6.19
|
||||
# rooflights = 55.54 net = worksheet "Roof room remaining".
|
||||
residual_area = max(
|
||||
0.0, a_rr_shell - rr_walls_in_a_rr_area - rw_area_on_rr
|
||||
)
|
||||
if residual_area > 0.0:
|
||||
rr_detailed_area += residual_area
|
||||
roof += residual_area * u_rr_default_all_elements(
|
||||
|
|
|
|||
|
|
@ -83,8 +83,8 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = (
|
|||
cert_number="0240-0200-5706-2365-8010",
|
||||
actual_sap=73,
|
||||
expected_sap_resid=-1,
|
||||
expected_pe_resid_kwh_per_m2=+2.5812,
|
||||
expected_co2_resid_tonnes_per_yr=+0.1269,
|
||||
expected_pe_resid_kwh_per_m2=+2.1519,
|
||||
expected_co2_resid_tonnes_per_yr=+0.1051,
|
||||
notes=(
|
||||
"Detached house, TFA 118, age J, oil boiler PCDB-listed + PV + "
|
||||
"RR on BP[0]. Mapper DOES extract sap_room_in_roof.room_in_roof_"
|
||||
|
|
@ -163,7 +163,13 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = (
|
|||
"internal gain lowers space-heating demand → SAP cont 72.18 → "
|
||||
"72.24 (integer 72 unchanged), PE +2.8092 → +2.5812, CO2 "
|
||||
"+0.1385 → +0.1269 (both closer to zero). Validated against "
|
||||
"case 6 worksheet (70) = 10 (= 3 Main 1 + 7 Main 2)."
|
||||
"case 6 worksheet (70) = 10 (= 3 Main 1 + 7 Main 2). "
|
||||
"Slice S0380.203 routed this cert's 6 'Roof of Room' rooflights "
|
||||
"(window_wall_type=4) to deduct from the §3.10.1 RR residual "
|
||||
"instead of the regular roof (the case-6 worksheet rule). 0240 "
|
||||
"is detailed-RR gables-only like case 6 → roof drops → space-"
|
||||
"heating demand falls → PE +2.5812 → +2.1519, CO2 +0.1269 → "
|
||||
"+0.1051 (both closer to zero; SAP integer 72 unchanged)."
|
||||
),
|
||||
),
|
||||
_GoldenExpectation(
|
||||
|
|
@ -237,8 +243,8 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = (
|
|||
cert_number="6035-7729-2309-0879-2296",
|
||||
actual_sap=70,
|
||||
expected_sap_resid=+0,
|
||||
expected_pe_resid_kwh_per_m2=+1.3743,
|
||||
expected_co2_resid_tonnes_per_yr=-0.0004,
|
||||
expected_pe_resid_kwh_per_m2=-0.1357,
|
||||
expected_co2_resid_tonnes_per_yr=-0.0362,
|
||||
notes=(
|
||||
"Mid-terrace, TFA 128, age A, gas combi Table 4b code 104. "
|
||||
"S0380.189 fixed the dominant driver: walls are solid brick "
|
||||
|
|
@ -288,8 +294,17 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = (
|
|||
"rooflights) which were billed as vertical glazing; routing "
|
||||
"them to roof windows (27a) at inclined U=2.30 + 45° solar "
|
||||
"tightened PE +1.84 → +1.37 and CO2 +0.01 → -0.0004 (SAP still "
|
||||
"exact). Remaining +1.37 PE is unrelated gains/HW (no "
|
||||
"worksheet for 6035 itself to pin further)."
|
||||
"exact). "
|
||||
"Slice S0380.203 CLOSED the remaining +1.37 PE (it was NOT "
|
||||
"'unrelated gains/HW'): the 2 'Roof of Room' rooflights pierce "
|
||||
"the room-in-roof sloped ceiling, so their 1.92 m² deducts from "
|
||||
"the §3.10.1 RR residual (uninsulated U_RR=2.30) — not the "
|
||||
"insulated loft (U=0.14) the S0380.198 assumption used. Roof "
|
||||
"78.0648 → 73.9176 (−4.42 W/K); space-heating demand drops → "
|
||||
"PE +1.37 → -0.14, CO2 -0.0004 → -0.0362 (SAP still exact 70). "
|
||||
"Validated against the simulated-case-6 worksheet, the only "
|
||||
"worksheet evidence for 'Roof of Room' rooflight deduction "
|
||||
"(6035's site-notes case-4 replica lodges no rooflights)."
|
||||
),
|
||||
),
|
||||
_GoldenExpectation(
|
||||
|
|
@ -856,9 +871,16 @@ def test_6035_api_room_in_roof_gables_deduct_from_roof() -> None:
|
|||
# Act
|
||||
roof_w_per_k = heat_transmission_section_from_cert(epc).roof_w_per_k
|
||||
|
||||
# Assert — 78.3336 (gable-deducted residual + loft + ext roof) less
|
||||
# the S0380.198 deduction of 6035's 2 room-in-roof rooflights
|
||||
# (window_wall_type=4, 2 × 1.2×0.8 = 1.92 m²) from the gross roof at
|
||||
# U_roof=0.14 → 78.3336 − 0.2688 = 78.0648. The rooflights' own A×U
|
||||
# moves to roof_windows_w_per_k.
|
||||
assert abs(roof_w_per_k - 78.0648) <= 1e-4
|
||||
# Assert — 78.3336 (gable-deducted residual + loft + ext roof). The 2
|
||||
# room-in-roof rooflights (window_wall_type=4 = "Roof of Room", 1.92 m²)
|
||||
# pierce the RR sloped ceiling, so per S0380.203 their area deducts from
|
||||
# the §3.10.1 residual (at the uninsulated U_RR=2.30) — NOT the insulated
|
||||
# loft at U_roof=0.14 as the unvalidated S0380.198 assumption had it.
|
||||
# 78.3336 − 1.92 × 2.30 = 78.3336 − 4.416 = 73.9176. The rooflights' own
|
||||
# A×U stays on roof_windows_w_per_k. This matches the simulated-case-6
|
||||
# worksheet, where the only worksheet evidence for "Roof of Room"
|
||||
# rooflight deduction shows them billed against "Roof room remaining"
|
||||
# (the RR residual), not the flat/loft roof. Cert 6035 is API-only and
|
||||
# its site-notes case-4 worksheet replica lodges no rooflights, so the
|
||||
# case-6 worksheet is the spec authority for this archetype.
|
||||
assert abs(roof_w_per_k - 73.9176) <= 1e-4
|
||||
|
|
|
|||
|
|
@ -69,6 +69,13 @@ LINE_27_WINDOWS_W_PER_K: Final[float] = 22.7408
|
|||
LINE_27A_ROOF_WINDOWS_W_PER_K: Final[float] = 13.0375
|
||||
LINE_31_TOTAL_EXTERNAL_AREA_M2: Final[float] = 336.13
|
||||
|
||||
# Worksheet (30) Roof total W/K = RR remaining (net of the 6.19 m² roof
|
||||
# windows) 55.54 × 0.30 = 16.6620 + External roof Main 14.52 × 0.11 =
|
||||
# 1.5972 + External roof Ext1 7.21 × 0.11 = 0.7931 → 19.0523. The 6 "Roof
|
||||
# of Room" rooflights pierce the room-in-roof sloped ceiling, so their
|
||||
# area deducts from the RR residual area, NOT the external flat roof.
|
||||
LINE_30_ROOF_W_PER_K: Final[float] = 19.0523
|
||||
|
||||
# Worksheet (231) "Total electricity for the above, kWh/year" (Block 1).
|
||||
# Decomposes as (230c) central heating pump 156 + (230d) oil boiler pump
|
||||
# 200. (230c) = 41 (Main 1 circ pump, "2013 or later") + 115 (Main 2 circ
|
||||
|
|
|
|||
|
|
@ -276,6 +276,11 @@ def test_section_3_roof_windows_case6_match_pdf() -> None:
|
|||
_w001431_case6.LINE_31_TOTAL_EXTERNAL_AREA_M2,
|
||||
"§3 (31) case6",
|
||||
)
|
||||
_pin(
|
||||
ht.roof_w_per_k,
|
||||
_w001431_case6.LINE_30_ROOF_W_PER_K,
|
||||
"§3 (30) case6",
|
||||
)
|
||||
|
||||
|
||||
def test_section_4f_pumps_fans_case6_match_pdf() -> None:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue