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