Slice 30: §10a fuel costs cascade pin (192/192) + electric-shower plumb

Adds `fuel_cost_section_from_cert(epc)` (delegates to `cert_to_inputs`
which already wires `_fuel_cost` with full upstream context). Pins
(240a)..(255) — 32 line refs × 6 fixtures = 192 cascade pins, all PASS.

Three calculator changes needed for closure:

1. Electric shower (247a) — for 000487 the cert lodges 1 electric shower
   and the PDF reports (247a) = 79.3036 GBP (= (64a)m × std electricity
   price). The §4 cascade already computes electric-shower kWh via
   App J step 8 (slice 25d); now exposed on `WaterHeatingResult` as
   `electric_shower_kwh_per_yr` and plumbed into `_fuel_cost`. The
   instant-shower input was previously hardcoded to 0.

2. (241a/241b) main 2 + (242a/242b) secondary fractions — when a row's
   kWh is zero the PDF reports BOTH high/low fractions as 0 (not 1/0).
   `_split` in fuel_cost now zeros both fractions when kwh_per_yr <= 0.
   Cost columns already collapse via multiplication, so this is
   presentation-only.

3. (242a/242b) secondary fractions for 000474 — same pattern: when no
   secondary system is lodged, both fractions = 0.

Adds §10a LINE_ constants to all 6 fixtures. Extracted from
`sap worksheets/U985-0001-NNNNNN.txt` PDF blocks.

Cascade scoreboard: 468/468 → 660/660 (§7..§10a closed).
e2e SapResult: 6 remaining failures (all `co2_kg_per_yr`, await §12).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-24 00:42:52 +00:00
parent 049694e1e6
commit 74bfac049a
10 changed files with 339 additions and 7 deletions

View file

@ -1042,6 +1042,22 @@ def fabric_energy_efficiency_from_cert(epc: EpcPropertyData) -> Optional[float]:
)
def fuel_cost_section_from_cert(
epc: EpcPropertyData,
) -> Optional[FuelCostResult]:
"""SAP 10.2 §10a cert→inputs cascade for `fuel_cost`. Off-peak certs
return the zero sentinel (Table 12a high-rate-fraction split deferred).
For STANDARD-tariff certs returns the full (240)..(255) FuelCostResult.
Composes via `cert_to_inputs(epc)` `_fuel_cost` is invoked there with
all upstream §4/§5/§6/§7/§8/§9a values plumbed in. Returns None when
TFA missing.
"""
if epc.total_floor_area_m2 is None:
return None
return cert_to_inputs(epc).fuel_cost
def energy_requirements_section_from_cert(
epc: EpcPropertyData,
) -> Optional[EnergyRequirementsResult]:
@ -1388,6 +1404,7 @@ def _fuel_cost(
pumps_fans_kwh: float,
lighting_kwh: float,
cooling_kwh: float,
electric_shower_kwh: float = 0.0,
) -> FuelCostResult:
"""SAP10.2 §10a fuel-cost precompute — produce a `FuelCostResult` from
the cert + the §9a `energy_requirements_result`. RdSAP10 target per
@ -1443,19 +1460,25 @@ def _fuel_cost(
tariff=tariff,
)
# Worksheet display convention: when a row's kWh is zero (no main 2, no
# secondary system, etc.) the PDF reports the (high-rate fraction)
# column as 0 rather than 1. Cost columns already collapse to 0 via
# multiplication, so this is presentation-only.
main_2_kwh = energy_requirements_result.main_2_fuel_kwh_per_yr
secondary_kwh = energy_requirements_result.secondary_fuel_kwh_per_yr
return fuel_cost(
main_1_kwh_per_yr=energy_requirements_result.main_1_fuel_kwh_per_yr,
main_1_high_rate_gbp_per_kwh=main_1_high_rate_gbp_per_kwh,
main_1_low_rate_gbp_per_kwh=0.0,
main_1_high_rate_fraction=1.0,
main_2_kwh_per_yr=energy_requirements_result.main_2_fuel_kwh_per_yr,
main_2_kwh_per_yr=main_2_kwh,
main_2_high_rate_gbp_per_kwh=main_1_high_rate_gbp_per_kwh,
main_2_low_rate_gbp_per_kwh=0.0,
main_2_high_rate_fraction=1.0,
secondary_kwh_per_yr=energy_requirements_result.secondary_fuel_kwh_per_yr,
main_2_high_rate_fraction=1.0 if main_2_kwh > 0.0 else 0.0,
secondary_kwh_per_yr=secondary_kwh,
secondary_high_rate_gbp_per_kwh=secondary_high_rate_gbp_per_kwh,
secondary_low_rate_gbp_per_kwh=0.0,
secondary_high_rate_fraction=1.0,
secondary_high_rate_fraction=1.0 if secondary_kwh > 0.0 else 0.0,
hot_water_kwh_per_yr=hot_water_kwh,
hot_water_high_rate_gbp_per_kwh=water_high_rate_gbp_per_kwh,
hot_water_low_rate_gbp_per_kwh=0.0,
@ -1464,8 +1487,8 @@ def _fuel_cost(
lighting_kwh_per_yr=lighting_kwh,
cooling_kwh_per_yr=cooling_kwh,
other_uses_gbp_per_kwh=other_uses_gbp_per_kwh,
instant_shower_kwh_per_yr=0.0,
instant_shower_gbp_per_kwh=0.0,
instant_shower_kwh_per_yr=electric_shower_kwh,
instant_shower_gbp_per_kwh=other_uses_gbp_per_kwh,
pv_generation_kwh_per_yr=_pv_generation_kwh_per_yr(epc),
pv_export_credit_gbp_per_kwh=pv_export_credit_gbp_per_kwh,
additional_standing_charges_gbp=standing,
@ -1781,6 +1804,9 @@ def cert_to_inputs(
fuel_cost=_fuel_cost(
epc=epc,
main=main,
electric_shower_kwh=(
wh_result.electric_shower_kwh_per_yr if wh_result is not None else 0.0
),
energy_requirements_result=energy_requirements_result,
hot_water_kwh=hw_kwh,
pumps_fans_kwh=pumps_fans_kwh,

View file

@ -37,7 +37,20 @@ def _split(
high_rate_fraction: float,
) -> _OffPeakSplit:
"""Off-peak split arithmetic shared by main 1 / main 2 / secondary /
water-heating rows. (240c)=Q×frac×P_high, (240d)=Q×(1-frac)×P_low."""
water-heating rows. (240c)=Q×frac×P_high, (240d)=Q×(1-frac)×P_low.
Worksheet display convention: when the row's kWh is zero (e.g. no main
2 system) the PDF reports BOTH fractions as 0 rather than 1/0. Cost
columns already collapse to 0 via the kWh×fraction multiplications,
so this is presentation-only the math is unaffected."""
if kwh_per_yr <= 0.0:
return _OffPeakSplit(
high_rate_fraction=0.0,
low_rate_fraction=0.0,
high_rate_cost=0.0,
low_rate_cost=0.0,
total=0.0,
)
low_rate_fraction = 1.0 - high_rate_fraction
high_rate_cost = kwh_per_yr * high_rate_fraction * high_rate_gbp_per_kwh
low_rate_cost = kwh_per_yr * low_rate_fraction * low_rate_gbp_per_kwh

View file

@ -450,3 +450,41 @@ LINE_213_ANNUAL_KWH: float = 0.0000
LINE_215_M_SECONDARY_FUEL_KWH: tuple[float, ...] = (0.0,) * 12
LINE_215_ANNUAL_KWH: float = 0.0000
LINE_221_COOLING_FUEL_KWH: float = 0.0000
# ============================================================================
# §10a Fuel costs — using Table 12 (RdSAP10 Table 32) prices
# ============================================================================
# STANDARD-tariff fixture — off-peak splits collapse (high_rate_fraction=1,
# low_rate_fraction=0, high_rate_cost = total).
LINE_240A_MAIN_1_HIGH_RATE_FRACTION: float = 1.0000
LINE_240B_MAIN_1_LOW_RATE_FRACTION: float = 0.0000
LINE_240C_MAIN_1_HIGH_RATE_COST: float = 416.3783
LINE_240D_MAIN_1_LOW_RATE_COST: float = 0.0000
LINE_240E_MAIN_1_OTHER_FUEL_COST: float = 0.0000
LINE_240_MAIN_1_TOTAL_COST: float = 416.3783
LINE_241A_MAIN_2_HIGH_RATE_FRACTION: float = 0.0000
LINE_241B_MAIN_2_LOW_RATE_FRACTION: float = 0.0000
LINE_241C_MAIN_2_HIGH_RATE_COST: float = 0.0000
LINE_241D_MAIN_2_LOW_RATE_COST: float = 0.0000
LINE_241E_MAIN_2_OTHER_FUEL_COST: float = 0.0000
LINE_241_MAIN_2_TOTAL_COST: float = 0.0000
LINE_242A_SECONDARY_HIGH_RATE_FRACTION: float = 0.0000
LINE_242B_SECONDARY_LOW_RATE_FRACTION: float = 0.0000
LINE_242C_SECONDARY_HIGH_RATE_COST: float = 0.0000
LINE_242D_SECONDARY_LOW_RATE_COST: float = 0.0000
LINE_242E_SECONDARY_OTHER_FUEL_COST: float = 0.0000
LINE_242_SECONDARY_TOTAL_COST: float = 0.0000
LINE_243_WATER_HIGH_RATE_FRACTION: float = 1.0000
LINE_244_WATER_LOW_RATE_FRACTION: float = 0.0000
LINE_245_WATER_HIGH_RATE_COST: float = 79.7539
LINE_246_WATER_LOW_RATE_COST: float = 0.0000
LINE_247_WATER_OTHER_FUEL_COST: float = 0.0000
LINE_247A_INSTANT_SHOWER_COST: float = 0.0000
LINE_248_SPACE_COOLING_COST: float = 0.0000
LINE_249_PUMPS_FANS_COST: float = 21.1040
LINE_250_LIGHTING_COST: float = 18.4588
LINE_251_STANDING_CHARGES: float = 120.0000
LINE_252_PV_CREDIT: float = 0.0000
LINE_253_APPENDIX_Q_SAVED: float = 0.0000
LINE_254_APPENDIX_Q_USED: float = 0.0000
LINE_255_TOTAL_COST: float = 655.6949

View file

@ -419,3 +419,39 @@ LINE_215_M_SECONDARY_FUEL_KWH: tuple[float, ...] = (
)
LINE_215_ANNUAL_KWH: float = 1011.1202
LINE_221_COOLING_FUEL_KWH: float = 0.0000
# ============================================================================
# §10a Fuel costs — using Table 12 (RdSAP10 Table 32) prices
# ============================================================================
LINE_240A_MAIN_1_HIGH_RATE_FRACTION: float = 1.0000
LINE_240B_MAIN_1_LOW_RATE_FRACTION: float = 0.0000
LINE_240C_MAIN_1_HIGH_RATE_COST: float = 357.4298
LINE_240D_MAIN_1_LOW_RATE_COST: float = 0.0000
LINE_240E_MAIN_1_OTHER_FUEL_COST: float = 0.0000
LINE_240_MAIN_1_TOTAL_COST: float = 357.4298
LINE_241A_MAIN_2_HIGH_RATE_FRACTION: float = 0.0000
LINE_241B_MAIN_2_LOW_RATE_FRACTION: float = 0.0000
LINE_241C_MAIN_2_HIGH_RATE_COST: float = 0.0000
LINE_241D_MAIN_2_LOW_RATE_COST: float = 0.0000
LINE_241E_MAIN_2_OTHER_FUEL_COST: float = 0.0000
LINE_241_MAIN_2_TOTAL_COST: float = 0.0000
LINE_242A_SECONDARY_HIGH_RATE_FRACTION: float = 1.0000
LINE_242B_SECONDARY_LOW_RATE_FRACTION: float = 0.0000
LINE_242C_SECONDARY_HIGH_RATE_COST: float = 133.3668
LINE_242D_SECONDARY_LOW_RATE_COST: float = 0.0000
LINE_242E_SECONDARY_OTHER_FUEL_COST: float = 0.0000
LINE_242_SECONDARY_TOTAL_COST: float = 133.3668
LINE_243_WATER_HIGH_RATE_FRACTION: float = 1.0000
LINE_244_WATER_LOW_RATE_FRACTION: float = 0.0000
LINE_245_WATER_HIGH_RATE_COST: float = 73.6381
LINE_246_WATER_LOW_RATE_COST: float = 0.0000
LINE_247_WATER_OTHER_FUEL_COST: float = 0.0000
LINE_247A_INSTANT_SHOWER_COST: float = 0.0000
LINE_248_SPACE_COOLING_COST: float = 0.0000
LINE_249_PUMPS_FANS_COST: float = 21.1040
LINE_250_LIGHTING_COST: float = 26.6010
LINE_251_STANDING_CHARGES: float = 120.0000
LINE_252_PV_CREDIT: float = 0.0000
LINE_253_APPENDIX_Q_SAVED: float = 0.0000
LINE_254_APPENDIX_Q_USED: float = 0.0000
LINE_255_TOTAL_COST: float = 732.1396

View file

@ -461,3 +461,39 @@ LINE_215_M_SECONDARY_FUEL_KWH: tuple[float, ...] = (
)
LINE_215_ANNUAL_KWH: float = 1239.8578
LINE_221_COOLING_FUEL_KWH: float = 0.0000
# ============================================================================
# §10a Fuel costs — using Table 12 (RdSAP10 Table 32) prices
# ============================================================================
LINE_240A_MAIN_1_HIGH_RATE_FRACTION: float = 1.0000
LINE_240B_MAIN_1_LOW_RATE_FRACTION: float = 0.0000
LINE_240C_MAIN_1_HIGH_RATE_COST: float = 437.7942
LINE_240D_MAIN_1_LOW_RATE_COST: float = 0.0000
LINE_240E_MAIN_1_OTHER_FUEL_COST: float = 0.0000
LINE_240_MAIN_1_TOTAL_COST: float = 437.7942
LINE_241A_MAIN_2_HIGH_RATE_FRACTION: float = 0.0000
LINE_241B_MAIN_2_LOW_RATE_FRACTION: float = 0.0000
LINE_241C_MAIN_2_HIGH_RATE_COST: float = 0.0000
LINE_241D_MAIN_2_LOW_RATE_COST: float = 0.0000
LINE_241E_MAIN_2_OTHER_FUEL_COST: float = 0.0000
LINE_241_MAIN_2_TOTAL_COST: float = 0.0000
LINE_242A_SECONDARY_HIGH_RATE_FRACTION: float = 1.0000
LINE_242B_SECONDARY_LOW_RATE_FRACTION: float = 0.0000
LINE_242C_SECONDARY_HIGH_RATE_COST: float = 163.5372
LINE_242D_SECONDARY_LOW_RATE_COST: float = 0.0000
LINE_242E_SECONDARY_OTHER_FUEL_COST: float = 0.0000
LINE_242_SECONDARY_TOTAL_COST: float = 163.5372
LINE_243_WATER_HIGH_RATE_FRACTION: float = 1.0000
LINE_244_WATER_LOW_RATE_FRACTION: float = 0.0000
LINE_245_WATER_HIGH_RATE_COST: float = 84.3426
LINE_246_WATER_LOW_RATE_COST: float = 0.0000
LINE_247_WATER_OTHER_FUEL_COST: float = 0.0000
LINE_247A_INSTANT_SHOWER_COST: float = 0.0000
LINE_248_SPACE_COOLING_COST: float = 0.0000
LINE_249_PUMPS_FANS_COST: float = 21.1040
LINE_250_LIGHTING_COST: float = 28.0358
LINE_251_STANDING_CHARGES: float = 120.0000
LINE_252_PV_CREDIT: float = 0.0000
LINE_253_APPENDIX_Q_SAVED: float = 0.0000
LINE_254_APPENDIX_Q_USED: float = 0.0000
LINE_255_TOTAL_COST: float = 854.8139

View file

@ -484,3 +484,40 @@ LINE_215_M_SECONDARY_FUEL_KWH: tuple[float, ...] = (
)
LINE_215_ANNUAL_KWH: float = 1083.4778
LINE_221_COOLING_FUEL_KWH: float = 0.0000
# ============================================================================
# §10a Fuel costs — using Table 12 (RdSAP10 Table 32) prices
# ============================================================================
# Has electric shower → (247a) instant-shower cost = 79.3036.
LINE_240A_MAIN_1_HIGH_RATE_FRACTION: float = 1.0000
LINE_240B_MAIN_1_LOW_RATE_FRACTION: float = 0.0000
LINE_240C_MAIN_1_HIGH_RATE_COST: float = 383.4409
LINE_240D_MAIN_1_LOW_RATE_COST: float = 0.0000
LINE_240E_MAIN_1_OTHER_FUEL_COST: float = 0.0000
LINE_240_MAIN_1_TOTAL_COST: float = 383.4409
LINE_241A_MAIN_2_HIGH_RATE_FRACTION: float = 0.0000
LINE_241B_MAIN_2_LOW_RATE_FRACTION: float = 0.0000
LINE_241C_MAIN_2_HIGH_RATE_COST: float = 0.0000
LINE_241D_MAIN_2_LOW_RATE_COST: float = 0.0000
LINE_241E_MAIN_2_OTHER_FUEL_COST: float = 0.0000
LINE_241_MAIN_2_TOTAL_COST: float = 0.0000
LINE_242A_SECONDARY_HIGH_RATE_FRACTION: float = 1.0000
LINE_242B_SECONDARY_LOW_RATE_FRACTION: float = 0.0000
LINE_242C_SECONDARY_HIGH_RATE_COST: float = 142.9107
LINE_242D_SECONDARY_LOW_RATE_COST: float = 0.0000
LINE_242E_SECONDARY_OTHER_FUEL_COST: float = 0.0000
LINE_242_SECONDARY_TOTAL_COST: float = 142.9107
LINE_243_WATER_HIGH_RATE_FRACTION: float = 1.0000
LINE_244_WATER_LOW_RATE_FRACTION: float = 0.0000
LINE_245_WATER_HIGH_RATE_COST: float = 51.8208
LINE_246_WATER_LOW_RATE_COST: float = 0.0000
LINE_247_WATER_OTHER_FUEL_COST: float = 0.0000
LINE_247A_INSTANT_SHOWER_COST: float = 79.3036
LINE_248_SPACE_COOLING_COST: float = 0.0000
LINE_249_PUMPS_FANS_COST: float = 21.1040
LINE_250_LIGHTING_COST: float = 30.0318
LINE_251_STANDING_CHARGES: float = 120.0000
LINE_252_PV_CREDIT: float = 0.0000
LINE_253_APPENDIX_Q_SAVED: float = 0.0000
LINE_254_APPENDIX_Q_USED: float = 0.0000
LINE_255_TOTAL_COST: float = 828.6119

View file

@ -435,3 +435,39 @@ LINE_215_M_SECONDARY_FUEL_KWH: tuple[float, ...] = (
)
LINE_215_ANNUAL_KWH: float = 1118.3275
LINE_221_COOLING_FUEL_KWH: float = 0.0000
# ============================================================================
# §10a Fuel costs — using Table 12 (RdSAP10 Table 32) prices
# ============================================================================
LINE_240A_MAIN_1_HIGH_RATE_FRACTION: float = 1.0000
LINE_240B_MAIN_1_LOW_RATE_FRACTION: float = 0.0000
LINE_240C_MAIN_1_HIGH_RATE_COST: float = 397.1204
LINE_240D_MAIN_1_LOW_RATE_COST: float = 0.0000
LINE_240E_MAIN_1_OTHER_FUEL_COST: float = 0.0000
LINE_240_MAIN_1_TOTAL_COST: float = 397.1204
LINE_241A_MAIN_2_HIGH_RATE_FRACTION: float = 0.0000
LINE_241B_MAIN_2_LOW_RATE_FRACTION: float = 0.0000
LINE_241C_MAIN_2_HIGH_RATE_COST: float = 0.0000
LINE_241D_MAIN_2_LOW_RATE_COST: float = 0.0000
LINE_241E_MAIN_2_OTHER_FUEL_COST: float = 0.0000
LINE_241_MAIN_2_TOTAL_COST: float = 0.0000
LINE_242A_SECONDARY_HIGH_RATE_FRACTION: float = 1.0000
LINE_242B_SECONDARY_LOW_RATE_FRACTION: float = 0.0000
LINE_242C_SECONDARY_HIGH_RATE_COST: float = 147.5074
LINE_242D_SECONDARY_LOW_RATE_COST: float = 0.0000
LINE_242E_SECONDARY_OTHER_FUEL_COST: float = 0.0000
LINE_242_SECONDARY_TOTAL_COST: float = 147.5074
LINE_243_WATER_HIGH_RATE_FRACTION: float = 1.0000
LINE_244_WATER_LOW_RATE_FRACTION: float = 0.0000
LINE_245_WATER_HIGH_RATE_COST: float = 99.1998
LINE_246_WATER_LOW_RATE_COST: float = 0.0000
LINE_247_WATER_OTHER_FUEL_COST: float = 0.0000
LINE_247A_INSTANT_SHOWER_COST: float = 0.0000
LINE_248_SPACE_COOLING_COST: float = 0.0000
LINE_249_PUMPS_FANS_COST: float = 21.1040
LINE_250_LIGHTING_COST: float = 22.6105
LINE_251_STANDING_CHARGES: float = 120.0000
LINE_252_PV_CREDIT: float = 0.0000
LINE_253_APPENDIX_Q_SAVED: float = 0.0000
LINE_254_APPENDIX_Q_USED: float = 0.0000
LINE_255_TOTAL_COST: float = 807.5421

View file

@ -461,3 +461,39 @@ LINE_215_M_SECONDARY_FUEL_KWH: tuple[float, ...] = (
)
LINE_215_ANNUAL_KWH: float = 1241.0317
LINE_221_COOLING_FUEL_KWH: float = 0.0000
# ============================================================================
# §10a Fuel costs — using Table 12 (RdSAP10 Table 32) prices
# ============================================================================
LINE_240A_MAIN_1_HIGH_RATE_FRACTION: float = 1.0000
LINE_240B_MAIN_1_LOW_RATE_FRACTION: float = 0.0000
LINE_240C_MAIN_1_HIGH_RATE_COST: float = 438.7033
LINE_240D_MAIN_1_LOW_RATE_COST: float = 0.0000
LINE_240E_MAIN_1_OTHER_FUEL_COST: float = 0.0000
LINE_240_MAIN_1_TOTAL_COST: float = 438.7033
LINE_241A_MAIN_2_HIGH_RATE_FRACTION: float = 0.0000
LINE_241B_MAIN_2_LOW_RATE_FRACTION: float = 0.0000
LINE_241C_MAIN_2_HIGH_RATE_COST: float = 0.0000
LINE_241D_MAIN_2_LOW_RATE_COST: float = 0.0000
LINE_241E_MAIN_2_OTHER_FUEL_COST: float = 0.0000
LINE_241_MAIN_2_TOTAL_COST: float = 0.0000
LINE_242A_SECONDARY_HIGH_RATE_FRACTION: float = 1.0000
LINE_242B_SECONDARY_LOW_RATE_FRACTION: float = 0.0000
LINE_242C_SECONDARY_HIGH_RATE_COST: float = 163.6921
LINE_242D_SECONDARY_LOW_RATE_COST: float = 0.0000
LINE_242E_SECONDARY_OTHER_FUEL_COST: float = 0.0000
LINE_242_SECONDARY_TOTAL_COST: float = 163.6921
LINE_243_WATER_HIGH_RATE_FRACTION: float = 1.0000
LINE_244_WATER_LOW_RATE_FRACTION: float = 0.0000
LINE_245_WATER_HIGH_RATE_COST: float = 86.7630
LINE_246_WATER_LOW_RATE_COST: float = 0.0000
LINE_247_WATER_OTHER_FUEL_COST: float = 0.0000
LINE_247A_INSTANT_SHOWER_COST: float = 0.0000
LINE_248_SPACE_COOLING_COST: float = 0.0000
LINE_249_PUMPS_FANS_COST: float = 21.1040
LINE_250_LIGHTING_COST: float = 30.4538
LINE_251_STANDING_CHARGES: float = 120.0000
LINE_252_PV_CREDIT: float = 0.0000
LINE_253_APPENDIX_Q_SAVED: float = 0.0000
LINE_254_APPENDIX_Q_USED: float = 0.0000
LINE_255_TOTAL_COST: float = 860.7162

View file

@ -20,6 +20,7 @@ from domain.sap.rdsap.cert_to_inputs import (
cert_to_inputs,
energy_requirements_section_from_cert,
fabric_energy_efficiency_from_cert,
fuel_cost_section_from_cert,
heat_transmission_section_from_cert,
internal_gains_section_from_cert,
mean_internal_temperature_section_from_cert,
@ -780,3 +781,72 @@ def test_section_9a_scalar_line_refs_match_pdf(
# Assert
_pin(actual, expected, f"§9a {fixture_attr} {fixture_name}")
# ============================================================================
# §10a Fuel costs — LINE_240..LINE_255 (using Table 32 prices)
# ============================================================================
_SECTION_10A_PINS: Final[tuple[tuple[str, str], ...]] = (
("LINE_240A_MAIN_1_HIGH_RATE_FRACTION", "main_1_high_rate_fraction"),
("LINE_240B_MAIN_1_LOW_RATE_FRACTION", "main_1_low_rate_fraction"),
("LINE_240C_MAIN_1_HIGH_RATE_COST", "main_1_high_rate_cost_gbp"),
("LINE_240D_MAIN_1_LOW_RATE_COST", "main_1_low_rate_cost_gbp"),
("LINE_240E_MAIN_1_OTHER_FUEL_COST", "main_1_other_fuel_cost_gbp"),
("LINE_240_MAIN_1_TOTAL_COST", "main_1_total_cost_gbp"),
("LINE_241A_MAIN_2_HIGH_RATE_FRACTION", "main_2_high_rate_fraction"),
("LINE_241B_MAIN_2_LOW_RATE_FRACTION", "main_2_low_rate_fraction"),
("LINE_241C_MAIN_2_HIGH_RATE_COST", "main_2_high_rate_cost_gbp"),
("LINE_241D_MAIN_2_LOW_RATE_COST", "main_2_low_rate_cost_gbp"),
("LINE_241E_MAIN_2_OTHER_FUEL_COST", "main_2_other_fuel_cost_gbp"),
("LINE_241_MAIN_2_TOTAL_COST", "main_2_total_cost_gbp"),
("LINE_242A_SECONDARY_HIGH_RATE_FRACTION", "secondary_high_rate_fraction"),
("LINE_242B_SECONDARY_LOW_RATE_FRACTION", "secondary_low_rate_fraction"),
("LINE_242C_SECONDARY_HIGH_RATE_COST", "secondary_high_rate_cost_gbp"),
("LINE_242D_SECONDARY_LOW_RATE_COST", "secondary_low_rate_cost_gbp"),
("LINE_242E_SECONDARY_OTHER_FUEL_COST", "secondary_other_fuel_cost_gbp"),
("LINE_242_SECONDARY_TOTAL_COST", "secondary_total_cost_gbp"),
("LINE_243_WATER_HIGH_RATE_FRACTION", "water_high_rate_fraction"),
("LINE_244_WATER_LOW_RATE_FRACTION", "water_low_rate_fraction"),
("LINE_245_WATER_HIGH_RATE_COST", "water_high_rate_cost_gbp"),
("LINE_246_WATER_LOW_RATE_COST", "water_low_rate_cost_gbp"),
("LINE_247_WATER_OTHER_FUEL_COST", "water_other_fuel_cost_gbp"),
("LINE_247A_INSTANT_SHOWER_COST", "instant_shower_cost_gbp"),
("LINE_248_SPACE_COOLING_COST", "space_cooling_cost_gbp"),
("LINE_249_PUMPS_FANS_COST", "pumps_fans_cost_gbp"),
("LINE_250_LIGHTING_COST", "lighting_cost_gbp"),
("LINE_251_STANDING_CHARGES", "additional_standing_charges_gbp"),
("LINE_252_PV_CREDIT", "pv_credit_gbp"),
("LINE_253_APPENDIX_Q_SAVED", "appendix_q_saved_gbp"),
("LINE_254_APPENDIX_Q_USED", "appendix_q_used_gbp"),
("LINE_255_TOTAL_COST", "total_cost_gbp"),
)
@pytest.mark.parametrize(
"fixture_name,fixture_attr,result_attr",
[
(fix, line, attr)
for fix in _FIXTURES
for line, attr in _SECTION_10A_PINS
],
ids=lambda x: x if isinstance(x, str) else None,
)
def test_section_10a_line_refs_match_pdf(
fixture_name: str, fixture_attr: str, result_attr: str
) -> None:
"""§10a pins — every (240a)..(255) line ref of `_fuel_cost` matches the
U985 PDF to abs=1e-4. STANDARD-tariff scope A (all 6 fixtures); off-
peak splits collapse: high_rate_fraction=1, low_rate_fraction=0."""
# Arrange
mod = _FIXTURES[fixture_name]
epc = mod.build_epc() # type: ignore[attr-defined]
expected = getattr(mod, fixture_attr)
# Act
fc = fuel_cost_section_from_cert(epc)
assert fc is not None, f"{fixture_name}: fuel_cost_from_cert returned None"
actual = getattr(fc, result_attr)
# Assert
_pin(actual, expected, f"§10a {fixture_attr} {fixture_name}")

View file

@ -62,7 +62,9 @@ class WaterHeatingResult:
total_demand_monthly_kwh: tuple[float, ...]
output_monthly_kwh: tuple[float, ...]
heat_gains_monthly_kwh: tuple[float, ...]
electric_shower_monthly_kwh: tuple[float, ...] # (64a)m — App J step 8
output_kwh_per_yr: float
electric_shower_kwh_per_yr: float # Σ (64a)m — feeds §10a (247a)
# Table J2 — monthly factors for hot water use (also used by Appendix J
# equation J11 for "other uses"). Symmetric about the year midpoint.
@ -763,5 +765,7 @@ def water_heating_from_cert(
total_demand_monthly_kwh=total_demand,
output_monthly_kwh=output,
heat_gains_monthly_kwh=gains,
electric_shower_monthly_kwh=electric_shower,
output_kwh_per_yr=sum(output),
electric_shower_kwh_per_yr=sum(electric_shower),
)