mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice 41: schema-21.0.1 ventilation completeness — 7 vent / draught fields plumbed
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 <noreply@anthropic.com>
This commit is contained in:
parent
fb3973457a
commit
81392208c4
5 changed files with 76 additions and 18 deletions
|
|
@ -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
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue