mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice S0380.172: Heat-network heat-source-eff CO2/PE factor scaling
Closes the CO2 / PE residuals for CH1 (boiler community heating, SAP code 301) and CH3 (HP community heating, SAP code 304) via SAP 10.2 Table 4a (PDF p.164) heat-network heat-source efficiency: "Boilers (RdSAP)" → 80% → code 301 "Heat pump (RdSAP)" → 300% → code 304 Spec block 13a (PDF p.153) (467) "PE associated with heat source 2" = [(307b)+(310b)] × 100 / (467b) — i.e. fuel input = network_input × 100 / heat_source_eff before applying Table 12 PE factor. Block 12b (367) mirrors for CO2. The cascade meters network_input directly (eff = 1/DLF for the cost path via Table 12 heat-network rate), so PE / CO2 factors are scaled by 1/heat_source_eff at lookup time — mathematically equivalent to spec's (network_input / eff) × factor. Three changes: 1. New `_HEAT_NETWORK_HEAT_SOURCE_EFFICIENCY: Final[dict[int, float]]` keyed on SAP code: 301 → 0.80, 304 → 3.00. SAP 302 (CHP+boilers) is omitted — the 35%/65% split + displaced-electricity credit per spec block 13b (464)/(466)/(364)/(366) needs the .171 follow-up. 2. New `_heat_network_heat_source_efficiency_scaling(main)` helper returning 1.0 for non-heat-network mains + SAP 302, and 1/heat_source_eff for SAP 301 / 304. 3. Wired into `_main_heating_co2_factor_kg_per_kwh` and `_main_heating_primary_factor` non-electric branches (heat networks are non-electric per `_is_electric_main`). Both functions return `Table_12_factor × scaling` so the cascade's `network_input × scaled_factor` lands on the spec `(network_input / eff) × Table_12_factor`. Closures vs pre-S0380.172 residuals (heating-systems corpus block 11b): variant ΔCO2 ΔPE notes CH1 (Boilers/Gas) -787→-126 -3827→-967 ~75-84% closure CH2 (CHP/Gas) unchanged unchanged excluded — SAP 302 CH3 (HP/Elec) +1614→+473 +11879→+1749 ~71-85% closure CH4 (CHP/Oil) unchanged unchanged excluded — SAP 302 CH6 (CHP/Coal) unchanged unchanged excluded — SAP 302 Cost + SAP unchanged on all 5 (heat-network rate × network_input via Table 12 is correct regardless of heat-source efficiency). Residual CH1 / CH3 gap drivers (follow-up scope): - WHC=901 HW path: cascade reads cert-lodged "Mains gas" as HW fuel on community-heating certs; should fall through to main fuel for the heat-network so the scaling applies on HW side too. - Elmhurst 0.8523 multiplier on heat-network energy column (worksheet (467) energy = spec_formula × 0.8523 uniformly across non-CHP heat-network rows; mechanism not yet identified — spec divergence candidate for SAP_CALCULATOR.md §8). Cohort no-regression verified: 9 ASHP + 38 cohort-2 golden fixtures pass unchanged; the 41-variant heating-systems corpus has identical residuals for non-heat-network certs. The 2 closed CH variants are re-pinned at their new sub-1000 magnitudes. Test baseline at HEAD: 926 pass + 1 skipped (was 926 + 1 at predecessor a4b5f4e7; pin updates net to 0). Pyright net-zero on affected files (cert_to_inputs.py, test_heating_systems_corpus.py): 32 → 32. Per [[feedback-spec-citation-in-commits]] the dispatch table cites SAP 10.2 Table 4a (PDF p.164) verbatim row labels. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
a4b5f4e74d
commit
36d4bf8750
2 changed files with 96 additions and 4 deletions
|
|
@ -546,9 +546,25 @@ _EXPECTATIONS: tuple[_CorpusExpectation, ...] = (
|
|||
# (worksheet (464)/(466)/(364)/(366) per SAP 10.2 §13b spec) +
|
||||
# (2) community-HP COP cascade for CH3 + (3) heat-network overall
|
||||
# factor (486)/(386) calc — separate follow-up slices).
|
||||
_CorpusExpectation(variant='community heating 1', block='11b', expected_sap_resid=+0.5915, expected_cost_resid_gbp=-13.6289, expected_co2_resid_kg=-787.2531, expected_pe_resid_kwh=-3827.1887),
|
||||
#
|
||||
# Slice S0380.172 closed the CH1 (boiler) + CH3 (HP) CO2 / PE
|
||||
# residuals via SAP 10.2 Table 4a (PDF p.164) heat-network heat-
|
||||
# source efficiency scaling: code 301 (boilers) eff = 80%, code
|
||||
# 304 (HP) eff = 300%. Spec block 13a (467) = (307+310) × 100 /
|
||||
# heat_source_eff × Table 12 PE factor; cascade meters network_
|
||||
# input directly so PE/CO2 factors are scaled by 1/heat_source_eff
|
||||
# at lookup time. CH1 ΔCO2 −787 → −126 (~84% closed) and ΔPE
|
||||
# −3827 → −967 (~75% closed); CH3 ΔCO2 +1614 → +473 (~71%
|
||||
# closed) and ΔPE +11879 → +1749 (~85% closed). Code 302 (CHP+
|
||||
# boilers) is omitted from the scaling table — the 35%/65% split
|
||||
# requires the displaced-electricity credit line per spec block
|
||||
# 13b (464)/(466); follow-up slice scope. Residual CH1/CH3 gap is
|
||||
# the WHC=901 HW path (cascade reads cert-lodged "Mains gas" as
|
||||
# HW fuel; should fall through to main fuel for community heating)
|
||||
# + the Elmhurst 0.8523 multiplier on heat-network energy column.
|
||||
_CorpusExpectation(variant='community heating 1', block='11b', expected_sap_resid=+0.5915, expected_cost_resid_gbp=-13.6289, expected_co2_resid_kg=-126.4571, expected_pe_resid_kwh=-967.3648),
|
||||
_CorpusExpectation(variant='community heating 2', block='11b', expected_sap_resid=-0.0076, expected_cost_resid_gbp=+0.1744, expected_co2_resid_kg=-1430.3212, expected_pe_resid_kwh=+1506.0355),
|
||||
_CorpusExpectation(variant='community heating 3', block='11b', expected_sap_resid=+0.5915, expected_cost_resid_gbp=-13.6289, expected_co2_resid_kg=+1613.7837, expected_pe_resid_kwh=+11878.7588),
|
||||
_CorpusExpectation(variant='community heating 3', block='11b', expected_sap_resid=+0.5915, expected_cost_resid_gbp=-13.6289, expected_co2_resid_kg=+472.5996, expected_pe_resid_kwh=+1748.7395),
|
||||
_CorpusExpectation(variant='community heating 4', block='11b', expected_sap_resid=-0.0076, expected_cost_resid_gbp=+0.1744, expected_co2_resid_kg=-4397.0794, expected_pe_resid_kwh=+494.6090),
|
||||
_CorpusExpectation(variant='community heating 6', block='11b', expected_sap_resid=-8.0295, expected_cost_resid_gbp=+185.0120, expected_co2_resid_kg=-2934.9021, expected_pe_resid_kwh=+7864.5950),
|
||||
)
|
||||
|
|
@ -881,3 +897,15 @@ def test_community_heating_mapper_populates_chp_split_fields(
|
|||
main_1 = main_heating_details[0]
|
||||
assert main_1.community_heating_chp_fraction == expected_chp_fraction
|
||||
assert main_1.community_heating_boiler_fuel_type == expected_boiler_fuel_code
|
||||
|
||||
|
||||
# S0380.172 — Heat-network heat-source-eff scaling residual coverage.
|
||||
#
|
||||
# Per SAP 10.2 Table 4a (PDF p.164): "Boilers (RdSAP)" eff=80%, "Heat
|
||||
# pump (RdSAP)" eff=300%. The cascade's CO2/PE factor functions scale
|
||||
# Table 12 factors by 1/heat_source_eff so that network_input × scaled
|
||||
# factor lands on the spec block 13a (467) / 12b (367) "(307+310) ×
|
||||
# 100 / eff × Table 12 factor" formula. SAP code 302 (CHP+boilers) is
|
||||
# excluded — 35%/65% split + displaced-electricity credit is follow-up.
|
||||
# Coverage is asserted via the residual-pin test above (CH1 / CH3
|
||||
# closure; CH2 / CH4 / CH6 unchanged).
|
||||
|
|
|
|||
|
|
@ -825,6 +825,31 @@ _HEAT_NETWORK_MAIN_CODES: Final[frozenset[int]] = frozenset({301, 302, 303, 304}
|
|||
_HEAT_NETWORK_CATEGORY: Final[int] = 6
|
||||
|
||||
|
||||
# SAP 10.2 Table 4a (PDF p.164) heat-network heat-source efficiency by
|
||||
# SAP code. Verbatim:
|
||||
# 301 "Boilers (RdSAP)" → 80%
|
||||
# 302 "CHP and boilers (RdSAP)" → 75% (overall — per RdSAP 10 §C)
|
||||
# 304 "Heat pump (RdSAP)" → 300% (= COP 3.0)
|
||||
# Used by the block 13a/12b PE/CO2 cascade to convert delivered network
|
||||
# input (post-DLF) into FUEL input by dividing by the heat-source
|
||||
# efficiency: spec (467) = (307+310) × 100 / (467a). The cascade meters
|
||||
# heat-network input directly (eff = 1/DLF for cost via Table 12
|
||||
# heat-network rate), so PE/CO2 factors are scaled by 1/heat_source_eff
|
||||
# at lookup time to land at the spec's fuel-input × Table-12-factor.
|
||||
#
|
||||
# Code 302 (CHP+boilers) is omitted here because the 35%/65% heat-
|
||||
# fraction split applies different efficiencies to the two heat sources
|
||||
# (CHP 75% overall + boilers 80%) and a single composite efficiency
|
||||
# can't model the displaced-electricity credit line per spec block
|
||||
# 13b (464)/(466). The cascade for code 302 keeps the current
|
||||
# 1/DLF override (giving large CO2/PE residuals on CH2/CH4/CH6 —
|
||||
# follow-up slice scope).
|
||||
_HEAT_NETWORK_HEAT_SOURCE_EFFICIENCY: Final[dict[int, float]] = {
|
||||
301: 0.80,
|
||||
304: 3.00,
|
||||
}
|
||||
|
||||
|
||||
def _is_heat_network_main(main: Optional[MainHeatingDetail]) -> bool:
|
||||
"""True when the cert's main heating is a heat network — either by
|
||||
SAP code (Table 4a 301-304) or by `main_heating_category` (6)."""
|
||||
|
|
@ -836,6 +861,28 @@ def _is_heat_network_main(main: Optional[MainHeatingDetail]) -> bool:
|
|||
return main.main_heating_category == _HEAT_NETWORK_CATEGORY
|
||||
|
||||
|
||||
def _heat_network_heat_source_efficiency_scaling(
|
||||
main: Optional[MainHeatingDetail],
|
||||
) -> float:
|
||||
"""Return the multiplicative scaling factor to apply to Table 12
|
||||
CO2 / PE factors when the main is a heat-network boiler (SAP 301) or
|
||||
heat pump (SAP 304). Cascade computes CO2/PE = network_input ×
|
||||
Table_12_factor; spec block 13a/12b computes (network_input /
|
||||
heat_source_eff) × Table_12_factor. Equivalent transform: scale the
|
||||
factor by 1/heat_source_eff. Returns 1.0 for code 302 (CHP+boilers
|
||||
— separate split-formula path) and non-heat-network mains.
|
||||
"""
|
||||
if not _is_heat_network_main(main):
|
||||
return 1.0
|
||||
code = main.sap_main_heating_code if main is not None else None
|
||||
if not isinstance(code, int):
|
||||
return 1.0
|
||||
eff = _HEAT_NETWORK_HEAT_SOURCE_EFFICIENCY.get(code)
|
||||
if eff is None:
|
||||
return 1.0
|
||||
return 1.0 / eff
|
||||
|
||||
|
||||
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.
|
||||
|
|
@ -2413,7 +2460,16 @@ def _main_heating_co2_factor_kg_per_kwh(
|
|||
annual factor is the safe degenerate value)
|
||||
"""
|
||||
if not _is_electric_main(main):
|
||||
return _co2_factor_kg_per_kwh(main)
|
||||
# Heat-network mains (SAP codes 301 / 304) are non-electric per
|
||||
# `_is_electric_main` but require a heat-source-efficiency scaling
|
||||
# per spec block 12b (363)/(367) = network_input × 100 /
|
||||
# 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)
|
||||
)
|
||||
if tariff is Tariff.STANDARD:
|
||||
monthly = _effective_monthly_co2_factor(
|
||||
main_fuel_monthly_kwh, _STANDARD_ELECTRICITY_FUEL_CODE,
|
||||
|
|
@ -2470,7 +2526,15 @@ def _main_heating_primary_factor(
|
|||
unknown dual-rate codes, zero-fuel)."""
|
||||
fuel = _main_fuel_code(main)
|
||||
if not _is_electric_main(main):
|
||||
return primary_energy_factor(fuel)
|
||||
# PE-side mirror of `_main_heating_co2_factor_kg_per_kwh`
|
||||
# heat-network heat-source-eff scaling. Spec block 13a (463)/
|
||||
# (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)
|
||||
)
|
||||
if tariff is Tariff.STANDARD:
|
||||
monthly = _effective_monthly_pe_factor(
|
||||
main_fuel_monthly_kwh, _STANDARD_ELECTRICITY_FUEL_CODE,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue