mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
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:
parent
2bfecad272
commit
fc1b009bf9
10 changed files with 425 additions and 11 deletions
|
|
@ -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 = (
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue