mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
Mapper synthesises SapFloorDimension for SAP-16.0 flats with empty floor dims 🟩
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c1e754057b
commit
8a909dc15b
3 changed files with 52 additions and 18 deletions
|
|
@ -799,7 +799,13 @@ class EpcPropertyDataMapper:
|
|||
# D1: derive heat-loss perimeter + party-wall length from the
|
||||
# measured wall areas (full SAP lodges neither directly).
|
||||
sap_building_parts=[
|
||||
_sap_17_1_building_part(bp, i)
|
||||
_sap_17_1_building_part(
|
||||
bp,
|
||||
i,
|
||||
total_floor_area=float(schema.total_floor_area),
|
||||
is_single_part=len(schema.sap_building_parts) == 1,
|
||||
is_flat=schema.sap_flat_details is not None,
|
||||
)
|
||||
for i, bp in enumerate(schema.sap_building_parts)
|
||||
],
|
||||
# D6: full-SAP heating — translate the differing field names onto
|
||||
|
|
@ -2903,7 +2909,11 @@ def _with_recorded_performance(
|
|||
|
||||
|
||||
def _sap_17_1_building_part(
|
||||
bp: SapBuildingPart_SAP_17_1, index: int
|
||||
bp: SapBuildingPart_SAP_17_1,
|
||||
index: int,
|
||||
total_floor_area: float,
|
||||
is_single_part: bool,
|
||||
is_flat: bool,
|
||||
) -> SapBuildingPart:
|
||||
"""D1: build one `SapBuildingPart`, deriving the heat-loss perimeter and
|
||||
party-wall length the engine needs from the measured wall areas full SAP
|
||||
|
|
@ -2913,6 +2923,12 @@ def _sap_17_1_building_part(
|
|||
length, 5 internal → discarded; any other code fails loud. Exposed/party
|
||||
areas divide by Σ storey-heights so the per-storey perimeter, summed as
|
||||
Σ(perimeter × height) by the engine, reconstructs the measured area exactly.
|
||||
|
||||
When `bp.sap_floor_dimensions` is absent (SAP-16.0 flat certs) and the cert
|
||||
is confirmed single-part and flat-type, synthesises one SapFloorDimension
|
||||
from the cert-level `total_floor_area`. Only safe for single-storey
|
||||
single-part flats; multi-part or multi-storey cases are left empty so
|
||||
downstream failures stay diagnosable.
|
||||
"""
|
||||
exposed_area = 0.0
|
||||
party_area = 0.0
|
||||
|
|
@ -2930,17 +2946,28 @@ def _sap_17_1_building_part(
|
|||
perimeter_per_storey = exposed_area / total_height
|
||||
party_length_per_storey = party_area / total_height
|
||||
|
||||
floor_dimensions = [
|
||||
SapFloorDimension(
|
||||
room_height_m=fd.storey_height,
|
||||
total_floor_area_m2=fd.total_floor_area,
|
||||
heat_loss_perimeter_m=perimeter_per_storey,
|
||||
party_wall_length_m=party_length_per_storey,
|
||||
floor=fd.storey,
|
||||
floor_construction=fd.floor_type,
|
||||
)
|
||||
for fd in bp.sap_floor_dimensions
|
||||
]
|
||||
if not bp.sap_floor_dimensions and is_single_part and is_flat:
|
||||
floor_dimensions = [
|
||||
SapFloorDimension(
|
||||
room_height_m=1.0,
|
||||
total_floor_area_m2=total_floor_area,
|
||||
heat_loss_perimeter_m=perimeter_per_storey,
|
||||
party_wall_length_m=party_length_per_storey,
|
||||
floor=0,
|
||||
)
|
||||
]
|
||||
else:
|
||||
floor_dimensions = [
|
||||
SapFloorDimension(
|
||||
room_height_m=fd.storey_height,
|
||||
total_floor_area_m2=fd.total_floor_area,
|
||||
heat_loss_perimeter_m=perimeter_per_storey,
|
||||
party_wall_length_m=party_length_per_storey,
|
||||
floor=fd.storey,
|
||||
floor_construction=fd.floor_type,
|
||||
)
|
||||
for fd in bp.sap_floor_dimensions
|
||||
]
|
||||
|
||||
if index == 0:
|
||||
identifier = BuildingPartIdentifier.MAIN
|
||||
|
|
|
|||
|
|
@ -777,14 +777,14 @@ class TestFullSapSchema16xNoFloorDimensions:
|
|||
assert epc.uprn == 10090783001
|
||||
assert epc.total_floor_area_m2 == 72.0
|
||||
|
||||
def test_building_part_has_empty_floor_dimensions(
|
||||
def test_building_part_floor_dimensions_synthesised(
|
||||
self, epc: EpcPropertyData
|
||||
) -> None:
|
||||
# The API lodges no sap_floor_dimensions for this cert family.
|
||||
# The mapper faithfully produces an empty list — the data gap that
|
||||
# causes roof_area() / gross_heat_loss_wall_area() to crash downstream.
|
||||
# The API lodges no sap_floor_dimensions for this cert family, but the
|
||||
# mapper synthesises one from the cert-level total_floor_area so that
|
||||
# roof_area() and gross_heat_loss_wall_area() do not crash downstream.
|
||||
main_part = epc.sap_building_parts[0]
|
||||
assert main_part.sap_floor_dimensions == []
|
||||
assert len(main_part.sap_floor_dimensions) == 1
|
||||
|
||||
def test_total_floor_area_available_at_cert_level(
|
||||
self, epc: EpcPropertyData
|
||||
|
|
|
|||
|
|
@ -171,6 +171,11 @@ class EnergyElement:
|
|||
environmental_efficiency_rating: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class SapFlatDetails:
|
||||
level: Optional[int] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class SapSchema17_1:
|
||||
uprn: int
|
||||
|
|
@ -199,3 +204,5 @@ class SapSchema17_1:
|
|||
# Some 17.0 full-SAP certs omit the top-level flag and lodge it only under
|
||||
# sap_heating; the mapper falls back to sap_heating.has_hot_water_cylinder.
|
||||
has_hot_water_cylinder: Optional[str] = None
|
||||
# Present for flat-type dwellings; absence means not a flat.
|
||||
sap_flat_details: Optional[SapFlatDetails] = None
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue