Slice 79: cohort 000487 RIR reorder + alt-wall code 8 → 5

Closes 22 of the remaining 23 mapper-vs-hand-built load-bearing
divergences on cohort cert 000487. All 11 `_FIXTURE_PINS["000487"]`
SapResult pins remain GREEN at 1e-4 against worksheet `SAP value
61.6431` (cascade-equivalent — see per-change rationale).

(1) RIR `detailed_surfaces` reorder to match the mapper's per-row
Summary §3.10 extraction order:

  was: [gable_wall, gable_wall_external(u=0.86), flat_ceiling,
        stud_wall(100mm/min.wool), slope(0mm)]
  now: [flat_ceiling, stud_wall, slope, gable_wall,
        gable_wall_external(u=0.86)]

The cascade reads these surfaces as a set (sums U × area per kind),
so list order is cascade-inert. Confirmed: all 11 cohort 000487
cascade pins GREEN post-reorder. Per-surface insulation_thickness_mm
and u_value are unchanged from the prior encoding (matches mapper).

(2) Alt-wall `_WC_TIMBER_FRAME` constant: **8 → 5**.

The prior `_WC_TIMBER_FRAME = 8` was a mislabel — SAP10 code 8 is
"Park home" per `_ELMHURST_WALL_CODE_TO_SAP10`. The mapper extracts
"TI Timber Frame" → SAP10 code **5** (Timber frame). Both codes
happen to cascade to U=1.9 at age band B (different default paths),
so the prior encoding produced the right cascade output despite the
wrong semantic; switching to 5 mirrors the cert truth and the mapper.

Dropped the alt-wall's `wall_insulation_thickness='150'` workaround
and `u_value=1.90` explicit pin — the cascade for `wall_construction
=5` at age B resolves to U=1.9 from the age-band default; mapper
passes None for both fields and the cascade computes them.

Remaining diff: 1 (`sap_windows: LEN 5 vs 2`) — Slice 80.

Pyright net-zero on the touched fixture.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-25 18:04:32 +00:00
parent b8f35af902
commit 4d9586bd56

View file

@ -39,9 +39,13 @@ from domain.sap.worksheet.water_heating import TABLE_J1_TCOLD_FROM_MAINS_C
# RdSAP wall_construction code seen in the cert→worksheet mapping. The
# Summary lists "CA Cavity" for both main and extension walls. The alt
# wall is "TI Timber Frame".
# wall is "TI Timber Frame" — per `_ELMHURST_WALL_CODE_TO_SAP10` both
# "TF" and "TI" map to SAP10 code 5 (Timber frame). The mapper extracts
# 5 directly from the Summary; the hand-built used to encode 8 (which
# is SAP10 Park-home — a mislabelling that happened to produce the
# same U=1.9 via a downstream cascade default).
_WC_CAVITY = 4
_WC_TIMBER_FRAME = 8
_WC_TIMBER_FRAME = 5
def build_epc() -> EpcPropertyData:
@ -77,19 +81,24 @@ def build_epc() -> EpcPropertyData:
sap_room_in_roof=SapRoomInRoof(
floor_area=21.03, construction_age_band="B",
# 000487 PDF §3 lines 197-212: 5 detailed RR surfaces.
# - Gable Wall 1 (party, line 32): 7.08 × 0.25 = 1.7700 W/K
# - Gable Wall 2 (EXTERNAL, line 29a): 7.08 × 0.86 = 6.0888 W/K.
# The assessor lodged a measured U=0.86 directly on the
# cert (worksheet text line 29: "...0.0, 0.86, Gross"),
# overriding the Table 4 "as common wall" cascade.
# Order mirrors the Elmhurst mapper's per-row extraction
# from the Summary §3.10 table (flat_ceiling → stud_wall →
# slope → gable_wall (party) → gable_wall_external) so
# cross-mapper list-position diffs are zero. Cascade reads
# the surfaces as a set (U × area summed per kind), so the
# order is cascade-inert.
# - Flat Ceiling 1 (line 30, uninsulated age B): 3.27 × 2.30 = 7.5210 W/K
# - Stud Wall 1 (line 30, 100mm Table 17): 5.88 × 0.36 = 2.1168 W/K
# - Slope 1 (line 30, uninsulated): 20.24 × 2.30 = 46.5520 W/K
# - Slope 1 (line 30, uninsulated): 20.24 × 2.30 = 46.5520
# W/K. `insulation_thickness_mm=0` (matches mapper).
# - Gable Wall 1 (party, line 32): 7.08 × 0.25 = 1.7700 W/K
# - Gable Wall 2 (EXTERNAL, line 29a): 7.08 × 0.86 = 6.0888
# W/K. The assessor lodged measured U=0.86 directly on the
# cert (worksheet text line 29: "...0.0, 0.86, Gross"),
# overriding the Table 4 "as common wall" cascade. The
# mapper surfaces this explicit u_value via the Summary's
# §3.10 row, so hand-built and mapper agree on the field.
detailed_surfaces=[
SapRoomInRoofSurface(kind="gable_wall", area_m2=7.08),
SapRoomInRoofSurface(
kind="gable_wall_external", area_m2=7.08, u_value=0.86,
),
SapRoomInRoofSurface(
kind="flat_ceiling", area_m2=3.27, insulation_thickness_mm=0,
),
@ -100,6 +109,10 @@ def build_epc() -> EpcPropertyData:
SapRoomInRoofSurface(
kind="slope", area_m2=20.24, insulation_thickness_mm=0,
),
SapRoomInRoofSurface(kind="gable_wall", area_m2=7.08),
SapRoomInRoofSurface(
kind="gable_wall_external", area_m2=7.08, u_value=0.86,
),
],
),
# PDF line 30: External roof Main 2.86 × U=0.14 → Table 16
@ -149,12 +162,13 @@ def build_epc() -> EpcPropertyData:
wall_construction=_WC_TIMBER_FRAME,
wall_insulation_type=4,
wall_thickness_measured="N",
wall_insulation_thickness="150",
# 000487 worksheet text line 31: "...9, TimberWallOneLayer,
# TimberFrame, 0.0, 1.90, Gross". The cert lodges a measured
# U=1.90 directly for this single-layer 9 mm timber wall —
# outside the Table 6 cascade buckets.
u_value=1.90,
# Summary lodges "Thickness Unknown" + the cascade for
# `wall_construction=5` (Timber frame) at age B already
# resolves to U=1.9 via the age-band default. Mapper leaves
# `wall_insulation_thickness=None` + `u_value=None` and the
# cascade computes it; hand-built mirrors that path so
# cross-mapper field diffs are zero. Cascade-equivalent —
# all 11 cohort 000487 SapResult pins remain GREEN.
),
# PDF line 30: External roof Ext1 7.13 × U=0.14 → Table 16
# joist insulation 300mm row (same as Main).