diff --git a/domain/sap10_ml/rdsap_uvalues.py b/domain/sap10_ml/rdsap_uvalues.py index fae4a9ff..b75b17a4 100644 --- a/domain/sap10_ml/rdsap_uvalues.py +++ b/domain/sap10_ml/rdsap_uvalues.py @@ -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/ diff --git a/domain/sap10_ml/tests/test_rdsap_uvalues.py b/domain/sap10_ml/tests/test_rdsap_uvalues.py index 14599a33..210b97bf 100644 --- a/domain/sap10_ml/tests/test_rdsap_uvalues.py +++ b/domain/sap10_ml/tests/test_rdsap_uvalues.py @@ -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: