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,
|
pipework_insulation_fraction=pipework_p,
|
||||||
has_cylinder_thermostat=epc.sap_heating.cylinder_thermostat == "Y",
|
has_cylinder_thermostat=epc.sap_heating.cylinder_thermostat == "Y",
|
||||||
separately_timed_dhw=_separately_timed_dhw(epc, main),
|
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
|
# 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
|
# 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,
|
has_cylinder_thermostat: bool,
|
||||||
separately_timed_dhw: bool,
|
separately_timed_dhw: bool,
|
||||||
|
heat_network: bool = False,
|
||||||
) -> tuple[float, float]:
|
) -> tuple[float, float]:
|
||||||
"""SAP 10.2 Table 3 (PDF p.159) — hours of primary circulation per
|
"""SAP 10.2 Table 3 (PDF p.159-160) — hours of primary circulation
|
||||||
day, returned as `(winter_hours, summer_hours)`:
|
per day, returned as `(winter_hours, summer_hours)`:
|
||||||
no thermostat → (11, 3)
|
no thermostat → (11, 3)
|
||||||
thermostat, not separately timed → ( 5, 3)
|
thermostat, not separately timed → ( 5, 3)
|
||||||
thermostat, separately timed → ( 3, 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:
|
if not has_cylinder_thermostat:
|
||||||
return (11.0, 3.0)
|
return (11.0, 3.0)
|
||||||
if separately_timed_dhw:
|
if separately_timed_dhw:
|
||||||
|
|
@ -569,6 +577,7 @@ def primary_loss_monthly_kwh(
|
||||||
pipework_insulation_fraction: float,
|
pipework_insulation_fraction: float,
|
||||||
has_cylinder_thermostat: bool,
|
has_cylinder_thermostat: bool,
|
||||||
separately_timed_dhw: bool,
|
separately_timed_dhw: bool,
|
||||||
|
heat_network: bool = False,
|
||||||
) -> tuple[float, ...]:
|
) -> tuple[float, ...]:
|
||||||
"""SAP 10.2 §4 line (59)m via Table 3 (PDF p.159):
|
"""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]
|
(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
|
hours of primary circulation per day (winter / summer split per
|
||||||
`primary_circuit_hours_per_day_table_3`).
|
`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
|
Returns 12 monthly values in calendar order Jan..Dec. Callers must
|
||||||
gate this helper on the spec's zero-loss configurations
|
gate this helper on the spec's zero-loss configurations
|
||||||
(combi boilers, integral-vessel HPs, CPSUs, thermal stores ≤ 1.5 m
|
(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(
|
winter_h, summer_h = primary_circuit_hours_per_day_table_3(
|
||||||
has_cylinder_thermostat=has_cylinder_thermostat,
|
has_cylinder_thermostat=has_cylinder_thermostat,
|
||||||
separately_timed_dhw=separately_timed_dhw,
|
separately_timed_dhw=separately_timed_dhw,
|
||||||
|
heat_network=heat_network,
|
||||||
)
|
)
|
||||||
return tuple(
|
return tuple(
|
||||||
n * 14.0 * (
|
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
|
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:
|
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,
|
# Arrange — heat-network main (Table 4a code 301 = community heating,
|
||||||
# category 6). SAP 10.2 Appendix C §C3.2 (PDF p.51): distribution
|
# category 6). SAP 10.2 Appendix C §C3.2 (PDF p.51): distribution
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue