1#  ___________________________________________________________________________
2#
3#  Pyomo: Python Optimization Modeling Objects
4#  Copyright 2017 National Technology and Engineering Solutions of Sandia, LLC
5#  Under the terms of Contract DE-NA0003525 with National Technology and
6#  Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
7#  rights in this software.
8#  This software is distributed under the 3-clause BSD License.
9#  ___________________________________________________________________________
10"""
11The cyipopt_solver module includes the python interface to the
12Cythonized ipopt solver cyipopt (see more:
13https://github.com/mechmotum/cyipopt.git). To use the solver,
14you can create a derived implementation from the abstract base class
15CyIpoptProblemInterface that provides the necessary methods.
16
17Note: This module also includes a default implementation CyIpopt
18that works with problems derived from AslNLP as long as those
19classes return numpy ndarray objects for the vectors and coo_matrix
20objects for the matrices (e.g., AmplNLP and PyomoNLP)
21"""
22import io
23import sys
24import logging
25import os
26import abc
27
28from pyomo.common.dependencies import (
29    attempt_import,
30    numpy as np, numpy_available,
31)
32from pyomo.common.tee import redirect_fd, TeeStream
33
34def _cyipopt_importer():
35    import cyipopt
36    # cyipopt before version 1.0.3 called the problem class "Problem"
37    if not hasattr(cyipopt, 'Problem'):
38        cyipopt.Problem = cyipopt.problem
39    # cyipopt before version 1.0.3 put the __version__ flag in the ipopt
40    # module (which was deprecated starting in 1.0.3)
41    if not hasattr(cyipopt, '__version__'):
42        import ipopt
43        cyipopt.__version__ = ipopt.__version__
44    # Beginning in 1.0.3, STATUS_MESSAGES is in a separate
45    # ipopt_wrapper module
46    if not hasattr(cyipopt, 'STATUS_MESSAGES'):
47        import ipopt_wrapper
48        cyipopt.STATUS_MESSAGES = ipopt_wrapper.STATUS_MESSAGES
49    return cyipopt
50
51cyipopt, cyipopt_available = attempt_import(
52     'ipopt',
53     error_message='cyipopt solver relies on the ipopt module from cyipopt. '
54     'See https://github.com/mechmotum/cyipopt.git for cyipopt '
55     'installation instructions.',
56     importer=_cyipopt_importer,
57)
58
59# Because pynumero.interfaces requires numpy, we will leverage deferred
60# imports here so that the solver can be registered even when numpy is
61# not available.
62pyomo_nlp = attempt_import('pyomo.contrib.pynumero.interfaces.pyomo_nlp')[0]
63pyomo_grey_box = attempt_import('pyomo.contrib.pynumero.interfaces.pyomo_grey_box_nlp')[0]
64egb = attempt_import('pyomo.contrib.pynumero.interfaces.external_grey_box')[0]
65
66from pyomo.common.config import ConfigBlock, ConfigValue
67from pyomo.common.timing import TicTocTimer
68from pyomo.core.base import Block, Objective, minimize
69from pyomo.opt import (
70    SolverStatus, SolverResults, TerminationCondition, ProblemSense
71)
72
73logger = logging.getLogger(__name__)
74
75# This maps the cyipopt STATUS_MESSAGES back to string representations
76# of the Ipopt ApplicationReturnStatus enum
77_cyipopt_status_enum = [
78    "Solve_Succeeded", (b"Algorithm terminated successfully at a locally "
79                        b"optimal point, satisfying the convergence tolerances "
80                        b"(can be specified by options)."),
81    "Solved_To_Acceptable_Level", (b"Algorithm stopped at a point that was "
82                                   b"converged, not to \"desired\" tolerances, "
83                                   b"but to \"acceptable\" tolerances (see the "
84                                   b"acceptable-... options)."),
85    "Infeasible_Problem_Detected", (b"Algorithm converged to a point of local "
86                                    b"infeasibility. Problem may be "
87                                    b"infeasible."),
88    "Search_Direction_Becomes_Too_Small", (b"Algorithm proceeds with very "
89                                           b"little progress."),
90    "Diverging_Iterates", b"It seems that the iterates diverge.",
91    "User_Requested_Stop", (b"The user call-back function intermediate_callback "
92                            b"(see Section 3.3.4 in the documentation) returned "
93                            b"false, i.e., the user code requested a premature "
94                            b"termination of the optimization."),
95    "Feasible_Point_Found", b"Feasible point for square problem found.",
96    "Maximum_Iterations_Exceeded", (b"Maximum number of iterations exceeded "
97                                    b"(can be specified by an option)."),
98    "Restoration_Failed", (b"Restoration phase failed, algorithm doesn\'t know "
99                           b"how to proceed."),
100    "Error_In_Step_Computation", (b"An unrecoverable error occurred while Ipopt "
101                                  b"tried to compute the search direction."),
102    "Maximum_CpuTime_Exceeded", b"Maximum CPU time exceeded.",
103    "Not_Enough_Degrees_Of_Freedom", b"Problem has too few degrees of freedom.",
104    "Invalid_Problem_Definition", b"Invalid problem definition.",
105    "Invalid_Option", b"Invalid option encountered.",
106    "Invalid_Number_Detected", (b"Algorithm received an invalid number (such as "
107                                b"NaN or Inf) from the NLP; see also option "
108                                b"check_derivatives_for_naninf."),
109    # Note that the concluding "." was missing before cyipopt 1.0.3
110    "Invalid_Number_Detected", (b"Algorithm received an invalid number (such as "
111                                b"NaN or Inf) from the NLP; see also option "
112                                b"check_derivatives_for_naninf"),
113    "Unrecoverable_Exception", b"Some uncaught Ipopt exception encountered.",
114    "NonIpopt_Exception_Thrown", b"Unknown Exception caught in Ipopt.",
115    # Note that the concluding "." was missing before cyipopt 1.0.3
116    "NonIpopt_Exception_Thrown", b"Unknown Exception caught in Ipopt",
117    "Insufficient_Memory", b"Not enough memory.",
118    "Internal_Error", (b"An unknown internal error occurred. Please contact "
119                       b"the Ipopt authors through the mailing list."),
120]
121_cyipopt_status_enum = {
122    _cyipopt_status_enum[i+1]: _cyipopt_status_enum[i]
123    for i in range(0, len(_cyipopt_status_enum), 2)
124}
125
126# This maps Ipopt ApplicationReturnStatus enum strings to an appropriate
127# Pyomo TerminationCondition
128_ipopt_term_cond = {
129    'Solve_Succeeded': TerminationCondition.optimal,
130    'Solved_To_Acceptable_Level': TerminationCondition.feasible,
131    'Infeasible_Problem_Detected': TerminationCondition.infeasible,
132    'Search_Direction_Becomes_Too_Small': TerminationCondition.minStepLength,
133    'Diverging_Iterates': TerminationCondition.unbounded,
134    'User_Requested_Stop': TerminationCondition.userInterrupt,
135    'Feasible_Point_Found': TerminationCondition.feasible,
136    'Maximum_Iterations_Exceeded': TerminationCondition.maxIterations,
137    'Restoration_Failed': TerminationCondition.noSolution,
138    'Error_In_Step_Computation': TerminationCondition.solverFailure,
139    'Maximum_CpuTime_Exceeded': TerminationCondition.maxTimeLimit,
140    'Not_Enough_Degrees_Of_Freedom': TerminationCondition.invalidProblem,
141    'Invalid_Problem_Definition': TerminationCondition.invalidProblem,
142    'Invalid_Option': TerminationCondition.error,
143    'Invalid_Number_Detected': TerminationCondition.internalSolverError,
144    'Unrecoverable_Exception': TerminationCondition.internalSolverError,
145    'NonIpopt_Exception_Thrown': TerminationCondition.error,
146    'Insufficient_Memory': TerminationCondition.resourceInterrupt,
147    'Internal_Error': TerminationCondition.internalSolverError,
148}
149
150class CyIpoptProblemInterface(object, metaclass=abc.ABCMeta):
151    @abc.abstractmethod
152    def x_init(self):
153        """Return the initial values for x as a numpy ndarray
154        """
155        pass
156
157    @abc.abstractmethod
158    def x_lb(self):
159        """Return the lower bounds on x as a numpy ndarray
160        """
161        pass
162
163    @abc.abstractmethod
164    def x_ub(self):
165        """Return the upper bounds on x as a numpy ndarray
166        """
167        pass
168
169    @abc.abstractmethod
170    def g_lb(self):
171        """Return the lower bounds on the constraints as a numpy ndarray
172        """
173        pass
174
175    @abc.abstractmethod
176    def g_ub(self):
177        """Return the upper bounds on the constraints as a numpy ndarray
178        """
179        pass
180
181    @abc.abstractmethod
182    def scaling_factors(self):
183        """Return the values for scaling factors as a tuple
184        (objective_scaling, x_scaling, g_scaling). Return None
185        if the scaling factors are to be ignored
186        """
187        pass
188
189    @abc.abstractmethod
190    def objective(self, x):
191        """Return the value of the objective
192        function evaluated at x
193        """
194        pass
195
196    @abc.abstractmethod
197    def gradient(self, x):
198        """Return the gradient of the objective
199        function evaluated at x as a numpy ndarray
200        """
201        pass
202
203    @abc.abstractmethod
204    def constraints(self, x):
205        """Return the residuals of the constraints
206        evaluated at x as a numpy ndarray
207        """
208        pass
209
210    @abc.abstractmethod
211    def jacobianstructure(self):
212        """Return the structure of the jacobian
213        in coordinate format. That is, return (rows,cols)
214        where rows and cols are both numpy ndarray
215        objects that contain the row and column indices
216        for each of the nonzeros in the jacobian.
217        """
218        pass
219
220    @abc.abstractmethod
221    def jacobian(self, x):
222        """Return the values for the jacobian evaluated at x
223        as a numpy ndarray of nonzero values corresponding
224        to the rows and columns specified in the jacobianstructure
225        """
226        pass
227
228    @abc.abstractmethod
229    def hessianstructure(self):
230        """Return the structure of the hessian
231        in coordinate format. That is, return (rows,cols)
232        where rows and cols are both numpy ndarray
233        objects that contain the row and column indices
234        for each of the nonzeros in the hessian.
235        Note: return ONLY the lower diagonal of this symmetric matrix.
236        """
237        pass
238
239    @abc.abstractmethod
240    def hessian(self, x, y, obj_factor):
241        """Return the values for the hessian evaluated at x
242        as a numpy ndarray of nonzero values corresponding
243        to the rows and columns specified in the
244        hessianstructure method.
245        Note: return ONLY the lower diagonal of this symmetric matrix.
246        """
247        pass
248
249    def intermediate(self, alg_mod, iter_count, obj_value,
250            inf_pr, inf_du, mu, d_norm, regularization_size,
251            alpha_du, alpha_pr, ls_trials):
252        """Callback that can be used to examine or report intermediate
253        results. This method is called each iteration
254        """
255        # TODO: Document the arguments
256        pass
257
258
259class CyIpoptNLP(CyIpoptProblemInterface):
260    def __init__(self, nlp, intermediate_callback=None):
261        """This class provides a CyIpoptProblemInterface for use
262        with the CyIpoptSolver class that can take in an NLP
263        as long as it provides vectors as numpy ndarrays and
264        matrices as scipy.sparse.coo_matrix objects. This class
265        provides the interface between AmplNLP or PyomoNLP objects
266        and the CyIpoptSolver
267        """
268        self._nlp = nlp
269        self._intermediate_callback = intermediate_callback
270
271        x = nlp.init_primals()
272        y = nlp.init_duals()
273        if np.any(np.isnan(y)):
274            # did not get initial values for y, use this default
275            y.fill(1.0)
276
277        self._cached_x = x.copy()
278        self._cached_y = y.copy()
279        self._cached_obj_factor = 1.0
280
281        nlp.set_primals(self._cached_x)
282        nlp.set_duals(self._cached_y)
283
284        # get jacobian and hessian structures
285        self._jac_g = nlp.evaluate_jacobian()
286        try:
287            self._hess_lag = nlp.evaluate_hessian_lag()
288            self._hess_lower_mask = self._hess_lag.row >= self._hess_lag.col
289            self._hessian_available = True
290        except (AttributeError, NotImplementedError):
291            self._hessian_available = False
292            self._hess_lag = None
293            self._hess_lower_mask = None
294
295    def _set_primals_if_necessary(self, x):
296        if not np.array_equal(x, self._cached_x):
297            self._nlp.set_primals(x)
298            self._cached_x = x.copy()
299
300    def _set_duals_if_necessary(self, y):
301        if not np.array_equal(y, self._cached_y):
302            self._nlp.set_duals(y)
303            self._cached_y = y.copy()
304
305    def _set_obj_factor_if_necessary(self, obj_factor):
306        if obj_factor != self._cached_obj_factor:
307            self._nlp.set_obj_factor(obj_factor)
308            self._cached_obj_factor = obj_factor
309
310    def x_init(self):
311        return self._nlp.init_primals()
312
313    def x_lb(self):
314        return self._nlp.primals_lb()
315
316    def x_ub(self):
317        return self._nlp.primals_ub()
318
319    def g_lb(self):
320        return self._nlp.constraints_lb()
321
322    def g_ub(self):
323        return self._nlp.constraints_ub()
324
325    def scaling_factors(self):
326        obj_scaling = self._nlp.get_obj_scaling()
327        x_scaling = self._nlp.get_primals_scaling()
328        g_scaling = self._nlp.get_constraints_scaling()
329        return obj_scaling, x_scaling, g_scaling
330
331    def objective(self, x):
332        self._set_primals_if_necessary(x)
333        return self._nlp.evaluate_objective()
334
335    def gradient(self, x):
336        self._set_primals_if_necessary(x)
337        return self._nlp.evaluate_grad_objective()
338
339    def constraints(self, x):
340        self._set_primals_if_necessary(x)
341        return self._nlp.evaluate_constraints()
342
343    def jacobianstructure(self):
344        return self._jac_g.row, self._jac_g.col
345
346    def jacobian(self, x):
347        self._set_primals_if_necessary(x)
348        self._nlp.evaluate_jacobian(out=self._jac_g)
349        return self._jac_g.data
350
351    def hessianstructure(self):
352        if not self._hessian_available:
353            return np.zeros(0), np.zeros(0)
354
355        row = np.compress(self._hess_lower_mask, self._hess_lag.row)
356        col = np.compress(self._hess_lower_mask, self._hess_lag.col)
357        return row, col
358
359
360    def hessian(self, x, y, obj_factor):
361        if not self._hessian_available:
362            raise ValueError("Hessian requested, but not supported by the NLP")
363
364        self._set_primals_if_necessary(x)
365        self._set_duals_if_necessary(y)
366        self._set_obj_factor_if_necessary(obj_factor)
367        self._nlp.evaluate_hessian_lag(out=self._hess_lag)
368        data = np.compress(self._hess_lower_mask, self._hess_lag.data)
369        return data
370
371    def intermediate(
372            self,
373            alg_mod,
374            iter_count,
375            obj_value,
376            inf_pr,
377            inf_du,
378            mu,
379            d_norm,
380            regularization_size,
381            alpha_du,
382            alpha_pr,
383            ls_trials
384    ):
385        if self._intermediate_callback is not None:
386            return self._intermediate_callback(self._nlp, alg_mod, iter_count, obj_value,
387                                               inf_pr, inf_du, mu, d_norm, regularization_size,
388                                               alpha_du, alpha_pr, ls_trials)
389        return True
390
391
392class CyIpoptSolver(object):
393    def __init__(self, problem_interface, options=None):
394        """Create an instance of the CyIpoptSolver. You must
395        provide a problem_interface that corresponds to
396        the abstract class CyIpoptProblemInterface
397        options can be provided as a dictionary of key value
398        pairs
399        """
400        self._problem = problem_interface
401
402        self._options = options
403        if options is not None:
404            assert isinstance(options, dict)
405        else:
406            self._options = dict()
407
408    def solve(self, x0=None, tee=False):
409        xl = self._problem.x_lb()
410        xu = self._problem.x_ub()
411        gl = self._problem.g_lb()
412        gu = self._problem.g_ub()
413
414        if x0 is None:
415            x0 = self._problem.x_init()
416        xstart = x0
417
418        nx = len(xstart)
419        ng = len(gl)
420
421        cyipopt_solver = cyipopt.Problem(
422            n=nx,
423            m=ng,
424            problem_obj=self._problem,
425            lb=xl,
426            ub=xu,
427            cl=gl,
428            cu=gu
429        )
430
431        # check if we need scaling
432        obj_scaling, x_scaling, g_scaling = self._problem.scaling_factors()
433        if any(_ is not None for _ in (obj_scaling, x_scaling, g_scaling)):
434            # need to set scaling factors
435            if obj_scaling is None:
436                obj_scaling = 1.0
437            if x_scaling is None:
438                x_scaling = np.ones(nx)
439            if g_scaling is None:
440                g_scaling = np.ones(ng)
441            try:
442                set_scaling = cyipopt_solver.set_problem_scaling
443            except AttributeError:
444                # Fall back to pre-1.0.0 API
445                set_scaling = cyipopt_solver.setProblemScaling
446            set_scaling(obj_scaling, x_scaling, g_scaling)
447
448        # add options
449        try:
450            add_option = cyipopt_solver.add_option
451        except AttributeError:
452            # Fall back to pre-1.0.0 API
453            add_option = cyipopt_solver.addOption
454        for k, v in self._options.items():
455            add_option(k, v)
456
457        # We preemptively set up the TeeStream, even if we aren't
458        # going to use it: the implementation is such that the
459        # context manager does nothing (i.e., doesn't start up any
460        # processing threads) until afer a client accesses
461        # STDOUT/STDERR
462        with TeeStream(sys.stdout) as _teeStream:
463            if tee:
464                try:
465                    fd = sys.stdout.fileno()
466                except (io.UnsupportedOperation, AttributeError):
467                    # If sys,stdout doesn't have a valid fileno,
468                    # then create one using the TeeStream
469                    fd = _teeStream.STDOUT.fileno()
470            else:
471                fd = None
472            with redirect_fd(fd=1, output=fd, synchronize=False):
473                x, info = cyipopt_solver.solve(xstart)
474
475        return x, info
476
477
478def _numpy_vector(val):
479    ans = np.array(val, np.float64)
480    if len(ans.shape) != 1:
481        raise ValueError("expected a vector, but recieved a matrix "
482                         "with shape %s" % (ans.shape,))
483    return ans
484
485
486class PyomoCyIpoptSolver(object):
487
488    CONFIG = ConfigBlock("cyipopt")
489    CONFIG.declare("tee", ConfigValue(
490        default=False,
491        domain=bool,
492        description="Stream solver output to console",
493    ))
494    CONFIG.declare("load_solutions", ConfigValue(
495        default=True,
496        domain=bool,
497        description="Store the final solution into the original Pyomo model",
498    ))
499    CONFIG.declare("return_nlp", ConfigValue(
500        default=False,
501        domain=bool,
502        description="Return the results object and the underlying nlp"
503                    " NLP object from the solve call.",
504    ))
505    CONFIG.declare("options", ConfigBlock(implicit=True))
506    CONFIG.declare("intermediate_callback", ConfigValue(
507        default=None,
508        description="Set the function that will be called each"
509                    " iteration."
510    ))
511
512    def __init__(self, **kwds):
513        """Create an instance of the CyIpoptSolver. You must
514        provide a problem_interface that corresponds to
515        the abstract class CyIpoptProblemInterface
516
517        options can be provided as a dictionary of key value
518        pairs
519        """
520        self.config = self.CONFIG(kwds)
521
522    def _set_model(self, model):
523        self._model = model
524
525    def available(self, exception_flag=False):
526        return bool(numpy_available and cyipopt_available)
527
528    def license_is_valid(self):
529        return True
530
531    def version(self):
532        return tuple(int(_) for _ in cyipopt.__version__.split('.'))
533
534    def solve(self, model, **kwds):
535        config = self.config(kwds, preserve_implicit=True)
536
537        if not isinstance(model, Block):
538            raise ValueError("PyomoCyIpoptSolver.solve(model): model "
539                             "must be a Pyomo Block")
540
541        # If this is a Pyomo model / block, then we need to create
542        # the appropriate PyomoNLP, then wrap it in a CyIpoptNLP
543        grey_box_blocks = list(model.component_data_objects(
544            egb.ExternalGreyBoxBlock, active=True))
545        if grey_box_blocks:
546            # nlp = pyomo_nlp.PyomoGreyBoxNLP(model)
547            nlp = pyomo_grey_box.PyomoNLPWithGreyBoxBlocks(model)
548        else:
549            nlp = pyomo_nlp.PyomoNLP(model)
550
551        problem = CyIpoptNLP(nlp, intermediate_callback=config.intermediate_callback)
552
553        xl = problem.x_lb()
554        xu = problem.x_ub()
555        gl = problem.g_lb()
556        gu = problem.g_ub()
557
558        nx = len(xl)
559        ng = len(gl)
560
561        cyipopt_solver = cyipopt.Problem(
562            n=nx,
563            m=ng,
564            problem_obj=problem,
565            lb=xl,
566            ub=xu,
567            cl=gl,
568            cu=gu
569        )
570
571        # check if we need scaling
572        obj_scaling, x_scaling, g_scaling = problem.scaling_factors()
573        if any(_ is not None for _ in (obj_scaling, x_scaling, g_scaling)):
574            # need to set scaling factors
575            if obj_scaling is None:
576                obj_scaling = 1.0
577            if x_scaling is None:
578                x_scaling = np.ones(nx)
579            if g_scaling is None:
580                g_scaling = np.ones(ng)
581            try:
582                set_scaling = cyipopt_solver.set_problem_scaling
583            except AttributeError:
584                # Fall back to pre-1.0.0 API
585                set_scaling = cyipopt_solver.setProblemScaling
586            set_scaling(obj_scaling, x_scaling, g_scaling)
587
588        # add options
589        try:
590            add_option = cyipopt_solver.add_option
591        except AttributeError:
592            # Fall back to pre-1.0.0 API
593            add_option = cyipopt_solver.addOption
594        for k, v in config.options.items():
595            add_option(k, v)
596
597        timer = TicTocTimer()
598        try:
599            # We preemptively set up the TeeStream, even if we aren't
600            # going to use it: the implementation is such that the
601            # context manager does nothing (i.e., doesn't start up any
602            # processing threads) until afer a client accesses
603            # STDOUT/STDERR
604            with TeeStream(sys.stdout) as _teeStream:
605                if config.tee:
606                    try:
607                        fd = sys.stdout.fileno()
608                    except (io.UnsupportedOperation, AttributeError):
609                        # If sys,stdout doesn't have a valid fileno,
610                        # then create one using the TeeStream
611                        fd = _teeStream.STDOUT.fileno()
612                else:
613                    fd = None
614                with redirect_fd(fd=1, output=fd, synchronize=False):
615                    x, info = cyipopt_solver.solve(problem.x_init())
616            solverStatus = SolverStatus.ok
617        except:
618            msg = "Exception encountered during cyipopt solve:"
619            logger.error(msg, exc_info=sys.exc_info())
620            solverStatus = SolverStatus.unknown
621            raise
622
623        wall_time = timer.toc(None)
624
625        results = SolverResults()
626
627        if config.load_solutions:
628            nlp.set_primals(x)
629            nlp.set_duals(info['mult_g'])
630            nlp.load_state_into_pyomo(
631                bound_multipliers=(info['mult_x_L'], info['mult_x_U']))
632        else:
633            soln = results.solution.add()
634            soln.variable.update(
635                (i, {'Value':j, 'ipopt_zL_out': zl, 'ipopt_zU_out': zu})
636                for i,j,zl,zu in zip( nlp.variable_names(),
637                                      x,
638                                      info['mult_x_L'],
639                                      info['mult_x_U'] )
640            )
641            soln.constraint.update(
642                (i, {'Dual':j}) for i,j in zip(
643                    nlp.constraint_names(), info['mult_g']))
644
645
646        results.problem.name = model.name
647        obj = next(model.component_data_objects(Objective, active=True))
648        if obj.sense == minimize:
649            results.problem.sense = ProblemSense.minimize
650            results.problem.upper_bound = info['obj_val']
651        else:
652            results.problem.sense = ProblemSense.maximize
653            results.problem.lower_bound = info['obj_val']
654        results.problem.number_of_objectives = 1
655        results.problem.number_of_constraints = ng
656        results.problem.number_of_variables = nx
657        results.problem.number_of_binary_variables = 0
658        results.problem.number_of_integer_variables = 0
659        results.problem.number_of_continuous_variables = nx
660        # TODO: results.problem.number_of_nonzeros
661
662        results.solver.name = 'cyipopt'
663        results.solver.return_code = info['status']
664        results.solver.message = info['status_msg']
665        results.solver.wallclock_time = wall_time
666        status_enum = _cyipopt_status_enum[info['status_msg']]
667        results.solver.termination_condition = _ipopt_term_cond[status_enum]
668        results.solver.status = TerminationCondition.to_solver_status(
669            results.solver.termination_condition)
670
671        if config.return_nlp:
672            return results, nlp
673
674        return results
675
676    #
677    # Support "with" statements.
678    #
679    def __enter__(self):
680        return self
681
682    def __exit__(self, t, v, traceback):
683        pass
684