Model/domain/epc/property_overlays/main_heating_system_overlay.py
Khalim Conn-Kowlessar 9153fa2523 A community override keeps an assessor-observed Group 3 control 🟩
The heat-network mirror of keep_existing_off_peak_meter: the override's
2313 default applies only when the replaced system's control is
cross-family (stale); an observed community control survives.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 16:35:08 +00:00

454 lines
25 KiB
Python

"""Map a Landlord-Override main-heating-system value to a heating Simulation Overlay.
A main-heating-system value is one canonical system archetype ("Gas boiler,
combi", "Electric storage heaters, fan"). The calculator reads the primary
system's `sap_main_heating_code` (SAP Table 4a/4b), so the overlay maps the
archetype to a representative code and emits a whole-dwelling `HeatingOverlay`
targeting `main_heating_details[0]` (`building_part` is ignored). It composes
field-wise with the main_fuel / water_heating overlays.
**Coherent Heating System / drag-along (ADR-0035):** a landlord tells us the
*system*, not the dependent fields a coherent heating system carries — its
electricity tariff (meter) and, for storage heaters, its charge control. Rather
than hand-attach those per archetype (easy to forget when a new system is
added), they are **derived from the SAP code**: the off-peak meter from the
overlay's assumed-Dual classification (`_ASSUMED_DUAL_METER_CODES` — the §12
off-peak systems plus all-electric room-heater dwellings), and the conservative
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.
"""
from __future__ import annotations
from typing import Optional
from domain.modelling.simulation import EpcSimulation, HeatingOverlay
from domain.sap10_calculator.tables.table_12a import (
OFF_PEAK_IMPLYING_HEATING_CODES,
)
# 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
# the §12 dispatch resolve the specific tariff (storage 7-hour, CPSU 10-hour).
_OFF_PEAK_METER = "Dual"
# Single-rate meter (SAP 10.2 Table 12a code 2 → STANDARD tariff). Every non-off-
# peak archetype synthesises this *explicitly* rather than leaving the meter
# untouched: switching OFF storage heaters must not let the dwelling's old "Dual"
# meter bleed through and bill the new gas/direct-acting system on an Economy-7
# split (the mirror of the storage→Dual drag, ADR-0035).
_SINGLE_RATE_METER = "Single"
# Meter-agnostic codes: the archetype cannot determine the tariff, so the overlay
# defers `meter_type` to the cert (`_meter_for` → None). Screed underfloor (424) is
# "standard or off peak" — 81% of the corpus lodge off-peak (ADR-0046).
_METER_AGNOSTIC_CODES = frozenset({424})
# Electric room heaters (SAP Table 4a 691). They don't *require* off-peak the way
# storage/CPSU do, so they're absent from the calculator's §12
# `OFF_PEAK_IMPLYING_HEATING_CODES` (Rules 1-2). But a dwelling heated by them is
# all-electric and realistically billed on Economy 7 — its immersion hot water
# charges overnight and §12 Rule 3 gives the room heaters a 10-hour off-peak
# window. So when the landlord names only the system, the coherent meter to
# assume is Dual; the §12 dispatch then applies the realistic high/low split
# (not a single-rate over-penalty, nor an all-low over-credit).
_ROOM_HEATER_CODES = frozenset({691})
# Codes for which the overlay assumes a Dual (off-peak) meter: the §12-mandated
# off-peak systems plus the all-electric room-heater dwellings above.
_ASSUMED_DUAL_METER_CODES = OFF_PEAK_IMPLYING_HEATING_CODES | _ROOM_HEATER_CODES
# SAP Table 4e Group 4 storage charge-control code. Manual charge control is the
# *conservative* assumption when the landlord didn't tell us the control: its
# +0.7 C mean-internal-temperature adjustment is the largest of the storage
# controls (automatic / Celect +0.4, HHR 0), so it never over-credits an
# unobserved control. Scoped to storage *heaters* (Table 4a 401-409) — the only
# systems that take a charge control.
_MANUAL_CHARGE_CONTROL = 2401
_STORAGE_HEATER_CODES = frozenset(range(401, 410))
# SAP Table 4a "electric storage heaters" category. Set on a storage-system
# override so the overlaid cert is internally consistent (code 401-409 ↔ category
# 7) — mirroring the control/fuel/meter companions the overlay already drags. A
# storage code with a stale non-storage category is a latent trap: the SAP 10.2
# Table 12a resolver classifies by category, so a leftover room-heater category
# 10 would price the off-peak storage heating at the peak rate.
_STORAGE_HEATER_CATEGORY = 7
# High heat retention storage heaters (SAP Table 4a 409) take a SINGLE intrinsic
# charge control — Table 4e 2404 (HHR, 0 C adjustment). It is named by the
# archetype, not unobserved, so 409 drags 2404 rather than the manual default
# (ADR-0044) — the only storage code that overrides `_MANUAL_CHARGE_CONTROL`.
_HHR_STORAGE_CODE = 409
_HHR_CHARGE_CONTROL = 2404
# SAP Table 4a category 10 ("Room heaters") and its conservative Table 4e Group 6
# control. A landlord names a room heater, not its control; code 2601 ("no
# thermostatic control of room temperature") is the lowest-SAP room-heater
# control — the modal one real solid-fuel room-heater certs lodge — so it never
# over-credits an unobserved control (the room-heater mirror of the storage
# manual-charge default). Scoped per family as archetypes land; solid-fuel room
# heaters are Table 4a 631-636.
_ROOM_HEATER_CATEGORY = 10
_ROOM_HEATER_CONTROL = 2601
_SOLID_FUEL_ROOM_HEATER_CODES = frozenset(range(631, 637))
# Oil room heaters (SAP Table 4a 621-625) — category 10, conservative room-heater
# control, natural fuel heating oil (RdSAP main_fuel 28).
_OIL_ROOM_HEATER_CODES = frozenset(range(621, 626))
_OIL_FUEL = 28
# Gas (incl. LPG) room heaters (SAP Table 4a 601-613) — category 10, conservative
# control, natural fuel mains gas (26); an LPG dwelling is refined by a main_fuel
# override (the overlay can't see the mains connection).
_GAS_ROOM_HEATER_CODES = frozenset(range(601, 614))
# Fuel-burning room heaters (solid + oil + gas) share category 10 + the
# conservative room-heater control; only the natural fuel differs by family.
# (Distinct from `_ROOM_HEATER_CODES` above, the electric-room-heater dual-meter
# set.)
_CATEGORY_10_ROOM_HEATER_CODES = (
_SOLID_FUEL_ROOM_HEATER_CODES | _OIL_ROOM_HEATER_CODES | _GAS_ROOM_HEATER_CODES
)
# Natural-fuel coherence (ADR-0041): where an archetype unambiguously implies a
# fuel, the overlay drags it so a system-only override is self-coherent on a cert
# that lodged a different fuel. A later `main_fuel` override still wins (last-wins
# composition); a contradicting landlord fuel is logged, not silently overridden.
# Electric room heaters (Table 4a 691-701) are unambiguously electricity (RdSAP
# main_fuel code 29). Solid fuel is ambiguous (coal / anthracite / smokeless /
# dual fuel / wood logs / pellets), so it defaults to house coal (33) — the most
# common solid fuel — when the landlord gives no `main_fuel` override; a specific
# solid-fuel override still wins by last-wins composition.
_ELECTRICITY_FUEL = 29
_HOUSE_COAL_FUEL = 33
_ELECTRIC_ROOM_HEATER_CODES = frozenset(range(691, 702))
# Electric boilers — SAP Table 4a 191 (direct-acting) and 192 (electric CPSU) —
# unambiguously electric, so they drag electricity like the electric room heaters
# (ADR-0045, closing the prior no-fuel gap on 191/192).
_ELECTRIC_BOILER_CODES = frozenset({191, 192})
# SAP Table 4a "boiler system" category — codes 191/192 are wet electric
# boilers, same category the gas-boiler overlay stamps. Forced so an
# electric-boiler override never keeps the replaced system's category: on
# portfolio 796 an inherited 6 (community) or 4 (heat pump) suppressed the
# HHRSH pathway, and an inherited 7 (storage) made the Table 12a resolver bill
# direct-acting heat wholly at the off-peak low rate (ADR-0052, #1444).
_ELECTRIC_BOILER_CATEGORY = 2
# SAP Table 4e Group 1 boiler control: 2101 ("no time or thermostatic control
# of room temperature", +0.6 C) is the conservative default when the landlord
# named only the system — the largest Group 1 mean-internal-temperature
# adjustment, so an unobserved control is never over-credited (the boiler
# mirror of storage 2401 / room-heater 2601 / underfloor 2701). Deliberately
# NOT the gas boilers' modern 2106: the gas archetypes assume a whole modern
# condensing install; the electric-boiler archetypes describe an existing
# system of unknown vintage (ADR-0052).
_CONSERVATIVE_BOILER_CONTROL = 2101
# Electric underfloor — SAP Table 4a 421/422 (off-peak) and 424 (screed) — also
# unambiguously electric (ADR-0046).
_ELECTRIC_UNDERFLOOR_CODES = frozenset({421, 422, 424})
# SAP Table 4a "electric underfloor heating" category. Stamped so an underfloor
# override never leaves the replaced system's category behind — property 711795
# (PRD #1435 WS2) kept a replaced heat network's category 6, which the heating
# generator's `is_heat_network_main` gate read as community heating: no heating
# upgrade was offered at all and the baseline mis-rated 51 vs 67 (ADR-0050).
_UNDERFLOOR_CATEGORY = 8
# SAP Table 4e Group 7 underfloor control: 2701 ("no time or thermostatic
# control of room temperature", +0.3 C) is the conservative default when the
# landlord named only the system — the largest mean-internal-temperature
# adjustment in the group, so an unobserved control is never over-credited
# (the underfloor mirror of storage-manual 2401 and room-heater 2601, ADR-0050).
_UNDERFLOOR_CONTROL = 2701
# Heat pumps (SAP Table 4a 211-224 wet, 521-527 warm-air) are category 4 and
# unambiguously electric (natural fuel 29). Modellable on the default code's SPF
# without a PCDB index (ADR-0041).
_HEAT_PUMP_CATEGORY = 4
_HEAT_PUMP_CODES = frozenset(range(211, 225)) | frozenset(range(521, 528))
# SAP Table 4e Group 2 heat-pump control: 2201 ("no time or thermostatic
# control of room temperature", +0.3 C) is the conservative default when the
# landlord named only the system — the largest Group 2 adjustment, so an
# unobserved control is never over-credited. Deliberately NOT the ASHP
# Measure's 2210 zone control: a measure designs a modern end-state
# (ADR-0024); an override describes an existing pump (ADR-0053). Scoped to the
# WET heat pumps (211-224) — warm-air pumps (521-527) take Table 4e Group 5
# and stay unmapped (no archetype maps to them; the incomplete-companion raise
# guards that a future one cannot land without choosing its Group 5 control).
_WET_HEAT_PUMP_CODES = frozenset(range(211, 225))
_CONSERVATIVE_HEAT_PUMP_CONTROL = 2201
# Community / heat-network heating (SAP Table 4a 301-304) is category 6; the
# calculator's `_is_heat_network` keys off code OR category 6. The boiler-driven
# schemes (301/302/303) are dominantly mains gas (community) — RdSAP main_fuel 20
# — per real community certs; the heat-pump scheme (304) is electricity
# (community), added with its archetype (ADR-0041).
_HEAT_NETWORK_CATEGORY = 6
_HEAT_NETWORK_CODES = frozenset({301, 302, 303, 304})
_COMMUNITY_BOILER_CODES = frozenset({301, 302, 303})
# SAP Table 4e Group 3 heat-network control: 2313 ("flat rate charging, room
# thermostat and TRVs") is the default when the landlord named only the scheme
# — a per-axis choice (ADR-0053, decided with Khalim). KIT axis: community
# schemes skew modern and the portfolio's lodged community certs are
# overwhelmingly thermostatted (modal 2306; none lodge 2301), so a room
# thermostat + TRVs is the evidence-backed assumption (temperature adjustment
# 0.0). CHARGING axis: flat rate (Table 4c(3) space 1.05 / DHW 1.05) — the
# billing arrangement is scheme paperwork an override cannot see, so the
# usage-linked credit (1.0/1.0) is never assumed. An assessor-OBSERVED Group 3
# control on the cert is kept in preference to this default (the overlay sets
# `keep_existing_heat_network_control`; the applicator honours it).
_DEFAULT_HEAT_NETWORK_CONTROL = 2313
_COMMUNITY_GAS_FUEL = 20
# SAP Table 4c full boiler-control code: programmer + room thermostat + TRVs. The
# landlord names the boiler, not its controls — but a gas boiler installed under
# modern Building Regs must carry compliant controls, and this overlay already
# assumes the modern condensing efficiency for the boiler (A-G deferred), so
# assuming modern controls is the *coherent* default (decided with Khalim). It
# also overwrites any stale storage charge control (2401) the dwelling carried
# before the switch. (Conservative-vs-modern trade-off documented in ADR-0035.)
_FULL_BOILER_CONTROL = 2106
# Gas-boiler archetypes (Table 4b 102 regular / 104 combi / 120 CPSU) and the
# subset that heats hot water instantaneously (a combi has no cylinder; a regular
# boiler and a CPSU heat a cylinder). A gas boiler implies a mains-gas connection
# + a gas main fuel + a gas-boiler heating category (Table 4a cat 2) + a fanned
# room-sealed flue — all set so a heating-system-only override is self-coherent.
_GAS_BOILER_CODES = frozenset({102, 104, 120})
_COMBI_CODES = frozenset({104})
_GAS_BOILER_CATEGORY = 2
_MAINS_GAS_FUEL = 26
# SAP Table 4a "from the main system" water-heating code — a gas boiler heats hot
# water from itself, so the override routes water heating to the main system on
# mains gas (clearing a storage dwelling's old electric-immersion arrangement).
_FROM_MAIN_WATER_HEATING_CODE = 901
# 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
# direct-acting electric, 691 panel/convector/radiant electric room heaters
# (Table 4a — direct-acting, so a single-rate meter, NOT off-peak storage).
# Companion fields (meter / control / fuel / hot water) are NOT listed here —
# they are derived from the code below, so a new archetype is just a code
# (ADR-0035 drag-along).
_MAIN_HEATING_CODES: dict[str, int] = {
"Gas boiler, combi": 104,
"Gas boiler, regular": 102,
"Gas CPSU": 120,
"Electric storage heaters, old": 401,
"Electric storage heaters, slimline": 402,
"Electric storage heaters, convector": 403,
"Electric storage heaters, fan": 404,
# High heat retention storage heaters — SAP Table 4a 409. Its intrinsic HHR
# charge control (2404) and electricity fuel drag from the code below, not the
# generic manual-storage default (ADR-0044).
"Electric storage heaters, high heat retention": 409,
"Direct-acting electric": 191,
# A-rated electric boiler off the Gas CPSU dumping ground — SAP Table 4a 191
# (direct-acting electric boiler). Unambiguously electric, so it drags
# electricity below (ADR-0045).
"Electric boiler": 191,
# Electric CPSU — SAP Table 4a 192. Electric (drags 29), on an off-peak Dual
# meter (already in _ASSUMED_DUAL_METER_CODES; §12 Rule 1 10-hour) — ADR-0045.
"Electric CPSU": 192,
# Electric underfloor — dedicated SAP Table 4a codes, all electric (drag 29).
# 421 concrete slab / 422 integrated are off-peak (Dual, §12 Rule 2); 424
# screed is tariff-ambiguous ("standard or off peak") so it DEFERS the meter to
# the cert (ADR-0046).
"Electric underfloor, in concrete slab (off-peak)": 421,
"Electric underfloor, integrated storage and direct-acting": 422,
"Electric underfloor, in screed above insulation": 424,
"Electric room heaters": 691,
"Solid fuel room heater, closed": 633,
# Solid-fuel room heaters off the Gas CPSU dumping ground (ADR-0045). SAP
# Table 4a: 631 open fire, 632 open fire + back boiler, 634 closed + boiler.
# Companions (house-coal fuel 33, room-heater control 2601, category 10) drag
# from the code — 631/632/634 are already in the 631-636 solid-fuel set.
"Solid fuel room heater, open fire": 631,
"Solid fuel room heater, open fire with back boiler": 632,
"Solid fuel room heater, closed with boiler": 634,
# Default air-source heat pump — SAP Table 4a 211 ("flow temp in other
# cases", default SPF 2.30). Modellable without a PCDB index; an actual
# product index would refine it (ADR-0041).
"Air source heat pump": 211,
# Boiler-driven community / heat-network heating — SAP Table 4a 301. The
# calculator derives the heat-source efficiency (80%) + DLF (1.50 default)
# from the code (cert_to_inputs). A generic "Community heating" with no named
# source stays unmapped (-> None) — the source can't be assumed (ADR-0041).
"Community heating, boilers": 301,
# Community CHP + boilers — SAP Table 4a 302 (heat network, category 6).
"Community heating, CHP and boilers": 302,
# Modern standalone oil room heater — SAP Table 4a 623 ("Oil room heater,
# 2000 or later", no boiler). Natural fuel heating oil (ADR-0041).
"Oil room heater, 2000 or later": 623,
# Gas room heaters — each catalogue variant to its own SAP Table 4a code
# (wide efficiency spread, so no single representative). Natural fuel mains
# gas; an LPG dwelling is refined by a `main_fuel` override (ADR-0041).
"Gas room heater, condensing fire": 611,
"Gas room heater, decorative fuel-effect": 612,
"Gas room heater, flush live-effect": 605,
"Gas room heater, open flue 1980 or later": 603,
"Gas room heater, open flue pre-1980": 601,
}
def _meter_for(code: int) -> Optional[str]:
"""The coherent meter a heating code implies: an off-peak ("Dual") meter for
the §12 off-peak systems and all-electric room-heater dwellings
(`_ASSUMED_DUAL_METER_CODES`), an explicit single-rate ("Single") meter for
every other system — set explicitly so it never bleeds.
Exception: a **meter-agnostic** code (`_METER_AGNOSTIC_CODES` — screed
underfloor 424, "standard or off peak") returns ``None`` so the overlay leaves
`meter_type` unset and the cert's lodged tariff stands. The archetype genuinely
cannot know the tariff, and unlike a switched-off storage system it is not a
re-metering, so deferring is faithful, not a bleed (ADR-0046)."""
if code in _METER_AGNOSTIC_CODES:
return None
return _OFF_PEAK_METER if code in _ASSUMED_DUAL_METER_CODES else _SINGLE_RATE_METER
def _control_for(code: int) -> Optional[int]:
"""The control to assume when the landlord named only the system: a
conservative manual charge control for storage heaters, full modern controls
for a gas boiler, the conservative in-group default for the other mapped
families (room heaters, underfloor, electric boilers). Overwrites a stale
control inherited from the system being replaced."""
if code == _HHR_STORAGE_CODE:
return _HHR_CHARGE_CONTROL
if code in _STORAGE_HEATER_CODES:
return _MANUAL_CHARGE_CONTROL
if code in _GAS_BOILER_CODES:
return _FULL_BOILER_CONTROL
if code in _CATEGORY_10_ROOM_HEATER_CODES or code in _ELECTRIC_ROOM_HEATER_CODES:
return _ROOM_HEATER_CONTROL
if code in _ELECTRIC_UNDERFLOOR_CODES:
return _UNDERFLOOR_CONTROL
if code in _ELECTRIC_BOILER_CODES:
return _CONSERVATIVE_BOILER_CONTROL
if code in _WET_HEAT_PUMP_CODES:
return _CONSERVATIVE_HEAT_PUMP_CONTROL
if code in _HEAT_NETWORK_CODES:
return _DEFAULT_HEAT_NETWORK_CONTROL
return None
def _category_for(code: int) -> Optional[int]:
"""The SAP Table 4a heating category a code implies, where the archetype
fixes it. Set so a system-only override reads as the new system's category,
not whatever category the replaced system carried — room heaters are 10,
electric storage heaters (incl. HHR 409) are 7, heat pumps 4, heat networks
6. Setting storage → 7 keeps the overlaid cert consistent (code 401-409 ↔
category 7); without it a storage override left a stale category behind,
which the SAP 10.2 Table 12a resolver (keyed on category) could misread as a
non-storage system and price the off-peak storage heating at the peak rate."""
if code in _CATEGORY_10_ROOM_HEATER_CODES or code in _ELECTRIC_ROOM_HEATER_CODES:
return _ROOM_HEATER_CATEGORY
if code in _STORAGE_HEATER_CODES:
return _STORAGE_HEATER_CATEGORY
if code in _ELECTRIC_UNDERFLOOR_CODES:
return _UNDERFLOOR_CATEGORY
if code in _ELECTRIC_BOILER_CODES:
return _ELECTRIC_BOILER_CATEGORY
if code in _HEAT_PUMP_CODES:
return _HEAT_PUMP_CATEGORY
if code in _HEAT_NETWORK_CODES:
return _HEAT_NETWORK_CATEGORY
return None
def _natural_fuel_for(code: int) -> Optional[int]:
"""The fuel an archetype unambiguously implies (a coherent default), or None
where the fuel is ambiguous and must come from the `main_fuel` override. A
present `main_fuel` override wins by last-wins composition."""
if (
code in _ELECTRIC_ROOM_HEATER_CODES
or code in _HEAT_PUMP_CODES
or code == _HHR_STORAGE_CODE
or code in _ELECTRIC_BOILER_CODES
or code in _ELECTRIC_UNDERFLOOR_CODES
):
return _ELECTRICITY_FUEL
if code in _SOLID_FUEL_ROOM_HEATER_CODES:
return _HOUSE_COAL_FUEL
if code in _OIL_ROOM_HEATER_CODES:
return _OIL_FUEL
if code in _GAS_ROOM_HEATER_CODES:
return _MAINS_GAS_FUEL
if code in _COMMUNITY_BOILER_CODES:
return _COMMUNITY_GAS_FUEL
return None
def _gas_boiler_overlay(code: int) -> 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)."""
return HeatingOverlay(
sap_main_heating_code=code,
main_heating_category=_GAS_BOILER_CATEGORY,
main_fuel_type=_MAINS_GAS_FUEL,
gas_connection_available=True,
main_heating_control=_FULL_BOILER_CONTROL,
fan_flue_present=True,
meter_type=_SINGLE_RATE_METER,
water_heating_code=_FROM_MAIN_WATER_HEATING_CODE,
water_heating_fuel=_MAINS_GAS_FUEL,
has_hot_water_cylinder=code not in _COMBI_CODES,
)
def natural_fuel_for(main_heating_value: str) -> Optional[int]:
"""The RdSAP `main_fuel` code a heating archetype unambiguously implies — its
natural fuel (ADR-0041) — or None for an unmapped archetype. Exposed so a
plausibility check can compare it against a landlord `main_fuel` override."""
code = _MAIN_HEATING_CODES.get(main_heating_value)
if code is None:
return None
return _natural_fuel_for(code)
def main_heating_overlay_for(
main_heating_value: str, building_part: int
) -> 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))
category = _category_for(code)
control = _control_for(code)
if category is None or control is None:
# A system-replacing override must stamp a complete, coherent companion
# set; an unmapped category or control leaves the field unset, so the
# effective cert inherits the REPLACED system's value — the silent
# mis-rating class ADR-0048 exists to stop. Every mapped archetype now
# carries confident defaults (ADR-0050/0052/0053), so an incomplete set
# can only mean a new archetype landed without choosing its companions:
# fail loudly at overlay time rather than ship an incoherent cert
# (ADR-0048's intended end-state, #1444 Part C).
raise ValueError(
f"Landlord main_heating_system {main_heating_value!r} (SAP code "
f"{code}) resolves an incomplete companion set (category="
f"{category}, control={control}); add the archetype's SAP Table 4a "
f"category and conservative Table 4e control before mapping it"
)
return EpcSimulation(
heating=HeatingOverlay(
sap_main_heating_code=code,
main_heating_category=category,
main_fuel_type=_natural_fuel_for(code),
meter_type=_meter_for(code),
main_heating_control=control,
# A landlord override describes the existing dwelling, so its assumed
# off-peak meter must not downgrade a more-off-peak cert meter
# (e.g. a 24-hour all-low tariff). Measures, which re-meter for real,
# build their HeatingOverlay directly and leave this False.
keep_existing_off_peak_meter=True,
# Likewise a heat-network override's control default (2313) must
# not overwrite an assessor-OBSERVED Group 3 control on a community
# cert being confirmed — only a stale cross-family control is
# replaced (ADR-0053).
keep_existing_heat_network_control=code in _HEAT_NETWORK_CODES,
)
)