Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions pyomo/contrib/alternative_solutions/solnpool.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,14 @@ def gurobi_generate_solutions(
):
"""
Finds alternative optimal solutions for discrete variables using Gurobi's
built-in Solution Pool capability. See the Gurobi Solution Pool
documentation for additional details.
built-in Solution Pool capability.

This method defaults to the optimality-enforced discovery method with PoolSearchMode = 2.
There are two other options, standard single optimal solution (PoolSearchMode = 0) and
best-effort discovery with no guarantees (PoolSearchMode = 1). Please consult the Gurobi
documentation on PoolSearchMode for details on impact on Gurobi results.
Changes to this mode can be made by included PoolSearchMode set to the intended value
in solver_options.

Parameters
----------
Expand Down Expand Up @@ -69,16 +75,29 @@ 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!")

opt.config.stream_solver = tee
opt.config.load_solution = False
opt.gurobi_options["PoolSolutions"] = num_solutions
opt.gurobi_options["PoolSearchMode"] = 2
if rel_opt_gap is not None:
opt.gurobi_options["PoolGap"] = rel_opt_gap
if abs_opt_gap is not None:
opt.gurobi_options["PoolGapAbs"] = abs_opt_gap
for parameter, value in solver_options.items():
opt.gurobi_options[parameter] = value
if "PoolSearchMode" not in opt.gurobi_options:
opt.gurobi_options["PoolSearchMode"] = 2
elif opt.gurobi_options["PoolSearchMode"] == 0:
logger.warning(
"Running gurobi_solnpool with PoolSearchMode=0, this is single search mode and not the intended use case for gurobi_generate_solutions"
)
elif opt.gurobi_options["PoolSearchMode"] == 1:
logger.warning(
"Running gurobi_solnpool with PoolSearchMode=1, best effort search may lead to unexpected behavior"
)
#
# Run gurobi
#
Expand Down
67 changes: 67 additions & 0 deletions pyomo/contrib/alternative_solutions/tests/test_solnpool.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from pyomo.contrib.appsi.solvers import Gurobi

import pyomo.contrib.alternative_solutions.tests.test_cases as tc
from pyomo.common.log import LoggingIntercept

gurobipy_available = Gurobi().available()

Expand Down Expand Up @@ -48,6 +49,72 @@ 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_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()
with LoggingIntercept() as LOG:
results = gurobi_generate_solutions(
m, num_solutions=8, solver_options={"PoolSearchMode": 1}
)
self.assertRegex(
'Running gurobi_solnpool with PoolSearchMode=1, best effort search may lead to unexpected behavior\n',
LOG.getvalue(),
)
assert len(results) >= 1, 'Need to find some solutions'

def test_ip_num_solutions_standard_single_solution_solve(self):
"""
Enumerate solutions for an ip: triangle_ip.
Test single solve mode in solution pool.

Check that the correct number of solutions (1) are found.
This is not the intended use case for this method.
This is a warning check.
"""
m = tc.get_triangle_ip()
with LoggingIntercept() as LOG:
results = gurobi_generate_solutions(
m, num_solutions=8, solver_options={"PoolSearchMode": 0}
)
self.assertRegex(
'Running gurobi_solnpool with PoolSearchMode=0, this is single search mode and not the intended use case for gurobi_generate_solutions\n',
LOG.getvalue(),
)
assert len(results) == 1, 'Need to find only 1 solution'

def test_ip_num_solutions_seeking_one(self):
"""
Enumerate solutions for an ip: triangle_ip.
Test case where only one solution is asked for.

This is not the intended use case for this code.
This is a warning check.
"""
m = tc.get_triangle_ip()
with LoggingIntercept() as LOG:
results = gurobi_generate_solutions(m, num_solutions=1)
self.assertRegex(
'Running alternative_solutions method to find only 1 solution!\n',
LOG.getvalue(),
)
assert len(results) == 1, 'Need to find only 1 solution'

def test_ip_num_solutions_seeking_zero(self):
"""
Enumerate solutions for an ip: triangle_ip.
Test case where zero solutions are asked for to check assert error.
"""
m = tc.get_triangle_ip()
with self.assertRaisesRegex(
AssertionError, "num_solutions must be positive integer"
):
gurobi_generate_solutions(m, num_solutions=0)

@unittest.skipIf(not numpy_available, "Numpy not installed")
def test_ip_num_solutions(self):
"""
Expand Down
Loading