From 31c74ab5009fa466b33ded186f3ba2a0988390f3 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 9 Jun 2026 16:16:46 +0000 Subject: [PATCH] feat(modelling): gas-boiler-upgrade-with-cylinder option in the heating rec Add the first boiler-upgrade option to the single "Heating & Hot Water" Recommendation (ADR-0024 expansion): a dwelling whose existing wet boiler heats a hot-water cylinder is offered a new gas condensing boiler, with the cylinder jacketed when under-insulated and given a thermostat when absent. One competing Option (the Optimiser picks <=1), folded into one composite Plan line. The end-state is read from the Elmhurst before/after re-lodgements (cert 001431, gas boiler upgrade - with cylinder), which REVISE ADR-0024: - Target is always a gas condensing boiler, not fuel-preserving: every after lodges fuel 26. Gas->gas always; a non-gas wet boiler ->gas only with a mains-gas connection; electric boilers are left alone (electrification is the upgrade path). Eligibility = wet-boiler SAP code (Table 4a/4b 101-141 / 151-161 / 191-196) + not an electric boiler + mains gas present. - End-state is a Table 4b SAP code, not a PCDB index: code 102 (regular boiler + cylinder). The calculator derives the condensing seasonal efficiency from the code, so no efficiency input exists or is needed. - A modern condensing boiler has a fanned flue: the after flips `fan_flue_present` False->True on every cert (SAP 10.2 Table 4f flue-fan + the Table 4b condensing-efficiency basis). Added as a new HeatingOverlay field, routed to main_heating_details[0]. - Cylinder thermostat is always added when absent (user-locked); the jacket is the 80 mm `cylinder_insulation_type=2` end-state, applied only when the cylinder is below 80 mm (never downgrading a better one). Both are conditional per-dwelling components, not a frozen overlay. Cascade-pinned delta-0 (SAP/CO2/PE) against the relodged after via `_assert_overlay_reproduces_after`. NB the absolute SAP on this dwelling is subject to a separate Summary-path mapper roof-fidelity gap (we read the roof better-insulated than Elmhurst, scoring ~75 vs the printed 56); the gap is identical on before+after (the boiler measure never touches the roof) so it cancels and the pin still proves the exact heating field-delta. Tracked on the calculator branch. Wires the new `gas_boiler_upgrade` MeasureType through contingencies (0.26), the offline sample catalogue, the catalogue-coverage list, and the ARA first-run integration seed (the option fires on any mains-gas boiler+cylinder dwelling). Co-Authored-By: Claude Opus 4.8 --- domain/modelling/contingencies.py | 1 + .../generators/heating_recommendation.py | 123 +++++++++++++++++ domain/modelling/measure_type.py | 1 + .../modelling/scoring/overlay_applicator.py | 1 + domain/modelling/simulation.py | 4 + harness/sample_catalogue.json | 1 + .../fixtures/boiler_cyl_gas_001431_after.pdf | Bin 0 -> 77066 bytes .../fixtures/boiler_cyl_gas_001431_before.pdf | Bin 0 -> 78820 bytes .../modelling/test_elmhurst_cascade_pins.py | 30 +++++ .../modelling/test_heating_recommendation.py | 126 ++++++++++++++++++ tests/domain/modelling/test_measure_type.py | 1 + tests/harness/test_console.py | 1 + ...test_ara_first_run_pipeline_integration.py | 8 ++ 13 files changed, 297 insertions(+) create mode 100644 tests/domain/modelling/fixtures/boiler_cyl_gas_001431_after.pdf create mode 100644 tests/domain/modelling/fixtures/boiler_cyl_gas_001431_before.pdf diff --git a/domain/modelling/contingencies.py b/domain/modelling/contingencies.py index b4308ed6..ea2f3c4c 100644 --- a/domain/modelling/contingencies.py +++ b/domain/modelling/contingencies.py @@ -20,6 +20,7 @@ _CONTINGENCY_RATES: dict[str, float] = { "low_energy_lighting": 0.26, "high_heat_retention_storage_heaters": 0.10, "air_source_heat_pump": 0.25, + "gas_boiler_upgrade": 0.26, "solar_pv": 0.15, } diff --git a/domain/modelling/generators/heating_recommendation.py b/domain/modelling/generators/heating_recommendation.py index 1c851541..88fea108 100644 --- a/domain/modelling/generators/heating_recommendation.py +++ b/domain/modelling/generators/heating_recommendation.py @@ -27,6 +27,7 @@ _HEATING_SURFACE = "Heating & Hot Water" _HHR_STORAGE_MEASURE_TYPE = MeasureType.HIGH_HEAT_RETENTION_STORAGE_HEATERS _ASHP_MEASURE_TYPE = MeasureType.AIR_SOURCE_HEAT_PUMP +_GAS_BOILER_UPGRADE_MEASURE_TYPE = MeasureType.GAS_BOILER_UPGRADE # Electricity main-fuel code (Elmhurst → SAP10 Table 12). _ELECTRICITY_FUEL = 30 @@ -111,6 +112,45 @@ _ASHP_OVERLAY = HeatingOverlay( ) +# --- Gas boiler upgrade (Heating/HW expansion): replace an existing wet boiler +# with a modern gas condensing boiler. Validated against Elmhurst before/after +# re-lodgements (cert 001431): the upgrade always targets mains gas — gas->gas +# directly, and a non-gas wet boiler (oil/LPG/solid) ->gas ONLY where a mains-gas +# connection is present (electric boilers are left alone; electrification is the +# national target). The end-state is a Table 4b SAP code (not a PCDB index): code +# 102 for a regular boiler heating a hot-water cylinder, code 104 for a combi +# (no cylinder, a later slice). The calculator derives the condensing-boiler +# seasonal efficiency from the code, so no efficiency input is needed. --- + +# Mains-gas main/water fuel code (Elmhurst -> SAP10 Table 12). +_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. +_REGULAR_GAS_BOILER_SAP_CODE = 102 +# Water-heating code 901 — hot water from the main heating system. +_WATER_FROM_MAIN_SYSTEM_CODE = 901 + +# 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, +# mirroring `domain/sap10_calculator/rdsap/cert_to_inputs.py`). Electric boilers +# are a wet system but are deliberately not upgraded to gas. +_WET_BOILER_SAP_CODE_RANGES: tuple[range, ...] = ( + range(101, 142), + range(151, 162), + range(191, 197), +) +_ELECTRIC_BOILER_SAP_CODE_RANGE = range(191, 197) + +# Cylinder jacket end-state (from the after-cert): an 80 mm jacket +# (`cylinder_insulation_type=2`). The jacket is added only when the existing +# cylinder is below this thickness — bringing every cylinder up to 80 mm and +# never downgrading a better-insulated one. +_CYLINDER_JACKET_INSULATION_TYPE = 2 +_MIN_CYLINDER_INSULATION_MM = 80 + + # --- ASHP cost interpretation (ADR-0025): read the dwelling into the typed # inputs the catalogue math needs. The modelling-layer half of the split; the # pricing itself lives on `Products`. --- @@ -201,11 +241,94 @@ def recommend_heating( if ashp_option is not None: options.append(ashp_option) + boiler_option = _boiler_upgrade_with_cylinder_option(epc, products) + if boiler_option is not None: + options.append(boiler_option) + if not options: return None return Recommendation(surface=_HEATING_SURFACE, options=tuple(options)) +def _boiler_upgrade_with_cylinder_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).""" + if not _boiler_upgrade_eligible(epc): + return None + if not epc.has_hot_water_cylinder: + return None + 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)), + cost=Cost( + total=product.unit_cost_per_m2, contingency_rate=product.contingency_rate + ), + material_id=product.id, + ) + + +def _boiler_upgrade_eligible(epc: EpcPropertyData) -> bool: + """Whether a dwelling's existing wet boiler can be upgraded to a gas + condensing boiler. The gas end-state is installable only with a mains-gas + connection, so gas dwellings always qualify and a non-gas wet boiler + (oil/LPG/solid) qualifies only where mains gas is present. Electric boilers + are left alone — electrification, not a gas swap, is their upgrade path.""" + main: MainHeatingDetail = epc.sap_heating.main_heating_details[0] + code: Optional[int] = main.sap_main_heating_code + if code is None: + return False + if not any(code in r for r in _WET_BOILER_SAP_CODE_RANGES): + return False + if code in _ELECTRIC_BOILER_SAP_CODE_RANGE: + return False + return epc.sap_energy_source.mains_gas + + +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.""" + sap_heating = epc.sap_heating + jacket_type: Optional[int] = None + jacket_thickness_mm: Optional[int] = None + if _cylinder_under_insulated(sap_heating.cylinder_insulation_thickness_mm): + jacket_type = _CYLINDER_JACKET_INSULATION_TYPE + jacket_thickness_mm = _MIN_CYLINDER_INSULATION_MM + thermostat: Optional[str] = ( + "Y" if sap_heating.cylinder_thermostat != "Y" else None + ) + return HeatingOverlay( + main_fuel_type=_MAINS_GAS_FUEL, + heat_emitter_type=_RADIATOR_EMITTER, + sap_main_heating_code=_REGULAR_GAS_BOILER_SAP_CODE, + fan_flue_present=True, + water_heating_code=_WATER_FROM_MAIN_SYSTEM_CODE, + water_heating_fuel=_MAINS_GAS_FUEL, + cylinder_insulation_type=jacket_type, + cylinder_insulation_thickness_mm=jacket_thickness_mm, + cylinder_thermostat=thermostat, + has_hot_water_cylinder=True, + ) + + +def _cylinder_under_insulated(thickness_mm: Optional[int]) -> bool: + """Whether a hot-water cylinder is below the 80 mm jacket end-state (an + un-jacketed cylinder lodges no thickness).""" + return thickness_mm is None or thickness_mm < _MIN_CYLINDER_INSULATION_MM + + def _ashp_option( epc: EpcPropertyData, products: ProductRepository, diff --git a/domain/modelling/measure_type.py b/domain/modelling/measure_type.py index d28bff39..b1c4bd9c 100644 --- a/domain/modelling/measure_type.py +++ b/domain/modelling/measure_type.py @@ -32,4 +32,5 @@ class MeasureType(StrEnum): MECHANICAL_VENTILATION = "mechanical_ventilation" HIGH_HEAT_RETENTION_STORAGE_HEATERS = "high_heat_retention_storage_heaters" AIR_SOURCE_HEAT_PUMP = "air_source_heat_pump" + GAS_BOILER_UPGRADE = "gas_boiler_upgrade" SOLAR_PV = "solar_pv" diff --git a/domain/modelling/scoring/overlay_applicator.py b/domain/modelling/scoring/overlay_applicator.py index 60caed17..2696d7ab 100644 --- a/domain/modelling/scoring/overlay_applicator.py +++ b/domain/modelling/scoring/overlay_applicator.py @@ -70,6 +70,7 @@ _MAIN_HEATING_FIELDS: tuple[str, ...] = ( "sap_main_heating_code", "main_heating_index_number", "main_heating_category", + "fan_flue_present", ) _SAP_HEATING_FIELDS: tuple[str, ...] = ( "water_heating_code", diff --git a/domain/modelling/simulation.py b/domain/modelling/simulation.py index 8fdb1205..40d6fc95 100644 --- a/domain/modelling/simulation.py +++ b/domain/modelling/simulation.py @@ -118,6 +118,10 @@ class HeatingOverlay: sap_main_heating_code: Optional[int] = None main_heating_index_number: Optional[int] = None main_heating_category: Optional[int] = 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). + fan_flue_present: Optional[bool] = None # sap_heating (top-level) water_heating_code: Optional[int] = None water_heating_fuel: Optional[int] = None diff --git a/harness/sample_catalogue.json b/harness/sample_catalogue.json index ef21d58f..e15e4537 100644 --- a/harness/sample_catalogue.json +++ b/harness/sample_catalogue.json @@ -13,5 +13,6 @@ "low_energy_lighting": { "unit_cost_per_m2": 8.0 }, "high_heat_retention_storage_heaters": { "unit_cost_per_m2": 3500.0 }, "air_source_heat_pump": { "unit_cost_per_m2": 12000.0 }, + "gas_boiler_upgrade": { "unit_cost_per_m2": 3000.0 }, "solar_pv": { "unit_cost_per_m2": 0.0 } } diff --git a/tests/domain/modelling/fixtures/boiler_cyl_gas_001431_after.pdf b/tests/domain/modelling/fixtures/boiler_cyl_gas_001431_after.pdf new file mode 100644 index 0000000000000000000000000000000000000000..e61f3466a02fe2e390858a38b4a9f1ee04e34c77 GIT binary patch literal 77066 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 Z9)O~hQjnjjjil@FNAVHUp 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 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 77981ab2..63ffb141 100644 --- a/tests/domain/modelling/test_elmhurst_cascade_pins.py +++ b/tests/domain/modelling/test_elmhurst_cascade_pins.py @@ -755,6 +755,36 @@ def test_gas_boiler_instant_hw_before_baselines() -> None: assert result.sap_score_continuous > 0.0 +def test_boiler_with_cylinder_overlay_reproduces_the_relodged_after() -> None: + # Arrange — a mains-gas wet boiler (SAP code 114) heating an uninsulated + # hot-water cylinder (no insulation, no thermostat) re-lodged as a new gas + # condensing boiler with a cylinder (SAP code 102, fanned flue), the cylinder + # jacketed (insulation type 2 / 80 mm) and given a thermostat. The boiler + # upgrade leaves the (already adequate) controls + cylinder size + meter + # unchanged. Validates the boiler-with-cylinder option end-state at delta 0. + # + # NB the absolute SAP on this dwelling is subject to a separate Summary-path + # mapper roof-fidelity gap (our calculator reads the roof better-insulated + # than Elmhurst, so it scores ~75 where Elmhurst prints 56); the gap is + # identical on before + after (the boiler measure never touches the roof), so + # it cancels and this pin still proves the overlay applies Elmhurst's exact + # heating field-delta. Tracked on the calculator branch, not here. + before: EpcPropertyData = parse_recommendation_summary( + "boiler_cyl_gas_001431_before.pdf" + ) + after: EpcPropertyData = parse_recommendation_summary( + "boiler_cyl_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 9a0cf285..ad6392d5 100644 --- a/tests/domain/modelling/test_heating_recommendation.py +++ b/tests/domain/modelling/test_heating_recommendation.py @@ -250,3 +250,129 @@ def test_existing_heat_pump_yields_no_ashp_bundle() -> None: assert "air_source_heat_pump" not in { o.measure_type for o in recommendation.options } + + +# --- Gas boiler upgrade (Heating/HW expansion) ---------------------------- + + +def _gas_boiler_with_cylinder_baseline() -> EpcPropertyData: + """A mains-gas wet boiler (Table 4b code 114) heating an uninsulated, un- + thermostatted hot-water cylinder — the boiler-with-cylinder dwelling.""" + return parse_recommendation_summary("boiler_cyl_gas_001431_before.pdf") + + +def test_gas_boiler_with_cylinder_dwelling_yields_a_boiler_upgrade_bundle() -> None: + # Arrange — a mains-gas wet boiler with an uninsulated, un-thermostatted + # cylinder: the upgrade fires both conditional cylinder fixes. + baseline: EpcPropertyData = _gas_boiler_with_cylinder_baseline() + + # Act + recommendation: Recommendation | None = recommend_heating(baseline, _StubProducts()) + + # Assert — the absolute boiler end-state (code 102, fanned flue) with the + # cylinder jacketed (type 2 / 80 mm) and thermostatted; controls, cylinder + # size, and meter are left unchanged. + 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=102, + fan_flue_present=True, + water_heating_code=901, + water_heating_fuel=26, + cylinder_insulation_type=2, + cylinder_insulation_thickness_mm=80, + cylinder_thermostat="Y", + has_hot_water_cylinder=True, + ) + + +def test_boiler_upgrade_skips_jacket_when_cylinder_already_insulated() -> None: + # Arrange — the same dwelling but with an already well-insulated cylinder + # (100 mm > the 80 mm jacket end-state): the jacket must not be re-applied + # (and must never downgrade it). + baseline: EpcPropertyData = _gas_boiler_with_cylinder_baseline() + baseline.sap_heating.cylinder_insulation_thickness_mm = 100 + + # Act + recommendation: Recommendation | None = recommend_heating(baseline, _StubProducts()) + + # Assert — no jacket fields, but the thermostat still added (absent before). + 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.cylinder_insulation_type is None + assert overlay.cylinder_insulation_thickness_mm is None + assert overlay.cylinder_thermostat == "Y" + + +def test_boiler_upgrade_skips_thermostat_when_already_present() -> None: + # Arrange — the same dwelling but the cylinder already has a thermostat. + baseline: EpcPropertyData = _gas_boiler_with_cylinder_baseline() + baseline.sap_heating.cylinder_thermostat = "Y" + + # Act + recommendation: Recommendation | None = recommend_heating(baseline, _StubProducts()) + + # Assert — no thermostat field, but the jacket still added (uninsulated). + 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.cylinder_thermostat is 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. + baseline: EpcPropertyData = _gas_boiler_with_cylinder_baseline() + baseline.sap_heating.main_heating_details[0].sap_main_heating_code = 191 + + # 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_off_gas_boiler_yields_no_gas_boiler_upgrade() -> None: + # Arrange — an oil boiler (Table 4b code 130) with no mains-gas connection: + # a gas boiler cannot be installed, so no upgrade is offered (the gas end- + # state is gated on a mains-gas connection). + baseline: EpcPropertyData = _gas_boiler_with_cylinder_baseline() + baseline.sap_heating.main_heating_details[0].sap_main_heating_code = 130 + baseline.sap_energy_source.mains_gas = 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 + } diff --git a/tests/domain/modelling/test_measure_type.py b/tests/domain/modelling/test_measure_type.py index 60c1a816..56fd5a4c 100644 --- a/tests/domain/modelling/test_measure_type.py +++ b/tests/domain/modelling/test_measure_type.py @@ -26,6 +26,7 @@ _EXPECTED_VALUES = { "mechanical_ventilation", "high_heat_retention_storage_heaters", "air_source_heat_pump", + "gas_boiler_upgrade", "solar_pv", } diff --git a/tests/harness/test_console.py b/tests/harness/test_console.py index 8b95e455..1adc9951 100644 --- a/tests/harness/test_console.py +++ b/tests/harness/test_console.py @@ -38,6 +38,7 @@ _GENERATOR_MEASURE_TYPES = ( "low_energy_lighting", "high_heat_retention_storage_heaters", "air_source_heat_pump", + "gas_boiler_upgrade", ) diff --git a/tests/orchestration/test_ara_first_run_pipeline_integration.py b/tests/orchestration/test_ara_first_run_pipeline_integration.py index 8ef68627..3b72ba13 100644 --- a/tests/orchestration/test_ara_first_run_pipeline_integration.py +++ b/tests/orchestration/test_ara_first_run_pipeline_integration.py @@ -168,6 +168,14 @@ def test_first_run_baselines_through_repos_and_is_idempotent_on_rerun( is_active=True, description="LED bulb", ), + MaterialRow( + id=6, + type="gas_boiler_upgrade", + total_cost=3000.0, + cost_unit="gbp_per_unit", + is_active=True, + description="Gas condensing boiler", + ), ] ) session.commit()