From f8cc8e90c655d3f8ec5b18fbc8bccc16a661199e Mon Sep 17 00:00:00 2001 From: Amir Mardan <46511946+AmirMardan@users.noreply.github.com> Date: Fri, 13 Dec 2024 11:13:47 -0500 Subject: [PATCH 01/44] when `sampling` is not given, it's calculated based on `spataxis` and `taxis` --- pylops/waveeqprocessing/seismicinterpolation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pylops/waveeqprocessing/seismicinterpolation.py b/pylops/waveeqprocessing/seismicinterpolation.py index 7fb224334..68b3e3230 100644 --- a/pylops/waveeqprocessing/seismicinterpolation.py +++ b/pylops/waveeqprocessing/seismicinterpolation.py @@ -272,8 +272,8 @@ def SeismicInterpolation( ) else: sampling = ( - np.abs(spataxis[1] - spataxis[1]), - np.abs(taxis[1] - taxis[1]), + np.abs(spataxis[1] - spataxis[0]), + np.abs(taxis[1] - taxis[0]), ) Pop = FFT2D(dims=dims, nffts=nffts, sampling=sampling) Pop = Pop.H From 86c38dbdb35d6a4209ee459cd9e48420e653c68b Mon Sep 17 00:00:00 2001 From: Amir Mardan <46511946+AmirMardan@users.noreply.github.com> Date: Fri, 13 Dec 2024 11:15:55 -0500 Subject: [PATCH 02/44] instead of recalculation, we use the precalculated sampling values --- pylops/waveeqprocessing/seismicinterpolation.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pylops/waveeqprocessing/seismicinterpolation.py b/pylops/waveeqprocessing/seismicinterpolation.py index 68b3e3230..67c17f48b 100644 --- a/pylops/waveeqprocessing/seismicinterpolation.py +++ b/pylops/waveeqprocessing/seismicinterpolation.py @@ -271,10 +271,7 @@ def SeismicInterpolation( f"and taxis for kind={kind}" ) else: - sampling = ( - np.abs(spataxis[1] - spataxis[0]), - np.abs(taxis[1] - taxis[0]), - ) + sampling = (dspat, dt) Pop = FFT2D(dims=dims, nffts=nffts, sampling=sampling) Pop = Pop.H SIop = Rop * Pop From dfc2c7a1278b912009aaab33e3b290968a2d9528 Mon Sep 17 00:00:00 2001 From: Amir Mardan <46511946+AmirMardan@users.noreply.github.com> Date: Tue, 17 Dec 2024 11:53:10 -0500 Subject: [PATCH 03/44] fix the sampling issue for 3D --- pylops/waveeqprocessing/seismicinterpolation.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pylops/waveeqprocessing/seismicinterpolation.py b/pylops/waveeqprocessing/seismicinterpolation.py index 67c17f48b..e1159d4aa 100644 --- a/pylops/waveeqprocessing/seismicinterpolation.py +++ b/pylops/waveeqprocessing/seismicinterpolation.py @@ -256,11 +256,7 @@ def SeismicInterpolation( f"spat1axis and taxis for kind=%{kind}" ) else: - sampling = ( - np.abs(spataxis[1] - spataxis[1]), - np.abs(spat1axis[1] - spat1axis[1]), - np.abs(taxis[1] - taxis[1]), - ) + sampling = (dspat, dspat1, dt) Pop = FFTND(dims=dims, nffts=nffts, sampling=sampling) Pop = Pop.H else: From 12c8c04b2c0c5a066a73ee24f7a2a6ec9d9d96b8 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Thu, 2 Jan 2025 22:24:45 +0000 Subject: [PATCH 04/44] minor: switch np.dot to np.matmul in code snipped in installation --- docs/source/installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/installation.rst b/docs/source/installation.rst index c0eff42d8..cfc60a265 100755 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -264,7 +264,7 @@ and run the following code in Python: B = np.random.random((size, size)) print("Time with %s threads: %f s" \ %(os.environ.get("OMP_NUM_THREADS"), - timeit(lambda: np.dot(A, B), number=4))) + timeit(lambda: np.matmul(A, B), number=4))) Subsequently set the environment variables to ``2`` or any higher number of threads available in your hardware (multi-threaded), and run the same code. From e8ad8a7ec3de5b07ad90928be36511c752932c7c Mon Sep 17 00:00:00 2001 From: mrava87 Date: Wed, 15 Jan 2025 21:43:05 +0000 Subject: [PATCH 05/44] doc: fix typo in wavedecomposition docstrings --- pylops/waveeqprocessing/wavedecomposition.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pylops/waveeqprocessing/wavedecomposition.py b/pylops/waveeqprocessing/wavedecomposition.py index 715fb2c1c..c6e229441 100644 --- a/pylops/waveeqprocessing/wavedecomposition.py +++ b/pylops/waveeqprocessing/wavedecomposition.py @@ -787,7 +787,7 @@ def WavefieldDecomposition( \end{bmatrix}(k_x, \omega) if ``kind='analytical'`` or alternatively by solving the equation in - :func:`ptcpy.waveeqprocessing.UpDownComposition2D` as an inverse problem, + :func:`pylops.waveeqprocessing.UpDownComposition2D` as an inverse problem, if ``kind='inverse'``. The latter approach has several advantages as data regularization @@ -807,9 +807,9 @@ def WavefieldDecomposition( 0 & \mathbf{F}^H \mathbf{S} \end{bmatrix} \mathbf{p^{\pm}} - where :math:`\mathbf{R}` is a :class:`ptcpy.basicoperators.Restriction` + where :math:`\mathbf{R}` is a :class:`pylops.basicoperators.Restriction` operator and :math:`\mathbf{S}` is sparsyfing transform operator (e.g., - :class:`ptcpy.signalprocessing.Radon2D`). + :class:`pylops.signalprocessing.Radon2D`). .. [1] Wapenaar, K. "Reciprocity properties of one-way propagators", Geophysics, vol. 63, pp. 1795-1798. 1998. From 6d34db021d71cf80abd7f98a74698a9fcefea1b3 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Thu, 23 Jan 2025 14:46:13 +0000 Subject: [PATCH 06/44] minor: added all classes to __all__ in cls_sparsity --- pylops/optimization/cls_sparsity.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pylops/optimization/cls_sparsity.py b/pylops/optimization/cls_sparsity.py index 6ab4a30dc..8a82a71a3 100644 --- a/pylops/optimization/cls_sparsity.py +++ b/pylops/optimization/cls_sparsity.py @@ -1,4 +1,12 @@ -__all__ = ["IRLS"] +__all__ = [ + "IRLS", + "OMP", + "ISTA", + "FISTA", + "SPGL1", + "SplitBregman", +] + import logging import time from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple From 51f1b7fe115424ab84b86261501296942912c9cd Mon Sep 17 00:00:00 2001 From: IruNikZe Date: Fri, 31 Jan 2025 11:06:47 +0100 Subject: [PATCH 07/44] DOC: **Issue [Documentation] `pylops.lsqr` documentation lists 9 results even though there is 11 #637** - added `iiter` and `xnorm` to the documentation of the `pylops.lsqr` solver and related methods --- pylops/optimization/basic.py | 4 ++++ pylops/optimization/cls_basic.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/pylops/optimization/basic.py b/pylops/optimization/basic.py index 272e530cc..34b03bb71 100644 --- a/pylops/optimization/basic.py +++ b/pylops/optimization/basic.py @@ -242,6 +242,8 @@ def lsqr( ``7`` means the iteration limit has been reached + iiter : :obj:`int` + Iteration number upon termination r1norm : :obj:`float` :math:`||\mathbf{r}||_2^2`, where :math:`\mathbf{r} = \mathbf{y} - \mathbf{Op}\,\mathbf{x}` @@ -257,6 +259,8 @@ def lsqr( arnorm : :obj:`float` Estimate of norm of :math:`\cond(\mathbf{Op}^H\mathbf{r}- \epsilon^2\mathbf{x})` + xnorm : :obj:`float` + :math:`||\mathbf{x}||_2` var : :obj:`float` Diagonals of :math:`(\mathbf{Op}^H\mathbf{Op})^{-1}` (if ``damp=0``) or more generally :math:`(\mathbf{Op}^H\mathbf{Op} + diff --git a/pylops/optimization/cls_basic.py b/pylops/optimization/cls_basic.py index b7c98e0aa..1fb1ee7da 100644 --- a/pylops/optimization/cls_basic.py +++ b/pylops/optimization/cls_basic.py @@ -1043,6 +1043,8 @@ def solve( ``7`` means the iteration limit has been reached + iiter : :obj:`int` + Iteration number upon termination r1norm : :obj:`float` :math:`||\mathbf{r}||_2^2`, where :math:`\mathbf{r} = \mathbf{y} - \mathbf{Op}\,\mathbf{x}` @@ -1058,6 +1060,8 @@ def solve( arnorm : :obj:`float` Estimate of norm of :math:`\cond(\mathbf{Op}^H\mathbf{r}- \epsilon^2\mathbf{x})` + xnorm : :obj:`float` + :math:`||\mathbf{x}||_2` var : :obj:`float` Diagonals of :math:`(\mathbf{Op}^H\mathbf{Op})^{-1}` (if ``damp=0``) or more generally :math:`(\mathbf{Op}^H\mathbf{Op} + From 7d77f432293599d6b34b960b20779f1e511e46be Mon Sep 17 00:00:00 2001 From: mrava87 Date: Wed, 5 Feb 2025 16:23:47 +0000 Subject: [PATCH 08/44] doc: fixed problem statement for omp in example --- examples/plot_ista.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/plot_ista.py b/examples/plot_ista.py index d6359905d..c8c3d71af 100755 --- a/examples/plot_ista.py +++ b/examples/plot_ista.py @@ -12,7 +12,9 @@ mathematically translates to solving the following constrained problem: .. math:: - \quad \|\mathbf{Op}\mathbf{x}- \mathbf{b}\|_2 <= \sigma, + \|\mathbf{x}\|_0 \quad \text{subject to} \quad + \|\mathbf{Op}\,\mathbf{x}-\mathbf{y}\|_2^2 \leq \sigma^2, + while IRLS, ISTA and FISTA solve an uncostrained problem with a L1 regularization term: From 4fc89790cccc37874c3080f3696e92e6e4166648 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Thu, 6 Feb 2025 21:47:02 +0000 Subject: [PATCH 09/44] minor: improved documentation of PhaseShift --- pylops/waveeqprocessing/oneway.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/pylops/waveeqprocessing/oneway.py b/pylops/waveeqprocessing/oneway.py index 14e5f7f12..ae7aa3c92 100644 --- a/pylops/waveeqprocessing/oneway.py +++ b/pylops/waveeqprocessing/oneway.py @@ -103,10 +103,10 @@ def PhaseShift( freq : :obj:`numpy.ndarray` Positive frequency axis kx : :obj:`int`, optional - Horizontal wavenumber axis (centered around 0) of size + Horizontal spectroscopic wavenumber axis (centered around 0) of size :math:`[n_x \times 1]`. ky : :obj:`int`, optional - Second horizontal wavenumber axis for 3d phase shift + Second horizontal spectroscopic wavenumber axis for 3d phase shift (centered around 0) of size :math:`[n_y \times 1]`. dtype : :obj:`str`, optional Type of elements in input array @@ -130,9 +130,14 @@ def PhaseShift( d(f, k_x, k_y) = m(f, k_x, k_y) e^{-j \Delta z \sqrt{\omega^2/v^2 - k_x^2 - k_y^2}} - where :math:`v` is the constant propagation velocity and - :math:`\Delta z` is the propagation depth. In adjoint mode, the data is - propagated backward using the following transformation: + where :math:`v` is the constant propagation velocity, + :math:`\Delta z` is the propagation depth, :math:`\omega=2\pi f` is the + angular frequency axis (where :math:`f` is represented by ``freq``), + :math:`k_x=2\pi \tilde{k}_x` is the horizontal wavenumber (where + :math:`\tilde{k}_x` is represented by ``kx``), and :math:`k_y=2\pi \tilde{k}_y` + is the second horizontal wavenumber (where :math:`\tilde{k}_y` + is represented by ``ky``). In adjoint mode, the data is propagated backward + using the following transformation: .. math:: m(f, k_x, k_y) = d(f, k_x, k_y) From 6af0771ca8459f006a41802fdc2a200717f72a4a Mon Sep 17 00:00:00 2001 From: mrava87 Date: Wed, 12 Feb 2025 20:23:57 +0000 Subject: [PATCH 10/44] bug: missing cols in callback of omp solver This commit changes the signature of the callback of the omp solver, which provided only the non-zero coefficients (in x) without their indices - added now in the second input parameter (col). --- pylops/optimization/cls_sparsity.py | 2 +- pylops/optimization/sparsity.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pylops/optimization/cls_sparsity.py b/pylops/optimization/cls_sparsity.py index 8a82a71a3..fb5366beb 100644 --- a/pylops/optimization/cls_sparsity.py +++ b/pylops/optimization/cls_sparsity.py @@ -914,7 +914,7 @@ def run( else False ) x, cols = self.step(x, cols, showstep) - self.callback(x) + self.callback(x, cols) return x, cols def finalize( diff --git a/pylops/optimization/sparsity.py b/pylops/optimization/sparsity.py index 40c560477..f612aa1b4 100644 --- a/pylops/optimization/sparsity.py +++ b/pylops/optimization/sparsity.py @@ -166,8 +166,9 @@ def omp( and every N3 steps in between where N1, N2, N3 are the three element of the list. callback : :obj:`callable`, optional - Function with signature (``callback(x)``) to call after each iteration - where ``x`` is the current model vector + Function with signature (``callback(x, cols)``) to call after each iteration + where ``x`` contains the non-zero model coefficient and ``cols`` are the + indices where the current model vector is non-zero Returns ------- From 5d3e42c50a85f722f528e3c110a047ac72a9e80e Mon Sep 17 00:00:00 2001 From: mrava87 Date: Wed, 19 Feb 2025 08:37:40 +0000 Subject: [PATCH 11/44] minor: fix broken links in plot_blending --- examples/plot_blending.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/plot_blending.py b/examples/plot_blending.py index ee6f42cb2..539c26c99 100644 --- a/examples/plot_blending.py +++ b/examples/plot_blending.py @@ -1,9 +1,10 @@ """ Blending ======== -This example shows how to use the :py:class:`pylops.waveeqprocessing.blending.Blending` -operator to blend seismic data to mimic state-of-the-art simultaneous shooting -acquisition systems. +This example shows how to use the :py:class:`pylops.waveeqprocessing.blending.BlendingContinuous`, +:py:class:`pylops.waveeqprocessing.blending.BlendingGroup` and +:py:class:`pylops.waveeqprocessing.blending.BlendingHalf` operators to blend seismic data +to mimic state-of-the-art simultaneous shooting acquisition systems. """ import matplotlib.pyplot as plt import numpy as np From 7e977987f088a40b7702803f65dfa23deb798ebd Mon Sep 17 00:00:00 2001 From: mrava87 Date: Wed, 19 Feb 2025 10:46:35 +0000 Subject: [PATCH 12/44] minor: one more fix of broken links plot_blending --- examples/plot_blending.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/plot_blending.py b/examples/plot_blending.py index 539c26c99..940533669 100644 --- a/examples/plot_blending.py +++ b/examples/plot_blending.py @@ -1,9 +1,9 @@ """ Blending ======== -This example shows how to use the :py:class:`pylops.waveeqprocessing.blending.BlendingContinuous`, -:py:class:`pylops.waveeqprocessing.blending.BlendingGroup` and -:py:class:`pylops.waveeqprocessing.blending.BlendingHalf` operators to blend seismic data +This example shows how to use the :py:class:`pylops.waveeqprocessing.BlendingContinuous`, +:py:class:`pylops.waveeqprocessing.BlendingGroup` and +:py:class:`pylops.waveeqprocessing.BlendingHalf` operators to blend seismic data to mimic state-of-the-art simultaneous shooting acquisition systems. """ import matplotlib.pyplot as plt From 623447896c3e45633cba3ddf8fc96daa3e2c1578 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Sun, 23 Feb 2025 20:08:01 +0000 Subject: [PATCH 13/44] feat: added kwargs_fft to all fft operators --- pylops/signalprocessing/fft.py | 58 +++++++++---- pylops/signalprocessing/fft2d.py | 43 +++++++-- pylops/signalprocessing/fftnd.py | 32 +++++-- pytests/test_ffts.py | 144 ++++++++++++++++++++++++++++++- 4 files changed, 240 insertions(+), 37 deletions(-) diff --git a/pylops/signalprocessing/fft.py b/pylops/signalprocessing/fft.py index 6af81a30a..1209106ef 100644 --- a/pylops/signalprocessing/fft.py +++ b/pylops/signalprocessing/fft.py @@ -37,6 +37,7 @@ def __init__( ifftshift_before: bool = False, fftshift_after: bool = False, dtype: DTypeLike = "complex128", + **kwargs_fft, ) -> None: super().__init__( dims=dims, @@ -54,6 +55,7 @@ def __init__( f"numpy backend always returns complex128 dtype. To respect the passed dtype, data will be casted to {self.cdtype}." ) + self._kwargs_fft = kwargs_fft self._norm_kwargs = {"norm": None} # equivalent to "backward" in Numpy/Scipy if self.norm is _FFTNorms.ORTHO: self._norm_kwargs["norm"] = "ortho" @@ -74,14 +76,18 @@ def _matvec(self, x: NDArray) -> NDArray: if not self.clinear: x = ncp.real(x) if self.real: - y = ncp.fft.rfft(x, n=self.nfft, axis=self.axis, **self._norm_kwargs) + y = ncp.fft.rfft( + x, n=self.nfft, axis=self.axis, **self._norm_kwargs, **self._kwargs_fft + ) # Apply scaling to obtain a correct adjoint for this operator y = ncp.swapaxes(y, -1, self.axis) # y[..., 1 : 1 + (self.nfft - 1) // 2] *= ncp.sqrt(2) y = inplace_multiply(ncp.sqrt(2), y, self.slice) y = ncp.swapaxes(y, self.axis, -1) else: - y = ncp.fft.fft(x, n=self.nfft, axis=self.axis, **self._norm_kwargs) + y = ncp.fft.fft( + x, n=self.nfft, axis=self.axis, **self._norm_kwargs, **self._kwargs_fft + ) if self.norm is _FFTNorms.ONE_OVER_N: y *= self._scale if self.fftshift_after: @@ -101,9 +107,13 @@ def _rmatvec(self, x: NDArray) -> NDArray: # x[..., 1 : 1 + (self.nfft - 1) // 2] /= ncp.sqrt(2) x = inplace_divide(ncp.sqrt(2), x, self.slice) x = ncp.swapaxes(x, self.axis, -1) - y = ncp.fft.irfft(x, n=self.nfft, axis=self.axis, **self._norm_kwargs) + y = ncp.fft.irfft( + x, n=self.nfft, axis=self.axis, **self._norm_kwargs, **self._kwargs_fft + ) else: - y = ncp.fft.ifft(x, n=self.nfft, axis=self.axis, **self._norm_kwargs) + y = ncp.fft.ifft( + x, n=self.nfft, axis=self.axis, **self._norm_kwargs, **self._kwargs_fft + ) if self.norm is _FFTNorms.NONE: y *= self._scale @@ -139,6 +149,7 @@ def __init__( ifftshift_before: bool = False, fftshift_after: bool = False, dtype: DTypeLike = "complex128", + **kwargs_fft, ) -> None: super().__init__( dims=dims, @@ -152,6 +163,7 @@ def __init__( dtype=dtype, ) + self._kwargs_fft = kwargs_fft self._norm_kwargs = {"norm": None} # equivalent to "backward" in Numpy/Scipy if self.norm is _FFTNorms.ORTHO: self._norm_kwargs["norm"] = "ortho" @@ -167,13 +179,17 @@ def _matvec(self, x: NDArray) -> NDArray: if not self.clinear: x = np.real(x) if self.real: - y = scipy.fft.rfft(x, n=self.nfft, axis=self.axis, **self._norm_kwargs) + y = scipy.fft.rfft( + x, n=self.nfft, axis=self.axis, **self._norm_kwargs, **self._kwargs_fft + ) # Apply scaling to obtain a correct adjoint for this operator y = np.swapaxes(y, -1, self.axis) y[..., 1 : 1 + (self.nfft - 1) // 2] *= np.sqrt(2) y = np.swapaxes(y, self.axis, -1) else: - y = scipy.fft.fft(x, n=self.nfft, axis=self.axis, **self._norm_kwargs) + y = scipy.fft.fft( + x, n=self.nfft, axis=self.axis, **self._norm_kwargs, **self._kwargs_fft + ) if self.norm is _FFTNorms.ONE_OVER_N: y *= self._scale if self.fftshift_after: @@ -190,9 +206,13 @@ def _rmatvec(self, x: NDArray) -> NDArray: x = np.swapaxes(x, -1, self.axis) x[..., 1 : 1 + (self.nfft - 1) // 2] /= np.sqrt(2) x = np.swapaxes(x, self.axis, -1) - y = scipy.fft.irfft(x, n=self.nfft, axis=self.axis, **self._norm_kwargs) + y = scipy.fft.irfft( + x, n=self.nfft, axis=self.axis, **self._norm_kwargs, **self._kwargs_fft + ) else: - y = scipy.fft.ifft(x, n=self.nfft, axis=self.axis, **self._norm_kwargs) + y = scipy.fft.ifft( + x, n=self.nfft, axis=self.axis, **self._norm_kwargs, **self._kwargs_fft + ) if self.norm is _FFTNorms.NONE: y *= self._scale @@ -227,7 +247,7 @@ def __init__( ifftshift_before: bool = False, fftshift_after: bool = False, dtype: DTypeLike = "complex128", - **kwargs_fftw, + **kwargs_fft, ) -> None: if np.dtype(dtype) == np.float16: warnings.warn( @@ -236,13 +256,13 @@ def __init__( dtype = np.float32 for badop in ["ortho", "normalise_idft"]: - if badop in kwargs_fftw: + if badop in kwargs_fft: if badop == "ortho" and norm == "ortho": continue warnings.warn( f"FFTW option '{badop}' will be overwritten by norm={norm}" ) - del kwargs_fftw[badop] + del kwargs_fft[badop] super().__init__( dims=dims, @@ -298,10 +318,10 @@ def __init__( self._scale = 1.0 / self.nfft self.fftplan = pyfftw.FFTW( - self.x, self.y, axes=(self.axis,), direction="FFTW_FORWARD", **kwargs_fftw + self.x, self.y, axes=(self.axis,), direction="FFTW_FORWARD", **kwargs_fft ) self.ifftplan = pyfftw.FFTW( - self.y, self.x, axes=(self.axis,), direction="FFTW_BACKWARD", **kwargs_fftw + self.y, self.x, axes=(self.axis,), direction="FFTW_BACKWARD", **kwargs_fft ) @reshaped @@ -386,7 +406,7 @@ def FFT( engine: str = "numpy", dtype: DTypeLike = "complex128", name: str = "F", - **kwargs_fftw, + **kwargs_fft, ) -> LinearOperator: r"""One dimensional Fast-Fourier Transform. @@ -479,9 +499,9 @@ def FFT( .. versionadded:: 2.0.0 Name of operator (to be used by :func:`pylops.utils.describe.describe`) - **kwargs_fftw - Arbitrary keyword arguments - for :py:class:`pyfftw.FTTW` + **kwargs_fft + Arbitrary keyword arguments to be passed to the selected fft method + Attributes ---------- @@ -557,7 +577,7 @@ def FFT( ifftshift_before=ifftshift_before, fftshift_after=fftshift_after, dtype=dtype, - **kwargs_fftw, + **kwargs_fft, ) elif engine == "numpy" or (engine == "fftw" and pyfftw_message is not None): if engine == "fftw" and pyfftw_message is not None: @@ -572,6 +592,7 @@ def FFT( ifftshift_before=ifftshift_before, fftshift_after=fftshift_after, dtype=dtype, + **kwargs_fft, ) elif engine == "scipy": f = _FFT_scipy( @@ -584,6 +605,7 @@ def FFT( ifftshift_before=ifftshift_before, fftshift_after=fftshift_after, dtype=dtype, + **kwargs_fft, ) else: raise NotImplementedError("engine must be numpy, fftw or scipy") diff --git a/pylops/signalprocessing/fft2d.py b/pylops/signalprocessing/fft2d.py index 2f4b5f151..7c4b0e951 100644 --- a/pylops/signalprocessing/fft2d.py +++ b/pylops/signalprocessing/fft2d.py @@ -30,6 +30,7 @@ def __init__( ifftshift_before: bool = False, fftshift_after: bool = False, dtype: DTypeLike = "complex128", + **kwargs_fft, ) -> None: super().__init__( dims=dims, @@ -56,6 +57,7 @@ def __init__( self.f1, self.f2 = self.fs del self.fs + self._kwargs_fft = kwargs_fft self._norm_kwargs: Dict[str, Union[None, str]] = { "norm": None } # equivalent to "backward" in Numpy/Scipy @@ -74,13 +76,17 @@ def _matvec(self, x): if not self.clinear: x = ncp.real(x) if self.real: - y = ncp.fft.rfft2(x, s=self.nffts, axes=self.axes, **self._norm_kwargs) + y = ncp.fft.rfft2( + x, s=self.nffts, axes=self.axes, **self._norm_kwargs, **self._kwargs_fft + ) # Apply scaling to obtain a correct adjoint for this operator y = ncp.swapaxes(y, -1, self.axes[-1]) y[..., 1 : 1 + (self.nffts[-1] - 1) // 2] *= ncp.sqrt(2) y = ncp.swapaxes(y, self.axes[-1], -1) else: - y = ncp.fft.fft2(x, s=self.nffts, axes=self.axes, **self._norm_kwargs) + y = ncp.fft.fft2( + x, s=self.nffts, axes=self.axes, **self._norm_kwargs, **self._kwargs_fft + ) if self.norm is _FFTNorms.ONE_OVER_N: y *= self._scale y = y.astype(self.cdtype) @@ -99,9 +105,13 @@ def _rmatvec(self, x): x = ncp.swapaxes(x, -1, self.axes[-1]) x[..., 1 : 1 + (self.nffts[-1] - 1) // 2] /= ncp.sqrt(2) x = ncp.swapaxes(x, self.axes[-1], -1) - y = ncp.fft.irfft2(x, s=self.nffts, axes=self.axes, **self._norm_kwargs) + y = ncp.fft.irfft2( + x, s=self.nffts, axes=self.axes, **self._norm_kwargs, **self._kwargs_fft + ) else: - y = ncp.fft.ifft2(x, s=self.nffts, axes=self.axes, **self._norm_kwargs) + y = ncp.fft.ifft2( + x, s=self.nffts, axes=self.axes, **self._norm_kwargs, **self._kwargs_fft + ) if self.norm is _FFTNorms.NONE: y *= self._scale if self.nffts[0] > self.dims[self.axes[0]]: @@ -137,6 +147,7 @@ def __init__( ifftshift_before: bool = False, fftshift_after: bool = False, dtype: DTypeLike = "complex128", + **kwargs_fft, ) -> None: super().__init__( dims=dims, @@ -159,6 +170,7 @@ def __init__( self.f1, self.f2 = self.fs del self.fs + self._kwargs_fft = kwargs_fft self._norm_kwargs: Dict[str, Union[None, str]] = { "norm": None } # equivalent to "backward" in Numpy/Scipy @@ -176,13 +188,17 @@ def _matvec(self, x): if not self.clinear: x = np.real(x) if self.real: - y = scipy.fft.rfft2(x, s=self.nffts, axes=self.axes, **self._norm_kwargs) + y = scipy.fft.rfft2( + x, s=self.nffts, axes=self.axes, **self._norm_kwargs, **self._kwargs_fft + ) # Apply scaling to obtain a correct adjoint for this operator y = np.swapaxes(y, -1, self.axes[-1]) y[..., 1 : 1 + (self.nffts[-1] - 1) // 2] *= np.sqrt(2) y = np.swapaxes(y, self.axes[-1], -1) else: - y = scipy.fft.fft2(x, s=self.nffts, axes=self.axes, **self._norm_kwargs) + y = scipy.fft.fft2( + x, s=self.nffts, axes=self.axes, **self._norm_kwargs, **self._kwargs_fft + ) if self.norm is _FFTNorms.ONE_OVER_N: y *= self._scale if self.fftshift_after.any(): @@ -199,9 +215,13 @@ def _rmatvec(self, x): x = np.swapaxes(x, -1, self.axes[-1]) x[..., 1 : 1 + (self.nffts[-1] - 1) // 2] /= np.sqrt(2) x = np.swapaxes(x, self.axes[-1], -1) - y = scipy.fft.irfft2(x, s=self.nffts, axes=self.axes, **self._norm_kwargs) + y = scipy.fft.irfft2( + x, s=self.nffts, axes=self.axes, **self._norm_kwargs, **self._kwargs_fft + ) else: - y = scipy.fft.ifft2(x, s=self.nffts, axes=self.axes, **self._norm_kwargs) + y = scipy.fft.ifft2( + x, s=self.nffts, axes=self.axes, **self._norm_kwargs, **self._kwargs_fft + ) if self.norm is _FFTNorms.NONE: y *= self._scale y = np.take(y, range(self.dims[self.axes[0]]), axis=self.axes[0]) @@ -230,6 +250,7 @@ def FFT2D( engine: str = "numpy", dtype: DTypeLike = "complex128", name: str = "F", + **kwargs_fft, ) -> LinearOperator: r"""Two dimensional Fast-Fourier Transform. @@ -328,6 +349,10 @@ def FFT2D( .. versionadded:: 2.0.0 Name of operator (to be used by :func:`pylops.utils.describe.describe`) + **kwargs_fft + .. versionadded:: 2.5.0 + + Arbitrary keyword arguments to be passed to the selected fft method Attributes ---------- @@ -409,6 +434,7 @@ def FFT2D( ifftshift_before=ifftshift_before, fftshift_after=fftshift_after, dtype=dtype, + **kwargs_fft, ) elif engine == "scipy": f = _FFT2D_scipy( @@ -421,6 +447,7 @@ def FFT2D( ifftshift_before=ifftshift_before, fftshift_after=fftshift_after, dtype=dtype, + **kwargs_fft, ) else: raise NotImplementedError("engine must be numpy or scipy") diff --git a/pylops/signalprocessing/fftnd.py b/pylops/signalprocessing/fftnd.py index cf2de78f7..6379bcfb6 100644 --- a/pylops/signalprocessing/fftnd.py +++ b/pylops/signalprocessing/fftnd.py @@ -65,13 +65,17 @@ def _matvec(self, x: NDArray) -> NDArray: if not self.clinear: x = ncp.real(x) if self.real: - y = ncp.fft.rfftn(x, s=self.nffts, axes=self.axes, **self._norm_kwargs) + y = ncp.fft.rfftn( + x, s=self.nffts, axes=self.axes, **self._norm_kwargs, **self._kwargs_fft + ) # Apply scaling to obtain a correct adjoint for this operator y = ncp.swapaxes(y, -1, self.axes[-1]) y[..., 1 : 1 + (self.nffts[-1] - 1) // 2] *= ncp.sqrt(2) y = ncp.swapaxes(y, self.axes[-1], -1) else: - y = ncp.fft.fftn(x, s=self.nffts, axes=self.axes, **self._norm_kwargs) + y = ncp.fft.fftn( + x, s=self.nffts, axes=self.axes, **self._norm_kwargs, **self._kwargs_fft + ) if self.norm is _FFTNorms.ONE_OVER_N: y *= self._scale y = y.astype(self.cdtype) @@ -90,9 +94,13 @@ def _rmatvec(self, x: NDArray) -> NDArray: x = ncp.swapaxes(x, -1, self.axes[-1]) x[..., 1 : 1 + (self.nffts[-1] - 1) // 2] /= ncp.sqrt(2) x = ncp.swapaxes(x, self.axes[-1], -1) - y = ncp.fft.irfftn(x, s=self.nffts, axes=self.axes, **self._norm_kwargs) + y = ncp.fft.irfftn( + x, s=self.nffts, axes=self.axes, **self._norm_kwargs, **self._kwargs_fft + ) else: - y = ncp.fft.ifftn(x, s=self.nffts, axes=self.axes, **self._norm_kwargs) + y = ncp.fft.ifftn( + x, s=self.nffts, axes=self.axes, **self._norm_kwargs, **self._kwargs_fft + ) if self.norm is _FFTNorms.NONE: y *= self._scale for ax, nfft in zip(self.axes, self.nffts): @@ -157,13 +165,17 @@ def _matvec(self, x: NDArray) -> NDArray: if not self.clinear: x = np.real(x) if self.real: - y = sp_fft.rfftn(x, s=self.nffts, axes=self.axes, **self._norm_kwargs) + y = sp_fft.rfftn( + x, s=self.nffts, axes=self.axes, **self._norm_kwargs, **self._kwargs_fft + ) # Apply scaling to obtain a correct adjoint for this operator y = np.swapaxes(y, -1, self.axes[-1]) y[..., 1 : 1 + (self.nffts[-1] - 1) // 2] *= np.sqrt(2) y = np.swapaxes(y, self.axes[-1], -1) else: - y = sp_fft.fftn(x, s=self.nffts, axes=self.axes, **self._norm_kwargs) + y = sp_fft.fftn( + x, s=self.nffts, axes=self.axes, **self._norm_kwargs, **self._kwargs_fft + ) if self.norm is _FFTNorms.ONE_OVER_N: y *= self._scale if self.fftshift_after.any(): @@ -181,9 +193,13 @@ def _rmatvec(self, x: NDArray) -> NDArray: x = np.swapaxes(x, -1, self.axes[-1]) x[..., 1 : 1 + (self.nffts[-1] - 1) // 2] /= np.sqrt(2) x = np.swapaxes(x, self.axes[-1], -1) - y = sp_fft.irfftn(x, s=self.nffts, axes=self.axes, **self._norm_kwargs) + y = sp_fft.irfftn( + x, s=self.nffts, axes=self.axes, **self._norm_kwargs, **self._kwargs_fft + ) else: - y = sp_fft.ifftn(x, s=self.nffts, axes=self.axes, **self._norm_kwargs) + y = sp_fft.ifftn( + x, s=self.nffts, axes=self.axes, **self._norm_kwargs, **self._kwargs_fft + ) if self.norm is _FFTNorms.NONE: y *= self._scale for ax, nfft in zip(self.axes, self.nffts): diff --git a/pytests/test_ffts.py b/pytests/test_ffts.py index f912291c4..77f49d22f 100755 --- a/pytests/test_ffts.py +++ b/pytests/test_ffts.py @@ -39,6 +39,7 @@ def _choose_random_axes(ndim, n_choices=2): "engine": "numpy", "ifftshift_before": False, "dtype": np.complex128, + "kwargs": {}, } # nfft=nt, complex input, numpy engine par2 = { "nt": 41, @@ -49,6 +50,7 @@ def _choose_random_axes(ndim, n_choices=2): "engine": "numpy", "ifftshift_before": False, "dtype": np.complex64, + "kwargs": {}, } # nfft>nt, complex input, numpy engine par3 = { "nt": 41, @@ -59,6 +61,7 @@ def _choose_random_axes(ndim, n_choices=2): "engine": "numpy", "ifftshift_before": False, "dtype": np.float64, + "kwargs": {}, } # nfft=nt, real input, numpy engine par4 = { "nt": 41, @@ -69,6 +72,7 @@ def _choose_random_axes(ndim, n_choices=2): "engine": "numpy", "ifftshift_before": False, "dtype": np.float64, + "kwargs": {}, } # nfft>nt, real input, numpy engine par5 = { "nt": 41, @@ -79,6 +83,7 @@ def _choose_random_axes(ndim, n_choices=2): "engine": "numpy", "ifftshift_before": True, "dtype": np.float32, + "kwargs": {}, } # nfft>nt, real input and ifftshift_before, numpy engine par6 = { "nt": 41, @@ -89,7 +94,74 @@ def _choose_random_axes(ndim, n_choices=2): "engine": "numpy", "ifftshift_before": False, "dtype": np.complex128, -} # nfftnt, complex input, scipy engine +par3s = { + "nt": 41, + "nx": 31, + "ny": 10, + "nfft": None, + "real": True, + "engine": "numpy", + "ifftshift_before": False, + "dtype": np.float64, + "kwargs": {}, +} # nfft=nt, real input, scipy engine +par4s = { + "nt": 41, + "nx": 31, + "ny": 10, + "nfft": 64, + "real": True, + "engine": "numpy", + "ifftshift_before": False, + "dtype": np.float64, + "kwargs": {}, +} # nfft>nt, real input, scipy engine +par5s = { + "nt": 41, + "nx": 31, + "ny": 10, + "nfft": 16, + "real": False, + "engine": "numpy", + "ifftshift_before": False, + "dtype": np.complex128, + "kwargs": {}, +} # nfftnt, complex input, fftw engine par3w = { "nt": 41, @@ -119,6 +193,7 @@ def _choose_random_axes(ndim, n_choices=2): "engine": "fftw", "ifftshift_before": False, "dtype": np.float64, + "kwargs": {}, } # nfft=nt, real input, fftw engine par4w = { "nt": 41, @@ -129,6 +204,7 @@ def _choose_random_axes(ndim, n_choices=2): "engine": "fftw", "ifftshift_before": False, "dtype": np.float32, + "kwargs": {}, } # nfft>nt, real input, fftw engine par5w = { "nt": 41, @@ -139,6 +215,7 @@ def _choose_random_axes(ndim, n_choices=2): "engine": "fftw", "ifftshift_before": False, "dtype": np.complex128, + "kwargs": {}, } # nfft Date: Thu, 27 Feb 2025 15:04:16 -0300 Subject: [PATCH 14/44] Removes the creation of the solver from within the shots iteration loop. --- pylops/waveeqprocessing/twoway.py | 63 ++++++++++++++++++------------- 1 file changed, 36 insertions(+), 27 deletions(-) diff --git a/pylops/waveeqprocessing/twoway.py b/pylops/waveeqprocessing/twoway.py index f74de122c..b7298ba6e 100644 --- a/pylops/waveeqprocessing/twoway.py +++ b/pylops/waveeqprocessing/twoway.py @@ -229,11 +229,13 @@ def updatesrc(self, wav): time_range=self.geometry.time_axis, ) - def _srcillumination_oneshot(self, isrc: int) -> Tuple[NDArray, NDArray]: + def _srcillumination_oneshot(self, solver: AcousticWaveSolverType, isrc: int) -> Tuple[NDArray, NDArray]: """Source wavefield and illumination for one shot Parameters ---------- + solver : :obj:`AcousticWaveSolver` + Devito's solver object. isrc : :obj:`int` Index of source to model @@ -245,17 +247,6 @@ def _srcillumination_oneshot(self, isrc: int) -> Tuple[NDArray, NDArray]: Source illumination """ - # create geometry for single source - geometry = AcquisitionGeometry( - self.model, - self.geometry.rec_positions, - self.geometry.src_positions[isrc, :], - self.geometry.t0, - self.geometry.tn, - f0=self.geometry.f0, - src_type=self.geometry.src_type, - ) - solver = AcousticWaveSolver(self.model, geometry, space_order=self.space_order) # assign source location to source object with custom wavelet if hasattr(self, "wav"): @@ -279,13 +270,27 @@ def srcillumination_allshots(self, savewav: bool = False) -> None: Save source wavefield (``True``) or not (``False``) """ + # create geometry for single source + geometry = AcquisitionGeometry( + self.model, + self.geometry.rec_positions, + self.geometry.src_positions[0, :], + self.geometry.t0, + self.geometry.tn, + f0=self.geometry.f0, + src_type=self.geometry.src_type, + ) + + solver = AcousticWaveSolver(self.model, geometry, space_order=self.space_order) + nsrc = self.geometry.src_positions.shape[0] if savewav: self.src_wavefield = [] self.src_illumination = np.zeros(self.model.shape) for isrc in range(nsrc): - src_wav, src_ill = self._srcillumination_oneshot(isrc) + solver.geometry.src_positions = self.geometry.src_positions[isrc, :] + src_wav, src_ill = self._srcillumination_oneshot(solver, isrc) if savewav: self.src_wavefield.append(src_wav) self.src_illumination += src_ill @@ -359,11 +364,13 @@ def _born_allshots(self, dm: NDArray) -> NDArray: dtot = np.array(dtot).reshape(nsrc, d.shape[0], d.shape[1]) return dtot - def _bornadj_oneshot(self, isrc, dobs): + def _bornadj_oneshot(self, solver: AcousticWaveSolverType, isrc, dobs): """Adjoint born modelling for one shot Parameters ---------- + solver : :obj:`AcousticWaveSolver` + Devito's solver object. isrc : :obj:`float` Index of source to model dobs : :obj:`np.ndarray` @@ -375,22 +382,10 @@ def _bornadj_oneshot(self, isrc, dobs): Model """ - # create geometry for single source - geometry = AcquisitionGeometry( - self.model, - self.geometry.rec_positions, - self.geometry.src_positions[isrc, :], - self.geometry.t0, - self.geometry.tn, - f0=self.geometry.f0, - src_type=self.geometry.src_type, - ) # create boundary data recs = self.geometry.rec.copy() recs.data[:] = dobs.T[:] - solver = AcousticWaveSolver(self.model, geometry, space_order=self.space_order) - # assign source location to source object with custom wavelet if hasattr(self, "wav"): self.wav.coordinates.data[0, :] = self.geometry.src_positions[isrc, :] @@ -423,11 +418,25 @@ def _bornadj_allshots(self, dobs: NDArray) -> NDArray: Model """ + # create geometry for single source + geometry = AcquisitionGeometry( + self.model, + self.geometry.rec_positions, + self.geometry.src_positions[0, :], + self.geometry.t0, + self.geometry.tn, + f0=self.geometry.f0, + src_type=self.geometry.src_type, + ) + nsrc = self.geometry.src_positions.shape[0] mtot = np.zeros(self.model.shape, dtype=np.float32) + solver = AcousticWaveSolver(self.model, geometry, space_order=self.space_order) + for isrc in range(nsrc): - m = self._bornadj_oneshot(isrc, dobs[isrc]) + solver.geometry.src_positions = self.geometry.src_positions[isrc, :] + m = self._bornadj_oneshot(solver, isrc, dobs[isrc]) mtot += self._crop_model(m.data, self.model.nbl) return mtot From f866365699e768bb4856866dc0b4c5adb888b5f7 Mon Sep 17 00:00:00 2001 From: Gustavo Coelho Date: Thu, 27 Feb 2025 15:09:46 -0300 Subject: [PATCH 15/44] Fix flake8 --- pylops/waveeqprocessing/twoway.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylops/waveeqprocessing/twoway.py b/pylops/waveeqprocessing/twoway.py index b7298ba6e..4312c523a 100644 --- a/pylops/waveeqprocessing/twoway.py +++ b/pylops/waveeqprocessing/twoway.py @@ -428,7 +428,7 @@ def _bornadj_allshots(self, dobs: NDArray) -> NDArray: f0=self.geometry.f0, src_type=self.geometry.src_type, ) - + nsrc = self.geometry.src_positions.shape[0] mtot = np.zeros(self.model.shape, dtype=np.float32) From 2751eea98852393c364055bdf8b6dc597c8895d1 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Mon, 3 Mar 2025 14:27:38 +0000 Subject: [PATCH 16/44] ci: relax numpy requirements for dev env --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 8eb9f87d8..cdc9d0351 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,4 @@ -numpy>=1.21.0,<2 +numpy>=1.21.0 scipy>=1.11.0 jax numba From c59745b34d26662efda0055c6906c96f9ada6b4c Mon Sep 17 00:00:00 2001 From: mrava87 Date: Mon, 3 Mar 2025 14:35:00 +0000 Subject: [PATCH 17/44] ci: remove python3.9 from GA --- .github/workflows/build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index bd0345544..72645648d 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -7,7 +7,7 @@ jobs: strategy: matrix: platform: [ ubuntu-latest, macos-13 ] - python-version: ["3.9", "3.10", "3.11"] + python-version: ["3.10", "3.11"] runs-on: ${{ matrix.platform }} steps: From cd77a150bc5104ced0381e0b28318aaeb57759ce Mon Sep 17 00:00:00 2001 From: mrava87 Date: Mon, 3 Mar 2025 14:43:25 +0000 Subject: [PATCH 18/44] ci: move to python3.11 in azure-pipelines --- azure-pipelines.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index ab636d61d..706038bac 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -55,7 +55,7 @@ jobs: steps: - task: UsePythonVersion@0 inputs: - versionSpec: '3.9' + versionSpec: '3.11' architecture: 'x64' - script: | @@ -85,7 +85,7 @@ jobs: steps: - task: UsePythonVersion@0 inputs: - versionSpec: '3.9' + versionSpec: '3.11' architecture: 'x64' - script: | From 7488bb5f6b35d096e5482140c3fec22d4fe03797 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Wed, 5 Mar 2025 20:26:21 +0000 Subject: [PATCH 19/44] ci: switched to py311 in codacy-coverage GA --- .github/workflows/codacy-coverage-reporter.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codacy-coverage-reporter.yaml b/.github/workflows/codacy-coverage-reporter.yaml index ed90ca30d..05919f474 100644 --- a/.github/workflows/codacy-coverage-reporter.yaml +++ b/.github/workflows/codacy-coverage-reporter.yaml @@ -9,7 +9,7 @@ jobs: strategy: matrix: platform: [ ubuntu-latest, ] - python-version: ["3.9", ] + python-version: ["3.11", ] runs-on: ${{ matrix.platform }} steps: From bed03f7f6be7061cb747a846e655ed0a435585f4 Mon Sep 17 00:00:00 2001 From: Gustavo Coelho Date: Thu, 6 Mar 2025 12:32:58 -0300 Subject: [PATCH 20/44] Change to get source position directly from solver instead of self.geometry --- pylops/waveeqprocessing/twoway.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pylops/waveeqprocessing/twoway.py b/pylops/waveeqprocessing/twoway.py index 4312c523a..f4c4945fb 100644 --- a/pylops/waveeqprocessing/twoway.py +++ b/pylops/waveeqprocessing/twoway.py @@ -250,7 +250,7 @@ def _srcillumination_oneshot(self, solver: AcousticWaveSolverType, isrc: int) -> # assign source location to source object with custom wavelet if hasattr(self, "wav"): - self.wav.coordinates.data[0, :] = self.geometry.src_positions[isrc, :] + self.wav.coordinates.data[0, :] = solver.geometry.src_positions[:] # source wavefield u0 = solver.forward( @@ -388,7 +388,7 @@ def _bornadj_oneshot(self, solver: AcousticWaveSolverType, isrc, dobs): # assign source location to source object with custom wavelet if hasattr(self, "wav"): - self.wav.coordinates.data[0, :] = self.geometry.src_positions[isrc, :] + self.wav.coordinates.data[0, :] = solver.geometry.src_positions[:] # source wavefield if hasattr(self, "src_wavefield"): From b13122c12e9692eb0a291a182630cded50d3e2e0 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Wed, 12 Mar 2025 20:46:46 +0000 Subject: [PATCH 21/44] doc: fixed mistake in docstring of ps method --- pylops/avo/avo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pylops/avo/avo.py b/pylops/avo/avo.py index 6d74d04b4..35e979930 100644 --- a/pylops/avo/avo.py +++ b/pylops/avo/avo.py @@ -532,9 +532,9 @@ def ps( .. math:: \begin{align} - G_2(\theta) &= \tan \frac{\theta}{2} \left\{4 (V_S/V_P)^2 \sin^2 \theta + G_2(\theta) &= \frac{\tan \theta}{2} \left\{4 (V_S/V_P)^2 \sin^2 \theta - 4(V_S/V_P) \cos \theta \cos \phi \right\},\\ - G_3(\theta) &= -\tan \frac{\theta}{2} \left\{1 - 2 (V_S/V_P)^2 \sin^2 \theta + + G_3(\theta) &= - \frac{\tan \theta}{2} \left\{1 - 2 (V_S/V_P)^2 \sin^2 \theta + 2(V_S/V_P) \cos \theta \cos \phi\right\},\\ \frac{\Delta V_S}{\overline{V_S}} &= 2 \frac{V_{S,2}-V_{S,1}}{V_{S,2}+V_{S,1}},\\ \frac{\Delta \rho}{\overline{\rho}} &= 2 \frac{\rho_2-\rho_1}{\rho_2+\rho_1}. From 81d1548e8992b5f9915b53116ad89e16ae6a45d8 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Wed, 12 Mar 2025 21:44:45 +0000 Subject: [PATCH 22/44] minor: small change to ToCupy docstring --- pylops/basicoperators/tocupy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylops/basicoperators/tocupy.py b/pylops/basicoperators/tocupy.py index 8a3a7c6b9..c5b408f7b 100644 --- a/pylops/basicoperators/tocupy.py +++ b/pylops/basicoperators/tocupy.py @@ -13,7 +13,7 @@ class ToCupy(LinearOperator): r"""Convert to CuPy. - Convert an input array to CuPy in forward mode, + Convert an input NumPy array to CuPy in forward mode, and convert back to NumPy in adjoint mode. Parameters From 254beae797db1233edc3a1f8e2b833a2bd2ea97e Mon Sep 17 00:00:00 2001 From: mrava87 Date: Sun, 16 Mar 2025 21:52:39 +0000 Subject: [PATCH 23/44] feat: added optimal_coeff to omp --- examples/plot_ista.py | 25 ++++++-- pylops/optimization/cls_sparsity.py | 94 +++++++++++++++++++++++------ pylops/optimization/sparsity.py | 12 ++++ pytests/test_sparsity.py | 21 +++++++ 4 files changed, 127 insertions(+), 25 deletions(-) diff --git a/examples/plot_ista.py b/examples/plot_ista.py index c8c3d71af..caf0b995c 100755 --- a/examples/plot_ista.py +++ b/examples/plot_ista.py @@ -53,24 +53,26 @@ # MP/OMP eps = 1e-2 maxit = 500 -x_mp = pylops.optimization.sparsity.omp( +x_mp, nitermp, costmp = pylops.optimization.sparsity.omp( Aop, y, niter_outer=maxit, niter_inner=0, sigma=1e-4 -)[0] -x_omp = pylops.optimization.sparsity.omp(Aop, y, niter_outer=maxit, sigma=1e-4)[0] +) +x_omp, niteromp, costomp = pylops.optimization.sparsity.omp( + Aop, y, niter_outer=maxit, sigma=1e-4 +) # IRLS x_irls = pylops.optimization.sparsity.irls( - Aop, y, nouter=50, epsI=1e-5, kind="model", **dict(iter_lim=10) + Aop, y, nouter=maxit, epsI=1e-5, kind="model", **dict(iter_lim=10) )[0] # ISTA -x_ista = pylops.optimization.sparsity.ista( +x_ista, niteri, costi = pylops.optimization.sparsity.ista( Aop, y, niter=maxit, eps=eps, tol=1e-3, -)[0] +) fig, ax = plt.subplots(1, 1, figsize=(8, 3)) m, s, b = ax.stem(x, linefmt="k", basefmt="k", markerfmt="ko", label="True") @@ -87,6 +89,17 @@ ax.legend() plt.tight_layout() +fig, ax = plt.subplots(1, 1, figsize=(8, 3)) +ax.plot(costmp, "c", lw=2, label=r"$x_{MP} (niter=%d)$" % nitermp) +ax.plot(costomp, "g", lw=2, label=r"$x_{OMP} (niter=%d)$" % niteromp) +ax.plot(costi[: nitermp + 5], "r", lw=2, label=r"$x_{ISTA} (niter=%d)$" % niteri) +ax.set_title("Cost function", size=15, fontweight="bold") +ax.set_xlabel("Iteration") +ax.legend() +ax.grid(True, which="both") +plt.tight_layout() + + ############################################################################### # We now consider a more interesting problem problem, *wavelet deconvolution* # from a signal that we assume being composed by a train of spikes convolved diff --git a/pylops/optimization/cls_sparsity.py b/pylops/optimization/cls_sparsity.py index fb5366beb..ceb4c6700 100644 --- a/pylops/optimization/cls_sparsity.py +++ b/pylops/optimization/cls_sparsity.py @@ -696,21 +696,42 @@ class OMP(Solver): \DeclareMathOperator*{\argmin}{arg\,min} \DeclareMathOperator*{\argmax}{arg\,max} \Lambda_k = \Lambda_{k-1} \cup \left\{\argmax_j - \left|\mathbf{Op}_j^H\,\mathbf{r}_k\right| \right\} \\ + \left|\mathbf{Op}^{j H}\,\mathbf{r}_k\right| \right\} \\ \mathbf{x}_k = \argmin_{\mathbf{x}} \left\|\mathbf{Op}_{\Lambda_k}\,\mathbf{x} - \mathbf{y}\right\|_2^2 + where :math:`\mathbf{Op}^j` is the :math:`j`-th column of the operator, + :math:`\mathbf{r}_k` is the residual at iteration :math:`k`, and + :math:`\mathbf{Op}_{\Lambda_k}` is the operator restricted to the columns + in the set :math:`\Lambda_k`. + Note that by choosing ``niter_inner=0`` the basic Matching Pursuit (MP) algorithm is implemented instead. In other words, instead of solving an optimization at each iteration to find the best :math:`\mathbf{x}` for the - currently selected basis functions, the vector :math:`\mathbf{x}` is just - updated at the new basis function by taking directly the value from - the inner product :math:`\mathbf{Op}_j^H\,\mathbf{r}_k`. - - In this case it is highly recommended to provide a normalized basis - function. If different basis have different norms, the solver is likely - to diverge. Similar observations apply to OMP, even though mild unbalancing - between the basis is generally properly handled. + currently selected basis functions, either the vector :math:`\mathbf{x}` + is just updated at the new basis function by adding the value from + the inner product :math:`\mathbf{Op}_j^H\,\mathbf{r}_k` to the current value + (``optimal_coeff=False``) or the optimal coefficient that minimizes the norm + of the residual :math:`\mathbf{r} - c * \mathbf{Op}^j` is estimated + (``optimal_coeff=True``) and added to the current value. + + In the case the MP solver is used, it is highly recommended to provide a + normalized basis function. If different basis have different norms, the + solver is likely to diverge. Similar observations apply to OMP, even + though mild unbalancing between the basis is generally properly handled. + Two possible ways to handle the scenario fo non-normalized basis functions + are: + + - Find the normalization factor of the the basis functions before + running the solver (this is done by choosing ``normalizecols=True``); + - Find the optimal coefficient that minimizes the norm of the residual + :math:`\mathbf{r} - c * \mathbf{Op}^j` at every iteration (this is + done by choosing ``optimal_coeff=True``). + + Finally, when the operator is a chain of operators, with the rigth-most + representing the basis function, if the operator of the basis function is + provided in the ``Opbasis`` parameter, the solver will use this operator + to find the normalization factor for each column of the operator. """ @@ -742,6 +763,8 @@ def setup( niter_inner: int = 40, sigma: float = 1e-4, normalizecols: bool = False, + Opbasis: Optional["LinearOperator"] = None, + optimal_coeff: bool = False, show: bool = False, ) -> None: r"""Setup solver @@ -763,6 +786,14 @@ def setup( :math:`n_{cols}` times to unit vectors (i.e., containing 1 at position j and zero otherwise); use only when the columns of the operator are expected to have highly varying norms. + Opbasis : :obj:`pylops.LinearOperator` + Operator representing the basis functions. If ``None``, the entire + operator used for inversion `Op` is used. + optimal_coeff : :obj:`bool`, optional + Estimate optimal coefficient that minimizes the norm of the residual + :math:`\mathbf{r} - c * \mathbf{Op}^j) norm (``True``) or use the + directly the value from the inner product + :math:`\mathbf{Op}_j^H\,\mathbf{r}_k`. show : :obj:`bool`, optional Display setup log @@ -772,17 +803,19 @@ def setup( self.niter_inner = niter_inner self.sigma = sigma self.normalizecols = normalizecols + self.Opbasis = Opbasis if Opbasis is not None else self.Op + self.optimal_coeff = optimal_coeff self.ncp = get_array_module(y) # find normalization factor for each column if self.normalizecols: - ncols = self.Op.shape[1] + ncols = self.Opbasis.shape[1] self.norms = self.ncp.zeros(ncols) for icol in range(ncols): - unit = self.ncp.zeros(ncols, dtype=self.Op.dtype) + unit = self.ncp.zeros(ncols, dtype=self.Opbasis.dtype) unit[icol] = 1 - self.norms[icol] = np.linalg.norm(self.Op.matvec(unit)) - + self.norms[icol] = np.linalg.norm(self.Opbasis.matvec(unit)) + print(f"{self.norms = }") # create variables to track the residual norm and iterations self.res = self.y.copy() self.cost = [ @@ -820,9 +853,9 @@ def step( """ # compute inner products cres = self.Op.rmatvec(self.res) - cres_abs = np.abs(cres) if self.normalizecols: - cres_abs = cres_abs / self.norms + cres = cres / self.norms + cres_abs = np.abs(cres) # choose column with max cres cres_max = np.max(cres_abs) imax = np.argwhere(cres_abs == cres_max).ravel() @@ -847,11 +880,22 @@ def step( int(imax), ] ) - self.res -= Opcol.matvec(cres[imax] * self.ncp.ones(1)) - if addnew: - x.append(cres[imax]) + if not self.optimal_coeff: + # update with coefficient that maximizes the inner product + self.res -= Opcol.matvec(cres[imax] * self.ncp.ones(1)) + if addnew: + x.append(cres[imax]) + else: + x[imax_in_cols] += cres[imax] else: - x[imax_in_cols] += cres[imax] + # find optimal coefficient that minimizes the residual (r - cres * col) + col = Opcol.matvec(self.ncp.ones(1, dtype=Opcol.dtype)) + cresopt = (Opcol.rmatvec(self.res) / Opcol.rmatvec(col))[0] + self.res -= Opcol.matvec(cresopt * self.ncp.ones(1)) + if addnew: + x.append(cresopt) + else: + x[imax_in_cols] += cresopt else: # OMP update Opcol = self.Op.apply_columns(cols) @@ -958,6 +1002,8 @@ def solve( niter_inner: int = 40, sigma: float = 1e-4, normalizecols: bool = False, + Opbasis: Optional["LinearOperator"] = None, + optimal_coeff: bool = False, show: bool = False, itershow: Tuple[int, int, int] = (10, 10, 10), ) -> Tuple[NDArray, int, NDArray]: @@ -980,6 +1026,14 @@ def solve( :math:`n_{cols}` times to unit vectors (i.e., containing 1 at position j and zero otherwise); use only when the columns of the operator are expected to have highly varying norms. + Opbasis : :obj:`pylops.LinearOperator` + Operator representing the basis functions. If ``None``, the entire + operator used for inversion `Op` is used. + optimal_coeff : :obj:`bool`, optional + Estimate optimal coefficient that minimizes the norm of the residual + :math:`\mathbf{r} - c * \mathbf{Op}^j) norm (``True``) or use the + directly the value from the inner product + :math:`\mathbf{Op}_j^H\,\mathbf{r}_k`. show : :obj:`bool`, optional Display logs itershow : :obj:`tuple`, optional @@ -1003,6 +1057,8 @@ def solve( niter_inner=niter_inner, sigma=sigma, normalizecols=normalizecols, + Opbasis=Opbasis, + optimal_coeff=optimal_coeff, show=show, ) x: List[NDArray] = [] diff --git a/pylops/optimization/sparsity.py b/pylops/optimization/sparsity.py index f612aa1b4..6838e9461 100644 --- a/pylops/optimization/sparsity.py +++ b/pylops/optimization/sparsity.py @@ -129,6 +129,8 @@ def omp( niter_inner: int = 40, sigma: float = 1e-4, normalizecols: bool = False, + Opbasis: Optional["LinearOperator"] = None, + optimal_coeff: bool = False, show: bool = False, itershow: Tuple[int, int, int] = (10, 10, 10), callback: Optional[Callable] = None, @@ -159,6 +161,14 @@ def omp( :math:`n_{cols}` times to unit vectors (i.e., containing 1 at position j and zero otherwise); use only when the columns of the operator are expected to have highly varying norms. + Opbasis : :obj:`pylops.LinearOperator` + Operator representing the basis functions. If not provided, the entire + operator used for inversion `Op` is used. + optimal_coeff : :obj:`bool`, optional + Estimate optimal coefficient that minimizes the norm of the residual + :math:`\mathbf{r} - c * \mathbf{Op}^j) norm (``True``) or use the + directly the value from the inner product + :math:`\mathbf{Op}_j^H\,\mathbf{r}_k`. show : :obj:`bool`, optional Display iterations log itershow : :obj:`tuple`, optional @@ -200,6 +210,8 @@ def omp( niter_inner=niter_inner, sigma=sigma, normalizecols=normalizecols, + Opbasis=Opbasis, + optimal_coeff=optimal_coeff, show=show, itershow=itershow, ) diff --git a/pytests/test_sparsity.py b/pytests/test_sparsity.py index ef4c0d6e0..671c28894 100644 --- a/pytests/test_sparsity.py +++ b/pytests/test_sparsity.py @@ -171,6 +171,27 @@ def test_IRLS_model(par): assert_array_almost_equal(x, xinv, decimal=1) +@pytest.mark.parametrize("par", [(par1), (par3), (par5), (par1j), (par3j), (par5j)]) +def test_MP(par): + """Invert problem with MP""" + np.random.seed(42) + Aop = MatrixMult(np.random.randn(par["ny"], par["nx"])) + + x = np.zeros(par["nx"]) + x[par["nx"] // 2] = 1 + x[3] = 1 + x[par["nx"] - 4] = -1 + y = Aop * x + + sigma = 1e-4 + maxit = 100 + + xinv, _, _ = omp( + Aop, y, maxit, niter_inner=0, optimal_coeff=True, sigma=sigma, show=False + ) + assert_array_almost_equal(x, xinv, decimal=1) + + @pytest.mark.parametrize("par", [(par1), (par3), (par5), (par1j), (par3j), (par5j)]) def test_OMP(par): """Invert problem with OMP""" From 934f935ac77bd5d63c71afd99427266d7d1cebda Mon Sep 17 00:00:00 2001 From: mrava87 Date: Tue, 18 Mar 2025 21:26:47 +0000 Subject: [PATCH 24/44] bug: avoid passing directly explicit to _ColumnLinearOperator --- pylops/linearoperator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylops/linearoperator.py b/pylops/linearoperator.py index 661178f53..ce1b0202b 100644 --- a/pylops/linearoperator.py +++ b/pylops/linearoperator.py @@ -1328,7 +1328,7 @@ def __init__( ) -> None: if not isinstance(Op, LinearOperator): raise TypeError("Op must be a LinearOperator") - super(_ColumnLinearOperator, self).__init__(Op, explicit=Op.explicit) + super(_ColumnLinearOperator, self).__init__(Op) self.Op = Op self.cols = cols self._shape = (Op.shape[0], len(cols)) From 30fb3eb648f8c476cbf6fa196abaff9ab3fb67b6 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Thu, 10 Apr 2025 22:16:51 +0100 Subject: [PATCH 25/44] bug: fix pad in convolve1d to work with cupy --- pylops/signalprocessing/convolve1d.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pylops/signalprocessing/convolve1d.py b/pylops/signalprocessing/convolve1d.py index bc154a94e..5050149f3 100644 --- a/pylops/signalprocessing/convolve1d.py +++ b/pylops/signalprocessing/convolve1d.py @@ -144,11 +144,11 @@ def __init__( self.offset -= 1 self.hstar = ncp.flip(self.h, axis=-1) - self.pad = ncp.zeros((len(dims), 2), dtype=int) + self.pad = np.zeros((len(dims), 2), dtype=int) self.pad[self.axis, 0] = max(self.offset, 0) self.pad[self.axis, 1] = -min(self.offset, 0) - self.padd = ncp.zeros((len(dims), 2), dtype=int) + self.padd = np.zeros((len(dims), 2), dtype=int) self.padd[self.axis, 1] = max(self.offset, 0) self.padd[self.axis, 0] = -min(self.offset, 0) From 5fea17248c740933e4e8dc1545acae06ff759035 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Fri, 11 Apr 2025 23:07:22 +0100 Subject: [PATCH 26/44] bug: fix num_threads_per_blocks from size of 2 to 3 --- pylops/signalprocessing/fourierradon3d.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pylops/signalprocessing/fourierradon3d.py b/pylops/signalprocessing/fourierradon3d.py index b6c8a309a..6b2c3c7d4 100755 --- a/pylops/signalprocessing/fourierradon3d.py +++ b/pylops/signalprocessing/fourierradon3d.py @@ -50,17 +50,17 @@ class FourierRadon3D(LinearOperator): flims : :obj:`tuple`, optional Indices of lower and upper limits of Fourier axis to be used in the application of the Radon matrix (when ``None``, use entire axis) - kind : :obj:`tuple` + kind : :obj:`tuple`, optional Curves to be used for stacking/spreading along the y- and x- axes (``("linear", "linear")``, ``("linear", "parabolic")``, ``("parabolic", "linear")``, or ``("parabolic", "parabolic")``) - engine : :obj:`str` + engine : :obj:`str`, optional Engine used for computation (``numpy`` or ``numba`` or ``cuda``) - num_threads_per_blocks : :obj:`tuple` + num_threads_per_blocks : :obj:`tuple`, optional Number of threads in each block (only when ``engine=cuda``) - dtype : :obj:`str` + dtype : :obj:`str`, optional Type of elements in input array. - name : :obj:`str` + name : :obj:`str`, optional Name of operator (to be used by :func:`pylops.utils.describe.describe`) Attributes @@ -128,7 +128,7 @@ def __init__( flims: Optional[Tuple[int, int]] = None, kind: Tuple[str, str] = ("linear", "linear"), engine: str = "numpy", - num_threads_per_blocks: Tuple[int, int] = (32, 32), + num_threads_per_blocks: Tuple[int, int, int] = (2, 16, 16), dtype: DTypeLike = "float64", name: str = "R", ) -> None: From 7a1b4c6d8aeb52837bbf170e03f642835dc274ec Mon Sep 17 00:00:00 2001 From: mrava87 Date: Sat, 26 Apr 2025 22:44:44 +0100 Subject: [PATCH 27/44] bug: fix mdd to run with cupy when twosided=True and add_negative=True --- pylops/waveeqprocessing/mdd.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/pylops/waveeqprocessing/mdd.py b/pylops/waveeqprocessing/mdd.py index 90e2cba3b..02f61b3d3 100644 --- a/pylops/waveeqprocessing/mdd.py +++ b/pylops/waveeqprocessing/mdd.py @@ -405,17 +405,19 @@ def MDD( # Add negative part to data and model if twosided and add_negative: - G = np.concatenate((ncp.zeros((ns, nr, nt - 1)), G), axis=-1) - d = np.concatenate((np.squeeze(np.zeros((ns, nv, nt - 1))), d), axis=-1) + G = ncp.concatenate((ncp.zeros((ns, nr, nt - 1), dtype=G.dtype), G), axis=-1) + d = ncp.concatenate( + (ncp.squeeze(ncp.zeros((ns, nv, nt - 1), dtype=d.dtype)), d), axis=-1 + ) # Bring kernel to frequency domain - Gfft = np.fft.rfft(G, nt2, axis=-1) + Gfft = ncp.fft.rfft(G, nt2, axis=-1) Gfft = Gfft[..., :nfmax] # Bring frequency/time to first dimension - Gfft = np.moveaxis(Gfft, -1, 0) - d = np.moveaxis(d, -1, 0) + Gfft = ncp.moveaxis(Gfft, -1, 0) + d = ncp.moveaxis(d, -1, 0) if psf: - G = np.moveaxis(G, -1, 0) + G = ncp.moveaxis(G, -1, 0) # Define MDC linear operator MDCop = MDC( @@ -455,12 +457,12 @@ def MDD( # Adjoint if adjoint: madj = MDCop.H * d.ravel() - madj = np.squeeze(madj.reshape(nt2, nr, nv)) - madj = np.moveaxis(madj, 0, -1) + madj = ncp.squeeze(madj.reshape(nt2, nr, nv)) + madj = ncp.moveaxis(madj, 0, -1) if psf: psfadj = PSFop.H * G.ravel() - psfadj = np.squeeze(psfadj.reshape(nt2, nr, nr)) - psfadj = np.moveaxis(psfadj, 0, -1) + psfadj = ncp.squeeze(psfadj.reshape(nt2, nr, nr)) + psfadj = ncp.moveaxis(psfadj, 0, -1) # Inverse if twosided and causality_precond: @@ -481,8 +483,8 @@ def MDD( ncp.zeros(int(MDCop.shape[1]), dtype=MDCop.dtype), **kwargs_solver )[0] - minv = np.squeeze(minv.reshape(nt2, nr, nv)) - minv = np.moveaxis(minv, 0, -1) + minv = ncp.squeeze(minv.reshape(nt2, nr, nv)) + minv = ncp.moveaxis(minv, 0, -1) if wav is not None: wav1 = wav.copy() @@ -500,8 +502,8 @@ def MDD( ncp.zeros(int(PSFop.shape[1]), dtype=PSFop.dtype), **kwargs_solver )[0] - psfinv = np.squeeze(psfinv.reshape(nt2, nr, nr)) - psfinv = np.moveaxis(psfinv, 0, -1) + psfinv = ncp.squeeze(psfinv.reshape(nt2, nr, nr)) + psfinv = ncp.moveaxis(psfinv, 0, -1) if wav is not None: wav1 = wav.copy() for _ in range(psfinv.ndim - 1): From b1b2cbaa3d9b11721abebde25a1cd204757b34cd Mon Sep 17 00:00:00 2001 From: mrava87 Date: Sun, 27 Apr 2025 23:15:22 +0100 Subject: [PATCH 28/44] ci: modified test suite to run on cupy --- Makefile | 10 +- environment-dev-arm.yml | 2 +- environment-dev-gpu.yml | 34 ++ environment-dev.yml | 2 +- environment.yml | 2 +- pytests/test_avo.py | 102 ++-- pytests/test_basicoperators.py | 189 ++++-- pytests/test_blending.py | 30 +- pytests/test_causalintegration.py | 47 +- pytests/test_chirpradon.py | 38 +- pytests/test_combine.py | 223 ++++++-- pytests/test_convolve.py | 113 +++- pytests/test_dct.py | 14 + pytests/test_derivative.py | 124 +++- pytests/test_describe.py | 7 +- pytests/test_diagonal.py | 78 ++- pytests/test_directwave.py | 9 + pytests/test_dtcwt.py | 14 + pytests/test_dwts.py | 26 + pytests/test_eigs.py | 21 +- pytests/test_estimators.py | 54 +- pytests/test_ffts.py | 825 ++++++++++++++++----------- pytests/test_fourierradon.py | 145 +++-- pytests/test_fredholm.py | 30 +- pytests/test_functionoperator.py | 29 +- pytests/test_interpolation.py | 0 pytests/test_jaxoperator.py | 18 +- pytests/test_kirchhoff.py | 23 + pytests/test_kronecker.py | 39 +- pytests/test_leastsquares.py | 67 ++- pytests/test_linearoperator.py | 32 +- pytests/test_lsm.py | 8 + pytests/test_marchenko.py | 107 ++-- pytests/test_memoizeoperator.py | 16 +- pytests/test_metrics.py | 13 +- pytests/test_nonstatconvolve.py | 104 +++- pytests/test_oneway.py | 35 +- pytests/test_pad.py | 18 +- pytests/test_patching.py | 17 +- pytests/test_poststack.py | 54 +- pytests/test_prestack.py | 92 ++- pytests/test_pytensoroperator.py | 16 +- pytests/test_radon.py | 11 + pytests/test_restriction.py | 45 +- pytests/test_seislet.py | 11 + pytests/test_seismicevents.py | 26 + pytests/test_seismicinterpolation.py | 9 +- pytests/test_shift.py | 104 +++- pytests/test_signalutils.py | 14 + pytests/test_sliding.py | 35 +- pytests/test_smoothing.py | 75 ++- pytests/test_solver.py | 21 +- pytests/test_sparsity.py | 137 +++-- pytests/test_tapers.py | 8 + pytests/test_torchoperator.py | 37 +- pytests/test_transpose.py | 33 +- pytests/test_twoway.py | 16 +- pytests/test_utils.py | 8 +- pytests/test_wavedecomposition.py | 64 ++- pytests/test_waveeqprocessing.py | 99 ++-- pytests/test_wavelets.py | 14 + requirements-dev-gpu.txt | 25 + requirements-torch-gpu.txt | 2 + 63 files changed, 2638 insertions(+), 983 deletions(-) mode change 100755 => 100644 Makefile mode change 100755 => 100644 environment-dev-arm.yml create mode 100644 environment-dev-gpu.yml mode change 100755 => 100644 environment-dev.yml mode change 100755 => 100644 environment.yml mode change 100755 => 100644 pytests/test_avo.py mode change 100755 => 100644 pytests/test_basicoperators.py mode change 100755 => 100644 pytests/test_blending.py mode change 100755 => 100644 pytests/test_causalintegration.py mode change 100755 => 100644 pytests/test_chirpradon.py mode change 100755 => 100644 pytests/test_combine.py mode change 100755 => 100644 pytests/test_convolve.py mode change 100755 => 100644 pytests/test_derivative.py mode change 100755 => 100644 pytests/test_describe.py mode change 100755 => 100644 pytests/test_diagonal.py mode change 100755 => 100644 pytests/test_directwave.py mode change 100755 => 100644 pytests/test_dwts.py mode change 100755 => 100644 pytests/test_eigs.py mode change 100755 => 100644 pytests/test_ffts.py mode change 100755 => 100644 pytests/test_fourierradon.py mode change 100755 => 100644 pytests/test_fredholm.py mode change 100755 => 100644 pytests/test_functionoperator.py mode change 100755 => 100644 pytests/test_interpolation.py mode change 100755 => 100644 pytests/test_jaxoperator.py mode change 100755 => 100644 pytests/test_kirchhoff.py mode change 100755 => 100644 pytests/test_kronecker.py mode change 100755 => 100644 pytests/test_leastsquares.py mode change 100755 => 100644 pytests/test_linearoperator.py mode change 100755 => 100644 pytests/test_lsm.py mode change 100755 => 100644 pytests/test_marchenko.py mode change 100755 => 100644 pytests/test_memoizeoperator.py mode change 100755 => 100644 pytests/test_metrics.py mode change 100755 => 100644 pytests/test_nonstatconvolve.py mode change 100755 => 100644 pytests/test_oneway.py mode change 100755 => 100644 pytests/test_pad.py mode change 100755 => 100644 pytests/test_patching.py mode change 100755 => 100644 pytests/test_poststack.py mode change 100755 => 100644 pytests/test_prestack.py mode change 100755 => 100644 pytests/test_pytensoroperator.py mode change 100755 => 100644 pytests/test_radon.py mode change 100755 => 100644 pytests/test_restriction.py mode change 100755 => 100644 pytests/test_seislet.py mode change 100755 => 100644 pytests/test_seismicevents.py mode change 100755 => 100644 pytests/test_seismicinterpolation.py mode change 100755 => 100644 pytests/test_shift.py mode change 100755 => 100644 pytests/test_signalutils.py mode change 100755 => 100644 pytests/test_sliding.py mode change 100755 => 100644 pytests/test_smoothing.py mode change 100755 => 100644 pytests/test_tapers.py mode change 100755 => 100644 pytests/test_torchoperator.py mode change 100755 => 100644 pytests/test_transpose.py mode change 100755 => 100644 pytests/test_twoway.py mode change 100755 => 100644 pytests/test_wavedecomposition.py mode change 100755 => 100644 pytests/test_waveeqprocessing.py mode change 100755 => 100644 pytests/test_wavelets.py create mode 100644 requirements-dev-gpu.txt create mode 100644 requirements-torch-gpu.txt diff --git a/Makefile b/Makefile old mode 100755 new mode 100644 index 14a0a26bf..8ae376e78 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ PIP := $(shell command -v pip3 2> /dev/null || command which pip 2> /dev/null) PYTHON := $(shell command -v python3 2> /dev/null || command which python 2> /dev/null) -.PHONY: install dev-install install_conda dev-install_conda tests doc docupdate servedoc lint typeannot coverage +.PHONY: install dev-install dev-install_gpu install_conda dev-install_conda dev-install_conda_arm tests doc docupdate servedoc lint typeannot coverage pipcheck: ifndef PIP @@ -24,6 +24,11 @@ dev-install: $(PIP) install -r requirements-dev.txt &&\ $(PIP) install -r requirements-torch.txt && $(PIP) install -e . +dev-install_gpu: + make pipcheck + $(PIP) install -r requirements-dev-gpu.txt &&\ + $(PIP) install -e . + install_conda: conda env create -f environment.yml && conda activate pylops && pip install . @@ -33,6 +38,9 @@ dev-install_conda: dev-install_conda_arm: conda env create -f environment-dev-arm.yml && conda activate pylops && pip install -e . +dev-install_conda_gpu: + conda env create -f environment-dev-gpu.yml && conda activate pylops_gpu && pip install -e . + tests: make pythoncheck pytest diff --git a/environment-dev-arm.yml b/environment-dev-arm.yml old mode 100755 new mode 100644 index 0081a0ad4..c98dd04ec --- a/environment-dev-arm.yml +++ b/environment-dev-arm.yml @@ -5,7 +5,7 @@ channels: - numba - pytorch dependencies: - - python>=3.6.4 + - python>=3.9.0 - pip - numpy>=1.21.0 - scipy>=1.11.0 diff --git a/environment-dev-gpu.yml b/environment-dev-gpu.yml new file mode 100644 index 000000000..7273e7f7c --- /dev/null +++ b/environment-dev-gpu.yml @@ -0,0 +1,34 @@ +name: pylops_gpu +channels: + - conda-forge + - defaults + - numba +dependencies: + - python>=3.11.0 + - pip + - numpy>=1.21.0 + - scipy>=1.11.0 + - cupy + - sympy + - matplotlib + - ipython + - pytest + - Sphinx + - numpydoc + - numba + - icc_rt + - pre-commit + - autopep8 + - isort + - black + - pip: + - torch + - pytest-runner + - setuptools_scm + - pydata-sphinx-theme + - sphinx-gallery + - nbsphinx + - sphinxemoji + - image + - flake8 + - mypy diff --git a/environment-dev.yml b/environment-dev.yml old mode 100755 new mode 100644 index eb51c4dc0..dfefcd9d4 --- a/environment-dev.yml +++ b/environment-dev.yml @@ -5,7 +5,7 @@ channels: - numba - pytorch dependencies: - - python>=3.6.4 + - python>=3.9.0 - pip - numpy>=1.21.0 - scipy>=1.11.0 diff --git a/environment.yml b/environment.yml old mode 100755 new mode 100644 index e09650ded..86be65b17 --- a/environment.yml +++ b/environment.yml @@ -2,6 +2,6 @@ name: pylops channels: - defaults dependencies: - - python>=3.6.4 + - python>=3.9.0 - numpy>=1.21.0 - scipy>=1.14.0 diff --git a/pytests/test_avo.py b/pytests/test_avo.py old mode 100755 new mode 100644 index 2d0261ff4..ab526d507 --- a/pytests/test_avo.py +++ b/pytests/test_avo.py @@ -1,8 +1,18 @@ -import numpy as np +import os + +if int(os.environ.get("TEST_CUPY_PYLOPS", 0)): + import cupy as np + from cupy.testing import assert_array_almost_equal + + backend = "cupy" +else: + import numpy as np + from numpy.testing import assert_array_almost_equal + + backend = "numpy" +import numpy as npp import pytest -from numpy.testing import assert_array_almost_equal from scipy.signal import filtfilt -from scipy.sparse.linalg import lsqr from pylops.avo.avo import ( akirichards, @@ -13,7 +23,9 @@ zoeppritz_scattering, ) from pylops.avo.prestack import AVOLinearModelling +from pylops.optimization.basic import lsqr from pylops.utils import dottest +from pylops.utils.backend import to_numpy np.random.seed(0) @@ -24,16 +36,20 @@ # Create medium parameters for multiple contrasts nt0 = 201 dt0 = 0.004 -t0 = np.arange(nt0) * dt0 -vp = 1200 + np.arange(nt0) + filtfilt(np.ones(5) / 5.0, 1, np.random.normal(0, 80, nt0)) -vs = 600 + vp / 2 + filtfilt(np.ones(5) / 5.0, 1, np.random.normal(0, 20, nt0)) -rho = 1000 + vp + filtfilt(np.ones(5) / 5.0, 1, np.random.normal(0, 30, nt0)) -m = (np.stack((np.log(vp), np.log(vs), np.log(rho)), axis=1)).ravel() +t0 = npp.arange(nt0) * dt0 +vp = ( + 1200 + + npp.arange(nt0) + + filtfilt(npp.ones(5) / 5.0, 1, npp.random.normal(0, 80, nt0)) +) +vs = 600 + vp / 2 + filtfilt(npp.ones(5) / 5.0, 1, npp.random.normal(0, 20, nt0)) +rho = 1000 + vp + filtfilt(npp.ones(5) / 5.0, 1, npp.random.normal(0, 30, nt0)) +m = npp.stack((npp.log(vp), npp.log(vs), npp.log(rho)), axis=1).ravel() # Angles ntheta = 21 thetamin, thetamax = 0, 40 -theta = np.linspace(thetamin, thetamax, ntheta) +theta = npp.linspace(thetamin, thetamax, ntheta) # Parameters par1 = {"vsvp": 0.5, "linearization": "akirich"} # constant vsvp @@ -47,16 +63,16 @@ def test_zoeppritz(): ``_ as benchmark """ - r_zoep = zoeppritz_scattering(vp1, vs1, rho1, vp0, vs0, rho0, theta[0]) + r_zoep = zoeppritz_scattering(vp1, vs1, rho1, vp0, vs0, rho0, np.asarray(theta)[:1]) rpp_zoep = zoeppritz_element( - vp1, vs1, rho1, vp0, vs0, rho0, theta[0], element="PdPu" + vp1, vs1, rho1, vp0, vs0, rho0, np.asarray(theta)[:1], element="PdPu" ) - rpp_zoep1 = zoeppritz_pp(vp1, vs1, rho1, vp0, vs0, rho0, theta[0]) + rpp_zoep1 = zoeppritz_pp(vp1, vs1, rho1, vp0, vs0, rho0, np.asarray(theta)[:1]) assert r_zoep.shape == (4, 4, 1) - assert r_zoep[0, 0] == pytest.approx(0.04658, rel=1e-3) - assert rpp_zoep == pytest.approx(0.04658, rel=1e-3) - assert rpp_zoep1 == pytest.approx(0.04658, rel=1e-3) + assert to_numpy(r_zoep[0, 0, 0]) == pytest.approx(0.04658, rel=1e-3) + assert to_numpy(rpp_zoep) == pytest.approx(0.04658, rel=1e-3) + assert to_numpy(rpp_zoep1) == pytest.approx(0.04658, rel=1e-3) def test_zoeppritz_and_approx_zeroangle(): @@ -66,22 +82,24 @@ def test_zoeppritz_and_approx_zeroangle(): ai1, si1, _ = vp1 * rho1, vs1 * rho1, vp1 / vs1 # Zoeppritz - rpp_zoep = zoeppritz_pp(vp1, vs1, rho1, vp0, vs0, rho0, theta[0]) - rpp_zoep_approx = approx_zoeppritz_pp(vp1, vs1, rho1, vp0, vs0, rho0, theta[0]) + rpp_zoep = zoeppritz_pp(vp1, vs1, rho1, vp0, vs0, rho0, np.asarray(theta)[:1]) + rpp_zoep_approx = approx_zoeppritz_pp( + vp1, vs1, rho1, vp0, vs0, rho0, np.asarray(theta)[:1] + ) # Aki Richards - rvp = np.log(vp0) - np.log(vp1) - rvs = np.log(vs0) - np.log(vs1) - rrho = np.log(rho0) - np.log(rho1) + rvp = np.asarray(np.log(vp0) - np.log(vp1)) + rvs = np.asarray(np.log(vs0) - np.log(vs1)) + rrho = np.asarray(np.log(rho0) - np.log(rho1)) - G1, G2, G3 = akirichards(theta[0], vs1 / vp1) + G1, G2, G3 = akirichards(np.asarray(theta)[:1], vs1 / vp1) rpp_aki = G1 * rvp + G2 * rvs + G3 * rrho # Fatti - rai = np.log(ai0) - np.log(ai1) - rsi = np.log(si0) - np.log(si1) + rai = np.asarray(np.log(ai0) - np.log(ai1)) + rsi = np.asarray(np.log(si0) - np.log(si1)) - G1, G2, G3 = fatti(theta[0], vs1 / vp1) + G1, G2, G3 = fatti(np.asarray(theta)[:1], vs1 / vp1) rpp_fatti = G1 * rai + G2 * rsi + G3 * rrho assert_array_almost_equal(rpp_zoep, rpp_zoep_approx, decimal=3) @@ -97,22 +115,24 @@ def test_zoeppritz_and_approx_multipleangles(): ai1, si1 = vp1 * rho1, vs1 * rho1 # Zoeppritz - rpp_zoep = zoeppritz_pp(vp1, vs1, rho1, vp0, vs0, rho0, theta) - rpp_zoep_approx = approx_zoeppritz_pp(vp1, vs1, rho1, vp0, vs0, rho0, theta) + rpp_zoep = zoeppritz_pp(vp1, vs1, rho1, vp0, vs0, rho0, np.asarray(theta)) + rpp_zoep_approx = approx_zoeppritz_pp( + vp1, vs1, rho1, vp0, vs0, rho0, np.asarray(theta) + ) # Aki Richards - rvp = np.log(vp0) - np.log(vp1) - rvs = np.log(vs0) - np.log(vs1) - rrho = np.log(rho0) - np.log(rho1) + rvp = np.asarray(np.log(vp0) - np.log(vp1)) + rvs = np.asarray(np.log(vs0) - np.log(vs1)) + rrho = np.asarray(np.log(rho0) - np.log(rho1)) - G1, G2, G3 = akirichards(theta, vs1 / vp1) + G1, G2, G3 = akirichards(np.asarray(theta), vs1 / vp1) rpp_aki = G1 * rvp + G2 * rvs + G3 * rrho # Fatti - rai = np.log(ai0) - np.log(ai1) - rsi = np.log(si0) - np.log(si1) + rai = np.asarray(np.log(ai0) - np.log(ai1)) + rsi = np.asarray(np.log(si0) - np.log(si1)) - G1, G2, G3 = fatti(theta, vs1 / vp1) + G1, G2, G3 = fatti(np.asarray(theta), vs1 / vp1) rpp_fatti = G1 * rai + G2 * rsi + G3 * rrho assert_array_almost_equal(rpp_zoep, rpp_zoep_approx, decimal=3) @@ -124,11 +144,21 @@ def test_zoeppritz_and_approx_multipleangles(): def test_AVOLinearModelling(par): """Dot-test and inversion for AVOLinearModelling""" AVOop = AVOLinearModelling( - theta, vsvp=par["vsvp"], nt0=nt0, linearization=par["linearization"] + np.asarray(theta), + vsvp=par["vsvp"] if isinstance(par["vsvp"], float) else np.asarray(par["vsvp"]), + nt0=nt0, + linearization=par["linearization"], ) - assert dottest(AVOop, ntheta * nt0, 3 * nt0) + assert dottest(AVOop, ntheta * nt0, 3 * nt0, backend=backend) minv = lsqr( - AVOop, AVOop * m, damp=1e-20, iter_lim=1000, atol=1e-8, btol=1e-8, show=0 + AVOop, + AVOop * np.asarray(m), + x0=np.zeros_like(m), + damp=1e-20, + niter=1000, + atol=1e-8, + btol=1e-8, + show=0, )[0] assert_array_almost_equal(m, minv, decimal=3) diff --git a/pytests/test_basicoperators.py b/pytests/test_basicoperators.py old mode 100755 new mode 100644 index f12c8c686..e7260bea7 --- a/pytests/test_basicoperators.py +++ b/pytests/test_basicoperators.py @@ -1,8 +1,19 @@ -import numpy as np +import os + +if int(os.environ.get("TEST_CUPY_PYLOPS", 0)): + import cupy as np + from cupy.testing import assert_array_almost_equal, assert_array_equal + from cupyx.scipy.sparse import rand + + backend = "cupy" +else: + import numpy as np + from numpy.testing import assert_array_almost_equal, assert_array_equal + from scipy.sparse import rand + + backend = "numpy" +import numpy as npp import pytest -from numpy.testing import assert_array_almost_equal, assert_array_equal -from scipy.sparse import rand -from scipy.sparse.linalg import lsqr from pylops.basicoperators import ( Conj, @@ -19,6 +30,7 @@ ToCupy, Zero, ) +from pylops.optimization.basic import lsqr from pylops.utils import dottest par1 = {"ny": 11, "nx": 11, "imag": 0, "dtype": "float64"} # square real @@ -40,13 +52,21 @@ def test_Regression(par): """Dot-test, inversion and apply for Regression operator""" np.random.seed(10) order = 4 - t = np.arange(par["ny"], dtype=np.float32) + t = np.arange(par["ny"], dtype=np.float64) LRop = Regression(t, order=order, dtype=par["dtype"]) - assert dottest(LRop, par["ny"], order + 1) + assert dottest(LRop, par["ny"], order + 1, backend=backend) - x = np.array([1.0, 2.0, 0.0, 3.0, -1.0], dtype=np.float32) + x = np.array([1.0, 2.0, 0.0, 3.0, -1.0], dtype=np.float64) xlsqr = lsqr( - LRop, LRop * x, damp=1e-10, iter_lim=300, atol=1e-8, btol=1e-8, show=0 + LRop, + LRop * x, + x0=np.zeros_like(x), + damp=1e-10, + niter=300, + atol=0, + btol=0, + conlim=np.inf, + show=0, )[0] assert_array_almost_equal(x, xlsqr, decimal=3) @@ -61,11 +81,19 @@ def test_LinearRegression(par): np.random.seed(10) t = np.arange(par["ny"], dtype=np.float32) LRop = LinearRegression(t, dtype=par["dtype"]) - assert dottest(LRop, par["ny"], 2) + assert dottest(LRop, par["ny"], 2, backend=backend) - x = np.array([1.0, 2.0], dtype=np.float32) + x = np.array([1.0, 2.0], dtype=np.float64) xlsqr = lsqr( - LRop, LRop * x, damp=1e-10, iter_lim=300, atol=1e-8, btol=1e-8, show=0 + LRop, + LRop * x, + x0=np.zeros_like(x), + damp=1e-10, + niter=300, + atol=0, + btol=0, + conlim=np.inf, + show=0, )[0] assert_array_almost_equal(x, xlsqr, decimal=3) @@ -82,12 +110,25 @@ def test_MatrixMult(par): "imag" ] * np.random.normal(0, 10, (par["ny"], par["nx"])).astype("float32") Gop = MatrixMult(G, dtype=par["dtype"]) - assert dottest(Gop, par["ny"], par["nx"], complexflag=0 if par["imag"] == 0 else 3) + assert dottest( + Gop, + par["ny"], + par["nx"], + complexflag=0 if par["imag"] == 0 else 3, + backend=backend, + ) x = np.ones(par["nx"]) + par["imag"] * np.ones(par["nx"]) - xlsqr = lsqr(Gop, Gop * x, damp=1e-20, iter_lim=300, atol=1e-8, btol=1e-8, show=0)[ - 0 - ] + xlsqr = lsqr( + Gop, + Gop * x, + x0=np.zeros_like(x), + damp=1e-20, + niter=300, + atol=1e-8, + btol=1e-8, + show=0, + )[0] assert_array_almost_equal(x, xlsqr, decimal=4) @@ -102,12 +143,25 @@ def test_MatrixMult_sparse(par): ).astype("float32") Gop = MatrixMult(G, dtype=par["dtype"]) - assert dottest(Gop, par["ny"], par["nx"], complexflag=0 if par["imag"] == 1 else 3) + assert dottest( + Gop, + par["ny"], + par["nx"], + complexflag=0 if par["imag"] == 1 else 3, + backend=backend, + ) x = np.ones(par["nx"]) + par["imag"] * np.ones(par["nx"]) - xlsqr = lsqr(Gop, Gop * x, damp=1e-20, iter_lim=300, atol=1e-8, btol=1e-8, show=0)[ - 0 - ] + xlsqr = lsqr( + Gop, + Gop * x, + x0=np.zeros_like(x), + damp=1e-20, + niter=300, + atol=1e-8, + btol=1e-8, + show=0, + )[0] assert_array_almost_equal(x, xlsqr, decimal=4) @@ -136,13 +190,24 @@ def test_MatrixMult_repeated(par): ] * np.random.normal(0, 10, (par["ny"], par["nx"])).astype("float32") Gop = MatrixMult(G, otherdims=5, dtype=par["dtype"]) assert dottest( - Gop, par["ny"] * 5, par["nx"] * 5, complexflag=0 if par["imag"] == 1 else 3 + Gop, + par["ny"] * 5, + par["nx"] * 5, + complexflag=0 if par["imag"] == 1 else 3, + backend=backend, ) x = (np.ones((par["nx"], 5)) + par["imag"] * np.ones((par["nx"], 5))).ravel() - xlsqr = lsqr(Gop, Gop * x, damp=1e-20, iter_lim=300, atol=1e-8, btol=1e-8, show=0)[ - 0 - ] + xlsqr = lsqr( + Gop, + Gop * x, + x0=np.zeros_like(x), + damp=1e-20, + niter=300, + atol=1e-8, + btol=1e-8, + show=0, + )[0] assert_array_almost_equal(x, xlsqr, decimal=4) @@ -151,7 +216,13 @@ def test_Identity_inplace(par): """Dot-test, forward and adjoint for Identity operator""" np.random.seed(10) Iop = Identity(par["ny"], par["nx"], dtype=par["dtype"], inplace=True) - assert dottest(Iop, par["ny"], par["nx"], complexflag=0 if par["imag"] == 0 else 3) + assert dottest( + Iop, + par["ny"], + par["nx"], + complexflag=0 if par["imag"] == 0 else 3, + backend=backend, + ) x = np.ones(par["nx"]) + par["imag"] * np.ones(par["nx"]) y = Iop * x @@ -170,7 +241,13 @@ def test_Identity_noinplace(par): """Dot-test, forward and adjoint for Identity operator (not in place)""" np.random.seed(10) Iop = Identity(par["ny"], par["nx"], dtype=par["dtype"], inplace=False) - assert dottest(Iop, par["ny"], par["nx"], complexflag=0 if par["imag"] == 0 else 3) + assert dottest( + Iop, + par["ny"], + par["nx"], + complexflag=0 if par["imag"] == 0 else 3, + backend=backend, + ) x = np.ones(par["nx"]) + par["imag"] * np.ones(par["nx"]) y = Iop * x @@ -193,7 +270,7 @@ def test_Zero(par): """Dot-test, forward and adjoint for Zero operator""" np.random.seed(10) Zop = Zero(par["ny"], par["nx"], dtype=par["dtype"]) - assert dottest(Zop, par["ny"], par["nx"]) + assert dottest(Zop, par["ny"], par["nx"], backend=backend) x = np.ones(par["nx"]) + par["imag"] * np.ones(par["nx"]) y = Zop * x @@ -210,7 +287,7 @@ def test_Flip1D(par): x = np.arange(par["ny"]) + par["imag"] * np.arange(par["ny"]) Fop = Flip(par["ny"], dtype=par["dtype"]) - assert dottest(Fop, par["ny"], par["ny"]) + assert dottest(Fop, par["ny"], par["ny"], backend=backend) y = Fop * x xadj = Fop.H * y @@ -235,7 +312,9 @@ def test_Flip2D(par): axis=axis, dtype=par["dtype"], ) - assert dottest(Fop, par["ny"] * par["nx"], par["ny"] * par["nx"]) + assert dottest( + Fop, par["ny"] * par["nx"], par["ny"] * par["nx"], backend=backend + ) y = Fop * x[str(axis)].ravel() xadj = Fop.H * y.ravel() @@ -284,7 +363,10 @@ def test_Flip3D(par): dtype=par["dtype"], ) assert dottest( - Fop, par["ny"] * par["nx"] * par["nx"], par["ny"] * par["nx"] * par["nx"] + Fop, + par["ny"] * par["nx"] * par["nx"], + par["ny"] * par["nx"] * par["nx"], + backend=backend, ) y = Fop * x[str(axis)].ravel() @@ -300,7 +382,7 @@ def test_Symmetrize1D(par): x = np.arange(par["ny"]) + par["imag"] * np.arange(par["ny"]) Sop = Symmetrize(par["ny"], dtype=par["dtype"]) - dottest(Sop, par["ny"] * 2 - 1, par["ny"], verb=True) + dottest(Sop, par["ny"] * 2 - 1, par["ny"], verb=True, backend=backend) y = Sop * x xinv = Sop / y @@ -326,7 +408,7 @@ def test_Symmetrize2D(par): dtype=par["dtype"], ) y = Sop * x[str(axis)].ravel() - assert dottest(Sop, y.size, par["ny"] * par["nx"]) + assert dottest(Sop, y.size, par["ny"] * par["nx"], backend=backend) xinv = Sop / y assert_array_almost_equal(x[str(axis)].ravel(), xinv, decimal=3) @@ -373,7 +455,7 @@ def test_Symmetrize3D(par): dtype=par["dtype"], ) y = Sop * x[str(axis)].ravel() - assert dottest(Sop, y.size, par["ny"] * par["nx"] * par["nx"]) + assert dottest(Sop, y.size, par["ny"] * par["nx"] * par["nx"], backend=backend) xinv = Sop / y assert_array_almost_equal(x[str(axis)].ravel(), xinv, decimal=3) @@ -386,7 +468,7 @@ def test_Roll1D(par): x = np.arange(par["ny"]) + par["imag"] * np.arange(par["ny"]) Rop = Roll(par["ny"], shift=2, dtype=par["dtype"]) - assert dottest(Rop, par["ny"], par["ny"]) + assert dottest(Rop, par["ny"], par["ny"], backend=backend) y = Rop * x xadj = Rop.H * y @@ -413,7 +495,9 @@ def test_Roll2D(par): dtype=par["dtype"], ) y = Rop * x[str(axis)].ravel() - assert dottest(Rop, par["ny"] * par["nx"], par["ny"] * par["nx"]) + assert dottest( + Rop, par["ny"] * par["nx"], par["ny"] * par["nx"], backend=backend + ) xadj = Rop.H * y assert_array_almost_equal(x[str(axis)].ravel(), xadj, decimal=3) @@ -462,7 +546,10 @@ def test_Roll3D(par): ) y = Rop * x[str(axis)].ravel() assert dottest( - Rop, par["ny"] * par["nx"] * par["nx"], par["ny"] * par["nx"] * par["nx"] + Rop, + par["ny"] * par["nx"] * par["nx"], + par["ny"] * par["nx"] * par["nx"], + backend=backend, ) xinv = Rop.H * y @@ -476,7 +563,7 @@ def test_Sum2D(par): dim_d = [par["ny"], par["nx"]] dim_d.pop(axis) Sop = Sum(dims=(par["ny"], par["nx"]), axis=axis, dtype=par["dtype"]) - assert dottest(Sop, np.prod(dim_d), par["ny"] * par["nx"]) + assert dottest(Sop, npp.prod(dim_d), par["ny"] * par["nx"], backend=backend) @pytest.mark.parametrize("par", [(par1), (par2), (par1j), (par2j), (par3)]) @@ -525,10 +612,12 @@ def test_Sum3D(par): dim_d = [par["ny"], par["nx"], par["nx"]] dim_d.pop(axis) Sop = Sum(dims=(par["ny"], par["nx"], par["nx"]), axis=axis, dtype=par["dtype"]) - assert dottest(Sop, np.prod(dim_d), par["ny"] * par["nx"] * par["nx"]) + assert dottest( + Sop, npp.prod(dim_d), par["ny"] * par["nx"] * par["nx"], backend=backend + ) -@pytest.mark.parametrize("par", [(par1), (par2), (par1j), (par2j), (par3)]) +@pytest.mark.parametrize("par", [(par1j), (par2j)]) def test_Real(par): """Dot-test, forward and adjoint for Real operator""" Rop = Real(dims=(par["ny"], par["nx"]), dtype=par["dtype"]) @@ -537,7 +626,11 @@ def test_Real(par): else: complexflag = 0 assert dottest( - Rop, par["ny"] * par["nx"], par["ny"] * par["nx"], complexflag=complexflag + Rop, + par["ny"] * par["nx"], + par["ny"] * par["nx"], + complexflag=complexflag, + backend=backend, ) np.random.seed(10) @@ -553,7 +646,7 @@ def test_Real(par): assert_array_equal(x, np.real(y) + 0j) -@pytest.mark.parametrize("par", [(par1), (par2), (par1j), (par2j), (par3)]) +@pytest.mark.parametrize("par", [(par1j), (par2j)]) def test_Imag(par): """Dot-test, forward and adjoint for Imag operator""" Iop = Imag(dims=(par["ny"], par["nx"]), dtype=par["dtype"]) @@ -562,7 +655,11 @@ def test_Imag(par): else: complexflag = 0 assert dottest( - Iop, par["ny"] * par["nx"], par["ny"] * par["nx"], complexflag=complexflag + Iop, + par["ny"] * par["nx"], + par["ny"] * par["nx"], + complexflag=complexflag, + backend=backend, ) np.random.seed(10) @@ -590,7 +687,11 @@ def test_Conj(par): else: complexflag = 0 assert dottest( - Cop, par["ny"] * par["nx"], par["ny"] * par["nx"], complexflag=complexflag + Cop, + par["ny"] * par["nx"], + par["ny"] * par["nx"], + complexflag=complexflag, + backend=backend, ) np.random.seed(10) @@ -612,8 +713,8 @@ def test_ToCupy(par): Top = ToCupy(par["nx"], dtype=par["dtype"]) np.random.seed(10) - x = np.random.randn(par["nx"]) + par["imag"] * np.random.randn(par["nx"]) + x = npp.random.randn(par["nx"]) + par["imag"] * npp.random.randn(par["nx"]) y = Top * x xadj = Top.H * y assert_array_equal(x, xadj) - assert_array_equal(y, x) + assert_array_equal(y, np.asarray(x)) diff --git a/pytests/test_blending.py b/pytests/test_blending.py old mode 100755 new mode 100644 index 61dc0693f..4f89e180c --- a/pytests/test_blending.py +++ b/pytests/test_blending.py @@ -1,4 +1,14 @@ -import numpy as np +import os + +if int(os.environ.get("TEST_CUPY_PYLOPS", 0)): + import cupy as np + + backend = "cupy" +else: + import numpy as np + + backend = "numpy" +import numpy as npp import pytest from pylops.utils import dottest @@ -13,14 +23,15 @@ @pytest.mark.parametrize("par", [(par)]) def test_Blending_continuous(par): """Dot-test for continuous Blending operator""" - np.random.seed(0) + npp.random.seed(0) # ignition times overlap = 0.5 - ignition_times = 2.0 * np.random.rand(par["ns"]) - 1.0 + ignition_times = 2.0 * npp.random.rand(par["ns"]) - 1.0 ignition_times += ( - np.arange(0, overlap * par["nt"] * par["ns"], overlap * par["nt"]) * dt + npp.arange(0, overlap * par["nt"] * par["ns"], overlap * par["nt"]) * dt ) ignition_times[0] = 0.0 + Bop = BlendingContinuous( par["nt"], par["nr"], @@ -33,16 +44,17 @@ def test_Blending_continuous(par): Bop, Bop.nttot * par["nr"], par["nt"] * par["ns"] * par["nr"], + backend=backend, ) @pytest.mark.parametrize("par", [(par)]) def test_Blending_group(par): """Dot-test for group Blending operator""" - np.random.seed(0) + npp.random.seed(0) group_size = 2 n_groups = par["ns"] // group_size - ignition_times = 0.8 * np.random.rand(par["ns"]) + ignition_times = 0.8 * npp.random.rand(par["ns"]) Bop = BlendingGroup( par["nt"], @@ -58,16 +70,17 @@ def test_Blending_group(par): Bop, par["nt"] * n_groups * par["nr"], par["nt"] * par["ns"] * par["nr"], + backend=backend, ) @pytest.mark.parametrize("par", [(par)]) def test_Blending_half(par): """Dot-test for half Blending operator""" - np.random.seed(0) + npp.random.seed(0) group_size = 2 n_groups = par["ns"] // group_size - ignition_times = 0.8 * np.random.rand(par["ns"]) + ignition_times = 0.8 * npp.random.rand(par["ns"]) Bop = BlendingHalf( par["nt"], @@ -83,4 +96,5 @@ def test_Blending_half(par): Bop, par["nt"] * n_groups * par["nr"], par["nt"] * par["ns"] * par["nr"], + backend=backend, ) diff --git a/pytests/test_causalintegration.py b/pytests/test_causalintegration.py old mode 100755 new mode 100644 index 6be75b520..48856a911 --- a/pytests/test_causalintegration.py +++ b/pytests/test_causalintegration.py @@ -1,11 +1,23 @@ +import os + +if int(os.environ.get("TEST_CUPY_PYLOPS", 0)): + import cupy as np + from cupy.testing import assert_array_almost_equal + + backend = "cupy" +else: + import numpy as np + from numpy.testing import assert_array_almost_equal + + backend = "numpy" import itertools -import numpy as np import pytest -from numpy.testing import assert_array_almost_equal from pylops.basicoperators import CausalIntegration, FirstDerivative +from pylops.optimization.basic import lsqr from pylops.utils import dottest +from pylops.utils.backend import get_module_name par1 = { "nt": 20, @@ -76,6 +88,7 @@ def test_CausalIntegration1d(par): x = t + par["imag"] * t for kind, rf in itertools.product(("full", "half", "trapezoidal"), (False, True)): + rf = rf if get_module_name == "numpy" else False Cop = CausalIntegration( par["nt"], sampling=par["dt"], @@ -85,7 +98,11 @@ def test_CausalIntegration1d(par): ) rf1 = 1 if rf else 0 assert dottest( - Cop, par["nt"] - rf1, par["nt"], complexflag=0 if par["imag"] == 0 else 3 + Cop, + par["nt"] - rf1, + par["nt"], + complexflag=0 if par["imag"] == 0 else 3, + backend=backend, ) # test analytical integration and derivative inversion only for @@ -109,7 +126,16 @@ def test_CausalIntegration1d(par): xder = Dop * y.ravel() # derivative by inversion - xinv = Cop / y + xinv = lsqr( + Cop, + y, + x0=np.zeros_like(x), + niter=100, + atol=0, + btol=0, + conlim=np.inf, + show=0, + )[0] assert_array_almost_equal(x[:-1], xder[:-1], decimal=4) assert_array_almost_equal(x, xinv, decimal=4) @@ -127,6 +153,7 @@ def test_CausalIntegration2d(par): ) for kind, rf in itertools.product(("full", "half", "trapezoidal"), (False, True)): + rf = rf if get_module_name == "numpy" else False Cop = CausalIntegration( (par["nt"], par["nx"]), sampling=dt, @@ -141,6 +168,7 @@ def test_CausalIntegration2d(par): (par["nt"] - rf1) * par["nx"], par["nt"] * par["nx"], complexflag=0 if par["imag"] == 0 else 3, + backend=backend, ) # test analytical integration and derivative inversion only for @@ -169,7 +197,16 @@ def test_CausalIntegration2d(par): xder = xder.reshape(par["nt"], par["nx"]) # derivative by inversion - xinv = Cop / y.ravel() + xinv = lsqr( + Cop, + y.ravel(), + x0=np.zeros_like(x).ravel(), + niter=100, + atol=0, + btol=0, + conlim=np.inf, + show=0, + )[0] xinv = xinv.reshape(par["nt"], par["nx"]) assert_array_almost_equal(x[:-1], xder[:-1], decimal=2) diff --git a/pytests/test_chirpradon.py b/pytests/test_chirpradon.py old mode 100755 new mode 100644 index 785edd21a..c8084baec --- a/pytests/test_chirpradon.py +++ b/pytests/test_chirpradon.py @@ -1,5 +1,20 @@ +import os + +if int(os.environ.get("TEST_CUPY_PYLOPS", 0)): + import cupy as np + from cupy.testing import assert_array_almost_equal, assert_array_equal + from cupyx.scipy.sparse import rand + + backend = "cupy" +else: + import numpy as np + from numpy.testing import assert_array_almost_equal, assert_array_equal + from scipy.sparse import rand + + backend = "numpy" +import itertools + import pytest -from numpy.testing import assert_array_almost_equal from pylops.optimization.sparsity import fista from pylops.signalprocessing import ChirpRadon2D, ChirpRadon3D @@ -72,10 +87,9 @@ def test_ChirpRadon2D(par): wav, _, wav_c = ricker(t[:41], f0=parmod["f0"]) # Generate model - _, x = linear2d(hx, t, 1500.0, t0, theta, amp, wav) - - Rop = ChirpRadon2D(t, hx, par["pxmax"], dtype="float64") - assert dottest(Rop, par["nhx"] * par["nt"], par["nhx"] * par["nt"]) + x = np.asarray(linear2d(hx, t, 1500.0, t0, theta, amp, wav)[1]) + Rop = ChirpRadon2D(np.asarray(t), np.asarray(hx), par["pxmax"], dtype="float64") + assert dottest(Rop, par["nhx"] * par["nt"], par["nhx"] * par["nt"], backend=backend) y = Rop * x.ravel() xinvana = Rop.inverse(y) @@ -122,18 +136,22 @@ def test_ChirpRadon3D(par): wav, _, wav_c = ricker(t[:41], f0=parmod["f0"]) # Generate model - _, x = linear3d(hy, hx, t, 1500.0, t0, theta, phi, amp, wav) + x = np.asarray(linear3d(hy, hx, t, 1500.0, t0, theta, phi, amp, wav)[1]) + Rop = ChirpRadon3D( - t, - hy, - hx, + np.asarray(t), + np.asarray(hy), + np.asarray(hx), (par["pymax"], par["pxmax"]), engine=par["engine"], dtype="float64", **dict(flags=("FFTW_ESTIMATE",), threads=2) ) assert dottest( - Rop, par["nhy"] * par["nhx"] * par["nt"], par["nhy"] * par["nhx"] * par["nt"] + Rop, + par["nhy"] * par["nhx"] * par["nt"], + par["nhy"] * par["nhx"] * par["nt"], + backend=backend, ) y = Rop * x.ravel() diff --git a/pytests/test_combine.py b/pytests/test_combine.py old mode 100755 new mode 100644 index 114379996..36518a536 --- a/pytests/test_combine.py +++ b/pytests/test_combine.py @@ -1,10 +1,21 @@ -import numpy as np +import os + +if int(os.environ.get("TEST_CUPY_PYLOPS", 0)): + import cupy as np + from cupy.testing import assert_array_almost_equal + from cupyx.scipy.sparse import random as sp_random + + backend = "cupy" +else: + import numpy as np + from numpy.testing import assert_array_almost_equal + from scipy.sparse import random as sp_random + + backend = "numpy" import pytest -from numpy.testing import assert_array_almost_equal -from scipy.sparse import random as sp_random -from scipy.sparse.linalg import lsqr from pylops.basicoperators import Block, BlockDiag, HStack, MatrixMult, Real, VStack +from pylops.optimization.basic import lsqr from pylops.utils import dottest par1 = {"ny": 101, "nx": 101, "imag": 0, "dtype": "float64"} # square real @@ -54,25 +65,44 @@ def test_VStack(par): dtype=par["dtype"], ) assert dottest( - Vop, 2 * par["ny"], par["nx"], complexflag=0 if par["imag"] == 0 else 3 + Vop, + 2 * par["ny"], + par["nx"], + complexflag=0 if par["imag"] == 0 else 3, + backend=backend, ) - xlsqr = lsqr(Vop, Vop * x, damp=1e-20, iter_lim=300, atol=1e-8, btol=1e-8, show=0)[ - 0 - ] + xlsqr = lsqr( + Vop, + Vop * x, + x0=np.zeros_like(x).ravel(), + damp=1e-20, + niter=300, + atol=1e-8, + btol=1e-8, + show=0, + )[0] assert_array_almost_equal(x, xlsqr, decimal=4) # use numpy matrix directly in the definition of the operator V1op = VStack([G1, MatrixMult(G2, dtype=par["dtype"])], dtype=par["dtype"]) assert dottest( - V1op, 2 * par["ny"], par["nx"], complexflag=0 if par["imag"] == 0 else 3 + V1op, + 2 * par["ny"], + par["nx"], + complexflag=0 if par["imag"] == 0 else 3, + backend=backend, ) # use scipy matrix directly in the definition of the operator G1 = sp_random(par["ny"], par["nx"], density=0.4).astype("float32") V2op = VStack([G1, MatrixMult(G2, dtype=par["dtype"])], dtype=par["dtype"]) assert dottest( - V2op, 2 * par["ny"], par["nx"], complexflag=0 if par["imag"] == 0 else 3 + V2op, + 2 * par["ny"], + par["nx"], + complexflag=0 if par["imag"] == 0 else 3, + backend=backend, ) @@ -86,25 +116,43 @@ def test_HStack(par): Hop = HStack([G1, MatrixMult(G2, dtype=par["dtype"])], dtype=par["dtype"]) assert dottest( - Hop, par["ny"], 2 * par["nx"], complexflag=0 if par["imag"] == 0 else 3 + Hop, + par["ny"], + 2 * par["nx"], + complexflag=0 if par["imag"] == 0 else 3, + backend=backend, ) - xlsqr = lsqr(Hop, Hop * x, damp=1e-20, iter_lim=300, atol=1e-8, btol=1e-8, show=0)[ - 0 - ] + xlsqr = lsqr( + Hop, + Hop * x, + x0=np.zeros_like(x).ravel(), + niter=300, + atol=1e-8, + btol=1e-8, + show=0, + )[0] assert_array_almost_equal(x, xlsqr, decimal=4) # use numpy matrix directly in the definition of the operator H1op = HStack([G1, MatrixMult(G2, dtype=par["dtype"])], dtype=par["dtype"]) assert dottest( - H1op, par["ny"], 2 * par["nx"], complexflag=0 if par["imag"] == 0 else 3 + H1op, + par["ny"], + 2 * par["nx"], + complexflag=0 if par["imag"] == 0 else 3, + backend=backend, ) # use scipy matrix directly in the definition of the operator G1 = sp_random(par["ny"], par["nx"], density=0.4).astype("float32") H2op = HStack([G1, MatrixMult(G2, dtype=par["dtype"])], dtype=par["dtype"]) assert dottest( - H2op, par["ny"], 2 * par["nx"], complexflag=0 if par["imag"] == 0 else 3 + H2op, + par["ny"], + 2 * par["nx"], + complexflag=0 if par["imag"] == 0 else 3, + backend=backend, ) @@ -127,12 +175,23 @@ def test_Block(par): dtype=par["dtype"], ) assert dottest( - Bop, 2 * par["ny"], 2 * par["nx"], complexflag=0 if par["imag"] == 0 else 3 + Bop, + 2 * par["ny"], + 2 * par["nx"], + complexflag=0 if par["imag"] == 0 else 3, + backend=backend, ) - xlsqr = lsqr(Bop, Bop * x, damp=1e-20, iter_lim=500, atol=1e-8, btol=1e-8, show=0)[ - 0 - ] + xlsqr = lsqr( + Bop, + Bop * x, + x0=np.zeros_like(x).ravel(), + damp=1e-20, + niter=500, + atol=1e-8, + btol=1e-8, + show=0, + )[0] assert_array_almost_equal(x, xlsqr, decimal=3) # use numpy matrix directly in the definition of the operator @@ -144,7 +203,11 @@ def test_Block(par): dtype=par["dtype"], ) assert dottest( - B1op, 2 * par["ny"], 2 * par["nx"], complexflag=0 if par["imag"] == 0 else 3 + B1op, + 2 * par["ny"], + 2 * par["nx"], + complexflag=0 if par["imag"] == 0 else 3, + backend=backend, ) # use scipy matrix directly in the definition of the operator @@ -157,7 +220,11 @@ def test_Block(par): dtype=par["dtype"], ) assert dottest( - B2op, 2 * par["ny"], 2 * par["nx"], complexflag=0 if par["imag"] == 0 else 3 + B2op, + 2 * par["ny"], + 2 * par["nx"], + complexflag=0 if par["imag"] == 0 else 3, + backend=backend, ) @@ -174,28 +241,50 @@ def test_BlockDiag(par): dtype=par["dtype"], ) assert dottest( - BDop, 2 * par["ny"], 2 * par["nx"], complexflag=0 if par["imag"] == 0 else 3 + BDop, + 2 * par["ny"], + 2 * par["nx"], + complexflag=0 if par["imag"] == 0 else 3, + backend=backend, ) xlsqr = lsqr( - BDop, BDop * x, damp=1e-20, iter_lim=500, atol=1e-8, btol=1e-8, show=0 + BDop, + BDop * x, + x0=np.zeros_like(x).ravel(), + damp=1e-20, + niter=500, + atol=1e-8, + btol=1e-8, + show=0, )[0] assert_array_almost_equal(x, xlsqr, decimal=3) # use numpy matrix directly in the definition of the operator BD1op = BlockDiag([MatrixMult(G1, dtype=par["dtype"]), G2], dtype=par["dtype"]) assert dottest( - BD1op, 2 * par["ny"], 2 * par["nx"], complexflag=0 if par["imag"] == 0 else 3 + BD1op, + 2 * par["ny"], + 2 * par["nx"], + complexflag=0 if par["imag"] == 0 else 3, + backend=backend, ) # use scipy matrix directly in the definition of the operator G2 = sp_random(par["ny"], par["nx"], density=0.4).astype("float32") BD2op = BlockDiag([MatrixMult(G1, dtype=par["dtype"]), G2], dtype=par["dtype"]) assert dottest( - BD2op, 2 * par["ny"], 2 * par["nx"], complexflag=0 if par["imag"] == 0 else 3 + BD2op, + 2 * par["ny"], + 2 * par["nx"], + complexflag=0 if par["imag"] == 0 else 3, + backend=backend, ) +@pytest.mark.skipif( + int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" +) @pytest.mark.parametrize("par", [(par1), (par2), (par1j), (par2j)]) def test_VStack_multiproc(par): """Single and multiprocess consistentcy for VStack operator""" @@ -210,7 +299,11 @@ def test_VStack_multiproc(par): [MatrixMult(G, dtype=par["dtype"])] * 4, nproc=nproc, dtype=par["dtype"] ) assert dottest( - Vmultiop, 4 * par["ny"], par["nx"], complexflag=0 if par["imag"] == 0 else 3 + Vmultiop, + 4 * par["ny"], + par["nx"], + complexflag=0 if par["imag"] == 0 else 3, + backend=backend, ) # forward assert_array_almost_equal(Vop * x, Vmultiop * x, decimal=4) @@ -221,6 +314,9 @@ def test_VStack_multiproc(par): Vmultiop.pool.close() +@pytest.mark.skipif( + int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" +) @pytest.mark.parametrize("par", [(par2), (par2j)]) def test_HStack_multiproc(par): """Single and multiprocess consistentcy for HStack operator""" @@ -235,7 +331,11 @@ def test_HStack_multiproc(par): [MatrixMult(G, dtype=par["dtype"])] * 4, nproc=nproc, dtype=par["dtype"] ) assert dottest( - Hmultiop, par["ny"], 4 * par["nx"], complexflag=0 if par["imag"] == 0 else 3 + Hmultiop, + par["ny"], + 4 * par["nx"], + complexflag=0 if par["imag"] == 0 else 3, + backend=backend, ) # forward assert_array_almost_equal(Hop * x, Hmultiop * x, decimal=4) @@ -246,6 +346,9 @@ def test_HStack_multiproc(par): Hmultiop.pool.close() +@pytest.mark.skipif( + int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" +) @pytest.mark.parametrize("par", [(par1), (par2), (par1j), (par2j)]) def test_Block_multiproc(par): """Single and multiprocess consistentcy for Block operator""" @@ -260,7 +363,11 @@ def test_Block_multiproc(par): Bop = Block(Ghor, dtype=par["dtype"]) Bmultiop = Block(Ghor, nproc=nproc, dtype=par["dtype"]) assert dottest( - Bmultiop, 4 * par["ny"], 2 * par["nx"], complexflag=0 if par["imag"] == 0 else 3 + Bmultiop, + 4 * par["ny"], + 2 * par["nx"], + complexflag=0 if par["imag"] == 0 else 3, + backend=backend, ) # forward assert_array_almost_equal(Bop * x, Bmultiop * x, decimal=3) @@ -271,6 +378,9 @@ def test_Block_multiproc(par): Bmultiop.pool.close() +@pytest.mark.skipif( + int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" +) @pytest.mark.parametrize("par", [(par1), (par2), (par1j), (par2j)]) def test_BlockDiag_multiproc(par): """Single and multiprocess consistentcy for BlockDiag operator""" @@ -289,6 +399,7 @@ def test_BlockDiag_multiproc(par): 4 * par["ny"], 4 * par["nx"], complexflag=0 if par["imag"] == 0 else 3, + backend=backend, ) # forward assert_array_almost_equal(BDop * x, BDmultiop * x, decimal=4) @@ -299,23 +410,24 @@ def test_BlockDiag_multiproc(par): BDmultiop.pool.close() -@pytest.mark.parametrize("par", [(par1), (par2), (par1j), (par2j)]) +@pytest.mark.parametrize("par", [(par1j), (par2j)]) def test_VStack_rlinear(par): """VStack operator applied to mix of R-linear and C-linear operators""" np.random.seed(0) - if np.dtype(par["dtype"]).kind == "c": - G = ( - np.random.normal(0, 10, (par["ny"], par["nx"])) - + 1j * np.random.normal(0, 10, (par["ny"], par["nx"])) - ).astype(par["dtype"]) - else: - G = np.random.normal(0, 10, (par["ny"], par["nx"])).astype(par["dtype"]) + G = ( + np.random.normal(0, 10, (par["ny"], par["nx"])) + + 1j * np.random.normal(0, 10, (par["ny"], par["nx"])) + ).astype(par["dtype"]) Rop = Real(dims=(par["nx"],), dtype=par["dtype"]) VSop = VStack([Rop, MatrixMult(G, dtype=par["dtype"])], dtype=par["dtype"]) assert VSop.clinear is False assert dottest( - VSop, par["nx"] + par["ny"], par["nx"], complexflag=0 if par["imag"] == 0 else 3 + VSop, + par["nx"] + par["ny"], + par["nx"], + complexflag=0 if par["imag"] == 0 else 3, + backend=backend, ) # forward x = np.random.randn(par["nx"]) + par["imag"] * np.random.randn(par["nx"]) @@ -323,43 +435,41 @@ def test_VStack_rlinear(par): assert_array_almost_equal(expected, VSop * x, decimal=4) -@pytest.mark.parametrize("par", [(par1), (par2), (par1j), (par2j)]) +@pytest.mark.parametrize("par", [(par1j), (par2j)]) def test_HStack_rlinear(par): """HStack operator applied to mix of R-linear and C-linear operators""" np.random.seed(0) - if np.dtype(par["dtype"]).kind == "c": - G = ( - np.random.normal(0, 10, (par["ny"], par["nx"])) - + 1j * np.random.normal(0, 10, (par["ny"], par["nx"])) - ).astype(par["dtype"]) - else: - G = np.random.normal(0, 10, (par["ny"], par["nx"])).astype(par["dtype"]) + G = ( + np.random.normal(0, 10, (par["ny"], par["nx"])) + + 1j * np.random.normal(0, 10, (par["ny"], par["nx"])) + ).astype(par["dtype"]) Rop = Real(dims=(par["ny"],), dtype=par["dtype"]) HSop = HStack([Rop, MatrixMult(G, dtype=par["dtype"])], dtype=par["dtype"]) assert HSop.clinear is False assert dottest( - HSop, par["ny"], par["nx"] + par["ny"], complexflag=0 if par["imag"] == 0 else 3 + HSop, + par["ny"], + par["nx"] + par["ny"], + complexflag=0 if par["imag"] == 0 else 3, + backend=backend, ) # forward x = np.random.randn(par["nx"] + par["ny"]) + par["imag"] * np.random.randn( par["nx"] + par["ny"] ) - expected = np.sum([np.real(x[: par["ny"]]), G @ x[par["ny"] :]], axis=0) + expected = np.sum(np.array([np.real(x[: par["ny"]]), G @ x[par["ny"] :]]), axis=0) assert_array_almost_equal(expected, HSop * x, decimal=4) -@pytest.mark.parametrize("par", [(par1), (par2), (par1j), (par2j)]) +@pytest.mark.parametrize("par", [(par1j), (par2j)]) def test_BlockDiag_rlinear(par): """BlockDiag operator applied to mix of R-linear and C-linear operators""" np.random.seed(0) - if np.dtype(par["dtype"]).kind == "c": - G = ( - np.random.normal(0, 10, (par["ny"], par["nx"])) - + 1j * np.random.normal(0, 10, (par["ny"], par["nx"])) - ).astype(par["dtype"]) - else: - G = np.random.normal(0, 10, (par["ny"], par["nx"])).astype(par["dtype"]) + G = ( + np.random.normal(0, 10, (par["ny"], par["nx"])) + + 1j * np.random.normal(0, 10, (par["ny"], par["nx"])) + ).astype(par["dtype"]) Rop = Real(dims=(par["nx"],), dtype=par["dtype"]) BDop = BlockDiag([Rop, MatrixMult(G, dtype=par["dtype"])], dtype=par["dtype"]) @@ -369,6 +479,7 @@ def test_BlockDiag_rlinear(par): par["nx"] + par["ny"], 2 * par["nx"], complexflag=0 if par["imag"] == 0 else 3, + backend=backend, ) # forward x = np.random.randn(2 * par["nx"]) + par["imag"] * np.random.randn(2 * par["nx"]) diff --git a/pytests/test_convolve.py b/pytests/test_convolve.py old mode 100755 new mode 100644 index d26938baf..6e7206f92 --- a/pytests/test_convolve.py +++ b/pytests/test_convolve.py @@ -1,9 +1,22 @@ -import numpy as np +import os + +if int(os.environ.get("TEST_CUPY_PYLOPS", 0)): + import cupy as np + from cupy.testing import assert_array_almost_equal + from cupyx.scipy.signal.windows import triang + + backend = "cupy" +else: + import numpy as np + from numpy.testing import assert_array_almost_equal + from scipy.signal.windows import triang + + backend = "numpy" +import itertools + import pytest -from numpy.testing import assert_array_almost_equal -from scipy.signal.windows import triang -from scipy.sparse.linalg import lsqr +from pylops.optimization.basic import lsqr from pylops.signalprocessing import Convolve1D, Convolve2D, ConvolveND from pylops.utils import dottest @@ -129,12 +142,19 @@ def test_Convolve1D(par): # 1D if par["axis"] == 0: Cop = Convolve1D(par["nx"], h=h1, offset=par["offset"], dtype="float64") - assert dottest(Cop, par["nx"], par["nx"]) + assert dottest(Cop, par["nx"], par["nx"], backend=backend) x = np.zeros((par["nx"])) x[par["nx"] // 2] = 1.0 xlsqr = lsqr( - Cop, Cop * x, damp=1e-20, iter_lim=200, atol=1e-8, btol=1e-8, show=0 + Cop, + Cop * x, + x0=np.zeros_like(x), + damp=1e-20, + niter=200, + atol=1e-8, + btol=1e-8, + show=0, )[0] assert_array_almost_equal(x, xlsqr, decimal=1) @@ -147,16 +167,24 @@ def test_Convolve1D(par): axis=par["axis"], dtype="float64", ) - assert dottest(Cop, par["ny"] * par["nx"], par["ny"] * par["nx"]) + assert dottest( + Cop, par["ny"] * par["nx"], par["ny"] * par["nx"], backend=backend + ) x = np.zeros((par["ny"], par["nx"])) x[ int(par["ny"] / 2 - 3) : int(par["ny"] / 2 + 3), int(par["nx"] / 2 - 3) : int(par["nx"] / 2 + 3), ] = 1.0 - x = x.ravel() xlsqr = lsqr( - Cop, Cop * x, damp=1e-20, iter_lim=200, atol=1e-8, btol=1e-8, show=0 + Cop, + Cop * x.ravel(), + x0=np.zeros_like(x), + damp=1e-20, + niter=200, + atol=1e-8, + btol=1e-8, + show=0, )[0] assert_array_almost_equal(x, xlsqr, decimal=1) @@ -169,7 +197,10 @@ def test_Convolve1D(par): dtype="float64", ) assert dottest( - Cop, par["nz"] * par["ny"] * par["nx"], par["nz"] * par["ny"] * par["nx"] + Cop, + par["nz"] * par["ny"] * par["nx"], + par["nz"] * par["ny"] * par["nx"], + backend=backend, ) x = np.zeros((par["nz"], par["ny"], par["nx"])) @@ -178,10 +209,16 @@ def test_Convolve1D(par): int(par["ny"] / 2 - 3) : int(par["ny"] / 2 + 3), int(par["nx"] / 2 - 3) : int(par["nx"] / 2 + 3), ] = 1.0 - x = x.ravel() - xlsqr = lsqr(Cop, Cop * x, damp=1e-20, iter_lim=200, atol=1e-8, btol=1e-8, show=0)[ - 0 - ] + xlsqr = lsqr( + Cop, + Cop * x.ravel(), + x0=np.zeros_like(x), + damp=1e-20, + niter=200, + atol=1e-8, + btol=1e-8, + show=0, + )[0] assert_array_almost_equal(x, xlsqr, decimal=1) @@ -196,10 +233,10 @@ def test_Convolve1D_long(par): x = np.zeros((par["nx"])) x[par["nx"] // 2] = 1.0 Xop = Convolve1D(nfilt[0], h=x, offset=nfilt[0] // 2, dtype="float64") - assert dottest(Xop, par["nx"], nfilt[0]) + assert dottest(Xop, par["nx"], nfilt[0], backend=backend) h1lsqr = lsqr( - Xop, Xop * h1, damp=1e-20, iter_lim=200, atol=1e-8, btol=1e-8, show=0 + Xop, Xop * h1, damp=1e-20, niter=200, atol=1e-8, btol=1e-8, show=0 )[0] assert_array_almost_equal(h1, h1lsqr, decimal=1) @@ -217,16 +254,24 @@ def test_Convolve2D(par): offset=par["offset"], dtype="float64", ) - assert dottest(Cop, par["ny"] * par["nx"], par["ny"] * par["nx"]) + assert dottest( + Cop, par["ny"] * par["nx"], par["ny"] * par["nx"], backend=backend + ) x = np.zeros((par["ny"], par["nx"])) x[ int(par["ny"] / 2 - 3) : int(par["ny"] / 2 + 3), int(par["nx"] / 2 - 3) : int(par["nx"] / 2 + 3), ] = 1.0 - x = x.ravel() xlsqr = lsqr( - Cop, Cop * x, damp=1e-20, iter_lim=200, atol=1e-8, btol=1e-8, show=0 + Cop, + Cop * x.ravel(), + x0=np.zeros_like(x), + damp=1e-20, + niter=200, + atol=1e-8, + btol=1e-8, + show=0, )[0] assert_array_almost_equal(x, xlsqr, decimal=1) @@ -241,7 +286,10 @@ def test_Convolve2D(par): dtype="float64", ) assert dottest( - Cop, par["nz"] * par["ny"] * par["nx"], par["nz"] * par["ny"] * par["nx"] + Cop, + par["nz"] * par["ny"] * par["nx"], + par["nz"] * par["ny"] * par["nx"], + backend=backend, ) x = np.zeros((par["nz"], par["ny"], par["nx"])) @@ -250,10 +298,16 @@ def test_Convolve2D(par): int(par["ny"] / 2 - 3) : int(par["ny"] / 2 + 3), int(par["nx"] / 2 - 3) : int(par["nx"] / 2 + 3), ] = 1.0 - x = x.ravel() - xlsqr = lsqr(Cop, Cop * x, damp=1e-20, iter_lim=200, atol=1e-8, btol=1e-8, show=0)[ - 0 - ] + xlsqr = lsqr( + Cop, + Cop * x.ravel(), + x0=np.zeros_like(x), + damp=1e-20, + niter=200, + atol=1e-8, + btol=1e-8, + show=0, + )[0] # due to ringing in solution we cannot use assert_array_almost_equal assert np.linalg.norm(xlsqr - x) / np.linalg.norm(xlsqr) < 2e-1 @@ -269,7 +323,10 @@ def test_Convolve3D(par): dtype="float64", ) assert dottest( - Cop, par["nz"] * par["ny"] * par["nx"], par["nz"] * par["ny"] * par["nx"] + Cop, + par["nz"] * par["ny"] * par["nx"], + par["nz"] * par["ny"] * par["nx"], + backend=backend, ) x = np.zeros((par["nz"], par["ny"], par["nx"])) @@ -278,9 +335,10 @@ def test_Convolve3D(par): int(par["ny"] / 2 - 3) : int(par["ny"] / 2 + 3), int(par["nx"] / 2 - 3) : int(par["nx"] / 2 + 3), ] = 1.0 - x = x.ravel() y = Cop * x - xlsqr = lsqr(Cop, y, damp=1e-20, iter_lim=400, atol=1e-8, btol=1e-8, show=0)[0] + xlsqr = lsqr( + Cop, y, x0=np.zeros_like(x), damp=1e-20, niter=400, atol=1e-8, btol=1e-8, show=0 + )[0] # due to ringing in solution we cannot use assert_array_almost_equal assert np.linalg.norm(xlsqr - x) / np.linalg.norm(xlsqr) < 2e-1 @@ -296,4 +354,5 @@ def test_Convolve3D(par): Cop, par["nz"] * par["ny"] * par["nx"] * par["nt"], par["nz"] * par["ny"] * par["nx"] * par["nt"], + backend=backend, ) diff --git a/pytests/test_dct.py b/pytests/test_dct.py index 4bd0f9b7c..864186966 100644 --- a/pytests/test_dct.py +++ b/pytests/test_dct.py @@ -1,3 +1,5 @@ +import os + import numpy as np import pytest @@ -9,6 +11,9 @@ par3 = {"ny": 21, "nx": 21, "imag": 0, "dtype": "float64"} +@pytest.mark.skipif( + int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" +) @pytest.mark.parametrize("par", [(par1), (par3)]) def test_DCT1D(par): """Dot test for Discrete Cosine Transform Operator 1D""" @@ -23,6 +28,9 @@ def test_DCT1D(par): np.testing.assert_allclose(t, y) +@pytest.mark.skipif( + int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" +) @pytest.mark.parametrize("par", [(par1), (par2), (par3)]) def test_DCT2D(par): """Dot test for Discrete Cosine Transform Operator 2D""" @@ -45,6 +53,9 @@ def test_DCT2D(par): np.testing.assert_allclose(t, y) +@pytest.mark.skipif( + int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" +) @pytest.mark.parametrize("par", [(par1), (par2), (par3)]) def test_DCT3D(par): """Dot test for Discrete Cosine Transform Operator 3D""" @@ -67,6 +78,9 @@ def test_DCT3D(par): np.testing.assert_allclose(t, y) +@pytest.mark.skipif( + int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" +) @pytest.mark.parametrize("par", [(par1), (par3)]) def test_DCT_workers(par): """Dot test for Discrete Cosine Transform Operator with workers""" diff --git a/pytests/test_derivative.py b/pytests/test_derivative.py old mode 100755 new mode 100644 index 718235f11..2c213d071 --- a/pytests/test_derivative.py +++ b/pytests/test_derivative.py @@ -1,6 +1,16 @@ -import numpy as np +import os + +if int(os.environ.get("TEST_CUPY_PYLOPS", 0)): + import cupy as np + from cupy.testing import assert_array_almost_equal, assert_array_equal + + backend = "cupy" +else: + import numpy as np + from numpy.testing import assert_array_almost_equal, assert_array_equal + + backend = "numpy" import pytest -from numpy.testing import assert_array_almost_equal, assert_array_equal from pylops.basicoperators import ( FirstDerivative, @@ -102,7 +112,7 @@ def test_FirstDerivative_centered(par): order=order, dtype="float32", ) - assert dottest(D1op, par["nx"], par["nx"], rtol=1e-3) + assert dottest(D1op, par["nx"], par["nx"], rtol=1e-3, backend=backend) x = (par["dx"] * np.arange(par["nx"])) ** 2 yana = 2 * par["dx"] * np.arange(par["nx"]) @@ -120,7 +130,13 @@ def test_FirstDerivative_centered(par): order=order, dtype="float32", ) - assert dottest(D1op, par["ny"] * par["nx"], par["ny"] * par["nx"], rtol=1e-3) + assert dottest( + D1op, + par["ny"] * par["nx"], + par["ny"] * par["nx"], + rtol=1e-3, + backend=backend, + ) x = np.outer((par["dy"] * np.arange(par["ny"])) ** 2, np.ones(par["nx"])) yana = np.outer(2 * par["dy"] * np.arange(par["ny"]), np.ones(par["nx"])) @@ -139,7 +155,13 @@ def test_FirstDerivative_centered(par): order=order, dtype="float32", ) - assert dottest(D1op, par["ny"] * par["nx"], par["ny"] * par["nx"], rtol=1e-3) + assert dottest( + D1op, + par["ny"] * par["nx"], + par["ny"] * par["nx"], + rtol=1e-3, + backend=backend, + ) x = np.outer((par["dy"] * np.arange(par["ny"])) ** 2, np.ones(par["nx"])) yana = np.zeros((par["ny"], par["nx"])) @@ -163,6 +185,7 @@ def test_FirstDerivative_centered(par): par["nz"] * par["ny"] * par["nx"], par["nz"] * par["ny"] * par["nx"], rtol=1e-3, + backend=backend, ) x = np.outer( @@ -191,6 +214,7 @@ def test_FirstDerivative_centered(par): par["nz"] * par["ny"] * par["nx"], par["nz"] * par["ny"] * par["nx"], rtol=1e-3, + backend=backend, ) x = np.outer( @@ -217,6 +241,7 @@ def test_FirstDerivative_centered(par): par["nz"] * par["ny"] * par["nx"], par["nz"] * par["ny"] * par["nx"], rtol=1e-3, + backend=backend, ) yana = np.zeros((par["nz"], par["ny"], par["nx"])) @@ -240,7 +265,7 @@ def test_FirstDerivative_forwaback(par): D1op = FirstDerivative( par["nx"], sampling=par["dx"], edge=par["edge"], kind=kind, dtype="float32" ) - assert dottest(D1op, par["nx"], par["nx"], rtol=1e-3) + assert dottest(D1op, par["nx"], par["nx"], rtol=1e-3, backend=backend) # 2d - derivative on 1st direction D1op = FirstDerivative( @@ -251,7 +276,13 @@ def test_FirstDerivative_forwaback(par): kind=kind, dtype="float32", ) - assert dottest(D1op, par["ny"] * par["nx"], par["ny"] * par["nx"], rtol=1e-3) + assert dottest( + D1op, + par["ny"] * par["nx"], + par["ny"] * par["nx"], + rtol=1e-3, + backend=backend, + ) # 2d - derivative on 2nd direction D1op = FirstDerivative( @@ -262,7 +293,13 @@ def test_FirstDerivative_forwaback(par): kind=kind, dtype="float32", ) - assert dottest(D1op, par["ny"] * par["nx"], par["ny"] * par["nx"], rtol=1e-3) + assert dottest( + D1op, + par["ny"] * par["nx"], + par["ny"] * par["nx"], + rtol=1e-3, + backend=backend, + ) # 3d - derivative on 1st direction D1op = FirstDerivative( @@ -278,6 +315,7 @@ def test_FirstDerivative_forwaback(par): par["nz"] * par["ny"] * par["nx"], par["nz"] * par["ny"] * par["nx"], rtol=1e-3, + backend=backend, ) # 3d - derivative on 2nd direction @@ -294,6 +332,7 @@ def test_FirstDerivative_forwaback(par): par["nz"] * par["ny"] * par["nx"], par["nz"] * par["ny"] * par["nx"], rtol=1e-3, + backend=backend, ) # 3d - derivative on 3rd direction @@ -310,6 +349,7 @@ def test_FirstDerivative_forwaback(par): par["nz"] * par["ny"] * par["nx"], par["nz"] * par["ny"] * par["nx"], rtol=1e-3, + backend=backend, ) @@ -333,7 +373,7 @@ def test_SecondDerivative_centered(par): D2op = SecondDerivative( par["nx"], sampling=par["dx"], edge=par["edge"], dtype="float32" ) - assert dottest(D2op, par["nx"], par["nx"], rtol=1e-3) + assert dottest(D2op, par["nx"], par["nx"], rtol=1e-3, backend=backend) # polynomial f(x) = x^3, f''(x) = 6x f = x**3 @@ -350,7 +390,9 @@ def test_SecondDerivative_centered(par): dtype="float32", ) - assert dottest(D2op, par["ny"] * par["nx"], par["ny"] * par["nx"], rtol=1e-3) + assert dottest( + D2op, par["ny"] * par["nx"], par["ny"] * par["nx"], rtol=1e-3, backend=backend + ) # polynomial f(x,y) = y^3, f_{yy}(x,y) = 6y f = yy**3 @@ -368,7 +410,9 @@ def test_SecondDerivative_centered(par): dtype="float32", ) - assert dottest(D2op, par["ny"] * par["nx"], par["ny"] * par["nx"], rtol=1e-3) + assert dottest( + D2op, par["ny"] * par["nx"], par["ny"] * par["nx"], rtol=1e-3, backend=backend + ) # polynomial f(x,y) = x^3, f_{xx}(x,y) = 6x f = xx**3 @@ -391,6 +435,7 @@ def test_SecondDerivative_centered(par): par["nz"] * par["ny"] * par["nx"], par["nz"] * par["ny"] * par["nx"], rtol=1e-3, + backend=backend, ) # polynomial f(x,y,z) = y^3, f_{yy}(x,y,z) = 6y @@ -415,6 +460,7 @@ def test_SecondDerivative_centered(par): par["nz"] * par["ny"] * par["nx"], par["nz"] * par["ny"] * par["nx"], rtol=1e-3, + backend=backend, ) # polynomial f(x,y,z) = x^3, f_{xx}(x,y,z) = 6x @@ -439,6 +485,7 @@ def test_SecondDerivative_centered(par): par["nz"] * par["ny"] * par["nx"], par["ny"] * par["nx"] * par["nz"], rtol=1e-3, + backend=backend, ) # polynomial f(x,y,z) = z^3, f_{zz}(x,y,z) = 6z @@ -473,7 +520,7 @@ def test_SecondDerivative_forwaback(par): kind="forward", dtype="float32", ) - assert dottest(D2op, par["nx"], par["nx"], rtol=1e-3) + assert dottest(D2op, par["nx"], par["nx"], rtol=1e-3, backend=backend) # 2d - derivative on 1st direction D2op = SecondDerivative( @@ -485,7 +532,13 @@ def test_SecondDerivative_forwaback(par): dtype="float32", ) - assert dottest(D2op, par["ny"] * par["nx"], par["ny"] * par["nx"], rtol=1e-3) + assert dottest( + D2op, + par["ny"] * par["nx"], + par["ny"] * par["nx"], + rtol=1e-3, + backend=backend, + ) # 2d - derivative on 2nd direction D2op = SecondDerivative( @@ -497,7 +550,13 @@ def test_SecondDerivative_forwaback(par): dtype="float32", ) - assert dottest(D2op, par["ny"] * par["nx"], par["ny"] * par["nx"], rtol=1e-3) + assert dottest( + D2op, + par["ny"] * par["nx"], + par["ny"] * par["nx"], + rtol=1e-3, + backend=backend, + ) # 3d - derivative on 1st direction D2op = SecondDerivative( @@ -514,6 +573,7 @@ def test_SecondDerivative_forwaback(par): par["nz"] * par["ny"] * par["nx"], par["nz"] * par["ny"] * par["nx"], rtol=1e-3, + backend=backend, ) # 3d - derivative on 2nd direction @@ -531,6 +591,7 @@ def test_SecondDerivative_forwaback(par): par["nz"] * par["ny"] * par["nx"], par["nz"] * par["ny"] * par["nx"], rtol=1e-3, + backend=backend, ) # 3d - derivative on 3rd direction @@ -548,6 +609,7 @@ def test_SecondDerivative_forwaback(par): par["nz"] * par["ny"] * par["nx"], par["ny"] * par["nx"] * par["nz"], rtol=1e-3, + backend=backend, ) @@ -565,7 +627,9 @@ def test_Laplacian(par): edge=par["edge"], dtype="float32", ) - assert dottest(Dlapop, par["ny"] * par["nx"], par["ny"] * par["nx"], rtol=1e-3) + assert dottest( + Dlapop, par["ny"] * par["nx"], par["ny"] * par["nx"], rtol=1e-3, backend=backend + ) # 2d - asymmetrical Dlapop = Laplacian( @@ -576,7 +640,9 @@ def test_Laplacian(par): edge=par["edge"], dtype="float32", ) - assert dottest(Dlapop, par["ny"] * par["nx"], par["ny"] * par["nx"], rtol=1e-3) + assert dottest( + Dlapop, par["ny"] * par["nx"], par["ny"] * par["nx"], rtol=1e-3, backend=backend + ) # 3d - symmetrical on 1st and 2nd direction Dlapop = Laplacian( @@ -592,6 +658,7 @@ def test_Laplacian(par): par["nz"] * par["ny"] * par["nx"], par["nz"] * par["ny"] * par["nx"], rtol=1e-3, + backend=backend, ) # 3d - symmetrical on 1st and 2nd direction @@ -608,6 +675,7 @@ def test_Laplacian(par): par["nz"] * par["ny"] * par["nx"], par["nz"] * par["ny"] * par["nx"], rtol=1e-3, + backend=backend, ) # 3d - symmetrical on all directions @@ -624,6 +692,7 @@ def test_Laplacian(par): par["nz"] * par["ny"] * par["nx"], par["nz"] * par["ny"] * par["nx"], rtol=1e-3, + backend=backend, ) @@ -641,7 +710,13 @@ def test_Gradient(par): kind=kind, dtype="float32", ) - assert dottest(Gop, 2 * par["ny"] * par["nx"], par["ny"] * par["nx"], rtol=1e-3) + assert dottest( + Gop, + 2 * par["ny"] * par["nx"], + par["ny"] * par["nx"], + rtol=1e-3, + backend=backend, + ) # 3d Gop = Gradient( @@ -656,6 +731,7 @@ def test_Gradient(par): 3 * par["nz"] * par["ny"] * par["nx"], par["nz"] * par["ny"] * par["nx"], rtol=1e-3, + backend=backend, ) @@ -674,7 +750,13 @@ def test_FirstDirectionalDerivative(par): kind=kind, dtype="float32", ) - assert dottest(Fdop, par["ny"] * par["nx"], par["ny"] * par["nx"], rtol=1e-3) + assert dottest( + Fdop, + par["ny"] * par["nx"], + par["ny"] * par["nx"], + rtol=1e-3, + backend=backend, + ) # 3d Fdop = FirstDirectionalDerivative( @@ -690,6 +772,7 @@ def test_FirstDirectionalDerivative(par): par["nz"] * par["ny"] * par["nx"], par["nz"] * par["ny"] * par["nx"], rtol=1e-3, + backend=backend, ) @@ -706,7 +789,9 @@ def test_SecondDirectionalDerivative(par): edge=par["edge"], dtype="float32", ) - assert dottest(Fdop, par["ny"] * par["nx"], par["ny"] * par["nx"], rtol=1e-3) + assert dottest( + Fdop, par["ny"] * par["nx"], par["ny"] * par["nx"], rtol=1e-3, backend=backend + ) # 3d Fdop = SecondDirectionalDerivative( @@ -721,6 +806,7 @@ def test_SecondDirectionalDerivative(par): par["nz"] * par["ny"] * par["nx"], par["nz"] * par["ny"] * par["nx"], rtol=1e-3, + backend=backend, ) diff --git a/pytests/test_describe.py b/pytests/test_describe.py old mode 100755 new mode 100644 index 755fdc799..6f8c4c037 --- a/pytests/test_describe.py +++ b/pytests/test_describe.py @@ -1,4 +1,9 @@ -import numpy as np +import os + +if int(os.environ.get("TEST_CUPY_PYLOPS", 0)): + import cupy as np +else: + import numpy as np from pylops.basicoperators import BlockDiag, Diagonal, HStack, MatrixMult from pylops.utils.describe import describe diff --git a/pytests/test_diagonal.py b/pytests/test_diagonal.py old mode 100755 new mode 100644 index 44bdd7bdb..e7c0dc556 --- a/pytests/test_diagonal.py +++ b/pytests/test_diagonal.py @@ -1,7 +1,16 @@ -import numpy as np +import os + +if int(os.environ.get("TEST_CUPY_PYLOPS", 0)): + import cupy as np + from cupy.testing import assert_array_almost_equal + + backend = "cupy" +else: + import numpy as np + from numpy.testing import assert_array_almost_equal + + backend = "numpy" import pytest -from numpy.testing import assert_array_almost_equal -from scipy.sparse.linalg import lsqr as sp_lsqr from pylops.basicoperators import Diagonal from pylops.optimization.basic import lsqr @@ -20,10 +29,21 @@ def test_Diagonal_1dsignal(par): d = np.arange(ddim) + 1.0 + par["imag"] * (np.arange(ddim) + 1.0) Dop = Diagonal(d, dtype=par["dtype"]) - assert dottest(Dop, ddim, ddim, complexflag=0 if par["imag"] == 0 else 3) + assert dottest( + Dop, ddim, ddim, complexflag=0 if par["imag"] == 0 else 3, backend=backend + ) x = np.ones(ddim) + par["imag"] * np.ones(ddim) - xlsqr = sp_lsqr(Dop, Dop * x, damp=1e-20, iter_lim=300, atol=1e-8, btol=1e-8, show=0)[0] + xlsqr = lsqr( + Dop, + Dop * x, + x0=np.zeros_like(x).ravel(), + damp=1e-20, + niter=300, + atol=1e-8, + btol=1e-8, + show=0, + )[0] assert_array_almost_equal(x, xlsqr, decimal=4) @@ -40,12 +60,22 @@ def test_Diagonal_2dsignal(par): par["nx"] * par["nt"], par["nx"] * par["nt"], complexflag=0 if par["imag"] == 0 else 3, + backend=backend, ) x = np.ones((par["nx"], par["nt"])) + par["imag"] * np.ones( (par["nx"], par["nt"]) ) - xlsqr = sp_lsqr(Dop, Dop * x.ravel(), damp=1e-20, iter_lim=300, atol=1e-8, btol=1e-8, show=0)[0] + xlsqr = lsqr( + Dop, + Dop * x.ravel(), + x0=np.zeros_like(x).ravel(), + damp=1e-20, + niter=300, + atol=1e-8, + btol=1e-8, + show=0, + )[0] assert_array_almost_equal(x.ravel(), xlsqr.ravel(), decimal=4) @@ -64,12 +94,22 @@ def test_Diagonal_3dsignal(par): par["ny"] * par["nx"] * par["nt"], par["ny"] * par["nx"] * par["nt"], complexflag=0 if par["imag"] == 0 else 3, + backend=backend, ) x = np.ones((par["ny"], par["nx"], par["nt"])) + par["imag"] * np.ones( (par["ny"], par["nx"], par["nt"]) ) - xlsqr = sp_lsqr(Dop, Dop * x.ravel(), damp=1e-20, iter_lim=300, atol=1e-8, btol=1e-8, show=0)[0] + xlsqr = lsqr( + Dop, + Dop * x.ravel(), + x0=np.zeros_like(x).ravel(), + damp=1e-20, + niter=300, + atol=1e-8, + btol=1e-8, + show=0, + )[0] assert_array_almost_equal(x.ravel(), xlsqr.ravel(), decimal=4) @@ -86,12 +126,22 @@ def test_Diagonal_2dsignal_unflattened(par): par["nx"] * par["nt"], par["nx"] * par["nt"], complexflag=0 if par["imag"] == 0 else 3, + backend=backend, ) x = np.ones((par["nx"], par["nt"])) + par["imag"] * np.ones( (par["nx"], par["nt"]) ) - xlsqr = lsqr(Dop, Dop * x, damp=1e-20, niter=300, atol=1e-8, btol=1e-8, show=0)[0] + xlsqr = lsqr( + Dop, + Dop * x, + x0=np.zeros_like(x), + damp=1e-20, + niter=300, + atol=1e-8, + btol=1e-8, + show=0, + )[0] assert_array_almost_equal(x, xlsqr, decimal=4) @@ -110,11 +160,21 @@ def test_Diagonal_3dsignal_unflattened(par): par["ny"] * par["nx"] * par["nt"], par["ny"] * par["nx"] * par["nt"], complexflag=0 if par["imag"] == 0 else 3, + backend=backend, ) x = np.ones((par["ny"], par["nx"], par["nt"])) + par["imag"] * np.ones( (par["ny"], par["nx"], par["nt"]) ) - xlsqr = lsqr(Dop, Dop * x, damp=1e-20, niter=300, atol=1e-8, btol=1e-8, show=0)[0] + xlsqr = lsqr( + Dop, + Dop * x, + x0=np.zeros_like(x), + damp=1e-20, + niter=300, + atol=1e-8, + btol=1e-8, + show=0, + )[0] assert_array_almost_equal(x, xlsqr, decimal=4) diff --git a/pytests/test_directwave.py b/pytests/test_directwave.py old mode 100755 new mode 100644 index 2d83380a1..80a716463 --- a/pytests/test_directwave.py +++ b/pytests/test_directwave.py @@ -1,4 +1,7 @@ +import os + import numpy as np +import pytest from numpy.testing import assert_array_almost_equal from scipy.signal import convolve @@ -12,6 +15,9 @@ vel = 2400.0 # velocity +@pytest.mark.skipif( + int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" +) def test_direct2D(): """Check consistency of analytical 2D Green's function with FD modelling""" inputdata = np.load(inputfile2d) @@ -48,6 +54,9 @@ def test_direct2D(): ) +@pytest.mark.skipif( + int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" +) def test_direct3D(): """Check consistency of analytical 3D Green's function with FD modelling""" inputdata = np.load(inputfile3d) diff --git a/pytests/test_dtcwt.py b/pytests/test_dtcwt.py index 979a7f766..d2902aa21 100644 --- a/pytests/test_dtcwt.py +++ b/pytests/test_dtcwt.py @@ -1,3 +1,5 @@ +import os + import numpy as np import pytest @@ -17,6 +19,9 @@ def sequential_array(shape): return result +@pytest.mark.skipif( + int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" +) @pytest.mark.parametrize("par", [(par1), (par2)]) def test_dtcwt1D_input1D(par): """Test for DTCWT with 1D input""" @@ -33,6 +38,9 @@ def test_dtcwt1D_input1D(par): np.testing.assert_allclose(t, y) +@pytest.mark.skipif( + int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" +) @pytest.mark.parametrize("par", [(par1), (par2)]) def test_dtcwt1D_input2D(par): """Test for DTCWT with 2D input (forward-inverse pair)""" @@ -54,6 +62,9 @@ def test_dtcwt1D_input2D(par): np.testing.assert_allclose(t, y) +@pytest.mark.skipif( + int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" +) @pytest.mark.parametrize("par", [(par1), (par2)]) def test_dtcwt1D_input3D(par): """Test for DTCWT with 3D input (forward-inverse pair)""" @@ -70,6 +81,9 @@ def test_dtcwt1D_input3D(par): np.testing.assert_allclose(t, y) +@pytest.mark.skipif( + int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" +) @pytest.mark.parametrize("par", [(par1), (par2)]) def test_dtcwt1D_birot(par): """Test for DTCWT birot (forward-inverse pair)""" diff --git a/pytests/test_dwts.py b/pytests/test_dwts.py old mode 100755 new mode 100644 index 09f567dcc..bc47870f7 --- a/pytests/test_dwts.py +++ b/pytests/test_dwts.py @@ -1,3 +1,5 @@ +import os + import numpy as np import pytest from numpy.testing import assert_array_almost_equal @@ -21,6 +23,9 @@ np.random.seed(10) +@pytest.mark.skipif( + int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" +) @pytest.mark.parametrize("par", [(par1)]) def test_unknown_wavelet(par): """Check error is raised if unknown wavelet is chosen is passed""" @@ -28,6 +33,9 @@ def test_unknown_wavelet(par): _ = DWT(dims=par["nt"], wavelet="foo") +@pytest.mark.skipif( + int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" +) @pytest.mark.parametrize("par", [(par1), (par2)]) def test_DWT_1dsignal(par): """Dot-test and inversion for DWT operator for 1d signal""" @@ -48,6 +56,9 @@ def test_DWT_1dsignal(par): assert_array_almost_equal(x, xinv, decimal=8) +@pytest.mark.skipif( + int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" +) @pytest.mark.parametrize("par", [(par1), (par2)]) def test_DWT_2dsignal(par): """Dot-test and inversion for DWT operator for 2d signal""" @@ -72,6 +83,9 @@ def test_DWT_2dsignal(par): assert_array_almost_equal(x.ravel(), xinv, decimal=8) +@pytest.mark.skipif( + int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" +) @pytest.mark.parametrize("par", [(par1), (par2)]) def test_DWT_3dsignal(par): """Dot-test and inversion for DWT operator for 3d signal""" @@ -98,6 +112,9 @@ def test_DWT_3dsignal(par): assert_array_almost_equal(x.ravel(), xinv, decimal=8) +@pytest.mark.skipif( + int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" +) @pytest.mark.parametrize("par", [(par1), (par2)]) def test_DWT2D_2dsignal(par): """Dot-test and inversion for DWT2D operator for 2d signal""" @@ -118,6 +135,9 @@ def test_DWT2D_2dsignal(par): assert_array_almost_equal(x.ravel(), xinv, decimal=8) +@pytest.mark.skipif( + int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" +) @pytest.mark.parametrize("par", [(par1), (par2)]) def test_DWT2D_3dsignal(par): """Dot-test and inversion for DWT operator for 3d signal""" @@ -144,6 +164,9 @@ def test_DWT2D_3dsignal(par): assert_array_almost_equal(x.ravel(), xinv, decimal=8) +@pytest.mark.skipif( + int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" +) @pytest.mark.parametrize("par", [(par3), (par4)]) def test_DWTND_3dsignal(par): """Dot-test and inversion for DWTND operator for 3d signal""" @@ -166,6 +189,9 @@ def test_DWTND_3dsignal(par): assert_array_almost_equal(x.ravel(), xinv, decimal=8) +@pytest.mark.skipif( + int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" +) @pytest.mark.parametrize("par", [(par3), (par4)]) def test_DWTND_4dsignal(par): """Dot-test and inversion for DWTND operator for 4d signal""" diff --git a/pytests/test_eigs.py b/pytests/test_eigs.py old mode 100755 new mode 100644 index 2861165b8..9a73c3575 --- a/pytests/test_eigs.py +++ b/pytests/test_eigs.py @@ -1,8 +1,19 @@ -import numpy as np +import os + +if int(os.environ.get("TEST_CUPY_PYLOPS", 0)): + import cupy as np + + backend = "cupy" +else: + import numpy as np + + backend = "numpy" +import numpy as npp import pytest from pylops.basicoperators import MatrixMult from pylops.optimization.eigs import power_iteration +from pylops.utils.backend import to_numpy par1 = {"n": 21, "imag": 0, "dtype": "float32"} # square, real par2 = {"n": 21, "imag": 1j, "dtype": "complex64"} # square, complex @@ -20,14 +31,14 @@ def test_power_iteration(par): # non-symmetric Aop = MatrixMult(A) - eig = power_iteration(Aop, niter=200, tol=0)[0] - eig_np = np.max(np.abs(np.linalg.eig(A)[0])) + eig = power_iteration(Aop, niter=200, tol=0, backend=backend)[0] + eig_np = npp.max(npp.abs(npp.linalg.eig(to_numpy(A))[0])) assert np.abs(np.abs(eig) - eig_np) < 1e-3 # symmetric A1op = MatrixMult(A1) - eig = power_iteration(A1op, niter=200, tol=0)[0] - eig_np = np.max(np.abs(np.linalg.eig(A1)[0])) + eig = power_iteration(A1op, niter=200, tol=0, backend=backend)[0] + eig_np = npp.max(npp.abs(npp.linalg.eig(to_numpy(A1))[0])) assert np.abs(np.abs(eig) - eig_np) < 1e-3 diff --git a/pytests/test_estimators.py b/pytests/test_estimators.py index 9a470c11e..38b1ab85a 100644 --- a/pytests/test_estimators.py +++ b/pytests/test_estimators.py @@ -1,8 +1,19 @@ -import numpy as np +import os + +if int(os.environ.get("TEST_CUPY_PYLOPS", 0)): + import cupy as np + + backend = "cupy" +else: + import numpy as np + + backend = "numpy" +import numpy as npp import pytest from numpy.testing import assert_almost_equal from pylops.basicoperators import MatrixMult +from pylops.utils.backend import to_numpy SAMPLERS = ["gaussian", "rayleigh", "rademacher", "unitvector"] DTYPES = ["float32", "float64"] @@ -28,12 +39,12 @@ def test_trace_hutchison(par): A = np.random.randn(n, n).astype(dtype) Aop = MatrixMult(A, dtype=dtype) - trace_true = np.trace(A) - assert type(trace_true) == np.dtype(dtype) + trace_true = npp.trace(to_numpy(A)) + assert type(trace_true) == npp.dtype(dtype) - trace_expl = Aop.trace() - assert type(trace_expl) == np.dtype(dtype) - assert_almost_equal(trace_true, trace_expl) + trace_expl = Aop.trace(backend=backend) + assert to_numpy(trace_expl).dtype == np.dtype(dtype) + assert_almost_equal(trace_true, trace_expl, decimal=5) # Hutchinson trace_est = Aop.trace( @@ -41,9 +52,10 @@ def test_trace_hutchison(par): batch_size=n + 1, method="hutchinson", sampler=sampler, + backend=backend, ) - assert type(trace_est) == np.dtype(dtype) - decimal = 7 if sampler == "unitvector" else -1 + assert to_numpy(trace_est).dtype == np.dtype(dtype) + decimal = 5 if sampler == "unitvector" else -1 assert_almost_equal(trace_true, trace_est, decimal=decimal) @@ -56,20 +68,21 @@ def test_trace_hutchpp(par): A = np.random.randn(n, n).astype(dtype) Aop = MatrixMult(A, dtype=dtype) - trace_true = np.trace(A) - assert type(trace_true) == np.dtype(dtype) + trace_true = npp.trace(to_numpy(A)) + assert type(trace_true) == npp.dtype(dtype) - trace_expl = Aop.trace() - assert type(trace_expl) == np.dtype(dtype) - assert_almost_equal(trace_true, trace_expl) + trace_expl = Aop.trace(backend=backend) + assert to_numpy(trace_expl).dtype == np.dtype(dtype) + assert_almost_equal(trace_true, trace_expl, decimal=5) # Hutch++ trace_est = Aop.trace( neval=10 * n, method="hutch++", sampler=sampler, + backend=backend, ) - assert type(trace_est) == np.dtype(dtype) + assert to_numpy(trace_est).dtype == np.dtype(dtype) assert_almost_equal(trace_true, trace_est, decimal=5) @@ -82,18 +95,19 @@ def test_trace_nahutchpp(par): A = np.random.randn(n, n).astype(dtype) Aop = MatrixMult(A, dtype=dtype) - trace_true = np.trace(A) - assert type(trace_true) == np.dtype(dtype) + trace_true = npp.trace(to_numpy(A)) + assert type(trace_true) == npp.dtype(dtype) - trace_expl = Aop.trace() - assert type(trace_expl) == np.dtype(dtype) - assert_almost_equal(trace_true, trace_expl) + trace_expl = Aop.trace(backend=backend) + assert to_numpy(trace_expl).dtype == np.dtype(dtype) + assert_almost_equal(trace_true, trace_expl, decimal=5) # NA-Hutch++ trace_est = Aop.trace( neval=10 * n, method="na-hutch++", sampler=sampler, + backend=backend, ) - assert type(trace_est) == np.dtype(dtype) + assert to_numpy(trace_est).dtype == np.dtype(dtype) assert_almost_equal(trace_true, trace_est, decimal=-1) diff --git a/pytests/test_ffts.py b/pytests/test_ffts.py old mode 100755 new mode 100644 index 77f49d22f..2c8d13c8a --- a/pytests/test_ffts.py +++ b/pytests/test_ffts.py @@ -1,10 +1,20 @@ import itertools +import os -import numpy as np +if int(os.environ.get("TEST_CUPY_PYLOPS", 0)): + import cupy as np + from cupy.testing import assert_array_almost_equal + + backend = "cupy" +else: + import numpy as np + from numpy.testing import assert_array_almost_equal + + backend = "numpy" +import numpy as npp import pytest -from numpy.testing import assert_array_almost_equal -from scipy.sparse.linalg import lsqr +from pylops.optimization.basic import lsqr from pylops.signalprocessing import FFT, FFT2D, FFTND from pylops.utils import dottest @@ -22,7 +32,7 @@ def _choose_random_axes(ndim, n_choices=2): axes_choices = list(range(-ndim, ndim)) axes = [] for _ in range(n_choices): - axis_chosen = np.random.choice(axes_choices) + axis_chosen = npp.random.choice(axes_choices) # Remove chosen and its symmetrical counterpart axes_choices.remove(axis_chosen) axes_choices.remove(axis_chosen - (1 if axis_chosen >= 0 else -1) * ndim) @@ -234,13 +244,16 @@ def test_unknown_engine(par): ) +dtype_precision = [ + (np.float16, 1), + (np.float32, 4), + (np.float64, 11), +] +if backend == "numpy": + dtype_precision.append((np.longdouble, 11)) + par_lists_fft_small_real = dict( - dtype_precision=[ - (np.float16, 1), - (np.float32, 4), - (np.float64, 11), - (np.longdouble, 11), - ], + dtype_precision=dtype_precision, norm=["ortho", "none", "1/n"], ifftshift_before=[False, True], engine=["numpy", "fftw", "scipy"], @@ -254,65 +267,68 @@ def test_unknown_engine(par): @pytest.mark.parametrize("par", pars_fft_small_real) def test_FFT_small_real(par): - dtype, decimal = par["dtype_precision"] - norm = par["norm"] - ifftshift_before = par["ifftshift_before"] - engine = par["engine"] + np.random.seed(5) - x = np.array([1, 0, -1, 1], dtype=dtype) + if backend == "numpy" or (backend == "cupy" and par["engine"] == "numpy"): + dtype, decimal = par["dtype_precision"] + norm = par["norm"] + ifftshift_before = par["ifftshift_before"] + engine = par["engine"] - FFTop = FFT( - dims=x.shape, - axis=0, - norm=norm, - real=True, - ifftshift_before=ifftshift_before, - dtype=dtype, - engine=engine, - ) - y = FFTop * x.ravel() - - if norm == "ortho": - y_true = np.array([0.5, 1 + 0.5j, -0.5], dtype=FFTop.cdtype) - elif norm == "none": - y_true = np.array([1, 2 + 1j, -1], dtype=FFTop.cdtype) - elif norm == "1/n": - y_true = np.array([0.25, 0.5 + 0.25j, -0.25], dtype=FFTop.cdtype) + x = np.array([1, 0, -1, 1], dtype=dtype) - y_true[1:-1] *= np.sqrt(2) # Zero and Nyquist - if ifftshift_before: - # `ifftshift_before`` is useful when the time-axis is centered around zero as - # it ensures the time axis to starts at zero: - # [-2, -1, 0, 1] ---ifftshift--> [0, 1, -2, -1] - # This does not alter the amplitude of the FFT, but does alter the phase. To - # match the results without ifftshift, we need to add a phase shift opposite to - # the one introduced by FFT as given below. See "An FFT Primer for physicists", - # by Thomas Kaiser. - # https://www.iap.uni-jena.de/iapmedia/de/Lecture/Computational+Photonics/CoPho19_supp_FFT_primer.pdf - x0 = -np.ceil(len(x) / 2) - y_true *= np.exp(2 * np.pi * 1j * FFTop.f * x0) - - assert_array_almost_equal(y, y_true, decimal=decimal) - assert dottest(FFTop, len(y), len(x), complexflag=0, rtol=10 ** (-decimal)) - assert dottest(FFTop, len(y), len(x), complexflag=2, rtol=10 ** (-decimal)) + FFTop = FFT( + dims=x.shape, + axis=0, + norm=norm, + real=True, + ifftshift_before=ifftshift_before, + dtype=dtype, + engine=engine, + ) + FFTop.f = np.asarray(FFTop.f) + y = FFTop * x.ravel() + + if norm == "ortho": + y_true = np.array([0.5, 1 + 0.5j, -0.5], dtype=FFTop.cdtype) + elif norm == "none": + y_true = np.array([1, 2 + 1j, -1], dtype=FFTop.cdtype) + elif norm == "1/n": + y_true = np.array([0.25, 0.5 + 0.25j, -0.25], dtype=FFTop.cdtype) + + y_true[1:-1] *= np.sqrt(2) # Zero and Nyquist + if ifftshift_before: + # `ifftshift_before`` is useful when the time-axis is centered around zero as + # it ensures the time axis to starts at zero: + # [-2, -1, 0, 1] ---ifftshift--> [0, 1, -2, -1] + # This does not alter the amplitude of the FFT, but does alter the phase. To + # match the results without ifftshift, we need to add a phase shift opposite to + # the one introduced by FFT as given below. See "An FFT Primer for physicists", + # by Thomas Kaiser. + # https://www.iap.uni-jena.de/iapmedia/de/Lecture/Computational+Photonics/CoPho19_supp_FFT_primer.pdf + x0 = -np.ceil(len(x) / 2) + y_true *= np.exp(2 * np.pi * 1j * FFTop.f * x0) + + assert_array_almost_equal(y, y_true, decimal=decimal) + assert dottest( + FFTop, len(y), len(x), complexflag=0, rtol=10 ** (-decimal), backend=backend + ) + assert dottest( + FFTop, len(y), len(x), complexflag=2, rtol=10 ** (-decimal), backend=backend + ) - x_inv = FFTop / y - x_inv = x_inv.reshape(x.shape) - assert_array_almost_equal(x_inv, x, decimal=decimal) + x_inv = FFTop / y + x_inv = x_inv.reshape(x.shape) + assert_array_almost_equal(x_inv, x, decimal=decimal) par_lists_fft_random_real = dict( shape=[ - np.random.randint(1, 20, size=(1,)), - np.random.randint(1, 20, size=(2,)), - np.random.randint(1, 20, size=(3,)), - ], - dtype_precision=[ - (np.float16, 1), - (np.float32, 3), - (np.float64, 11), - (np.longdouble, 11), + npp.random.randint(1, 20, size=(1,)), + npp.random.randint(1, 20, size=(2,)), + npp.random.randint(1, 20, size=(3,)), ], + dtype_precision=dtype_precision, ifftshift_before=[False, True], engine=["numpy", "fftw", "scipy"], ) @@ -322,8 +338,14 @@ def test_FFT_small_real(par): ] +@pytest.mark.skipif( + int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, + reason="Dot-test failure with CuPy enabled when running entire test suite", +) @pytest.mark.parametrize("par", pars_fft_random_real) def test_FFT_random_real(par): + np.random.seed(5) + shape = par["shape"] dtype, decimal = par["dtype_precision"] ifftshift_before = par["ifftshift_before"] @@ -346,18 +368,25 @@ def test_FFT_random_real(par): # Ensure inverse and adjoint recover x xadj = FFTop.H * y # adjoint is same as inverse for fft - xinv = lsqr(FFTop, y, damp=0, iter_lim=10, atol=1e-8, btol=1e-8, show=0)[0] + xinv = lsqr(FFTop, y, damp=0, niter=10, atol=1e-8, btol=1e-8, show=0)[0].ravel() assert_array_almost_equal(x, xadj, decimal=decimal) assert_array_almost_equal(x, xinv, decimal=decimal) # Dot tests nr, nc = FFTop.shape - assert dottest(FFTop, nr, nc, complexflag=0, rtol=10 ** (-decimal)) - assert dottest(FFTop, nr, nc, complexflag=2, rtol=10 ** (-decimal)) + assert dottest(FFTop, nr, nc, complexflag=0, rtol=10 ** (-decimal), backend=backend) + assert dottest(FFTop, nr, nc, complexflag=2, rtol=10 ** (-decimal), backend=backend) +dtype_precision_cpx = [ + (np.complex64, 4), + (np.complex128, 11), +] +if backend == "numpy": + dtype_precision_cpx.append((np.clongdouble, 11)) + par_lists_fft_small_cpx = dict( - dtype_precision=[(np.complex64, 4), (np.complex128, 11), (np.clongdouble, 11)], + dtype_precision=dtype_precision_cpx, norm=["ortho", "none", "1/n"], ifftshift_before=[False, True], fftshift_after=[False, True], @@ -371,6 +400,7 @@ def test_FFT_random_real(par): @pytest.mark.parametrize("par", pars_fft_small_cpx) def test_FFT_small_complex(par): + np.random.seed(5) dtype, decimal = par["dtype_precision"] norm = par["norm"] ifftshift_before = par["ifftshift_before"] @@ -399,33 +429,37 @@ def test_FFT_small_complex(par): y_true = np.fft.fftshift(y_true) if ifftshift_before: x0 = -np.ceil(x.shape[0] / 2) - y_true *= np.exp(2 * np.pi * 1j * FFTop.f * x0) + y_true *= np.exp(2 * np.pi * 1j * np.asarray(FFTop.f) * x0) # Compute FFT with FFTop and compare with y_true y = FFTop * x.ravel() assert_array_almost_equal(y, y_true, decimal=decimal) - assert dottest(FFTop, *FFTop.shape, complexflag=3, rtol=10 ** (-decimal)) + assert dottest( + FFTop, *FFTop.shape, complexflag=3, rtol=10 ** (-decimal), backend=backend + ) x_inv = FFTop / y x_inv = x_inv.reshape(x.shape) assert_array_almost_equal(x_inv, x, decimal=decimal) +dtype_precision_cpx1 = [ + (np.float16, 1), + (np.float32, 3), + (np.float64, 11), + (np.complex64, 3), + (np.complex128, 11), +] +if backend == "numpy": + dtype_precision_cpx1.append((np.longdouble, 11)) + dtype_precision_cpx1.append((np.clongdouble, 11)) par_lists_fft_random_cpx = dict( shape=[ - np.random.randint(1, 20, size=(1,)), - np.random.randint(1, 20, size=(2,)), - np.random.randint(1, 20, size=(3,)), - ], - dtype_precision=[ - (np.float16, 1), - (np.float32, 3), - (np.float64, 11), - (np.longdouble, 11), - (np.complex64, 3), - (np.complex128, 11), - (np.clongdouble, 11), + npp.random.randint(1, 20, size=(1,)), + npp.random.randint(1, 20, size=(2,)), + npp.random.randint(1, 20, size=(3,)), ], + dtype_precision=dtype_precision_cpx1, ifftshift_before=[False, True], fftshift_after=[False, True], engine=["numpy", "fftw", "scipy"], @@ -438,73 +472,78 @@ def test_FFT_small_complex(par): @pytest.mark.parametrize("par", pars_fft_random_cpx) def test_FFT_random_complex(par): - shape = par["shape"] - dtype, decimal = par["dtype_precision"] - ifftshift_before = par["ifftshift_before"] - fftshift_after = par["fftshift_after"] - engine = par["engine"] - - x = np.random.randn(*shape).astype(dtype) - if np.issubdtype(dtype, np.complexfloating): - x += 1j * np.random.randn(*shape).astype(dtype) - - # Select an axis to apply FFT on. It can be any integer - # in [0,..., ndim-1] but also in [-ndim, ..., -1] - axis = _choose_random_axes(x.ndim, n_choices=1)[0] - - FFTop = FFT( - dims=x.shape, - axis=axis, - ifftshift_before=ifftshift_before, - fftshift_after=fftshift_after, - dtype=dtype, - engine=engine, - ) - - # Compute FFT of x independently - y_true = np.fft.fft(x, axis=axis, norm="ortho") - if fftshift_after: - y_true = np.fft.fftshift(y_true, axes=axis) - if ifftshift_before: - y_true = np.swapaxes(y_true, axis, -1) - x0 = -np.ceil(x.shape[axis] / 2) - phase_correction = np.exp(2 * np.pi * 1j * FFTop.f * x0) - y_true *= phase_correction - y_true = np.swapaxes(y_true, -1, axis) - y_true = y_true.ravel() - - # Compute FFT with FFTop and compare with y_true - x = x.ravel() - y = FFTop * x - assert_array_almost_equal(y, y_true, decimal=decimal) - - # Ensure inverse and adjoint recover x - xadj = FFTop.H * y # adjoint is same as inverse for fft - xinv = lsqr(FFTop, y, damp=0, iter_lim=10, atol=1e-8, btol=1e-8, show=0)[0] - assert_array_almost_equal(x, xadj, decimal=decimal) - assert_array_almost_equal(x, xinv, decimal=decimal) + np.random.seed(5) + if backend == "numpy" or (backend == "cupy" and par["engine"] == "numpy"): + shape = par["shape"] + dtype, decimal = par["dtype_precision"] + ifftshift_before = par["ifftshift_before"] + fftshift_after = par["fftshift_after"] + engine = par["engine"] + + x = np.random.randn(*shape).astype(dtype) + if np.issubdtype(dtype, np.complexfloating): + x += 1j * np.random.randn(*shape).astype(dtype) + + # Select an axis to apply FFT on. It can be any integer + # in [0,..., ndim-1] but also in [-ndim, ..., -1] + axis = _choose_random_axes(x.ndim, n_choices=1)[0] + + FFTop = FFT( + dims=x.shape, + axis=axis, + ifftshift_before=ifftshift_before, + fftshift_after=fftshift_after, + dtype=dtype, + engine=engine, + ) - # Dot tests - nr, nc = FFTop.shape - assert dottest(FFTop, nr, nc, complexflag=0, rtol=10 ** (-decimal)) - assert dottest(FFTop, nr, nc, complexflag=2, rtol=10 ** (-decimal)) - if np.issubdtype(dtype, np.complexfloating): - assert dottest(FFTop, nr, nc, complexflag=1, rtol=10 ** (-decimal)) - assert dottest(FFTop, nr, nc, complexflag=3, rtol=10 ** (-decimal)) + # Compute FFT of x independently + y_true = np.fft.fft(x, axis=axis, norm="ortho") + if fftshift_after: + y_true = np.fft.fftshift(y_true, axes=int(axis)) + if ifftshift_before: + y_true = np.swapaxes(y_true, axis, -1) + x0 = -np.ceil(x.shape[axis] / 2) + phase_correction = np.exp(2 * np.pi * 1j * np.asarray(FFTop.f) * x0) + y_true *= phase_correction + y_true = np.swapaxes(y_true, -1, axis) + y_true = y_true.ravel() + + # Compute FFT with FFTop and compare with y_true + x = x.ravel() + y = FFTop * x + assert_array_almost_equal(y, y_true, decimal=decimal) + + # Ensure inverse and adjoint recover x + xadj = FFTop.H * y # adjoint is same as inverse for fft + xinv = lsqr(FFTop, y, damp=0, niter=10, atol=1e-8, btol=1e-8, show=0)[0].ravel() + assert_array_almost_equal(x, xadj, decimal=decimal) + assert_array_almost_equal(x, xinv, decimal=decimal) + + # Dot tests + nr, nc = FFTop.shape + assert dottest( + FFTop, nr, nc, complexflag=0, rtol=10 ** (-decimal), backend=backend + ) + assert dottest( + FFTop, nr, nc, complexflag=2, rtol=10 ** (-decimal), backend=backend + ) + if np.issubdtype(dtype, np.complexfloating): + assert dottest( + FFTop, nr, nc, complexflag=1, rtol=10 ** (-decimal), backend=backend + ) + assert dottest( + FFTop, nr, nc, complexflag=3, rtol=10 ** (-decimal), backend=backend + ) par_lists_fft2d_random_real = dict( shape=[ - np.random.randint(1, 5, size=(2,)), - np.random.randint(1, 5, size=(3,)), - np.random.randint(1, 5, size=(4,)), - ], - dtype_precision=[ - (np.float16, 1), - (np.float32, 3), - (np.float64, 11), - (np.longdouble, 11), + npp.random.randint(1, 5, size=(2,)), + npp.random.randint(1, 5, size=(3,)), + npp.random.randint(1, 5, size=(4,)), ], + dtype_precision=dtype_precision, ifftshift_before=[False, True], engine=["numpy", "scipy"], ) @@ -514,58 +553,60 @@ def test_FFT_random_complex(par): ] +@pytest.mark.skipif( + int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, + reason="Dot-test failure with CuPy enabled when running entire test suite", +) @pytest.mark.parametrize("par", pars_fft2d_random_real) def test_FFT2D_random_real(par): - shape = par["shape"] - dtype, decimal = par["dtype_precision"] - ifftshift_before = par["ifftshift_before"] - engine = par["engine"] - - x = np.random.randn(*shape).astype(dtype) - - # Select an axis to apply FFT on. It can be any integer - # in [0,..., ndim-1] but also in [-ndim, ..., -1] - # However, dimensions cannot be repeated - axes = _choose_random_axes(x.ndim, n_choices=2) - - FFTop = FFT2D( - dims=x.shape, - axes=axes, - ifftshift_before=ifftshift_before, - real=True, - dtype=dtype, - engine=engine, - ) - x = x.ravel() - y = FFTop * x + np.random.seed(5) + if backend == "numpy" or (backend == "cupy" and par["engine"] == "numpy"): + shape = par["shape"] + dtype, decimal = par["dtype_precision"] + ifftshift_before = par["ifftshift_before"] + engine = par["engine"] + + x = np.random.randn(*shape).astype(dtype) + + # Select an axis to apply FFT on. It can be any integer + # in [0,..., ndim-1] but also in [-ndim, ..., -1] + # However, dimensions cannot be repeated + axes = _choose_random_axes(x.ndim, n_choices=2) + + FFTop = FFT2D( + dims=x.shape, + axes=axes, + ifftshift_before=ifftshift_before, + real=True, + dtype=dtype, + engine=engine, + ) + x = x.ravel() + y = FFTop * x - # Ensure inverse and adjoint recover x - xadj = FFTop.H * y # adjoint is same as inverse for fft - xinv = lsqr(FFTop, y, damp=0, iter_lim=10, atol=1e-8, btol=1e-8, show=0)[0] - assert_array_almost_equal(x, xadj, decimal=decimal) - assert_array_almost_equal(x, xinv, decimal=decimal) + # Ensure inverse and adjoint recover x + xadj = FFTop.H * y # adjoint is same as inverse for fft + xinv = lsqr(FFTop, y, damp=0, niter=10, atol=1e-8, btol=1e-8, show=0)[0].ravel() + assert_array_almost_equal(x, xadj, decimal=decimal) + assert_array_almost_equal(x, xinv, decimal=decimal) - # Dot tests - nr, nc = FFTop.shape - assert dottest(FFTop, nr, nc, complexflag=0, rtol=10 ** (-decimal)) - assert dottest(FFTop, nr, nc, complexflag=2, rtol=10 ** (-decimal)) + # Dot tests + nr, nc = FFTop.shape + assert dottest( + FFTop, nr, nc, complexflag=0, rtol=10 ** (-decimal), backend=backend + ) + assert dottest( + FFTop, nr, nc, complexflag=2, rtol=10 ** (-decimal), backend=backend + ) par_lists_fft2d_random_cpx = dict( shape=[ - np.random.randint(1, 5, size=(2,)), - np.random.randint(1, 5, size=(3,)), - np.random.randint(1, 5, size=(5,)), - ], - dtype_precision=[ - (np.float16, 1), - (np.float32, 3), - (np.float64, 11), - (np.longdouble, 11), - (np.complex64, 3), - (np.complex128, 11), - (np.clongdouble, 11), + npp.random.randint(1, 5, size=(2,)), + npp.random.randint(1, 5, size=(3,)), + npp.random.randint(1, 5, size=(5,)), ], + dtype_precision=dtype_precision_cpx1, ifftshift_before=itertools.product([False, True], [False, True]), fftshift_after=itertools.product([False, True], [False, True]), engine=["numpy", "scipy"], @@ -579,72 +620,77 @@ def test_FFT2D_random_real(par): @pytest.mark.parametrize("par", pars_fft2d_random_cpx) def test_FFT2D_random_complex(par): - shape = par["shape"] - dtype, decimal = par["dtype_precision"] - ifftshift_before = par["ifftshift_before"] - fftshift_after = par["fftshift_after"] - engine = par["engine"] - - x = np.random.randn(*shape).astype(dtype) - if np.issubdtype(dtype, np.complexfloating): - x += 1j * np.random.randn(*shape).astype(dtype) - - # Select an axis to apply FFT on. It can be any integer - # in [0,..., ndim-1] but also in [-ndim, ..., -1] - # However, dimensions cannot be repeated - axes = _choose_random_axes(x.ndim, n_choices=2) - - FFTop = FFT2D( - dims=x.shape, - axes=axes, - ifftshift_before=ifftshift_before, - fftshift_after=fftshift_after, - dtype=dtype, - engine=engine, - ) - - # Compute FFT of x independently - x_ishift = x.copy() - for axis, ishift in zip(axes, ifftshift_before): - if ishift: - x_ishift = np.fft.ifftshift(x_ishift, axes=axis) - y_true = np.fft.fft2(x_ishift, axes=axes, norm="ortho") - for axis, fshift in zip(axes, fftshift_after): - if fshift: - y_true = np.fft.fftshift(y_true, axes=axis) - y_true = y_true.ravel() - - # Compute FFT with FFTop and compare with y_true - x = x.ravel() - y = FFTop * x - assert_array_almost_equal(y, y_true, decimal=decimal) - - # Ensure inverse and adjoint recover x - xadj = FFTop.H * y # adjoint is same as inverse for fft - xinv = lsqr(FFTop, y, damp=0, iter_lim=10, atol=1e-8, btol=1e-8, show=0)[0] - assert_array_almost_equal(x, xadj, decimal=decimal) - assert_array_almost_equal(x, xinv, decimal=decimal) + np.random.seed(5) + if backend == "numpy" or (backend == "cupy" and par["engine"] == "numpy"): + shape = par["shape"] + dtype, decimal = par["dtype_precision"] + ifftshift_before = par["ifftshift_before"] + fftshift_after = par["fftshift_after"] + engine = par["engine"] + + x = np.random.randn(*shape).astype(dtype) + if np.issubdtype(dtype, np.complexfloating): + x += 1j * np.random.randn(*shape).astype(dtype) + + # Select an axis to apply FFT on. It can be any integer + # in [0,..., ndim-1] but also in [-ndim, ..., -1] + # However, dimensions cannot be repeated + axes = _choose_random_axes(x.ndim, n_choices=2) + + FFTop = FFT2D( + dims=x.shape, + axes=axes, + ifftshift_before=ifftshift_before, + fftshift_after=fftshift_after, + dtype=dtype, + engine=engine, + ) - # Dot tests - nr, nc = FFTop.shape - assert dottest(FFTop, nr, nc, complexflag=0, rtol=10 ** (-decimal)) - assert dottest(FFTop, nr, nc, complexflag=2, rtol=10 ** (-decimal)) - if np.issubdtype(dtype, np.complexfloating): - assert dottest(FFTop, nr, nc, complexflag=1, rtol=10 ** (-decimal)) - assert dottest(FFTop, nr, nc, complexflag=3, rtol=10 ** (-decimal)) + # Compute FFT of x independently + x_ishift = x.copy() + for axis, ishift in zip(axes, ifftshift_before): + if ishift: + x_ishift = np.fft.ifftshift(x_ishift, axes=int(axis)) + y_true = np.fft.fft2(x_ishift, axes=axes, norm="ortho") + for axis, fshift in zip(axes, fftshift_after): + if fshift: + y_true = np.fft.fftshift(y_true, axes=int(axis)) + y_true = y_true.ravel() + + # Compute FFT with FFTop and compare with y_true + x = x.ravel() + y = FFTop * x + assert_array_almost_equal(y, y_true, decimal=decimal) + + # Ensure inverse and adjoint recover x + xadj = FFTop.H * y # adjoint is same as inverse for fft + xinv = lsqr(FFTop, y, damp=0, niter=10, atol=1e-8, btol=1e-8, show=0)[0].ravel() + assert_array_almost_equal(x, xadj, decimal=decimal) + assert_array_almost_equal(x, xinv, decimal=decimal) + + # Dot tests + nr, nc = FFTop.shape + assert dottest( + FFTop, nr, nc, complexflag=0, rtol=10 ** (-decimal), backend=backend + ) + assert dottest( + FFTop, nr, nc, complexflag=2, rtol=10 ** (-decimal), backend=backend + ) + if np.issubdtype(dtype, np.complexfloating): + assert dottest( + FFTop, nr, nc, complexflag=1, rtol=10 ** (-decimal), backend=backend + ) + assert dottest( + FFTop, nr, nc, complexflag=3, rtol=10 ** (-decimal), backend=backend + ) par_lists_fftnd_random_real = dict( shape=[ - np.random.randint(1, 5, size=(3,)), - np.random.randint(1, 5, size=(4,)), - ], - dtype_precision=[ - (np.float16, 1), - (np.float32, 3), - (np.float64, 11), - (np.longdouble, 11), + npp.random.randint(1, 5, size=(3,)), + npp.random.randint(1, 5, size=(4,)), ], + dtype_precision=dtype_precision, engine=["numpy", "scipy"], ) pars_fftnd_random_real = [ @@ -655,58 +701,56 @@ def test_FFT2D_random_complex(par): @pytest.mark.parametrize("par", pars_fftnd_random_real) def test_FFTND_random_real(par): - shape = par["shape"] - dtype, decimal = par["dtype_precision"] - engine = par["engine"] - - x = np.random.randn(*shape).astype(dtype) - - # Select an axis to apply FFT on. It can be any integer - # in [0,..., ndim-1] but also in [-ndim, ..., -1] - # However, dimensions cannot be repeated - n_choices = np.random.randint(3, x.ndim + 1) - axes = _choose_random_axes(x.ndim, n_choices=n_choices) - - # Trying out all posibilities is very cumbersome, let's select some shifts randomly - ifftshift_before = np.random.choice([False, True], size=n_choices) - - FFTop = FFTND( - dims=x.shape, - axes=axes, - ifftshift_before=ifftshift_before, - real=True, - dtype=dtype, - engine=engine, - ) - x = x.ravel() - y = FFTop * x + np.random.seed(5) + if backend == "numpy" or (backend == "cupy" and par["engine"] == "numpy"): + shape = par["shape"] + dtype, decimal = par["dtype_precision"] + engine = par["engine"] + + x = np.random.randn(*shape).astype(dtype) + + # Select an axis to apply FFT on. It can be any integer + # in [0,..., ndim-1] but also in [-ndim, ..., -1] + # However, dimensions cannot be repeated + n_choices = npp.random.randint(3, x.ndim + 1) + axes = _choose_random_axes(x.ndim, n_choices=n_choices) + + # Trying out all posibilities is very cumbersome, let's select some shifts randomly + ifftshift_before = npp.random.choice([False, True], size=n_choices) + + FFTop = FFTND( + dims=x.shape, + axes=axes, + ifftshift_before=ifftshift_before, + real=True, + dtype=dtype, + engine=engine, + ) + x = x.ravel() + y = FFTop * x - # Ensure inverse and adjoint recover x - xadj = FFTop.H * y # adjoint is same as inverse for fft - xinv = lsqr(FFTop, y, damp=0, iter_lim=10, atol=1e-8, btol=1e-8, show=0)[0] - assert_array_almost_equal(x, xadj, decimal=decimal) - assert_array_almost_equal(x, xinv, decimal=decimal) + # Ensure inverse and adjoint recover x + xadj = FFTop.H * y # adjoint is same as inverse for fft + xinv = lsqr(FFTop, y, damp=0, niter=10, atol=1e-8, btol=1e-8, show=0)[0].ravel() + assert_array_almost_equal(x, xadj, decimal=decimal) + assert_array_almost_equal(x, xinv, decimal=decimal) - # Dot tests - nr, nc = FFTop.shape - assert dottest(FFTop, nr, nc, complexflag=0, rtol=10 ** (-decimal)) - assert dottest(FFTop, nr, nc, complexflag=2, rtol=10 ** (-decimal)) + # Dot tests + nr, nc = FFTop.shape + assert dottest( + FFTop, nr, nc, complexflag=0, rtol=10 ** (-decimal), backend=backend + ) + assert dottest( + FFTop, nr, nc, complexflag=2, rtol=10 ** (-decimal), backend=backend + ) par_lists_fftnd_random_cpx = dict( shape=[ - np.random.randint(1, 5, size=(3,)), - np.random.randint(1, 5, size=(5,)), - ], - dtype_precision=[ - (np.float16, 1), - (np.float32, 3), - (np.float64, 11), - (np.longdouble, 11), - (np.complex64, 3), - (np.complex128, 11), - (np.clongdouble, 11), + npp.random.randint(1, 5, size=(3,)), + npp.random.randint(1, 5, size=(5,)), ], + dtype_precision=dtype_precision_cpx1, engine=["numpy", "scipy"], ) # Generate all combinations of the above parameters @@ -718,6 +762,7 @@ def test_FFTND_random_real(par): @pytest.mark.parametrize("par", pars_fftnd_random_cpx) def test_FFTND_random_complex(par): + np.random.seed(5) shape = par["shape"] dtype, decimal = par["dtype_precision"] engine = par["engine"] @@ -729,12 +774,12 @@ def test_FFTND_random_complex(par): # Select an axis to apply FFT on. It can be any integer # in [0,..., ndim-1] but also in [-ndim, ..., -1] # However, dimensions cannot be repeated - n_choices = np.random.randint(3, x.ndim + 1) + n_choices = npp.random.randint(3, x.ndim + 1) axes = _choose_random_axes(x.ndim, n_choices=n_choices) # Trying out all posibilities is very cumbersome, let's select some shifts randomly - ifftshift_before = np.random.choice([False, True], size=n_choices) - fftshift_after = np.random.choice([True, False], size=n_choices) + ifftshift_before = npp.random.choice([False, True], size=n_choices) + fftshift_after = npp.random.choice([True, False], size=n_choices) FFTop = FFTND( dims=x.shape, @@ -749,11 +794,11 @@ def test_FFTND_random_complex(par): x_ishift = x.copy() for axis, ishift in zip(axes, ifftshift_before): if ishift: - x_ishift = np.fft.ifftshift(x_ishift, axes=axis) + x_ishift = np.fft.ifftshift(x_ishift, axes=int(axis)) y_true = np.fft.fft2(x_ishift, axes=axes, norm="ortho") for axis, fshift in zip(axes, fftshift_after): if fshift: - y_true = np.fft.fftshift(y_true, axes=axis) + y_true = np.fft.fftshift(y_true, axes=int(axis)) y_true = y_true.ravel() # Compute FFT with FFTop and compare with y_true @@ -763,21 +808,25 @@ def test_FFTND_random_complex(par): # Ensure inverse and adjoint recover x xadj = FFTop.H * y # adjoint is same as inverse for fft - xinv = lsqr(FFTop, y, damp=0, iter_lim=10, atol=1e-8, btol=1e-8, show=0)[0] + xinv = lsqr(FFTop, y, damp=0, niter=10, atol=1e-8, btol=1e-8, show=0)[0].ravel() assert_array_almost_equal(x, xadj, decimal=decimal) assert_array_almost_equal(x, xinv, decimal=decimal) # Dot tests nr, nc = FFTop.shape - assert dottest(FFTop, nr, nc, complexflag=0, rtol=10 ** (-decimal)) - assert dottest(FFTop, nr, nc, complexflag=2, rtol=10 ** (-decimal)) + assert dottest(FFTop, nr, nc, complexflag=0, rtol=10 ** (-decimal), backend=backend) + assert dottest(FFTop, nr, nc, complexflag=2, rtol=10 ** (-decimal), backend=backend) if np.issubdtype(dtype, np.complexfloating): - assert dottest(FFTop, nr, nc, complexflag=1, rtol=10 ** (-decimal)) - assert dottest(FFTop, nr, nc, complexflag=3, rtol=10 ** (-decimal)) + assert dottest( + FFTop, nr, nc, complexflag=1, rtol=10 ** (-decimal), backend=backend + ) + assert dottest( + FFTop, nr, nc, complexflag=3, rtol=10 ** (-decimal), backend=backend + ) par_lists_fft2dnd_small_cpx = dict( - dtype_precision=[(np.complex64, 5), (np.complex128, 11), (np.clongdouble, 11)], + dtype_precision=dtype_precision_cpx, norm=["ortho", "none", "1/n"], engine=["numpy", "scipy"], ) @@ -789,6 +838,7 @@ def test_FFTND_random_complex(par): @pytest.mark.parametrize("par", pars_fft2dnd_small_cpx) def test_FFT2D_small_complex(par): + np.random.seed(5) dtype, decimal = par["dtype_precision"] norm = par["norm"] @@ -827,7 +877,9 @@ def test_FFT2D_small_complex(par): y = FFTop * x.ravel() y = y.reshape(FFTop.dimsd) assert_array_almost_equal(y, y_true, decimal=decimal) - assert dottest(FFTop, *FFTop.shape, complexflag=3, rtol=10 ** (-decimal)) + assert dottest( + FFTop, *FFTop.shape, complexflag=3, rtol=10 ** (-decimal), backend=backend + ) x_inv = FFTop / y.ravel() x_inv = x_inv.reshape(x.shape) @@ -836,6 +888,7 @@ def test_FFT2D_small_complex(par): @pytest.mark.parametrize("par", pars_fft2dnd_small_cpx) def test_FFTND_small_complex(par): + np.random.seed(5) dtype, decimal = par["dtype_precision"] norm = par["norm"] @@ -874,7 +927,9 @@ def test_FFTND_small_complex(par): y = FFTop * x.ravel() y = y.reshape(FFTop.dimsd) assert_array_almost_equal(y, y_true, decimal=decimal) - assert dottest(FFTop, *FFTop.shape, complexflag=3, rtol=10 ** (-decimal)) + assert dottest( + FFTop, *FFTop.shape, complexflag=3, rtol=10 ** (-decimal), backend=backend + ) x_inv = FFTop / y.ravel() x_inv = x_inv.reshape(x.shape) @@ -903,6 +958,7 @@ def test_FFTND_small_complex(par): ], ) def test_FFT_1dsignal(par): + np.random.seed(5) """Dot-test and inversion for FFT operator for 1d signal""" decimal = 3 if np.real(np.ones(1, par["dtype"])).dtype == np.float32 else 8 @@ -925,15 +981,34 @@ def test_FFT_1dsignal(par): if par["real"]: assert dottest( - FFTop, nfft // 2 + 1, par["nt"], complexflag=2, rtol=10 ** (-decimal) + FFTop, + nfft // 2 + 1, + par["nt"], + complexflag=2, + rtol=10 ** (-decimal), + backend=backend, ) else: - assert dottest(FFTop, nfft, par["nt"], complexflag=2, rtol=10 ** (-decimal)) - assert dottest(FFTop, nfft, par["nt"], complexflag=3, rtol=10 ** (-decimal)) + assert dottest( + FFTop, + nfft, + par["nt"], + complexflag=2, + rtol=10 ** (-decimal), + backend=backend, + ) + assert dottest( + FFTop, + nfft, + par["nt"], + complexflag=3, + rtol=10 ** (-decimal), + backend=backend, + ) y = FFTop * x xadj = FFTop.H * y # adjoint is same as inverse for fft - xinv = lsqr(FFTop, y, damp=1e-10, iter_lim=10, atol=1e-8, btol=1e-8, show=0)[0] + xinv = lsqr(FFTop, y, damp=1e-10, niter=10, atol=1e-8, btol=1e-8, show=0)[0] # check all signal if nt>nfft and only up to nfft if nfft d[ittot + 1, par["ny"] // 2, par["nx"] // 2] ) + solver_dict = ( + dict(damp=1e-10, iter_lim=50) + if backend == "numpy" + else dict(damp=1e-10, niter=50) + ) minv = MDD( - Gwav[:, :, par["nt"] - 1 :] if par["twosided"] else Gwav, + np.asarray(Gwav[:, :, par["nt"] - 1 :]) + if par["twosided"] + else np.asarray(Gwav), d[par["nt"] - 1 :].transpose(1, 2, 0) if par["twosided"] else d.transpose(1, 2, 0), @@ -244,6 +273,6 @@ def test_MDC_Nvirtualsources(par): adjoint=False, psf=False, dottest=False, - **dict(damp=1e-10, iter_lim=50, show=0) + **solver_dict ) assert_array_almost_equal(mwav, minv.transpose(2, 0, 1), decimal=2) diff --git a/pytests/test_wavelets.py b/pytests/test_wavelets.py old mode 100755 new mode 100644 index 87bdea4d2..dc6408bb1 --- a/pytests/test_wavelets.py +++ b/pytests/test_wavelets.py @@ -1,3 +1,5 @@ +import os + import numpy as np import pytest @@ -7,6 +9,9 @@ par2 = {"nt": 20, "dt": 0.004} # even samples +@pytest.mark.skipif( + int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" +) @pytest.mark.parametrize("par", [(par1), (par2)]) def test_gaussian(par): """Create gaussian wavelet and check size and central value""" @@ -18,6 +23,9 @@ def test_gaussian(par): assert wav[wcenter] == 1 +@pytest.mark.skipif( + int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" +) @pytest.mark.parametrize("par", [(par1), (par2)]) def test_klauder(par): """Create klauder wavelet and check size and central value""" @@ -29,6 +37,9 @@ def test_klauder(par): assert wav[wcenter] == 1 +@pytest.mark.skipif( + int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" +) @pytest.mark.parametrize("par", [(par1), (par2)]) def test_ormsby(par): """Create ormsby wavelet and check size and central value""" @@ -40,6 +51,9 @@ def test_ormsby(par): assert wav[wcenter] == 1 +@pytest.mark.skipif( + int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" +) @pytest.mark.parametrize("par", [(par1), (par2)]) def test_ricker(par): """Create ricker wavelet and check size and central value""" diff --git a/requirements-dev-gpu.txt b/requirements-dev-gpu.txt new file mode 100644 index 000000000..898f09273 --- /dev/null +++ b/requirements-dev-gpu.txt @@ -0,0 +1,25 @@ +numpy>=1.21.0 +scipy>=1.11.0 +cupy-cuda12x +torch +numba +sympy +matplotlib +ipython +pytest +pytest-runner +setuptools_scm +docutils<0.18 +Sphinx +pydata-sphinx-theme +sphinx-gallery +sphinxemoji +numpydoc +nbsphinx +image +pre-commit +autopep8 +isort +black +flake8 +mypy diff --git a/requirements-torch-gpu.txt b/requirements-torch-gpu.txt new file mode 100644 index 000000000..3f94f19f2 --- /dev/null +++ b/requirements-torch-gpu.txt @@ -0,0 +1,2 @@ +--index-url https://download.pytorch.org/whl/cpu +torch>=1.2.0,<2.5 From fa2773c5e9f81b34bd0b4c6951f0e2cc2d14e272 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Wed, 30 Apr 2025 22:36:49 +0100 Subject: [PATCH 29/44] minor: removed unused requirements file --- requirements-torch-gpu.txt | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 requirements-torch-gpu.txt diff --git a/requirements-torch-gpu.txt b/requirements-torch-gpu.txt deleted file mode 100644 index 3f94f19f2..000000000 --- a/requirements-torch-gpu.txt +++ /dev/null @@ -1,2 +0,0 @@ ---index-url https://download.pytorch.org/whl/cpu -torch>=1.2.0,<2.5 From 676c84cd810c4e85934689adfee7f3ef5d0aa242 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Wed, 30 Apr 2025 23:55:01 +0100 Subject: [PATCH 30/44] minor: fix jax import in test_jaxoperator --- pytests/test_jaxoperator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytests/test_jaxoperator.py b/pytests/test_jaxoperator.py index 4c9198c7e..1b11adb92 100644 --- a/pytests/test_jaxoperator.py +++ b/pytests/test_jaxoperator.py @@ -7,7 +7,7 @@ from pylops import JaxOperator, MatrixMult from pylops.utils import deps -jax_message = deps.devito_import("the jax module") +jax_message = deps.jax_import("the jax module") if jax_message is None: import jax From ce89a2d950ea888739e33304da8409f16b94111f Mon Sep 17 00:00:00 2001 From: mrava87 Date: Fri, 2 May 2025 21:57:37 +0100 Subject: [PATCH 31/44] bug: added safeguards to get_module_name Added safeguards to ensure cp and jnp are reached only if installed. --- pylops/utils/backend.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pylops/utils/backend.py b/pylops/utils/backend.py index 318362652..af315a29c 100644 --- a/pylops/utils/backend.py +++ b/pylops/utils/backend.py @@ -114,9 +114,9 @@ def get_module_name(mod: ModuleType) -> str: """ if mod == np: backend = "numpy" - elif mod == cp: + elif deps.cupy_enabled and mod == cp: backend = "cupy" - elif mod == jnp: + elif deps.jax_enabled and mod == jnp: backend = "jax" else: raise ValueError("module must be numpy, cupy, or jax") From fa9f2aa7c23f352b7cfb90bd91f15b47ca0c5730 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Sat, 3 May 2025 18:55:44 +0100 Subject: [PATCH 32/44] =?UTF-8?q?minor:=20improved=20docstring=20of=20?= =?UTF-8?q?=E2=89=88=E2=89=88inplace=5Fset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pylops/utils/backend.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pylops/utils/backend.py b/pylops/utils/backend.py index af315a29c..0c58f6caf 100644 --- a/pylops/utils/backend.py +++ b/pylops/utils/backend.py @@ -577,11 +577,11 @@ def inplace_set(x: npt.ArrayLike, y: npt.ArrayLike, idx: list) -> NDArray: Parameters ---------- x : :obj:`numpy.ndarray` or :obj:`jax.Array` - Array to sum + Array whose values are placed at indices ``idx`` y : :obj:`numpy.ndarray` or :obj:`jax.Array` Output array idx : :obj:`list` - Indices to sum at + Indices where values ``x`` are set Returns ------- From 4f3bb29acfc4ba164b1bd1c47e31665b98b6fa11 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Fri, 9 May 2025 21:10:39 +0100 Subject: [PATCH 33/44] test: reduce strenght of initial guess in irls tests --- pytests/test_sparsity.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pytests/test_sparsity.py b/pytests/test_sparsity.py index ad17772f4..1e7380782 100644 --- a/pytests/test_sparsity.py +++ b/pytests/test_sparsity.py @@ -100,8 +100,8 @@ def test_IRLS_data(par): Gop = MatrixMult(G, dtype=par["dtype"]) x = np.ones(par["nx"]) + par["imag"] * np.ones(par["nx"]) x0 = ( - np.random.normal(0, 10, par["nx"]) - + par["imag"] * np.random.normal(0, 10, par["nx"]) + np.random.normal(0, 1, par["nx"]) + + par["imag"] * np.random.normal(0, 1, par["nx"]) if par["x0"] else None ) @@ -139,8 +139,8 @@ def test_IRLS_datamodel(par): x[3] = 1 x[par["nx"] - 4] = -1 x0 = ( - np.random.normal(0, 10, par["nx"]) - + par["imag"] * np.random.normal(0, 10, par["nx"]) + np.random.normal(0, 1, par["nx"]) + + par["imag"] * np.random.normal(0, 1, par["nx"]) if par["x0"] else None ) From d2c5c0034dc323223d0d7e7546aea1a5e80f6805 Mon Sep 17 00:00:00 2001 From: Carlos da Costa Date: Sat, 7 Jun 2025 12:45:56 -0700 Subject: [PATCH 34/44] fixes #664 --- environment-dev-arm.yml | 2 ++ environment-dev.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/environment-dev-arm.yml b/environment-dev-arm.yml index c98dd04ec..5561967a7 100644 --- a/environment-dev-arm.yml +++ b/environment-dev-arm.yml @@ -15,6 +15,8 @@ dependencies: - pyfftw - pywavelets - sympy + - pymc>=5 + - pytensor - matplotlib - ipython - pytest diff --git a/environment-dev.yml b/environment-dev.yml index dfefcd9d4..07c50357b 100644 --- a/environment-dev.yml +++ b/environment-dev.yml @@ -15,6 +15,8 @@ dependencies: - pyfftw - pywavelets - sympy + - pymc>=5 + - pytensor - matplotlib - ipython - pytest From 2eaf978c1da606df1dddcc932ce209008d4205d8 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Mon, 9 Jun 2025 15:06:09 +0100 Subject: [PATCH 35/44] test: make pytensoroperator tests run on macOS --- pytests/test_pytensoroperator.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pytests/test_pytensoroperator.py b/pytests/test_pytensoroperator.py index caec04df5..02a3431a4 100644 --- a/pytests/test_pytensoroperator.py +++ b/pytests/test_pytensoroperator.py @@ -1,4 +1,5 @@ import os +import platform import numpy as np import pytest @@ -12,6 +13,10 @@ if pytensor_message is None: import pytensor + # avoid compile error on mac + if platform.system() == "Darwin": + pytensor.config.gcc__cxxflags = "-Wno-c++11-narrowing" + par1 = {"ny": 11, "nx": 11, "dtype": np.float32} # square par2 = {"ny": 21, "nx": 11, "dtype": np.float32} # overdetermined From 2f7371897a64fcd151a93fe6805598b681334205 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Wed, 11 Jun 2025 22:21:04 +0100 Subject: [PATCH 36/44] feat: add cuda backend to Kirchhoff --- pylops/waveeqprocessing/_kirchhoff_cuda.py | 488 +++++++++++++++++++++ pylops/waveeqprocessing/kirchhoff.py | 46 +- 2 files changed, 522 insertions(+), 12 deletions(-) create mode 100644 pylops/waveeqprocessing/_kirchhoff_cuda.py diff --git a/pylops/waveeqprocessing/_kirchhoff_cuda.py b/pylops/waveeqprocessing/_kirchhoff_cuda.py new file mode 100644 index 000000000..4a73347e1 --- /dev/null +++ b/pylops/waveeqprocessing/_kirchhoff_cuda.py @@ -0,0 +1,488 @@ +from math import cos, fabs + +import numpy as np +from numba import cuda + +from pylops.utils.backend import to_cupy + + +class _KirchhoffCudaHelper: + """A helper class to perform Kirchhoff demigration/migration using Numba CUDA. + + This class provides methods to compute the forward and adjoint operations for the + Kirchhoff operator, utilizing GPU acceleration through Numba's CUDA capabilities. + + Parameters + ---------- + ns : :obj:`int` + Number of sources. + nr : :obj:`int` + Number of receivers. + nt : :obj:`int` + Number of time samples. + ni : :obj:`int` + Number of image points. + dynamic : :obj:`bool`, optional + Flag indicating whether to use dynamic computation + (default is ``False``). + + """ + + def __init__(self, ns, nr, nt, ni, dynamic=False): + self.ns, self.nr, self.nt, self.ni = ns, nr, nt, ni + self.dynamic = dynamic + self._grid_setup() + + def _grid_setup(self): + """Set up CUDA grid and block dimensions. + + This method configures the number of blocks and threads per block for + CUDA kernels, depending on the number of sources and receivers. + """ + # use warp size as number of threads per block + current_device = cuda.get_current_device() + warp = current_device.WARP_SIZE // 2 + self.num_threads_per_blocks = (warp, warp) + # configure number of blocks + self.num_blocks = ( + (self.ns + self.num_threads_per_blocks[0] - 1) + // self.num_threads_per_blocks[0], + (self.nr + self.num_threads_per_blocks[1] - 1) + // self.num_threads_per_blocks[1], + ) + + @staticmethod + @cuda.jit + def _travsrcrec_kirch_matvec_cuda(x, y, ns, nr, nt, ni, dt, trav_srcs, trav_recs): + isrc, irec = cuda.grid(2) + if ns > isrc and nr > irec: + for ii in range(ni): + travisrc = trav_srcs[ii, isrc] + travirec = trav_recs[ii, irec] + trav = travisrc + travirec + itravii = int(trav / dt) + travdii = trav / dt - itravii + if 0 <= itravii < nt - 1: + ind1 = (isrc * nr + irec, itravii) + val1 = x[ii] * (1 - travdii) + ind2 = (isrc * nr + irec, itravii + 1) + val2 = x[ii] * travdii + cuda.atomic.add(y, ind1, val1) + cuda.atomic.add(y, ind2, val2) + + @staticmethod + @cuda.jit + def _travsrcrec_kirch_rmatvec_cuda(x, y, ns, nr, nt, ni, dt, trav_srcs, trav_recs): + isrc, irec = cuda.grid(2) + if ns > isrc and nr > irec: + for ii in range(ni): + travisrc = trav_srcs[ii, isrc] + travirec = trav_recs[ii, irec] + trav = travisrc + travirec + itravii = int(trav / dt) + travdii = trav / dt - itravii + if 0 <= itravii < nt - 1: + vii = ( + x[isrc * nr + irec, itravii] * (1 - travdii) + + x[isrc * nr + irec, itravii + 1] * travdii + ) + cuda.atomic.add(y, ii, vii) + + @staticmethod + @cuda.jit + def _ampsrcrec_kirch_matvec_cuda( + x, + y, + ns, + nr, + nt, + ni, + dt, + vel, + trav_srcs, + trav_recs, + amp_srcs, + amp_recs, + aperturemin, + aperturemax, + aperturetap, + nz, + six, + rix, + angleaperturemin, + angleaperturemax, + angles_srcs, + angles_recs, + ): + daperture = aperturemax - aperturemin + dangleaperture = angleaperturemax - angleaperturemin + + isrc, irec = cuda.grid(2) + if ns > isrc and nr > irec: + for ii in range(ni): + sixisrcrec = six[isrc * nr + irec] + rixisrcrec = rix[isrc * nr + irec] + travisrc = trav_srcs[ii, isrc] + travirec = trav_recs[ii, irec] + trav = travisrc + travirec + itravii = int(trav / dt) + travdii = trav / dt - itravii + ampisrc = amp_srcs[ii, isrc] + ampirec = amp_recs[ii, irec] + # extract source and receiver angle at given image point + angle_src = angles_srcs[ii, isrc] + angle_rec = angles_recs[ii, irec] + abs_angle_src = fabs(angle_src) + abs_angle_rec = fabs(angle_rec) + # compute cosine of half opening angle and total amplitude scaling + cosangle = cos((angle_src - angle_rec) / 2.0) + damp = 2.0 * cosangle * ampisrc * ampirec / vel[ii] + # identify z-index of image point + iz = ii % nz + # angle apertures checks + aptap = 1.0 + if ( + abs_angle_src < angleaperturemax + and abs_angle_rec < angleaperturemax + ): + if abs_angle_src >= angleaperturemin: + # extract source angle aperture taper value + aptap = ( + aptap + * aperturetap[ + int( + 20 + * (abs_angle_src - angleaperturemin) + // dangleaperture + ) + ] + ) + if abs_angle_rec >= angleaperturemin: + # extract receiver angle aperture taper value + aptap = ( + aptap + * aperturetap[ + int( + 20 + * (abs_angle_rec - angleaperturemin) + // dangleaperture + ) + ] + ) + + # aperture check + aperture = abs(sixisrcrec - rixisrcrec) / (iz + 1) + if aperture < aperturemax: + if aperture >= aperturemin: + # extract aperture taper value + aptap = ( + aptap + * aperturetap[ + int(20 * ((aperture - aperturemin) // daperture)) + ] + ) + # time limit check + if 0 <= itravii < nt - 1: + ind1 = (isrc * nr + irec, itravii) + val1 = x[ii] * (1 - travdii) * damp * aptap + ind2 = (isrc * nr + irec, itravii + 1) + val2 = x[ii] * travdii * damp * aptap + cuda.atomic.add(y, ind1, val1) + cuda.atomic.add(y, ind2, val2) + + @staticmethod + @cuda.jit + def _ampsrcrec_kirch_rmatvec_cuda( + x, + y, + ns, + nr, + nt, + ni, + dt, + vel, + trav_srcs, + trav_recs, + amp_srcs, + amp_recs, + aperturemin, + aperturemax, + aperturetap, + nz, + six, + rix, + angleaperturemin, + angleaperturemax, + angles_srcs, + angles_recs, + ): + daperture = aperturemax - aperturemin + dangleaperture = angleaperturemax - angleaperturemin + + isrc, irec = cuda.grid(2) + if ns > isrc and nr > irec: + for ii in range(ni): + sixisrcrec = six[isrc * nr + irec] + rixisrcrec = rix[isrc * nr + irec] + travisrc = trav_srcs[ii, isrc] + travirec = trav_recs[ii, irec] + trav = travisrc + travirec + itravii = int(trav / dt) + travdii = trav / dt - itravii + ampisrc = amp_srcs[ii, isrc] + ampirec = amp_recs[ii, irec] + # extract source and receiver angle at given image point + angle_src = angles_srcs[ii, isrc] + angle_rec = angles_recs[ii, irec] + abs_angle_src = fabs(angle_src) + abs_angle_rec = fabs(angle_rec) + # compute cosine of half opening angle and total amplitude scaling + cosangle = cos((angle_src - angle_rec) / 2.0) + damp = 2.0 * cosangle * ampisrc * ampirec / vel[ii] + # identify z-index of image point + iz = ii % nz + # angle apertures checks + aptap = 1.0 + if ( + abs_angle_src < angleaperturemax + and abs_angle_rec < angleaperturemax + ): + if abs_angle_src >= angleaperturemin: + # extract source angle aperture taper value + aptap = ( + aptap + * aperturetap[ + int( + 20 + * (abs_angle_src - angleaperturemin) + // dangleaperture + ) + ] + ) + if abs_angle_rec >= angleaperturemin: + # extract receiver angle aperture taper value + aptap = ( + aptap + * aperturetap[ + int( + 20 + * (abs_angle_rec - angleaperturemin) + // dangleaperture + ) + ] + ) + + # aperture check + aperture = abs(sixisrcrec - rixisrcrec) / (iz + 1) + if aperture < aperturemax: + if aperture >= aperturemin: + # extract aperture taper value + aptap = ( + aptap + * aperturetap[ + int(20 * ((aperture - aperturemin) // daperture)) + ] + ) + # time limit check + if 0 <= itravii < nt - 1: + ind1 = ii + val1 = ( + ( + x[isrc * nr + irec, itravii] * (1 - travdii) + + x[isrc * nr + irec, itravii + 1] * travdii + ) + * damp + * aptap + ) + cuda.atomic.add(y, ind1, val1) + + def _call_kinematic(self, opt, *inputs): + """Kinematic-only computations using CUDA. + + This method handles data preparation and execution of CUDA kernels + for both forward and adjoint operations of kinematic-only operator. + + Parameters + ---------- + opt : :obj:`str` + Operation type, either '_matvec' for forward or '_rmatvec' + for adjoint. + *inputs : :obj:`list` + List of input parameters required by the kernels. + + Returns + ------- + y_d : :obj:`cupy.ndarray` + Output data. + + """ + x_d = inputs[0] + y_d = inputs[1] + ns_d = np.int32(inputs[2]) + nr_d = np.int32(inputs[3]) + nt_d = np.int32(inputs[4]) + ni_d = np.int32(inputs[5]) + dt_d = np.float32(inputs[6]) + trav_srcs_d = to_cupy(inputs[7]) + trav_recs_d = to_cupy(inputs[8]) + + if opt == "_matvec": + self._travsrcrec_kirch_matvec_cuda[ + self.num_blocks, self.num_threads_per_blocks + ](x_d, y_d, ns_d, nr_d, nt_d, ni_d, dt_d, trav_srcs_d, trav_recs_d) + elif opt == "_rmatvec": + self._travsrcrec_kirch_rmatvec_cuda[ + self.num_blocks, self.num_threads_per_blocks + ](x_d, y_d, ns_d, nr_d, nt_d, ni_d, dt_d, trav_srcs_d, trav_recs_d) + cuda.synchronize() + + return y_d + + def _call_dynamic(self, opt, *inputs): + """Synamic computations using CUDA. + + This method handles data preparation and execution of CUDA kernels + for both forward and adjoint operations of dynamic operator. + + Parameters + ---------- + opt : :obj:`str` + Operation type, either '_matvec' for forward or '_rmatvec' + for adjoint. + *inputs : :obj:`list` + List of input parameters required by the kernels. + + Returns + ------- + y_d : :obj:`cupy.ndarray` + Output data. + + """ + x_d = inputs[0] + y_d = inputs[1] + ns_d = np.int32(inputs[2]) + nr_d = np.int32(inputs[3]) + nt_d = np.int32(inputs[4]) + ni_d = np.int32(inputs[5]) + dt_d = np.float32(inputs[6]) + vel_d = to_cupy(inputs[7]) + trav_srcs_d = to_cupy(inputs[8]) + trav_recs_d = to_cupy(inputs[9]) + amp_srcs_d = to_cupy(inputs[10]) + amp_recs_d = to_cupy(inputs[11]) + aperturemin_d = np.float32(inputs[12]) + aperturemax_d = np.float32(inputs[13]) + aperturetap_d = to_cupy(inputs[14]) + nz_d = np.int32(inputs[15]) + six_d = to_cupy(inputs[16]) + rix_d = to_cupy(inputs[17]) + angleaperturemin_d = np.float32(inputs[18]) + angleaperturemax_d = np.float32(inputs[19]) + angles_srcs_d = to_cupy(inputs[20]) + angles_recs_d = to_cupy(inputs[21]) + + if opt == "_matvec": + self._ampsrcrec_kirch_matvec_cuda[ + self.num_blocks, self.num_threads_per_blocks + ]( + x_d, + y_d, + ns_d, + nr_d, + nt_d, + ni_d, + dt_d, + vel_d, + trav_srcs_d, + trav_recs_d, + amp_srcs_d, + amp_recs_d, + aperturemin_d, + aperturemax_d, + aperturetap_d, + nz_d, + six_d, + rix_d, + angleaperturemin_d, + angleaperturemax_d, + angles_srcs_d, + angles_recs_d, + ) + elif opt == "_rmatvec": + self._ampsrcrec_kirch_rmatvec_cuda[ + self.num_blocks, self.num_threads_per_blocks + ]( + x_d, + y_d, + ns_d, + nr_d, + nt_d, + ni_d, + dt_d, + vel_d, + trav_srcs_d, + trav_recs_d, + amp_srcs_d, + amp_recs_d, + aperturemin_d, + aperturemax_d, + aperturetap_d, + nz_d, + six_d, + rix_d, + angleaperturemin_d, + angleaperturemax_d, + angles_srcs_d, + angles_recs_d, + ) + cuda.synchronize() + + return y_d + + def _matvec_cuda(self, *inputs): + """Forward with dispatching to appropriate CUDA kernels. + + This method selects the appropriate kernel to execute based on the + computation flags, and performs the forward operation. + + Parameters + ---------- + *inputs : :obj:`list` + List of input parameters required by the kernels. + + Returns + ------- + y_d : :obj:`cupy.ndarray` + Output (seismic data) of the forward operator. + + """ + if self.dynamic: + y_d = self._call_dynamic("_matvec", *inputs) + else: + y_d = self._call_kinematic("_matvec", *inputs) + + return y_d + + def _rmatvec_cuda(self, *inputs): + """Adjoint with dispatching to appropriate CUDA kernels. + + This method selects the appropriate kernel to execute based on the + computation flags, and performs the adjoint operation. + + Parameters + ---------- + *inputs : :obj:`list` + List of input parameters required by the kernels. + + Returns + ------- + y_d : :obj:`cupy.ndarray` + Output data (image) of the adjoint operator. + + """ + if self.dynamic: + y_d = self._call_dynamic("_rmatvec", *inputs) + else: + y_d = self._call_kinematic("_rmatvec", *inputs) + + return y_d diff --git a/pylops/waveeqprocessing/kirchhoff.py b/pylops/waveeqprocessing/kirchhoff.py index bdd5be878..9eed7dd6b 100644 --- a/pylops/waveeqprocessing/kirchhoff.py +++ b/pylops/waveeqprocessing/kirchhoff.py @@ -12,6 +12,7 @@ from pylops.signalprocessing import Convolve1D from pylops.utils import deps from pylops.utils._internal import _value_or_sized_to_array +from pylops.utils.backend import get_array_module from pylops.utils.decorators import reshaped from pylops.utils.tapers import taper from pylops.utils.typing import DTypeLike, NDArray @@ -25,6 +26,8 @@ if jit_message is None: from numba import jit, prange + from ._kirchhoff_cuda import _KirchhoffCudaHelper + # detect whether to use parallel or not numba_threads = int(os.getenv("NUMBA_NUM_THREADS", "1")) parallel = True if numba_threads != 1 else False @@ -82,8 +85,8 @@ class Kirchhoff(LinearOperator): :math:`\lbrack (n_y) n_x n_z \times n_s n_r \rbrack` or pair of traveltime tables of size :math:`\lbrack (n_y) n_x n_z \times n_s \rbrack` and :math:`\lbrack (n_y) n_x n_z \times n_r \rbrack` (to be provided if ``mode='byot'``). Note that the latter approach is recommended as less memory demanding - than the former. Moreover, only ``mode='dynamic'`` is only possible when traveltimes are provided in - the latter form. + than the former. Moreover, ``mode='dynamic'`` and ``engine='cuda'`` are only possible when traveltimes are + provided in the latter form. amp : :obj:`numpy.ndarray`, optional .. versionadded:: 2.0.0 @@ -108,7 +111,8 @@ class Kirchhoff(LinearOperator): Deprecated, will be removed in v3.0.0. Simply kept for back-compatibility with previous implementation, but effectively not affecting the behaviour of the operator. engine : :obj:`str`, optional - Engine used for computations (``numpy`` or ``numba``). + Engine used for computations (``numpy``, ``numba`` or ``cuda``). Note that the ``cuda`` engine + currently supports only ``dynamic=False``. dtype : :obj:`str`, optional Type of elements in input array. name : :obj:`str`, optional @@ -127,7 +131,10 @@ class Kirchhoff(LinearOperator): Raises ------ NotImplementedError - If ``mode`` is neither ``analytic``, ``eikonal``, or ``byot`` + If ``mode`` is neither ``analytic``, ``eikonal``, or ``byot``. + + NotImplementedError + If ``dynamic=True`` and ``engine="cuda"`` Notes ----- @@ -857,7 +864,7 @@ def _ampsrcrec_kirch_matvec( ] ) - # identify x-index of image point + # identify z-index of image point iz = ii % nz # aperture check aperture = abs(sixisrcrec - rixisrcrec) / (iz + 1) @@ -917,7 +924,7 @@ def _ampsrcrec_kirch_rmatvec( velii = vel[ii] angle_srcsii = angles_srcs[ii] angle_recsii = angles_recs[ii] - # identify x-index of image point + # identify z-index of image point iz = ii % nz for isrc in range(ns): trav_srcii = trav_srcsii[isrc] @@ -995,10 +1002,9 @@ def _ampsrcrec_kirch_rmatvec( return y def _register_multiplications(self, engine: str) -> None: - if engine not in ["numpy", "numba"]: - raise KeyError("engine must be numpy or numba") + if engine not in ["numpy", "numba", "cuda"]: + raise KeyError("engine must be numpy or numba or cuda") if engine == "numba" and jit_message is None: - # numba numba_opts = dict( nopython=True, nogil=True, parallel=parallel ) # fastmath=True, @@ -1011,7 +1017,21 @@ def _register_multiplications(self, engine: str) -> None: elif not self.travsrcrec: self._kirch_matvec = jit(**numba_opts)(self._trav_kirch_matvec) self._kirch_rmatvec = jit(**numba_opts)(self._trav_kirch_rmatvec) - + elif engine == "cuda": + if self.dynamic and self.travsrcrec: + self.cuda_helper = _KirchhoffCudaHelper( + self.ns, self.nr, self.nt, self.ni, True + ) + elif self.travsrcrec: + self.cuda_helper = _KirchhoffCudaHelper( + self.ns, self.nr, self.nt, self.ni, False + ) + elif not self.travsrcrec: + raise NotImplementedError( + "cuda not implemented for traveltimes " "provided in one table" + ) + self._kirch_matvec = self.cuda_helper._matvec_cuda + self._kirch_rmatvec = self.cuda_helper._rmatvec_cuda else: if engine == "numba" and jit_message is not None: logging.warning(jit_message) @@ -1027,7 +1047,8 @@ def _register_multiplications(self, engine: str) -> None: @reshaped def _matvec(self, x: NDArray) -> NDArray: - y = np.zeros((self.nsnr, self.nt), dtype=self.dtype) + ncp = get_array_module(x) + y = ncp.zeros((self.nsnr, self.nt), dtype=self.dtype) if self.dynamic and self.travsrcrec: inputs = ( x.ravel(), @@ -1074,9 +1095,10 @@ def _matvec(self, x: NDArray) -> NDArray: @reshaped def _rmatvec(self, x: NDArray) -> NDArray: + ncp = get_array_module(x) x = self.cop._rmatvec(x.ravel()) x = x.reshape(self.nsnr, self.nt) - y = np.zeros(self.ni, dtype=self.dtype) + y = ncp.zeros(self.ni, dtype=self.dtype) if self.dynamic and self.travsrcrec: inputs = ( x, From b944dcb92d1831f7ba3180cede83c2f471ab44e3 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Wed, 11 Jun 2025 23:11:28 +0100 Subject: [PATCH 37/44] minor: fix error message --- pylops/waveeqprocessing/kirchhoff.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pylops/waveeqprocessing/kirchhoff.py b/pylops/waveeqprocessing/kirchhoff.py index 9eed7dd6b..3f8e0a10f 100644 --- a/pylops/waveeqprocessing/kirchhoff.py +++ b/pylops/waveeqprocessing/kirchhoff.py @@ -1028,7 +1028,8 @@ def _register_multiplications(self, engine: str) -> None: ) elif not self.travsrcrec: raise NotImplementedError( - "cuda not implemented for traveltimes " "provided in one table" + "engine='cuda' not implemented for traveltimes " + "provided in one table" ) self._kirch_matvec = self.cuda_helper._matvec_cuda self._kirch_rmatvec = self.cuda_helper._rmatvec_cuda From af21b95de59b32733e0a813426731560b79abd38 Mon Sep 17 00:00:00 2001 From: Matteo Ravasi Date: Sun, 15 Jun 2025 21:46:54 +0100 Subject: [PATCH 38/44] CI: Python 3.10 in CI (until scikit-fmm is fixed) Temporarily use Python 3.10 in CI (until scikit-fmm is fixed) --- .github/workflows/build.yaml | 6 +++--- .github/workflows/codacy-coverage-reporter.yaml | 2 +- azure-pipelines.yml | 11 ++++++----- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 72645648d..86b5da1c3 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -1,4 +1,4 @@ -name: PyLops +name: PyLops Testing on: [push, pull_request] @@ -7,7 +7,7 @@ jobs: strategy: matrix: platform: [ ubuntu-latest, macos-13 ] - python-version: ["3.10", "3.11"] + python-version: ["3.10", ] # "3.11" temporarely removed due to issues with scikit-fmm runs-on: ${{ matrix.platform }} steps: @@ -30,6 +30,6 @@ jobs: run: | python -m setuptools_scm pip install . - - name: Test with pytest + - name: Tests with pytest run: | pytest diff --git a/.github/workflows/codacy-coverage-reporter.yaml b/.github/workflows/codacy-coverage-reporter.yaml index 05919f474..113357add 100644 --- a/.github/workflows/codacy-coverage-reporter.yaml +++ b/.github/workflows/codacy-coverage-reporter.yaml @@ -9,7 +9,7 @@ jobs: strategy: matrix: platform: [ ubuntu-latest, ] - python-version: ["3.11", ] + python-version: ["3.10", ] runs-on: ${{ matrix.platform }} steps: diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 706038bac..049f19bfe 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -18,7 +18,7 @@ jobs: # displayName: 'Windows' # # pool: -# vmImage: 'windows-2019' +# vmImage: 'windows-2025' # # variables: # NUMBA_NUM_THREADS: 1 @@ -26,17 +26,18 @@ jobs: # steps: # - task: UsePythonVersion@0 # inputs: -# versionSpec: '3.9' +# versionSpec: '3.10' # architecture: 'x64' # # - script: | # python -m pip install --upgrade pip setuptools wheel django # pip install -r requirements-dev.txt +# pip install -r requirements-torch.txt # pip install . # displayName: 'Install prerequisites and library' # # - script: | -# python setup.py test +# pytest # condition: succeededOrFailed() # displayName: 'Run tests' @@ -55,7 +56,7 @@ jobs: steps: - task: UsePythonVersion@0 inputs: - versionSpec: '3.11' + versionSpec: '3.10' architecture: 'x64' - script: | @@ -85,7 +86,7 @@ jobs: steps: - task: UsePythonVersion@0 inputs: - versionSpec: '3.11' + versionSpec: '3.10' architecture: 'x64' - script: | From 40624affd42b3bdb2cedd616391a2e5e32d5473f Mon Sep 17 00:00:00 2001 From: mrava87 Date: Mon, 16 Jun 2025 21:14:05 +0100 Subject: [PATCH 39/44] build: added make rules for tests on gpu and more on documentation --- CONTRIBUTING.md | 6 ++++++ Makefile | 13 ++++++++++++- docs/source/contributing.rst | 7 +++++++ docs/source/gpu.rst | 14 ++++++++++++++ 4 files changed, 39 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b2792a5e7..58ce812c6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -63,6 +63,12 @@ that the both old and new tests pass successfully: make tests ``` +If you have access to a GPU, it is advised also that old and new tests run with the CuPy +backend pass successfully: + ``` + make tests_gpu + ``` + 4. Run flake8 to check the quality of your code: ``` make lint diff --git a/Makefile b/Makefile index 8ae376e78..56355e51b 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ PIP := $(shell command -v pip3 2> /dev/null || command which pip 2> /dev/null) PYTHON := $(shell command -v python3 2> /dev/null || command which python 2> /dev/null) -.PHONY: install dev-install dev-install_gpu install_conda dev-install_conda dev-install_conda_arm tests doc docupdate servedoc lint typeannot coverage +.PHONY: install dev-install dev-install_gpu install_conda dev-install_conda dev-install_conda_arm tests tests_cpu_ongpu tests_gpu doc docupdate servedoc lint typeannot coverage pipcheck: ifndef PIP @@ -42,9 +42,20 @@ dev-install_conda_gpu: conda env create -f environment-dev-gpu.yml && conda activate pylops_gpu && pip install -e . tests: + # Run tests with CPU make pythoncheck pytest +tests_cpu_ongpu: + # Run tests with CPU on a system with GPU (and CuPy installed) + make pythoncheck + export CUPY_PYLOPS=0 && export TEST_CUPY_PYLOPS=0 && pytest + +tests_gpu: + # Run tests with GPU (requires CuPy to be installed) + make pythoncheck + export TEST_CUPY_PYLOPS=1 && pytest + doc: cd docs && rm -rf source/api/generated && rm -rf source/gallery &&\ rm -rf source/tutorials && rm -rf build && make html && cd .. diff --git a/docs/source/contributing.rst b/docs/source/contributing.rst index 6be387a69..600eb8daf 100755 --- a/docs/source/contributing.rst +++ b/docs/source/contributing.rst @@ -75,6 +75,13 @@ that the both old and new tests pass successfully: >> make tests +If you have access to a GPU, it is advised also that old and new tests run with the CuPy +backend pass successfully: + +.. code-block:: bash + + >> make tests_gpu + 4. Run flake8 to check the quality of your code: .. code-block:: bash diff --git a/docs/source/gpu.rst b/docs/source/gpu.rst index 842770b29..487786694 100755 --- a/docs/source/gpu.rst +++ b/docs/source/gpu.rst @@ -32,6 +32,20 @@ be also wrapped into a :class:`pylops.JaxOperator`. See below for a comphrensive list of supported operators and additional functionalities for both the ``cupy`` and ``jax`` backends. +Install dependencies +-------------------- +GPU-enabled development environments can created using ``conda`` + +.. code-block:: bash + + >> make dev-install_conda_gpu + +or ``pip`` + +.. code-block:: bash + + >> make dev-install_gpu + Examples -------- From 527b5398db4b3b284e06e94fa2665b6d7110d7de Mon Sep 17 00:00:00 2001 From: Yuxi Hong Date: Wed, 18 Jun 2025 16:32:41 -0400 Subject: [PATCH 40/44] CI: Cupy Testing GA (#672) Added new GA to run tests with CuPy backed on self-hosted runner. --------- Co-authored-by: Matteo Ravasi --- .github/workflows/buildcupy.yaml | 35 ++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/buildcupy.yaml diff --git a/.github/workflows/buildcupy.yaml b/.github/workflows/buildcupy.yaml new file mode 100644 index 000000000..d0e2fc381 --- /dev/null +++ b/.github/workflows/buildcupy.yaml @@ -0,0 +1,35 @@ +name: PyLops Testing (CuPy) + +on: [push, pull_request] + +jobs: + build: + runs-on: self-hosted + steps: + - name: Check out source repository + uses: actions/checkout@v4 + - name: Print checked code and check environments + run: | + ls -lah + echo $(pwd) + echo $(which python3) + echo $(which pip3) + - name: Set up Python environment and Install dependencies + run: | + python3 -m venv pylopsvenv + source pylopsvenv/bin/activate + echo $(which python3) + echo $(which pip3) + python3 -m pip install --upgrade pip setuptools + pip install flake8 pytest setuptools-scm + pip install -r requirements-dev-gpu.txt + - name: Install pylops + run: | + source pylopsvenv/bin/activate + python3 -m setuptools_scm + pip install . + - name: Tests with pytest + run: | + export CUPY_PYLOPS=1; export TEST_CUPY_PYLOPS=1; + source pylopsvenv/bin/activate + pytest From b7cdbaba7b591e001cd6b4374f68b7ab6a9f85bb Mon Sep 17 00:00:00 2001 From: mrava87 Date: Wed, 18 Jun 2025 23:12:24 +0100 Subject: [PATCH 41/44] test: added cuda tests for Kirchhoff --- pytests/test_kirchhoff.py | 128 ++++++++++++++++++++++++-------------- 1 file changed, 82 insertions(+), 46 deletions(-) diff --git a/pytests/test_kirchhoff.py b/pytests/test_kirchhoff.py index 50de034bd..4a37008b6 100644 --- a/pytests/test_kirchhoff.py +++ b/pytests/test_kirchhoff.py @@ -1,8 +1,19 @@ import os import numpy as np + +if int(os.environ.get("TEST_CUPY_PYLOPS", 0)): + import cupy as np + from cupy.testing import assert_array_almost_equal + + backend = "cuda" +else: + import numpy as np + from numpy.testing import assert_array_almost_equal + + backend = "numpy" +import numpy as npp import pytest -from numpy.testing import assert_array_almost_equal from pylops.utils import dottest from pylops.utils.wavelets import ricker @@ -34,22 +45,22 @@ skfmm_enabled = False v0 = 500 -y = np.arange(PAR["ny"]) * PAR["dy"] -x = np.arange(PAR["nx"]) * PAR["dx"] -z = np.arange(PAR["nz"]) * PAR["dz"] -t = np.arange(PAR["nt"]) * PAR["dt"] - -sy = np.linspace(y.min(), y.max(), PAR["nsy"]) -sx = np.linspace(x.min(), x.max(), PAR["nsx"]) -syy, sxx = np.meshgrid(sy, sx, indexing="ij") -s2d = np.vstack((sx, 2 * np.ones(PAR["nsx"]))) -s3d = np.vstack((syy.ravel(), sxx.ravel(), 2 * np.ones(PAR["nsx"] * PAR["nsy"]))) - -ry = np.linspace(y.min(), y.max(), PAR["nry"]) -rx = np.linspace(x.min(), x.max(), PAR["nrx"]) -ryy, rxx = np.meshgrid(ry, rx, indexing="ij") -r2d = np.vstack((rx, 2 * np.ones(PAR["nrx"]))) -r3d = np.vstack((ryy.ravel(), rxx.ravel(), 2 * np.ones(PAR["nrx"] * PAR["nry"]))) +y = npp.arange(PAR["ny"]) * PAR["dy"] +x = npp.arange(PAR["nx"]) * PAR["dx"] +z = npp.arange(PAR["nz"]) * PAR["dz"] +t = npp.arange(PAR["nt"]) * PAR["dt"] + +sy = npp.linspace(y.min(), y.max(), PAR["nsy"]) +sx = npp.linspace(x.min(), x.max(), PAR["nsx"]) +syy, sxx = npp.meshgrid(sy, sx, indexing="ij") +s2d = npp.vstack((sx, 2 * npp.ones(PAR["nsx"]))) +s3d = npp.vstack((syy.ravel(), sxx.ravel(), 2 * npp.ones(PAR["nsx"] * PAR["nsy"]))) + +ry = npp.linspace(y.min(), y.max(), PAR["nry"]) +rx = npp.linspace(x.min(), x.max(), PAR["nrx"]) +ryy, rxx = npp.meshgrid(ry, rx, indexing="ij") +r2d = npp.vstack((rx, 2 * npp.ones(PAR["nrx"]))) +r3d = npp.vstack((ryy.ravel(), rxx.ravel(), 2 * npp.ones(PAR["nrx"] * PAR["nry"]))) wav, _, wavc = ricker(t[:21], f0=40) @@ -157,7 +168,7 @@ def test_traveltime_table(): trav_ana, trav_srcs_ana, trav_recs_ana, - dist_ana, + _, _, _, ) = Kirchhoff._traveltime_table(z, x, s2d, r2d, v0, mode="analytic") @@ -166,7 +177,7 @@ def test_traveltime_table(): trav_eik, trav_srcs_eik, trav_recs_eik, - dist_eik, + _, _, _, ) = Kirchhoff._traveltime_table( @@ -181,20 +192,13 @@ def test_traveltime_table(): ( trav_srcs_ana, trav_recs_ana, - dist_srcs_ana, - dist_recs_ana, _, _, - ) = Kirchhoff._traveltime_table(z, x, s3d, r3d, v0, y=y, mode="analytic") - - ( - trav_srcs_eik, - trav_recs_eik, - dist_srcs_eik, - dist_recs_eik, _, _, - ) = Kirchhoff._traveltime_table( + ) = Kirchhoff._traveltime_table(z, x, s3d, r3d, v0, y=y, mode="analytic") + + (trav_srcs_eik, trav_recs_eik, _, _, _, _,) = Kirchhoff._traveltime_table( z, x, s3d, @@ -206,12 +210,8 @@ def test_traveltime_table(): assert_array_almost_equal(trav_srcs_ana, trav_srcs_eik, decimal=2) assert_array_almost_equal(trav_recs_ana, trav_recs_eik, decimal=2) - assert_array_almost_equal(trav_ana, trav_eik, decimal=2) -@pytest.mark.skipif( - int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" -) @pytest.mark.parametrize("par", [(par1), (par2), (par3), (par1d), (par2d), (par3d)]) def test_kirchhoff2d(par): """Dot-test for Kirchhoff operator""" @@ -226,8 +226,6 @@ def test_kirchhoff2d(par): ) + trav_recs.reshape(PAR["nx"] * PAR["nz"], 1, PAR["nrx"]) trav = trav.reshape(PAR["nx"] * PAR["nz"], PAR["nsx"] * PAR["nrx"]) amp = None - # amp = 1 / (dist + 1e-2 * dist.max()) - else: trav = None amp = None @@ -240,19 +238,31 @@ def test_kirchhoff2d(par): s2d, r2d, vel if par["mode"] == "eikonal" else v0, - wav, + np.asarray(wav), wavc, y=None, trav=trav, amp=amp, mode=par["mode"], + engine=backend, + ) + if par["mode"] == "byot": + Dop.trav = np.asarray(Dop.trav) + else: + Dop.trav_srcs = np.asarray(Dop.trav_srcs) + Dop.trav_recs = np.asarray(Dop.trav_recs) + if par["mode"] == "dynamic": + Dop.amp_srcs = np.asarray(Dop.amp_srcs) + Dop.amp_recs = np.asarray(Dop.amp_recs) + + assert dottest( + Dop, + PAR["nsx"] * PAR["nrx"] * PAR["nt"], + PAR["nz"] * PAR["nx"], + backend=backend, ) - assert dottest(Dop, PAR["nsx"] * PAR["nrx"] * PAR["nt"], PAR["nz"] * PAR["nx"]) -@pytest.mark.skipif( - int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" -) @pytest.mark.parametrize("par", [(par1), (par2), (par3)]) def test_kirchhoff3d(par): """Dot-test for Kirchhoff operator""" @@ -287,17 +297,22 @@ def test_kirchhoff3d(par): y=y, trav=trav, mode=par["mode"], + engine=backend, ) + if par["mode"] == "byot": + Dop.trav = np.asarray(Dop.trav) + else: + Dop.trav_srcs = np.asarray(Dop.trav_srcs) + Dop.trav_recs = np.asarray(Dop.trav_recs) + assert dottest( Dop, PAR["nsx"] * PAR["nrx"] * PAR["nsy"] * PAR["nry"] * PAR["nt"], PAR["nz"] * PAR["nx"] * PAR["ny"], + backend=backend, ) -@pytest.mark.skipif( - int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" -) @pytest.mark.parametrize( "par", [ @@ -323,7 +338,13 @@ def test_kirchhoff2d_trav_vs_travsrcrec(par): mode=par["mode"], dynamic=par["dynamic"], angleaperture=None, + engine=backend, ) + Dop.trav_srcs = np.asarray(Dop.trav_srcs) + Dop.trav_recs = np.asarray(Dop.trav_recs) + if par["dynamic"]: + Dop.amp_srcs = np.asarray(Dop.amp_srcs) + Dop.amp_recs = np.asarray(Dop.amp_recs) # old behaviour trav = Dop.trav_srcs.reshape( @@ -348,7 +369,13 @@ def test_kirchhoff2d_trav_vs_travsrcrec(par): mode=par["mode"], dynamic=par["dynamic"], angleaperture=None, + engine=backend, ) + D1op.trav_srcs = np.asarray(D1op.trav_srcs) + D1op.trav_recs = np.asarray(D1op.trav_recs) + if par["dynamic"]: + D1op.amp_srcs = np.asarray(D1op.amp_srcs) + D1op.amp_recs = np.asarray(D1op.amp_recs) # forward xx = np.random.normal(0, 1, PAR["nx"] * PAR["nz"]) @@ -359,9 +386,6 @@ def test_kirchhoff2d_trav_vs_travsrcrec(par): assert_array_almost_equal(Dop.H @ yy, D1op.H @ yy, decimal=2) -@pytest.mark.skipif( - int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" -) @pytest.mark.parametrize( "par", [ @@ -384,7 +408,13 @@ def test_kirchhoff3d_trav_vs_travsrcrec(par): wavc, y=y, mode=par["mode"], + engine=backend, ) + Dop.trav_srcs = np.asarray(Dop.trav_srcs) + Dop.trav_recs = np.asarray(Dop.trav_recs) + if par["dynamic"]: + Dop.amp_srcs = np.asarray(Dop.amp_srcs) + Dop.amp_recs = np.asarray(Dop.amp_recs) # old behaviour trav = Dop.trav_srcs.reshape( @@ -409,7 +439,13 @@ def test_kirchhoff3d_trav_vs_travsrcrec(par): y=y, trav=trav, mode=par["mode"], + engine=backend, ) + D1op.trav_srcs = np.asarray(D1op.trav_srcs) + D1op.trav_recs = np.asarray(D1op.trav_recs) + if par["dynamic"]: + D1op.amp_srcs = np.asarray(D1op.amp_srcs) + D1op.amp_recs = np.asarray(D1op.amp_recs) # forward xx = np.random.normal(0, 1, PAR["ny"] * PAR["nx"] * PAR["nz"]) From b66656777ddd987633940d60dca5a00267f6c6ae Mon Sep 17 00:00:00 2001 From: mrava87 Date: Wed, 18 Jun 2025 23:29:08 +0100 Subject: [PATCH 42/44] test: fix test_kirchhoff for cuda engine --- pytests/test_kirchhoff.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/pytests/test_kirchhoff.py b/pytests/test_kirchhoff.py index 4a37008b6..da672b0dd 100644 --- a/pytests/test_kirchhoff.py +++ b/pytests/test_kirchhoff.py @@ -6,7 +6,7 @@ import cupy as np from cupy.testing import assert_array_almost_equal - backend = "cuda" + backend = "cupy" else: import numpy as np from numpy.testing import assert_array_almost_equal @@ -215,6 +215,8 @@ def test_traveltime_table(): @pytest.mark.parametrize("par", [(par1), (par2), (par3), (par1d), (par2d), (par3d)]) def test_kirchhoff2d(par): """Dot-test for Kirchhoff operator""" + if backend == "cupy" and par["mode"] == "byot": + pytest.skip("cuda engine not available for single trav table") vel = v0 * np.ones((PAR["nx"], PAR["nz"])) if par["mode"] == "byot": @@ -244,7 +246,7 @@ def test_kirchhoff2d(par): trav=trav, amp=amp, mode=par["mode"], - engine=backend, + engine="numpy" if backend == "numpy" else "cuda", ) if par["mode"] == "byot": Dop.trav = np.asarray(Dop.trav) @@ -266,6 +268,8 @@ def test_kirchhoff2d(par): @pytest.mark.parametrize("par", [(par1), (par2), (par3)]) def test_kirchhoff3d(par): """Dot-test for Kirchhoff operator""" + if backend == "cupy" and par["mode"] == "byot": + pytest.skip("cuda engine not available for single trav table") vel = v0 * np.ones((PAR["ny"], PAR["nx"], PAR["nz"])) if par["mode"] == "byot": @@ -297,7 +301,7 @@ def test_kirchhoff3d(par): y=y, trav=trav, mode=par["mode"], - engine=backend, + engine="numpy" if backend == "numpy" else "cuda", ) if par["mode"] == "byot": Dop.trav = np.asarray(Dop.trav) @@ -338,7 +342,7 @@ def test_kirchhoff2d_trav_vs_travsrcrec(par): mode=par["mode"], dynamic=par["dynamic"], angleaperture=None, - engine=backend, + engine="numpy" if backend == "numpy" else "cuda", ) Dop.trav_srcs = np.asarray(Dop.trav_srcs) Dop.trav_recs = np.asarray(Dop.trav_recs) @@ -369,7 +373,7 @@ def test_kirchhoff2d_trav_vs_travsrcrec(par): mode=par["mode"], dynamic=par["dynamic"], angleaperture=None, - engine=backend, + engine="numpy" if backend == "numpy" else "cuda", ) D1op.trav_srcs = np.asarray(D1op.trav_srcs) D1op.trav_recs = np.asarray(D1op.trav_recs) @@ -408,7 +412,7 @@ def test_kirchhoff3d_trav_vs_travsrcrec(par): wavc, y=y, mode=par["mode"], - engine=backend, + engine="numpy" if backend == "numpy" else "cuda", ) Dop.trav_srcs = np.asarray(Dop.trav_srcs) Dop.trav_recs = np.asarray(Dop.trav_recs) @@ -439,7 +443,7 @@ def test_kirchhoff3d_trav_vs_travsrcrec(par): y=y, trav=trav, mode=par["mode"], - engine=backend, + engine="numpy" if backend == "numpy" else "cuda", ) D1op.trav_srcs = np.asarray(D1op.trav_srcs) D1op.trav_recs = np.asarray(D1op.trav_recs) From 768f9c55f1fe411d730818123d480f50cc639369 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Thu, 19 Jun 2025 22:34:24 +0100 Subject: [PATCH 43/44] doc: minor updates to docstring removing mention of dynamic not implemented with cuda --- pylops/waveeqprocessing/_kirchhoff_cuda.py | 11 ++++++----- pylops/waveeqprocessing/kirchhoff.py | 5 ++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pylops/waveeqprocessing/_kirchhoff_cuda.py b/pylops/waveeqprocessing/_kirchhoff_cuda.py index 4a73347e1..f987960f5 100644 --- a/pylops/waveeqprocessing/_kirchhoff_cuda.py +++ b/pylops/waveeqprocessing/_kirchhoff_cuda.py @@ -7,10 +7,10 @@ class _KirchhoffCudaHelper: - """A helper class to perform Kirchhoff demigration/migration using Numba CUDA. + """Helper class for Kirchhoff operator using Numba CUDA. - This class provides methods to compute the forward and adjoint operations for the - Kirchhoff operator, utilizing GPU acceleration through Numba's CUDA capabilities. + This class provides methods to compute the forward and adjoint operations of the + Kirchhoff operator utilizing GPU acceleration through Numba's CUDA capabilities. Parameters ---------- @@ -39,7 +39,8 @@ def _grid_setup(self): This method configures the number of blocks and threads per block for CUDA kernels, depending on the number of sources and receivers. """ - # use warp size as number of threads per block + # use half of warp size as number of threads per block + # although this will be ideally lifted up to the user current_device = cuda.get_current_device() warp = current_device.WARP_SIZE // 2 self.num_threads_per_blocks = (warp, warp) @@ -339,7 +340,7 @@ def _call_kinematic(self, opt, *inputs): return y_d def _call_dynamic(self, opt, *inputs): - """Synamic computations using CUDA. + """Dynamic computations using CUDA. This method handles data preparation and execution of CUDA kernels for both forward and adjoint operations of dynamic operator. diff --git a/pylops/waveeqprocessing/kirchhoff.py b/pylops/waveeqprocessing/kirchhoff.py index 3f8e0a10f..a3aa9a22b 100644 --- a/pylops/waveeqprocessing/kirchhoff.py +++ b/pylops/waveeqprocessing/kirchhoff.py @@ -111,8 +111,7 @@ class Kirchhoff(LinearOperator): Deprecated, will be removed in v3.0.0. Simply kept for back-compatibility with previous implementation, but effectively not affecting the behaviour of the operator. engine : :obj:`str`, optional - Engine used for computations (``numpy``, ``numba`` or ``cuda``). Note that the ``cuda`` engine - currently supports only ``dynamic=False``. + Engine used for computations (``numpy``, ``numba`` or ``cuda``). dtype : :obj:`str`, optional Type of elements in input array. name : :obj:`str`, optional @@ -134,7 +133,7 @@ class Kirchhoff(LinearOperator): If ``mode`` is neither ``analytic``, ``eikonal``, or ``byot``. NotImplementedError - If ``dynamic=True`` and ``engine="cuda"`` + If ``engine="cuda"`` and ``trav`` is provided as a single table Notes ----- From f8a853186724a6d90a5ea50c38762219f33c3a6f Mon Sep 17 00:00:00 2001 From: mrava87 Date: Sat, 21 Jun 2025 21:09:42 +0100 Subject: [PATCH 44/44] release: prepare for v2.5.0 --- CHANGELOG.md | 26 +++ docs/source/changelog.rst | 35 +++- docs/source/sg_execution_times.rst | 271 +++++++++++++++++++++++++++++ 3 files changed, 330 insertions(+), 2 deletions(-) create mode 100644 docs/source/sg_execution_times.rst diff --git a/CHANGELOG.md b/CHANGELOG.md index 8989f3059..cd8009518 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,32 @@ Changelog ========= +# 2.5.0 +* Added `cuda` engine to `pylops.waveeqprocessing.Kirchhoff` + operator +* Added `Opbasis` and `optimal_coeff` to + `pylops.optimization.cls_sparsity.OMP` +* Added `solver` to the input parameters of the `_oneshot` + internal methods of `pylops.waveeqprocessing.AcousticWave2D` + to avoid recreating it for every shot +* Added `kwargs_fft` to `pylops.signalprocessing.FFT`, + `pylops.signalprocessing.FFT2D`, and + `pylops.signalprocessing.FFTND` +* Fix bug in `pylops.waveeqprocessing.MDD` when using + CuPy arrays for `G` and `d` with `twosided=True` and `add_negative=True` +* Fix bug in `pylops.signalprocessing.FourierRadon3D` + in the default choice of `num_threads_per_blocks` +* Fix bug in `pylops.signalprocessing.Convolve1D` + in the definition of `pad` and `padd` when applying the + operator to a CuPy array +* Fix bug in `pylops.optimization.cls_sparsity.OMP` avoiding + passing `explicit` in the creation of `_ColumnLinearOperator` +* Fix bug in `pylops.optimization.cls_sparsity.OMP` callback + method as `cols` was not passed not allowing ``x`` to be + properly reconstructed +* Fix bug in `pylops.waveeqprocessing.SeismicInterpolation` + in calculation of `sampling` when not passed + # 2.4.0 * Added `pylops.signalprocessing.FourierRadon2d` and `pylops.signalprocessing.FourierRadon3d` operators diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 43d59cacf..6193c9002 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -3,6 +3,37 @@ Changelog ========= +Version 2.5.0 +------------- + +*Released on: 21/06/2025* + +* Added `cuda` engine to :py:class:`pylops.waveeqprocessing.Kirchhoff` + operator +* Added `Opbasis` and `optimal_coeff` to + :py:class:`pylops.optimization.cls_sparsity.OMP` +* Added `solver` to the input parameters of the `_oneshot` + internal methods of :py:class:`pylops.waveeqprocessing.AcousticWave2D` + to avoid recreating it for every shot +* Added `kwargs_fft` to `pylops.signalprocessing.FFT`, + :py:class:`pylops.signalprocessing.FFT2D`, and + :py:class:`pylops.signalprocessing.FFTND` +* Fix bug in :py:func:`pylops.waveeqprocessing.MDD` when using + CuPy arrays for `G` and `d` with `twosided=True` and `add_negative=True` +* Fix bug in :py:class:`pylops.signalprocessing.FourierRadon3D` + in the default choice of `num_threads_per_blocks` +* Fix bug in :py:class:`pylops.signalprocessing.Convolve1D` + in the definition of `pad` and `padd` when applying the + operator to a CuPy array +* Fix bug in :py:class:`pylops.optimization.cls_sparsity.OMP` avoiding + passing `explicit` in the creation of `_ColumnLinearOperator` +* Fix bug in :py:class:`pylops.optimization.cls_sparsity.OMP` callback + method as `cols` was not passed not allowing ``x`` to be + properly reconstructed +* Fix bug in :py:func:`pylops.waveeqprocessing.SeismicInterpolation` + in calculation of `sampling` when not passed + + Version 2.4.0 ------------- @@ -189,7 +220,7 @@ Version 1.18.0 * Extended :py:func:`pylops.Laplacian` to N-dimensional arrays * Added `forward` kind to :py:class:`pylops.SecondDerivative` and :py:func:`pylops.Laplacian` -* Added `chirp-sliding` kind to :py:class:`pylops.waveeqprocessing.seismicinterpolation.SeismicInterpolation` +* Added `chirp-sliding` kind to :py:func:`pylops.waveeqprocessing.SeismicInterpolation` * Fixed bug due to the new internal structure of `LinearOperator` submodule introduced in `scipy1.8.0` @@ -389,7 +420,7 @@ Version 1.9.1 :py:class:`pylops.signalprocessing.FFT2D` to ensure that the type of the input vector is retained when applying forward and adjoint. * Added ``dtype`` parameter to the ``FFT`` calls in the definition of the - :py:class:`pylops.waveeqprocessing.MDD` operation. This ensure that the type + :py:func:`pylops.waveeqprocessing.MDD` operation. This ensure that the type of the real part of ``G`` input is enforced to the output vectors of the forward and adjoint operations. diff --git a/docs/source/sg_execution_times.rst b/docs/source/sg_execution_times.rst new file mode 100644 index 000000000..9641132d2 --- /dev/null +++ b/docs/source/sg_execution_times.rst @@ -0,0 +1,271 @@ + +:orphan: + +.. _sphx_glr_sg_execution_times: + + +Computation times +================= +**00:00.703** total execution time for 79 files **from all galleries**: + +.. container:: + + .. raw:: html + + + + + + + + .. list-table:: + :header-rows: 1 + :class: table table-striped sg-datatable + + * - Example + - Time + - Mem (MB) + * - :ref:`sphx_glr_tutorials_jaxop.py` (``../../tutorials/jaxop.py``) + - 00:00.703 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_avo.py` (``../../examples/plot_avo.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_bayeslinearregr.py` (``../../examples/plot_bayeslinearregr.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_bilinear.py` (``../../examples/plot_bilinear.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_blending.py` (``../../examples/plot_blending.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_causalintegration.py` (``../../examples/plot_causalintegration.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_cgls.py` (``../../examples/plot_cgls.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_chirpradon.py` (``../../examples/plot_chirpradon.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_conj.py` (``../../examples/plot_conj.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_convolve.py` (``../../examples/plot_convolve.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_dct.py` (``../../examples/plot_dct.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_derivative.py` (``../../examples/plot_derivative.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_describe.py` (``../../examples/plot_describe.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_diagonal.py` (``../../examples/plot_diagonal.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_dtcwt.py` (``../../examples/plot_dtcwt.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_fft.py` (``../../examples/plot_fft.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_flip.py` (``../../examples/plot_flip.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_fourierradon.py` (``../../examples/plot_fourierradon.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_identity.py` (``../../examples/plot_identity.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_imag.py` (``../../examples/plot_imag.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_ista.py` (``../../examples/plot_ista.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_l1l1.py` (``../../examples/plot_l1l1.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_linearregr.py` (``../../examples/plot_linearregr.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_matrixmult.py` (``../../examples/plot_matrixmult.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_mdc.py` (``../../examples/plot_mdc.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_multiproc.py` (``../../examples/plot_multiproc.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_nmo.py` (``../../examples/plot_nmo.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_nonstatconvolve.py` (``../../examples/plot_nonstatconvolve.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_nonstatfilter.py` (``../../examples/plot_nonstatfilter.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_pad.py` (``../../examples/plot_pad.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_patching.py` (``../../examples/plot_patching.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_phaseshift.py` (``../../examples/plot_phaseshift.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_prestack.py` (``../../examples/plot_prestack.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_radon.py` (``../../examples/plot_radon.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_real.py` (``../../examples/plot_real.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_regr.py` (``../../examples/plot_regr.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_restriction.py` (``../../examples/plot_restriction.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_roll.py` (``../../examples/plot_roll.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_seislet.py` (``../../examples/plot_seislet.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_seismicevents.py` (``../../examples/plot_seismicevents.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_shift.py` (``../../examples/plot_shift.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_sliding.py` (``../../examples/plot_sliding.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_slopeest.py` (``../../examples/plot_slopeest.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_smoothing1d.py` (``../../examples/plot_smoothing1d.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_smoothing2d.py` (``../../examples/plot_smoothing2d.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_spread.py` (``../../examples/plot_spread.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_stacking.py` (``../../examples/plot_stacking.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_sum.py` (``../../examples/plot_sum.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_symmetrize.py` (``../../examples/plot_symmetrize.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_tapers.py` (``../../examples/plot_tapers.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_tndarray.py` (``../../examples/plot_tndarray.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_transpose.py` (``../../examples/plot_transpose.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_tvreg.py` (``../../examples/plot_tvreg.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_twoway.py` (``../../examples/plot_twoway.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_wavelet.py` (``../../examples/plot_wavelet.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_wavest.py` (``../../examples/plot_wavest.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_wavs.py` (``../../examples/plot_wavs.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_gallery_plot_zero.py` (``../../examples/plot_zero.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_tutorials_bayesian.py` (``../../tutorials/bayesian.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_tutorials_classsolvers.py` (``../../tutorials/classsolvers.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_tutorials_ctscan.py` (``../../tutorials/ctscan.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_tutorials_deblending.py` (``../../tutorials/deblending.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_tutorials_deblurring.py` (``../../tutorials/deblurring.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_tutorials_deghosting.py` (``../../tutorials/deghosting.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_tutorials_dottest.py` (``../../tutorials/dottest.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_tutorials_ilsm.py` (``../../tutorials/ilsm.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_tutorials_interpolation.py` (``../../tutorials/interpolation.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_tutorials_linearoperator.py` (``../../tutorials/linearoperator.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_tutorials_lsm.py` (``../../tutorials/lsm.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_tutorials_marchenko.py` (``../../tutorials/marchenko.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_tutorials_mdd.py` (``../../tutorials/mdd.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_tutorials_poststack.py` (``../../tutorials/poststack.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_tutorials_prestack.py` (``../../tutorials/prestack.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_tutorials_radonfiltering.py` (``../../tutorials/radonfiltering.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_tutorials_realcomplex.py` (``../../tutorials/realcomplex.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_tutorials_seismicinterpolation.py` (``../../tutorials/seismicinterpolation.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_tutorials_solvers.py` (``../../tutorials/solvers.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_tutorials_torchop.py` (``../../tutorials/torchop.py``) + - 00:00.000 + - 0.0 + * - :ref:`sphx_glr_tutorials_wavefielddecomposition.py` (``../../tutorials/wavefielddecomposition.py``) + - 00:00.000 + - 0.0