Slice S0380.60: RdSAP 10 §12 page 62 — Dual-meter tariff dispatch (Rules 1-4)

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 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-28 23:42:45 +00:00 committed by Jun-te Kim
parent 71d9738749
commit a12d373eaf

View file

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