Source code for pyproximal.proximal.Quadratic

from typing import TYPE_CHECKING, Optional

import numpy as np
from pylops import Identity, MatrixMult
from pylops.utils.typing import NDArray
from scipy.sparse.linalg import lsqr

from pyproximal.ProxOperator import ProxOperator, _check_tau

if TYPE_CHECKING:
    from pylops.linearoperator import LinearOperator


[docs]class Quadratic(ProxOperator): r"""Quadratic function proximal operator. Proximal operator for a quadratic function: :math:`f(\mathbf{x}) = \frac{1}{2} \mathbf{x}^T \mathbf{Op} \mathbf{x} + \mathbf{b}^T \mathbf{x} + c`. Parameters ---------- Op : :obj:`pylops.LinearOperator`, optional Linear operator (must be square) b : :obj:`np.ndarray`, optional Vector c : :obj:`float`, optional Scalar niter : :obj:`int`, optional Number of iterations of iterative scheme used to compute the proximal x0 : :obj:`np.ndarray`, optional Initial vector warm : :obj:`bool`, optional Warm start (``True``) or not (``False``). Uses estimate from previous call of ``prox`` method. Raises ------ ValueError If ``Op`` is not square Notes ----- The Quadratic proximal operator is defined as: .. math:: \prox_{\tau f}(\mathbf{x}) = \left(\mathbf{I} + \tau \mathbf{Op} \right)^{-1} \left(\mathbf{x} - \tau \mathbf{b}\right) when both ``Op`` and ``b`` are provided. This formula shows that the proximal operator requires the solution of an inverse problem. If the operator ``Op`` is of kind ``explicit=True``, we can solve this problem directly. On the other hand if ``Op`` is of kind ``explicit=False``, an iterative solver is employed. In this case it is possible to provide a warm start via the ``x0`` input parameter. When only ``b`` is provided, the proximal operator reduces to: .. math:: \prox_{\mathbf{b}^T \mathbf{x} + c}(\mathbf{x}) = \mathbf{x} - \tau \mathbf{b} Finally if also ``b`` is not provided, the proximal operator of a constant function simply becomes :math:`\prox_c(\mathbf{x}) = \mathbf{x}` """ def __init__( self, Op: Optional["LinearOperator"] = None, b: Optional[NDArray] = None, c: float = 0.0, niter: int = 10, x0: Optional[NDArray] = None, warm: bool = True, ) -> None: if Op is not None: if Op.shape[0] != Op.shape[1]: raise ValueError("Op must be square") super().__init__(Op, True) self.b = b if self.Op is not None and self.b is None: self.b = np.zeros(self.Op.shape[1], dtype=self.Op.dtype) self.c = c self.niter = niter self.x0 = x0 self.warm = warm def __call__(self, x: NDArray) -> float: if self.Op is not None and self.b is not None: f = np.dot(x, self.Op * x) / 2.0 + np.dot(self.b, x) + self.c elif self.b is not None: f = np.dot(self.b, x) + self.c else: f = self.c return float(f) @_check_tau def prox(self, x: NDArray, tau: float) -> NDArray: if self.Op is not None and self.b is not None: y = x - tau * self.b if self.Op.explicit: Op1 = MatrixMult(np.eye(self.Op.shape[0]) + tau * self.Op.A) x = Op1.div(y) else: Op1 = Identity(self.Op.shape[0], dtype=self.Op.dtype) + tau * self.Op.A x = lsqr(Op1, y, iter_lim=self.niter, x0=self.x0)[0] if self.warm: self.x0 = x elif self.b is not None: x = x - tau * self.b return x def grad(self, x: NDArray) -> NDArray: """Compute gradient Parameters ---------- x : :obj:`np.ndarray` Vector Returns ------- g : :obj:`np.ndarray` Gradient vector """ if self.Op is not None and self.b is not None: g = self.Op.matvec(x) / 2.0 + x elif self.b is not None: g = x else: g = 0.0 return g