mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice S0380.110: per-rooflight g_L in Appendix L L2a (SAP 10.2 p.88)
SAP 10.2 Appendix L §L2a (PDF p.88) verbatim:
GL = 0.9 × Σ (Aw × gL × FF × ZL) / TFA (L2a)
where
FF is the frame factor (fraction of window that is glazed) for
the actual window or from Table 6c
Aw is the area of a window, m²
gL is the light transmittance factor from Table 6b
ZL is the light access factor from Table 6d
Table 6b gL (PDF p.178) — light transmittance column:
Single glazed 0.90
Double glazed (any variant) 0.80
Triple glazed (any variant) 0.70
Table 6d note 2 (PDF p.178): "A solar access factor of 1.0 and a light
access factor of 1.0 should be used for roof windows/rooflights."
Pre-slice `_daylight_factor_from_cert` collapsed every rooflight into
a single `rooflight_total_area_m2 × _G_LIGHT_DEFAULT (0.80) ×
_FRAME_FACTOR_DEFAULT (0.70)` product, overcounting any Triple-glazed
rooflight (gL=0.70) or any non-default frame factor.
Cert 000565 §11 lodges 2 rooflights (per S0380.107 routing):
Item 2 (Ext2 NR rooflight): 1.2 m², "Triple between 2002 and 2021",
PVC FF=0.70 → gL=0.70 (Table 6b Triple). Correct numerator
contribution 1.2 × 0.70 × 0.70 = 0.588; pre-slice cascade used
1.2 × 0.80 × 0.70 = 0.672 (+0.084 over).
Item 5 (Ext4 A rooflight): 0.5 m², "Double between 2002 and 2021",
Wood FF=0.70 → gL=0.80 (Table 6b Double). Already matched.
The +0.084 numerator delta lowered GL → lowered C_daylight → lowered
worksheet (232) by 2.17 kWh/yr.
3-layer fix:
1. `datatypes/epc/domain/epc_property_data.py`: add `glazing_type:
int = 3` to SapRoofWindow (default = Double 2002-2021, the cohort
modal).
2. `datatypes/epc/domain/mapper.py` `_map_elmhurst_roof_window`:
populate `glazing_type` via `_elmhurst_glazing_type_code(w.
glazing_type)` — mirror of `_map_elmhurst_window`.
3. `domain/sap10_calculator/worksheet/internal_gains.py`
`_daylight_factor_from_cert`: iterate `epc.sap_roof_windows` for
the rooflight g_L numerator, dispatching via existing
`_G_LIGHT_BY_GLAZING_CODE` + `rw.frame_factor`. Z_L = 1.0 per
Table 6d note 2.
Test coverage:
- AAA test `test_summary_000565_rooflight_per_window_g_l_routes_via_
glazing_type_per_sap_10_2_appendix_l_l2a` pins both per-rooflight
glazing codes (9 Triple / 3 Double) AND `inputs.lighting_kwh_per_
yr` at 1384.8353 ±1e-4.
- 000516 hand-built fixture updated to explicitly set glazing_type=2
("Double pre 2002") matching the lodged label.
Cert 000565 cascade snapshot (HEAD 98a4b5b9 → this):
sap_score (int) 29 ✓ EXACT (preserved)
lighting_kwh_per_yr 1382.6657 → 1384.8353 ✓ EXACT (-2.17 → 0)
sap_score_continuous 28.5028 → 28.5002 (Δ -0.0059 → -0.0085)
ecf 5.3874 → 5.3877 (Δ +0.0008 → +0.0011)
total_fuel_cost_gbp 4680.78 → 4681.01 (+0.52 → +0.75)
co2_kg_per_yr 6448.34 → 6448.59 (+0.72 → +0.96)
space_heating_kwh 59020.02 → 59019.18 (+11.67 → +10.83)
main_heating_fuel 34717.66 → 34717.16 (+6.87 → +6.37)
Lighting closure exposes a previously-cancelling residual elsewhere —
continuous SAP magnitude widens slightly (-0.0059 → -0.0085) but the
spec-correct path is now in place, per [[feedback-spec-floor-
skepticism]]. SH + main_heating_fuel improve (added lighting energy
contributes internal gains, reducing SH demand). Integer SAP 29 ✓
EXACT preserved.
Cohort safety: 6 cohort certs have at most 1 rooflight each
(000516 W6 only, lodged "Double pre 2002" → code 2). Their gL still
resolves to 0.80 via the existing `_G_LIGHT_BY_GLAZING_CODE` table,
so the per-rooflight dispatch produces the same numerator as the
old default branch.
Pyright net-zero (50 → 50 errors across touched files).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
98a4b5b9e6
commit
9461e657a5
5 changed files with 116 additions and 9 deletions
|
|
@ -2265,6 +2265,91 @@ def test_summary_000565_mev_decentralised_routes_to_extract_or_piv_outside_mv_ki
|
|||
assert epc.sap_ventilation.mechanical_ventilation_kind == "EXTRACT_OR_PIV_OUTSIDE"
|
||||
|
||||
|
||||
def test_summary_000565_rooflight_per_window_g_l_routes_via_glazing_type_per_sap_10_2_appendix_l_l2a() -> None:
|
||||
# Arrange — SAP 10.2 Appendix L §L2a (PDF p.88) verbatim:
|
||||
#
|
||||
# 0.9 × Σ Aw × gL × FF × ZL
|
||||
# GL = --------------------------- (L2a)
|
||||
# TFA
|
||||
#
|
||||
# "where
|
||||
# FF is the frame factor (fraction of window that is glazed) for
|
||||
# the actual window or from Table 6c
|
||||
# Aw is the area of a window, m²
|
||||
# TFA is the total floor area of the dwelling, m²
|
||||
# gL is the light transmittance factor from Table 6b
|
||||
# ZL is the light access factor from Table 6d"
|
||||
#
|
||||
# Table 6b gL by glazing type (PDF p.178):
|
||||
# Single glazed 0.90
|
||||
# Double glazed (any variant) 0.80
|
||||
# Triple glazed (any variant) 0.70
|
||||
#
|
||||
# Table 6d note 2 (PDF p.178): "A solar access factor of 1.0 and a
|
||||
# light access factor of 1.0 should be used for roof windows/
|
||||
# rooflights." → ZL = 1.0 for every rooflight regardless of cert
|
||||
# overshading.
|
||||
#
|
||||
# The numerator sum is PER WINDOW — each rooflight contributes its
|
||||
# own gL and FF, not a single dwelling-wide default. Pre-slice the
|
||||
# cascade collapsed every rooflight into a single
|
||||
# `rooflight_total_area_m2 × _G_LIGHT_DEFAULT (0.80) × _FRAME_FACTOR_
|
||||
# DEFAULT (0.70)` product, which over-counted any rooflight whose
|
||||
# actual gL or FF was below the default.
|
||||
#
|
||||
# Cert 000565 §11 lodges 2 rooflights (per S0380.107 routing):
|
||||
# Item 2 (Ext2 NR rooflight): 1.2 m², "Triple between 2002 and
|
||||
# 2021", PVC frame FF=0.70 → gL=0.70 (Table 6b Triple)
|
||||
# Item 5 (Ext4 A rooflight): 0.5 m², "Double between 2002 and
|
||||
# 2021", Wood frame FF=0.70 → gL=0.80 (Table 6b Double)
|
||||
#
|
||||
# Per-rooflight L2a numerator contributions (Z_L=1.0):
|
||||
# Item 2: 1.2 × 0.70 × 0.70 × 1.0 = 0.5880
|
||||
# Item 5: 0.5 × 0.80 × 0.70 × 1.0 = 0.2800
|
||||
# Sum : 0.8680
|
||||
#
|
||||
# Pre-slice cascade (defaults across both):
|
||||
# Sum : 1.7 × 0.80 × 0.70 × 1.0 = 0.9520 (over by +0.0840)
|
||||
#
|
||||
# The +0.0840 numerator delta lowers GL → lowers C_daylight (via the
|
||||
# L2b convex quadratic 52.2 GL² − 9.94 GL + 1.433) → lowers
|
||||
# E_L,fixed (L9a) → lowers worksheet (232). The cascade was 2.17
|
||||
# kWh/yr under the worksheet's (232) = 1384.8353 kWh/yr until this
|
||||
# spec-correct fix.
|
||||
pages = _summary_pdf_to_textract_style_pages(_SUMMARY_000565_PDF)
|
||||
site_notes = ElmhurstSiteNotesExtractor(pages).extract()
|
||||
|
||||
# Act
|
||||
epc = EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes)
|
||||
inputs = cert_to_inputs(epc)
|
||||
|
||||
# Assert — sap_roof_windows lodge the lodged glazing types so the
|
||||
# cascade's L2a per-rooflight gL dispatch can fire. SAP 10.2 codes:
|
||||
# 9 = "Triple between 2002 and 2021"; 3 = "Double between 2002 and
|
||||
# 2021" (and "Double with unknown install date" variants).
|
||||
assert epc.sap_roof_windows is not None
|
||||
rooflights_by_area = {
|
||||
round(float(rw.area_m2), 2): rw for rw in epc.sap_roof_windows
|
||||
}
|
||||
assert rooflights_by_area[1.2].glazing_type == 9, (
|
||||
f"Ext2 rooflight glazing_type={rooflights_by_area[1.2].glazing_type} "
|
||||
f"(expected 9 'Triple between 2002 and 2021' for gL=0.70 dispatch)"
|
||||
)
|
||||
assert rooflights_by_area[0.5].glazing_type == 3, (
|
||||
f"Ext4 rooflight glazing_type={rooflights_by_area[0.5].glazing_type} "
|
||||
f"(expected 3 'Double between 2002 and 2021' for gL=0.80 dispatch)"
|
||||
)
|
||||
|
||||
# Assert — worksheet (232) closes to PDF lodgement at abs=1e-4 after
|
||||
# the per-rooflight gL dispatch corrects the daylight factor.
|
||||
assert abs(inputs.lighting_kwh_per_yr - 1384.8353) <= 1e-4, (
|
||||
f"cascade lighting_kwh_per_yr={inputs.lighting_kwh_per_yr:.4f}; "
|
||||
f"ws (232)=1384.8353; Δ={inputs.lighting_kwh_per_yr - 1384.8353:+.4f} "
|
||||
f"(expected within 1e-4 after L2a iterates sap_roof_windows for "
|
||||
f"per-rooflight gL × FF instead of applying defaults to total area)"
|
||||
)
|
||||
|
||||
|
||||
def test_summary_mapper_raises_on_unmapped_wall_type_code() -> None:
|
||||
# Arrange — strict-coverage gate per [[reference-unmapped-api-
|
||||
# code]] mirror: an Elmhurst wall_type lodgement that isn't in
|
||||
|
|
|
|||
|
|
@ -205,6 +205,13 @@ class SapRoofWindow:
|
|||
feed `solar_gains_from_cert` — defaults match the modal RdSAP roof
|
||||
window (45° pitch, manufacturer-default DG g⊥=0.76, PVC FF=0.70,
|
||||
N-facing) and are intended to be overridden per-fixture.
|
||||
|
||||
`glazing_type` is the SAP 10.2 Table U2 integer code (e.g. 1=Single,
|
||||
3=Double 2002-2021, 9=Triple 2002-2021) that drives the Appendix L
|
||||
§L2a daylight-factor cascade's per-rooflight g_L lookup (Table 6b
|
||||
Light transmittance column). Defaults to 3 (Double 2002-2021) — the
|
||||
modal cohort lodgement and the type assumed by hand-built worksheet
|
||||
fixtures that pre-date this field.
|
||||
"""
|
||||
|
||||
area_m2: float
|
||||
|
|
@ -213,6 +220,7 @@ class SapRoofWindow:
|
|||
pitch_deg: float = 45.0
|
||||
g_perpendicular: float = 0.76
|
||||
frame_factor: float = 0.70
|
||||
glazing_type: int = 3 # SAP10.2 Table U2; 3 = Double 2002-2021 (cohort modal).
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
|
|||
|
|
@ -3693,6 +3693,12 @@ def _map_elmhurst_roof_window(w: ElmhurstWindow) -> SapRoofWindow:
|
|||
pitch_deg=45.0,
|
||||
g_perpendicular=w.g_value,
|
||||
frame_factor=w.frame_factor,
|
||||
# SAP 10.2 Appendix L §L2a (PDF p.88): the per-rooflight gL
|
||||
# dispatch in `_daylight_factor_from_cert` reads Table 6b via
|
||||
# this code (Single=0.90, Double=0.80, Triple=0.70). Mirrors
|
||||
# `_map_elmhurst_window` so cohort and 000565 rooflights both
|
||||
# carry their lodged glazing-type signal end-to-end.
|
||||
glazing_type=_elmhurst_glazing_type_code(w.glazing_type),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -589,11 +589,17 @@ def _daylight_factor_from_cert(
|
|||
"""Compute C_daylight via L2a + L2b from the cert's windows + any
|
||||
rooflights.
|
||||
|
||||
Per Table 6d note 3 a single Z_L applies to all wall glazing. Per
|
||||
Table 6d note 2 rooflights use Z_L = 1.0 regardless of overshading.
|
||||
Rooflights are summed at default g_L = 0.80 (Table 6b DG) × FF = 0.7
|
||||
(Table 6c PVC) — non-default rooflight glazing or framing requires a
|
||||
richer cert-derived representation in a future slice.
|
||||
Per SAP 10.2 Appendix L §L2a (PDF p.88) the G_L numerator sums each
|
||||
window's `A_w × g_L × FF × Z_L` product — per-window, NOT a single
|
||||
dwelling-wide default. Vertical glazing uses Table 6d's overshading-
|
||||
bucketed Z_L (note 3: same factor across the dwelling). Rooflights
|
||||
use Z_L = 1.0 regardless of overshading (Table 6d note 2).
|
||||
|
||||
Per-rooflight g_L and FF route via `SapRoofWindow.glazing_type` +
|
||||
`frame_factor` — mirrors the per-window dispatch on `sap_windows`.
|
||||
Pre-S0380.110 the rooflight contribution defaulted to
|
||||
`total_area × 0.80 × 0.70`, overcounting Triple-glazed rooflights
|
||||
(g_L=0.70) and any non-default frame factor.
|
||||
|
||||
When `total_floor_area_m2` is missing or no windows are lodged the
|
||||
SAP "no-bonus" default 1.433 is used.
|
||||
|
|
@ -610,11 +616,12 @@ def _daylight_factor_from_cert(
|
|||
* _g_light(w) * _frame_factor(w) * z_l
|
||||
for w in epc.sap_windows
|
||||
)
|
||||
rooflight_g_l_numerator = (
|
||||
rooflight_total_area_m2
|
||||
* _G_LIGHT_DEFAULT
|
||||
* _FRAME_FACTOR_DEFAULT
|
||||
rooflight_g_l_numerator = sum(
|
||||
float(rw.area_m2)
|
||||
* _G_LIGHT_BY_GLAZING_CODE.get(rw.glazing_type, _G_LIGHT_DEFAULT)
|
||||
* float(rw.frame_factor)
|
||||
* 1.0 # Z_L = 1.0 for rooflights per Table 6d note 2
|
||||
for rw in epc.sap_roof_windows or []
|
||||
)
|
||||
g_l = 0.9 * (wall_g_l_numerator + rooflight_g_l_numerator) / tfa
|
||||
if g_l > 0.095:
|
||||
|
|
|
|||
|
|
@ -169,6 +169,7 @@ def build_epc() -> EpcPropertyData:
|
|||
pitch_deg=45.0,
|
||||
g_perpendicular=0.76,
|
||||
frame_factor=0.70,
|
||||
glazing_type=2, # SAP10.2 Table U2 "Double pre 2002"
|
||||
),
|
||||
],
|
||||
percent_draughtproofed=75,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue