mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
u_floor: route age A,B unknowns to suspended-timber branch (Table 19 fn 1)
RdSAP10 §5.12 Table 19 footnote (1): when floor_construction is unknown, age bands A and B default to suspended timber, not solid. Previously u_floor always used the BS EN ISO 13370 solid-floor formula, which under-counted ~14% on pre-1929 dwellings. Elmhurst worksheet U985-0001-000490 Main Dwelling (A=14.85, P=7.42, w=0.400, age B) records floor U=0.71 W/m²K — the suspended-floor formula on §5.12 page 46 reproduces this exactly. The solid branch returned 0.66. Description prefixes "Solid, ..." / "Suspended, ..." take precedence over the age-band default since they're explicit assessor observations. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
49e8c65ae8
commit
344a9c9d5e
2 changed files with 90 additions and 0 deletions
|
|
@ -460,6 +460,54 @@ _FLOOR_INSULATION_DEFAULT_MM: Final[dict[str, int]] = {
|
|||
"H": 0, "I": 25, "J": 75, "K": 100, "L": 100, "M": 140,
|
||||
}
|
||||
|
||||
# Table 19 footnote (1): age bands whose default floor_construction is
|
||||
# suspended timber (when unknown). All other bands default to solid.
|
||||
_SUSPENDED_TIMBER_DEFAULT_BANDS: Final[frozenset[str]] = frozenset({"A", "B"})
|
||||
|
||||
|
||||
def _floor_is_suspended_from_description(description: Optional[str]) -> Optional[bool]:
|
||||
"""Parse the cert's floor description prefix ("Solid, ..." vs
|
||||
"Suspended, ...") into a tri-state: True if explicitly suspended,
|
||||
False if explicitly solid, None if the description carries no
|
||||
construction signal. `EpcFloorDescriptions` in `datatypes.epc.floor`
|
||||
enumerates the canonical prefixes."""
|
||||
if description is None:
|
||||
return None
|
||||
desc = description.lower().lstrip()
|
||||
if desc.startswith("suspended"):
|
||||
return True
|
||||
if desc.startswith("solid"):
|
||||
return False
|
||||
return None
|
||||
|
||||
|
||||
def _u_floor_suspended(
|
||||
*,
|
||||
area_m2: float,
|
||||
perimeter_m: float,
|
||||
wall_thickness_mm: Optional[int],
|
||||
insulation_thickness_mm: int,
|
||||
) -> float:
|
||||
"""Suspended ground-floor U-value per RdSAP10 §5.12 (page 46). Uses
|
||||
BS EN ISO 13370 with the suspended-floor adjustments — underfloor
|
||||
ventilation Ux is added to the soil Ug term before inverting.
|
||||
|
||||
Parameter defaults are pinned by the spec: thermal resistance of an
|
||||
uninsulated deck Rf=0.2 m²K/W (adds insulation R when present);
|
||||
underfloor height h=0.3 m; mean wind speed v=5 m/s; wind shielding
|
||||
fw=0.05; ventilation openings ε=0.003 m²/m; wall-to-underfloor U_w=1.5.
|
||||
"""
|
||||
w = (wall_thickness_mm or 300) / 1000.0
|
||||
soil_g = 1.5
|
||||
r_si = 0.17
|
||||
r_se = 0.04
|
||||
r_f = 0.2 + (insulation_thickness_mm / 1000.0) / 0.035
|
||||
d_g = w + soil_g * (r_si + r_se)
|
||||
b = 2.0 * area_m2 / perimeter_m
|
||||
u_g = 2.0 * soil_g * log(pi * b / d_g + 1.0) / (pi * b + d_g)
|
||||
u_x = (2.0 * 0.3 * 1.5 / b) + (1450.0 * 0.003 * 5.0 * 0.05 / b)
|
||||
return 1.0 / (2.0 * r_si + r_f + 1.0 / (u_g + u_x))
|
||||
|
||||
|
||||
def u_floor(
|
||||
country: Optional[Country],
|
||||
|
|
@ -512,6 +560,25 @@ def u_floor(
|
|||
# Table 19 footnote (2): "use the greater of 50 mm and the
|
||||
# thickness according to the age band".
|
||||
ins_mm = max(50, age_default_mm)
|
||||
# Table 19 footnote (1): if floor_construction is unknown, age bands
|
||||
# A and B default to suspended timber (the rest default to solid).
|
||||
# A description prefix of "Solid, ..." or "Suspended, ..." takes
|
||||
# precedence over the age-band default since it's an explicit assessor
|
||||
# observation about the construction.
|
||||
band_upper = age_band.upper() if age_band else None
|
||||
described_suspended = _floor_is_suspended_from_description(description)
|
||||
use_suspended_branch = (
|
||||
described_suspended
|
||||
if described_suspended is not None
|
||||
else (construction is None and band_upper in _SUSPENDED_TIMBER_DEFAULT_BANDS)
|
||||
)
|
||||
if use_suspended_branch:
|
||||
return _u_floor_suspended(
|
||||
area_m2=area_m2,
|
||||
perimeter_m=perimeter_m,
|
||||
wall_thickness_mm=wall_thickness_mm,
|
||||
insulation_thickness_mm=ins_mm or 0,
|
||||
)
|
||||
r_f = ((ins_mm or 0) / 1000.0) / 0.035
|
||||
d_t = w + soil_g * (r_si + r_f + r_se)
|
||||
b = 2.0 * area_m2 / perimeter_m
|
||||
|
|
|
|||
|
|
@ -742,6 +742,29 @@ def test_u_floor_solid_uninsulated_typical_geometry_returns_iso_13370_value() ->
|
|||
assert result == pytest.approx(0.68, abs=0.05)
|
||||
|
||||
|
||||
def test_u_floor_age_b_unknown_construction_uses_suspended_timber_per_table_19_footnote_1() -> None:
|
||||
# Arrange — RdSAP10 §5.12 Table 19 footnote (1) routes age A, B with
|
||||
# unknown floor_construction to the suspended-timber branch. Geometry
|
||||
# is taken from Elmhurst worksheet U985-0001-000490 Main Dwelling
|
||||
# (A=14.85, P=7.42, w=0.400) — the worksheet records U=0.71 W/m²K,
|
||||
# confirming the suspended-floor formula on §5.12 (page 46) is the
|
||||
# one Elmhurst applies for this fixture.
|
||||
|
||||
# Act
|
||||
result = u_floor(
|
||||
country=Country.ENG,
|
||||
age_band="B",
|
||||
construction=None,
|
||||
insulation_thickness_mm=None,
|
||||
area_m2=14.85,
|
||||
perimeter_m=7.42,
|
||||
wall_thickness_mm=400,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result == pytest.approx(0.71, abs=0.01)
|
||||
|
||||
|
||||
def test_u_floor_with_insulation_lowers_u_value() -> None:
|
||||
# Arrange — same geometry but with 100mm insulation -> R_f = 0.1/0.035 = 2.857.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue