import pytest import numpy as np from types import SimpleNamespace from recommendations.tests.test_data.measures_to_optimise import measures_to_optimise from recommendations.optimiser import optimiser_functions from recommendations.optimiser.funding_optimiser import optimise_with_scenarios from recommendations.optimiser.GainOptimiser import GainOptimiser from recommendations.optimiser.CostOptimiser import CostOptimiser from recommendations.optimiser.StrategicOptimiser import StrategicOptimiser @pytest.fixture def property_instance(): return SimpleNamespace( id="P1", has_ventilation=False, data={"current-energy-efficiency": "52"}, epc_record=SimpleNamespace(current_energy_efficiency=52), ) class TestPrepareInputMeasures: def test_returns_expected_structure_without_ventilation(self): recs = [ [ # loft insulation measure {"recommendation_id": "loft1", "type": "loft_insulation", "total": 100, "kwh_savings": 200, "energy_cost_savings": 10, "has_battery": False, "measure_type": "loft_insulation", "partial_project_funding": 0, "partial_project_score": 0, "uplift_project_score": 0, }, ], ] measures = optimiser_functions.prepare_input_measures(recs, goal="Energy Savings", needs_ventilation=False) assert isinstance(measures, list) assert measures[0][0]["id"] == "loft1" assert measures[0][0]["cost"] == 100 assert measures[0][0]["gain"] == 200 def test_bundles_ventilation_when_needed(self, monkeypatch): # patch measures_needing_ventilation so that "wall_insulation" needs ventilation monkeypatch.setattr(optimiser_functions.assumptions, "measures_needing_ventilation", ["internal_wall_insulation"]) recs = [ [{"recommendation_id": "wall1", "type": "internal_wall_insulation", "total": 500, "kwh_savings": 300, "energy_cost_savings": 5, "has_battery": False, "measure_type": "internal_wall_insulation", "partial_project_funding": 0, "partial_project_score": 0, "uplift_project_score": 0, }], [{"recommendation_id": "vent1", "type": "mechanical_ventilation", "total": 50, "kwh_savings": 30, "energy_cost_savings": 5, "has_battery": False, "measure_type": "mechanical_ventilation", "partial_project_funding": 0, "partial_project_score": 0, "uplift_project_score": 0, }], ] measures = optimiser_functions.prepare_input_measures(recs, goal="Energy Savings", needs_ventilation=True) wall_option = measures[0][0] assert wall_option["cost"] == 550 assert wall_option["gain"] == 330 assert "+mechanical_ventilation" in wall_option["type"] def test_filters_out_negative_cost_savings(self): recs = [ [{"recommendation_id": "bad1", "type": "loft_insulation", "total": 200, "kwh_savings": 100, "energy_cost_savings": -100, "has_battery": False, "partial_project_funding": 0, "partial_project_score": 0, "uplift_project_score": 0, "measure_type": "roof_insulation"}], ] measures = optimiser_functions.prepare_input_measures(recs, goal="Energy Savings", needs_ventilation=False) assert measures == [] # should skip negative cost saving recs class TestCalculateFixedGain: def test_no_required_measures_returns_zero(self): fixed_gain = optimiser_functions.calculate_fixed_gain( [], {}, SimpleNamespace(id="P1"), needs_ventilation=False ) assert fixed_gain == 0 def test_sums_max_sap_points_per_type(self, monkeypatch): monkeypatch.setattr(optimiser_functions.assumptions, "measures_needing_ventilation", ["internal_wall_insulation"]) required_measures = [ [{"type": "internal_wall_insulation", "sap_points": 5}, {"type": "internal_wall_insulation", "sap_points": 10}], [{"type": "loft_insulation", "sap_points": 3}] ] recommendations = {"P1": [[{"type": "mechanical_ventilation", "sap_points": 2}]]} prop = SimpleNamespace(id="P1") gain = optimiser_functions.calculate_fixed_gain( required_measures, recommendations, prop, needs_ventilation=True ) # Should take max of wall (10) + loft (3) + ventilation (2) assert gain == 15 class TestCalculateGain: def test_returns_none_for_energy_savings_goal(self): body = SimpleNamespace(goal="Energy Savings") prop = SimpleNamespace( data={"current-energy-efficiency": "50"}, epc_record=SimpleNamespace(current_energy_efficiency=50) ) gain = optimiser_functions.calculate_gain(body, prop, fixed_gain=2) assert gain is None def test_returns_zero_for_already_installed_getting_to_target(self): body = SimpleNamespace(goal="Increasing EPC", goal_value="C") epc_record = SimpleNamespace(current_energy_efficiency=67) p = SimpleNamespace(epc_record=epc_record, id=1) fixed_gain = 0 eco_packages = {1: (None, None, None, [])} already_installed_sap = 2 gain = optimiser_functions.calculate_gain( body=body, p=p, fixed_gain=fixed_gain, already_installed_gain=already_installed_sap ) assert gain == 0 def test_calculates_gain_for_epc(self, monkeypatch): # patch cost optimiser calculation monkeypatch.setattr(optimiser_functions, "epc_to_sap_lower_bound", lambda goal_value: 69) body = SimpleNamespace(goal="Increasing EPC", goal_value="C", simulate_sap_10=False) prop = SimpleNamespace( data={"current-energy-efficiency": "50"}, epc_record=SimpleNamespace(current_energy_efficiency=50) ) gain = optimiser_functions.calculate_gain(body, prop, fixed_gain=2) assert gain == 17.5 class TestAddRequiredMeasures: def test_adds_cheapest_required_measure(self): property_id = "P1" required_measures = [ [{"recommendation_id": "a", "total": 100, "sap_points": 5, "type": "loft_insulation"}, {"recommendation_id": "b", "total": 80, "sap_points": 6, "type": "loft_insulation"}] ] recommendations = { "P1": [[{"recommendation_id": "a", "total": 100, "sap_points": 5, "type": "loft_insulation"}, {"recommendation_id": "b", "total": 80, "sap_points": 6, "type": "loft_insulation"}]] } selected = set() result = optimiser_functions.add_required_measures(property_id, required_measures, recommendations, selected) # cheapest should be b assert "b" in selected assert any(rec["id"] == "b" for rec in result) class TestAddBestPracticeMeasures: def test_adds_ventilation_and_trickle_vents(self, monkeypatch): monkeypatch.setattr(optimiser_functions.assumptions, "measures_needing_ventilation", ["internal_wall_insulation"]) property_id = "P1" solution = [{"type": "internal_wall_insulation", "id": "w1", "gain": 10, "cost": 100}] recommendations = { "P1": [ [{"type": "mechanical_ventilation", "recommendation_id": "vent1"}], [{"type": "trickle_vents", "recommendation_id": "trickle1"}] ] } selected = set() updated = optimiser_functions.add_best_practice_measures( property_id, solution, recommendations, selected ) assert "vent1" in updated assert "trickle1" in updated class TestFlattenRecommendationsWithDefaults: def test_marks_selected_and_flattens(self): property_id = "P1" recommendations = { "P1": [ [{"recommendation_id": "a", "foo": 1}, {"recommendation_id": "b", "foo": 2}], [{"recommendation_id": "c", "foo": 3}] ] } selected = {"b", "c"} result = optimiser_functions.flatten_recommendations_with_defaults(property_id, recommendations, selected) # All recs should now have a default key assert all("default" in rec for rec in result) assert next(r for r in result if r["recommendation_id"] == "b")["default"] is True assert next(r for r in result if r["recommendation_id"] == "a")["default"] is False class TestIncreasingEpcE2e: """ Test out the classic increasing EPC optimisation flow end-to-end. We have a goal (Increasing EPC), no budget, and we expect the optimiser to choose the best set of measures and include best-practice ventilation. """ @pytest.fixture def setup_case(self): # Dummy property object p = SimpleNamespace( id="P1", has_ventilation=False, data={"current-energy-efficiency": "52"}, epc_record=SimpleNamespace(current_energy_efficiency=52), ) # Dummy request body body = SimpleNamespace( goal="Increasing EPC", goal_value="C", optimise=True, budget=None, simulate_sap_10=False, required_measures=[] ) recommendations = {"P1": measures_to_optimise} return p, body, recommendations def test_end_to_end_increasing_epc(self, setup_case): p, body, recommendations = setup_case # --------------------- # RUN THE OPTIMISATION LOOP # --------------------- property_measure_types = {rec["type"] for recs in recommendations[p.id] for rec in recs} property_required_measures = [m for m in recommendations[p.id] if m[0]["type"] in body.required_measures] measures_to_optimise = [m for m in recommendations[p.id] if m[0]["type"] not in body.required_measures] # ventilation flag needs_ventilation = any( x in property_measure_types for x in optimiser_functions.assumptions.measures_needing_ventilation ) and not p.has_ventilation assert needs_ventilation # Input the various things we need - set all to 0 for group in measures_to_optimise: for r in group: ( r["partial_project_score"], r["partial_project_funding"], r["innovation_uplift"], r["uplift_project_score"], ) = ( 0, 0, 0, 0 ) input_measures = optimiser_functions.prepare_input_measures(measures_to_optimise, body.goal, needs_ventilation) assert input_measures, "Expected measures to optimise" assert len(input_measures) == 7 fixed_gain = optimiser_functions.calculate_fixed_gain( property_required_measures, recommendations, p, needs_ventilation ) assert fixed_gain == 0, "No required measures should mean fixed gain is 0" gain = optimiser_functions.calculate_gain(body=body, p=p, fixed_gain=fixed_gain) assert gain == 17.5, "Expected gain to be calculated correctly based on fixed gain and SAP target" optimiser = ( GainOptimiser( input_measures, max_cost=body.budget, max_gain=gain, allow_slack=body.goal == "Increasing EPC" ) if body.budget else CostOptimiser(input_measures, min_gain=gain) ) optimiser.setup() optimiser.solve() solution = optimiser.solution assert solution, "Optimiser should return a non-empty solution" assert all("id" in m for m in solution) assert any("solar_pv" in m["type"] for m in solution), "Expected solar PV to be included" # Collect selected measure IDs selected = {r["id"] for r in solution} assert selected == {'7_phase=6', '5_phase=4', '10_phase=7'} assert float(optimiser.solution_gain) == 17.6 # Add required measures (none here) solution = optimiser_functions.add_required_measures( property_id=p.id, property_required_measures=property_required_measures, recommendations=recommendations, selected=selected, ) assert solution == [ {'id': '5_phase=4', 'cost': 58.8, 'gain': 2, 'type': 'low_energy_lighting'}, {'id': '7_phase=6', 'cost': 30.0, 'gain': np.float64(3.6), 'type': 'secondary_heating'}, {'id': '10_phase=7', 'cost': 5826.491999999999, 'gain': np.float64(12.0), 'type': 'solar_pv'} ] total_optimised_gain = sum(m["gain"] for m in solution) assert total_optimised_gain == 17.6, "Total gain of optimised measures should meet or exceed target gain" selected = optimiser_functions.add_best_practice_measures(p.id, solution, recommendations, selected) # Flatten recommendations for output flattened = optimiser_functions.flatten_recommendations_with_defaults(p.id, recommendations, selected) # --------------------- # FINAL ASSERTIONS # --------------------- assert isinstance(flattened, list) assert all("default" in rec for rec in flattened) assert any(rec["default"] for rec in flattened), "Some measures should be marked as default" # We don't add ventilation as major insulation work isn't done ventilation_added = any(rec["recommendation_id"] == "3_phase=2" and rec["default"] for rec in flattened) assert not ventilation_added, "Ventilation should not be added without major insulation work" class TestStrategicOptimiser: @pytest.fixture def components(self): components = [ [ {'id': '0_phase=0', 'cost': 819.0, 'gain': 5.6, 'type': 'loft_insulation', 'innovation_uplift': 0, 'cost_minus_uplift': 819.0, 'raw_cost': 819.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 0}, {'id': '1_phase=0', 'cost': 702.0, 'gain': 5.6, 'type': 'loft_insulation', 'innovation_uplift': 0, 'cost_minus_uplift': 702.0, 'raw_cost': 702.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 0}, {'id': '2_phase=0', 'cost': 585.0, 'gain': 5.6, 'type': 'loft_insulation', 'innovation_uplift': 0, 'cost_minus_uplift': 585.0, 'raw_cost': 585.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 0}], [{'id': '4_phase=2', 'cost': 3656.25, 'gain': 2.0, 'type': 'suspended_floor_insulation', 'innovation_uplift': 0, 'cost_minus_uplift': 3656.25, 'raw_cost': 3656.25, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 0}], [{'id': '5_phase=3', 'cost': 17.5, 'gain': 1.0, 'type': 'low_energy_lighting', 'innovation_uplift': 0, 'cost_minus_uplift': 17.5, 'raw_cost': 17.5, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 0}], [{'id': '6_phase=4', 'cost': 140, 'gain': 3.4, 'type': 'roomstat_programmer_trvs', 'innovation_uplift': 0, 'cost_minus_uplift': 140, 'raw_cost': 140, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 0}, {'id': '7_phase=4', 'cost': 874.5680000000001, 'gain': 4.2, 'type': 'time_temperature_zone_control', 'innovation_uplift': 0, 'cost_minus_uplift': 874.5680000000001, 'raw_cost': 874.5680000000001, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 0}], [{'id': '9_phase=6', 'cost': 5420.0, 'gain': 13.2, 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 5420.0, 'raw_cost': 5420.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 3.6}, {'id': '10_phase=6', 'cost': 6210.0, 'gain': 16.2, 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 6210.0, 'raw_cost': 6210.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': True, 'array_size': 3.6, 'battery_gain': 3}, {'id': '11_phase=6', 'cost': 6820.0, 'gain': 16.2, 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 6820.0, 'raw_cost': 6820.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': True, 'array_size': 3.6, 'battery_gain': 3}, {'id': '12_phase=6', 'cost': 7202.0, 'gain': 14.5, 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 7202.0, 'raw_cost': 7202.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 3.915}, {'id': '13_phase=6', 'cost': 6495.0, 'gain': 14.5, 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 6495.0, 'raw_cost': 6495.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 3.92}, {'id': '14_phase=6', 'cost': 7285.0, 'gain': 17.5, 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 7285.0, 'raw_cost': 7285.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': True, 'array_size': 3.92, 'battery_gain': 3}, {'id': '15_phase=6', 'cost': 7895.0, 'gain': 17.5, 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 7895.0, 'raw_cost': 7895.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': True, 'array_size': 3.92, 'battery_gain': 3}, {'id': '16_phase=6', 'cost': 5520.0, 'gain': 15.0, 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 5520.0, 'raw_cost': 5520.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 4.0}, {'id': '17_phase=6', 'cost': 6310.0, 'gain': 18.0, 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 6310.0, 'raw_cost': 6310.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': True, 'array_size': 4.0, 'battery_gain': 3}, {'id': '18_phase=6', 'cost': 6920.0, 'gain': 18.0, 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 6920.0, 'raw_cost': 6920.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': True, 'array_size': 4.0, 'battery_gain': 3}, {'id': '19_phase=6', 'cost': 5320.0, 'gain': 12.1, 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 5320.0, 'raw_cost': 5320.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 3.2}, {'id': '20_phase=6', 'cost': 6110.0, 'gain': 14.1, 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 6110.0, 'raw_cost': 6110.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': True, 'array_size': 3.2, 'battery_gain': 2}, {'id': '21_phase=6', 'cost': 6720.0, 'gain': 14.1, 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 6720.0, 'raw_cost': 6720.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': True, 'array_size': 3.2, 'battery_gain': 2}, {'id': '22_phase=6', 'cost': 6932.0, 'gain': 13.2, 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 6932.0, 'raw_cost': 6932.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 3.48}, {'id': '23_phase=6', 'cost': 6295.0, 'gain': 13.2, 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 6295.0, 'raw_cost': 6295.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 3.48}, {'id': '24_phase=6', 'cost': 7085.0, 'gain': 16.2, 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 7085.0, 'raw_cost': 7085.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': True, 'array_size': 3.48, 'battery_gain': 3}, {'id': '25_phase=6', 'cost': 7695.0, 'gain': 16.2, 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 7695.0, 'raw_cost': 7695.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': True, 'array_size': 3.48, 'battery_gain': 3}, {'id': '26_phase=6', 'cost': 5220.0, 'gain': 10.2, 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 5220.0, 'raw_cost': 5220.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 2.8}, {'id': '27_phase=6', 'cost': 6662.0, 'gain': 12.3, 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 6662.0, 'raw_cost': 6662.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 3.045}, {'id': '28_phase=6', 'cost': 6095.0, 'gain': 12.3, 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 6095.0, 'raw_cost': 6095.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 3.05}, {'id': '29_phase=6', 'cost': 5160.0, 'gain': 9.0, 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 5160.0, 'raw_cost': 5160.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 2.4}, {'id': '30_phase=6', 'cost': 6392.0, 'gain': 10.2, 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 6392.0, 'raw_cost': 6392.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 2.61}, {'id': '31_phase=6', 'cost': 5910.0, 'gain': 10.2, 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 5910.0, 'raw_cost': 5910.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 2.61}, {'id': '32_phase=6', 'cost': 5100.0, 'gain': 8.0, 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 5100.0, 'raw_cost': 5100.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 2.0}, {'id': '33_phase=6', 'cost': 6098.0, 'gain': 8.0, 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 6098.0, 'raw_cost': 6098.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 2.175}, {'id': '34_phase=6', 'cost': 5725.0, 'gain': 8.0, 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 5725.0, 'raw_cost': 5725.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 2.18}, {'id': '35_phase=6', 'cost': 5040.0, 'gain': 6.0, 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 5040.0, 'raw_cost': 5040.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 1.6}, {'id': '36_phase=6', 'cost': 5828.0, 'gain': 7.0, 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 5828.0, 'raw_cost': 5828.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 1.74}, {'id': '37_phase=6', 'cost': 5540.0, 'gain': 7.0, 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 5540.0, 'raw_cost': 5540.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 1.74} ] ] return components def test_budget_and_target_gain_strategy_case_1_try_min_cost_with_constraints(self, components): budget = 5000 target_gain = 11.5 opt = StrategicOptimiser( components=components, target_gain=target_gain, budget=budget, ) opt.solve() # check strategy used assert opt.strategy_used.value == "case_1_try_min_cost_with_constraints" # Check the solution values assert opt.solution_cost == 4398.75 assert opt.solution_gain == 12 def test_budget_and_target_gain_expecting_case_1_solve_max_gain_under_budget_strategy(self, components): budget = 4000 target_gain = 11.5 opt = StrategicOptimiser( components=components, target_gain=target_gain, budget=budget, ) opt.solve() # We expect to use case 1, but we won't be able to meet the target gain, so we should get the best solution # possible within the budget. We end up with an infeasible solution when we try # case_1_try_min_cost_with_constraints assert opt.strategy_used.value == "case_1_solve_max_gain_under_budget" assert opt.solution_cost == 1477.0680000000002 assert opt.solution_gain == 10.8 def test_just_gain_expecting_case_3_solve_min_cost_for_target_strategy(self, components): budget = None target_gain = 11.5 opt = StrategicOptimiser( components=components, target_gain=target_gain, budget=budget, ) opt.solve() # Should be case 3 - minimise cost for target gain assert opt.strategy_used.value == "case_3_solve_min_cost_for_target" assert opt.solution_cost == 4398.75 assert opt.solution_gain == 12 def test_just_gain_of_20_expecting_case_3_solve_min_cost_for_target_strategy(self, components): budget = None target_gain = 20 opt = StrategicOptimiser( components=components, target_gain=target_gain, budget=budget, ) opt.solve() # Should be case 3 - minimise cost for target gain assert opt.strategy_used.value == "case_3_solve_min_cost_for_target" assert opt.solution_cost == 5962.5 assert opt.solution_gain == 20.2 def test_just_budget_expecting_case_2_solve_max_gain_under_budget_strategy(self, components): budget = 10000 target_gain = None opt = StrategicOptimiser( components=components, target_gain=target_gain, budget=budget, ) opt.solve() # Should be case 2 - minimise cost for target gain assert opt.strategy_used.value == "case_2_solve_max_gain_under_budget" assert opt.solution_cost == 7787.068 assert opt.solution_gain == 28.8 class TestCheckNeedsVentilation: def measure_types_includes_ventilation_no_existing_ventilation(self): property_measure_types = {'mechanical_ventilation', 'cavity_wall_insulation', 'suspended_floor_insulation', 'secondary_heating', 'loft_insulation', 'heating', 'low_energy_lighting'} measures_needing_ventilation = ['internal_wall_insulation', 'external_wall_insulation', 'cavity_wall_insulation'] has_ventilation = False ventilation_included = True result = optimiser_functions.check_needs_ventilation( property_measure_types, measures_needing_ventilation, has_ventilation, ventilation_included ) assert result == True def measure_types_includes_ventilation_existing_ventilation(self): property_measure_types = {'mechanical_ventilation', 'cavity_wall_insulation', 'suspended_floor_insulation', 'secondary_heating', 'loft_insulation', 'heating', 'low_energy_lighting'} measures_needing_ventilation = ['internal_wall_insulation', 'external_wall_insulation', 'cavity_wall_insulation'] has_ventilation = True ventilation_included = True result = optimiser_functions.check_needs_ventilation( property_measure_types, measures_needing_ventilation, has_ventilation, ventilation_included ) assert result == False def measure_types_includes_ventilation_existing_ventilation(self): property_measure_types_without_ventilation = { 'cavity_wall_insulation', 'suspended_floor_insulation', 'secondary_heating', 'loft_insulation', 'heating', 'low_energy_lighting' } measures_needing_ventilation = ['internal_wall_insulation', 'external_wall_insulation', 'cavity_wall_insulation'] has_ventilation = False ventilation_included = True result = optimiser_functions.check_needs_ventilation( property_measure_types_without_ventilation, measures_needing_ventilation, has_ventilation, ventilation_included ) assert result == False class TestOptimiseWithScenarios: def test_zero_gain(self, property_instance): input_measures = [ [ {'already_installed': False, 'id': '0_phase=0', 'type': 'internal_wall_insulation+mechanical_ventilation', 'gain': np.float64(2.0), 'cost': 16901.01977922431} ], [ {'already_installed': False, 'id': '1_phase=1', 'type': 'loft_insulation', 'gain': 0, 'cost': 1197.0}, ], [ {'already_installed': False, 'id': '5_phase=3', 'type': 'suspended_floor_insulation', 'gain': 1, 'cost': 5343.75}], [ {'already_installed': False, 'id': '6_phase=4', 'type': 'time_temperature_zone_control', 'gain': np.float64(0.9000000000000057), 'cost': 1009.5600000000001}, {'already_installed': False, 'id': '7_phase=4', 'type': 'air_source_heat_pump', 'gain': np.float64(6.9), 'cost': 18979.9}], [ {'already_installed': False, 'id': '8_phase=5', 'type': 'solar_pv', 'gain': np.float64(9.0), 'cost': 5420.0, "has_battery": False}, {'already_installed': False, 'id': '9_phase=5', 'type': 'solar_pv', 'gain': np.float64(9.0), 'cost': 6210.0, "has_battery": False}, ] ] solutions = optimise_with_scenarios( p=property_instance, input_measures=input_measures, budget=None, target_gain=0, enforce_heat_pump_insulation=True, enforce_fabric_first=False, already_installed_sap=0, # To be passed to output ) assert solutions.empty def test_ashp_needing_cwi_first(self, property_instance): input_measures = [ [ {'id': '0_phase=0', 'cost': 1653.5495595376553, 'gain': 1, 'type': 'cavity_wall_insulation+mechanical_ventilation', 'already_installed': False}, {'id': '1_phase=0', 'cost': 1535.3279855335845, 'gain': 1, 'type': 'cavity_wall_insulation+mechanical_ventilation', 'already_installed': False}, {'id': '2_phase=0', 'cost': 1801.326527042744, 'gain': 1, 'type': 'cavity_wall_insulation+mechanical_ventilation', 'already_installed': False}, {'id': '3_phase=0', 'cost': 1505.7725920325668, 'gain': 1, 'type': 'cavity_wall_insulation+mechanical_ventilation', 'already_installed': False} ], [ {'id': '4_phase=1', 'cost': 766.5, 'gain': 0, 'type': 'loft_insulation', 'already_installed': False}, {'id': '5_phase=1', 'cost': 657.0, 'gain': 0, 'type': 'loft_insulation', 'already_installed': False}, {'id': '6_phase=1', 'cost': 547.5, 'gain': 0, 'type': 'loft_insulation', 'already_installed': False} ], [ {'id': '8_phase=3', 'cost': 7.0, 'gain': 0, 'type': 'low_energy_lighting', 'already_installed': False} ], [ {'id': '9_phase=4', 'cost': 1009.5600000000001, 'gain': np.float64(0.3), 'type': 'time_temperature_zone_control', 'already_installed': False}, {'id': '10_phase=4', 'cost': 18979.9, 'gain': np.float64(7.5), 'type': 'air_source_heat_pump', 'already_installed': False} ], [ {'id': '11_phase=5', 'cost': 150.0, 'gain': np.float64(3.3), 'type': 'secondary_heating', 'already_installed': False} ], [ {'id': '12_phase=6', 'cost': 5420.0, 'gain': np.float64(15.4), 'type': 'solar_pv', 'already_installed': False, "has_battery": False}, {'id': '13_phase=6', 'cost': 6210.0, 'gain': np.float64(15.4), 'type': 'solar_pv', 'already_installed': False, "has_battery": False} ] ] solutions = optimise_with_scenarios( p=property_instance, input_measures=input_measures, budget=None, target_gain=7.5, enforce_heat_pump_insulation=True, enforce_fabric_first=False, already_installed_sap=0, # To be passed to output ) # heat pump solutions heat_pump_solutions = solutions[solutions["scenario"] == "heat_pump_with_insulation"] assert len(heat_pump_solutions) == 12 for x in heat_pump_solutions["items"].values: res = [y["type"] for y in x] # All results should include loft & CWI assert "loft_insulation" in res assert "cavity_wall_insulation+mechanical_ventilation" in res def test_fabric_first(self, property_instance): input_measures = [ [{'id': '0_phase=0', 'cost': 1653.5495595376553, 'gain': 1, 'type': 'cavity_wall_insulation+mechanical_ventilation', 'innovation_uplift': 0, 'cost_minus_uplift': 1653.5495595376553, 'raw_cost': 1093.5495595376553, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 0}, {'id': '1_phase=0', 'cost': 1535.3279855335845, 'gain': 1, 'type': 'cavity_wall_insulation+mechanical_ventilation', 'innovation_uplift': 0, 'cost_minus_uplift': 1535.3279855335845, 'raw_cost': 975.3279855335845, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 0}, {'id': '2_phase=0', 'cost': 1801.326527042744, 'gain': 1, 'type': 'cavity_wall_insulation+mechanical_ventilation', 'innovation_uplift': 0, 'cost_minus_uplift': 1801.326527042744, 'raw_cost': 1241.326527042744, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 0}, {'id': '3_phase=0', 'cost': 1505.7725920325668, 'gain': 1, 'type': 'cavity_wall_insulation+mechanical_ventilation', 'innovation_uplift': 0, 'cost_minus_uplift': 1505.7725920325668, 'raw_cost': 945.7725920325668, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 0}], [{'id': '4_phase=1', 'cost': 766.5, 'gain': 1, 'type': 'loft_insulation', 'innovation_uplift': 0, 'cost_minus_uplift': 766.5, 'raw_cost': 766.5, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 0}, {'id': '5_phase=1', 'cost': 657.0, 'gain': 1, 'type': 'loft_insulation', 'innovation_uplift': 0, 'cost_minus_uplift': 657.0, 'raw_cost': 657.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 0}, {'id': '6_phase=1', 'cost': 547.5, 'gain': 1, 'type': 'loft_insulation', 'innovation_uplift': 0, 'cost_minus_uplift': 547.5, 'raw_cost': 547.5, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 0}], [{'id': '8_phase=3', 'cost': 7.0, 'gain': 1, 'type': 'low_energy_lighting', 'innovation_uplift': 0, 'cost_minus_uplift': 7.0, 'raw_cost': 7.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 0}], [{'id': '9_phase=4', 'cost': 1009.5600000000001, 'gain': np.float64(0.3), 'type': 'time_temperature_zone_control', 'innovation_uplift': 0, 'cost_minus_uplift': 1009.5600000000001, 'raw_cost': 1009.5600000000001, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 0}, {'id': '10_phase=4', 'cost': 18979.9, 'gain': np.float64(7.5), 'type': 'air_source_heat_pump', 'innovation_uplift': 0, 'cost_minus_uplift': 18979.9, 'raw_cost': 18979.9, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 0}], [{'id': '11_phase=5', 'cost': 150.0, 'gain': np.float64(3.3), 'type': 'secondary_heating', 'innovation_uplift': 0, 'cost_minus_uplift': 150.0, 'raw_cost': 150.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 0}], [{'id': '12_phase=6', 'cost': 5420.0, 'gain': np.float64(15.4), 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 5420.0, 'raw_cost': 5420.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 3.6}, {'id': '13_phase=6', 'cost': 6210.0, 'gain': np.float64(15.4), 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 6210.0, 'raw_cost': 6210.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': True, 'array_size': 3.6}, {'id': '14_phase=6', 'cost': 6820.0, 'gain': np.float64(15.4), 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 6820.0, 'raw_cost': 6820.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': True, 'array_size': 3.6}, {'id': '15_phase=6', 'cost': 7202.0, 'gain': np.float64(15.9), 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 7202.0, 'raw_cost': 7202.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 3.915}, {'id': '16_phase=6', 'cost': 6495.0, 'gain': np.float64(15.9), 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 6495.0, 'raw_cost': 6495.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 3.92}, {'id': '17_phase=6', 'cost': 7285.0, 'gain': np.float64(15.9), 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 7285.0, 'raw_cost': 7285.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': True, 'array_size': 3.92}, {'id': '18_phase=6', 'cost': 7895.0, 'gain': np.float64(15.9), 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 7895.0, 'raw_cost': 7895.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': True, 'array_size': 3.92}, {'id': '19_phase=6', 'cost': 5520.0, 'gain': np.float64(16.7), 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 5520.0, 'raw_cost': 5520.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 4.0}, {'id': '20_phase=6', 'cost': 6310.0, 'gain': np.float64(16.7), 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 6310.0, 'raw_cost': 6310.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': True, 'array_size': 4.0}, {'id': '21_phase=6', 'cost': 6920.0, 'gain': np.float64(16.7), 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 6920.0, 'raw_cost': 6920.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': True, 'array_size': 4.0}, {'id': '22_phase=6', 'cost': 5320.0, 'gain': np.float64(13.6), 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 5320.0, 'raw_cost': 5320.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 3.2}, {'id': '23_phase=6', 'cost': 6110.0, 'gain': np.float64(13.6), 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 6110.0, 'raw_cost': 6110.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': True, 'array_size': 3.2}, {'id': '24_phase=6', 'cost': 6720.0, 'gain': np.float64(13.6), 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 6720.0, 'raw_cost': 6720.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': True, 'array_size': 3.2}, {'id': '25_phase=6', 'cost': 6932.0, 'gain': np.float64(15.4), 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 6932.0, 'raw_cost': 6932.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 3.48}, {'id': '26_phase=6', 'cost': 6295.0, 'gain': np.float64(15.4), 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 6295.0, 'raw_cost': 6295.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 3.48}, {'id': '27_phase=6', 'cost': 7085.0, 'gain': np.float64(15.4), 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 7085.0, 'raw_cost': 7085.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': True, 'array_size': 3.48}, {'id': '28_phase=6', 'cost': 7695.0, 'gain': np.float64(15.4), 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 7695.0, 'raw_cost': 7695.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': True, 'array_size': 3.48}, {'id': '29_phase=6', 'cost': 5220.0, 'gain': np.float64(12.2), 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 5220.0, 'raw_cost': 5220.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 2.8}, {'id': '30_phase=6', 'cost': 6662.0, 'gain': np.float64(12.8), 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 6662.0, 'raw_cost': 6662.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 3.045}, {'id': '31_phase=6', 'cost': 6095.0, 'gain': np.float64(12.8), 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 6095.0, 'raw_cost': 6095.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 3.05}, {'id': '32_phase=6', 'cost': 5160.0, 'gain': np.float64(10.1), 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 5160.0, 'raw_cost': 5160.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 2.4}, {'id': '33_phase=6', 'cost': 6392.0, 'gain': np.float64(10.1), 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 6392.0, 'raw_cost': 6392.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 2.61}, {'id': '34_phase=6', 'cost': 5910.0, 'gain': np.float64(10.1), 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 5910.0, 'raw_cost': 5910.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 2.61}, {'id': '35_phase=6', 'cost': 5100.0, 'gain': np.float64(8.0), 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 5100.0, 'raw_cost': 5100.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 2.0}, {'id': '36_phase=6', 'cost': 6098.0, 'gain': np.float64(9.1), 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 6098.0, 'raw_cost': 6098.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 2.175}, {'id': '37_phase=6', 'cost': 5725.0, 'gain': np.float64(9.1), 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 5725.0, 'raw_cost': 5725.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 2.18}, {'id': '38_phase=6', 'cost': 5040.0, 'gain': np.float64(7.0), 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 5040.0, 'raw_cost': 5040.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 1.6}, {'id': '39_phase=6', 'cost': 5828.0, 'gain': np.float64(7.0), 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 5828.0, 'raw_cost': 5828.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 1.74}, {'id': '40_phase=6', 'cost': 5540.0, 'gain': np.float64(7.0), 'type': 'solar_pv', 'innovation_uplift': 0, 'cost_minus_uplift': 5540.0, 'raw_cost': 5540.0, 'partial_project_funding': 0, 'partial_project_score': 0, 'uplift_project_score': 0, 'already_installed': False, 'has_battery': False, 'array_size': 1.74}] ] solutions = optimise_with_scenarios( p=property_instance, input_measures=input_measures, budget=None, target_gain=7.5, enforce_heat_pump_insulation=True, enforce_fabric_first=True, already_installed_sap=0, # To be passed to output ) assert solutions.shape[0] == 1 items = solutions["items"].values[0] types = [x["type"] for x in items] assert types == ['cavity_wall_insulation+mechanical_ventilation', 'loft_insulation', 'solar_pv']