mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
fix(mapper): read dropped detailed room-in-roof slope + stud-wall surfaces
The gov-EPC API lodges a Detailed RR (RdSAP 10 §3.9, Figure 4) with up to two sloping ceilings (`slope_*`) and two vertical stud/knee walls (`stud_wall_*`) in addition to the gable + flat-ceiling surfaces. Those slope/stud fields were undeclared on the 21.0.x schema, so `from_dict` silently dropped them and `_api_rir_detailed_surfaces` built ONLY the gable + flat-ceiling surfaces. The (large) sloping roof and the knee walls contributed ZERO heat loss → undercounted RR fabric loss → a systematic over-rate. Fix: declare `slope_*`/`stud_wall_*` on `RoomInRoofDetails` (rdsap_schema_21_0_0 + _21_0_1) and build `kind="slope"` / `kind="stud_wall"` surfaces in the mapper. The cascade's Detailed-RR branch already routes both to the roof aggregate via `u_rr_slope` (Table 17 col 1) and `u_rr_stud_wall` (Table 17 col 3) — RdSAP 10 §5.11.3, p.43-44 — so no calculator change is needed (Summary path worksheet-validated by the 000565 detailed-RR fixtures). insulation_type is left None to defer to the Table 17 col-(a) mineral-wool default, mirroring the existing flat_ceiling branch. 15 /tmp certs carry `slope_height_1`: cohort mean|err| 4.26 -> 2.05, signed +4.09 -> centred (14/15 were over-rating; e.g. 0390-2538 +5.95 -> +3.56). Gauges: corpus within-0.5 67.3% -> 67.5% (MAE 1.020 -> 0.987); /tmp 71.4% -> 71.6% (MAE 0.882 -> 0.846). Harness 47/47 0 raised; regression = the 3 pre-existing fails; pyright net-zero (65=65). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
b55b969b84
commit
363f14fbb2
4 changed files with 142 additions and 0 deletions
|
|
@ -3960,6 +3960,39 @@ def _api_rir_detailed_surfaces(
|
|||
if length is not None and height is not None and length > 0 and height > 0:
|
||||
area = _round_half_up_2dp(float(length), float(height))
|
||||
surfaces.append(SapRoomInRoofSurface(kind=gable_kind, area_m2=area))
|
||||
# Sloping ceiling + stud walls — up to two of each (RdSAP §3.9 Figure 4).
|
||||
# Both route to the roof aggregate (line (30)) via the cascade's
|
||||
# Detailed-RR branch (`u_rr_slope` / `u_rr_stud_wall`, Table 17 cols 1/3).
|
||||
# insulation_type is left None so the cascade defers to the Table 17
|
||||
# column (a) mineral-wool default, mirroring the flat_ceiling branch.
|
||||
slope_specs = (
|
||||
(details.slope_length_1, details.slope_height_1,
|
||||
details.slope_insulation_thickness_1),
|
||||
(details.slope_length_2, details.slope_height_2,
|
||||
details.slope_insulation_thickness_2),
|
||||
)
|
||||
stud_specs = (
|
||||
(details.stud_wall_length_1, details.stud_wall_height_1,
|
||||
details.stud_wall_insulation_thickness_1),
|
||||
(details.stud_wall_length_2, details.stud_wall_height_2,
|
||||
details.stud_wall_insulation_thickness_2),
|
||||
)
|
||||
for kind, specs in (("slope", slope_specs), ("stud_wall", stud_specs)):
|
||||
for length, height, thickness_str in specs:
|
||||
if (
|
||||
length is not None and height is not None
|
||||
and length > 0 and height > 0
|
||||
):
|
||||
area = _round_half_up_2dp(float(length), float(height))
|
||||
surfaces.append(
|
||||
SapRoomInRoofSurface(
|
||||
kind=kind,
|
||||
area_m2=area,
|
||||
insulation_thickness_mm=(
|
||||
_parse_rir_insulation_thickness_mm(thickness_str)
|
||||
),
|
||||
)
|
||||
)
|
||||
if (
|
||||
details.flat_ceiling_length_1 is not None
|
||||
and details.flat_ceiling_height_1 is not None
|
||||
|
|
|
|||
|
|
@ -2152,3 +2152,72 @@ class TestRdSap17_1ReducedFieldSynthesis:
|
|||
|
||||
assert result.sap_heating.number_baths == expected_baths
|
||||
assert result.sap_heating.mixer_shower_count == expected_mixers
|
||||
|
||||
|
||||
class TestRoomInRoofDetailedSlopeAndStudWall:
|
||||
"""RdSAP 10 §3.9 Detailed RR — the gov API lodges the sloping ceiling
|
||||
and stud-wall surfaces under `room_in_roof_details.slope_*` /
|
||||
`stud_wall_*`. These were undeclared on the schema, so `from_dict`
|
||||
dropped them and the API mapper built ONLY the gable + flat-ceiling
|
||||
surfaces — omitting the (large) sloping roof and vertical knee walls →
|
||||
undercounted RR heat loss → a systematic ~+4 SAP over-rate across the
|
||||
15 detailed-RR corpus certs carrying `slope_height_1`."""
|
||||
|
||||
def test_slope_surface_survives_from_dict_round_trip(self) -> None:
|
||||
# Arrange — a 21.0.1 detailed-RR block (cert 0390-2538 shape).
|
||||
from datatypes.epc.schema.rdsap_schema_21_0_1 import RoomInRoofDetails
|
||||
|
||||
raw = {
|
||||
"slope_length_1": 7.0,
|
||||
"slope_height_1": 1.4,
|
||||
"slope_insulation_thickness_1": "100mm",
|
||||
"stud_wall_length_1": 7.0,
|
||||
"stud_wall_height_1": 1.03,
|
||||
"stud_wall_insulation_thickness_1": "75mm",
|
||||
}
|
||||
|
||||
# Act
|
||||
details = from_dict(RoomInRoofDetails, raw)
|
||||
|
||||
# Assert — the fields are no longer silently dropped.
|
||||
assert details.slope_height_1 == 1.4
|
||||
assert details.slope_insulation_thickness_1 == "100mm"
|
||||
assert details.stud_wall_height_1 == 1.03
|
||||
|
||||
def test_from_api_response_builds_slope_and_stud_wall_surfaces(self) -> None:
|
||||
# Arrange — drive the PUBLIC API path: take the 21.0.1 fixture's RR
|
||||
# building part and replace its Simplified Type-1 block with a
|
||||
# Detailed RR carrying two sloping ceilings (7 × 1.4) + two stud
|
||||
# walls (7 × 1.03). cert 0390-2538 went +5.95 -> +3.56 SAP once these
|
||||
# surfaces entered the roof aggregate.
|
||||
cert = load("21_0_1.json")
|
||||
rir = cert["sap_building_parts"][0]["sap_room_in_roof"]
|
||||
rir.pop("room_in_roof_type_1", None)
|
||||
rir["room_in_roof_details"] = {
|
||||
"slope_length_1": 7.0, "slope_height_1": 1.4,
|
||||
"slope_length_2": 7.0, "slope_height_2": 1.4,
|
||||
"slope_insulation_thickness_1": "100mm",
|
||||
"slope_insulation_thickness_2": "100mm",
|
||||
"stud_wall_length_1": 7.0, "stud_wall_height_1": 1.03,
|
||||
"stud_wall_length_2": 7.0, "stud_wall_height_2": 1.03,
|
||||
"stud_wall_insulation_thickness_1": "75mm",
|
||||
"stud_wall_insulation_thickness_2": "75mm",
|
||||
}
|
||||
|
||||
# Act
|
||||
result = EpcPropertyDataMapper.from_api_response(cert)
|
||||
|
||||
# Assert — both slopes + both stud walls reach the cascade, with the
|
||||
# lodged thickness parsed and the L × H area to 2 d.p.
|
||||
rir_part = result.sap_building_parts[0].sap_room_in_roof
|
||||
assert rir_part is not None
|
||||
surfaces = rir_part.detailed_surfaces
|
||||
assert surfaces is not None
|
||||
slopes = [s for s in surfaces if s.kind == "slope"]
|
||||
studs = [s for s in surfaces if s.kind == "stud_wall"]
|
||||
assert len(slopes) == 2
|
||||
assert len(studs) == 2
|
||||
assert abs(slopes[0].area_m2 - 9.8) <= 1e-9
|
||||
assert slopes[0].insulation_thickness_mm == 100
|
||||
assert abs(studs[0].area_m2 - 7.21) <= 1e-9
|
||||
assert studs[0].insulation_thickness_mm == 75
|
||||
|
|
|
|||
|
|
@ -209,6 +209,25 @@ class RoomInRoofDetails:
|
|||
flat_ceiling_height_1: Optional[float] = None
|
||||
flat_ceiling_insulation_type_1: Optional[int] = None
|
||||
flat_ceiling_insulation_thickness_1: Optional[str] = None
|
||||
# Sloping-ceiling + stud-wall surfaces of a Detailed RR — see
|
||||
# `rdsap_schema_21_0_1.RoomInRoofDetails`. Previously undeclared and
|
||||
# dropped by `from_dict`.
|
||||
slope_length_1: Optional[float] = None
|
||||
slope_length_2: Optional[float] = None
|
||||
slope_height_1: Optional[float] = None
|
||||
slope_height_2: Optional[float] = None
|
||||
slope_insulation_type_1: Optional[int] = None
|
||||
slope_insulation_type_2: Optional[int] = None
|
||||
slope_insulation_thickness_1: Optional[str] = None
|
||||
slope_insulation_thickness_2: Optional[str] = None
|
||||
stud_wall_length_1: Optional[float] = None
|
||||
stud_wall_length_2: Optional[float] = None
|
||||
stud_wall_height_1: Optional[float] = None
|
||||
stud_wall_height_2: Optional[float] = None
|
||||
stud_wall_insulation_type_1: Optional[int] = None
|
||||
stud_wall_insulation_type_2: Optional[int] = None
|
||||
stud_wall_insulation_thickness_1: Optional[str] = None
|
||||
stud_wall_insulation_thickness_2: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
|
|||
|
|
@ -246,6 +246,27 @@ class RoomInRoofDetails:
|
|||
flat_ceiling_height_1: Optional[float] = None
|
||||
flat_ceiling_insulation_type_1: Optional[int] = None
|
||||
flat_ceiling_insulation_thickness_1: Optional[str] = None
|
||||
# The sloping-ceiling and stud-wall surfaces of a Detailed RR. Up to two
|
||||
# of each per spec Figure 4. Previously undeclared, so `from_dict`
|
||||
# silently dropped them and the API mapper built ONLY the gable + flat-
|
||||
# ceiling surfaces — omitting the (large) sloping roof and the vertical
|
||||
# stud walls → undercounted RR heat loss → systematic over-rate.
|
||||
slope_length_1: Optional[float] = None
|
||||
slope_length_2: Optional[float] = None
|
||||
slope_height_1: Optional[float] = None
|
||||
slope_height_2: Optional[float] = None
|
||||
slope_insulation_type_1: Optional[int] = None
|
||||
slope_insulation_type_2: Optional[int] = None
|
||||
slope_insulation_thickness_1: Optional[str] = None
|
||||
slope_insulation_thickness_2: Optional[str] = None
|
||||
stud_wall_length_1: Optional[float] = None
|
||||
stud_wall_length_2: Optional[float] = None
|
||||
stud_wall_height_1: Optional[float] = None
|
||||
stud_wall_height_2: Optional[float] = None
|
||||
stud_wall_insulation_type_1: Optional[int] = None
|
||||
stud_wall_insulation_type_2: Optional[int] = None
|
||||
stud_wall_insulation_thickness_1: Optional[str] = None
|
||||
stud_wall_insulation_thickness_2: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue