mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
S0380.180: heat-network distribution pumping electricity (§C3.2) — closes CH1
SAP 10.2 Appendix C §C3.2 (PDF p.51), verbatim: "CO2 emissions and
Primary Energy associated with the electricity used for pumping water
through the distribution system are allowed for by adding electrical
energy equal to 1% of the energy required for space and water heating."
Worksheet line (313) = 0.01 × [(307)+(310)]; its CO2 (372) and PE (472)
bill on the Table 12d/12e monthly factors for fuel code 50 ("electricity
for pumping in distribution network"), weighted by the monthly heat
profile per worksheet footnote (a). (307)m/(310)m = (space_demand +
hw_output) / efficiency (the cascade models a heat network's generator
efficiency as 1/DLF).
This un-defers the (372)/(472) front the post-S0380.179 handover flagged
"don't guess until the factor source is identified": the source is
§C3.2 + Table 12d/12e code 50, NOT an empirical constant. The apparent
0.1994/0.2114 "factor" is an Elmhurst DISPLAY artifact — the worksheet
shows the (372) energy column as 0.01×(307) (space only) while computing
emissions on 0.01×(307+310) per the §C3.2 text. Verified EXACT line-by-
line against the CH2 corpus worksheet: (372)=23.6007 CO2 (rating),
(472)=208.2267 PE (demand).
New `_heat_network_distribution_electricity` helper (gated on
`_is_heat_network_main`) precomputes the energy + effective CO2/PE
factors; three new CalculatorInputs fields + calculator.py CO2/PE
summation terms (0.0/None → no-op for individually-heated certs).
Closures:
CH1 (Boilers/Gas) CO2 −23.60→−0.00, PE −208.23→+0.00 — FULLY EXACT
CH3 (HP/Elec) CO2 −98.92→−75.32, PE −457.54→−249.32 (distribution
component closed; code-304 community-HP COP remains)
CH2/CH4/CH6 gain their (372)/(472) component (CO2 +23.6, PE
+208.2); dominant CHP displaced-electricity credit
residual (Table 12f + block 12b/13b) is next slice.
No regression on the other 36 corpus variants (helper returns None off
heat-network mains) + golden + U985 fixtures. 2223 pass + 1 skip + 0
fail; pyright net-zero 43→43.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ba2d6e1cbb
commit
8452cf9e2d
4 changed files with 223 additions and 5 deletions
|
|
@ -674,11 +674,27 @@ _EXPECTATIONS: tuple[_CorpusExpectation, ...] = (
|
|||
# distribution" line — 118.38 kWh billed at electricity factors
|
||||
# (CO2 0.1993, PE 1.760), not heat-network factors — the cascade
|
||||
# doesn't currently meter this. Next follow-up slice.
|
||||
_CorpusExpectation(variant='community heating 1', block='11b', expected_sap_resid=+0.0000, expected_cost_resid_gbp=-0.0000, expected_co2_resid_kg=-23.6007, expected_pe_resid_kwh=-208.2267),
|
||||
_CorpusExpectation(variant='community heating 2', block='11b', expected_sap_resid=+0.5277, expected_cost_resid_gbp=-12.1598, expected_co2_resid_kg=-1435.0874, expected_pe_resid_kwh=+1123.0063),
|
||||
_CorpusExpectation(variant='community heating 3', block='11b', expected_sap_resid=+0.0000, expected_cost_resid_gbp=-0.0000, expected_co2_resid_kg=-98.9235, expected_pe_resid_kwh=-457.5428),
|
||||
_CorpusExpectation(variant='community heating 4', block='11b', expected_sap_resid=+0.5277, expected_cost_resid_gbp=-12.1598, expected_co2_resid_kg=-4401.8456, expected_pe_resid_kwh=+111.5798),
|
||||
_CorpusExpectation(variant='community heating 6', block='11b', expected_sap_resid=-7.4942, expected_cost_resid_gbp=+172.6778, expected_co2_resid_kg=-2939.6683, expected_pe_resid_kwh=+7481.5658),
|
||||
# Slice S0380.180 wired the SAP 10.2 Appendix C §C3.2 (PDF p.51)
|
||||
# heat-network distribution pumping electricity (worksheet (313) =
|
||||
# 0.01 × [(307)+(310)]; CO2 (372) / PE (472) on Table 12d/12e fuel-
|
||||
# code-50 monthly factors weighted by the monthly heat profile).
|
||||
# CH1 (Boilers/Gas) closes FULLY — the (372)/(472) line was its
|
||||
# entire remaining residual (un-defers the front the predecessor
|
||||
# handover flagged "don't guess"; the factor source is §C3.2 +
|
||||
# Table 12f, not an empirical constant). CH3 (HP/Elec) closes its
|
||||
# distribution component (CO2 −98.92→−75.32, PE −457.54→−249.32);
|
||||
# the remainder is the code-304 community-HP COP cascade (separate
|
||||
# follow-up). CH2/CH4/CH6 gain their (372)/(472) component (CO2
|
||||
# +23.6, PE +208.2/+208.2/+208.2); their dominant CHP displaced-
|
||||
# electricity credit residual (Table 12f + block 12b/13b) remains
|
||||
# for the next slice. Elmhurst DISPLAYS the (372) energy column as
|
||||
# 0.01 × (307) (space only) but computes emissions on 0.01 ×
|
||||
# (307+310) per the §C3.2 text — verified EXACT line-by-line.
|
||||
_CorpusExpectation(variant='community heating 1', block='11b', expected_sap_resid=+0.0000, expected_cost_resid_gbp=-0.0000, expected_co2_resid_kg=-0.0000, expected_pe_resid_kwh=+0.0000),
|
||||
_CorpusExpectation(variant='community heating 2', block='11b', expected_sap_resid=+0.5277, expected_cost_resid_gbp=-12.1598, expected_co2_resid_kg=-1411.4867, expected_pe_resid_kwh=+1331.2330),
|
||||
_CorpusExpectation(variant='community heating 3', block='11b', expected_sap_resid=+0.0000, expected_cost_resid_gbp=-0.0000, expected_co2_resid_kg=-75.3228, expected_pe_resid_kwh=-249.3161),
|
||||
_CorpusExpectation(variant='community heating 4', block='11b', expected_sap_resid=+0.5277, expected_cost_resid_gbp=-12.1598, expected_co2_resid_kg=-4378.2449, expected_pe_resid_kwh=+319.8065),
|
||||
_CorpusExpectation(variant='community heating 6', block='11b', expected_sap_resid=-7.4942, expected_cost_resid_gbp=+172.6778, expected_co2_resid_kg=-2916.0676, expected_pe_resid_kwh=+7689.7925),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -236,6 +236,17 @@ class CalculatorInputs:
|
|||
pumps_fans_primary_factor: Optional[float] = None
|
||||
lighting_primary_factor: Optional[float] = None
|
||||
electric_shower_primary_factor: Optional[float] = None
|
||||
# SAP 10.2 Appendix C §C3.2 (PDF p.51) — heat-network distribution
|
||||
# pumping electricity. For community-heating mains the network pump
|
||||
# energy = 1% of (space + water) heat generated (worksheet (313));
|
||||
# its CO2 / PE (worksheet (372)/(472)) bill on Table 12d/12e monthly
|
||||
# electricity factors (fuel code 50) weighted by the monthly heat
|
||||
# profile. The energy + effective factors are precomputed in
|
||||
# cert_to_inputs. 0.0 / None for individually-heated certs (no
|
||||
# distribution loop) leaves the cascade unchanged.
|
||||
heat_network_distribution_kwh_per_yr: float = 0.0
|
||||
heat_network_distribution_co2_factor_kg_per_kwh: Optional[float] = None
|
||||
heat_network_distribution_primary_factor: Optional[float] = None
|
||||
# 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).
|
||||
|
|
@ -596,6 +607,13 @@ def calculate_sap_from_inputs(inputs: CalculatorInputs) -> SapResult:
|
|||
electric_shower_co2 = (
|
||||
inputs.electric_shower_kwh_per_yr * electric_shower_co2_factor
|
||||
)
|
||||
# SAP 10.2 Appendix C §C3.2 (PDF p.51) worksheet (372) — electricity
|
||||
# for pumping water through a heat network's distribution system.
|
||||
# Zero for individually-heated certs (factor None → 0.0).
|
||||
heat_network_distribution_co2 = (
|
||||
inputs.heat_network_distribution_kwh_per_yr
|
||||
* (inputs.heat_network_distribution_co2_factor_kg_per_kwh or 0.0)
|
||||
)
|
||||
co2 = (
|
||||
main_heating_co2
|
||||
+ secondary_heating_co2
|
||||
|
|
@ -603,6 +621,7 @@ def calculate_sap_from_inputs(inputs: CalculatorInputs) -> SapResult:
|
|||
+ pumps_fans_co2
|
||||
+ lighting_co2
|
||||
+ electric_shower_co2
|
||||
+ heat_network_distribution_co2
|
||||
)
|
||||
# SAP 10.2 Appendix M1 §7 — subtract PV CO2 credit. Onsite consumption
|
||||
# offsets grid imports at the IMPORT CO2 factor (Table 12d weighted
|
||||
|
|
@ -662,6 +681,12 @@ def calculate_sap_from_inputs(inputs: CalculatorInputs) -> SapResult:
|
|||
+ inputs.lighting_kwh_per_yr * lighting_primary_factor
|
||||
+ inputs.electric_shower_kwh_per_yr * electric_shower_primary_factor
|
||||
)
|
||||
# SAP 10.2 Appendix C §C3.2 (PDF p.51) worksheet (472) — heat-network
|
||||
# distribution pumping electricity primary energy (CO2 sister above).
|
||||
heat_network_distribution_primary_kwh = (
|
||||
inputs.heat_network_distribution_kwh_per_yr
|
||||
* (inputs.heat_network_distribution_primary_factor or 0.0)
|
||||
)
|
||||
# SAP 10.2 Appendix M1 §8: PV onsite consumption credits at IMPORT
|
||||
# PEF (offsets grid imports); PV exports credit at the EXPORT PEF
|
||||
# ("electricity sold to grid, PV" — Table 12 code 60 = 0.501). When
|
||||
|
|
@ -696,6 +721,7 @@ def calculate_sap_from_inputs(inputs: CalculatorInputs) -> SapResult:
|
|||
space_heating_primary_kwh
|
||||
+ hot_water_primary_kwh
|
||||
+ other_primary_kwh
|
||||
+ heat_network_distribution_primary_kwh
|
||||
- pv_primary_offset_kwh,
|
||||
)
|
||||
primary_energy_per_m2 = primary_energy_kwh / tfa if tfa > 0 else 0.0
|
||||
|
|
@ -738,6 +764,8 @@ def calculate_sap_from_inputs(inputs: CalculatorInputs) -> SapResult:
|
|||
"hot_water_co2_kg_per_yr": hot_water_co2,
|
||||
"pumps_fans_co2_kg_per_yr": pumps_fans_co2,
|
||||
"lighting_co2_kg_per_yr": lighting_co2,
|
||||
"heat_network_distribution_co2_kg_per_yr": heat_network_distribution_co2,
|
||||
"heat_network_distribution_pe_kwh_per_yr": heat_network_distribution_primary_kwh,
|
||||
"space_heating_pe_kwh_per_m2": space_heating_primary_kwh / tfa if tfa > 0 else 0.0,
|
||||
"hot_water_pe_kwh_per_m2": hot_water_primary_kwh / tfa if tfa > 0 else 0.0,
|
||||
"other_pe_kwh_per_m2": other_primary_kwh / tfa if tfa > 0 else 0.0,
|
||||
|
|
|
|||
|
|
@ -913,6 +913,77 @@ def _heat_network_dlf(age_band: Optional[str]) -> float:
|
|||
raise UnmappedSapCode("heat_network_age_band", age_band)
|
||||
|
||||
|
||||
# SAP 10.2 Table 12 fuel code 50 — "electricity for pumping in
|
||||
# distribution network". Its CO2 / PE factors vary by month per Table
|
||||
# 12d / 12e (= standard-electricity profile); worksheet (372)/(472)
|
||||
# footnote (a) applies the monthly factors weighted by the heat profile.
|
||||
_ELECTRICITY_FOR_DISTRIBUTION_PUMPING_FUEL_CODE: Final[int] = 50
|
||||
# SAP 10.2 Appendix C §C3.2 (PDF p.51) — pumping energy = 1% of the
|
||||
# energy required for space and water heating.
|
||||
_HEAT_NETWORK_PUMPING_FRACTION_OF_HEAT: Final[float] = 0.01
|
||||
|
||||
|
||||
def _heat_network_distribution_electricity(
|
||||
main: Optional[MainHeatingDetail],
|
||||
space_heating_monthly_kwh: tuple[float, ...],
|
||||
hot_water_output_monthly_kwh: tuple[float, ...],
|
||||
efficiency: float,
|
||||
) -> Optional[tuple[float, float, float]]:
|
||||
"""SAP 10.2 Appendix C §C3.2 (PDF p.51) — electricity for pumping
|
||||
water through a heat network's distribution system.
|
||||
|
||||
Spec verbatim: "CO2 emissions and Primary Energy associated with the
|
||||
electricity used for pumping water through the distribution system
|
||||
are allowed for by adding electrical energy equal to 1% of the
|
||||
energy required for space and water heating." Worksheet line (313) =
|
||||
0.01 × [(307) + (310)]; its CO2 (372) and PE (472) bill on the
|
||||
Table 12d / 12e monthly factors for fuel code 50 ("electricity for
|
||||
pumping in distribution network"), weighted by the monthly heat
|
||||
profile per worksheet footnote (a).
|
||||
|
||||
(307)m = space-heating fuel and (310)m = water-heating fuel: for a
|
||||
heat network the cascade models the heat-generator efficiency as
|
||||
1/DLF, so fuel = q_useful / efficiency = q_useful × DLF. The
|
||||
monthly weighting of the Table 12d/12e factor is shape-only (the DLF
|
||||
scalar cancels), and the energy carries the DLF.
|
||||
|
||||
Returns (energy_kwh, co2_factor, pe_factor) for heat-network mains
|
||||
(Table 4a 301-304 / category 6); None otherwise so the default
|
||||
0.0 / None fields leave individually-heated certs unchanged.
|
||||
|
||||
NB Elmhurst's worksheet DISPLAYS the (372) energy column as 0.01 ×
|
||||
(307) (space only) but computes the EMISSIONS on 0.01 × (307+310)
|
||||
per the §C3.2 text — verified line-by-line against the community-
|
||||
heating corpus worksheets. We mirror the spec text (space + water).
|
||||
"""
|
||||
if not _is_heat_network_main(main) or efficiency <= 0.0:
|
||||
return None
|
||||
distribution_monthly_kwh = tuple(
|
||||
_HEAT_NETWORK_PUMPING_FRACTION_OF_HEAT * (sh + hw) / efficiency
|
||||
for sh, hw in zip(
|
||||
space_heating_monthly_kwh, hot_water_output_monthly_kwh
|
||||
)
|
||||
)
|
||||
energy_kwh = sum(distribution_monthly_kwh)
|
||||
if energy_kwh <= 0.0:
|
||||
return None
|
||||
co2_monthly = co2_monthly_factors_kg_per_kwh(
|
||||
_ELECTRICITY_FOR_DISTRIBUTION_PUMPING_FUEL_CODE
|
||||
)
|
||||
pe_monthly = pe_monthly_factors_kwh_per_kwh(
|
||||
_ELECTRICITY_FOR_DISTRIBUTION_PUMPING_FUEL_CODE
|
||||
)
|
||||
if co2_monthly is None or pe_monthly is None:
|
||||
return None
|
||||
co2_factor = sum(
|
||||
kwh * f for kwh, f in zip(distribution_monthly_kwh, co2_monthly)
|
||||
) / energy_kwh
|
||||
pe_factor = sum(
|
||||
kwh * f for kwh, f in zip(distribution_monthly_kwh, pe_monthly)
|
||||
) / energy_kwh
|
||||
return (energy_kwh, co2_factor, pe_factor)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PriceTable:
|
||||
"""Seam between the spec-correct SAP 10.2/10.3 Table 12 prices and
|
||||
|
|
@ -6125,6 +6196,16 @@ def cert_to_inputs(
|
|||
tariff=_rdsap_tariff(epc),
|
||||
) + _hw_extra_standing
|
||||
|
||||
# SAP 10.2 Appendix C §C3.2 (PDF p.51) — heat-network distribution
|
||||
# pumping electricity (worksheet (313)/(372)/(472)). None for
|
||||
# individually-heated certs.
|
||||
heat_network_distribution = _heat_network_distribution_electricity(
|
||||
main,
|
||||
space_heating_result.total_space_heating_monthly_kwh,
|
||||
hw_monthly_kwh_for_factors,
|
||||
eff,
|
||||
)
|
||||
|
||||
return CalculatorInputs(
|
||||
dimensions=dim,
|
||||
heat_transmission=ht,
|
||||
|
|
@ -6214,6 +6295,21 @@ def cert_to_inputs(
|
|||
epc, energy_requirements_result.secondary_fuel_monthly_kwh,
|
||||
),
|
||||
hot_water_co2_factor_kg_per_kwh=hw_co2_factor,
|
||||
# SAP 10.2 Appendix C §C3.2 (PDF p.51) — heat-network distribution
|
||||
# pumping electricity (worksheet (313)/(372)/(472)). 0.0 / None
|
||||
# on individually-heated certs.
|
||||
heat_network_distribution_kwh_per_yr=(
|
||||
heat_network_distribution[0]
|
||||
if heat_network_distribution is not None else 0.0
|
||||
),
|
||||
heat_network_distribution_co2_factor_kg_per_kwh=(
|
||||
heat_network_distribution[1]
|
||||
if heat_network_distribution is not None else None
|
||||
),
|
||||
heat_network_distribution_primary_factor=(
|
||||
heat_network_distribution[2]
|
||||
if heat_network_distribution is not None else None
|
||||
),
|
||||
# SAP 10.2 Table 12a Grid 2 (p.191) + Table 12d (p.194): pumps,
|
||||
# lighting, and the electric-shower end-use all bill via the
|
||||
# "All other uses" row → on off-peak tariffs blend the high /
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ from domain.sap10_calculator.exceptions import (
|
|||
from domain.sap10_calculator.rdsap.cert_to_inputs import (
|
||||
SAP_10_2_SPEC_PRICES,
|
||||
_has_suspended_timber_floor_per_spec, # pyright: ignore[reportPrivateUsage]
|
||||
_heat_network_distribution_electricity, # pyright: ignore[reportPrivateUsage]
|
||||
_heat_network_dlf, # pyright: ignore[reportPrivateUsage]
|
||||
_is_electric_main, # pyright: ignore[reportPrivateUsage]
|
||||
_is_electric_water, # pyright: ignore[reportPrivateUsage]
|
||||
|
|
@ -153,6 +154,83 @@ def test_heat_network_main_applies_table12c_dlf_to_main_heating_efficiency() ->
|
|||
assert inputs.main_heating_efficiency == pytest.approx(1.0 / 1.41, abs=0.005)
|
||||
|
||||
|
||||
def test_heat_network_distribution_electricity_per_sap_10_2_appendix_c_3_2() -> None:
|
||||
# Arrange — heat-network main (Table 4a code 301 = community heating,
|
||||
# category 6). SAP 10.2 Appendix C §C3.2 (PDF p.51): distribution
|
||||
# pumping electricity = 1% of the (space + water) heat generated,
|
||||
# i.e. 0.01 × [(307) + (310)] where (307)m/(310)m = (space_demand +
|
||||
# hw_output) / efficiency. Its CO2 (372) / PE (472) bill on the
|
||||
# Table 12d / 12e monthly factors for fuel code 50 ("electricity for
|
||||
# pumping in distribution network"), weighted by the monthly heat
|
||||
# profile per worksheet footnote (a).
|
||||
from domain.sap10_calculator.tables.table_12 import (
|
||||
co2_monthly_factors_kg_per_kwh,
|
||||
pe_monthly_factors_kwh_per_kwh,
|
||||
)
|
||||
|
||||
main = MainHeatingDetail(
|
||||
has_fghrs=False,
|
||||
main_fuel_type=20,
|
||||
heat_emitter_type=1,
|
||||
emitter_temperature=1,
|
||||
main_heating_control=2106,
|
||||
main_heating_category=6,
|
||||
sap_main_heating_code=301,
|
||||
)
|
||||
space = (1000.0, 800.0, 600.0, 400.0, 200.0, 0.0,
|
||||
0.0, 0.0, 0.0, 300.0, 700.0, 1000.0)
|
||||
hw = (200.0,) * 12
|
||||
efficiency = 1.0 / 1.45 # heat network models efficiency as 1/DLF
|
||||
|
||||
# Act
|
||||
result = _heat_network_distribution_electricity(main, space, hw, efficiency)
|
||||
|
||||
# Assert — energy = 0.01 × Σ((space + hw) / eff) = 0.01 × 7400 × 1.45;
|
||||
# factors = code-50 monthly weighted by the (space + hw) heat profile.
|
||||
assert result is not None
|
||||
energy_kwh, co2_factor, pe_factor = result
|
||||
distribution_monthly = tuple(
|
||||
0.01 * (s + w) / efficiency for s, w in zip(space, hw)
|
||||
)
|
||||
co2_monthly = co2_monthly_factors_kg_per_kwh(50)
|
||||
pe_monthly = pe_monthly_factors_kwh_per_kwh(50)
|
||||
assert co2_monthly is not None
|
||||
assert pe_monthly is not None
|
||||
expected_energy = 0.01 * (5000.0 + 2400.0) / efficiency
|
||||
expected_co2_factor = sum(
|
||||
d * f for d, f in zip(distribution_monthly, co2_monthly)
|
||||
) / expected_energy
|
||||
expected_pe_factor = sum(
|
||||
d * f for d, f in zip(distribution_monthly, pe_monthly)
|
||||
) / expected_energy
|
||||
assert abs(energy_kwh - expected_energy) <= 1e-9
|
||||
assert abs(energy_kwh - 0.01 * 7400.0 * 1.45) <= 1e-9
|
||||
assert abs(co2_factor - expected_co2_factor) <= 1e-9
|
||||
assert abs(pe_factor - expected_pe_factor) <= 1e-9
|
||||
|
||||
|
||||
def test_heat_network_distribution_electricity_none_for_individual_main() -> None:
|
||||
# Arrange — an individually-heated gas-boiler main (category 2, no
|
||||
# heat-network SAP code). §C3.2 pumping electricity applies only to
|
||||
# heat networks, so no distribution line should be emitted.
|
||||
main = MainHeatingDetail(
|
||||
has_fghrs=False,
|
||||
main_fuel_type=26,
|
||||
heat_emitter_type=1,
|
||||
emitter_temperature=1,
|
||||
main_heating_control=2106,
|
||||
main_heating_category=2,
|
||||
)
|
||||
space = (1000.0,) * 12
|
||||
hw = (200.0,) * 12
|
||||
|
||||
# Act
|
||||
result = _heat_network_distribution_electricity(main, space, hw, 0.85)
|
||||
|
||||
# Assert
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_heat_network_main_with_hw_from_main_dlf_scales_hot_water_kwh() -> None:
|
||||
# Arrange — when main heating is a heat network AND water heating
|
||||
# inherits from main (water_heating_code=901), the HW also incurs
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue