mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-27 14:35:07 +00:00
Merge pull request #1507 from Hestia-Homes/worktree-rdsap-glazing-cascade-fix
Cascade raw API glazing_type on lodged sap_windows (was passed through raw)
This commit is contained in:
commit
a76e49e133
2 changed files with 108 additions and 92 deletions
|
|
@ -728,22 +728,7 @@ class EpcPropertyDataMapper:
|
||||||
# rich certs keep their lodged per-window geometry (used directly:
|
# rich certs keep their lodged per-window geometry (used directly:
|
||||||
# window_width = area, height = 1.0).
|
# window_width = area, height = 1.0).
|
||||||
sap_windows=(
|
sap_windows=(
|
||||||
[
|
[_reduced_field_api_sap_window(w) for w in schema.sap_windows]
|
||||||
SapWindow(
|
|
||||||
frame_material=None,
|
|
||||||
glazing_gap=0,
|
|
||||||
orientation=w.orientation,
|
|
||||||
window_type=w.window_type,
|
|
||||||
glazing_type=w.glazing_type,
|
|
||||||
window_width=_measurement_value(w.window_area),
|
|
||||||
window_height=1.0,
|
|
||||||
draught_proofed=False,
|
|
||||||
window_location=w.window_location,
|
|
||||||
window_wall_type=0,
|
|
||||||
permanent_shutters_present=False,
|
|
||||||
)
|
|
||||||
for w in schema.sap_windows
|
|
||||||
]
|
|
||||||
if schema.sap_windows
|
if schema.sap_windows
|
||||||
else _synthesise_17_0_sap_windows(schema)
|
else _synthesise_17_0_sap_windows(schema)
|
||||||
),
|
),
|
||||||
|
|
@ -1301,23 +1286,7 @@ class EpcPropertyDataMapper:
|
||||||
cylinder_insulation_thickness_mm=schema.sap_heating.cylinder_insulation_thickness,
|
cylinder_insulation_thickness_mm=schema.sap_heating.cylinder_insulation_thickness,
|
||||||
),
|
),
|
||||||
sap_windows=(
|
sap_windows=(
|
||||||
[
|
[_reduced_field_api_sap_window(w) for w in schema.sap_windows]
|
||||||
SapWindow(
|
|
||||||
frame_material=None,
|
|
||||||
glazing_gap=0,
|
|
||||||
orientation=w.orientation,
|
|
||||||
window_type=w.window_type,
|
|
||||||
glazing_type=w.glazing_type,
|
|
||||||
# ADR-0028: 14 rich certs lodge real per-window area.
|
|
||||||
window_width=_measurement_value(w.window_area),
|
|
||||||
window_height=1.0,
|
|
||||||
draught_proofed=False,
|
|
||||||
window_location=w.window_location,
|
|
||||||
window_wall_type=0,
|
|
||||||
permanent_shutters_present=False,
|
|
||||||
)
|
|
||||||
for w in schema.sap_windows
|
|
||||||
]
|
|
||||||
if schema.sap_windows
|
if schema.sap_windows
|
||||||
else _synthesise_17_1_sap_windows(schema)
|
else _synthesise_17_1_sap_windows(schema)
|
||||||
),
|
),
|
||||||
|
|
@ -1508,26 +1477,7 @@ class EpcPropertyDataMapper:
|
||||||
# glazed_area band + TFA (added in a later slice). The 10 rich
|
# glazed_area band + TFA (added in a later slice). The 10 rich
|
||||||
# certs keep their lodged per-window geometry.
|
# certs keep their lodged per-window geometry.
|
||||||
sap_windows=(
|
sap_windows=(
|
||||||
[
|
[_reduced_field_api_sap_window(w) for w in schema.sap_windows]
|
||||||
SapWindow(
|
|
||||||
frame_material=None,
|
|
||||||
glazing_gap=0,
|
|
||||||
orientation=w.orientation,
|
|
||||||
window_type=w.window_type,
|
|
||||||
glazing_type=w.glazing_type,
|
|
||||||
# ADR-0028: the 10 rich certs lodge a real per-window
|
|
||||||
# window_area (Measurement) -- use it directly as geometry
|
|
||||||
# (width = area, height = 1.0) rather than the placeholder
|
|
||||||
# windowless 0x0.
|
|
||||||
window_width=_measurement_value(w.window_area),
|
|
||||||
window_height=1.0,
|
|
||||||
draught_proofed=False,
|
|
||||||
window_location=w.window_location,
|
|
||||||
window_wall_type=0,
|
|
||||||
permanent_shutters_present=False,
|
|
||||||
)
|
|
||||||
for w in schema.sap_windows
|
|
||||||
]
|
|
||||||
if schema.sap_windows
|
if schema.sap_windows
|
||||||
else _synthesise_18_0_sap_windows(schema)
|
else _synthesise_18_0_sap_windows(schema)
|
||||||
),
|
),
|
||||||
|
|
@ -1754,22 +1704,7 @@ class EpcPropertyDataMapper:
|
||||||
# rich certs keep their lodged per-window geometry (used directly:
|
# rich certs keep their lodged per-window geometry (used directly:
|
||||||
# window_width = area, height = 1.0).
|
# window_width = area, height = 1.0).
|
||||||
sap_windows=(
|
sap_windows=(
|
||||||
[
|
[_reduced_field_api_sap_window(w) for w in schema.sap_windows]
|
||||||
SapWindow(
|
|
||||||
frame_material=None,
|
|
||||||
glazing_gap=0,
|
|
||||||
orientation=w.orientation,
|
|
||||||
window_type=w.window_type,
|
|
||||||
glazing_type=w.glazing_type,
|
|
||||||
window_width=_measurement_value(w.window_area),
|
|
||||||
window_height=1.0,
|
|
||||||
draught_proofed=False,
|
|
||||||
window_location=w.window_location,
|
|
||||||
window_wall_type=0,
|
|
||||||
permanent_shutters_present=False,
|
|
||||||
)
|
|
||||||
for w in schema.sap_windows
|
|
||||||
]
|
|
||||||
if schema.sap_windows
|
if schema.sap_windows
|
||||||
else _synthesise_19_0_sap_windows(schema)
|
else _synthesise_19_0_sap_windows(schema)
|
||||||
),
|
),
|
||||||
|
|
@ -2020,28 +1955,7 @@ class EpcPropertyDataMapper:
|
||||||
# ADR-0027: 993/1000 omit sap_windows → synthesise from glazed_area
|
# ADR-0027: 993/1000 omit sap_windows → synthesise from glazed_area
|
||||||
# band + TFA. The 7 rich certs keep their lodged per-window geometry.
|
# band + TFA. The 7 rich certs keep their lodged per-window geometry.
|
||||||
sap_windows=(
|
sap_windows=(
|
||||||
[
|
[_reduced_field_api_sap_window(w) for w in schema.sap_windows]
|
||||||
SapWindow(
|
|
||||||
frame_material=None,
|
|
||||||
glazing_gap=0,
|
|
||||||
orientation=w.orientation,
|
|
||||||
window_type=w.window_type,
|
|
||||||
glazing_type=w.glazing_type,
|
|
||||||
# ADR-0027: the 7 rich certs lodge a real per-window
|
|
||||||
# window_area (Measurement) — use it directly as
|
|
||||||
# geometry (width = area, height = 1.0, matching the
|
|
||||||
# synthesis convention) rather than the placeholder
|
|
||||||
# windowless 0x0 that modelled these data-richest certs
|
|
||||||
# as having no glazing at all.
|
|
||||||
window_width=_measurement_value(w.window_area),
|
|
||||||
window_height=1.0,
|
|
||||||
draught_proofed=False,
|
|
||||||
window_location=w.window_location,
|
|
||||||
window_wall_type=0,
|
|
||||||
permanent_shutters_present=False,
|
|
||||||
)
|
|
||||||
for w in schema.sap_windows
|
|
||||||
]
|
|
||||||
if schema.sap_windows
|
if schema.sap_windows
|
||||||
else _synthesise_20_0_0_sap_windows(schema)
|
else _synthesise_20_0_0_sap_windows(schema)
|
||||||
),
|
),
|
||||||
|
|
@ -2289,7 +2203,11 @@ class EpcPropertyDataMapper:
|
||||||
orientation=w.orientation,
|
orientation=w.orientation,
|
||||||
window_type=w.window_type,
|
window_type=w.window_type,
|
||||||
frame_factor=w.frame_factor,
|
frame_factor=w.frame_factor,
|
||||||
glazing_type=w.glazing_type,
|
# See `_reduced_field_api_sap_window`: the raw API glazing
|
||||||
|
# code is the RdSAP-21 glazing enum, not the SAP 10.2
|
||||||
|
# cascade enum the calculator's U/g-value tables are keyed
|
||||||
|
# on — must be canonicalised, not passed through raw.
|
||||||
|
glazing_type=_api_cascade_glazing_type(w.glazing_type),
|
||||||
window_width=_measurement_value(w.window_width),
|
window_width=_measurement_value(w.window_width),
|
||||||
window_height=_measurement_value(w.window_height),
|
window_height=_measurement_value(w.window_height),
|
||||||
draught_proofed=w.draught_proofed == "true",
|
draught_proofed=w.draught_proofed == "true",
|
||||||
|
|
@ -5233,6 +5151,48 @@ def _api_sap_roof_window(w: Any) -> SapRoofWindow:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _reduced_field_api_sap_window(w: Any) -> SapWindow:
|
||||||
|
"""Build a `SapWindow` from one reduced-field API schema `sap_windows`
|
||||||
|
entry (RdSAP 17.0/17.1/18.0/19.0/20.0.0's minimal shape: orientation,
|
||||||
|
window_area, window_type, glazing_type, window_location — no glazing_gap,
|
||||||
|
frame_factor or window_transmission_details lodged).
|
||||||
|
|
||||||
|
Mirrors `_api_sap_window`'s glazing-type cascade: the raw API
|
||||||
|
`glazing_type` must be canonicalised via `_api_cascade_glazing_type`
|
||||||
|
before storage — these codes are the RdSAP-21 glazing enum (e.g. code 1
|
||||||
|
= "DG pre-2002"), NOT the SAP 10.2 Table 6b cascade enum the calculator's
|
||||||
|
`_G_LIGHT_BY_GLAZING_CODE`/U-value lookups are keyed on (cascade code 1 =
|
||||||
|
single glazed). Previously stored raw, this silently modelled any window
|
||||||
|
lodging API code 1 as single-glazed instead of double — cert
|
||||||
|
0141-2860-6891-9124-5625 (uprn 100021969385, property 753950, RdSAP-16.2)
|
||||||
|
over-stated windows_w_per_k 82.27 vs Elmhurst's 51.44 (a 31 W/K gap, the
|
||||||
|
dominant single contributor to a SAP 42 vs Elmhurst 44 mismatch)."""
|
||||||
|
transmission = _api_glazing_transmission(w.glazing_type, 0)
|
||||||
|
return SapWindow(
|
||||||
|
frame_material=None,
|
||||||
|
glazing_gap=0,
|
||||||
|
orientation=w.orientation,
|
||||||
|
window_type=w.window_type,
|
||||||
|
glazing_type=_api_cascade_glazing_type(w.glazing_type),
|
||||||
|
window_width=_measurement_value(w.window_area),
|
||||||
|
window_height=1.0,
|
||||||
|
draught_proofed=False,
|
||||||
|
window_location=w.window_location,
|
||||||
|
window_wall_type=0,
|
||||||
|
permanent_shutters_present=False,
|
||||||
|
frame_factor=transmission[2] if transmission is not None else None,
|
||||||
|
window_transmission_details=(
|
||||||
|
WindowTransmissionDetails(
|
||||||
|
u_value=transmission[0],
|
||||||
|
data_source="SAP10 lookup (glazing_type, glazing_gap)",
|
||||||
|
solar_transmittance=transmission[1],
|
||||||
|
)
|
||||||
|
if transmission is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _api_sap_window(w: Any) -> SapWindow:
|
def _api_sap_window(w: Any) -> SapWindow:
|
||||||
"""Build a `SapWindow` from one API schema sap_windows entry,
|
"""Build a `SapWindow` from one API schema sap_windows entry,
|
||||||
routing the glazing-type + glazing-gap pair through the spec
|
routing the glazing-type + glazing-gap pair through the spec
|
||||||
|
|
|
||||||
|
|
@ -385,6 +385,25 @@ class TestFromRdSapSchema21_0_0:
|
||||||
# photovoltaic_supply is None when the measured shape is present
|
# photovoltaic_supply is None when the measured shape is present
|
||||||
assert result.sap_energy_source.photovoltaic_supply is None
|
assert result.sap_energy_source.photovoltaic_supply is None
|
||||||
|
|
||||||
|
def test_lodged_window_glazing_type_is_cascaded_not_raw(self) -> None:
|
||||||
|
# Arrange — the fixture's own glazing_type (14) happens to be a
|
||||||
|
# coincident code (cascade(14) == 14), which would hide this bug.
|
||||||
|
# Override to code 1 ("DG pre-2002" in the RdSAP-21 glazing enum),
|
||||||
|
# which must canonicalise to cascade code 2 — passed through raw it
|
||||||
|
# would misread as cascade code 1 (single glazed). See the sibling
|
||||||
|
# `_reduced_field_api_sap_window` fix for the reduced-schema seams;
|
||||||
|
# this is the from_rdsap_schema_21_0_0 site of the same bug class.
|
||||||
|
data = load("21_0_0.json")
|
||||||
|
data["sap_windows"][0]["glazing_type"] = 1
|
||||||
|
|
||||||
|
schema = from_dict(RdSapSchema21_0_0, data)
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = EpcPropertyDataMapper.from_rdsap_schema_21_0_0(schema)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert result.sap_windows[0].glazing_type == 2
|
||||||
|
|
||||||
def test_photovoltaic_array_orientation_nd_nulls_only_that_field(self) -> None:
|
def test_photovoltaic_array_orientation_nd_nulls_only_that_field(self) -> None:
|
||||||
# Arrange — a 3-array dwelling where the middle array lodges the RdSAP
|
# Arrange — a 3-array dwelling where the middle array lodges the RdSAP
|
||||||
# 'ND' ("Not Defined") sentinel for orientation. Regression for the
|
# 'ND' ("Not Defined") sentinel for orientation. Regression for the
|
||||||
|
|
@ -2969,6 +2988,43 @@ def test_nd_glazing_without_single_signal_keeps_double_modal_default(
|
||||||
assert all(w.glazing_type == 2 for w in result.sap_windows)
|
assert all(w.glazing_type == 2 for w in result.sap_windows)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("schema_cls, mapper, fixture", _REDUCED_FIELD_SEAMS)
|
||||||
|
def test_lodged_sap_windows_glazing_type_is_cascaded_not_raw(
|
||||||
|
schema_cls: Any, mapper: Any, fixture: str
|
||||||
|
) -> None:
|
||||||
|
# Arrange — a real lodged `sap_windows[]` entry (the rich-cert path, not
|
||||||
|
# synthesis) whose raw API `glazing_type` code (1 = "DG pre-2002" in the
|
||||||
|
# RdSAP-21 glazing enum) differs from the SAP 10.2 cascade enum the
|
||||||
|
# calculator's U/g-value tables are keyed on (cascade code 1 = single
|
||||||
|
# glazed). Previously stored raw and unmodified — cert
|
||||||
|
# 0141-2860-6891-9124-5625 (uprn 100021969385, property 753950) lodges
|
||||||
|
# exactly this shape and was silently modelled as single glazed on its
|
||||||
|
# dominant window (91% of glazed area), overstating windows_w_per_k
|
||||||
|
# 82.27 vs Elmhurst's 51.44 W/K.
|
||||||
|
data = load(fixture)
|
||||||
|
data["sap_windows"] = [
|
||||||
|
{
|
||||||
|
"orientation": 5,
|
||||||
|
"window_area": {"value": 18.66, "quantity": "square metres"},
|
||||||
|
"window_type": 1,
|
||||||
|
"glazing_type": 1,
|
||||||
|
"window_location": 0,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = mapper(from_dict(schema_cls, data))
|
||||||
|
|
||||||
|
# Assert — glazing_type is the CASCADED value (2), not the raw API code
|
||||||
|
# (1) that would misread as single glazed; transmission details/U-value
|
||||||
|
# are populated from the SAP10 lookup rather than left None.
|
||||||
|
assert result.sap_windows
|
||||||
|
window = result.sap_windows[0]
|
||||||
|
assert window.glazing_type == 2
|
||||||
|
assert window.window_transmission_details is not None
|
||||||
|
assert window.window_transmission_details.u_value == pytest.approx(2.8)
|
||||||
|
|
||||||
|
|
||||||
class TestReducedFieldNdGlazingResolver:
|
class TestReducedFieldNdGlazingResolver:
|
||||||
"""Unit-level pins for the shared ND resolver (schema-independent)."""
|
"""Unit-level pins for the shared ND resolver (schema-independent)."""
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue