refactor(overlay): band uses cert-native anchors, not a synthesis field

Reviewer feedback: seasonal_efficiency_override_pct had no lodged-cert analogue
and polluted the EpcPropertyData/MainHeatingDetail cert datatype. Replace it with
the cert-native efficiency anchors a real cert already uses:
- C–G  -> sap_main_heating_code = the band's Table 4b code (combi/regular
  preserved by the code itself); resolved by the existing Table 4b path.
- A/B  -> main_heating_index_number = a type-matched representative PCDB product;
  resolved by the existing PCDB Appendix D2.1 path (winter+summer+combi-loss).
  _fold_heating clears the base code, so the effective cert reads as a real PCDB
  cert. Ready for future user-supplied product -> PCDB id.

Deletes seasonal_efficiency_override_pct from MainHeatingDetail, HeatingOverlay
and _MAIN_HEATING_FIELDS, and REVERTS both cert_to_inputs branches — those four
files are now byte-identical to main (no synthesis on the cert type, no new
calculator branch). Each A/B PCDB id is CI-guarded to its accredited (winter,
summer) so a PCDB refresh fails loudly. Oil A-combi (no >90% product, ≈0 homes)
keeps its code-130 default. Real certs: A-combi +2.06 SAP (idx 18964), D-regular
-0.98 (code 106); corpus unmoved; 810 tests green (ADR-0068).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-07-30 13:06:18 +00:00
parent df9073d80a
commit 307fdfb270
8 changed files with 222 additions and 256 deletions

View file

@ -125,12 +125,6 @@ class MainHeatingDetail:
)
main_heating_index_number: Optional[int] = None
sap_main_heating_code: Optional[int] = None # TODO: make enum?
# Boiler SEDBUK-band `(winter, summer)` seasonal efficiency (percent), set by
# the main-heating Landlord-Override overlay (ADR-0068 — the fifth Heating
# Companion). When present it overrides the Table 4b code default in the §206
# / Appendix D Eq D1 efficiency cascade; None keeps the code's Table 4b value.
# Override-supplied only — the lodged-cert path never sets it.
seasonal_efficiency_override_pct: Optional[tuple[float, float]] = None
main_heating_number: Optional[int] = None
main_heating_category: Optional[int] = None
main_heating_fraction: Optional[int] = None

View file

@ -40,9 +40,6 @@ from domain.modelling.simulation import EpcSimulation, HeatingOverlay
from domain.sap10_calculator.tables.table_12a import (
OFF_PEAK_IMPLYING_HEATING_CODES,
)
from domain.sap10_calculator.tables.table_4b import (
table_4b_seasonal_efficiencies_pct,
)
# Off-peak (Economy 7) meter. Electric storage / CPSU systems charge overnight at
# the low rate and cannot run economically on a single-rate meter; "Dual" lets
@ -247,17 +244,20 @@ _FAN_FLUE_BOILER_CODES = _OIL_BOILER_CODES
# mains gas (clearing a storage dwelling's old electric-immersion arrangement).
_FROM_MAIN_WATER_HEATING_CODE = 901
# Boiler Efficiency Band → seasonal efficiency, the fifth Heating Companion
# (ADR-0068). Keyed by the resolved boiler's base `sap_main_heating_code`
# (102 gas regular, 104 gas combi, 127 oil regular, 130 oil combi — the four
# SEDBUK-rated wet boilers) + its SEDBUK AG band. The base code is unchanged
# (it still drives combi-vs-regular hot-water behaviour); only the efficiency
# slot changes.
# Boiler Efficiency Band → the boiler's **cert-native efficiency anchor** the
# overlay emits — the fifth Heating Companion (ADR-0068). Keyed by the resolved
# boiler's base `sap_main_heating_code` (102 gas regular, 104 gas combi, 127 oil
# regular, 130 oil combi — the four SEDBUK-rated wet boilers) + its SEDBUK AG
# band. There is NO synthesised efficiency field: C-and-below emit the band's
# Table 4b `sap_main_heating_code`; A/B emit a representative PCDB
# `main_heating_index_number`. Both are real cert fields the calculator already
# resolves (Table 4b / PCDB Appendix D2.1), and the base code still drives the
# companion set (combi vs regular hot water).
#
# C-and-below reuse the *accredited Table 4b* `(winter, summer)` of the code the
# PRD/ADR maps each band to — recorded here as that code so the numbers stay in
# one place (`table_4b.py`). Oil's Table 4b codes are sparser than gas and do not
# line up cleanly with SEDBUK bands, so oil E/F/G are approximate nearest-fit
# C-and-below → the band's Table 4b code. The code is itself combi/regular-
# specific (regular 102→106/101/105/115; combi 104→108/103/107/118), so system-
# type behaviour is preserved by the code alone. Oil's Table 4b codes are sparser
# and don't line up cleanly with SEDBUK bands, so oil E/F/G are nearest-fit
# (⚠ ADR-0068 open question): E/F→125, D/E→129, F/G→128.
_BAND_TO_TABLE_4B_CODE: dict[tuple[int, BoilerEfficiencyBand], int] = {
# Gas boiler, regular (base 102)
@ -286,52 +286,54 @@ _BAND_TO_TABLE_4B_CODE: dict[tuple[int, BoilerEfficiencyBand], int] = {
(130, BoilerEfficiencyBand.G): 128, # (71, 62) ⚠
}
# A/B exceed Table 4b's 84 % gas / 8284 % oil ceiling, so they cannot borrow a
# Table 4b code — they take SEDBUK-derived `(winter, summer)` slot values
# validated against an **accredited Elmhurst build** (ADR-0068 gated pre-req).
# A/B exceed Table 4b's 84 % gas / 8284 % oil ceiling, so there is no Table 4b
# code for them — the cert-native anchor is a representative PCDB product
# `main_heating_index_number`, the same mechanism a real cert (or a future
# user-supplied product) uses. The calculator's existing PCDB path resolves
# winter + summer + combi-loss from the record; `_fold_heating` clears the base
# code when an index is present, so the effective cert reads exactly like a real
# PCDB-lodged cert. Products are **type-matched** to the archetype (combi vs
# regular) because the combi-loss cascade reads the record's keep-hot/DHW profile.
#
# GAS (validated 2026-07-29 on Elmhurst RdSAP10, dwelling ref 000567):
# A → PCDB 18790 (Worcester Greenstar 8000 Life): worksheet (206)=90.6 / (217)=81.6
# B → PCDB 15029 (Baxi Duo-tec Combi 24 HE class): worksheet (206)=88.0 / (217)=79.4
# Gas/LPG/biogas share codes 101119, so one pair per band covers all three; applied
# to both gas regular (102) and combi (104). (Enabling A moves ~14.6k A-rated homes
# up ~+13 SAP — flagged to stakeholders; some cross a band → Recs/eligibility.)
#
# OIL (124132): pinned from the accredited PCDB records we hold, applied to oil
# regular (127) and combi (130):
# A → PCDB 17292 (Firebird Enviromax Blue Supreme): (90.6, 82.8)
# B → PCDB 10498 (Worcester Greenstar Danesmoor): (88.0, 80.2)
# No separate Elmhurst build was needed: the gas builds above proved Elmhurst's
# worksheet (206)/(217) for a database boiler are its PCDB winter/summer read back
# verbatim (18790 → 90.6/81.6, 15029 → 88.0/79.4, both exact), per SAP 10.2 App
# D2.1 (PCDB overrides Table 4b). So the PCDB record IS the accredited value.
_BAND_SLOT_EFFICIENCY_PCT: dict[tuple[int, BoilerEfficiencyBand], tuple[float, float]] = {
(102, BoilerEfficiencyBand.A): (90.6, 81.6),
(104, BoilerEfficiencyBand.A): (90.6, 81.6),
(102, BoilerEfficiencyBand.B): (88.0, 79.4),
(104, BoilerEfficiencyBand.B): (88.0, 79.4),
(127, BoilerEfficiencyBand.A): (90.6, 82.8),
(130, BoilerEfficiencyBand.A): (90.6, 82.8),
(127, BoilerEfficiencyBand.B): (88.0, 80.2),
(130, BoilerEfficiencyBand.B): (88.0, 80.2),
# Provenance — gas A/B were built in accredited Elmhurst RdSAP10 (worksheet
# (206)/(217) == the PCDB record verbatim, SAP 10.2 App D2.1); that proved
# Elmhurst reads the PCDB winter/summer, which licensed the oil picks straight
# from the PCDB. Each id is CI-guarded (see test_boiler_efficiency_band_overlay)
# so a PCDB refresh that renumbers a product fails loudly, never silently
# mis-scores. NOTE: oil A combi (130, A) has no PCDB product above 90 % (only an
# "Illustrative" placeholder), so it is intentionally absent — an oil A-combi
# (≈0 dwellings) keeps its code-130 condensing default (82 %).
_BAND_TO_PCDB_INDEX: dict[tuple[int, BoilerEfficiencyBand], int] = {
(102, BoilerEfficiencyBand.A): 18790, # Worcester GS 8000 Life (gas/LPG reg) 90.6/81.6
(104, BoilerEfficiencyBand.A): 18964, # SIME EDEA (gas combi) 90.1/86.6
(102, BoilerEfficiencyBand.B): 8106, # Vaillant Ecomax (gas reg) 88.0/79.0
(104, BoilerEfficiencyBand.B): 15029, # Baxi Duo-tec Combi (gas combi) 88.0/79.4
(127, BoilerEfficiencyBand.A): 17292, # Firebird Enviromax (oil reg) 90.6/82.8
(127, BoilerEfficiencyBand.B): 10498, # Worcester Danesmoor (oil reg) 88.0/80.2
(130, BoilerEfficiencyBand.B): 15959, # Firebird Enviromax Combi (oil combi) 89.0/82.9
}
def band_seasonal_efficiency_pct(
base_code: int, band: BoilerEfficiencyBand
) -> Optional[tuple[float, float]]:
"""The SEDBUK-band `(winter, summer)` seasonal efficiency (percent) for a
resolved boiler `base_code`, or ``None`` when the band does not apply a
non-banded boiler (solid fuel / electric / CPSU), an A/B band still pending
Elmhurst validation, or `UNKNOWN`. C-and-below resolve to the accredited
Table 4b pair of the code the band maps to (ADR-0068)."""
slot = _BAND_SLOT_EFFICIENCY_PCT.get((base_code, band))
if slot is not None:
return slot
table_4b_code = _BAND_TO_TABLE_4B_CODE.get((base_code, band))
if table_4b_code is None:
return None
return table_4b_seasonal_efficiencies_pct(table_4b_code)
def band_efficiency_anchor(
base_code: int, band: Optional[BoilerEfficiencyBand]
) -> tuple[int, Optional[int]]:
"""The `(sap_main_heating_code, main_heating_index_number)` a boiler `base_code`
emits under a SEDBUK `band` (ADR-0068):
- **A/B** the base code kept + a representative PCDB index (the calculator's
PCDB path wins; `_fold_heating` then clears the code, giving a PCDB cert);
- **CG** the band's Table 4b code, no index (Table 4b path);
- no band / no applicable mapping (non-banded boiler, `UNKNOWN`, or the oil
A-combi gap) the base code unchanged, no index.
"""
if band is not None:
index = _BAND_TO_PCDB_INDEX.get((base_code, band))
if index is not None:
return base_code, index
table_4b_code = _BAND_TO_TABLE_4B_CODE.get((base_code, band))
if table_4b_code is not None:
return table_4b_code, None
return base_code, None
# Canonical system archetype → representative SAP `sap_main_heating_code`. Codes
# map to the modern/condensing variant (A-G efficiency deferred): 102 regular
@ -503,16 +505,6 @@ def _natural_fuel_for(code: int) -> Optional[int]:
return None
def _band_slot(
code: int, band: Optional[BoilerEfficiencyBand]
) -> Optional[tuple[float, float]]:
"""The `(winter, summer)` efficiency slot the Boiler Efficiency Band forces
on a resolved boiler `code`, or ``None`` when no band applies (ADR-0068)."""
if band is None:
return None
return band_seasonal_efficiency_pct(code, band)
def _gas_boiler_overlay(
code: int, band: Optional[BoilerEfficiencyBand] = None
) -> HeatingOverlay:
@ -521,11 +513,15 @@ def _gas_boiler_overlay(
controls, a single-rate meter, and a hot-water arrangement drawn from the
main system (a combi has no cylinder; a regular boiler / CPSU keeps one).
The SEDBUK band (when supplied and applicable) sets the `(winter, summer)`
efficiency slot the calculator consumes ahead of the code's Table 4b default
the fifth Heating Companion (ADR-0068)."""
The SEDBUK band (when supplied and applicable) picks the boiler's cert-native
efficiency anchor the band's Table 4b `sap_main_heating_code` (CG) or a
representative PCDB `main_heating_index_number` (A/B) the fifth Heating
Companion (ADR-0068). The base `code` still drives the combi/regular hot-water
companion; `_fold_heating` clears the code when an index is emitted."""
emitted_code, index = band_efficiency_anchor(code, band)
return HeatingOverlay(
sap_main_heating_code=code,
sap_main_heating_code=emitted_code,
main_heating_index_number=index,
main_heating_category=_GAS_BOILER_CATEGORY,
main_fuel_type=_MAINS_GAS_FUEL,
gas_connection_available=True,
@ -535,7 +531,6 @@ def _gas_boiler_overlay(
water_heating_code=_FROM_MAIN_WATER_HEATING_CODE,
water_heating_fuel=_MAINS_GAS_FUEL,
has_hot_water_cylinder=code not in _COMBI_CODES,
seasonal_efficiency_override_pct=_band_slot(code, band),
)
@ -554,12 +549,14 @@ def _fuel_boiler_overlay(
forces it True; and a solid-fuel boiler vents through a conventional
(non-fanned) flue.
The SEDBUK band applies only to the oil boilers here (`band_seasonal_
efficiency_pct` returns None for the solid-fuel code 151, which is intrinsic-
efficiency) the fifth Heating Companion (ADR-0068)."""
The SEDBUK band applies only to the oil boilers here `band_efficiency_anchor`
is a no-op for the solid-fuel code 151 (intrinsic-efficiency), returning the
code unchanged with no index the fifth Heating Companion (ADR-0068)."""
fuel = _natural_fuel_for(code)
emitted_code, index = band_efficiency_anchor(code, band)
return HeatingOverlay(
sap_main_heating_code=code,
sap_main_heating_code=emitted_code,
main_heating_index_number=index,
main_heating_category=_FUEL_BOILER_CATEGORY,
main_fuel_type=fuel,
main_heating_control=_FULL_BOILER_CONTROL,
@ -568,7 +565,6 @@ def _fuel_boiler_overlay(
water_heating_code=_FROM_MAIN_WATER_HEATING_CODE,
water_heating_fuel=fuel,
has_hot_water_cylinder=code not in _OIL_BOILER_COMBI_CODES,
seasonal_efficiency_override_pct=_band_slot(code, band),
)

View file

@ -137,7 +137,6 @@ _MAIN_HEATING_FIELDS: tuple[str, ...] = (
"sap_main_heating_code",
"main_heating_index_number",
"main_heating_category",
"seasonal_efficiency_override_pct",
"fan_flue_present",
"boiler_flue_type",
)

View file

@ -168,12 +168,6 @@ class HeatingOverlay:
sap_main_heating_code: Optional[int] = None
main_heating_index_number: Optional[int] = None
main_heating_category: Optional[int] = None
# The boiler's SEDBUK-band `(winter, summer)` seasonal efficiency (percent),
# the fifth Heating Companion (ADR-0068). Set by the main-heating overlay from
# the Boiler Efficiency Band for a gas/oil boiler; the calculator consumes it
# ahead of the Table 4b code default (the slot the PCDB `winter_efficiency_pct`
# already occupies). `None` leaves the code's Table 4b efficiency in force.
seasonal_efficiency_override_pct: Optional[tuple[float, float]] = None
# A modern condensing boiler has a fanned (room-sealed) flue; the boiler
# upgrade sets this True (SAP 10.2 Table 4f flue-fan electricity + the
# Table 4b condensing-boiler seasonal-efficiency basis depend on it).

View file

@ -2112,18 +2112,7 @@ def _main_heating_detail_efficiency(
if main is not None and main.main_heating_index_number is not None
else None
)
override_pct = (
main.seasonal_efficiency_override_pct if main is not None else None
)
if override_pct is not None:
# Landlord-Override SEDBUK-band efficiency — the fifth Heating Companion
# (ADR-0068). "Override wins": a system-replacing override makes the cert
# efficiency stale, so its `(winter, summer)` slot takes precedence over
# the Table 4b code default, occupying the same slot the PCDB
# `winter_efficiency_pct` does. Override-supplied only (lodged certs never
# set it), so the corpus is unmoved.
eff = override_pct[0] / 100.0
elif pcdb_main is not None and pcdb_main.winter_efficiency_pct is not None:
if pcdb_main is not None and pcdb_main.winter_efficiency_pct is not None:
eff = pcdb_main.winter_efficiency_pct / 100.0
else:
eff = seasonal_efficiency(main_code, main_category, main_fuel)
@ -8165,21 +8154,8 @@ def cert_to_inputs(
# 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.
override_pct = main.seasonal_efficiency_override_pct if main is not None else None
eq_d1_winter_summer_pct: Optional[tuple[float, float]] = None
if (
override_pct is not None
and epc.sap_heating.water_heating_code == _WHC_FROM_MAIN_HEATING
# Not when DHW is a separate electric immersion (WHC 903): the boiler
# does not provide water heating, so its efficiency must not reach Eq D1.
and not dhw_is_electric_immersion
):
# Landlord-Override SEDBUK-band `(winter, summer)` — the fifth Heating
# Companion (ADR-0068). It wins the Eq D1 water slot the same way it wins
# the §206 winter efficiency, ahead of the PCDB/Table 4b defaults, so a
# banded boiler's summer efficiency drives its hot-water fuel too.
eq_d1_winter_summer_pct = override_pct
elif (
pcdb_main is not None
and pcdb_main.winter_efficiency_pct is not None
and pcdb_main.summer_efficiency_pct is not None

View file

@ -4,133 +4,147 @@ from typing import Optional
import pytest
from datatypes.epc.domain.epc_property_data import EpcPropertyData
from domain.epc.property_overrides.boiler_efficiency_band import BoilerEfficiencyBand
from domain.epc.property_overlays.main_heating_system_overlay import (
band_seasonal_efficiency_pct,
band_efficiency_anchor,
main_heating_overlay_for,
)
from domain.modelling.scoring.overlay_applicator import apply_simulations
from domain.sap10_calculator.tables.pcdb import gas_oil_boiler_record
from tests.domain.sap10_calculator.worksheet._elmhurst_worksheet_000490 import (
build_epc,
)
# --- band → cert-native efficiency anchor (code for CG, PCDB index for A/B) ---
@pytest.mark.parametrize(
("base_code", "band", "expected"),
[
# Gas regular (base 102) — C-and-below reuse the accredited Table 4b
# (winter, summer) of the code the band maps to (ADR-0068).
(102, BoilerEfficiencyBand.C, (84.0, 74.0)), # code 102
(102, BoilerEfficiencyBand.D, (80.0, 70.0)), # code 106
(102, BoilerEfficiencyBand.E, (74.0, 64.0)), # code 101
(102, BoilerEfficiencyBand.F, (70.0, 60.0)), # code 105
(102, BoilerEfficiencyBand.G, (66.0, 56.0)), # code 115
# Gas combi (base 104).
(104, BoilerEfficiencyBand.C, (84.0, 75.0)), # code 104
(104, BoilerEfficiencyBand.D, (80.0, 71.0)), # code 108
(104, BoilerEfficiencyBand.G, (66.0, 57.0)), # code 118
# Oil regular (base 127) — E/F nearest-fit to code 125 (⚠ ADR-0068).
(127, BoilerEfficiencyBand.C, (84.0, 72.0)), # code 127
(127, BoilerEfficiencyBand.D, (80.0, 68.0)), # code 126
(127, BoilerEfficiencyBand.E, (71.0, 59.0)), # code 125
(127, BoilerEfficiencyBand.G, (66.0, 54.0)), # code 124
# Oil combi (base 130) — D/E to code 129, F/G to code 128 (⚠ ADR-0068).
(130, BoilerEfficiencyBand.C, (82.0, 73.0)), # code 130
(130, BoilerEfficiencyBand.D, (77.0, 68.0)), # code 129
(130, BoilerEfficiencyBand.F, (71.0, 62.0)), # code 128
# C-and-below → the band's Table 4b sap_main_heating_code, no index. The
# code is itself regular/combi-specific, so system type is preserved.
(102, BoilerEfficiencyBand.C, (102, None)),
(102, BoilerEfficiencyBand.D, (106, None)),
(102, BoilerEfficiencyBand.G, (115, None)),
(104, BoilerEfficiencyBand.D, (108, None)),
(104, BoilerEfficiencyBand.G, (118, None)),
(127, BoilerEfficiencyBand.E, (125, None)),
(130, BoilerEfficiencyBand.D, (129, None)),
# A/B → base code kept + a representative PCDB index (>84%, no Table 4b code).
(102, BoilerEfficiencyBand.A, (102, 18790)),
(104, BoilerEfficiencyBand.A, (104, 18964)),
(102, BoilerEfficiencyBand.B, (102, 8106)),
(104, BoilerEfficiencyBand.B, (104, 15029)),
(127, BoilerEfficiencyBand.A, (127, 17292)),
(127, BoilerEfficiencyBand.B, (127, 10498)),
(130, BoilerEfficiencyBand.B, (130, 15959)),
],
)
def test_c_and_below_bands_resolve_to_the_table_4b_pair(
base_code: int, band: BoilerEfficiencyBand, expected: tuple[float, float]
def test_band_resolves_to_the_cert_native_anchor(
base_code: int, band: BoilerEfficiencyBand, expected: tuple[int, Optional[int]]
) -> None:
assert band_seasonal_efficiency_pct(base_code, band) == expected
assert band_efficiency_anchor(base_code, band) == expected
@pytest.mark.parametrize(
("base_code", "band", "expected"),
("base_code", "band"),
[
# Gas A/B validated against accredited Elmhurst (ADR-0068): A → PCDB 18790
# (Worcester Greenstar 8000 Life) 90.6/81.6; B → PCDB 15029 (Baxi Duo-tec
# Combi 24 HE) 88.0/79.4. Gas/LPG/biogas share codes — one pair per band on
# both regular (102) and combi (104).
# Gas A/B — validated on accredited Elmhurst (PCDB 18790 / 15029).
(102, BoilerEfficiencyBand.A, (90.6, 81.6)),
(104, BoilerEfficiencyBand.A, (90.6, 81.6)),
(102, BoilerEfficiencyBand.B, (88.0, 79.4)),
(104, BoilerEfficiencyBand.B, (88.0, 79.4)),
# Oil A/B — pinned from the accredited PCDB records (17292 / 10498). The
# gas builds proved Elmhurst's (206)/(217) == the PCDB winter/summer, so
# the PCDB record is the accredited value (no separate oil build needed).
(127, BoilerEfficiencyBand.A, (90.6, 82.8)),
(130, BoilerEfficiencyBand.A, (90.6, 82.8)),
(127, BoilerEfficiencyBand.B, (88.0, 80.2)),
(130, BoilerEfficiencyBand.B, (88.0, 80.2)),
# No band → base code unchanged, no index.
(102, None),
(104, None),
# Oil A-combi gap: no PCDB product above 90% exists → keep the code-130
# condensing default (82%). ≈0 dwellings (ADR-0068).
(130, BoilerEfficiencyBand.A),
# Non-banded boilers ignore the band letter entirely.
(151, BoilerEfficiencyBand.G), # solid fuel
(191, BoilerEfficiencyBand.A), # electric boiler
(192, BoilerEfficiencyBand.D), # electric CPSU
(120, BoilerEfficiencyBand.D), # gas CPSU
],
)
def test_a_and_b_bands_use_the_accredited_slot(
base_code: int, band: BoilerEfficiencyBand, expected: tuple[float, float]
def test_no_applicable_band_keeps_the_base_code_with_no_index(
base_code: int, band: Optional[BoilerEfficiencyBand]
) -> None:
# A/B exceed Table 4b's 84% ceiling (the under-credit the fix removes).
assert band_seasonal_efficiency_pct(base_code, band) == expected
assert band_efficiency_anchor(base_code, band) == (base_code, None)
@pytest.mark.parametrize(
"base_code",
("pcdb_id", "expected_winter_summer"),
[
151, # Solid fuel boiler — Table 4a 75%, intrinsic (SEDBUK doesn't rate it)
191, # Electric boiler — Table 4a 100%, intrinsic
192, # Electric CPSU — intrinsic electric
120, # Gas CPSU — non-condensing, not SEDBUK-banded
(18790, (90.6, 81.6)), # gas A regular — Worcester GS 8000 Life
(18964, (90.1, 86.6)), # gas A combi — SIME EDEA
(8106, (88.0, 79.0)), # gas B regular — Vaillant Ecomax
(15029, (88.0, 79.4)), # gas B combi — Baxi Duo-tec Combi
(17292, (90.6, 82.8)), # oil A regular — Firebird Enviromax
(10498, (88.0, 80.2)), # oil B regular — Worcester Danesmoor
(15959, (89.0, 82.9)), # oil B combi — Firebird Enviromax Combi
],
)
@pytest.mark.parametrize("band", list(BoilerEfficiencyBand))
def test_non_banded_boilers_never_take_a_slot(
base_code: int, band: BoilerEfficiencyBand
def test_each_ab_pcdb_index_still_resolves_to_its_accredited_efficiency(
pcdb_id: int, expected_winter_summer: tuple[float, float]
) -> None:
assert band_seasonal_efficiency_pct(base_code, band) is None
# CI guard: a PCDB refresh that renumbers/removes a product must fail here
# loudly, never silently mis-score an A/B boiler (ADR-0068).
record = gas_oil_boiler_record(pcdb_id)
assert record is not None
assert (record.winter_efficiency_pct, record.summer_efficiency_pct) == (
expected_winter_summer
)
# --- overlay wiring (the HeatingOverlay a band produces) ---
@pytest.mark.parametrize(
("archetype", "band", "expected"),
("archetype", "band", "code", "index"),
[
("Gas boiler, regular", BoilerEfficiencyBand.D, (80.0, 70.0)),
("Gas boiler, combi", BoilerEfficiencyBand.G, (66.0, 57.0)),
("Oil boiler, regular", BoilerEfficiencyBand.E, (71.0, 59.0)),
("Oil boiler, combi", BoilerEfficiencyBand.D, (77.0, 68.0)),
# Elmhurst-validated A/B upside on gas (the A under-credit fix).
("Gas boiler, regular", BoilerEfficiencyBand.A, (90.6, 81.6)),
("Gas boiler, combi", BoilerEfficiencyBand.A, (90.6, 81.6)),
("Gas boiler, combi", BoilerEfficiencyBand.B, (88.0, 79.4)),
# CG → emit the band Table 4b code, no index.
("Gas boiler, regular", BoilerEfficiencyBand.D, 106, None),
("Gas boiler, combi", BoilerEfficiencyBand.G, 118, None),
("Oil boiler, regular", BoilerEfficiencyBand.E, 125, None),
("Oil boiler, combi", BoilerEfficiencyBand.D, 129, None),
# A/B → keep the base code + set the representative PCDB index.
("Gas boiler, regular", BoilerEfficiencyBand.A, 102, 18790),
("Gas boiler, combi", BoilerEfficiencyBand.A, 104, 18964),
("Gas boiler, combi", BoilerEfficiencyBand.B, 104, 15029),
("Oil boiler, regular", BoilerEfficiencyBand.A, 127, 17292),
("Oil boiler, combi", BoilerEfficiencyBand.B, 130, 15959),
],
)
def test_overlay_sets_the_band_efficiency_slot_on_gas_and_oil_boilers(
archetype: str, band: BoilerEfficiencyBand, expected: tuple[float, float]
def test_overlay_emits_the_band_anchor_on_gas_and_oil_boilers(
archetype: str, band: BoilerEfficiencyBand, code: int, index: Optional[int]
) -> None:
simulation = main_heating_overlay_for(archetype, 0, band)
assert simulation is not None
assert simulation.heating is not None
# The base code is unchanged (it still drives combi/regular HW behaviour);
# only the efficiency slot is set from the band.
assert simulation.heating.seasonal_efficiency_override_pct == expected
assert simulation.heating.sap_main_heating_code == code
assert simulation.heating.main_heating_index_number == index
@pytest.mark.parametrize(
("archetype", "band"),
[
# No band → no slot (today's behaviour: keep the condensing code default).
# No band → base code, no index (today's behaviour).
("Gas boiler, combi", None),
# Non-banded boilers ignore the band letter entirely.
# Oil A-combi gap → base code 130, no index.
("Oil boiler, combi", BoilerEfficiencyBand.A),
# Non-banded boilers ignore the band entirely.
("Electric boiler", BoilerEfficiencyBand.D),
("Electric boiler", BoilerEfficiencyBand.A),
("Solid fuel boiler", BoilerEfficiencyBand.G),
("Gas CPSU", BoilerEfficiencyBand.D),
],
)
def test_overlay_sets_no_slot_when_band_absent_or_not_applicable(
def test_overlay_sets_no_index_when_band_absent_or_not_applicable(
archetype: str, band: Optional[BoilerEfficiencyBand]
) -> None:
simulation = main_heating_overlay_for(archetype, 0, band)
assert simulation is not None
assert simulation.heating is not None
assert simulation.heating.seasonal_efficiency_override_pct is None
assert simulation.heating.main_heating_index_number is None
def test_overlay_defaults_to_no_band_when_not_supplied() -> None:
@ -139,4 +153,31 @@ def test_overlay_defaults_to_no_band_when_not_supplied() -> None:
assert simulation is not None
assert simulation.heating is not None
assert simulation.heating.seasonal_efficiency_override_pct is None
assert simulation.heating.sap_main_heating_code == 104
assert simulation.heating.main_heating_index_number is None
# --- effective cert (post-fold): A/B become a PCDB cert; CG keep the code ---
def _apply_gas_combi(band: Optional[BoilerEfficiencyBand]) -> EpcPropertyData:
overlay = main_heating_overlay_for("Gas boiler, combi", 0, band)
assert overlay is not None
return apply_simulations(build_epc(), [overlay])
def test_ab_effective_cert_is_a_pcdb_cert_with_the_code_cleared() -> None:
# `_fold_heating`'s index/code mutual-exclusion: an A/B override yields exactly
# a PCDB-lodged cert — index set, code cleared — which the calculator's PCDB
# Appendix D2.1 path resolves (winter+summer+combi-loss).
eff = _apply_gas_combi(BoilerEfficiencyBand.A)
main = eff.sap_heating.main_heating_details[0]
assert main.main_heating_index_number == 18964
assert main.sap_main_heating_code is None
def test_c_to_g_effective_cert_keeps_the_band_table_4b_code() -> None:
eff = _apply_gas_combi(BoilerEfficiencyBand.G)
main = eff.sap_heating.main_heating_details[0]
assert main.sap_main_heating_code == 118 # gas combi G
assert main.main_heating_index_number is None

View file

@ -2885,65 +2885,6 @@ def test_living_area_rounds_half_up_at_2_dp_decimal_boundary_per_rdsap_15() -> N
assert abs(fla_off_boundary - (14.06 / 46.87)) <= 1e-12
def _epc_with_override_boiler(
base: object, seasonal_efficiency_override_pct: Optional[tuple[float, float]]
):
"""A copy of the typical semi-detached cert whose primary gas boiler (Table 4b
code 102) carries a Landlord-Override `(winter, summer)` efficiency slot the
fifth Heating Companion (ADR-0068)."""
import dataclasses
typical = _typical_semi_detached_epc()
detail = dataclasses.replace(
_gas_boiler_detail(sap_main_heating_code=102),
seasonal_efficiency_override_pct=seasonal_efficiency_override_pct,
)
return make_minimal_sap10_epc(
total_floor_area_m2=_TYPICAL_TFA_M2,
habitable_rooms_count=4,
region_code="1",
sap_building_parts=typical.sap_building_parts,
sap_windows=typical.sap_windows,
sap_heating=make_sap_heating(main_heating_details=[detail]),
)
def test_seasonal_efficiency_override_wins_over_the_table_4b_code_default() -> None:
# A gas boiler override forces the SEDBUK-band efficiency ahead of the Table
# 4b code default: a G-rated boiler on code 102 (84%) is modelled at 66%, not
# the condensing 84% — the fix at the heart of #1704 / ADR-0068. "Override
# wins": the band-derived efficiency replaces the code's, symmetric up or down.
epc = _epc_with_override_boiler(None, (66.0, 56.0)) # G band (winter, summer)
inputs = cert_to_inputs(epc)
assert inputs.main_heating_efficiency == 0.66
def test_no_override_keeps_the_table_4b_code_efficiency() -> None:
# Absent the slot, the code default stands (today's behaviour) — the override
# is the only thing that moves it.
epc = _epc_with_override_boiler(None, None)
inputs = cert_to_inputs(epc)
assert inputs.main_heating_efficiency == 0.84
def test_seasonal_efficiency_override_feeds_the_eq_d1_water_summer_efficiency() -> None:
# The slot is a `(winter, summer)` pair; the summer leg drives SAP Appendix D
# §D2.1 Eq D1 water-heating efficiency (WHC 901, from main). A lower-band
# boiler burns less efficiently in summer, so it needs MORE hot-water fuel —
# a direction check that the override reaches the Eq D1 branch, not just §206.
default_code = _epc_with_override_boiler(None, None) # code 102 → summer 74%
g_band = _epc_with_override_boiler(None, (66.0, 56.0)) # summer 56%
hw_default = cert_to_inputs(default_code).hot_water_kwh_per_yr
hw_g_band = cert_to_inputs(g_band).hot_water_kwh_per_yr
assert hw_g_band > hw_default
def test_main_heating_efficiency_reads_sap_main_heating_code() -> None:
# Arrange — Direction check: a gas combi (Table 4b code 102, 84% eff)
# vs a non-condensing gas boiler (code 105, 70% eff) must show through

View file

@ -227,7 +227,7 @@ def test_main_heating_system_row_produces_a_heating_overlay() -> None:
assert overlays[0].heating.sap_main_heating_code == 104
def test_band_parsed_from_the_heating_description_sets_the_efficiency_slot() -> None:
def test_band_parsed_from_the_heating_description_sets_the_code() -> None:
# Ship-before-FE (parse-on-read): the SEDBUK band is already carried in the
# main_heating_system row's original_spreadsheet_description, so the modelling
# honours it without waiting for the new boiler_efficiency_band override row.
@ -246,8 +246,10 @@ def test_band_parsed_from_the_heating_description_sets_the_efficiency_slot() ->
assert len(overlays) == 1
assert overlays[0].heating is not None
# G-rated gas regular → Table 4b (66, 56), not the condensing 84% default.
assert overlays[0].heating.seasonal_efficiency_override_pct == (66.0, 56.0)
# G-rated gas regular → the band's Table 4b code 115 (66/56), not the
# condensing 84% default (102).
assert overlays[0].heating.sap_main_heating_code == 115
assert overlays[0].heating.main_heating_index_number is None
def test_explicit_band_override_row_wins_over_the_parsed_description() -> None:
@ -268,8 +270,8 @@ def test_explicit_band_override_row_wins_over_the_parsed_description() -> None:
overlays = overlays_from(overrides)
heating = next(o.heating for o in overlays if o.heating is not None)
# D wins over the description's G → Table 4b (80, 70).
assert heating.seasonal_efficiency_override_pct == (80.0, 70.0)
# D wins over the description's G → Table 4b code 106 (80/70), not 115.
assert heating.sap_main_heating_code == 106
def test_band_repoints_with_the_resolved_fuel_for_an_oil_boiler() -> None:
@ -290,12 +292,34 @@ def test_band_repoints_with_the_resolved_fuel_for_an_oil_boiler() -> None:
overlays = overlays_from(overrides)
heating = next(o.heating for o in overlays if o.heating is not None)
assert heating.sap_main_heating_code == 127 # oil regular
assert heating.seasonal_efficiency_override_pct == (80.0, 68.0) # oil D → code 126
# Oil regular D → the oil Table 4b code 126 (80/68), off the base oil code 127.
assert heating.sap_main_heating_code == 126
assert heating.main_heating_index_number is None
def test_no_band_information_leaves_the_efficiency_slot_unset() -> None:
# A plain boiler description carries no band → no slot → condensing default.
def test_a_band_sets_a_pcdb_index_not_a_table_4b_code() -> None:
# A/B exceed Table 4b's ceiling → the overlay emits a representative PCDB
# index (base code kept; _fold clears it into a PCDB cert).
overrides = ResolvedPropertyOverrides(
rows=(
ResolvedPropertyOverride(
"main_heating_system",
0,
"Gas boiler, combi",
original_spreadsheet_description="Boiler: A rated Combi",
),
)
)
overlays = overlays_from(overrides)
heating = next(o.heating for o in overlays if o.heating is not None)
assert heating.main_heating_index_number == 18964 # gas A combi (SIME EDEA)
assert heating.sap_main_heating_code == 104 # base kept; _fold clears it
def test_no_band_information_leaves_the_base_code_unchanged() -> None:
# A plain boiler description carries no band → base condensing code, no index.
overrides = ResolvedPropertyOverrides(
rows=(
ResolvedPropertyOverride(
@ -310,7 +334,8 @@ def test_no_band_information_leaves_the_efficiency_slot_unset() -> None:
overlays = overlays_from(overrides)
heating = next(o.heating for o in overlays if o.heating is not None)
assert heating.seasonal_efficiency_override_pct is None
assert heating.sap_main_heating_code == 104
assert heating.main_heating_index_number is None
def test_unresolvable_rows_are_skipped() -> None: