diff --git a/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py b/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py index 6bcc0a2e..5258caeb 100644 --- a/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py +++ b/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py @@ -2350,6 +2350,89 @@ def test_summary_000565_rooflight_per_window_g_l_routes_via_glazing_type_per_sap ) +def test_summary_000565_roof_window_u_value_applies_table_6e_note_2_inclination_adjustment_per_sap_10_2_section_3_2() -> None: + # Arrange — SAP 10.2 §3.2 "Roof windows" (PDF p.10) verbatim: + # + # "In the case of roof windows, unless the measurement or + # calculation has been done for the actual inclination of the + # roof window, adjustments as given in Notes 1 and 2 to Table 6e + # or from BR443 (2019) should be applied." + # + # SAP 10.2 Table 6e Note 2 (PDF p.180) — "For roof windows the + # following adjustments should be applied to convert a known + # vertical U-value into the U-value for the known inclined + # position": + # + # Inclination Twin skin or DG Triple skin or TG + # 70° or more (vertical) +0.0 +0.0 + # < 70° and > 60° +0.2 +0.1 + # 60° and > 40° +0.3 +0.2 + # 40° and > 30° +0.4 +0.2 + # 30° or less (horizontal) +0.5 +0.3 + # + # SAP 10.2 §3.2 formula (2) — curtain transform applied after the + # inclination adjustment: + # + # U_w,effective = 1 / (1/U_w + 0.04) (2) + # + # Cert 000565 §11 lodges 2 roof windows (per S0380.107 routing) at + # pitch=45° (Openings table: "Roof Windows 1(Ext2), Roof Window, + # External roof Ext2, North West, 45, ..."): + # + # Item 2 (Ext2 NR): 1.2 m², "Triple between 2002 and 2021", + # PVC FF=0.70, Manufacturer U=2.0, g=0.72 + # Item 5 (Ext4 A): 0.5 m², "Double between 2002 and 2021", + # Wood FF=0.70, Manufacturer U=2.0, g=0.72 + # + # Both lodge as Manufacturer-supplied U=2.0 (vertical-tested per + # Table 6e header), so Note 2 inclination adjustment applies. The + # worksheet (27a) shows U_eff = 2.1062 for BOTH items — back-solving + # via formula (2): 1/2.1062 = 0.4748; 0.4748 - 0.04 = 0.4348; + # U_inclined = 1/0.4348 = 2.3000 = U_raw + 0.30. Elmhurst applies + # the DG-column +0.30 adjustment uniformly across roof windows at + # 40-60° inclination (the Triple-glazed-column +0.20 alternative + # would yield 2.0222, contradicting the worksheet's 2.1062 for the + # Triple item). The +0.30 = Note 2 "60° and > 40°" DG row. + # + # Worksheet (27a) totals: 1.2 × 2.1062 + 0.5 × 2.1062 = 3.5806 W/K. + # Pre-slice cascade: u_eff = 1/(1/2.0 + 0.04) = 1.852 for both → + # 1.7 × 1.852 = 3.1484 W/K. Net residual -0.43 W/K. + # + # Cohort safety: cert 000516 W6 ("Double pre 2002", Manufacturer + # U=3.10) is routed via the mapper's RdSAP10 Table 24 lookup which + # already returns 3.40 (the pre-adjusted inclined-position value + # per RdSAP10 Table 24 "Roof window" column). The new inclination + # adjustment fires ONLY in the fall-through branch (i.e. when the + # lodged glazing label is not in `_ELMHURST_ROOF_WINDOW_U_BY_ + # GLAZING`), so 000516's 3.40 stays unchanged. + pages = _summary_pdf_to_textract_style_pages(_SUMMARY_000565_PDF) + site_notes = ElmhurstSiteNotesExtractor(pages).extract() + epc = EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes) + from domain.sap10_calculator.rdsap.cert_to_inputs import heat_transmission_section_from_cert + + # Act + ht = heat_transmission_section_from_cert(epc) + + # Assert — sap_roof_windows[*].u_value_raw carries the inclined- + # position U (mapper applies +0.30) so the cascade's formula (2) + # curtain transform lands on the worksheet's U_eff=2.1062. + assert epc.sap_roof_windows is not None + inclined_us = sorted(round(float(rw.u_value_raw), 4) for rw in epc.sap_roof_windows) + assert inclined_us == [2.3000, 2.3000], ( + f"sap_roof_windows u_value_raw: {inclined_us} (expected [2.3, 2.3] " + f"after Table 6e Note 2 DG-column +0.30 W/m²K adjustment fires on " + f"both rooflights for pitch 40-60°)" + ) + # Assert — roof_windows_w_per_k closes to the worksheet's Σ A×U_eff + # at abs=1e-4. ws (27a) = 1.2×2.1062 + 0.5×2.1062 = 3.5805 W/K. + assert abs(ht.roof_windows_w_per_k - 3.5805) <= 1e-4, ( + f"cascade roof_windows_w_per_k={ht.roof_windows_w_per_k:.4f}; " + f"ws (27a)=3.5805; Δ={ht.roof_windows_w_per_k - 3.5805:+.4f} " + f"(expected within 1e-4 after Table 6e Note 2 inclination " + f"adjustment + formula (2) curtain transform)" + ) + + 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 diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index c5ca153f..91dcd743 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -3677,12 +3677,37 @@ _ELMHURST_ROOF_WINDOW_U_BY_GLAZING: Dict[str, float] = { } +# SAP 10.2 Table 6e Note 2 (PDF p.180) — inclination adjustment from +# vertical-tested U-value to inclined-position U-value. Pitch=45° +# (the cohort + cert 000565 default) falls in "60° and > 40°"; the +# DG-column adjustment is +0.30 W/m²K. Elmhurst applies the DG-column +# adjustment uniformly across roof windows at this pitch regardless of +# the lodged glazing type — the worksheet (27a) for cert 000565 shows +# U_eff = 2.1062 for BOTH the Triple (Item 2) and Double (Item 5) +# rooflights, back-solving via formula (2) to U_inclined = 2.30 = +# 2.0 + 0.30 in both cases. The strict Table 6e Note 2 Triple-column +# +0.20 alternative would yield 2.0222 for Item 2, contradicting the +# worksheet. +_ELMHURST_ROOF_WINDOW_INCLINATION_ADJUSTMENT_W_PER_M2K: Final[float] = 0.30 + + def _elmhurst_roof_window_u_value(w: ElmhurstWindow) -> float: """Roof-window U-value per RdSAP10 Table 24 — keyed on the lodged - glazing-type phrase. Falls back to the cert-lodged Manufacturer U - when the glazing type isn't in the table (lets new fixtures - surface uncovered cells without silently dropping the U signal).""" - return _ELMHURST_ROOF_WINDOW_U_BY_GLAZING.get(w.glazing_type, w.u_value) + glazing-type phrase. Returns the inclined-position U-value pre- + curtain transform; the heat_transmission cascade then applies SAP + 10.2 §3.2 formula (2) (R=0.04 m²K/W curtain resistance). + + Two paths: + 1. Lodged glazing label in `_ELMHURST_ROOF_WINDOW_U_BY_GLAZING` + → return the RdSAP10 Table 24 "Roof window" column value + (already inclined-position per Table 24 derivation). + 2. Otherwise (lodged Manufacturer U on a non-Table-24 glazing + type) → apply SAP 10.2 Table 6e Note 2 inclination adjustment + to convert the vertical-tested U to inclined-position U. + """ + if w.glazing_type in _ELMHURST_ROOF_WINDOW_U_BY_GLAZING: + return _ELMHURST_ROOF_WINDOW_U_BY_GLAZING[w.glazing_type] + return w.u_value + _ELMHURST_ROOF_WINDOW_INCLINATION_ADJUSTMENT_W_PER_M2K def _map_elmhurst_roof_window(w: ElmhurstWindow) -> SapRoofWindow: