mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
fix(tariff): Unknown meter + storage/CPSU main → off-peak (§12)
Electric storage heaters (and CPSU) charge overnight and cannot run economically on a single rate, so their presence is physical evidence the dwelling is on an off-peak tariff. RdSAP 10 §12 (PDF p.62) applied Rules 1-4 only for a Dual meter; an "Unknown" (code 3) meter returned STANDARD without consulting the heating type, so a cat-7 storage main billed its overnight charge at the standard 13.19 p/kWh instead of the 7-hour low rate (5.50 p/kWh) — ~2.4x too high → large under-rate. Two coupled fixes: - `rdsap_tariff_for_cert`: for an Unknown meter, infer the off-peak tariff from a Rule-1 CPSU (→10-hour) or Rule-2 storage (→7-hour) main; keep STANDARD otherwise. Direct-acting/room heaters/heat pumps (Rule 3) are NOT off-peak evidence (run on demand, exist on single-rate meters) so they stay STANDARD — billing them 100% at the low rate over-credits. - `_fuel_cost` now resolves its tariff via the §12-aware `_rdsap_tariff` (not the raw `tariff_from_meter_type`), so the off-peak branch fires for these storage certs and the legacy scalar fields bill the low rate. Mirrors `_is_off_peak_meter`'s existing Unknown+electric heuristic (which already routes HW/secondary off-peak), closing the main-space-heating gap. Meter-3 electric cluster: mean |err| 11.18 → 6.52, within-1.0 3 → 5 (cert 7336 -26.1 → -0.16, 0380 -19.9 → +1.0). Eval headline 44.9% → 45.0%, mean |err| 1.82 → 1.76, mean signed -0.08 → +0.02. A few storage certs overshoot (other residuals the standard rate was masking). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
678aa7affd
commit
4d1a58b828
3 changed files with 110 additions and 19 deletions
|
|
@ -6114,8 +6114,14 @@ def _fuel_cost(
|
|||
is the natural extension point for the Table 12a `_SH_HIGH_RATE_
|
||||
FRACTION` lookup + `Table12aSystem` mapping (deferred per slice 3
|
||||
docs `Q11` follow-ups)."""
|
||||
meter_type = epc.sap_energy_source.meter_type
|
||||
tariff = tariff_from_meter_type(meter_type)
|
||||
# Use the §12-Rules-aware tariff (not the raw meter→tariff): it routes
|
||||
# an "Unknown" (code 3) meter with an electric storage / heat-pump /
|
||||
# room-heater main to its off-peak tariff (storage heaters can't run on
|
||||
# a single rate), so the off-peak branch below fires and the legacy
|
||||
# scalar fields bill the overnight charge at the low rate instead of
|
||||
# the standard 13.19 p/kWh. A non-electric Unknown-meter dwelling still
|
||||
# resolves STANDARD here, keeping the full §10a precompute.
|
||||
tariff = _rdsap_tariff(epc)
|
||||
if tariff is not Tariff.STANDARD:
|
||||
# Off-peak path defers to the legacy scalar fuel-cost fields on
|
||||
# CalculatorInputs (the pre-§10a `_space_heating_fuel_cost_gbp_
|
||||
|
|
|
|||
|
|
@ -263,6 +263,21 @@ _RULE_3_TEN_HOUR_CODES: Final[frozenset[int]] = frozenset(
|
|||
)
|
||||
|
||||
|
||||
def _meter_is_unknown(meter_type: object) -> bool:
|
||||
"""True when the meter is the RdSAP "Unknown" sentinel (code 3 / the
|
||||
"unknown" / "" / "3" string aliases) — the assessor did not record the
|
||||
tariff. Distinct from Single (code 2), an explicit single-rate
|
||||
lodgement. Mirrors `_is_off_peak_meter`'s code extraction so the main-
|
||||
heating tariff inference stays consistent with the HW/secondary path."""
|
||||
if isinstance(meter_type, bool):
|
||||
return False
|
||||
if isinstance(meter_type, int):
|
||||
return meter_type == 3
|
||||
if isinstance(meter_type, str):
|
||||
return meter_type.strip().lower() in {"unknown", "3", ""}
|
||||
return False
|
||||
|
||||
|
||||
def rdsap_tariff_for_cert(
|
||||
meter_type: object,
|
||||
*,
|
||||
|
|
@ -297,23 +312,46 @@ def rdsap_tariff_for_cert(
|
|||
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
|
||||
|
||||
def _rules_1_to_3() -> Optional[Tariff]:
|
||||
"""§12 Rules 1-3 — the explicit electric-system tariff matches.
|
||||
Returns None when no electric storage / CPSU / heat-pump / room-
|
||||
heater main is present (i.e. Rule 4 territory)."""
|
||||
# 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
|
||||
return None
|
||||
|
||||
# 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) are NOT off-peak evidence (they run on demand
|
||||
# and exist on single-rate meters too), so they keep STANDARD here
|
||||
# rather than being mis-billed 100% at the off-peak low rate. A
|
||||
# non-electric main also keeps STANDARD (no Rule 4 default — Unknown
|
||||
# must not force off-peak on a gas dwelling).
|
||||
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
|
||||
return Tariff.STANDARD
|
||||
# Single (code 2) or any other explicit non-off-peak meter.
|
||||
return base
|
||||
|
|
|
|||
|
|
@ -46,6 +46,53 @@ def test_dual_meter_electric_room_heater_resolves_to_ten_hour_tariff() -> None:
|
|||
assert rdsap_tariff_for_cert(1, main_1_sap_code=601) is Tariff.SEVEN_HOUR
|
||||
|
||||
|
||||
def test_unknown_meter_infers_off_peak_from_electric_storage_main() -> None:
|
||||
# Arrange — RdSAP 10 §12 (PDF p.62). An "Unknown" meter (code 3) was
|
||||
# not recorded by the assessor, but an electric STORAGE main (SAP
|
||||
# 401-409, Rule 2) or CPSU (192, Rule 1) 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 infer the §12
|
||||
# off-peak tariff rather than billing the overnight charge at the
|
||||
# standard rate. Certs 7336/2080 (cat-7 storage, meter 3) under-rated
|
||||
# ~25 SAP from standard-rate space heating.
|
||||
|
||||
# Act / Assert — storage (Rule 2) → 7-hour; CPSU (Rule 1) → 10-hour.
|
||||
assert rdsap_tariff_for_cert(3, main_1_sap_code=402) is Tariff.SEVEN_HOUR
|
||||
assert rdsap_tariff_for_cert(3, main_1_sap_code=192) is Tariff.TEN_HOUR
|
||||
|
||||
|
||||
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).
|
||||
|
||||
# Act / Assert
|
||||
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_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
|
||||
# (7-hour); it stays STANDARD. (The off-peak inference fires only when
|
||||
# a Rule 1/2 storage/CPSU system is present.)
|
||||
|
||||
# Act / Assert
|
||||
assert rdsap_tariff_for_cert(3, main_1_sap_code=102) is Tariff.STANDARD
|
||||
assert rdsap_tariff_for_cert(3, main_1_sap_code=None) is Tariff.STANDARD
|
||||
|
||||
|
||||
def test_single_meter_with_storage_stays_standard() -> None:
|
||||
# Arrange — code 2 (Single) is an EXPLICIT single-rate lodgement, not
|
||||
# "unknown", so it is NOT overridden even with a storage main: the
|
||||
# off-peak inference is only for the Unknown (code 3) sentinel.
|
||||
|
||||
# Act / Assert
|
||||
assert rdsap_tariff_for_cert(2, main_1_sap_code=402) is Tariff.STANDARD
|
||||
|
||||
|
||||
def test_tariff_enum_has_five_members() -> None:
|
||||
"""Table 12a columns: standard (no off-peak split), 7-hour, 10-hour,
|
||||
18-hour, 24-hour. Worksheet-shape fidelity: TEN_HOUR is included for
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue