mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
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>
96 lines
4.7 KiB
Python
96 lines
4.7 KiB
Python
"""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)
|