mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-08-03 05:18:22 +00:00
feat(overlay): GREEN — band sets a (winter,summer) efficiency slot on gas/oil boilers
band_seasonal_efficiency_pct maps (base_code, band) -> Table 4b (winter,summer) for C-and-below; A/B pending Elmhurst (empty slot dict, no invented numbers); non-banded boilers (solid/electric/CPSU) return None. main_heating_overlay_for takes an optional band and sets seasonal_efficiency_override_pct, threaded onto MainHeatingDetail via _fold_heating (ADR-0068). Calculator consumption next. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7a35d177b2
commit
4244b16d17
4 changed files with 126 additions and 13 deletions
|
|
@ -125,6 +125,12 @@ 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
|
||||
|
|
|
|||
|
|
@ -18,22 +18,31 @@ manual charge control for storage heaters. So adding a heating archetype is just
|
|||
adding its code — coherent companions fall out. Synthesis owns coherence; the
|
||||
calculator never normalises a lodged cert.
|
||||
|
||||
The SEDBUK A-G efficiency band the Hyde "Heating" column carries is NOT honoured
|
||||
yet (no efficiency slot on the overlay/MainHeatingDetail) -- archetypes map to
|
||||
their modern/condensing Table 4b code, so an old low-rated boiler is currently
|
||||
modelled at the condensing efficiency. Heat pumps and community heating (which
|
||||
resolve via main_heating_index_number / community codes, not a Table 4b code)
|
||||
are left UNKNOWN until modelled. Unresolvable values produce no overlay.
|
||||
The SEDBUK A-G efficiency band the "Heating" column carries is honoured as the
|
||||
fifth Heating Companion (ADR-0068): a gas/oil boiler archetype still maps to its
|
||||
modern/condensing Table 4b code (which drives combi-vs-regular hot-water
|
||||
behaviour), but the `band` argument sets a `(winter, summer)` efficiency slot the
|
||||
calculator consumes ahead of that code's Table 4b default — so an old low-rated
|
||||
boiler is modelled at its band efficiency, not the condensing 84 %. C-and-below
|
||||
reuse accredited Table 4b values; A/B are pending Elmhurst validation (no slot
|
||||
until pinned). Solid-fuel / electric / CPSU boilers are intrinsic-efficiency and
|
||||
take no band. Heat pumps and community heating (which resolve via
|
||||
main_heating_index_number / community codes, not a Table 4b code) are left
|
||||
UNKNOWN until modelled. Unresolvable values produce no overlay.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from domain.epc.property_overrides.boiler_efficiency_band import BoilerEfficiencyBand
|
||||
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
|
||||
|
|
@ -238,6 +247,71 @@ _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 A–G band. The base code is unchanged
|
||||
# (it still drives combi-vs-regular hot-water behaviour); only the efficiency
|
||||
# slot changes.
|
||||
#
|
||||
# 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
|
||||
# (⚠ 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)
|
||||
(102, BoilerEfficiencyBand.C): 102, # (84, 74)
|
||||
(102, BoilerEfficiencyBand.D): 106, # (80, 70)
|
||||
(102, BoilerEfficiencyBand.E): 101, # (74, 64)
|
||||
(102, BoilerEfficiencyBand.F): 105, # (70, 60)
|
||||
(102, BoilerEfficiencyBand.G): 115, # (66, 56)
|
||||
# Gas boiler, combi (base 104)
|
||||
(104, BoilerEfficiencyBand.C): 104, # (84, 75)
|
||||
(104, BoilerEfficiencyBand.D): 108, # (80, 71)
|
||||
(104, BoilerEfficiencyBand.E): 103, # (74, 65)
|
||||
(104, BoilerEfficiencyBand.F): 107, # (70, 61)
|
||||
(104, BoilerEfficiencyBand.G): 118, # (66, 57)
|
||||
# Oil boiler, regular (base 127) — ⚠ E/F nearest-fit to code 125
|
||||
(127, BoilerEfficiencyBand.C): 127, # (84, 72)
|
||||
(127, BoilerEfficiencyBand.D): 126, # (80, 68)
|
||||
(127, BoilerEfficiencyBand.E): 125, # (71, 59) ⚠
|
||||
(127, BoilerEfficiencyBand.F): 125, # (71, 59)
|
||||
(127, BoilerEfficiencyBand.G): 124, # (66, 54)
|
||||
# Oil boiler, combi (base 130) — ⚠ D/E to 129, F/G to 128
|
||||
(130, BoilerEfficiencyBand.C): 130, # (82, 73)
|
||||
(130, BoilerEfficiencyBand.D): 129, # (77, 68)
|
||||
(130, BoilerEfficiencyBand.E): 129, # (77, 68) ⚠
|
||||
(130, BoilerEfficiencyBand.F): 128, # (71, 62)
|
||||
(130, BoilerEfficiencyBand.G): 128, # (71, 62) ⚠
|
||||
}
|
||||
|
||||
# A/B exceed Table 4b's 84 % gas / 82–84 % oil ceiling, so they cannot borrow a
|
||||
# Table 4b code — they take SEDBUK-derived `(winter, summer)` slot values that
|
||||
# MUST be validated against an accredited Elmhurst build before they are enabled
|
||||
# (ADR-0068 gated pre-req; ~14.6k A-rated homes move up + ripple into
|
||||
# Recommendations / eligibility). PENDING VALIDATION: intentionally empty — until
|
||||
# the pairs are pinned, an A/B boiler takes no slot and keeps its condensing code
|
||||
# default, so no invented numbers ship.
|
||||
_BAND_SLOT_EFFICIENCY_PCT: dict[tuple[int, BoilerEfficiencyBand], tuple[float, float]] = {}
|
||||
|
||||
|
||||
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)
|
||||
|
||||
# Canonical system archetype → representative SAP `sap_main_heating_code`. Codes
|
||||
# map to the modern/condensing variant (A-G efficiency deferred): 102 regular
|
||||
# condensing, 104 condensing combi, 120 CPSU, 401-404 storage heaters, 191
|
||||
|
|
@ -408,11 +482,27 @@ def _natural_fuel_for(code: int) -> Optional[int]:
|
|||
return None
|
||||
|
||||
|
||||
def _gas_boiler_overlay(code: int) -> HeatingOverlay:
|
||||
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:
|
||||
"""The coherent gas-boiler companion set: a mains-gas connection + gas main
|
||||
fuel, the gas-boiler heating category, a fanned room-sealed flue, full modern
|
||||
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)."""
|
||||
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)."""
|
||||
return HeatingOverlay(
|
||||
sap_main_heating_code=code,
|
||||
main_heating_category=_GAS_BOILER_CATEGORY,
|
||||
|
|
@ -424,10 +514,13 @@ def _gas_boiler_overlay(code: int) -> HeatingOverlay:
|
|||
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),
|
||||
)
|
||||
|
||||
|
||||
def _fuel_boiler_overlay(code: int) -> HeatingOverlay:
|
||||
def _fuel_boiler_overlay(
|
||||
code: int, band: Optional[BoilerEfficiencyBand] = None
|
||||
) -> HeatingOverlay:
|
||||
"""The coherent companion set for a non-gas wet boiler (oil / solid fuel),
|
||||
modelled on the gas-boiler pattern (ADR-0067): SAP Table 4a category 2, full
|
||||
modern controls (Table 4e 2106), a single-rate meter, hot water from the main
|
||||
|
|
@ -438,7 +531,11 @@ def _fuel_boiler_overlay(code: int) -> HeatingOverlay:
|
|||
carbon); `gas_connection_available` is left UNSET (None) so it inherits the
|
||||
lodged EPC — an oil/solid boiler needs no gas main, unlike a gas boiler which
|
||||
forces it True; and a solid-fuel boiler vents through a conventional
|
||||
(non-fanned) flue."""
|
||||
(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)."""
|
||||
fuel = _natural_fuel_for(code)
|
||||
return HeatingOverlay(
|
||||
sap_main_heating_code=code,
|
||||
|
|
@ -450,6 +547,7 @@ def _fuel_boiler_overlay(code: int) -> HeatingOverlay:
|
|||
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),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -464,15 +562,17 @@ def natural_fuel_for(main_heating_value: str) -> Optional[int]:
|
|||
|
||||
|
||||
def main_heating_overlay_for(
|
||||
main_heating_value: str, building_part: int
|
||||
main_heating_value: str,
|
||||
building_part: int,
|
||||
band: Optional[BoilerEfficiencyBand] = None,
|
||||
) -> Optional[EpcSimulation]:
|
||||
code = _MAIN_HEATING_CODES.get(main_heating_value)
|
||||
if code is None:
|
||||
return None
|
||||
if code in _GAS_BOILER_CODES:
|
||||
return EpcSimulation(heating=_gas_boiler_overlay(code))
|
||||
return EpcSimulation(heating=_gas_boiler_overlay(code, band))
|
||||
if code in _FUEL_BOILER_CODES:
|
||||
return EpcSimulation(heating=_fuel_boiler_overlay(code))
|
||||
return EpcSimulation(heating=_fuel_boiler_overlay(code, band))
|
||||
category = _category_for(code)
|
||||
control = _control_for(code)
|
||||
if category is None or control is None:
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ _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",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -168,6 +168,12 @@ 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).
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue