mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
Collapse full-SAP door openings onto door count and area-weighted U-value 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
70460935b8
commit
36929accf7
2 changed files with 62 additions and 7 deletions
|
|
@ -2,7 +2,7 @@ import re
|
|||
from dataclasses import replace
|
||||
from datetime import date
|
||||
from decimal import ROUND_HALF_UP, Decimal
|
||||
from typing import Any, Dict, Final, List, Optional, Sequence, Union, cast
|
||||
from typing import Any, Dict, Final, List, Optional, Sequence, Tuple, Union, cast
|
||||
from datatypes.epc.schema.helpers import from_dict
|
||||
|
||||
from datatypes.epc.domain.epc_property_data import (
|
||||
|
|
@ -51,6 +51,7 @@ from datatypes.epc.schema.sap_schema_17_1 import (
|
|||
|
||||
# full-SAP opening-type codes: 1/2/3 = door, 4 = window, 5 = roof window.
|
||||
_SAP_OPENING_TYPE_WINDOW: Final[int] = 4
|
||||
_SAP_OPENING_TYPE_DOORS: Final[frozenset[int]] = frozenset({1, 2, 3})
|
||||
# SAP-typical glazing solar transmittance when an opening-type omits it.
|
||||
_SAP_DEFAULT_SOLAR_TRANSMITTANCE: Final[float] = 0.63
|
||||
from datatypes.epc.schema.rdsap_schema_18_0 import (
|
||||
|
|
@ -632,10 +633,10 @@ class EpcPropertyDataMapper:
|
|||
@staticmethod
|
||||
def from_sap_schema_17_1(schema: SapSchema17_1) -> EpcPropertyData:
|
||||
# Built incrementally via TDD — see scripts/hyde/mapping_decisions.md.
|
||||
# Tracer slice: identity + scalars are mapped; the load-bearing
|
||||
# collections (walls/floors/roofs, sap_windows, sap_building_parts,
|
||||
# door/room counts) are left empty here and filled by later slices
|
||||
# (D1 perimeter, D2 openings, D3 living-area, D4 fabric-U).
|
||||
# Identity + scalars + fabric descriptions (D4) + openings (D2) are
|
||||
# mapped; sap_building_parts (D1 perimeter) and habitable_rooms_count
|
||||
# (D3 living-area) are still filled by later slices.
|
||||
door_count, insulated_door_u = _sap_door_aggregates(schema)
|
||||
return EpcPropertyData(
|
||||
uprn=schema.uprn,
|
||||
dwelling_type=(
|
||||
|
|
@ -653,13 +654,16 @@ class EpcPropertyDataMapper:
|
|||
has_hot_water_cylinder=schema.has_hot_water_cylinder == "true",
|
||||
has_fixed_air_conditioning=schema.has_fixed_air_conditioning == "true",
|
||||
solar_water_heating=False,
|
||||
door_count=0,
|
||||
wet_rooms_count=0,
|
||||
extensions_count=0,
|
||||
heated_rooms_count=0,
|
||||
open_chimneys_count=0,
|
||||
habitable_rooms_count=0,
|
||||
insulated_door_count=0,
|
||||
# D2: door openings (1/2/3) → counts + area-weighted U. New-build
|
||||
# doors are treated insulated, so insulated_door_count == door_count.
|
||||
door_count=door_count,
|
||||
insulated_door_count=door_count,
|
||||
insulated_door_u_value=insulated_door_u,
|
||||
cfl_fixed_lighting_bulbs_count=0,
|
||||
led_fixed_lighting_bulbs_count=0,
|
||||
incandescent_fixed_lighting_bulbs_count=0,
|
||||
|
|
@ -2384,6 +2388,31 @@ class EpcPropertyDataMapper:
|
|||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _sap_door_aggregates(schema: SapSchema17_1) -> Tuple[int, Optional[float]]:
|
||||
"""D2: collapse door openings (opening-type 1/2/3) onto the engine's door
|
||||
model — a count and a single U-value. When a cert lodges doors with
|
||||
differing U (≈5% of full-SAP certs), the U is area-weighted so the larger
|
||||
door dominates the door heat loss. Returns (door_count, u_value); U is None
|
||||
when no doors are lodged."""
|
||||
types: Dict[Union[str, int], SapOpeningType_SAP_17_1] = {
|
||||
ot.name: ot for ot in schema.sap_opening_types
|
||||
}
|
||||
count = 0
|
||||
weighted_u_area = 0.0
|
||||
total_area = 0.0
|
||||
for bp in schema.sap_building_parts:
|
||||
for op in bp.sap_openings:
|
||||
ot = types.get(op.type)
|
||||
if ot is None or ot.type not in _SAP_OPENING_TYPE_DOORS:
|
||||
continue
|
||||
count += 1
|
||||
area = float(op.width) * float(op.height)
|
||||
weighted_u_area += ot.u_value * area
|
||||
total_area += area
|
||||
u_value = weighted_u_area / total_area if total_area > 0 else None
|
||||
return count, u_value
|
||||
|
||||
|
||||
def _clear_basement_flag_when_system_built(
|
||||
epc: EpcPropertyData,
|
||||
) -> EpcPropertyData:
|
||||
|
|
|
|||
|
|
@ -139,3 +139,29 @@ class TestFromSapSchema17_1Windows:
|
|||
|
||||
def test_window_orientation_passes_through(self, sample: EpcPropertyData) -> None:
|
||||
assert sample.sap_windows[0].orientation == 8
|
||||
|
||||
|
||||
class TestFromSapSchema17_1Doors:
|
||||
"""Slice 4b (D2): door openings (opening-type 1/2/3) collapse onto the
|
||||
engine's door_count + insulated_door_u_value. New-build doors are treated
|
||||
insulated, so insulated_door_count == door_count."""
|
||||
|
||||
def _map(self, fixture: str) -> EpcPropertyData:
|
||||
schema = from_dict(SapSchema17_1, load(fixture))
|
||||
return EpcPropertyDataMapper.from_sap_schema_17_1(schema)
|
||||
|
||||
def test_single_door_count_and_u(self) -> None:
|
||||
result = self._map("sap_17_1.json")
|
||||
assert result.door_count == 1
|
||||
assert result.insulated_door_u_value == 1.4
|
||||
|
||||
def test_doors_treated_insulated(self) -> None:
|
||||
result = self._map("sap_17_1.json")
|
||||
assert result.insulated_door_count == 1
|
||||
|
||||
def test_multi_door_u_is_area_weighted(self) -> None:
|
||||
# Flat lodges 2 doors with distinct U (1.4 over 1.89 m², 1.8 over 2.12 m²)
|
||||
# → area-weighted mean so the dominant door drives the door heat loss.
|
||||
result = self._map("sap_17_1_flat.json")
|
||||
assert result.door_count == 2
|
||||
assert result.insulated_door_u_value == pytest.approx(1.6114713, abs=1e-6)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue