diff --git a/pyomo/contrib/alternative_solutions/solnpool.py b/pyomo/contrib/alternative_solutions/solnpool.py index 5c75a6261c3..d8a1d873a3d 100644 --- a/pyomo/contrib/alternative_solutions/solnpool.py +++ b/pyomo/contrib/alternative_solutions/solnpool.py @@ -29,6 +29,7 @@ def gurobi_generate_solutions( abs_opt_gap=None, solver_options={}, tee=False, + pool_search_mode=2, ): """ Finds alternative optimal solutions for discrete variables using Gurobi's @@ -56,6 +57,10 @@ def gurobi_generate_solutions( Solver option-value pairs to be passed to the Gurobi solver. tee : boolean Boolean indicating that the solver output should be displayed. + pool_search_mode : 1 or 2 + The generation method for filling the pool. + This parameter maps to the PoolSearchMode in gurobi. + Method designed to work with value 2 as optimality ordered. Returns ------- @@ -69,10 +74,20 @@ def gurobi_generate_solutions( if not opt.available(): raise ApplicationError("Solver (gurobi) not available") + assert num_solutions >= 1, "num_solutions must be positive integer" + if num_solutions == 1: + logger.warning("Running alternative_solutions method to find only 1 solution!") + + assert pool_search_mode in [1, 2], "pool_search_mode must be 1 or 2" + if pool_search_mode == 1: + logger.warning( + "Running gurobi_solnpool with PoolSearchMode=1, best effort search may lead to unexpected behavior" + ) + opt.config.stream_solver = tee opt.config.load_solution = False opt.gurobi_options["PoolSolutions"] = num_solutions - opt.gurobi_options["PoolSearchMode"] = 2 + opt.gurobi_options["PoolSearchMode"] = pool_search_mode if rel_opt_gap is not None: opt.gurobi_options["PoolGap"] = rel_opt_gap if abs_opt_gap is not None: diff --git a/pyomo/contrib/alternative_solutions/tests/test_solnpool.py b/pyomo/contrib/alternative_solutions/tests/test_solnpool.py index 5fef32facc9..ec6b3bc5c93 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_solnpool.py +++ b/pyomo/contrib/alternative_solutions/tests/test_solnpool.py @@ -48,6 +48,26 @@ def test_ip_feasibility(self): unique_solns_by_obj = [val for val in Counter(objectives).values()] np.testing.assert_array_almost_equal(unique_solns_by_obj, actual_solns_by_obj) + def test_invalid_search_mode(self): + """ + Confirm that an exception is thrown with pool_search_mode not in [1,2] + """ + m = tc.get_triangle_ip() + with self.assertRaisesRegex(AssertionError, "pool_search_mode must be 1 or 2"): + gurobi_generate_solutions(m, pool_search_mode=0) + + @unittest.skipIf(not numpy_available, "Numpy not installed") + def test_ip_num_solutions_best_effort(self): + """ + Enumerate solutions for an ip: triangle_ip. + Test best effort mode in solution pool. + + Check that the correct number of alternate solutions are found. + """ + m = tc.get_triangle_ip() + results = gurobi_generate_solutions(m, pool_search_mode=1, num_solutions=8) + assert len(results) >= 1, 'Need to find some solutions' + @unittest.skipIf(not numpy_available, "Numpy not installed") def test_ip_num_solutions(self): """