mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
fix(water-heating): assume cylinder thermostat present for electric/immersion/heat-network DHW (SAP 9.4.9)
SAP 10.2 §9.4.9 (PDF p.32) verbatim: "A cylinder thermostat should be
assumed to be present when the domestic hot water is obtained from a heat
network, an immersion heater, a thermal store, a combi boiler or a CPSU."
RdSAP 10 Table 29 (p.56) points the no-access default at this rule.
The storage-loss Table 2b temperature factor previously read only the
lodged `cylinder_thermostat` ("Y") — so an unlodged thermostat always took
the ×1.3 absent-penalty, over-stating storage loss by 30%. New
`_cylinder_thermostat_present` assumes it present when DHW is from a heat
network, WHC 903 (immersion), or a direct-acting electric boiler (SAP code
191 — electric-resistance, immersion-equivalent).
Found via the worksheet-folder harness: cert 2474-3059-4202-4496-3200
(Summary path: WHC 901, main SAP 191, electric, no lodged cylinder stat)
diverged −1.86 from its dr87 worksheet. The worksheet lodges (53)
temperature factor 0.6000 (present) and "add cylinder thermostat (SAP
increase too small)" — already assumed present. Fix lands HW output (64)
2701.99 → 2323.88, EXACT to the worksheet; 2474 −1.86 → −0.87 (residual is
a separate space-demand fabric thread). No other worksheet in the 47-cert
harness moved.
API eval within-0.5 56.9% → 57.6%; mean|err| 1.197 → 1.185; signed
−0.202 → −0.165. Regression green (only pre-existing fails); goldens +
heating corpus unaffected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
00921f71e8
commit
3cb2711418
2 changed files with 92 additions and 1 deletions
|
|
@ -6174,6 +6174,42 @@ def _primary_loss_override(
|
|||
return base
|
||||
|
||||
|
||||
def _cylinder_thermostat_present(
|
||||
epc: EpcPropertyData, main: Optional[MainHeatingDetail],
|
||||
) -> bool:
|
||||
"""Whether a cylinder thermostat is present for the Table 2b temperature
|
||||
factor (absent → ×1.3 penalty on the storage loss).
|
||||
|
||||
A lodged "Y" wins. Otherwise SAP 10.2 §9.4.9 (PDF p.32) verbatim: "A
|
||||
cylinder thermostat should be assumed to be present when the domestic
|
||||
hot water is obtained from a heat network, an immersion heater, a
|
||||
thermal store, a combi boiler or a CPSU." RdSAP 10 Table 29 (PDF p.56)
|
||||
points the no-access default at this rule. So a cylinder heated by an
|
||||
immersion (WHC 903), a direct-acting electric boiler (SAP code 191 —
|
||||
electric-resistance, immersion-equivalent), or a heat network gets the
|
||||
base Table 2b factor (no absent-thermostat ×1.3).
|
||||
|
||||
Cert 2474 (Summary path: WHC 901, main SAP 191, electric, no lodged
|
||||
cylinder thermostat) is the case: the dr87 worksheet lodges (53)
|
||||
temperature factor 0.6000 (thermostat present), and the "add cylinder
|
||||
thermostat" recommendation reads "SAP increase too small" because it
|
||||
is already assumed present. Without this the cascade applied ×1.3 and
|
||||
over-stated storage loss by ~378 kWh/yr (SAP −1.86)."""
|
||||
if epc.sap_heating.cylinder_thermostat == "Y":
|
||||
return True
|
||||
dhw_main = _water_heating_main(epc)
|
||||
if _is_heat_network_main(dhw_main):
|
||||
return True
|
||||
if epc.sap_heating.water_heating_code == _WHC_ELECTRIC_IMMERSION:
|
||||
return True
|
||||
if (
|
||||
dhw_main is not None
|
||||
and dhw_main.sap_main_heating_code == _DIRECT_ACTING_ELECTRIC_BOILER_CODE
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _cylinder_storage_loss_override(
|
||||
epc: EpcPropertyData,
|
||||
main: Optional[MainHeatingDetail],
|
||||
|
|
@ -6217,7 +6253,7 @@ def _cylinder_storage_loss_override(
|
|||
volume_l=volume_l,
|
||||
insulation_type=insulation_label,
|
||||
thickness_mm=float(thickness_mm),
|
||||
has_cylinder_thermostat=sh.cylinder_thermostat == "Y",
|
||||
has_cylinder_thermostat=_cylinder_thermostat_present(epc, main),
|
||||
# SAP 10.2 Table 2b note b (PDF p.159) verbatim restricts the
|
||||
# ×0.9 multiplier to boiler / warm-air / heat-pump systems —
|
||||
# community heating excluded. Gate via the dedicated helper so
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ from domain.sap10_calculator.exceptions import (
|
|||
from domain.sap10_calculator.rdsap.cert_to_inputs import (
|
||||
SAP_10_2_SPEC_PRICES,
|
||||
_apply_heat_network_hiu_default_store, # pyright: ignore[reportPrivateUsage]
|
||||
_cylinder_thermostat_present, # pyright: ignore[reportPrivateUsage]
|
||||
_has_suspended_timber_floor_per_spec, # pyright: ignore[reportPrivateUsage]
|
||||
_heat_network_code_302_effective_factor, # pyright: ignore[reportPrivateUsage]
|
||||
_heat_network_community_fuel_code, # pyright: ignore[reportPrivateUsage]
|
||||
|
|
@ -365,6 +366,60 @@ def test_heat_network_primary_loss_uses_p1_h3_all_months_per_table_3() -> None:
|
|||
assert abs(sum(wh_result.primary_loss_monthly_kwh) - expected_primary_kwh) <= 1e-6
|
||||
|
||||
|
||||
def test_cylinder_thermostat_assumed_present_per_sap_9_4_9() -> None:
|
||||
# Arrange — SAP 10.2 §9.4.9 (PDF p.32): "A cylinder thermostat should be
|
||||
# assumed to be present when the domestic hot water is obtained from a
|
||||
# heat network, an immersion heater, a thermal store, a combi boiler or
|
||||
# a CPSU." A direct-acting electric boiler (SAP code 191) heats the
|
||||
# cylinder by electric resistance (immersion-equivalent) → assumed
|
||||
# present even with no lodged cylinder thermostat. Reproduces cert
|
||||
# 2474-3059-4202-4496-3200 (Summary path: WHC 901, main SAP 191).
|
||||
direct_electric = MainHeatingDetail(
|
||||
has_fghrs=False,
|
||||
main_fuel_type=30, # standard electricity
|
||||
heat_emitter_type=0,
|
||||
emitter_temperature="NA",
|
||||
main_heating_control=2106,
|
||||
main_heating_category=2,
|
||||
sap_main_heating_code=191,
|
||||
)
|
||||
part = make_building_part(construction_age_band="D")
|
||||
epc_191 = make_minimal_sap10_epc(
|
||||
total_floor_area_m2=_TYPICAL_TFA_M2,
|
||||
habitable_rooms_count=4,
|
||||
country_code="ENG",
|
||||
sap_building_parts=[part],
|
||||
sap_heating=make_sap_heating(
|
||||
main_heating_details=[direct_electric],
|
||||
water_heating_code=901, # from main, no lodged cylinder stat
|
||||
),
|
||||
)
|
||||
# A gas boiler is NOT in the §9.4.9 list — its cylinder keeps the
|
||||
# absent-thermostat default (Table 2b ×1.3) when none is lodged.
|
||||
gas_boiler = MainHeatingDetail(
|
||||
has_fghrs=False,
|
||||
main_fuel_type=26,
|
||||
heat_emitter_type=1,
|
||||
emitter_temperature=1,
|
||||
main_heating_control=2106,
|
||||
main_heating_category=2,
|
||||
sap_main_heating_code=102,
|
||||
)
|
||||
epc_gas = make_minimal_sap10_epc(
|
||||
total_floor_area_m2=_TYPICAL_TFA_M2,
|
||||
habitable_rooms_count=4,
|
||||
country_code="ENG",
|
||||
sap_building_parts=[part],
|
||||
sap_heating=make_sap_heating(
|
||||
main_heating_details=[gas_boiler], water_heating_code=901,
|
||||
),
|
||||
)
|
||||
|
||||
# Act / Assert — 191 electric DHW assumes present; gas boiler does not.
|
||||
assert _cylinder_thermostat_present(epc_191, direct_electric) is True
|
||||
assert _cylinder_thermostat_present(epc_gas, gas_boiler) is False
|
||||
|
||||
|
||||
def test_heat_network_distribution_electricity_per_sap_10_2_appendix_c_3_2() -> None:
|
||||
# Arrange — heat-network main (Table 4a code 301 = community heating,
|
||||
# category 6). SAP 10.2 Appendix C §C3.2 (PDF p.51): distribution
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue