Skip to content

Commit 62f070d

Browse files
authored
Merge pull request #816 from mrava87/feat-eigs_cupy
Feat: added CuPy support to LinearOperator.eigs and LinearOperator.cond
2 parents 5879e7d + 098d466 commit 62f070d

9 files changed

Lines changed: 586 additions & 150 deletions

File tree

‎docs/source/gpu.rst‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ GPU with CuPy, and GPU with JAX):
226226
- GPU/TPU with JAX
227227
* - :meth:`pylops.LinearOperator.cond`
228228
- |:white_check_mark:|
229-
- |:red_circle:|
229+
- |:white_check_mark:|
230230
- |:red_circle:|
231231
* - :meth:`pylops.LinearOperator.conj`
232232
- |:white_check_mark:|
@@ -238,7 +238,7 @@ GPU with CuPy, and GPU with JAX):
238238
- |:white_check_mark:|
239239
* - :meth:`pylops.LinearOperator.eigs`
240240
- |:white_check_mark:|
241-
- |:red_circle:|
241+
- |:white_check_mark:|
242242
- |:red_circle:|
243243
* - :meth:`pylops.LinearOperator.todense`
244244
- |:white_check_mark:|
@@ -632,12 +632,19 @@ Medical:
632632

633633
.. warning::
634634

635-
1. The JAX backend of the :class:`pylops.signalprocessing.Convolve1D` operator
635+
1. The CuPy backend of :meth:`pylops.LinearOperator.eigs` does not support
636+
non-symmetric operators, since CuPy does not provide an equivalent
637+
of :func:`scipy.sparse.linalg.eigs`. A :obj:`NotImplementedError` is raised.
638+
639+
2. The CuPy backend of :meth:`pylops.LinearOperator.cond` can only be computed
640+
with the ``uselobpcg=True`` option (and therefore for symmetric operators).
641+
642+
3. The JAX backend of the :class:`pylops.signalprocessing.Convolve1D` operator
636643
currently works only with 1d-arrays due to a different behaviour of
637644
:meth:`scipy.signal.convolve` and :meth:`jax.scipy.signal.convolve` with
638645
nd-arrays.
639646

640-
2. The JAX backend of the :class:`pylops.avo.prestack.PrestackLinearModelling`
647+
4. The JAX backend of the :class:`pylops.avo.prestack.PrestackLinearModelling`
641648
operator currently works only with ``explicit=True`` due to the same issue as
642649
in point 1 for the :class:`pylops.signalprocessing.Convolve1D` operator employed
643650
when ``explicit=False``.

‎pylops/linearoperator.py‎

Lines changed: 208 additions & 104 deletions
Large diffs are not rendered by default.

‎pylops/optimization/eigs.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
__all__ = ["power_iteration"]
22

3+
from typing import TYPE_CHECKING
34

45
import numpy as np
56

6-
from pylops import LinearOperator
77
from pylops.utils.backend import get_module
88
from pylops.utils.typing import NDArray, Tbackend
99

10+
if TYPE_CHECKING:
11+
from pylops.linearoperator import LinearOperator
12+
1013

1114
def power_iteration(
12-
Op: LinearOperator,
15+
Op: "LinearOperator",
1316
niter: int = 10,
1417
tol: float = 1e-5,
1518
dtype: str = "float32",

‎pylops/signalprocessing/_baseffts.py‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,11 @@ def __init__(
4242
dims = _value_or_sized_to_array(dims)
4343
_raise_on_wrong_dtype(dims, np.integer, "dims")
4444

45-
self.ndim = len(dims)
45+
self.ndims = len(dims)
4646

4747
axes = _value_or_sized_to_array(axis)
4848
_raise_on_wrong_dtype(axes, np.integer, "axis")
49-
self.axis = get_normalize_axis_index()(axes[0], self.ndim)
49+
self.axis = get_normalize_axis_index()(axes[0], self.ndims)
5050

5151
if nfft is None:
5252
self.nfft = dims[self.axis]
@@ -63,7 +63,7 @@ def __init__(
6363
self.doifftpad = False
6464
if self.nfft < dims[self.axis]:
6565
self.doifftpad = True
66-
self.ifftpad = [(0, 0)] * self.ndim
66+
self.ifftpad = [(0, 0)] * self.ndims
6767
self.ifftpad[self.axis] = (0, dims[self.axis] - self.nfft)
6868
warnings.warn(
6969
f"nfft={self.nfft} has been selected to be smaller than the size of "
@@ -148,11 +148,11 @@ def __init__(
148148
dims = _value_or_sized_to_array(dims)
149149
_raise_on_wrong_dtype(dims, np.integer, "dims")
150150

151-
self.ndim = len(dims)
151+
self.ndims = len(dims)
152152

153153
axes = _value_or_sized_to_array(axes)
154154
_raise_on_wrong_dtype(axes, np.integer, "axes")
155-
self.axes = np.array([get_normalize_axis_index()(d, self.ndim) for d in axes])
155+
self.axes = np.array([get_normalize_axis_index()(d, self.ndims) for d in axes])
156156
self.naxes = len(self.axes)
157157
if self.naxes != len(np.unique(self.axes)):
158158
warnings.warn(
@@ -210,7 +210,7 @@ def __init__(
210210
]
211211
self.doifftpad = any(nfftshort)
212212
if self.doifftpad:
213-
self.ifftpad = [(0, 0)] * self.ndim
213+
self.ifftpad = [(0, 0)] * self.ndims
214214
for idir, (direction, nfshort) in enumerate(
215215
zip(self.axes, nfftshort, strict=True)
216216
):

‎pylops/signalprocessing/dtcwt.py‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,9 @@ def __init__(
109109
if dtcwt_message is not None:
110110
raise NotImplementedError(dtcwt_message)
111111

112-
dims = _value_or_sized_to_tuple(dims)
113-
self.ndim = len(dims)
114112
self.axis = axis
115113

114+
dims = _value_or_sized_to_tuple(dims)
116115
self.otherdims = int(np.prod(dims) / dims[self.axis])
117116
self.dims_swapped = list(dims)
118117
self.dims_swapped[0], self.dims_swapped[self.axis] = (

‎pylops/signalprocessing/fft.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ def __init__(
306306
self.dims_t = dims_t
307307

308308
# define padding(fftw requires the user to provide padded input signal)
309-
self.pad = np.zeros((self.ndim, 2), dtype=int)
309+
self.pad = np.zeros((self.ndims, 2), dtype=int)
310310
if self.real:
311311
if self.nfft % 2:
312312
self.pad[self.axis, 1] = (

‎pylops/signalprocessing/fft2d.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def __init__(
5656
)
5757

5858
# checks
59-
if self.ndim < 2:
59+
if self.ndims < 2:
6060
msg = "FFT2D requires at least two input dimensions"
6161
raise ValueError(msg)
6262
if self.naxes != 2:
@@ -174,7 +174,7 @@ def __init__(
174174
)
175175

176176
# checks
177-
if self.ndim < 2:
177+
if self.ndims < 2:
178178
msg = "FFT2D requires at least two input dimensions"
179179
raise ValueError(msg)
180180
if self.naxes != 2:
@@ -284,7 +284,7 @@ def __init__(
284284
)
285285

286286
# checks
287-
if self.ndim < 2:
287+
if self.ndims < 2:
288288
msg = "FFT2D requires at least two input dimensions"
289289
raise ValueError(msg)
290290
if self.naxes != 2:

‎pylops/utils/backend.py‎

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
"get_sparse_eye",
1919
"get_lstsq",
2020
"get_sp_fft",
21+
"get_eigvals",
22+
"get_eigsh",
23+
"get_lobpcg",
2124
"get_complex_dtype",
2225
"get_real_dtype",
2326
"to_cupy",
@@ -36,10 +39,11 @@
3639

3740
import numpy as np
3841
import scipy.fft as sp_fft
39-
from scipy.linalg import block_diag, lstsq, toeplitz
42+
from scipy.linalg import block_diag, eigvals, lstsq, toeplitz
4043
from scipy.ndimage import gaussian_filter
4144
from scipy.signal import convolve, correlate, fftconvolve, oaconvolve
4245
from scipy.sparse import csc_matrix, csr_matrix, dia_matrix, eye
46+
from scipy.sparse.linalg import eigsh, lobpcg
4347

4448
from pylops.utils import deps
4549
from pylops.utils.typing import ArrayLike, DTypeLike, NDArray, Tfftengine_ncj
@@ -48,6 +52,7 @@
4852
import cupy as cp
4953
import cupyx
5054
import cupyx.scipy.fft as cp_fft
55+
from cupy.linalg import eigvals as cp_eigvals
5156
from cupyx.scipy.linalg import block_diag as cp_block_diag
5257
from cupyx.scipy.linalg import toeplitz as cp_toeplitz
5358
from cupyx.scipy.ndimage import gaussian_filter as cp_gaussian_filter
@@ -59,6 +64,8 @@
5964
from cupyx.scipy.sparse import csr_matrix as cp_csr_matrix
6065
from cupyx.scipy.sparse import dia_matrix as cp_dia_matrix
6166
from cupyx.scipy.sparse import eye as cp_eye
67+
from cupyx.scipy.sparse.linalg import eigsh as cp_eigsh
68+
from cupyx.scipy.sparse.linalg import lobpcg as cp_lobpcg
6269

6370
if deps.jax_enabled:
6471
import jax
@@ -527,6 +534,75 @@ def get_sp_fft(x: ArrayLike) -> Callable:
527534
return cp_fft
528535

529536

537+
def get_eigvals(x: ArrayLike) -> Callable:
538+
"""Returns correct eigvals module based on input
539+
540+
Parameters
541+
----------
542+
x : :obj:`numpy.ndarray`
543+
Array
544+
545+
Returns
546+
-------
547+
f : :obj:`callable`
548+
Function to be used to process array
549+
550+
"""
551+
if not deps.cupy_enabled:
552+
return eigvals
553+
554+
if cp.get_array_module(x) == np:
555+
return eigvals
556+
else:
557+
return cp_eigvals
558+
559+
560+
def get_eigsh(x: ArrayLike) -> Callable:
561+
"""Returns correct eigsh module based on input
562+
563+
Parameters
564+
----------
565+
x : :obj:`numpy.ndarray`
566+
Array
567+
568+
Returns
569+
-------
570+
f : :obj:`callable`
571+
Function to be used to process array
572+
573+
"""
574+
if not deps.cupy_enabled:
575+
return eigsh
576+
577+
if cp.get_array_module(x) == np:
578+
return eigsh
579+
else:
580+
return cp_eigsh
581+
582+
583+
def get_lobpcg(x: ArrayLike) -> Callable:
584+
"""Returns correct lobpcg module based on input
585+
586+
Parameters
587+
----------
588+
x : :obj:`numpy.ndarray`
589+
Array
590+
591+
Returns
592+
-------
593+
f : :obj:`callable`
594+
Function to be used to process array
595+
596+
"""
597+
if not deps.cupy_enabled:
598+
return lobpcg
599+
600+
if cp.get_array_module(x) == np:
601+
return lobpcg
602+
else:
603+
return cp_lobpcg
604+
605+
530606
def get_complex_dtype(dtype: DTypeLike) -> DTypeLike:
531607
"""Returns a complex type in the precision of the input type.
532608

0 commit comments

Comments
 (0)