Carry the surveyed room-in-roof block onto the SAP building part 🟩

_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) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-07-14 22:01:30 +00:00
parent 9599fd8239
commit 0ca5108584

View file

@ -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