Slice S0380.81: RdSAP 10 Table 32 default prices close cert 000565 sap_score 28 → 29 EXACT

Per ADR-0010 §10a amendment (2026-05-21) + RdSAP 10 Specification §19.1
(PDF page 80-81):

  "The SAP rating for RdSAP 10 is to be calculated using Table 32 prices
   (not Table 12) for section 10a and 10b."

The §10a `fuel_cost` orchestrator already used RdSAP 10 Table 32 prices
for STANDARD-tariff certs (via `table_32_unit_price_p_per_kwh`). The
legacy off-peak fallback scalar path on `CalculatorInputs.*_fuel_cost_
gbp_per_kwh` (which fires when `tariff is not STANDARD` →
`_ZERO_FUEL_COST_FOR_OFF_PEAK`) was reading from `prices.unit_price_p_
per_kwh`, which was still wired to SAP 10.2 Table 12 prices via the
`SAP_10_2_SPEC_PRICES` PriceTable constant. For cert 000565 (Dual meter
→ TEN_HOUR tariff + mains-gas DHW via WHC 914), this leaked Table 12's
3.64 p/kWh mains gas rate (vs Table 32's 3.48) into HW cost — a £6
over-count on 3755 HW kWh that landed continuous SAP 0.041 below the
28.5 integer rounding boundary, flipping sap_score 29 → 28.

Verbatim Table 32 (PDF page 95) rows touched by this slice:
  Mains gas             3.48 p/kWh   (Table 12 was 3.64)
  7-hour low tariff     5.50 p/kWh   (Table 12 was 9.40)
  Standard electricity  13.19 p/kWh  (Table 12 was 16.49)

Fix is one PriceTable constant — `RDSAP_10_TABLE_32_PRICES` wires
`table_32_unit_price_p_per_kwh` + the 5.50 / 13.19 scalars per the
amendment. `SAP_10_2_SPEC_PRICES` becomes a back-compat alias so existing
`prices=SAP_10_2_SPEC_PRICES` test imports continue working but route
through Table 32 at the call site.

Cert 000565 movements at HEAD (this commit):
- sap_score:               28 → **29 ✓ EXACT**  (was Δ−1)
- sap_score_continuous:    28.4680 → 28.5355  (Δ−0.041 → Δ+0.027)
- total_fuel_cost_gbp:     4683.88 → 4677.87  (Δ+3.62 → Δ−2.39)
- ecf:                     5.3910 → 5.3841    (Δ+0.004 → Δ−0.003)
- hot_water_kwh_per_yr:    3755.03 = 3755.03 ✓ EXACT (unchanged)

Cumulative cert 000565 closure across S0380.77/78/79/80/81:
- hot_water_kwh:          +1399 → +260 → −238 → 0 → 0 ✓
- sap_score (integer):    +1 → −1 → 0 → −1 → 0 ✓ EXACT
- sap_score_continuous:   +0.60 → −0.035 → +0.057 → −0.041 → +0.027

Cohort impact: STANDARD-tariff certs (the 6 U985 fixtures
000474/000477/000480/000487/000490/000516 and all cohort-2/golden gas
combi certs) route through the §10a orchestrator that already used
Table 32 — zero shift. Off-peak certs in the test suite are cert 000565
only at this point (Dual / TEN_HOUR); golden cohort unaffected. Three
existing scalar-pin tests in `test_cert_to_inputs.py` re-pinned to
Table 32 values:
- `test_gas_heating_with_electric_immersion_charges_hw_at_electricity_
   rate` (0.0364 → 0.0348 gas; 0.1649 → 0.1319 std elec)
- `test_off_peak_meter_routes_electric_costs_to_low_rate` (0.094 → 0.055
   7-hour low fallback)
- `test_standard_meter_keeps_electric_costs_on_standard_rate` (0.1649 →
   0.1319 std elec)

New test pins the rule:
`test_rdsap_10_table_32_prices_charge_mains_gas_hot_water_at_3p48_per_kwh`
quotes the §19.1 spec and asserts cert 000565 HW £/kWh = 0.0348.

Test baseline: 553 pass + 8 expected `test_sap_result_pin[000565-*]`
fails (was 551 + 9; sap_score now closes EXACT). The remaining 8 fails
on cert 000565 are the documented work-queue residuals — RR fold-in
(space_heating, main_heating_fuel), Table 12d/12e dual-rate blend for
lighting + CO2, MEV PCDB record (pumps_fans). Pyright net-zero on
touched files (45 errors, matching baseline).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-29 22:32:59 +00:00
parent 69710f5882
commit 9338914f8a
2 changed files with 84 additions and 17 deletions

View file

@ -92,7 +92,6 @@ from domain.sap10_calculator.tables.table_12 import (
co2_factor_kg_per_kwh,
pe_monthly_factors_kwh_per_kwh,
primary_energy_factor,
unit_price_p_per_kwh,
)
from domain.sap10_calculator.tables.table_12a import (
OtherUse,
@ -471,14 +470,35 @@ _SPEC_E7_ELIGIBLE_MAIN_CODES: Final[frozenset[int]] = frozenset(
)
SAP_10_2_SPEC_PRICES: Final[PriceTable] = PriceTable(
unit_price_p_per_kwh=unit_price_p_per_kwh,
e7_low_rate_p_per_kwh=9.40,
standard_electricity_p_per_kwh=16.49,
# RdSAP 10 Table 32 (PDF page 95) — the canonical SAP-rating price set per
# the RdSAP 10 §19.1 spec text:
#
# "The SAP rating for RdSAP 10 is to be calculated using Table 32 prices
# (not Table 12) for section 10a and 10b."
#
# Table 32 mains gas = 3.48 p/kWh (vs SAP 10.2 Table 12 = 3.64);
# 7-hour low = 5.50 p/kWh (vs Table 12 = 9.40);
# standard electricity = 13.19 p/kWh (vs Table 12 = 16.49).
#
# Wired into `cert_to_inputs` as the default PriceTable per ADR-0010
# §10a amendment (2026-05-21). Off-peak fallback scalars
# (`hot_water_fuel_cost_gbp_per_kwh` etc.) read `unit_price_p_per_kwh`
# directly so this is where the cohort-wide tariff lands.
RDSAP_10_TABLE_32_PRICES: Final[PriceTable] = PriceTable(
unit_price_p_per_kwh=table_32_unit_price_p_per_kwh,
e7_low_rate_p_per_kwh=5.50, # Table 32 code 31 (7-hour low)
standard_electricity_p_per_kwh=13.19, # Table 32 code 30
e7_eligible_main_codes=_SPEC_E7_ELIGIBLE_MAIN_CODES,
)
# Legacy alias retained so existing imports keep working. Per ADR-0010
# §10a amendment the SAP rating uses Table 32 prices, NOT SAP 10.2
# Table 12 — the name is preserved for back-compat; both constants point
# at the same Table 32 PriceTable instance.
SAP_10_2_SPEC_PRICES: Final[PriceTable] = RDSAP_10_TABLE_32_PRICES
# SAP 10.2 Table 4e (page 171) main_heating_control codes → control type
# (1/2/3 per Table 9 "Heating control type" column). Type drives the
# elsewhere-zone off-hours pattern in Table 9: types 1+2 use (7, 8),

View file

@ -751,12 +751,14 @@ def test_gas_heating_with_electric_immersion_charges_hw_at_electricity_rate() ->
# Assert — gas main → space heating at gas rate; HW switches to electric
# rate when water_heating_fuel is electric; lighting/pumps always electric.
assert inputs_gas.space_heating_fuel_cost_gbp_per_kwh == 0.0364
assert inputs_gas.hot_water_fuel_cost_gbp_per_kwh == 0.0364
assert inputs_gas.other_fuel_cost_gbp_per_kwh == 0.1649
assert inputs_hw.space_heating_fuel_cost_gbp_per_kwh == 0.0364
assert inputs_hw.hot_water_fuel_cost_gbp_per_kwh == 0.1649
assert inputs_hw.other_fuel_cost_gbp_per_kwh == 0.1649
# Prices are RdSAP 10 Table 32 (PDF p.95) per the ADR-0010 §10a amendment:
# mains gas = 3.48 p/kWh; standard electricity (code 30) = 13.19 p/kWh.
assert inputs_gas.space_heating_fuel_cost_gbp_per_kwh == 0.0348
assert inputs_gas.hot_water_fuel_cost_gbp_per_kwh == 0.0348
assert inputs_gas.other_fuel_cost_gbp_per_kwh == 0.1319
assert inputs_hw.space_heating_fuel_cost_gbp_per_kwh == 0.0348
assert inputs_hw.hot_water_fuel_cost_gbp_per_kwh == 0.1319
assert inputs_hw.other_fuel_cost_gbp_per_kwh == 0.1319
def test_main_heating_control_code_maps_to_sap_control_type() -> None:
@ -812,7 +814,8 @@ def test_off_peak_meter_routes_electric_costs_to_low_rate() -> None:
# now apply Table 12a Grid 2 ALL_OTHER_USES + SEVEN_HOUR = 0.90
# high → blended 0.90 * 15.29 + 0.10 * 5.50 = 14.311 p/kWh per
# Slice S0380.61 (was 16.49 under the pre-Table-12a empirical
# override).
# override). RdSAP 10 Table 32 (PDF p.95) per ADR-0010 §10a
# amendment: 7-hour low (code 31) = 5.50 p/kWh.
epc = make_minimal_sap10_epc(
total_floor_area_m2=_TYPICAL_TFA_M2,
habitable_rooms_count=3,
@ -846,8 +849,8 @@ def test_off_peak_meter_routes_electric_costs_to_low_rate() -> None:
inputs = cert_to_inputs(epc)
# Assert
assert inputs.space_heating_fuel_cost_gbp_per_kwh == 0.094
assert inputs.hot_water_fuel_cost_gbp_per_kwh == 0.094
assert inputs.space_heating_fuel_cost_gbp_per_kwh == 0.055
assert inputs.hot_water_fuel_cost_gbp_per_kwh == 0.055
assert abs(inputs.other_fuel_cost_gbp_per_kwh - 0.14311) < 1e-5
@ -1266,9 +1269,11 @@ def test_standard_meter_keeps_electric_costs_on_standard_rate() -> None:
inputs = cert_to_inputs(epc)
# Assert — no off-peak routing; all-electric dwelling pays standard rates.
assert inputs.space_heating_fuel_cost_gbp_per_kwh == 0.1649
assert inputs.hot_water_fuel_cost_gbp_per_kwh == 0.1649
assert inputs.other_fuel_cost_gbp_per_kwh == 0.1649
# RdSAP 10 Table 32 (PDF p.95) standard electricity (code 30) = 13.19
# p/kWh per ADR-0010 §10a amendment.
assert inputs.space_heating_fuel_cost_gbp_per_kwh == 0.1319
assert inputs.hot_water_fuel_cost_gbp_per_kwh == 0.1319
assert inputs.other_fuel_cost_gbp_per_kwh == 0.1319
def test_mid_floor_flat_dwelling_type_zeroes_floor_and_roof_heat_transmission() -> None:
@ -2083,3 +2088,45 @@ def test_air_source_heat_pump_main_heating_zeroes_table_3a_combi_loss_per_sap_4_
f"month {month_idx}: combi loss {value!r} should be 0 for "
f"non-combi main heating per SAP 10.2 §4 line 7702"
)
def test_rdsap_10_table_32_prices_charge_mains_gas_hot_water_at_3p48_per_kwh() -> None:
"""RdSAP 10 Specification §19.1 (PDF page 80-81) — the §10a fuel-cost
block uses RdSAP 10 Table 32 (PDF page 95) prices, NOT SAP 10.2
Table 12 (PDF page 189):
"The SAP rating for RdSAP 10 is to be calculated using Table 32
prices (not Table 12) for section 10a and 10b."
Table 32 row "Mains gas" = 3.48 p/kWh; the SAP 10.2 Table 12 row is
3.64 p/kWh. Per ADR-0010 amendment (2026-05-21), the §10a orchestrator
already targets Table 32. This pin closes the residual gap on the
legacy off-peak scalar fallback `inputs.hot_water_fuel_cost_gbp_per_
kwh` so it ALSO reads Table 32 the cohort's `prices` PriceTable
callable must return 3.48 for mains-gas DHW.
Cert 000565 lodges Dual meter (Tariff.TEN_HOUR) + gas-combi DHW via
WHC 914 (mains gas Table 32 code 1). It hits the off-peak fallback
branch in the calculator (`fuel_cost is _ZERO_FUEL_COST_FOR_OFF_PEAK`)
so this scalar IS the consumed cost the +£6 over-count on the
cohort handover trace is exactly the £0.16 p/kWh × 3755 HW kWh
delta. Closes cert 000565 sap_score 28 29 EXACT at the 28.5
rounding boundary.
"""
# Arrange — mapper-driven cohort fixture (Summary_000565 → cert_to_
# inputs), Dual meter / mains gas DHW.
from domain.sap10_calculator.worksheet.tests import (
_elmhurst_worksheet_000565 as _w000565,
)
epc = _w000565.build_epc()
# Act
inputs = cert_to_inputs(epc)
# Assert — HW £/kWh equals Table 32 mains gas (3.48 p/kWh = 0.0348
# £/kWh), NOT Table 12 (3.64 p/kWh = 0.0364 £/kWh).
assert abs(inputs.hot_water_fuel_cost_gbp_per_kwh - 0.0348) <= 1e-6, (
f"hot_water_fuel_cost_gbp_per_kwh = "
f"{inputs.hot_water_fuel_cost_gbp_per_kwh!r}, expected 0.0348 per "
f"RdSAP 10 Table 32 mains gas (§19.1 amendment, ADR-0010)"
)