mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
S0380.214: as-built sloping-ceiling roof → Table 18 col (3)
A "Pitched, sloping ceiling" roof (roof_construction code 8) lodged with
"As Built" insulation (no measured thickness → None) was wrongly routed to
RdSAP 10 Table 18 column (1) "insulation between joists or unknown". A
sloping ceiling has no joist void, so per RdSAP 10 §5.11 roof-input item
5-5 ("Sloping ceiling insulation … unknown / as built → Table 18") and
Table 18 note (b) ("Applies also to roof with sloping ceiling") it takes
column (3) — band F = 0.68, band L = 0.18 (vs col 1 0.40 / 0.16).
Discriminator is the code-8 "sloping ceiling" string only: code-5 vaulted
ceilings stay on column (1) per the 33 cohort-2 "ND" vaulted certs
(S0380.211), and the "NI"/"ND" unknown case is untouched. New
`is_pitched_sloping_ceiling` flag threaded from heat_transmission to
`u_roof`; pre-1950 bands already reach the same col (3) value (2.30) via
the mapper's thickness=0 → Table 16 row-0 override, so the new branch
carries the post-1950 bands where col 1 ≠ col 3.
Worksheet-validated by simulated case 15 (the 7536 replica): our cascade
on its Summary matches the P960 worksheet exactly — roof HLC 29.17 W/K,
cont SAP 65.04 vs 65. Re-pins golden cert 7536: roof 26.77 → 29.17, cont
SAP 69.071 → 68.924, PE -7.0776 → -6.1952, CO2 -0.1875 → -0.1639 (SAP
integer 68, resid +1 unchanged — the remaining +0.92 is a diffuse demand
under-count needing a fully-faithful worksheet). Blast radius: 7536 only.
Suite: 2388 passed, 1 skipped (main); sap10_ml 233 passed + 2 pre-existing
stone-formula failures (out of scope). Zero new pyright errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ec64c39d74
commit
ac7f510ccb
4 changed files with 108 additions and 7 deletions
|
|
@ -791,7 +791,13 @@ def heat_transmission_from_cert(
|
|||
is_sloping_ceiling = (
|
||||
"sloping ceiling" in roof_type_lower or "vaulted" in roof_type_lower
|
||||
)
|
||||
ur = u_roof(country=country, age_band=age_band, insulation_thickness_mm=roof_thickness, description=effective_roof_description, is_flat_roof=is_flat_roof, is_sloping_ceiling=is_sloping_ceiling)
|
||||
# RdSAP 10 Table 18 col (3) routing for an AS-BUILT "Pitched,
|
||||
# sloping ceiling" (code 8). Narrower than `is_sloping_ceiling`
|
||||
# (which also covers code-5 vaulted): vaulted ceilings stay on
|
||||
# col (1) per the cohort, so only the literal "sloping ceiling"
|
||||
# string triggers the col (3) age-band default in `u_roof`.
|
||||
is_pitched_sloping_ceiling = "sloping ceiling" in roof_type_lower
|
||||
ur = u_roof(country=country, age_band=age_band, insulation_thickness_mm=roof_thickness, description=effective_roof_description, is_flat_roof=is_flat_roof, is_sloping_ceiling=is_sloping_ceiling, is_pitched_sloping_ceiling=is_pitched_sloping_ceiling)
|
||||
# Floor U-value routing (in priority order):
|
||||
# 1. Basement floor — Table 23 F-column override (whole floor=0).
|
||||
# 2. Exposed/semi-exposed upper floor — Table 20 lookup; no
|
||||
|
|
|
|||
|
|
@ -721,6 +721,7 @@ def u_roof(
|
|||
description: Optional[str] = None,
|
||||
is_flat_roof: bool = False,
|
||||
is_sloping_ceiling: bool = False,
|
||||
is_pitched_sloping_ceiling: bool = False,
|
||||
) -> float:
|
||||
"""RdSAP10 roof U-value in W/m^2K, never null.
|
||||
|
||||
|
|
@ -745,6 +746,18 @@ def u_roof(
|
|||
default (band J = 0.16) — the same value a vaulted roof lodged "ND"
|
||||
(thickness None) already reaches by falling through. The 33 cohort-2
|
||||
"ND" vaulted certs (code 5, band D → 0.40 = col 1) are the evidence.
|
||||
|
||||
`is_pitched_sloping_ceiling` is the narrower code-8 ("Pitched, sloping
|
||||
ceiling") signal for the AS-BUILT case (insulation lodged "As Built",
|
||||
parsed to thickness None — distinct from the "NI"/"ND" unknown case
|
||||
above). Per RdSAP 10 roof-input item 5-5 ("Sloping ceiling insulation
|
||||
... as built → Table 18") and Table 18 note (b) ("applies also to roof
|
||||
with sloping ceiling"), an as-built sloping ceiling takes the column
|
||||
(3) age-band default (band F = 0.68, band L = 0.18), NOT the column (1)
|
||||
loft-joist default (band F = 0.40, band L = 0.16). Vaulted ceilings
|
||||
(code 5) are deliberately excluded — they stay on column (1) per the
|
||||
cohort evidence above. Worksheet-validated by simulated case 15 (the
|
||||
7536 replica): Ext1 band L → 0.18, Ext2 band F → 0.68.
|
||||
"""
|
||||
measured = _measured_u_from_description(description)
|
||||
if measured is not None:
|
||||
|
|
@ -789,6 +802,15 @@ def u_roof(
|
|||
return _ROOF_BY_THICKNESS[1][1] # 1.50 W/m^2K (12mm row)
|
||||
if age_band is None:
|
||||
return 0.4
|
||||
if is_pitched_sloping_ceiling:
|
||||
# RdSAP 10 §5.11 Table 18 page 45 column (3) + roof-input item 5-5:
|
||||
# an as-built "Pitched, sloping ceiling" (code 8) with no measured
|
||||
# thickness takes the column (3) age-band default, not the column
|
||||
# (1) loft-joist default. Note (b): column (3) "applies also to
|
||||
# roof with sloping ceiling". (Pre-1950 bands reach the same value
|
||||
# via the mapper's thickness=0 → Table 16 row-0 2.30 override, so
|
||||
# this branch carries the post-1950 bands where col 1 ≠ col 3.)
|
||||
return _FLAT_ROOF_BY_AGE.get(age_band.upper(), 0.4)
|
||||
if is_flat_roof:
|
||||
return _FLAT_ROOF_BY_AGE.get(age_band.upper(), 0.4)
|
||||
return _ROOF_BY_AGE.get(age_band.upper(), 0.4)
|
||||
|
|
|
|||
|
|
@ -945,6 +945,66 @@ def test_u_roof_flat_age_band_l_returns_table18_col3_value() -> None:
|
|||
assert abs(result - 0.18) <= 1e-4
|
||||
|
||||
|
||||
def test_u_roof_pitched_sloping_ceiling_as_built_band_f_uses_col3() -> None:
|
||||
# Arrange — RdSAP 10 §5.11 Table 18 page 45 + roof-input item 5-5
|
||||
# ("Sloping ceiling insulation ... unknown / as built → Table 18").
|
||||
# A "Pitched, sloping ceiling" roof (roof_construction code 8) with an
|
||||
# "As Built" insulation lodgement (no measured thickness → None) takes
|
||||
# the Table 18 column (3) age-band default, NOT the column (1)
|
||||
# "insulation between joists" default. Note (b) on column (3) states it
|
||||
# "applies also to roof with sloping ceiling". For age band F the
|
||||
# column (3) value is 0.68 W/m²K (vs column (1) 0.40 — the loft-joist
|
||||
# assumption that is wrong for a sloping ceiling with no joist void).
|
||||
#
|
||||
# Worksheet-validated: simulated case 15 (7536 replica) lodges Ext2 as
|
||||
# band F "PS Pitched, sloping ceiling, As Built"; its P960 worksheet
|
||||
# pins `External roof Ext2 … 0.68`, and the full-cascade roof HLC and
|
||||
# SAP match Elmhurst exactly only with column (3).
|
||||
|
||||
# Act
|
||||
result = u_roof(
|
||||
country=Country.ENG, age_band="F", insulation_thickness_mm=None,
|
||||
is_pitched_sloping_ceiling=True,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert abs(result - 0.68) <= 1e-4
|
||||
|
||||
|
||||
def test_u_roof_pitched_sloping_ceiling_as_built_band_l_uses_col3() -> None:
|
||||
# Arrange — same rule at band L (2012-2022): Table 18 column (3) gives
|
||||
# 0.18 W/m²K, where columns (2)/(3) coincide. Simulated case 15's Ext1
|
||||
# (band L PS sloping ceiling, As Built) pins worksheet U=0.18 (vs the
|
||||
# column (1) value 0.16 the cascade returned pre-fix).
|
||||
|
||||
# Act
|
||||
result = u_roof(
|
||||
country=Country.ENG, age_band="L", insulation_thickness_mm=None,
|
||||
is_pitched_sloping_ceiling=True,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert abs(result - 0.18) <= 1e-4
|
||||
|
||||
|
||||
def test_u_roof_vaulted_nd_unknown_band_d_still_col1_not_col3() -> None:
|
||||
# Arrange — regression guard for the discriminator: a code-5 "vaulted"
|
||||
# roof lodged "ND" (thickness None) is the UNKNOWN-insulation case and
|
||||
# must stay on Table 18 column (1) — band D = 0.40 — per the 33
|
||||
# cohort-2 vaulted certs (S0380.211). The col (3) routing fires only
|
||||
# for code-8 "Pitched, sloping ceiling" (is_pitched_sloping_ceiling),
|
||||
# NOT for vaulted ceilings, so this defaults False here and resolves
|
||||
# to column (1) 0.40, NOT column (3) 2.30.
|
||||
|
||||
# Act
|
||||
result = u_roof(
|
||||
country=Country.ENG, age_band="D", insulation_thickness_mm=None,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert abs(result - 0.40) <= 1e-4
|
||||
|
||||
|
||||
def test_u_roof_description_no_insulation_overrides_age_band_default() -> None:
|
||||
# Arrange — surveyor description on a Victorian roof says uninsulated;
|
||||
# Table 18 age-B default (0.40) is far too optimistic. Table 16 row 0mm
|
||||
|
|
|
|||
|
|
@ -370,8 +370,8 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = (
|
|||
cert_number="7536-3827-0600-0600-0276",
|
||||
actual_sap=68,
|
||||
expected_sap_resid=+1,
|
||||
expected_pe_resid_kwh_per_m2=-7.0776,
|
||||
expected_co2_resid_tonnes_per_yr=-0.1875,
|
||||
expected_pe_resid_kwh_per_m2=-6.1952,
|
||||
expected_co2_resid_tonnes_per_yr=-0.1639,
|
||||
notes=(
|
||||
"Detached + 2 extensions, TFA 152. Multi-age bps (Main=D, "
|
||||
"Ext1=L, Ext2=F). Slice 59 (per-bp window apportionment) and "
|
||||
|
|
@ -379,10 +379,23 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = (
|
|||
"age band, not per-bp) jointly tightened: SAP +4 → +3, PE "
|
||||
"-27.17 → -22.53, CO2 -0.72 → -0.60. Slice 97 added "
|
||||
"glazing_type=2 (Table 24 spec U=2.0): SAP 0 → +1, PE/CO2 "
|
||||
"widened. The cert's actual lodged U for glazing_type=2 "
|
||||
"appears higher than the spec's table default — multi-age "
|
||||
"geometry probably surfaces a per-bp U-value the spec table "
|
||||
"doesn't capture exactly."
|
||||
"widened. Slice S0380.214 fixed the as-built sloping-ceiling "
|
||||
"roof U: Ext1 (band L) and Ext2 (band F) lodge "
|
||||
"roof_construction=8 'Pitched, sloping ceiling' + 'As Built', "
|
||||
"which take RdSAP 10 Table 18 col (3) (L=0.18, F=0.68) not "
|
||||
"col (1) (0.16/0.40) — per item 5-5 + note (b). Roof HLC "
|
||||
"26.77 → 29.17 W/K; cont SAP 69.071 → 68.924, PE -7.0776 → "
|
||||
"-6.1952, CO2 -0.1875 → -0.1639 (SAP integer still 68 vs "
|
||||
"lodged → resid +1). Worksheet-validated by simulated case 15 "
|
||||
"(the 7536 replica): our cascade on its Summary matches the "
|
||||
"P960 worksheet exactly (roof 29.17, SAP 65.04 vs 65). The "
|
||||
"glazing hypothesis from the prior handover was wrong — maxing "
|
||||
"the glazing U past spec can't flip 69→68, and every per-bp "
|
||||
"fabric U is spec-plausible. The residual +0.92 cont SAP is a "
|
||||
"diffuse demand under-count (window split + Main suspended-"
|
||||
"timber floor) not capturable from the API-only JSON; needs a "
|
||||
"fully-faithful 7536 worksheet (in progress) to localise or "
|
||||
"conclude it is 0240-like."
|
||||
),
|
||||
),
|
||||
_GoldenExpectation(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue