From f12e94a27a78703b2f101d3cde03e7be86cfd40d Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Sat, 30 May 2026 17:40:42 +0000 Subject: [PATCH] =?UTF-8?q?Slice=20S0380.108:=20Connected-to-heated-space?= =?UTF-8?q?=20RR=20gables=20deduct=20from=20A=5FRR=20(RdSAP=2010=20=C2=A73?= =?UTF-8?q?.9.2=20+=20Table=204=20row=204)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the largest single localised fabric residual on cert 000565 (roof +1.59 W/K over, area +4.70 m² over) by routing Connected-gable surfaces through a new `connected_wall` kind that deducts area from the residual A_RR per the spec but contributes 0 W/K per RdSAP 10 Table 4 row 4. RdSAP 10 §3.9.2 step (d) (PDF p.23) verbatim: "The areas of gable walls are deducted from the calculated total RR area, and the remaining area of RR, ARR_final is then calculated. This area is treated as roof structure. ARR_final = ARR_wall − (ΣARR_common_wall + ΣARR_gable + ΣARR_party + ΣARR_sheltered + ΣARR_connected)" RdSAP 10 Table 4 row 4 (PDF p.22): "ARR_connected — Adjacent to heated space — U-value = 0" The U=0 means no heat-loss contribution, but the area STILL appears in the deduction equation as ΣARR_connected. Pre-slice the mapper's `_map_elmhurst_rir_surface` returned None for Connected gables, dropping them entirely from `detailed_surfaces` so the cascade neither billed them nor deducted them. The residual A_RR was therefore over by their lodged area. Cert 000565 Ext1 §8.1 lodges (Simplified Type 2): Gable Wall 1 L=4.00 H=6.00 Connected U=0 Gable Wall 2 L=8.00 H=9.00 Exposed U=1.70 Common Wall 1 L=9.00 H=1.00 U=1.70 Common Wall 2 L=5.00 H=1.80 U=1.70 Gable Wall 1 area via §3.9.2 quadratic: A_gable_1 = 4 × (0.25 + 6) − (6 − 1)²/2 ← subtract triangle above Common Wall 1 − (6 − 1.8)²/2 ← subtract triangle above Common Wall 2 = 25.0 − 12.5 − 8.82 = 3.68 m² Pre-slice: A_RR shell = 12.5 × √(34 / 1.5) = 59.51 m² Σ wall areas = 11.25 + 10.25 + 16.08 = 37.58 m² Residual = 21.93 m² (worksheet: 18.25; over by +3.68) Roof W/K = 21.93 × 0.35 = 7.68 (worksheet: 6.39; over by +1.29) 3-layer fix: 1. Mapper `_map_elmhurst_rir_surface` (datatypes/epc/domain/mapper.py) now routes "Connected" gable_type to kind="connected_wall" with u_value=0 and area via the Simplified Type 2 quadratic correction. 2. Heat transmission `heat_transmission_from_cert` (domain/sap10_ calculator/worksheet/heat_transmission.py) adds a connected_wall branch that deducts area from rr_walls_in_a_rr_area but skips walls/party W/K contribution. 3. AAA test pins Ext1 Connected gable area at 3.68 m² and U=0. Movement at HEAD `b7fa5f74` → post-slice (cert 000565): Fabric (cascade vs ws): walls 602.53 → 602.53 (Δ -1.54 W/K; unchanged) roof 52.97 → 51.68 (Δ +1.59 → +0.30 W/K; closes 81%) TB 129.35 → 128.80 (Δ +0.70 → +0.15 W/K; closes 79%) total area 862.34 → 858.66 (Δ +4.70 → +1.02 m²; closes 78%) total W/K 937.40 → 935.54 (Δ +0.33 → -1.52 W/K; sign flips) End-result pins: **sap_score (int) 28 → 29 ✓ EXACT vs ws 29** (RECOVERED from S0380.107 transient rounding flip) sap_score_continuous 28.4959 → 28.5380 (Δ -0.0128 → +0.0293) ecf 5.3881 → 5.3838 (Δ +0.0015 → -0.0028) total_fuel_cost_gbp 4681.39 → 4677.64 (Δ +1.13 → -2.62) co2_kg_per_yr 6449.13 → 6444.27 (Δ +1.51 → -3.35) space_heating_kwh 59028.80 → 58974.84 (Δ +20.5 → -33.5) main_heating_fuel 34722.83 → 34691.09 (Δ +12.0 → -19.7) lighting_kwh 1382.67 → 1382.67 (unchanged) pumps_fans_kwh ✓ EXACT (unchanged) Continuous SAP and downstream pins SIGN-FLIPPED again (cascade was over post-.107, now under post-.108). Per user direction: transient drift acceptable while closing a true intermediate-value bug. The remaining net HTC -1.52 W/K is mostly walls (-1.54 W/K) — closing the Detailed-RR walls residual is the next leverage front. Cohort safety: none of the 6 cohort certs lodge a Connected gable (grep audit across all Summary fixtures). The new `connected_wall` branch only fires for the cert 000565 Ext1 BP. Test count: 606 pass + 8 expected 000565 fails → **608 pass + 7 expected 000565 fails** (sap_score back to exact + new Connected-gable test green). Pyright net-zero per touched file (57 baseline → 57 post-change). Co-Authored-By: Claude Opus 4.7 --- .../tests/test_summary_pdf_mapper_chain.py | 69 +++++++++++++++++++ datatypes/epc/domain/mapper.py | 29 ++++++-- .../worksheet/heat_transmission.py | 9 +++ 3 files changed, 102 insertions(+), 5 deletions(-) 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 8838999f..921c8d5c 100644 --- a/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py +++ b/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py @@ -2065,6 +2065,75 @@ def test_summary_000565_window_routing_uses_bp_roof_type_per_rdsap_10_section_3_ ) +def test_summary_000565_ext1_rir_connected_gable_deducts_from_a_rr_per_rdsap_10_section_3_9_2() -> None: + # Arrange — RdSAP 10 §3.9.2 (PDF p.23) step (d) verbatim: + # + # "The areas of gable walls are deducted from the calculated total + # RR area, and the remaining area of RR, ARR_final is then + # calculated. This area is treated as roof structure. + # ARR_final = ARR_wall − (ΣARR_common_wall + ΣARR_gable + + # ΣARR_party + ΣARR_sheltered + + # ΣARR_connected)" + # + # RdSAP 10 Table 4 row 4 (PDF p.22): "ARR_connected — Adjacent to + # heated space — U-value = 0". The U=0 means no heat-loss + # contribution, but the area STILL deducts from the residual A_RR + # (spec step (d) explicitly sums ARR_connected in the deduction). + # + # Cert 000565 Ext1 §8.1 lodges (Simplified Type 2 RR): + # + # Gable Wall 1 L=4.00 H=6.00 Connected U=0 + # Gable Wall 2 L=8.00 H=9.00 Exposed U=1.70 + # Common Wall 1 L=9.00 H=1.00 U=1.70 + # Common Wall 2 L=5.00 H=1.80 U=1.70 + # + # Gable area via §3.9.2 quadratic (subtract triangular slice above + # each common wall): + # + # A_gable_1 = 4 × (0.25 + 6) − (6 − 1)²/2 − (6 − 1.8)²/2 + # = 25.0 − 12.5 − 8.82 + # = 3.68 m² + # + # Pre-S0380.108 the mapper dropped Connected gables entirely + # (`_map_elmhurst_rir_surface` returned None). The cascade's + # residual A_RR was therefore over by +3.68 m²: + # + # A_RR shell = 12.5 × √(34 / 1.5) = 59.51 m² + # Σ wall areas (current) = 11.25 + 10.25 + 16.08 = 37.58 m² + # Residual (cascade) = 59.51 − 37.58 = 21.93 m² (over) + # Residual (worksheet) = 59.51 − 37.58 − 3.68 = 18.25 m² + # + # Worksheet (30) row "Roof room Ext1 remaining area: 18.25" at U=0.35 + # → 6.3875 W/K. Cascade pre-slice 21.93 × 0.35 → 7.6755 W/K + # (over by +1.29 W/K on roof — the largest single localised + # residual on cert 000565 per HANDOVER_POST_S0380_103.md). + pages = _summary_pdf_to_textract_style_pages(_SUMMARY_000565_PDF) + site_notes = ElmhurstSiteNotesExtractor(pages).extract() + + # Act + epc = EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes) + + # Assert — Ext1 RIR detailed_surfaces holds the Connected gable + # with the quadratic-corrected area, so the cascade deducts it + # from A_RR per step (d). + ext1_rir = epc.sap_building_parts[1].sap_room_in_roof + assert ext1_rir is not None + assert ext1_rir.detailed_surfaces is not None + connected_gables = [ + s for s in ext1_rir.detailed_surfaces + if s.kind == "connected_wall" + ] + assert len(connected_gables) == 1, ( + f"expected 1 Connected gable; got {len(connected_gables)} " + f"(detailed_surfaces kinds: " + f"{[s.kind for s in ext1_rir.detailed_surfaces]})" + ) + # 4 × (0.25 + 6) − (6 − 1)²/2 − (6 − 1.8)²/2 = 3.68 + assert abs(connected_gables[0].area_m2 - 3.68) <= 1e-4 + # U-value = 0 per Table 4 row 4 (no heat-loss contribution) + assert connected_gables[0].u_value == 0.0 + + def test_summary_000565_main_1_ashp_sap_code_224_routes_to_main_heating_category_4_per_sap_table_4a() -> None: # Arrange — SAP 10.2 Table 4a (PDF p.165) "Main heating systems": # the category column lists "Heat pumps" as category 4. Codes in diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index 0c3df483..e8e2d4b6 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -3349,12 +3349,31 @@ def _map_elmhurst_rir_surface( """ if surface.length_m <= 0 or surface.height_m <= 0: return None - # RdSAP 10 §3.10 Table 4 row 4 — "Connected to heated space" gables - # are internal partitions, not heat-loss surfaces. Per Summary PDF - # schema the column reads "Connected" (or the verbose "Connected - # to heated space"); drop either form. + # RdSAP 10 §3.9.2 step (d) (PDF p.23) — Connected-to-heated-space + # gables contribute U=0 (Table 4 row 4, PDF p.22) but their area + # STILL deducts from the residual A_RR per the explicit + # ΣARR_connected term in the spec equation. Route to a discrete + # "connected_wall" kind so heat_transmission can deduct the area + # without adding to walls or party W/K. Area follows the same + # Simplified Type 2 quadratic as exposed gables. if surface.gable_type in ("Connected", "Connected to heated space"): - return None + length_m, height_m = surface.length_m, surface.height_m + if is_simplified and common_wall_heights: + correction = sum( + ((height_m - h) ** 2) / 2.0 + for h in common_wall_heights + if height_m > h + ) + area_m2 = _round_half_up_2dp( + 1.0, max(0.0, length_m * (0.25 + height_m) - correction) + ) + else: + area_m2 = _round_half_up_2dp(length_m, height_m) + return SapRoomInRoofSurface( + kind="connected_wall", + area_m2=area_m2, + u_value=0.0, + ) if surface.name.startswith("Common Wall"): # RdSAP 10 §3.9.2 Simplified Type 2 — common walls billing into # the RR carry the storey-below main-wall U via the lodged diff --git a/domain/sap10_calculator/worksheet/heat_transmission.py b/domain/sap10_calculator/worksheet/heat_transmission.py index dc8670d8..159f7739 100644 --- a/domain/sap10_calculator/worksheet/heat_transmission.py +++ b/domain/sap10_calculator/worksheet/heat_transmission.py @@ -914,6 +914,15 @@ def heat_transmission_from_cert( rr_detailed_area += area walls += u_common * area rr_walls_in_a_rr_area += area + elif kind == "connected_wall": + # RdSAP 10 Table 4 row 4 (PDF p.22) — "Adjacent to + # heated space" gables have U=0 (no heat-loss + # contribution) but §3.9.2 step (d) explicitly + # deducts ΣA_RR_connected from the residual A_RR. + # Mapper precomputes the area via the Simplified + # Type 2 quadratic. Skip walls/party W/K; count the + # area in the A_RR deduction only. + rr_walls_in_a_rr_area += area # RdSAP 10 §3.10.1 residual area = simplified A_RR shell # minus the wall surfaces just enumerated. Uses the same # §3.9.1 `12.5 × √(A_RR_floor / 1.5)` formula as the