diff --git a/packages/domain/src/domain/sap/calculator.py b/packages/domain/src/domain/sap/calculator.py index 7c49190b..a4522f1a 100644 --- a/packages/domain/src/domain/sap/calculator.py +++ b/packages/domain/src/domain/sap/calculator.py @@ -105,6 +105,11 @@ class CalculatorInputs: hot_water_fuel_cost_gbp_per_kwh: float other_fuel_cost_gbp_per_kwh: float co2_factor_kg_per_kwh: float + # Generation offsets — applied as a cost credit against the ECF + # numerator. SAP 10.2 Appendix M: PV self-consumption + export + # collapse to a single credit at the export rate (Table 12 code 60). + pv_generation_kwh_per_yr: float = 0.0 + pv_export_credit_gbp_per_kwh: float = 0.0 @dataclass(frozen=True) @@ -265,11 +270,14 @@ def calculate_sap_from_inputs(inputs: CalculatorInputs) -> SapResult: + inputs.pumps_fans_kwh_per_yr + inputs.lighting_kwh_per_yr ) - total_cost = ( + pv_credit = inputs.pv_generation_kwh_per_yr * inputs.pv_export_credit_gbp_per_kwh + total_cost = max( + 0.0, main_fuel_kwh * inputs.space_heating_fuel_cost_gbp_per_kwh + inputs.hot_water_kwh_per_yr * inputs.hot_water_fuel_cost_gbp_per_kwh + (inputs.pumps_fans_kwh_per_yr + inputs.lighting_kwh_per_yr) * inputs.other_fuel_cost_gbp_per_kwh + - pv_credit, ) ecf = energy_cost_factor(total_cost_gbp=total_cost, total_floor_area_m2=tfa) sap_int = sap_rating_integer(ecf=ecf) diff --git a/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py b/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py index cffbd2d3..ffcc7e7a 100644 --- a/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py +++ b/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py @@ -134,6 +134,19 @@ _DEFAULT_PUMPS_FANS_KWH_PER_YR: Final[float] = 130.0 _INSTANTANEOUS_WATER_CODES: Final[frozenset[int]] = frozenset({907, 909}) +# UK-average annual PV yield (kWh per kWp). SAP 10.2 Appendix M references +# regional yield factors; for rating purposes (Appendix U: ratings use UK +# average weather) the single national figure applies. Derived from +# `domain.ml.ecf._PV_YIELD_BY_REGION` UK-average baseline. +_PV_ANNUAL_YIELD_KWH_PER_KWP: Final[float] = 850.0 + + +# SAP 10.2 Table 12 code 60 — PV export tariff. The calculator uses this +# rate as the per-kWh PV cost credit applied against total annual fuel +# cost in the ECF numerator. +_PV_EXPORT_TARIFF_CODE: Final[int] = 60 + + @dataclass(frozen=True) class PriceTable: """Seam between the spec-correct SAP 10.2/10.3 Table 12 prices and @@ -475,6 +488,25 @@ def _hot_water_fuel_cost_gbp_per_kwh( return _fuel_cost_gbp_per_kwh(main, prices) +def _pv_generation_kwh_per_yr(epc: EpcPropertyData) -> float: + """Annual PV generation (kWh/yr) summed across all photovoltaic + arrays on the cert. SAP 10.2 Appendix M: yield = peak power × annual + yield factor. We use the UK-average yield (Appendix U rule for + ratings).""" + arrays = epc.sap_energy_source.photovoltaic_arrays + if not arrays: + return 0.0 + total_kwp = sum(a.peak_power for a in arrays if a.peak_power is not None) + return total_kwp * _PV_ANNUAL_YIELD_KWH_PER_KWP + + +def _pv_export_credit_gbp_per_kwh(prices: PriceTable) -> float: + """PV cost credit per kWh generated. SAP 10.2 Table 12 code 60 (PV + export to grid) — 5.59 p/kWh on the spec table, 13.19 p/kWh under + cert calibration (legacy unit prices).""" + return prices.unit_price_p_per_kwh(_PV_EXPORT_TARIFF_CODE) * _PENCE_TO_GBP + + def _other_fuel_cost_gbp_per_kwh( meter_type: object, prices: PriceTable ) -> float: @@ -665,4 +697,6 @@ def cert_to_inputs( epc.sap_energy_source.meter_type, prices ), co2_factor_kg_per_kwh=_co2_factor_kg_per_kwh(main), + pv_generation_kwh_per_yr=_pv_generation_kwh_per_yr(epc), + pv_export_credit_gbp_per_kwh=_pv_export_credit_gbp_per_kwh(prices), )