From fd0530159facd9526cc334ddc231913facba1999 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 23 Jun 2026 10:09:59 +0000 Subject: [PATCH] =?UTF-8?q?feat(tariff):=20complete=20=C2=A712=20Unknown-m?= =?UTF-8?q?eter=20clause=20=E2=80=94=20GSHP/WSHP=20main=20triggers=20off-p?= =?UTF-8?q?eak=20(RdSAP=2010=20PDF=20p.62)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the follow-up from 22fe4f41. RdSAP 10 §12's third Unknown-meter exception bullet — "main heating is ground source or water source heat pump" — was unimplemented. Add `_GROUND_OR_WATER_SOURCE_HEAT_PUMP_CODES` (Table 4a, SAP 10.2 PDF p.176-177: ground 211/215/221/225 + warm-air 521/525; water 213/216/223/226 + warm-air 523/526) to the Unknown-meter off-peak triggers; once dual, Rules 1-4 (Rule 3) resolve it to 10-hour. AIR-source heat pumps (214/217/224/227, 524/527) are deliberately EXCLUDED — the spec names only ground/water source. Verified the only Unknown-meter heat pumps in the corpus are "3/10 Bedford House" (main 214 = AIR source), which correctly KEEP STANDARD. 0 corpus certs carry a GSHP/WSHP on an Unknown meter, so this is a spec-completeness forward guard (gauge unchanged 72.5% / 0.789), same family as the Scotland-J wall / rafters-M roof 0-impact spec fixes. Coverage gap noted in-code: a database-index heat pump without a 211/213-style SAP code can't have its source type read from the code alone (rare). Spec-pinned (test_unknown_meter_ground_or_water_source_heat_pump_triggers_off_ peak). Gates green: corpus 72.5%/0.789, batch worksheet 0 raised/0 diverge, suite 2989 passed (2 known pre-existing fails). pyright not installed locally. Co-Authored-By: Claude Opus 4.8 (1M context) --- domain/sap10_calculator/tables/table_12a.py | 22 ++++++++++++++++++- .../domain/sap10_calculator/test_table_12a.py | 19 ++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/domain/sap10_calculator/tables/table_12a.py b/domain/sap10_calculator/tables/table_12a.py index d79d9fbe..72ea8f7b 100644 --- a/domain/sap10_calculator/tables/table_12a.py +++ b/domain/sap10_calculator/tables/table_12a.py @@ -262,6 +262,19 @@ _RULE_3_TEN_HOUR_CODES: Final[frozenset[int]] = frozenset( + [691, 692, 693, 694, 699] # electric room heaters (Table 4a) ) +# §12 Unknown-meter exception: "main heating is ground source or water source +# heat pump" makes the dwelling dual even though the same heat pump on a Single +# meter stays standard. This is the GROUND/WATER-source subset of Table 4a +# (SAP 10.2 PDF p.176-177): ground 211/215/221/225 + warm-air 521/525, water +# 213/216/223/226 + warm-air 523/526. AIR-source (214/217/224/227, 524/527) is +# DELIBERATELY EXCLUDED — the spec names only ground/water source. A heat pump +# lodged via a PCDB database index without one of these SAP codes can't have +# its source type read from the code alone (coverage gap: rare, 0 corpus certs). +_GROUND_OR_WATER_SOURCE_HEAT_PUMP_CODES: Final[frozenset[int]] = frozenset( + {211, 215, 221, 225, 521, 525} # ground source (radiator + warm-air) + | {213, 216, 223, 226, 523, 526} # water source (radiator + warm-air) +) + def _meter_is_unknown(meter_type: object) -> bool: """True when the meter is the RdSAP "Unknown" sentinel (code 3 / the @@ -367,11 +380,18 @@ def rdsap_tariff_for_cert( # immersion); every other Unknown+dual-immersion cert already has a # storage main (Rule 2). Single-immersion 691 certs (Flat 7, Flat 2) and # whc-909 instantaneous certs correctly STAY standard — they carry no - # text-box off-peak system. (GSHP/WSHP-main trigger: separate follow-up.) + # text-box off-peak system. The §12 third exception bullet ("main heating + # is ground source or water source heat pump") is the + # `_GROUND_OR_WATER_SOURCE_HEAT_PUMP_CODES` check — AIR-source heat pumps + # (214/224 etc.) are NOT a trigger and stay standard (e.g. corpus certs + # "3/10 Bedford House", main 214 ASHP on Unknown meters — verified 2026-06- + # 23 they keep STANDARD). No corpus cert carries a GSHP/WSHP on an Unknown + # meter, so this trigger is a spec-completeness forward guard (0 impact). if _meter_is_unknown(meter_type): off_peak_evidence = ( bool(main_codes & _RULE_1_CPSU_CODES) or bool(main_codes & _RULE_2_STORAGE_CODES) + or bool(main_codes & _GROUND_OR_WATER_SOURCE_HEAT_PUMP_CODES) or water_is_off_peak_dual_immersion ) if off_peak_evidence: diff --git a/tests/domain/sap10_calculator/test_table_12a.py b/tests/domain/sap10_calculator/test_table_12a.py index 8ce42b09..9a70dbfa 100644 --- a/tests/domain/sap10_calculator/test_table_12a.py +++ b/tests/domain/sap10_calculator/test_table_12a.py @@ -106,6 +106,25 @@ def test_unknown_meter_dual_electric_immersion_triggers_off_peak_via_rules() -> ) is Tariff.STANDARD +def test_unknown_meter_ground_or_water_source_heat_pump_triggers_off_peak() -> None: + # Arrange — RdSAP 10 §12 (PDF p.62) third Unknown-meter exception bullet: + # "main heating is ground source or water source heat pump." Per SAP 10.2 + # Table 4a (PDF p.176-177) ground source = 211/215/221/225 (+ warm-air + # 521/525), water source = 213/216/223/226 (+ warm-air 523/526); AIR + # source (214/224 etc.) is NOT named, so it stays standard. Once the GSHP/ + # WSHP makes the meter dual, Rules 1-4 (Rule 3, heat pump 211-224) → 10h. + # 0 corpus certs carry a GSHP/WSHP on an Unknown meter (the only Unknown- + # meter HPs are 3/10 Bedford House, main 214 = AIR source) — this is a + # spec-completeness forward guard. + + # Act / Assert — ground/water source → 10-hour; air source → STANDARD. + assert rdsap_tariff_for_cert(3, main_1_sap_code=211) is Tariff.TEN_HOUR + assert rdsap_tariff_for_cert(3, main_1_sap_code=213) is Tariff.TEN_HOUR + assert rdsap_tariff_for_cert(3, main_1_sap_code=521) is Tariff.TEN_HOUR + assert rdsap_tariff_for_cert(3, main_1_sap_code=214) is Tariff.STANDARD + assert rdsap_tariff_for_cert(3, main_1_sap_code=224) is Tariff.STANDARD + + def test_unknown_meter_with_non_electric_main_stays_standard() -> None: # Arrange — an "Unknown" meter on a GAS-heated dwelling (SAP 102) has # no off-peak evidence, so it must NOT pick up the Rule-4 Dual default