mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Cohort residual slice 10: u_rr_slope / u_rr_flat_ceiling / u_rr_stud_wall — RdSAP10 Table 17
Adds the three Table 17 lookups for rooms in roof where insulation
thickness is known. Each column of Table 17 splits into (a) mineral
wool / EPS slab vs (b) PUR or PIR rigid foam — pinned verbatim from
spec page 44 across all 16 thickness rows (0, 12, 25, ..., >400).
The three public functions share a single private `_u_rr_table_17` row
picker indexed by (column-a, column-b) pair, so a `u_rr_slope`,
`u_rr_flat_ceiling`, or `u_rr_stud_wall` call boils down to one row
descent through the same tuple-of-tuples. Falls back to
`u_rr_default_all_elements` (Table 18 col 4) when thickness is None —
matches the spec text at §5.11.3 / §5.11.4 ("U-values in Table 18 are
used when thickness of insulation cannot be determined").
Reference: RdSAP 10 (10-06-2025) Table 17 page 44; key on same page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
82627ebbfa
commit
0ff814451f
2 changed files with 260 additions and 0 deletions
|
|
@ -462,6 +462,131 @@ def u_roof(
|
|||
return _ROOF_BY_AGE.get(age_band.upper(), 0.4)
|
||||
|
||||
|
||||
# RdSAP10 Table 17 — U-values for rooms in roof where insulation thickness
|
||||
# is known. Each tuple row is (thickness_mm, col_1a, col_1b, col_2a, col_2b,
|
||||
# col_3a, col_3b) per spec page 44 (mineral wool/EPS for "a", PUR/PIR for
|
||||
# "b"). Row "none" represents thickness 0 mm. Row ">400" represents any
|
||||
# thickness ≥ 400 mm.
|
||||
_RR_TABLE_17_ROWS: Final[tuple[tuple[int, float, float, float, float, float, float], ...]] = (
|
||||
(0, 2.30, 2.30, 2.30, 2.30, 2.30, 2.30),
|
||||
(12, 1.50, 1.25, 1.75, 1.50, 0.95, 0.85),
|
||||
(25, 1.00, 0.80, 1.25, 1.00, 0.70, 0.60),
|
||||
(50, 0.68, 0.52, 0.88, 0.69, 0.52, 0.45),
|
||||
(75, 0.50, 0.38, 0.67, 0.51, 0.43, 0.35),
|
||||
(100, 0.40, 0.30, 0.54, 0.41, 0.36, 0.29),
|
||||
(125, 0.35, 0.25, 0.45, 0.34, 0.31, 0.24),
|
||||
(150, 0.30, 0.21, 0.39, 0.29, 0.27, 0.21),
|
||||
(175, 0.25, 0.17, 0.32, 0.23, 0.24, 0.19),
|
||||
(200, 0.21, 0.15, 0.29, 0.20, 0.22, 0.17),
|
||||
(225, 0.19, 0.13, 0.25, 0.18, 0.20, 0.15),
|
||||
(250, 0.17, 0.11, 0.23, 0.15, 0.18, 0.14),
|
||||
(270, 0.16, 0.10, 0.21, 0.14, 0.17, 0.13),
|
||||
(300, 0.14, 0.09, 0.19, 0.13, 0.16, 0.12),
|
||||
(350, 0.12, 0.08, 0.16, 0.11, 0.14, 0.11),
|
||||
(400, 0.11, 0.07, 0.14, 0.09, 0.12, 0.10),
|
||||
)
|
||||
|
||||
# Aliases mapping (insulation_type, column) → tuple index above. The PDF
|
||||
# splits each Table 17 column into "(a) mineral wool or EPS slab" vs "(b)
|
||||
# PUR or PIR optional". Aliases collapse common synonyms.
|
||||
_RR_RIGID_FOAM_INSULATION_TYPES: Final[frozenset[str]] = frozenset({"pur", "pir", "rigid"})
|
||||
|
||||
|
||||
def _is_rigid_foam(insulation_type: Optional[str]) -> bool:
|
||||
"""True if the insulation type names rigid foam (PUR/PIR). Falls back
|
||||
to False (i.e. mineral wool / EPS slab column) on None or any other
|
||||
string — same convention as `u_roof`'s mineral-wool default."""
|
||||
if insulation_type is None:
|
||||
return False
|
||||
return insulation_type.strip().lower() in _RR_RIGID_FOAM_INSULATION_TYPES
|
||||
|
||||
|
||||
def _u_rr_table_17(
|
||||
country: Optional[Country],
|
||||
age_band: Optional[str],
|
||||
insulation_thickness_mm: Optional[int],
|
||||
insulation_type: Optional[str],
|
||||
col_a_index: int,
|
||||
col_b_index: int,
|
||||
) -> float:
|
||||
"""Generic Table 17 row picker. Returns the U-value at the nearest
|
||||
tabulated thickness ≤ supplied. Falls back to `u_rr_default_all_
|
||||
elements` (Table 18 col 4) when thickness is None — matches the
|
||||
spec text at §5.11.3 / §5.11.4."""
|
||||
if insulation_thickness_mm is None:
|
||||
return u_rr_default_all_elements(country=country, age_band=age_band)
|
||||
col = col_b_index if _is_rigid_foam(insulation_type) else col_a_index
|
||||
u = _RR_TABLE_17_ROWS[0][col]
|
||||
for row in _RR_TABLE_17_ROWS:
|
||||
if insulation_thickness_mm >= row[0]:
|
||||
u = row[col]
|
||||
return u
|
||||
|
||||
|
||||
def u_rr_slope(
|
||||
*,
|
||||
country: Optional[Country],
|
||||
age_band: Optional[str],
|
||||
insulation_thickness_mm: Optional[int],
|
||||
insulation_type: Optional[str] = None,
|
||||
) -> float:
|
||||
"""RdSAP10 §5.11.3 + Table 17 column (1): U-value for an insulated
|
||||
sloping ceiling section of a room-in-roof. Column (1a) is mineral
|
||||
wool / EPS slab (default), (1b) is PUR/PIR rigid foam.
|
||||
|
||||
Falls back to `u_rr_default_all_elements` (Table 18 col 4) when
|
||||
thickness is unknown.
|
||||
"""
|
||||
return _u_rr_table_17(
|
||||
country=country,
|
||||
age_band=age_band,
|
||||
insulation_thickness_mm=insulation_thickness_mm,
|
||||
insulation_type=insulation_type,
|
||||
col_a_index=1,
|
||||
col_b_index=2,
|
||||
)
|
||||
|
||||
|
||||
def u_rr_flat_ceiling(
|
||||
*,
|
||||
country: Optional[Country],
|
||||
age_band: Optional[str],
|
||||
insulation_thickness_mm: Optional[int],
|
||||
insulation_type: Optional[str] = None,
|
||||
) -> float:
|
||||
"""RdSAP10 §5.11.3 + Table 17 column (2): U-value for the flat ceiling
|
||||
section of a room-in-roof (the "External roof" element in the U985
|
||||
worksheet vocabulary)."""
|
||||
return _u_rr_table_17(
|
||||
country=country,
|
||||
age_band=age_band,
|
||||
insulation_thickness_mm=insulation_thickness_mm,
|
||||
insulation_type=insulation_type,
|
||||
col_a_index=3,
|
||||
col_b_index=4,
|
||||
)
|
||||
|
||||
|
||||
def u_rr_stud_wall(
|
||||
*,
|
||||
country: Optional[Country],
|
||||
age_band: Optional[str],
|
||||
insulation_thickness_mm: Optional[int],
|
||||
insulation_type: Optional[str] = None,
|
||||
) -> float:
|
||||
"""RdSAP10 §5.11.3 + Table 17 column (3): U-value for a stud wall
|
||||
inside a room-in-roof (typically the short vertical wall between
|
||||
the RR floor and the slope)."""
|
||||
return _u_rr_table_17(
|
||||
country=country,
|
||||
age_band=age_band,
|
||||
insulation_thickness_mm=insulation_thickness_mm,
|
||||
insulation_type=insulation_type,
|
||||
col_a_index=5,
|
||||
col_b_index=6,
|
||||
)
|
||||
|
||||
|
||||
def u_rr_default_all_elements(
|
||||
country: Optional[Country],
|
||||
age_band: Optional[str],
|
||||
|
|
|
|||
|
|
@ -33,6 +33,9 @@ from domain.ml.rdsap_uvalues import (
|
|||
u_party_wall,
|
||||
u_roof,
|
||||
u_rr_default_all_elements,
|
||||
u_rr_flat_ceiling,
|
||||
u_rr_slope,
|
||||
u_rr_stud_wall,
|
||||
u_wall,
|
||||
u_window,
|
||||
)
|
||||
|
|
@ -1031,3 +1034,135 @@ def test_u_rr_default_all_elements_unknown_age_band_falls_back_to_mid_range() ->
|
|||
|
||||
# Assert
|
||||
assert result == pytest.approx(0.50, abs=0.001)
|
||||
|
||||
|
||||
# ----- Room-in-roof Table 17 lookups (insulation thickness known) -----
|
||||
|
||||
|
||||
def test_u_rr_slope_table17_col1a_mineral_wool_100mm_returns_0_40() -> None:
|
||||
"""RdSAP10 §5.11.3 + Table 17 column (1a): "Insulated slope - sloping
|
||||
ceiling, mineral wool or EPS slab" 100 mm row → 0.40 W/m²K."""
|
||||
# Arrange / Act
|
||||
result = u_rr_slope(
|
||||
country=Country.ENG,
|
||||
age_band="B",
|
||||
insulation_thickness_mm=100,
|
||||
insulation_type="mineral_wool",
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result == pytest.approx(0.40, abs=0.001)
|
||||
|
||||
|
||||
def test_u_rr_slope_table17_col1b_pur_pir_100mm_returns_0_30() -> None:
|
||||
"""Table 17 column (1b): "Insulated slope - sloping ceiling, PUR or
|
||||
PIR optional" 100 mm row → 0.30 W/m²K. The PUR/PIR rigid foam route
|
||||
gives a tighter U than mineral wool at the same thickness."""
|
||||
# Arrange / Act
|
||||
result = u_rr_slope(
|
||||
country=Country.ENG,
|
||||
age_band="B",
|
||||
insulation_thickness_mm=100,
|
||||
insulation_type="pir",
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result == pytest.approx(0.30, abs=0.001)
|
||||
|
||||
|
||||
def test_u_rr_flat_ceiling_table17_col2a_mineral_wool_100mm_returns_0_54() -> None:
|
||||
"""Table 17 column (2a): "Insulated slope - flat ceiling, mineral wool
|
||||
or EPS slab" 100 mm row → 0.54 W/m²K."""
|
||||
# Arrange / Act
|
||||
result = u_rr_flat_ceiling(
|
||||
country=Country.ENG,
|
||||
age_band="B",
|
||||
insulation_thickness_mm=100,
|
||||
insulation_type="mineral_wool",
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result == pytest.approx(0.54, abs=0.001)
|
||||
|
||||
|
||||
def test_u_rr_stud_wall_table17_col3a_mineral_wool_100mm_returns_0_36() -> None:
|
||||
"""Table 17 column (3a): "Stud wall u-value For Room in Roof, mineral
|
||||
wool or EPS slab" 100 mm row → 0.36 W/m²K. (Used by the U985 worksheet
|
||||
for 000477's RR stud walls.)"""
|
||||
# Arrange / Act
|
||||
result = u_rr_stud_wall(
|
||||
country=Country.ENG,
|
||||
age_band="B",
|
||||
insulation_thickness_mm=100,
|
||||
insulation_type="mineral_wool",
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result == pytest.approx(0.36, abs=0.001)
|
||||
|
||||
|
||||
def test_u_rr_slope_table17_none_row_uninsulated_returns_2_30() -> None:
|
||||
"""Table 17 "none" row (every column collapses to 2.3 when no
|
||||
insulation). Used by the U985 worksheet for 000477's RR slope panels
|
||||
that lodge as uninsulated."""
|
||||
# Arrange / Act
|
||||
result = u_rr_slope(
|
||||
country=Country.ENG,
|
||||
age_band="B",
|
||||
insulation_thickness_mm=0,
|
||||
insulation_type="mineral_wool",
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result == pytest.approx(2.30, abs=0.001)
|
||||
|
||||
|
||||
def test_u_rr_flat_ceiling_table17_col2b_pir_over_400mm_returns_0_09() -> None:
|
||||
"""Table 17 row ">400 mm" column (2b) PUR/PIR → 0.09 W/m²K. The U985
|
||||
worksheet for 000477 lodges 0.14 for "External roof Main" which is
|
||||
Table 17 col (2a) row >400 (mineral wool) — but this test uses the
|
||||
PIR column for completeness."""
|
||||
# Arrange / Act
|
||||
result = u_rr_flat_ceiling(
|
||||
country=Country.ENG,
|
||||
age_band="B",
|
||||
insulation_thickness_mm=450,
|
||||
insulation_type="pir",
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result == pytest.approx(0.09, abs=0.001)
|
||||
|
||||
|
||||
def test_u_rr_slope_unknown_thickness_falls_back_to_table18_all_elements() -> None:
|
||||
"""When `insulation_thickness_mm is None`, Table 17 doesn't apply and
|
||||
we cascade to Table 18 col (4) "Room-in-roof, all elements" by age
|
||||
band — same fallback as the spec text at §5.11.3 / §5.11.4.
|
||||
|
||||
For age band B, that's the 2.30 W/m²K uninsulated default."""
|
||||
# Arrange / Act
|
||||
result = u_rr_slope(
|
||||
country=Country.ENG,
|
||||
age_band="B",
|
||||
insulation_thickness_mm=None,
|
||||
insulation_type="mineral_wool",
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result == pytest.approx(2.30, abs=0.001)
|
||||
|
||||
|
||||
def test_u_rr_stud_wall_thickness_125mm_takes_nearest_tabulated_row_below() -> None:
|
||||
"""Table 17 row alignment: an arbitrary thickness picks the nearest
|
||||
tabulated row ≤ supplied (the same convention `u_roof` uses against
|
||||
Table 16). 125 mm matches the exact row → col (3a) = 0.31 W/m²K."""
|
||||
# Arrange / Act
|
||||
result = u_rr_stud_wall(
|
||||
country=Country.ENG,
|
||||
age_band="B",
|
||||
insulation_thickness_mm=125,
|
||||
insulation_type="mineral_wool",
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result == pytest.approx(0.31, abs=0.001)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue