From b5cbfe83de05413776ed48182f6b259be44b10e0 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 25 May 2026 16:49:37 +0000 Subject: [PATCH] Slice 64: bulk-update cohort 000474 hand-built for Cat A diff parity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes 36 of the 50 mapper-vs-hand-built load-bearing divergences by populating fields the Elmhurst mapper extracts 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["000474"]` SapResult fields (all 11 1e-4 pins remain GREEN against worksheet `SAP value 62.2584`). Per-SapBuildingPart additions (Main, Ext1, Ext2): - `wall_thickness_measured`: False → True. Summary §7 lodges Wall Thickness 280 mm explicitly; the cascade doesn't read this field (grep `wall_thickness_measured` across domain/sap/ returns no consumer outside test fixtures), so flipping it is field-level- only. - `floor_type`, `floor_construction_type`, `floor_insulation_type_str`, `floor_u_value_known`: surfaced from Summary §9 ("G Ground floor" / "U Above unheated space" / "T Suspended timber" / "A As built" / U-value Known = No). Strings carry the lodged text for cross-mapper parity; cascade reads the int codes on SapFloorDimension. - `roof_insulation_location`, `roof_insulation_thickness`: surfaced from Summary §8 ("J Joists" + "100 mm"). Cascade's `u_roof` for age B at thickness=100 returns the same 0.40 W/m²K as the age-B default (thickness=None falls through to `_ROOF_BY_AGE['B']=0.40`), so the cascade output is identical. SapVentilation additions (all cascade-equivalent — `None` defaults to 0 throughout the §2 cascade chain): - 6 explicit zero counts (`open_flues`, `closed_flues`, `boiler_flues`, `other_flues`, `passive_vents`, `flueless_gas_fires`) - `pressure_test="Not available"` (descriptive, no test was lodged) - `draught_lobby=True` (the legacy field; cascade reads `has_draught_lobby=False` which is set already, so True on the legacy field has no cascade effect) Top-level additions via `make_minimal_sap10_epc`: - `extensions_count=2` (Slice 54 fix on mapper made this surface; the hand-built was carrying the pre-Slice-54 hard-coded 0) - `blocked_chimneys_count=0`, `dwelling_type="Mid-Terrace house"`, `built_form="Mid-Terrace"`, `property_type="House"` Post-construction mutations (helper doesn't expose these as kwargs): - `has_conservatory=False`, `any_unheated_rooms=False`, `number_of_storeys=2`, `hydro=False`, `photovoltaic_array=False` Diff count: 50 → **14**. The remaining 14 are real semantic gaps for the next slices to close: Cat B (mapper needs to surface 7 fields): - country_code (Elmhurst mapper produces None; should set 'ENG') - sap_heating.water_heating_fuel (None vs 26 — gas main heating should imply gas water heating fuel) - main_heating_details[0].boiler_flue_type (None vs 2 — Summary §14.1 lodges "Balanced" flue type) - main_heating_details[0].emitter_temperature ('Unknown' vs 1) - main_heating_details[0].main_heating_number (None vs 1) - sap_ventilation.has_draught_lobby (None vs False) - dual-encoded central_heating_pump_age int/str Cat C (structural shape, 2 diffs): - sap_windows: LEN 7 vs 5 (mapper 1:1 with §11 table vs hand-built collapsed by glazing-type group, preserving total area — cascade-equivalent but not field-equal) - sap_building_parts[*].party_wall_construction: None vs 0 (cohort convention sentinel; the cohort 000474 docstring established `0 = "Unable to determine"`) Cat B handbuilt-needs (hand-built should add 2 fields the mapper already surfaces): - sap_heating.shower_outlets (mapper extracts 'Non-electric shower') - sap_heating.number_baths (mapper extracts 1) 11 cohort cascade pins still GREEN; pyright net-zero (0 errors on the touched fixture file). Tracer-bullet diff test stays RED with 14 divergences (was 50). Co-Authored-By: Claude Opus 4.7 --- .../tests/_elmhurst_worksheet_000474.py | 71 +++++++++++++++++-- 1 file changed, 66 insertions(+), 5 deletions(-) diff --git a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000474.py b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000474.py index f0ef9d86..1b9ef77b 100644 --- a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000474.py +++ b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000474.py @@ -49,13 +49,25 @@ _WC_CAVITY = 4 def build_epc() -> EpcPropertyData: - """EpcPropertyData mirroring the Elmhurst 000474 inputs.""" + """EpcPropertyData mirroring the Elmhurst 000474 inputs. + + Field-level parity with `from_elmhurst_site_notes(Summary_000474. + 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_NNNNNN` 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 280 mm explicitly; matches mapper. + wall_thickness_measured=True, party_wall_construction=0, sap_floor_dimensions=[ SapFloorDimension( @@ -72,13 +84,22 @@ 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). + 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=100, ) extension_1 = 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=[ # Ext1 hangs off the main from the first storey upward — its @@ -98,13 +119,19 @@ def build_epc() -> EpcPropertyData: ), ], 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", + roof_insulation_thickness=100, ) extension_2 = SapBuildingPart( identifier=BuildingPartIdentifier.EXTENSION_2, 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( @@ -115,11 +142,18 @@ def build_epc() -> EpcPropertyData: ), ], wall_thickness_mm=380, + floor_type="Ground floor", + floor_construction_type="Suspended timber", + floor_insulation_type_str="As built", + floor_u_value_known=False, + # Summary §8 Ext2: "PN Pitched (slates/tiles), no access" + Joists + # + Insulation Thickness "Unknown" → mapper leaves thickness=None. + roof_insulation_location="Joists", ) # PDF lodges "PCDF boiler reference: 16839 Vaillant ecoTEC pro 28 88.70%". # The 16839 is the BRE PCDB index_number (Table 105 Vaillant ecoTEC pro # 28kW VUW GB 286/5-3, 2005-2015, winter eff 88.7%, summer eff 87.0%). - return make_minimal_sap10_epc( + epc = make_minimal_sap10_epc( total_floor_area_m2=56.79, country_code="ENG", postcode="bd3 8aq", @@ -130,11 +164,29 @@ def build_epc() -> EpcPropertyData: low_energy_fixed_lighting_bulbs_count=8, sap_windows=list(SECTION_6_VERTICAL_WINDOWS), percent_draughtproofed=78, + extensions_count=2, + blocked_chimneys_count=0, + dwelling_type="Mid-Terrace house", + built_form="Mid-Terrace", + property_type="House", sap_ventilation=SapVentilation( extract_fans_count=2, sheltered_sides=2, 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. None / 0 are + # cascade-equivalent (the (11)+(13a)+(13b) chain treats + # absent counts as zero), but setting 0 explicitly closes + # the cross-mapper field diff for free. + 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( main_heating_details=[ @@ -145,6 +197,15 @@ def build_epc() -> EpcPropertyData: ], ), ) + # 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 = 2 + epc.hydro = False + epc.photovoltaic_array = False + return epc # ============================================================================