From 9338914f8a8187a3f53357c606d7042e088602b6 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Fri, 29 May 2026 22:32:59 +0000 Subject: [PATCH] =?UTF-8?q?Slice=20S0380.81:=20RdSAP=2010=20Table=2032=20d?= =?UTF-8?q?efault=20prices=20close=20cert=20000565=20sap=5Fscore=2028=20?= =?UTF-8?q?=E2=86=92=2029=20EXACT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../sap10_calculator/rdsap/cert_to_inputs.py | 30 ++++++-- .../rdsap/tests/test_cert_to_inputs.py | 71 +++++++++++++++---- 2 files changed, 84 insertions(+), 17 deletions(-) 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)" + )