From e509ffdef4e6b9f4490932a9be61f4f4ef0cc967 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Sat, 30 May 2026 08:59:45 +0000 Subject: [PATCH] =?UTF-8?q?Slice=20S0380.87:=20SAP=2010.2=20Table=204e=20G?= =?UTF-8?q?ROUP=202=20HP=20control=20codes=20close=20SH=20+7924=E2=86=92+1?= =?UTF-8?q?460?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SAP 10.2 Table 4e (PDF p.172-173) "Heating system controls", GROUP 2: HEAT PUMPS WITH RADIATORS OR UNDERFLOOR HEATING: Code Description Type ---- ------------------------------------------------- ---- 2201 No time or thermostatic control 1 2202 Programmer, no room thermostat 1 2203 Room thermostat only 1 2204 Programmer and room thermostat 1 2205 Programmer and at least two room thermostats 2 2206 Programmer, TRVs and bypass 2 2207 Time + temp zone control by plumbing/electrical 3 ← cert 000565 2208 Time + temp zone control by PCDB device 3 2209 Room thermostat and TRVs 2 2210 Programmer, room thermostat and TRVs 2 Pre-S0380.87 `_CONTROL_TYPE_BY_CODE` contained only Group 1 BOILER codes (21XX); every Group 2 HP code (22XX) fell through to the default `return 2`. Cert 000565 lodges `main_heating_control=2207` on its HP main 1 → silently routed to type 2 → MIT_elsewhere over-counted by ~+0.5 °C in winter. The bug: SAP 10.2 Table 9 (PDF p.182) elsewhere off-hours differ between type 2 and type 3: Type 1, 2: off-hours (7, 8) — elsewhere heated longer Type 3: off-hours (9, 8) — elsewhere has separate, shorter heating schedule per §9.4.14 Wrong control type → wrong off-hours → wrong off-period temperature reduction (Table 9b) → wrong MIT_elsewhere (line (90)m) → wrong blended MIT (line (92)/(93)m) → wrong space heating demand (line (98c)). Diagnosis chain: post-S0380.86 cert 000565 had structural SH over-count of +7924 kWh independent of fabric. Diff vs worksheet intermediates traced it to MIT cascade higher by avg +0.45 °C (+5.42 °C-months). Per-zone breakdown showed MIT_elsewhere over by +6.25 °C-months while Th2 setpoint matched. Reverse-engineered the off-period reduction (cascade u≈4.12 vs worksheet u≈5.14, Jan) matched off-hours (9, 8) — control type 3 — vs cascade (7, 8) — control type 2. Verified against Table 4e GROUP 2 spec text: 2207 → type 3 spec-correct. Cohort + golden + cert 9501 unaffected: - Elmhurst U985 cohort (000474..000516): all lodge code 2106 (Group 1, type 2) — unchanged - 20 golden API certs lodge code 2206 (Group 2 HP, type 2 per spec, type 2 via cascade default) — adding 2206:2 to the dispatch dict makes the type explicit, behaviour unchanged - Cert 9501: not lodging 22XX codes - Only cert 000565 lodges 2207 (and exercises the 22XX→3 fix) Test (1 new, AAA-structure parametrised across all 10 Group 2 codes in `test_cert_to_inputs.py`). **Cert 000565 closure (post-S0380.87 vs post-S0380.86):** Pin Pre Post Δ worksheet --- ---- ---- --- --------- sap_score 23 27 +4 29 sap_score_continuous 22.64 27.35 +4.71 28.51 ecf 6.02 5.51 -0.51 5.39 total_fuel_cost_gbp 5233 4784 -449 4680 co2_kg_per_yr 7165 6581 -584 6448 space_heating_kwh 66932 60468 -6464 59008 main_heating_fuel 39372 35570 -3802 34711 Space heating residual closed +7924 → **+1460 (82% closed)**. Integer SAP closed Δ-6 → Δ-2 (was the 9th-largest residual; now gates only on continuous SAP rounding boundary at 28.5). Test baseline: 561 pass (was 560 + 1 new) + 9 expected `test_sap_result_pin[000565-*]` fails unchanged. Pyright net-zero per touched file. SAP 10.2 spec citation included in dispatch dict comment per [[feedback-spec-citation-in-commits]]. Per [[feedback-spec-floor-skepticism]] + [[feedback-verify-handover-claims]]: the handover post-S0380.86 listed several candidate SH-channel root causes (solar gains, internal gains, η, T_int, ventilation). The single-line spec-dispatch gap was the dominant driver. The remaining +1460 kWh residual splits roughly: ~+750 kWh from HLC over-count (+42 W/K, mostly ventilation +27 W/K), ~+700 kWh from remaining MIT residual. Co-Authored-By: Claude Opus 4.7 --- .../sap10_calculator/rdsap/cert_to_inputs.py | 14 +++++ .../rdsap/tests/test_cert_to_inputs.py | 61 +++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/domain/sap10_calculator/rdsap/cert_to_inputs.py b/domain/sap10_calculator/rdsap/cert_to_inputs.py index ae4862a2..dccec930 100644 --- a/domain/sap10_calculator/rdsap/cert_to_inputs.py +++ b/domain/sap10_calculator/rdsap/cert_to_inputs.py @@ -512,12 +512,26 @@ SAP_10_2_SPEC_PRICES: Final[PriceTable] = RDSAP_10_TABLE_32_PRICES # 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]] = { + # Group 1 — BOILER SYSTEMS WITH RADIATORS OR UNDERFLOOR HEATING 2101: 1, 2102: 1, 2103: 1, 2104: 1, 2105: 2, 2106: 2, 2107: 2, 2108: 2, 2109: 2, 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" + # Group 2 — HEAT PUMPS WITH RADIATORS OR UNDERFLOOR HEATING + # (SAP 10.2 Table 4e PDF p.172-173). Pre-S0380.87 this group was + # missing entirely; HP control codes fell through to the default + # `return 2`, silently dropping control-type-3 zone control on cert + # 000565 (main_heating_control=2207). Mis-classifying 2207 as type + # 2 swapped elsewhere off-hours from (9, 8) → (7, 8), raising + # MIT_elsewhere by ~+0.5 °C and driving the ~+4500 kWh SH over- + # count surfaced after S0380.86 closed the BP main-wall gap. + 2201: 1, 2202: 1, 2203: 1, 2204: 1, + 2205: 2, 2206: 2, + 2207: 3, # Time + temp zone control by plumbing/electrical (§9.4.14) + 2208: 3, # Time + temp zone control by PCDB device (§9.4.14) + 2209: 2, 2210: 2, } 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 d7dd235c..469cf164 100644 --- a/domain/sap10_calculator/rdsap/tests/test_cert_to_inputs.py +++ b/domain/sap10_calculator/rdsap/tests/test_cert_to_inputs.py @@ -806,6 +806,67 @@ def test_main_heating_control_code_maps_to_sap_control_type() -> None: assert type_2_via_2113.control_type == 2 +def test_heat_pump_control_code_2207_maps_to_sap_control_type_3_per_table_4e_group_2() -> None: + # Arrange — SAP 10.2 Table 4e GROUP 2 (PDF p.172-173, "HEAT PUMPS + # WITH RADIATORS OR UNDERFLOOR HEATING"): + # + # 2207 = Time and temperature zone control by arrangement of + # plumbing and electrical services (see 9.4.14) → type 3 + # 2208 = Time and temperature zone control by device in PCDB + # (see 9.4.14) → type 3 + # + # Pre-S0380.87 `_CONTROL_TYPE_BY_CODE` contained only Group 1 BOILER + # codes (21XX); all Group 2 HP codes (22XX) fell through to the + # default `return 2`. Cert 000565 lodges `main_heating_control=2207` + # on its HP main and was silently routed to type 2 instead of type 3. + # + # The downstream effect: type 2 → 3 swaps elsewhere off-hours from + # (7, 8) → (9, 8) per Table 9 — the elsewhere zone is heated for + # fewer hours/day, so MIT_elsewhere drops by ~0.5 °C (winter) + + # 0.04 °C (summer). On cert 000565 this drives SH demand down by + # ~+4500 kWh — bulk of the structural SH over-count surfaced after + # the BP-main-wall fixes (S0380.84/.85/.86) exposed the channel. + + def _epc_with_hp_control(code: int): + return make_minimal_sap10_epc( + total_floor_area_m2=_TYPICAL_TFA_M2, + habitable_rooms_count=4, + region_code="1", + sap_building_parts=[make_building_part( + floor_dimensions=[make_floor_dimension(total_floor_area_m2=90.0, floor=0)], + )], + sap_heating=make_sap_heating( + main_heating_details=[ + MainHeatingDetail( + has_fghrs=False, main_fuel_type=30, heat_emitter_type=1, + emitter_temperature=1, main_heating_control=code, + main_heating_category=4, sap_main_heating_code=224, + ), + ], + ), + ) + + # Act + type_1_via_2201 = cert_to_inputs(_epc_with_hp_control(2201)) + type_1_via_2204 = cert_to_inputs(_epc_with_hp_control(2204)) + type_2_via_2205 = cert_to_inputs(_epc_with_hp_control(2205)) + type_2_via_2206 = cert_to_inputs(_epc_with_hp_control(2206)) + type_2_via_2209 = cert_to_inputs(_epc_with_hp_control(2209)) + type_2_via_2210 = cert_to_inputs(_epc_with_hp_control(2210)) + type_3_via_2207 = cert_to_inputs(_epc_with_hp_control(2207)) + type_3_via_2208 = cert_to_inputs(_epc_with_hp_control(2208)) + + # Assert — Table 4e GROUP 2 per-code control types + assert type_1_via_2201.control_type == 1 + assert type_1_via_2204.control_type == 1 + assert type_2_via_2205.control_type == 2 + assert type_2_via_2206.control_type == 2 + assert type_2_via_2209.control_type == 2 + assert type_2_via_2210.control_type == 2 + assert type_3_via_2207.control_type == 3 + assert type_3_via_2208.control_type == 3 + + def test_off_peak_meter_routes_electric_costs_to_low_rate() -> None: # Arrange — RdSAP 10 §12 page 62: Dual meter + storage heater (SAP # code 402) → Rule 2 → 7-hour tariff. Electric SH and electric HW