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

Add the support of GreyBox in MindtPy #2988

Merged
merged 38 commits into from
Nov 27, 2023
Merged

Conversation

ZedongPeng
Copy link
Contributor

Fixes #2767

Summary/Motivation:

This Pull Request adds the support of the Greybox in MindtPy.

Changes proposed in this PR:

Legal Acknowledgement

By contributing to this software project, I have read the contribution guide and agree to the following terms and conditions for my contribution:

  1. I agree my contributions are submitted under the BSD license.
  2. I represent I am authorized to make the contributions and grant the license. If my employer has rights to intellectual property that includes these contributions, I represent that I have received permission to make contributions and grant the required license on behalf of that employer.

Copy link
Contributor

@bernalde bernalde left a comment

Choose a reason for hiding this comment

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

Good work, Zedong! I request some changes and also some revisions from our friends that motivated this development! @adowling2 !

tmp=True,
ignore_infeasible=False,
tolerance=config.constraint_tolerance,
)
Copy link
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 'contrib.deactivate_trivial_constraints' transformation and revert it after solving. I missed the feasibility subproblem and this might be a bug.

- (slack_var if config.add_slack else 0)
<= 0
)
# TODO: gurobi_persistent currently does not support greybox model.
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this become an issue?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Gurobi will fail even when we deactivate the greybox block. Therefore, we cannot use Gurobi as the mip solver.

Copy link
Contributor

Choose a reason for hiding this comment

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

Is this a bug in gurobi_persistent? Why does it behave differently?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I will submit an issue related to this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@emma58 Could you provide me a reference example?

Copy link
Contributor

Choose a reason for hiding this comment

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

from pyomo.environ import *

m = ConcreteModel()
m.b = Block()
m.b.x = Var(bounds=(2, 5))
m.y = Var(domain=NonNegativeReals)
m.c = Constraint(expr=m.b.x - m.y <= 4)
m.obj = Objective(expr=m.b.x + m.y)

m.b.deactivate()

m.somewhere_safe_to_keep_things = Block()
m.somewhere_safe_to_keep_things.x = Reference(m.b.x)

opt = SolverFactory('gurobi_persistent')
opt.set_instance(m)

opt.solve(m, tee=True)

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

#3000 Issue opened

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@emma58 . I don't think Reference is an elegant way to resolve this issue. I will comment the code of adding Gurobi lazy constraints and only support greybox in multi-tree implementation this time.
Btw, since persistent solver will be totally replaced by appsi_solver, does appsi_solver support both CPLEX and Gurobi callbacks as persistent solver now? @michaelbynum

Copy link
Contributor

Choose a reason for hiding this comment

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

appsi_gurobi does support callbacks. appsi_cplex will, but does not currently.

pyomo/contrib/mindtpy/cut_generation.py Show resolved Hide resolved
@bernalde
Copy link
Contributor

bernalde commented Sep 9, 2023

The tests are failing because:
FAILED pyomo/contrib/mindtpy/tests/test_mindtpy.py::TestMindtPy::test_OA_APPSI_ipopt - RuntimeError: A feasible solution was not found, so no solution can be loaded.Please set opt.config.load_solution=False and check results.termination_condition and results.best_feasible_objective before loading a solution.
This has to do with the new option passed. Let's solve this first

@ZedongPeng
Copy link
Contributor Author

This PR is ready for review. @emma58

@bernalde
Copy link
Contributor

I don't think we're ready yet, Zedong.
Some errors from the tests:

=========================== short test summary info ============================
FAILED pyomo/contrib/mindtpy/tests/test_mindtpy.py::TestMindtPy::test_OA_APPSI_ipopt - RuntimeError: A feasible solution was not found, so no solution can be loaded.Please set opt.config.load_solution=False and check results.termination_condition and results.best_feasible_objective before loading a solution.
FAILED pyomo/environ/tests/test_environ.py::TestPyomoEnviron::test_tpl_import_time - AssertionError: Items in the first set but not the second:
'scipy' : Unexpected module found in 5 slowest-loading TPL modules
==== 2 failed, 12120 passed, 5009 skipped, 29 xfailed in 954.63s (0:15:54) =====

We are responsible for the first, and scipy taking so much longer seems to be a new thing. Let's aim to fix the first error and then we can keep trying.

@jsiirola
Copy link
Member

The scipy error needs to be resolved, too: it indicates that something is causing scipy to be imported when we import pyomo.environ. This is likely because something in mindpy is doing import scipy instead of from pyomo.common.dependencies import scipy.

@ZedongPeng
Copy link
Contributor Author

Hi @jsiirola . scipy cannot be imported from Pyomo dependency. How can I fix this?

Import code
from pyomo.common.dependencies.scipy.sparse import coo_matrix

Output

  File "/home/runner/work/pyomo/pyomo/pyomo/contrib/pynumero/interfaces/external_grey_box.py", line 15, in <module>
    from pyomo.common.dependencies.scipy.sparse import coo_matrix
  File "/home/runner/work/pyomo/pyomo/pyomo/common/dependencies.py", line 168, in __getattr__
    return getattr(_mod, attr)
  File "/home/runner/work/pyomo/pyomo/pyomo/common/dependencies.py", line 75, in __getattr__
    raise DeferredImportError(self._moduleunavailable_message())
pyomo.common.errors.DeferredImportError: The scipy.sparse module (an optional Pyomo dependency) failed to import: ModuleNotFoundError: No module named 'scipy'

@jsiirola
Copy link
Member

@ZedongPeng, you want to do something like:

import pyomo.common.dependencies.scipy.sparse as scipy_sparse

# then later...
return scipy_sparse.coo_matrix((data, (row, col)), shape=(1, 5))

@ZedongPeng
Copy link
Contributor Author

The error is not inside MindtPy now. How can we deal with it? @jsiirola

=========================== short test summary info ============================
FAILED pyomo/environ/tests/test_environ.py::TestPyomoEnviron::test_tpl_import_time - AssertionError: Items in the first set but not the second:
'scipy' : Unexpected module found in 5 slowest-loading TPL modules
==== 1 failed, 12121 passed, 5009 skipped, 29 xfailed in 1001.61s (0:16:41) ====

@jsiirola
Copy link
Member

Ahh - the issue is that mindtpy is importing PyNumero (in pyomo/contrib/mindtpy/algorithm_base_class.py), which is importing scipy.

You can see this by running python pyomo/environ/tests/test_environ.py. You will see (in a lot of other output):

TPLS:
                            __future__:    179  (from pyomo.core.expr.visitor)
[....]
                          scipy.sparse:  12384  (from pyomo.contrib.pynumero.interfaces.external_grey_box)
                           scipy.stats: 134511  (from pyomo.contrib.pynumero.dependencies)

You can resolve things by deferring the import of pynumero using the attempt_import mechanism. Replace the pynumero import with:

from pyomo.common.dependencies import attempt_import

egb = attempt_import('pyomo.contrib.pynumero.interfaces.external_grey_box')[0]

and then replace instances of ExternalGreyBoxBlock with egb.ExternalGreyBoxBlock.

Copy link
Contributor

@bernalde bernalde left a comment

Choose a reason for hiding this comment

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

There seem to be some test failing still.
E pyomo.common.errors.DeferredImportError: The pyomo.contrib.pynumero.interfaces.external_grey_box module (an optional Pyomo dependency) failed to import: ModuleNotFoundError: No module named 'scipy' ------- generated xml file: /home/runner/work/pyomo/pyomo/TEST-pyomo.xml ------- =========================== short test summary info ============================ ERROR pyomo/contrib/mindtpy/tests/test_mindtpy.py ERROR pyomo/contrib/mindtpy/tests/test_mindtpy_ECP.py ERROR pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py ERROR pyomo/contrib/mindtpy/tests/test_mindtpy_grey_box.py ERROR pyomo/contrib/mindtpy/tests/test_mindtpy_lp_nlp.py !!!!!!!!!!!!!!!!!!! Interrupted: 5 errors during collection !!!!!!!!!!!!!!!!!!!! ================== 30 skipped, 5 errors in 100.53s (0:01:40) ===================
These tests fail on the slim verrsion and pypy versions (those without numpy). We are importing numpy instead of attempting that import

Copy link
Contributor

@emma58 emma58 left a comment

Choose a reason for hiding this comment

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

This looks pretty good. A few questions/comments, and you still need to get the tests passing, which will just involve making sure you're never accidentally importing numpy or scipy via pynumero when they're not actually available.

@@ -802,18 +810,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()
Copy link
Contributor

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.

Comment on lines 2238 to 2247
# 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
)
):
config.load_solutions = False
Copy link
Contributor

Choose a reason for hiding this comment

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

This makes me think that load_solutions shouldn't be up to the user then, since there are cases where you need to force it to be False.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. This option should not be up to the users. Is there a way to define an option (not open to users) instead of config (open to users) in Pyomo?

Copy link
Contributor

Choose a reason for hiding this comment

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

It's not really an option at all, just something you calculate based on some of the config options, right? You could probably store it privately on the algorithm class. Just make sure to restore state afterwards if you do keep it on the class.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is it a good choice to change load_solutions to the attribute of the Solver Object?

Copy link
Contributor

Choose a reason for hiding this comment

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

That sounds fine to me. You should probably make it private as well. The only risk of doing this is that you should be careful to restore the state after the solve.

Comment on lines 497 to 504
CONFIG.declare(
'load_solutions',
ConfigValue(
default=True,
description='Whether to load solutions in solve() function',
domain=bool,
),
)
Copy link
Contributor

Choose a reason for hiding this comment

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

See my comment above, but I'm not sure of why this should be up to the user. I think it would be fine (and perhaps easier to support in the future) to keep this detail locally, or maybe privately on the solver object, and manage it yourself. Especially since it looks users don't actually have a choice if they want to use appsi solvers.

pyomo/contrib/mindtpy/algorithm_base_class.py Outdated Show resolved Hide resolved
pyomo/contrib/mindtpy/algorithm_base_class.py Outdated Show resolved Hide resolved
Comment on lines +199 to +216
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
)
Copy link
Contributor

Choose a reason for hiding this comment

The 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 enumerate over getting the values() from dictionaries coming from greybox. You will get deterministic ordering from values() as long as the dictionary is constructed in the same order every time, but if that changes, is this still right? Or does greybox make a promise about consistency with that order and the jacobian? (This is me wondering out of ignorance, but it may be worth adding a comment even if it is fine.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we don't call enumerate over values, any suggestions about how to implement it? Directly enumerate over variable?

Copy link
Contributor

Choose a reason for hiding this comment

The 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 (jacobian_matrix[index][var_index]) paired with the wrong var - value(var) term if the target_model_greyboox.inputs dictionary is every constructed in a different order that doesn't correspond to the indices in jacobian_matrix. I don't know greybox well at all, so maybe it makes a promise that what I just said will never happen, in which case your code is totally fine. I'm just asking if you know if that's the case or not. It's always worth thinking carefully when you are relying on the order things come up when iterating over a dictionary...

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 evaluate_jacobian_outputs returns a scipy.sparse._coo.coo_matrix.

Copy link
Contributor

Choose a reason for hiding this comment

The 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 target_model_greybox.inputs dictionary, so that future you (or someone else) will have a hint if this ever doesn't work.

pyomo/contrib/mindtpy/tests/MINLP_simple_grey_box.py Outdated Show resolved Hide resolved
pyomo/contrib/mindtpy/tests/MINLP_simple_grey_box.py Outdated Show resolved Hide resolved
pyomo/contrib/mindtpy/tests/MINLP_simple_grey_box.py Outdated Show resolved Hide resolved
pyomo/contrib/mindtpy/tests/test_mindtpy_grey_box.py Outdated Show resolved Hide resolved
@bernalde
Copy link
Contributor

Test failures point to import of scipy when it is not available. A conditional attempt import should resolve it.

@ZedongPeng
Copy link
Contributor Author

Now all the tests passed except the Jenkins one. Could you show me the output of Jenkins? @emma58 Thanks.

@emma58
Copy link
Contributor

emma58 commented Nov 14, 2023

@ZedongPeng, I'm not convinced those failures are actually your fault... I just merged main, and we'll see what happens. It looks like you still have a few comments above to address though?

Copy link

codecov bot commented Nov 14, 2023

Codecov Report

Attention: 1 lines in your changes are missing coverage. Please review.

Comparison is base (15a7bff) 87.96% compared to head (3cf0fc2) 87.96%.
Report is 11 commits behind head on main.

Files Patch % Lines
pyomo/contrib/mindtpy/cut_generation.py 88.88% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #2988   +/-   ##
=======================================
  Coverage   87.96%   87.96%           
=======================================
  Files         770      770           
  Lines       90162    90187   +25     
=======================================
+ Hits        79310    79332   +22     
- Misses      10852    10855    +3     
Flag Coverage Δ
linux 85.29% <96.96%> (+<0.01%) ⬆️
osx 75.09% <15.15%> (-0.02%) ⬇️
other 85.47% <90.90%> (+<0.01%) ⬆️
win 82.54% <90.90%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@ZedongPeng
Copy link
Contributor Author

I think this PR is ready to merge if all the tests pass.

@emma58
Copy link
Contributor

emma58 commented Nov 15, 2023

This is ready for a second review. @jsiirola, @mrmundt, @blnicho, @michaelbynum, any volunteers?

@emma58 emma58 merged commit 90291b0 into Pyomo:main Nov 27, 2023
27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Use cyipopt with Mindtpy
7 participants