diff --git a/packages/domain/src/domain/sap/calculator.py b/packages/domain/src/domain/sap/calculator.py index f17d23fc..8ef3e429 100644 --- a/packages/domain/src/domain/sap/calculator.py +++ b/packages/domain/src/domain/sap/calculator.py @@ -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( diff --git a/packages/domain/src/domain/sap/tests/test_calculator.py b/packages/domain/src/domain/sap/tests/test_calculator.py index 22915dd2..86abc224 100644 --- a/packages/domain/src/domain/sap/tests/test_calculator.py +++ b/packages/domain/src/domain/sap/tests/test_calculator.py @@ -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.