diff --git a/domain/sap10_calculator/calculator.py b/domain/sap10_calculator/calculator.py index 83f05510..03525a8e 100644 --- a/domain/sap10_calculator/calculator.py +++ b/domain/sap10_calculator/calculator.py @@ -234,6 +234,15 @@ class CalculatorInputs: # SAP 10.2 Table 12 code 60 ("electricity sold to grid, PV") PE # factor = 0.501. Applied to E_PV,ex when split is set. pv_export_primary_factor: float = 0.501 + # SAP 10.2 Appendix M1 §7 — per-cert CO2 factors for the PV split. + # The dwelling factor is the effective monthly Table 12d IMPORT + # factor (Σ(E_PV,dw,m × CO2_30,m) / Σ(E_PV,dw,m)); the exported + # factor is the effective monthly Table 12d code-60 ("electricity + # sold to grid, PV") factor. Both are computed in cert_to_inputs. + # Synthetic CalculatorInputs constructions leave these None → no + # PV CO2 credit applied (legacy behaviour). + pv_dwelling_co2_factor_kg_per_kwh: Optional[float] = None + pv_exported_co2_factor_kg_per_kwh: Optional[float] = None # Secondary heating — SAP 10.2 Table 11 routes a fraction of space # heating demand to a secondary system (0.10 for gas/oil/solid main # systems; 0.15-0.20 for electric room/storage heaters). Fraction @@ -501,6 +510,28 @@ def calculate_sap_from_inputs(inputs: CalculatorInputs) -> SapResult: + lighting_co2 + electric_shower_co2 ) + # SAP 10.2 Appendix M1 §7 — subtract PV CO2 credit. Onsite consumption + # offsets grid imports at the IMPORT CO2 factor (Table 12d weighted + # by E_PV,dw,m); exports credit at the EXPORT CO2 factor (Table 12d + # code 60 weighted by E_PV,ex,m). Both factors are precomputed in + # cert_to_inputs; None preserves the legacy zero-credit behaviour + # for synthetic CalculatorInputs constructions. + if ( + inputs.pv_dwelling_kwh_per_yr is not None + and inputs.pv_dwelling_co2_factor_kg_per_kwh is not None + ): + co2 -= ( + inputs.pv_dwelling_kwh_per_yr + * inputs.pv_dwelling_co2_factor_kg_per_kwh + ) + if ( + inputs.pv_exported_kwh_per_yr is not None + and inputs.pv_exported_co2_factor_kg_per_kwh is not None + ): + co2 -= ( + inputs.pv_exported_kwh_per_yr + * inputs.pv_exported_co2_factor_kg_per_kwh + ) # Per-end-use effective PE factors. Same shape as the CO2 cascade: # electricity end-uses use Table 12e (p.195) monthly factors weighted diff --git a/domain/sap10_calculator/rdsap/cert_to_inputs.py b/domain/sap10_calculator/rdsap/cert_to_inputs.py index 54592767..f304dde7 100644 --- a/domain/sap10_calculator/rdsap/cert_to_inputs.py +++ b/domain/sap10_calculator/rdsap/cert_to_inputs.py @@ -1151,6 +1151,10 @@ def _days_in_month_proportioned( _DAYS_IN_MONTH: Final[tuple[int, ...]] = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) _STANDARD_ELECTRICITY_FUEL_CODE: Final[int] = 30 +# SAP 10.2 Table 12 code 60 — "electricity sold to grid, PV". Used as +# the EXPORT factor key for the Appendix M1 §6/§7/§8 PV split: +# (1-β)·E_PV credits at this code's monthly Table 12d/12e factor. +_PV_EXPORT_FUEL_CODE_TABLE_12: Final[int] = 60 def _co2_factor_kg_per_kwh(main: Optional[MainHeatingDetail]) -> float: @@ -3201,8 +3205,19 @@ def cert_to_inputs( # SAP 10.2 Appendix M1 §3-4 PV split — the cascade applies # IMPORT PEF (Table 12) to the onsite portion and EXPORT PEF # (Table 12 code 60 = 0.501) to the exported portion per §8. + # The CO2 factors per §7 are the effective monthly Table 12d + # values weighted by the monthly E_PV,dw / E_PV,ex split: + # dwelling uses code 30 (Standard electricity); exported uses + # code 60 (Electricity sold to grid, PV). pv_dwelling_kwh_per_yr=pv_split.epv_dwelling_kwh_per_yr, pv_exported_kwh_per_yr=pv_split.epv_exported_kwh_per_yr, + pv_dwelling_co2_factor_kg_per_kwh=_effective_monthly_co2_factor( + pv_split.epv_dwelling_monthly_kwh, _STANDARD_ELECTRICITY_FUEL_CODE, + ), + pv_exported_co2_factor_kg_per_kwh=_effective_monthly_co2_factor( + pv_split.epv_exported_monthly_kwh, + _PV_EXPORT_FUEL_CODE_TABLE_12, + ), secondary_heating_fraction=secondary_fraction_value, secondary_heating_efficiency=secondary_efficiency_value, energy_requirements=energy_requirements_result, diff --git a/domain/sap10_calculator/rdsap/tests/test_golden_fixtures.py b/domain/sap10_calculator/rdsap/tests/test_golden_fixtures.py index d0137580..a9700b50 100644 --- a/domain/sap10_calculator/rdsap/tests/test_golden_fixtures.py +++ b/domain/sap10_calculator/rdsap/tests/test_golden_fixtures.py @@ -201,7 +201,7 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = ( actual_sap=82, expected_sap_resid=+1, expected_pe_resid_kwh_per_m2=-9.6962, - expected_co2_resid_tonnes_per_yr=+0.2993, + expected_co2_resid_tonnes_per_yr=-0.0456, notes=( "End-terrace + 1 extension, TFA 64, gas combi PCDB index 17505, " "postcode DE22 (PCDB Table 172 match), PV: 2× 2.04 kWp arrays " @@ -257,7 +257,7 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = ( actual_sap=89, expected_sap_resid=+0, expected_pe_resid_kwh_per_m2=+8.0916, - expected_co2_resid_tonnes_per_yr=+0.2785, + expected_co2_resid_tonnes_per_yr=-0.0540, notes=( "Mitsubishi PUZ-WM50VHA PCDB 104568, semi-detached bungalow " "TFA 60.43 age D, PV 3 kWp + 5 kWh battery. Worksheet SAP " @@ -275,7 +275,7 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = ( actual_sap=84, expected_sap_resid=+0, expected_pe_resid_kwh_per_m2=+2.7315, - expected_co2_resid_tonnes_per_yr=+0.1709, + expected_co2_resid_tonnes_per_yr=-0.0841, notes=( "Mitsubishi PUZ-WM50VHA PCDB 104568, ASHP cohort cert with " "PV + 5 kWh battery. Worksheet SAP 84.1367. Slice S0380.45 " @@ -288,7 +288,7 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = ( actual_sap=89, expected_sap_resid=+0, expected_pe_resid_kwh_per_m2=+4.4804, - expected_co2_resid_tonnes_per_yr=+0.2628, + expected_co2_resid_tonnes_per_yr=-0.0711, notes=( "Mitsubishi PUZ-WM50VHA PCDB 104568, ASHP cohort cert with " "PV + 5 kWh battery. Worksheet SAP 88.7921. Slice S0380.45 " @@ -300,7 +300,7 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = ( actual_sap=86, expected_sap_resid=+0, expected_pe_resid_kwh_per_m2=+3.4216, - expected_co2_resid_tonnes_per_yr=+0.2194, + expected_co2_resid_tonnes_per_yr=-0.0581, notes=( "Mitsubishi PUZ-WM50VHA PCDB 104568, ASHP cohort cert with " "PV + 5 kWh battery + 3.74 m² cantilever + 12.76 m² alt wall. " @@ -313,7 +313,7 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = ( actual_sap=86, expected_sap_resid=+0, expected_pe_resid_kwh_per_m2=+3.5809, - expected_co2_resid_tonnes_per_yr=+0.2609, + expected_co2_resid_tonnes_per_yr=-0.0142, notes=( "Mitsubishi PUZ-WM50VHA PCDB 104568, ASHP cohort cert with " "PV + 5 kWh battery. Worksheet SAP 86.1458. Slice S0380.45 " @@ -325,7 +325,7 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = ( actual_sap=84, expected_sap_resid=+0, expected_pe_resid_kwh_per_m2=+3.1982, - expected_co2_resid_tonnes_per_yr=+0.1571, + expected_co2_resid_tonnes_per_yr=-0.0979, notes=( "Mitsubishi PUZ-WM50VHA PCDB 104568, ASHP cohort cert with " "PV + 5 kWh battery. Worksheet SAP 84.1369. Slice S0380.45 " @@ -337,7 +337,7 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = ( actual_sap=85, expected_sap_resid=+0, expected_pe_resid_kwh_per_m2=+4.6681, - expected_co2_resid_tonnes_per_yr=+0.2317, + expected_co2_resid_tonnes_per_yr=-0.0463, notes=( "Daikin Altherma EDLQ05CAV3 PCDB 102421 (heating_duration " "code '24' — continuous, all days at Th) + 5 kWh battery. "