fix(uvalues): default unknown-thickness stone to §3.5 Table 3 thickness, not flat 1.7 (RdSAP 10 Table 3, PDF p.20)

When a stone wall lodges no documentary thickness, RdSAP 10 §3.5 Table 3
supplies the default thickness to feed the §5.6 formula — it does NOT fall
to a flat 1.7. Table 3 stone row: A-D = 500 mm, E = 450, F-H = 420;
Scotland footnote (*) adds 200 mm for bands A,B and 100 mm otherwise.

This matches accredited Elmhurst: an England age-B granite/whinstone wall,
as built, unknown thickness defaults to 500 mm →
  U = 45.315 × 500^(-0.513) = 1.87   (sandstone → 1.68).
The previous flat-1.7 fallback was a setup error — Table 6 reads "According
to 5.6" for bands A-D with no 1.7 entry, so the formula (not 1.7) is the
spec target. The age-E footnote-(a) cap still applies on top.

Adds `_table_3_stone_thickness(band, country)`. Updated the stale
`..._returns_table_6_age_a_default` pin (was 1.7 → now 1.87 via Table-3)
and added sandstone (1.68) + Scotland-+200mm (700 mm → 1.39) pins.

Corpus: within-0.5 71.6% unchanged, SAP MAE 0.821 → 0.819, PE MAE 3.7 → 3.6.
pyright not installed in this container — strict type 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-20 13:26:36 +00:00
parent 7e187078b9
commit 54ae05d04b
2 changed files with 83 additions and 12 deletions

View file

@ -226,6 +226,26 @@ def _u_stone_thin_wall_age_a_to_e(
return None
def _table_3_stone_thickness(band: str, country: Country) -> int:
"""RdSAP 10 §3.5 Table 3 (PDF p.20) — default stone wall thickness (mm)
used "only when the wall thickness could not be measured".
Stone row: A-D = 500, E = 450, F-H = 420, I+ = 450.
Scotland footnote (*): add 200 mm for bands A and B, 100 mm for other
bands. Only A-E reach this helper (the §5.6 formula gate), so the F+
branches are defensive.
"""
if band in ("A", "B", "C", "D"):
base = 500
elif band == "E":
base = 450
else:
base = 420
if country == Country.SCT:
base += 200 if band in ("A", "B") else 100
return base
def _u_brick_thin_wall_age_a_to_e(wall_thickness_mm: int) -> float:
"""RdSAP 10 §5.7 Table 13 (PDF p.41) — default U-value for an
uninsulated solid brick wall by lodged thickness, age bands A-E.
@ -585,12 +605,22 @@ def u_wall(
# 000565 Ext1 (granite 50 mm, age E, insulation Unknown) → min(6.09, 1.7)
# = 1.70, matching the U985 worksheet WITHOUT a flat-table detour — the
# age-E cap, not an insulation gate, is what produces the 1.70.
#
# When no documentary thickness is lodged, §3.5 Table 3 (PDF p.20) gives
# the default thickness to feed the formula (stone A-D = 500 mm, E = 450,
# Scotland +200/+100) — NOT a flat 1.7. This matches Elmhurst: an age-B
# granite as-built wall with unknown thickness defaults to 500 mm →
# 45.315 × 500^(-0.513) = 1.87 (sandstone → 1.68).
if (
wall_thickness_mm is not None
and band in _STONE_AGE_A_TO_E
band in _STONE_AGE_A_TO_E
and construction in (WALL_STONE_GRANITE, WALL_STONE_SANDSTONE)
):
u0 = _u_stone_thin_wall_age_a_to_e(construction, wall_thickness_mm)
w = (
wall_thickness_mm
if wall_thickness_mm is not None
else _table_3_stone_thickness(band, ctry)
)
u0 = _u_stone_thin_wall_age_a_to_e(construction, w)
if u0 is not None:
# Footnote (a) cap is age-E only: clamp the as-built U to the
# Table-6/7 age-E default (1.7, or 1.5 for Scotland sandstone/

View file

@ -826,13 +826,14 @@ def test_u_wall_stone_granite_age_g_with_wall_thickness_ignores_5_6_formula_per_
assert abs(result - 0.60) <= 1e-3
def test_u_wall_stone_granite_age_a_without_wall_thickness_returns_table_6_age_a_default() -> None:
# Arrange — §5.6 formula only fires when a wall thickness is
# lodged. Without documentary wall-thickness evidence, fall back
# to the Table 6 row (which represents typical thickness). For
# age A stone granite without thickness, the cascade preserves
# its existing "as-built typical" U value rather than the formula
# extrapolation.
def test_u_wall_stone_granite_age_a_without_wall_thickness_uses_table_3_default_500mm() -> None:
# Arrange — when no documentary wall thickness is lodged, RdSAP 10 §3.5
# Table 3 (PDF p.20) supplies the default stone thickness (A-D = 500 mm),
# which feeds the §5.6 formula — NOT a flat 1.7. This matches Elmhurst:
# an as-built granite/whinstone wall with unknown thickness defaults to
# 500 mm → U = 45.315 × 500^(-0.513) = 1.8693. (The earlier 1.7
# expectation was a setup error: Table 6 reads "According to 5.6" for
# bands A-D, with no 1.7 entry.)
# Act
result = u_wall(
@ -845,8 +846,48 @@ def test_u_wall_stone_granite_age_a_without_wall_thickness_returns_table_6_age_a
wall_thickness_mm=None,
)
# Assert — _TYPICAL_STONE_UNINSULATED at age A = 1.7 (cohort default).
assert abs(result - 1.7) <= 1e-3
# Assert — §5.6 formula at the Table-3 default 500 mm.
assert abs(result - 1.8693) <= 1e-3
def test_u_wall_stone_sandstone_age_b_without_wall_thickness_uses_table_3_default_500mm() -> None:
# Arrange — sandstone/limestone variant of the Table-3 default: age B,
# unknown thickness → 500 mm → 54.876 × 500^(-0.561) = 1.6798. This is
# the "500 mm → sandstone 1.68" Elmhurst default.
# Act
result = u_wall(
country=Country.ENG,
age_band="B",
construction=WALL_STONE_SANDSTONE,
insulation_thickness_mm=None,
insulation_present=False,
wall_insulation_type=4,
wall_thickness_mm=None,
)
# Assert
assert abs(result - 1.6798) <= 1e-3
def test_u_wall_stone_sandstone_scotland_age_a_without_thickness_adds_200mm_per_table_3() -> None:
# Arrange — Table 3 Scotland footnote (*): add 200 mm for bands A and B.
# Age-A Scotland sandstone unknown thickness → 500 + 200 = 700 mm →
# 54.876 × 700^(-0.561) = 1.3909 (< 1.7, no age-E cap at band A).
# Act
result = u_wall(
country=Country.SCT,
age_band="A",
construction=WALL_STONE_SANDSTONE,
insulation_thickness_mm=None,
insulation_present=False,
wall_insulation_type=4,
wall_thickness_mm=None,
)
# Assert
assert abs(result - 1.3909) <= 1e-3
def test_u_wall_stone_granite_age_e_50mm_caps_at_table6_default_1_7() -> None: