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