"""Elmhurst before/after cascade pins for the Recommendation Generators. Each measure has an Elmhurst `before` Summary (baseline cert) and an `after` Summary (the same cert re-lodged with the measure applied). The pin drives the matching generator on the parsed `before`, scores its Option's overlay through the `PackageScorer`, and asserts the result equals the calculator's score on the parsed `after` at `abs(diff) <= 1e-4` for SAP / CO2 / primary energy. This is the real cert→generator→overlay→calculator cascade, not a per-section isolation test (see `[[feedback-cascade-pin-methodology]]`): a non-zero delta is a named generator/overlay/calculator gap to fix, never a tolerance to widen (`[[feedback-zero-error-strict]]`). """ from __future__ import annotations from dataclasses import replace from typing import Final import pytest from datatypes.epc.domain.epc_property_data import ( BuildingPartIdentifier, EpcPropertyData, ) from domain.modelling.scoring.package_scorer import PackageScorer, Score from domain.modelling.product import Product from domain.modelling.recommendation import Recommendation from domain.modelling.generators.floor_recommendation import recommend_floor_insulation from domain.modelling.generators.roof_recommendation import ( recommend_roof_insulation, ) from domain.modelling.simulation import BuildingPartOverlay, EpcSimulation from domain.modelling.generators.wall_recommendation import recommend_cavity_wall from domain.geospatial.planning_restrictions import PlanningRestrictions from domain.modelling.generators.solid_wall_recommendation import ( recommend_solid_wall, ) from domain.modelling.generators.glazing_recommendation import recommend_glazing from domain.modelling.generators.lighting_recommendation import recommend_lighting from domain.modelling.generators.heating_recommendation import recommend_heating from domain.modelling.scoring.overlay_applicator import apply_simulations from domain.modelling.recommendation import MeasureOption from domain.sap10_calculator.calculator import Sap10Calculator, SapResult from repositories.product.product_repository import ProductRepository from tests.domain.modelling._elmhurst_recommendation import ( parse_recommendation_summary, ) # Pin tolerance: the Summary PDFs are deterministic test vectors, so the # overlay must reproduce the re-lodged cert exactly. Matches the worksheet # e2e tolerance. _PIN_ABS: Final[float] = 1e-4 # RdSAP wall_insulation_type codes for solid-wall insulation (Elmhurst # Summary "E External" / "I Internal"); cf. domain/sap10_ml/rdsap_uvalues.py. _WALL_INSULATION_EXTERNAL: Final[int] = 1 _WALL_INSULATION_INTERNAL: Final[int] = 3 # Recommended solid-wall insulation depth (mm); the calculator's λ default # (0.04 W/m·K) matches Elmhurst's lodged thermal conductivity. _SOLID_WALL_INSULATION_MM: Final[int] = 100 class _AnyProduct(ProductRepository): """In-memory ProductRepository returning a fixed Product for any Measure Type. The pins assert the SAP cascade, not Cost, so the unit cost is immaterial — only the generator's overlay is exercised.""" def get(self, measure_type: str) -> Product: return Product( measure_type=measure_type, unit_cost_per_m2=1.0, contingency_rate=0.0 ) def _assert_overlay_reproduces_after( before: EpcPropertyData, after: EpcPropertyData, overlay: EpcSimulation ) -> None: """Score ``overlay`` on ``before`` and assert it matches the calculator's score on the re-lodged ``after`` across all three metrics.""" calculator = Sap10Calculator() relodged: SapResult = calculator.calculate(after) scored: Score = PackageScorer(calculator).score(before, [overlay]) assert abs(scored.sap_continuous - relodged.sap_score_continuous) <= _PIN_ABS assert abs(scored.co2_kg_per_yr - relodged.co2_kg_per_yr) <= _PIN_ABS assert ( abs(scored.primary_energy_kwh_per_yr - relodged.primary_energy_kwh_per_yr) <= _PIN_ABS ) def test_cavity_wall_overlay_reproduces_the_relodged_after() -> None: # Arrange before: EpcPropertyData = parse_recommendation_summary( "cavity_wall_001431_before.pdf" ) after: EpcPropertyData = parse_recommendation_summary( "cavity_wall_001431_after.pdf" ) recommendation: Recommendation | None = recommend_cavity_wall( before, _AnyProduct() ) assert recommendation is not None # Act / Assert _assert_overlay_reproduces_after( before, after, recommendation.options[0].overlay ) def test_solid_brick_ewi_overlay_reproduces_the_relodged_after() -> None: # Arrange — 100 mm external wall insulation on a solid-brick main wall. before: EpcPropertyData = parse_recommendation_summary( "solid_brick_ewi_001431_before.pdf" ) after: EpcPropertyData = parse_recommendation_summary( "solid_brick_ewi_001431_after.pdf" ) overlay = EpcSimulation( building_parts={ BuildingPartIdentifier.MAIN: BuildingPartOverlay( wall_insulation_type=_WALL_INSULATION_EXTERNAL, wall_insulation_thickness=_SOLID_WALL_INSULATION_MM, ) } ) # Act / Assert _assert_overlay_reproduces_after(before, after, overlay) def test_solid_brick_iwi_overlay_reproduces_the_relodged_after() -> None: # Arrange — 100 mm internal wall insulation on a solid-brick main wall # (also lowers the thermal-mass parameter, unlike EWI). before: EpcPropertyData = parse_recommendation_summary( "solid_brick_iwi_001431_before.pdf" ) after: EpcPropertyData = parse_recommendation_summary( "solid_brick_iwi_001431_after.pdf" ) overlay = EpcSimulation( building_parts={ BuildingPartIdentifier.MAIN: BuildingPartOverlay( wall_insulation_type=_WALL_INSULATION_INTERNAL, wall_insulation_thickness=_SOLID_WALL_INSULATION_MM, ) } ) # Act / Assert _assert_overlay_reproduces_after(before, after, overlay) def test_solid_brick_generator_offers_ewi_and_iwi_each_pinning_its_after() -> None: # Arrange — one uninsulated solid-brick before, two re-lodged afters. before: EpcPropertyData = parse_recommendation_summary( "solid_brick_ewi_001431_before.pdf" ) ewi_after: EpcPropertyData = parse_recommendation_summary( "solid_brick_ewi_001431_after.pdf" ) iwi_after: EpcPropertyData = parse_recommendation_summary( "solid_brick_iwi_001431_after.pdf" ) # Act — solid brick is suitable for both, unrestricted. recommendation: Recommendation | None = recommend_solid_wall(before, _AnyProduct()) assert recommendation is not None options: dict[str, MeasureOption] = { option.measure_type: option for option in recommendation.options } # Assert — both Options offered, and each Option's overlay reproduces its # own re-lodged after at the pin tolerance. assert set(options) == {"external_wall_insulation", "internal_wall_insulation"} _assert_overlay_reproduces_after( before, ewi_after, options["external_wall_insulation"].overlay ) _assert_overlay_reproduces_after( before, iwi_after, options["internal_wall_insulation"].overlay ) def test_system_built_generator_offers_ewi_and_iwi_each_pinning_its_after() -> None: # Arrange — system-built (precast concrete) takes both Options like solid # brick (ADR-0019): one uninsulated before, two re-lodged afters. before: EpcPropertyData = parse_recommendation_summary( "system_built_ewi_001431_before.pdf" ) ewi_after: EpcPropertyData = parse_recommendation_summary( "system_built_ewi_001431_after.pdf" ) iwi_after: EpcPropertyData = parse_recommendation_summary( "system_built_iwi_001431_after.pdf" ) # Act recommendation: Recommendation | None = recommend_solid_wall(before, _AnyProduct()) assert recommendation is not None options: dict[str, MeasureOption] = { option.measure_type: option for option in recommendation.options } # Assert — both Options offered, each reproducing its own re-lodged after. assert set(options) == {"external_wall_insulation", "internal_wall_insulation"} _assert_overlay_reproduces_after( before, ewi_after, options["external_wall_insulation"].overlay ) _assert_overlay_reproduces_after( before, iwi_after, options["internal_wall_insulation"].overlay ) def test_timber_frame_generator_offers_iwi_only_pinning_its_after() -> None: # Arrange — timber frame takes IWI but EWI is not constructable (ADR-0019). before: EpcPropertyData = parse_recommendation_summary( "timber_frame_iwi_001431_before.pdf" ) iwi_after: EpcPropertyData = parse_recommendation_summary( "timber_frame_iwi_001431_after.pdf" ) # Act recommendation: Recommendation | None = recommend_solid_wall(before, _AnyProduct()) assert recommendation is not None options: dict[str, MeasureOption] = { option.measure_type: option for option in recommendation.options } # Assert — IWI only, and it reproduces the re-lodged after. assert set(options) == {"internal_wall_insulation"} _assert_overlay_reproduces_after( before, iwi_after, options["internal_wall_insulation"].overlay ) def test_conservation_area_drops_ewi_but_keeps_iwi() -> None: # Arrange — a conservation area blocks the external-appearance change only. before: EpcPropertyData = parse_recommendation_summary( "solid_brick_ewi_001431_before.pdf" ) # Act recommendation: Recommendation | None = recommend_solid_wall( before, _AnyProduct(), PlanningRestrictions(in_conservation_area=True) ) # Assert — IWI survives, EWI is gone. assert recommendation is not None assert {option.measure_type for option in recommendation.options} == { "internal_wall_insulation" } def test_listed_building_blocks_all_solid_wall_insulation() -> None: # Arrange — listed/heritage protect the fabric, so both EWI and IWI go. before: EpcPropertyData = parse_recommendation_summary( "solid_brick_ewi_001431_before.pdf" ) # Act / Assert assert ( recommend_solid_wall( before, _AnyProduct(), PlanningRestrictions(is_listed=True) ) is None ) def test_flat_drops_ewi_but_keeps_iwi() -> None: # Arrange — a flat can take IWI to its own unit, but EWI needs whole-block # coordination (ADR-0019). property_type "Flat" is the Elmhurst name form. before: EpcPropertyData = parse_recommendation_summary( "solid_brick_ewi_001431_before.pdf" ) flat: EpcPropertyData = replace(before, property_type="Flat") # Act recommendation: Recommendation | None = recommend_solid_wall(flat, _AnyProduct()) # Assert assert recommendation is not None assert {option.measure_type for option in recommendation.options} == { "internal_wall_insulation" } def test_flat_detected_from_api_property_type_code() -> None: # Arrange — the API path lodges property_type as a stringified code # ("2" = Flat per PROPERTY_TYPE_LOOKUP), not the name. before: EpcPropertyData = parse_recommendation_summary( "solid_brick_ewi_001431_before.pdf" ) flat: EpcPropertyData = replace(before, property_type="2") # Act recommendation: Recommendation | None = recommend_solid_wall(flat, _AnyProduct()) # Assert — same gate fires regardless of representation. assert recommendation is not None assert {option.measure_type for option in recommendation.options} == { "internal_wall_insulation" } def test_cavity_wall_gets_no_solid_wall_recommendation() -> None: # Arrange — a cavity wall is handled by recommend_cavity_wall, never here. before: EpcPropertyData = parse_recommendation_summary( "cavity_wall_001431_before.pdf" ) # Act / Assert assert recommend_solid_wall(before, _AnyProduct()) is None def test_loft_overlay_reproduces_the_relodged_after() -> None: # Arrange before: EpcPropertyData = parse_recommendation_summary( "loft_001431_before.pdf" ) after: EpcPropertyData = parse_recommendation_summary( "loft_001431_after.pdf" ) recommendation: Recommendation | None = recommend_roof_insulation( before, _AnyProduct() ) assert recommendation is not None # Act / Assert _assert_overlay_reproduces_after( before, after, recommendation.options[0].overlay ) def test_roof_generator_insulates_a_sloping_ceiling_pinning_its_after() -> None: # Arrange — a pitched roof with an uninsulated sloping ceiling; the re-lodged # after raises its insulation from As Built to 100 mm (ADR-0021). before: EpcPropertyData = parse_recommendation_summary( "sloping_ceiling_001431_before.pdf" ) after: EpcPropertyData = parse_recommendation_summary( "sloping_ceiling_001431_after.pdf" ) # Act — the dispatcher detects "sloping ceiling" and offers the sloping # measure (not loft). recommendation: Recommendation | None = recommend_roof_insulation( before, _AnyProduct() ) assert recommendation is not None options: dict[str, MeasureOption] = { option.measure_type: option for option in recommendation.options } # Assert — one sloping-ceiling Option whose overlay reproduces the after. assert set(options) == {"sloping_ceiling_insulation"} _assert_overlay_reproduces_after( before, after, options["sloping_ceiling_insulation"].overlay ) def test_roof_generator_insulates_a_thatched_roof_as_loft_pinning_its_after() -> None: # Arrange — a thatched pitched roof. Thatch is NOT excluded: the covering # doesn't block insulating the loft floor, so it takes loft (joist) # insulation, re-lodged None → 300 mm (ADR-0021). before: EpcPropertyData = parse_recommendation_summary( "loft_thatched_001431_before.pdf" ) after: EpcPropertyData = parse_recommendation_summary( "loft_thatched_001431_after.pdf" ) # Act — the dispatcher routes a thatched roof to the loft branch. recommendation: Recommendation | None = recommend_roof_insulation( before, _AnyProduct() ) assert recommendation is not None options: dict[str, MeasureOption] = { option.measure_type: option for option in recommendation.options } # Assert — one loft Option whose overlay reproduces the after. assert set(options) == {"loft_insulation"} _assert_overlay_reproduces_after( before, after, options["loft_insulation"].overlay ) def test_roof_generator_insulates_a_flat_roof_pinning_its_after() -> None: # Arrange — a flat roof, uninsulated (As Built → None on the Elmhurst path); # the re-lodged after raises it to 200 mm (ADR-0021). before: EpcPropertyData = parse_recommendation_summary( "flat_roof_001431_before.pdf" ) after: EpcPropertyData = parse_recommendation_summary( "flat_roof_001431_after.pdf" ) # Act recommendation: Recommendation | None = recommend_roof_insulation( before, _AnyProduct() ) assert recommendation is not None options: dict[str, MeasureOption] = { option.measure_type: option for option in recommendation.options } # Assert — one flat-roof Option whose overlay reproduces the after. assert set(options) == {"flat_roof_insulation"} _assert_overlay_reproduces_after( before, after, options["flat_roof_insulation"].overlay ) def test_solid_floor_overlay_reproduces_the_relodged_after() -> None: # Arrange before: EpcPropertyData = parse_recommendation_summary( "solid_floor_001431_before.pdf" ) after: EpcPropertyData = parse_recommendation_summary( "solid_floor_001431_after.pdf" ) recommendation: Recommendation | None = recommend_floor_insulation( before, _AnyProduct() ) assert recommendation is not None # Act / Assert _assert_overlay_reproduces_after( before, after, recommendation.options[0].overlay ) def test_suspended_floor_overlay_reproduces_the_relodged_after() -> None: # Arrange before: EpcPropertyData = parse_recommendation_summary( "suspended_floor_001431_before.pdf" ) after: EpcPropertyData = parse_recommendation_summary( "suspended_floor_001431_after.pdf" ) recommendation: Recommendation | None = recommend_floor_insulation( before, _AnyProduct() ) assert recommendation is not None # Act / Assert _assert_overlay_reproduces_after( before, after, recommendation.options[0].overlay ) def test_double_glazing_overlay_reproduces_the_relodged_after_windows() -> None: # The full-SAP pin below is xfail (draught-proofing coupling), but the # overlay's actual job — turning every single-glazed window into the # relodged spec — is deterministic and must hold exactly: it proves the # generator detects BOTH single-glazing codes (1 and 15) on the real cert. # Arrange before: EpcPropertyData = parse_recommendation_summary( "double_glazing_001431_before.pdf" ) after: EpcPropertyData = parse_recommendation_summary( "double_glazing_001431_after.pdf" ) recommendation: Recommendation | None = recommend_glazing(before, _AnyProduct()) assert recommendation is not None # Act — apply the overlay to the parsed before. applied: EpcPropertyData = apply_simulations( before, [recommendation.options[0].overlay] ) # Assert — every window's glazing_type + lodged U/g matches the after. def _window_spec(epc: EpcPropertyData) -> list[tuple[object, object, object]]: specs: list[tuple[object, object, object]] = [] for window in epc.sap_windows: details = window.window_transmission_details specs.append( ( window.glazing_type, details.u_value if details is not None else None, details.solar_transmittance if details is not None else None, ) ) return specs assert _window_spec(applied) == _window_spec(after) _GLAZING_DRAUGHT_COUPLING_REASON: Final[str] = ( "Blocked on the glazing measure's draught-proofing coupling. The window " "U/g overlay reproduces the after's 14 windows EXACTLY (all four single-" "glazed panes — codes 1 and 15 — become the relodged double/secondary " "spec). The residual ~0.7 SAP is a secondary effect the overlay does not " "model: replacing the single-glazed (lodged draught_proofed=No) windows " "with sealed units re-lodges percent_draughtproofed 84->100 (~0.3 SAP) and " "lowers fabric heat loss by ~+150 kWh space heating (~0.4 SAP) not yet " "isolated. Flips green once the glazing overlay propagates draught-proofing " "(and the residual fabric coupling is modelled)." ) @pytest.mark.xfail(strict=True, reason=_GLAZING_DRAUGHT_COUPLING_REASON) def test_double_glazing_overlay_reproduces_the_relodged_after() -> None: # Arrange — cert 001431 lodges four single-glazed windows (codes 1 and 15, # "single glazing, known data"); the after re-lodges every one as double # (gt=5, U=1.40, g=0.72). before: EpcPropertyData = parse_recommendation_summary( "double_glazing_001431_before.pdf" ) after: EpcPropertyData = parse_recommendation_summary( "double_glazing_001431_after.pdf" ) recommendation: Recommendation | None = recommend_glazing(before, _AnyProduct()) assert recommendation is not None # Act / Assert _assert_overlay_reproduces_after( before, after, recommendation.options[0].overlay ) @pytest.mark.xfail(strict=True, reason=_GLAZING_DRAUGHT_COUPLING_REASON) def test_secondary_glazing_overlay_reproduces_the_relodged_after() -> None: # Arrange — a planning protection forces secondary glazing; the after # re-lodges every single-glazed window as secondary (gt=11, U=2.90, g=0.85). before: EpcPropertyData = parse_recommendation_summary( "secondary_glazing_001431_before.pdf" ) after: EpcPropertyData = parse_recommendation_summary( "secondary_glazing_001431_after.pdf" ) recommendation: Recommendation | None = recommend_glazing( before, _AnyProduct(), PlanningRestrictions(in_conservation_area=True) ) assert recommendation is not None # Act / Assert _assert_overlay_reproduces_after( before, after, recommendation.options[0].overlay ) def test_lighting_overlay_reproduces_the_relodged_after_zero_existing_leds() -> None: # Arrange — a dwelling with no LEDs (20 incandescent); the after re-lodges # all 20 as LED. Lighting only changes bulb counts → Appendix L (232), with # no fabric coupling, so the full-SAP pin closes cleanly. before: EpcPropertyData = parse_recommendation_summary( "low_energy_lighting_zero_leds_001431_before.pdf" ) after: EpcPropertyData = parse_recommendation_summary( "low_energy_lighting_zero_leds_001431_after.pdf" ) recommendation: Recommendation | None = recommend_lighting(before, _AnyProduct()) assert recommendation is not None # Act / Assert _assert_overlay_reproduces_after( before, after, recommendation.options[0].overlay ) def test_lighting_overlay_reproduces_the_relodged_after_some_existing_leds() -> None: # Arrange — a dwelling with some LEDs already (5 LED + 15 incandescent); the # after re-lodges all 20 as LED. Exercises the partial-upgrade path: the # overlay tops led up to the total rather than starting from zero. before: EpcPropertyData = parse_recommendation_summary( "low_energy_lighting_some_leds_001431_before.pdf" ) after: EpcPropertyData = parse_recommendation_summary( "low_energy_lighting_some_leds_001431_after.pdf" ) recommendation: Recommendation | None = recommend_lighting(before, _AnyProduct()) assert recommendation is not None # Act / Assert _assert_overlay_reproduces_after( before, after, recommendation.options[0].overlay ) def test_hhr_storage_overlay_reproduces_the_relodged_after_from_electric_storage() -> None: # Arrange — an existing electric storage system re-lodged as high-heat- # retention storage (Table 4a 402 -> 409, control 2401 -> 2404), gaining an # off-peak immersion cylinder and a dual meter (ADR-0024). before: EpcPropertyData = parse_recommendation_summary( "hhr_storage_from_electric_storage_001431_before.pdf" ) after: EpcPropertyData = parse_recommendation_summary( "hhr_storage_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 == "high_heat_retention_storage_heaters" ) # Act / Assert _assert_overlay_reproduces_after(before, after, option.overlay) def test_hhr_storage_overlay_reproduces_the_relodged_after_from_no_system() -> None: # Arrange — a "no system present" electric dwelling re-lodged as HHR storage; # the same absolute-target overlay must reproduce the common after. before: EpcPropertyData = parse_recommendation_summary( "hhr_storage_from_no_system_001431_before.pdf" ) after: EpcPropertyData = parse_recommendation_summary( "hhr_storage_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 == "high_heat_retention_storage_heaters" ) # Act / Assert _assert_overlay_reproduces_after(before, after, option.overlay) def test_ashp_overlay_reproduces_the_relodged_after_from_a_gas_boiler() -> None: # Arrange — a typical mains-gas combi house re-lodged as an air-source heat # pump (fuel 26 -> 30, SAP code 104 -> PCDB index 101413 + category 4, # control 2106 -> 2210), off mains gas, gaining a heat-pump cylinder # (ADR-0024). before: EpcPropertyData = parse_recommendation_summary( "ashp_from_gas_boiler_001431_before.pdf" ) after: EpcPropertyData = parse_recommendation_summary( "ashp_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 == "air_source_heat_pump" ) # Act / Assert _assert_overlay_reproduces_after(before, after, option.overlay) def test_ashp_overlay_reproduces_the_relodged_after_from_a_gas_boiler_instant_hw() -> None: # Arrange — a gas boiler whose hot water is electric/instantaneous (water- # heating SAP code 909, no cylinder) re-lodged as an ASHP. This exercises the # overlay's water_heating_code reset (909 -> 901, "from the heat pump") that # boiler-1 didn't (its HW was already 901). The relodged after lodges control # 2209 vs the overlay's 2210 — SAP-equivalent zone controls, so the cascade # still closes at 1e-4. before: EpcPropertyData = parse_recommendation_summary( "ashp_from_gas_boiler_instant_hw_001431_before.pdf" ) after: EpcPropertyData = parse_recommendation_summary( "ashp_from_gas_boiler_instant_hw_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 == "air_source_heat_pump" ) # Act / Assert — overlay applied to the before reproduces the after exactly. _assert_overlay_reproduces_after(before, after, option.overlay) def test_ashp_overlay_reproduces_the_relodged_after_from_a_system_boiler_with_cylinder() -> None: # Arrange — a mains-gas *regular/system* boiler (SAP code 101, not a combi) # that already heats its own hot-water cylinder (size 2 / insulation type 2 / # 80 mm) re-lodged as an ASHP. This exercises the cylinder OVERWRITE path that # boiler-1/boiler-2 didn't: those added a cylinder where none existed, whereas # here the overlay must overwrite the existing cylinder to the fixed heat-pump # cylinder (size 4 / insulation type 1 / 50 mm). The dwelling also goes off # mains gas (fuel 26 -> 30, code 101 -> index 101413 + category 4, control # 2113 -> 2210). before: EpcPropertyData = parse_recommendation_summary( "ashp_from_system_boiler_with_cylinder_001431_before.pdf" ) after: EpcPropertyData = parse_recommendation_summary( "ashp_from_system_boiler_with_cylinder_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 == "air_source_heat_pump" ) # Act / Assert — the absolute overlay overwrites the existing cylinder and # reproduces the after exactly. _assert_overlay_reproduces_after(before, after, option.overlay) _BOILER_INSTANT_HW_FUEL_REASON: Final[str] = ( "Blocked on the Elmhurst mapper deriving main_fuel_type for a gas boiler " "lodged with EES code 'BGB' / Main Heating SAP code 102: it currently maps " "to '' (empty), so Sap10Calculator raises MissingMainFuelType when " "baselining the raw before. (boiler-1's 'RGE' resolves to mains gas 26; " "'BGB' has no mapping.) The ASHP overlay still reproduces the after exactly " "because it overwrites main_fuel_type -> 30, so the cascade pin above passes " "— only baselining the unmodified before is blocked. Flips green once the " "mapper derives mains gas (26) from the gas-boiler SAP code. Owner: " "mapper/extractor front." ) @pytest.mark.xfail(strict=True, reason=_BOILER_INSTANT_HW_FUEL_REASON) def test_gas_boiler_instant_hw_before_baselines() -> None: # The Modelling pipeline baselines the dwelling before modelling it, so the # before must be scorable on its own. This one is not yet: its main fuel is # unresolved (see reason). A failing tripwire for the separate mapper fix. # Arrange before: EpcPropertyData = parse_recommendation_summary( "ashp_from_gas_boiler_instant_hw_001431_before.pdf" ) # Act / Assert — currently raises MissingMainFuelType. Sap10Calculator().calculate(before)