Cohort residual slice 9: u_rr_default_all_elements — RdSAP10 Table 18 col (4)

Adds the "Room-in-roof, all elements" U-value lookup keyed by age band,
with Scotland override for age K per Table 18 footnote (2). This is the
fallback U-value for the §3.9 Simplified RR cascade when no detailed
per-surface lodgement is available (the "as built / unknown" path per
footnote (1)).

Tests cover the spec table verbatim:
  - A-D 2.30, E 1.50, F 0.80, G 0.50, H 0.35, I 0.35, J 0.30,
  - K 0.25 (England) / 0.20 (Scotland), L 0.18, M 0.15.
Mid-range fallback 0.50 (matching age G) when neither age band nor
country lodged — robustness contract identical to u_roof.

Reference: RdSAP 10 (10-06-2025) Table 18 page 45.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-22 19:16:15 +00:00
parent 639b7ee2d7
commit 82627ebbfa
2 changed files with 91 additions and 0 deletions

View file

@ -390,6 +390,19 @@ _ROOF_BY_AGE: Final[dict[str, float]] = {
"K": 0.16, "L": 0.16, "M": 0.15,
}
# Table 18 column (4): "Room-in-roof, all elements" default U by age band
# when no detailed RR lodgement is available. Footnote (1) on each entry
# confirms "value from the table applies for unknown and as built".
# Scotland override per footnote (2): age band K → 0.20 W/m²K (other bands
# unchanged).
_RR_ALL_ELEMENTS_BY_AGE: Final[dict[str, float]] = {
"A": 2.30, "B": 2.30, "C": 2.30, "D": 2.30,
"E": 1.50, "F": 0.80, "G": 0.50, "H": 0.35,
"I": 0.35, "J": 0.30, "K": 0.25, "L": 0.18, "M": 0.15,
}
_RR_ALL_ELEMENTS_SCOTLAND_OVERRIDES: Final[dict[str, float]] = {"K": 0.20}
_RR_ALL_ELEMENTS_MID_RANGE_FALLBACK: Final[float] = 0.50
_ROOF_NO_INSULATION_MARKERS: Final[tuple[str, ...]] = (
"no insulation",
@ -449,6 +462,30 @@ def u_roof(
return _ROOF_BY_AGE.get(age_band.upper(), 0.4)
def u_rr_default_all_elements(
country: Optional[Country],
age_band: Optional[str],
) -> float:
"""RdSAP10 Table 18 column (4) — "Room-in-roof, all elements" default
U-value when no detailed RR lodgement is available.
Used as the catch-all fallback for the §3.9 Simplified RR cascade
when assessor didn't measure individual surfaces. The spec footnote
(1) on the table confirms: "value from the table applies for unknown
and as built". Footnote (2) overrides age K to 0.20 W/m²K in Scotland.
The mid-range fallback (0.50, matching age G) fires when the cert
lodges neither age band nor country same robustness contract as
`u_roof` and the other cascade helpers (never raise on missing input).
"""
if age_band is None:
return _RR_ALL_ELEMENTS_MID_RANGE_FALLBACK
band = age_band.upper()
if country is Country.SCT and band in _RR_ALL_ELEMENTS_SCOTLAND_OVERRIDES:
return _RR_ALL_ELEMENTS_SCOTLAND_OVERRIDES[band]
return _RR_ALL_ELEMENTS_BY_AGE.get(band, _RR_ALL_ELEMENTS_MID_RANGE_FALLBACK)
# ---------------------------------------------------------------------------
# Floor U-value (BS EN ISO 13370 + Table 19 defaults)
# ---------------------------------------------------------------------------

View file

@ -32,6 +32,7 @@ from domain.ml.rdsap_uvalues import (
u_floor,
u_party_wall,
u_roof,
u_rr_default_all_elements,
u_wall,
u_window,
)
@ -977,3 +978,56 @@ def test_country_from_code_recognises_known_codes() -> None:
assert Country.from_code("SCT") is Country.SCT
assert Country.from_code("NIR") is Country.NIR
assert Country.from_code("EAW") is Country.ENG # England-and-Wales aggregate maps to ENG
def test_u_rr_default_all_elements_age_band_b_returns_table18_col4_value() -> None:
"""RdSAP10 §5.11.4 + Table 18 column (4) — "Room-in-roof, all elements"
as-built / unknown default. Age band B (1900-1929) 2.30 W/m²K (the
uninsulated row carries footnote (1): "value from the table applies
for unknown and as built")."""
# Arrange / Act
result = u_rr_default_all_elements(country=Country.ENG, age_band="B")
# Assert
assert result == pytest.approx(2.30, abs=0.001)
def test_u_rr_default_all_elements_table18_col4_matches_spec_across_age_bands() -> None:
"""Table 18 column (4) per RdSAP10 spec page 45:
A-D 2.30, E 1.50, F 0.80, G 0.50, H 0.35, I 0.35, J 0.30,
K 0.25, L 0.18, M 0.15.
"""
# Arrange — expected RR-all-elements U-values for England.
expected = {
"A": 2.30, "B": 2.30, "C": 2.30, "D": 2.30,
"E": 1.50, "F": 0.80, "G": 0.50, "H": 0.35,
"I": 0.35, "J": 0.30, "K": 0.25, "L": 0.18, "M": 0.15,
}
# Act / Assert
for age_band, want in expected.items():
got = u_rr_default_all_elements(country=Country.ENG, age_band=age_band)
assert got == pytest.approx(want, abs=0.001), (
f"age={age_band}: got {got}, want {want}"
)
def test_u_rr_default_all_elements_scotland_age_band_k_returns_0_20_per_footnote() -> None:
"""Table 18 footnote (2): "0.20 W/m²K in Scotland" applies to the
age band K row of column (4). Other age bands unchanged."""
# Arrange / Act
result = u_rr_default_all_elements(country=Country.SCT, age_band="K")
# Assert
assert result == pytest.approx(0.20, abs=0.001)
def test_u_rr_default_all_elements_unknown_age_band_falls_back_to_mid_range() -> None:
"""Robustness: no age band → return the mid-range default rather than
raising. Picks the column (4) value at age G (0.50) as a sensible
middle estimate, matching the cascade convention used by `u_roof`."""
# Arrange / Act
result = u_rr_default_all_elements(country=None, age_band=None)
# Assert
assert result == pytest.approx(0.50, abs=0.001)