mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
S0380.223: complete _part_geometry early-return key contract (RR KeyError)
5 certs in a 2026 API sample raised `KeyError: 'rr_common_wall_area_m2'` and were blocked from computing. Root cause: `_part_geometry`'s early return (taken when a building part lodges no sap_floor_dimensions — e.g. a party-wall-only or RR-only extension as bp[0]) returned only 6 of the 9 keys the full return exposes, omitting rr_common_wall_area_m2, rr_gable_area_m2 and cantilever_floor_area_m2. The §3.9 RR contribution block reads geom["rr_common_wall_area_m2"] / ["rr_gable_area_m2"] for EVERY part, so the floorless part's truncated dict raised KeyError at heat_transmission.py:974. Fix: the early return now exposes all 9 keys, the three RR/cantilever geometry values defaulting to 0.0 — correct, since a part with no floor dimensions has no derivable RR shell or cantilever (no floor area). Pure contract-completion bug; no spec/U-value change. Regression test pins the invariant directly: a floorless part's _part_geometry keys must equal a with-floors part's keys. Validated: all 5 certs now compute (4 within ~2 SAP of lodged; the 5th, 8536, has a separate residual). §4 suite 2393 passed; heat_transmission.py pyright unchanged at 12, test file at 71. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
28634e8ae5
commit
69fdbf9f1d
2 changed files with 35 additions and 0 deletions
|
|
@ -341,6 +341,13 @@ def _joined_descriptions(elements: list[Any]) -> Optional[str]:
|
||||||
|
|
||||||
def _part_geometry(part: SapBuildingPart) -> dict[str, float]:
|
def _part_geometry(part: SapBuildingPart) -> dict[str, float]:
|
||||||
if not part.sap_floor_dimensions:
|
if not part.sap_floor_dimensions:
|
||||||
|
# A part with no floor dimensions has no derivable RR shell or
|
||||||
|
# cantilever geometry, but the early return must still expose the
|
||||||
|
# SAME keys as the full return below: the §3.9 RR block reads
|
||||||
|
# geom["rr_common_wall_area_m2"] / ["rr_gable_area_m2"] /
|
||||||
|
# ["cantilever_floor_area_m2"] for every part, so omitting them
|
||||||
|
# here raised KeyError on multi-part certs whose first bp lodges
|
||||||
|
# no sap_floor_dimensions (5 certs in a 2026 API sample).
|
||||||
return {
|
return {
|
||||||
"ground_floor_area_m2": 0.0,
|
"ground_floor_area_m2": 0.0,
|
||||||
"top_floor_area_m2": 0.0,
|
"top_floor_area_m2": 0.0,
|
||||||
|
|
@ -348,6 +355,9 @@ def _part_geometry(part: SapBuildingPart) -> dict[str, float]:
|
||||||
"party_wall_area_m2": 0.0,
|
"party_wall_area_m2": 0.0,
|
||||||
"rr_floor_area_m2": 0.0,
|
"rr_floor_area_m2": 0.0,
|
||||||
"rr_simplified_a_rr_m2": 0.0,
|
"rr_simplified_a_rr_m2": 0.0,
|
||||||
|
"rr_common_wall_area_m2": 0.0,
|
||||||
|
"rr_gable_area_m2": 0.0,
|
||||||
|
"cantilever_floor_area_m2": 0.0,
|
||||||
}
|
}
|
||||||
fds = list(part.sap_floor_dimensions)
|
fds = list(part.sap_floor_dimensions)
|
||||||
ground = next((fd for fd in fds if fd.floor == 0), fds[0])
|
ground = next((fd for fd in fds if fd.floor == 0), fds[0])
|
||||||
|
|
|
||||||
|
|
@ -36,11 +36,36 @@ from domain.sap10_calculator.worksheet.heat_transmission import (
|
||||||
heat_transmission_from_cert,
|
heat_transmission_from_cert,
|
||||||
)
|
)
|
||||||
from domain.sap10_calculator.worksheet.heat_transmission import (
|
from domain.sap10_calculator.worksheet.heat_transmission import (
|
||||||
|
_part_geometry, # pyright: ignore[reportPrivateUsage]
|
||||||
_round_half_up, # pyright: ignore[reportPrivateUsage]
|
_round_half_up, # pyright: ignore[reportPrivateUsage]
|
||||||
_window_bp_index, # pyright: ignore[reportPrivateUsage]
|
_window_bp_index, # pyright: ignore[reportPrivateUsage]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_part_geometry_floorless_part_honours_full_key_contract() -> None:
|
||||||
|
# Arrange — a building part lodged with NO sap_floor_dimensions (e.g.
|
||||||
|
# a party-wall-only or RR-only extension; observed on 5 certs in a
|
||||||
|
# 2026 API sample). `_part_geometry`'s early return must expose the
|
||||||
|
# same dict keys as its full return: the §3.9 RR contribution block
|
||||||
|
# reads geom["rr_common_wall_area_m2"] / ["rr_gable_area_m2"] for
|
||||||
|
# EVERY part, so a missing key raises KeyError and blocks the cert.
|
||||||
|
floorless = make_building_part(floor_dimensions=[])
|
||||||
|
with_floors = make_building_part(
|
||||||
|
floor_dimensions=[make_floor_dimension(total_floor_area_m2=50.0)]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Act
|
||||||
|
early = _part_geometry(floorless)
|
||||||
|
full = _part_geometry(with_floors)
|
||||||
|
|
||||||
|
# Assert — identical key contract; the RR/cantilever geometry is 0.0
|
||||||
|
# for a floorless part (no floor area ⇒ no RR shell or cantilever).
|
||||||
|
assert set(early.keys()) == set(full.keys())
|
||||||
|
assert early["rr_common_wall_area_m2"] == 0.0
|
||||||
|
assert early["rr_gable_area_m2"] == 0.0
|
||||||
|
assert early["cantilever_floor_area_m2"] == 0.0
|
||||||
|
|
||||||
|
|
||||||
def test_roof_insulated_assumed_with_ni_thickness_uses_50mm_per_section_5_11_4() -> None:
|
def test_roof_insulated_assumed_with_ni_thickness_uses_50mm_per_section_5_11_4() -> None:
|
||||||
# Arrange — 346 corpus certs lodge roof_insulation_thickness="NI"
|
# Arrange — 346 corpus certs lodge roof_insulation_thickness="NI"
|
||||||
# with descriptions like "Pitched, insulated (assumed)". The
|
# with descriptions like "Pitched, insulated (assumed)". The
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue