mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice S0380.141: §9.4.11 boiler interlock — extend −5pp adjustment to both space-heating efficiency and the PCDB Equation D1 water cascade
SAP 10.2 §9.4.11 (PDF p.30) "Boiler interlock":
For the purposes of the SAP, an interlocked system is one in which
both the space and stored water heating are interlocked. If either
is not, the 5% seasonal efficiency reduction is applied to both
space and water heating; if both are interlocked no reductions are
made.
Table 4c (PDF p.169-170) lodges -5 for both Space and DHW columns on
the "No boiler interlock — regular boiler" row. Pre-slice the cascade
applied the -5pp adjustment ONLY to the `water_eff` scalar fallback
(`cert_to_inputs.py:4354`) and missed:
(a) the SH efficiency path (cascade kept the raw PCDB winter eff for
space heating);
(b) the PCDB Equation D1 monthly cascade (Eq D1 received raw
winter/summer values without the -5pp adjustment).
RdSAP §3 (PDF p.57) defines boiler interlock as "Assumed present if
there is a room thermostat and (for stored hot water systems heated
by the boiler) a cylinder thermostat. Otherwise not interlocked."
Cert pcdb 1 (Potterton KOA PCDB 716 + 110 L cylinder + Cylinder Stat:
No) reproduces the pattern: worksheet (210) = 60% = PCDB winter
65 - 5; worksheet (217)m monthly Eq D1 pivots on (winter 60,
summer 48) not (65, 53).
The SH path is further gated on `pcdb_main is not None` because
§9.4.11 only applies to "gas and liquid fuel boilers" — cert 000565
(ASHP Main 1) keeps its raw SH eff. The combi-fed-cylinder DHW path
(cert 000565 WHC 914 to PCDB combi Main 2) continues to receive its
existing -5pp via the `water_pcdb_main` gate (unchanged).
Corpus impact: pcdb 1 SAP residual +6.95 → +3.40; cost -£157.61 →
-£75.68; CO2 -845.81 → -397.02; PE -3135.30 → -1601.74. No other
variant has PCDB main + cylinder + no thermostat, so the other 24
corpus pins are unchanged.
Extended handover suite: 884 pass, 0 fail (was 883 + 1 new AAA test
pinning the §9.4.11 SH eff path).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
8ee877e44c
commit
6636f1c333
3 changed files with 141 additions and 18 deletions
|
|
@ -233,7 +233,7 @@ _EXPECTATIONS: tuple[_CorpusExpectation, ...] = (
|
|||
_CorpusExpectation(variant='oil pcdb 1', block='11a', expected_sap_resid=+0.4239, expected_cost_resid_gbp=-9.7668, expected_co2_resid_kg=-35.9551, expected_pe_resid_kwh=-83.8239),
|
||||
_CorpusExpectation(variant='oil pcdb 2', block='11a', expected_sap_resid=+0.4239, expected_cost_resid_gbp=-9.7668, expected_co2_resid_kg=-35.9551, expected_pe_resid_kwh=-83.8239),
|
||||
_CorpusExpectation(variant='oil pcdb 3', block='11a', expected_sap_resid=+1.1597, expected_cost_resid_gbp=-26.7204, expected_co2_resid_kg=-53.1709, expected_pe_resid_kwh=-271.4351),
|
||||
_CorpusExpectation(variant='pcdb 1', block='11a', expected_sap_resid=+6.9521, expected_cost_resid_gbp=-157.6055, expected_co2_resid_kg=-845.8065, expected_pe_resid_kwh=-3135.2991),
|
||||
_CorpusExpectation(variant='pcdb 1', block='11a', expected_sap_resid=+3.3965, expected_cost_resid_gbp=-75.6799, expected_co2_resid_kg=-397.0228, expected_pe_resid_kwh=-1601.7416),
|
||||
# Slice S0380.133 unblocked 10 solid-fuel variants by routing the
|
||||
# Elmhurst §14.0 "Main Heating EES Code" through the new
|
||||
# `_ELMHURST_MAIN_HEATING_EES_TO_FUEL_CODE` dict. Pre-slice the
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ Reference: RdSAP 10 specification (10-06-2025); SAP 10.2 specification
|
|||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, replace
|
||||
from decimal import ROUND_HALF_UP, Decimal
|
||||
from typing import Callable, Final, Literal, Optional
|
||||
|
||||
|
|
@ -4334,24 +4334,53 @@ def cert_to_inputs(
|
|||
main_category=water_main.main_heating_category if water_main is not None else None,
|
||||
main_fuel=_main_fuel_code(water_main),
|
||||
)
|
||||
# SAP 10.2 Table 4c row "No boiler interlock — regular boiler:
|
||||
# DHW −5%" (PDF p.169). RdSAP §3 (PDF p.57) defines boiler
|
||||
# interlock as "Assumed present if there is a room thermostat and
|
||||
# (for stored hot water systems heated by the boiler) a cylinder
|
||||
# thermostat. Otherwise not interlocked." A combi-fed cylinder
|
||||
# routes the boiler as a regular boiler for the DHW circuit (the
|
||||
# combi's instantaneous-DHW capability is bypassed), so the
|
||||
# regular-boiler row applies. Note c): the adjustment caps at
|
||||
# −5pp (no thermostatic control and no boiler interlock do not
|
||||
# accumulate). Cert 000565 (cylinder lodged + cyl-stat absent +
|
||||
# WHC 914 to PCDB combi Main 2) closes 79% → 74% — matches
|
||||
# worksheet (217)m exactly.
|
||||
if (
|
||||
# SAP 10.2 §9.4.11 (PDF p.30) "Boiler interlock": "For the purposes
|
||||
# of the SAP, an interlocked system is one in which both the space
|
||||
# and stored water heating are interlocked. If either is not, the
|
||||
# 5% seasonal efficiency reduction is applied to BOTH space and
|
||||
# water heating; if both are interlocked no reductions are made."
|
||||
# Table 4c (PDF p.169-170) row "No boiler interlock — regular
|
||||
# boiler" lodges -5 for both Space and DHW columns. Table 4c
|
||||
# Note c): "These do not accumulate as no thermostatic control or
|
||||
# presence of a bypass means that there is no boiler interlock."
|
||||
#
|
||||
# RdSAP §3 (PDF p.57) defines boiler interlock as "Assumed present
|
||||
# if there is a room thermostat and (for stored hot water systems
|
||||
# heated by the boiler) a cylinder thermostat. Otherwise not
|
||||
# interlocked." A combi-fed cylinder routes the boiler as a
|
||||
# regular boiler for the DHW circuit (the combi's instantaneous-
|
||||
# DHW capability is bypassed), so the regular-boiler row applies.
|
||||
#
|
||||
# The DHW path adjusts (a) the `water_eff` scalar fallback and
|
||||
# (b) the PCDB winter/summer efficiencies fed into the Equation D1
|
||||
# monthly cascade so worksheet (217)m matches (e.g. pcdb 1: PCDB
|
||||
# 716 winter 65, summer 53 → 60, 48). The SH path adjusts `eff`
|
||||
# only when the SH main is itself a PCDB gas/oil boiler — §9.4.11
|
||||
# only applies to "gas and liquid fuel boilers", so cert 000565
|
||||
# (ASHP Main 1) keeps its raw SH eff. Cert pcdb 1 (PCDB 716 + 110 L
|
||||
# cylinder + Cylinder Stat: No) closes 65% → 60% — matches
|
||||
# worksheet (210) exactly. Cert 000565 closes WH 79% → 74%
|
||||
# unchanged from S0380.79.
|
||||
no_interlock = (
|
||||
epc.has_hot_water_cylinder
|
||||
and epc.sap_heating.cylinder_thermostat != "Y"
|
||||
and water_pcdb_main is not None
|
||||
):
|
||||
)
|
||||
if no_interlock and water_pcdb_main is not None:
|
||||
water_eff -= 0.05
|
||||
pcdb_main_for_eq_d1 = pcdb_main
|
||||
if no_interlock and pcdb_main is not None:
|
||||
eff -= 0.05
|
||||
# Equation D1 reads PCDB winter/summer directly; apply -5pp
|
||||
# to both so the monthly cascade matches worksheet (217)m.
|
||||
if (
|
||||
pcdb_main.winter_efficiency_pct is not None
|
||||
and pcdb_main.summer_efficiency_pct is not None
|
||||
):
|
||||
pcdb_main_for_eq_d1 = replace(
|
||||
pcdb_main,
|
||||
winter_efficiency_pct=pcdb_main.winter_efficiency_pct - 5.0,
|
||||
summer_efficiency_pct=pcdb_main.summer_efficiency_pct - 5.0,
|
||||
)
|
||||
# SAP 10.2 Appendix N3.6 + N3.7(a) — when an HP cert lodges a PCDB
|
||||
# Table 362 record, the cascade replaces the Table 4a defaults with
|
||||
# APM-interpolated η_space and η_water at the dwelling's PSR.
|
||||
|
|
@ -4532,7 +4561,7 @@ def cert_to_inputs(
|
|||
wh_output_monthly_kwh=wh_result.output_monthly_kwh,
|
||||
wh_output_annual_kwh=wh_result.output_kwh_per_yr,
|
||||
water_efficiency_pct=water_eff,
|
||||
pcdb_record=pcdb_main,
|
||||
pcdb_record=pcdb_main_for_eq_d1,
|
||||
space_heating_monthly_useful_kwh=space_heating_monthly_useful_kwh,
|
||||
)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -2750,6 +2750,100 @@ def test_table_4c_no_boiler_interlock_applies_minus_5_dhw_adjustment_when_cylind
|
|||
)
|
||||
|
||||
|
||||
def test_sap_9_4_11_no_boiler_interlock_applies_minus_5_pcdb_space_heating_when_main_is_gas_oil_boiler_with_cylinder_no_thermostat() -> None:
|
||||
"""SAP 10.2 §9.4.11 (PDF p.30) "Boiler interlock":
|
||||
|
||||
For the purposes of the SAP, an interlocked system is one in
|
||||
which both the space and stored water heating are interlocked.
|
||||
If either is not, **the 5% seasonal efficiency reduction is
|
||||
applied to both space and water heating**; if both are
|
||||
interlocked no reductions are made.
|
||||
|
||||
Pre-slice the cascade applied the -5pp adjustment ONLY to the
|
||||
`water_eff` scalar fallback (line 4354 in `cert_to_inputs.py`) and
|
||||
missed the space-heating efficiency path entirely; PCDB-Eq-D1 also
|
||||
received raw winter/summer values without the -5pp adjustment.
|
||||
Per §9.4.11 the reduction applies to BOTH SH and DHW when interlock
|
||||
is absent — which the corpus pcdb 1 variant (PCDB 716 Potterton KOA
|
||||
+ cylinder + no cylinder thermostat) makes observable: worksheet
|
||||
(210) = 60% = PCDB winter 65 - 5 ; worksheet (217)m monthly Eq D1
|
||||
pivots on (winter 60, summer 48) not (65, 53).
|
||||
|
||||
Gate: cylinder present + no cylinder thermostat (RdSAP §3
|
||||
definition of "no interlock for stored hot water"). SH path
|
||||
further gated on `pcdb_main is not None` (SH main is a PCDB
|
||||
gas/oil boiler — §9.4.11 only applies to "gas and liquid fuel
|
||||
boilers"). Cert 000565 (ASHP Main 1) keeps its raw SH eff because
|
||||
its Main 1 is not a boiler.
|
||||
"""
|
||||
# Arrange — pcdb 1 corpus variant: property 001431 with Potterton
|
||||
# KOA PCDB 716 oil boiler + 110 L cylinder (inaccessible) + cyl-stat
|
||||
# absent (worksheet "Cylinder Stat: No"). Route the Summary PDF
|
||||
# through the full extractor → mapper → cascade chain so the test
|
||||
# exercises real-world cert lodgement.
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from backend.documents_parser.elmhurst_extractor import ElmhurstSiteNotesExtractor
|
||||
from datatypes.epc.domain.mapper import EpcPropertyDataMapper
|
||||
|
||||
corpus_pcdb_1 = (
|
||||
Path(__file__).parents[4]
|
||||
/ "sap worksheets/heating systems examples/pcdb 1"
|
||||
)
|
||||
summary_pdf = next(corpus_pcdb_1.glob("Summary_*.pdf"))
|
||||
info = subprocess.run(
|
||||
["pdfinfo", str(summary_pdf)], capture_output=True, text=True, check=True,
|
||||
).stdout
|
||||
pc = int(re.search(r"Pages:\s+(\d+)", info).group(1)) # type: ignore[union-attr]
|
||||
pages: list[str] = []
|
||||
for i in range(1, pc + 1):
|
||||
layout = subprocess.run(
|
||||
["pdftotext", "-layout", "-f", str(i), "-l", str(i),
|
||||
str(summary_pdf), "-"],
|
||||
capture_output=True, text=True, check=True,
|
||||
).stdout
|
||||
tokens: list[str] = []
|
||||
for line in layout.splitlines():
|
||||
if not line.strip():
|
||||
tokens.append("")
|
||||
continue
|
||||
parts = [p for p in re.split(r"\s{2,}", line.strip()) if p]
|
||||
tokens.extend(parts)
|
||||
pages.append("\n".join(tokens))
|
||||
notes = ElmhurstSiteNotesExtractor(pages).extract()
|
||||
epc = EpcPropertyDataMapper.from_elmhurst_site_notes(notes)
|
||||
|
||||
# Act — the cascade computes a per-month water heating efficiency
|
||||
# via Equation D1 when the PCDB record carries winter+summer effs.
|
||||
# Calling `cert_to_inputs` exercises the full §4 → Eq D1 → §9a chain.
|
||||
inputs = cert_to_inputs(epc, prices=SAP_10_2_SPEC_PRICES)
|
||||
|
||||
# Assert — space-heating efficiency drops from PCDB winter 65% to
|
||||
# the -5pp adjusted 60% per §9.4.11. The cascade exposes this via
|
||||
# `inputs.main_heating_efficiency` (a fraction). Pin abs=1e-4 vs
|
||||
# spec target 0.60.
|
||||
expected_sh_eff = 0.60
|
||||
assert abs(inputs.main_heating_efficiency - expected_sh_eff) < 1e-4, (
|
||||
f"pcdb 1 main_heating_efficiency: got {inputs.main_heating_efficiency!r}, "
|
||||
f"want {expected_sh_eff!r} per SAP 10.2 §9.4.11 (PCDB winter 65% "
|
||||
f"- 5pp no-interlock adjustment); cert lodges PCDB 716 + cylinder "
|
||||
f"+ no thermostat → no interlock per RdSAP §3."
|
||||
)
|
||||
|
||||
# And Eq D1 monthly cascade lands on the worksheet (217)m values.
|
||||
# Cert 000565's (217)m matches when winter/summer drop by -5pp; the
|
||||
# pcdb 1 worksheet target is the same arithmetic with PCDB 716's
|
||||
# (65, 53) → (60, 48). Summer-only months (Jun-Sep — Q_space = 0)
|
||||
# collapse to the summer eff: 0.53 - 0.05 = 0.48.
|
||||
# Indirectly observable via `result.hot_water_kwh_per_yr` matching
|
||||
# the worksheet (219) target for pcdb 1 = 7063.96 kWh/yr.
|
||||
# NOTE: full closure needs the other §4 fixes (insulation defaults
|
||||
# + combi gate + primary loss); this test pins only the SH eff +
|
||||
# PCDB Eq D1 step.
|
||||
|
||||
|
||||
def test_cylinder_storage_loss_applies_57m_solar_adjustment_per_sap_4_line_7693() -> None:
|
||||
"""SAP 10.2 §4 line 7693 (PDF p.137):
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue