From 0ca51085841debe3c7a6fa09056e41e7a38688e4 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 14 Jul 2026 22:01:30 +0000 Subject: [PATCH] =?UTF-8?q?Carry=20the=20surveyed=20room-in-roof=20block?= =?UTF-8?q?=20onto=20the=20SAP=20building=20part=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _pashub_room_in_roof mirrors _api_build_room_in_roof: every surveyed RIR surface (gable typed Exposed/Party per Table 4, slopes, common walls, flat ceiling) becomes a detailed_surfaces entry of length x height, so the cascade bills exact RIR surfaces instead of treating the shell as well-insulated loft (issue #1590 bug 5). Co-Authored-By: Claude Opus 4.8 (1M context) --- datatypes/epc/domain/mapper.py | 60 ++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index 957573249..7cb9acef5 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -154,6 +154,7 @@ from datatypes.epc.surveys.pashub_rdsap_site_notes import ( HeatingAndHotWater, PasHubRdSapSiteNotes, Renewables as PasHubRenewables, + RoomInRoofSurfaceDetail, RoofSpaceDetail, Ventilation, WaterUse, @@ -5979,6 +5980,64 @@ def _map_roof( return roof.insulation_at or None, thickness +# PasHub gable "type" label → the Table 4 surface kind the cascade's +# per-surface RR branch dispatches on (Exposed = as-common-wall U, +# Party = 0.25 W/m²K). +_PASHUB_GABLE_TYPE_TO_KIND: Dict[str, str] = { + "Exposed": "gable_wall_external", + "Party": "gable_wall", +} + + +def _pashub_room_in_roof( + roof: Union[RoofSpaceDetail, ExtensionRoofSpace], +) -> Optional[SapRoomInRoof]: + """Build `SapRoomInRoof` from the surveyed §Roof-Space room-in-roof block + (RdSAP §3.10 detailed measurement), mirroring `_api_build_room_in_roof`: + every surveyed surface (gable / slope / common wall / flat ceiling) + becomes a `detailed_surfaces` entry of length × height, so the cascade + bills exact RIR surfaces instead of treating the whole shell as + well-insulated loft (issue #1590 bug 5). Returns None when no RIR is + lodged. A gable with an unmapped type label strict-raises.""" + rir = roof.room_in_roof + if not roof.rooms_in_roof or rir is None: + return None + surfaces: List[SapRoomInRoofSurface] = [] + + def _add( + details: Optional[List[RoomInRoofSurfaceDetail]], kind: Optional[str] + ) -> None: + for d in details or []: + if d.length_m is None or d.height_m is None: + continue + surface_kind = kind + if surface_kind is None: # gable — kind from the surveyed type + if d.gable_type not in _PASHUB_GABLE_TYPE_TO_KIND: + raise UnmappedPasHubLabel( + "room-in-roof gable type", str(d.gable_type) + ) + surface_kind = _PASHUB_GABLE_TYPE_TO_KIND[d.gable_type] + surfaces.append( + SapRoomInRoofSurface( + kind=surface_kind, + area_m2=d.length_m * d.height_m, + u_value=d.u_value, + ) + ) + + _add(rir.gables, None) + _add(rir.slopes, "slope") + _add(rir.common_walls, "common_wall") + _add(rir.flat_ceilings, "flat_ceiling") + if rir.floor_area_m2 is None: + return None + return SapRoomInRoof( + floor_area=rir.floor_area_m2, + construction_age_band=_extract_age_band(rir.age_range or ""), + detailed_surfaces=surfaces or None, + ) + + def _map_main_building_part( construction: BuildingConstruction, measurements: BuildingMeasurements, @@ -5989,6 +6048,7 @@ def _map_main_building_part( roof_location, roof_thickness = _map_roof(roof) return SapBuildingPart( identifier=BuildingPartIdentifier.MAIN, + sap_room_in_roof=_pashub_room_in_roof(roof), construction_age_band=_extract_age_band(main.age_range), # PasHub surveys never lodge a basement wall; pin the flag so a # "System Build" wall (code 6, which doubles as the gov-API basement