From 877d0043cb628f3f22757ea12ef067ac80a14386 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 9 Jul 2026 13:59:14 +0000 Subject: [PATCH] =?UTF-8?q?Ventilation-once=20test=20pairs=20the=20wall=20?= =?UTF-8?q?with=20glazing,=20not=20a=20competing=20wall=20=F0=9F=9F=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review feedback: cavity and internal wall insulation are competing options, so a package selecting both read as nonsense even in a synthetic fixture. The behaviour under test (a dependency triggered in both phases injects once) now uses an airtightness pair that genuinely coexists — cavity wall in phase 1, double glazing re-entering in phase 2 — with the same numbers. Co-Authored-By: Claude Fable 5 --- .../modelling/test_optimiser_fabric_first.py | 60 ++++++++----------- 1 file changed, 25 insertions(+), 35 deletions(-) diff --git a/tests/domain/modelling/test_optimiser_fabric_first.py b/tests/domain/modelling/test_optimiser_fabric_first.py index 09b01e646..1f7dce7c2 100644 --- a/tests/domain/modelling/test_optimiser_fabric_first.py +++ b/tests/domain/modelling/test_optimiser_fabric_first.py @@ -47,6 +47,7 @@ _ROOF_OVERLAY = EpcSimulation( } ) _HEATING_OVERLAY = EpcSimulation(heating=HeatingOverlay(sap_main_heating_code=201)) +_GLAZING_OVERLAY = EpcSimulation(glazing=GlazingOverlay(glazing_type=2)) _BOILER_OVERLAY = EpcSimulation(heating=HeatingOverlay(sap_main_heating_code=201)) _ASHP_OVERLAY = EpcSimulation( heating=HeatingOverlay(main_heating_index_number=13000) @@ -177,52 +178,44 @@ def test_fabric_spend_comes_out_of_the_shared_budget_before_phase_two() -> None: assert abs(package.score.sap_continuous - 65.0) <= 1e-9 -_IWI_OVERLAY = EpcSimulation( - building_parts={ - BuildingPartIdentifier.MAIN: BuildingPartOverlay(wall_insulation_type=3) - } -) _VENT_OVERLAY = EpcSimulation( ventilation=VentilationOverlay(mechanical_ventilation_kind="EXTRACT_OR_PIV_OUTSIDE") ) -class _TwoWallScorer: - """A stub with two wall treatments (cavity type=2, internal type=3): the - internal wall is worthless raw but +4 once the cavity is done, and every - ventilation overlay present costs −1 — so both phases trigger the same - forced ventilation dependency and a double injection is visible in the - package score.""" +class _AirtightnessScorer: + """A stub where tightening the envelope demands ventilation: the cavity + wall is +5 SAP, the new double glazing is worthless on the raw dwelling + but +4 once the wall is insulated, and every ventilation overlay present + costs −1 — so a double injection is visible in the package score.""" def score( self, baseline: EpcPropertyData, simulations: Sequence[EpcSimulation] ) -> Score: - cavity = any( - part.wall_insulation_type == 2 - for sim in simulations - for part in sim.building_parts.values() - ) - internal = any( - part.wall_insulation_type == 3 + wall = any( + part.wall_insulation_type is not None for sim in simulations for part in sim.building_parts.values() ) + glazing = any(sim.glazing is not None for sim in simulations) vents = sum(1 for sim in simulations if sim.ventilation is not None) sap = 60.0 - sap += 5.0 if cavity else 0.0 - sap += (4.0 if cavity else 0.0) if internal else 0.0 + sap += 5.0 if wall else 0.0 + sap += (4.0 if wall else 0.0) if glazing else 0.0 sap -= float(vents) return Score( sap_continuous=sap, co2_kg_per_yr=0.0, primary_energy_kwh_per_yr=0.0 ) -def _wall_ventilation_dependency(*, cost: float) -> MeasureDependency: +def _airtightness_ventilation_dependency(*, cost: float) -> MeasureDependency: + """A forced 'airtightness requires ventilation' edge: both the wall and + the sealed new glazing trigger the same mechanical ventilation.""" return MeasureDependency( triggers=frozenset( { MeasureType.CAVITY_WALL_INSULATION, - MeasureType.INTERNAL_WALL_INSULATION, + MeasureType.DOUBLE_GLAZING, } ), required=ScoredOption( @@ -238,17 +231,17 @@ def _wall_ventilation_dependency(*, cost: float) -> MeasureDependency: def test_ventilation_dependency_is_injected_once_across_both_phases() -> None: - # Arrange — the cavity wall (phase 1) and the internal wall (picked in - # phase 2 on its post-cavity worth) both trigger the same forced - # ventilation. It must land in the package exactly once — phase 2 sees the - # phase-1 dwelling as already ventilated. + # Arrange — the cavity wall (phase 1) and the double glazing (skipped in + # phase 1 on merit, picked in phase 2 on its post-fabric worth) both + # trigger the same forced ventilation. It must land in the package exactly + # once — phase 2 sees the phase-1 dwelling as already ventilated. groups: list[list[ScoredOption]] = [ [_scored("cavity_wall_insulation", gain=5.0, cost=1000.0, overlay=_WALL_OVERLAY)], - [_scored("internal_wall_insulation", gain=0.0, cost=2000.0, overlay=_IWI_OVERLAY)], + [_scored("double_glazing", gain=0.0, cost=500.0, overlay=_GLAZING_OVERLAY)], ] - scorer = _TwoWallScorer() + scorer = _AirtightnessScorer() - # Act — target 68: phase 1 gives 60 + 5 − 1 = 64; the internal wall's + # Act — target 68: phase 1 gives 60 + 5 − 1 = 64; the glazing's # post-fabric +4 closes it, but only if ventilation is not double-counted. package: OptimisedPackage = optimise_package_fabric_first( groups=groups, @@ -256,11 +249,11 @@ def test_ventilation_dependency_is_injected_once_across_both_phases() -> None: baseline_epc=build_epc(), budget=10000.0, target_sap=68.0, - dependencies=[_wall_ventilation_dependency(cost=300.0)], + dependencies=[_airtightness_ventilation_dependency(cost=300.0)], ) # Assert — one ventilation, and the truthful total counts its penalty once: - # 60 + 5 cavity + 4 internal − 1 ventilation = 68. + # 60 + 5 wall + 4 glazing − 1 ventilation = 68. ventilation_count = sum( 1 for scored in package.selected @@ -269,7 +262,7 @@ def test_ventilation_dependency_is_injected_once_across_both_phases() -> None: assert ventilation_count == 1 assert _selected_types(package) == { "cavity_wall_insulation", - "internal_wall_insulation", + "double_glazing", "mechanical_ventilation", } assert abs(package.score.sap_continuous - 68.0) <= 1e-9 @@ -349,9 +342,6 @@ class _InteractionScorer: ) -_GLAZING_OVERLAY = EpcSimulation(glazing=GlazingOverlay(glazing_type=2)) - - class _GlazingInteractionScorer: """A stub where glazing is worthless on the raw dwelling (+0) but worth +4 once the wall is insulated — so phase 1's max-gain fabric pass leaves it