diff --git a/domain/sap10_calculator/rdsap/cert_to_inputs.py b/domain/sap10_calculator/rdsap/cert_to_inputs.py index efc2d20a..c7218cd8 100644 --- a/domain/sap10_calculator/rdsap/cert_to_inputs.py +++ b/domain/sap10_calculator/rdsap/cert_to_inputs.py @@ -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), diff --git a/domain/sap10_calculator/rdsap/tests/test_cert_to_inputs.py b/domain/sap10_calculator/rdsap/tests/test_cert_to_inputs.py index 21aa00ef..97c0cc83 100644 --- a/domain/sap10_calculator/rdsap/tests/test_cert_to_inputs.py +++ b/domain/sap10_calculator/rdsap/tests/test_cert_to_inputs.py @@ -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)" + )