mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-27 23:35:01 +00:00
Merge pull request #1597 from Hestia-Homes/fix/1592-water-only-heat-network-dhw
Fix the water-only heat-network DHW plant-efficiency double count (#1592 B)
This commit is contained in:
commit
70d65291c3
4 changed files with 346 additions and 23 deletions
|
|
@ -142,7 +142,36 @@ _MIN_WITHIN_HALF: float = 0.23
|
|||
# RdSAP corpus). 16 Bingley Close reverts +3.2 -> +4.3, so observed MAE 1.405 ->
|
||||
# 1.4103; re-tighten when the water-only branch is fixed correctly under #1592
|
||||
# (the cylinder-insulation MAPPER fix is retained, so the revert is partial).
|
||||
_MAX_SAP_MAE: float = 1.411
|
||||
# DELIBERATELY LOOSENED 1.411 -> 1.415 by the #1592 sub-task B water-only DHW
|
||||
# fix — the SECOND sanctioned exception to "ratchets never loosen", and the
|
||||
# note above expected to RE-TIGHTEN here, so this needs @KhalimCK's explicit
|
||||
# call before merge.
|
||||
#
|
||||
# The fix is spec-verified against the SAP 10.2 PDF (not ticket prose): §4.3
|
||||
# p.24 "The efficiency for water heating is incorporated in the price of heat
|
||||
# from a heat network in Table 12"; C4 p.53 scopes the plant efficiency to
|
||||
# "the calculation of CO2 emissions and Primary Energy"; C6 p.54 + worksheet
|
||||
# (310) = (64) × (305a) × (306) carry no efficiency term. The dwelling-side
|
||||
# `plant_eff / DLF` divisor was applying it a SECOND time.
|
||||
#
|
||||
# Exactly ONE cohort property is WHC 950 — 507644414148 (16 Bingley Close),
|
||||
# +5.946 -> +6.818 = 0.872/205 = +0.0043 MAE, the whole movement. within-0.5
|
||||
# UNCHANGED at 23.9% (49/205); mean signed +1.050 -> +1.054. Observed 1.41458.
|
||||
#
|
||||
# Why this is not a regression signal: Bingley's `pre_sap` E52 is Khalim-
|
||||
# verified correct (2026-07-14), but its ~+6 residual is a KNOWN extraction bug
|
||||
# still being hunted (see HANDOVER_PASHUB_EXTRACTOR_ACCURACY.md: "community-
|
||||
# heating residual — ... community standing charge/tariff detail, or DLF
|
||||
# 'Unknown' distribution type"). This hybrid gauge conflates that extraction
|
||||
# error with the SAP-10.2-vs-lodged-2012-RdSAP engine offset, so per #1592
|
||||
# Problem 2 it CANNOT adjudicate a calculator fix — the same reasoning the
|
||||
# orientation exception above rests on. The gov-API RdSAP corpus, which gauges
|
||||
# the calculator alone, IMPROVES on this change (within-0.5 78.7% -> 78.8%,
|
||||
# MAE 0.6273 -> 0.6272; exactly 3 certs move, all WHC 950).
|
||||
#
|
||||
# Re-tighten under #1592 sub-task C, once an accredited Elmhurst 10.2 worksheet
|
||||
# pins WHC-950 — Bingley is the natural capture candidate.
|
||||
_MAX_SAP_MAE: float = 1.415
|
||||
|
||||
_KNOWN_GAP_REASON = (
|
||||
"pashub `from_site_notes` mapper does not int-code a main-heating "
|
||||
|
|
|
|||
|
|
@ -1187,6 +1187,13 @@ _HEAT_NETWORK_DHW_CHARGING_FACTOR_BY_CODE: Final[dict[int, float]] = {
|
|||
2306: 1.00, 2308: 1.00, 2309: 1.00, 2310: 1.00, 2312: 1.00, 2314: 1.00,
|
||||
}
|
||||
|
||||
# SAP 10.2 Table 4c(3) (PDF p.169) worksheet (305a), "DHW-only system,
|
||||
# flat rate charging" row — the RdSAP 10 (spec p.59) default for a
|
||||
# water-ONLY heat network (WHC 950/951/952), whose own charging method is
|
||||
# never lodged: the dwelling's heating-control code describes the SPACE
|
||||
# system's charging, not this network's.
|
||||
_TABLE_4C3_FLAT_RATE_CHARGING_FACTOR: Final[float] = 1.05
|
||||
|
||||
|
||||
def _heat_network_space_charging_factor(main: Optional[MainHeatingDetail]) -> float:
|
||||
"""SAP 10.2 Table 4c(3) (PDF p.169) worksheet (305) — heat-network
|
||||
|
|
@ -6585,6 +6592,18 @@ def _primary_loss_applies(
|
|||
# kWh/yr — zero before this branch.
|
||||
if water_heating_code in _WATER_HEATING_BOILER_CIRCULATOR_CODES:
|
||||
return True
|
||||
# SAP 10.2 §4.3 "Heat networks" (PDF p.24), scoped "Where hot water is
|
||||
# provided by a heat network": "Primary circuit loss for insulated
|
||||
# pipework and cylinderstat should be included (see Table 3)." A water-
|
||||
# ONLY community scheme (WHC 950/951/952) feeds the dwelling-side
|
||||
# cylinder via primary pipework from the network connection whatever
|
||||
# the space main is, so the loss applies exactly as for heat-network
|
||||
# mains with WHC 901/902/914 (below). Checked off `water_heating_code`
|
||||
# before the main-keyed branches so a non-boiler space main (warm air,
|
||||
# HP, direct-acting electric) can't zero it out — cert 9093 is the
|
||||
# cohort case (whc 950 + warm-air main 502).
|
||||
if water_heating_code in _WATER_HEAT_NETWORK_ONLY_CODES:
|
||||
return True
|
||||
# SAP 10.2 Table 3 (PDF p.160) zero-loss list names "Direct-acting
|
||||
# electric boiler" verbatim. RdSAP 10 §12 (p.62) classifies SAP code
|
||||
# 191 as the direct-acting electric boiler: its cylinder is immersion-
|
||||
|
|
@ -7089,13 +7108,21 @@ def _primary_loss_override(
|
|||
water_heating_code=epc.sap_heating.water_heating_code,
|
||||
):
|
||||
return None
|
||||
# Heat-network DHW: either the space main is a network (WHC 901/902/914
|
||||
# inherit its DHW) or the DHW is its own water-only community scheme
|
||||
# (WHC 950/951/952). SAP 10.2 §4.3 (PDF p.24) and Table 3's heat-network
|
||||
# row both scope on the DHW source — "Where hot water is provided by a
|
||||
# heat network" — not on the space main.
|
||||
heat_network_dhw = is_heat_network_main(main) or (
|
||||
epc.sap_heating.water_heating_code in _WATER_HEAT_NETWORK_ONLY_CODES
|
||||
)
|
||||
# SAP 10.2 §4 "Heat networks" (PDF p.17 line 1482) pins community-
|
||||
# heating primary pipework to "insulated" (p=1.0), overriding the
|
||||
# RdSAP §3 age-band default which would otherwise return 0 for
|
||||
# pre-2007 stock. See `_HEAT_NETWORK_PIPEWORK_INSULATION_FRACTION`.
|
||||
pipework_p = (
|
||||
_HEAT_NETWORK_PIPEWORK_INSULATION_FRACTION
|
||||
if is_heat_network_main(main)
|
||||
if heat_network_dhw
|
||||
else _pipework_insulation_fraction_table_3(primary_age)
|
||||
)
|
||||
base = primary_loss_monthly_kwh(
|
||||
|
|
@ -7107,7 +7134,7 @@ def _primary_loss_override(
|
|||
# row applies regardless of the thermostat / separate-timing
|
||||
# lodgement (and so is robust to the community-fuel-as-electric
|
||||
# collision that would otherwise route DHW to the h=5 row).
|
||||
heat_network=is_heat_network_main(main),
|
||||
heat_network=heat_network_dhw,
|
||||
)
|
||||
# SAP 10.2 §12.4.4 (PDF p.36-37): for back-boiler combos summer DHW
|
||||
# comes from an electric immersion, not from the boiler — the boiler
|
||||
|
|
@ -8001,13 +8028,28 @@ def cert_to_inputs(
|
|||
* _heat_network_dhw_charging_factor(main)
|
||||
)
|
||||
elif epc.sap_heating.water_heating_code in _WATER_HEAT_NETWORK_ONLY_CODES:
|
||||
# HW-only heat network (whc 950/951/952): the Table 4a plant
|
||||
# efficiency is already in `water_eff`; apply the Table 12c
|
||||
# distribution loss on top per RdSAP 10 §10 (spec p.36 "water
|
||||
# heating only ... distribution loss"). q_generated = q_useful ×
|
||||
# DLF, so delivered-per-fuel efficiency = plant_eff / DLF. Fires
|
||||
# on the WHC alone — the HW network is independent of the main.
|
||||
water_eff = water_eff / _heat_network_dlf(primary_age)
|
||||
# HW-only heat network (whc 950/951/952). SAP 10.2 §4.3 (p.24):
|
||||
# "The efficiency for water heating is incorporated in the price of
|
||||
# heat from a heat network in Table 12" — the dwelling buys
|
||||
# DELIVERED heat (fuel 51), so the Table 4a plant efficiency must
|
||||
# NOT divide here as well. C4 (p.53) scopes it: "For heat networks
|
||||
# not listed in the PCDB, for the calculation of CO2 emissions and
|
||||
# Primary Energy, the efficiency to be used is that of the heat
|
||||
# generator" — CO2/PE only, never delivered energy.
|
||||
#
|
||||
# C6 "Heat network providing DHW only" (p.54): "Allow for the
|
||||
# control factor from Table 4c(3), for the distribution loss factor
|
||||
# as in C3.1" — i.e. worksheet (310) = (64) × (305a) × (306), with
|
||||
# no efficiency term. Same shape as the WHC-901/902/914 branch
|
||||
# above, differing only in that (305a) takes the Table 4c(3)
|
||||
# DHW-only flat-rate default rather than the space system's lodged
|
||||
# charging code. Fires on the WHC alone — §4.3 keys on the DHW
|
||||
# source ("Where hot water is provided by a heat network"), not on
|
||||
# the space main.
|
||||
water_eff = 1.0 / (
|
||||
_heat_network_dlf(primary_age)
|
||||
* _TABLE_4C3_FLAT_RATE_CHARGING_FACTOR
|
||||
)
|
||||
is_instantaneous = epc.sap_heating.water_heating_code in _INSTANTANEOUS_WATER_CODES
|
||||
# §9a Table 11 secondary fraction — pulled forward of §4 so the
|
||||
# post-§8 Equation D1 cascade can derive Q_space = (98c)m × (204)
|
||||
|
|
|
|||
|
|
@ -1399,17 +1399,22 @@ def test_heat_network_main_with_hw_from_main_dlf_scales_hot_water_kwh() -> None:
|
|||
assert abs(hn_hw_fuel / wh_result.output_kwh_per_yr - 1.41) <= 1e-6
|
||||
|
||||
|
||||
def test_hot_water_only_heat_network_whc_950_applies_table12c_dlf() -> None:
|
||||
def test_hot_water_only_heat_network_whc_950_costs_more_fuel_than_boiler_dhw() -> None:
|
||||
# Arrange — water_heating_code 950 = "hot-water-only heat network
|
||||
# (boilers)" (SAP 10.2 Table 4a HW section, plant eff 0.80). RdSAP 10
|
||||
# §10 (spec p.36) requires the Table 12c distribution loss applied on
|
||||
# top of the plant efficiency for water-heating-only heat networks, so
|
||||
# the delivered HW fuel = q_useful × DLF / 0.80. The DLF must fire on
|
||||
# the WHC ALONE — independent of the main, which here is an ordinary
|
||||
# gas boiler (NOT a heat network), mirroring cert 9093 (whc 950 + a
|
||||
# warm-air main). Compare against a non-heat-network baseline at the
|
||||
# same 0.80 water efficiency (whc 901 from the same gas main): the 950
|
||||
# HW fuel must exceed it by exactly the DLF (age E → 1.41).
|
||||
# (boilers)". The Table 12c distribution loss must fire on the WHC
|
||||
# ALONE — independent of the main, which here is an ordinary gas boiler
|
||||
# (NOT a heat network), mirroring cert 9093 (whc 950 + a warm-air main).
|
||||
# Compared against the SAME dwelling taking DHW from that gas boiler
|
||||
# (whc 901, Table 4a eff 0.80), the water-only network bills:
|
||||
#
|
||||
# 950: q_useful × (306) DLF 1.41 × (305a) 1.05 = q × 1.4805
|
||||
# 901: q_useful ÷ 0.80 = q × 1.25
|
||||
#
|
||||
# → the network's delivered fuel is 1.4805/1.25 ≈ 1.18× the boiler's.
|
||||
# The network pays a distribution loss and a flat-rate charging penalty
|
||||
# but NO plant-efficiency divisor (SAP 10.2 §4.3 p.24: that efficiency
|
||||
# is in the Table 12 heat price) — see the absolute-efficiency pin in
|
||||
# `test_water_only_heat_network_dhw_prices_heat_without_plant_efficiency`.
|
||||
part = make_building_part(construction_age_band="E")
|
||||
gas_main = MainHeatingDetail(
|
||||
has_fghrs=False,
|
||||
|
|
@ -1445,8 +1450,236 @@ def test_hot_water_only_heat_network_whc_950_applies_table12c_dlf() -> None:
|
|||
hw_network: float = cert_to_inputs(hw_network_epc).hot_water_kwh_per_yr
|
||||
baseline: float = cert_to_inputs(baseline_epc).hot_water_kwh_per_yr
|
||||
|
||||
# Assert — the HW-only-heat-network fuel is scaled up by the age-E DLF.
|
||||
assert abs(hw_network / baseline - 1.41) <= 0.02
|
||||
# Assert — DLF × flat-rate charging, against the boiler's 1/0.80.
|
||||
assert abs(hw_network / baseline - (1.41 * 1.05) * 0.80) <= 0.02
|
||||
|
||||
|
||||
def test_water_only_heat_network_dhw_prices_heat_without_plant_efficiency() -> None:
|
||||
# Arrange — SAP 10.2 §4.3 (PDF p.24): "The efficiency for water heating
|
||||
# is incorporated in the price of heat from a heat network in Table 12."
|
||||
# The dwelling buys DELIVERED heat at the Table 12 fuel-51 rate, so the
|
||||
# RdSAP Table 4a plant efficiency (950 → 0.80) must NOT also divide on
|
||||
# the dwelling side — worksheet (310) is "(64) × (305a) × (306)", with
|
||||
# no efficiency term. C4 (p.53) scopes plant efficiency explicitly: "For
|
||||
# heat networks not listed in the PCDB, FOR THE CALCULATION OF CO2
|
||||
# EMISSIONS AND PRIMARY ENERGY, the efficiency to be used is that of the
|
||||
# heat generator" — i.e. never for delivered energy.
|
||||
#
|
||||
# (305a) is the Table 4c(3) "DHW-only system, flat rate charging" row =
|
||||
# 1.05 (p.169); RdSAP 10 (spec p.59) defaults a non-PCDB water-only
|
||||
# network to flat-rate charging. (306) is the age-E Table 12c DLF = 1.41.
|
||||
#
|
||||
# So HW fuel = q_useful × 1.41 × 1.05, i.e. water_eff = 1/(DLF × 1.05).
|
||||
# Asserted as fuel ÷ q_useful (the §4 output at 100% efficiency) to pin
|
||||
# the efficiency absolutely, mirroring the WHC-901 heat-network test
|
||||
# above. The main is an ordinary gas boiler — NOT a heat network — since
|
||||
# §4.3 keys on the DHW source ("Where hot water is provided by a heat
|
||||
# network"), not on the space main.
|
||||
part = make_building_part(construction_age_band="E")
|
||||
gas_main = MainHeatingDetail(
|
||||
has_fghrs=False,
|
||||
main_fuel_type=26,
|
||||
heat_emitter_type=1,
|
||||
emitter_temperature=1,
|
||||
main_heating_control=2106,
|
||||
main_heating_category=2,
|
||||
)
|
||||
hw_network_epc = make_minimal_sap10_epc(
|
||||
total_floor_area_m2=_TYPICAL_TFA_M2,
|
||||
habitable_rooms_count=4,
|
||||
country_code="ENG",
|
||||
sap_building_parts=[part],
|
||||
sap_heating=make_sap_heating(
|
||||
main_heating_details=[gas_main],
|
||||
water_heating_code=950, # hot-water-only heat network (boilers)
|
||||
),
|
||||
)
|
||||
|
||||
# Act
|
||||
hw_fuel: float = cert_to_inputs(hw_network_epc).hot_water_kwh_per_yr
|
||||
wh_result, _ = _water_heating_worksheet_and_gains(
|
||||
epc=hw_network_epc,
|
||||
water_efficiency_pct=100.0,
|
||||
is_instantaneous=False,
|
||||
primary_age="E",
|
||||
pcdb_record=None,
|
||||
)
|
||||
|
||||
# Assert — delivered HW fuel = q_useful × (305a) × (306); no 1/0.80.
|
||||
assert wh_result is not None
|
||||
assert abs(hw_fuel / wh_result.output_kwh_per_yr - 1.41 * 1.05) <= 1e-6
|
||||
|
||||
|
||||
def test_water_only_heat_network_dhw_fuel_independent_of_plant_type() -> None:
|
||||
# Arrange — the three water-only heat-network codes differ ONLY in the
|
||||
# plant behind the network: 950 boilers (Table 4a eff 0.80), 951 CHP
|
||||
# (0.75), 952 community heat pump (3.00). Because SAP 10.2 §4.3 (p.24)
|
||||
# puts that efficiency in the Table 12 price of delivered heat, the
|
||||
# DWELLING's delivered-heat demand cannot depend on it: worksheet (310)
|
||||
# is "(64) × (305a) × (306)" whatever the plant is. C6 (p.54) likewise
|
||||
# gives one rule for a DHW-only network, and the Table 4c(3) (p.169)
|
||||
# "DHW-only system" rows make no 950/951/952 distinction.
|
||||
#
|
||||
# So all three must bill identical HW fuel. The plant efficiency still
|
||||
# matters — but at (342)/(373)/(473), the network's CO2/PE factors (C4,
|
||||
# p.53) — never as a dwelling-side divisor.
|
||||
#
|
||||
# This is the sharpest statement of the double-count fix: under the old
|
||||
# `water_eff = plant_eff / DLF` the three diverged by their plant
|
||||
# efficiencies, and 952 under-billed its DHW fuel by ~3× (0.47 × q_useful
|
||||
# against the spec's 1.4805 × q_useful).
|
||||
part = make_building_part(construction_age_band="E")
|
||||
gas_main = MainHeatingDetail(
|
||||
has_fghrs=False,
|
||||
main_fuel_type=26,
|
||||
heat_emitter_type=1,
|
||||
emitter_temperature=1,
|
||||
main_heating_control=2106,
|
||||
main_heating_category=2,
|
||||
)
|
||||
|
||||
def hw_fuel_for(water_heating_code: int) -> float:
|
||||
epc = make_minimal_sap10_epc(
|
||||
total_floor_area_m2=_TYPICAL_TFA_M2,
|
||||
habitable_rooms_count=4,
|
||||
country_code="ENG",
|
||||
sap_building_parts=[part],
|
||||
sap_heating=make_sap_heating(
|
||||
main_heating_details=[gas_main],
|
||||
water_heating_code=water_heating_code,
|
||||
),
|
||||
)
|
||||
return cert_to_inputs(epc).hot_water_kwh_per_yr
|
||||
|
||||
# Act — boilers / CHP / community heat pump behind the same network.
|
||||
boilers = hw_fuel_for(950)
|
||||
chp = hw_fuel_for(951)
|
||||
heat_pump = hw_fuel_for(952)
|
||||
|
||||
# Assert — the plant behind the network never reaches the dwelling.
|
||||
assert abs(chp - boilers) <= 1e-6
|
||||
assert abs(heat_pump - boilers) <= 1e-6
|
||||
|
||||
|
||||
def test_water_only_heat_network_cylinder_accrues_table_3_primary_loss() -> None:
|
||||
# Arrange — SAP 10.2 §4.3 (PDF p.24) scopes itself "Where hot water is
|
||||
# provided by a heat network", then requires: "Primary circuit loss for
|
||||
# insulated pipework and cylinderstat should be included (see Table 3)."
|
||||
# A water-ONLY network (WHC 950) feeds the dwelling-side cylinder via
|
||||
# primary pipework from the network connection, so the loss applies on
|
||||
# the DHW source alone — whatever the SPACE main is.
|
||||
#
|
||||
# The space main here is warm air (Table 4a code 502, category 9),
|
||||
# mirroring cert 9093 (whc 950 + warm-air main). That main is not a
|
||||
# boiler, so no boiler-category branch can accrue the loss on its
|
||||
# behalf: without a WHC-keyed gate the cylinder silently gets (59)m = 0.
|
||||
part = make_building_part(construction_age_band="E")
|
||||
warm_air_main = MainHeatingDetail(
|
||||
has_fghrs=False,
|
||||
main_fuel_type=26,
|
||||
heat_emitter_type=1,
|
||||
emitter_temperature=1,
|
||||
main_heating_control=2106,
|
||||
main_heating_category=9,
|
||||
sap_main_heating_code=502,
|
||||
)
|
||||
epc = make_minimal_sap10_epc(
|
||||
total_floor_area_m2=_TYPICAL_TFA_M2,
|
||||
habitable_rooms_count=4,
|
||||
country_code="ENG",
|
||||
has_hot_water_cylinder=True,
|
||||
sap_building_parts=[part],
|
||||
sap_heating=make_sap_heating(
|
||||
main_heating_details=[warm_air_main],
|
||||
water_heating_code=950, # hot-water-only heat network
|
||||
cylinder_size=2, # Normal / 110 L
|
||||
cylinder_thermostat="Y",
|
||||
),
|
||||
)
|
||||
|
||||
# Act
|
||||
wh_result, _ = _water_heating_worksheet_and_gains(
|
||||
epc=epc,
|
||||
water_efficiency_pct=100.0,
|
||||
is_instantaneous=False,
|
||||
primary_age="E",
|
||||
pcdb_record=None,
|
||||
)
|
||||
|
||||
# Assert — the primary circuit runs, so (59)m is billed every month.
|
||||
assert wh_result is not None
|
||||
assert all(m > 0.0 for m in wh_result.primary_loss_monthly_kwh)
|
||||
|
||||
|
||||
def test_water_only_heat_network_primary_loss_matches_heat_network_main() -> None:
|
||||
# Arrange — SAP 10.2 Table 3 (PDF p.160): "For heat networks apply the
|
||||
# formula above with p = 1.0 and h = 3 for all months", and §4.3 (p.24)
|
||||
# pins the pipework literal: "Primary circuit loss for INSULATED
|
||||
# pipework". Both scope on the DHW source, so the two ways a dwelling
|
||||
# can take DHW from a heat network must accrue the SAME (59)m:
|
||||
#
|
||||
# - WHC 901 "from main" where the space main IS the network, and
|
||||
# - WHC 950, a water-only network behind a warm-air space main.
|
||||
#
|
||||
# Without this the water-only cert falls back to the RdSAP §3 age-band
|
||||
# default (band E → p = 0.0, uninsulated), billing a heat network for
|
||||
# bare primary pipework the spec says is insulated.
|
||||
part = make_building_part(construction_age_band="E")
|
||||
warm_air_main = MainHeatingDetail(
|
||||
has_fghrs=False,
|
||||
main_fuel_type=26,
|
||||
heat_emitter_type=1,
|
||||
emitter_temperature=1,
|
||||
main_heating_control=2106,
|
||||
main_heating_category=9,
|
||||
sap_main_heating_code=502,
|
||||
)
|
||||
heat_network_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,
|
||||
)
|
||||
|
||||
def primary_loss_for(
|
||||
main: MainHeatingDetail, water_heating_code: int
|
||||
) -> tuple[float, ...]:
|
||||
epc = make_minimal_sap10_epc(
|
||||
total_floor_area_m2=_TYPICAL_TFA_M2,
|
||||
habitable_rooms_count=4,
|
||||
country_code="ENG",
|
||||
has_hot_water_cylinder=True,
|
||||
sap_building_parts=[part],
|
||||
sap_heating=make_sap_heating(
|
||||
main_heating_details=[main],
|
||||
water_heating_code=water_heating_code,
|
||||
cylinder_size=2, # Normal / 110 L
|
||||
cylinder_thermostat="Y",
|
||||
),
|
||||
)
|
||||
wh_result, _ = _water_heating_worksheet_and_gains(
|
||||
epc=epc,
|
||||
water_efficiency_pct=100.0,
|
||||
is_instantaneous=False,
|
||||
primary_age="E",
|
||||
pcdb_record=None,
|
||||
)
|
||||
assert wh_result is not None
|
||||
return wh_result.primary_loss_monthly_kwh
|
||||
|
||||
# Act — DHW from a water-only network vs DHW from a network space main.
|
||||
water_only = primary_loss_for(warm_air_main, 950)
|
||||
from_network_main = primary_loss_for(heat_network_main, 901)
|
||||
|
||||
# Assert — Table 3's heat-network row keys on the DHW source, not the
|
||||
# space main, so both bill the same insulated p=1.0 / h=3 loss.
|
||||
assert all(
|
||||
abs(water - from_main) <= 1e-6
|
||||
for water, from_main in zip(water_only, from_network_main)
|
||||
)
|
||||
|
||||
|
||||
def test_gas_boiler_main_efficiency_unchanged_by_dlf_override() -> None:
|
||||
|
|
|
|||
|
|
@ -241,7 +241,26 @@ _CORPUS = Path(
|
|||
# fallback (see the `_MAX_SAP_MAE` note below for the mechanism).
|
||||
# 77.8% -> 78.6% via the RdSAP 10 §5.7 Table 13 200/280mm solid-brick-wall
|
||||
# boundary fix (see the `_MAX_SAP_MAE` note below).
|
||||
_MIN_WITHIN_HALF_SAP = 0.786
|
||||
# 78.7% -> 78.8% (MAE 0.6273 -> 0.6272) via the water-only heat-network DHW
|
||||
# fix (#1592 sub-task B): SAP 10.2 §4.3 (PDF p.24) puts the water-heating
|
||||
# efficiency in the Table 12 price of delivered heat, and C4 (p.53) scopes the
|
||||
# plant efficiency to "the calculation of CO2 emissions and Primary Energy" —
|
||||
# so worksheet (310) = (64) × (305a) × (306) carries NO efficiency divisor.
|
||||
# The WHC-950/951/952 branch had been applying it a second time dwelling-side.
|
||||
# Also re-applies the Table 3 primary loss (p=1.0, h=3) for these certs.
|
||||
# Only 3 corpus certs can move (all lodge WHC 950), enumerated individually:
|
||||
# 5300047580 err 0.695 -> 0.249 (crosses inside 0.5 — this is the +0.1%)
|
||||
# 10010254955 err 1.535 -> 0.795 (improves, still outside)
|
||||
# 22032926 err 1.707 -> 2.737 (WORSENS — it over-rates, and the fix
|
||||
# raises SAP further; a separate baseline residual that pashub's lodged
|
||||
# `pre_sap` cannot adjudicate. Tracked as #1592 sub-task C, which pins an
|
||||
# accredited Elmhurst worksheet as the oracle.)
|
||||
# Net MAE −0.00016 over 1000 certs. PE 2.845 -> 2.867 and CO2 0.0701 -> 0.0702
|
||||
# both drift slightly the wrong way but stay well inside their loose ceilings;
|
||||
# the plant efficiency C4 reserves for CO2/PE is a known follow-up. Spec-
|
||||
# verified against the SAP 10.2 PDF, not the ticket prose; unit-pinned in
|
||||
# test_cert_to_inputs (950/951/952 must bill identical DHW fuel).
|
||||
_MIN_WITHIN_HALF_SAP = 0.788
|
||||
# 0.793 -> 0.789 via the §12 Unknown-meter + dual-electric-immersion off-peak
|
||||
# trigger (RdSAP 10 PDF p.62): Apartment 241 (main 691 + 903 dual immersion)
|
||||
# -5.38 -> -1.05. Worksheet-validated on "simulated case 48" (Elmhurst SAP 57,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue