From 81392208c42cb440b7fd36b7269d95c073683924 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Sun, 24 May 2026 14:27:32 +0000 Subject: [PATCH] =?UTF-8?q?Slice=2041:=20schema-21.0.1=20ventilation=20com?= =?UTF-8?q?pleteness=20=E2=80=94=207=20vent=20/=20draught=20fields=20plumb?= =?UTF-8?q?ed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit of raw-JSON keys vs RdSapSchema21_0_1 across the 9-fixture golden cohort surfaced 7 vent / draught fields silently dropped at deserialization: blocked_chimneys_count, open_flues_count, closed_flues_count, boilers_flues_count, other_flues_count, psv_count, has_draught_lobby. cert_to_inputs reads all of them for the §2 infiltration cascade; without them the calc treats every dwelling as flue-free / vent-free / no draught lobby and under-counts ACH. Fix: declare the 7 fields on RdSapSchema21_0_1; extend the mapper to surface blocked_chimneys_count on EpcPropertyData top-level (already declared) and the other 6 on SapVentilation (extends the slice 37 extract_fans_count work). has_draught_lobby coerces "true"/"false" strings to bool to match the SapVentilation type. Cohort residual shifts after re-pinning: - LN12 (0390-2254) — SAP +1 → 0 (FIRST CERT TO HIT LODGED SAP EXACTLY). blocked_chimneys=2 reduces infiltration, tightens both SAP and PE (PE −10.62 → −3.14, CO2 −0.11 → +0.04). - 0300 — PE +18.92 → +17.34, CO2 −0.43 → −0.54 (open_flues=1 + has_draught_lobby=true cross-cancel near-zero). - 0390-2954 — PE −25.62 → −27.64, CO2 −2.45 → −2.58 (has_draught_lobby=true). - 8135 — PE −17.58 → −14.37, CO2 −0.22 → −0.15 (blocked_chimneys=1). - Other 5 fixtures (0240, DE22, 6035, 7536, plus retired 9390): no shift — their certs lodge zeros or no vent fields beyond what Slice 37 plumbed. Rounded-SAP cohort distribution post-slice: 0 (LN12), +1 (8135), +2 (9390), +3 (7536), +8 (DE22, spec-drift), -6 (6035), -7 (0390-2954), -9 (0300), -12 (0240, RR-driven). Schema scope: 21.0.1 only. 21.0.0 schema's SapBuildingPart shares the same mapper code but no 21.0.0 fixtures live in the cohort to anchor against; defer to a future slice if needed. 930/930 Elmhurst cascade green. 14/14 golden cohort green at new pinned residuals. 77/77 mapper tests green. Pyright net-zero (34 errors before and after). Co-Authored-By: Claude Opus 4.7 --- datatypes/epc/domain/mapper.py | 15 ++++++- .../domain/tests/test_from_rdsap_schema.py | 22 ++++++++++ datatypes/epc/schema/rdsap_schema_21_0_1.py | 10 +++++ .../epc/schema/tests/fixtures/21_0_1.json | 7 ++++ .../sap/rdsap/tests/test_golden_fixtures.py | 40 +++++++++++-------- 5 files changed, 76 insertions(+), 18 deletions(-) diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index 8068485f..722e92c2 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -1563,6 +1563,7 @@ class EpcPropertyDataMapper: # Dwelling-level inputs used as ML features. multiple_glazed_proportion=schema.multiple_glazed_proportion, extract_fans_count=schema.extract_fans_count, + blocked_chimneys_count=schema.blocked_chimneys_count, insulated_door_u_value=schema.insulated_door_u_value, mechanical_vent_duct_placement=schema.mechanical_vent_duct_placement, mechanical_vent_duct_insulation=schema.mechanical_vent_duct_insulation, @@ -1615,9 +1616,21 @@ class EpcPropertyDataMapper: # SapVentilation slice — calculator reads cert→§2 ventilation # counts via epc.sap_ventilation.*; without this the cascade # defaults to zero on all flue / fan / vent counts and - # under-states infiltration. + # under-states infiltration. has_draught_lobby arrives from + # the API as the string "true"/"false"; we coerce here so the + # cascade sees a typed bool (None → False at the read site). sap_ventilation=SapVentilation( extract_fans_count=schema.extract_fans_count, + open_flues_count=schema.open_flues_count, + closed_flues_count=schema.closed_flues_count, + boiler_flues_count=schema.boilers_flues_count, + other_flues_count=schema.other_flues_count, + passive_vents_count=schema.psv_count, + has_draught_lobby=( + schema.has_draught_lobby == "true" + if schema.has_draught_lobby is not None + else None + ), ), ) diff --git a/datatypes/epc/domain/tests/test_from_rdsap_schema.py b/datatypes/epc/domain/tests/test_from_rdsap_schema.py index cebad47c..ce83ddce 100644 --- a/datatypes/epc/domain/tests/test_from_rdsap_schema.py +++ b/datatypes/epc/domain/tests/test_from_rdsap_schema.py @@ -622,6 +622,28 @@ class TestFromRdSapSchema21_0_1: assert sv is not None assert sv.extract_fans_count == 2 + def test_ventilation_completeness_all_seven_vent_fields_flow_through( + self, result: EpcPropertyData + ) -> None: + # Arrange — schema-21.0.1 carries seven vent / draught fields the + # cert→inputs cascade reads for the §2 infiltration calculation. + # Without these the calc treats the dwelling as flue-free / vent- + # free / no draught lobby, under-counting infiltration ACH. + # blocked_chimneys is top-level; the other 6 live on SapVentilation. + + # Act + sv = result.sap_ventilation + + # Assert + assert result.blocked_chimneys_count == 1 + assert sv is not None + assert sv.open_flues_count == 1 + assert sv.closed_flues_count == 1 + assert sv.boiler_flues_count == 1 + assert sv.other_flues_count == 1 + assert sv.passive_vents_count == 2 + assert sv.has_draught_lobby is True + # --- renewable heat incentive (RHI) --- def test_renewable_heat_incentive(self, result: EpcPropertyData) -> None: diff --git a/datatypes/epc/schema/rdsap_schema_21_0_1.py b/datatypes/epc/schema/rdsap_schema_21_0_1.py index db89194b..8fdadb72 100644 --- a/datatypes/epc/schema/rdsap_schema_21_0_1.py +++ b/datatypes/epc/schema/rdsap_schema_21_0_1.py @@ -362,6 +362,16 @@ class RdSapSchema21_0_1: extract_fans_count: Optional[int] = None wet_rooms_count: Optional[int] = None open_chimneys_count: Optional[int] = None + # Ventilation / draught completeness — surfaced into SapVentilation + # (or EpcPropertyData top-level for chimney counts) so the §2 cascade + # gets the real flue / vent / draught lobby state instead of zeros. + blocked_chimneys_count: Optional[int] = None + open_flues_count: Optional[int] = None + closed_flues_count: Optional[int] = None + boilers_flues_count: Optional[int] = None + other_flues_count: Optional[int] = None + psv_count: Optional[int] = None + has_draught_lobby: Optional[str] = None # "true" / "false" / "unknown" insulated_door_u_value: Optional[float] = None suggested_improvements: Optional[List[SuggestedImprovement]] = None mechanical_vent_duct_type: Optional[int] = None diff --git a/datatypes/epc/schema/tests/fixtures/21_0_1.json b/datatypes/epc/schema/tests/fixtures/21_0_1.json index a8c8d645..9eaed954 100644 --- a/datatypes/epc/schema/tests/fixtures/21_0_1.json +++ b/datatypes/epc/schema/tests/fixtures/21_0_1.json @@ -164,6 +164,13 @@ ], "open_chimneys_count": 1, "extract_fans_count": 2, + "blocked_chimneys_count": 1, + "open_flues_count": 1, + "closed_flues_count": 1, + "boilers_flues_count": 1, + "other_flues_count": 1, + "psv_count": 2, + "has_draught_lobby": "true", "solar_water_heating": "N", "habitable_room_count": 5, "heating_cost_current": 365.98, diff --git a/packages/domain/src/domain/sap/rdsap/tests/test_golden_fixtures.py b/packages/domain/src/domain/sap/rdsap/tests/test_golden_fixtures.py index 347b5850..8af56e93 100644 --- a/packages/domain/src/domain/sap/rdsap/tests/test_golden_fixtures.py +++ b/packages/domain/src/domain/sap/rdsap/tests/test_golden_fixtures.py @@ -96,17 +96,21 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = ( cert_number="0300-2747-7640-2526-2135", actual_sap=78, expected_sap_resid=-9, - expected_pe_resid_kwh_per_m2=+18.92, - expected_co2_resid_tonnes_per_yr=-0.4273, - notes="Large semi-detached, TFA 526, age D, gas boiler PCDB-listed (no Table 4b code).", + expected_pe_resid_kwh_per_m2=+17.3417, + expected_co2_resid_tonnes_per_yr=-0.5359, + notes=( + "Large semi-detached, TFA 526, age D, gas boiler PCDB-listed " + "(no Table 4b code). Cert lodges open_flues_count=1 + " + "has_draught_lobby=true." + ), ), _GoldenExpectation( cert_number="0390-2954-3640-2196-4175", actual_sap=60, expected_sap_resid=-7, - expected_pe_resid_kwh_per_m2=-25.62, - expected_co2_resid_tonnes_per_yr=-2.4491, - notes="Large detached, TFA 360, age F, oil PCDB-listed.", + expected_pe_resid_kwh_per_m2=-27.6371, + expected_co2_resid_tonnes_per_yr=-2.5816, + notes="Large detached, TFA 360, age F, oil PCDB-listed. Cert lodges has_draught_lobby=true.", ), _GoldenExpectation( cert_number="6035-7729-2309-0879-2296", @@ -128,9 +132,9 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = ( cert_number="8135-1728-8500-0511-3296", actual_sap=72, expected_sap_resid=+1, - expected_pe_resid_kwh_per_m2=-17.58, - expected_co2_resid_tonnes_per_yr=-0.2234, - notes="Semi-detached, TFA 102, age C, gas PCDB-listed.", + expected_pe_resid_kwh_per_m2=-14.3709, + expected_co2_resid_tonnes_per_yr=-0.1537, + notes="Semi-detached, TFA 102, age C, gas PCDB-listed. Cert lodges blocked_chimneys_count=1.", ), _GoldenExpectation( cert_number="2130-1033-4050-5007-8395", @@ -155,17 +159,19 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = ( _GoldenExpectation( cert_number="0390-2254-6420-2126-5561", actual_sap=65, - expected_sap_resid=+1, - expected_pe_resid_kwh_per_m2=-10.6249, - expected_co2_resid_tonnes_per_yr=-0.1059, + expected_sap_resid=0, + expected_pe_resid_kwh_per_m2=-3.1420, + expected_co2_resid_tonnes_per_yr=+0.0413, notes=( "End-terrace + 1 extension, TFA 80, gas combi PCDB index 18119, " "no PV, no secondary, postcode LN12 (PCDB Table 172 match). " - "Cleanest bread-and-butter cert in the cohort; the +1 SAP / -10.6 " - "PE residual is the post-sap_ventilation-fix floor under the " - "remaining mapper gaps (notably schema-21 doesn't carry " - "led_/cfl_fixed_lighting_bulbs_count for this cert, so the §5 " - "lighting efficacy falls back to defaults)." + "Cleanest bread-and-butter cert in the cohort and the first to " + "hit SAP = exact lodged value (post Slice 41 vent-completeness " + "sweep — cert lodges blocked_chimneys_count=2 which reduces " + "infiltration vs the pre-fix zero default). PE / CO2 residuals " + "are now small enough that the remaining drivers are likely " + "lighting efficacy (schema-21 doesn't carry led_/cfl bulb " + "counts for this cert) + boiler PCDB winter efficiency lookup." ), ), # Retired early at P2.2: 9390-2722-3520-2105-8715 (mid-floor flat,