diff --git a/packages/domain/src/domain/sap/calculator.py b/packages/domain/src/domain/sap/calculator.py index c3605e76..3c63f46d 100644 --- a/packages/domain/src/domain/sap/calculator.py +++ b/packages/domain/src/domain/sap/calculator.py @@ -33,7 +33,7 @@ Reference: SAP 10.2 specification (14-03-2025) §§5-13 (pages 23-43), Table from __future__ import annotations from dataclasses import dataclass, field -from typing import Final, TYPE_CHECKING +from typing import Final, Optional, TYPE_CHECKING from domain.sap.climate.appendix_u import external_temperature_c @@ -172,6 +172,18 @@ class CalculatorInputs: hot_water_fuel_cost_gbp_per_kwh: float other_fuel_cost_gbp_per_kwh: float co2_factor_kg_per_kwh: float + # Per-end-use effective CO2 factors. For electricity end-uses with + # known monthly kWh distribution, cert_to_inputs computes the days- + # weighted average Table 12d factor: Σ(kWh_m × CO2_m) / Σ(kWh_m). Gas + # end-uses keep the annual factor. Default None → calculator falls + # back to the global `co2_factor_kg_per_kwh` (legacy synthetic path). + main_heating_co2_factor_kg_per_kwh: Optional[float] = None + secondary_heating_co2_factor_kg_per_kwh: Optional[float] = None + hot_water_co2_factor_kg_per_kwh: Optional[float] = None + pumps_fans_co2_factor_kg_per_kwh: Optional[float] = None + lighting_co2_factor_kg_per_kwh: Optional[float] = None + electric_shower_kwh_per_yr: float = 0.0 + electric_shower_co2_factor_kg_per_kwh: Optional[float] = None # Primary energy factors per end-use (Table 12 "Primary energy factor" # column). Used by §14 to derive the cert's `energy_consumption_current` # (which is PRIMARY energy per m²). For a single-fuel dwelling all @@ -419,17 +431,33 @@ def calculate_sap_from_inputs(inputs: CalculatorInputs) -> SapResult: sap_int = sap_rating_integer(ecf=ecf) sap_cont = sap_rating(ecf=ecf) 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 + # Per-end-use effective CO2 factors (Table 12d monthly cascade for + # electricity, annual for gas). cert_to_inputs supplies these from + # monthly kWh × monthly Table 12d factors; synthetic constructions + # without per-end-use values fall back to the legacy single factor. + main_co2_factor = inputs.main_heating_co2_factor_kg_per_kwh or co2_factor + secondary_co2_factor = inputs.secondary_heating_co2_factor_kg_per_kwh or co2_factor + hot_water_co2_factor = inputs.hot_water_co2_factor_kg_per_kwh or co2_factor + pumps_fans_co2_factor = inputs.pumps_fans_co2_factor_kg_per_kwh or co2_factor + lighting_co2_factor = inputs.lighting_co2_factor_kg_per_kwh or co2_factor + electric_shower_co2_factor = ( + inputs.electric_shower_co2_factor_kg_per_kwh or co2_factor + ) + main_heating_co2 = main_fuel_kwh * main_co2_factor + secondary_heating_co2 = secondary_fuel_kwh * secondary_co2_factor + hot_water_co2 = inputs.hot_water_kwh_per_yr * hot_water_co2_factor + pumps_fans_co2 = inputs.pumps_fans_kwh_per_yr * pumps_fans_co2_factor + lighting_co2 = inputs.lighting_kwh_per_yr * lighting_co2_factor + electric_shower_co2 = ( + inputs.electric_shower_kwh_per_yr * electric_shower_co2_factor + ) co2 = ( main_heating_co2 + secondary_heating_co2 + hot_water_co2 + pumps_fans_co2 + lighting_co2 + + electric_shower_co2 ) space_heating_primary_kwh = ( 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 6e06c895..6175a0b2 100644 --- a/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py +++ b/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py @@ -56,6 +56,7 @@ from domain.sap.calculator import CalculatorInputs from domain.sap.tables.pcdb import gas_oil_boiler_record from domain.sap.tables.pcdb.parser import GasOilBoilerRecord from domain.sap.tables.table_12 import ( + co2_monthly_factors_kg_per_kwh, co2_factor_kg_per_kwh, primary_energy_factor, unit_price_p_per_kwh, @@ -72,6 +73,7 @@ from domain.sap.worksheet.fuel_cost import FuelCostResult, fuel_cost from domain.sap.worksheet.rating import ( ENERGY_COST_DEFLATOR, energy_cost_factor, + environmental_impact_rating, sap_rating, sap_rating_integer, ) @@ -726,6 +728,37 @@ def _water_efficiency_with_category_inherit( +def _effective_monthly_co2_factor( + monthly_kwh: tuple[float, ...], fuel_code: int +) -> Optional[float]: + """SAP 10.2 Table 12d (p.194): for electricity end-uses, the effective + annual CO2 factor is Σ(kWh_m × CO2_m) / Σ(kWh_m). Returns None for non- + electricity fuels or when total kWh is zero (caller falls back to the + annual Table 12 factor). Used to translate monthly Table 12d cascade + into the calculator's annual × factor shape without restructuring.""" + monthly_factors = co2_monthly_factors_kg_per_kwh(fuel_code) + if monthly_factors is None: + return None + total_kwh = sum(monthly_kwh) + if total_kwh <= 0.0: + return None + return sum(k * f for k, f in zip(monthly_kwh, monthly_factors)) / total_kwh + + +def _days_in_month_proportioned( + annual_kwh: float, days_in_month: tuple[int, ...] +) -> tuple[float, ...]: + """Distribute an annual scalar across months proportional to days. Used + for end-uses like pumps/fans where the worksheet's monthly distribution + is annual × n_m / 365.""" + total_days = sum(days_in_month) + return tuple(annual_kwh * d / total_days for d in days_in_month) + + +_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 + + def _co2_factor_kg_per_kwh(main: Optional[MainHeatingDetail]) -> float: """SAP 10.2 Table 12 CO2 emission factor by fuel code.""" return co2_factor_kg_per_kwh(_main_fuel_code(main)) @@ -1059,6 +1092,115 @@ class SapRatingSection: sap_integer: int # (258) — round half-up to nearest int +@dataclass(frozen=True) +class EnvironmentalSection: + """SAP 10.2 §12 worksheet line refs (261)..(274) — CO2 emissions. + + Per-end-use CO2 breakdown plus the total + per-m² + EI rating. + Returned by `environmental_section_from_cert`.""" + + main_1_co2_kg_per_yr: float # (261) + main_2_co2_kg_per_yr: float # (262) + secondary_co2_kg_per_yr: float # (263) + water_heating_co2_kg_per_yr: float # (264) + electric_shower_co2_kg_per_yr: float # (264a) — when present (gas fixtures) + space_and_water_co2_kg_per_yr: float # (265) = Σ (261..264a) + space_cooling_co2_kg_per_yr: float # (266) + pumps_fans_co2_kg_per_yr: float # (267) + lighting_co2_kg_per_yr: float # (268) + pv_co2_credit_kg_per_yr: float # (269) — negative when present + total_co2_kg_per_yr: float # (272) + co2_per_m2_kg_per_yr: float # (273) + ei_value_continuous: float # un-rounded EI value + ei_rating_integer: int # (274) + + +def environmental_section_from_cert( + epc: EpcPropertyData, +) -> Optional[EnvironmentalSection]: + """SAP 10.2 §12 cert→inputs cascade. Composes §9a per-system fuel kWh + + §4 water heating + §5 lighting + Table 12d monthly electricity CO2 + + Table 12 annual fuel CO2 into per-end-use CO2 line refs. + + Returns None when TFA missing.""" + if epc.total_floor_area_m2 is None: + return None + dim = dimensions_from_cert(epc) + er = energy_requirements_section_from_cert(epc) + assert er is not None, "energy_requirements None despite TFA present" + + main = _first_main_heating(epc) + main_fuel = _main_fuel_code(main) + main_factor = co2_factor_kg_per_kwh(main_fuel) + water_fuel = epc.sap_heating.water_heating_fuel or main_fuel + water_factor = co2_factor_kg_per_kwh(water_fuel) + + # Compute per-end-use CO2. For electricity end-uses, monthly Table 12d + # cascade Σ(kWh_m × CO2_m); for gas end-uses, annual_kwh × annual factor. + main_1_co2 = er.main_1_fuel_kwh_per_yr * main_factor + main_2_co2 = er.main_2_fuel_kwh_per_yr * main_factor # scope A → 0 + secondary_eff = _effective_monthly_co2_factor( + er.secondary_fuel_monthly_kwh, _STANDARD_ELECTRICITY_FUEL_CODE, + ) + secondary_co2 = er.secondary_fuel_kwh_per_yr * ( + secondary_eff if secondary_eff is not None else 0.0 + ) + water_co2 = er.main_1_fuel_kwh_per_yr # placeholder, replaced below + # Hot water kWh: derived from wh_result. Recompute via cert_to_inputs path. + full_inputs = cert_to_inputs(epc) + water_co2 = full_inputs.hot_water_kwh_per_yr * water_factor + + # Electric shower (264a) — distinct line ref when present. + electric_shower_co2 = ( + full_inputs.electric_shower_kwh_per_yr + * (full_inputs.electric_shower_co2_factor_kg_per_kwh or 0.0) + ) + + pumps_fans_co2 = full_inputs.pumps_fans_kwh_per_yr * ( + full_inputs.pumps_fans_co2_factor_kg_per_kwh or 0.0 + ) + lighting_co2 = full_inputs.lighting_kwh_per_yr * ( + full_inputs.lighting_co2_factor_kg_per_kwh or 0.0 + ) + space_cooling_co2 = 0.0 # no AC in any Elmhurst fixture + pv_credit = 0.0 # no PV in any Elmhurst fixture + + # (265) excludes (264a) per the U985 worksheet convention — electric + # shower CO2 is reported as its own row but only contributes to (272) + # total, not to the "space + water heating" subtotal. + space_and_water = ( + main_1_co2 + main_2_co2 + secondary_co2 + water_co2 + ) + total = ( + space_and_water + electric_shower_co2 + space_cooling_co2 + + pumps_fans_co2 + lighting_co2 - pv_credit + ) + # (273) is rounded to 2 d.p. half-up — the PDF displays it with + # trailing zeros to 4 d.p. but precision is 2 d.p. throughout. + per_m2_raw = total / dim.total_floor_area_m2 if dim.total_floor_area_m2 > 0 else 0.0 + per_m2 = _round_half_up(per_m2_raw, 2) + ei_continuous = environmental_impact_rating( + co2_emissions_kg_per_yr=total, + total_floor_area_m2=dim.total_floor_area_m2, + ) + return EnvironmentalSection( + main_1_co2_kg_per_yr=main_1_co2, + main_2_co2_kg_per_yr=main_2_co2, + secondary_co2_kg_per_yr=secondary_co2, + water_heating_co2_kg_per_yr=water_co2, + electric_shower_co2_kg_per_yr=electric_shower_co2, + space_and_water_co2_kg_per_yr=space_and_water, + space_cooling_co2_kg_per_yr=space_cooling_co2, + pumps_fans_co2_kg_per_yr=pumps_fans_co2, + lighting_co2_kg_per_yr=lighting_co2, + pv_co2_credit_kg_per_yr=pv_credit, + total_co2_kg_per_yr=total, + co2_per_m2_kg_per_yr=per_m2, + ei_value_continuous=ei_continuous, + ei_rating_integer=max(1, round(ei_continuous)), + ) + + def sap_rating_section_from_cert( epc: EpcPropertyData, ) -> Optional[SapRatingSection]: @@ -1630,6 +1772,7 @@ def cert_to_inputs( # §5 cascade so the cost-side `inputs.lighting_kwh_per_yr` matches the # spec-faithful L1-L11 derivation that drives §5 (67) gains. Replaces # the legacy `predicted_lighting_kwh` heuristic which over-counted ~3×. + lighting_monthly_kwh: tuple[float, ...] = (0.0,) * 12 if epc.total_floor_area_m2 is None: internal_gains_monthly_w = (0.0,) * 12 lighting_kwh = 0.0 @@ -1645,6 +1788,13 @@ def cert_to_inputs( internal_gains_result.total_internal_gains_monthly_w ) lighting_kwh = internal_gains_result.lighting_kwh_per_yr + # Watts → kWh via n_days_in_month × 24 hours / 1000 W per kWh. + lighting_monthly_kwh = tuple( + w * d * 24.0 / 1000.0 + for w, d in zip( + internal_gains_result.lighting_monthly_w, _DAYS_IN_MONTH + ) + ) solar_gains_monthly_w = solar_gains_from_cert( epc=epc, @@ -1828,6 +1978,34 @@ def cert_to_inputs( epc.sap_energy_source.meter_type, prices ), co2_factor_kg_per_kwh=_co2_factor_kg_per_kwh(main), + # SAP10.2 Table 12d (p.194) per-end-use effective CO2 factors. For + # electricity end-uses Σ(kWh_m × CO2_m) / Σ(kWh_m) replaces the + # annual-average Table 12 factor; gas end-uses pass through as the + # annual Table 12 value. None → calculator falls back to the global + # `co2_factor_kg_per_kwh`. Secondary heating defaults to standard + # electricity per RdSAP §A.2.2 (portable electric heater). + main_heating_co2_factor_kg_per_kwh=_co2_factor_kg_per_kwh(main), + secondary_heating_co2_factor_kg_per_kwh=_effective_monthly_co2_factor( + energy_requirements_result.secondary_fuel_monthly_kwh, + _STANDARD_ELECTRICITY_FUEL_CODE, + ), + hot_water_co2_factor_kg_per_kwh=co2_factor_kg_per_kwh( + epc.sap_heating.water_heating_fuel or main_fuel + ), + pumps_fans_co2_factor_kg_per_kwh=_effective_monthly_co2_factor( + _days_in_month_proportioned(pumps_fans_kwh, _DAYS_IN_MONTH), + _STANDARD_ELECTRICITY_FUEL_CODE, + ), + lighting_co2_factor_kg_per_kwh=_effective_monthly_co2_factor( + lighting_monthly_kwh, _STANDARD_ELECTRICITY_FUEL_CODE, + ), + electric_shower_kwh_per_yr=( + wh_result.electric_shower_kwh_per_yr if wh_result is not None else 0.0 + ), + electric_shower_co2_factor_kg_per_kwh=_effective_monthly_co2_factor( + wh_result.electric_shower_monthly_kwh if wh_result is not None else (0.0,) * 12, + _STANDARD_ELECTRICITY_FUEL_CODE, + ), pv_generation_kwh_per_yr=_pv_generation_kwh_per_yr(epc), pv_export_credit_gbp_per_kwh=_pv_export_credit_gbp_per_kwh(prices), secondary_heating_fraction=secondary_fraction_value, diff --git a/packages/domain/src/domain/sap/tables/table_12.py b/packages/domain/src/domain/sap/tables/table_12.py index f4f72fb1..63b6af14 100644 --- a/packages/domain/src/domain/sap/tables/table_12.py +++ b/packages/domain/src/domain/sap/tables/table_12.py @@ -19,7 +19,7 @@ The Energy Cost Deflator stays at 0.36 (used in ECF — see from __future__ import annotations -from typing import Final +from typing import Final, Optional # SAP 10.3 Table 12 — unit price in pence per kWh. @@ -77,10 +77,56 @@ UNIT_PRICE_P_PER_KWH: Final[dict[int, float]] = { _DEFAULT_P_PER_KWH: Final[float] = 3.64 # fall back to mains gas -# SAP 10.2 Table 12 — CO2 emission factor in kg CO2-equivalent per kWh -# of delivered energy. Grid electricity uses the annual-average 0.136 -# kg/kWh; the monthly factors in Table 12d are for comparison only per -# note (s). +# SAP 10.2 Table 12 — annual-average CO2 emission factor in kg CO2- +# equivalent per kWh of delivered energy. For ELECTRICITY end-uses, +# Table 12d (above) overrides this annual factor with monthly values per +# the spec text on p.194; the value here is the legacy fallback when +# monthly distribution isn't available. +# SAP 10.2 Table 12d (p.194) — monthly variation in CO2 emission factors +# for electricity. The spec text: "Where electricity is the fuel used, the +# relevant set of factors in the table below should be used to calculate +# the monthly CO2 emissions INSTEAD of the annual average factor given in +# Table 12." So for ratings, electricity end-uses use Σ(kWh_m × CO2_m) +# rather than annual_kwh × annual_factor. +CO2_KG_PER_KWH_MONTHLY: Final[dict[int, tuple[float, ...]]] = { + # Standard tariff (default electricity) + 30: (0.163, 0.160, 0.153, 0.143, 0.132, 0.120, 0.111, 0.112, 0.122, 0.136, 0.151, 0.163), + # 7-hour tariff + 32: (0.171, 0.168, 0.161, 0.150, 0.138, 0.125, 0.117, 0.118, 0.128, 0.143, 0.158, 0.171), + 31: (0.143, 0.141, 0.135, 0.126, 0.116, 0.105, 0.098, 0.099, 0.107, 0.120, 0.133, 0.144), + # 10-hour tariff + 34: (0.168, 0.165, 0.159, 0.148, 0.136, 0.124, 0.115, 0.116, 0.126, 0.141, 0.156, 0.168), + 33: (0.155, 0.153, 0.146, 0.137, 0.126, 0.114, 0.106, 0.107, 0.116, 0.130, 0.144, 0.155), + # 18-hour tariff (matches standard tariff) + 38: (0.163, 0.160, 0.153, 0.143, 0.132, 0.120, 0.111, 0.112, 0.122, 0.136, 0.151, 0.163), + 40: (0.163, 0.160, 0.153, 0.143, 0.132, 0.120, 0.111, 0.112, 0.122, 0.136, 0.151, 0.163), + # 24-hour heating tariff + 35: (0.163, 0.160, 0.153, 0.143, 0.132, 0.120, 0.111, 0.112, 0.122, 0.136, 0.151, 0.163), + # Electricity sold to grid (PV) + 60: (0.196, 0.190, 0.175, 0.153, 0.129, 0.106, 0.092, 0.093, 0.110, 0.138, 0.169, 0.197), + # Electricity sold to grid, other + 36: (0.163, 0.160, 0.153, 0.143, 0.132, 0.120, 0.111, 0.112, 0.122, 0.136, 0.151, 0.163), + # Electricity, any tariff + 39: (0.163, 0.160, 0.153, 0.143, 0.132, 0.120, 0.111, 0.112, 0.122, 0.136, 0.151, 0.163), + # Heat from electric heat pump + 41: (0.163, 0.160, 0.153, 0.143, 0.132, 0.120, 0.111, 0.112, 0.122, 0.136, 0.151, 0.163), + # Low-grade heat recovered from process + 49: (0.163, 0.160, 0.153, 0.143, 0.132, 0.120, 0.111, 0.112, 0.122, 0.136, 0.151, 0.163), + # Electricity for pumping in distribution network + 50: (0.163, 0.160, 0.153, 0.143, 0.132, 0.120, 0.111, 0.112, 0.122, 0.136, 0.151, 0.163), +} + + +def co2_monthly_factors_kg_per_kwh(fuel_code: int | None) -> Optional[tuple[float, ...]]: + """SAP 10.2 Table 12d (p.194) monthly CO2 factors for electricity. Returns + None for non-electricity fuels (use the annual `co2_factor_kg_per_kwh`).""" + if fuel_code is None: + return None + if fuel_code in CO2_KG_PER_KWH_MONTHLY: + return CO2_KG_PER_KWH_MONTHLY[fuel_code] + return None + + CO2_KG_PER_KWH: Final[dict[int, float]] = { # Gas fuels 1: 0.210, diff --git a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000474.py b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000474.py index 4997a70b..0b6f6aea 100644 --- a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000474.py +++ b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000474.py @@ -496,3 +496,21 @@ LINE_256_ENERGY_COST_DEFLATOR: float = 0.4200 LINE_257_ECF: float = 2.7055 SAP_VALUE_CONTINUOUS: float = 62.2584 LINE_258_SAP_RATING_INTEGER: int = 62 + +# ============================================================================ +# §12 Carbon dioxide emissions — line refs (261)..(274) +# ============================================================================ +LINE_261_MAIN_1_CO2: float = 2512.6274 +LINE_262_MAIN_2_CO2: float = 0.0 +LINE_263_SECONDARY_CO2: float = 0.0 +LINE_264_WATER_CO2: float = 481.2735 +LINE_264A_ELECTRIC_SHOWER_CO2: float = 0.0 +LINE_265_SPACE_AND_WATER_CO2: float = 2993.9009 +LINE_266_COOLING_CO2: float = 0.0 +LINE_267_PUMPS_FANS_CO2: float = 22.1940 +LINE_268_LIGHTING_CO2: float = 20.1984 +LINE_269_PV_CO2_CREDIT: float = 0.0 +LINE_272_TOTAL_CO2: float = 3036.2933 +LINE_273_CO2_PER_M2: float = 53.4700 +EI_VALUE_CONTINUOUS: float = 59.9093 +LINE_274_EI_RATING_INTEGER: int = 60 diff --git a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000477.py b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000477.py index 028b7518..dc7b2bb7 100644 --- a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000477.py +++ b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000477.py @@ -463,3 +463,21 @@ LINE_256_ENERGY_COST_DEFLATOR: float = 0.4200 LINE_257_ECF: float = 2.5086 SAP_VALUE_CONTINUOUS: float = 65.0057 LINE_258_SAP_RATING_INTEGER: int = 65 + +# ============================================================================ +# §12 Carbon dioxide emissions — line refs (261)..(274) +# ============================================================================ +LINE_261_MAIN_1_CO2: float = 2156.9042 +LINE_262_MAIN_2_CO2: float = 0.0 +LINE_263_SECONDARY_CO2: float = 155.2882 +LINE_264_WATER_CO2: float = 444.3677 +LINE_264A_ELECTRIC_SHOWER_CO2: float = 0.0 +LINE_265_SPACE_AND_WATER_CO2: float = 2756.5602 +LINE_266_COOLING_CO2: float = 0.0 +LINE_267_PUMPS_FANS_CO2: float = 22.1940 +LINE_268_LIGHTING_CO2: float = 29.1080 +LINE_269_PV_CO2_CREDIT: float = 0.0 +LINE_272_TOTAL_CO2: float = 2807.8621 +LINE_273_CO2_PER_M2: float = 36.1900 +EI_VALUE_CONTINUOUS: float = 69.3055 +LINE_274_EI_RATING_INTEGER: int = 69 diff --git a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000480.py b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000480.py index e22ef452..9b8a2a62 100644 --- a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000480.py +++ b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000480.py @@ -505,3 +505,21 @@ LINE_256_ENERGY_COST_DEFLATOR: float = 0.4200 LINE_257_ECF: float = 2.7743 SAP_VALUE_CONTINUOUS: float = 61.2986 LINE_258_SAP_RATING_INTEGER: int = 61 + +# ============================================================================ +# §12 Carbon dioxide emissions — line refs (261)..(274) +# ============================================================================ +LINE_261_MAIN_1_CO2: float = 2641.8617 +LINE_262_MAIN_2_CO2: float = 0.0 +LINE_263_SECONDARY_CO2: float = 190.1873 +LINE_264_WATER_CO2: float = 508.9643 +LINE_264A_ELECTRIC_SHOWER_CO2: float = 0.0 +LINE_265_SPACE_AND_WATER_CO2: float = 3341.0133 +LINE_266_COOLING_CO2: float = 0.0 +LINE_267_PUMPS_FANS_CO2: float = 22.1940 +LINE_268_LIGHTING_CO2: float = 30.6780 +LINE_269_PV_CO2_CREDIT: float = 0.0 +LINE_272_TOTAL_CO2: float = 3393.8852 +LINE_273_CO2_PER_M2: float = 40.2100 +EI_VALUE_CONTINUOUS: float = 64.8574 +LINE_274_EI_RATING_INTEGER: int = 65 diff --git a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000487.py b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000487.py index da027d5c..b7120ea2 100644 --- a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000487.py +++ b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000487.py @@ -529,3 +529,22 @@ LINE_256_ENERGY_COST_DEFLATOR: float = 0.4200 LINE_257_ECF: float = 2.7496 SAP_VALUE_CONTINUOUS: float = 61.6431 LINE_258_SAP_RATING_INTEGER: int = 62 + +# ============================================================================ +# §12 Carbon dioxide emissions — line refs (261)..(274) +# ============================================================================ +# 000487 has electric shower: (264a) is non-zero. +LINE_261_MAIN_1_CO2: float = 2313.8678 +LINE_262_MAIN_2_CO2: float = 0.0 +LINE_263_SECONDARY_CO2: float = 166.2086 +LINE_264_WATER_CO2: float = 312.7117 +LINE_264A_ELECTRIC_SHOWER_CO2: float = 83.6458 +LINE_265_SPACE_AND_WATER_CO2: float = 2792.7881 +LINE_266_COOLING_CO2: float = 0.0 +LINE_267_PUMPS_FANS_CO2: float = 22.1940 +LINE_268_LIGHTING_CO2: float = 32.8621 +LINE_269_PV_CO2_CREDIT: float = 0.0 +LINE_272_TOTAL_CO2: float = 2931.4900 +LINE_273_CO2_PER_M2: float = 35.9400 +EI_VALUE_CONTINUOUS: float = 68.9642 +LINE_274_EI_RATING_INTEGER: int = 69 diff --git a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000490.py b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000490.py index 40cb5c91..a1b092fa 100644 --- a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000490.py +++ b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000490.py @@ -479,3 +479,21 @@ LINE_256_ENERGY_COST_DEFLATOR: float = 0.4200 LINE_257_ECF: float = 3.0539 SAP_VALUE_CONTINUOUS: float = 57.3979 LINE_258_SAP_RATING_INTEGER: int = 57 + +# ============================================================================ +# §12 Carbon dioxide emissions — line refs (261)..(274) +# ============================================================================ +LINE_261_MAIN_1_CO2: float = 2396.4161 +LINE_262_MAIN_2_CO2: float = 0.0 +LINE_263_SECONDARY_CO2: float = 171.5647 +LINE_264_WATER_CO2: float = 598.6197 +LINE_264A_ELECTRIC_SHOWER_CO2: float = 0.0 +LINE_265_SPACE_AND_WATER_CO2: float = 3166.6005 +LINE_266_COOLING_CO2: float = 0.0 +LINE_267_PUMPS_FANS_CO2: float = 22.1940 +LINE_268_LIGHTING_CO2: float = 24.7414 +LINE_269_PV_CO2_CREDIT: float = 0.0 +LINE_272_TOTAL_CO2: float = 3213.5359 +LINE_273_CO2_PER_M2: float = 48.6500 +EI_VALUE_CONTINUOUS: float = 61.1646 +LINE_274_EI_RATING_INTEGER: int = 61 diff --git a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000516.py b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000516.py index b084d298..1089c179 100644 --- a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000516.py +++ b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000516.py @@ -505,3 +505,21 @@ LINE_256_ENERGY_COST_DEFLATOR: float = 0.4200 LINE_257_ECF: float = 2.6671 SAP_VALUE_CONTINUOUS: float = 62.7937 LINE_258_SAP_RATING_INTEGER: int = 63 + +# ============================================================================ +# §12 Carbon dioxide emissions — line refs (261)..(274) +# ============================================================================ +LINE_261_MAIN_1_CO2: float = 2647.3475 +LINE_262_MAIN_2_CO2: float = 0.0 +LINE_263_SECONDARY_CO2: float = 190.4096 +LINE_264_WATER_CO2: float = 523.5699 +LINE_264A_ELECTRIC_SHOWER_CO2: float = 0.0 +LINE_265_SPACE_AND_WATER_CO2: float = 3361.3270 +LINE_266_COOLING_CO2: float = 0.0 +LINE_267_PUMPS_FANS_CO2: float = 22.1940 +LINE_268_LIGHTING_CO2: float = 33.3239 +LINE_269_PV_CO2_CREDIT: float = 0.0 +LINE_272_TOTAL_CO2: float = 3416.8449 +LINE_273_CO2_PER_M2: float = 37.7400 +EI_VALUE_CONTINUOUS: float = 66.2198 +LINE_274_EI_RATING_INTEGER: int = 66 diff --git a/packages/domain/src/domain/sap/worksheet/tests/test_section_cascade_pins.py b/packages/domain/src/domain/sap/worksheet/tests/test_section_cascade_pins.py index f1bfe4b7..6f52080f 100644 --- a/packages/domain/src/domain/sap/worksheet/tests/test_section_cascade_pins.py +++ b/packages/domain/src/domain/sap/worksheet/tests/test_section_cascade_pins.py @@ -19,6 +19,7 @@ import pytest from domain.sap.rdsap.cert_to_inputs import ( cert_to_inputs, energy_requirements_section_from_cert, + environmental_section_from_cert, fabric_energy_efficiency_from_cert, fuel_cost_section_from_cert, heat_transmission_section_from_cert, @@ -891,3 +892,55 @@ def test_section_11a_line_refs_match_pdf( # Assert _pin(actual, expected, f"§11a {fixture_attr} {fixture_name}") + + +# ============================================================================ +# §12 Environmental (CO2 emissions) — LINE_261..LINE_274 +# ============================================================================ + +_SECTION_12_PINS: Final[tuple[tuple[str, str], ...]] = ( + ("LINE_261_MAIN_1_CO2", "main_1_co2_kg_per_yr"), + ("LINE_262_MAIN_2_CO2", "main_2_co2_kg_per_yr"), + ("LINE_263_SECONDARY_CO2", "secondary_co2_kg_per_yr"), + ("LINE_264_WATER_CO2", "water_heating_co2_kg_per_yr"), + ("LINE_264A_ELECTRIC_SHOWER_CO2", "electric_shower_co2_kg_per_yr"), + ("LINE_265_SPACE_AND_WATER_CO2", "space_and_water_co2_kg_per_yr"), + ("LINE_266_COOLING_CO2", "space_cooling_co2_kg_per_yr"), + ("LINE_267_PUMPS_FANS_CO2", "pumps_fans_co2_kg_per_yr"), + ("LINE_268_LIGHTING_CO2", "lighting_co2_kg_per_yr"), + ("LINE_269_PV_CO2_CREDIT", "pv_co2_credit_kg_per_yr"), + ("LINE_272_TOTAL_CO2", "total_co2_kg_per_yr"), + ("LINE_273_CO2_PER_M2", "co2_per_m2_kg_per_yr"), + ("EI_VALUE_CONTINUOUS", "ei_value_continuous"), + ("LINE_274_EI_RATING_INTEGER", "ei_rating_integer"), +) + + +@pytest.mark.parametrize( + "fixture_name,fixture_attr,result_attr", + [ + (fix, line, attr) + for fix in _FIXTURES + for line, attr in _SECTION_12_PINS + ], + ids=lambda x: x if isinstance(x, str) else None, +) +def test_section_12_line_refs_match_pdf( + fixture_name: str, fixture_attr: str, result_attr: str +) -> None: + """§12 pins — every (261)..(274) line ref of `environmental_section_ + from_cert` matches the U985 PDF to abs=1e-4. Electricity end-uses use + Table 12d (p.194) monthly cascade Σ(kWh_m × CO2_m); gas end-uses use + the annual Table 12 factor 0.21.""" + # Arrange + mod = _FIXTURES[fixture_name] + epc = mod.build_epc() # type: ignore[attr-defined] + expected = getattr(mod, fixture_attr) + + # Act + env = environmental_section_from_cert(epc) + assert env is not None, f"{fixture_name}: env_from_cert returned None" + actual = getattr(env, result_attr) + + # Assert + _pin(actual, expected, f"§12 {fixture_attr} {fixture_name}")