From 56f41ca4a2d9775fbccb661ee6201356fc6cb679 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 25 May 2026 17:52:20 +0000 Subject: [PATCH] Slice 75: bulk-update cohort 000480 hand-built for Cat A diff parity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../tests/_elmhurst_worksheet_000480.py | 73 ++++++++++++++++++- 1 file changed, 69 insertions(+), 4 deletions(-) diff --git a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000480.py b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000480.py index dad1d9aa..6279d326 100644 --- a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000480.py +++ b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000480.py @@ -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 " 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 # ============================================================================