From 1cea73df7c7b46f6f028c5ed4286a032d71b632e Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 28 May 2026 16:22:04 +0000 Subject: [PATCH] =?UTF-8?q?Slice=20S0380.37:=20drop=20cert=20001479=20hand?= =?UTF-8?q?-built=20fixture=20=E2=80=94=20covered=20by=20passing=20product?= =?UTF-8?q?ion-path=20chain=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cert 001479 was added in ee98dbe0 as "skeleton + 11 RED pins" — a hand-built EpcPropertyData intended to cascade to worksheet P960-0001-001479.pdf at 1e-4 for 9 SapResult fields. The skeleton was never finished; the 9 _FIXTURE_PINS pin-checks have been red the entire time (at HEAD: sap_score 65 vs 69, space_heating 9715 vs 8104 kWh, etc.). Meanwhile the production-path chain tests for the same cert have landed at 1e-4 vs the worksheet's continuous SAP 69.0094 and are GREEN at HEAD: - test_summary_001479_full_chain_sap_matches_worksheet_pdf_exactly (Summary PDF -> extractor -> mapper -> calc, 1e-4 vs worksheet) - test_api_001479_full_chain_sap_matches_worksheet_pdf_exactly (API JSON -> mapper -> calc, 1e-4 vs worksheet) - 5 test_summary_001479_*_ mapper-shape unit tests These exercise the actual from_elmhurst_site_notes / from_api_response code paths the production runtime uses, which is strictly stronger coverage than a hand-built mirror. Drops 001479 from _FIXTURE_PINS / _FIXTURE_MODULES and deletes the stub _elmhurst_worksheet_001479.py. Also fixes the stale "Slice 62 iteration" reference in test_summary_pdf_mapper_chain.py. Test baseline: 9 fewer fails (10 -> 1; remaining FEE-round-trip 1e-9 noise to be fixed in S0380.38). Co-Authored-By: Claude Opus 4.7 --- .../tests/test_summary_pdf_mapper_chain.py | 3 +- .../tests/_elmhurst_worksheet_001479.py | 267 ------------------ .../tests/test_e2e_elmhurst_sap_score.py | 12 - 3 files changed, 1 insertion(+), 281 deletions(-) delete mode 100644 domain/sap10_calculator/worksheet/tests/_elmhurst_worksheet_001479.py diff --git a/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py b/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py index 21857309..50269060 100644 --- a/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py +++ b/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py @@ -2057,8 +2057,7 @@ def test_from_elmhurst_site_notes_matches_hand_built_000474() -> None: # EpcPropertyData; any divergence is a mapper-coverage gap. # # Tracer-bullet scope: cert 000474 only. Once GREEN, parametrize - # over the 5 other cohort fixtures and add cert 001479 (after - # `_elmhurst_worksheet_001479` lands at 1e-4 via Slice 62 iteration). + # over the 5 other cohort fixtures. pages = _summary_pdf_to_textract_style_pages(_SUMMARY_000474_PDF) site_notes = ElmhurstSiteNotesExtractor(pages).extract() mapped = EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes) diff --git a/domain/sap10_calculator/worksheet/tests/_elmhurst_worksheet_001479.py b/domain/sap10_calculator/worksheet/tests/_elmhurst_worksheet_001479.py deleted file mode 100644 index 37faca80..00000000 --- a/domain/sap10_calculator/worksheet/tests/_elmhurst_worksheet_001479.py +++ /dev/null @@ -1,267 +0,0 @@ -"""Inputs + expected outputs from Elmhurst SAP10.2 worksheet P960-0001-001479. - -Source: Summary_001479.pdf + P960-0001-001479.pdf (GOV.UK EPB cert -`0535-9020-6509-0821-6222`, lodged 31 Oct 2025). Semi-detached house -on Howick Park Drive, PR1 0LX. **First cohort fixture with a real -GOV.UK API counterpart** — this is the cross-mapper parity-test -reference for the API mapper. - -Worksheet header: - Property Type House, Semi-Detached - Storeys 2 - Habitable Rooms 4 (all heated) - Property Age Band C, Ext1: L, Ext2: C - Sheltered Sides 1 - Living Area 17.13 m² / 28.0% - Thermal MassValue 250.00 (medium default) - Main Heating PCDF 17507 Worcester Greenstar 30i ErP - Mains gas, 89% winter / 86.6% summer - Controls SAP 2106 Programmer + Roomstat + TRVs - Boiler interlock yes, pump in heated space - Combi standard, gas/oil time-clock keep-hot - Secondary Heating SAP 605 — Flush-fitting live-effect gas fire, - sealed to chimney, 40% efficiency, MAINS GAS - Water Heating From Main Heating 1 (combi, no cylinder) - Mechanical Ventilation None - Intermittent Fans 2 - -Building parts: - Main: age C, 2 storeys (30.45 m² ground + 30.77 m² first); cavity - wall U=0.70 (worksheet); party wall CU (cavity unfilled, - U=0.50); 300 mm joist roof insulation U=0.14. - Ext1: age L (worksheet header — Summary §3 says "M 2023 onwards"; - this fixture mirrors the worksheet at 1e-4 since the - worksheet is the calculator's source of truth). 5.37 m² - single-storey extension at ground level. Filled-cavity wall - U=0.26; PS sloping-ceiling roof insulated U=0.15; - insulated floor U=0.20. - Ext2: age C, **cantilevered upper-storey** extension hanging over - the back garden — 1.92 m² with exposed timber floor at U=1.20 - (Table 20). Cavity wall U=0.70; PS sloping-ceiling roof - **uninsulated** at U=2.30 (Slice 57: pre-1950 PS + As Built - thickness → 0 mm). - -Distinct features vs prior cohort fixtures (000474–000516): -- **Cert has a real GOV.UK API counterpart** — first cross-mapper - parity-test fixture (0535-9020-6509-0821-6222). -- **Multi-age building parts** (C, L, C) — Slice 60 dwelling-wide y - bridging convention picks up the dwelling primary age (C → 0.15). -- **Cantilevered upper-storey Ext2** with exposed floor (1.20). -- **PS Pitched sloping-ceiling roofs** on Ext1 (insulated, 0.15) and - Ext2 (uninsulated, 2.30). -- **Per-window U lodgement** — 8 Main windows at U=2.8 (g=0.76), - 1 Ext1 window at U=1.4 (g=0.72) — manufacturer Argon-filled DG. -- **Mains-gas secondary heating** (SAP code 605, η=40%) — first - non-electric secondary in the cohort; exercises Slice 58's - secondary fuel cost routing through `secondary_fuel_type=26`. - -Source-data caveat: Summary §3 lodges Ext1 age band as `M 2023 -onwards`; the worksheet header records `Ext1: L` (2012-2022). The -hand-built encodes 'L' to reproduce the worksheet at 1e-4; the -Elmhurst mapper trusts the Summary (M) and will diverge on this field -during cross-mapper parity testing. -""" - -from datatypes.epc.domain.epc_property_data import ( - BuildingPartIdentifier, - EpcPropertyData, - SapBuildingPart, - SapFloorDimension, - SapVentilation, - SapWindow, -) -from domain.sap10_ml.tests._fixtures import ( - make_main_heating_detail, - make_minimal_sap10_epc, - make_sap_heating, - make_window, -) - -_WC_CAVITY = 4 -_WALL_INSULATION_NONE = 4 # "As built" / uninsulated cavity -_WALL_INSULATION_FILLED_CAVITY = 2 - - -def build_epc() -> EpcPropertyData: - """EpcPropertyData mirroring the Elmhurst 001479 worksheet inputs. - - Floor `room_height_m` mirrors the worksheet `(2x)` height column, - which adds +0.25 m to every storey above the lowest per the SAP - convention (cohort 000474 docstring §"Storey height convention"). - """ - main = SapBuildingPart( - identifier=BuildingPartIdentifier.MAIN, - construction_age_band="C", - wall_construction=_WC_CAVITY, - wall_insulation_type=_WALL_INSULATION_NONE, - wall_thickness_measured=True, - # Summary §7 lodges "CU Cavity masonry unfilled" → U=0.50 per - # `u_party_wall`; Slice 55 added "CU" to the Elmhurst code map. - party_wall_construction=_WC_CAVITY, - sap_floor_dimensions=[ - SapFloorDimension( - room_height_m=2.39, # lowest internal, no +0.25 - total_floor_area_m2=30.45, - party_wall_length_m=6.94, - heat_loss_perimeter_m=11.99, - floor=0, - ), - SapFloorDimension( - room_height_m=2.53, # = 2.28 internal + 0.25 floor-void - total_floor_area_m2=30.77, - party_wall_length_m=6.94, - heat_loss_perimeter_m=13.55, - floor=1, - ), - ], - wall_thickness_mm=280, - # Worksheet §3: 300 mm joist roof insulation → U=0.14. - roof_insulation_thickness=300, - # Floor descriptive fields — required for the RdSAP 10 §5 (12) - # spec rule in `_has_suspended_timber_floor_per_spec` to recognise - # this as a "suspended timber ground floor" (cascade derives - # (12)=0.2 unsealed for age C with U=0.65 ≥ 0.5). - floor_type="Ground floor", - floor_construction_type="Suspended timber", - ) - ext_1 = SapBuildingPart( - identifier=BuildingPartIdentifier.EXTENSION_1, - construction_age_band="L", # worksheet header (Summary §3 says M; - # cross-mapper diff will flag this) - wall_construction=_WC_CAVITY, - wall_insulation_type=_WALL_INSULATION_FILLED_CAVITY, - wall_thickness_measured=True, - # Ext1 sits flush against neighbours on no party walls - # (worksheet `Party wall length=0.00`). `party_wall_construction` - # is still type-required as int; 0 = "Unable to determine" - # (Slice 54 cohort convention) — the cascade multiplies by - # party_wall_length=0 so the U is irrelevant here. - party_wall_construction=0, - sap_floor_dimensions=[ - SapFloorDimension( - room_height_m=2.48, - total_floor_area_m2=5.37, - party_wall_length_m=0.0, - heat_loss_perimeter_m=6.67, - floor=0, - ), - ], - wall_thickness_mm=280, - # Worksheet §3 lodges Ext1 sloping-ceiling roof U=0.15 — cascade - # default for age L pitched roof with no thickness lodged matches. - ) - ext_2 = SapBuildingPart( - identifier=BuildingPartIdentifier.EXTENSION_2, - construction_age_band="C", - wall_construction=_WC_CAVITY, - wall_insulation_type=_WALL_INSULATION_NONE, - wall_thickness_measured=True, - # Ext2 has no party walls either (worksheet PWL=0). Use the - # "Unable to determine" sentinel 0 (cohort convention). - party_wall_construction=0, - sap_floor_dimensions=[ - # Cantilevered upper-storey extension: single floor_dim with - # `is_exposed_floor=True` routes through Table 20 → U=1.20. - SapFloorDimension( - room_height_m=2.10, - total_floor_area_m2=1.92, - party_wall_length_m=0.0, - heat_loss_perimeter_m=2.81, - floor=0, - is_exposed_floor=True, - ), - ], - wall_thickness_mm=280, - # Slice 57: PS sloping-ceiling + As Built + pre-1950 → thickness=0 - # → Table 16 row 0 U=2.30. - roof_insulation_thickness=0, - ) - - # §11 Windows: 8 Main + 1 Ext1. All double-glazed; Ext1 has a low-U - # Argon-filled unit (Manufacturer 1.40 / g=0.72). Heights default to - # 1.0 m per the Elmhurst W×H=Area area-preserving convention; widths - # set to lodged Area / 1.0 = lodged Area. - main_windows: tuple[SapWindow, ...] = ( - # Windows 1(Main) — area 3.34, orientation NW (8) - make_window(orientation=8, width=3.34, height=1.0, - solar_transmittance=0.76, u_value=2.8, window_location=0), - # Windows 2(Main) — area 0.73, NE (2) - make_window(orientation=2, width=0.73, height=1.0, - solar_transmittance=0.76, u_value=2.8, window_location=0), - # Windows 3(Main) — 6 entries - make_window(orientation=8, width=3.04, height=1.0, - solar_transmittance=0.76, u_value=2.8, window_location=0), - make_window(orientation=2, width=1.33, height=1.0, - solar_transmittance=0.76, u_value=2.8, window_location=0), - make_window(orientation=2, width=0.70, height=1.0, - solar_transmittance=0.76, u_value=2.8, window_location=0), - make_window(orientation=2, width=0.99, height=1.0, - solar_transmittance=0.76, u_value=2.8, window_location=0), - make_window(orientation=4, width=2.13, height=1.0, - solar_transmittance=0.76, u_value=2.8, window_location=0), - make_window(orientation=1, width=1.70, height=1.0, - solar_transmittance=0.76, u_value=2.8, window_location=0), - ) - ext_1_window = make_window( - # Windows 2(Ext1) — area 6.37, orientation SE (4) - orientation=4, width=6.37, height=1.0, - solar_transmittance=0.72, u_value=1.4, window_location=1, - ) - - return make_minimal_sap10_epc( - total_floor_area_m2=68.51, - country_code="ENG", - postcode="pr1 0lx", - sap_building_parts=[main, ext_1, ext_2], - habitable_rooms_count=4, - heated_rooms_count=4, - door_count=1, - # §13 Lightings: 17 LED + 6 CFL = 23 fittings, 73.91% LEL. - # SAP10 Appendix L scales each bulb type by its own efficacy ratio - # — keeping LED and CFL separate (not collapsed into `low_energy_*`) - # matches the worksheet's per-fitting lighting demand split. - led_fixed_lighting_bulbs_count=17, - cfl_fixed_lighting_bulbs_count=6, - incandescent_fixed_lighting_bulbs_count=0, - sap_windows=[*main_windows, ext_1_window], - percent_draughtproofed=90, - sap_ventilation=SapVentilation( - extract_fans_count=2, - sheltered_sides=1, - # `has_suspended_timber_floor` left None — the cascade - # derives the §2(12) value per RdSAP 10 spec rule (cert - # 001479 Main is G+T age C with U=0.65 ≥ 0.5 → unsealed → - # (12)=0.2). The lodged sap_ventilation block previously - # encoded the worksheet's (12) value directly via this - # boolean; the cascade now reproduces it mechanically. - has_draught_lobby=False, - ), - sap_heating=make_sap_heating( - main_heating_details=[ - make_main_heating_detail( - main_heating_index_number=17507, - main_heating_data_source=1, - ), - ], - # SAP code 605, 40%, mains gas (fuel 26) — exercises Slice 58. - secondary_heating_type=605, - secondary_fuel_type=26, - ), - ) - - -# ============================================================================ -# Cascade pins extracted from P960-0001-001479.pdf (Table 12 prices, -# Section 10a). All values at the worksheet's 4 d.p. precision. -# ============================================================================ -# (258) SAP rating = 69 -# "SAP value" = 69.0094 -# (257) Energy cost factor = 2.2215 -# (255) Total energy cost = 600.4001 -# (272) Total CO2 kg/year = 2687.3610 -# (98c) Σ monthly space heating = 8103.7054 kWh/yr -# (211) Main system 1 fuel = 8194.7583 kWh/yr -# (215) Secondary fuel = 2025.9264 kWh/yr -# (219) Water heating fuel = 2358.3123 kWh/yr -# (231) Pumps and fans = 160.0000 kWh/yr -# (232) Lighting electricity = 163.3584 kWh/yr diff --git a/domain/sap10_calculator/worksheet/tests/test_e2e_elmhurst_sap_score.py b/domain/sap10_calculator/worksheet/tests/test_e2e_elmhurst_sap_score.py index cb1ddc81..514c9c41 100644 --- a/domain/sap10_calculator/worksheet/tests/test_e2e_elmhurst_sap_score.py +++ b/domain/sap10_calculator/worksheet/tests/test_e2e_elmhurst_sap_score.py @@ -33,7 +33,6 @@ from domain.sap10_calculator.worksheet.tests import ( _elmhurst_worksheet_000487 as _w000487, _elmhurst_worksheet_000490 as _w000490, _elmhurst_worksheet_000516 as _w000516, - _elmhurst_worksheet_001479 as _w001479, ) from domain.sap10_calculator.worksheet.tests._elmhurst_fixtures import ( ALL_FIXTURES as _ELMHURST_FIXTURES, @@ -130,16 +129,6 @@ _FIXTURE_PINS: Final[dict[str, FixtureCascadePins]] = { lighting_kwh_per_yr=230.8853, pumps_fans_kwh_per_yr=160.0, ), - "001479": FixtureCascadePins( - sap_score=69, sap_score_continuous=69.0094, ecf=2.2215, - total_fuel_cost_gbp=600.4001, co2_kg_per_yr=2687.3610, - space_heating_kwh_per_yr=8103.7054, - main_heating_fuel_kwh_per_yr=8194.7583, - secondary_heating_fuel_kwh_per_yr=2025.9264, - hot_water_kwh_per_yr=2358.3123, - lighting_kwh_per_yr=163.3584, - pumps_fans_kwh_per_yr=160.0, - ), } @@ -150,7 +139,6 @@ _FIXTURE_MODULES: Final[dict[str, ModuleType]] = { "000487": _w000487, "000490": _w000490, "000516": _w000516, - "001479": _w001479, }