Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix MindtPy bugs #3034

Merged
merged 73 commits into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
6d442b2
disable ipopt warmstart for feasibility subproblem solver
ZedongPeng Sep 21, 2023
a6e92c5
create new copy_var_list_values function
ZedongPeng Sep 21, 2023
f766c0a
update log format
ZedongPeng Sep 21, 2023
8306925
add update_solver_timelimit
ZedongPeng Sep 23, 2023
816e3d6
handle appsi solver unbounded situation
ZedongPeng Oct 9, 2023
5c47040
add skip_validation when ignore integrality
ZedongPeng Oct 10, 2023
a767f28
add special handle for rnlp infeasible
ZedongPeng Oct 10, 2023
a30cf82
fix bug
ZedongPeng Oct 10, 2023
2a6f1d7
add comments
ZedongPeng Oct 12, 2023
6bcdadb
fix bug
Oct 27, 2023
a66f955
improve copy_var_list_values function
ZedongPeng Oct 29, 2023
cefd4a6
fix FP bug
ZedongPeng Oct 29, 2023
dec70eb
Merge branch 'benchmark' of github.com:ZedongPeng/pyomo into benchmark
ZedongPeng Oct 29, 2023
905503b
fix gurobi single tree termination check bug
ZedongPeng Nov 1, 2023
3b01104
fix Gurobi single tree cycle handling
ZedongPeng Nov 2, 2023
906fff7
black format
ZedongPeng Nov 13, 2023
f57c938
Merge branch 'add_grey_box' into benchmark
ZedongPeng Nov 21, 2023
f261fdc
fix load_solutions bug
ZedongPeng Nov 21, 2023
892021f
Merge branch 'main' into benchmark
ZedongPeng Nov 21, 2023
1f2ab73
Merge branch 'main' into benchmark
emma58 Nov 27, 2023
015ebaf
fix typo: change try to trying
ZedongPeng Nov 27, 2023
a6079d5
add more details of the error in copy_var_list_values
ZedongPeng Nov 27, 2023
e8b3b72
create copy_var_value function
ZedongPeng Nov 27, 2023
dc41b8e
add exc_info for the error message
ZedongPeng Nov 28, 2023
dbe9f49
black format
ZedongPeng Nov 28, 2023
655692a
Merge branch 'main' into benchmark
ZedongPeng Nov 28, 2023
4ac390e
change dir() to locals()
ZedongPeng Nov 28, 2023
a755067
improve int_sol_2_cuts_ind
ZedongPeng Nov 28, 2023
51be801
Merge branch 'main' into benchmark
mrmundt Nov 28, 2023
d9d29bf
rename copy_var_value to set_var_value
ZedongPeng Nov 28, 2023
f04424e
add unit test for mindtpy
ZedongPeng Nov 28, 2023
ef66660
improve var_val description
ZedongPeng Nov 29, 2023
04ea15e
rename set_var_value to set_var_valid_value
ZedongPeng Nov 29, 2023
f84ff8d
change v_to to var
ZedongPeng Nov 29, 2023
83b28cb
move NOTE from docstring to comment
ZedongPeng Nov 29, 2023
6ee3f58
Merge branch 'main' into benchmark
ZedongPeng Nov 30, 2023
355df8b
remove redundant test
ZedongPeng Nov 30, 2023
a7a01c2
add test_add_var_bound
ZedongPeng Nov 30, 2023
875269f
delete redundant set_up_logger function
ZedongPeng Nov 30, 2023
65e58f5
add test_FP_L1_norm
ZedongPeng Dec 1, 2023
c8eead9
improve mindtpy logging
ZedongPeng Dec 1, 2023
baa816c
Merge branch 'main' into benchmark
ZedongPeng Dec 5, 2023
d229461
fix greybox cuts bug
ZedongPeng Dec 7, 2023
3d1db13
redesign calc_jacobians function
ZedongPeng Dec 7, 2023
7e69413
redesign initialize_feas_subproblem function
ZedongPeng Dec 7, 2023
c9f7888
redesign calc_jacobians function
ZedongPeng Dec 7, 2023
934b153
Merge branch 'main' into benchmark
ZedongPeng Dec 8, 2023
973fb23
Merge branch 'main' into benchmark
blnicho Dec 19, 2023
b4e9f47
Merge branch 'main' into benchmark
blnicho Jan 9, 2024
a93d793
change all ''' to """
ZedongPeng Jan 10, 2024
666cbaa
rename int_sol_2_cuts_ind to integer_solution_to_cuts_index
ZedongPeng Jan 10, 2024
14ebdcd
add one more comment to CPLEX lazy constraint callback
ZedongPeng Jan 10, 2024
b143e87
remove the finished TODO
ZedongPeng Jan 10, 2024
09bda47
add TODO for self.abort()
ZedongPeng Jan 10, 2024
767da46
Merge branch 'main' into benchmark
bernalde Jan 11, 2024
317dae8
update the version of MindtPy
ZedongPeng Jan 11, 2024
601ec30
Merge branch 'main' into benchmark
mrmundt Jan 15, 2024
76acc80
Merge branch 'main' into benchmark
bernalde Jan 24, 2024
f3414da
Merge branch 'main' into benchmark
blnicho Jan 24, 2024
43f0d24
Merge branch 'main' into benchmark
ZedongPeng Jan 24, 2024
70fe040
Merge branch 'main' into benchmark
ZedongPeng Jan 29, 2024
194e328
Merge branch 'main' into benchmark
ZedongPeng Jan 30, 2024
ff4c031
Merge branch 'main' into benchmark
emma58 Jan 31, 2024
1b73570
correct typos
ZedongPeng Jan 31, 2024
4ec0e8c
remove unused log
ZedongPeng Jan 31, 2024
acb10de
add one condition for fix dual bound
ZedongPeng Feb 1, 2024
de73340
remove fix_dual_bound for ECP method
ZedongPeng Feb 1, 2024
92d9477
black format
ZedongPeng Feb 1, 2024
8d051b0
black format
ZedongPeng Feb 1, 2024
66f5025
Merge branch 'main' into benchmark
ZedongPeng Feb 1, 2024
a77a19b
Update pyomo/contrib/mindtpy/util.py
ZedongPeng Feb 13, 2024
a0997d8
Update pyomo/contrib/mindtpy/util.py
ZedongPeng Feb 13, 2024
6c9fa3e
update the differentiate.Modes
ZedongPeng Feb 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 68 additions & 5 deletions pyomo/contrib/mindtpy/algorithm_base_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@
SuppressInfeasibleWarning,
_DoNothing,
lower_logger_level_to,
copy_var_list_values,
get_main_elapsed_time,
time_code,
)
Expand All @@ -80,6 +79,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')
Expand Down Expand Up @@ -108,6 +108,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
Expand All @@ -126,6 +128,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
Expand Down Expand Up @@ -805,6 +810,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)
)
ZedongPeng marked this conversation as resolved.
Show resolved Hide resolved
elif config.init_strategy == 'FP':
self.init_rNLP()
self.fp_loop()
Expand Down Expand Up @@ -840,6 +848,29 @@ 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
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.'
ZedongPeng marked this conversation as resolved.
Show resolved Hide resolved
)
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=self.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:
Expand Down Expand Up @@ -880,12 +911,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,
Expand Down Expand Up @@ -1219,7 +1252,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:
Expand All @@ -1241,7 +1285,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')
ZedongPeng marked this conversation as resolved.
Show resolved Hide resolved
feas_subproblem, feas_subproblem_results = self.solve_feasibility_subproblem()
# TODO: do we really need this?
if self.should_terminate:
Expand Down Expand Up @@ -1375,6 +1419,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()
Expand Down Expand Up @@ -1601,6 +1657,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(
Expand All @@ -1626,7 +1683,11 @@ 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.
if 'main_mip_results' in dir():
ZedongPeng marked this conversation as resolved.
Show resolved Hide resolved
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 = (
Expand Down Expand Up @@ -1658,6 +1719,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)

main_mip_results = self.mip_opt.solve(
self.mip,
Expand Down Expand Up @@ -2342,6 +2404,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)

Expand Down Expand Up @@ -2585,7 +2648,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()
Expand Down
133 changes: 67 additions & 66 deletions pyomo/contrib/mindtpy/single_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,12 @@
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.gdpopt.util import (
copy_var_list_values,
get_main_elapsed_time,
time_code,
)
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.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')

Expand All @@ -35,14 +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,
ignore_integrality=False,
self, opt, from_list, to_list, config, skip_stale=False, skip_fixed=True
):
"""This function copies variable values from one list to another.

Expand Down Expand Up @@ -72,43 +62,48 @@ 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,
Expand Down Expand Up @@ -708,8 +703,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()
)
)
Expand All @@ -718,6 +713,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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is to mention some reasons why we trigger tests?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be more precise, the algorithm will terminate during the MIP start process as soon as the bounds converge. In such scenarios, we utilize self.abort() to terminate CPLEX. As a consequence, the results.solver.status will be set to error, accompanied by an error message. Our initial choice to use self.abort() was driven by the need for immediate termination upon meeting the convergence criteria. However, I am currently uncertain about the extent of the difference it would make if we allowed CPLEX to terminate on its own.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the error will be captured by the try except command. We don't need to worry about it and it is still the most efficient way to terminate CPLEX. I add a TODO here. We can decide based on the benchmark results in the future.

if (
self.get_solution_source()
!= cplex.callbacks.SolutionSource.mipstart_solution
Expand Down Expand Up @@ -910,19 +906,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

Expand Down Expand Up @@ -953,15 +937,32 @@ 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):
Expand Down
Loading