# ____________________________________________________________________________________
#
# Pyomo: Python Optimization Modeling Objects
# Copyright (c) 2008-2026 National Technology and Engineering Solutions of Sandia, LLC
# Under the terms of Contract DE-NA0003525 with National Technology and Engineering
# Solutions of Sandia, LLC, the U.S. Government retains certain rights in this
# software. This software is distributed under the 3-clause BSD License.
# ____________________________________________________________________________________
from pyomo.common.dependencies import attempt_import
from pyomo.solvers.plugins.solvers.gurobi_direct import gurobipy
from pyomo.contrib.mindtpy.cut_generation import add_oa_cuts, add_no_good_cuts
from pyomo.contrib.mcpp.pyomo_mcpp import McCormick as mc, MCPP_Error
from pyomo.repn import generate_standard_repn
import pyomo.core.expr as EXPR
from math import copysign
from pyomo.contrib.mindtpy.util import (
get_integer_solution,
copy_var_list_values,
set_var_valid_value,
)
from pyomo.contrib.gdpopt.util import get_main_elapsed_time, time_code
from pyomo.opt import TerminationCondition as tc
from pyomo.core import minimize, value
from pyomo.core.expr import identify_variables
cplex, cplex_available = attempt_import('cplex')
[docs]
class LazyOACallback_cplex(
cplex.callbacks.LazyConstraintCallback if cplex_available else object
):
"""Inherent class in CPLEX to call Lazy callback."""
[docs]
def copy_lazy_var_list_values(
self, opt, from_list, to_list, config, skip_stale=False, skip_fixed=True
):
"""This function copies variable values from one list to another.
Rounds to Binary/Integer if necessary.
Sets to zero for NonNegativeReals if necessary.
Parameters
----------
opt : SolverFactory
The cplex_persistent solver.
from_list : list
The variable list that provides the values to copy from.
to_list : list
The variable list that needs to set value.
config : ConfigBlock
The specific configurations for MindtPy.
skip_stale : bool, optional
Whether to skip the stale variables, by default False.
skip_fixed : bool, optional
Whether to skip the fixed variables, by default True.
"""
for v_from, v_to in zip(from_list, to_list):
if skip_stale and v_from.stale:
continue # Skip stale variable values.
if skip_fixed and v_to.is_fixed():
continue # Skip fixed variables.
v_val = self.get_values(opt._pyomo_var_to_solver_var_map[v_from])
set_var_valid_value(
v_to,
v_val,
config.integer_tolerance,
config.zero_tolerance,
ignore_integrality=False,
)
[docs]
def add_lazy_oa_cuts(
self,
target_model,
dual_values,
mindtpy_solver,
config,
opt,
linearize_active=True,
linearize_violated=True,
):
"""Linearizes nonlinear constraints; add the OA cuts through CPLEX inherent function self.add()
For nonconvex problems, turn on 'config.add_slack'. Slack variables will always be used for
nonlinear equality constraints.
Parameters
----------
target_model : Pyomo model
The MIP main problem.
dual_values : list
The value of the duals for each constraint.
mindtpy_solver : object
The mindtpy solver class.
config : ConfigBlock
The specific configurations for MindtPy.
opt : SolverFactory
The cplex_persistent solver.
linearize_active : bool, optional
Whether to linearize the active nonlinear constraints, by default True.
linearize_violated : bool, optional
Whether to linearize the violated nonlinear constraints, by default True.
"""
config.logger.debug('Adding OA cuts')
with time_code(mindtpy_solver.timing, 'OA cut generation'):
for index, constr in enumerate(target_model.MindtPy_utils.constraint_list):
if (
constr.body.polynomial_degree()
in mindtpy_solver.mip_constraint_polynomial_degree
):
continue
constr_vars = list(identify_variables(constr.body))
jacs = mindtpy_solver.jacobians
# Equality constraint (makes the problem nonconvex)
if (
constr.has_ub()
and constr.has_lb()
and value(constr.lower) == value(constr.upper)
):
sign_adjust = (
-1 if mindtpy_solver.objective_sense == minimize else 1
)
rhs = constr.lower
# Since CPLEX requires the lazy cuts in CPLEX type,
# we need to transform the pyomo expression into CPLEX expression.
pyomo_expr = copysign(1, sign_adjust * dual_values[index]) * (
sum(
value(jacs[constr][var]) * (var - value(var))
for var in EXPR.identify_variables(constr.body)
)
+ value(constr.body)
- rhs
)
cplex_expr, _ = opt._get_expr_from_pyomo_expr(pyomo_expr)
cplex_rhs = -generate_standard_repn(pyomo_expr).constant
self.add(
constraint=cplex.SparsePair(
ind=cplex_expr.variables, val=cplex_expr.coefficients
),
sense='L',
rhs=cplex_rhs,
)
if (
self.get_solution_source()
== cplex.callbacks.SolutionSource.mipstart_solution
):
mindtpy_solver.mip_start_lazy_oa_cuts.append(
[
cplex.SparsePair(
ind=cplex_expr.variables,
val=cplex_expr.coefficients,
),
'L',
cplex_rhs,
]
)
else: # Inequality constraint (possibly two-sided)
if (
constr.has_ub()
and (
linearize_active
and abs(constr.uslack()) < config.zero_tolerance
)
or (linearize_violated and constr.uslack() < 0)
or (config.linearize_inactive and constr.uslack() > 0)
) or (
'MindtPy_utils.objective_constr' in constr.name
and constr.has_ub()
):
pyomo_expr = sum(
value(jacs[constr][var]) * (var - var.value)
for var in constr_vars
) + value(constr.body)
cplex_rhs = -generate_standard_repn(pyomo_expr).constant
cplex_expr, _ = opt._get_expr_from_pyomo_expr(pyomo_expr)
self.add(
constraint=cplex.SparsePair(
ind=cplex_expr.variables, val=cplex_expr.coefficients
),
sense='L',
rhs=value(constr.upper) + cplex_rhs,
)
if (
self.get_solution_source()
== cplex.callbacks.SolutionSource.mipstart_solution
):
mindtpy_solver.mip_start_lazy_oa_cuts.append(
[
cplex.SparsePair(
ind=cplex_expr.variables,
val=cplex_expr.coefficients,
),
'L',
value(constr.upper) + cplex_rhs,
]
)
if (
constr.has_lb()
and (
linearize_active
and abs(constr.lslack()) < config.zero_tolerance
)
or (linearize_violated and constr.lslack() < 0)
or (config.linearize_inactive and constr.lslack() > 0)
) or (
'MindtPy_utils.objective_constr' in constr.name
and constr.has_lb()
):
pyomo_expr = sum(
value(jacs[constr][var])
* (
var
- self.get_values(opt._pyomo_var_to_solver_var_map[var])
)
for var in constr_vars
) + value(constr.body)
cplex_rhs = -generate_standard_repn(pyomo_expr).constant
cplex_expr, _ = opt._get_expr_from_pyomo_expr(pyomo_expr)
self.add(
constraint=cplex.SparsePair(
ind=cplex_expr.variables, val=cplex_expr.coefficients
),
sense='G',
rhs=value(constr.lower) + cplex_rhs,
)
if (
self.get_solution_source()
== cplex.callbacks.SolutionSource.mipstart_solution
):
mindtpy_solver.mip_start_lazy_oa_cuts.append(
[
cplex.SparsePair(
ind=cplex_expr.variables,
val=cplex_expr.coefficients,
),
'G',
value(constr.lower) + cplex_rhs,
]
)
[docs]
def add_lazy_affine_cuts(self, mindtpy_solver, config, opt):
"""Adds affine cuts using MCPP.
Add affine cuts through CPLEX inherent function self.add().
Parameters
----------
mindtpy_solver : object
The mindtpy solver class.
config : ConfigBlock
The specific configurations for MindtPy.
opt : SolverFactory
The cplex_persistent solver.
"""
with time_code(mindtpy_solver.timing, 'Affine cut generation'):
m = mindtpy_solver.mip
config.logger.debug('Adding affine cuts')
counter = 0
for constr in m.MindtPy_utils.nonlinear_constraint_list:
vars_in_constr = list(identify_variables(constr.body))
if any(var.value is None for var in vars_in_constr):
continue # a variable has no values
# mcpp stuff
try:
mc_eqn = mc(constr.body)
except MCPP_Error as e:
config.logger.error(e, exc_info=True)
config.logger.debug(
'Skipping constraint %s due to MCPP error' % (constr.name)
)
continue # skip to the next constraint
ccSlope = mc_eqn.subcc()
cvSlope = mc_eqn.subcv()
ccStart = mc_eqn.concave()
cvStart = mc_eqn.convex()
concave_cut_valid = True
convex_cut_valid = True
for var in vars_in_constr:
if not var.fixed:
if ccSlope[var] == float('nan') or ccSlope[var] == float('inf'):
concave_cut_valid = False
if cvSlope[var] == float('nan') or cvSlope[var] == float('inf'):
convex_cut_valid = False
if ccStart == float('nan') or ccStart == float('inf'):
concave_cut_valid = False
if cvStart == float('nan') or cvStart == float('inf'):
convex_cut_valid = False
# check if the value of ccSlope and cvSlope all equals zero. if so, we skip this.
if not any(ccSlope.values()):
concave_cut_valid = False
if not any(cvSlope.values()):
convex_cut_valid = False
if not (concave_cut_valid or convex_cut_valid):
continue
ub_int = (
min(value(constr.upper), mc_eqn.upper())
if constr.has_ub()
else mc_eqn.upper()
)
lb_int = (
max(value(constr.lower), mc_eqn.lower())
if constr.has_lb()
else mc_eqn.lower()
)
if concave_cut_valid:
pyomo_concave_cut = (
sum(
ccSlope[var] * (var - var.value)
for var in vars_in_constr
if not var.fixed
)
+ ccStart
)
cplex_concave_rhs = generate_standard_repn(
pyomo_concave_cut
).constant
cplex_concave_cut, _ = opt._get_expr_from_pyomo_expr(
pyomo_concave_cut
)
self.add(
constraint=cplex.SparsePair(
ind=cplex_concave_cut.variables,
val=cplex_concave_cut.coefficients,
),
sense='G',
rhs=lb_int - cplex_concave_rhs,
)
counter += 1
if convex_cut_valid:
pyomo_convex_cut = (
sum(
cvSlope[var] * (var - var.value)
for var in vars_in_constr
if not var.fixed
)
+ cvStart
)
cplex_convex_rhs = generate_standard_repn(pyomo_convex_cut).constant
cplex_convex_cut, _ = opt._get_expr_from_pyomo_expr(
pyomo_convex_cut
)
self.add(
constraint=cplex.SparsePair(
ind=cplex_convex_cut.variables,
val=cplex_convex_cut.coefficients,
),
sense='L',
rhs=ub_int - cplex_convex_rhs,
)
counter += 1
config.logger.debug('Added %s affine cuts' % counter)
[docs]
def add_lazy_no_good_cuts(
self, var_values, mindtpy_solver, config, opt, feasible=False
):
"""Adds no-good cuts.
Add the no-good cuts through Cplex inherent function self.add().
Parameters
----------
var_values : list
The variable values of the incumbent solution, used to generate the cut.
mindtpy_solver : object
The mindtpy solver class.
config : ConfigBlock
The specific configurations for MindtPy.
opt : SolverFactory
The cplex_persistent solver.
feasible : bool, optional
Whether the integer combination yields a feasible or infeasible NLP, by default False.
Raises
------
ValueError
The value of binary variable is not 0 or 1.
"""
if not config.add_no_good_cuts:
return
config.logger.debug('Adding no-good cuts')
with time_code(mindtpy_solver.timing, 'No-good cut generation'):
m = mindtpy_solver.mip
MindtPy = m.MindtPy_utils
int_tol = config.integer_tolerance
binary_vars = [v for v in MindtPy.variable_list if v.is_binary()]
# copy variable values over
for var, val in zip(MindtPy.variable_list, var_values):
if not var.is_binary():
continue
# We don't want to trigger the reset of the global stale
# indicator, so we will set this variable to be "stale",
# knowing that set_value will switch it back to "not
# stale"
var.stale = True
var.set_value(val, skip_validation=True)
# check to make sure that binary variables are all 0 or 1
for v in binary_vars:
if value(abs(v - 1)) > int_tol and value(abs(v)) > int_tol:
raise ValueError(
'Binary {} = {} is not 0 or 1'.format(v.name, value(v))
)
if not binary_vars: # if no binary variables, skip
return
pyomo_no_good_cut = sum(
1 - v for v in binary_vars if value(abs(v - 1)) <= int_tol
) + sum(v for v in binary_vars if value(abs(v)) <= int_tol)
cplex_no_good_rhs = generate_standard_repn(pyomo_no_good_cut).constant
cplex_no_good_cut, _ = opt._get_expr_from_pyomo_expr(pyomo_no_good_cut)
self.add(
constraint=cplex.SparsePair(
ind=cplex_no_good_cut.variables, val=cplex_no_good_cut.coefficients
),
sense='G',
rhs=1 - cplex_no_good_rhs,
)
[docs]
def handle_lazy_main_feasible_solution(self, main_mip, mindtpy_solver, config, opt):
"""This function is called during the branch and bound of main mip, more
exactly when a feasible solution is found and LazyCallback is activated.
Copy the result to working model and update upper or lower bound.
In LP-NLP, upper or lower bound are updated during solving the main problem.
Parameters
----------
main_mip : Pyomo model
The MIP main problem.
mindtpy_solver : object
The mindtpy solver class.
config : ConfigBlock
The specific configurations for MindtPy.
opt : SolverFactory
The cplex_persistent solver.
"""
# proceed. Just need integer values
# this value copy is useful since we need to fix subproblem based on the solution of the main problem
self.copy_lazy_var_list_values(
opt,
main_mip.MindtPy_utils.variable_list,
mindtpy_solver.fixed_nlp.MindtPy_utils.variable_list,
config,
skip_fixed=False,
)
mindtpy_solver.update_dual_bound(self.get_best_objective_value())
config.logger.info(
mindtpy_solver.log_formatter.format(
mindtpy_solver.mip_iter,
'restrLP',
self.get_objective_value(),
mindtpy_solver.primal_bound,
mindtpy_solver.dual_bound,
mindtpy_solver.rel_gap,
get_main_elapsed_time(mindtpy_solver.timing),
)
)
[docs]
def handle_lazy_subproblem_optimal(self, fixed_nlp, mindtpy_solver, config, opt):
"""This function copies the optimal solution of the fixed NLP subproblem to the MIP
main problem(explanation see below), updates bound, adds OA and no-good cuts,
stores incumbent solution if it has been improved.
Parameters
----------
fixed_nlp : Pyomo model
Integer-variable-fixed NLP model.
mindtpy_solver : object
The mindtpy solver class.
config : ConfigBlock
The specific configurations for MindtPy.
opt : SolverFactory
The cplex_persistent solver.
"""
if config.calculate_dual_at_solution:
for c in fixed_nlp.tmp_duals:
if fixed_nlp.dual.get(c, None) is None:
fixed_nlp.dual[c] = fixed_nlp.tmp_duals[c]
elif (
config.nlp_solver == 'cyipopt'
and mindtpy_solver.objective_sense == minimize
):
# TODO: recover the opposite dual when cyipopt issue #2831 is solved.
fixed_nlp.dual[c] = -fixed_nlp.dual[c]
dual_values = list(
fixed_nlp.dual[c] for c in fixed_nlp.MindtPy_utils.constraint_list
)
else:
dual_values = None
main_objective = fixed_nlp.MindtPy_utils.objective_list[-1]
mindtpy_solver.update_primal_bound(value(main_objective.expr))
if mindtpy_solver.primal_bound_improved:
mindtpy_solver.best_solution_found = fixed_nlp.clone()
mindtpy_solver.best_solution_found_time = get_main_elapsed_time(
mindtpy_solver.timing
)
if config.add_no_good_cuts or config.use_tabu_list:
mindtpy_solver.stored_bound.update(
{mindtpy_solver.primal_bound: mindtpy_solver.dual_bound}
)
config.logger.info(
mindtpy_solver.fixed_nlp_log_formatter.format(
'*' if mindtpy_solver.primal_bound_improved else ' ',
mindtpy_solver.nlp_iter,
'Fixed NLP',
value(main_objective.expr),
mindtpy_solver.primal_bound,
mindtpy_solver.dual_bound,
mindtpy_solver.rel_gap,
get_main_elapsed_time(mindtpy_solver.timing),
)
)
# In OA algorithm, OA cuts are generated based on the solution of the subproblem
# We need to first copy the value of variables from the subproblem and then add cuts
# since value(constr.body), value(jacs[constr][var]), value(var) are used in self.add_lazy_oa_cuts()
copy_var_list_values(
fixed_nlp.MindtPy_utils.variable_list,
mindtpy_solver.mip.MindtPy_utils.variable_list,
config,
)
if config.strategy == 'OA':
self.add_lazy_oa_cuts(
mindtpy_solver.mip, dual_values, mindtpy_solver, config, opt
)
if config.add_regularization is not None:
add_oa_cuts(
mindtpy_solver.mip,
dual_values,
mindtpy_solver.jacobians,
mindtpy_solver.objective_sense,
mindtpy_solver.mip_constraint_polynomial_degree,
mindtpy_solver.mip_iter,
config,
mindtpy_solver.timing,
)
elif config.strategy == 'GOA':
self.add_lazy_affine_cuts(mindtpy_solver, config, opt)
if config.add_no_good_cuts:
var_values = list(v.value for v in fixed_nlp.MindtPy_utils.variable_list)
self.add_lazy_no_good_cuts(var_values, mindtpy_solver, config, opt)
[docs]
def handle_lazy_subproblem_infeasible(self, fixed_nlp, mindtpy_solver, config, opt):
"""Solves feasibility NLP subproblem and adds cuts according to the specified strategy.
Parameters
----------
fixed_nlp : Pyomo model
Integer-variable-fixed NLP model.
mindtpy_solver : object
The mindtpy solver class.
config : ConfigBlock
The specific configurations for MindtPy.
opt : SolverFactory
The cplex_persistent solver.
"""
# TODO try something else? Reinitialize with different initial
# value?
config.logger.info('NLP subproblem was locally infeasible.')
mindtpy_solver.nlp_infeasible_counter += 1
if config.calculate_dual_at_solution:
for c in fixed_nlp.MindtPy_utils.constraint_list:
rhs = (0 if c.upper is None else c.upper) + (
0 if c.lower is None else c.lower
)
sign_adjust = 1 if c.upper is None else -1
fixed_nlp.dual[c] = sign_adjust * max(
0, sign_adjust * (rhs - value(c.body))
)
dual_values = list(
fixed_nlp.dual[c] for c in fixed_nlp.MindtPy_utils.constraint_list
)
else:
dual_values = None
config.logger.info('Solving feasibility problem')
feas_subproblem, feas_subproblem_results = (
mindtpy_solver.solve_feasibility_subproblem()
)
# In OA algorithm, OA cuts are generated based on the solution of the subproblem
# We need to first copy the value of variables from the subproblem and then add cuts
copy_var_list_values(
feas_subproblem.MindtPy_utils.variable_list,
mindtpy_solver.mip.MindtPy_utils.variable_list,
config,
)
if config.strategy == 'OA':
self.add_lazy_oa_cuts(
mindtpy_solver.mip, dual_values, mindtpy_solver, config, opt
)
if config.add_regularization is not None:
add_oa_cuts(
mindtpy_solver.mip,
dual_values,
mindtpy_solver.jacobians,
mindtpy_solver.objective_sense,
mindtpy_solver.mip_constraint_polynomial_degree,
mindtpy_solver.mip_iter,
config,
mindtpy_solver.timing,
)
elif config.strategy == 'GOA':
self.add_lazy_affine_cuts(mindtpy_solver, config, opt)
if config.add_no_good_cuts:
var_values = list(v.value for v in fixed_nlp.MindtPy_utils.variable_list)
self.add_lazy_no_good_cuts(var_values, mindtpy_solver, config, opt)
[docs]
def handle_lazy_subproblem_other_termination(
self, fixed_nlp, termination_condition, mindtpy_solver, config
):
"""Handles the result of the latest iteration of solving the NLP subproblem given
a solution that is neither optimal nor infeasible.
Parameters
----------
fixed_nlp : Pyomo model
Integer-variable-fixed NLP model.
termination_condition : Pyomo TerminationCondition
The termination condition of the fixed NLP subproblem.
mindtpy_solver : object
The mindtpy solver class.
config : ConfigBlock
The specific configurations for MindtPy.
Raises
------
ValueError
MindtPy unable to handle the termination condition of the fixed NLP subproblem.
"""
if termination_condition is tc.maxIterations:
# TODO try something else? Reinitialize with different initial value?
config.logger.info(
'NLP subproblem failed to converge within iteration limit.'
)
var_values = list(v.value for v in fixed_nlp.MindtPy_utils.variable_list)
else:
raise ValueError(
'MindtPy unable to handle NLP subproblem termination '
'condition of {}'.format(termination_condition)
)
def __call__(self):
"""This is an inherent function in LazyConstraintCallback in CPLEX.
This function is called whenever an integer solution is found during the branch and bound process.
"""
mindtpy_solver = self.mindtpy_solver
config = self.config
opt = self.opt
main_mip = self.main_mip
mindtpy_solver = self.mindtpy_solver
# The lazy constraint callback may be invoked during MIP start processing. In that case get_solution_source returns mip_start_solution.
# Reference: https://www.ibm.com/docs/en/icos/22.1.1?topic=SSSA5P_22.1.1/ilog.odms.cplex.help/refpythoncplex/html/cplex.callbacks.SolutionSource-class.htm
# Another solution source is user_solution = 118, but it will not be encountered in LazyConstraintCallback.
config.logger.info(
"Solution source: {} (111 node_solution, 117 heuristic_solution, 119 mipstart_solution)".format(
self.get_solution_source()
)
)
# The solution found in MIP start process might be revisited in branch and bound.
# Lazy constraints separated when processing a MIP start will be discarded after that MIP start has been processed.
# This means that the callback may have to separate the same constraint again for the next MIP start or for a solution that is found later in the solution process.
# https://www.ibm.com/docs/en/icos/22.1.1?topic=SSSA5P_22.1.1/ilog.odms.cplex.help/refpythoncplex/html/cplex.callbacks.LazyConstraintCallback-class.htm
# For the MINLP3_simple example, all the solutions are obtained from mip_start (solution source). Therefore, it will not go to a branch and bound process.Cause an error output.
if (
self.get_solution_source()
!= cplex.callbacks.SolutionSource.mipstart_solution
and len(mindtpy_solver.mip_start_lazy_oa_cuts) > 0
):
for constraint, sense, rhs in mindtpy_solver.mip_start_lazy_oa_cuts:
self.add(constraint, sense, rhs)
mindtpy_solver.mip_start_lazy_oa_cuts = []
if mindtpy_solver.should_terminate:
# TODO: check the performance difference if we don't use self.abort() and let cplex terminate by itself.
self.abort()
return
self.handle_lazy_main_feasible_solution(main_mip, mindtpy_solver, config, opt)
if config.add_cuts_at_incumbent:
self.copy_lazy_var_list_values(
opt,
main_mip.MindtPy_utils.variable_list,
mindtpy_solver.mip.MindtPy_utils.variable_list,
config,
)
if config.strategy == 'OA':
# The solution obtained from mip start might be infeasible and even introduce a math domain error, like log(-1).
try:
self.add_lazy_oa_cuts(
mindtpy_solver.mip, None, mindtpy_solver, config, opt
)
except ValueError as e:
config.logger.error(e, exc_info=True)
config.logger.error(
"Usually this error is caused by the MIP start solution causing a math domain error. "
"We will skip it."
)
return
# regularization is activated after the first feasible solution is found.
if (
config.add_regularization is not None
and mindtpy_solver.best_solution_found is not None
):
# The main problem might be unbounded, regularization is activated only when a valid bound is provided.
if (
not mindtpy_solver.dual_bound_improved
and not mindtpy_solver.primal_bound_improved
):
config.logger.debug(
'The bound and the best found solution have neither been improved.'
'We will skip solving the regularization problem and the Fixed-NLP subproblem'
)
mindtpy_solver.primal_bound_improved = False
return
if mindtpy_solver.dual_bound != mindtpy_solver.dual_bound_progress[0]:
mindtpy_solver.add_regularization()
if (
abs(mindtpy_solver.primal_bound - mindtpy_solver.dual_bound)
<= config.absolute_bound_tolerance
):
config.logger.info(
'MindtPy exiting on bound convergence. '
'|Primal Bound: {} - Dual Bound: {}| <= (absolute tolerance {}) \n'.format(
mindtpy_solver.primal_bound,
mindtpy_solver.dual_bound,
config.absolute_bound_tolerance,
)
)
mindtpy_solver.results.solver.termination_condition = tc.optimal
# TODO: check the performance difference if we don't use self.abort() and let cplex terminate by itself.
self.abort()
return
# check if the same integer combination is obtained.
mindtpy_solver.curr_int_sol = get_integer_solution(
mindtpy_solver.fixed_nlp, string_zero=True
)
if mindtpy_solver.curr_int_sol in set(mindtpy_solver.integer_list):
config.logger.debug(
'This integer combination has been explored. '
'We will skip solving the Fixed-NLP subproblem.'
)
mindtpy_solver.primal_bound_improved = False
if config.strategy == 'GOA':
if config.add_no_good_cuts:
var_values = list(
v.value
for v in mindtpy_solver.working_model.MindtPy_utils.variable_list
)
self.add_lazy_no_good_cuts(var_values, mindtpy_solver, config, opt)
return
elif config.strategy == 'OA':
return
else:
mindtpy_solver.integer_list.append(mindtpy_solver.curr_int_sol)
# solve subproblem
# Call the NLP pre-solve callback
with time_code(mindtpy_solver.timing, 'Call before subproblem solve'):
config.call_before_subproblem_solve(mindtpy_solver.fixed_nlp)
# The constraint linearization happens in the handlers
fixed_nlp, fixed_nlp_result = mindtpy_solver.solve_subproblem()
# add oa cuts
if fixed_nlp_result.solver.termination_condition in {
tc.optimal,
tc.locallyOptimal,
tc.feasible,
}:
self.handle_lazy_subproblem_optimal(fixed_nlp, mindtpy_solver, config, opt)
if (
abs(mindtpy_solver.primal_bound - mindtpy_solver.dual_bound)
<= config.absolute_bound_tolerance
):
config.logger.info(
'MindtPy exiting on bound convergence. '
'|Primal Bound: {} - Dual Bound: {}| <= (absolute tolerance {}) \n'.format(
mindtpy_solver.primal_bound,
mindtpy_solver.dual_bound,
config.absolute_bound_tolerance,
)
)
mindtpy_solver.results.solver.termination_condition = tc.optimal
return
elif fixed_nlp_result.solver.termination_condition in {
tc.infeasible,
tc.noSolution,
}:
self.handle_lazy_subproblem_infeasible(
fixed_nlp, mindtpy_solver, config, opt
)
else:
self.handle_lazy_subproblem_other_termination(
fixed_nlp,
fixed_nlp_result.solver.termination_condition,
mindtpy_solver,
config,
)
# Gurobi
[docs]
def LazyOACallback_gurobi(cb_m, cb_opt, cb_where, mindtpy_solver, config):
"""This is a Gurobi callback function defined for LP/NLP based B&B algorithm.
Parameters
----------
cb_m : Pyomo model
The MIP main problem.
cb_opt : SolverFactory
The gurobi_persistent solver.
cb_where : int
An enum member of gurobipy.GRB.Callback.
mindtpy_solver : object
The mindtpy solver class.
config : ConfigBlock
The specific configurations for MindtPy.
"""
if cb_where == gurobipy.GRB.Callback.MIPSOL:
# gurobipy.GRB.Callback.MIPSOL means that an integer solution is found during the branch and bound process
if mindtpy_solver.should_terminate:
cb_opt._solver_model.terminate()
return
cb_opt.cbGetSolution(vars=cb_m.MindtPy_utils.variable_list)
handle_lazy_main_feasible_solution_gurobi(cb_m, cb_opt, mindtpy_solver, config)
if config.add_cuts_at_incumbent:
if config.strategy == 'OA':
add_oa_cuts(
mindtpy_solver.mip,
None,
mindtpy_solver.jacobians,
mindtpy_solver.objective_sense,
mindtpy_solver.mip_constraint_polynomial_degree,
mindtpy_solver.mip_iter,
config,
mindtpy_solver.timing,
cb_opt=cb_opt,
)
# Regularization is activated after the first feasible solution is found.
if (
config.add_regularization is not None
and mindtpy_solver.best_solution_found is not None
):
# The main problem might be unbounded, regularization is activated only when a valid bound is provided.
if (
not mindtpy_solver.dual_bound_improved
and not mindtpy_solver.primal_bound_improved
):
config.logger.debug(
'The bound and the best found solution have neither been improved.'
'We will skip solving the regularization problem and the Fixed-NLP subproblem'
)
mindtpy_solver.primal_bound_improved = False
return
if mindtpy_solver.dual_bound != mindtpy_solver.dual_bound_progress[0]:
mindtpy_solver.add_regularization()
if mindtpy_solver.bounds_converged() or mindtpy_solver.reached_time_limit():
cb_opt._solver_model.terminate()
return
# check if the same integer combination is obtained.
mindtpy_solver.curr_int_sol = get_integer_solution(
mindtpy_solver.fixed_nlp, string_zero=True
)
if mindtpy_solver.curr_int_sol in set(mindtpy_solver.integer_list):
config.logger.debug(
'This integer combination has been explored. '
'We will skip solving the Fixed-NLP subproblem.'
)
mindtpy_solver.primal_bound_improved = False
if config.strategy == 'GOA':
if config.add_no_good_cuts:
var_values = list(
v.value
for v in mindtpy_solver.fixed_nlp.MindtPy_utils.variable_list
)
add_no_good_cuts(
mindtpy_solver.mip,
var_values,
config,
mindtpy_solver.timing,
mip_iter=mindtpy_solver.mip_iter,
cb_opt=cb_opt,
)
return
elif config.strategy == 'OA':
# Refer to the official document of GUROBI.
# Your callback should be prepared to cut off solutions that violate any of your lazy constraints, including those that have already been added. Node solutions will usually respect previously added lazy constraints, but not always.
# https://www.gurobi.com/documentation/current/refman/cs_cb_addlazy.html
# If this happens, MindtPy will look for the index of corresponding cuts, instead of solving the fixed-NLP again.
begin_index, end_index = mindtpy_solver.integer_solution_to_cuts_index[
mindtpy_solver.curr_int_sol
]
for ind in range(begin_index, end_index + 1):
cb_opt.cbLazy(mindtpy_solver.mip.MindtPy_utils.cuts.oa_cuts[ind])
return
else:
mindtpy_solver.integer_list.append(mindtpy_solver.curr_int_sol)
if config.strategy == 'OA':
cut_ind = len(mindtpy_solver.mip.MindtPy_utils.cuts.oa_cuts)
# solve subproblem
# Call the NLP pre-solve callback
with time_code(mindtpy_solver.timing, 'Call before subproblem solve'):
config.call_before_subproblem_solve(mindtpy_solver.fixed_nlp)
# The constraint linearization happens in the handlers
fixed_nlp, fixed_nlp_result = mindtpy_solver.solve_subproblem()
mindtpy_solver.handle_nlp_subproblem_tc(fixed_nlp, fixed_nlp_result, cb_opt)
if config.strategy == 'OA':
# store the cut index corresponding to current integer solution.
mindtpy_solver.integer_solution_to_cuts_index[
mindtpy_solver.curr_int_sol
] = [cut_ind + 1, len(mindtpy_solver.mip.MindtPy_utils.cuts.oa_cuts)]
[docs]
def handle_lazy_main_feasible_solution_gurobi(cb_m, cb_opt, mindtpy_solver, config):
"""This function is called during the branch and bound of main MIP problem,
more exactly when a feasible solution is found and LazyCallback is activated.
Copy the solution to working model and update upper or lower bound.
In LP-NLP, upper or lower bound are updated during solving the main problem.
Parameters
----------
cb_m : Pyomo model
The MIP main problem.
cb_opt : SolverFactory
The gurobi_persistent solver.
mindtpy_solver : object
The mindtpy solver class.
config : ConfigBlock
The specific configurations for MindtPy.
"""
# proceed. Just need integer values
cb_opt.cbGetSolution(vars=cb_m.MindtPy_utils.variable_list)
# this value copy is useful since we need to fix subproblem based on the solution of the main problem
copy_var_list_values(
cb_m.MindtPy_utils.variable_list,
mindtpy_solver.fixed_nlp.MindtPy_utils.variable_list,
config,
skip_fixed=False,
)
copy_var_list_values(
cb_m.MindtPy_utils.variable_list,
mindtpy_solver.mip.MindtPy_utils.variable_list,
config,
)
mindtpy_solver.update_dual_bound(cb_opt.cbGet(gurobipy.GRB.Callback.MIPSOL_OBJBND))
config.logger.info(
mindtpy_solver.log_formatter.format(
mindtpy_solver.mip_iter,
'restrLP',
cb_opt.cbGet(gurobipy.GRB.Callback.MIPSOL_OBJ),
mindtpy_solver.primal_bound,
mindtpy_solver.dual_bound,
mindtpy_solver.rel_gap,
get_main_elapsed_time(mindtpy_solver.timing),
)
)