From 36a3219dfb538ba9ecf1240aa22154d98d6c9657 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 28 May 2026 10:04:34 +0000 Subject: [PATCH] =?UTF-8?q?Slice=20S0380.25:=20SAP=20codes=202111/2113=20a?= =?UTF-8?q?re=20type=202=20not=20type=203=20=E2=80=94=20closes=200652=20+?= =?UTF-8?q?=206835?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per SAP 10.2 spec page 171 Table 4e "Heating system controls" — boiler systems with radiators (Group 1): 2110: "Time and temperature zone control by arrangement of plumbing and electrical services" → type 3 2111: "TRVs and bypass" → type 2 2112: "Time and temperature zone control by device in PCDB" → type 3 2113: "Room thermostat and TRVs" → type 2 `_CONTROL_TYPE_BY_CODE` previously bucketed 2111 + 2113 with the type 3 codes, but neither lodges any time-zone control — they're TRV-class controls (closer to programmer + room thermostat). The misclassification propagated through SAP 10.2 Table 9 to swap the elsewhere-zone off-period pattern from (7, 8) to (9, 8) — i.e. the spec's "heating 0700-0900 and 1800-2300" pattern (footnote b) instead of "heating 0700-0900 and 1600-2300" (footnote a). Under-counted MIT by ~0.67 °C across the year, dropping space-heating demand and over-predicting SAP: - cert 0652-3022-1205-2826-1200: +1.93 → -1e-5 - cert 6835-3920-2509-0933-5226: +0.72 → +0.015 Cohort-2 outcome (38 certs, Summary path): exact (<1e-4): 21 → **22** (+1: cert 0652 closes) ≤±0.07: 13 → **14** (+1: cert 6835 moves from ±0.5..1) ±0.5..1: 2 → **1** (-1: cert 6835 closes out) ±1..5: 1 → **0** (-1: cert 0652 closes out) No cohort-1 regressions (all certs there use codes 2106 / 2206; neither uses 2111/2113). Pyright net-zero (cert_to_inputs.py 35→35, test 13→13). Tests: 704 pass (existing control-type test extended; +2 new assertions for codes 2111/2113), 10 expected fails unchanged. Co-Authored-By: Claude Opus 4.7 --- .../sap10_calculator/rdsap/cert_to_inputs.py | 20 ++++++++++++++----- .../rdsap/tests/test_cert_to_inputs.py | 10 ++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/domain/sap10_calculator/rdsap/cert_to_inputs.py b/domain/sap10_calculator/rdsap/cert_to_inputs.py index 5f7c2310..fcf2ed75 100644 --- a/domain/sap10_calculator/rdsap/cert_to_inputs.py +++ b/domain/sap10_calculator/rdsap/cert_to_inputs.py @@ -412,15 +412,25 @@ SAP_10_2_SPEC_PRICES: Final[PriceTable] = PriceTable( ) -# SAP 10.2 Table 9 main_heating_control codes → control type (1/2/3). +# SAP 10.2 Table 4e (page 171) main_heating_control codes → control type +# (1/2/3 per Table 9 "Heating control type" column). Type drives the +# elsewhere-zone off-hours pattern in Table 9: types 1+2 use (7, 8), +# type 3 uses (9, 8) per footnote (b) "heating 0700-0900 and 1800-2300". +# # Type 1: no time + temp control, or one but not both. -# Type 2: programmer + room thermostat (+/− TRVs). -# Type 3: time-and-temperature zone control (e.g. separate living-zone -# programmer + thermostat). +# Type 2: programmer + room thermostat (+/− TRVs); also bare TRV-class +# controls (2111 "TRVs and bypass", 2113 "Room thermostat and +# TRVs") — these were misclassified as type 3 pre-S0380.25 and +# pushed cert 0652 to +1.93 SAP / cert 6835 to +0.72. +# Type 3: time-and-temperature zone control (separate living-zone +# schedule via plumbing/electrical arrangement or PCDB device). _CONTROL_TYPE_BY_CODE: Final[dict[int, int]] = { 2101: 1, 2102: 1, 2103: 1, 2104: 1, 2105: 2, 2106: 2, 2107: 2, 2108: 2, 2109: 2, - 2110: 3, 2111: 3, 2112: 3, 2113: 3, + 2110: 3, + 2111: 2, # TRVs and bypass — Table 4e row "2 0" + 2112: 3, + 2113: 2, # Room thermostat and TRVs — Table 4e row "2 0" } diff --git a/domain/sap10_calculator/rdsap/tests/test_cert_to_inputs.py b/domain/sap10_calculator/rdsap/tests/test_cert_to_inputs.py index c1a0f59a..5f154fc1 100644 --- a/domain/sap10_calculator/rdsap/tests/test_cert_to_inputs.py +++ b/domain/sap10_calculator/rdsap/tests/test_cert_to_inputs.py @@ -707,11 +707,21 @@ def test_main_heating_control_code_maps_to_sap_control_type() -> None: type_1 = cert_to_inputs(_epc_with_control(2103)) type_2 = cert_to_inputs(_epc_with_control(2106)) type_3 = cert_to_inputs(_epc_with_control(2110)) + # Slice S0380.25: SAP 10.2 Table 4e (page 171) classifies codes 2111 + # ("TRVs and bypass") and 2113 ("Room thermostat and TRVs") as + # control type 2, NOT type 3 — they lack the time-zone control that + # type 3 requires. Mis-classifying these as type 3 swapped the + # elsewhere-zone off-hours from (7, 8) to (9, 8), under-counting MIT + # by ~0.67 °C → cert 0652 +1.93 SAP / cert 6835 +0.72 SAP. + type_2_via_2111 = cert_to_inputs(_epc_with_control(2111)) + type_2_via_2113 = cert_to_inputs(_epc_with_control(2113)) # Assert assert type_1.control_type == 1 assert type_2.control_type == 2 assert type_3.control_type == 3 + assert type_2_via_2111.control_type == 2 + assert type_2_via_2113.control_type == 2 def test_off_peak_meter_routes_electric_costs_to_low_rate() -> None: