P5.9: SapResult.intermediate exposes primary-energy breakdown

Lifts the inlined primary-energy sum into four named components:
space-heating (main + secondary × space_heating PEF), hot water,
other (pumps_fans + lighting × other PEF), and the PV offset at
other PEF (Appendix M). Together with the top-level
primary_energy_kwh_per_yr they make whether the floor-at-zero
clipped visible.
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-19 10:35:10 +00:00
parent 537e18bc2e
commit 3d56898944
2 changed files with 56 additions and 8 deletions

View file

@ -328,19 +328,23 @@ def calculate_sap_from_inputs(inputs: CalculatorInputs) -> SapResult:
sap_cont = sap_rating(ecf=ecf)
co2 = delivered_fuel_kwh * inputs.co2_factor_kg_per_kwh
primary_energy_kwh = (
main_fuel_kwh * inputs.space_heating_primary_factor
+ secondary_fuel_kwh * inputs.space_heating_primary_factor
+ inputs.hot_water_kwh_per_yr * inputs.hot_water_primary_factor
+ (inputs.pumps_fans_kwh_per_yr + inputs.lighting_kwh_per_yr)
* inputs.other_primary_factor
)
space_heating_primary_kwh = (
main_fuel_kwh + secondary_fuel_kwh
) * inputs.space_heating_primary_factor
hot_water_primary_kwh = inputs.hot_water_kwh_per_yr * inputs.hot_water_primary_factor
other_primary_kwh = (
inputs.pumps_fans_kwh_per_yr + inputs.lighting_kwh_per_yr
) * inputs.other_primary_factor
# PV offsets primary energy at the same PEF (Appendix M: export PEF =
# standard-electricity PEF for ratings, since the displaced grid kWh
# would have been imported electricity).
pv_primary_offset_kwh = inputs.pv_generation_kwh_per_yr * inputs.other_primary_factor
primary_energy_kwh = max(
0.0,
primary_energy_kwh - inputs.pv_generation_kwh_per_yr * inputs.other_primary_factor,
space_heating_primary_kwh
+ hot_water_primary_kwh
+ other_primary_kwh
- pv_primary_offset_kwh,
)
primary_energy_per_m2 = primary_energy_kwh / tfa if tfa > 0 else 0.0
@ -373,6 +377,10 @@ def calculate_sap_from_inputs(inputs: CalculatorInputs) -> SapResult:
"deflator": ENERGY_COST_DEFLATOR,
"delivered_fuel_kwh_per_yr": delivered_fuel_kwh,
"co2_factor_kg_per_kwh": inputs.co2_factor_kg_per_kwh,
"space_heating_primary_kwh_per_yr": space_heating_primary_kwh,
"hot_water_primary_kwh_per_yr": hot_water_primary_kwh,
"other_primary_kwh_per_yr": other_primary_kwh,
"pv_primary_offset_kwh_per_yr": pv_primary_offset_kwh,
}
return SapResult(

View file

@ -308,6 +308,46 @@ def test_calculate_exposes_co2_chain() -> None:
) == pytest.approx(result.co2_kg_per_yr, rel=1e-9)
def test_calculate_exposes_primary_energy_breakdown() -> None:
# Arrange — P5 trace mode: primary energy splits across three PEFs
# (space-heating, hot-water, other) and a PV offset at the other-PEF
# (Appendix M). Exposing the four components makes the top-level
# primary_energy_kwh_per_yr auditable, including whether the
# floor-at-zero was hit when PV exceeded gross primary.
inputs = _baseline_inputs()
# Act
result = calculate_sap_from_inputs(inputs)
# Assert
space_heating_primary = (
result.main_heating_fuel_kwh_per_yr + result.secondary_heating_fuel_kwh_per_yr
) * inputs.space_heating_primary_factor
hot_water_primary = inputs.hot_water_kwh_per_yr * inputs.hot_water_primary_factor
other_primary = (
inputs.pumps_fans_kwh_per_yr + inputs.lighting_kwh_per_yr
) * inputs.other_primary_factor
pv_offset = inputs.pv_generation_kwh_per_yr * inputs.other_primary_factor
assert result.intermediate["space_heating_primary_kwh_per_yr"] == pytest.approx(
space_heating_primary, rel=1e-9
)
assert result.intermediate["hot_water_primary_kwh_per_yr"] == pytest.approx(
hot_water_primary, rel=1e-9
)
assert result.intermediate["other_primary_kwh_per_yr"] == pytest.approx(
other_primary, rel=1e-9
)
assert result.intermediate["pv_primary_offset_kwh_per_yr"] == pytest.approx(
pv_offset, rel=1e-9
)
expected_total = max(
0.0,
space_heating_primary + hot_water_primary + other_primary - pv_offset,
)
assert result.primary_energy_kwh_per_yr == pytest.approx(expected_total, rel=1e-9)
def test_higher_main_heating_efficiency_reduces_fuel_use() -> None:
# Arrange — Direction check: doubling the boiler efficiency must halve
# the main-heating fuel kWh, holding everything else constant.