Slice S0380.62: wire Table 32 standing charges into the off-peak cost fallback

The cascade's `additional_standing_charges_gbp(main_fuel_code,
water_heating_fuel_code, tariff)` function (table_32.py:178) was
already producing the right values — for cert 000565 it returns
£143 (£120 mains gas standing + £23 10-hour high-rate electricity
standing per Table 32 page 95). But the value only landed in
`FuelCostResult.additional_standing_charges_gbp` inside `_fuel_cost`,
which returns `_ZERO_FUEL_COST_FOR_OFF_PEAK` for non-STANDARD
tariff. The calculator then falls back to the inline cost math
(scalar fuel-cost × kWh) which had no standing-charge component →
£143 was silently dropped from the off-peak cost cascade.

New `CalculatorInputs.standing_charges_gbp: float = 0.0` field
carries the standing-charge total into the fallback path. The
inline cost summation adds it before max-clamp + PV credit.
STANDARD-tariff certs route via `fuel_cost.additional_standing_
charges_gbp` (set inside `_fuel_cost`) and the calculator ignores
this scalar on that path — no double-count.

`cert_to_inputs` populates the new field unconditionally; the value
is just zero on standard-tariff certs (Table 12 note (a) gates
standing-charge inclusion regardless).

Cert 000565 cascade impact:
- standing_charges_gbp = £143.00 ✓ (exact match to worksheet line 251)
- total_fuel_cost_gbp:  Δ −310 → −167  (46% reduction)
- sap_score_continuous: Δ +3.61 → +1.91 (47% reduction)
- co2_kg_per_yr:        Δ unchanged (standing charges don't bill CO2)

Cohort regression check: 427 pass + 10 expected 000565 fails. The
14 existing Elmhurst fixtures + JSON fixtures all have meter_type=
None → STANDARD → standing routes via FuelCostResult unchanged.

Spec source: RdSAP 10 Table 32 page 95 standing-charge column;
SAP 10.2 Table 12 note (a) inclusion gating.

Pyright net-zero on both files (0 / 34).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-28 23:58:35 +00:00
parent b732ceac83
commit e19145aca0
2 changed files with 17 additions and 0 deletions

View file

@ -300,6 +300,13 @@ class CalculatorInputs:
fuel_cost: FuelCostResult = field(
default_factory=lambda: _ZERO_FUEL_COST_RESULT
)
# Table 32 standing charges (electric off-peak high-rate code +
# mains gas) — added to `total_cost` when the calculator's off-
# peak fallback path fires. STANDARD-tariff certs route through
# `fuel_cost.additional_standing_charges_gbp` instead and ignore
# this field. cert_to_inputs sets this via `additional_standing_
# charges_gbp(main_fuel_code, water_heating_fuel_code, tariff)`.
standing_charges_gbp: float = 0.0
@dataclass(frozen=True)
@ -518,6 +525,7 @@ def calculate_sap_from_inputs(inputs: CalculatorInputs) -> SapResult:
+ hot_water_cost
+ pumps_fans_cost
+ lighting_cost
+ inputs.standing_charges_gbp
- pv_credit,
)
ecf = energy_cost_factor(total_cost_gbp=total_cost, total_floor_area_m2=tfa)

View file

@ -3405,6 +3405,15 @@ def cert_to_inputs(
other_fuel_cost_gbp_per_kwh=_other_fuel_cost_gbp_per_kwh(
_rdsap_tariff(epc), prices
),
# Table 32 standing charges for the off-peak fallback path.
# STANDARD-tariff certs route via `fuel_cost.additional_
# standing_charges_gbp` (set inside `_fuel_cost`) and the
# calculator ignores this scalar on that path.
standing_charges_gbp=additional_standing_charges_gbp(
main_fuel_code=_main_fuel_code(main),
water_heating_fuel_code=_water_heating_fuel_code(epc),
tariff=_rdsap_tariff(epc),
),
co2_factor_kg_per_kwh=_co2_factor_kg_per_kwh(main),
# SAP10.2 Table 12d (p.194) per-end-use effective CO2 factors. For
# electricity end-uses Σ(kWh_m × CO2_m) / Σ(kWh_m) replaces the