Source code for pyomo.contrib.pynumero.algorithms.solvers.cyipopt_solver

# ____________________________________________________________________________________
#
# Pyomo: Python Optimization Modeling Objects
# Copyright (c) 2008-2026 National Technology and Engineering Solutions of Sandia, LLC
# Under the terms of Contract DE-NA0003525 with National Technology and Engineering
# Solutions of Sandia, LLC, the U.S. Government retains certain rights in this
# software.  This software is distributed under the 3-clause BSD License.
# ____________________________________________________________________________________
"""
The cyipopt_solver module includes two solvers that call CyIpopt. One,
CyIpoptSolver, is a solver that operates on a CyIpoptProblemInterface
(such as CyIpoptNLP). The other, PyomoCyIpoptSolver, operates directly on a
Pyomo model.

"""

import io
import sys
import logging
import os
import abc

from pyomo.common.deprecation import relocated_module_attribute
from pyomo.common.dependencies import attempt_import, numpy as np, numpy_available
from pyomo.common.tee import capture_output
from pyomo.common.modeling import unique_component_name
from pyomo.core.base.objective import Objective

# Because pynumero.interfaces requires numpy, we will leverage deferred
# imports here so that the solver can be registered even when numpy is
# not available.
pyomo_nlp = attempt_import("pyomo.contrib.pynumero.interfaces.pyomo_nlp")[0]
pyomo_grey_box = attempt_import("pyomo.contrib.pynumero.interfaces.pyomo_grey_box_nlp")[
    0
]
egb = attempt_import("pyomo.contrib.pynumero.interfaces.external_grey_box")[0]

# Defer this import so that importing this module (PyomoCyIpoptSolver in
# particular) does not rely on an attempted cyipopt import.
cyipopt_interface, _ = attempt_import(
    "pyomo.contrib.pynumero.interfaces.cyipopt_interface"
)

# 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.6.0",
)
relocated_module_attribute(
    "CyIpoptProblemInterface",
    "pyomo.contrib.pynumero.interfaces.cyipopt_interface.CyIpoptProblemInterface",
    "6.6.0",
)
relocated_module_attribute(
    "CyIpoptNLP",
    "pyomo.contrib.pynumero.interfaces.cyipopt_interface.CyIpoptNLP",
    "6.6.0",
)

from pyomo.common.config import ConfigBlock, ConfigValue
from pyomo.common.timing import TicTocTimer
from pyomo.core.base import Block, Objective, minimize
from pyomo.opt import SolverStatus, SolverResults, TerminationCondition
from pyomo.opt.results.solution import Solution

logger = logging.getLogger(__name__)

# This maps the cyipopt STATUS_MESSAGES back to string representations
# of the Ipopt ApplicationReturnStatus enum
_cyipopt_status_enum = [
    "Solve_Succeeded",
    (
        b"Algorithm terminated successfully at a locally "
        b"optimal point, satisfying the convergence tolerances "
        b"(can be specified by options)."
    ),
    "Solved_To_Acceptable_Level",
    (
        b"Algorithm stopped at a point that was "
        b'converged, not to "desired" tolerances, '
        b'but to "acceptable" tolerances (see the '
        b"acceptable-... options)."
    ),
    "Infeasible_Problem_Detected",
    (
        b"Algorithm converged to a point of local "
        b"infeasibility. Problem may be "
        b"infeasible."
    ),
    "Search_Direction_Becomes_Too_Small",
    (b"Algorithm proceeds with very little progress."),
    "Diverging_Iterates",
    b"It seems that the iterates diverge.",
    "User_Requested_Stop",
    (
        b"The user call-back function intermediate_callback "
        b"(see Section 3.3.4 in the documentation) returned "
        b"false, i.e., the user code requested a premature "
        b"termination of the optimization."
    ),
    "Feasible_Point_Found",
    b"Feasible point for square problem found.",
    "Maximum_Iterations_Exceeded",
    (b"Maximum number of iterations exceeded (can be specified by an option)."),
    "Restoration_Failed",
    (b"Restoration phase failed, algorithm doesn't know how to proceed."),
    "Error_In_Step_Computation",
    (
        b"An unrecoverable error occurred while Ipopt "
        b"tried to compute the search direction."
    ),
    "Maximum_CpuTime_Exceeded",
    b"Maximum CPU time exceeded.",
    "Not_Enough_Degrees_Of_Freedom",
    b"Problem has too few degrees of freedom.",
    "Invalid_Problem_Definition",
    b"Invalid problem definition.",
    "Invalid_Option",
    b"Invalid option encountered.",
    "Invalid_Number_Detected",
    (
        b"Algorithm received an invalid number (such as "
        b"NaN or Inf) from the NLP; see also option "
        b"check_derivatives_for_naninf."
    ),
    # Note that the concluding "." was missing before cyipopt 1.0.3
    "Invalid_Number_Detected",
    (
        b"Algorithm received an invalid number (such as "
        b"NaN or Inf) from the NLP; see also option "
        b"check_derivatives_for_naninf"
    ),
    "Unrecoverable_Exception",
    b"Some uncaught Ipopt exception encountered.",
    "NonIpopt_Exception_Thrown",
    b"Unknown Exception caught in Ipopt.",
    # Note that the concluding "." was missing before cyipopt 1.0.3
    "NonIpopt_Exception_Thrown",
    b"Unknown Exception caught in Ipopt",
    "Insufficient_Memory",
    b"Not enough memory.",
    "Internal_Error",
    (
        b"An unknown internal error occurred. Please contact "
        b"the Ipopt authors through the mailing list."
    ),
]
_cyipopt_status_enum = {
    _cyipopt_status_enum[i + 1]: _cyipopt_status_enum[i]
    for i in range(0, len(_cyipopt_status_enum), 2)
}

# This maps Ipopt ApplicationReturnStatus enum strings to an appropriate
# Pyomo TerminationCondition
_ipopt_term_cond = {
    "Solve_Succeeded": TerminationCondition.optimal,
    "Solved_To_Acceptable_Level": TerminationCondition.feasible,
    "Infeasible_Problem_Detected": TerminationCondition.infeasible,
    "Search_Direction_Becomes_Too_Small": TerminationCondition.minStepLength,
    "Diverging_Iterates": TerminationCondition.unbounded,
    "User_Requested_Stop": TerminationCondition.userInterrupt,
    "Feasible_Point_Found": TerminationCondition.feasible,
    "Maximum_Iterations_Exceeded": TerminationCondition.maxIterations,
    "Restoration_Failed": TerminationCondition.noSolution,
    "Error_In_Step_Computation": TerminationCondition.solverFailure,
    "Maximum_CpuTime_Exceeded": TerminationCondition.maxTimeLimit,
    "Not_Enough_Degrees_Of_Freedom": TerminationCondition.invalidProblem,
    "Invalid_Problem_Definition": TerminationCondition.invalidProblem,
    "Invalid_Option": TerminationCondition.error,
    "Invalid_Number_Detected": TerminationCondition.internalSolverError,
    "Unrecoverable_Exception": TerminationCondition.internalSolverError,
    "NonIpopt_Exception_Thrown": TerminationCondition.error,
    "Insufficient_Memory": TerminationCondition.resourceInterrupt,
    "Internal_Error": TerminationCondition.internalSolverError,
}


[docs] class CyIpoptSolver:
[docs] def __init__(self, problem_interface, options=None): """Create an instance of the CyIpoptSolver. You must provide a problem_interface that corresponds to the abstract class CyIpoptProblemInterface options can be provided as a dictionary of key value pairs """ self._problem = problem_interface self._options = options if options is not None: assert isinstance(options, dict) else: self._options = dict()
def solve(self, x0=None, tee=False): if x0 is None: x0 = self._problem.x_init() xstart = x0 cyipopt_solver = self._problem # check if we need scaling obj_scaling, x_scaling, g_scaling = self._problem.scaling_factors() if any(_ is not None for _ in (obj_scaling, x_scaling, g_scaling)): # need to set scaling factors if obj_scaling is None: obj_scaling = 1.0 if x_scaling is None: x_scaling = np.ones(nx) if g_scaling is None: g_scaling = np.ones(ng) try: set_scaling = cyipopt_solver.set_problem_scaling except AttributeError: # Fall back to pre-1.0.0 API set_scaling = cyipopt_solver.setProblemScaling set_scaling(obj_scaling, x_scaling, g_scaling) # add options try: add_option = cyipopt_solver.add_option except AttributeError: # Fall back to pre-1.0.0 API add_option = cyipopt_solver.addOption for k, v in self._options.items(): add_option(k, v) with capture_output(sys.stdout if tee else None, capture_fd=True): x, info = cyipopt_solver.solve(xstart) return x, info
def _numpy_vector(val): ans = np.array(val, np.float64) if len(ans.shape) != 1: raise ValueError( "expected a vector, but received a matrix with shape %s" % (ans.shape,) ) return ans
[docs] class PyomoCyIpoptSolver: CONFIG = ConfigBlock("cyipopt") CONFIG.declare( "tee", ConfigValue( default=False, domain=bool, description="Stream solver output to console" ), ) CONFIG.declare( "load_solutions", ConfigValue( default=True, domain=bool, description="Store the final solution into the original Pyomo model", ), ) CONFIG.declare( "return_nlp", ConfigValue( default=False, domain=bool, description="Return the results object and the underlying nlp" " NLP object from the solve call.", ), ) CONFIG.declare("options", ConfigBlock(implicit=True)) CONFIG.declare( "intermediate_callback", ConfigValue( default=None, description="Set the function that will be called each iteration.", ), ) CONFIG.declare( "halt_on_evaluation_error", ConfigValue( default=None, description="Whether to halt if a function or derivative evaluation fails", ), )
[docs] def __init__(self, **kwds): """Create an instance of the CyIpoptSolver. You must provide a problem_interface that corresponds to the abstract class CyIpoptProblemInterface options can be provided as a dictionary of key value pairs """ self.config = self.CONFIG(kwds)
def _set_model(self, model): self._model = model def available(self, exception_flag=False): return bool(numpy_available and cyipopt_interface.cyipopt_available) def license_is_valid(self): return True def version(self): def _int(x): try: return int(x) except: return x return tuple(_int(_) for _ in cyipopt_interface.cyipopt.__version__.split(".")) def solve(self, model, **kwds): config = self.config(kwds, preserve_implicit=True) if not isinstance(model, Block): raise ValueError( "PyomoCyIpoptSolver.solve(model): model must be a Pyomo Block" ) # If this is a Pyomo model / block, then we need to create # the appropriate PyomoNLP, then wrap it in a CyIpoptNLP grey_box_blocks = list( model.component_data_objects(egb.ExternalGreyBoxBlock, active=True) ) # if there is no objective, add one temporarily so we can construct an NLP objectives = list(model.component_data_objects(Objective, active=True)) n_obj = len(objectives) for gbb in grey_box_blocks: if gbb.get_external_model().has_objective(): n_obj += 1 if n_obj == 0: objname = unique_component_name(model, "_obj") objective = model.add_component(objname, Objective(expr=0.0)) try: if grey_box_blocks: # nlp = pyomo_nlp.PyomoGreyBoxNLP(model) nlp = pyomo_grey_box.PyomoNLPWithGreyBoxBlocks(model) else: nlp = pyomo_nlp.PyomoNLP(model) finally: # We only need the objective to construct the NLP, so we delete # it from the model ASAP if n_obj == 0: model.del_component(objective) problem = cyipopt_interface.CyIpoptNLP( nlp, intermediate_callback=config.intermediate_callback, halt_on_evaluation_error=config.halt_on_evaluation_error, ) ng = len(problem.g_lb()) nx = len(problem.x_lb()) cyipopt_solver = problem # check if we need scaling obj_scaling, x_scaling, g_scaling = problem.scaling_factors() if any(_ is not None for _ in (obj_scaling, x_scaling, g_scaling)): # need to set scaling factors if obj_scaling is None: obj_scaling = 1.0 if x_scaling is None: x_scaling = np.ones(nx) if g_scaling is None: g_scaling = np.ones(ng) try: set_scaling = cyipopt_solver.set_problem_scaling except AttributeError: # Fall back to pre-1.0.0 API set_scaling = cyipopt_solver.setProblemScaling set_scaling(obj_scaling, x_scaling, g_scaling) # add options try: add_option = cyipopt_solver.add_option except AttributeError: # Fall back to pre-1.0.0 API add_option = cyipopt_solver.addOption for k, v in config.options.items(): add_option(k, v) timer = TicTocTimer() try: with capture_output(sys.stdout if config.tee else None, capture_fd=True): x, info = cyipopt_solver.solve(problem.x_init()) solverStatus = SolverStatus.ok except: msg = "Exception encountered during cyipopt solve:" logger.error(msg, exc_info=sys.exc_info()) solverStatus = SolverStatus.unknown raise wall_time = timer.toc(None) results = SolverResults() if config.load_solutions: nlp.set_primals(x) nlp.set_duals(info["mult_g"]) nlp.load_state_into_pyomo( bound_multipliers=(info["mult_x_L"], info["mult_x_U"]) ) else: soln = Solution() sm = nlp.symbol_map soln.variable.update( (sm.getSymbol(i), {'Value': j, 'ipopt_zL_out': zl, 'ipopt_zU_out': zu}) for i, j, zl, zu in zip( nlp.get_pyomo_variables(), x, info['mult_x_L'], info['mult_x_U'] ) ) soln.constraint.update( (sm.getSymbol(i), {'Dual': j}) for i, j in zip(nlp.get_pyomo_constraints(), info['mult_g']) ) model.solutions.add_symbol_map(sm) results._smap_id = id(sm) results.solution.insert(soln) results.problem.name = model.name obj = next(model.component_data_objects(Objective, active=True)) results.problem.sense = obj.sense if obj.sense == minimize: results.problem.upper_bound = info["obj_val"] else: results.problem.lower_bound = info["obj_val"] results.problem.number_of_objectives = 1 results.problem.number_of_constraints = ng results.problem.number_of_variables = nx results.problem.number_of_binary_variables = 0 results.problem.number_of_integer_variables = 0 results.problem.number_of_continuous_variables = nx # TODO: results.problem.number_of_nonzeros results.solver.name = "cyipopt" results.solver.return_code = info["status"] results.solver.message = info["status_msg"] results.solver.wallclock_time = wall_time status_enum = _cyipopt_status_enum[info["status_msg"]] results.solver.termination_condition = _ipopt_term_cond[status_enum] results.solver.status = TerminationCondition.to_solver_status( results.solver.termination_condition ) problem.close() if config.return_nlp: return results, nlp return results # # Support "with" statements. # def __enter__(self): return self def __exit__(self, t, v, traceback): pass