mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
fix(tariff): Unknown meter + dual electric immersion → off-peak per §12 (RdSAP 10 PDF p.62)
Supersedes the previous "verified non-fix" doc (3548f1f3): the spec DOES make
this a fix — Khalim was right that the Unknown-meter branch is driven by the
heating/water system, not a blanket STANDARD.
RdSAP 10 §12 (PDF p.62): "If the electricity meter is unknown, treat as single
meter EXCEPT where main heating OR WATER HEATING are intended to run off an
off-peak tariff (per systems listed in the text box above) ... If that results
in a dual meter, assign tariff per rules 1 to 4." The text-box off-peak systems
include DUAL ELECTRIC IMMERSION. Our `rdsap_tariff_for_cert` only triggered the
Unknown→off-peak exception on a storage/CPSU MAIN — it ignored the
dual-electric-immersion WATER-heating trigger, so an Unknown-meter dwelling
with a non-storage main (e.g. room heaters) + dual immersion was billed
STANDARD (13.19p flat) when it should be dual → Rules 1-4 on the main.
Fix: thread `water_is_off_peak_dual_immersion` (whc 903 + immersion lodged dual
via `_immersion_is_single is False`) into the Unknown-meter branch; when any
text-box trigger is present, resolve via the same Rules 1-4 dispatch (room
heaters → Rule 3 → 10-hour). Single-immersion / instantaneous (whc 909) certs
correctly stay STANDARD (no text-box system).
Worksheet-validated on "simulated case 48" (main 691 + Unknown meter + 903 dual
immersion): Elmhurst 10-Hour Off Peak, SAP 57; ours 45 → 55 (7-hour gives 45,
confirming 10-hour). Flips exactly ONE corpus cert — Apartment 241 (the genuine
-5.38 under-rater, main 691 + dual immersion) -5.38 → -1.05; every other
Unknown+dual-immersion cert already has a storage main (Rule 2). Corpus
within-0.5 holds 72.5%, MAE 0.793 → 0.789 (improved). CO2/PE unchanged.
GSHP/WSHP-main trigger (the other §12 Unknown exception bullet) is a separate
follow-up. Gates green: corpus 72.5%/0.789, batch worksheet 0 raised/0 diverge,
000565 e2e 11/11, suite 2987 passed (2 known pre-existing fails). pyright not
installed in this container — strict type gate not run locally.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3548f1f31a
commit
22fe4f41b8
3 changed files with 88 additions and 34 deletions
|
|
@ -1741,12 +1741,24 @@ def _rdsap_tariff(epc: EpcPropertyData) -> Tariff:
|
|||
and heat_pump_record(detail.main_heating_index_number) is not None
|
||||
)
|
||||
|
||||
# §12 Unknown-meter exception: "water heating ... intended to run off an
|
||||
# off-peak tariff" via the text-box "dual electric immersion" system —
|
||||
# whc 903 (HW from a separate electric immersion) lodged as dual
|
||||
# (`_immersion_is_single` is False). This makes an Unknown-meter dwelling
|
||||
# "dual" even when the main is a single-rate-capable system (e.g. room
|
||||
# heaters), per RdSAP 10 §12 (PDF p.62).
|
||||
water_dual_immersion = (
|
||||
_int_or_none(epc.sap_heating.water_heating_code) == _WHC_ELECTRIC_IMMERSION
|
||||
and _immersion_is_single(epc) is False
|
||||
)
|
||||
|
||||
return rdsap_tariff_for_cert(
|
||||
epc.sap_energy_source.meter_type,
|
||||
main_1_sap_code=main_1.sap_main_heating_code if main_1 else None,
|
||||
main_2_sap_code=main_2.sap_main_heating_code if main_2 else None,
|
||||
main_1_is_heat_pump_database=_hp_db(main_1),
|
||||
main_2_is_heat_pump_database=_hp_db(main_2),
|
||||
water_is_off_peak_dual_immersion=water_dual_immersion,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -285,6 +285,7 @@ def rdsap_tariff_for_cert(
|
|||
main_2_sap_code: Optional[int] = None,
|
||||
main_1_is_heat_pump_database: bool = False,
|
||||
main_2_is_heat_pump_database: bool = False,
|
||||
water_is_off_peak_dual_immersion: bool = False,
|
||||
) -> Tariff:
|
||||
"""RdSAP 10 §12 page 62 — full meter+heating tariff dispatch.
|
||||
|
||||
|
|
@ -308,6 +309,12 @@ def rdsap_tariff_for_cert(
|
|||
Table 362 heat-pump record. Callers compute this via
|
||||
`heat_pump_record(main_heating_index_number) is not None`.
|
||||
|
||||
`water_is_off_peak_dual_immersion` signals the §12 Unknown-meter
|
||||
exception "water heating ... intended to run off an off-peak tariff"
|
||||
via the text-box "dual electric immersion" system (whc 903 + dual
|
||||
immersion). On an Unknown meter this is enough to make the dwelling
|
||||
"dual"; the 7-/10-hour choice then follows Rules 1-4 on the main.
|
||||
|
||||
Cert 000565 (Main 1 SAP code 224 ASHP + Dual meter) → Rule 3 →
|
||||
TEN_HOUR, matching the worksheet's "10 Hour Off Peak" lodging.
|
||||
"""
|
||||
|
|
@ -337,36 +344,38 @@ def rdsap_tariff_for_cert(
|
|||
# Dual meter — §12 Rules 1-4, where Rule 4 is the 7-hour default.
|
||||
if base is Tariff.SEVEN_HOUR:
|
||||
return _rules_1_to_3() or Tariff.SEVEN_HOUR
|
||||
# "Unknown" meter (code 3): the assessor didn't record the tariff, but
|
||||
# an electric CPSU (Rule 1) or STORAGE (Rule 2) main is physical
|
||||
# evidence the dwelling is on an off-peak tariff — these charge
|
||||
# overnight at the low rate and cannot run economically on a single
|
||||
# rate, so the tariff is implied. Direct-acting electric / room heaters
|
||||
# / heat pumps (Rule 3) keep STANDARD here. A non-electric main also
|
||||
# keeps STANDARD (no Rule 4 default — Unknown must not force off-peak
|
||||
# on a gas dwelling).
|
||||
# "Unknown" meter (code 3, inaccessible): §12 (PDF p.62) — "treat as
|
||||
# single meter EXCEPT where: main heating OR WATER HEATING are intended to
|
||||
# run off an off-peak tariff (per systems listed in the text box above) or
|
||||
# main heating is ground source or water source heat pump. If that results
|
||||
# in a dual meter, assign tariff per rules 1 to 4." The text-box off-peak
|
||||
# systems are electric storage heaters (401-409), underfloor (421/422),
|
||||
# dry-core/water-storage boiler (193/195), CPSU (192), and DUAL ELECTRIC
|
||||
# IMMERSION. So the off-peak trigger is NOT "any electric main" — a
|
||||
# direct-acting / room-heater main on its own keeps the dwelling on a
|
||||
# single meter (STANDARD); it only goes off-peak when one of the text-box
|
||||
# systems is present. Once triggered, the meter becomes "dual" and the
|
||||
# 7-hour/10-hour choice is made by the SAME Rules 1-4 on the main heating
|
||||
# (so e.g. room heaters + dual immersion → Rule 3 → 10-hour).
|
||||
#
|
||||
# VERIFIED NON-FIX (2026-06-23, "simulated case 48" Elmhurst worksheet):
|
||||
# Elmhurst DOES resolve an Unknown meter + room-heater main (SAP 691) to
|
||||
# 10-Hour Off Peak (its Rule 3) — a hand-built case-48 Summary (main 691,
|
||||
# Unknown meter, 903 dual electric immersion) scores Elmhurst SAP 57; our
|
||||
# STANDARD gives 45, routing Unknown+Rule3 to off-peak gives 55 (7-hour
|
||||
# gives 45 — confirms Elmhurst uses 10-hour). So the "Rule 3 is not off-
|
||||
# peak evidence" intuition is WRONG about Elmhurst's behaviour. BUT
|
||||
# adopting it REGRESSES the lodged-register corpus (72.5%->71.8%, MAE
|
||||
# 0.793->0.827): of 11 Unknown+Rule3 corpus certs only ONE improves
|
||||
# (Apartment 241 -5.38->-1.05); the other 10 overshoot +2.7..+9.1 (Flat 2
|
||||
# +9.11). The register's meter_type=3 certs were lodged with STANDARD-
|
||||
# tariff costing (their low ratings only reconcile at 13.19p flat) — the
|
||||
# gov-API "Unknown" is lossy and does NOT mean off-peak was used. Since
|
||||
# our north star is reproducing the lodged register (not Elmhurst's
|
||||
# deliberate-Unknown worksheet path), KEEP STANDARD. Same "Elmhurst != the
|
||||
# noisy register" family as roof-windows/shutters. Do NOT re-litigate.
|
||||
# Worksheet-validated (2026-06-23, Khalim's "simulated case 48": main 691
|
||||
# room heaters + Unknown meter + 903 DUAL electric immersion): Elmhurst
|
||||
# SAP 57; ours 45 when this stayed STANDARD, 55 once the dual-immersion
|
||||
# trigger routes it through Rule 3 → 10-hour (7-hour gives 45 — confirms
|
||||
# 10-hour). The dual-immersion trigger flips exactly ONE corpus cert
|
||||
# (Apartment 241, the genuine -5.38 under-rater, main 691 + 903 dual
|
||||
# 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.)
|
||||
if _meter_is_unknown(meter_type):
|
||||
if main_codes & _RULE_1_CPSU_CODES:
|
||||
return Tariff.TEN_HOUR
|
||||
if main_codes & _RULE_2_STORAGE_CODES:
|
||||
return Tariff.SEVEN_HOUR
|
||||
off_peak_evidence = (
|
||||
bool(main_codes & _RULE_1_CPSU_CODES)
|
||||
or bool(main_codes & _RULE_2_STORAGE_CODES)
|
||||
or water_is_off_peak_dual_immersion
|
||||
)
|
||||
if off_peak_evidence:
|
||||
return _rules_1_to_3() or Tariff.SEVEN_HOUR
|
||||
return Tariff.STANDARD
|
||||
# Single (code 2) or any other explicit non-off-peak meter.
|
||||
return base
|
||||
|
|
|
|||
|
|
@ -62,17 +62,50 @@ def test_unknown_meter_infers_off_peak_from_electric_storage_main() -> None:
|
|||
|
||||
|
||||
def test_unknown_meter_does_not_infer_off_peak_for_room_heater_or_heat_pump() -> None:
|
||||
# Arrange — direct-acting electric room heaters (Rule 3, SAP 691) and
|
||||
# heat pumps run ON DEMAND and exist on single-rate meters too, so they
|
||||
# are NOT evidence of an off-peak tariff. On an Unknown meter they keep
|
||||
# STANDARD — billing them 100% at the off-peak low rate would
|
||||
# over-credit (room heaters draw mostly at the high rate).
|
||||
# Arrange — RdSAP 10 §12 (PDF p.62): on an Unknown meter the off-peak
|
||||
# exception fires only when a text-box off-peak SYSTEM is present (storage
|
||||
# / underfloor / dry-core / CPSU main, OR dual electric immersion water
|
||||
# heating, OR GSHP/WSHP main). A direct-acting room-heater (Rule 3, SAP
|
||||
# 691) or heat-pump main is NOT by itself such a system — absent a
|
||||
# dual-immersion (or GSHP/WSHP) trigger the dwelling stays on a single
|
||||
# meter (STANDARD). The 7-/10-hour choice in Rules 1-4 only applies ONCE
|
||||
# the meter is already established as dual.
|
||||
|
||||
# Act / Assert
|
||||
# Act / Assert — room heater / heat pump with no dual-immersion trigger.
|
||||
assert rdsap_tariff_for_cert(3, main_1_sap_code=691) is Tariff.STANDARD
|
||||
assert rdsap_tariff_for_cert(3, main_1_is_heat_pump_database=True) is Tariff.STANDARD
|
||||
|
||||
|
||||
def test_unknown_meter_dual_electric_immersion_triggers_off_peak_via_rules() -> None:
|
||||
# Arrange — RdSAP 10 §12 (PDF p.62): "If the electricity meter is unknown,
|
||||
# treat as single meter EXCEPT where ... water heating [is] intended to
|
||||
# run off an off-peak tariff (per systems listed in the text box above)"
|
||||
# — the text box lists DUAL ELECTRIC IMMERSION. "If that results in a dual
|
||||
# meter, assign tariff per rules 1 to 4." So an Unknown meter + dual
|
||||
# electric immersion makes the dwelling dual; the 7-/10-hour choice then
|
||||
# follows Rules 1-4 on the MAIN heating. Worksheet-validated on Khalim's
|
||||
# "simulated case 48" (main 691 room heaters + Unknown meter + 903 dual
|
||||
# immersion → Elmhurst 10-Hour Off Peak, SAP 57; ours 45→55). Corpus cert
|
||||
# "Apartment 241" (main 691 + dual immersion) moved -5.38 → -1.05.
|
||||
|
||||
# Act / Assert — dual immersion + room-heater main → Rule 3 → 10-hour;
|
||||
# + storage main → Rule 2 → 7-hour (already off-peak); + gas main → Rule 4
|
||||
# default → 7-hour. WITHOUT the dual-immersion flag, the room-heater main
|
||||
# stays STANDARD (single electric immersion is not a trigger).
|
||||
assert rdsap_tariff_for_cert(
|
||||
3, main_1_sap_code=691, water_is_off_peak_dual_immersion=True
|
||||
) is Tariff.TEN_HOUR
|
||||
assert rdsap_tariff_for_cert(
|
||||
3, main_1_sap_code=402, water_is_off_peak_dual_immersion=True
|
||||
) is Tariff.SEVEN_HOUR
|
||||
assert rdsap_tariff_for_cert(
|
||||
3, main_1_sap_code=102, water_is_off_peak_dual_immersion=True
|
||||
) is Tariff.SEVEN_HOUR
|
||||
assert rdsap_tariff_for_cert(
|
||||
3, main_1_sap_code=691, water_is_off_peak_dual_immersion=False
|
||||
) 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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue