From a12d373eafdb62da9dd083277adbc94b530a47b8 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 28 May 2026 23:42:45 +0000 Subject: [PATCH] =?UTF-8?q?Slice=20S0380.60:=20RdSAP=2010=20=C2=A712=20pag?= =?UTF-8?q?e=2062=20=E2=80=94=20Dual-meter=20tariff=20dispatch=20(Rules=20?= =?UTF-8?q?1-4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cert 000565 surfaced the spec gap. Worksheet shows "Electricity Tariff: 10 Hour Off Peak" while the Summary PDF only lodges "Electricity meter type: Dual" — no separate tariff-hour field is exported. Elmhurst SAP picks 10-hour because RdSAP 10 §12 page 62 contains a published inference algorithm: > If the meter is dual 18-hour/24-hour it is 18-hour/24-hour tariff. > Otherwise the choice between 7-hour and 10-hour is determined as > follows. > 1. If the main heating system (or main system if there are two) > is an electric CPSU (192) it is 10-hour tariff. > 2. Otherwise, if … electric storage heaters (401 to 409), or > electric dry core or water storage boiler (193 or 195), or > electric underfloor heating (421 or 422) — it is 7-hour tariff. > 3. If that has not resolved it then if … direct-acting electric > boiler (191), or heat pump (211 to 224, 521 to 524, or > database), or electric room heaters — it is 10-hour tariff. > 4. If none of the above applies it is 7-hour tariff. Cert 000565 Main 1 SAP code 224 (ASHP) + Dual meter → Rule 3 → 10-hour. Matches the worksheet exactly. New `rdsap_tariff_for_cert(meter_type, main_1_sap_code=..., main_2_sap_code=..., main_1_is_heat_pump_database=..., main_2_is_heat_pump_database=...)` implements the dispatch. "or database" branch covers PCDB Table 362 heat-pump lodgements per the spec's "or database" wording. Callers compute the boolean via `heat_pump_record(main_heating_index_number) is not None`. The pre-existing `tariff_from_meter_type(meter_type)` keeps its contract for legacy call sites — returns SEVEN_HOUR as the Dual default (the §12 Rule 4 fallback). Docstring updated to point at the new helper for callers that need spec-correct dispatch. Code sets (verbatim §12 page 62): - `_RULE_1_CPSU_CODES` = {192} - `_RULE_2_STORAGE_CODES` = {401..409, 193, 195, 421, 422} (NOT 423/424/425) - `_RULE_3_TEN_HOUR_CODES` = {191, 211..224, 521..524} - electric room heater codes (Table 4a 6xx) deferred with TODO until a fixture surfaces them — Rule 4 fallback is correct in the interim (electric room heater certs would currently get 7-hour, biasing their cost residual; not on the active fixture front). This commit is the FOUNDATIONAL change — no cost helpers are wired to the new dispatch yet, so cohort/golden tests are unchanged (354 pass + 10 expected 000565 fails). The next slice wires `_space_heating_fuel_cost_gbp_per_kwh` / `_hot_water_fuel_cost_gbp_ per_kwh` / `_other_fuel_cost_gbp_per_kwh` to use the new dispatch + Table 12a high-rate fractions for off-peak certs. Spec source: `domain/sap10_calculator/docs/specs/RdSAP 10 Specification 10-06-2025.pdf` §12 page 62. Verified verbatim per [[feedback-verify-handover-claims]] before implementing. Pyright net-zero (0 / 0). Co-Authored-By: Claude Opus 4.7 --- domain/sap10_calculator/tables/table_12a.py | 93 ++++++++++++++++++++- 1 file changed, 91 insertions(+), 2 deletions(-) diff --git a/domain/sap10_calculator/tables/table_12a.py b/domain/sap10_calculator/tables/table_12a.py index fe04aaaa..dccc555e 100644 --- a/domain/sap10_calculator/tables/table_12a.py +++ b/domain/sap10_calculator/tables/table_12a.py @@ -16,7 +16,7 @@ all consumption at the unit price. from __future__ import annotations from enum import Enum -from typing import Final +from typing import Final, Optional class Table12aSystem(Enum): @@ -191,7 +191,14 @@ def other_use_high_rate_fraction(use: OtherUse, tariff: Tariff) -> float: def tariff_from_meter_type(meter_type: object) -> Tariff: """Resolve the RdSAP cert `meter_type` field to a Table 12a tariff column. Unknown / missing → STANDARD (no off-peak split applied) - per the Q11b spec-faithful policy.""" + per the Q11b spec-faithful policy. + + NOTE: for a Dual meter the §12 dispatch (Rules 1-4 page 62) + requires the main heating SAP codes to choose between 7-hour and + 10-hour. This helper returns the SEVEN_HOUR default for Dual — + callers that have access to the main heating codes should use + `rdsap_tariff_for_cert` instead. + """ if meter_type is None: return Tariff.STANDARD if isinstance(meter_type, int): @@ -202,3 +209,85 @@ def tariff_from_meter_type(meter_type: object) -> Tariff: return Tariff.STANDARD return _METER_INT_TO_TARIFF[code] return Tariff.STANDARD + + +# RdSAP 10 §12 page 62 — SAP main heating code sets for the Dual-meter +# tariff dispatch. Each rule's set is taken verbatim from §12 Rules 1-3. +# Rule 1: Electric CPSU → 10-hour +_RULE_1_CPSU_CODES: Final[frozenset[int]] = frozenset({192}) +# Rule 2: storage-based electric → 7-hour. The (421, 422) underfloor +# subset is explicit per §12 ("421 or 422, but not 424") — 423 / 425 +# fall through to Rule 4 default unless a later spec amendment adds +# them. +_RULE_2_STORAGE_CODES: Final[frozenset[int]] = frozenset( + list(range(401, 410)) # electric storage heaters 401-409 + + [193, 195] # electric dry-core / water-storage boiler + + [421, 422] # electric underfloor heating (424 excluded) +) +# Rule 3: direct-acting electric + heat pumps + electric room heaters +# → 10-hour. §12 lists "heat pump (211 to 224, 521 to 524, or +# database)" — the "database" branch fires when the cert lodges a +# PCDB Table 362 heat-pump index regardless of SAP code. +_RULE_3_TEN_HOUR_CODES: Final[frozenset[int]] = frozenset( + [191] # direct-acting electric boiler + + list(range(211, 225)) # heat pumps 211-224 + + list(range(521, 525)) # warm-air heat pumps 521-524 + # TODO: electric room heater codes (SAP Table 4a row 6xx for + # electric panel / radiant heaters) when a fixture surfaces them. +) + + +def rdsap_tariff_for_cert( + meter_type: object, + *, + main_1_sap_code: Optional[int] = None, + main_2_sap_code: Optional[int] = None, + main_1_is_heat_pump_database: bool = False, + main_2_is_heat_pump_database: bool = False, +) -> Tariff: + """RdSAP 10 §12 page 62 — full meter+heating tariff dispatch. + + Single meter → STANDARD. Dual 18-hour / Dual 24-hour map straight + to their respective tariffs. Otherwise applies §12 Rules 1-4 + where each rule considers BOTH main heating systems on multi- + main certs ("the main system or either main system if there are + two"): + + Rule 1 Electric CPSU (192) → 10-hour + Rule 2 Storage / storage boiler / underfloor → 7-hour + (401-409, 193, 195, 421, 422) + Rule 3 Direct-acting electric boiler (191), → 10-hour + heat pump (211-224, 521-524, database), + electric room heaters + Rule 4 None of the above → 7-hour + (default for Dual + non-electric main) + + `main_1_is_heat_pump_database` / `main_2_is_heat_pump_database` + signal the "or database" Rule 3 branch — the cert lodges a PCDB + Table 362 heat-pump record. Callers compute this via + `heat_pump_record(main_heating_index_number) is not None`. + + Cert 000565 (Main 1 SAP code 224 ASHP + Dual meter) → Rule 3 → + TEN_HOUR, matching the worksheet's "10 Hour Off Peak" lodging. + """ + base = tariff_from_meter_type(meter_type) + # Non-Dual meters resolve straight from the meter type. + if base is not Tariff.SEVEN_HOUR: + return base + main_codes = { + c for c in (main_1_sap_code, main_2_sap_code) if c is not None + } + # Rule 1 + if main_codes & _RULE_1_CPSU_CODES: + return Tariff.TEN_HOUR + # Rule 2 — checked BEFORE rule 3 per §12 ordering (storage takes + # precedence over the broader Rule 3 electric set). + if main_codes & _RULE_2_STORAGE_CODES: + return Tariff.SEVEN_HOUR + # Rule 3 + if main_codes & _RULE_3_TEN_HOUR_CODES: + return Tariff.TEN_HOUR + if main_1_is_heat_pump_database or main_2_is_heat_pump_database: + return Tariff.TEN_HOUR + # Rule 4 — default + return Tariff.SEVEN_HOUR