slice S-B29: parse measured U from full-SAP floor + roof descriptions

Parallel of S-B24 (walls) for the other envelope elements. Full-SAP
assessments lodge a measured/calculated U-value directly in the
description ("Average thermal transmittance X W/m²K") for floors
(~1 391 corpus certs) and roofs (~1 140 certs). Per spec:
  - §5.11 (roofs) opening clause defers to assessor's value when
    present
  - §5.12 (floors): "Unless provided by the assessor the floor
    U-value is calculated according to BS EN ISO 13370"

Both u_floor and u_roof now invoke `_measured_u_from_description`
first; if it parses a value, they return it directly and skip the
cascade. No range cap (consistent with S-B24 design — calculator
mirrors what the assessor lodged).

Parity probe at 300 certs, seed=7: headlines unchanged (same parquet
sampling gap as S-B24 — full-SAP certs filtered out upstream). Slice
correctness proved by:
- 1 unit test for u_floor measured-U parse
- 1 unit test for u_roof measured-U parse
- existing 287 tests passing, no regressions

A bulk-zip-based probe to measure the corpus-wide impact remains the
needed tooling investment (see S-B24 commit message).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-18 21:54:17 +00:00
parent 25261d5c8b
commit 3ab09845e7
2 changed files with 57 additions and 0 deletions

View file

@ -417,6 +417,12 @@ def u_roof(
as uninsulated.
3. Table 18 age-band default.
"""
measured = _measured_u_from_description(description)
if measured is not None:
# Full-SAP cert lodges a measured roof U-value in the description
# ("Average thermal transmittance X W/m²K"); spec §5.11 opening
# clause defers to the assessor's value when present.
return measured
if insulation_thickness_mm == 0 and _described_as_insulated(description):
# Spec §5.11.4 (page 44 footnote): "If retrofit insulation
# present of unknown thickness use 50 mm". The cert encodes
@ -478,7 +484,16 @@ def u_floor(
age band". The cert encodes "thickness unknown" as either a missing
field (`insulation_thickness_mm=None`) or "NI" which
`_parse_thickness_mm` returns as 0.
Full-SAP assessments lodge a measured floor U-value directly in
the description ("Average thermal transmittance X W/m²K"); when
present this supersedes the BS EN ISO 13370 calculation per spec
§5.12 opening clause ("Unless provided by the assessor the floor
U-value is calculated according to BS EN ISO 13370").
"""
measured = _measured_u_from_description(description)
if measured is not None:
return measured
if area_m2 is None or perimeter_m is None or perimeter_m <= 0:
return 0.7
w = (wall_thickness_mm or 300) / 1000.0

View file

@ -466,6 +466,25 @@ def test_u_wall_uses_rdsap_unknown_thickness_default_of_50mm_when_insulated_but_
# ----- Roofs -----
def test_u_roof_description_with_measured_transmittance_returns_parsed_value() -> None:
# Arrange — ~1 140 corpus certs lodge a full-SAP measured roof
# U-value in the description, e.g. "Average thermal transmittance
# 0.11 W/m²K". The age-band cascade is bypassed: the assessor's
# measured/calculated value is used directly. Same contract as
# `u_wall` (S-B24) and `u_floor` (S-B29 cycle 1).
# Act
result = u_roof(
country=Country.ENG,
age_band="C",
insulation_thickness_mm=None,
description="Average thermal transmittance 0.11 W/m²K",
)
# Assert
assert result == pytest.approx(0.11, abs=0.001)
def test_u_roof_ni_thickness_with_insulated_description_applies_50mm_per_section_5_11_4() -> None:
# Arrange — 346 corpus certs lodge roof_insulation_thickness="NI"
# (Not Indicated, parsed to 0 by _parse_thickness_mm). When the
@ -621,6 +640,29 @@ def test_u_roof_explicit_thickness_beats_description() -> None:
# ----- Floors -----
def test_u_floor_description_with_measured_transmittance_returns_parsed_value() -> None:
# Arrange — ~1 391 corpus certs lodge a full-SAP measured floor
# U-value in the description, e.g. "Average thermal transmittance
# 0.18 W/m²K". The BS EN ISO 13370 calculation is bypassed: the
# assessor's measured/calculated value is used directly. Same
# contract as `u_wall` (S-B24).
# Act
result = u_floor(
country=Country.ENG,
age_band="B",
construction=None,
insulation_thickness_mm=None,
area_m2=100.0,
perimeter_m=40.0,
wall_thickness_mm=300,
description="Average thermal transmittance 0.18 W/m²K",
)
# Assert
assert result == pytest.approx(0.18, abs=0.001)
def test_u_floor_ni_thickness_with_insulated_description_applies_50mm_per_table19_footnote() -> None:
# Arrange — 2 413 corpus certs (~12%) lodge floors with
# floor_insulation_thickness="NI" (Not Indicated, which our