mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
S0380.184: community electric-HP network CO2/PE uses monthly Table 12d/12e — closes CH3
SAP 10.2 worksheet block 12b/13b (367)/(467) for a community heating
electric heat pump (Table 4a code 304 → Table 12 fuel 41 "heat from
electric heat pump"). The HP meters grid electricity, so per Table 12
note (s)/(t) + block 12b/13b footnote (a) its emission/PE factor is the
MONTHLY Table 12d/12e cascade (fuel 41 = standard-electricity profile),
weighted by the network heat profile, then × 1/heat-source-eff (1/COP):
(367)/(467) = [(307)+(310)] / COP × Σ((307+310)_m × factor_m)/Σ(...)
Per-line walk of CH3 (the displayed (367) 0.1535 / (467) 1.5717 are PDF
artifacts; the (373)/(473) totals reconcile only with):
CO2 factor = 0.15040 (monthly Table 12d wtd) vs cascade annual 0.136
PE factor = 1.55692 (monthly Table 12e wtd) vs cascade annual 1.501
Pre-slice the cascade routed code 304 through the non-electric branch
(`_co2_factor_kg_per_kwh(main) × 1/COP` = annual × scaling). New
`_is_heat_network_electric_main` (heat-network main whose fuel has a
Table 12d monthly set — i.e. fuel 41) routes all four factor helpers
(main + HW, CO2 + PE) through the monthly cascade × 1/COP. Non-electric
heat networks (gas 51 / oil 53 / coal 54) have no monthly set → annual
path unchanged (CH1, CH6 untouched).
Closure (CH3 was already SAP+cost EXACT):
CH3 (HP/Elec) CO2 −75.32→+0.0000 (= [(307+310)/3]×(0.1504−0.136)),
PE −249.32→−0.0000 (× (1.5569−1.501)) — FULLY EXACT
Corpus now 40/41 EXACT on all four metrics. Only CH6 remains: its
worksheet lodges a manual DLF=1.0 ("two adjoining dwellings") absent
from the Summary PDF (byte-identical to CH4 bar fuel type) — an
architectural limit, not a cascade gap. 2226 pass + 1 skip + 0 fail
(tolerances 1e-4 all metrics); pyright net-zero 43→43.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
803da062a2
commit
82f7315f8d
3 changed files with 108 additions and 17 deletions
|
|
@ -728,9 +728,21 @@ _EXPECTATIONS: tuple[_CorpusExpectation, ...] = (
|
|||
# on all four metrics. CH6 SAP −7.49→−8.02 / cost +£172.68→+£184.84
|
||||
# (its HW now also bills the blend, compounding the DLF=1.0 quirk —
|
||||
# same root, still the separate CH6 DLF front).
|
||||
#
|
||||
# Slice S0380.184 closed CH3 (HP/Elec, code 304) CO2 + PE: an
|
||||
# electric-HP heat network meters grid electricity, so per SAP 10.2
|
||||
# Table 12 note (s)/(t) + block 12b/13b footnote (a) its (367)/(467)
|
||||
# factor is the MONTHLY Table 12d/12e (fuel code 41) weighted by the
|
||||
# network heat profile, then × 1/COP — not the annual 0.136/1.501.
|
||||
# New `_is_heat_network_electric_main` routes the four factor helpers
|
||||
# through the monthly cascade for code 304 (fuel 41). CH3 was
|
||||
# SAP/cost EXACT; CO2 −75.32→+0.0000 (= (307+310)/3 × (0.1504−0.136))
|
||||
# and PE −249.32→−0.0000 (× (1.5569−1.501)) now EXACT. Non-electric
|
||||
# heat networks (CH1 gas 51, CH6 coal 54) have no monthly factor set
|
||||
# → unchanged.
|
||||
_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.0000, expected_cost_resid_gbp=-0.0000, expected_co2_resid_kg=+0.0000, expected_pe_resid_kwh=+0.0000),
|
||||
_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 3', 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 4', 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 6', block='11b', expected_sap_resid=-8.0219, expected_cost_resid_gbp=+184.8376, expected_co2_resid_kg=+2411.5399, expected_pe_resid_kwh=+5023.4766),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -898,6 +898,21 @@ def _heat_network_heat_source_efficiency_scaling(
|
|||
return 1.0 / eff
|
||||
|
||||
|
||||
def _is_heat_network_electric_main(main: Optional[MainHeatingDetail]) -> bool:
|
||||
"""True when the main is a heat network whose generator runs on grid
|
||||
electricity (Table 4a code 304 → Table 12 fuel code 41 "heat from
|
||||
electric heat pump"). Such networks meter electricity, so SAP 10.2
|
||||
Table 12 note (s)/(t) + worksheet block 12b/13b footnote (a) require
|
||||
the MONTHLY Table 12d/12e factors (not the annual average), weighted
|
||||
by the network heat profile, before the 1/heat-source-eff (1/COP)
|
||||
scaling. Non-electric heat networks (gas/oil/coal boilers, codes
|
||||
51/53/54) have no monthly factor set and keep the annual Table 12
|
||||
value."""
|
||||
if not _is_heat_network_main(main):
|
||||
return False
|
||||
return co2_monthly_factors_kg_per_kwh(_main_fuel_code(main)) is not None
|
||||
|
||||
|
||||
def _heat_network_dlf(age_band: Optional[str]) -> float:
|
||||
"""RdSAP 10 §10.11 + SAP 10.2 Table 12c distribution loss factor by
|
||||
age band. Defaults to the K-or-newer value (1.50) when band missing.
|
||||
|
|
@ -2716,10 +2731,18 @@ def _main_heating_co2_factor_kg_per_kwh(
|
|||
# heat_source_eff × Table 12 CO2 factor. The cascade meters
|
||||
# network_input directly so scale the factor by 1/eff to land at
|
||||
# the spec's fuel-input × factor.
|
||||
return (
|
||||
_co2_factor_kg_per_kwh(main)
|
||||
* _heat_network_heat_source_efficiency_scaling(main)
|
||||
)
|
||||
scaling = _heat_network_heat_source_efficiency_scaling(main)
|
||||
hn_fuel = _main_fuel_code(main)
|
||||
if _is_heat_network_electric_main(main) and hn_fuel is not None:
|
||||
# Electric-HP heat network (code 304 / fuel 41): the HP runs
|
||||
# on grid electricity → MONTHLY Table 12d factors weighted by
|
||||
# the network heat profile, then × 1/COP (S0380.184).
|
||||
monthly = _effective_monthly_co2_factor(
|
||||
main_fuel_monthly_kwh, hn_fuel,
|
||||
)
|
||||
if monthly is not None:
|
||||
return monthly * scaling
|
||||
return _co2_factor_kg_per_kwh(main) * scaling
|
||||
if tariff is Tariff.STANDARD:
|
||||
monthly = _effective_monthly_co2_factor(
|
||||
main_fuel_monthly_kwh, _STANDARD_ELECTRICITY_FUEL_CODE,
|
||||
|
|
@ -2788,10 +2811,17 @@ def _main_heating_primary_factor(
|
|||
# (467) = network_input × 100 / heat_source_eff × Table 12 PE
|
||||
# factor; cascade meters network_input directly so scale by
|
||||
# 1/eff at lookup time.
|
||||
return (
|
||||
primary_energy_factor(fuel)
|
||||
* _heat_network_heat_source_efficiency_scaling(main)
|
||||
)
|
||||
scaling = _heat_network_heat_source_efficiency_scaling(main)
|
||||
if _is_heat_network_electric_main(main) and fuel is not None:
|
||||
# Electric-HP heat network (code 304 / fuel 41): MONTHLY
|
||||
# Table 12e factors weighted by the network heat profile,
|
||||
# then × 1/COP (S0380.184).
|
||||
monthly = _effective_monthly_pe_factor(
|
||||
main_fuel_monthly_kwh, fuel,
|
||||
)
|
||||
if monthly is not None:
|
||||
return monthly * scaling
|
||||
return primary_energy_factor(fuel) * scaling
|
||||
if tariff is Tariff.STANDARD:
|
||||
monthly = _effective_monthly_pe_factor(
|
||||
main_fuel_monthly_kwh, _STANDARD_ELECTRICITY_FUEL_CODE,
|
||||
|
|
@ -3056,10 +3086,18 @@ def _hot_water_co2_factor_kg_per_kwh(
|
|||
# gas" is an Elmhurst placeholder that mis-routes the lookup.
|
||||
if _is_community_heating_hw_from_main(epc):
|
||||
main = _water_heating_main(epc)
|
||||
return (
|
||||
_co2_factor_kg_per_kwh(main)
|
||||
* _heat_network_heat_source_efficiency_scaling(main)
|
||||
)
|
||||
scaling = _heat_network_heat_source_efficiency_scaling(main)
|
||||
hn_fuel = _main_fuel_code(main)
|
||||
if _is_heat_network_electric_main(main) and hn_fuel is not None:
|
||||
# Electric-HP heat network HW (code 304 / fuel 41): MONTHLY
|
||||
# Table 12d factors weighted by the HW profile, × 1/COP
|
||||
# (S0380.184) — mirror of the SH branch.
|
||||
monthly = _effective_monthly_co2_factor(
|
||||
hw_monthly_kwh, hn_fuel,
|
||||
)
|
||||
if monthly is not None:
|
||||
return monthly * scaling
|
||||
return _co2_factor_kg_per_kwh(main) * scaling
|
||||
fuel = _water_heating_fuel_code(epc)
|
||||
if fuel is None:
|
||||
return _DEFAULT_CO2_KG_PER_KWH
|
||||
|
|
@ -3112,10 +3150,18 @@ def _hot_water_primary_factor(
|
|||
# scaled by 1/heat_source_eff per spec block 13a (463)/(467).
|
||||
if _is_community_heating_hw_from_main(epc):
|
||||
main = _water_heating_main(epc)
|
||||
return (
|
||||
primary_energy_factor(_main_fuel_code(main))
|
||||
* _heat_network_heat_source_efficiency_scaling(main)
|
||||
)
|
||||
scaling = _heat_network_heat_source_efficiency_scaling(main)
|
||||
hn_fuel = _main_fuel_code(main)
|
||||
if _is_heat_network_electric_main(main) and hn_fuel is not None:
|
||||
# Electric-HP heat network HW (code 304 / fuel 41): MONTHLY
|
||||
# Table 12e factors weighted by the HW profile, × 1/COP
|
||||
# (S0380.184) — mirror of the SH branch.
|
||||
monthly = _effective_monthly_pe_factor(
|
||||
hw_monthly_kwh, hn_fuel,
|
||||
)
|
||||
if monthly is not None:
|
||||
return monthly * scaling
|
||||
return primary_energy_factor(_main_fuel_code(main)) * scaling
|
||||
fuel = _water_heating_fuel_code(epc)
|
||||
if fuel is None:
|
||||
return _DEFAULT_PEF
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ from domain.sap10_calculator.rdsap.cert_to_inputs import (
|
|||
_heat_network_distribution_electricity, # pyright: ignore[reportPrivateUsage]
|
||||
_heat_network_dlf, # pyright: ignore[reportPrivateUsage]
|
||||
_is_electric_main, # pyright: ignore[reportPrivateUsage]
|
||||
_is_heat_network_electric_main, # pyright: ignore[reportPrivateUsage]
|
||||
_is_electric_water, # pyright: ignore[reportPrivateUsage]
|
||||
_is_off_peak_meter, # pyright: ignore[reportPrivateUsage]
|
||||
_main_floor_u_value, # pyright: ignore[reportPrivateUsage]
|
||||
|
|
@ -244,6 +245,38 @@ def test_heat_network_code_302_chp_effective_factor_per_sap_10_2_block_12b_13b()
|
|||
assert abs(pe - 1.29455) <= 1e-9
|
||||
|
||||
|
||||
def test_is_heat_network_electric_main_true_only_for_electric_hp_network() -> None:
|
||||
# Arrange — code 304 community heat pump (Table 12 fuel 41 = "heat
|
||||
# from electric heat pump", which HAS monthly Table 12d/12e factors)
|
||||
# vs code 301 community gas boilers (fuel 51, annual-only). SAP 10.2
|
||||
# Table 12 note (s)/(t): grid-electricity factors vary monthly, so
|
||||
# the HP network must use Table 12d/12e; the gas-boiler network keeps
|
||||
# the annual factor.
|
||||
hp_main = MainHeatingDetail(
|
||||
has_fghrs=False,
|
||||
main_fuel_type=41, # Table 12 fuel 41 = heat from electric HP
|
||||
heat_emitter_type=1,
|
||||
emitter_temperature=1,
|
||||
main_heating_control=2306,
|
||||
main_heating_category=6,
|
||||
sap_main_heating_code=304,
|
||||
)
|
||||
gas_boiler_main = MainHeatingDetail(
|
||||
has_fghrs=False,
|
||||
main_fuel_type=51, # Table 12 fuel 51 = heat from gas boilers
|
||||
heat_emitter_type=1,
|
||||
emitter_temperature=1,
|
||||
main_heating_control=2306,
|
||||
main_heating_category=6,
|
||||
sap_main_heating_code=301,
|
||||
)
|
||||
|
||||
# Act / Assert
|
||||
assert _is_heat_network_electric_main(hp_main) is True
|
||||
assert _is_heat_network_electric_main(gas_boiler_main) is False
|
||||
assert _is_heat_network_electric_main(None) is False
|
||||
|
||||
|
||||
def test_heat_network_code_302_effective_factor_none_for_non_302_main() -> None:
|
||||
# Arrange — a code-301 heat-network boiler main (no CHP split). The
|
||||
# §12b/13b CHP+boilers blend applies only to code 302; code 301
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue