Example #1
0
    def todense(self, backend='numpy'):
        r"""Return dense matrix.

        The operator is converted into its dense matrix equivalent. In order
        to do so, square or tall operators are applied to an identity matrix
        whose number of rows and columns is equivalent to the number of
        columns of the operator. Conversely, for skinny operators, the
        transpose operator is applied to an identity matrix
        whose number of rows and columns is equivalent to the number of
        rows of the operator and the resulting matrix is transposed
        (and complex conjugated).

        Note that this operation may be costly for operators with large number
        of rows and columns and it should be used mostly as a way to inspect
        the structure of the matricial equivalent of the operator.

        Parameters
        ----------
        backend : :obj:`str`, optional
            Backend used to densify matrix (``numpy`` or ``cupy``). Note that
            this must be consistent with how the operator has been created.

        Returns
        -------
        matrix : :obj:`numpy.ndarray` or :obj:`cupy.ndarray`
            Dense matrix.

        """
        ncp = get_module(backend)

        # Wrap self into a LinearOperator. This is done for cases where self
        # is a _SumLinearOperator or _ProductLinearOperator, so that it regains
        # the dense method
        Op = aslinearoperator(self)

        # Create identity matrix
        shapemin = min(Op.shape)
        if shapemin <= 1e3:
            # use numpy for small matrices (faster but heavier on memory)
            identity = ncp.eye(shapemin, dtype=self.dtype)
        else:
            # use scipy for small matrices (slower but lighter on memory)
            identity = get_sparse_eye(ncp.ones(1))(shapemin,
                                                   dtype=self.dtype).tocsc()

        # Apply operator
        if Op.shape[1] == shapemin:
            matrix = Op.matmat(identity)
        else:
            matrix = np.conj(Op.rmatmat(identity)).T
        return matrix
Example #2
0
def dottest(Op,
            nr,
            nc,
            tol=1e-6,
            complexflag=0,
            raiseerror=True,
            verb=False,
            backend='numpy'):
    r"""Dot test.

    Generate random vectors :math:`\mathbf{u}` and :math:`\mathbf{v}`
    and perform dot-test to verify the validity of forward and adjoint
    operators. This test can help to detect errors in the operator
    implementation.

    Parameters
    ----------
    Op : :obj:`pylops.LinearOperator`
        Linear operator to test.
    nr : :obj:`int`
        Number of rows of operator (i.e., elements in data)
    nc : :obj:`int`
        Number of columns of operator (i.e., elements in model)
    tol : :obj:`float`, optional
        Dottest tolerance
    complexflag : :obj:`bool`, optional
        generate random vectors with real (0) or complex numbers
        (1: only model, 2: only data, 3:both)
    raiseerror : :obj:`bool`, optional
        Raise error or simply return ``False`` when dottest fails
    verb : :obj:`bool`, optional
        Verbosity
    backend : :obj:`str`, optional
        Backend used for dot test computations (``numpy`` or ``cupy``). This
        parameter will be used to choose how to create the random vectors.

    Raises
    ------
    ValueError
        If dot-test is not verified within chosen tolerance.

    Notes
    -----
    A dot-test is mathematical tool used in the development of numerical
    linear operators.

    More specifically, a correct implementation of forward and adjoint for
    a linear operator should verify the following *equality*
    within a numerical tolerance:

    .. math::
        (\mathbf{Op}*\mathbf{u})^H*\mathbf{v} =
        \mathbf{u}^H*(\mathbf{Op}^H*\mathbf{v})

    """
    ncp = get_module(backend)

    if complexflag in (0, 2):
        u = ncp.random.randn(nc)
    else:
        u = ncp.random.randn(nc) + 1j * ncp.random.randn(nc)

    if complexflag in (0, 1):
        v = ncp.random.randn(nr)
    else:
        v = ncp.random.randn(nr) + 1j * ncp.random.randn(nr)

    y = Op.matvec(u)  # Op * u
    x = Op.rmatvec(v)  # Op'* v

    if complexflag == 0:
        yy = ncp.dot(y, v)  # (Op  * u)' * v
        xx = ncp.dot(u, x)  # u' * (Op' * v)
    else:
        yy = ncp.vdot(y, v)  # (Op  * u)' * v
        xx = ncp.vdot(u, x)  # u' * (Op' * v)

    # convert back to numpy (in case cupy arrays where used), make into a numpy
    # array and extract the first element. This is ugly but allows to handle
    # complex numbers in subsequent prints also when using cupy arrays.
    xx, yy = np.array([to_numpy(xx)])[0], np.array([to_numpy(yy)])[0]

    # evaluate if dot test is passed
    if complexflag == 0:
        if np.abs((yy - xx) / ((yy + xx + 1e-15) / 2)) < tol:
            if verb:
                print('Dot test passed, v^T(Opu)=%f - u^T(Op^Tv)=%f' %
                      (yy, xx))
            return True
        else:
            if raiseerror:
                raise ValueError(
                    'Dot test failed, v^T(Opu)=%f - u^T(Op^Tv)=%f' % (yy, xx))
            if verb:
                print('Dot test failed, v^T(Opu)=%f - u^T(Op^Tv)=%f' %
                      (yy, xx))
            return False
    else:
        checkreal = np.abs((np.real(yy) - np.real(xx)) /
                           ((np.real(yy) + np.real(xx) + 1e-15) / 2)) < tol
        checkimag = np.abs((np.real(yy) - np.real(xx)) /
                           ((np.real(yy) + np.real(xx) + 1e-15) / 2)) < tol
        if checkreal and checkimag:
            if verb:
                print('Dot test passed, v^T(Opu)=%f%+fi - u^T(Op^Tv)=%f%+fi' %
                      (yy.real, yy.imag, xx.real, xx.imag))
            return True
        else:
            if raiseerror:
                raise ValueError('Dot test failed, v^H(Opu)=%f%+fi '
                                 '- u^H(Op^Hv)=%f%+fi' %
                                 (yy.real, yy.imag, xx.real, xx.imag))
            if verb:
                print('Dot test failed, v^H(Opu)=%f%+fi - u^H(Op^Hv)=%f%+fi' %
                      (yy.real, yy.imag, xx.real, xx.imag))
            return False
Example #3
0
def power_iteration(Op, niter=10, tol=1e-5, dtype="float32", backend="numpy"):
    """Power iteration algorithm.

    Power iteration algorithm, used to compute the largest eigenvector and
    corresponding eigenvalue. Note that for complex numbers, the eigenvalue
    with largest module is found.

    This implementation closely follow that of
    https://en.wikipedia.org/wiki/Power_iteration.

    Parameters
    ----------
    Op : :obj:`pylops.LinearOperator`
        Square operator
    niter : :obj:`int`, optional
        Number of iterations
    tol : :obj:`float`, optional
        Update tolerance
    dtype : :obj:`str`, optional
        Type of elements in input array.
    backend : :obj:`str`, optional
        Backend to use (`numpy` or `cupy`)

    Returns
    -------
    maxeig : :obj:`int`
        Largest eigenvalue
    b_k : :obj:`np.ndarray` or :obj:`cp.ndarray`
        Largest eigenvector
    iiter : :obj:`int`
        Effective number of iterations

    """
    ncp = get_module(backend)

    # Identify if operator is complex
    if np.issubdtype(dtype, np.complexfloating):
        cmpx = 1j
    else:
        cmpx = 0

    # Choose a random vector to decrease the chance that vector
    # is orthogonal to the eigenvector
    b_k = ncp.random.rand(Op.shape[1]).astype(dtype) + cmpx * ncp.random.rand(
        Op.shape[1]
    ).astype(dtype)
    b_k = b_k / ncp.linalg.norm(b_k)

    niter = 10 if niter is None else niter
    maxeig_old = 0.0
    for iiter in range(niter):
        # compute largest eigenvector
        b1_k = Op.matvec(b_k)

        # compute largest eigevalue
        maxeig = ncp.vdot(b_k, b1_k)

        # renormalize the vector
        b_k = b1_k / ncp.linalg.norm(b1_k)

        if ncp.abs(maxeig - maxeig_old) < tol * maxeig_old:
            break
        maxeig_old = maxeig

    return maxeig, b_k, iiter + 1
Example #4
0
    def trace(
        self,
        neval=None,
        method=None,
        backend="numpy",
        **kwargs_trace,
    ):
        r"""Trace of linear operator.

        Returns the trace (or its estimate) of the linear operator.

        Parameters
        ----------
        neval : :obj:`int`, optional
            Maximum number of matrix-vector products compute. Default depends
            ``method``.
        method : :obj:`str`, optional
            Should be one of the following:

                - **explicit**: If the operator is not explicit, will convert to
                  dense first.
                - **hutchinson**: see :obj:`pylops.utils.trace_hutchinson`
                - **hutch++**: see :obj:`pylops.utils.trace_hutchpp`
                - **na-hutch++**: see :obj:`pylops.utils.trace_nahutchpp`

            Defaults to 'explicit' for explicit operators, and 'Hutch++' for
            the rest.
        backend : :obj:`str`, optional
            Backend used to densify matrix (``numpy`` or ``cupy``). Note that
            this must be consistent with how the operator has been created.
        **kwargs_trace
            Arbitrary keyword arguments passed to
            :obj:`pylops.utils.trace_hutchinson`,
            :obj:`pylops.utils.trace_hutchpp`, or
            :obj:`pylops.utils.trace_nahutchpp`

        Returns
        -------
        trace : :obj:`self.dtype`
            Operator trace.

        Raises
        -------
        ValueError
             If the operator has rectangular shape (``shape[0] != shape[1]``)

        NotImplementedError
            If the ``method`` is not one of the available methods.
        """
        if self.shape[0] != self.shape[1]:
            raise ValueError("operator is not square.")

        ncp = get_module(backend)

        if method is None:
            method = "explicit" if self.explicit else "hutch++"

        method_l = method.lower()
        if method_l == "explicit":
            A = self.A if self.explicit else self.todense(backend=backend)
            return ncp.trace(A)
        elif method_l == "hutchinson":
            return trace_hutchinson(self,
                                    neval=neval,
                                    backend=backend,
                                    **kwargs_trace)
        elif method_l == "hutch++":
            return trace_hutchpp(self,
                                 neval=neval,
                                 backend=backend,
                                 **kwargs_trace)
        elif method_l == "na-hutch++":
            return trace_nahutchpp(self,
                                   neval=neval,
                                   backend=backend,
                                   **kwargs_trace)
        else:
            raise NotImplementedError(f"method {method} not available.")
Example #5
0
def _obliquity2D(
    nt,
    nr,
    dt,
    dr,
    rho,
    vel,
    nffts,
    critical=100.0,
    ntaper=10,
    composition=True,
    backend="numpy",
    dtype="complex128",
):
    r"""2D Obliquity operator and FFT operator

    Parameters
    ----------
    nt : :obj:`int`
        Number of samples along the time axis
    nr : :obj:`int`
        Number of samples along the receiver axis
    dt : :obj:`float`
        Sampling along the time axis
    dr : :obj:`float`
        Sampling along the receiver array
    rho : :obj:`float`
        Density along the receiver array (must be constant)
    vel : :obj:`float`
        Velocity along the receiver array (must be constant)
    nffts : :obj:`tuple`, optional
        Number of samples along the wavenumber and frequency axes
    critical : :obj:`float`, optional
        Percentage of angles to retain in obliquity factor. For example, if
        ``critical=100`` only angles below the critical angle
        :math:`|k_x| < \frac{f(k_x)}{vel}` will be retained
    ntaper : :obj:`float`, optional
        Number of samples of taper applied to obliquity factor around critical
        angle
    composition : :obj:`bool`, optional
        Create obliquity factor for composition (``True``) or
        decomposition (``False``)
    backend : :obj:`str`, optional
        Backend used for creation of obliquity factor operator
        (``numpy`` or ``cupy``)
    dtype : :obj:`str`, optional
        Type of elements in input array.

    Returns
    -------
    FFTop : :obj:`pylops.LinearOperator`
        FFT operator
    OBLop : :obj:`pylops.LinearOperator`
        Obliquity factor operator

    """
    # create Fourier operator
    FFTop = FFT2D(dims=[nr, nt], nffts=nffts, sampling=[dr, dt], dtype=dtype)

    # create obliquity operator
    [Kx, F] = np.meshgrid(FFTop.f1, FFTop.f2, indexing="ij")
    k = F / vel
    Kz = np.sqrt((k**2 - Kx**2).astype(dtype))
    Kz[np.isnan(Kz)] = 0

    if composition:
        OBL = Kz / (rho * np.abs(F))
        OBL[F == 0] = 0
    else:
        OBL = rho * (np.abs(F) / Kz)
        OBL[Kz == 0] = 0

    # cut off and taper
    OBL = _filter_obliquity(OBL, F, Kx, vel, critical, ntaper)
    OBL = get_module(backend).asarray(OBL)
    OBLop = Diagonal(OBL.ravel(), dtype=dtype)
    return FFTop, OBLop
Example #6
0
def trace_hutchinson(Op,
                     neval=None,
                     batch_size=None,
                     sampler="rademacher",
                     backend="numpy"):
    r"""Trace of linear operator using the Hutchinson method.

    Returns an estimate of the trace of a linear operator using the Hutchinson
    method [1]_.

    Parameters
    ----------
    neval : :obj:`int`, optional
        Maximum number of matrix-vector products compute. Defaults to 10%
        of ``shape[1]``.
    batch_size : :obj:`int`, optional
        Vectorize computations by sampling sketching matrices instead of
        vectors. Set this value to as high as permitted by memory, but there is
        no guarantee of speedup. Coerced to never exceed ``neval``. When using
        "unitvector" as sampler, is coerced to not exceed ``shape[1]``.
        Defaults to 100 or ``neval``.
    sampler : :obj:`str`, optional
        Sample sketching matrices from the following distributions:

            - "gaussian": Mean zero, unit variance Gaussian.
            - "rayleigh": Sample from mean zero, unit variance Gaussian and
              normalize the columns.
            - "rademacher": Random sign.
            - "unitvector": Samples from the unit vectors :math:`\mathrm{e}_i`
              without replacement.

    backend : :obj:`str`, optional
        Backend used to densify matrix (``numpy`` or ``cupy``). Note that
        this must be consistent with how the operator has been created.

    Returns
    -------
    trace : :obj:`self.dtype`
        Operator trace.

    Raises
    -------
    ValueError
        If ``neval`` is smaller than 3.

    NotImplementedError
        If the ``sampler`` is not one of the available samplers.

    Notes
    -----
    Let :math:`m` = ``shape[1]`` and :math:`k` = ``neval``. This algorithm
    estimates the trace via

    .. math::
        \frac{1}{k}\sum\limits_{i=1}^k \mathbf{z}_i^T\,\mathbf{Op}\,\mathbf{z}_i

    where vectors :math:`\mathbf{z}_i` are sampled according to the sampling
    function. See [2]_ for a description of the variance and
    :math:`\epsilon`-approximation of different samplers.

    Prefer the Rademacher sampler if the goal is to minimize variance, but the
    Gaussian for a better probability of approximating the correct value. Use
    the Unit Vector approach if you are sampling a large number of ``neval``
    (compared to ``shape[1]``), especially if the operator is highly-structured.

    .. [1] Hutchinson, M. F. (1990). *A stochastic estimator of the trace of
           the influence matrix for laplacian smoothing splines*.
           Communications in Statistics - Simulation and Computation, 19(2),
           433–450.
    .. [2] Avron, H., and Toledo, S. (2011). *Randomized algorithms for
           estimating the trace of an implicit symmetric positive semi-definite
           matrix*. Journal of the ACM, 58(2), 1–34.
    """
    ncp = get_module(backend)
    m = Op.shape[1]
    neval = int(numpy.round(m * 0.1)) if neval is None else neval
    batch_size = min(neval, 100 if batch_size is None else batch_size)

    n_missing = neval - batch_size * (neval // batch_size)
    batch_range = chain(
        (batch_size for _ in range(0, neval - n_missing, batch_size)),
        (n_missing for _ in range(int(n_missing != 0))),
    )

    trace = ncp.zeros(1, dtype=Op.dtype)

    if sampler == "unitvector":
        remaining_vectors = list(range(m))
        n_total = 0
        while remaining_vectors:
            batch = min(batch_size, len(remaining_vectors))
            z = ncp.zeros((m, batch), dtype=Op.dtype)
            z_idx = ncp.random.choice(remaining_vectors, batch, replace=False)
            for i, idx in enumerate(z_idx):
                z[idx, i] = 1.0
                remaining_vectors.remove(idx)
            trace += ncp.trace((z.T @ (Op @ z)))
            n_total += batch
        trace *= m / n_total
        return trace[0]

    if sampler not in _SAMPLERS:
        raise NotImplementedError(f"sampler {sampler} not available.")

    sampler_fun = _SAMPLERS[sampler]
    for batch in batch_range:
        z = sampler_fun(m, batch, backend_module=ncp).astype(Op.dtype)
        trace += ncp.trace((z.T @ (Op @ z)))
    trace /= neval
    return trace[0]
Example #7
0
def trace_nahutchpp(
    Op,
    neval=None,
    sampler="rademacher",
    c1=1.0 / 6.0,
    c2=1.0 / 3.0,
    backend="numpy",
):
    r"""Trace of linear operator using the NA-Hutch++ method.

    Returns an estimate of the trace of a linear operator using the
    Non-Adaptive variant of Hutch++ method [1]_.

    Parameters
    ----------
    neval : :obj:`int`, optional
        Maximum number of matrix-vector products compute. Defaults to 10%
        of ``shape[1]``.
    sampler : :obj:`str`, optional
        Sample sketching matrices from the following distributions:

            - "gaussian": Mean zero, unit variance Gaussian.
            - "rayleigh": Sample from mean zero, unit variance Gaussian and
              normalize the columns.
            - "rademacher": Random sign.

    c1 : :obj:`float`, optional
        Fraction of ``neval`` for sketching matrix :math:`\mathbf{S}`.
    c2 : :obj:`float`, optional
        Fraction of ``neval`` for sketching matrix :math:`\mathbf{R}`. Must be
        larger than ``c2``, ideally by a factor of at least 2.
    backend : :obj:`str`, optional
        Backend used to densify matrix (``numpy`` or ``cupy``). Note that
        this must be consistent with how the operator has been created.

    Returns
    -------
    trace : :obj:`self.dtype`
        Operator trace.

    Raises
    -------
    ValueError
        If ``neval`` not large enough to accomodate ``c1`` and ``c2``.

    NotImplementedError
        If the ``sampler`` is not one of the available samplers.

    Notes
    -----
    This function follows Algorithm 2 of [1]_. Let :math:`m` = ``shape[1]``
    and :math:`k` = ``neval``.

        1. Fix constants :math:`c_1`, :math:`c_2`, :math:`c_3` such that
           :math:`c_1 < c_2` and :math:`c_1 + c_2 + c_3 = 1`.
        2. Sample sketching matrices
           :math:`\mathbf{S} \in \mathbb{R}^{m \times c_1 k}`,
           :math:`\mathbf{R} \in \mathbb{R}^{m \times c_2 k}`,
           and
           :math:`\mathbf{G} \in \mathbb{R}^{m \times c_3 k}`
           from sub-Gaussian distributions.
        3. Compute :math:`\mathbf{Z} = \mathbf{Op}\,\mathbf{R}`,
           :math:`\mathbf{W} = \mathbf{Op}\,\mathbf{S}`, and
           :math:`\mathbf{Y} = (\mathbf{S}^T \mathbf{Z})^+`, where :math:`+`
           denotes the Moore–Penrose inverse.
        4. Return :math:`\operatorname{tr}(\mathbf{Y} \mathbf{W}^T \mathbf{Z}) + \frac{1}{c_3 k} \left[ \operatorname{tr}(\mathbf{G}^T\,\mathbf{Op}\,\mathbf{G}) - \operatorname{tr}(\mathbf{G}^T\mathbf{Z}\mathbf{Y}\mathbf{W}^T\mathbf{G})\right]`

    The default values for :math:`c_1` and :math:`c_2` are set to :math:`1/6`
    and :math:`1/3`, respectively, but [1]_ suggests :math:`1/4` and :math:`1/2`.

    Use the Rademacher sampler unless you know what you are doing.

    .. [1] Meyer, R. A., Musco, C., Musco, C., & Woodruff, D. P. (2021).
        *Hutch++: Optimal Stochastic Trace Estimation*. In Symposium on Simplicity
        in Algorithms (SOSA) (pp. 142–155). Philadelphia, PA: Society for
        Industrial and Applied Mathematics. `link <https://arxiv.org/abs/2010.09649>`_
    """

    ncp = get_module(backend)
    m = Op.shape[1]
    neval = int(numpy.round(m * 0.1)) if neval is None else neval

    if sampler not in _SAMPLERS:
        raise NotImplementedError(f"sampler {sampler} not available.")

    sampler_fun = _SAMPLERS[sampler]

    batch1 = int(numpy.round(neval * c1))
    batch2 = int(numpy.round(neval * c2))
    batch3 = neval - batch1 - batch2
    if batch1 <= 0 or batch2 <= 0 or batch3 <= 0:
        msg = f"Sampler '{sampler}' not supported with {neval} samples."
        msg += " Try increasing it."
        raise ValueError(msg)

    S = sampler_fun(m, batch1, backend_module=ncp).astype(Op.dtype)
    R = sampler_fun(m, batch2, backend_module=ncp).astype(Op.dtype)
    G = sampler_fun(m, batch3, backend_module=ncp).astype(Op.dtype)

    Z = Op @ R
    Wt = (Op @ S).T
    Y = ncp.linalg.pinv(S.T @ Z)
    trace = ncp.zeros(1, dtype=Op.dtype)
    trace += (
        ncp.trace(Y @ Wt @ Z) +
        (ncp.trace(G.T @ (Op @ G)) - ncp.trace(G.T @ Z @ Y @ Wt @ G)) / batch3)
    return trace[0]
Example #8
0
def trace_hutchpp(Op, neval=None, sampler="rademacher", backend="numpy"):
    r"""Trace of linear operator using the Hutch++ method.

    Returns an estimate of the trace of a linear operator using the Hutch++
    method [1]_.

    Parameters
    ----------
    neval : :obj:`int`, optional
        Maximum number of matrix-vector products compute. Defaults to 10%
        of ``shape[1]``.
    sampler : :obj:`str`, optional
        Sample sketching matrices from the following distributions:

            - "gaussian": Mean zero, unit variance Gaussian.
            - "rayleigh": Sample from mean zero, unit variance Gaussian and
              normalize the columns.
            - "rademacher": Random sign.

    backend : :obj:`str`, optional
        Backend used to densify matrix (``numpy`` or ``cupy``). Note that
        this must be consistent with how the operator has been created.

    Returns
    -------
    trace : :obj:`self.dtype`
        Operator trace.

    Raises
    -------
    ValueError
        If ``neval`` is smaller than 3.

    NotImplementedError
        If the ``sampler`` is not one of the available samplers.

    Notes
    -----
    This function follows Algorithm 1 of [1]_. Let :math:`m` = ``shape[1]``
    and :math:`k` = ``neval``.

        1. Sample sketching matrices
           :math:`\mathbf{S} \in \mathbb{R}^{m \times \lfloor k/3\rfloor}`,
           and
           :math:`\mathbf{G} \in \mathbb{R}^{m \times \lfloor k/3\rfloor}`,
           from sub-Gaussian distributions.
        2. Compute reduced QR decomposition of :math:`\mathbf{Op}\,\mathbf{S}`,
           retaining only :math:`\mathbf{Q}`.
        3. Return :math:`\operatorname{tr}(\mathbf{Q}^T\,\mathbf{Op}\,\mathbf{Q}) + \frac{1}{\lfloor k/3\rfloor}\operatorname{tr}\left(\mathbf{G}^T(\mathbf{I} - \mathbf{Q}\mathbf{Q}^T)\,\mathbf{Op}\,(\mathbf{I} - \mathbf{Q}\mathbf{Q}^T)\mathbf{G}\right)`

    Use the Rademacher sampler unless you know what you are doing.

    .. [1] Meyer, R. A., Musco, C., Musco, C., & Woodruff, D. P. (2021).
        *Hutch++: Optimal Stochastic Trace Estimation*. In Symposium on Simplicity
        in Algorithms (SOSA) (pp. 142–155). Philadelphia, PA: Society for
        Industrial and Applied Mathematics. `link <https://arxiv.org/abs/2010.09649>`_
    """

    ncp = get_module(backend)
    m = Op.shape[1]

    neval = int(numpy.round(m * 0.1)) if neval is None else neval

    if sampler not in _SAMPLERS:
        raise NotImplementedError(f"sampler {sampler} not available.")

    sampler_fun = _SAMPLERS[sampler]

    batch = neval // 3
    if batch <= 0:
        msg = f"Sampler '{sampler}' not supported with {neval} samples."
        msg += " Try increasing it."
        raise ValueError(msg)

    S = sampler_fun(m, batch, backend_module=ncp).astype(Op.dtype)
    G = sampler_fun(m, batch, backend_module=ncp).astype(Op.dtype)

    Q, _ = ncp.linalg.qr(Op @ S)
    del S
    G = G - Q @ (Q.T @ G)

    trace = ncp.zeros(1, dtype=Op.dtype)
    trace += ncp.trace(Q.T @ (Op @ Q)) + ncp.trace(G.T @ (Op @ G)) / batch
    return trace[0]
Example #9
0
def dottest(
    Op,
    nr=None,
    nc=None,
    tol=1e-6,
    complexflag=0,
    raiseerror=True,
    verb=False,
    backend="numpy",
):
    r"""Dot test.

    Generate random vectors :math:`\mathbf{u}` and :math:`\mathbf{v}`
    and perform dot-test to verify the validity of forward and adjoint
    operators. This test can help to detect errors in the operator
    implementation.

    Parameters
    ----------
    Op : :obj:`pylops.LinearOperator`
        Linear operator to test.
    nr : :obj:`int`
        Number of rows of operator (i.e., elements in data)
    nc : :obj:`int`
        Number of columns of operator (i.e., elements in model)
    tol : :obj:`float`, optional
        Dottest tolerance
    complexflag : :obj:`bool`, optional
        Generate random vectors with

        * ``0``: Real entries for model and data

        * ``1``: Complex entries for model and real entries for data

        * ``2``: Real entries for model and complex entries for data

        * ``3``: Complex entries for model and  data
    raiseerror : :obj:`bool`, optional
        Raise error or simply return ``False`` when dottest fails
    verb : :obj:`bool`, optional
        Verbosity
    backend : :obj:`str`, optional
        Backend used for dot test computations (``numpy`` or ``cupy``). This
        parameter will be used to choose how to create the random vectors.

    Raises
    ------
    ValueError
        If dot-test is not verified within chosen tolerance.

    Notes
    -----
    A dot-test is mathematical tool used in the development of numerical
    linear operators.

    More specifically, a correct implementation of forward and adjoint for
    a linear operator should verify the following *equality*
    within a numerical tolerance:

    .. math::
        (\mathbf{Op}\,\mathbf{u})^H\mathbf{v} =
        \mathbf{u}^H(\mathbf{Op}^H\mathbf{v})

    """
    ncp = get_module(backend)

    if nr is None:
        nr = Op.shape[0]
    if nc is None:
        nc = Op.shape[1]

    assert (nr,
            nc) == Op.shape, "Provided nr and nc do not match operator shape"

    # make u and v vectors
    if complexflag != 0:
        rdtype = np.real(np.ones(1, Op.dtype)).dtype

    if complexflag in (0, 2):
        u = ncp.random.randn(nc).astype(Op.dtype)
    else:
        u = ncp.random.randn(nc).astype(
            rdtype) + 1j * ncp.random.randn(nc).astype(rdtype)

    if complexflag in (0, 1):
        v = ncp.random.randn(nr).astype(Op.dtype)
    else:
        v = ncp.random.randn(nr).astype(
            rdtype) + 1j * ncp.random.randn(nr).astype(rdtype)

    y = Op.matvec(u)  # Op * u
    x = Op.rmatvec(v)  # Op'* v

    if getattr(Op, "clinear", True):
        yy = ncp.vdot(y, v)  # (Op  * u)' * v
        xx = ncp.vdot(u, x)  # u' * (Op' * v)
    else:
        # Op is only R-linear, so treat complex numbers as elements of R^2
        yy = ncp.dot(y.real, v.real) + ncp.dot(y.imag, v.imag)
        xx = ncp.dot(u.real, x.real) + ncp.dot(u.imag, x.imag)

    # convert back to numpy (in case cupy arrays were used), make into a numpy
    # array and extract the first element. This is ugly but allows to handle
    # complex numbers in subsequent prints also when using cupy arrays.
    xx, yy = np.array([to_numpy(xx)])[0], np.array([to_numpy(yy)])[0]

    # evaluate if dot test is passed
    if complexflag == 0:
        if np.abs((yy - xx) / ((yy + xx + 1e-15) / 2)) < tol:
            if verb:
                print("Dot test passed, v^T(Opu)=%f - u^T(Op^Tv)=%f" %
                      (yy, xx))
            return True
        else:
            if raiseerror:
                raise ValueError(
                    "Dot test failed, v^T(Opu)=%f - u^T(Op^Tv)=%f" % (yy, xx))
            if verb:
                print("Dot test failed, v^T(Opu)=%f - u^T(Op^Tv)=%f" %
                      (yy, xx))
            return False
    else:
        # Check both real and imag parts
        checkreal = (np.abs((np.real(yy) - np.real(xx)) /
                            ((np.real(yy) + np.real(xx) + 1e-15) / 2)) < tol)
        checkimag = (np.abs((np.imag(yy) - np.imag(xx)) /
                            ((np.imag(yy) + np.imag(xx) + 1e-15) / 2)) < tol)
        if checkreal and checkimag:
            if verb:
                print("Dot test passed, v^T(Opu)=%f%+fi - u^T(Op^Tv)=%f%+fi" %
                      (yy.real, yy.imag, xx.real, xx.imag))
            return True
        else:
            if raiseerror:
                raise ValueError("Dot test failed, v^H(Opu)=%f%+fi "
                                 "- u^H(Op^Hv)=%f%+fi" %
                                 (yy.real, yy.imag, xx.real, xx.imag))
            if verb:
                print("Dot test failed, v^H(Opu)=%f%+fi - u^H(Op^Hv)=%f%+fi" %
                      (yy.real, yy.imag, xx.real, xx.imag))
            return False