Model/domain/building_geometry.py
Khalim Conn-Kowlessar 4c10405071 feat(modelling): floor Recommendation Generator + ground-floor-area geometry
recommend_floor_insulation(epc, products) detects an uninsulated ground floor
(SapBuildingPart.floor_insulation_thickness blank/zero) and its construction
from floor_construction_type — 'Suspended timber' -> suspended_floor_insulation,
'Solid' -> solid_floor_insulation — emitting the matching single Option (a
floor is one construction, like a cavity wall) with the overlay
(floor_insulation_thickness = 100 mm) and a priced Cost (ground-floor area x
the Product's fully-loaded unit cost + contingency).

- building_geometry.ground_floor_area(epc, identifier): the lowest floor's
  (floor == 0) area. Pinned 14.85 m^2 on 000490 MAIN.
- BuildingPartOverlay gains floor_insulation_thickness (generic Applicator
  writes it unchanged). suspended (0.20) / solid (0.26) floor contingencies.

Progress on #1159 (generator + geometry); end-to-end + Elmhurst pin pending
the orchestrator (#1157) and parser. Four behaviour tests (suspended / solid
/ none / cost) + geometry pin. pyright strict clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 09:12:29 +00:00

62 lines
2.2 KiB
Python

"""Building geometry derived purely from an EpcPropertyData.
Reusable outside the SAP calculator (e.g. for Modelling cost quantities).
Today this re-derives the heat-loss wall area; the calculator computes the
same quantity inline (`heat_transmission._part_geometry`). A later, calculator-
branch-coordinated refactor should DRY the two onto this module so there is a
single source of truth. See the project memory on calculator geometry.
"""
from datatypes.epc.domain.epc_property_data import (
BuildingPartIdentifier,
EpcPropertyData,
)
def gross_heat_loss_wall_area(
epc: EpcPropertyData, identifier: BuildingPartIdentifier
) -> float:
"""Gross external wall area of one building part, in m^2: the sum over its
storeys of heat-loss perimeter x room height. This is the heat-loss area
(party walls are excluded — they are not on the heat-loss perimeter); it is
not netted of window/door openings.
"""
part = next(
candidate
for candidate in epc.sap_building_parts
if candidate.identifier is identifier
)
area = sum(
fd.heat_loss_perimeter_m * fd.room_height_m
for fd in part.sap_floor_dimensions
)
return round(area, 2)
def roof_area(epc: EpcPropertyData, identifier: BuildingPartIdentifier) -> float:
"""Roof area of one building part, in m^2. Per RdSAP10 §3.8 the roof area is
the greatest of the part's per-storey floor areas (not the top-floor area,
which can be smaller)."""
part = next(
candidate
for candidate in epc.sap_building_parts
if candidate.identifier is identifier
)
return round(
max(fd.total_floor_area_m2 for fd in part.sap_floor_dimensions), 2
)
def ground_floor_area(
epc: EpcPropertyData, identifier: BuildingPartIdentifier
) -> float:
"""Ground-floor area of one building part, in m^2 — the area of its lowest
floor (``floor == 0``), the surface a ground-floor insulation measure
treats."""
part = next(
candidate
for candidate in epc.sap_building_parts
if candidate.identifier is identifier
)
ground = next(fd for fd in part.sap_floor_dimensions if fd.floor == 0)
return round(ground.total_floor_area_m2, 2)