mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
fix(thermal-mass): gov-API system built (wall code 8) is masonry, not park home
The §5.16 Table 22 thermal-mass-parameter (TMP) "always low-mass" set was
{timber 5, cob 7, park home 8}. But wall_construction code 8 is OVERLOADED by
the same gov-API/calc code-space divergence as the wall-U fix: the Summary
path's "PH" mapping uses 8 for park home, while the gov-EPC API enum uses 8
for SYSTEM BUILD (Summary system build = code 6). So every API system-built
cert was mis-rated as low-mass 100 kJ/m²K instead of masonry 250 (Table 22
lists system build as masonry — PDF p.48, line "System build 250...").
A too-low TMP shortens the §7 time constant tau = Cm/(3.6·H), over-cutting
the temperature reduction so mean internal temperature is UNDER-stated →
space-heating demand under-stated → SAP over-rated. This was the cause of the
uninsulated system-built over-rate cluster (n=9 gas-boiler certs at signed
+2.39 vs cavity +0.43 / solid-brick +0.08 at the same bands — a system-built-
specific anomaly with a spec-correct wall U).
Fix: drop 8 from the always-low set and gate it on `property_type` — code 8 is
the low-mass park-home value only when the dwelling really is a park home,
otherwise it is gov-API system build and keeps masonry 250. Disambiguated by
the same `property_type == "park home"` signal used elsewhere in the cascade.
Worksheet harness UNAFFECTED (47/47, 0 divergers): the Summary path uses code
6 for system build and code 8 only for genuine park homes (which stay
low-mass via the property_type gate). API gauge 65.3% -> 67.1% within-0.5
(mean|err| 1.059 -> 1.024, signed +0.050 -> -0.002). The uninsulated
system-built cluster collapses +2.82 -> +0.28 signed (0/11 -> 7/11 within
0.5). 2 AAA tests (parametrised code-8 system-built -> 250; park-home
property -> 100). pyright net-zero.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
aab75cf902
commit
42e0bb3122
2 changed files with 55 additions and 6 deletions
|
|
@ -234,8 +234,15 @@ _PENCE_TO_GBP: Final[float] = 0.01
|
|||
_DEFAULT_THERMAL_MASS_PARAMETER_KJ_PER_M2_K: Final[float] = 250.0
|
||||
_TMP_LOW_KJ_PER_M2_K: Final[float] = 100.0
|
||||
# `wall_construction` int codes (domain/sap10_ml/rdsap_uvalues.py):
|
||||
# 5 = timber frame, 7 = cob, 8 = park home — Table 22's "three types".
|
||||
_TMP_ALWAYS_LOW_WALL_CONSTRUCTION_CODES: Final[frozenset[int]] = frozenset({5, 7, 8})
|
||||
# 5 = timber frame, 7 = cob — always Table 22 low-mass. Park home is the
|
||||
# third "always low-mass" type, but its wall code (8) is OVERLOADED: the
|
||||
# Summary path's "PH" mapping uses 8 for park home, whereas the gov-API
|
||||
# enum uses 8 for SYSTEM BUILT (a masonry type, Summary system build = code
|
||||
# 6). Code 8 therefore takes the low-mass value only when the dwelling is
|
||||
# actually a park home (`property_type`); otherwise it is system built and
|
||||
# keeps the masonry 250 — see `_thermal_mass_parameter_kj_per_m2_k`.
|
||||
_TMP_ALWAYS_LOW_WALL_CONSTRUCTION_CODES: Final[frozenset[int]] = frozenset({5, 7})
|
||||
_TMP_PARK_HOME_OR_SYSTEM_BUILT_WALL_CODE: Final[int] = 8
|
||||
# `wall_insulation_type` int codes that are INTERNAL insulation
|
||||
# (Table 22 "masonry … with internal insulation"): 3 = internal wall
|
||||
# insulation, 7 = filled cavity + internal. External (1), filled cavity
|
||||
|
|
@ -3871,7 +3878,14 @@ def _thermal_mass_parameter_kj_per_m2_k(epc: EpcPropertyData) -> float:
|
|||
if not parts:
|
||||
return _DEFAULT_THERMAL_MASS_PARAMETER_KJ_PER_M2_K
|
||||
main: SapBuildingPart = parts[0]
|
||||
if _int_or_none(main.wall_construction) in _TMP_ALWAYS_LOW_WALL_CONSTRUCTION_CODES:
|
||||
wall_code: Optional[int] = _int_or_none(main.wall_construction)
|
||||
if wall_code in _TMP_ALWAYS_LOW_WALL_CONSTRUCTION_CODES:
|
||||
return _TMP_LOW_KJ_PER_M2_K
|
||||
# Wall code 8 is a park home (low-mass) ONLY when the dwelling really is
|
||||
# one; on the gov-API path code 8 is system built (masonry). Disambiguate
|
||||
# by property_type so an API system build is not mis-rated as low-mass.
|
||||
is_park_home: bool = (epc.property_type or "").strip().lower() == "park home"
|
||||
if wall_code == _TMP_PARK_HOME_OR_SYSTEM_BUILT_WALL_CODE and is_park_home:
|
||||
return _TMP_LOW_KJ_PER_M2_K
|
||||
if _int_or_none(main.wall_insulation_type) in _TMP_INTERNAL_WALL_INSULATION_CODES:
|
||||
return _TMP_LOW_KJ_PER_M2_K
|
||||
|
|
|
|||
|
|
@ -148,12 +148,17 @@ def _typical_semi_detached_epc():
|
|||
@pytest.mark.parametrize(
|
||||
"wall_construction, wall_insulation_type, expected_tmp",
|
||||
[
|
||||
# RdSAP 10 §5.16 Table 22 (PDF p.48) — timber frame (5), cob (7),
|
||||
# park home (8) are always low-mass, regardless of insulation.
|
||||
# RdSAP 10 §5.16 Table 22 (PDF p.48) — timber frame (5), cob (7)
|
||||
# are always low-mass, regardless of insulation.
|
||||
(5, 4, 100.0), # timber frame, as-built
|
||||
(5, 2, 100.0), # timber frame, filled cavity — still 100
|
||||
(7, 4, 100.0), # cob
|
||||
(8, 1, 100.0), # park home, external insulation — still 100
|
||||
# Wall code 8 with no park-home property is gov-API SYSTEM BUILT
|
||||
# (calc 8 = park home only on the Summary path) → masonry. Table 22
|
||||
# lists system build as masonry (250 as-built); the park-home
|
||||
# low-mass case is covered separately below (needs property_type).
|
||||
(8, 1, 250.0), # system built (gov-API code 8), external insulation
|
||||
(8, 4, 250.0), # system built (gov-API code 8), as-built
|
||||
# Masonry WITH internal insulation (ins 3 = internal, 7 =
|
||||
# filled cavity + internal) → low-mass 100.
|
||||
(3, 3, 100.0), # solid brick + internal
|
||||
|
|
@ -203,6 +208,36 @@ def test_thermal_mass_parameter_follows_rdsap_table_22(
|
|||
assert abs(tmp - expected_tmp) <= 1e-9
|
||||
|
||||
|
||||
def test_thermal_mass_wall_code_8_is_park_home_only_when_property_type_says_so() -> None:
|
||||
# Arrange — RdSAP 10 §5.16 Table 22 (PDF p.48): timber frame / cob /
|
||||
# PARK HOME are low-mass (100); system build is masonry (250 as-built).
|
||||
# Wall_construction code 8 is overloaded: the Summary path's "PH"
|
||||
# mapping uses 8 for park home, but the gov-API enum uses 8 for SYSTEM
|
||||
# BUILT (Summary system build is code 6). So code 8 may only take the
|
||||
# park-home low-mass value when the dwelling really is a park home —
|
||||
# otherwise it is gov-API system built (masonry). Same construction
|
||||
# code, opposite thermal mass, disambiguated by `property_type`.
|
||||
def _epc(property_type: Optional[str]) -> EpcPropertyData:
|
||||
return make_minimal_sap10_epc(
|
||||
total_floor_area_m2=_TYPICAL_TFA_M2, habitable_rooms_count=4,
|
||||
country_code="ENG", property_type=property_type,
|
||||
sap_building_parts=[
|
||||
make_building_part(wall_construction=8, wall_insulation_type=4),
|
||||
],
|
||||
sap_heating=make_sap_heating(
|
||||
main_heating_details=[_gas_boiler_detail(sap_main_heating_code=102)],
|
||||
),
|
||||
)
|
||||
|
||||
# Act
|
||||
tmp_park_home: float = _thermal_mass_parameter_kj_per_m2_k(_epc("Park home"))
|
||||
tmp_system_built: float = _thermal_mass_parameter_kj_per_m2_k(_epc("House"))
|
||||
|
||||
# Assert — park home → low-mass 100; system built (code 8) → masonry 250.
|
||||
assert abs(tmp_park_home - 100.0) <= 1e-9
|
||||
assert abs(tmp_system_built - 250.0) <= 1e-9
|
||||
|
||||
|
||||
def test_heat_network_main_applies_table12c_dlf_to_main_heating_efficiency() -> None:
|
||||
# Arrange — heat-network main heating (Table 4a code 301 = community
|
||||
# heating with CHP/boilers; main_heating_category=6). Cert age band
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue