mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
S0380.198: API window_wall_type=4 → roof window (SAP 10.2 §3 (27a) + Table 6e Note 2)
Cert 0240's SAP residual (-1) and a chunk of its PE/CO2 was an API-mapper
bug: it flattened ALL windows into sap_windows, so the 6 windows lodged
with window_wall_type=4 — the RdSAP code for a roof window ("Roof of Room"
rooflight / inclined glazing) — were billed as vertical wall glazing on
worksheet (27) at U=2.0, instead of roof windows on (27a) at the Table 6e
Note 2 inclination-adjusted U (DG 2002+ vertical 2.0 + 0.30 = 2.30) with
45°-inclined solar gains.
window_wall_type=4 is the discriminator, NOT window_type=2 (certs 0390 /
7536 lodge window_type=2 on ordinary main-wall windows). Fix: partition
the 21.0.1 API window list into sap_windows (wall_type≠4) + sap_roof_
windows (wall_type=4); `_api_sap_roof_window` mirrors the site-notes
`_map_elmhurst_roof_window` (vertical U from the glazing Table-24 lookup +
0.30 inclination; 45° pitch; g/FF from the same lookup).
Validated against the simulated-case-6 worksheet, which bills these
identical windows on (27a) at U_eff 2.1062 (= 2.30 with the §3.2 R=0.04
curtain transform). The inclined solar gain dominates the higher U-loss,
RAISING the SAP:
- 0240: SAP cont 72.14 → 72.55 (resid -1 → +0 EXACT), PE +3.91 → +1.95,
CO2 +0.22 → +0.12
- 6035: 2 wall_type=4 rooflights — SAP still +0 exact, PE +1.84 → +1.37,
CO2 +0.01 → -0.0004
Blast radius is exactly these two certs (only golden fixtures with
wall_type=4). Suite: 2354 passed, 1 skipped. New code: 0 pyright errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
570df83459
commit
999eced9fb
2 changed files with 112 additions and 12 deletions
|
|
@ -1587,9 +1587,17 @@ class EpcPropertyDataMapper:
|
|||
schema.sap_heating.shower_outlets, _API_SHOWER_OUTLET_CODE_MIXER,
|
||||
),
|
||||
),
|
||||
# SAP windows
|
||||
# SAP windows — split vertical wall windows (27) from roof
|
||||
# windows (27a) on the RdSAP `window_wall_type=4` signal.
|
||||
sap_windows=[
|
||||
_api_sap_window(w) for w in schema.sap_windows
|
||||
_api_sap_window(w)
|
||||
for w in schema.sap_windows
|
||||
if not _api_is_roof_window(w)
|
||||
],
|
||||
sap_roof_windows=[
|
||||
_api_sap_roof_window(w)
|
||||
for w in schema.sap_windows
|
||||
if _api_is_roof_window(w)
|
||||
],
|
||||
# SAP energy source
|
||||
sap_energy_source=SapEnergySource(
|
||||
|
|
@ -2631,6 +2639,50 @@ def _api_secondary_fuel_type(
|
|||
return lodged_fuel_type
|
||||
|
||||
|
||||
# RdSAP API `window_wall_type` code 4 = roof window ("Roof of Room"
|
||||
# rooflight / inclined glazing). Codes 1=main wall, 2=alt wall 1, 3=alt
|
||||
# wall 2 (see `_window_on_alt_wall`). A roof window is billed on worksheet
|
||||
# (27a) at the Table 6e Note 2 inclination-adjusted U and draws 45°-
|
||||
# inclined solar gains, NOT on (27) as vertical glazing. Cert 0240's 6
|
||||
# "Roof of Room" windows lodge this code; the simulated-case-6 worksheet
|
||||
# confirms the (27a) treatment at U_eff 2.1062.
|
||||
_API_WINDOW_WALL_TYPE_ROOF: Final[int] = 4
|
||||
|
||||
|
||||
def _api_is_roof_window(w: Any) -> bool:
|
||||
"""True when an API sap_windows entry is a roof window (rooflight),
|
||||
keyed on `window_wall_type == 4`. `window_type` is NOT the signal —
|
||||
certs 0390 / 7536 lodge `window_type=2` on ordinary main-wall
|
||||
(wall_type=1) windows."""
|
||||
return w.window_wall_type == _API_WINDOW_WALL_TYPE_ROOF
|
||||
|
||||
|
||||
def _api_sap_roof_window(w: Any) -> SapRoofWindow:
|
||||
"""Build a `SapRoofWindow` from one API roof-window entry
|
||||
(`window_wall_type=4`). The lodged glazing type gives the vertical
|
||||
U / g / frame-factor via the same SAP 10.2 Table 24 lookup the
|
||||
vertical-window path uses; the U is then raised by the SAP 10.2
|
||||
Table 6e Note 2 inclination adjustment (+0.30 W/m²K at 45° pitch) to
|
||||
the inclined-position value the worksheet bills on (27a). Mirror of
|
||||
the site-notes `_map_elmhurst_roof_window`."""
|
||||
transmission = _api_glazing_transmission(w.glazing_type, w.glazing_gap)
|
||||
vertical_u = transmission[0] if transmission is not None else 2.0
|
||||
g_perp = transmission[1] if transmission is not None else 0.76
|
||||
frame_factor = w.frame_factor
|
||||
if frame_factor is None:
|
||||
frame_factor = transmission[2] if transmission is not None else 0.70
|
||||
return SapRoofWindow(
|
||||
area_m2=_measurement_value(w.window_width) * _measurement_value(w.window_height),
|
||||
u_value_raw=vertical_u + _ELMHURST_ROOF_WINDOW_INCLINATION_ADJUSTMENT_W_PER_M2K,
|
||||
orientation=w.orientation,
|
||||
pitch_deg=45.0,
|
||||
g_perpendicular=g_perp,
|
||||
frame_factor=frame_factor,
|
||||
glazing_type=_api_cascade_glazing_type(w.glazing_type),
|
||||
window_location=w.window_location,
|
||||
)
|
||||
|
||||
|
||||
def _api_sap_window(w: Any) -> SapWindow:
|
||||
"""Build a `SapWindow` from one API schema sap_windows entry,
|
||||
routing the glazing-type + glazing-gap pair through the spec
|
||||
|
|
|
|||
|
|
@ -82,9 +82,9 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = (
|
|||
_GoldenExpectation(
|
||||
cert_number="0240-0200-5706-2365-8010",
|
||||
actual_sap=73,
|
||||
expected_sap_resid=-1,
|
||||
expected_pe_resid_kwh_per_m2=+3.9138,
|
||||
expected_co2_resid_tonnes_per_yr=+0.2213,
|
||||
expected_sap_resid=+0,
|
||||
expected_pe_resid_kwh_per_m2=+1.9459,
|
||||
expected_co2_resid_tonnes_per_yr=+0.1226,
|
||||
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_"
|
||||
|
|
@ -127,7 +127,16 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = (
|
|||
"Party) with no height, previously dropped → roof over-count. "
|
||||
"Routing them through `detailed_surfaces` deducts 2×(6.4×2.45) "
|
||||
"from the A_RR shell → roof drops, tightening PE +5.8007 → "
|
||||
"+3.9138, CO2 +0.3173 → +0.2213. SAP integer unchanged at 72."
|
||||
"+3.9138, CO2 +0.3173 → +0.2213. SAP integer unchanged at 72. "
|
||||
"Slice S0380.198 CLOSED the SAP: this cert lodges 6 windows "
|
||||
"with `window_wall_type=4` = roof windows ('Roof of Room' "
|
||||
"rooflights). The API mapper had flattened them into "
|
||||
"`sap_windows` (vertical glazing, (27), U=2.0); they belong on "
|
||||
"(27a) at the Table 6e Note 2 inclination-adjusted U=2.30 with "
|
||||
"45°-inclined solar gains. Validated against the simulated-"
|
||||
"case-6 worksheet ((27a) U_eff 2.1062). The inclined solar "
|
||||
"gain dominates → SAP cont 72.14 → 72.55 (resid -1 → +0 "
|
||||
"EXACT), PE +3.9138 → +1.9459, CO2 +0.2213 → +0.1226."
|
||||
),
|
||||
),
|
||||
_GoldenExpectation(
|
||||
|
|
@ -201,8 +210,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.8379,
|
||||
expected_co2_resid_tonnes_per_yr=+0.0103,
|
||||
expected_pe_resid_kwh_per_m2=+1.3743,
|
||||
expected_co2_resid_tonnes_per_yr=-0.0004,
|
||||
notes=(
|
||||
"Mid-terrace, TFA 128, age A, gas combi Table 4b code 104. "
|
||||
"S0380.189 fixed the dominant driver: walls are solid brick "
|
||||
|
|
@ -246,8 +255,14 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = (
|
|||
"height 2.45 m; Exposed → gable_wall_external, Party → "
|
||||
"gable_wall) so the cascade's Detailed-RR residual fires. "
|
||||
"SAP resid -2 → +0 (exact), PE +19.16 → +1.84, CO2 "
|
||||
"+0.42 → +0.01. Remaining +1.84 PE is unrelated gains/HW "
|
||||
"(no worksheet for 6035 itself to pin further)."
|
||||
"+0.42 → +0.01. "
|
||||
"Slice S0380.198 (the 0240 roof-window fix) also applies: "
|
||||
"6035 lodges 2 windows with `window_wall_type=4` (room-in-roof "
|
||||
"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)."
|
||||
),
|
||||
),
|
||||
_GoldenExpectation(
|
||||
|
|
@ -757,6 +772,35 @@ def test_api_to_domain_mapper_preserves_main_heating_index_number(
|
|||
assert abs(inputs.main_heating_efficiency - expected_winter_eff) <= 1e-3
|
||||
|
||||
|
||||
def test_0240_api_wall_type_4_windows_map_to_roof_windows() -> None:
|
||||
"""Cert 0240 lodges 6 windows with `window_wall_type=4` — the RdSAP
|
||||
API code for a roof window ("Roof of Room" rooflight / inclined
|
||||
glazing), distinct from main-wall (1) and alternative-wall (2/3)
|
||||
windows. They belong on worksheet line (27a) Roof Windows at the
|
||||
Table 6e Note 2 inclination-adjusted U (DG 2002+ vertical 2.0 + 0.30
|
||||
= 2.30 W/m²K), with 45°-inclined solar gains — NOT on (27) as vertical
|
||||
wall windows at U=2.0.
|
||||
|
||||
Before the fix the API mapper flattened all windows into
|
||||
`sap_windows`, so these 6 billed as vertical glazing (wrong U *and*
|
||||
wrong solar). Validated against the simulated-case-6 worksheet, which
|
||||
bills the identical 6 windows on (27a) at U_eff 2.1062 (= 2.30 with
|
||||
the §3.2 R=0.04 curtain transform).
|
||||
"""
|
||||
# Arrange
|
||||
doc = _load_cert("0240-0200-5706-2365-8010")
|
||||
|
||||
# Act
|
||||
epc = EpcPropertyDataMapper.from_api_response(doc)
|
||||
|
||||
# Assert — the 6 wall_type=4 windows route to roof windows; the other
|
||||
# 5 (wall_type=1, main wall) stay vertical.
|
||||
assert epc.sap_roof_windows is not None
|
||||
assert len(epc.sap_roof_windows) == 6
|
||||
assert len(epc.sap_windows) == 5
|
||||
assert all(abs(rw.u_value_raw - 2.30) <= 1e-9 for rw in epc.sap_roof_windows)
|
||||
|
||||
|
||||
def test_6035_api_room_in_roof_gables_deduct_from_roof() -> None:
|
||||
"""Cert 6035 lodges a Simplified Type 1 room-in-roof (`room_in_roof_
|
||||
type_1`) with two gable walls (L=4.65 each). Per RdSAP 10 §3.9.1(e)
|
||||
|
|
@ -785,5 +829,9 @@ 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
|
||||
assert abs(roof_w_per_k - 78.3336) <= 1e-4
|
||||
# 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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue