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
|
# D1: derive heat-loss perimeter + party-wall length from the
|
||||||
# measured wall areas (full SAP lodges neither directly).
|
# measured wall areas (full SAP lodges neither directly).
|
||||||
sap_building_parts=[
|
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)
|
for i, bp in enumerate(schema.sap_building_parts)
|
||||||
],
|
],
|
||||||
# D6: full-SAP heating — translate the differing field names onto
|
# D6: full-SAP heating — translate the differing field names onto
|
||||||
|
|
@ -2903,7 +2909,11 @@ def _with_recorded_performance(
|
||||||
|
|
||||||
|
|
||||||
def _sap_17_1_building_part(
|
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:
|
) -> SapBuildingPart:
|
||||||
"""D1: build one `SapBuildingPart`, deriving the heat-loss perimeter and
|
"""D1: build one `SapBuildingPart`, deriving the heat-loss perimeter and
|
||||||
party-wall length the engine needs from the measured wall areas full SAP
|
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
|
length, 5 internal → discarded; any other code fails loud. Exposed/party
|
||||||
areas divide by Σ storey-heights so the per-storey perimeter, summed as
|
areas divide by Σ storey-heights so the per-storey perimeter, summed as
|
||||||
Σ(perimeter × height) by the engine, reconstructs the measured area exactly.
|
Σ(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
|
exposed_area = 0.0
|
||||||
party_area = 0.0
|
party_area = 0.0
|
||||||
|
|
@ -2930,17 +2946,28 @@ def _sap_17_1_building_part(
|
||||||
perimeter_per_storey = exposed_area / total_height
|
perimeter_per_storey = exposed_area / total_height
|
||||||
party_length_per_storey = party_area / total_height
|
party_length_per_storey = party_area / total_height
|
||||||
|
|
||||||
floor_dimensions = [
|
if not bp.sap_floor_dimensions and is_single_part and is_flat:
|
||||||
SapFloorDimension(
|
floor_dimensions = [
|
||||||
room_height_m=fd.storey_height,
|
SapFloorDimension(
|
||||||
total_floor_area_m2=fd.total_floor_area,
|
room_height_m=1.0,
|
||||||
heat_loss_perimeter_m=perimeter_per_storey,
|
total_floor_area_m2=total_floor_area,
|
||||||
party_wall_length_m=party_length_per_storey,
|
heat_loss_perimeter_m=perimeter_per_storey,
|
||||||
floor=fd.storey,
|
party_wall_length_m=party_length_per_storey,
|
||||||
floor_construction=fd.floor_type,
|
floor=0,
|
||||||
)
|
)
|
||||||
for fd in bp.sap_floor_dimensions
|
]
|
||||||
]
|
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:
|
if index == 0:
|
||||||
identifier = BuildingPartIdentifier.MAIN
|
identifier = BuildingPartIdentifier.MAIN
|
||||||
|
|
|
||||||
|
|
@ -777,14 +777,14 @@ class TestFullSapSchema16xNoFloorDimensions:
|
||||||
assert epc.uprn == 10090783001
|
assert epc.uprn == 10090783001
|
||||||
assert epc.total_floor_area_m2 == 72.0
|
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
|
self, epc: EpcPropertyData
|
||||||
) -> None:
|
) -> None:
|
||||||
# The API lodges no sap_floor_dimensions for this cert family.
|
# The API lodges no sap_floor_dimensions for this cert family, but the
|
||||||
# The mapper faithfully produces an empty list — the data gap that
|
# mapper synthesises one from the cert-level total_floor_area so that
|
||||||
# causes roof_area() / gross_heat_loss_wall_area() to crash downstream.
|
# roof_area() and gross_heat_loss_wall_area() do not crash downstream.
|
||||||
main_part = epc.sap_building_parts[0]
|
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(
|
def test_total_floor_area_available_at_cert_level(
|
||||||
self, epc: EpcPropertyData
|
self, epc: EpcPropertyData
|
||||||
|
|
|
||||||
|
|
@ -171,6 +171,11 @@ class EnergyElement:
|
||||||
environmental_efficiency_rating: int
|
environmental_efficiency_rating: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SapFlatDetails:
|
||||||
|
level: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SapSchema17_1:
|
class SapSchema17_1:
|
||||||
uprn: int
|
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
|
# 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.
|
# sap_heating; the mapper falls back to sap_heating.has_hot_water_cylinder.
|
||||||
has_hot_water_cylinder: Optional[str] = None
|
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