mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
S0380.189: thermal mass parameter per RdSAP 10 §5.16 Table 22, not hardcoded 250
The §7 mean-internal-temperature cascade hardcoded the thermal mass parameter
(TMP) to 250 kJ/m²K at all 5 call sites, ignoring construction. RdSAP 10
§5.16 Table 22 (PDF p.48) makes TMP construction-dependent:
100 kJ/m²K — timber frame, cob, park home (regardless of internal
insulation); OR masonry (stone/solid brick/cavity/system
built) WITH internal insulation.
250 kJ/m²K — masonry WITHOUT internal insulation.
A too-high TMP inflates the §7 time constant τ = Cm/(3.6·H) (e.g. 40 h vs
16 h), under-cuts the temperature reduction between heating periods, and
over-states mean internal temperature → over-states space heating.
`_thermal_mass_parameter_kj_per_m2_k(epc)` classifies the MAIN building's
wall via the RdSAP `wall_construction` codes (5/7/8 = timber/cob/park) and
`wall_insulation_type` codes (3/7 = internal); unknown/curtain fall back to
the masonry 250 (no regression on unlisted classes). 17-case parametrised
test covers every Table 22 branch.
Diagnosis (per-line walk vs the user-simulated 001431 worksheet, same
archetype as golden cert 6035): fabric (26-37), internal gains (73), climate
(96)m and HTC (39) all EXACT; the entire +8.78 PE / -1.76 SAP gap was §7 MIT
(92) +0.71 °C, traced to TMP 250 vs Table 22's 100 (solid brick WITH internal
insulation). Fix closes the simulated case to 1e-4 on PE and CO2.
Blast radius: only golden cert 6035 re-pins (solid brick + internal
insulation) — SAP resid -6 → -2, PE +46.42 → +19.16, CO2 +1.07 → +0.42. The
47 dr87 cohort, 6 U985 fixtures and 41-variant heating corpus are all
masonry-no-internal → TMP unchanged at 250, all still pass. 2290 pass
(+17 new), 0 fail; pyright net-zero.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
1382c8c886
commit
e03f08cdc8
3 changed files with 119 additions and 8 deletions
|
|
@ -210,7 +210,26 @@ _LIVING_AREA_FRACTION_MIN: Final[float] = 0.13
|
|||
|
||||
|
||||
_PENCE_TO_GBP: Final[float] = 0.01
|
||||
# RdSAP 10 §5.16 Table 22 (PDF p.48) — thermal mass parameter (TMP),
|
||||
# keyed on the construction type of the MAIN building (not extensions):
|
||||
# 100 kJ/m²K — timber frame, cob, park home (the three types regardless
|
||||
# of internal insulation); OR masonry (stone, solid brick,
|
||||
# cavity, system built) WITH internal insulation.
|
||||
# 250 kJ/m²K — masonry WITHOUT internal insulation.
|
||||
# This default is the masonry-no-internal value; `_thermal_mass_parameter_
|
||||
# kj_per_m2_k` lowers it to 100 for the Table 22 low-mass cases. Unknown /
|
||||
# unmapped / curtain-wall constructions keep the 250 default (the
|
||||
# pre-Table-22 behaviour, so no fixture regresses on a missing class).
|
||||
_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})
|
||||
# `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
|
||||
# (2), cavity+external (6), as-built (4), none (5) keep the masonry 250.
|
||||
_TMP_INTERNAL_WALL_INSULATION_CODES: Final[frozenset[int]] = frozenset({3, 7})
|
||||
|
||||
# SAP 10.2 Table 4f (PDF p.174) — Heating system circulation pump
|
||||
# rows. Keyed on RdSAP API `central_heating_pump_age` enum:
|
||||
|
|
@ -3269,6 +3288,30 @@ def _int_or_none(value: object) -> Optional[int]:
|
|||
return value if isinstance(value, int) else None
|
||||
|
||||
|
||||
def _thermal_mass_parameter_kj_per_m2_k(epc: EpcPropertyData) -> float:
|
||||
"""RdSAP 10 §5.16 Table 22 (PDF p.48) — thermal mass parameter from
|
||||
the MAIN building's wall construction.
|
||||
|
||||
Timber frame / cob / park home → 100 kJ/m²K regardless of insulation.
|
||||
Masonry (stone, solid brick, cavity, system built) → 100 with internal
|
||||
insulation, else 250. Unknown / unmapped / curtain-wall constructions
|
||||
fall back to the masonry default (250). See the Table 22 constant
|
||||
comments above for the `wall_construction` / `wall_insulation_type`
|
||||
code sets. TMP feeds the §7 time constant τ = Cm/(3.6·H); a wrong
|
||||
(too-high) TMP slows the cooling rate, under-cuts the §7 temperature
|
||||
reduction, and over-states mean internal temperature → space heating.
|
||||
"""
|
||||
parts: list[SapBuildingPart] = epc.sap_building_parts or []
|
||||
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:
|
||||
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
|
||||
return _DEFAULT_THERMAL_MASS_PARAMETER_KJ_PER_M2_K
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _VentilationCounts:
|
||||
open_flues: int = 0
|
||||
|
|
@ -3513,7 +3556,7 @@ def mean_internal_temperature_section_from_cert(
|
|||
),
|
||||
monthly_total_gains_w=monthly_total_gains_w,
|
||||
monthly_heat_transfer_coefficient_w_per_k=monthly_htc_w_per_k,
|
||||
thermal_mass_parameter_kj_per_m2_k=_DEFAULT_THERMAL_MASS_PARAMETER_KJ_PER_M2_K,
|
||||
thermal_mass_parameter_kj_per_m2_k=_thermal_mass_parameter_kj_per_m2_k(epc),
|
||||
total_floor_area_m2=dim.total_floor_area_m2,
|
||||
control_type=_control_type(main),
|
||||
responsiveness=_responsiveness(main, tariff=tariff),
|
||||
|
|
@ -3612,7 +3655,7 @@ def space_cooling_section_from_cert(
|
|||
monthly_external_temperature_c=monthly_external_temp_c,
|
||||
monthly_total_gains_w=(0.0,) * 12,
|
||||
total_floor_area_m2=dim.total_floor_area_m2,
|
||||
thermal_mass_parameter_kj_per_m2_k=_DEFAULT_THERMAL_MASS_PARAMETER_KJ_PER_M2_K,
|
||||
thermal_mass_parameter_kj_per_m2_k=_thermal_mass_parameter_kj_per_m2_k(epc),
|
||||
cooled_area_fraction=0.0,
|
||||
intermittency_factor=0.25,
|
||||
)
|
||||
|
|
@ -6174,7 +6217,7 @@ def cert_to_inputs(
|
|||
),
|
||||
monthly_total_gains_w=monthly_total_gains_w,
|
||||
monthly_heat_transfer_coefficient_w_per_k=monthly_htc_w_per_k,
|
||||
thermal_mass_parameter_kj_per_m2_k=_DEFAULT_THERMAL_MASS_PARAMETER_KJ_PER_M2_K,
|
||||
thermal_mass_parameter_kj_per_m2_k=_thermal_mass_parameter_kj_per_m2_k(epc),
|
||||
total_floor_area_m2=dim.total_floor_area_m2,
|
||||
control_type=control_type_value,
|
||||
responsiveness=responsiveness_value,
|
||||
|
|
@ -6270,7 +6313,7 @@ def cert_to_inputs(
|
|||
monthly_external_temperature_c=monthly_external_temp_c,
|
||||
monthly_total_gains_w=(0.0,) * 12,
|
||||
total_floor_area_m2=dim.total_floor_area_m2,
|
||||
thermal_mass_parameter_kj_per_m2_k=_DEFAULT_THERMAL_MASS_PARAMETER_KJ_PER_M2_K,
|
||||
thermal_mass_parameter_kj_per_m2_k=_thermal_mass_parameter_kj_per_m2_k(epc),
|
||||
cooled_area_fraction=0.0,
|
||||
intermittency_factor=0.25,
|
||||
)
|
||||
|
|
@ -6454,7 +6497,7 @@ def cert_to_inputs(
|
|||
responsiveness=responsiveness_value,
|
||||
living_area_fraction=living_area_fraction_value,
|
||||
control_temperature_adjustment_c=_control_temperature_adjustment_c(main),
|
||||
thermal_mass_parameter_kj_per_m2_k=_DEFAULT_THERMAL_MASS_PARAMETER_KJ_PER_M2_K,
|
||||
thermal_mass_parameter_kj_per_m2_k=_thermal_mass_parameter_kj_per_m2_k(epc),
|
||||
main_heating_efficiency=eff,
|
||||
hot_water_kwh_per_yr=hw_kwh,
|
||||
pumps_fans_kwh_per_yr=pumps_fans_kwh,
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ from domain.sap10_calculator.rdsap.cert_to_inputs import (
|
|||
_separately_timed_dhw, # pyright: ignore[reportPrivateUsage]
|
||||
_space_heating_fuel_cost_gbp_per_kwh, # pyright: ignore[reportPrivateUsage]
|
||||
_tariff_high_low_rates_p_per_kwh, # pyright: ignore[reportPrivateUsage]
|
||||
_thermal_mass_parameter_kj_per_m2_k, # pyright: ignore[reportPrivateUsage]
|
||||
_water_efficiency_with_category_inherit, # pyright: ignore[reportPrivateUsage]
|
||||
_water_heating_worksheet_and_gains, # pyright: ignore[reportPrivateUsage]
|
||||
cert_to_demand_inputs,
|
||||
|
|
@ -122,6 +123,64 @@ 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.
|
||||
(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
|
||||
# Masonry WITH internal insulation (ins 3 = internal, 7 =
|
||||
# filled cavity + internal) → low-mass 100.
|
||||
(3, 3, 100.0), # solid brick + internal
|
||||
(3, 7, 100.0), # solid brick + filled cavity + internal
|
||||
(4, 3, 100.0), # cavity + internal
|
||||
(6, 3, 100.0), # system built + internal
|
||||
(1, 3, 100.0), # stone granite + internal
|
||||
# Masonry WITHOUT internal insulation → high-mass 250. External
|
||||
# (1), filled cavity (2), cavity+external (6), as-built (4) all
|
||||
# leave the structural mass coupled.
|
||||
(3, 4, 250.0), # solid brick, as-built
|
||||
(4, 2, 250.0), # cavity, filled
|
||||
(4, 1, 250.0), # cavity, external insulation (NOT internal)
|
||||
(4, 6, 250.0), # cavity + external
|
||||
(1, 4, 250.0), # stone, as-built
|
||||
(6, 4, 250.0), # system built, as-built (Table 22 lists it as masonry)
|
||||
# Unmapped / curtain (9) / unknown (10) → masonry default 250
|
||||
# (pre-Table-22 behaviour; no fixture regresses on a missing class).
|
||||
(9, 4, 250.0), # curtain wall
|
||||
(10, 4, 250.0), # unknown
|
||||
],
|
||||
)
|
||||
def test_thermal_mass_parameter_follows_rdsap_table_22(
|
||||
wall_construction: int, wall_insulation_type: int, expected_tmp: float
|
||||
) -> None:
|
||||
# Arrange — a single-part dwelling carrying the wall construction +
|
||||
# insulation under test (RdSAP 10 §5.16 Table 22, PDF p.48).
|
||||
epc = make_minimal_sap10_epc(
|
||||
total_floor_area_m2=_TYPICAL_TFA_M2,
|
||||
habitable_rooms_count=4,
|
||||
country_code="ENG",
|
||||
sap_building_parts=[
|
||||
make_building_part(
|
||||
wall_construction=wall_construction,
|
||||
wall_insulation_type=wall_insulation_type,
|
||||
),
|
||||
],
|
||||
sap_heating=make_sap_heating(
|
||||
main_heating_details=[_gas_boiler_detail(sap_main_heating_code=102)],
|
||||
),
|
||||
)
|
||||
|
||||
# Act
|
||||
tmp: float = _thermal_mass_parameter_kj_per_m2_k(epc)
|
||||
|
||||
# Assert
|
||||
assert abs(tmp - expected_tmp) <= 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
|
||||
|
|
|
|||
|
|
@ -193,11 +193,20 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = (
|
|||
_GoldenExpectation(
|
||||
cert_number="6035-7729-2309-0879-2296",
|
||||
actual_sap=70,
|
||||
expected_sap_resid=-6,
|
||||
expected_pe_resid_kwh_per_m2=+46.4156,
|
||||
expected_co2_resid_tonnes_per_yr=+1.0677,
|
||||
expected_sap_resid=-2,
|
||||
expected_pe_resid_kwh_per_m2=+19.1566,
|
||||
expected_co2_resid_tonnes_per_yr=+0.4211,
|
||||
notes=(
|
||||
"Mid-terrace, TFA 128, age A, gas combi Table 4b code 104. "
|
||||
"S0380.189 fixed the dominant driver: walls are solid brick "
|
||||
"WITH internal insulation (wall_insulation_type=3), so "
|
||||
"RdSAP 10 §5.16 Table 22 sets TMP=100 (not the old hardcoded "
|
||||
"250). Correct TMP → §7 time constant ~16h not ~40h → larger "
|
||||
"temperature reduction → MIT down ~0.7C → space heating drops. "
|
||||
"SAP resid -6 → -2, PE +46.42 → +19.16, CO2 +1.07 → +0.42. "
|
||||
"Validated 1e-4 against the user-simulated 001431 worksheet "
|
||||
"(same archetype). Remaining +19 PE is other gaps + lodged "
|
||||
"divergence (no worksheet for 6035 itself to pin further). "
|
||||
"Slice 59 per-bp window apportionment tightens all 3 "
|
||||
"residuals: SAP -5 → -4, PE +36.15 → +34.02, CO2 +0.81 → "
|
||||
"+0.76 (2 of 8 windows route to Ext1 with ins_type 4 vs "
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue