From 6d442b24542d524c0ff414a6009408d27deb39df Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 20 Sep 2023 22:38:14 -0400 Subject: [PATCH 01/53] disable ipopt warmstart for feasibility subproblem solver --- pyomo/contrib/mindtpy/algorithm_base_class.py | 2 +- pyomo/contrib/mindtpy/util.py | 21 ++++++++++--------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index f3877304adb..c254c5d3f3d 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -2574,7 +2574,7 @@ def initialize_subsolvers(self): self.nlp_opt, config.nlp_solver, config ) set_solver_constraint_violation_tolerance( - self.feasibility_nlp_opt, config.nlp_solver, config + self.feasibility_nlp_opt, config.nlp_solver, config, warm_start=False ) self.set_appsi_solver_update_config() diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index e336715cc8f..068cd61aba1 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -566,7 +566,7 @@ def set_solver_mipgap(opt, solver_name, config): opt.options['add_options'].append('option optcr=%s;' % config.mip_solver_mipgap) -def set_solver_constraint_violation_tolerance(opt, solver_name, config): +def set_solver_constraint_violation_tolerance(opt, solver_name, config, warm_start=True): """Set constraint violation tolerance for solvers. Parameters @@ -600,15 +600,16 @@ def set_solver_constraint_violation_tolerance(opt, solver_name, config): opt.options['add_options'].append( 'constr_viol_tol ' + str(config.zero_tolerance) ) - # Ipopt warmstart options - opt.options['add_options'].append( - 'warm_start_init_point yes\n' - 'warm_start_bound_push 1e-9\n' - 'warm_start_bound_frac 1e-9\n' - 'warm_start_slack_bound_frac 1e-9\n' - 'warm_start_slack_bound_push 1e-9\n' - 'warm_start_mult_bound_push 1e-9\n' - ) + if warm_start: + # Ipopt warmstart options + opt.options['add_options'].append( + 'warm_start_init_point yes\n' + 'warm_start_bound_push 1e-9\n' + 'warm_start_bound_frac 1e-9\n' + 'warm_start_slack_bound_frac 1e-9\n' + 'warm_start_slack_bound_push 1e-9\n' + 'warm_start_mult_bound_push 1e-9\n' + ) elif config.nlp_solver_args['solver'] == 'conopt': opt.options['add_options'].append( 'RTNWMA ' + str(config.zero_tolerance) From a6e92c53e63b9febad79e8a19a5230c8c308328a Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Thu, 21 Sep 2023 01:44:41 -0400 Subject: [PATCH 02/53] create new copy_var_list_values function --- pyomo/contrib/mindtpy/algorithm_base_class.py | 4 ++- pyomo/contrib/mindtpy/single_tree.py | 3 +- pyomo/contrib/mindtpy/util.py | 29 +++++++++++++++++++ 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index c254c5d3f3d..836df9fff78 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -56,7 +56,6 @@ SuppressInfeasibleWarning, _DoNothing, lower_logger_level_to, - copy_var_list_values, get_main_elapsed_time, time_code, ) @@ -81,6 +80,7 @@ set_solver_mipgap, set_solver_constraint_violation_tolerance, update_solver_timelimit, + copy_var_list_values ) single_tree, single_tree_available = attempt_import('pyomo.contrib.mindtpy.single_tree') @@ -866,12 +866,14 @@ def init_rNLP(self, add_oa_cuts=True): self.rnlp.MindtPy_utils.variable_list, self.mip.MindtPy_utils.variable_list, config, + ignore_integrality=True ) if config.init_strategy == 'FP': copy_var_list_values( self.rnlp.MindtPy_utils.variable_list, self.working_model.MindtPy_utils.variable_list, config, + ignore_integrality=True ) self.add_cuts( dual_values=dual_values, diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index 9776920f434..f3be27cbc4c 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -16,9 +16,8 @@ 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 +from pyomo.contrib.mindtpy.util import get_integer_solution, copy_var_list_values from pyomo.contrib.gdpopt.util import ( - copy_var_list_values, get_main_elapsed_time, time_code, ) diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index 068cd61aba1..59490248e49 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -23,6 +23,7 @@ RangeSet, ConstraintList, TransformationFactory, + value ) from pyomo.repn import generate_standard_repn from pyomo.contrib.mcpp.pyomo_mcpp import mcpp_available, McCormick @@ -964,3 +965,31 @@ def generate_norm_constraint(fp_nlp_model, mip_model, config): mip_model.MindtPy_utils.discrete_variable_list, ): fp_nlp_model.norm_constraint.add(nlp_var - mip_var.value <= rhs) + +def copy_var_list_values(from_list, to_list, config, + skip_stale=False, skip_fixed=True, + ignore_integrality=False): + """Copy variable values from one list to another. + Rounds to Binary/Integer if necessary + Sets to zero for NonNegativeReals if necessary + """ + 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. + var_val = value(v_from, exception=False) + rounded_val = int(round(var_val)) + if var_val in v_to.domain: + v_to.set_value(value(v_from, exception=False)) + elif ignore_integrality and v_to.is_integer(): + v_to.set_value(value(v_from, exception=False)) + elif abs(var_val) <= config.zero_tolerance and 0 in v_to.domain: + v_to.set_value(0) + elif v_to.is_integer() and (math.fabs(var_val - rounded_val) <= + config.integer_tolerance): + print('var_val', var_val) + v_to.pprint() + v_to.set_value(rounded_val) + else: + raise From f766c0a9d6f66da8fea8b11f277bb719ef94f48f Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Thu, 21 Sep 2023 19:23:03 -0400 Subject: [PATCH 03/53] update log format --- pyomo/contrib/mindtpy/algorithm_base_class.py | 30 +++++++++++++++++-- pyomo/contrib/mindtpy/util.py | 2 -- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index 836df9fff78..514c2edaedb 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -124,6 +124,9 @@ def __init__(self, **kwds): self.fixed_nlp_log_formatter = ( '{:1}{:>9} {:>15} {:>15g} {:>12g} {:>12g} {:>7.2%} {:>7.2f}' ) + self.infeasible_fixed_nlp_log_formatter = ( + '{:1}{:>9} {:>15} {:>15} {:>12g} {:>12g} {:>7.2%} {:>7.2f}' + ) self.log_note_formatter = ' {:>9} {:>15} {:>15}' # Flag indicating whether the solution improved in the past @@ -1210,7 +1213,18 @@ def handle_subproblem_infeasible(self, fixed_nlp, cb_opt=None): # TODO try something else? Reinitialize with different initial # value? config = self.config - config.logger.info('NLP subproblem was locally infeasible.') + config.logger.info( + self.infeasible_fixed_nlp_log_formatter.format( + ' ', + self.nlp_iter, + 'Fixed NLP', + 'Infeasible', + self.primal_bound, + self.dual_bound, + self.rel_gap, + get_main_elapsed_time(self.timing), + ) + ) self.nlp_infeasible_counter += 1 if config.calculate_dual_at_solution: for c in fixed_nlp.MindtPy_utils.constraint_list: @@ -1232,7 +1246,7 @@ def handle_subproblem_infeasible(self, fixed_nlp, cb_opt=None): # elif var.has_lb() and abs(value(var) - var.lb) < config.absolute_bound_tolerance: # fixed_nlp.ipopt_zU_out[var] = -1 - config.logger.info('Solving feasibility problem') + # config.logger.info('Solving feasibility problem') feas_subproblem, feas_subproblem_results = self.solve_feasibility_subproblem() # TODO: do we really need this? if self.should_terminate: @@ -1366,6 +1380,18 @@ def solve_feasibility_subproblem(self): self.handle_feasibility_subproblem_tc( feas_soln.solver.termination_condition, MindtPy ) + config.logger.info( + self.fixed_nlp_log_formatter.format( + ' ', + self.nlp_iter, + 'Feasibility NLP', + value(feas_subproblem.MindtPy_utils.feas_obj), + self.primal_bound, + self.dual_bound, + self.rel_gap, + get_main_elapsed_time(self.timing), + ) + ) MindtPy.feas_opt.deactivate() for constr in MindtPy.nonlinear_constraint_list: constr.activate() diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index 59490248e49..4a4b77767a9 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -988,8 +988,6 @@ def copy_var_list_values(from_list, to_list, config, v_to.set_value(0) elif v_to.is_integer() and (math.fabs(var_val - rounded_val) <= config.integer_tolerance): - print('var_val', var_val) - v_to.pprint() v_to.set_value(rounded_val) else: raise From 83069253086f5199c35ee22aadf1b1ea85fdf891 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Sat, 23 Sep 2023 15:08:00 -0400 Subject: [PATCH 04/53] add update_solver_timelimit --- pyomo/contrib/mindtpy/algorithm_base_class.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index 514c2edaedb..0eb602bdf7e 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -1618,6 +1618,7 @@ def solve_main(self): # setup main problem self.setup_main() mip_args = self.set_up_mip_solver() + update_solver_timelimit(self.mip_opt, config.mip_solver, self.timing, config) try: main_mip_results = self.mip_opt.solve( @@ -1675,6 +1676,9 @@ def solve_fp_main(self): config = self.config self.setup_fp_main() mip_args = self.set_up_mip_solver() + update_solver_timelimit( + self.mip_opt, config.mip_solver, self.timing, config + ) main_mip_results = self.mip_opt.solve( self.mip, From 816e3d66492da945e01bad7a906810b400e90f71 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Mon, 9 Oct 2023 18:16:09 -0400 Subject: [PATCH 05/53] handle appsi solver unbounded situation --- pyomo/contrib/mindtpy/algorithm_base_class.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index 0eb602bdf7e..89575f5c4f2 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -1644,7 +1644,8 @@ def solve_main(self): "No-good cuts are added and GOA algorithm doesn't converge within the time limit. " 'No integer solution is found, so the CPLEX solver will report an error status. ' ) - return None, None + # Value error will be raised if the MIP problem is unbounded and appsi solver is used when loading solutions. Although the problem is unbounded, a valid result is provided and we do not return None to let the algorithm continue. + return self.mip, main_mip_results if config.solution_pool: main_mip_results._solver_model = self.mip_opt._solver_model main_mip_results._pyomo_var_to_solver_var_map = ( From 5c47040d5b0fee4bb54656e49dff54f670ec6680 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Tue, 10 Oct 2023 15:57:39 -0400 Subject: [PATCH 06/53] add skip_validation when ignore integrality --- pyomo/contrib/mindtpy/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index 4a4b77767a9..4dfb912e611 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -983,7 +983,7 @@ def copy_var_list_values(from_list, to_list, config, if var_val in v_to.domain: v_to.set_value(value(v_from, exception=False)) elif ignore_integrality and v_to.is_integer(): - v_to.set_value(value(v_from, exception=False)) + v_to.set_value(value(v_from, exception=False), skip_validation=True) elif abs(var_val) <= config.zero_tolerance and 0 in v_to.domain: v_to.set_value(0) elif v_to.is_integer() and (math.fabs(var_val - rounded_val) <= From a767f281acd08629aea99f6c616e59b11372d2f5 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Tue, 10 Oct 2023 16:28:58 -0400 Subject: [PATCH 07/53] add special handle for rnlp infeasible --- pyomo/contrib/mindtpy/algorithm_base_class.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index 89575f5c4f2..f6192ad4687 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -829,6 +829,23 @@ def init_rNLP(self, add_oa_cuts=True): if len(results.solution) > 0: self.rnlp.solutions.load_from(results) subprob_terminate_cond = results.solver.termination_condition + + # Sometimes, the NLP solver might be trapped in a infeasible solution if the objective function is nonlinear and partition_obj_nonlinear_terms is True. If this happens, we will use the original objective function instead. + if subprob_terminate_cond == tc.infeasible and config.partition_obj_nonlinear_terms: + config.logger.info( + 'Initial relaxed NLP problem is infeasible. This might be related to partition_obj_nonlinear_terms. Try to solve it again without partitioning nonlinear objective function.') + self.rnlp.MindtPy_utils.objective.deactivate() + self.rnlp.MindtPy_utils.objective_list[0].activate() + results = self.nlp_opt.solve( + self.rnlp, + tee=config.nlp_solver_tee, + load_solutions=config.load_solutions, + **nlp_args, + ) + if len(results.solution) > 0: + self.rnlp.solutions.load_from(results) + subprob_terminate_cond = results.solver.termination_condition + if subprob_terminate_cond in {tc.optimal, tc.feasible, tc.locallyOptimal}: main_objective = MindtPy.objective_list[-1] if subprob_terminate_cond == tc.optimal: From a30cf823fbe9e370f0feb2c7ce5ed659d5db112e Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Tue, 10 Oct 2023 16:30:08 -0400 Subject: [PATCH 08/53] fix bug --- pyomo/contrib/mindtpy/algorithm_base_class.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index f6192ad4687..c254a8db72b 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -1662,7 +1662,10 @@ def solve_main(self): 'No integer solution is found, so the CPLEX solver will report an error status. ' ) # Value error will be raised if the MIP problem is unbounded and appsi solver is used when loading solutions. Although the problem is unbounded, a valid result is provided and we do not return None to let the algorithm continue. - return self.mip, main_mip_results + if 'main_mip_results' in dir(): + return self.mip, main_mip_results + else: + return None, None if config.solution_pool: main_mip_results._solver_model = self.mip_opt._solver_model main_mip_results._pyomo_var_to_solver_var_map = ( From 2a6f1d7c9b41f3d018a6095c86d4d5f54dd2bbdf Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 11 Oct 2023 23:12:28 -0400 Subject: [PATCH 09/53] add comments --- pyomo/contrib/mindtpy/single_tree.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index f3be27cbc4c..8b8e171c577 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -707,8 +707,8 @@ def __call__(self): # 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.debug( - "Solution source: %s (111 node_solution, 117 heuristic_solution, 119 mipstart_solution)".format( + config.logger.info( + "Solution source: {} (111 node_solution, 117 heuristic_solution, 119 mipstart_solution)".format( self.get_solution_source() ) ) @@ -717,6 +717,7 @@ def __call__(self): # 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 From 6bcdadbfcdb9b81abe987aed665c5137c45b7c07 Mon Sep 17 00:00:00 2001 From: Zedong Peng Date: Thu, 26 Oct 2023 21:12:46 -0400 Subject: [PATCH 10/53] fix bug --- pyomo/contrib/mindtpy/algorithm_base_class.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index c254a8db72b..c0afea58a99 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -831,7 +831,7 @@ def init_rNLP(self, add_oa_cuts=True): subprob_terminate_cond = results.solver.termination_condition # Sometimes, the NLP solver might be trapped in a infeasible solution if the objective function is nonlinear and partition_obj_nonlinear_terms is True. If this happens, we will use the original objective function instead. - if subprob_terminate_cond == tc.infeasible and config.partition_obj_nonlinear_terms: + if subprob_terminate_cond == tc.infeasible and config.partition_obj_nonlinear_terms and self.rnlp.MindtPy_utils.objective_list[0].expr.polynomial_degree() not in self.mip_objective_polynomial_degree: config.logger.info( 'Initial relaxed NLP problem is infeasible. This might be related to partition_obj_nonlinear_terms. Try to solve it again without partitioning nonlinear objective function.') self.rnlp.MindtPy_utils.objective.deactivate() From a66f955500686fef09d8605571ef6f5c238101f8 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Sun, 29 Oct 2023 15:04:27 -0400 Subject: [PATCH 11/53] improve copy_var_list_values function --- pyomo/contrib/mindtpy/single_tree.py | 76 ++++++++++++------------- pyomo/contrib/mindtpy/util.py | 83 +++++++++++++++------------- 2 files changed, 83 insertions(+), 76 deletions(-) diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index 8b8e171c577..2174d093009 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -24,6 +24,7 @@ from pyomo.opt import TerminationCondition as tc from pyomo.core import minimize, value from pyomo.core.expr import identify_variables +import math cplex, cplex_available = attempt_import('cplex') @@ -41,7 +42,6 @@ def copy_lazy_var_list_values( config, skip_stale=False, skip_fixed=True, - ignore_integrality=False, ): """This function copies variable values from one list to another. @@ -71,43 +71,43 @@ def copy_lazy_var_list_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]) - try: - # 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" - v_to.stale = True - # NOTE: PEP 2180 changes the var behavior so that domain - # / bounds violations no longer generate exceptions (and - # instead log warnings). This means that the following - # will always succeed and the ValueError should never be - # raised. - v_to.set_value(v_val, skip_validation=True) - except ValueError as e: - # Snap the value to the bounds - config.logger.error(e) - if ( - v_to.has_lb() - and v_val < v_to.lb - and v_to.lb - v_val <= config.variable_tolerance - ): - v_to.set_value(v_to.lb, skip_validation=True) - elif ( - v_to.has_ub() - and v_val > v_to.ub - and v_val - v_to.ub <= config.variable_tolerance - ): - v_to.set_value(v_to.ub, skip_validation=True) - # ... or the nearest integer - elif v_to.is_integer(): - rounded_val = int(round(v_val)) - if ( - ignore_integrality - or abs(v_val - rounded_val) <= config.integer_tolerance - ) and rounded_val in v_to.domain: - v_to.set_value(rounded_val, skip_validation=True) - else: - raise + rounded_val = int(round(v_val)) + # 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" + v_to.stale = True + # NOTE: PEP 2180 changes the var behavior so that domain + # / bounds violations no longer generate exceptions (and + # instead log warnings). This means that the following + # will always succeed and the ValueError should never be + # raised. + if v_val in v_to.domain \ + and not ((v_to.has_lb() and v_val < v_to.lb)) \ + and not ((v_to.has_ub() and v_val > v_to.ub)): + v_to.set_value(v_val) + # Snap the value to the bounds + # TODO: check the performance of + # v_to.lb - v_val <= config.variable_tolerance + elif ( + v_to.has_lb() + and v_val < v_to.lb + # and v_to.lb - v_val <= config.variable_tolerance + ): + v_to.set_value(v_to.lb) + elif ( + v_to.has_ub() + and v_val > v_to.ub + # and v_val - v_to.ub <= config.variable_tolerance + ): + v_to.set_value(v_to.ub) + # ... or the nearest integer + elif v_to.is_integer() and math.fabs(v_val - rounded_val) <= config.integer_tolerance: # and rounded_val in v_to.domain: + v_to.set_value(rounded_val) + elif abs(v_val) <= config.zero_tolerance and 0 in v_to.domain: + v_to.set_value(0) + else: + raise ValueError('copy_lazy_var_list_values failed.') def add_lazy_oa_cuts( self, diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index 4dfb912e611..da1534b49ac 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -684,41 +684,42 @@ def copy_var_list_values_from_solution_pool( Whether to ignore the integrality of integer variables, by default False. """ for v_from, v_to in zip(from_list, to_list): - try: - if config.mip_solver == 'cplex_persistent': - var_val = solver_model.solution.pool.get_values( - solution_name, var_map[v_from] - ) - elif config.mip_solver == 'gurobi_persistent': - solver_model.setParam(gurobipy.GRB.Param.SolutionNumber, solution_name) - var_val = var_map[v_from].Xn - # 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" - v_to.stale = True - # NOTE: PEP 2180 changes the var behavior so that domain / - # bounds violations no longer generate exceptions (and - # instead log warnings). This means that the following will - # always succeed and the ValueError should never be raised. + if config.mip_solver == 'cplex_persistent': + var_val = solver_model.solution.pool.get_values( + solution_name, var_map[v_from] + ) + elif config.mip_solver == 'gurobi_persistent': + solver_model.setParam(gurobipy.GRB.Param.SolutionNumber, solution_name) + var_val = var_map[v_from].Xn + # 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" + v_to.stale = True + rounded_val = int(round(var_val)) + # NOTE: PEP 2180 changes the var behavior so that domain / + # bounds violations no longer generate exceptions (and + # instead log warnings). This means that the following will + # always succeed and the ValueError should never be raised. + if var_val in v_to.domain \ + and not ((v_to.has_lb() and var_val < v_to.lb)) \ + and not ((v_to.has_ub() and var_val > v_to.ub)): v_to.set_value(var_val, skip_validation=True) - except ValueError as e: - config.logger.error(e) - rounded_val = int(round(var_val)) - # Check to see if this is just a tolerance issue - if ignore_integrality and v_to.is_integer(): - v_to.set_value(var_val, skip_validation=True) - elif v_to.is_integer() and ( - abs(var_val - rounded_val) <= config.integer_tolerance - ): - v_to.set_value(rounded_val, skip_validation=True) - elif abs(var_val) <= config.zero_tolerance and 0 in v_to.domain: - v_to.set_value(0, skip_validation=True) - else: - config.logger.error( - 'Unknown validation domain error setting variable %s' % (v_to.name,) - ) - raise + elif v_to.has_lb() and var_val < v_to.lb: + v_to.set_value(v_to.lb) + elif v_to.has_ub() and var_val > v_to.ub: + v_to.set_value(v_to.ub) + # Check to see if this is just a tolerance issue + elif ignore_integrality and v_to.is_integer(): + v_to.set_value(var_val, skip_validation=True) + elif v_to.is_integer() and ( + abs(var_val - rounded_val) <= config.integer_tolerance + ): + v_to.set_value(rounded_val, skip_validation=True) + elif abs(var_val) <= config.zero_tolerance and 0 in v_to.domain: + v_to.set_value(0, skip_validation=True) + else: + raise ValueError("copy_var_list_values_from_solution_pool failed.") class GurobiPersistent4MindtPy(GurobiPersistent): @@ -980,14 +981,20 @@ def copy_var_list_values(from_list, to_list, config, continue # Skip fixed variables. var_val = value(v_from, exception=False) rounded_val = int(round(var_val)) - if var_val in v_to.domain: + if var_val in v_to.domain \ + and not ((v_to.has_lb() and var_val < v_to.lb)) \ + and not ((v_to.has_ub() and var_val > v_to.ub)): v_to.set_value(value(v_from, exception=False)) + elif v_to.has_lb() and var_val < v_to.lb: + v_to.set_value(v_to.lb) + elif v_to.has_ub() and var_val > v_to.ub: + v_to.set_value(v_to.ub) elif ignore_integrality and v_to.is_integer(): v_to.set_value(value(v_from, exception=False), skip_validation=True) - elif abs(var_val) <= config.zero_tolerance and 0 in v_to.domain: - v_to.set_value(0) elif v_to.is_integer() and (math.fabs(var_val - rounded_val) <= config.integer_tolerance): v_to.set_value(rounded_val) + elif abs(var_val) <= config.zero_tolerance and 0 in v_to.domain: + v_to.set_value(0) else: - raise + raise ValueError("copy_var_list_values failed.") From cefd4a66a06711e90005d20cd470efca762754a6 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Sun, 29 Oct 2023 15:12:02 -0400 Subject: [PATCH 12/53] fix FP bug --- pyomo/contrib/mindtpy/algorithm_base_class.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index c254a8db72b..3e53559b3df 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -2384,6 +2384,7 @@ def handle_fp_subproblem_optimal(self, fp_nlp): fp_nlp.MindtPy_utils.variable_list, self.working_model.MindtPy_utils.variable_list, self.config, + ignore_integrality=True ) add_orthogonality_cuts(self.working_model, self.mip, self.config) From 905503b13907a610a1e7f9d8620ee88127551ec5 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 1 Nov 2023 00:42:53 -0400 Subject: [PATCH 13/53] fix gurobi single tree termination check bug --- pyomo/contrib/mindtpy/single_tree.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index 2174d093009..9595a9fc9be 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -910,19 +910,7 @@ def LazyOACallback_gurobi(cb_m, cb_opt, cb_where, mindtpy_solver, config): 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 + if mindtpy_solver.bounds_converged() or mindtpy_solver.reached_time_limit(): cb_opt._solver_model.terminate() return From 3b01104f586f9a33d1386ff89c5c3e57b3ae7fc7 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 1 Nov 2023 21:24:39 -0400 Subject: [PATCH 14/53] fix Gurobi single tree cycle handling --- pyomo/contrib/mindtpy/algorithm_base_class.py | 3 +++ pyomo/contrib/mindtpy/single_tree.py | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index a0216b5d054..33b2f2c1d04 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -106,6 +106,8 @@ def __init__(self, **kwds): self.curr_int_sol = [] self.should_terminate = False self.integer_list = [] + # dictionary {integer solution (list): cuts index (list)} + self.int_sol_2_cuts_ind = dict() # Set up iteration counters self.nlp_iter = 0 @@ -794,6 +796,7 @@ def MindtPy_initialization(self): self.integer_list.append(self.curr_int_sol) fixed_nlp, fixed_nlp_result = self.solve_subproblem() self.handle_nlp_subproblem_tc(fixed_nlp, fixed_nlp_result) + self.int_sol_2_cuts_ind[self.curr_int_sol] = list(range(1, len(self.mip.MindtPy_utils.cuts.oa_cuts) + 1)) elif config.init_strategy == 'FP': self.init_rNLP() self.fp_loop() diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index 9595a9fc9be..dacde73a79e 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -941,15 +941,26 @@ def LazyOACallback_gurobi(cb_m, cb_opt, cb_where, mindtpy_solver, config): ) 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. + for ind in mindtpy_solver.int_sol_2_cuts_ind[mindtpy_solver.curr_int_sol]: + 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 # 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.int_sol_2_cuts_ind[mindtpy_solver.curr_int_sol] = list(range(cut_ind + 1, len(mindtpy_solver.mip.MindtPy_utils.cuts.oa_cuts) + 1)) def handle_lazy_main_feasible_solution_gurobi(cb_m, cb_opt, mindtpy_solver, config): From 906fff7d18e9cc98c6a7d7e0a1b9d4657aaa9be9 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Mon, 13 Nov 2023 10:59:38 -0500 Subject: [PATCH 15/53] black format --- pyomo/contrib/mindtpy/algorithm_base_class.py | 26 ++++++++----- pyomo/contrib/mindtpy/single_tree.py | 38 +++++++++--------- pyomo/contrib/mindtpy/util.py | 39 ++++++++++++------- 3 files changed, 62 insertions(+), 41 deletions(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index 33b2f2c1d04..9771c04fc62 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -80,7 +80,7 @@ set_solver_mipgap, set_solver_constraint_violation_tolerance, update_solver_timelimit, - copy_var_list_values + copy_var_list_values, ) single_tree, single_tree_available = attempt_import('pyomo.contrib.mindtpy.single_tree') @@ -796,7 +796,9 @@ def MindtPy_initialization(self): self.integer_list.append(self.curr_int_sol) fixed_nlp, fixed_nlp_result = self.solve_subproblem() self.handle_nlp_subproblem_tc(fixed_nlp, fixed_nlp_result) - self.int_sol_2_cuts_ind[self.curr_int_sol] = list(range(1, len(self.mip.MindtPy_utils.cuts.oa_cuts) + 1)) + self.int_sol_2_cuts_ind[self.curr_int_sol] = list( + range(1, len(self.mip.MindtPy_utils.cuts.oa_cuts) + 1) + ) elif config.init_strategy == 'FP': self.init_rNLP() self.fp_loop() @@ -834,9 +836,15 @@ def init_rNLP(self, add_oa_cuts=True): subprob_terminate_cond = results.solver.termination_condition # Sometimes, the NLP solver might be trapped in a infeasible solution if the objective function is nonlinear and partition_obj_nonlinear_terms is True. If this happens, we will use the original objective function instead. - if subprob_terminate_cond == tc.infeasible and config.partition_obj_nonlinear_terms and self.rnlp.MindtPy_utils.objective_list[0].expr.polynomial_degree() not in self.mip_objective_polynomial_degree: + if ( + subprob_terminate_cond == tc.infeasible + and config.partition_obj_nonlinear_terms + and self.rnlp.MindtPy_utils.objective_list[0].expr.polynomial_degree() + not in self.mip_objective_polynomial_degree + ): config.logger.info( - 'Initial relaxed NLP problem is infeasible. This might be related to partition_obj_nonlinear_terms. Try to solve it again without partitioning nonlinear objective function.') + 'Initial relaxed NLP problem is infeasible. This might be related to partition_obj_nonlinear_terms. Try to solve it again without partitioning nonlinear objective function.' + ) self.rnlp.MindtPy_utils.objective.deactivate() self.rnlp.MindtPy_utils.objective_list[0].activate() results = self.nlp_opt.solve( @@ -889,14 +897,14 @@ def init_rNLP(self, add_oa_cuts=True): self.rnlp.MindtPy_utils.variable_list, self.mip.MindtPy_utils.variable_list, config, - ignore_integrality=True + ignore_integrality=True, ) if config.init_strategy == 'FP': copy_var_list_values( self.rnlp.MindtPy_utils.variable_list, self.working_model.MindtPy_utils.variable_list, config, - ignore_integrality=True + ignore_integrality=True, ) self.add_cuts( dual_values=dual_values, @@ -1700,9 +1708,7 @@ def solve_fp_main(self): config = self.config self.setup_fp_main() mip_args = self.set_up_mip_solver() - update_solver_timelimit( - self.mip_opt, config.mip_solver, self.timing, config - ) + update_solver_timelimit(self.mip_opt, config.mip_solver, self.timing, config) main_mip_results = self.mip_opt.solve( self.mip, @@ -2387,7 +2393,7 @@ def handle_fp_subproblem_optimal(self, fp_nlp): fp_nlp.MindtPy_utils.variable_list, self.working_model.MindtPy_utils.variable_list, self.config, - ignore_integrality=True + ignore_integrality=True, ) add_orthogonality_cuts(self.working_model, self.mip, self.config) diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index dacde73a79e..66435c2587f 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -17,10 +17,7 @@ import pyomo.core.expr as EXPR from math import copysign from pyomo.contrib.mindtpy.util import get_integer_solution, copy_var_list_values -from pyomo.contrib.gdpopt.util import ( - get_main_elapsed_time, - time_code, -) +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 @@ -35,13 +32,7 @@ class LazyOACallback_cplex( """Inherent class in CPLEX to call Lazy callback.""" def copy_lazy_var_list_values( - self, - opt, - from_list, - to_list, - config, - skip_stale=False, - skip_fixed=True, + self, opt, from_list, to_list, config, skip_stale=False, skip_fixed=True ): """This function copies variable values from one list to another. @@ -82,12 +73,14 @@ def copy_lazy_var_list_values( # instead log warnings). This means that the following # will always succeed and the ValueError should never be # raised. - if v_val in v_to.domain \ - and not ((v_to.has_lb() and v_val < v_to.lb)) \ - and not ((v_to.has_ub() and v_val > v_to.ub)): + if ( + v_val in v_to.domain + and not ((v_to.has_lb() and v_val < v_to.lb)) + and not ((v_to.has_ub() and v_val > v_to.ub)) + ): v_to.set_value(v_val) # Snap the value to the bounds - # TODO: check the performance of + # TODO: check the performance of # v_to.lb - v_val <= config.variable_tolerance elif ( v_to.has_lb() @@ -102,7 +95,10 @@ def copy_lazy_var_list_values( ): v_to.set_value(v_to.ub) # ... or the nearest integer - elif v_to.is_integer() and math.fabs(v_val - rounded_val) <= config.integer_tolerance: # and rounded_val in v_to.domain: + elif ( + v_to.is_integer() + and math.fabs(v_val - rounded_val) <= config.integer_tolerance + ): # and rounded_val in v_to.domain: v_to.set_value(rounded_val) elif abs(v_val) <= config.zero_tolerance and 0 in v_to.domain: v_to.set_value(0) @@ -945,7 +941,9 @@ def LazyOACallback_gurobi(cb_m, cb_opt, cb_where, mindtpy_solver, config): # 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. - for ind in mindtpy_solver.int_sol_2_cuts_ind[mindtpy_solver.curr_int_sol]: + for ind in mindtpy_solver.int_sol_2_cuts_ind[ + mindtpy_solver.curr_int_sol + ]: cb_opt.cbLazy(mindtpy_solver.mip.MindtPy_utils.cuts.oa_cuts[ind]) return else: @@ -960,7 +958,11 @@ def LazyOACallback_gurobi(cb_m, cb_opt, cb_where, mindtpy_solver, config): 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.int_sol_2_cuts_ind[mindtpy_solver.curr_int_sol] = list(range(cut_ind + 1, len(mindtpy_solver.mip.MindtPy_utils.cuts.oa_cuts) + 1)) + mindtpy_solver.int_sol_2_cuts_ind[mindtpy_solver.curr_int_sol] = list( + range( + cut_ind + 1, len(mindtpy_solver.mip.MindtPy_utils.cuts.oa_cuts) + 1 + ) + ) def handle_lazy_main_feasible_solution_gurobi(cb_m, cb_opt, mindtpy_solver, config): diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index da1534b49ac..48c8aab31c4 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -23,7 +23,7 @@ RangeSet, ConstraintList, TransformationFactory, - value + value, ) from pyomo.repn import generate_standard_repn from pyomo.contrib.mcpp.pyomo_mcpp import mcpp_available, McCormick @@ -567,7 +567,9 @@ def set_solver_mipgap(opt, solver_name, config): opt.options['add_options'].append('option optcr=%s;' % config.mip_solver_mipgap) -def set_solver_constraint_violation_tolerance(opt, solver_name, config, warm_start=True): +def set_solver_constraint_violation_tolerance( + opt, solver_name, config, warm_start=True +): """Set constraint violation tolerance for solvers. Parameters @@ -701,9 +703,11 @@ def copy_var_list_values_from_solution_pool( # bounds violations no longer generate exceptions (and # instead log warnings). This means that the following will # always succeed and the ValueError should never be raised. - if var_val in v_to.domain \ - and not ((v_to.has_lb() and var_val < v_to.lb)) \ - and not ((v_to.has_ub() and var_val > v_to.ub)): + if ( + var_val in v_to.domain + and not ((v_to.has_lb() and var_val < v_to.lb)) + and not ((v_to.has_ub() and var_val > v_to.ub)) + ): v_to.set_value(var_val, skip_validation=True) elif v_to.has_lb() and var_val < v_to.lb: v_to.set_value(v_to.lb) @@ -967,9 +971,15 @@ def generate_norm_constraint(fp_nlp_model, mip_model, config): ): fp_nlp_model.norm_constraint.add(nlp_var - mip_var.value <= rhs) -def copy_var_list_values(from_list, to_list, config, - skip_stale=False, skip_fixed=True, - ignore_integrality=False): + +def copy_var_list_values( + from_list, + to_list, + config, + skip_stale=False, + skip_fixed=True, + ignore_integrality=False, +): """Copy variable values from one list to another. Rounds to Binary/Integer if necessary Sets to zero for NonNegativeReals if necessary @@ -981,9 +991,11 @@ def copy_var_list_values(from_list, to_list, config, continue # Skip fixed variables. var_val = value(v_from, exception=False) rounded_val = int(round(var_val)) - if var_val in v_to.domain \ - and not ((v_to.has_lb() and var_val < v_to.lb)) \ - and not ((v_to.has_ub() and var_val > v_to.ub)): + if ( + var_val in v_to.domain + and not ((v_to.has_lb() and var_val < v_to.lb)) + and not ((v_to.has_ub() and var_val > v_to.ub)) + ): v_to.set_value(value(v_from, exception=False)) elif v_to.has_lb() and var_val < v_to.lb: v_to.set_value(v_to.lb) @@ -991,8 +1003,9 @@ def copy_var_list_values(from_list, to_list, config, v_to.set_value(v_to.ub) elif ignore_integrality and v_to.is_integer(): v_to.set_value(value(v_from, exception=False), skip_validation=True) - elif v_to.is_integer() and (math.fabs(var_val - rounded_val) <= - config.integer_tolerance): + elif v_to.is_integer() and ( + math.fabs(var_val - rounded_val) <= config.integer_tolerance + ): v_to.set_value(rounded_val) elif abs(var_val) <= config.zero_tolerance and 0 in v_to.domain: v_to.set_value(0) From f261fdc146d1de7b75c13d7fbc2d8a3e97a0fe7f Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Tue, 21 Nov 2023 15:19:10 -0500 Subject: [PATCH 16/53] fix load_solutions bug --- pyomo/contrib/mindtpy/algorithm_base_class.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index d485dc4651f..4ea492bd7c9 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -864,7 +864,7 @@ def init_rNLP(self, add_oa_cuts=True): results = self.nlp_opt.solve( self.rnlp, tee=config.nlp_solver_tee, - load_solutions=config.load_solutions, + load_solutions=self.load_solutions, **nlp_args, ) if len(results.solution) > 0: From 015ebaf8593d0f21336af323ac8d0d440e17c9f4 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Mon, 27 Nov 2023 14:41:36 -0500 Subject: [PATCH 17/53] fix typo: change try to trying --- pyomo/contrib/mindtpy/algorithm_base_class.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index 4ea492bd7c9..a7a8a41cd70 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -857,7 +857,7 @@ def init_rNLP(self, add_oa_cuts=True): not in self.mip_objective_polynomial_degree ): config.logger.info( - 'Initial relaxed NLP problem is infeasible. This might be related to partition_obj_nonlinear_terms. Try to solve it again without partitioning nonlinear objective function.' + 'Initial relaxed NLP problem is infeasible. This might be related to partition_obj_nonlinear_terms. Trying to solve it again without partitioning nonlinear objective function.' ) self.rnlp.MindtPy_utils.objective.deactivate() self.rnlp.MindtPy_utils.objective_list[0].activate() From a6079d50b319cc0288ee8612bf58e2f02a88fe09 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Mon, 27 Nov 2023 15:37:57 -0500 Subject: [PATCH 18/53] add more details of the error in copy_var_list_values --- pyomo/contrib/mindtpy/util.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index 48c8aab31c4..ea2136b0589 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -1010,4 +1010,5 @@ def copy_var_list_values( elif abs(var_val) <= config.zero_tolerance and 0 in v_to.domain: v_to.set_value(0) else: - raise ValueError("copy_var_list_values failed.") + raise ValueError("copy_var_list_values failed with variable {}, value = {} and rounded value = {}" + "".format(v_to.name, var_val, rounded_val)) From e8b3b72df0d5c869be0a169ea5310940da342049 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Mon, 27 Nov 2023 18:26:12 -0500 Subject: [PATCH 19/53] create copy_var_value function --- pyomo/contrib/mindtpy/algorithm_base_class.py | 1 - pyomo/contrib/mindtpy/single_tree.py | 53 +------- pyomo/contrib/mindtpy/tests/test_mindtpy.py | 1 + pyomo/contrib/mindtpy/util.py | 121 ++++++++++-------- 4 files changed, 74 insertions(+), 102 deletions(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index a7a8a41cd70..92e1075fe90 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -1853,7 +1853,6 @@ def handle_main_optimal(self, main_mip, update_bound=True): f"Integer variable {var.name} not initialized. " "Setting it to its lower bound" ) - # nlp_var.bounds[0] var.set_value(var.lb, skip_validation=True) # warm start for the nlp subproblem copy_var_list_values( diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index 66435c2587f..a5d4401d623 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -16,12 +16,11 @@ 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 +from pyomo.contrib.mindtpy.util import get_integer_solution, copy_var_list_values, copy_var_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 -import math cplex, cplex_available = attempt_import('cplex') @@ -35,7 +34,6 @@ 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. @@ -44,17 +42,15 @@ def copy_lazy_var_list_values( opt : SolverFactory The cplex_persistent solver. from_list : list - The variables that provides the values to copy from. + The variable list that provides the values to copy from. to_list : list - The variables that need to set value. + 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. - ignore_integrality : bool, optional - Whether to ignore the integrality of integer variables, by default False. """ for v_from, v_to in zip(from_list, to_list): if skip_stale and v_from.stale: @@ -62,48 +58,7 @@ def copy_lazy_var_list_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]) - rounded_val = int(round(v_val)) - # 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" - v_to.stale = True - # NOTE: PEP 2180 changes the var behavior so that domain - # / bounds violations no longer generate exceptions (and - # instead log warnings). This means that the following - # will always succeed and the ValueError should never be - # raised. - if ( - v_val in v_to.domain - and not ((v_to.has_lb() and v_val < v_to.lb)) - and not ((v_to.has_ub() and v_val > v_to.ub)) - ): - v_to.set_value(v_val) - # Snap the value to the bounds - # TODO: check the performance of - # v_to.lb - v_val <= config.variable_tolerance - elif ( - v_to.has_lb() - and v_val < v_to.lb - # and v_to.lb - v_val <= config.variable_tolerance - ): - v_to.set_value(v_to.lb) - elif ( - v_to.has_ub() - and v_val > v_to.ub - # and v_val - v_to.ub <= config.variable_tolerance - ): - v_to.set_value(v_to.ub) - # ... or the nearest integer - elif ( - v_to.is_integer() - and math.fabs(v_val - rounded_val) <= config.integer_tolerance - ): # and rounded_val in v_to.domain: - v_to.set_value(rounded_val) - elif abs(v_val) <= config.zero_tolerance and 0 in v_to.domain: - v_to.set_value(0) - else: - raise ValueError('copy_lazy_var_list_values failed.') + copy_var_value(v_from, v_to, v_val, config, ignore_integrality=False) def add_lazy_oa_cuts( self, diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy.py b/pyomo/contrib/mindtpy/tests/test_mindtpy.py index e872eccc670..ae531f9bd84 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy.py @@ -327,6 +327,7 @@ def test_OA_APPSI_ipopt(self): value(model.objective.expr), model.optimal_value, places=1 ) + # CYIPOPT will raise WARNING (W1002) during loading solution. @unittest.skipUnless( SolverFactory('cyipopt').available(exception_flag=False), "APPSI_IPOPT not available.", diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index ea2136b0589..2970a805540 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -693,37 +693,7 @@ def copy_var_list_values_from_solution_pool( elif config.mip_solver == 'gurobi_persistent': solver_model.setParam(gurobipy.GRB.Param.SolutionNumber, solution_name) var_val = var_map[v_from].Xn - # 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" - v_to.stale = True - rounded_val = int(round(var_val)) - # NOTE: PEP 2180 changes the var behavior so that domain / - # bounds violations no longer generate exceptions (and - # instead log warnings). This means that the following will - # always succeed and the ValueError should never be raised. - if ( - var_val in v_to.domain - and not ((v_to.has_lb() and var_val < v_to.lb)) - and not ((v_to.has_ub() and var_val > v_to.ub)) - ): - v_to.set_value(var_val, skip_validation=True) - elif v_to.has_lb() and var_val < v_to.lb: - v_to.set_value(v_to.lb) - elif v_to.has_ub() and var_val > v_to.ub: - v_to.set_value(v_to.ub) - # Check to see if this is just a tolerance issue - elif ignore_integrality and v_to.is_integer(): - v_to.set_value(var_val, skip_validation=True) - elif v_to.is_integer() and ( - abs(var_val - rounded_val) <= config.integer_tolerance - ): - v_to.set_value(rounded_val, skip_validation=True) - elif abs(var_val) <= config.zero_tolerance and 0 in v_to.domain: - v_to.set_value(0, skip_validation=True) - else: - raise ValueError("copy_var_list_values_from_solution_pool failed.") + copy_var_value(v_from, v_to, var_val, config, ignore_integrality) class GurobiPersistent4MindtPy(GurobiPersistent): @@ -983,6 +953,19 @@ def copy_var_list_values( """Copy variable values from one list to another. Rounds to Binary/Integer if necessary Sets to zero for NonNegativeReals if necessary + + from_list : list + The variables that provides the values to copy from. + to_list : list + The variables that need 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. + ignore_integrality : bool, optional + Whether to ignore the integrality of integer variables, by default False. """ for v_from, v_to in zip(from_list, to_list): if skip_stale and v_from.stale: @@ -990,25 +973,59 @@ def copy_var_list_values( if skip_fixed and v_to.is_fixed(): continue # Skip fixed variables. var_val = value(v_from, exception=False) - rounded_val = int(round(var_val)) - if ( - var_val in v_to.domain - and not ((v_to.has_lb() and var_val < v_to.lb)) - and not ((v_to.has_ub() and var_val > v_to.ub)) - ): - v_to.set_value(value(v_from, exception=False)) - elif v_to.has_lb() and var_val < v_to.lb: - v_to.set_value(v_to.lb) - elif v_to.has_ub() and var_val > v_to.ub: - v_to.set_value(v_to.ub) - elif ignore_integrality and v_to.is_integer(): - v_to.set_value(value(v_from, exception=False), skip_validation=True) - elif v_to.is_integer() and ( - math.fabs(var_val - rounded_val) <= config.integer_tolerance + copy_var_value(v_from, v_to, var_val, config, ignore_integrality) + + +def copy_var_value(v_from, v_to, var_val, config, ignore_integrality): + """This function copies variable value from one to another. + Rounds to Binary/Integer if necessary. + Sets to zero for NonNegativeReals if necessary. + + NOTE: PEP 2180 changes the var behavior so that domain / + bounds violations no longer generate exceptions (and + instead log warnings). This means that the following will + always succeed and the ValueError should never be raised. + + Parameters + ---------- + v_from : Var + The variable that provides the values to copy from. + v_to : Var + The variable that needs to set value. + var_val : float + The value of v_to variable. + config : ConfigBlock + The specific configurations for MindtPy. + ignore_integrality : bool, optional + Whether to ignore the integrality of integer variables, by default False. + + Raises + ------ + ValueError + Cannot successfully set the value to variable v_to. + """ + # 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". + v_to.stale = True + rounded_val = int(round(var_val)) + if (var_val in v_to.domain + and not ((v_to.has_lb() and var_val < v_to.lb)) + and not ((v_to.has_ub() and var_val > v_to.ub)) ): - v_to.set_value(rounded_val) - elif abs(var_val) <= config.zero_tolerance and 0 in v_to.domain: - v_to.set_value(0) - else: - raise ValueError("copy_var_list_values failed with variable {}, value = {} and rounded value = {}" - "".format(v_to.name, var_val, rounded_val)) + v_to.set_value(var_val) + elif v_to.has_lb() and var_val < v_to.lb: + v_to.set_value(v_to.lb) + elif v_to.has_ub() and var_val > v_to.ub: + v_to.set_value(v_to.ub) + elif ignore_integrality and v_to.is_integer(): + v_to.set_value(var_val, skip_validation=True) + elif v_to.is_integer() and ( + math.fabs(var_val - rounded_val) <= config.integer_tolerance + ): + v_to.set_value(rounded_val) + elif abs(var_val) <= config.zero_tolerance and 0 in v_to.domain: + v_to.set_value(0) + else: + raise ValueError("copy_var_list_values failed with variable {}, value = {} and rounded value = {}" + "".format(v_to.name, var_val, rounded_val)) From dc41b8e969490e15d1c7948e386a8f2ba3ebedb8 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Mon, 27 Nov 2023 20:07:43 -0500 Subject: [PATCH 20/53] add exc_info for the error message --- pyomo/contrib/mindtpy/algorithm_base_class.py | 17 ++++++++++------- pyomo/contrib/mindtpy/cut_generation.py | 11 +++++++---- pyomo/contrib/mindtpy/extended_cutting_plane.py | 4 ++-- .../mindtpy/global_outer_approximation.py | 3 ++- pyomo/contrib/mindtpy/single_tree.py | 9 +++++---- 5 files changed, 26 insertions(+), 18 deletions(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index 92e1075fe90..141e7f9f09f 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -802,7 +802,7 @@ def MindtPy_initialization(self): try: self.curr_int_sol = get_integer_solution(self.working_model) except TypeError as e: - config.logger.error(e) + config.logger.error(e, exc_info=True) raise ValueError( 'The initial integer combination is not provided or not complete. ' 'Please provide the complete integer combination or use other initialization strategy.' @@ -1083,7 +1083,7 @@ def solve_subproblem(self): 0, c_geq * (rhs - value(c.body)) ) except (ValueError, OverflowError) as e: - config.logger.error(e) + config.logger.error(e, exc_info=True) self.fixed_nlp.tmp_duals[c] = None evaluation_error = True if evaluation_error: @@ -1100,8 +1100,9 @@ def solve_subproblem(self): tolerance=config.constraint_tolerance, ) except InfeasibleConstraintException as e: + config.logger.error(e, exc_info=True) config.logger.error( - str(e) + '\nInfeasibility detected in deactivate_trivial_constraints.' + 'Infeasibility detected in deactivate_trivial_constraints.' ) results = SolverResults() results.solver.termination_condition = tc.infeasible @@ -1401,7 +1402,7 @@ def solve_feasibility_subproblem(self): if len(feas_soln.solution) > 0: feas_subproblem.solutions.load_from(feas_soln) except (ValueError, OverflowError) as e: - config.logger.error(e) + config.logger.error(e, exc_info=True) for nlp_var, orig_val in zip( MindtPy.variable_list, self.initial_var_values ): @@ -1542,8 +1543,9 @@ def fix_dual_bound(self, last_iter_cuts): try: self.dual_bound = self.stored_bound[self.primal_bound] except KeyError as e: + config.logger.error(e, exc_info=True) config.logger.error( - str(e) + '\nNo stored bound found. Bound fix failed.' + 'No stored bound found. Bound fix failed.' ) else: config.logger.info( @@ -1670,7 +1672,7 @@ def solve_main(self): if len(main_mip_results.solution) > 0: self.mip.solutions.load_from(main_mip_results) except (ValueError, AttributeError, RuntimeError) as e: - config.logger.error(e) + config.logger.error(e, exc_info=True) if config.single_tree: config.logger.warning('Single tree terminate.') if get_main_elapsed_time(self.timing) >= config.time_limit: @@ -2369,8 +2371,9 @@ def solve_fp_subproblem(self): tolerance=config.constraint_tolerance, ) except InfeasibleConstraintException as e: + config.logger.error(e, exc_info=True) config.logger.error( - str(e) + '\nInfeasibility detected in deactivate_trivial_constraints.' + 'Infeasibility detected in deactivate_trivial_constraints.' ) results = SolverResults() results.solver.termination_condition = tc.infeasible diff --git a/pyomo/contrib/mindtpy/cut_generation.py b/pyomo/contrib/mindtpy/cut_generation.py index 28d302104a3..343170aabac 100644 --- a/pyomo/contrib/mindtpy/cut_generation.py +++ b/pyomo/contrib/mindtpy/cut_generation.py @@ -271,8 +271,9 @@ def add_ecp_cuts( try: upper_slack = constr.uslack() except (ValueError, OverflowError) as e: + config.logger.error(e, exc_info=True) config.logger.error( - str(e) + '\nConstraint {} has caused either a ' + 'Constraint {} has caused either a ' 'ValueError or OverflowError.' '\n'.format(constr) ) @@ -300,8 +301,9 @@ def add_ecp_cuts( try: lower_slack = constr.lslack() except (ValueError, OverflowError) as e: + config.logger.error(e, exc_info=True) config.logger.error( - str(e) + '\nConstraint {} has caused either a ' + 'Constraint {} has caused either a ' 'ValueError or OverflowError.' '\n'.format(constr) ) @@ -424,9 +426,10 @@ def add_affine_cuts(target_model, config, timing): try: mc_eqn = mc(constr.body) except MCPP_Error as e: + config.logger.error(e, exc_info=True) config.logger.error( - '\nSkipping constraint %s due to MCPP error %s' - % (constr.name, str(e)) + 'Skipping constraint %s due to MCPP error' + % (constr.name) ) continue # skip to the next constraint diff --git a/pyomo/contrib/mindtpy/extended_cutting_plane.py b/pyomo/contrib/mindtpy/extended_cutting_plane.py index 446304b1361..3a09af155a0 100644 --- a/pyomo/contrib/mindtpy/extended_cutting_plane.py +++ b/pyomo/contrib/mindtpy/extended_cutting_plane.py @@ -140,7 +140,7 @@ def all_nonlinear_constraint_satisfied(self): lower_slack = nlc.lslack() except (ValueError, OverflowError) as e: # Set lower_slack (upper_slack below) less than -config.ecp_tolerance in this case. - config.logger.error(e) + config.logger.error(e, exc_info=True) lower_slack = -10 * config.ecp_tolerance if lower_slack < -config.ecp_tolerance: config.logger.debug( @@ -153,7 +153,7 @@ def all_nonlinear_constraint_satisfied(self): try: upper_slack = nlc.uslack() except (ValueError, OverflowError) as e: - config.logger.error(e) + config.logger.error(e, exc_info=True) upper_slack = -10 * config.ecp_tolerance if upper_slack < -config.ecp_tolerance: config.logger.debug( diff --git a/pyomo/contrib/mindtpy/global_outer_approximation.py b/pyomo/contrib/mindtpy/global_outer_approximation.py index dfb7ef54630..817fb0bf4a8 100644 --- a/pyomo/contrib/mindtpy/global_outer_approximation.py +++ b/pyomo/contrib/mindtpy/global_outer_approximation.py @@ -108,4 +108,5 @@ def deactivate_no_good_cuts_when_fixing_bound(self, no_good_cuts): if self.config.use_tabu_list: self.integer_list = self.integer_list[:valid_no_good_cuts_num] except KeyError as e: - self.config.logger.error(str(e) + '\nDeactivating no-good cuts failed.') + self.config.logger.error(e, exc_info=True) + self.config.logger.error('Deactivating no-good cuts failed.') diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index a5d4401d623..5485e0298f2 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -259,9 +259,10 @@ def add_lazy_affine_cuts(self, mindtpy_solver, config, opt): 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 %s' - % (constr.name, str(e)) + 'Skipping constraint %s due to MCPP error' + % (constr.name) ) continue # skip to the next constraint # TODO: check if the value of ccSlope and cvSlope is not Nan or inf. If so, we skip this. @@ -696,9 +697,9 @@ def __call__(self): mindtpy_solver.mip, None, mindtpy_solver, config, opt ) except ValueError as e: + config.logger.error(e, exc_info=True) config.logger.error( - str(e) - + "\nUsually this error is caused by the MIP start solution causing a math domain error. " + "Usually this error is caused by the MIP start solution causing a math domain error. " "We will skip it." ) return From dbe9f490fd49e47ea5634c8425eeddd019d731ce Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Mon, 27 Nov 2023 20:34:04 -0500 Subject: [PATCH 21/53] black format --- pyomo/contrib/mindtpy/algorithm_base_class.py | 4 +--- pyomo/contrib/mindtpy/cut_generation.py | 3 +-- pyomo/contrib/mindtpy/single_tree.py | 9 ++++++--- pyomo/contrib/mindtpy/util.py | 11 +++++++---- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index 141e7f9f09f..b06a4c730b4 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -1544,9 +1544,7 @@ def fix_dual_bound(self, last_iter_cuts): self.dual_bound = self.stored_bound[self.primal_bound] except KeyError as e: config.logger.error(e, exc_info=True) - config.logger.error( - 'No stored bound found. Bound fix failed.' - ) + config.logger.error('No stored bound found. Bound fix failed.') else: config.logger.info( 'Solve the main problem without the last no_good cut to fix the bound.' diff --git a/pyomo/contrib/mindtpy/cut_generation.py b/pyomo/contrib/mindtpy/cut_generation.py index 343170aabac..e57cfd2eada 100644 --- a/pyomo/contrib/mindtpy/cut_generation.py +++ b/pyomo/contrib/mindtpy/cut_generation.py @@ -428,8 +428,7 @@ def add_affine_cuts(target_model, config, timing): except MCPP_Error as e: config.logger.error(e, exc_info=True) config.logger.error( - 'Skipping constraint %s due to MCPP error' - % (constr.name) + 'Skipping constraint %s due to MCPP error' % (constr.name) ) continue # skip to the next constraint diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index 5485e0298f2..5e4e378d6c5 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -16,7 +16,11 @@ 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, copy_var_value +from pyomo.contrib.mindtpy.util import ( + get_integer_solution, + copy_var_list_values, + copy_var_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 @@ -261,8 +265,7 @@ def add_lazy_affine_cuts(self, mindtpy_solver, config, opt): except MCPP_Error as e: config.logger.error(e, exc_info=True) config.logger.debug( - 'Skipping constraint %s due to MCPP error' - % (constr.name) + 'Skipping constraint %s due to MCPP error' % (constr.name) ) continue # skip to the next constraint # TODO: check if the value of ccSlope and cvSlope is not Nan or inf. If so, we skip this. diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index 2970a805540..7e3fbe415d4 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -1009,10 +1009,11 @@ def copy_var_value(v_from, v_to, var_val, config, ignore_integrality): # knowing that set_value will switch it back to "not stale". v_to.stale = True rounded_val = int(round(var_val)) - if (var_val in v_to.domain + if ( + var_val in v_to.domain and not ((v_to.has_lb() and var_val < v_to.lb)) and not ((v_to.has_ub() and var_val > v_to.ub)) - ): + ): v_to.set_value(var_val) elif v_to.has_lb() and var_val < v_to.lb: v_to.set_value(v_to.lb) @@ -1027,5 +1028,7 @@ def copy_var_value(v_from, v_to, var_val, config, ignore_integrality): elif abs(var_val) <= config.zero_tolerance and 0 in v_to.domain: v_to.set_value(0) else: - raise ValueError("copy_var_list_values failed with variable {}, value = {} and rounded value = {}" - "".format(v_to.name, var_val, rounded_val)) + raise ValueError( + "copy_var_list_values failed with variable {}, value = {} and rounded value = {}" + "".format(v_to.name, var_val, rounded_val) + ) From 4ac390e12fcfa3277a9808ff7f7325bfde808124 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Tue, 28 Nov 2023 09:34:22 -0500 Subject: [PATCH 22/53] change dir() to locals() --- pyomo/contrib/mindtpy/algorithm_base_class.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index b06a4c730b4..d5d015d180d 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -1684,7 +1684,7 @@ def solve_main(self): 'No integer solution is found, so the CPLEX solver will report an error status. ' ) # Value error will be raised if the MIP problem is unbounded and appsi solver is used when loading solutions. Although the problem is unbounded, a valid result is provided and we do not return None to let the algorithm continue. - if 'main_mip_results' in dir(): + if 'main_mip_results' in locals(): return self.mip, main_mip_results else: return None, None From a755067a6276e62569308d5ce80ef47574eaf63b Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Tue, 28 Nov 2023 10:51:56 -0500 Subject: [PATCH 23/53] improve int_sol_2_cuts_ind --- pyomo/contrib/mindtpy/algorithm_base_class.py | 9 +++++---- pyomo/contrib/mindtpy/single_tree.py | 14 +++++++------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index d5d015d180d..2eec150453f 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -108,7 +108,7 @@ def __init__(self, **kwds): self.curr_int_sol = [] self.should_terminate = False self.integer_list = [] - # dictionary {integer solution (list): cuts index (list)} + # dictionary {integer solution (list): [cuts begin index, cuts end index] (list)} self.int_sol_2_cuts_ind = dict() # Set up iteration counters @@ -810,9 +810,10 @@ def MindtPy_initialization(self): self.integer_list.append(self.curr_int_sol) fixed_nlp, fixed_nlp_result = self.solve_subproblem() self.handle_nlp_subproblem_tc(fixed_nlp, fixed_nlp_result) - self.int_sol_2_cuts_ind[self.curr_int_sol] = list( - range(1, len(self.mip.MindtPy_utils.cuts.oa_cuts) + 1) - ) + self.int_sol_2_cuts_ind[self.curr_int_sol] = [ + 1, + len(self.mip.MindtPy_utils.cuts.oa_cuts), + ] elif config.init_strategy == 'FP': self.init_rNLP() self.fp_loop() diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index 5e4e378d6c5..4733843d6a2 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -900,9 +900,10 @@ def LazyOACallback_gurobi(cb_m, cb_opt, cb_where, mindtpy_solver, config): # 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. - for ind in mindtpy_solver.int_sol_2_cuts_ind[ + begin_index, end_index = mindtpy_solver.int_sol_2_cuts_ind[ 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: @@ -917,11 +918,10 @@ def LazyOACallback_gurobi(cb_m, cb_opt, cb_where, mindtpy_solver, config): 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.int_sol_2_cuts_ind[mindtpy_solver.curr_int_sol] = list( - range( - cut_ind + 1, len(mindtpy_solver.mip.MindtPy_utils.cuts.oa_cuts) + 1 - ) - ) + mindtpy_solver.int_sol_2_cuts_ind[mindtpy_solver.curr_int_sol] = [ + cut_ind + 1, + len(mindtpy_solver.mip.MindtPy_utils.cuts.oa_cuts), + ] def handle_lazy_main_feasible_solution_gurobi(cb_m, cb_opt, mindtpy_solver, config): From d9d29bf04806a3d666cae8f6e20773440ed07928 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Tue, 28 Nov 2023 15:40:32 -0500 Subject: [PATCH 24/53] rename copy_var_value to set_var_value --- pyomo/contrib/mindtpy/single_tree.py | 10 +++++++-- pyomo/contrib/mindtpy/util.py | 32 ++++++++++++++++++---------- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index 4733843d6a2..481ff38df8f 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -19,7 +19,7 @@ from pyomo.contrib.mindtpy.util import ( get_integer_solution, copy_var_list_values, - copy_var_value, + set_var_value, ) from pyomo.contrib.gdpopt.util import get_main_elapsed_time, time_code from pyomo.opt import TerminationCondition as tc @@ -62,7 +62,13 @@ def copy_lazy_var_list_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]) - copy_var_value(v_from, v_to, v_val, config, ignore_integrality=False) + set_var_value( + v_to, + v_val, + config.integer_tolerance, + config.zero_tolerance, + ignore_integrality=False, + ) def add_lazy_oa_cuts( self, diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index 7e3fbe415d4..ea22eb1ec3a 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -693,7 +693,13 @@ def copy_var_list_values_from_solution_pool( elif config.mip_solver == 'gurobi_persistent': solver_model.setParam(gurobipy.GRB.Param.SolutionNumber, solution_name) var_val = var_map[v_from].Xn - copy_var_value(v_from, v_to, var_val, config, ignore_integrality) + set_var_value( + v_to, + var_val, + config.integer_tolerance, + config.zero_tolerance, + ignore_integrality, + ) class GurobiPersistent4MindtPy(GurobiPersistent): @@ -973,10 +979,16 @@ def copy_var_list_values( if skip_fixed and v_to.is_fixed(): continue # Skip fixed variables. var_val = value(v_from, exception=False) - copy_var_value(v_from, v_to, var_val, config, ignore_integrality) + set_var_value( + v_to, + var_val, + config.integer_tolerance, + config.zero_tolerance, + ignore_integrality, + ) -def copy_var_value(v_from, v_to, var_val, config, ignore_integrality): +def set_var_value(v_to, var_val, integer_tolerance, zero_tolerance, ignore_integrality): """This function copies variable value from one to another. Rounds to Binary/Integer if necessary. Sets to zero for NonNegativeReals if necessary. @@ -988,14 +1000,14 @@ def copy_var_value(v_from, v_to, var_val, config, ignore_integrality): Parameters ---------- - v_from : Var - The variable that provides the values to copy from. v_to : Var The variable that needs to set value. var_val : float The value of v_to variable. - config : ConfigBlock - The specific configurations for MindtPy. + integer_tolerance: float + Tolerance on integral values. + zero_tolerance: float + Tolerance on variable equal to zero. ignore_integrality : bool, optional Whether to ignore the integrality of integer variables, by default False. @@ -1021,11 +1033,9 @@ def copy_var_value(v_from, v_to, var_val, config, ignore_integrality): v_to.set_value(v_to.ub) elif ignore_integrality and v_to.is_integer(): v_to.set_value(var_val, skip_validation=True) - elif v_to.is_integer() and ( - math.fabs(var_val - rounded_val) <= config.integer_tolerance - ): + elif v_to.is_integer() and (math.fabs(var_val - rounded_val) <= integer_tolerance): v_to.set_value(rounded_val) - elif abs(var_val) <= config.zero_tolerance and 0 in v_to.domain: + elif abs(var_val) <= zero_tolerance and 0 in v_to.domain: v_to.set_value(0) else: raise ValueError( From f04424e0d747d4d6986b9224e3ca8d7e4ad246ac Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Tue, 28 Nov 2023 15:57:37 -0500 Subject: [PATCH 25/53] add unit test for mindtpy --- pyomo/contrib/mindtpy/tests/unit_test.py | 70 ++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 pyomo/contrib/mindtpy/tests/unit_test.py diff --git a/pyomo/contrib/mindtpy/tests/unit_test.py b/pyomo/contrib/mindtpy/tests/unit_test.py new file mode 100644 index 00000000000..d9b2e494ab0 --- /dev/null +++ b/pyomo/contrib/mindtpy/tests/unit_test.py @@ -0,0 +1,70 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# 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. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest +from pyomo.contrib.mindtpy.util import set_var_value + +from pyomo.environ import Var, Integers, ConcreteModel, Integers + + +class UnitTestMindtPy(unittest.TestCase): + def test_set_var_value(self): + m = ConcreteModel() + m.x1 = Var(within=Integers, bounds=(-1, 4), initialize=0) + + set_var_value( + m.x1, + var_val=5, + integer_tolerance=1e-6, + zero_tolerance=1e-6, + ignore_integrality=False, + ) + self.assertEqual(m.x1.value, 4) + + set_var_value( + m.x1, + var_val=-2, + integer_tolerance=1e-6, + zero_tolerance=1e-6, + ignore_integrality=False, + ) + self.assertEqual(m.x1.value, -1) + + set_var_value( + m.x1, + var_val=1.1, + integer_tolerance=1e-6, + zero_tolerance=1e-6, + ignore_integrality=True, + ) + self.assertEqual(m.x1.value, 1.1) + + set_var_value( + m.x1, + var_val=2.00000001, + integer_tolerance=1e-6, + zero_tolerance=1e-6, + ignore_integrality=False, + ) + self.assertEqual(m.x1.value, 2) + + set_var_value( + m.x1, + var_val=0.0000001, + integer_tolerance=1e-9, + zero_tolerance=1e-6, + ignore_integrality=False, + ) + self.assertEqual(m.x1.value, 0) + + +if __name__ == '__main__': + unittest.main() From ef6666085071f235458a36dbe3cb62192e391a2f Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 29 Nov 2023 17:28:51 -0500 Subject: [PATCH 26/53] improve var_val description --- pyomo/contrib/mindtpy/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index ea22eb1ec3a..51ed59e80a2 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -1003,7 +1003,7 @@ def set_var_value(v_to, var_val, integer_tolerance, zero_tolerance, ignore_integ v_to : Var The variable that needs to set value. var_val : float - The value of v_to variable. + The desired value to set for Var v_to. integer_tolerance: float Tolerance on integral values. zero_tolerance: float From 04ea15effcc83213c49a82b1230ffa3c0a945211 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 29 Nov 2023 17:31:55 -0500 Subject: [PATCH 27/53] rename set_var_value to set_var_valid_value --- pyomo/contrib/mindtpy/single_tree.py | 4 ++-- pyomo/contrib/mindtpy/tests/unit_test.py | 14 +++++++------- pyomo/contrib/mindtpy/util.py | 8 +++++--- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index 481ff38df8f..c4d49e3afd6 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -19,7 +19,7 @@ from pyomo.contrib.mindtpy.util import ( get_integer_solution, copy_var_list_values, - set_var_value, + set_var_valid_value, ) from pyomo.contrib.gdpopt.util import get_main_elapsed_time, time_code from pyomo.opt import TerminationCondition as tc @@ -62,7 +62,7 @@ def copy_lazy_var_list_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_value( + set_var_valid_value( v_to, v_val, config.integer_tolerance, diff --git a/pyomo/contrib/mindtpy/tests/unit_test.py b/pyomo/contrib/mindtpy/tests/unit_test.py index d9b2e494ab0..baf5e16bb4b 100644 --- a/pyomo/contrib/mindtpy/tests/unit_test.py +++ b/pyomo/contrib/mindtpy/tests/unit_test.py @@ -10,17 +10,17 @@ # ___________________________________________________________________________ import pyomo.common.unittest as unittest -from pyomo.contrib.mindtpy.util import set_var_value +from pyomo.contrib.mindtpy.util import set_var_valid_value from pyomo.environ import Var, Integers, ConcreteModel, Integers class UnitTestMindtPy(unittest.TestCase): - def test_set_var_value(self): + def test_set_var_valid_value(self): m = ConcreteModel() m.x1 = Var(within=Integers, bounds=(-1, 4), initialize=0) - set_var_value( + set_var_valid_value( m.x1, var_val=5, integer_tolerance=1e-6, @@ -29,7 +29,7 @@ def test_set_var_value(self): ) self.assertEqual(m.x1.value, 4) - set_var_value( + set_var_valid_value( m.x1, var_val=-2, integer_tolerance=1e-6, @@ -38,7 +38,7 @@ def test_set_var_value(self): ) self.assertEqual(m.x1.value, -1) - set_var_value( + set_var_valid_value( m.x1, var_val=1.1, integer_tolerance=1e-6, @@ -47,7 +47,7 @@ def test_set_var_value(self): ) self.assertEqual(m.x1.value, 1.1) - set_var_value( + set_var_valid_value( m.x1, var_val=2.00000001, integer_tolerance=1e-6, @@ -56,7 +56,7 @@ def test_set_var_value(self): ) self.assertEqual(m.x1.value, 2) - set_var_value( + set_var_valid_value( m.x1, var_val=0.0000001, integer_tolerance=1e-9, diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index 51ed59e80a2..f6cc0567286 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -693,7 +693,7 @@ def copy_var_list_values_from_solution_pool( elif config.mip_solver == 'gurobi_persistent': solver_model.setParam(gurobipy.GRB.Param.SolutionNumber, solution_name) var_val = var_map[v_from].Xn - set_var_value( + set_var_valid_value( v_to, var_val, config.integer_tolerance, @@ -979,7 +979,7 @@ def copy_var_list_values( if skip_fixed and v_to.is_fixed(): continue # Skip fixed variables. var_val = value(v_from, exception=False) - set_var_value( + set_var_valid_value( v_to, var_val, config.integer_tolerance, @@ -988,7 +988,9 @@ def copy_var_list_values( ) -def set_var_value(v_to, var_val, integer_tolerance, zero_tolerance, ignore_integrality): +def set_var_valid_value( + v_to, var_val, integer_tolerance, zero_tolerance, ignore_integrality +): """This function copies variable value from one to another. Rounds to Binary/Integer if necessary. Sets to zero for NonNegativeReals if necessary. From f84ff8d3429eb88bcd50021a8f4d22bcc691f2fb Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 29 Nov 2023 17:39:52 -0500 Subject: [PATCH 28/53] change v_to to var --- pyomo/contrib/mindtpy/util.py | 42 +++++++++++++++++------------------ 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index f6cc0567286..a9802a8bd1e 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -989,9 +989,9 @@ def copy_var_list_values( def set_var_valid_value( - v_to, var_val, integer_tolerance, zero_tolerance, ignore_integrality + var, var_val, integer_tolerance, zero_tolerance, ignore_integrality ): - """This function copies variable value from one to another. + """This function tries to set a valid value for variable with the given input. Rounds to Binary/Integer if necessary. Sets to zero for NonNegativeReals if necessary. @@ -1002,10 +1002,10 @@ def set_var_valid_value( Parameters ---------- - v_to : Var + var : Var The variable that needs to set value. var_val : float - The desired value to set for Var v_to. + The desired value to set for var. integer_tolerance: float Tolerance on integral values. zero_tolerance: float @@ -1016,31 +1016,31 @@ def set_var_valid_value( Raises ------ ValueError - Cannot successfully set the value to variable v_to. + Cannot successfully set the value to the variable. """ # 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". - v_to.stale = True + var.stale = True rounded_val = int(round(var_val)) if ( - var_val in v_to.domain - and not ((v_to.has_lb() and var_val < v_to.lb)) - and not ((v_to.has_ub() and var_val > v_to.ub)) + var_val in var.domain + and not ((var.has_lb() and var_val < var.lb)) + and not ((var.has_ub() and var_val > var.ub)) ): - v_to.set_value(var_val) - elif v_to.has_lb() and var_val < v_to.lb: - v_to.set_value(v_to.lb) - elif v_to.has_ub() and var_val > v_to.ub: - v_to.set_value(v_to.ub) - elif ignore_integrality and v_to.is_integer(): - v_to.set_value(var_val, skip_validation=True) - elif v_to.is_integer() and (math.fabs(var_val - rounded_val) <= integer_tolerance): - v_to.set_value(rounded_val) - elif abs(var_val) <= zero_tolerance and 0 in v_to.domain: - v_to.set_value(0) + var.set_value(var_val) + elif var.has_lb() and var_val < var.lb: + var.set_value(var.lb) + elif var.has_ub() and var_val > var.ub: + var.set_value(var.ub) + elif ignore_integrality and var.is_integer(): + var.set_value(var_val, skip_validation=True) + elif var.is_integer() and (math.fabs(var_val - rounded_val) <= integer_tolerance): + var.set_value(rounded_val) + elif abs(var_val) <= zero_tolerance and 0 in var.domain: + var.set_value(0) else: raise ValueError( "copy_var_list_values failed with variable {}, value = {} and rounded value = {}" - "".format(v_to.name, var_val, rounded_val) + "".format(var.name, var_val, rounded_val) ) From 83b28cb0d4216b337d8308d07ea090092a71a880 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 29 Nov 2023 17:42:10 -0500 Subject: [PATCH 29/53] move NOTE from docstring to comment --- pyomo/contrib/mindtpy/util.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index a9802a8bd1e..afcb129e40e 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -995,11 +995,6 @@ def set_var_valid_value( Rounds to Binary/Integer if necessary. Sets to zero for NonNegativeReals if necessary. - NOTE: PEP 2180 changes the var behavior so that domain / - bounds violations no longer generate exceptions (and - instead log warnings). This means that the following will - always succeed and the ValueError should never be raised. - Parameters ---------- var : Var @@ -1018,6 +1013,11 @@ def set_var_valid_value( ValueError Cannot successfully set the value to the variable. """ + # NOTE: PEP 2180 changes the var behavior so that domain + # bounds violations no longer generate exceptions (and + # instead log warnings). This means that the set_value method + # will always succeed and the ValueError should never be raised. + # 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". From 355df8b4a112597d4c1c45b0b6cd6a4ca13ff6af Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Thu, 30 Nov 2023 10:44:36 -0500 Subject: [PATCH 30/53] remove redundant test --- pyomo/repn/tests/ampl/test_nlv2.py | 34 ------------------------------ 1 file changed, 34 deletions(-) diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index 460c45b4ebb..fe5f422d323 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -1055,40 +1055,6 @@ def test_log_timing(self): re.sub(r'\d\.\d\d\]', '#.##]', LOG.getvalue()), ) - def test_log_timing(self): - # This tests an error possibly reported by #2810 - m = ConcreteModel() - m.x = Var(range(6)) - m.x[0].domain = pyo.Binary - m.x[1].domain = pyo.Integers - m.x[2].domain = pyo.Integers - m.p = Param(initialize=5, mutable=True) - m.o1 = Objective([1, 2], rule=lambda m, i: 1) - m.o2 = Objective(expr=m.x[1] * m.x[2]) - m.c1 = Constraint([1, 2], rule=lambda m, i: sum(m.x.values()) == 1) - m.c2 = Constraint(expr=m.p * m.x[1] ** 2 + m.x[2] ** 3 <= 100) - - self.maxDiff = None - OUT = io.StringIO() - with capture_output() as LOG: - with report_timing(level=logging.DEBUG): - nl_writer.NLWriter().write(m, OUT) - self.assertEqual( - """ [+ #.##] Initialized column order - [+ #.##] Collected suffixes - [+ #.##] Objective o1 - [+ #.##] Objective o2 - [+ #.##] Constraint c1 - [+ #.##] Constraint c2 - [+ #.##] Categorized model variables: 14 nnz - [+ #.##] Set row / column ordering: 6 var [3, 1, 2 R/B/Z], 3 con [2, 1 L/NL] - [+ #.##] Generated row/col labels & comments - [+ #.##] Wrote NL stream - [ #.##] Generated NL representation -""", - re.sub(r'\d\.\d\d\]', '#.##]', LOG.getvalue()), - ) - def test_linear_constraint_npv_const(self): # This tests an error possibly reported by #2810 m = ConcreteModel() From a7a01c229738fe680ad8d2f7f6814ab0e2c38a0c Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Thu, 30 Nov 2023 18:28:06 -0500 Subject: [PATCH 31/53] add test_add_var_bound --- pyomo/contrib/mindtpy/tests/unit_test.py | 31 ++++++++++++++++++++++++ pyomo/contrib/mindtpy/util.py | 4 +-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/mindtpy/tests/unit_test.py b/pyomo/contrib/mindtpy/tests/unit_test.py index baf5e16bb4b..a1ceadda41e 100644 --- a/pyomo/contrib/mindtpy/tests/unit_test.py +++ b/pyomo/contrib/mindtpy/tests/unit_test.py @@ -13,6 +13,10 @@ from pyomo.contrib.mindtpy.util import set_var_valid_value from pyomo.environ import Var, Integers, ConcreteModel, Integers +from pyomo.contrib.mindtpy.algorithm_base_class import _MindtPyAlgorithm +from pyomo.contrib.mindtpy.config_options import _get_MindtPy_OA_config +from pyomo.contrib.mindtpy.tests.MINLP5_simple import SimpleMINLP5 +from pyomo.contrib.mindtpy.util import add_var_bound class UnitTestMindtPy(unittest.TestCase): @@ -65,6 +69,33 @@ def test_set_var_valid_value(self): ) self.assertEqual(m.x1.value, 0) + def test_add_var_bound(self): + m = SimpleMINLP5().clone() + m.x.lb = None + m.x.ub = None + m.y.lb = None + m.y.ub = None + solver_object = _MindtPyAlgorithm() + solver_object.config = _get_MindtPy_OA_config() + solver_object.set_up_solve_data(m) + solver_object.create_utility_block(solver_object.working_model, 'MindtPy_utils') + add_var_bound(solver_object.working_model, solver_object.config) + self.assertEqual( + solver_object.working_model.x.lower, + -solver_object.config.continuous_var_bound - 1, + ) + self.assertEqual( + solver_object.working_model.x.upper, + solver_object.config.continuous_var_bound, + ) + self.assertEqual( + solver_object.working_model.y.lower, + -solver_object.config.integer_var_bound - 1, + ) + self.assertEqual( + solver_object.working_model.y.upper, solver_object.config.integer_var_bound + ) + if __name__ == '__main__': unittest.main() diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index afcb129e40e..1173dfe0cca 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -134,12 +134,12 @@ def add_var_bound(model, config): for var in EXPR.identify_variables(c.body): if var.has_lb() and var.has_ub(): continue - elif not var.has_lb(): + if not var.has_lb(): if var.is_integer(): var.setlb(-config.integer_var_bound - 1) else: var.setlb(-config.continuous_var_bound - 1) - elif not var.has_ub(): + if not var.has_ub(): if var.is_integer(): var.setub(config.integer_var_bound) else: From 875269fb7b7d5cdb3396ff1b7a2e5e2b5fc4e0d2 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Thu, 30 Nov 2023 18:29:28 -0500 Subject: [PATCH 32/53] delete redundant set_up_logger function --- pyomo/contrib/mindtpy/util.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index 1173dfe0cca..ec2829c6a18 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -724,25 +724,6 @@ def f(gurobi_model, where): return f -def set_up_logger(config): - """Set up the formatter and handler for logger. - - Parameters - ---------- - config : ConfigBlock - The specific configurations for MindtPy. - """ - config.logger.handlers.clear() - config.logger.propagate = False - ch = logging.StreamHandler() - ch.setLevel(config.logging_level) - # create formatter and add it to the handlers - formatter = logging.Formatter('%(message)s') - ch.setFormatter(formatter) - # add the handlers to logger - config.logger.addHandler(ch) - - def epigraph_reformulation(exp, slack_var_list, constraint_list, use_mcpp, sense): """Epigraph reformulation. From 65e58f531c40fa466da60954b4bc5cc9b508055d Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Thu, 30 Nov 2023 19:50:14 -0500 Subject: [PATCH 33/53] add test_FP_L1_norm --- .../mindtpy/tests/test_mindtpy_feas_pump.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py b/pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py index 697a63d17c8..dcb5c4bce75 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py @@ -17,7 +17,7 @@ from pyomo.contrib.mindtpy.tests.feasibility_pump1 import FeasPump1 from pyomo.contrib.mindtpy.tests.feasibility_pump2 import FeasPump2 -required_solvers = ('ipopt', 'cplex') +required_solvers = ('ipopt', 'glpk') # TODO: 'appsi_highs' will fail here. if all(SolverFactory(s).available(exception_flag=False) for s in required_solvers): subsolvers_available = True @@ -69,6 +69,22 @@ def test_FP(self): log_infeasible_constraints(model) self.assertTrue(is_feasible(model, self.get_config(opt))) + def test_FP_L1_norm(self): + """Test the feasibility pump algorithm.""" + with SolverFactory('mindtpy') as opt: + for model in model_list: + model = model.clone() + results = opt.solve( + model, + strategy='FP', + mip_solver=required_solvers[1], + nlp_solver=required_solvers[0], + absolute_bound_tolerance=1e-5, + fp_main_norm='L1', + ) + log_infeasible_constraints(model) + self.assertTrue(is_feasible(model, self.get_config(opt))) + def test_FP_OA_8PP(self): """Test the FP-OA algorithm.""" with SolverFactory('mindtpy') as opt: From c8eead976a96ee87e79ce22bf8866e6b28abeb66 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Thu, 30 Nov 2023 20:08:16 -0500 Subject: [PATCH 34/53] improve mindtpy logging --- pyomo/contrib/mindtpy/algorithm_base_class.py | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index 2eec150453f..78250d1ba59 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -125,6 +125,9 @@ def __init__(self, **kwds): self.log_formatter = ( ' {:>9} {:>15} {:>15g} {:>12g} {:>12g} {:>7.2%} {:>7.2f}' ) + self.termination_condition_log_formatter = ( + ' {:>9} {:>15} {:>15} {:>12g} {:>12g} {:>7.2%} {:>7.2f}' + ) self.fixed_nlp_log_formatter = ( '{:1}{:>9} {:>15} {:>15g} {:>12g} {:>12g} {:>7.2%} {:>7.2f}' ) @@ -1919,11 +1922,6 @@ def handle_main_max_timelimit(self, main_mip, main_mip_results): """ # If we have found a valid feasible solution, we take that. If not, we can at least use the dual bound. MindtPy = main_mip.MindtPy_utils - self.config.logger.info( - 'Unable to optimize MILP main problem ' - 'within time limit. ' - 'Using current solver feasible solution.' - ) copy_var_list_values( main_mip.MindtPy_utils.variable_list, self.fixed_nlp.MindtPy_utils.variable_list, @@ -1932,10 +1930,10 @@ def handle_main_max_timelimit(self, main_mip, main_mip_results): ) self.update_suboptimal_dual_bound(main_mip_results) self.config.logger.info( - self.log_formatter.format( + self.termination_condition_log_formatter.format( self.mip_iter, 'MILP', - value(MindtPy.mip_obj.expr), + 'maxTimeLimit', self.primal_bound, self.dual_bound, self.rel_gap, @@ -1962,8 +1960,18 @@ def handle_main_unbounded(self, main_mip): # to the constraints, and deactivated for the linear main problem. config = self.config MindtPy = main_mip.MindtPy_utils + config.logger.info( + self.termination_condition_log_formatter.format( + self.mip_iter, + 'MILP', + 'Unbounded', + self.primal_bound, + self.dual_bound, + self.rel_gap, + get_main_elapsed_time(self.timing), + ) + ) config.logger.warning( - 'main MILP was unbounded. ' 'Resolving with arbitrary bound values of (-{0:.10g}, {0:.10g}) on the objective. ' 'You can change this bound with the option obj_bound.'.format( config.obj_bound From d22946164ff018f41d42d9f554090b46fedac371 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Thu, 7 Dec 2023 13:29:39 -0500 Subject: [PATCH 35/53] fix greybox cuts bug --- pyomo/contrib/mindtpy/cut_generation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/mindtpy/cut_generation.py b/pyomo/contrib/mindtpy/cut_generation.py index e57cfd2eada..4ee7a6ff07b 100644 --- a/pyomo/contrib/mindtpy/cut_generation.py +++ b/pyomo/contrib/mindtpy/cut_generation.py @@ -210,8 +210,8 @@ def add_oa_cuts_for_grey_box( target_model_grey_box.inputs.values() ) ) + - (output - value(output)) ) - - (output - value(output)) - (slack_var if config.add_slack else 0) <= 0 ) From 3d1db1363f5e96069ef5f6deafe458a83f0fb0fe Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Thu, 7 Dec 2023 16:21:59 -0500 Subject: [PATCH 36/53] redesign calc_jacobians function --- pyomo/contrib/mindtpy/extended_cutting_plane.py | 4 +++- pyomo/contrib/mindtpy/feasibility_pump.py | 4 +++- pyomo/contrib/mindtpy/outer_approximation.py | 4 +++- pyomo/contrib/mindtpy/util.py | 10 +++++----- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/mindtpy/extended_cutting_plane.py b/pyomo/contrib/mindtpy/extended_cutting_plane.py index 3a09af155a0..08c89a4c5f0 100644 --- a/pyomo/contrib/mindtpy/extended_cutting_plane.py +++ b/pyomo/contrib/mindtpy/extended_cutting_plane.py @@ -86,7 +86,9 @@ def check_config(self): def initialize_mip_problem(self): '''Deactivate the nonlinear constraints to create the MIP problem.''' super().initialize_mip_problem() - self.jacobians = calc_jacobians(self.mip, self.config) # preload jacobians + self.jacobians = calc_jacobians( + self.mip, self.config.differentiate_mode + ) # preload jacobians self.mip.MindtPy_utils.cuts.ecp_cuts = ConstraintList( doc='Extended Cutting Planes' ) diff --git a/pyomo/contrib/mindtpy/feasibility_pump.py b/pyomo/contrib/mindtpy/feasibility_pump.py index 990f56b8f93..bf6fb8f84bb 100644 --- a/pyomo/contrib/mindtpy/feasibility_pump.py +++ b/pyomo/contrib/mindtpy/feasibility_pump.py @@ -46,7 +46,9 @@ def check_config(self): def initialize_mip_problem(self): '''Deactivate the nonlinear constraints to create the MIP problem.''' super().initialize_mip_problem() - self.jacobians = calc_jacobians(self.mip, self.config) # preload jacobians + self.jacobians = calc_jacobians( + self.mip, self.config.differentiate_mode + ) # preload jacobians self.mip.MindtPy_utils.cuts.oa_cuts = ConstraintList( doc='Outer approximation cuts' ) diff --git a/pyomo/contrib/mindtpy/outer_approximation.py b/pyomo/contrib/mindtpy/outer_approximation.py index 6cf0b26cb37..4fd140a0bba 100644 --- a/pyomo/contrib/mindtpy/outer_approximation.py +++ b/pyomo/contrib/mindtpy/outer_approximation.py @@ -96,7 +96,9 @@ def check_config(self): def initialize_mip_problem(self): '''Deactivate the nonlinear constraints to create the MIP problem.''' super().initialize_mip_problem() - self.jacobians = calc_jacobians(self.mip, self.config) # preload jacobians + self.jacobians = calc_jacobians( + self.mip, self.config.differentiate_mode + ) # preload jacobians self.mip.MindtPy_utils.cuts.oa_cuts = ConstraintList( doc='Outer approximation cuts' ) diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index ec2829c6a18..5ca4604d37e 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -41,7 +41,7 @@ numpy = attempt_import('numpy')[0] -def calc_jacobians(model, config): +def calc_jacobians(model, differentiate_mode): """Generates a map of jacobians for the variables in the model. This function generates a map of jacobians corresponding to the variables in the @@ -51,15 +51,15 @@ def calc_jacobians(model, config): ---------- model : Pyomo model Target model to calculate jacobian. - config : ConfigBlock - The specific configurations for MindtPy. + differentiate_mode : String + The differentiate mode to calculate Jacobians. """ # Map nonlinear_constraint --> Map( # variable --> jacobian of constraint w.r.t. variable) jacobians = ComponentMap() - if config.differentiate_mode == 'reverse_symbolic': + if differentiate_mode == 'reverse_symbolic': mode = EXPR.differentiate.Modes.reverse_symbolic - elif config.differentiate_mode == 'sympy': + elif differentiate_mode == 'sympy': mode = EXPR.differentiate.Modes.sympy for c in model.MindtPy_utils.nonlinear_constraint_list: vars_in_constr = list(EXPR.identify_variables(c.body)) From 7e694136a8ac8cd197c7b56f2613a40597acbb59 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Thu, 7 Dec 2023 16:26:05 -0500 Subject: [PATCH 37/53] redesign initialize_feas_subproblem function --- pyomo/contrib/mindtpy/algorithm_base_class.py | 2 +- pyomo/contrib/mindtpy/util.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index 78250d1ba59..05f1e4389d3 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -2629,7 +2629,7 @@ def initialize_mip_problem(self): self.fixed_nlp = self.working_model.clone() TransformationFactory('core.fix_integer_vars').apply_to(self.fixed_nlp) - initialize_feas_subproblem(self.fixed_nlp, config) + initialize_feas_subproblem(self.fixed_nlp, config.feasibility_norm) def initialize_subsolvers(self): """Initialize and set options for MIP and NLP subsolvers.""" diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index 5ca4604d37e..551945dfc67 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -70,7 +70,7 @@ def calc_jacobians(model, differentiate_mode): return jacobians -def initialize_feas_subproblem(m, config): +def initialize_feas_subproblem(m, feasibility_norm): """Adds feasibility slack variables according to config.feasibility_norm (given an infeasible problem). Defines the objective function of the feasibility subproblem. @@ -78,14 +78,14 @@ def initialize_feas_subproblem(m, config): ---------- m : Pyomo model The feasbility NLP subproblem. - config : ConfigBlock - The specific configurations for MindtPy. + feasibility_norm : String + The norm used to generate the objective function. """ MindtPy = m.MindtPy_utils # generate new constraints for i, constr in enumerate(MindtPy.nonlinear_constraint_list, 1): if constr.has_ub(): - if config.feasibility_norm in {'L1', 'L2'}: + if feasibility_norm in {'L1', 'L2'}: MindtPy.feas_opt.feas_constraints.add( constr.body - constr.upper <= MindtPy.feas_opt.slack_var[i] ) @@ -94,7 +94,7 @@ def initialize_feas_subproblem(m, config): constr.body - constr.upper <= MindtPy.feas_opt.slack_var ) if constr.has_lb(): - if config.feasibility_norm in {'L1', 'L2'}: + if feasibility_norm in {'L1', 'L2'}: MindtPy.feas_opt.feas_constraints.add( constr.body - constr.lower >= -MindtPy.feas_opt.slack_var[i] ) @@ -103,11 +103,11 @@ def initialize_feas_subproblem(m, config): constr.body - constr.lower >= -MindtPy.feas_opt.slack_var ) # Setup objective function for the feasibility subproblem. - if config.feasibility_norm == 'L1': + if feasibility_norm == 'L1': MindtPy.feas_obj = Objective( expr=sum(s for s in MindtPy.feas_opt.slack_var.values()), sense=minimize ) - elif config.feasibility_norm == 'L2': + elif feasibility_norm == 'L2': MindtPy.feas_obj = Objective( expr=sum(s * s for s in MindtPy.feas_opt.slack_var.values()), sense=minimize ) From c9f788849c0c15c197e85a8ce7e10df52bce2c98 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Thu, 7 Dec 2023 16:34:26 -0500 Subject: [PATCH 38/53] redesign calc_jacobians function --- pyomo/contrib/mindtpy/extended_cutting_plane.py | 3 ++- pyomo/contrib/mindtpy/feasibility_pump.py | 3 ++- pyomo/contrib/mindtpy/outer_approximation.py | 3 ++- pyomo/contrib/mindtpy/util.py | 10 +++++----- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/mindtpy/extended_cutting_plane.py b/pyomo/contrib/mindtpy/extended_cutting_plane.py index 08c89a4c5f0..f5fa205e091 100644 --- a/pyomo/contrib/mindtpy/extended_cutting_plane.py +++ b/pyomo/contrib/mindtpy/extended_cutting_plane.py @@ -87,7 +87,8 @@ def initialize_mip_problem(self): '''Deactivate the nonlinear constraints to create the MIP problem.''' super().initialize_mip_problem() self.jacobians = calc_jacobians( - self.mip, self.config.differentiate_mode + self.mip.MindtPy_utils.nonlinear_constraint_list, + self.config.differentiate_mode, ) # preload jacobians self.mip.MindtPy_utils.cuts.ecp_cuts = ConstraintList( doc='Extended Cutting Planes' diff --git a/pyomo/contrib/mindtpy/feasibility_pump.py b/pyomo/contrib/mindtpy/feasibility_pump.py index bf6fb8f84bb..9d5be89bab5 100644 --- a/pyomo/contrib/mindtpy/feasibility_pump.py +++ b/pyomo/contrib/mindtpy/feasibility_pump.py @@ -47,7 +47,8 @@ def initialize_mip_problem(self): '''Deactivate the nonlinear constraints to create the MIP problem.''' super().initialize_mip_problem() self.jacobians = calc_jacobians( - self.mip, self.config.differentiate_mode + self.mip.MindtPy_utils.nonlinear_constraint_list, + self.config.differentiate_mode, ) # preload jacobians self.mip.MindtPy_utils.cuts.oa_cuts = ConstraintList( doc='Outer approximation cuts' diff --git a/pyomo/contrib/mindtpy/outer_approximation.py b/pyomo/contrib/mindtpy/outer_approximation.py index 4fd140a0bba..6d790ce70d0 100644 --- a/pyomo/contrib/mindtpy/outer_approximation.py +++ b/pyomo/contrib/mindtpy/outer_approximation.py @@ -97,7 +97,8 @@ def initialize_mip_problem(self): '''Deactivate the nonlinear constraints to create the MIP problem.''' super().initialize_mip_problem() self.jacobians = calc_jacobians( - self.mip, self.config.differentiate_mode + self.mip.MindtPy_utils.nonlinear_constraint_list, + self.config.differentiate_mode, ) # preload jacobians self.mip.MindtPy_utils.cuts.oa_cuts = ConstraintList( doc='Outer approximation cuts' diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index 551945dfc67..5845b3047f5 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -41,16 +41,16 @@ numpy = attempt_import('numpy')[0] -def calc_jacobians(model, differentiate_mode): +def calc_jacobians(constraint_list, differentiate_mode): """Generates a map of jacobians for the variables in the model. This function generates a map of jacobians corresponding to the variables in the - model. + constraint list. Parameters ---------- - model : Pyomo model - Target model to calculate jacobian. + constraint_list : List + The list of constraints to calculate Jacobians. differentiate_mode : String The differentiate mode to calculate Jacobians. """ @@ -61,7 +61,7 @@ def calc_jacobians(model, differentiate_mode): mode = EXPR.differentiate.Modes.reverse_symbolic elif differentiate_mode == 'sympy': mode = EXPR.differentiate.Modes.sympy - for c in model.MindtPy_utils.nonlinear_constraint_list: + for c in constraint_list: vars_in_constr = list(EXPR.identify_variables(c.body)) jac_list = EXPR.differentiate(c.body, wrt_list=vars_in_constr, mode=mode) jacobians[c] = ComponentMap( From a93d793e49f9f209f188d5a7ee1a72829a7423c4 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 10 Jan 2024 14:33:02 -0500 Subject: [PATCH 39/53] change all ''' to """ --- pyomo/contrib/mindtpy/algorithm_base_class.py | 6 +++--- pyomo/contrib/mindtpy/extended_cutting_plane.py | 2 +- pyomo/contrib/mindtpy/feasibility_pump.py | 2 +- pyomo/contrib/mindtpy/global_outer_approximation.py | 2 +- pyomo/contrib/mindtpy/outer_approximation.py | 2 +- pyomo/contrib/mindtpy/tests/MINLP_simple_grey_box.py | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index 05f1e4389d3..d732e95c422 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -519,9 +519,9 @@ def get_primal_integral(self): return primal_integral def get_integral_info(self): - ''' + """ Obtain primal integral, dual integral and primal dual gap integral. - ''' + """ self.primal_integral = self.get_primal_integral() self.dual_integral = self.get_dual_integral() self.primal_dual_gap_integral = self.primal_integral + self.dual_integral @@ -2598,7 +2598,7 @@ def fp_loop(self): self.working_model.MindtPy_utils.cuts.del_component('fp_orthogonality_cuts') def initialize_mip_problem(self): - '''Deactivate the nonlinear constraints to create the MIP problem.''' + """Deactivate the nonlinear constraints to create the MIP problem.""" # if single tree is activated, we need to add bounds for unbounded variables in nonlinear constraints to avoid unbounded main problem. config = self.config if config.single_tree: diff --git a/pyomo/contrib/mindtpy/extended_cutting_plane.py b/pyomo/contrib/mindtpy/extended_cutting_plane.py index f5fa205e091..ac13e352e35 100644 --- a/pyomo/contrib/mindtpy/extended_cutting_plane.py +++ b/pyomo/contrib/mindtpy/extended_cutting_plane.py @@ -84,7 +84,7 @@ def check_config(self): super().check_config() def initialize_mip_problem(self): - '''Deactivate the nonlinear constraints to create the MIP problem.''' + """Deactivate the nonlinear constraints to create the MIP problem.""" super().initialize_mip_problem() self.jacobians = calc_jacobians( self.mip.MindtPy_utils.nonlinear_constraint_list, diff --git a/pyomo/contrib/mindtpy/feasibility_pump.py b/pyomo/contrib/mindtpy/feasibility_pump.py index 9d5be89bab5..a34cceb014c 100644 --- a/pyomo/contrib/mindtpy/feasibility_pump.py +++ b/pyomo/contrib/mindtpy/feasibility_pump.py @@ -44,7 +44,7 @@ def check_config(self): super().check_config() def initialize_mip_problem(self): - '''Deactivate the nonlinear constraints to create the MIP problem.''' + """Deactivate the nonlinear constraints to create the MIP problem.""" super().initialize_mip_problem() self.jacobians = calc_jacobians( self.mip.MindtPy_utils.nonlinear_constraint_list, diff --git a/pyomo/contrib/mindtpy/global_outer_approximation.py b/pyomo/contrib/mindtpy/global_outer_approximation.py index 817fb0bf4a8..70fc4cffb90 100644 --- a/pyomo/contrib/mindtpy/global_outer_approximation.py +++ b/pyomo/contrib/mindtpy/global_outer_approximation.py @@ -67,7 +67,7 @@ def check_config(self): super().check_config() def initialize_mip_problem(self): - '''Deactivate the nonlinear constraints to create the MIP problem.''' + """Deactivate the nonlinear constraints to create the MIP problem.""" super().initialize_mip_problem() self.mip.MindtPy_utils.cuts.aff_cuts = ConstraintList(doc='Affine cuts') diff --git a/pyomo/contrib/mindtpy/outer_approximation.py b/pyomo/contrib/mindtpy/outer_approximation.py index 6d790ce70d0..f6e6147724e 100644 --- a/pyomo/contrib/mindtpy/outer_approximation.py +++ b/pyomo/contrib/mindtpy/outer_approximation.py @@ -94,7 +94,7 @@ def check_config(self): _MindtPyAlgorithm.check_config(self) def initialize_mip_problem(self): - '''Deactivate the nonlinear constraints to create the MIP problem.''' + """Deactivate the nonlinear constraints to create the MIP problem.""" super().initialize_mip_problem() self.jacobians = calc_jacobians( self.mip.MindtPy_utils.nonlinear_constraint_list, diff --git a/pyomo/contrib/mindtpy/tests/MINLP_simple_grey_box.py b/pyomo/contrib/mindtpy/tests/MINLP_simple_grey_box.py index 547efc0a74c..9c1f33e80cc 100644 --- a/pyomo/contrib/mindtpy/tests/MINLP_simple_grey_box.py +++ b/pyomo/contrib/mindtpy/tests/MINLP_simple_grey_box.py @@ -114,7 +114,7 @@ def evaluate_jacobian_equality_constraints(self): """Evaluate the Jacobian of the equality constraints.""" return None - ''' + """ def _extract_and_assemble_fim(self): M = np.zeros((self.n_parameters, self.n_parameters)) for i in range(self.n_parameters): @@ -122,7 +122,7 @@ def _extract_and_assemble_fim(self): M[i,k] = self._input_values[self.ele_to_order[(i,k)]] return M - ''' + """ def evaluate_jacobian_outputs(self): """Evaluate the Jacobian of the outputs.""" From 666cbaa4e776947032e86887473f1296a9baacff Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 10 Jan 2024 14:39:32 -0500 Subject: [PATCH 40/53] rename int_sol_2_cuts_ind to integer_solution_to_cuts_index --- pyomo/contrib/mindtpy/algorithm_base_class.py | 8 ++++---- pyomo/contrib/mindtpy/single_tree.py | 9 ++++----- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index d732e95c422..7e8d390976c 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -102,14 +102,14 @@ def __init__(self, **kwds): self.fixed_nlp = None # We store bounds, timing info, iteration count, incumbent, and the - # expression of the original (possibly nonlinear) objective function. + # Expression of the original (possibly nonlinear) objective function. self.results = SolverResults() self.timing = Bunch() self.curr_int_sol = [] self.should_terminate = False self.integer_list = [] - # dictionary {integer solution (list): [cuts begin index, cuts end index] (list)} - self.int_sol_2_cuts_ind = dict() + # Dictionary {integer solution (list): [cuts begin index, cuts end index] (list)} + self.integer_solution_to_cuts_index = dict() # Set up iteration counters self.nlp_iter = 0 @@ -813,7 +813,7 @@ def MindtPy_initialization(self): self.integer_list.append(self.curr_int_sol) fixed_nlp, fixed_nlp_result = self.solve_subproblem() self.handle_nlp_subproblem_tc(fixed_nlp, fixed_nlp_result) - self.int_sol_2_cuts_ind[self.curr_int_sol] = [ + self.integer_solution_to_cuts_index[self.curr_int_sol] = [ 1, len(self.mip.MindtPy_utils.cuts.oa_cuts), ] diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index c4d49e3afd6..10b1f21ec5c 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -906,7 +906,7 @@ def LazyOACallback_gurobi(cb_m, cb_opt, cb_where, mindtpy_solver, config): # 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.int_sol_2_cuts_ind[ + 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): @@ -924,10 +924,9 @@ def LazyOACallback_gurobi(cb_m, cb_opt, cb_where, mindtpy_solver, config): 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.int_sol_2_cuts_ind[mindtpy_solver.curr_int_sol] = [ - cut_ind + 1, - len(mindtpy_solver.mip.MindtPy_utils.cuts.oa_cuts), - ] + mindtpy_solver.integer_solution_to_cuts_index[ + mindtpy_solver.curr_int_sol + ] = [cut_ind + 1, len(mindtpy_solver.mip.MindtPy_utils.cuts.oa_cuts)] def handle_lazy_main_feasible_solution_gurobi(cb_m, cb_opt, mindtpy_solver, config): From 14ebdcdc8a03c947164b64729a24e53c4b1b02db Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 10 Jan 2024 15:46:05 -0500 Subject: [PATCH 41/53] add one more comment to CPLEX lazy constraint callback --- pyomo/contrib/mindtpy/single_tree.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index 10b1f21ec5c..145d85e0d37 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -666,6 +666,7 @@ def __call__(self): 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( From b143e87de6eb464cfec2b190114c6f068c9433ae Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 10 Jan 2024 15:46:51 -0500 Subject: [PATCH 42/53] remove the finished TODO --- pyomo/contrib/mindtpy/single_tree.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index 145d85e0d37..09b5e704f75 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -274,7 +274,6 @@ def add_lazy_affine_cuts(self, mindtpy_solver, config, opt): 'Skipping constraint %s due to MCPP error' % (constr.name) ) continue # skip to the next constraint - # TODO: check if the value of ccSlope and cvSlope is not Nan or inf. If so, we skip this. ccSlope = mc_eqn.subcc() cvSlope = mc_eqn.subcv() ccStart = mc_eqn.concave() From 09bda47d460033736c2789e0a8e6d5dbaf581d6a Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 10 Jan 2024 15:48:44 -0500 Subject: [PATCH 43/53] add TODO for self.abort() --- pyomo/contrib/mindtpy/single_tree.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index 09b5e704f75..228810a8f90 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -689,6 +689,7 @@ def __call__(self): 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) @@ -744,6 +745,7 @@ def __call__(self): ) ) 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 From 317dae89cdb4eac00e006bc808d32d887a14d787 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Thu, 11 Jan 2024 14:30:08 -0500 Subject: [PATCH 44/53] update the version of MindtPy --- pyomo/contrib/mindtpy/MindtPy.py | 8 ++++++++ pyomo/contrib/mindtpy/__init__.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/mindtpy/MindtPy.py b/pyomo/contrib/mindtpy/MindtPy.py index 6eb27c4c649..bd873d950fd 100644 --- a/pyomo/contrib/mindtpy/MindtPy.py +++ b/pyomo/contrib/mindtpy/MindtPy.py @@ -50,6 +50,14 @@ - Add single-tree implementation. - Add support for cplex_persistent solver. - Fix bug in OA cut expression in cut_generation.py. + +24.1.11 changes: +- fix gurobi single tree termination check bug +- fix Gurobi single tree cycle handling +- fix bug in feasibility pump method +- add special handling for infeasible relaxed NLP +- update the log format of infeasible fixed NLP subproblems +- create a new copy_var_list_values function """ from pyomo.contrib.mindtpy import __version__ diff --git a/pyomo/contrib/mindtpy/__init__.py b/pyomo/contrib/mindtpy/__init__.py index 8e2c2d9eaa4..8dcd085211f 100644 --- a/pyomo/contrib/mindtpy/__init__.py +++ b/pyomo/contrib/mindtpy/__init__.py @@ -1 +1 @@ -__version__ = (0, 1, 0) +__version__ = (1, 0, 0) From 1b73570e6cceab7127d0dca416dfad2774fedca6 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 31 Jan 2024 18:39:13 -0500 Subject: [PATCH 45/53] correct typos --- pyomo/contrib/mindtpy/algorithm_base_class.py | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index ad462221ec5..b6a223ba24b 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -108,7 +108,7 @@ def __init__(self, **kwds): self.curr_int_sol = [] self.should_terminate = False self.integer_list = [] - # Dictionary {integer solution (list): [cuts begin index, cuts end index] (list)} + # Dictionary {integer solution (tuple): [cuts begin index, cuts end index] (list)} self.integer_solution_to_cuts_index = dict() # Set up iteration counters @@ -2679,9 +2679,9 @@ def initialize_subsolvers(self): if config.mip_regularization_solver == 'gams': self.regularization_mip_opt.options['add_options'] = [] if config.regularization_mip_threads > 0: - self.regularization_mip_opt.options['threads'] = ( - config.regularization_mip_threads - ) + self.regularization_mip_opt.options[ + 'threads' + ] = config.regularization_mip_threads else: self.regularization_mip_opt.options['threads'] = config.threads @@ -2691,9 +2691,9 @@ def initialize_subsolvers(self): 'cplex_persistent', }: if config.solution_limit is not None: - self.regularization_mip_opt.options['mip_limits_solutions'] = ( - config.solution_limit - ) + self.regularization_mip_opt.options[ + 'mip_limits_solutions' + ] = config.solution_limit # We don't need to solve the regularization problem to optimality. # We will choose to perform aggressive node probing during presolve. self.regularization_mip_opt.options['mip_strategy_presolvenode'] = 3 @@ -2706,9 +2706,9 @@ def initialize_subsolvers(self): self.regularization_mip_opt.options['optimalitytarget'] = 3 elif config.mip_regularization_solver == 'gurobi': if config.solution_limit is not None: - self.regularization_mip_opt.options['SolutionLimit'] = ( - config.solution_limit - ) + self.regularization_mip_opt.options[ + 'SolutionLimit' + ] = config.solution_limit # Same reason as mip_strategy_presolvenode. self.regularization_mip_opt.options['Presolve'] = 2 @@ -3055,9 +3055,10 @@ def add_regularization(self): # The main problem might be unbounded, regularization is activated only when a valid bound is provided. if self.dual_bound != self.dual_bound_progress[0]: with time_code(self.timing, 'regularization main'): - (regularization_main_mip, regularization_main_mip_results) = ( - self.solve_regularization_main() - ) + ( + regularization_main_mip, + regularization_main_mip_results, + ) = self.solve_regularization_main() self.handle_regularization_main_tc( regularization_main_mip, regularization_main_mip_results ) From 4ec0e8c69ac7d1992f6879ba9b0e3e52352a9fea Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 31 Jan 2024 18:40:21 -0500 Subject: [PATCH 46/53] remove unused log --- pyomo/contrib/mindtpy/algorithm_base_class.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index b6a223ba24b..a4f3075a1e9 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -1290,7 +1290,6 @@ def handle_subproblem_infeasible(self, fixed_nlp, cb_opt=None): # elif var.has_lb() and abs(value(var) - var.lb) < config.absolute_bound_tolerance: # fixed_nlp.ipopt_zU_out[var] = -1 - # config.logger.info('Solving feasibility problem') feas_subproblem, feas_subproblem_results = self.solve_feasibility_subproblem() # TODO: do we really need this? if self.should_terminate: From acb10de438ccc8c7acb5b8ba7d42af7eab3694e9 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 31 Jan 2024 19:32:34 -0500 Subject: [PATCH 47/53] add one condition for fix dual bound --- pyomo/contrib/mindtpy/algorithm_base_class.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index a4f3075a1e9..ca428563a68 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -1561,7 +1561,7 @@ def fix_dual_bound(self, last_iter_cuts): self.handle_nlp_subproblem_tc(fixed_nlp, fixed_nlp_result) MindtPy = self.mip.MindtPy_utils - # deactivate the integer cuts generated after the best solution was found. + # Deactivate the integer cuts generated after the best solution was found. self.deactivate_no_good_cuts_when_fixing_bound(MindtPy.cuts.no_good_cuts) if ( config.add_regularization is not None @@ -3013,10 +3013,12 @@ def MindtPy_iteration_loop(self): # if add_no_good_cuts is True, the bound obtained in the last iteration is no reliable. # we correct it after the iteration. + # There is no need to fix the dual bound if no feasible solution has been found. if ( (config.add_no_good_cuts or config.use_tabu_list) and not self.should_terminate and config.add_regularization is None + and self.best_solution_found is not None ): self.fix_dual_bound(self.last_iter_cuts) config.logger.info( From de73340cf82b50d932ebe25a165c9bc3d7c63230 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 31 Jan 2024 19:41:24 -0500 Subject: [PATCH 48/53] remove fix_dual_bound for ECP method --- pyomo/contrib/mindtpy/extended_cutting_plane.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pyomo/contrib/mindtpy/extended_cutting_plane.py b/pyomo/contrib/mindtpy/extended_cutting_plane.py index ac13e352e35..0a98f88ed3f 100644 --- a/pyomo/contrib/mindtpy/extended_cutting_plane.py +++ b/pyomo/contrib/mindtpy/extended_cutting_plane.py @@ -66,12 +66,6 @@ def MindtPy_iteration_loop(self): add_ecp_cuts(self.mip, self.jacobians, self.config, self.timing) - # if add_no_good_cuts is True, the bound obtained in the last iteration is no reliable. - # we correct it after the iteration. - if ( - self.config.add_no_good_cuts or self.config.use_tabu_list - ) and not self.should_terminate: - self.fix_dual_bound(self.last_iter_cuts) self.config.logger.info( ' ===============================================================================================' ) From 92d9477985e92bcccd2785c4862eb1491dca3488 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 31 Jan 2024 19:52:56 -0500 Subject: [PATCH 49/53] black format --- pyomo/contrib/mindtpy/single_tree.py | 7 ++++--- pyomo/contrib/mindtpy/tests/nonconvex3.py | 4 +++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index c1e52ed72d3..228810a8f90 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -588,9 +588,10 @@ def handle_lazy_subproblem_infeasible(self, fixed_nlp, mindtpy_solver, config, o dual_values = None config.logger.info('Solving feasibility problem') - (feas_subproblem, feas_subproblem_results) = ( - mindtpy_solver.solve_feasibility_subproblem() - ) + ( + 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( diff --git a/pyomo/contrib/mindtpy/tests/nonconvex3.py b/pyomo/contrib/mindtpy/tests/nonconvex3.py index b08deb67b63..dbb88bb1fad 100644 --- a/pyomo/contrib/mindtpy/tests/nonconvex3.py +++ b/pyomo/contrib/mindtpy/tests/nonconvex3.py @@ -40,7 +40,9 @@ def __init__(self, *args, **kwargs): m.objective = Objective(expr=7 * m.x1 + 10 * m.x2, sense=minimize) - m.c1 = Constraint(expr=(m.x1**1.2) * (m.x2**1.7) - 7 * m.x1 - 9 * m.x2 <= -24) + m.c1 = Constraint( + expr=(m.x1**1.2) * (m.x2**1.7) - 7 * m.x1 - 9 * m.x2 <= -24 + ) m.c2 = Constraint(expr=-m.x1 - 2 * m.x2 <= 5) m.c3 = Constraint(expr=-3 * m.x1 + m.x2 <= 1) m.c4 = Constraint(expr=4 * m.x1 - 3 * m.x2 <= 11) From 8d051b09ebaf84ec946b2d2e4f64a865bedde66e Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Thu, 1 Feb 2024 13:06:34 -0500 Subject: [PATCH 50/53] black format --- pyomo/contrib/mindtpy/algorithm_base_class.py | 25 +++++++++---------- pyomo/contrib/mindtpy/single_tree.py | 7 +++--- pyomo/contrib/mindtpy/tests/nonconvex3.py | 4 +-- 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index ca428563a68..3d5a7ebad03 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -2678,9 +2678,9 @@ def initialize_subsolvers(self): if config.mip_regularization_solver == 'gams': self.regularization_mip_opt.options['add_options'] = [] if config.regularization_mip_threads > 0: - self.regularization_mip_opt.options[ - 'threads' - ] = config.regularization_mip_threads + self.regularization_mip_opt.options['threads'] = ( + config.regularization_mip_threads + ) else: self.regularization_mip_opt.options['threads'] = config.threads @@ -2690,9 +2690,9 @@ def initialize_subsolvers(self): 'cplex_persistent', }: if config.solution_limit is not None: - self.regularization_mip_opt.options[ - 'mip_limits_solutions' - ] = config.solution_limit + self.regularization_mip_opt.options['mip_limits_solutions'] = ( + config.solution_limit + ) # We don't need to solve the regularization problem to optimality. # We will choose to perform aggressive node probing during presolve. self.regularization_mip_opt.options['mip_strategy_presolvenode'] = 3 @@ -2705,9 +2705,9 @@ def initialize_subsolvers(self): self.regularization_mip_opt.options['optimalitytarget'] = 3 elif config.mip_regularization_solver == 'gurobi': if config.solution_limit is not None: - self.regularization_mip_opt.options[ - 'SolutionLimit' - ] = config.solution_limit + self.regularization_mip_opt.options['SolutionLimit'] = ( + config.solution_limit + ) # Same reason as mip_strategy_presolvenode. self.regularization_mip_opt.options['Presolve'] = 2 @@ -3056,10 +3056,9 @@ def add_regularization(self): # The main problem might be unbounded, regularization is activated only when a valid bound is provided. if self.dual_bound != self.dual_bound_progress[0]: with time_code(self.timing, 'regularization main'): - ( - regularization_main_mip, - regularization_main_mip_results, - ) = self.solve_regularization_main() + (regularization_main_mip, regularization_main_mip_results) = ( + self.solve_regularization_main() + ) self.handle_regularization_main_tc( regularization_main_mip, regularization_main_mip_results ) diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index 228810a8f90..c1e52ed72d3 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -588,10 +588,9 @@ def handle_lazy_subproblem_infeasible(self, fixed_nlp, mindtpy_solver, config, o dual_values = None config.logger.info('Solving feasibility problem') - ( - feas_subproblem, - feas_subproblem_results, - ) = mindtpy_solver.solve_feasibility_subproblem() + (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( diff --git a/pyomo/contrib/mindtpy/tests/nonconvex3.py b/pyomo/contrib/mindtpy/tests/nonconvex3.py index dbb88bb1fad..b08deb67b63 100644 --- a/pyomo/contrib/mindtpy/tests/nonconvex3.py +++ b/pyomo/contrib/mindtpy/tests/nonconvex3.py @@ -40,9 +40,7 @@ def __init__(self, *args, **kwargs): m.objective = Objective(expr=7 * m.x1 + 10 * m.x2, sense=minimize) - m.c1 = Constraint( - expr=(m.x1**1.2) * (m.x2**1.7) - 7 * m.x1 - 9 * m.x2 <= -24 - ) + m.c1 = Constraint(expr=(m.x1**1.2) * (m.x2**1.7) - 7 * m.x1 - 9 * m.x2 <= -24) m.c2 = Constraint(expr=-m.x1 - 2 * m.x2 <= 5) m.c3 = Constraint(expr=-3 * m.x1 + m.x2 <= 1) m.c4 = Constraint(expr=4 * m.x1 - 3 * m.x2 <= 11) From a77a19b5302544aad90b457307f1a5f650583402 Mon Sep 17 00:00:00 2001 From: Zedong Date: Tue, 13 Feb 2024 13:24:13 -0500 Subject: [PATCH 51/53] Update pyomo/contrib/mindtpy/util.py Co-authored-by: Bethany Nicholson --- pyomo/contrib/mindtpy/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index 6061da0f0d9..575544eed5c 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -1024,6 +1024,6 @@ def set_var_valid_value( var.set_value(0) else: raise ValueError( - "copy_var_list_values failed with variable {}, value = {} and rounded value = {}" + "set_var_valid_value failed with variable {}, value = {} and rounded value = {}" "".format(var.name, var_val, rounded_val) ) From a0997d824d9079cc3621b4cb77e2d5933f16804c Mon Sep 17 00:00:00 2001 From: Zedong Date: Tue, 13 Feb 2024 13:24:28 -0500 Subject: [PATCH 52/53] Update pyomo/contrib/mindtpy/util.py Co-authored-by: Bethany Nicholson --- pyomo/contrib/mindtpy/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index 575544eed5c..fa6aec7f08f 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -944,7 +944,7 @@ def copy_var_list_values( Sets to zero for NonNegativeReals if necessary from_list : list - The variables that provides the values to copy from. + The variables that provide the values to copy from. to_list : list The variables that need to set value. config : ConfigBlock From 6c9fa3ee64bbaeb9d8bce565a857b584b886a401 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Tue, 13 Feb 2024 13:34:35 -0500 Subject: [PATCH 53/53] update the differentiate.Modes --- pyomo/contrib/mindtpy/util.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index fa6aec7f08f..69c7ca5030a 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -57,10 +57,7 @@ def calc_jacobians(constraint_list, differentiate_mode): # Map nonlinear_constraint --> Map( # variable --> jacobian of constraint w.r.t. variable) jacobians = ComponentMap() - if differentiate_mode == 'reverse_symbolic': - mode = EXPR.differentiate.Modes.reverse_symbolic - elif differentiate_mode == 'sympy': - mode = EXPR.differentiate.Modes.sympy + mode = EXPR.differentiate.Modes(differentiate_mode) for c in constraint_list: vars_in_constr = list(EXPR.identify_variables(c.body)) jac_list = EXPR.differentiate(c.body, wrt_list=vars_in_constr, mode=mode)