Source code for pyomo.contrib.mindtpy.single_tree

# ____________________________________________________________________________________
#
# 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), ) )