From f09e83b6a1761908b52e1ac0b4a6a619c098453a Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 19 May 2026 12:21:15 +0000 Subject: [PATCH] =?UTF-8?q?P5.12:=20align=20per-end-use=20primary=20energy?= =?UTF-8?q?=20to=20=C2=A711=20sketch=20(per-m=C2=B2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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`. --- packages/domain/src/domain/sap/calculator.py | 8 +-- .../src/domain/sap/tests/test_calculator.py | 50 ++++++++++--------- 2 files changed, 31 insertions(+), 27 deletions(-) diff --git a/packages/domain/src/domain/sap/calculator.py b/packages/domain/src/domain/sap/calculator.py index 4218c120..3fe92e5a 100644 --- a/packages/domain/src/domain/sap/calculator.py +++ b/packages/domain/src/domain/sap/calculator.py @@ -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, } diff --git a/packages/domain/src/domain/sap/tests/test_calculator.py b/packages/domain/src/domain/sap/tests/test_calculator.py index 56a4bdaa..1582cf8c 100644 --- a/packages/domain/src/domain/sap/tests/test_calculator.py +++ b/packages/domain/src/domain/sap/tests/test_calculator.py @@ -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: