From 0fa39e859cb1621734f3daf51cadd0261276a754 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 19 May 2026 12:24:59 +0000 Subject: [PATCH] P5.13: SapResult.intermediate exposes per-end-use CO2 breakdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the second §11-sketch gap noted in HANDOVER_SYSTEMATIC_REVIEW ("primary energy AND CO2 per end-use"). Lifts the single co2 = total × factor expression into five named locals (main_heating, secondary, hot_water, pumps_fans, lighting) and exposes them on `intermediate`. The five components sum exactly to the top-level co2_kg_per_yr — no PV deduction in the current implementation. --- packages/domain/src/domain/sap/calculator.py | 21 +++++++++- .../src/domain/sap/tests/test_calculator.py | 38 +++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/packages/domain/src/domain/sap/calculator.py b/packages/domain/src/domain/sap/calculator.py index 3fe92e5a..57a1b14b 100644 --- a/packages/domain/src/domain/sap/calculator.py +++ b/packages/domain/src/domain/sap/calculator.py @@ -328,7 +328,19 @@ def calculate_sap_from_inputs(inputs: CalculatorInputs) -> SapResult: ecf = energy_cost_factor(total_cost_gbp=total_cost, total_floor_area_m2=tfa) sap_int = sap_rating_integer(ecf=ecf) sap_cont = sap_rating(ecf=ecf) - co2 = delivered_fuel_kwh * inputs.co2_factor_kg_per_kwh + co2_factor = inputs.co2_factor_kg_per_kwh + main_heating_co2 = main_fuel_kwh * co2_factor + secondary_heating_co2 = secondary_fuel_kwh * co2_factor + hot_water_co2 = inputs.hot_water_kwh_per_yr * co2_factor + pumps_fans_co2 = inputs.pumps_fans_kwh_per_yr * co2_factor + lighting_co2 = inputs.lighting_kwh_per_yr * co2_factor + co2 = ( + main_heating_co2 + + secondary_heating_co2 + + hot_water_co2 + + pumps_fans_co2 + + lighting_co2 + ) space_heating_primary_kwh = ( main_fuel_kwh + secondary_fuel_kwh @@ -379,7 +391,12 @@ def calculate_sap_from_inputs(inputs: CalculatorInputs) -> SapResult: "ecf": ecf, "deflator": ENERGY_COST_DEFLATOR, "delivered_fuel_kwh_per_yr": delivered_fuel_kwh, - "co2_factor_kg_per_kwh": inputs.co2_factor_kg_per_kwh, + "co2_factor_kg_per_kwh": co2_factor, + "main_heating_co2_kg_per_yr": main_heating_co2, + "secondary_heating_co2_kg_per_yr": secondary_heating_co2, + "hot_water_co2_kg_per_yr": hot_water_co2, + "pumps_fans_co2_kg_per_yr": pumps_fans_co2, + "lighting_co2_kg_per_yr": lighting_co2, "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, diff --git a/packages/domain/src/domain/sap/tests/test_calculator.py b/packages/domain/src/domain/sap/tests/test_calculator.py index 1582cf8c..89c1e483 100644 --- a/packages/domain/src/domain/sap/tests/test_calculator.py +++ b/packages/domain/src/domain/sap/tests/test_calculator.py @@ -352,6 +352,44 @@ def test_calculate_exposes_primary_energy_breakdown() -> None: ) +def test_calculate_exposes_per_end_use_co2() -> None: + # Arrange — P5 trace mode: §11 sketch lists "primary energy AND CO2 + # per end-use". The calculator applies a single co2_factor_kg_per_kwh + # to total delivered fuel (no PV deduction on CO2 in the current + # implementation), so per-end-use CO2 is fuel_kwh × factor and the + # five components sum exactly to the top-level co2_kg_per_yr. + inputs = _baseline_inputs() + + # Act + result = calculate_sap_from_inputs(inputs) + + # Assert + factor = inputs.co2_factor_kg_per_kwh + assert result.intermediate["main_heating_co2_kg_per_yr"] == pytest.approx( + result.main_heating_fuel_kwh_per_yr * factor, rel=1e-9 + ) + assert result.intermediate["secondary_heating_co2_kg_per_yr"] == pytest.approx( + result.secondary_heating_fuel_kwh_per_yr * factor, rel=1e-9 + ) + assert result.intermediate["hot_water_co2_kg_per_yr"] == pytest.approx( + result.hot_water_kwh_per_yr * factor, rel=1e-9 + ) + assert result.intermediate["pumps_fans_co2_kg_per_yr"] == pytest.approx( + result.pumps_fans_kwh_per_yr * factor, rel=1e-9 + ) + assert result.intermediate["lighting_co2_kg_per_yr"] == pytest.approx( + result.lighting_kwh_per_yr * factor, rel=1e-9 + ) + breakdown_sum = ( + result.intermediate["main_heating_co2_kg_per_yr"] + + result.intermediate["secondary_heating_co2_kg_per_yr"] + + result.intermediate["hot_water_co2_kg_per_yr"] + + result.intermediate["pumps_fans_co2_kg_per_yr"] + + result.intermediate["lighting_co2_kg_per_yr"] + ) + assert breakdown_sum == pytest.approx(result.co2_kg_per_yr, 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