fix(uvalues): bill known-insulation stone walls by the §5.6 thickness formula, uncapped (RdSAP 10 §5.6)

An uninsulated stone wall of lodged thickness, age bands A-E, is billed by
the RdSAP 10 §5.6 Table-12 formula on its measured thickness — sandstone /
limestone U = 54.876·W^-0.561, granite / whinstone U = 45.315·W^-0.513. Two
bugs suppressed it:

1. CAP: the as-built branch capped the formula result at the Table-6
   typical-thickness default (`if u0 >= 1.7: return 1.7`). But the formula
   only dips below 1.7 past ~488 mm (sandstone) / ~640 mm (granite), so the
   cap nullified §5.6 for essentially every real-thickness stone wall,
   under-counting fabric loss and over-rating. A measured 400 mm sandstone
   age-B wall is 1.90 (Elmhurst-confirmed), not 1.70.

2. NO INSULATION-STATE GATE: the branch fired for any stone wall with a
   lodged thickness, including ones whose insulation is "Unknown". RdSAP
   treats an unknown-insulation wall via the Table-6 typical-thickness
   default, NOT as bare stone of the lodged thickness — so a 50 mm granite
   wall with Unknown insulation must read 1.70 (worksheet), not the formula's
   6.09. Gated on `wall_insulation_type is not None`.

Fixes the 2 long-standing stone-U unit tests (granite 120 mm → 3.8871,
sandstone 120 mm → 3.7408 — they were correct red tests, not "known fails").
Corpus within-0.5 70.3% -> 71.6% (MAE 0.833 -> 0.822); ratcheted floors to
0.71 / 0.83. Worksheet fixture 000565 (granite 50 mm Unknown → 1.70) still
passes via the insulation gate. The two stone groups (sandstone/limestone vs
granite/whinstone) keep their distinct §5.6 coefficients.

pyright not installed in this codespace (strict gate not run locally).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-19 14:32:31 +00:00
parent fc5f10ea92
commit 034d4b7cd5
2 changed files with 41 additions and 19 deletions

View file

@ -567,23 +567,25 @@ def u_wall(
ctry = country if country is not None else Country.ENG ctry = country if country is not None else Country.ENG
age_idx = _age_index(age_band) age_idx = _age_index(age_band)
band = _AGE_BANDS[age_idx] band = _AGE_BANDS[age_idx]
# RdSAP 10 §5.6 (PDF p.40) — uninsulated stone wall thin-wall # RdSAP 10 §5.6 (PDF p.40) — uninsulated stone wall formula, age
# formula, age bands A-E. Fires only when a documentary wall # bands A-E. Fires only when (a) a documentary wall thickness is lodged
# thickness is lodged (per §5.3 documentary-evidence rule). # (per §5.3 documentary-evidence rule) AND (b) the insulation STATE is
# §5.8 + Table 14 dry-line adjustment applies on top. # known (`wall_insulation_type` not None — As Built / external / internal).
# # When either is absent the cascade falls through to the Table-6
# Table 6 footnote (a) (PDF p.34): "Or from equations in 5.6 if # typical-thickness default (1.7) below: an "insulation Unknown" lodgement
# the calculated U-value is less than 1.7." The cap applies only # is NOT treated as bare stone of the lodged thickness (cert 000565 Ext1:
# to the AS-BUILT (no insulation, no dry-line) Table 6 row — for # granite 50 mm + insulation Unknown → Table-6 1.70 in the worksheet, NOT
# thin walls where §5.6 gives U ≥ 1.7 (e.g. granite at W=50 mm # the §5.6 formula's 6.09), and the footnote (a) "use §5.6 if the
# yields 6.09 → use Table 6 default 1.7 instead). When the wall # calculated U-value is less than 1.7" clause governs the unknown path. When the thickness IS
# is dry-lined or insulated, the raw §5.6 result feeds the §5.8 # lodged the raw §5.6 U is the spec target — it is NOT capped at 1.7,
# chain as the input U₀ — the Table 6 footnote doesn't cap that # because a thin/standard solid stone wall genuinely loses more than the
# path (verified empirically against cert 000565 Main alt_wall_1: # typical-thickness default (sandstone 400 mm → 1.90, granite 120 mm →
# granite W=120 mm dry-lined → U₀=3.88 raw + dry-line → 2.34 # 3.89). §5.8 + Table 14 insulation / dry-line adjustments apply on top
# matches worksheet, NOT 1.7 + dry-line → 1.32). # of the raw §5.6 U₀ (cert 000565 Main alt_wall_1: granite W=120 mm
# dry-lined → U₀=3.88 + dry-line → 2.34, matches worksheet).
if ( if (
wall_thickness_mm is not None wall_thickness_mm is not None
and wall_insulation_type is not None
and band in _STONE_AGE_A_TO_E and band in _STONE_AGE_A_TO_E
and construction in (WALL_STONE_GRANITE, WALL_STONE_SANDSTONE) and construction in (WALL_STONE_GRANITE, WALL_STONE_SANDSTONE)
): ):
@ -628,8 +630,15 @@ def u_wall(
Decimal("0.01"), rounding=ROUND_HALF_UP Decimal("0.01"), rounding=ROUND_HALF_UP
) )
) )
if u0 >= 1.7: # As-built (uninsulated, not dry-lined) stone wall of KNOWN
return 1.7 # Table-6 row cap per footnote (a) # thickness, age A-E: return the raw §5.6 result. The Table-6
# footnote (a) "< 1.7" clause governs the UNKNOWN-thickness path
# (which falls through to the Table-6 typical 1.7 default below) —
# NOT a documentary-thickness lodgement. A thin solid stone wall
# genuinely has U > 1.7 (e.g. sandstone 400 mm = 1.90, granite
# 120 mm = 3.89); capping it to 1.7 under-counts fabric loss and
# over-rates. Confirmed against Elmhurst (age-B sandstone 400 mm
# → 1.9) and the §5.6 Table-12 formula tests.
return u0 return u0
known_types = { known_types = {
WALL_STONE_GRANITE, WALL_STONE_SANDSTONE, WALL_SOLID_BRICK, WALL_CAVITY, WALL_STONE_GRANITE, WALL_STONE_SANDSTONE, WALL_SOLID_BRICK, WALL_CAVITY,

View file

@ -150,14 +150,27 @@ _CORPUS = Path(
# MAE 0.12 -> 0.08 t/yr (bias +0.04 -> 0.00). A prior session deferred enum 9 # MAE 0.12 -> 0.08 t/yr (bias +0.04 -> 0.00). A prior session deferred enum 9
# ("direction not understood") while the PE/CO2 lens was confounded by the # ("direction not understood") while the PE/CO2 lens was confounded by the
# climate-cascade bug (fc7c4d2d); the corrected lens shows the over-rate. # climate-cascade bug (fc7c4d2d); the corrected lens shows the over-rate.
# UNINSULATED STONE WALL §5.6 FORMULA (RdSAP 10 §5.6 Table 12, PDF p.40): a
# stone wall of KNOWN thickness whose insulation STATE is known (As Built /
# external / internal) is billed by the §5.6 formula on its lodged thickness
# (sandstone/limestone U = 54.876·W^-0.561, granite/whinstone 45.315·W^-0.513),
# NOT capped at the Table-6 typical-thickness 1.7. The old `if u0>=1.7: 1.7`
# cap nullified the formula for every real-thickness stone wall (it only dips
# below 1.7 past ~488 mm sandstone / ~640 mm granite) and under-counted fabric
# loss → over-rate. Gated on `wall_insulation_type is not None` so an
# "insulation Unknown" wall still falls to the Table-6 default (cert 000565
# Ext1: granite 50 mm + Unknown → worksheet 1.70, not the formula's 6.09).
# Took within-0.5 70.3% -> 71.6% (MAE 0.833 -> 0.822); fixed the 2 stone-U
# unit tests; worksheet-validated (Elmhurst age-B sandstone 400 mm → 1.90).
#
# SAP RATING FLOOR (SAP 10.2 §13 / RdSAP 10 §13): the rating is floored at 1 # SAP RATING FLOOR (SAP 10.2 §13 / RdSAP 10 §13): the rating is floored at 1
# ("if the result is less than 1, the rating is 1"). `calculate_sap_from_inputs` # ("if the result is less than 1, the rating is 1"). `calculate_sap_from_inputs`
# now applies that floor to the CONTINUOUS score too (was integer-only), so a # now applies that floor to the CONTINUOUS score too (was integer-only), so a
# degenerate dwelling no longer emits a negative SAP. Removed a -12.3 outlier # degenerate dwelling no longer emits a negative SAP. Removed a -12.3 outlier
# (cert 422000111926, lodged at the floor of 1, was computing -11.3): within-0.5 # (cert 422000111926, lodged at the floor of 1, was computing -11.3): within-0.5
# 70.2% -> 70.3%, MAE 0.845 -> 0.833. # 70.2% -> 70.3%, MAE 0.845 -> 0.833.
_MIN_WITHIN_HALF_SAP = 0.70 _MIN_WITHIN_HALF_SAP = 0.71
_MAX_SAP_MAE = 0.84 _MAX_SAP_MAE = 0.83
_MAX_CO2_MAE_TONNES = 0.09 # t CO2 / yr vs co2_emissions_current _MAX_CO2_MAE_TONNES = 0.09 # t CO2 / yr vs co2_emissions_current
_MAX_PE_PER_M2_MAE = 4.0 # kWh / m2 / yr vs energy_consumption_current _MAX_PE_PER_M2_MAE = 4.0 # kWh / m2 / yr vs energy_consumption_current