mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
Collapse full-SAP window openings onto the engine's sap_windows model 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0eaf87b106
commit
70460935b8
3 changed files with 125 additions and 3 deletions
|
|
@ -46,7 +46,13 @@ from datatypes.epc.schema.rdsap_schema_17_1 import (
|
|||
from datatypes.epc.schema.sap_schema_17_1 import (
|
||||
SapSchema17_1,
|
||||
EnergyElement as EnergyElement_SAP_17_1,
|
||||
SapOpeningType as SapOpeningType_SAP_17_1,
|
||||
)
|
||||
|
||||
# full-SAP opening-type codes: 1/2/3 = door, 4 = window, 5 = roof window.
|
||||
_SAP_OPENING_TYPE_WINDOW: Final[int] = 4
|
||||
# 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 (
|
||||
RdSapSchema18_0,
|
||||
EnergyElement as EnergyElement_18_0,
|
||||
|
|
@ -666,7 +672,8 @@ class EpcPropertyDataMapper:
|
|||
walls=EpcPropertyDataMapper._map_energy_elements(schema.walls),
|
||||
floors=EpcPropertyDataMapper._map_energy_elements(schema.floors),
|
||||
main_heating=[],
|
||||
sap_windows=[],
|
||||
# D2: vertical-window openings (opening-type 4) → sap_windows.
|
||||
sap_windows=EpcPropertyDataMapper._sap_17_1_windows(schema),
|
||||
sap_building_parts=[],
|
||||
sap_heating=SapHeating(
|
||||
instantaneous_wwhrs=InstantaneousWwhrs(),
|
||||
|
|
@ -685,6 +692,49 @@ class EpcPropertyDataMapper:
|
|||
),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _sap_17_1_windows(schema: SapSchema17_1) -> List[SapWindow]:
|
||||
"""D2: collapse vertical-window openings (opening-type 4) onto the
|
||||
engine's `SapWindow` model. Openings carry measured width/height +
|
||||
orientation; the joined opening-type carries the measured U-value,
|
||||
frame factor and (optionally) solar transmittance. Doors (1/2/3) and
|
||||
roof windows (5) are handled separately; unknown codes fail loud."""
|
||||
types: Dict[Union[str, int], SapOpeningType_SAP_17_1] = {
|
||||
ot.name: ot for ot in schema.sap_opening_types
|
||||
}
|
||||
windows: List[SapWindow] = []
|
||||
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 != _SAP_OPENING_TYPE_WINDOW:
|
||||
continue
|
||||
windows.append(
|
||||
SapWindow(
|
||||
frame_material=None,
|
||||
glazing_gap=0,
|
||||
orientation=op.orientation if op.orientation is not None else 0,
|
||||
window_type=ot.frame_type if ot.frame_type is not None else 0,
|
||||
glazing_type=ot.glazing_type,
|
||||
window_width=float(op.width),
|
||||
window_height=float(op.height),
|
||||
draught_proofed=False,
|
||||
window_location=op.location or "",
|
||||
window_wall_type=0,
|
||||
permanent_shutters_present=False,
|
||||
frame_factor=ot.frame_factor,
|
||||
window_transmission_details=WindowTransmissionDetails(
|
||||
u_value=ot.u_value,
|
||||
data_source=str(ot.data_source),
|
||||
solar_transmittance=(
|
||||
ot.solar_transmittance
|
||||
if ot.solar_transmittance is not None
|
||||
else _SAP_DEFAULT_SOLAR_TRANSMITTANCE
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
return windows
|
||||
|
||||
@staticmethod
|
||||
def from_rdsap_schema_17_1(schema: RdSapSchema17_1) -> EpcPropertyData:
|
||||
es = schema.sap_energy_source
|
||||
|
|
|
|||
|
|
@ -106,3 +106,36 @@ class TestFromSapSchema17_1FabricDescriptions:
|
|||
schema = from_dict(SapSchema17_1, load(fixture))
|
||||
result = EpcPropertyDataMapper.from_sap_schema_17_1(schema)
|
||||
assert result.walls and result.walls[0].description
|
||||
|
||||
|
||||
class TestFromSapSchema17_1Windows:
|
||||
"""Slice 4a (D2): vertical-window openings (opening-type 4) collapse onto
|
||||
sap_windows with measured per-window geometry and U-value."""
|
||||
|
||||
@pytest.fixture
|
||||
def sample(self) -> EpcPropertyData:
|
||||
schema = from_dict(SapSchema17_1, load("sap_17_1.json"))
|
||||
return EpcPropertyDataMapper.from_sap_schema_17_1(schema)
|
||||
|
||||
def test_maps_every_window_opening(self, sample: EpcPropertyData) -> None:
|
||||
# Sample lodges 5 window openings (+1 door, 0 roof windows).
|
||||
assert len(sample.sap_windows) == 5
|
||||
|
||||
def test_window_area_from_measured_width_height(
|
||||
self, sample: EpcPropertyData
|
||||
) -> None:
|
||||
# First window opening: width 1 × height 5.92 — engine computes area
|
||||
# as window_width × window_height, so both must pass through.
|
||||
w = sample.sap_windows[0]
|
||||
assert w.window_width == 1
|
||||
assert w.window_height == 5.92
|
||||
|
||||
def test_window_u_value_from_opening_type(self, sample: EpcPropertyData) -> None:
|
||||
# Joined opening-type "Windows (1)" lodges u_value 1.4 — must land in
|
||||
# window_transmission_details so the engine area-weights the real U.
|
||||
details = sample.sap_windows[0].window_transmission_details
|
||||
assert details is not None
|
||||
assert details.u_value == 1.4
|
||||
|
||||
def test_window_orientation_passes_through(self, sample: EpcPropertyData) -> None:
|
||||
assert sample.sap_windows[0].orientation == 8
|
||||
|
|
|
|||
|
|
@ -9,12 +9,49 @@ corpus.jsonl`), since the custom `from_dict` ignores unmodelled keys and
|
|||
raises only on a missing *required* field.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Union
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from .common import DescriptionV1
|
||||
|
||||
|
||||
@dataclass
|
||||
class SapOpeningType:
|
||||
"""A reusable opening definition (door / window / roof window) joined to
|
||||
each `SapOpening` by `name`. `type`: 1/2/3 = door, 4 = window, 5 = roof
|
||||
window/rooflight. `u_value` is the measured per-opening U."""
|
||||
|
||||
name: Union[str, int]
|
||||
type: int
|
||||
u_value: float
|
||||
glazing_type: int
|
||||
data_source: int
|
||||
frame_type: Optional[int] = None
|
||||
frame_factor: Optional[float] = None
|
||||
solar_transmittance: Optional[float] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class SapOpening:
|
||||
"""A placed opening within a building part. `type` is the join key into
|
||||
`sap_opening_types[].name`. `width`/`height` give the measured area."""
|
||||
|
||||
name: Union[str, int]
|
||||
type: Union[str, int]
|
||||
width: Union[int, float]
|
||||
height: Union[int, float]
|
||||
location: Optional[str] = None
|
||||
orientation: Optional[int] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class SapBuildingPart:
|
||||
"""A building part. Modelled incrementally — slice 4 needs `sap_openings`;
|
||||
slice 5 (perimeter) will add `sap_walls` / `sap_floor_dimensions`."""
|
||||
|
||||
sap_openings: List[SapOpening] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class EnergyElement:
|
||||
"""A fabric/system element with its lodged description. On full SAP the
|
||||
|
|
@ -46,3 +83,5 @@ class SapSchema17_1:
|
|||
roofs: List[EnergyElement]
|
||||
walls: List[EnergyElement]
|
||||
floors: List[EnergyElement]
|
||||
sap_opening_types: List[SapOpeningType]
|
||||
sap_building_parts: List[SapBuildingPart]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue