Пример #1
0
    def unpack_results(self, solver_name, results_dict):
        """Parses the output from a solver and updates the problem state.

        Updates problem.status, problem.value and value of
        primal and dual variables.

        Assumes the results are from the given solver.

        Parameters
        ----------
        solver_name : str
            The name of the solver being used.
        results_dict : dict
            The solver output.
        """
        if solver_name not in SOLVERS:
            raise SolverError("Unknown solver.")
        solver = SOLVERS[solver_name]

        objective, constraints = self.canonicalize()
        sym_data = solver.get_sym_data(objective, constraints,
                                       self._cached_data)
        data = {s.DIMS: sym_data.dims, s.OFFSET: 0}
        results_dict = solver.format_results(results_dict, data,
                                             self._cached_data)
        self._update_problem_state(results_dict, sym_data, solver)
Пример #2
0
    def unpack_results(self, solution, chain, inverse_data):
        """Updates the problem state given the solver results.

        Updates problem.status, problem.value and value of
        primal and dual variables.

        Parameters
        __________
        solution : object
            The solution returned by applying the chain to the problem
            and invoking the solver on the resulting data.
        chain : SolvingChain
            A solving chain that was used to solve the problem.
        inverse_data : list
            The inverse data returned by applying the chain to the problem.
        """
        solution = chain.invert(solution, inverse_data)
        try:
            self.unpack(solution)
        except ValueError:
            raise SolverError(
                "Solver '%s' failed. " % chain.solver.name() +
                "Try another solver or solve with verbose=True for more "
                "information. Try recentering the problem data around 0 and "
                "rescaling to reduce the dynamic range.")
        self._solver_stats = SolverStats(self._solution.attr,
                                         chain.solver.name())
Пример #3
0
    def unpack_results(self, solution, chain, inverse_data):
        """Updates the problem state given the solver results.

        Updates problem.status, problem.value and value of
        primal and dual variables.

        Parameters
        __________
        solution : object
            The solution returned by applying the chain to the problem
            and invoking the solver on the resulting data.
        chain : SolvingChain
            A solving chain that was used to solve the problem.
        inverse_data : list
            The inverse data returned by applying the chain to the problem.

        Raises
        ------
        cvxpy.error.SolverError
            If the solver failed
        """

        solution = chain.invert(solution, inverse_data)
        if solution.status in s.ERROR:
            raise SolverError(
                    "Solver '%s' failed. " % chain.solver.name() +
                    "Try another solver, or solve with verbose=True for more "
                    "information.")
        self.unpack(solution)
        self._solver_stats = SolverStats(self._solution.attr,
                                         chain.solver.name())
Пример #4
0
    def format(self, eq_constr, leq_constr, dims, solver):
        """Formats SDP constraints as inequalities for the solver.

        Parameters
        ----------
        eq_constr : list
            A list of the equality constraints in the canonical problem.
        leq_constr : list
            A list of the inequality constraints in the canonical problem.
        dims : dict
            A dict with the dimensions of the conic constraints.
        solver : str
            The solver being called.
        """
        if solver.name() in [s.CVXOPT, s.MOSEK]:
            new_eq_constr, new_leq_constr = self.__CVXOPT_format
        elif solver.name() == s.SCS:
            new_eq_constr, new_leq_constr = self.__SCS_format
        else:
            raise SolverError(
                "Solver does not support positive semidefinite cone."
            )

        if self.enforce_sym:
            # upper_tri(A) == upper_tri(A.T)
            eq_constr += new_eq_constr
            # Update dims.
            dims[s.EQ_DIM] += (self.size[0]*(self.size[1] - 1))//2
        # 0 <= A
        leq_constr += new_leq_constr
        # Update dims.
        dims[s.SDP_DIM].append(self.size[0])
Пример #5
0
    def _update_problem_state(self, results_dict, sym_data, solver):
        """Updates the problem state given the solver results.

        Updates problem.status, problem.value and value of
        primal and dual variables.

        Parameters
        ----------
        results_dict : dict
            A dictionary containing the solver results.
        sym_data : SymData
            The symbolic data for the problem.
        solver : Solver
            The solver type used to obtain the results.
        """
        if results_dict[s.STATUS] in s.SOLUTION_PRESENT:
            self._save_values(results_dict[s.PRIMAL], self.variables(),
                              sym_data.var_offsets)
            self._save_dual_values(results_dict[s.EQ_DUAL],
                                   sym_data.constr_map[s.EQ], EqConstraint)
            self._save_dual_values(results_dict[s.INEQ_DUAL],
                                   sym_data.constr_map[s.LEQ], LeqConstraint)
            # Correct optimal value if the objective was Maximize.
            value = results_dict[s.VALUE]
            self._value = self.objective.primal_to_result(value)
        # Infeasible or unbounded.
        elif results_dict[s.STATUS] in s.INF_OR_UNB:
            self._handle_no_solution(results_dict[s.STATUS])
        # Solver failed to solve.
        else:
            raise SolverError("Solver '%s' failed. Try another solver." %
                              solver.name())
        self._status = results_dict[s.STATUS]
Пример #6
0
    def unpack_results(self, solution, chain, inverse_data):
        """Updates the problem state given the solver results.

        Updates problem.status, problem.value and value of
        primal and dual variables.

        Parameters
        __________
        solution : Solution
            The solution returned by applying the chain to the problem
            and invoking the solver on the resulting data.
        chain : SolvingChain
            A solving chain that was used to solve the problem.
        inverse_data : list
            The inverse data returned by applying the chain to the problem.
        """
        solution = chain.invert(solution, inverse_data)
        self._value = solution.opt_val
        if solution.status in s.SOLUTION_PRESENT:
            for v in self.variables():
                v.save_value(solution.primal_vars[v.id])
            for c in self.constraints:
                if c.id in solution.dual_vars:
                    c.save_value(solution.dual_vars[c.id])
        elif solution.status in s.INF_OR_UNB:
            for v in self.variables():
                v.save_value(None)
            for constr in self.constraints:
                constr.save_value(None)
        else:
            raise SolverError(
                "Solver '%s' failed." % chain.solver.name() + \
                "Try another solver or solve with verbose=True for more information.")
        self._status = solution.status
        self._solver_stats = SolverStats(solution.attr, chain.solver.name())
Пример #7
0
    def _solve(self, solver=None, ignore_dcp=False, verbose=False, **kwargs):
        """Solves a DCP compliant optimization problem.

        Saves the values of primal and dual variables in the variable
        and constraint objects, respectively.

        Parameters
        ----------
        solver : str, optional
            The solver to use. Defaults to ECOS.
        ignore_dcp : bool, optional
            Overrides the default of raising an exception if the problem is not
            DCP.
        verbose : bool, optional
            Overrides the default of hiding solver output.
        kwargs : dict, optional
            A dict of options that will be passed to the specific solver.
            In general, these options will override any default settings
            imposed by cvxpy.

        Returns
        -------
        float
            The optimal value for the problem, or a string indicating
            why the problem could not be solved.
        """
        if not self.is_dcp():
            if ignore_dcp:
                print("Problem does not follow DCP rules. "
                      "Solving a convex relaxation.")
            else:
                raise Exception("Problem does not follow DCP rules.")

        objective, constraints = self.canonicalize()
        # Choose a solver/check the chosen solver.
        if solver is None:
            solver_name = Solver.choose_solver(constraints)
            solver = SOLVERS[solver_name]
        elif solver in SOLVERS:
            solver = SOLVERS[solver]
            solver.validate_solver(constraints)
        else:
            raise SolverError("Unknown solver.")

        sym_data = solver.get_sym_data(objective, constraints,
                                       self._cached_data)
        # Presolve couldn't solve the problem.
        if sym_data.presolve_status is None:
            results_dict = solver.solve(objective, constraints,
                                        self._cached_data, verbose, kwargs)
        # Presolve determined problem was unbounded or infeasible.
        else:
            results_dict = {s.STATUS: sym_data.presolve_status}

        self._update_problem_state(results_dict, sym_data, solver)
        return self.value
Пример #8
0
    def solve(self, *args, **kwargs):
        use_2step = kwargs.pop("two_step", True)
        use_slack = kwargs.pop("slack", False)

        # Reduce quantile atoms in objective.
        original = Problem(self._objective, self.constraints)
        if not Quantile2Chance().accepts(original):
            raise DCPError("Cannot convert quantiles to chance constraints")
        reduced, inv_data = Quantile2Chance().apply(original)

        # First pass with convex restrictions.
        chance_constraints = [
            cc for cc in reduced._chance_constraints if cc.fraction != 0
        ]
        for cc in chance_constraints:  # Define slack variable for each chance constraint.
            cc.slack = Variable(nonneg=True) if use_slack else 0
        restrictions = [cc.restriction for cc in chance_constraints]
        constrs1 = reduced._regular_constraints + restrictions
        prob1 = cvxprob.Problem(reduced.objective, constrs1)
        prob1.solve(*args, **kwargs)

        # Terminate if first pass does not produce solution.
        if prob1.status not in s.SOLUTION_PRESENT:
            self.save_results(prob1, [Quantile2Chance()], inv_data)
            raise SolverError("First pass failed with status {0}".format(
                self.status))

        if not use_2step:
            self.save_results(prob1, [Quantile2Chance()], inv_data)
            return self.value

        # Replace chance constraints with exact bounds where solution of
        # first pass yields a relatively low constraint violation.
        constrs2 = reduced._regular_constraints
        for cc in chance_constraints:
            subsets = self.best_subset(cc.margins(),
                                       (1.0 - cc.fraction) * cc.size)
            for constr, subset in zip(cc.constraints, subsets):
                # if not np.any(subset):
                #	continue
                if isinstance(constr, Inequality):
                    constrs2 += [constr.expr[subset] >= 0
                                 ]  # Flip direction of inequality.
                elif isinstance(constr, Equality):
                    constrs2 += [constr.expr[subset] == 0]
                else:
                    raise ValueError("Only (<=, ==, >=) constraints supported")

        # Second pass with exact bounds.
        prob2 = cvxprob.Problem(reduced.objective, constrs2)
        prob2.solve(*args, **kwargs)
        self.save_results(prob2, [Quantile2Chance()], inv_data)
        return self.value
Пример #9
0
    def validate_solver(self, constraints):
        """Raises an exception if the solver cannot solve the problem.

        Parameters
        ----------
        constraints: list
            The list of canonicalized constraints.
        """
        # Check the solver is installed.
        if not self.is_installed():
            raise SolverError("The solver %s is not installed." % self.name())
        # Check the solver can solve the problem.
        constr_map = SymData.filter_constraints(constraints)
        if ((constr_map[s.BOOL] or constr_map[s.INT]) \
            and not self.MIP_CAPABLE) or \
           (constr_map[s.SDP] and not self.SDP_CAPABLE) or \
           (constr_map[s.EXP] and not self.EXP_CAPABLE) or \
           (constr_map[s.SOC] and not self.SOCP_CAPABLE) or \
           (len(constraints) == 0 and self.name() in [s.SCS,
                                                      s.GLPK]):
            raise SolverError("The solver %s cannot solve the problem." %
                              self.name())
Пример #10
0
    def validate_solver(self, constraints):
        """Raises an exception if the solver cannot solve the problem.

        Parameters
        ----------
        constraints: list
            The list of canonicalized constraints.
        """
        constr_map = SymData.filter_constraints(constraints)
        if (constr_map[s.SDP] and not self.sdp_capable()) or \
           (constr_map[s.EXP] and not self.exp_capable()) or \
           (len(constraints) == 0 and self.name() == s.SCS):
            raise SolverError(
                "The solver %s cannot solve the problem." % self.name()
            )
Пример #11
0
    def _reject_problem(self, reason):
        """Raise an error indicating that the solver cannot solve a problem.

        Parameters
        ----------
        reason : str
            A short description of the reason the problem cannot be solved by
            this solver.

        Raises
        ------
        cvxpy.SolverError
            An error explaining why the problem could not be solved.
        """
        message = "The solver {} cannot solve the problem because {}.".format(
            self.name(), reason
        )
        raise SolverError(message)
Пример #12
0
    def format(self, eq_constr, leq_constr, dims, solver):
        """Formats EXP constraints for the solver.

        Parameters
        ----------
        eq_constr : list
            A list of the equality constraints in the canonical problem.
        leq_constr : list
            A list of the inequality constraints in the canonical problem.
        dims : dict
            A dict with the dimensions of the conic constraints.
        solver : str
            The solver being called.
        """
        if solver.name() == s.CVXOPT:
            eq_constr += self.__CVXOPT_format[0]
        elif solver.name() == s.SCS:
            leq_constr += self.__SCS_format[1]
        else:
            raise SolverError("Solver does not support exponential cone.")
        # Update dims.
        dims[s.EXP_DIM] += self.size[0] * self.size[1]
Пример #13
0
    def validate_solver(self, constraints):
        """Raises an exception if the solver cannot solve the problem.

        Parameters
        ----------
        constraints: list
            The list of canonicalized constraints.
        """
        # Check the solver is installed.
        if not self.is_installed():
            raise SolverError("The solver %s is not installed." % self.name())
        # Check the solver can solve the problem.
        constr_map = SymData.filter_constraints(constraints)

        if (constr_map[s.BOOL] or constr_map[s.INT]) and not self.MIP_CAPABLE:
            self._reject_problem("it cannot solve mixed-integer problems")
        elif constr_map[s.SDP] and not self.SDP_CAPABLE:
            self._reject_problem("it cannot solve semidefinite problems")
        elif constr_map[s.EXP] and not self.EXP_CAPABLE:
            self._reject_problem("it cannot solve exponential cone problems")
        elif constr_map[s.SOC] and not self.SOCP_CAPABLE:
            self._reject_problem("it cannot solve second-order cone problems")
        elif len(constraints) == 0 and self.name() in (s.SCS, s.GLPK):
            self._reject_problem("it cannot solve unconstrained problems")
Пример #14
0
    def _solve(self,
               solver=None,
               ignore_dcp=False,
               warm_start=False,
               verbose=False,
               parallel=False,
               **kwargs):
        """Solves a DCP compliant optimization problem.

        Saves the values of primal and dual variables in the variable
        and constraint objects, respectively.

        Parameters
        ----------
        solver : str, optional
            The solver to use. Defaults to ECOS.
        ignore_dcp : bool, optional
            Overrides the default of raising an exception if the problem is not
            DCP.
        warm_start : bool, optional
            Should the previous solver result be used to warm start?
        verbose : bool, optional
            Overrides the default of hiding solver output.
        parallel : bool, optional
            If problem is separable, solve in parallel.
        kwargs : dict, optional
            A dict of options that will be passed to the specific solver.
            In general, these options will override any default settings
            imposed by cvxpy.

        Returns
        -------
        float
            The optimal value for the problem, or a string indicating
            why the problem could not be solved.
        """
        if not self.is_dcp():
            if ignore_dcp:
                print("Problem does not follow DCP rules. "
                      "Solving a convex relaxation.")
            else:
                raise DCPError("Problem does not follow DCP rules.")

        if solver == s.LS:
            solver = SOLVERS[s.LS]
            solver.validate_solver(self)

            objective = self.objective
            constraints = self.constraints

            sym_data = solver.get_sym_data(objective, constraints)
            results_dict = solver.solve(objective, constraints,
                                        self._cached_data, warm_start, verbose,
                                        kwargs)
            self._update_problem_state(results_dict, sym_data, solver)
            return self.value

        # Standard cone problem
        objective, constraints = self.canonicalize()

        # Solve in parallel
        if parallel:
            # Check if the objective or constraint has changed

            if (objective != self._cached_data[s.PARALLEL].objective or
                    constraints != self._cached_data[s.PARALLEL].constraints):
                self._separable_problems = cvxpy.transforms.get_separable_problems(
                    self)
                self._cached_data[s.PARALLEL] = CachedProblem(
                    objective, constraints)
            if len(self._separable_problems) > 1:
                return self._parallel_solve(solver, ignore_dcp, warm_start,
                                            verbose, **kwargs)

        # Choose a solver/check the chosen solver.
        if solver is None:
            solver_name = Solver.choose_solver(constraints)
            solver = SOLVERS[solver_name]
        elif solver in SOLVERS:
            solver = SOLVERS[solver]
            solver.validate_solver(constraints)
        else:
            raise SolverError("Unknown solver.")

        sym_data = solver.get_sym_data(objective, constraints,
                                       self._cached_data)
        # Presolve couldn't solve the problem.
        if sym_data.presolve_status is None:
            results_dict = solver.solve(objective, constraints,
                                        self._cached_data, warm_start, verbose,
                                        kwargs)
        # Presolve determined problem was unbounded or infeasible.
        else:
            results_dict = {s.STATUS: sym_data.presolve_status}
        self._update_problem_state(results_dict, sym_data, solver)
        return self.value
Пример #15
0
def _reductions_for_problem_class(problem,
                                  candidates,
                                  gp: bool = False) -> List[Any]:
    """
    Builds a chain that rewrites a problem into an intermediate
    representation suitable for numeric reductions.

    Parameters
    ----------
    problem : Problem
        The problem for which to build a chain.
    candidates : dict
        Dictionary of candidate solvers divided in qp_solvers
        and conic_solvers.
    gp : bool
        If True, the problem is parsed as a Disciplined Geometric Program
        instead of as a Disciplined Convex Program.
    Returns
    -------
    list of Reduction objects
        A list of reductions that can be used to convert the problem to an
        intermediate form.
    Raises
    ------
    DCPError
        Raised if the problem is not DCP and `gp` is False.
    DGPError
        Raised if the problem is not DGP and `gp` is True.
    """
    reductions = []
    # TODO Handle boolean constraints.
    if complex2real.accepts(problem):
        reductions += [complex2real.Complex2Real()]
    if gp:
        reductions += [Dgp2Dcp()]

    if not gp and not problem.is_dcp():
        append = build_non_disciplined_error_msg(problem, 'DCP')
        if problem.is_dgp():
            append += ("\nHowever, the problem does follow DGP rules. "
                       "Consider calling solve() with `gp=True`.")
        elif problem.is_dqcp():
            append += ("\nHowever, the problem does follow DQCP rules. "
                       "Consider calling solve() with `qcp=True`.")
        raise DCPError("Problem does not follow DCP rules. Specifically:\n" +
                       append)
    elif gp and not problem.is_dgp():
        append = build_non_disciplined_error_msg(problem, 'DGP')
        if problem.is_dcp():
            append += ("\nHowever, the problem does follow DCP rules. "
                       "Consider calling solve() with `gp=False`.")
        elif problem.is_dqcp():
            append += ("\nHowever, the problem does follow DQCP rules. "
                       "Consider calling solve() with `qcp=True`.")
        raise DGPError("Problem does not follow DGP rules." + append)

    # Dcp2Cone and Qp2SymbolicQp require problems to minimize their objectives.
    if type(problem.objective) == Maximize:
        reductions += [FlipObjective()]

    if _solve_as_qp(problem, candidates):
        reductions += [CvxAttr2Constr(), qp2symbolic_qp.Qp2SymbolicQp()]
    else:
        # Canonicalize it to conic problem.
        if not candidates['conic_solvers']:
            raise SolverError("Problem could not be reduced to a QP, and no "
                              "conic solvers exist among candidate solvers "
                              "(%s)." % candidates)
        else:
            reductions += [Dcp2Cone(), CvxAttr2Constr()]

    constr_types = {type(c) for c in problem.constraints}
    if FiniteSet in constr_types:
        reductions += [Valinvec2mixedint()]

    return reductions
Пример #16
0
    def _solve(self, solver=None, ignore_dcp=False, verbose=False, **kwargs):
        """Solves a DCP compliant optimization problem.

        Saves the values of primal and dual variables in the variable
        and constraint objects, respectively.

        Parameters
        ----------
        solver : str, optional
            The solver to use. Defaults to ECOS.
        ignore_dcp : bool, optional
            Overrides the default of raising an exception if the problem is not
            DCP.
        verbose : bool, optional
            Overrides the default of hiding solver output.
        kwargs : dict, optional
            A dict of options that will be passed to the specific solver.
            In general, these options will override any default settings
            imposed by cvxpy.

        Returns
        -------
        float
            The optimal value for the problem, or a string indicating
            why the problem could not be solved.
        """
        if not self.is_dcp():
            if ignore_dcp:
                print("Problem does not follow DCP rules. "
                      "Solving a convex relaxation.")
            else:
                raise Exception("Problem does not follow DCP rules.")

        objective, constraints = self.canonicalize()
        # Choose a solver/check the chosen solver.
        if solver is None:
            solver_name = Solver.choose_solver(constraints)
            solver = SOLVERS[solver_name]
        elif solver in SOLVERS:
            solver = SOLVERS[solver]
            solver.validate_solver(constraints)
        else:
            raise SolverError("Unknown solver.")

        sym_data = solver.get_sym_data(objective, constraints,
                                       self._cached_data)
        # Presolve couldn't solve the problem.
        if sym_data.presolve_status is None:
            status, value, x, y, z = solver.solve(objective, constraints,
                                                  self._cached_data, verbose,
                                                  kwargs)
        # Presolve determined problem was unbounded or infeasible.
        else:
            status = sym_data.presolve_status

        if status in s.SOLUTION_PRESENT:
            self._save_values(x, self.variables(), sym_data.var_offsets)
            self._save_dual_values(y, sym_data.constr_map[s.EQ], EqConstraint)
            self._save_dual_values(z, sym_data.constr_map[s.LEQ],
                                   LeqConstraint)
            # Correct optimal value if the objective was Maximize.
            self._value = self.objective.primal_to_result(value)
        # Infeasible or unbounded.
        elif status in s.INF_OR_UNB:
            self._handle_no_solution(status)
        # Solver failed to solve.
        else:
            raise SolverError("Solver '%s' failed. Try another solver." %
                              solver.name())
        self._status = status
        return self.value
Пример #17
0
def construct_solving_chain(problem, solver=None, gp=False):
    """Build a reduction chain from a problem to an installed solver.

    Note that if the supplied problem has 0 variables, then the solver
    parameter will be ignored.

    Parameters
    ----------
    problem : Problem
        The problem for which to build a chain.
    solver : string
        The name of the solver with which to terminate the chain. If no solver
        is supplied (i.e., if solver is None), then the targeted solver may be
        any of those that are installed. If the problem is variable-free,
        then this parameter is ignored.
    gp : bool
        If True, the problem is parsed as a Disciplined Geometric Program
        instead of as a Disciplined Convex Program.

    Returns
    -------
    SolvingChain
        A SolvingChain that can be used to solve the problem.

    Raises
    ------
    DCPError
        Raised if the problem is not DCP and `gp` is False.
    DGPError
        Raised if the problem is not DGP and `gp` is True.
    SolverError
        Raised if no suitable solver exists among the installed solvers, or
        if the target solver is not installed.
    """
    if solver is not None:
        if solver not in slv_def.INSTALLED_SOLVERS:
            raise SolverError("The solver %s is not installed." % solver)
        candidates = [solver]
    else:
        candidates = slv_def.INSTALLED_SOLVERS

    reductions = []
    if problem.parameters():
        reductions += [EvalParams()]
    if len(problem.variables()) == 0:
        reductions += [ConstantSolver()]
        return SolvingChain(reductions=reductions)
    if Complex2Real().accepts(problem):
        reductions += [Complex2Real()]
    if gp:
        reductions += [Dgp2Dcp()]
        if solver is not None and solver not in slv_def.CONIC_SOLVERS:
            raise SolverError(
                "When `gp=True`, `solver` must be a conic solver "
                "(received '%s'); try calling `solve()` with `solver=cvxpy.ECOS`."
                % solver)
        elif solver is None:
            candidates = slv_def.INSTALLED_CONIC_SOLVERS

    if not gp and not problem.is_dcp():
        append = ""
        if problem.is_dgp():
            append = (" However, the problem does follow DGP rules. "
                      "Consider calling this function with `gp=True`.")
        raise DCPError("Problem does not follow DCP rules." + append)
    elif gp and not problem.is_dgp():
        append = ""
        if problem.is_dcp():
            append = (" However, the problem does follow DCP rules. "
                      "Consider calling this function with `gp=False`.")
        raise DGPError("Problem does not follow DGP rules." + append)

    # Dcp2Cone and Qp2SymbolicQp require problems to minimize their objectives.
    if type(problem.objective) == Maximize:
        reductions.append(FlipObjective())

    # Conclude the chain with one of the following:
    #   (1) Qp2SymbolicQp --> QpMatrixStuffing --> [a QpSolver],
    #   (2) Dcp2Cone --> ConeMatrixStuffing --> [a ConicSolver]
    #
    # First, attempt to canonicalize the problem to a linearly constrained QP.
    candidate_qp_solvers = [s for s in slv_def.QP_SOLVERS if s in candidates]
    # Consider only MIQP solvers if problem is integer
    if problem.is_mixed_integer():
        candidate_qp_solvers = [
            s for s in candidate_qp_solvers
            if slv_def.SOLVER_MAP_QP[s].MIP_CAPABLE
        ]
    if candidate_qp_solvers and Qp2SymbolicQp().accepts(problem):
        solver = sorted(candidate_qp_solvers,
                        key=lambda s: slv_def.QP_SOLVERS.index(s))[0]
        solver_instance = slv_def.SOLVER_MAP_QP[solver]
        reductions += [
            CvxAttr2Constr(),
            Qp2SymbolicQp(),
            QpMatrixStuffing(), solver_instance
        ]
        return SolvingChain(reductions=reductions)

    candidate_conic_solvers = [
        s for s in slv_def.CONIC_SOLVERS if s in candidates
    ]
    if problem.is_mixed_integer():
        candidate_conic_solvers = \
            [s for s in candidate_conic_solvers if
             slv_def.SOLVER_MAP_CONIC[s].MIP_CAPABLE]
        if not candidate_conic_solvers and \
                not candidate_qp_solvers:
            raise SolverError("Problem is mixed-integer, but candidate "
                              "QP/Conic solvers (%s) are not MIP-capable." %
                              [candidate_qp_solvers, candidate_conic_solvers])
    if not candidate_conic_solvers:
        raise SolverError("Problem could not be reduced to a QP, and no "
                          "conic solvers exist among candidate solvers "
                          "(%s)." % candidates)

    # Attempt to canonicalize the problem to a cone program.
    # Our choice of solver depends upon which atoms are present in the
    # problem. The types of atoms to check for are SOC atoms, PSD atoms,
    # and exponential atoms.
    atoms = problem.atoms()
    cones = []
    if (any(atom in SOC_ATOMS for atom in atoms)
            or any(type(c) == SOC for c in problem.constraints)):
        cones.append(SOC)
    if (any(atom in EXP_ATOMS for atom in atoms)
            or any(type(c) == ExpCone for c in problem.constraints)):
        cones.append(ExpCone)
    if (any(atom in PSD_ATOMS for atom in atoms)
            or any(type(c) == PSD for c in problem.constraints)
            or any(v.is_psd() or v.is_nsd() for v in problem.variables())):
        cones.append(PSD)

    # Here, we make use of the observation that canonicalization only
    # increases the number of constraints in our problem.
    has_constr = len(cones) > 0 or len(problem.constraints) > 0

    for solver in sorted(candidate_conic_solvers,
                         key=lambda s: slv_def.CONIC_SOLVERS.index(s)):
        solver_instance = slv_def.SOLVER_MAP_CONIC[solver]
        if (all(c in solver_instance.SUPPORTED_CONSTRAINTS for c in cones)
                and (has_constr or not solver_instance.REQUIRES_CONSTR)):
            reductions += [
                Dcp2Cone(),
                CvxAttr2Constr(),
                ConeMatrixStuffing(), solver_instance
            ]
            return SolvingChain(reductions=reductions)

    raise SolverError(
        "Either candidate conic solvers (%s) do not support the "
        "cones output by the problem (%s), or there are not "
        "enough constraints in the problem." %
        (candidate_conic_solvers, ", ".join([cone.__name__
                                             for cone in cones])))
Пример #18
0
 def validate_solver(self, prob):
     if not self.suitable(prob):
         raise SolverError("The solver %s cannot solve the problem." %
                           self.name())
Пример #19
0
def construct_solving_chain(problem, candidates,
                            gp: bool = False,
                            enforce_dpp: bool = False) -> "SolvingChain":
    """Build a reduction chain from a problem to an installed solver.

    Note that if the supplied problem has 0 variables, then the solver
    parameter will be ignored.

    Parameters
    ----------
    problem : Problem
        The problem for which to build a chain.
    candidates : dict
        Dictionary of candidate solvers divided in qp_solvers
        and conic_solvers.
    gp : bool
        If True, the problem is parsed as a Disciplined Geometric Program
        instead of as a Disciplined Convex Program.
    enforce_dpp : bool, optional
        When True, a DPPError will be thrown when trying to parse a non-DPP
        problem (instead of just a warning). Defaults to False.

    Returns
    -------
    SolvingChain
        A SolvingChain that can be used to solve the problem.

    Raises
    ------
    SolverError
        Raised if no suitable solver exists among the installed solvers, or
        if the target solver is not installed.
    """
    if len(problem.variables()) == 0:
        return SolvingChain(reductions=[ConstantSolver()])
    reductions = _reductions_for_problem_class(problem, candidates, gp)

    dpp_context = 'dcp' if not gp else 'dgp'
    dpp_error_msg = (
            "You are solving a parameterized problem that is not DPP. "
            "Because the problem is not DPP, subsequent solves will not be "
            "faster than the first one. For more information, see the "
            "documentation on Discplined Parametrized Programming, at\n"
            "\thttps://www.cvxpy.org/tutorial/advanced/index.html#"
            "disciplined-parametrized-programming")
    if not problem.is_dpp(dpp_context):
        if not enforce_dpp:
            warnings.warn(dpp_error_msg)
            reductions = [EvalParams()] + reductions
        else:
            raise DPPError(dpp_error_msg)
    elif any(param.is_complex() for param in problem.parameters()):
        reductions = [EvalParams()] + reductions

    # Conclude with matrix stuffing; choose one of the following paths:
    #   (1) QpMatrixStuffing --> [a QpSolver],
    #   (2) ConeMatrixStuffing --> [a ConicSolver]
    if _solve_as_qp(problem, candidates):
        # Canonicalize as a QP
        solver = candidates['qp_solvers'][0]
        solver_instance = slv_def.SOLVER_MAP_QP[solver]
        reductions += [QpMatrixStuffing(),
                       solver_instance]
        return SolvingChain(reductions=reductions)

    # Canonicalize as a cone program
    if not candidates['conic_solvers']:
        raise SolverError("Problem could not be reduced to a QP, and no "
                          "conic solvers exist among candidate solvers "
                          "(%s)." % candidates)

    constr_types = set()
    # ^ We use constr_types to infer an incomplete list of cones that
    # the solver will need after canonicalization.
    for c in problem.constraints:
        constr_types.add(type(c))
    ex_cos = [ct for ct in constr_types if ct in EXOTIC_CONES]
    # ^ The way we populate "ex_cos" will need to change if and when
    # we have atoms that require exotic cones.
    for co in ex_cos:
        sim_cos = EXOTIC_CONES[co]  # get the set of required simple cones
        constr_types.update(sim_cos)
        constr_types.remove(co)
    # We now go over individual elementary cones support by CVXPY (
    # SOC, ExpCone, NonNeg, Zero, PSD, PowCone3D) and check if
    # they've appeared in constr_types or if the problem has an atom
    # requiring that cone.
    cones = []
    atoms = problem.atoms()
    if SOC in constr_types or any(atom in SOC_ATOMS for atom in atoms):
        cones.append(SOC)
    if ExpCone in constr_types or any(atom in EXP_ATOMS for atom in atoms):
        cones.append(ExpCone)
    if any(t in constr_types for t in [Inequality, NonPos, NonNeg]) \
            or any(atom in NONPOS_ATOMS for atom in atoms):
        cones.append(NonNeg)
    if Equality in constr_types or Zero in constr_types:
        cones.append(Zero)
    if PSD in constr_types \
            or any(atom in PSD_ATOMS for atom in atoms) \
            or any(v.is_psd() or v.is_nsd() for v in problem.variables()):
        cones.append(PSD)
    if PowCone3D in constr_types:
        # if we add in atoms that specifically use the 3D power cone
        # (rather than the ND power cone), then we'll need to check
        # for those atoms here as well.
        cones.append(PowCone3D)

    # Here, we make use of the observation that canonicalization only
    # increases the number of constraints in our problem.
    has_constr = len(cones) > 0 or len(problem.constraints) > 0

    for solver in candidates['conic_solvers']:
        solver_instance = slv_def.SOLVER_MAP_CONIC[solver]
        if (all(c in solver_instance.SUPPORTED_CONSTRAINTS for c in cones)
                and (has_constr or not solver_instance.REQUIRES_CONSTR)):
            if ex_cos:
                reductions.append(Exotic2Common())
            reductions += [ConeMatrixStuffing(), solver_instance]
            return SolvingChain(reductions=reductions)

    raise SolverError("Either candidate conic solvers (%s) do not support the "
                      "cones output by the problem (%s), or there are not "
                      "enough constraints in the problem." % (
                          candidates['conic_solvers'],
                          ", ".join([cone.__name__ for cone in cones])))
Пример #20
0
def construct_solving_chain(problem, candidates, gp=False):
    """Build a reduction chain from a problem to an installed solver.

    Note that if the supplied problem has 0 variables, then the solver
    parameter will be ignored.

    Parameters
    ----------
    problem : Problem
        The problem for which to build a chain.
    candidates : dict
        Dictionary of candidate solvers divided in qp_solvers
        and conic_solvers.
    gp : bool
        If True, the problem is parsed as a Disciplined Geometric Program
        instead of as a Disciplined Convex Program.

    Returns
    -------
    SolvingChain
        A SolvingChain that can be used to solve the problem.

    Raises
    ------
    SolverError
        Raised if no suitable solver exists among the installed solvers, or
        if the target solver is not installed.
    """
    if len(problem.variables()) == 0:
        return SolvingChain(reductions=[ConstantSolver()])
    reductions = _reductions_for_problem_class(problem, candidates, gp)

    if gp:
        # Log-log convex programs need a specialized DPP ruleset (so that
        # the corresponding DCP program ends up being DPP), which we do not yet
        # have; for now, just evaluate the parameters.
        reductions += [EvalParams()]
    elif not problem.is_dpp():
        warnings.warn(
            "You are solving a parameterized problem that is not DPP. "
            "Because the problem is not DPP, subsequent solves will not be "
            "faster than the first one. For more information, see the "
            "documentation on Discplined Parametrized Programming, at\n"
            "\thttps://www.cvxpy.org/tutorial/advanced/index.html#"
            "disciplined-parametrized-programming")
        reductions += [EvalParams()]
    elif any(param.is_complex() for param in problem.parameters()):
        reductions += [EvalParams()]

    # Conclude with matrix stuffing; choose one of the following paths:
    #   (1) QpMatrixStuffing --> [a QpSolver],
    #   (2) ConeMatrixStuffing --> [a ConicSolver]
    if _solve_as_qp(problem, candidates):
        # Canonicalize as a QP
        solver = sorted(candidates['qp_solvers'],
                        key=lambda s: slv_def.QP_SOLVERS.index(s))[0]
        solver_instance = slv_def.SOLVER_MAP_QP[solver]
        reductions += [QpMatrixStuffing(),
                       solver_instance]
        return SolvingChain(reductions=reductions)

    # Canonicalize as a cone program
    if not candidates['conic_solvers']:
        raise SolverError("Problem could not be reduced to a QP, and no "
                          "conic solvers exist among candidate solvers "
                          "(%s)." % candidates)

    # Our choice of solver depends upon which atoms are present in the
    # problem. The types of atoms to check for are SOC atoms, PSD atoms,
    # and exponential atoms.
    atoms = problem.atoms()
    cones = []
    if (any(atom in SOC_ATOMS for atom in atoms)
            or any(type(c) == SOC for c in problem.constraints)):
        cones.append(SOC)
    if (any(atom in EXP_ATOMS for atom in atoms)
            or any(type(c) == ExpCone for c in problem.constraints)):
        cones.append(ExpCone)
    if (any(atom in NONPOS_ATOMS for atom in atoms)
            or any(type(c) in [Inequality, NonPos] for c in problem.constraints)):
        cones.append(NonPos)
    if (any(type(c) in [Equality, Zero] for c in problem.constraints)):
        cones.append(Zero)
    if (any(atom in PSD_ATOMS for atom in atoms)
            or any(type(c) == PSD for c in problem.constraints)
            or any(v.is_psd() or v.is_nsd()
                   for v in problem.variables())):
        cones.append(PSD)

    # Here, we make use of the observation that canonicalization only
    # increases the number of constraints in our problem.
    has_constr = len(cones) > 0 or len(problem.constraints) > 0

    for solver in sorted(candidates['conic_solvers'],
                         key=lambda s: slv_def.CONIC_SOLVERS.index(s)):
        solver_instance = slv_def.SOLVER_MAP_CONIC[solver]
        if (all(c in solver_instance.SUPPORTED_CONSTRAINTS for c in cones)
                and (has_constr or not solver_instance.REQUIRES_CONSTR)):
            reductions += [ConeMatrixStuffing(),
                           solver_instance]
            return SolvingChain(reductions=reductions)

    raise SolverError("Either candidate conic solvers (%s) do not support the "
                      "cones output by the problem (%s), or there are not "
                      "enough constraints in the problem." % (
                          candidates['conic_solvers'],
                          ", ".join([cone.__name__ for cone in cones])))
Пример #21
0
    def _find_candidate_solvers(self, solver=None, gp=False):
        """
        Find candiate solvers for the current problem. If solver
        is not None, it checks if the specified solver is compatible
        with the problem passed.

        Parameters
        ----------
        solver : string
            The name of the solver with which to solve the problem. If no
            solver is supplied (i.e., if solver is None), then the targeted
            solver may be any of those that are installed. If the problem
            is variable-free, then this parameter is ignored.
        gp : bool
            If True, the problem is parsed as a Disciplined Geometric Program
            instead of as a Disciplined Convex Program.

        Returns
        -------
        dict
            A dictionary of compatible solvers divided in `qp_solvers`
            and `conic_solvers`.

        Raises
        ------
        SolverError
            Raised if the problem is not DCP and `gp` is False.
        DGPError
            Raised if the problem is not DGP and `gp` is True.
        """
        candidates = {'qp_solvers': [], 'conic_solvers': []}

        if solver is not None:
            if solver not in slv_def.INSTALLED_SOLVERS:
                raise SolverError("The solver %s is not installed." % solver)
            if solver in slv_def.CONIC_SOLVERS:
                candidates['conic_solvers'] += [solver]
            if solver in slv_def.QP_SOLVERS:
                candidates['qp_solvers'] += [solver]
        else:
            candidates['qp_solvers'] = [
                s for s in slv_def.INSTALLED_SOLVERS if s in slv_def.QP_SOLVERS
            ]
            candidates['conic_solvers'] = [
                s for s in slv_def.INSTALLED_SOLVERS
                if s in slv_def.CONIC_SOLVERS
            ]

        # If gp we must have only conic solvers
        if gp:
            if solver is not None and solver not in slv_def.CONIC_SOLVERS:
                raise SolverError(
                    "When `gp=True`, `solver` must be a conic solver "
                    "(received '%s'); try calling " % solver +
                    " `solve()` with `solver=cvxpy.ECOS`.")
            elif solver is None:
                candidates['qp_solvers'] = []  # No QP solvers allowed

        if self.is_mixed_integer():
            candidates['qp_solvers'] = [
                s for s in candidates['qp_solvers']
                if slv_def.SOLVER_MAP_QP[s].MIP_CAPABLE
            ]
            candidates['conic_solvers'] = [
                s for s in candidates['conic_solvers']
                if slv_def.SOLVER_MAP_CONIC[s].MIP_CAPABLE
            ]
            if not candidates['conic_solvers'] and \
                    not candidates['qp_solvers']:
                raise SolverError(
                    "Problem is mixed-integer, but candidate "
                    "QP/Conic solvers (%s) are not MIP-capable." %
                    [candidates['qp_solvers'], candidates['conic_solvers']])

        return candidates
Пример #22
0
def construct_intermediate_chain(problem, candidates, gp: bool = False):
    """
    Builds a chain that rewrites a problem into an intermediate
    representation suitable for numeric reductions.

    Parameters
    ----------
    problem : Problem
        The problem for which to build a chain.
    candidates : dict
        Dictionary of candidate solvers divided in qp_solvers
        and conic_solvers.
    gp : bool
        If True, the problem is parsed as a Disciplined Geometric Program
        instead of as a Disciplined Convex Program.

    Returns
    -------
    Chain
        A Chain that can be used to convert the problem to an intermediate form.

    Raises
    ------
    DCPError
        Raised if the problem is not DCP and `gp` is False.
    DGPError
        Raised if the problem is not DGP and `gp` is True.
    """

    reductions = []
    if len(problem.variables()) == 0:
        return Chain(reductions=reductions)
    # TODO Handle boolean constraints.
    if complex2real.accepts(problem):
        reductions += [complex2real.Complex2Real()]
    if gp:
        reductions += [Dgp2Dcp()]

    if not gp and not problem.is_dcp():
        append = build_non_disciplined_error_msg(problem, 'DCP')
        if problem.is_dgp():
            append += ("\nHowever, the problem does follow DGP rules. "
                       "Consider calling solve() with `gp=True`.")
        elif problem.is_dqcp():
            append += ("\nHowever, the problem does follow DQCP rules. "
                       "Consider calling solve() with `qcp=True`.")
        raise DCPError("Problem does not follow DCP rules. Specifically:\n" +
                       append)

    elif gp and not problem.is_dgp():
        append = build_non_disciplined_error_msg(problem, 'DGP')
        if problem.is_dcp():
            append += ("\nHowever, the problem does follow DCP rules. "
                       "Consider calling solve() with `gp=False`.")
        elif problem.is_dqcp():
            append += ("\nHowever, the problem does follow DQCP rules. "
                       "Consider calling solve() with `qcp=True`.")
        raise DGPError("Problem does not follow DGP rules." + append)

    # Dcp2Cone and Qp2SymbolicQp require problems to minimize their objectives.
    if type(problem.objective) == Maximize:
        reductions += [FlipObjective()]

    # First, attempt to canonicalize the problem to a linearly constrained QP.
    if candidates['qp_solvers'] and qp2symbolic_qp.accepts(problem):
        reductions += [CvxAttr2Constr(), Qp2SymbolicQp()]
        return Chain(reductions=reductions)

    # Canonicalize it to conic problem.
    if not candidates['conic_solvers']:
        raise SolverError("Problem could not be reduced to a QP, and no "
                          "conic solvers exist among candidate solvers "
                          "(%s)." % candidates)
    reductions += [Dcp2Cone(), CvxAttr2Constr()]
    return Chain(reductions=reductions)
Пример #23
0
def construct_solving_chain(problem, candidates):
    """Build a reduction chain from a problem to an installed solver.

    Note that if the supplied problem has 0 variables, then the solver
    parameter will be ignored.

    Parameters
    ----------
    problem : Problem
        The problem for which to build a chain.
    candidates : dict
        Dictionary of candidate solvers divided in qp_solvers
        and conic_solvers.

    Returns
    -------
    SolvingChain
        A SolvingChain that can be used to solve the problem.

    Raises
    ------
    SolverError
        Raised if no suitable solver exists among the installed solvers, or
        if the target solver is not installed.
    """
    reductions = []
    if problem.parameters():
        reductions += [EvalParams()]
    if len(problem.variables()) == 0:
        reductions += [ConstantSolver()]
        return SolvingChain(reductions=reductions)

    # Conclude the chain with one of the following:
    #   (1) QpMatrixStuffing --> [a QpSolver],
    #   (2) ConeMatrixStuffing --> [a ConicSolver]

    # First, attempt to canonicalize the problem to a linearly constrained QP.
    if candidates['qp_solvers'] and QpMatrixStuffing.accepts(problem):
        solver = sorted(candidates['qp_solvers'],
                        key=lambda s: slv_def.QP_SOLVERS.index(s))[0]
        solver_instance = slv_def.SOLVER_MAP_QP[solver]
        reductions += [QpMatrixStuffing(), solver_instance]
        return SolvingChain(reductions=reductions)

    if not candidates['conic_solvers']:
        raise SolverError("Problem could not be reduced to a QP, and no "
                          "conic solvers exist among candidate solvers "
                          "(%s)." % candidates)

    # Our choice of solver depends upon which atoms are present in the
    # problem. The types of atoms to check for are SOC atoms, PSD atoms,
    # and exponential atoms.
    atoms = problem.atoms()
    cones = []
    if (any(atom in SOC_ATOMS for atom in atoms)
            or any(type(c) == SOC for c in problem.constraints)):
        cones.append(SOC)
    if (any(atom in EXP_ATOMS for atom in atoms)
            or any(type(c) == ExpCone for c in problem.constraints)):
        cones.append(ExpCone)
    if (any(atom in PSD_ATOMS for atom in atoms)
            or any(type(c) == PSD for c in problem.constraints)
            or any(v.is_psd() or v.is_nsd() for v in problem.variables())):
        cones.append(PSD)

    # Here, we make use of the observation that canonicalization only
    # increases the number of constraints in our problem.
    has_constr = len(cones) > 0 or len(problem.constraints) > 0

    for solver in sorted(candidates['conic_solvers'],
                         key=lambda s: slv_def.CONIC_SOLVERS.index(s)):
        solver_instance = slv_def.SOLVER_MAP_CONIC[solver]
        if (all(c in solver_instance.SUPPORTED_CONSTRAINTS for c in cones)
                and (has_constr or not solver_instance.REQUIRES_CONSTR)):
            reductions += [ConeMatrixStuffing(), solver_instance]
            return SolvingChain(reductions=reductions)

    raise SolverError("Either candidate conic solvers (%s) do not support the "
                      "cones output by the problem (%s), or there are not "
                      "enough constraints in the problem." %
                      (candidates['conic_solvers'], ", ".join(
                          [cone.__name__ for cone in cones])))
Пример #24
0
def construct_solving_chain(problem, solver=None):
    """Build a reduction chain from a problem to an installed solver.

    Note that if the supplied problem has 0 variables, then the solver
    parameter will be ignored.

    Parameters
    ----------
    problem : Problem
        The problem for which to build a chain.
    solver : string
        The name of the solver with which to terminate the chain. If no solver
        is supplied (i.e., if solver is None), then the targeted solver may be
        any of those that are installed. If the problem is variable-free,
        then this parameter is ignored.

    Returns
    -------
    SolvingChain
        A SolvingChain that can be used to solve the problem.

    Raises
    ------
    DCPError
        Raised if the problem is not DCP.
    SolverError
        Raised if no suitable solver exists among the installed solvers, or
        if the target solver is not installed.
    """
    if solver is not None:
        if solver not in INSTALLED_SOLVERS:
            raise SolverError("The solver %s is not installed." % solver)
        candidates = [solver]
    else:
        candidates = INSTALLED_SOLVERS

    reductions = []
    # Evaluate parameters and short-circuit the solver if the problem
    # is constant.
    if problem.parameters():
        reductions += [EvalParams()]
    if len(problem.variables()) == 0:
        reductions += [ConstantSolver()]
        return SolvingChain(reductions=reductions)
    if Complex2Real().accepts(problem):
        reductions += [Complex2Real()]

    #  Presently, we have but two reduction chains:
    #   (1) Qp2SymbolicQp --> QpMatrixStuffing --> [a QpSolver],
    #   (2) Dcp2Cone --> ConeMatrixStuffing --> [a ConicSolver]
    # Both of these chains require that the problem is DCP.
    if not problem.is_dcp():
        raise DCPError("Problem does not follow DCP rules.")

    # Both reduction chains exclusively accept minimization problems.
    if type(problem.objective) == Maximize:
        reductions.append(FlipObjective())

    # Attempt to canonicalize the problem to a linearly constrained QP.
    candidate_qp_solvers = [s for s in QP_SOLVERS if s in candidates]
    # Consider only MIQP solvers if problem is integer
    if problem.is_mixed_integer():
        candidate_qp_solvers = \
            [s for s in candidate_qp_solvers if
             SOLVER_MAP_QP[s].MIP_CAPABLE]
    if candidate_qp_solvers and Qp2SymbolicQp().accepts(problem):
        solver = sorted(candidate_qp_solvers,
                        key=lambda s: QP_SOLVERS.index(s))[0]
        solver_instance = SOLVER_MAP_QP[solver]
        reductions += [
            CvxAttr2Constr(),
            Qp2SymbolicQp(),
            QpMatrixStuffing(), solver_instance
        ]
        return SolvingChain(reductions=reductions)

    candidate_conic_solvers = [s for s in CONIC_SOLVERS if s in candidates]
    if problem.is_mixed_integer():
        candidate_conic_solvers = \
            [s for s in candidate_conic_solvers if
             SOLVER_MAP_CONIC[s].MIP_CAPABLE]
        if not candidate_conic_solvers and \
                not candidate_qp_solvers:
            raise SolverError("Problem is mixed-integer, but candidate "
                              "QP/Conic solvers (%s) are not MIP-capable." %
                              [candidate_qp_solvers, candidate_conic_solvers])
    if not candidate_conic_solvers:
        raise SolverError("Problem could not be reduced to a QP, and no "
                          "conic solvers exist among candidate solvers "
                          "(%s)." % candidates)

    # Attempt to canonicalize the problem to a cone program.
    # Our choice of solver depends upon which atoms are present in the
    # problem. The types of atoms to check for are SOC atoms, PSD atoms,
    # and exponential atoms.
    atoms = problem.atoms()
    cones = []
    if (any(atom in SOC_ATOMS for atom in atoms)
            or any(type(c) == SOC for c in problem.constraints)):
        cones.append(SOC)
    if (any(atom in EXP_ATOMS for atom in atoms)
            or any(type(c) == ExpCone for c in problem.constraints)):
        cones.append(ExpCone)
    if (any(atom in PSD_ATOMS for atom in atoms)
            or any(type(c) == PSD for c in problem.constraints)
            or any(v.is_psd() or v.is_nsd() for v in problem.variables())):
        cones.append(PSD)

    # Here, we make use of the observation that canonicalization only
    # increases the number of constraints in our problem.
    has_constr = len(cones) > 0 or len(problem.constraints) > 0

    for solver in sorted(candidate_conic_solvers,
                         key=lambda s: CONIC_SOLVERS.index(s)):
        solver_instance = SOLVER_MAP_CONIC[solver]
        if (all(c in solver_instance.SUPPORTED_CONSTRAINTS for c in cones)
                and (has_constr or not solver_instance.REQUIRES_CONSTR)):
            reductions += [
                Dcp2Cone(),
                CvxAttr2Constr(),
                ConeMatrixStuffing(), solver_instance
            ]
            return SolvingChain(reductions=reductions)

    raise SolverError(
        "Either candidate conic solvers (%s) do not support the "
        "cones output by the problem (%s), or there are not "
        "enough constraints in the problem." %
        (candidate_conic_solvers, ", ".join([cone.__name__
                                             for cone in cones])))