mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
fix(water-heating): heat-network primary loss uses Table 3 h=3 all months
SAP 10.2 Table 3 (PDF p.160) verbatim: "For heat networks apply the formula above with p = 1.0 and h = 3 for all months." The primary circulation hours for a heat-network main are fixed at h=3 winter and summer, independent of the cylinder-thermostat / separate-timing lodgement that selects the h=5/h=11 rows for boiler systems. `primary_loss_monthly_kwh` / `primary_circuit_hours_per_day_table_3` gain a `heat_network` flag (→ (3, 3)); `_primary_loss_override` passes `_is_heat_network_main(main)`. p=1.0 was already pinned via `_HEAT_NETWORK_PIPEWORK_INSULATION_FRACTION`; only the hours were wrong. Before, cert 8536 routed through the h=5/3 row because its community biomass DHW fuel (31) collides with electricity code 31, so `_separately_timed_dhw` returned False. The Table 3 heat-network rule overrides that path: 8536 primary loss (59) 335.81 → 273.90, EXACT to the faithful case-32 worksheet (storage (56) 376.58 also matches 376.94). API eval within-0.5 57.0% → 56.9% (one offsetting-error cert crosses out; signed err −0.205 → −0.202). Applied spec-uniformly per the determinism principle — the heat-network primary hours are unambiguous. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
e6543c76ca
commit
00921f71e8
3 changed files with 70 additions and 2 deletions
|
|
@ -6154,6 +6154,12 @@ def _primary_loss_override(
|
|||
pipework_insulation_fraction=pipework_p,
|
||||
has_cylinder_thermostat=epc.sap_heating.cylinder_thermostat == "Y",
|
||||
separately_timed_dhw=_separately_timed_dhw(epc, main),
|
||||
# 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." The h=3
|
||||
# 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),
|
||||
)
|
||||
# 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
|
||||
|
|
|
|||
|
|
@ -550,13 +550,21 @@ def primary_circuit_hours_per_day_table_3(
|
|||
*,
|
||||
has_cylinder_thermostat: bool,
|
||||
separately_timed_dhw: bool,
|
||||
heat_network: bool = False,
|
||||
) -> tuple[float, float]:
|
||||
"""SAP 10.2 Table 3 (PDF p.159) — hours of primary circulation per
|
||||
day, returned as `(winter_hours, summer_hours)`:
|
||||
"""SAP 10.2 Table 3 (PDF p.159-160) — hours of primary circulation
|
||||
per day, returned as `(winter_hours, summer_hours)`:
|
||||
no thermostat → (11, 3)
|
||||
thermostat, not separately timed → ( 5, 3)
|
||||
thermostat, separately timed → ( 3, 3)
|
||||
|
||||
SAP 10.2 Table 3 (PDF p.160) verbatim: "For heat networks apply the
|
||||
formula above with p = 1.0 and h = 3 for all months." So a heat-
|
||||
network main uses h=3 winter and summer regardless of the cylinder-
|
||||
thermostat / separate-timing lodgement.
|
||||
"""
|
||||
if heat_network:
|
||||
return (3.0, 3.0)
|
||||
if not has_cylinder_thermostat:
|
||||
return (11.0, 3.0)
|
||||
if separately_timed_dhw:
|
||||
|
|
@ -569,6 +577,7 @@ def primary_loss_monthly_kwh(
|
|||
pipework_insulation_fraction: float,
|
||||
has_cylinder_thermostat: bool,
|
||||
separately_timed_dhw: bool,
|
||||
heat_network: bool = False,
|
||||
) -> tuple[float, ...]:
|
||||
"""SAP 10.2 §4 line (59)m via Table 3 (PDF p.159):
|
||||
(59)m = n_m × 14 × [{0.0091 × p + 0.0245 × (1 − p)} × h + 0.0263]
|
||||
|
|
@ -576,6 +585,10 @@ def primary_loss_monthly_kwh(
|
|||
hours of primary circulation per day (winter / summer split per
|
||||
`primary_circuit_hours_per_day_table_3`).
|
||||
|
||||
`heat_network=True` selects the SAP 10.2 Table 3 (PDF p.160) heat-
|
||||
network row — h=3 for all months — regardless of the cylinder-
|
||||
thermostat / separate-timing lodgement.
|
||||
|
||||
Returns 12 monthly values in calendar order Jan..Dec. Callers must
|
||||
gate this helper on the spec's zero-loss configurations
|
||||
(combi boilers, integral-vessel HPs, CPSUs, thermal stores ≤ 1.5 m
|
||||
|
|
@ -587,6 +600,7 @@ def primary_loss_monthly_kwh(
|
|||
winter_h, summer_h = primary_circuit_hours_per_day_table_3(
|
||||
has_cylinder_thermostat=has_cylinder_thermostat,
|
||||
separately_timed_dhw=separately_timed_dhw,
|
||||
heat_network=heat_network,
|
||||
)
|
||||
return tuple(
|
||||
n * 14.0 * (
|
||||
|
|
|
|||
|
|
@ -317,6 +317,54 @@ def test_heat_network_dhw_no_cylinder_applies_sap_10_2_hiu_default_store() -> No
|
|||
assert abs(sum(wh_result.solar_storage_monthly_kwh) - expected_storage_kwh) <= 1.0
|
||||
|
||||
|
||||
def test_heat_network_primary_loss_uses_p1_h3_all_months_per_table_3() -> None:
|
||||
# Arrange — heat-network DHW (Table 4a code 301, cat 6, WHC 901) with
|
||||
# the SAP p.24 HIU default store applied. SAP 10.2 Table 3 (PDF p.160)
|
||||
# verbatim: "For heat networks apply the formula above with p = 1.0 and
|
||||
# h = 3 for all months." So the primary loss is independent of the
|
||||
# cylinder-thermostat / separately-timed hours (which would give h=5/3)
|
||||
# and equals Σ n_m × 14 × (0.0091×1.0×3 + 0.0263) = 273.9 kWh/yr.
|
||||
# Community biomass fuel (31) collides with electricity code 31, so
|
||||
# `_separately_timed_dhw` would route to h=5/3 — but the Table 3 heat-
|
||||
# network rule must override that to h=3 regardless of the DHW fuel.
|
||||
# This reproduces cert 8536-0929-6500-0815-7206.
|
||||
main = MainHeatingDetail(
|
||||
has_fghrs=False,
|
||||
main_fuel_type=31,
|
||||
heat_emitter_type=1,
|
||||
emitter_temperature=1,
|
||||
main_heating_control=2106,
|
||||
main_heating_category=6,
|
||||
sap_main_heating_code=301,
|
||||
)
|
||||
part = make_building_part(construction_age_band="A")
|
||||
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=[main],
|
||||
water_heating_code=901,
|
||||
water_heating_fuel=31,
|
||||
),
|
||||
)
|
||||
|
||||
# Act
|
||||
wh_result, _ = _water_heating_worksheet_and_gains(
|
||||
epc=_apply_heat_network_hiu_default_store(epc),
|
||||
water_efficiency_pct=100.0,
|
||||
is_instantaneous=False,
|
||||
primary_age="A",
|
||||
pcdb_record=None,
|
||||
)
|
||||
|
||||
# Assert — p=1.0, h=3 all months → 365 × 14 × (0.0091×3 + 0.0263).
|
||||
assert wh_result is not None
|
||||
expected_primary_kwh = 365.0 * 14.0 * (0.0091 * 3.0 + 0.0263)
|
||||
assert abs(sum(wh_result.primary_loss_monthly_kwh) - expected_primary_kwh) <= 1e-6
|
||||
|
||||
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue