Slice S0380.87: SAP 10.2 Table 4e GROUP 2 HP control codes close SH +7924→+1460

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 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-30 08:59:45 +00:00
parent 6c8bbbc9e2
commit c0328f4e18
2 changed files with 75 additions and 0 deletions

View file

@ -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,
}

View file

@ -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