-
Notifications
You must be signed in to change notification settings - Fork 518
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
Refactor CyIpopt interface to subclass cyipopt.Problem
#2760
Conversation
@@ -148,7 +148,7 @@ def _cyipopt_importer(): | |||
'Internal_Error': TerminationCondition.internalSolverError, | |||
} | |||
|
|||
class CyIpoptProblemInterface(object, metaclass=abc.ABCMeta): | |||
class CyIpoptProblemInterface(cyipopt.Problem, metaclass=abc.ABCMeta): |
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.
This fails if cyipopt is not available. We could get around this with
cyipopt_Problem = cyipopt.Problem if cyipopt_available else object
at the top of the file, but then we get a confusing error message when we try to call CyIpoptProblemInterface.solve
. Maybe do this but raise an error in __init_subclass__
if we're not an instance of cyipopt.Problem
?
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.
What about using cyipopt_Problem
as you suggest, then raising a sensible exception in __init__
if not cyipopt_available
?
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.
Not bad, we just have to remember to call the right combination of super().__init__
s in subclasses. But I could move the cyipopt.Problem.__init__
call into CyIpoptProblemInterface.__init__
, so subclass developers only have to remember to call one super().__init__
.
…nto cyipopt-subclass
Test failures (on windows and linux/conda) are
I'm a little hesitant to adjust this hand-coded tolerance when I don't know why import time has suddenly increased. Maybe cyipopt_Problem = cyipopt.Problem if cyipopt_available else object is taking a ton of time during import? |
pyomo/environ/tests/test_environ.py
Outdated
if cyipopt_available: | ||
ref.add('cyipopt') |
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.
We don't want to add cyipopt to this list: this is indicating that cyipopt is being unconditionally imported by pyomo.environ
. We should re-think the imports so that we don't have to update this list.
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.
Is this because PyomoCyIpoptSolver
is registered as a plugin so it can be accessed via pyo.SolverFactory("cyipopt")
?
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.
Yes: pyomo.environ
imports pyomo.contrib.pynumero.plugins
which imports pyomo.contrib.pynumero.algorithms.solers.cyipopt_solver
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.
I tried using a deferred import (via attempt_import
) for pyomo.contrib.pynumero.algorithms.solvers.cyipopt_solver
in pynumero.plugins
, but that didn't change anything, nor did moving from .algorithms.solvers.cyipopt_solver import PyomoCyIpoptSolver
to within the load
function of pynumero.plugins
.
Do you have any ideas?
Yes - I think the thing that changed is that we now cast |
OK - I think I know how to solve the chicken & egg issue here:
That should prevent importing cyipopt until the solver interface is actually instantiated (actually, it would be deferred all the way until the first |
…roduce a deferred import back into cyipopt_solver
Thanks for the suggestion, this works for me locally. |
# These attributes should no longer be imported from this module. These | ||
# deprecation paths provide a deferred import to these attributes so (a) they | ||
# can still be used until these paths are removed, and (b) the imports are not | ||
# triggered when this module is imported. | ||
relocated_module_attribute( | ||
"cyipopt_available", | ||
"pyomo.contrib.pynumero.interfaces.cyipopt_interface.cyipopt_available", | ||
"6.5.1.dev0", | ||
) | ||
relocated_module_attribute( | ||
"CyIpoptProblemInterface", | ||
"pyomo.contrib.pynumero.interfaces.cyipopt_interface.CyIpoptProblemInterface", | ||
"6.5.1.dev0", | ||
) | ||
relocated_module_attribute( | ||
"CyIpoptNLP", | ||
"pyomo.contrib.pynumero.interfaces.cyipopt_interface.CyIpoptNLP", | ||
"6.5.1.dev0", | ||
) |
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.
I commonly import cyipopt_available
from this module, so I added these relocation warnings so these imports will still be available. This seems not to increase import time, judging by test_environ.py still passing locally.
@jsiirola Actually, these are the options sent to Ipopt, so this file is created by Ipopt and probably not closed until the I just pushed a commit with |
@Robbybp, I think we can resolve the Windows test failures by calling Thoughts? |
@jsiirola Just pushed a commit that calls |
Codecov ReportPatch coverage:
Additional details and impacted files@@ Coverage Diff @@
## main #2760 +/- ##
==========================================
+ Coverage 87.02% 87.03% +0.01%
==========================================
Files 763 764 +1
Lines 87246 87268 +22
==========================================
+ Hits 75926 75956 +30
+ Misses 11320 11312 -8
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 2 files with indirect coverage changes Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here. ☔ View full report in Codecov by Sentry. |
Highlights:
@carldlaird can you take a look at this? |
…bclass conflicts were resolved by taking the cyipopt-subclass files and running black on them.
# Call CyIpoptProblemInterface.__init__, which calls | ||
# cyipopt.Problem.__init__ | ||
super(PyomoExternalCyIpoptProblem, self).__init__() | ||
|
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.
Note that this __init__
call, which wasn't previously necessary, is now necessary in subclasses of CyIpoptProblemInterface
to make sure that the cyipopt.Problem
get initialized so we don't segfault when we try to solve it later.
In this way, this PR could break some user code if they are subclassing CyIpoptProblemInterface
.
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.
I am thinking of adding an __initialized
flag to CyIpoptProblemInterface.__init__
so we can raise a warning/error if solve
is called when cyipopt.Problem
hasn't been initialized.
with open('_cyipopt-pyomo-ext-scaling.log', 'r') as fd: | ||
solver_trace = fd.read() | ||
cyipopt_problem.close() | ||
os.remove('_cyipopt-pyomo-ext-scaling.log') |
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.
This call to cyipopt.Problem.close
is now necessary on windows if we try to remove a file too early (I think before the garbage collector has deallocated the cyipopt.Problem
, but my testing was inconclusive).
This is another way this PR could break a user's code.
I've added a |
Summary/Motivation:
In mechmotum/cyipopt#182, I am working on an interface to let us call
GetIpoptCurrentIterate
andGetIpoptCurrentViolations
during an intermediate callback. The non-hack way to do this without augmenting CyIpopt's callback signature is to add methods oncyipopt.Problem
. This means that our callback in Pyomo will need to know about thecyipopt.Problem
object, which can be achieved by having our "interface object" subclasscyipopt.Problem
rather than passing our interface object tocyipopt.Problem
when it is constructed.This is a very small change in terms of lines of code, as
cyipopt.Problem
will automatically look for methods onself
, so we don't need a new wrapper class, if it is not given aproblem_obj
(our CyIpoptProblemInterface) during construction.Accidentally built this on top of #2759...
Changes
cyipopt.Problem
super().__init__
to initialize thecyipopt.Problem
(if we forget this on any derived class, we segfault!)cyipopt.Problem
during solve methodsLegal Acknowledgement
By contributing to this software project, I have read the contribution guide and agree to the following terms and conditions for my contribution: