mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice S0380.120: distinguish NI from explicit int(0) roof_insulation_thickness per RdSAP 10 §5.11.4
RdSAP 10 §5.11.4 (PDF p.44):
"If retrofit insulation present of unknown thickness use 50 mm."
The cascade encoded "unknown thickness" via the cert's "NI" (Not-
Indicated) sentinel which `_parse_thickness_mm` collapses to int(0).
But that conflates two structurally different signals:
(a) explicit int(0) — `_api_resolve_sloping_ceiling_thickness`
returns this for cert 001479 Ext2 PS sloping ceiling age C, a
per-BP "uninsulated" override of the dwelling-level description
("Pitched, insulated" from another BP).
(b) string "NI" — the cert lodgement marker for "thickness not
indicated; defer to description"; §5.11.4 should fire when the
description carries an "insulated" signal.
Pre-slice the heat_transmission cascade dropped `roof_description`
whenever `roof_thickness == 0`, killing the §5.11.4 path in `u_roof`
(line 711) for the (b) case. 346 corpus certs lodge the NI +
"insulated (assumed)" pattern per the §5.11.4 test's arrange comment.
Fix: inspect the raw `part.roof_insulation_thickness` value (pre-
parse) — drop the description only when the lodgement is the literal
int(0), keep it for the "NI" string sentinel so `u_roof`'s §5.11.4
branch fires (`_described_as_insulated` + thickness=0 → return 0.68).
Test movement:
test_roof_insulated_assumed_with_ni_thickness_uses_50mm_per_section_5_11_4 → PASS
test_summary_001479_full_chain_sap_matches_worksheet_pdf_exactly → PASS (cohort safe)
cert 000565 e2e — 11/11 PASS (unaffected — explicit per-BP thicknesses)
Golden corpus impact: cert 0240 had this exact pattern (BP[1] NI + global
description includes "Pitched, insulated (assumed)"). The fix drops its
roof U from 2.30 → 0.68 for that BP, closing massive mapper-gap residuals:
expected_sap_resid: -14 → -10 (Δ +4 SAP)
expected_pe_resid_kwh_per_m2: +12.49 → +0.054 (Δ −12.43 kWh/m²)
expected_co2_resid_tonnes_per_yr: +0.696 → +0.063 (Δ −0.633 t/yr)
Re-pinned per [[feedback-golden-residuals-near-zero]]: "Re-pin to the
new (smaller) value when a gap closes". The remaining 0240 residuals
(SAP -10 / PE +0.05 / CO2 +0.06) are tiny — the bulk of 0240's mapper
gap is now closed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
a77f1a284d
commit
f0305d5452
2 changed files with 25 additions and 12 deletions
|
|
@ -74,9 +74,9 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = (
|
|||
_GoldenExpectation(
|
||||
cert_number="0240-0200-5706-2365-8010",
|
||||
actual_sap=73,
|
||||
expected_sap_resid=-14,
|
||||
expected_pe_resid_kwh_per_m2=+12.4933,
|
||||
expected_co2_resid_tonnes_per_yr=+0.6957,
|
||||
expected_sap_resid=-10,
|
||||
expected_pe_resid_kwh_per_m2=+0.0542,
|
||||
expected_co2_resid_tonnes_per_yr=+0.0626,
|
||||
notes=(
|
||||
"Detached house, TFA 118, age J, oil boiler PCDB-listed + PV + "
|
||||
"RR on BP[0]. Mapper DOES extract sap_room_in_roof.room_in_roof_"
|
||||
|
|
|
|||
|
|
@ -636,7 +636,8 @@ def heat_transmission_from_cert(
|
|||
or _described_as_insulated(wall_description)
|
||||
)
|
||||
party_construction = _int_or_none(part.party_wall_construction)
|
||||
roof_thickness = _parse_thickness_mm(getattr(part, "roof_insulation_thickness", None))
|
||||
raw_roof_thickness = getattr(part, "roof_insulation_thickness", None)
|
||||
roof_thickness = _parse_thickness_mm(raw_roof_thickness)
|
||||
floor_ins_thickness = _parse_thickness_mm(getattr(part, "floor_insulation_thickness", None))
|
||||
|
||||
ground_fd = next(
|
||||
|
|
@ -676,15 +677,27 @@ def heat_transmission_from_cert(
|
|||
)
|
||||
# When the per-bp `roof_insulation_thickness` is explicitly lodged
|
||||
# as 0 (uninsulated — e.g. cert 001479 Ext2 PS sloping ceiling
|
||||
# age C from Slice 91's `_api_resolve_sloping_ceiling_thickness`)
|
||||
# the global `epc.roofs[].description` ("Pitched, insulated" from
|
||||
# another bp) must NOT override the per-bp truth via u_roof's
|
||||
# Table 18 footnote (2) assumed-insulation path. Drop the
|
||||
# description in that case so the cascade returns the spec
|
||||
# uninsulated U-value (Table 18 row 0). Cohort Summary mappers
|
||||
# leave `epc.roofs` empty so description is None there anyway.
|
||||
# age C from Slice 91's `_api_resolve_sloping_ceiling_thickness`,
|
||||
# which returns the int 0 sentinel) the global
|
||||
# `epc.roofs[].description` ("Pitched, insulated" from another bp)
|
||||
# must NOT override the per-bp truth via u_roof's Table 18 footnote
|
||||
# (2) assumed-insulation path. Drop the description in that case so
|
||||
# the cascade returns the spec uninsulated U-value (Table 18 row 0).
|
||||
# Cohort Summary mappers leave `epc.roofs` empty so description is
|
||||
# None there anyway.
|
||||
#
|
||||
# The "NI" string sentinel is the OPPOSITE signal — it means
|
||||
# "thickness not indicated; defer to description" per RdSAP 10
|
||||
# §5.11.4 (PDF p.44): "If retrofit insulation present of unknown
|
||||
# thickness use 50 mm". `_parse_thickness_mm` collapses BOTH int(0)
|
||||
# and "NI" to 0, so we distinguish by inspecting the RAW lodgement
|
||||
# value before the parse — explicit `int(0)` drops the description,
|
||||
# `"NI"` keeps it so `u_roof`'s §5.11.4 branch can fire.
|
||||
roof_thickness_explicitly_zero = (
|
||||
isinstance(raw_roof_thickness, int) and raw_roof_thickness == 0
|
||||
)
|
||||
effective_roof_description = (
|
||||
None if roof_thickness == 0 else roof_description
|
||||
None if roof_thickness_explicitly_zero else roof_description
|
||||
)
|
||||
# RdSAP 10 §5.11 Table 18 page 45: column (3) "Flat roof" applies
|
||||
# when the per-bp roof construction lodges as a flat roof and the
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue