-
Notifications
You must be signed in to change notification settings - Fork 526
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
Fix MindtPy bugs #3034
Changes from 60 commits
6d442b2
a6e92c5
f766c0a
8306925
816e3d6
5c47040
a767f28
a30cf82
2a6f1d7
6bcdadb
a66f955
cefd4a6
dec70eb
905503b
3b01104
906fff7
f57c938
f261fdc
892021f
1f2ab73
015ebaf
a6079d5
e8b3b72
dc41b8e
dbe9f49
655692a
4ac390e
a755067
51be801
d9d29bf
f04424e
ef66660
04ea15e
f84ff8d
83b28cb
6ee3f58
355df8b
a7a01c2
875269f
65e58f5
c8eead9
baa816c
d229461
3d1db13
7e69413
c9f7888
934b153
973fb23
b4e9f47
a93d793
666cbaa
14ebdcd
b143e87
09bda47
767da46
317dae8
601ec30
76acc80
f3414da
43f0d24
70fe040
194e328
ff4c031
1b73570
4ec0e8c
acb10de
de73340
92d9477
8d051b0
66f5025
a77a19b
a0997d8
6c9fa3e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
__version__ = (0, 1, 0) | ||
__version__ = (1, 0, 0) |
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,12 +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 ( | ||
from pyomo.contrib.mindtpy.util import ( | ||
get_integer_solution, | ||
copy_var_list_values, | ||
get_main_elapsed_time, | ||
time_code, | ||
set_var_valid_value, | ||
) | ||
from pyomo.contrib.gdpopt.util import get_main_elapsed_time, time_code | ||
from pyomo.opt import TerminationCondition as tc | ||
from pyomo.core import minimize, value | ||
from pyomo.core.expr import identify_variables | ||
|
@@ -35,17 +35,9 @@ 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. | ||
|
||
Rounds to Binary/Integer if necessary. | ||
Sets to zero for NonNegativeReals if necessary. | ||
|
||
|
@@ -54,61 +46,29 @@ 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: | ||
continue # Skip stale variable values. | ||
if skip_fixed and v_to.is_fixed(): | ||
continue # Skip fixed variables. | ||
v_val = self.get_values(opt._pyomo_var_to_solver_var_map[v_from]) | ||
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 | ||
set_var_valid_value( | ||
v_to, | ||
v_val, | ||
config.integer_tolerance, | ||
config.zero_tolerance, | ||
ignore_integrality=False, | ||
) | ||
|
||
def add_lazy_oa_cuts( | ||
self, | ||
|
@@ -309,12 +269,11 @@ 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. | ||
ccSlope = mc_eqn.subcc() | ||
cvSlope = mc_eqn.subcv() | ||
ccStart = mc_eqn.concave() | ||
|
@@ -706,10 +665,11 @@ 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.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() | ||
) | ||
) | ||
|
@@ -718,6 +678,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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This comment is to mention some reasons why we trigger tests? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since the error will be captured by the |
||
if ( | ||
self.get_solution_source() | ||
!= cplex.callbacks.SolutionSource.mipstart_solution | ||
|
@@ -728,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) | ||
|
@@ -745,9 +707,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 | ||
|
@@ -783,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 | ||
|
||
|
@@ -910,19 +873,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 | ||
|
||
|
@@ -953,15 +904,31 @@ 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. | ||
begin_index, end_index = mindtpy_solver.integer_solution_to_cuts_index[ | ||
mindtpy_solver.curr_int_sol | ||
] | ||
for ind in range(begin_index, end_index + 1): | ||
cb_opt.cbLazy(mindtpy_solver.mip.MindtPy_utils.cuts.oa_cuts[ind]) | ||
return | ||
else: | ||
mindtpy_solver.integer_list.append(mindtpy_solver.curr_int_sol) | ||
if config.strategy == 'OA': | ||
cut_ind = len(mindtpy_solver.mip.MindtPy_utils.cuts.oa_cuts) | ||
|
||
# solve subproblem | ||
# The constraint linearization happens in the handlers | ||
fixed_nlp, fixed_nlp_result = mindtpy_solver.solve_subproblem() | ||
|
||
mindtpy_solver.handle_nlp_subproblem_tc(fixed_nlp, fixed_nlp_result, cb_opt) | ||
if config.strategy == 'OA': | ||
# store the cut index corresponding to current integer solution. | ||
mindtpy_solver.integer_solution_to_cuts_index[ | ||
mindtpy_solver.curr_int_sol | ||
] = [cut_ind + 1, len(mindtpy_solver.mip.MindtPy_utils.cuts.oa_cuts)] | ||
|
||
|
||
def handle_lazy_main_feasible_solution_gurobi(cb_m, cb_opt, mindtpy_solver, config): | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How can this fail? Wouldn't this be a
DeveloperError
(if you need an error here at all) since mindtpy can keep track of what cuts it has added?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for pointing this out. I checked the log of the benchmark tests and found this error came out when
timelimit
was reached. Let me try to fix it before merging this PR. Thanks.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
KeyError
happens when no feasible solution has been found within thetimelimit
. I added one more condition for thefix_dual_bound
function in the OA, GOA, LP/NLP B&B and GLP/NLP B&B methods.I also removed
fix_dual_bound
for the ECP method since we will never add no-good cuts in the ECP method.I would appreciate it if @bernalde can give it a double-check.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, okay. I think a different error message might be clearer then--it's not deactivating the cuts that failed, it's the solve after you deactivated them (if I'm understanding correctly).