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
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
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
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.")
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
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]
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]
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]
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