mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice S0380.77: WHC 914 routes primary-loss gate to DHW main; cert 000565 +1175 kWh
SAP 10.2 §4 line 7700 + Table 3 (PDF p.159) defines primary loss
as the loss between "a heat generator (e.g. boiler) connected to a
hot water storage vessel via insulated or uninsulated pipes (the
primary pipework)". The spec keys eligibility off the heat
generator that feeds the cylinder, NOT the space-heating main.
Verbatim spec (Table 3, p.159):
Primary circuit loss applies when hot water is heated by a heat
generator (e.g. boiler) connected to a hot water storage vessel
via insulated or uninsulated pipes (the primary pipework).
Primary loss is set to zero for the following:
- Electric immersion heater
- Combi boiler (including when it is part of a combined heat
pump and boiler package and provides all the hot water)
- CPSU (including electric CPSU)
- Boiler and thermal store within a single casing
- Separate boiler and thermal store connected by no more than
1.5 m of insulated pipework
- Direct-acting electric boiler
- Heat pump (not combined heat pump and boiler package with a
non-combi boiler) from PCDB with hot water vessel integral
to package
For other cases (indirect cylinders and thermal stores connected
by uninsulated pipework or more than 1.5 m of insulated
pipework) the loss in kWh/month is calculated as follows.
Primary loss = n_m × 14 × [{0.0091 × p + 0.0245 × (1 − p)} × h
+ 0.0263]
`_water_heating_section` was routing the gate through
`_first_main_heating` (Main 1). Cert 000565 lodges ASHP Main 1 +
gas combi Main 2 + WHC 914 ("from second main system") + 160 L
combined cylinder + cylinder thermostat "N". The cylinder is
heated by Main 2 via uninsulated primary pipework, so the spec
formula applies; the cascade was zeroing (59)m because Main 1's
HP record (or its absence) drove the gate.
Fix: `_primary_loss_override` resolves its `main` via
`_water_heating_main(epc)` (the WHC-914 routing) so the
eligibility check follows the heat generator that physically
incurs the loss. Call site loses the `main` argument; the gate's
internals are unchanged.
Cert 000565 worksheet line (59)m (U985-0001-000565 Block 1):
128.3772, 115.9536, 128.3772, 124.2360, 128.3772, 41.9160,
43.3132, 43.3132, 41.9160, 128.3772, 124.2360, 128.3772
sums to 1174.79 kWh/yr. Cascade now matches every month at < 1e-4
kWh under the spec params (uninsulated p=0 from RdSAP §3 age band
A-J default, h_winter=11 / h_summer=3 from "no cylinder
thermostat" row).
Cert 000565 movements at HEAD:
- sap_score: 29 ✓ EXACT (unchanged)
- sap_score_continuous: 29.2905 → 29.1090 (Δ +0.78 → +0.60)
- space_heating_kwh: 59274.46 → 58725.77 (Δ +266 → −283)
- main_heating_fuel: 34867.33 → 34544.57 (Δ +157 → −166)
- hot_water_kwh: 3668.54 → 5154.02 (Δ −86 → +1399)
- co2_kg: 6352.61 → 6616.01 (Δ −95 → +168)
- total_fuel_cost_gbp: 4611.14 → 4627.10 (Δ −69 → −53)
- ecf: 5.3073 → 5.3256 (Δ −0.08 → −0.06)
HW pin over-shoots by +1399 as expected — the +1174.79 spec credit
is amplified by the gas combi's 0.88 water efficiency (≈ +1335
kWh) plus cascade coupling (heat-gain feedback into space heating
drops it −549 kWh). The +1399 will be pulled back by the next two
demand-cascade slices:
- (45)m energy_content over by ~903 kWh (occupancy + shower mix)
- (56)/(57)m storage-loss over by ~98 kWh + missing solar
`(57)m = (56)m × (H13 − H12)/H13` adjustment (~298 kWh)
New test pins the full 12-month (59)m tuple for cert 000565's
heating shape at abs=1e-4. Old `_first_main_heating`-based test
(`test_cert_with_hot_water_cylinder_computes_primary_loss_59m_…`)
unchanged — still passes via the WHC-901 fall-through to Main 1.
Test baseline: 547 → 548 pass (new test added) + 9 expected
`test_sap_result_pin[000565-*]` cascade-gap fails. Pyright
net-zero on both touched files (45 baseline = 45 after).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
69ba7a65bf
commit
9dc60c91d3
2 changed files with 93 additions and 3 deletions
|
|
@ -3216,8 +3216,11 @@ def _water_heating_worksheet_and_gains(
|
|||
storage_loss_override = _cylinder_storage_loss_override(epc, main)
|
||||
# SAP 10.2 §4 line 7700 + Table 3 (PDF p.159) — primary circuit loss
|
||||
# (59)m. Only fires for indirect cylinders; HPs with integral
|
||||
# vessels and combi boilers are in the spec's zero list.
|
||||
primary_loss_override = _primary_loss_override(epc, main, primary_age)
|
||||
# vessels and combi boilers are in the spec's zero list. The gate
|
||||
# keys off the *DHW* main (`_water_heating_main`) so WHC 914 ("from
|
||||
# second main system") routes the primary-loss eligibility check
|
||||
# to the heat generator that actually feeds the cylinder.
|
||||
primary_loss_override = _primary_loss_override(epc, primary_age)
|
||||
# SAP 10.2 Appendix H — solar HW contribution (63c)m. Only fires
|
||||
# when the cert lodges solar HW; orchestrator drives off lodged
|
||||
# collector geometry + RdSAP 10 §10.11 Table 29 defaults for
|
||||
|
|
@ -3260,7 +3263,6 @@ def _water_heating_worksheet_and_gains(
|
|||
|
||||
def _primary_loss_override(
|
||||
epc: EpcPropertyData,
|
||||
main: Optional[MainHeatingDetail],
|
||||
primary_age: Optional[str],
|
||||
) -> Optional[tuple[float, ...]]:
|
||||
"""Resolve (59)m for `water_heating_from_cert` from the cert + PCDB
|
||||
|
|
@ -3270,7 +3272,18 @@ def _primary_loss_override(
|
|||
comes from RdSAP §3 age-band default (no API field); circulation
|
||||
hours h come from Table 3 keyed on cylinder thermostat + separately-
|
||||
timed-DHW lodgement.
|
||||
|
||||
The gate keys off the DHW main resolved via `_water_heating_main`
|
||||
(the WHC-914 "from second main system" routing) rather than
|
||||
`_first_main_heating`. SAP 10.2 §4 line 7700 + Table 3 (PDF p.159)
|
||||
define primary loss as the loss between the *heat generator that
|
||||
heats the water* and the storage vessel — so the eligibility check
|
||||
must follow the DHW routing. Cert 000565 (ASHP Main 1 + gas combi
|
||||
Main 2 + WHC 914 + 160 L cylinder) is the cohort case: Main 1's
|
||||
HP record is irrelevant; Main 2's combi feeds the cylinder via
|
||||
primary pipework and incurs the loss.
|
||||
"""
|
||||
main = _water_heating_main(epc)
|
||||
cylinder_present = bool(epc.has_hot_water_cylinder)
|
||||
hp_record: Optional[HeatPumpRecord] = None
|
||||
if main is not None and main.main_heating_index_number is not None:
|
||||
|
|
|
|||
|
|
@ -1802,6 +1802,83 @@ def test_cert_with_hot_water_cylinder_computes_primary_loss_59m_from_sap_table_3
|
|||
)
|
||||
|
||||
|
||||
def test_whc_914_dhw_routes_primary_loss_gate_to_second_main_heating_per_sap_table_3() -> None:
|
||||
"""SAP 10.2 §4 line 7700 + Table 3 (PDF p.159) primary-loss eligibility
|
||||
is determined by the heat generator that feeds the hot water storage
|
||||
vessel, not by the space-heating main. Cert 000565 lodges Main 1 = ASHP
|
||||
(SAP 224) + Main 2 = gas combi (PCDB 15100) + water_heating_code 914
|
||||
("from second main system") + 160 L combined cylinder + cylinder
|
||||
thermostat absent ("N"). The cylinder is heated by Main 2 via uninsulated
|
||||
primary pipework, so the Table 3 formula applies with p=0 and the
|
||||
"no cylinder thermostat" hours (h=11 winter, h=3 summer).
|
||||
|
||||
Worksheet line (59)m for U985-0001-000565 (Block 1) reads
|
||||
128.3772, 115.9536, 128.3772, 124.2360, 128.3772, 41.9160,
|
||||
43.3132, 43.3132, 41.9160, 128.3772, 124.2360, 128.3772 kWh
|
||||
summing to 1174.79 kWh/yr. The cascade must route the primary-loss gate
|
||||
to `_water_heating_main` (the WHC-914-resolved DHW main) — gating on
|
||||
`_first_main_heating` mis-keys the gate to Main 1's HP record (or
|
||||
absent record) and zeroes (59)m even though the gas combi → external
|
||||
cylinder pipework physically incurs the loss.
|
||||
"""
|
||||
# Arrange — synthesise cert 000565's heating shape: ASHP Main 1 +
|
||||
# gas-combi Main 2 servicing DHW via WHC 914 + cylinder lodged with
|
||||
# no cylinder thermostat. The Elmhurst mapper produces
|
||||
# `main_heating_category=None` on Main 1 when the cert lodges a SAP
|
||||
# code without a PCDB Table 362 reference (see
|
||||
# `_elmhurst_main_heating_category` TODO docstring) — mirror that
|
||||
# shape here so the current `_first_main_heating` routing fails the
|
||||
# gate even though Main 2 (the DHW main) plainly carries the loss.
|
||||
hp_main = MainHeatingDetail(
|
||||
has_fghrs=False,
|
||||
main_fuel_type=29, # electricity
|
||||
heat_emitter_type=1,
|
||||
emitter_temperature=1,
|
||||
main_heating_control=2206,
|
||||
main_heating_category=None,
|
||||
sap_main_heating_code=224,
|
||||
)
|
||||
combi_main = _gas_boiler_detail(sap_main_heating_code=102)
|
||||
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=[make_building_part(construction_age_band="D")],
|
||||
sap_heating=make_sap_heating(
|
||||
main_heating_details=[hp_main, combi_main],
|
||||
water_heating_code=914, # DHW from second main system
|
||||
cylinder_size=3, # Medium = 160 L
|
||||
cylinder_insulation_type=1,
|
||||
cylinder_insulation_thickness_mm=50,
|
||||
cylinder_thermostat="N", # no cylinder thermostat → h_winter=11
|
||||
),
|
||||
)
|
||||
|
||||
# Act
|
||||
wh_result, _ = _water_heating_worksheet_and_gains(
|
||||
epc=epc,
|
||||
water_efficiency_pct=0.88,
|
||||
is_instantaneous=False,
|
||||
primary_age="D",
|
||||
pcdb_record=None,
|
||||
)
|
||||
|
||||
# Assert — full 12-tuple matches cert 000565 worksheet (59)m at 1e-4.
|
||||
assert wh_result is not None
|
||||
expected_59m = (
|
||||
128.3772, 115.9536, 128.3772, 124.2360, 128.3772, 41.9160,
|
||||
43.3132, 43.3132, 41.9160, 128.3772, 124.2360, 128.3772,
|
||||
)
|
||||
got_59m = wh_result.primary_loss_monthly_kwh
|
||||
for month_idx, (got, want) in enumerate(zip(got_59m, expected_59m)):
|
||||
assert abs(got - want) < 1e-4, (
|
||||
f"(59)m month {month_idx + 1}: got {got!r}, want {want!r} per "
|
||||
f"SAP 10.2 §4 line 7700 + Table 3 (uninsulated p=0, no "
|
||||
f"cylinder thermostat h=11/3); cert 000565 worksheet line (59)"
|
||||
)
|
||||
|
||||
|
||||
def test_air_source_heat_pump_main_heating_zeroes_table_3a_combi_loss_per_sap_4_line_7702() -> None:
|
||||
"""SAP 10.2 §4 line 7702 worksheet defines (61)m as 'Combi loss for
|
||||
each month from Table 3a, 3b or 3c (enter "0" if not a combi
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue