P5.11: SapResult.intermediate exposes PV export credit

Final P5 slice. PV credit was the missing term linking the per-end-use
fuel costs (P5.6) to the top-level total_fuel_cost_gbp: total =
max(0, sum(per-end-use) − pv_credit). With this key, every step of
the §13 cost chain — per-fuel cost → PV credit → total → ECF →
rating — is auditable from `intermediate`. P5 trace exposure is
complete.
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-19 10:41:18 +00:00
parent 02f92e2b0c
commit 550b1fbcd0
2 changed files with 34 additions and 0 deletions

View file

@ -375,6 +375,7 @@ def calculate_sap_from_inputs(inputs: CalculatorInputs) -> SapResult:
"hot_water_cost_gbp": hot_water_cost,
"pumps_fans_cost_gbp": pumps_fans_cost,
"lighting_cost_gbp": lighting_cost,
"pv_export_credit_gbp": pv_credit,
"ecf": ecf,
"deflator": ENERGY_COST_DEFLATOR,
"delivered_fuel_kwh_per_yr": delivered_fuel_kwh,

View file

@ -348,6 +348,39 @@ def test_calculate_exposes_primary_energy_breakdown() -> None:
assert result.primary_energy_kwh_per_yr == pytest.approx(expected_total, rel=1e-9)
def test_calculate_exposes_pv_export_credit() -> None:
# Arrange — P5 trace mode: total_fuel_cost_gbp = sum(per-end-use
# costs) pv_export_credit, floored at 0. The PV credit is the only
# missing term linking the P5.6 per-end-use cost breakdown to the
# top-level total. Set non-zero PV values so the credit is meaningful.
inputs = replace(
_baseline_inputs(),
pv_generation_kwh_per_yr=1000.0,
pv_export_credit_gbp_per_kwh=0.05,
)
# Act
result = calculate_sap_from_inputs(inputs)
# Assert
expected_credit = (
inputs.pv_generation_kwh_per_yr * inputs.pv_export_credit_gbp_per_kwh
)
assert result.intermediate["pv_export_credit_gbp"] == pytest.approx(
expected_credit, rel=1e-12
)
gross_cost = (
result.intermediate["main_heating_cost_gbp"]
+ result.intermediate["secondary_heating_cost_gbp"]
+ result.intermediate["hot_water_cost_gbp"]
+ result.intermediate["pumps_fans_cost_gbp"]
+ result.intermediate["lighting_cost_gbp"]
)
assert max(0.0, gross_cost - expected_credit) == pytest.approx(
result.total_fuel_cost_gbp, rel=1e-9
)
def test_calculate_exposes_rating_equation_spec_constants() -> None:
# Arrange — P5 trace mode: the §13 ECF denominator carries a 45 m²
# floor-area offset (Table 12) and the SAP rating splits between a