mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice 94: API mapper sheltered_sides + floor_type — cert 001479 to 1e-3
Two API mapper gaps surfacing the cert 001479 +1.18 SAP gap post Slice 93: (1) `SapVentilation.sheltered_sides` from API `built_form` The API schema doesn't lodge sheltered_sides as a discrete field — it's derived per RdSAP §S5 from the dwelling's built_form. The cascade defaults to 2 when missing (right for Mid-Terrace) but wrong for detached/semi/end-terrace. Cert 001479 (built_form=2 Semi- Detached) needs 1 sheltered side; default 2 over-counted shelter factor → line (21) under by 0.185 → ventilation under by ~2 ACH/yr. New `_api_sheltered_sides` translator + `_API_BUILT_FORM_TO_ SHELTERED_SIDES` table (1=Detached/0, 2=Semi/1, 3=End-T/1, 4=Mid-T/2, 5=Encl-End/2, 6=Encl-Mid/3) — mirrors the cohort Elmhurst `_ELMHURST_SHELTERED_SIDES_BY_BUILT_FORM` keyed by the API integer enum. (2) `SapBuildingPart.floor_type` from API `floor_heat_loss` The Slice 87 spec rule for §2(12) suspended-timber-floor infiltration (`_has_suspended_timber_floor_per_spec` in cert_to_inputs) requires the Main bp's lowest floor to have `floor_type == "Ground floor"` to apply the (12)=0.2/0.1 rule. The API mapper wasn't surfacing this string (only floor_construction_type), so the spec rule short- circuited to False even for genuine ground floors and the cascade's line (12) was 0.0 instead of 0.2. New `_api_floor_type_str` translator + `_API_FLOOR_HEAT_LOSS_TO_ FLOOR_TYPE` table (1="To external air" for cantilevered exposed floors, 7="Ground floor"). Routes correctly for cert 001479: Main + Ext1 carry floor_heat_loss=7 → both Ground floor; Ext2 carries floor_heat_loss=1 → exposed (its is_exposed_floor=True already lifts the floor U cascade to Table 20). **Result on cert 001479 API path:** SAP delta: +1.18 → +0.0006 (essentially exact match at integer SAP) Cascade SAP=69.0100 vs worksheet 69.0094 — within 1e-3 of target. The remaining ~0.001 SAP gap is dominated by: - hot_water_kwh_per_yr: +6.7 (API 2365.0 vs target 2358.3) - internal_gains Σ: +25.7 W·months (subtle gain-cascade differences) - solar_gains Σ: +1.5 W·months Sub-1e-3 SAP impact each; would need slice-by-slice diagnosis to close to the strict 1e-4 bar. Layer 3 API-mapper-vs-Summary-mapper EpcPropertyData equivalence: the API path now produces SAP within 0.001 of the Summary path (Summary Layer 2 = 69.0094 EXACT). API integer SAP = 69 = worksheet integer SAP = 69 ✓ — matches the API's published energy_rating_ current=69 (zero residual on the production goal metric). Golden cert residuals: 8 of 10 expectations shifted by Slices 90-94 cascade improvements. Spec-compliance shifts; new residuals pinned. Pyright: mapper.py 33 → 33. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
7281b7b300
commit
0320341837
2 changed files with 86 additions and 23 deletions
|
|
@ -1115,6 +1115,7 @@ class EpcPropertyDataMapper:
|
|||
# "Suspended"/"Solid" branch selection) and Slice
|
||||
# 89 (`roof_construction_type` containing "sloping
|
||||
# ceiling" → cos(30°) inclined-surface area).
|
||||
floor_type=_api_floor_type_str(bp.floor_heat_loss),
|
||||
floor_construction_type=_api_floor_construction_str(
|
||||
bp.sap_floor_dimensions[0].floor_construction
|
||||
if bp.sap_floor_dimensions else None
|
||||
|
|
@ -1328,6 +1329,7 @@ class EpcPropertyDataMapper:
|
|||
# "Suspended"/"Solid" branch selection) and Slice
|
||||
# 89 (`roof_construction_type` containing "sloping
|
||||
# ceiling" → cos(30°) inclined-surface area).
|
||||
floor_type=_api_floor_type_str(bp.floor_heat_loss),
|
||||
floor_construction_type=_api_floor_construction_str(
|
||||
bp.sap_floor_dimensions[0].floor_construction
|
||||
if bp.sap_floor_dimensions else None
|
||||
|
|
@ -1632,6 +1634,7 @@ class EpcPropertyDataMapper:
|
|||
# "Suspended"/"Solid" branch selection) and Slice
|
||||
# 89 (`roof_construction_type` containing "sloping
|
||||
# ceiling" → cos(30°) inclined-surface area).
|
||||
floor_type=_api_floor_type_str(bp.floor_heat_loss),
|
||||
floor_construction_type=_api_floor_construction_str(
|
||||
bp.sap_floor_dimensions[0].floor_construction
|
||||
if bp.sap_floor_dimensions else None
|
||||
|
|
@ -1793,6 +1796,15 @@ class EpcPropertyDataMapper:
|
|||
if schema.has_draught_lobby is not None
|
||||
else None
|
||||
),
|
||||
# `sheltered_sides` is derived per RdSAP §S5 from the
|
||||
# dwelling's built_form rather than lodged explicitly
|
||||
# in the API schema — the cascade defaults to 2 when
|
||||
# missing, which is right for mid-terrace but wrong
|
||||
# for detached/semi/end-terrace (cert 001479: built_
|
||||
# form=2 Semi-Detached → 1 sheltered side, cascade
|
||||
# default 2 → over-counted shelter factor → -2.42
|
||||
# ACH/month infiltration shortfall).
|
||||
sheltered_sides=_api_sheltered_sides(schema.built_form),
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -2075,6 +2087,30 @@ def _api_floor_construction_str(value: Optional[int]) -> Optional[str]:
|
|||
return _API_FLOOR_CONSTRUCTION_TO_STR.get(value) if value is not None else None
|
||||
|
||||
|
||||
# GOV.UK API `floor_heat_loss` integer → `floor_type` string the
|
||||
# RdSAP 10 §5 (12) spec rule reads in `_has_suspended_timber_floor_per
|
||||
# _spec` (cert_to_inputs.py). The spec applies (12)=0.2/0.1 only when
|
||||
# the Main bp's lowest floor is a "Ground floor" with "Suspended
|
||||
# timber" construction, so the API mapper has to surface "Ground
|
||||
# floor" as the floor_type — otherwise the spec rule short-circuits
|
||||
# to False even when the cert is genuinely G+T (e.g. cert 001479
|
||||
# Main, floor_heat_loss=7).
|
||||
_API_FLOOR_HEAT_LOSS_TO_FLOOR_TYPE: Dict[int, str] = {
|
||||
1: "To external air", # exposed (cantilevered / over passageway)
|
||||
7: "Ground floor",
|
||||
}
|
||||
|
||||
|
||||
def _api_floor_type_str(floor_heat_loss: Optional[int]) -> Optional[str]:
|
||||
"""Translate the API integer floor_heat_loss code to the
|
||||
human-readable floor_type the RdSAP 10 §5 (12) spec rule consumes
|
||||
(see `_has_suspended_timber_floor_per_spec`)."""
|
||||
return (
|
||||
_API_FLOOR_HEAT_LOSS_TO_FLOOR_TYPE.get(floor_heat_loss)
|
||||
if floor_heat_loss is not None else None
|
||||
)
|
||||
|
||||
|
||||
def _api_roof_construction_str(value: Optional[int]) -> Optional[str]:
|
||||
"""Translate the API integer roof_construction code to the
|
||||
human-readable string the cascade reads via Slice 89's
|
||||
|
|
@ -2092,6 +2128,33 @@ def _api_roof_construction_str(value: Optional[int]) -> Optional[str]:
|
|||
_API_FLOOR_HEAT_LOSS_EXPOSED: Final[int] = 1
|
||||
|
||||
|
||||
# GOV.UK API `built_form` integer → SAP10.2 sheltered_sides count per
|
||||
# RdSAP §S5. Detached has no neighbours shielding wind; terraced
|
||||
# variants pick up 1-3 sheltered sides via adjacent dwellings. Cross-
|
||||
# checked against the cohort built_form enum at line 3003 (Elmhurst's
|
||||
# string lookup) which itself matches U985 worksheet line (19) for the
|
||||
# 6 cohort fixtures.
|
||||
_API_BUILT_FORM_TO_SHELTERED_SIDES: Dict[int, int] = {
|
||||
1: 0, # Detached
|
||||
2: 1, # Semi-Detached
|
||||
3: 1, # End-Terrace
|
||||
4: 2, # Mid-Terrace
|
||||
5: 2, # Enclosed End-Terrace
|
||||
6: 3, # Enclosed Mid-Terrace
|
||||
}
|
||||
|
||||
|
||||
def _api_sheltered_sides(built_form: object) -> Optional[int]:
|
||||
"""Translate the API `built_form` integer code to the SAP10.2 §S5
|
||||
sheltered-sides count. Returns None when the form isn't recognised
|
||||
so the cascade applies its own default (currently 2)."""
|
||||
if isinstance(built_form, str) and built_form.isdigit():
|
||||
built_form = int(built_form)
|
||||
if not isinstance(built_form, int):
|
||||
return None
|
||||
return _API_BUILT_FORM_TO_SHELTERED_SIDES.get(built_form)
|
||||
|
||||
|
||||
# GOV.UK API `glazing_type` integer → (u_value W/m²K, g_perpendicular,
|
||||
# frame_factor) lookup the cascade reads via `window_transmission_
|
||||
# details` for per-window cascade fidelity. The cascade defaults to a
|
||||
|
|
|
|||
|
|
@ -74,9 +74,9 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = (
|
|||
_GoldenExpectation(
|
||||
cert_number="0240-0200-5706-2365-8010",
|
||||
actual_sap=73,
|
||||
expected_sap_resid=-13,
|
||||
expected_pe_resid_kwh_per_m2=+10.4527,
|
||||
expected_co2_resid_tonnes_per_yr=+0.5916,
|
||||
expected_sap_resid=-14,
|
||||
expected_pe_resid_kwh_per_m2=+14.6650,
|
||||
expected_co2_resid_tonnes_per_yr=+0.8060,
|
||||
notes=(
|
||||
"Detached house, TFA 202, age J, oil boiler, Table 4b code 130. "
|
||||
"API response lodges sap_room_in_roof.room_in_roof_type_1 with "
|
||||
|
|
@ -94,9 +94,9 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = (
|
|||
_GoldenExpectation(
|
||||
cert_number="0300-2747-7640-2526-2135",
|
||||
actual_sap=78,
|
||||
expected_sap_resid=+2,
|
||||
expected_pe_resid_kwh_per_m2=-0.2955,
|
||||
expected_co2_resid_tonnes_per_yr=-0.9443,
|
||||
expected_sap_resid=+1,
|
||||
expected_pe_resid_kwh_per_m2=+1.0093,
|
||||
expected_co2_resid_tonnes_per_yr=-0.8321,
|
||||
notes=(
|
||||
"Large semi-detached, TFA 526, age D, gas boiler PCDB-listed "
|
||||
"(no Table 4b code). Cert lodges open_flues_count=1 + "
|
||||
|
|
@ -110,17 +110,17 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = (
|
|||
_GoldenExpectation(
|
||||
cert_number="0390-2954-3640-2196-4175",
|
||||
actual_sap=60,
|
||||
expected_sap_resid=-5,
|
||||
expected_pe_resid_kwh_per_m2=-28.4884,
|
||||
expected_co2_resid_tonnes_per_yr=-2.7466,
|
||||
expected_sap_resid=-6,
|
||||
expected_pe_resid_kwh_per_m2=-26.4584,
|
||||
expected_co2_resid_tonnes_per_yr=-2.5618,
|
||||
notes="Large detached, TFA 360, age F, oil PCDB-listed. Cert lodges has_draught_lobby=true.",
|
||||
),
|
||||
_GoldenExpectation(
|
||||
cert_number="6035-7729-2309-0879-2296",
|
||||
actual_sap=70,
|
||||
expected_sap_resid=-5,
|
||||
expected_pe_resid_kwh_per_m2=+39.1452,
|
||||
expected_co2_resid_tonnes_per_yr=+0.8845,
|
||||
expected_sap_resid=-6,
|
||||
expected_pe_resid_kwh_per_m2=+48.2971,
|
||||
expected_co2_resid_tonnes_per_yr=+1.1016,
|
||||
notes=(
|
||||
"Mid-terrace, TFA 128, age A, gas combi Table 4b code 104. "
|
||||
"Slice 59 per-bp window apportionment tightens all 3 "
|
||||
|
|
@ -132,9 +132,9 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = (
|
|||
_GoldenExpectation(
|
||||
cert_number="7536-3827-0600-0600-0276",
|
||||
actual_sap=68,
|
||||
expected_sap_resid=+1,
|
||||
expected_pe_resid_kwh_per_m2=-8.8124,
|
||||
expected_co2_resid_tonnes_per_yr=-0.2337,
|
||||
expected_sap_resid=+0,
|
||||
expected_pe_resid_kwh_per_m2=-3.4482,
|
||||
expected_co2_resid_tonnes_per_yr=-0.0907,
|
||||
notes=(
|
||||
"Detached + 2 extensions, TFA 152. Multi-age bps (Main=D, "
|
||||
"Ext1=L, Ext2=F). Slice 59 (per-bp window apportionment) and "
|
||||
|
|
@ -146,9 +146,9 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = (
|
|||
_GoldenExpectation(
|
||||
cert_number="8135-1728-8500-0511-3296",
|
||||
actual_sap=72,
|
||||
expected_sap_resid=+1,
|
||||
expected_pe_resid_kwh_per_m2=-10.0737,
|
||||
expected_co2_resid_tonnes_per_yr=-0.1645,
|
||||
expected_sap_resid=+0,
|
||||
expected_pe_resid_kwh_per_m2=-2.4194,
|
||||
expected_co2_resid_tonnes_per_yr=-0.0198,
|
||||
notes=(
|
||||
"Semi-detached, TFA 102, age C, gas PCDB-listed. Cert lodges "
|
||||
"blocked_chimneys_count=1. Slice 59 per-bp window apportionment "
|
||||
|
|
@ -159,9 +159,9 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = (
|
|||
_GoldenExpectation(
|
||||
cert_number="2130-1033-4050-5007-8395",
|
||||
actual_sap=82,
|
||||
expected_sap_resid=+2,
|
||||
expected_pe_resid_kwh_per_m2=-43.5103,
|
||||
expected_co2_resid_tonnes_per_yr=+0.2414,
|
||||
expected_sap_resid=+1,
|
||||
expected_pe_resid_kwh_per_m2=-38.1521,
|
||||
expected_co2_resid_tonnes_per_yr=+0.3047,
|
||||
notes=(
|
||||
"End-terrace + 1 extension, TFA 64, gas combi PCDB index 17505, "
|
||||
"postcode DE22 (PCDB Table 172 match), PV: 2× 2.04 kWp arrays "
|
||||
|
|
@ -180,8 +180,8 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = (
|
|||
cert_number="0390-2254-6420-2126-5561",
|
||||
actual_sap=65,
|
||||
expected_sap_resid=+0,
|
||||
expected_pe_resid_kwh_per_m2=-1.9117,
|
||||
expected_co2_resid_tonnes_per_yr=+0.0102,
|
||||
expected_pe_resid_kwh_per_m2=+1.6837,
|
||||
expected_co2_resid_tonnes_per_yr=+0.0637,
|
||||
notes=(
|
||||
"End-terrace + 1 extension, TFA 80, gas combi PCDB index 18119, "
|
||||
"no PV, no secondary, postcode LN12 (PCDB Table 172 match). "
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue