From 63dd69ff8b25d6b7187a48b499525b723b5c41ea Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 9 Jun 2026 16:28:00 +0000 Subject: [PATCH] feat(modelling): gas combi boiler upgrade + controls-when-inadequate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend the gas-boiler-upgrade Option to combi (no-cylinder) dwellings and add the controls upgrade shared by both boiler shapes. A dwelling has a cylinder or it does not, so the one `gas_boiler_upgrade` Option is shaped per dwelling: - no cylinder -> a gas condensing combi (Table 4b code 104), no cylinder fields touched; - a cylinder -> a regular boiler (code 102) heating it, with the conditional cylinder jacket/thermostat (slice 1). Controls: bring an inadequate boiler control up to full programmer + room thermostat + TRVs (SAP 10.2 Table 4e Group 1 code 2106). "Inadequate" = the Group-1 codes with NO room thermostat (2101, 2102, 2107, 2108, 2109, 2111) — these lack boiler interlock (Table 4c(2) / footnote c) p.171), so adding a room thermostat genuinely improves SAP. Room-thermostatted (2103/2104/2105/2106/2113) or better zone controls (2110/2112) are left unchanged — never downgraded, so no phantom uplift. The with-cylinder cert (control 2106) is therefore untouched and its pin still holds at delta 0. Validated by the combi before/after re-lodgement (cert 001431, gas boiler upgrade - no cylinder): control 2111 "TRVs and bypass" -> 2106, fan flue False->True, SAP code 112 -> 104. Cascade-pinned delta 0 (SAP/CO2/PE). Removed the slice-1 placeholder test asserting no boiler Option fires without a cylinder (the combi Option now correctly fires there). Co-Authored-By: Claude Opus 4.8 --- .../generators/heating_recommendation.py | 95 ++++++++++++++---- .../boiler_combi_gas_001431_after.pdf | Bin 0 -> 76968 bytes .../boiler_combi_gas_001431_before.pdf | Bin 0 -> 78686 bytes .../modelling/test_elmhurst_cascade_pins.py | 24 +++++ .../modelling/test_heating_recommendation.py | 65 +++++++++--- 5 files changed, 149 insertions(+), 35 deletions(-) create mode 100644 tests/domain/modelling/fixtures/boiler_combi_gas_001431_after.pdf create mode 100644 tests/domain/modelling/fixtures/boiler_combi_gas_001431_before.pdf diff --git a/domain/modelling/generators/heating_recommendation.py b/domain/modelling/generators/heating_recommendation.py index 88fea108..fcbe0e0c 100644 --- a/domain/modelling/generators/heating_recommendation.py +++ b/domain/modelling/generators/heating_recommendation.py @@ -126,11 +126,26 @@ _ASHP_OVERLAY = HeatingOverlay( _MAINS_GAS_FUEL = 26 # Table 4a heat-emitter code for radiators (the wet-distribution end-state). _RADIATOR_EMITTER = 1 -# Table 4b SAP main-heating code for a regular gas boiler heating a cylinder. +# Table 4b SAP main-heating codes for the new gas condensing boiler: code 102 +# for a regular boiler heating a cylinder, code 104 for a combi (no cylinder). _REGULAR_GAS_BOILER_SAP_CODE = 102 +_COMBI_GAS_BOILER_SAP_CODE = 104 # Water-heating code 901 — hot water from the main heating system. _WATER_FROM_MAIN_SYSTEM_CODE = 901 +# Controls upgrade (SAP 10.2 Table 4e Group 1, PDF p.172): bring an inadequate +# boiler control up to full programmer + room thermostat + TRVs (code 2106). +# "Inadequate" = the Group-1 codes whose description carries NO room thermostat +# (2101 no control, 2102 programmer-only, 2107/2108/2109 programmer+TRVs without +# a room thermostat, 2111 TRVs and bypass) — these lack boiler interlock (Table +# 4c(2) / footnote c)), so adding a room thermostat is a genuine improvement. +# Controls with a room thermostat (2103/2104/2105/2106/2113) or better time-and- +# temperature zone control (2110/2112) are left unchanged — never downgraded. +_FULL_BOILER_CONTROL = 2106 +_INADEQUATE_BOILER_CONTROL_CODES: frozenset[int] = frozenset( + {2101, 2102, 2107, 2108, 2109, 2111} +) + # Wet-boiler SAP main_heating_code ranges (SAP 10.2 Table 4a + 4b): gas/oil # boilers 101-141, solid-fuel boilers 151-161, electric boilers 191-196 (held # locally so the generator does not depend on the calculator's internals, @@ -241,7 +256,7 @@ def recommend_heating( if ashp_option is not None: options.append(ashp_option) - boiler_option = _boiler_upgrade_with_cylinder_option(epc, products) + boiler_option = _boiler_upgrade_option(epc, products) if boiler_option is not None: options.append(boiler_option) @@ -250,26 +265,34 @@ def recommend_heating( return Recommendation(surface=_HEATING_SURFACE, options=tuple(options)) -def _boiler_upgrade_with_cylinder_option( +def _boiler_upgrade_option( epc: EpcPropertyData, products: ProductRepository ) -> Optional[MeasureOption]: - """The gas-condensing-boiler-with-cylinder bundle: a new regular gas boiler - (Table 4b code 102, fanned flue) for a dwelling whose existing wet boiler - heats a hot-water cylinder, plus the conditional cylinder fixes (a jacket - when under-insulated, a thermostat when absent). Offered only where a - mains-gas connection makes the gas end-state installable (ADR-0024 revised).""" + """The gas-condensing-boiler upgrade for a dwelling with an existing wet + boiler: a combi (Table 4b code 104) where there is no cylinder, or a regular + boiler (code 102) heating the existing cylinder where there is one. Both + upgrade inadequate controls and the cylinder variant adds the conditional + cylinder fixes (a jacket when under-insulated, a thermostat when absent). One + Option per dwelling — a dwelling has a cylinder or it does not — offered only + where a mains-gas connection makes the gas end-state installable (ADR-0024 + revised).""" if not _boiler_upgrade_eligible(epc): return None - if not epc.has_hot_water_cylinder: - return None + has_cylinder: bool = epc.has_hot_water_cylinder + overlay: HeatingOverlay = ( + _boiler_cylinder_overlay(epc) if has_cylinder else _boiler_combi_overlay(epc) + ) + description: str = ( + "Replace the boiler with a gas condensing boiler and insulate and " + "thermostat the hot-water cylinder" + if has_cylinder + else "Replace the boiler with a gas condensing combi boiler" + ) product = products.get(_GAS_BOILER_UPGRADE_MEASURE_TYPE) return MeasureOption( measure_type=_GAS_BOILER_UPGRADE_MEASURE_TYPE, - description=( - "Replace the boiler with a gas condensing boiler and insulate and " - "thermostat the hot-water cylinder" - ), - overlay=EpcSimulation(heating=_boiler_cylinder_overlay(epc)), + description=description, + overlay=EpcSimulation(heating=overlay), cost=Cost( total=product.unit_cost_per_m2, contingency_rate=product.contingency_rate ), @@ -294,13 +317,32 @@ def _boiler_upgrade_eligible(epc: EpcPropertyData) -> bool: return epc.sap_energy_source.mains_gas +def _boiler_combi_overlay(epc: EpcPropertyData) -> HeatingOverlay: + """Build the per-dwelling combi end-state: a gas condensing combi (Table 4b + code 104, fanned flue) on radiators with hot water from the boiler, plus a + controls upgrade when the existing controls are inadequate. No cylinder, so + no cylinder fields are touched.""" + main: MainHeatingDetail = epc.sap_heating.main_heating_details[0] + return HeatingOverlay( + main_fuel_type=_MAINS_GAS_FUEL, + heat_emitter_type=_RADIATOR_EMITTER, + sap_main_heating_code=_COMBI_GAS_BOILER_SAP_CODE, + fan_flue_present=True, + main_heating_control=_upgraded_boiler_control(main), + water_heating_code=_WATER_FROM_MAIN_SYSTEM_CODE, + water_heating_fuel=_MAINS_GAS_FUEL, + ) + + def _boiler_cylinder_overlay(epc: EpcPropertyData) -> HeatingOverlay: """Build the per-dwelling boiler-with-cylinder end-state: a regular gas - condensing boiler on radiators, hot water from the main system, and the - conditional cylinder fixes — an 80 mm jacket only when the cylinder is - under-insulated, a thermostat only when one is absent. The existing cylinder - size, heating controls, and meter are left unchanged.""" + condensing boiler on radiators, hot water from the main system, a controls + upgrade when the existing controls are inadequate, and the conditional + cylinder fixes — an 80 mm jacket only when the cylinder is under-insulated, a + thermostat only when one is absent. The existing cylinder size and meter are + left unchanged.""" sap_heating = epc.sap_heating + main: MainHeatingDetail = sap_heating.main_heating_details[0] jacket_type: Optional[int] = None jacket_thickness_mm: Optional[int] = None if _cylinder_under_insulated(sap_heating.cylinder_insulation_thickness_mm): @@ -314,6 +356,7 @@ def _boiler_cylinder_overlay(epc: EpcPropertyData) -> HeatingOverlay: heat_emitter_type=_RADIATOR_EMITTER, sap_main_heating_code=_REGULAR_GAS_BOILER_SAP_CODE, fan_flue_present=True, + main_heating_control=_upgraded_boiler_control(main), water_heating_code=_WATER_FROM_MAIN_SYSTEM_CODE, water_heating_fuel=_MAINS_GAS_FUEL, cylinder_insulation_type=jacket_type, @@ -329,6 +372,20 @@ def _cylinder_under_insulated(thickness_mm: Optional[int]) -> bool: return thickness_mm is None or thickness_mm < _MIN_CYLINDER_INSULATION_MM +def _upgraded_boiler_control(main: MainHeatingDetail) -> Optional[int]: + """The full-controls code (2106) when the existing boiler control is + inadequate (lacks a room thermostat — SAP 10.2 Table 4e Group 1), else + ``None`` to leave a room-thermostatted or better control unchanged. So the + overlay only ever moves controls where it genuinely improves them.""" + control = main.main_heating_control + code: Optional[int] = control if isinstance(control, int) else None + if code is None and isinstance(control, str) and control.isdigit(): + code = int(control) + if code in _INADEQUATE_BOILER_CONTROL_CODES: + return _FULL_BOILER_CONTROL + return None + + def _ashp_option( epc: EpcPropertyData, products: ProductRepository, diff --git a/tests/domain/modelling/fixtures/boiler_combi_gas_001431_after.pdf b/tests/domain/modelling/fixtures/boiler_combi_gas_001431_after.pdf new file mode 100644 index 0000000000000000000000000000000000000000..784ab1ca04b487ee718cae48e22cba4e885a5fe7 GIT binary patch literal 76968 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 L6g)h4)#U#N&|^IA literal 0 HcmV?d00001 diff --git a/tests/domain/modelling/test_elmhurst_cascade_pins.py b/tests/domain/modelling/test_elmhurst_cascade_pins.py index 63ffb141..9d8ce4ff 100644 --- a/tests/domain/modelling/test_elmhurst_cascade_pins.py +++ b/tests/domain/modelling/test_elmhurst_cascade_pins.py @@ -785,6 +785,30 @@ def test_boiler_with_cylinder_overlay_reproduces_the_relodged_after() -> None: _assert_overlay_reproduces_after(before, after, option.overlay) +def test_boiler_combi_overlay_reproduces_the_relodged_after() -> None: + # Arrange — a mains-gas combi (SAP code 112, no cylinder) with inadequate + # controls (2111 "TRVs and bypass" — no room thermostat, so no boiler + # interlock) re-lodged as a new gas condensing combi (code 104, fanned flue) + # with full programmer + room thermostat + TRV controls (2106). No cylinder, + # so no cylinder components. Validates the combi end-state + the controls- + # when-inadequate upgrade at delta 0. (Same Summary-path roof gap as the + # with-cylinder pin — it cancels across before/after.) + before: EpcPropertyData = parse_recommendation_summary( + "boiler_combi_gas_001431_before.pdf" + ) + after: EpcPropertyData = parse_recommendation_summary( + "boiler_combi_gas_001431_after.pdf" + ) + recommendation: Recommendation | None = recommend_heating(before, _AnyProduct()) + assert recommendation is not None + option = next( + o for o in recommendation.options if o.measure_type == "gas_boiler_upgrade" + ) + + # Act / Assert + _assert_overlay_reproduces_after(before, after, option.overlay) + + # --- Solar PV cascade pins (ADR-0026) ------------------------------------- # # The solar before/after Summaries lodge *synthetic* PV arrays (each 1.00 kWp, diff --git a/tests/domain/modelling/test_heating_recommendation.py b/tests/domain/modelling/test_heating_recommendation.py index ad6392d5..9b011437 100644 --- a/tests/domain/modelling/test_heating_recommendation.py +++ b/tests/domain/modelling/test_heating_recommendation.py @@ -328,22 +328,6 @@ def test_boiler_upgrade_skips_thermostat_when_already_present() -> None: assert overlay.cylinder_insulation_type == 2 -def test_no_cylinder_dwelling_yields_no_boiler_with_cylinder_bundle() -> None: - # Arrange — a wet gas boiler with no hot-water cylinder (a combi); the with- - # cylinder option does not apply (the combi option lands in a later slice). - baseline: EpcPropertyData = _gas_boiler_with_cylinder_baseline() - baseline.has_hot_water_cylinder = False - - # Act - recommendation: Recommendation | None = recommend_heating(baseline, _StubProducts()) - - # Assert - if recommendation is not None: - assert "gas_boiler_upgrade" not in { - o.measure_type for o in recommendation.options - } - - def test_electric_boiler_dwelling_yields_no_gas_boiler_upgrade() -> None: # Arrange — an electric boiler (Table 4a code 191) is left alone: # electrification, not a gas swap, is its upgrade path. @@ -376,3 +360,52 @@ def test_off_gas_boiler_yields_no_gas_boiler_upgrade() -> None: assert "gas_boiler_upgrade" not in { o.measure_type for o in recommendation.options } + + +def _gas_combi_baseline() -> EpcPropertyData: + """A mains-gas combi (Table 4b code 112, no cylinder) with inadequate + controls (2111 "TRVs and bypass" — no room thermostat).""" + return parse_recommendation_summary("boiler_combi_gas_001431_before.pdf") + + +def test_gas_combi_dwelling_yields_a_combi_boiler_upgrade_bundle() -> None: + # Arrange — a mains-gas combi with no cylinder and inadequate controls: + # the upgrade replaces it with a condensing combi (code 104) and upgrades + # the controls to 2106, touching no cylinder fields. + baseline: EpcPropertyData = _gas_combi_baseline() + + # Act + recommendation: Recommendation | None = recommend_heating(baseline, _StubProducts()) + + # Assert + assert recommendation is not None + options = {o.measure_type.value: o for o in recommendation.options} + assert "gas_boiler_upgrade" in options + assert options["gas_boiler_upgrade"].overlay.heating == HeatingOverlay( + main_fuel_type=26, + heat_emitter_type=1, + sap_main_heating_code=104, + fan_flue_present=True, + main_heating_control=2106, + water_heating_code=901, + water_heating_fuel=26, + ) + + +def test_boiler_upgrade_leaves_adequate_controls_unchanged() -> None: + # Arrange — the same combi but with already-adequate controls (2113, room + # thermostat and TRVs): the upgrade must not move the controls (and must + # never downgrade a better control). + baseline: EpcPropertyData = _gas_combi_baseline() + baseline.sap_heating.main_heating_details[0].main_heating_control = 2113 + + # Act + recommendation: Recommendation | None = recommend_heating(baseline, _StubProducts()) + + # Assert + assert recommendation is not None + overlay = next( + o for o in recommendation.options if o.measure_type == "gas_boiler_upgrade" + ).overlay.heating + assert overlay is not None + assert overlay.main_heating_control is None