-
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
Add the support of GreyBox in MindtPy #2988
Changes from all commits
bb03a18
1d508e7
ca54afc
ec05a3d
5048b42
c2877a3
ccff416
e461e93
13a7213
20ccbdc
4288e33
8d7f6c5
807e4f5
30771b3
231abc6
72b142b
ff401df
d82dcde
cb1c2a9
e80c6dc
8959666
e0c245b
5ffaeb1
260b2d6
c10e212
46cf8a5
ba135b6
8eacf0b
bd12664
ebe91a6
2b45756
96efbd4
73f2e16
a08fd36
ae84558
7bf0268
993290f
3cf0fc2
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 |
---|---|---|
|
@@ -84,6 +84,9 @@ | |
|
||
single_tree, single_tree_available = attempt_import('pyomo.contrib.mindtpy.single_tree') | ||
tabu_list, tabu_list_available = attempt_import('pyomo.contrib.mindtpy.tabu_list') | ||
egb, egb_available = attempt_import( | ||
'pyomo.contrib.pynumero.interfaces.external_grey_box' | ||
) | ||
|
||
|
||
class _MindtPyAlgorithm(object): | ||
|
@@ -140,6 +143,8 @@ def __init__(self, **kwds): | |
self.last_iter_cuts = False | ||
# Store the OA cuts generated in the mip_start_process. | ||
self.mip_start_lazy_oa_cuts = [] | ||
# Whether to load solutions in solve() function | ||
self.load_solutions = True | ||
|
||
# Support use as a context manager under current solver API | ||
def __enter__(self): | ||
|
@@ -289,7 +294,7 @@ def model_is_valid(self): | |
results = self.mip_opt.solve( | ||
self.original_model, | ||
tee=config.mip_solver_tee, | ||
load_solutions=False, | ||
load_solutions=self.load_solutions, | ||
**config.mip_solver_args, | ||
) | ||
if len(results.solution) > 0: | ||
|
@@ -323,6 +328,14 @@ def build_ordered_component_lists(self, model): | |
ctype=Constraint, active=True, descend_into=(Block) | ||
) | ||
) | ||
if egb_available: | ||
util_block.grey_box_list = list( | ||
model.component_data_objects( | ||
ctype=egb.ExternalGreyBoxBlock, active=True, descend_into=(Block) | ||
) | ||
) | ||
else: | ||
util_block.grey_box_list = [] | ||
util_block.linear_constraint_list = list( | ||
c | ||
for c in util_block.constraint_list | ||
|
@@ -350,11 +363,20 @@ def build_ordered_component_lists(self, model): | |
|
||
# We use component_data_objects rather than list(var_set) in order to | ||
# preserve a deterministic ordering. | ||
util_block.variable_list = list( | ||
v | ||
for v in model.component_data_objects(ctype=Var, descend_into=(Block)) | ||
if v in var_set | ||
) | ||
if egb_available: | ||
util_block.variable_list = list( | ||
v | ||
for v in model.component_data_objects( | ||
ctype=Var, descend_into=(Block, egb.ExternalGreyBoxBlock) | ||
) | ||
if v in var_set | ||
) | ||
else: | ||
util_block.variable_list = list( | ||
v | ||
for v in model.component_data_objects(ctype=Var, descend_into=(Block)) | ||
if v in var_set | ||
) | ||
util_block.discrete_variable_list = list( | ||
v for v in util_block.variable_list if v in var_set and v.is_integer() | ||
) | ||
|
@@ -802,18 +824,21 @@ def init_rNLP(self, add_oa_cuts=True): | |
MindtPy unable to handle the termination condition of the relaxed NLP. | ||
""" | ||
config = self.config | ||
m = self.working_model.clone() | ||
self.rnlp = self.working_model.clone() | ||
config.logger.debug('Relaxed NLP: Solve relaxed integrality') | ||
MindtPy = m.MindtPy_utils | ||
TransformationFactory('core.relax_integer_vars').apply_to(m) | ||
MindtPy = self.rnlp.MindtPy_utils | ||
TransformationFactory('core.relax_integer_vars').apply_to(self.rnlp) | ||
nlp_args = dict(config.nlp_solver_args) | ||
update_solver_timelimit(self.nlp_opt, config.nlp_solver, self.timing, config) | ||
with SuppressInfeasibleWarning(): | ||
results = self.nlp_opt.solve( | ||
m, tee=config.nlp_solver_tee, load_solutions=False, **nlp_args | ||
self.rnlp, | ||
tee=config.nlp_solver_tee, | ||
load_solutions=self.load_solutions, | ||
**nlp_args, | ||
) | ||
if len(results.solution) > 0: | ||
m.solutions.load_from(results) | ||
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] | ||
|
@@ -841,24 +866,24 @@ def init_rNLP(self, add_oa_cuts=True): | |
): | ||
# TODO: recover the opposite dual when cyipopt issue #2831 is solved. | ||
dual_values = ( | ||
list(-1 * m.dual[c] for c in MindtPy.constraint_list) | ||
list(-1 * self.rnlp.dual[c] for c in MindtPy.constraint_list) | ||
if config.calculate_dual_at_solution | ||
else None | ||
) | ||
else: | ||
dual_values = ( | ||
list(m.dual[c] for c in MindtPy.constraint_list) | ||
list(self.rnlp.dual[c] for c in MindtPy.constraint_list) | ||
if config.calculate_dual_at_solution | ||
else None | ||
) | ||
copy_var_list_values( | ||
m.MindtPy_utils.variable_list, | ||
self.rnlp.MindtPy_utils.variable_list, | ||
self.mip.MindtPy_utils.variable_list, | ||
config, | ||
) | ||
if config.init_strategy == 'FP': | ||
copy_var_list_values( | ||
m.MindtPy_utils.variable_list, | ||
self.rnlp.MindtPy_utils.variable_list, | ||
self.working_model.MindtPy_utils.variable_list, | ||
config, | ||
) | ||
|
@@ -867,6 +892,7 @@ def init_rNLP(self, add_oa_cuts=True): | |
linearize_active=True, | ||
linearize_violated=True, | ||
cb_opt=None, | ||
nlp=self.rnlp, | ||
) | ||
for var in self.mip.MindtPy_utils.discrete_variable_list: | ||
# We don't want to trigger the reset of the global stale | ||
|
@@ -936,7 +962,7 @@ def init_max_binaries(self): | |
mip_args = dict(config.mip_solver_args) | ||
update_solver_timelimit(self.mip_opt, config.mip_solver, self.timing, config) | ||
results = self.mip_opt.solve( | ||
m, tee=config.mip_solver_tee, load_solutions=False, **mip_args | ||
m, tee=config.mip_solver_tee, load_solutions=self.load_solutions, **mip_args | ||
) | ||
if len(results.solution) > 0: | ||
m.solutions.load_from(results) | ||
|
@@ -1055,7 +1081,7 @@ def solve_subproblem(self): | |
results = self.nlp_opt.solve( | ||
self.fixed_nlp, | ||
tee=config.nlp_solver_tee, | ||
load_solutions=False, | ||
load_solutions=self.load_solutions, | ||
**nlp_args, | ||
) | ||
if len(results.solution) > 0: | ||
|
@@ -1153,6 +1179,7 @@ def handle_subproblem_optimal(self, fixed_nlp, cb_opt=None, fp=False): | |
linearize_active=True, | ||
linearize_violated=True, | ||
cb_opt=cb_opt, | ||
nlp=self.fixed_nlp, | ||
) | ||
|
||
var_values = list(v.value for v in fixed_nlp.MindtPy_utils.variable_list) | ||
|
@@ -1229,6 +1256,7 @@ def handle_subproblem_infeasible(self, fixed_nlp, cb_opt=None): | |
linearize_active=True, | ||
linearize_violated=True, | ||
cb_opt=cb_opt, | ||
nlp=feas_subproblem, | ||
) | ||
# Add a no-good cut to exclude this discrete option | ||
var_values = list(v.value for v in fixed_nlp.MindtPy_utils.variable_list) | ||
|
@@ -1311,6 +1339,12 @@ def solve_feasibility_subproblem(self): | |
update_solver_timelimit( | ||
self.feasibility_nlp_opt, config.nlp_solver, self.timing, config | ||
) | ||
TransformationFactory('contrib.deactivate_trivial_constraints').apply_to( | ||
feas_subproblem, | ||
tmp=True, | ||
ignore_infeasible=False, | ||
tolerance=config.constraint_tolerance, | ||
) | ||
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. The use of this transformation is new. Is it motivated by our new tests? Is the transformation well-supported? @emma58 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. After MindtPy rewrite, we repeatedly use the fixed-nlp subproblem. Therefore, we will call |
||
with SuppressInfeasibleWarning(): | ||
try: | ||
with time_code(self.timing, 'feasibility subproblem'): | ||
|
@@ -1346,6 +1380,9 @@ def solve_feasibility_subproblem(self): | |
constr.activate() | ||
active_obj.activate() | ||
MindtPy.feas_obj.deactivate() | ||
TransformationFactory('contrib.deactivate_trivial_constraints').revert( | ||
feas_subproblem | ||
) | ||
return feas_subproblem, feas_soln | ||
|
||
def handle_feasibility_subproblem_tc(self, subprob_terminate_cond, MindtPy): | ||
|
@@ -1480,7 +1517,10 @@ def fix_dual_bound(self, last_iter_cuts): | |
self.mip_opt, config.mip_solver, self.timing, config | ||
) | ||
main_mip_results = self.mip_opt.solve( | ||
self.mip, tee=config.mip_solver_tee, load_solutions=False, **mip_args | ||
self.mip, | ||
tee=config.mip_solver_tee, | ||
load_solutions=self.load_solutions, | ||
**mip_args, | ||
) | ||
if len(main_mip_results.solution) > 0: | ||
self.mip.solutions.load_from(main_mip_results) | ||
|
@@ -1564,7 +1604,10 @@ def solve_main(self): | |
|
||
try: | ||
main_mip_results = self.mip_opt.solve( | ||
self.mip, tee=config.mip_solver_tee, load_solutions=False, **mip_args | ||
self.mip, | ||
tee=config.mip_solver_tee, | ||
load_solutions=self.load_solutions, | ||
**mip_args, | ||
) | ||
# update_attributes should be before load_from(main_mip_results), since load_from(main_mip_results) may fail. | ||
if len(main_mip_results.solution) > 0: | ||
|
@@ -1617,7 +1660,10 @@ def solve_fp_main(self): | |
mip_args = self.set_up_mip_solver() | ||
|
||
main_mip_results = self.mip_opt.solve( | ||
self.mip, tee=config.mip_solver_tee, load_solutions=False, **mip_args | ||
self.mip, | ||
tee=config.mip_solver_tee, | ||
load_solutions=self.load_solutions, | ||
**mip_args, | ||
) | ||
# update_attributes should be before load_from(main_mip_results), since load_from(main_mip_results) may fail. | ||
# if config.single_tree or config.use_tabu_list: | ||
|
@@ -1659,7 +1705,7 @@ def solve_regularization_main(self): | |
main_mip_results = self.regularization_mip_opt.solve( | ||
self.mip, | ||
tee=config.mip_solver_tee, | ||
load_solutions=False, | ||
load_solutions=self.load_solutions, | ||
**dict(config.mip_solver_args), | ||
) | ||
if len(main_mip_results.solution) > 0: | ||
|
@@ -1871,7 +1917,7 @@ def handle_main_unbounded(self, main_mip): | |
main_mip_results = self.mip_opt.solve( | ||
main_mip, | ||
tee=config.mip_solver_tee, | ||
load_solutions=False, | ||
load_solutions=self.load_solutions, | ||
**config.mip_solver_args, | ||
) | ||
if len(main_mip_results.solution) > 0: | ||
|
@@ -2200,6 +2246,17 @@ def check_config(self): | |
config.logger.info("Solution pool does not support APPSI solver.") | ||
config.mip_solver = 'cplex_persistent' | ||
|
||
# related to https://github.com/Pyomo/pyomo/issues/2363 | ||
if ( | ||
'appsi' in config.mip_solver | ||
or 'appsi' in config.nlp_solver | ||
or ( | ||
config.mip_regularization_solver is not None | ||
and 'appsi' in config.mip_regularization_solver | ||
) | ||
): | ||
self.load_solutions = False | ||
|
||
################################################################################################################################ | ||
# Feasibility Pump | ||
|
||
|
@@ -2263,7 +2320,10 @@ def solve_fp_subproblem(self): | |
with SuppressInfeasibleWarning(): | ||
with time_code(self.timing, 'fp subproblem'): | ||
results = self.nlp_opt.solve( | ||
fp_nlp, tee=config.nlp_solver_tee, load_solutions=False, **nlp_args | ||
fp_nlp, | ||
tee=config.nlp_solver_tee, | ||
load_solutions=self.load_solutions, | ||
**nlp_args, | ||
) | ||
if len(results.solution) > 0: | ||
fp_nlp.solutions.load_from(results) | ||
|
@@ -2482,6 +2542,9 @@ def initialize_mip_problem(self): | |
getattr(self.mip, 'ipopt_zU_out', _DoNothing()).deactivate() | ||
|
||
MindtPy = self.mip.MindtPy_utils | ||
if len(MindtPy.grey_box_list) > 0: | ||
for grey_box in MindtPy.grey_box_list: | ||
grey_box.deactivate() | ||
|
||
if config.init_strategy == 'FP': | ||
MindtPy.cuts.fp_orthogonality_cuts = ConstraintList( | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -181,6 +181,55 @@ def add_oa_cuts( | |
) | ||
|
||
|
||
def add_oa_cuts_for_grey_box( | ||
target_model, jacobians_model, config, objective_sense, mip_iter, cb_opt=None | ||
): | ||
sign_adjust = -1 if objective_sense == minimize else 1 | ||
if config.add_slack: | ||
slack_var = target_model.MindtPy_utils.cuts.slack_vars.add() | ||
for target_model_grey_box, jacobian_model_grey_box in zip( | ||
target_model.MindtPy_utils.grey_box_list, | ||
jacobians_model.MindtPy_utils.grey_box_list, | ||
): | ||
jacobian_matrix = ( | ||
jacobian_model_grey_box.get_external_model() | ||
.evaluate_jacobian_outputs() | ||
.toarray() | ||
) | ||
# Enumerate over values works well now. However, it might be stable if the values() method changes. | ||
for index, output in enumerate(target_model_grey_box.outputs.values()): | ||
dual_value = jacobians_model.dual[jacobian_model_grey_box][ | ||
output.name.replace("outputs", "output_constraints") | ||
] | ||
target_model.MindtPy_utils.cuts.oa_cuts.add( | ||
expr=copysign(1, sign_adjust * dual_value) | ||
* ( | ||
sum( | ||
jacobian_matrix[index][var_index] * (var - value(var)) | ||
for var_index, var in enumerate( | ||
target_model_grey_box.inputs.values() | ||
) | ||
) | ||
) | ||
- (output - value(output)) | ||
- (slack_var if config.add_slack else 0) | ||
<= 0 | ||
) | ||
Comment on lines
+200
to
+217
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. Are you sure that this is always going to create a correct cut, even if greybox changes its behavior? The part that looks potentially scary to me is calling 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. If we don't call 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. I don't really know--you would know better than me. It's completely possible that this is fine. My concern is that you will end up with the wrong coefficient ( 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. Currently, I cannot come up with a good way to improve this since the 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. Maybe just put in a comment that you are relying on the order in which items are added to the |
||
# TODO: gurobi_persistent currently does not support greybox model. | ||
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. Should this become an issue? 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.
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. Is this a bug in 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. I will submit an issue related to this. 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. @emma58 Could you provide me a 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 is the main idea. You'll want to figure out how to do your own accounting in terms of finding the Vars you need to reference and naming the references, etc. 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. #3000 Issue opened 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. @emma58 . I don't think 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. appsi_gurobi does support callbacks. appsi_cplex will, but does not currently. |
||
# https://github.com/Pyomo/pyomo/issues/3000 | ||
# if ( | ||
# config.single_tree | ||
# and config.mip_solver == 'gurobi_persistent' | ||
# and mip_iter > 0 | ||
# and cb_opt is not None | ||
# ): | ||
# cb_opt.cbLazy( | ||
# target_model.MindtPy_utils.cuts.oa_cuts[ | ||
# len(target_model.MindtPy_utils.cuts.oa_cuts) | ||
# ] | ||
# ) | ||
|
||
|
||
ZedongPeng marked this conversation as resolved.
Show resolved
Hide resolved
|
||
def add_ecp_cuts( | ||
target_model, | ||
jacobians, | ||
|
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.
Do you really want to store this publicly on the solver object? If your users can find it and it doesn't start with underscore, they might start to rely on it. That's not necessarily bad--just something to think through.