Slice 75: bulk-update cohort 000480 hand-built for Cat A diff parity

Closes 31 of 32 mapper-vs-hand-built load-bearing divergences by
populating fields the Elmhurst mapper extracts from Summary_000480.
pdf but the original cohort hand-built left at their `make_minimal_
sap10_epc` / dataclass-default values. Every change is cascade-
equivalent — none alter `_FIXTURE_PINS["000480"]` SapResult fields
(all 11 1e-4 pins remain GREEN against worksheet `SAP value 61.2986`).

Mirrors the Slice 64 / 72 pattern. 000480-specific deltas vs 000477:

- Two SapBuildingParts (Main + Ext1) → Cat A descriptive fields
  applied per-bp; Ext1 floor is "Above unheated space" (not "Ground
  floor") because the extension hangs over an open passageway (the
  cert's `is_exposed_floor=True` for the lowest Ext1 floor).
- `roof_insulation_thickness=300` on Main — cascade-inert because the
  RR (19.83 m²) is larger than the Main storey footprint (15.28 m²),
  so Main has no external roof line; set for field parity with the
  mapper, which extracts the §8 Main row's 300 mm regardless.
- `extensions_count=1` — was 0 by default; the mapper extracts it
  from `len(survey.extensions)` (Slice 54 fix).

Standard Cat A additions (per Slice 72 pattern): floor descriptive
fields, roof_insulation_location, 6 ventilation zero counts,
draught_lobby=True, pressure_test="Not available", top-level
descriptive strings + booleans + number_of_storeys=3, shower_outlets,
central_heating_pump_age_str.

Diff count: 32 → **1**. Remaining diff is structural:
- `sap_windows: LEN 7 vs 2` — closed via the next-slice 1:1 expansion.

11 cohort 000480 cascade pins still GREEN; 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 17:52:20 +00:00
parent e52e4b7f1b
commit 56f41ca4a2

View file

@ -27,6 +27,8 @@ from datatypes.epc.domain.epc_property_data import (
SapRoomInRoofSurface,
SapVentilation,
SapWindow,
ShowerOutlet,
ShowerOutlets,
)
from domain.ml.tests._fixtures import (
make_main_heating_detail,
@ -48,13 +50,25 @@ _WC_CAVITY = 4
def build_epc() -> EpcPropertyData:
"""EpcPropertyData mirroring the Elmhurst 000480 inputs."""
"""EpcPropertyData mirroring the Elmhurst 000480 inputs.
Field-level parity with `from_elmhurst_site_notes(Summary_000480.
pdf)` for the load-bearing field allow-list every cohort hand-
built doubles as the ground-truth diff target for the Elmhurst
mapper. Cascade-equivalent encoding-only fields (descriptive floor/
roof strings, ventilation zero counts) are populated explicitly to
eliminate noise from `test_from_elmhurst_site_notes_matches_hand_
built_000480` diffs without altering the SAP cascade output (the
Section-10a 1e-4 pins in `test_e2e_elmhurst_sap_score.py` remain
GREEN against the worksheet PDF).
"""
main = SapBuildingPart(
identifier=BuildingPartIdentifier.MAIN,
construction_age_band="B",
wall_construction=_WC_CAVITY,
wall_insulation_type=4,
wall_thickness_measured=False,
# Summary §7 lodges Wall Thickness 380 mm explicitly; matches mapper.
wall_thickness_measured=True,
party_wall_construction=0, # "Unable to determine" → u_party_wall = 0.25
sap_floor_dimensions=[
SapFloorDimension(
@ -104,13 +118,25 @@ def build_epc() -> EpcPropertyData:
],
),
wall_thickness_mm=380,
# Mapper-extracted descriptive fields (cascade reads the int
# codes on SapFloorDimension; these strings carry the lodged
# Summary text for cross-mapper field parity). `roof_insulation_
# thickness=300` is cascade-inert on Main because the entire
# Main roof is the RR (no external roof line); set for field
# parity with the mapper, which extracts §8 Main → 300 mm.
floor_type="Ground floor",
floor_construction_type="Suspended timber",
floor_insulation_type_str="As built",
floor_u_value_known=False,
roof_insulation_location="Joists",
roof_insulation_thickness=300,
)
extension = SapBuildingPart(
identifier=BuildingPartIdentifier.EXTENSION_1,
construction_age_band="B",
wall_construction=_WC_CAVITY,
wall_insulation_type=4,
wall_thickness_measured=False,
wall_thickness_measured=True,
party_wall_construction=0,
sap_floor_dimensions=[
SapFloorDimension(
@ -138,8 +164,13 @@ def build_epc() -> EpcPropertyData:
# joist insulation 300 mm (or ≈ 270 mm row at 0.16). Pin 300 mm.
roof_insulation_thickness=300,
wall_thickness_mm=380,
floor_type="Above unheated space",
floor_construction_type="Suspended timber",
floor_insulation_type_str="As built",
floor_u_value_known=False,
roof_insulation_location="Joists",
)
return make_minimal_sap10_epc(
epc = make_minimal_sap10_epc(
total_floor_area_m2=84.41,
country_code="ENG",
postcode="bd5 8dn",
@ -150,6 +181,11 @@ def build_epc() -> EpcPropertyData:
percent_draughtproofed=100,
low_energy_fixed_lighting_bulbs_count=SECTION_5_BULB_COUNT_LEL,
sap_windows=list(SECTION_6_VERTICAL_WINDOWS),
extensions_count=1,
blocked_chimneys_count=0,
dwelling_type="Mid-Terrace house",
built_form="Mid-Terrace",
property_type="House",
sap_ventilation=SapVentilation(
extract_fans_count=1,
sheltered_sides=2,
@ -159,6 +195,18 @@ def build_epc() -> EpcPropertyData:
# premium. Mirror the worksheet, not the cert input.
has_suspended_timber_floor=False,
has_draught_lobby=False,
# SAP10.2 §2 — explicit zero counts match the mapper, which
# parses the Summary's "No. of <flue>" rows. Cascade-equivalent
# to leaving these None (the (11)+(13a)+(13b) chain treats
# absent counts as zero).
open_flues_count=0,
closed_flues_count=0,
boiler_flues_count=0,
other_flues_count=0,
passive_vents_count=0,
flueless_gas_fires_count=0,
draught_lobby=True,
pressure_test="Not available",
),
sap_heating=make_sap_heating(
# 000480 line 89: PCDF Index 16839 — Vaillant ecoTEC pro 28
@ -177,6 +225,23 @@ def build_epc() -> EpcPropertyData:
number_baths=0, # 000480 line 124: Total number of baths = 0
),
)
# Top-level cert-lodgement booleans / counts the Elmhurst mapper
# surfaces from the Summary PDF but `make_minimal_sap10_epc` doesn't
# expose as kwargs. Set post-construction (dataclass is non-frozen).
epc.has_conservatory = False
epc.any_unheated_rooms = False
epc.number_of_storeys = 3
# `make_sap_heating` doesn't expose `shower_outlets` as a kwarg; the
# Elmhurst mapper surfaces it from Summary §16. Cascade-equivalent:
# 0 baths + 1 non-electric mixer is what Appendix J §1a's flow-rate
# back-solve already assumes for this fixture.
epc.sap_heating.shower_outlets = ShowerOutlets(
shower_outlet=ShowerOutlet(shower_outlet_type="Non-electric shower"),
)
# Summary §14 "Heat pump age: Unknown" — surfaced by the Elmhurst
# mapper as the str dual-encoding that internal_gains.py reads.
epc.sap_heating.main_heating_details[0].central_heating_pump_age_str = "Unknown"
return epc
# ============================================================================