Slice 32: §12 environmental closure (84/84) — Table 12d + per-end-use CO2

FULL CLOSURE. Cascade 768/768 + e2e 72/72 across all 6 Elmhurst fixtures.

Adds Table 12d (p.194) monthly CO2 emission factors for electricity to
`tables/table_12.py` + `co2_monthly_factors_kg_per_kwh(fuel_code)` helper.
Per 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 the annual average factor given in Table 12."

Calculator now consumes per-end-use CO2 factors on `CalculatorInputs`
(`main_heating_co2_factor_kg_per_kwh`, `secondary_heating_co2_factor_
kg_per_kwh`, `hot_water_co2_factor_kg_per_kwh`, `pumps_fans_co2_factor_
kg_per_kwh`, `lighting_co2_factor_kg_per_kwh`, `electric_shower_kwh_
per_yr`, `electric_shower_co2_factor_kg_per_kwh`). Defaults to None →
falls back to the global `co2_factor_kg_per_kwh` (legacy synthetic
path); cert_to_inputs supplies real values.

`_effective_monthly_co2_factor(monthly_kwh, fuel_code)` translates the
Table 12d monthly cascade into the calculator's annual×factor shape:
effective = Σ(kWh_m × CO2_m) / Σ(kWh_m). Used for the 4 electricity
end-uses (secondary, pumps/fans, lighting, electric shower). Gas end-
uses keep the annual Table 12 factor.

Adds `environmental_section_from_cert(epc) -> EnvironmentalSection`
exposing (261)..(274) line refs.

Worksheet display conventions:
- (265) excludes (264a) — electric shower CO2 contributes to (272)
  total but not the "space + water heating" subtotal.
- (273) is rounded to 2 d.p. half-up — the PDF displays with trailing
  zeros to 4 d.p. but precision is 2 d.p. throughout.

§12 LINE_ constants added to all 6 fixtures: (261), (262), (263),
(264), (264a), (265), (266), (267), (268), (269), (272), (273),
EI continuous, (274). 000487 (electric shower) has non-zero (264a).

FINAL SCOREBOARD:
- Cascade pins: 684/684 → 768/768 (§7..§12 all closed, 100%)
- e2e SapResult: 66/66 → 72/72 (all CO2 + sap + ecf + fuel cost)
- Wider regression: 1490/1490 PASS — zero failures anywhere

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-24 08:22:45 +00:00
parent 2bfecad272
commit fc1b009bf9
10 changed files with 425 additions and 11 deletions

View file

@ -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 = (

View file

@ -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- + 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,

View file

@ -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,

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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}")