Skip to content
Open
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
83 changes: 72 additions & 11 deletions pyomo/contrib/gdpopt/branch_and_bound.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
import traceback

from pyomo.common.collections import ComponentMap
from pyomo.common.config import document_kwargs_from_configdict
from pyomo.common.config import (
ConfigBlock,
ConfigValue,
document_kwargs_from_configdict,
)
from pyomo.common.errors import InfeasibleConstraintException
from pyomo.contrib.fbbt.fbbt import fbbt
from pyomo.contrib.gdpopt.algorithm_base_class import _GDPoptAlgorithm
Expand All @@ -32,6 +36,7 @@
_add_nlp_solve_configs,
)
from pyomo.contrib.gdpopt.nlp_initialization import restore_vars_to_original_values
from pyomo.contrib.gdpopt.solve_subproblem import detect_unfixed_discrete_vars
from pyomo.contrib.gdpopt.util import (
copy_var_list_values,
SuppressInfeasibleWarning,
Expand Down Expand Up @@ -76,6 +81,26 @@ class GDP_LBB_Solver(_GDPoptAlgorithm):
CONFIG = _GDPoptAlgorithm.CONFIG()
_add_mip_solver_configs(CONFIG)
_add_nlp_solver_configs(CONFIG, default_solver='ipopt')
CONFIG.declare(
"relaxed_nlp_solver",
ConfigValue(
default=None,
description="""
Continuous nonlinear solver to use for transformed LBB node
subproblems with no unfixed discrete variables. If unset, LBB uses
the relevant mixed-integer node solver to preserve existing global
bounding behavior.""",
),
)
CONFIG.declare(
"relaxed_nlp_solver_args",
ConfigBlock(
description="""
Keyword arguments to send to the relaxed NLP subsolver solve()
invocation.""",
implicit=True,
),
)
_add_nlp_solve_configs(
CONFIG, default_nlp_init_method=restore_vars_to_original_values
)
Expand Down Expand Up @@ -413,6 +438,40 @@ def _evaluate_node(self, node_data, node_model, config):
)
return new_node_data

def _get_rnGDP_subproblem_solver(self, subproblem, config, discrete_solver_name):
unfixed_discrete_vars = detect_unfixed_discrete_vars(subproblem)
discrete_solver = getattr(config, discrete_solver_name)
discrete_solver_args_name = discrete_solver_name + "_args"
discrete_solver_args = dict(getattr(config, discrete_solver_args_name))
if len(unfixed_discrete_vars) == 0:
if config.relaxed_nlp_solver is not None:
config.logger.debug(
"Transformed node subproblem has no unfixed discrete variables. "
"Solving with relaxed NLP solver %s." % config.relaxed_nlp_solver
)
return config.relaxed_nlp_solver, dict(config.relaxed_nlp_solver_args)
else:
config.logger.debug(
"Transformed node subproblem has no unfixed discrete variables, "
"but relaxed_nlp_solver is not specified. Solving with "
"mixed-integer solver %s." % discrete_solver
)
return discrete_solver, discrete_solver_args
else:
config.logger.debug(
"Transformed node subproblem has unfixed discrete variables: %s. "
"Solving with mixed-integer solver %s."
% (", ".join(v.name for v in unfixed_discrete_vars), discrete_solver)
)
return discrete_solver, discrete_solver_args

def _apply_rnGDP_subproblem_time_limit(self, solver_name, solver_args, config):
if config.time_limit is not None and solver_name == 'gams':
elapsed = get_main_elapsed_time(self.timing)
remaining = max(config.time_limit - elapsed, 1)
solver_args['add_options'] = solver_args.get('add_options', [])
solver_args['add_options'].append('option reslim=%s;' % remaining)

def _solve_rnGDP_subproblem(self, model, config):
subproblem = TransformationFactory('gdp.bigm').create_using(model)
obj_sense_correction = self.objective_sense != minimize
Expand All @@ -432,15 +491,13 @@ def _solve_rnGDP_subproblem(self, model, config):
ignore_integrality=True,
)
return float('inf'), float('inf')
minlp_args = dict(config.minlp_solver_args)
if config.time_limit is not None and config.minlp_solver == 'gams':
elapsed = get_main_elapsed_time(self.timing)
remaining = max(config.time_limit - elapsed, 1)
minlp_args['add_options'] = minlp_args.get('add_options', [])
minlp_args['add_options'].append('option reslim=%s;' % remaining)
result = SolverFactory(config.minlp_solver).solve(
subproblem, **minlp_args
solver_name, solver_args = self._get_rnGDP_subproblem_solver(
subproblem, config, 'minlp_solver'
)
self._apply_rnGDP_subproblem_time_limit(
solver_name, solver_args, config
)
result = SolverFactory(solver_name).solve(subproblem, **solver_args)
except RuntimeError as e:
config.logger.warning(
"Solver encountered RuntimeError. Treating as infeasible. "
Expand Down Expand Up @@ -533,9 +590,13 @@ def _solve_local_rnGDP_subproblem(self, model, config):

try:
with SuppressInfeasibleWarning():
result = SolverFactory(config.local_minlp_solver).solve(
subproblem, **config.local_minlp_solver_args
solver_name, solver_args = self._get_rnGDP_subproblem_solver(
subproblem, config, 'local_minlp_solver'
)
self._apply_rnGDP_subproblem_time_limit(
solver_name, solver_args, config
)
result = SolverFactory(solver_name).solve(subproblem, **solver_args)
except RuntimeError as e:
config.logger.warning(
"Solver encountered RuntimeError. Treating as infeasible. "
Expand Down
89 changes: 87 additions & 2 deletions pyomo/contrib/gdpopt/tests/test_LBB.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,25 @@
from pyomo.common.fileutils import import_file
from pyomo.common.log import LoggingIntercept
import pyomo.contrib.gdpopt.tests.common_tests as ct
from pyomo.contrib.gdpopt.branch_and_bound import GDP_LBB_Solver
from pyomo.contrib.gdpopt.create_oa_subproblems import (
add_algebraic_variable_list,
add_util_block,
)
from pyomo.contrib.satsolver.satsolver import z3_available
from pyomo.environ import SolverFactory, value, ConcreteModel, Var, Objective, maximize
from pyomo.environ import (
Binary,
Constraint,
SolverFactory,
value,
ConcreteModel,
Var,
Objective,
maximize,
minimize,
)
from pyomo.gdp import Disjunction
from pyomo.opt import TerminationCondition
from pyomo.opt import SolverResults, TerminationCondition

currdir = dirname(abspath(__file__))
exdir = normpath(join(currdir, '..', '..', '..', '..', 'examples', 'gdp'))
Expand All @@ -35,6 +50,76 @@
)


@unittest.skipUnless(
SolverFactory('glpk').available(exception_flag=False)
and SolverFactory('ipopt').available(exception_flag=False),
"The LBB node dispatch tests require GLPK and Ipopt",
)
class TestGDPoptLBBNodeSolverDispatch(unittest.TestCase):
def _make_lbb_solver(self, model):
solver = GDP_LBB_Solver()
solver.pyomo_results = SolverResults()
solver.pyomo_results.problem.sense = minimize
solver.original_util_block = add_util_block(model)
add_algebraic_variable_list(solver.original_util_block)
return solver

def _make_config(self, solver, relaxed_nlp_solver=None):
config = solver.CONFIG()
config.minlp_solver = 'glpk'
config.nlp_solver = 'sentinel_nlp'
config.local_minlp_solver = 'glpk'
config.relaxed_nlp_solver = relaxed_nlp_solver
config.integer_tolerance = 1e-5
config.time_limit = None
return config

def test_continuous_node_subproblem_uses_relaxed_nlp_solver(self):
m = ConcreteModel()
m.x = Var(bounds=(0, 2), initialize=1.5)
m.obj = Objective(expr=(m.x - 1) ** 2)

solver = self._make_lbb_solver(m)
config = self._make_config(solver, relaxed_nlp_solver='ipopt')
solver._solve_rnGDP_subproblem(m, config)

self.assertAlmostEqual(value(m.x), 1.0, places=6)

def test_continuous_node_subproblem_defaults_to_minlp_solver(self):
m = ConcreteModel()
m.x = Var(bounds=(1, 2))
m.obj = Objective(expr=m.x)

solver = self._make_lbb_solver(m)
config = self._make_config(solver)
solver._solve_rnGDP_subproblem(m, config)

self.assertAlmostEqual(value(m.x), 1.0)

def test_mixed_integer_node_subproblem_uses_minlp_solver(self):
m = ConcreteModel()
m.y = Var(domain=Binary)
m.c = Constraint(expr=m.y >= 0.5)
m.obj = Objective(expr=m.y)

solver = self._make_lbb_solver(m)
config = self._make_config(solver, relaxed_nlp_solver='ipopt')
solver._solve_rnGDP_subproblem(m, config)

self.assertAlmostEqual(value(m.y), 1.0)

def test_continuous_local_node_subproblem_uses_relaxed_nlp_solver(self):
m = ConcreteModel()
m.x = Var(bounds=(0, 2), initialize=1.5)
m.obj = Objective(expr=(m.x - 1) ** 2)

solver = self._make_lbb_solver(m)
config = self._make_config(solver, relaxed_nlp_solver='ipopt')
solver._solve_local_rnGDP_subproblem(m, config)

self.assertAlmostEqual(value(m.x), 1.0, places=6)


@unittest.skipUnless(
solver_available, "Required subsolver %s is not available" % (minlp_solver,)
)
Expand Down
Loading