From 575cdd539a0af38d771aea49492416b49bcc6b6e Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 27 May 2026 18:06:02 +0000 Subject: [PATCH] Slice S0380.3: surface wall_insulation_type=6 for 'FE Filled Cavity + External' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends `_ELMHURST_INSULATION_CODE_TO_SAP10` in `datatypes/epc/domain/mapper.py` with the two-letter dual codes documented on Elmhurst Summary PDFs: "FE" → 6 (Filled cavity + External insulation; cohort fixture) "FI" → 7 (Filled cavity + Internal insulation; mirror, no fixture) The cascade `wall_insulation_type` enum (per `domain/sap10_ml/rdsap_uvalues.py` lines 120-131) treats codes 6 and 7 as composite-resistance walls (filled cavity in series with an external/internal insulation layer), routing through a different U-value calc than the plain filled-cavity default. Cert 0380's Summary lodges `walls.insulation = "FE Filled Cavity + External"` which until this slice fell through `_leading_code` to a missing dict entry and the mapper produced `wall_insulation_type=None`, defaulting the cascade to the as-built path and overstating walls heat loss by +58 W/K. Forcing function (Slice S0380.1): cert 0380 Summary cascade SAP moves from 81.7528 (Δ -6.7576 — i.e. after Slice S0380.2 only) to 86.8671 (Δ -1.6433) — closes ~76% of the remaining gap. `walls_w_per_k` drops from 69.6900 to 24.6238. Residual ~13 W/K wall gap vs API's 11.6150 is the next workstream: `wall_insulation_thickness` is still None on the Summary EPC (API lodges '100mm'). Without the thickness the cascade applies the composite U-value at the dual-code's default thickness rather than the lodged 100 mm. Added focused unit test `test_summary_0380_filled_cavity_plus_external_insulation_routes_to_code_6` that pins both `wall_construction == 4` and `wall_insulation_type == 6` on the mapper boundary, so future debuggers can localise regressions in the dual-code lookup before walking the full chain. Pyright baseline preserved: datatypes/epc/domain/mapper.py: 32 errors (no new errors introduced) backend/documents_parser/tests/test_summary_pdf_mapper_chain.py: 0 errors Regression suite: 671 pass + 11 fail (vs handover baseline 669 + 10 — net +2 pass for the two new GREEN unit tests across Slices S0380.2-3, +1 fail still being the S0380.1 chain test that this slice continues to close but does not yet fully resolve). Spec refs: - SAP 10.2 §3.7 / Table S5 (U-values for masonry walls — composite filled-cavity-plus-insulation calc) - `domain/sap10_ml/rdsap_uvalues.py:120` (RdSAP schema `wall_insulation_type` enum: 6 = filled cavity + external) - Cert 0380 worksheet `dr87-0001-000899.pdf` (lodges Mitsubishi PUZ-WM50VHA ASHP on a cavity wall with subsequent external insulation — the composite-wall fixture) Co-Authored-By: Claude Opus 4.7 --- .../tests/test_summary_pdf_mapper_chain.py | 24 +++++++++++++++++++ datatypes/epc/domain/mapper.py | 9 +++++++ 2 files changed, 33 insertions(+) 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 a4fb4229..118ea9e9 100644 --- a/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py +++ b/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py @@ -514,6 +514,30 @@ def test_summary_0380_main_heating_category_is_heat_pump() -> None: assert main.main_heating_category == 4 +def test_summary_0380_filled_cavity_plus_external_insulation_routes_to_code_6() -> None: + # Arrange — cert 0380's Summary lodges main walls as + # `wall_type = "CA Cavity"` and `insulation = "FE Filled Cavity + + # External"` (a cavity wall with subsequent external-insulation + # upgrade). The cascade enum `wall_insulation_type=6` is + # "filled cavity + external insulation" (per + # `domain.sap10_ml.rdsap_uvalues` lines 120-131); without it the + # cascade defaults to the as-built routing and overstates walls + # heat loss by +58 W/K on cert 0380 (Summary 69.69 vs API 11.62 + # at HEAD before this slice). API path EPC for cert 0380 surfaces + # `wall_insulation_type=6` and is the ground-truth pin here. + pages = _summary_pdf_to_textract_style_pages(_SUMMARY_000899_PDF) + site_notes = ElmhurstSiteNotesExtractor(pages).extract() + + # Act + epc = EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes) + + # Assert + assert epc.sap_building_parts, "no building parts surfaced" + main = epc.sap_building_parts[0] + assert main.wall_construction == 4 # 4 = Cavity ('CA') + assert main.wall_insulation_type == 6 # 6 = filled cavity + external + + def test_summary_0380_full_chain_sap_matches_worksheet_pdf_exactly() -> None: # Arrange — cert 0380-2471-3250-2596-8761 (Summary_000899.pdf / # dr87-0001-000899.pdf) is the first heat-pump cert under per-cert diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index 05af513f..12c60a00 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -2040,12 +2040,21 @@ _ELMHURST_WALL_CODE_TO_SAP10: Dict[str, int] = { # Elmhurst wall-insulation-type codes mapped to the SAP10 integer enum # documented at domain.sap10_ml.rdsap_uvalues.WALL_INSULATION_FILLED_CAVITY. +# Two-letter dual codes ("FE", "FI") encode a cavity-wall that received a +# subsequent external- or internal-insulation upgrade — these map to the +# SAP10 composite codes 6 and 7, which the cascade routes through the +# series-resistance composite-U calc rather than the filled-cavity table +# default. Cert 0380's `walls.insulation = "FE Filled Cavity + External"` +# is the cohort fixture pinning the FE branch. _ELMHURST_INSULATION_CODE_TO_SAP10: Dict[str, int] = { "E": 1, # External wall insulation "F": 2, # Filled cavity "I": 3, # Internal wall insulation "A": 4, # As built / assumed (default cascade) "N": 5, # None specified + "FE": 6, # Filled cavity + external insulation (cohort: cert 0380) + "FI": 7, # Filled cavity + internal insulation (no fixture yet — + # mirror of FE for the internal-upgrade variant) }