Beispiel #1
0
    def _add_custom_solver_candidates(self, custom_solver: Solver):
        """
        Returns a list of candidate solvers where custom_solver is the only potential option.

        Arguments
        ---------
        custom_solver : Solver

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

        Raises
        ------
        cvxpy.error.SolverError
            Raised if the name of the custom solver conflicts with the name of some officially
            supported solver
        """
        if custom_solver.name() in SOLVERS:
            message = "Custom solvers must have a different name than the officially supported ones"
            raise(error.SolverError(message))

        candidates = {'qp_solvers': [], 'conic_solvers': []}
        if not self.is_mixed_integer() or custom_solver.MIP_CAPABLE:
            if isinstance(custom_solver, QpSolver):
                SOLVER_MAP_QP[custom_solver.name()] = custom_solver
                candidates['qp_solvers'] = [custom_solver.name()]
            elif isinstance(custom_solver, ConicSolver):
                SOLVER_MAP_CONIC[custom_solver.name()] = custom_solver
                candidates['conic_solvers'] = [custom_solver.name()]
        return candidates
Beispiel #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.

        Arguments
        _________
        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 error.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())
Beispiel #3
0
def _bisect(problem,
            solver,
            t,
            low,
            high,
            tighten_lower,
            tighten_higher,
            eps=1e-6,
            verbose=False,
            max_iters=100):
    """Bisect `problem` on the parameter `t`."""

    verbose_freq = 5
    soln = None
    for i in range(max_iters):
        assert low <= high
        if soln is not None and (high - low) <= eps:
            # the previous iteration might have been infeasible, but
            # the tigthen* functions might have narrowed the interval
            # to the optimal value in the previous iteration (hence the
            # soln is not None check)
            return soln, low, high
        query_pt = (low + high) / 2.0
        if verbose and i % verbose_freq == 0:
            print("(iteration %d) lower bound: %0.6f" % (i, low))
            print("(iteration %d) upper bound: %0.6f" % (i, high))
            print("(iteration %d) query point: %0.6f " % (i, query_pt))
        t.value = query_pt
        lowered = _lower_problem(problem)
        _solve(lowered, solver=solver)

        if _infeasible(lowered):
            if verbose and i % verbose_freq == 0:
                print("(iteration %d) query was infeasible.\n" % i)
            low = tighten_lower(query_pt)
        elif lowered.status in s.SOLUTION_PRESENT:
            if verbose and i % verbose_freq == 0:
                print("(iteration %d) query was feasible. %s)\n" %
                      (i, lowered.solution))
            soln = lowered.solution
            high = tighten_higher(query_pt)
        else:
            if verbose:
                print("Aborting; the solver failed ...\n")
            raise error.SolverError("Solver failed with status %s" %
                                    lowered.status)
    raise error.SolverError("Max iters hit during bisection.")
Beispiel #4
0
def _find_bisection_interval(problem,
                             t,
                             solver=None,
                             low=None,
                             high=None,
                             max_iters=100):
    """Finds an interval for bisection."""
    if low is None:
        low = 0 if t.is_nonneg() else -1
    if high is None:
        high = 0 if t.is_nonpos() else 1

    infeasible_low = t.is_nonneg()
    feasible_high = t.is_nonpos()
    for _ in range(max_iters):
        if not feasible_high:
            t.value = high
            lowered = _lower_problem(problem)
            _solve(lowered, solver)
            if _infeasible(lowered):
                low = high
                high *= 2
                continue
            elif lowered.status in s.SOLUTION_PRESENT:
                feasible_high = True
            else:
                raise error.SolverError("Solver failed with status %s" %
                                        lowered.status)

        if not infeasible_low:
            t.value = low
            lowered = _lower_problem(problem)
            _solve(lowered, solver=solver)
            if _infeasible(lowered):
                infeasible_low = True
            elif lowered.status in s.SOLUTION_PRESENT:
                high = low
                low *= 2
                continue
            else:
                raise error.SolverError("Solver failed with status %s" %
                                        lowered.status)

        if infeasible_low and feasible_high:
            return low, high

    raise error.SolverError("Unable to find suitable interval for bisection.")
Beispiel #5
0
    def backward(self):
        """Compute the gradient of a solution with respect to Parameters.

        This method differentiates through the solution map of the problem,
        obtaining the gradient of a solution with respect to the Parameters.
        In other words, it calculates the sensitivities of the Parameters
        with respect to perturbations in the optimal Variable values. This
        can be useful for integrating CVXPY into automatic differentation
        toolkits.

        ``backward()`` populates the ``gradient`` attribute of each Parameter
        in the problem as a side-effect. It can only be called after calling
        ``solve()`` with ``requires_grad=True``.

        Below is a simple example:

        ::

            import cvxpy as cp
            import numpy as np

            p = cp.Parameter()
            x = cp.Variable()
            quadratic = cp.square(x - 2 * p)
            problem = cp.Problem(cp.Minimize(quadratic), [x >= 0])
            p.value = 3.0
            problem.solve(requires_grad=True, eps=1e-10)
            # backward() populates the gradient attribute of the parameters
            problem.backward()
            # Because x* = 2 * p, dx*/dp = 2
            np.testing.assert_allclose(p.gradient, 2.0)

        In the above example, the gradient could easily be computed by hand.
        The ``backward()`` is useful because for almost all problems, the
        gradient cannot be computed analytically.

        This method can be used to differentiate through any DCP or DGP
        problem, as long as the problem is DPP compliant (i.e.,
        ``problem.is_dcp(dpp=True)`` or ``problem.is_dgp(dpp=True)`` evaluates to
        ``True``).

        This method uses the chain rule to evaluate the gradients of a
        scalar-valued function of the Variables with respect to the Parameters.
        For example, let x be a variable and p a Parameter; x and p might be
        scalars, vectors, or matrices. Let f be a scalar-valued function, with
        z = f(x). Then this method computes dz/dp = (dz/dx) (dx/p). dz/dx
        is chosen as the all-ones vector by default, corresponding to
        choosing f to be the sum function. You can specify a custom value for
        dz/dx by setting the ``gradient`` attribute on your variables. For example,

        ::

            import cvxpy as cp
            import numpy as np


            b = cp.Parameter()
            x = cp.Variable()
            quadratic = cp.square(x - 2 * b)
            problem = cp.Problem(cp.Minimize(quadratic), [x >= 0])
            b.value = 3.
            problem.solve(requires_grad=True, eps=1e-10)
            x.gradient = 4.
            problem.backward()
            # dz/dp = dz/dx dx/dp = 4. * 2. == 8.
            np.testing.assert_allclose(b.gradient, 8.)

        The ``gradient`` attribute on a variable can also be interpreted as a
        perturbation to its optimal value.

        Raises
        ------
            ValueError
                if solve was not called with ``requires_grad=True``
            SolverError
                if the problem is infeasible or unbounded
        """
        if s.DIFFCP not in self._solver_cache:
            raise ValueError("backward can only be called after calling "
                             "solve with `requires_grad=True`")
        elif self.status not in s.SOLUTION_PRESENT:
            raise error.SolverError("Backpropagating through "
                                    "infeasible/unbounded problems is not "
                                    "yet supported. Please file an issue on "
                                    "Github if you need this feature.")

        # TODO(akshayka): Backpropagate through dual variables as well.
        backward_cache = self._solver_cache[s.DIFFCP]
        DT = backward_cache["DT"]
        zeros = np.zeros(backward_cache["s"].shape)
        del_vars = {}

        gp = self._cache.gp()
        for variable in self.variables():
            if variable.gradient is None:
                del_vars[variable.id] = np.ones(variable.shape)
            else:
                del_vars[variable.id] = np.asarray(variable.gradient,
                                                   dtype=np.float64)
            if gp:
                # x_gp = exp(x_cone_program),
                # dx_gp/d x_cone_program = exp(x_cone_program) = x_gp
                del_vars[variable.id] *= variable.value

        dx = self._cache.param_prog.split_adjoint(del_vars)
        start = time.time()
        dA, db, dc = DT(dx, zeros, zeros)
        end = time.time()
        backward_cache['DT_TIME'] = end - start
        dparams = self._cache.param_prog.apply_param_jac(dc, -dA, db)

        if not gp:
            for param in self.parameters():
                param.gradient = dparams[param.id]
        else:
            dgp2dcp = self._cache.solving_chain.get(Dgp2Dcp)
            old_params_to_new_params = dgp2dcp.canon_methods._parameters
            for param in self.parameters():
                # Note: if param is an exponent in a power or gmatmul atom,
                # then the parameter passes through unchanged to the DCP
                # program; if the param is also used elsewhere (not as an
                # exponent), then param will also be in
                # old_params_to_new_params. Therefore, param.gradient =
                # dparams[param.id] (or 0) + 1/param*dparams[new_param.id]
                #
                # Note that param.id is in dparams if and only if
                # param was used as an exponent (because this means that
                # the parameter entered the DCP problem unchanged.)
                grad = 0.0 if param.id not in dparams else dparams[param.id]
                if param in old_params_to_new_params:
                    new_param = old_params_to_new_params[param]
                    # new_param.value == log(param), apply chain rule
                    grad += (1.0 / param.value) * dparams[new_param.id]
                param.gradient = grad
Beispiel #6
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.

        Arguments
        ---------
        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
        ------
        cvxpy.error.SolverError
            Raised if the problem is not DCP and `gp` is False.
        cvxpy.error.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 error.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 error.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():
            if len(slv_def.INSTALLED_MI_SOLVERS) == 0:
                msg = """

                    CVXPY needs additional software (a `mixed-integer solver`) to handle this model.
                    The web documentation
                        https://www.cvxpy.org/tutorial/advanced/index.html#mixed-integer-programs
                    reviews open-source and commercial options for mixed-integer solvers.

                    Quick fix: if you install the python package CVXOPT (pip install cvxopt),
                    then CVXPY can use the open-source mixed-integer solver `GLPK`.
                """
                raise error.SolverError(msg)
            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 error.SolverError(
                    "Problem is mixed-integer, but candidate "
                    "QP/Conic solvers (%s) are not MIP-capable." %
                    (candidates['qp_solvers'] + candidates['conic_solvers']))

        return candidates
Beispiel #7
0
    def backward(self):
        """Compute the gradient of a solution with respect to parameters.

        This method differentiates through the solution map of the problem,
        to obtain the gradient of a solution with respect to the parameters.
        In other words, it calculates the sensitivities of the parameters
        with respect to perturbations in the optimal variable values.

        .backward() populates the .gradient attribute of each parameter as a
        side-effect. It can only be called after calling .solve() with
        `requires_grad=True`.

        Below is a simple example:

        ::

            import cvxpy as cp
            import numpy as np

            p = cp.Parameter()
            x = cp.Variable()
            quadratic = cp.square(x - 2 * p)
            problem = cp.Problem(cp.Minimize(quadratic), [x >= 0])
            p.value = 3.0
            problem.solve(requires_grad=True, eps=1e-10)
            # .backward() populates the .gradient attribute of the parameters
            problem.backward()
            # Because x* = 2 * p, dx*/dp = 2
            np.testing.assert_allclose(p.gradient, 2.0)

        In the above example, the gradient could easily be computed by hand;
        however, .backward() can be used to differentiate through any DCP
        program (that is also DPP-compliant).

        This method uses the chain rule to evaluate the gradients of a
        scalar-valued function of the variables with respect to the parameters.
        For example, let x be a variable and p a parameter; x and p might be
        scalars, vectors, or matrices. Let f be a scalar-valued function, with
        z = f(x). Then this method computes dz/dp = (dz/dx) (dx/p). dz/dx
        is chosen to be the all ones vector by default, corresponding to
        choosing f to be the sum function. You can specify a custom value for
        dz/dx by setting the .gradient attribute on your variables. For example,

        ::

            import cvxpy as cp
            import numpy as np


            b = cp.Parameter()
            x = cp.Variable()
            quadratic = cp.square(x - 2 * b)
            problem = cp.Problem(cp.Minimize(quadratic), [x >= 0])
            b.value = 3.
            problem.solve(requires_grad=True, eps=1e-10)
            x.gradient = 4.
            problem.backward()
            # dz/dp = dz/dx dx/dp = 4. * 2. == 8.
            np.testing.assert_allclose(b.gradient, 8.)

        The .gradient attribute on a variable can also be interpreted as a
        perturbation to its optimal value.

        Raises
        ------
            ValueError
                if solve was not called with `requires_grad=True`
            SolverError
                if the problem is infeasible or unbounded
        """
        if s.DIFFCP not in self._solver_cache:
            raise ValueError("backward can only be called after calling "
                             "solve with `requires_grad=True`")
        elif self.status not in s.SOLUTION_PRESENT:
            raise error.SolverError("Backpropagating through "
                                    "infeasible/unbounded problems is not "
                                    "yet supported. Please file an issue on "
                                    "Github if you need this feature.")

        # TODO(akshayka): Backpropagate through dual variables as well.
        backward_cache = self._solver_cache[s.DIFFCP]
        DT = backward_cache["DT"]
        zeros = np.zeros(backward_cache["s"].shape)
        del_vars = {}
        for variable in self.variables():
            if variable.gradient is None:
                del_vars[variable.id] = np.ones(variable.shape)
            else:
                del_vars[variable.id] = np.asarray(variable.gradient,
                                                   dtype=np.float64)
        dx = self._cache.param_cone_prog.split_adjoint(del_vars)
        dA, db, dc = DT(dx, zeros, zeros)
        dparams = self._cache.param_cone_prog.apply_param_jac(dc, -dA, db)
        for parameter in self.parameters():
            parameter.gradient = dparams[parameter.id]
Beispiel #8
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
        ------
        cvxpy.error.SolverError
            Raised if the problem is not DCP and `gp` is False.
        cvxpy.error.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 error.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 error.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 error.SolverError(
                    "Problem is mixed-integer, but candidate "
                    "QP/Conic solvers (%s) are not MIP-capable." %
                    [candidates['qp_solvers'], candidates['conic_solvers']])

        return candidates
Beispiel #9
0
    def _find_candidate_solvers(self,
                                solver=None,
                                gp=False):
        """
        Find candidate solvers for the current problem. If solver
        is not None, it checks if the specified solver is compatible
        with the problem passed.

        Arguments
        ---------
        solver : Union[string, Solver, None]
            The name of the solver with which to solve the problem or an
            instance of a custom solver. 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
        ------
        cvxpy.error.SolverError
            Raised if the problem is not DCP and `gp` is False.
        cvxpy.error.DGPError
            Raised if the problem is not DGP and `gp` is True.
        """
        candidates = {'qp_solvers': [],
                      'conic_solvers': []}
        if isinstance(solver, Solver):
            return self._add_custom_solver_candidates(solver)

        if solver is not None:
            if solver not in slv_def.INSTALLED_SOLVERS:
                raise error.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'] = []
            # ECOS_BB can only be called explicitly.
            for slv in slv_def.INSTALLED_SOLVERS:
                if slv in slv_def.CONIC_SOLVERS and slv != s.ECOS_BB:
                    candidates['conic_solvers'].append(slv)

        # If gp we must have only conic solvers
        if gp:
            if solver is not None and solver not in slv_def.CONIC_SOLVERS:
                raise error.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():
            # ECOS_BB must be called explicitly.
            if slv_def.INSTALLED_MI_SOLVERS == [s.ECOS_BB] and solver != s.ECOS_BB:
                msg = """

                    You need a mixed-integer solver for this model. Refer to the documentation
                        https://www.cvxpy.org/tutorial/advanced/index.html#mixed-integer-programs
                    for discussion on this topic.

                    Quick fix 1: if you install the python package CVXOPT (pip install cvxopt),
                    then CVXPY can use the open-source mixed-integer solver `GLPK`.

                    Quick fix 2: you can explicitly specify solver='ECOS_BB'. This may result
                    in incorrect solutions and is not recommended.
                """
                raise error.SolverError(msg)
            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 error.SolverError(
                    "Problem is mixed-integer, but candidate "
                    "QP/Conic solvers (%s) are not MIP-capable." %
                    (candidates['qp_solvers'] +
                     candidates['conic_solvers']))

        return candidates