From 5e7ef5c7ffa3e78ab8a0112a229ebca28595bfab Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 8 Jun 2026 13:17:15 +0000 Subject: [PATCH] =?UTF-8?q?fix(control):=20no=20boiler=20interlock=20for?= =?UTF-8?q?=20TRVs+bypass=20controls=202107/2111=20(SAP=20=C2=A79.4.11)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `_BOILER_NO_ROOM_THERMOSTAT_CONTROL_CODES` held only {2101, 2102} — it was keyed off the Table 4e "+0.6 °C" annotation rather than the actual interlock criterion. SAP 10.2 §9.4.11 (PDF p.66): "A boiler system with no room thermostat (or a device equivalent in this context, such as a flow switch or boiler energy manager) ... must be considered as having no interlock", and "TRVs alone ... do not perform the boiler interlock function". A fixed bypass likewise provides no interlock (it keeps water circulating when TRVs close). So control 2107 ("Programmer, TRVs and bypass") and 2111 ("TRVs and bypass") lack interlock and must take the Table 4c(2) −5pp Space+DHW seasonal-efficiency adjustment and the Table 4f footnote a) ×1.3 circulation-pump uplift — both of which they previously missed. (2108 flow switch / 2109 boiler energy manager carry interlock-equivalent devices → excluded; 2103-2106/2113 have a room thermostat.) All affected certs are cat-2 gas boilers, where §9.4.11 applies. Eval: 909 computed, 45.3% → 46.9% within 0.5 (+14 certs: 412 → 426), mean|err| 1.659 → 1.633. Bucket means corrected: control 2107 +1.50 → +0.32 (n=38), 2111 +1.48 → +0.16 (n=4). 32 improved / 10 regressed (all small; the six that crossed out of ±0.5 were coincidentally-accurate offsetting-error certs). Co-Authored-By: Claude Opus 4.8 --- .../sap10_calculator/rdsap/cert_to_inputs.py | 44 ++++++++++++------- .../rdsap/test_cert_to_inputs.py | 38 ++++++++++++++++ 2 files changed, 66 insertions(+), 16 deletions(-) diff --git a/domain/sap10_calculator/rdsap/cert_to_inputs.py b/domain/sap10_calculator/rdsap/cert_to_inputs.py index 5298b0c5..29730560 100644 --- a/domain/sap10_calculator/rdsap/cert_to_inputs.py +++ b/domain/sap10_calculator/rdsap/cert_to_inputs.py @@ -385,7 +385,9 @@ def _table_4f_circulation_pump_kwh(main: Optional[MainHeatingDetail]) -> float: 2 → 41 kWh (2013 or later) Table 4f footnote a) then multiplies the row by 1.3 when the room - thermostat is absent (control code 2101 / 2102). + thermostat is absent — the same "no room thermostat" criterion as the + interlock rule, i.e. `_BOILER_NO_ROOM_THERMOSTAT_CONTROL_CODES` + (2101 / 2102 / 2107 / 2111; bypass and TRVs are not a room thermostat). """ if not _is_wet_boiler_main(main): return 0.0 @@ -1267,22 +1269,32 @@ _CONTROL_TYPE_BY_CODE: Final[dict[int, int]] = { } -# SAP 10.2 Table 4e Group 1 (PDF p.171) — boiler control codes providing -# NO thermostatic control of room temperature, i.e. no room thermostat -# ("No time or thermostatic control of room temperature" 2101 / -# "Programmer, no room thermostat" 2102 — the two Group-1 rows carrying -# the "+0.6 °C / Table 4c(2)" annotation). Per RdSAP 10 §3 (PDF p.57) -# boiler interlock is "assumed present if there is a room thermostat and -# (for stored hot water systems heated by the boiler) a cylinder -# thermostat. Otherwise not interlocked." A gas/liquid-fuel boiler under -# one of these controls therefore has NO boiler interlock regardless of -# the cylinder thermostat, triggering the Table 4c(2) (PDF p.169) "No -# thermostatic control of room temperature – regular boiler" -5pp Space -# + DHW seasonal-efficiency adjustment. The combi rows of Table 4c(2) -# take Space -5 / DHW 0; the DHW leg is gated separately on a cylinder -# being present (regular boiler) at the call site. +# SAP 10.2 Table 4e Group 1 (PDF p.171) — boiler control codes with NO +# boiler interlock because they lack a room thermostat (or an equivalent +# device). SAP 10.2 §9.4.11 (PDF p.66) is explicit: "A boiler system with +# no room thermostat (or a device equivalent in this context, such as a +# flow switch or boiler energy manager), even if there is a cylinder +# thermostat, must be considered as having no interlock", and "TRVs alone +# (other than some communicating TRVs) do not perform the boiler interlock +# function". A *fixed bypass* likewise provides no interlock — it exists to +# keep water circulating when the TRVs close. The Group-1 rows without a +# room thermostat / flow switch / boiler energy manager are therefore: +# 2101 "No time or thermostatic control of room temperature" +# 2102 "Programmer, no room thermostat" +# 2107 "Programmer, TRVs and bypass" ← bypass ≠ interlock +# 2111 "TRVs and bypass" ← bypass ≠ interlock +# (2108 "Programmer, TRVs and flow switch" and 2109 "… boiler energy +# manager" carry an interlock-equivalent device, so they are INTERLOCKED +# and excluded; 2103-2106/2113 all carry a room thermostat.) Each of these +# triggers the Table 4c(2) (PDF p.169) "No thermostatic control of room +# temperature – regular boiler" -5pp Space + DHW seasonal-efficiency +# adjustment. The combi rows of Table 4c(2) take Space -5 / DHW 0; the DHW +# leg is gated separately on a cylinder being present at the call site. +# NB this is the interlock criterion only — the separate "+0.6 °C" Table 4e +# temperature adjustment applies to 2101/2102 alone (it lives in +# `_CONTROL_TEMPERATURE_ADJUSTMENT_BY_CODE`, where 2107/2111 stay at 0.0). _BOILER_NO_ROOM_THERMOSTAT_CONTROL_CODES: Final[frozenset[int]] = frozenset( - {2101, 2102} + {2101, 2102, 2107, 2111} ) diff --git a/tests/domain/sap10_calculator/rdsap/test_cert_to_inputs.py b/tests/domain/sap10_calculator/rdsap/test_cert_to_inputs.py index 08a25d75..a00fca28 100644 --- a/tests/domain/sap10_calculator/rdsap/test_cert_to_inputs.py +++ b/tests/domain/sap10_calculator/rdsap/test_cert_to_inputs.py @@ -4720,6 +4720,44 @@ def test_table_4c_no_boiler_interlock_applies_minus_5_dhw_adjustment_when_cylind ) +def test_controls_2107_2111_count_as_no_room_thermostat_per_sap_9_4_11() -> None: + # Arrange — SAP 10.2 §9.4.11 (PDF p.66): a boiler with no room + # thermostat (or an equivalent device — flow switch / boiler energy + # manager) has no interlock; "TRVs alone ... do not perform the boiler + # interlock function" and a fixed bypass exists precisely to keep water + # circulating when the TRVs close. Control 2107 ("Programmer, TRVs and + # bypass") and 2111 ("TRVs and bypass") therefore carry the same + # no-room-thermostat treatment as 2101/2102 — including the Table 4f + # footnote a) ×1.3 circulation-pump uplift — which they previously + # missed (the set held only 2101/2102). Control 2106 ("Programmer, room + # thermostat and TRVs") HAS a room thermostat → interlock → no uplift. + # A 2013+ wet gas boiler pumps 41 kWh (Table 4f); ×1.3 = 53.3. + import dataclasses + + from domain.sap10_calculator.rdsap.cert_to_inputs import _table_4f_circulation_pump_kwh # pyright: ignore[reportPrivateUsage] + + base = _gas_boiler_detail() # cat-2 wet boiler + no_stat_2107 = dataclasses.replace( + base, main_heating_control=2107, central_heating_pump_age=2, + ) + no_stat_2111 = dataclasses.replace( + base, main_heating_control=2111, central_heating_pump_age=2, + ) + with_stat_2106 = dataclasses.replace( + base, main_heating_control=2106, central_heating_pump_age=2, + ) + + # Act + pump_2107 = _table_4f_circulation_pump_kwh(no_stat_2107) # pyright: ignore[reportPrivateUsage] + pump_2111 = _table_4f_circulation_pump_kwh(no_stat_2111) # pyright: ignore[reportPrivateUsage] + pump_2106 = _table_4f_circulation_pump_kwh(with_stat_2106) # pyright: ignore[reportPrivateUsage] + + # Assert — bypass/TRVs-only controls get the ×1.3 uplift; 2106 does not. + assert abs(pump_2107 - 41.0 * 1.3) <= 1e-9 + assert abs(pump_2111 - 41.0 * 1.3) <= 1e-9 + assert abs(pump_2106 - 41.0) <= 1e-9 + + def test_sap_9_4_11_no_boiler_interlock_applies_minus_5_pcdb_space_heating_when_main_is_gas_oil_boiler_with_cylinder_no_thermostat() -> None: """SAP 10.2 §9.4.11 (PDF p.30) "Boiler interlock":