mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice S0380.147: Appendix D Eq D1 — Table 4b non-PCDB boilers (winter/summer monthly cascade)
SAP 10.2 Appendix D §D2.1 (2) Equation (D1) (PDF p.57):
If the boiler provides both space and water heating, and the summer
seasonal efficiency is lower than the winter seasonal efficiency,
the efficiency is a combination of winter and summer seasonal
efficiencies according to the relative proportion of heat needed
from the boiler for space and water heating in the month concerned:
Q_space + Q_water
η_water,m = ───────────────────────────────
Q_space/η_winter + Q_water/η_summer
where Q_space (kWh/month) is the quantity calculated at (98c)m
multiplied by (204) or by (205);
Q_water (kWh/month) is the quantity calculated at (64)m;
η_winter and η_summer are the winter and summer seasonal
efficiencies (from Table 4b).
Pre-slice the cascade only wired Eq D1 for PCDB-tested boilers (the
`pcdb_record` branch in `_apply_water_efficiency`). For non-PCDB
Table 4b boilers (`sap_main_heating_code` 101-141) where the cert
lodges no `main_heating_index_number`, the cascade fell through to
the scalar `water_efficiency_pct` divisor — which resolved via WHC
901 inherit to Table 4b WINTER eff (wrong direction; spec wants the
monthly Eq D1 blend).
This slice:
- Adds `domain/sap10_calculator/tables/table_4b.py` with the full
41-row Table 4b (winter, summer) pair dict for codes 101-141
verbatim from SAP 10.2 PDF p.168 (Table 4b).
- Refactors `_apply_water_efficiency` parameter from
`pcdb_record: Optional[GasOilBoilerRecord]` to
`eq_d1_winter_summer_pct: Optional[tuple[float, float]]` —
decouples the Eq D1 input from the PCDB record so a Table 4b
fallback can populate it without faking a PCDB record.
- Resolves Eq D1 inputs at the call site with priority order:
1. PCDB Table 105 winter/summer (existing path)
2. SAP 10.2 Table 4b (PDF p.168) winter/summer when PCDB
absent + WHC=901 (`_WHC_FROM_MAIN_HEATING`, the spec form
of "boiler provides both space and water heating").
§9.4.11 -5pp interlock applies symmetrically to both columns of
whichever (winter, summer) tuple is resolved.
Oil 1 cert worksheet (217)m verified Jan 81.83 / Apr 81.42 / May
79.94 / Jun-Sep 72.00 / Dec 81.86 — exact back-solve to Eq D1 with
Table 4b code 127 (winter 84, summer 72). Annual HW fuel (219) =
Σ (64)m × 100 / (217)m = 3638.99 kWh/yr ≡ cascade post-slice.
Cascade impact:
Heating-systems corpus (worksheet-pinned, oil 1 only on pin grid):
oil 1 SAP +1.76 → +1.18 (Δ -0.59)
cost -£40.60 → -£27.12 (Δ +£13.48)
CO2 -129.22 → -55.36 (Δ +73.86 kg/yr)
PE -590.02 → -275.52 (Δ +314.50 kWh/yr)
Remaining oil 1 residual is Table 4f auxiliary energy (cascade
pumps_fans 130 kWh vs worksheet 265 kWh — missing the oil-boiler
pump 100 kWh + CH pump 130 vs ws 165). Follow-up slice.
Golden fixtures (cert-pinned, integer-rounded PE):
cert 0240 (dual oil combi 130, no cylinder): PE +0.05 → +1.02
cert 6035 (gas combi 104, no cylinder): PE +46.10 → +47.29
Both shifts reflect spec-correct Eq D1 now firing for non-PCDB
combi-no-cylinder configs. The pre-slice near-zero pin on cert
0240 was masking offsetting cascade gaps (likely Table 4f
auxiliary energy and/or dual-main Q_space split per (98c)m ×
(204) which the cascade currently treats as full demand).
Following [[reference-unmapped-sap-code]] discipline, the new Table
4b dict is the canonical spec-source — `domain.sap10_ml.sap_
efficiencies._SPACE_EFF_BY_CODE` still carries the winter column for
the ML feature cascade and is left in place per the sap10_ml
deprecation plan (separate migration).
Test:
test_sap_appendix_d_eq_d1_water_efficiency_monthly_for_non_pcdb_
table_4b_boiler_with_cylinder — asserts cert 1431 oil 1 HW fuel
annual = 3638.99 ± 1.0 kWh/yr (matches worksheet (219)).
Extended handover suite: 890 pass, 0 fail. Pyright net-zero (44=44).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
bd193e06fc
commit
7dceeff24b
5 changed files with 275 additions and 34 deletions
|
|
@ -229,7 +229,7 @@ _EXPECTATIONS: tuple[_CorpusExpectation, ...] = (
|
|||
_CorpusExpectation(variant='electric 8', block='11a', expected_sap_resid=-0.2568, expected_cost_resid_gbp=+5.9163, expected_co2_resid_kg=+11.6231, expected_pe_resid_kwh=+126.0896),
|
||||
_CorpusExpectation(variant='electric 9', block='11a', expected_sap_resid=-0.1181, expected_cost_resid_gbp=+2.7217, expected_co2_resid_kg=+5.6819, expected_pe_resid_kwh=+91.4145),
|
||||
_CorpusExpectation(variant='gshp', block='11a', expected_sap_resid=+1.1491, expected_cost_resid_gbp=-26.4775, expected_co2_resid_kg=-41.4461, expected_pe_resid_kwh=-454.5023),
|
||||
_CorpusExpectation(variant='oil 1', block='11a', expected_sap_resid=+1.7621, expected_cost_resid_gbp=-40.6035, expected_co2_resid_kg=-129.2211, expected_pe_resid_kwh=-590.0236),
|
||||
_CorpusExpectation(variant='oil 1', block='11a', expected_sap_resid=+1.1770, expected_cost_resid_gbp=-27.1207, expected_co2_resid_kg=-55.3633, expected_pe_resid_kwh=-275.5155),
|
||||
_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),
|
||||
|
|
|
|||
|
|
@ -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, replace
|
||||
from dataclasses import dataclass
|
||||
from decimal import ROUND_HALF_UP, Decimal
|
||||
from typing import Callable, Final, Literal, Optional
|
||||
|
||||
|
|
@ -109,6 +109,9 @@ from domain.sap10_calculator.tables.table_32 import (
|
|||
is_electric_fuel_code,
|
||||
unit_price_p_per_kwh as table_32_unit_price_p_per_kwh,
|
||||
)
|
||||
from domain.sap10_calculator.tables.table_4b import (
|
||||
table_4b_seasonal_efficiencies_pct,
|
||||
)
|
||||
from domain.sap10_calculator.worksheet.fuel_cost import FuelCostResult, fuel_cost
|
||||
from domain.sap10_calculator.worksheet.rating import (
|
||||
ENERGY_COST_DEFLATOR,
|
||||
|
|
@ -1919,6 +1922,10 @@ def _pumps_fans_fuel_cost_gbp_per_kwh(
|
|||
# the SAME cascade the main heating uses, including the main_heating_
|
||||
# category fallback (e.g. heat pumps return 2.30 via category 4).
|
||||
_WATER_INHERIT_FROM_MAIN_CODES: Final[frozenset[int]] = frozenset({901, 902, 914})
|
||||
# Water-heating code 901 = "From main heating system" — used by the
|
||||
# SAP 10.2 Appendix D §D2.1 (2) Equation D1 gate, which only applies
|
||||
# when "the boiler provides both space and water heating".
|
||||
_WHC_FROM_MAIN_HEATING: Final[int] = 901
|
||||
|
||||
|
||||
def _water_efficiency_with_category_inherit(
|
||||
|
|
@ -4255,25 +4262,24 @@ def _apply_water_efficiency(
|
|||
wh_output_monthly_kwh: tuple[float, ...],
|
||||
wh_output_annual_kwh: float,
|
||||
water_efficiency_pct: float,
|
||||
pcdb_record: Optional[GasOilBoilerRecord],
|
||||
eq_d1_winter_summer_pct: Optional[tuple[float, float]],
|
||||
space_heating_monthly_useful_kwh: tuple[float, ...],
|
||||
) -> float:
|
||||
"""Divide §4 (64)m by the appropriate efficiency to land HW fuel kWh.
|
||||
|
||||
For PCDB-tested combis with distinct winter/summer efficiencies (and
|
||||
a (98c)m × (204) tuple in hand): use the SAP 10.2 Appendix D §D2.1
|
||||
(2) Equation D1 monthly cascade. Otherwise stay on the legacy scalar
|
||||
`water_efficiency_pct` divisor (single-value PCDB or Table 4a/4b)."""
|
||||
When (winter, summer) seasonal efficiencies are provided — either
|
||||
from a PCDB Table 105 record OR from the SAP 10.2 Table 4b non-PCDB
|
||||
fallback (`tables.table_4b.table_4b_seasonal_efficiencies_pct`) —
|
||||
use the SAP 10.2 Appendix D §D2.1 (2) Equation D1 monthly cascade.
|
||||
Otherwise stay on the legacy scalar `water_efficiency_pct` divisor
|
||||
(single-value PCDB summer eff, Table 4a inherit, etc.)."""
|
||||
if water_efficiency_pct <= 0:
|
||||
return 0.0
|
||||
if (
|
||||
pcdb_record is not None
|
||||
and pcdb_record.winter_efficiency_pct is not None
|
||||
and pcdb_record.summer_efficiency_pct is not None
|
||||
):
|
||||
if eq_d1_winter_summer_pct is not None:
|
||||
winter_pct, summer_pct = eq_d1_winter_summer_pct
|
||||
monthly_eff = water_efficiency_monthly_via_equation_d1(
|
||||
winter_efficiency_pct=pcdb_record.winter_efficiency_pct,
|
||||
summer_efficiency_pct=pcdb_record.summer_efficiency_pct,
|
||||
winter_efficiency_pct=winter_pct,
|
||||
summer_efficiency_pct=summer_pct,
|
||||
space_heating_monthly_useful_kwh=space_heating_monthly_useful_kwh,
|
||||
water_heating_output_monthly_kwh=wh_output_monthly_kwh,
|
||||
)
|
||||
|
|
@ -4566,20 +4572,54 @@ def cert_to_inputs(
|
|||
)
|
||||
if no_interlock and water_pcdb_main is not None:
|
||||
water_eff -= 0.05
|
||||
pcdb_main_for_eq_d1 = pcdb_main
|
||||
# Resolve the (winter, summer) seasonal efficiency pair that feeds
|
||||
# the SAP 10.2 Appendix D §D2.1 (2) Equation D1 monthly cascade.
|
||||
# Priority order:
|
||||
# 1. PCDB Table 105 record on the SH main (gas/oil boiler) —
|
||||
# `pcdb_main.{winter,summer}_efficiency_pct` are spec-derived.
|
||||
# 2. SAP 10.2 Table 4b (PDF p.168) non-PCDB fallback when the
|
||||
# cert's `sap_main_heating_code` is in the 101-141 boiler
|
||||
# range AND the DHW is from the main (WHC 901). Eq D1 only
|
||||
# applies when "the boiler provides both space and water
|
||||
# heating" per spec — WHC 901 is the cert form of that.
|
||||
# Codes on the Table 3 zero-loss list (combi, CPSU) get no
|
||||
# primary loss but ARE still eligible for Eq D1 — the spec's
|
||||
# §D2.1 (2) test is "summer < winter" + "boiler provides both",
|
||||
# not the primary-loss test.
|
||||
eq_d1_winter_summer_pct: Optional[tuple[float, float]] = None
|
||||
if (
|
||||
pcdb_main is not None
|
||||
and pcdb_main.winter_efficiency_pct is not None
|
||||
and pcdb_main.summer_efficiency_pct is not None
|
||||
):
|
||||
eq_d1_winter_summer_pct = (
|
||||
pcdb_main.winter_efficiency_pct,
|
||||
pcdb_main.summer_efficiency_pct,
|
||||
)
|
||||
elif (
|
||||
pcdb_main is None
|
||||
and main is not None
|
||||
and epc.sap_heating.water_heating_code == _WHC_FROM_MAIN_HEATING
|
||||
):
|
||||
# Non-PCDB Table 4b boiler + DHW from main. SAP 10.2 Appendix D
|
||||
# §D2.1 (2) applies whenever "the boiler provides both space
|
||||
# and water heating" — combi (no cylinder) and regular (with
|
||||
# cylinder) alike. Spec text doesn't gate on cylinder presence.
|
||||
eq_d1_winter_summer_pct = table_4b_seasonal_efficiencies_pct(
|
||||
main.sap_main_heating_code
|
||||
)
|
||||
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,
|
||||
)
|
||||
# §9.4.11 -5pp interlock applies symmetrically to both winter and
|
||||
# summer columns of the Equation D1 input — matches worksheet
|
||||
# (217)m for pcdb 1 (PCDB 716 winter 65 / summer 53 → 60 / 48).
|
||||
# No -5pp on the Table 4b branch when interlock is present (oil 1
|
||||
# cert has cylinder thermostat → interlock OK → no adjustment).
|
||||
if no_interlock and eq_d1_winter_summer_pct is not None:
|
||||
eq_d1_winter_summer_pct = (
|
||||
eq_d1_winter_summer_pct[0] - 5.0,
|
||||
eq_d1_winter_summer_pct[1] - 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.
|
||||
|
|
@ -4760,7 +4800,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_for_eq_d1,
|
||||
eq_d1_winter_summer_pct=eq_d1_winter_summer_pct,
|
||||
space_heating_monthly_useful_kwh=space_heating_monthly_useful_kwh,
|
||||
)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -3640,3 +3640,92 @@ def test_sap_table_3_primary_loss_applies_to_non_pcdb_table_4b_regular_boiler_wi
|
|||
f"primary pipework + cylinder thermostat + separately timed "
|
||||
f"DHW)"
|
||||
)
|
||||
|
||||
|
||||
def test_sap_appendix_d_eq_d1_water_efficiency_monthly_for_non_pcdb_table_4b_boiler_with_cylinder() -> None:
|
||||
"""SAP 10.2 Appendix D §D2.1 (2) Equation (D1) (PDF p.57):
|
||||
|
||||
If the boiler provides both space and water heating, and the
|
||||
summer seasonal efficiency is lower than the winter seasonal
|
||||
efficiency, the efficiency is a combination of winter and
|
||||
summer seasonal efficiencies according to the relative
|
||||
proportion of heat needed from the boiler for space and water
|
||||
heating in the month concerned:
|
||||
|
||||
Q_space + Q_water
|
||||
η_water,m = ─────────────────────────
|
||||
Q_space/η_winter + Q_water/η_summer
|
||||
|
||||
Pre-slice the cascade only wired Equation D1 for PCDB-tested
|
||||
boilers (the `pcdb_record` branch in `_apply_water_efficiency`).
|
||||
For non-PCDB Table 4b boilers (`sap_main_heating_code` 101-141)
|
||||
where the cert lodges no `main_heating_index_number`, the cascade
|
||||
fell through to the scalar `water_efficiency_pct` divisor —
|
||||
which for oil 1 (WHC 901 → inherit) resolved to the Table 4b
|
||||
WINTER efficiency (84%), the wrong direction (worksheet (216)
|
||||
"Efficiency of water heater" = 72% summer).
|
||||
|
||||
This slice adds a Table 4b summer-eff lookup
|
||||
(`tables.table_4b.table_4b_seasonal_efficiencies_pct`, citing
|
||||
SAP 10.2 PDF p.168) and feeds (winter, summer) into Equation D1
|
||||
for non-PCDB Table 4b boilers that supply DHW from the main.
|
||||
|
||||
Oil 1 cert lodges sap_main_heating_code 127 ("Condensing oil
|
||||
boiler", Table 4b winter 84 / summer 72) + WHC 901 + cylinder.
|
||||
Worksheet (217)m oscillates 72.0 (summer Jun-Sep, no SH demand)
|
||||
to 81.86 (Dec, max SH demand) — back-solves to exactly Equation
|
||||
D1 with winter=84, summer=72. Annual HW fuel (219) =
|
||||
Σ (64)m / (217)m / 100 = 3638.99 kWh — matches the cascade exactly
|
||||
when we wire the (84, 72) Eq D1 path.
|
||||
"""
|
||||
# Arrange — oil 1 corpus variant: Table 4b code 127 + cylinder.
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from backend.documents_parser.elmhurst_extractor import ElmhurstSiteNotesExtractor
|
||||
from datatypes.epc.domain.mapper import EpcPropertyDataMapper
|
||||
|
||||
corpus_oil_1 = (
|
||||
Path(__file__).parents[4]
|
||||
/ "sap worksheets/heating systems examples/oil 1"
|
||||
)
|
||||
summary_pdf = next(corpus_oil_1.glob("Summary_*.pdf"))
|
||||
info = subprocess.run(
|
||||
["pdfinfo", str(summary_pdf)], capture_output=True, text=True, check=True,
|
||||
).stdout
|
||||
pc_match = re.search(r"Pages:\s+(\d+)", info)
|
||||
assert pc_match is not None
|
||||
pc = int(pc_match.group(1))
|
||||
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 — full cascade, read final HW fuel kWh.
|
||||
inputs = cert_to_inputs(epc, prices=SAP_10_2_SPEC_PRICES)
|
||||
|
||||
# Assert — HW fuel (219) annual matches worksheet to <1 kWh/yr.
|
||||
# Worksheet (219) annual = 3638.9862 (P960 line 358). Pre-slice
|
||||
# the cascade produced ~3392 kWh (under by ~247 kWh) because the
|
||||
# Eq D1 monthly blend wasn't wired for Table 4b.
|
||||
expected_hw_fuel = 3638.99
|
||||
got_hw_fuel = inputs.hot_water_kwh_per_yr
|
||||
assert abs(got_hw_fuel - expected_hw_fuel) < 1.0, (
|
||||
f"oil 1 HW fuel annual: got {got_hw_fuel!r}, "
|
||||
f"want {expected_hw_fuel!r} per SAP 10.2 Appendix D §D2.1 (2) "
|
||||
f"Equation D1 with Table 4b code 127 (winter 84%, summer 72%)"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -75,8 +75,8 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = (
|
|||
cert_number="0240-0200-5706-2365-8010",
|
||||
actual_sap=73,
|
||||
expected_sap_resid=+0,
|
||||
expected_pe_resid_kwh_per_m2=+0.0542,
|
||||
expected_co2_resid_tonnes_per_yr=+0.0626,
|
||||
expected_pe_resid_kwh_per_m2=+1.0211,
|
||||
expected_co2_resid_tonnes_per_yr=+0.1118,
|
||||
notes=(
|
||||
"Detached house, TFA 118, age J, oil boiler PCDB-listed + PV + "
|
||||
"RR on BP[0]. Mapper DOES extract sap_room_in_roof.room_in_roof_"
|
||||
|
|
@ -91,8 +91,17 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = (
|
|||
"residual was -10 throughout the Slice 97..130 range; "
|
||||
"Slice S0380.131 flipped table_32.py heating-oil price 7.64 → "
|
||||
"5.44 per Elmhurst worksheet evidence + this cert's gov.uk "
|
||||
"back-solve, closing SAP residual -10 → +0 exactly. PE / CO2 "
|
||||
"residuals are unaffected by the unit-price flip."
|
||||
"back-solve, closing SAP residual -10 → +0 exactly. "
|
||||
"Slice S0380.147 wired SAP 10.2 Appendix D §D2.1 (2) Equation "
|
||||
"D1 for non-PCDB Table 4b boilers (code 130 = condensing "
|
||||
"combi oil, winter 82 / summer 73); this cert is dual-main "
|
||||
"oil combi (51%/49%) with WHC=901 + no cylinder. Spec-correct "
|
||||
"Eq D1 monthly blend (mean ~78%) produces ~150 kWh/yr more HW "
|
||||
"fuel than the pre-slice flat-winter calc — PE residual "
|
||||
"+0.0542 → +1.0211, CO2 +0.0626 → +0.1118. The pre-slice "
|
||||
"near-zero pin was masking a compensating cascade gap (likely "
|
||||
"Table 4f auxiliary energy or the dual-main Q_space split for "
|
||||
"Eq D1 per (98c)m × (204))."
|
||||
),
|
||||
),
|
||||
_GoldenExpectation(
|
||||
|
|
@ -157,8 +166,8 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = (
|
|||
cert_number="6035-7729-2309-0879-2296",
|
||||
actual_sap=70,
|
||||
expected_sap_resid=-6,
|
||||
expected_pe_resid_kwh_per_m2=+46.0952,
|
||||
expected_co2_resid_tonnes_per_yr=+1.0495,
|
||||
expected_pe_resid_kwh_per_m2=+47.2928,
|
||||
expected_co2_resid_tonnes_per_yr=+1.0779,
|
||||
notes=(
|
||||
"Mid-terrace, TFA 128, age A, gas combi Table 4b code 104. "
|
||||
"Slice 59 per-bp window apportionment tightens all 3 "
|
||||
|
|
@ -170,7 +179,14 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = (
|
|||
"→ +46.76, CO2 +1.09 → +1.07. S0380.109: §5.7+§5.8 formula "
|
||||
"chain for solid-brick + lodged-thickness + insulation "
|
||||
"tightens BP[0] Main wall U from Table-6 bucket → spec "
|
||||
"formula → PE +46.76 → +46.09, CO2 +1.065 → +1.049."
|
||||
"formula → PE +46.76 → +46.09, CO2 +1.065 → +1.049. "
|
||||
"Slice S0380.147 wired SAP 10.2 Appendix D §D2.1 (2) Equation "
|
||||
"D1 for non-PCDB Table 4b boilers (code 104 = condensing combi "
|
||||
"gas, winter 84 / summer 75 — combi-no-cylinder routes via "
|
||||
"WHC=901 + main code 104). Eq D1 monthly blend (mean ~80%) "
|
||||
"produces ~150 kWh/yr more HW fuel than the pre-slice flat-"
|
||||
"winter calc → PE residual +46.0952 → +47.2928, CO2 +1.0495 "
|
||||
"→ +1.0779."
|
||||
),
|
||||
),
|
||||
_GoldenExpectation(
|
||||
|
|
|
|||
96
domain/sap10_calculator/tables/table_4b.py
Normal file
96
domain/sap10_calculator/tables/table_4b.py
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
"""SAP 10.2 Table 4b (PDF p.168) — "Seasonal efficiency for gas and
|
||||
liquid fuel boilers", winter / summer pair per Table 4b sub-row code
|
||||
(`sap_main_heating_code` 101-141).
|
||||
|
||||
This table is the spec-canonical fallback when a gas / oil boiler is
|
||||
NOT in the PCDB. Winter efficiency feeds (206)..(212) space heating;
|
||||
summer efficiency feeds Appendix D §D2.1 (2) Equation D1 alongside
|
||||
winter to derive the worksheet (217)m monthly water-heating efficiency.
|
||||
|
||||
Codes are grouped in Table 4b by boiler type:
|
||||
|
||||
101-109 Gas boilers (mains, LPG, biogas) 1998 or later
|
||||
110-114 Gas pre-1998 with fan-assisted flue
|
||||
115-119 Gas pre-1998 with balanced / open flue
|
||||
120-123 Combined Primary Storage Units (CPSU)
|
||||
124-132 Liquid fuel boilers (oil, etc.)
|
||||
133-141 Range cooker boilers (gas + liquid fuel)
|
||||
|
||||
The winter column is duplicated in `domain.sap10_ml.sap_efficiencies.
|
||||
_SPACE_EFF_BY_CODE` for backward-compat with that module's interim
|
||||
ML cascade; the canonical source for new cascade work is here per
|
||||
[[sap10_ml deprecation]] memory.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Final, Optional
|
||||
|
||||
|
||||
# Verbatim from SAP 10.2 spec PDF p.168 (the "Boiler ... Efficiency, %
|
||||
# Winter / Summer" table). All values percent.
|
||||
_TABLE_4B_SEASONAL_EFF_PCT_BY_CODE: Final[dict[int, tuple[float, float]]] = {
|
||||
# Gas boilers (including mains gas, LPG and biogas) 1998 or later
|
||||
101: (74.0, 64.0), # Regular non-condensing with automatic ignition
|
||||
102: (84.0, 74.0), # Regular condensing with automatic ignition
|
||||
103: (74.0, 65.0), # Non-condensing combi with automatic ignition
|
||||
104: (84.0, 75.0), # Condensing combi with automatic ignition
|
||||
105: (70.0, 60.0), # Regular non-condensing with permanent pilot
|
||||
106: (80.0, 70.0), # Regular condensing with permanent pilot
|
||||
107: (70.0, 61.0), # Non-condensing combi with permanent pilot
|
||||
108: (80.0, 71.0), # Condensing combi with permanent pilot
|
||||
109: (66.0, 56.0), # Back boiler to radiators
|
||||
# Gas pre-1998 with fan-assisted flue
|
||||
110: (73.0, 63.0), # Regular, low thermal capacity
|
||||
111: (69.0, 59.0), # Regular, high or unknown thermal capacity
|
||||
112: (71.0, 62.0), # Combi
|
||||
113: (84.0, 75.0), # Condensing combi
|
||||
114: (84.0, 74.0), # Regular, condensing
|
||||
# Gas pre-1998 with balanced or open flue
|
||||
115: (66.0, 56.0), # Regular, wall mounted
|
||||
116: (56.0, 46.0), # Regular, floor mounted, pre 1979
|
||||
117: (66.0, 56.0), # Regular, floor mounted, 1979 to 1997
|
||||
118: (66.0, 57.0), # Combi
|
||||
119: (66.0, 56.0), # Back boiler to radiators
|
||||
# Combined Primary Storage Units (CPSU)
|
||||
120: (74.0, 72.0), # With automatic ignition (non-condensing)
|
||||
121: (83.0, 81.0), # With automatic ignition (condensing)
|
||||
122: (70.0, 68.0), # With permanent pilot (non-condensing)
|
||||
123: (79.0, 77.0), # With permanent pilot (condensing)
|
||||
# Liquid fuel boilers
|
||||
124: (66.0, 54.0), # Standard oil boiler pre-1985
|
||||
125: (71.0, 59.0), # Standard oil boiler 1985 to 1997
|
||||
126: (80.0, 68.0), # Standard oil boiler, 1998 or later
|
||||
127: (84.0, 72.0), # Condensing oil boiler
|
||||
128: (71.0, 62.0), # Combi oil boiler, pre-1998
|
||||
129: (77.0, 68.0), # Combi oil boiler, 1998 or later
|
||||
130: (82.0, 73.0), # Condensing combi oil boiler
|
||||
131: (66.0, 54.0), # Oil room heater with boiler to radiators, pre 2000
|
||||
132: (71.0, 59.0), # Oil room heater with boiler to radiators, 2000 or later
|
||||
# Range cooker boilers (mains gas, LPG and biogas)
|
||||
133: (47.0, 37.0), # Single burner with permanent pilot
|
||||
134: (51.0, 41.0), # Single burner with automatic ignition
|
||||
135: (61.0, 51.0), # Twin burner with permanent pilot (non-condensing) pre 1998
|
||||
136: (66.0, 56.0), # Twin burner with automatic ignition (non-condensing) pre 1998
|
||||
137: (66.0, 56.0), # Twin burner with permanent pilot (non-condensing) 1998 or later
|
||||
138: (71.0, 61.0), # Twin burner with automatic ignition (non-condensing) 1998 or later
|
||||
# Range cooker boilers (liquid fuel)
|
||||
139: (61.0, 49.0), # Single burner
|
||||
140: (71.0, 59.0), # Twin burner (non-condensing) pre 1998
|
||||
141: (76.0, 64.0), # Twin burner (non-condensing) 1998 or later
|
||||
}
|
||||
|
||||
|
||||
def table_4b_seasonal_efficiencies_pct(
|
||||
sap_main_heating_code: Optional[int],
|
||||
) -> Optional[tuple[float, float]]:
|
||||
"""Return the SAP 10.2 Table 4b `(winter, summer)` efficiency pair
|
||||
as percentages, or `None` when the lodged code is not a Table 4b
|
||||
boiler sub-row (e.g. Table 4a category code, no lodging).
|
||||
|
||||
Total contract — never raises; non-Table-4b codes fall through to
|
||||
None so the caller can route to the scalar / category cascade.
|
||||
"""
|
||||
if sap_main_heating_code is None:
|
||||
return None
|
||||
return _TABLE_4B_SEASONAL_EFF_PCT_BY_CODE.get(sap_main_heating_code)
|
||||
Loading…
Add table
Reference in a new issue