P5.12: align per-end-use primary energy to §11 sketch (per-m²)

P5.9 exposed the four primary-energy components as absolute kWh/yr
keys (space_heating_primary_kwh_per_yr, …). HANDOVER_SYSTEMATIC_REVIEW
§11 specifies these as `_pe_kwh_per_m2` because primary energy enters
the rating equation per floor area. Renamed to match the sketch:
- space_heating_pe_kwh_per_m2
- hot_water_pe_kwh_per_m2
- other_pe_kwh_per_m2
- pv_pe_offset_kwh_per_m2

Chain check now verifies max(0, sum − pv_offset) ≈
result.primary_energy_kwh_per_m2 (the top-level per-m² field).
Absolute kWh/yr values remain recoverable via tfa_m2 on `intermediate`.
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-19 12:21:15 +00:00
parent 550b1fbcd0
commit f09e83b6a1
2 changed files with 31 additions and 27 deletions

View file

@ -380,10 +380,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,
"space_heating_pe_kwh_per_m2": space_heating_primary_kwh / tfa if tfa > 0 else 0.0,
"hot_water_pe_kwh_per_m2": hot_water_primary_kwh / tfa if tfa > 0 else 0.0,
"other_pe_kwh_per_m2": other_primary_kwh / tfa if tfa > 0 else 0.0,
"pv_pe_offset_kwh_per_m2": pv_primary_offset_kwh / tfa if tfa > 0 else 0.0,
"floor_area_offset_m2": FLOOR_AREA_OFFSET_M2,
"ecf_log_threshold": ECF_LOG_THRESHOLD,
}

View file

@ -311,41 +311,45 @@ def test_calculate_exposes_co2_chain() -> None:
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.
# (Appendix M). The §11 sketch in HANDOVER_SYSTEMATIC_REVIEW lists
# these as `_kwh_per_m2` because primary energy enters the rating
# equation per-floor-area; absolute values are recoverable via tfa_m2.
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
tfa = inputs.dimensions.total_floor_area_m2
space_heating_pe = (
(result.main_heating_fuel_kwh_per_yr + result.secondary_heating_fuel_kwh_per_yr)
* inputs.space_heating_primary_factor
/ tfa
)
hot_water_pe = inputs.hot_water_kwh_per_yr * inputs.hot_water_primary_factor / tfa
other_pe = (
(inputs.pumps_fans_kwh_per_yr + inputs.lighting_kwh_per_yr)
* inputs.other_primary_factor
/ tfa
)
pv_offset_pe = inputs.pv_generation_kwh_per_yr * inputs.other_primary_factor / tfa
assert result.intermediate["space_heating_primary_kwh_per_yr"] == pytest.approx(
space_heating_primary, rel=1e-9
assert result.intermediate["space_heating_pe_kwh_per_m2"] == pytest.approx(
space_heating_pe, rel=1e-9
)
assert result.intermediate["hot_water_primary_kwh_per_yr"] == pytest.approx(
hot_water_primary, rel=1e-9
assert result.intermediate["hot_water_pe_kwh_per_m2"] == pytest.approx(
hot_water_pe, rel=1e-9
)
assert result.intermediate["other_primary_kwh_per_yr"] == pytest.approx(
other_primary, rel=1e-9
assert result.intermediate["other_pe_kwh_per_m2"] == pytest.approx(other_pe, rel=1e-9)
assert result.intermediate["pv_pe_offset_kwh_per_m2"] == pytest.approx(
pv_offset_pe, rel=1e-9
)
assert result.intermediate["pv_primary_offset_kwh_per_yr"] == pytest.approx(
pv_offset, rel=1e-9
expected_total_per_m2 = max(
0.0, space_heating_pe + hot_water_pe + other_pe - pv_offset_pe
)
expected_total = max(
0.0,
space_heating_primary + hot_water_primary + other_primary - pv_offset,
assert result.primary_energy_kwh_per_m2 == pytest.approx(
expected_total_per_m2, rel=1e-9
)
assert result.primary_energy_kwh_per_yr == pytest.approx(expected_total, rel=1e-9)
def test_calculate_exposes_pv_export_credit() -> None: