Source code for pylops.torchoperator

__all__ = [
    "TorchOperator",
]


from math import prod

import numpy as np

from pylops import LinearOperator
from pylops.utils import deps

if deps.torch_enabled:
    from pylops._torchoperator import _TorchOperator
else:
    torch_message = (
        "Torch package not installed. In order to be able to use"
        'the torchoperator module run "pip install torch" or'
        '"conda install -c pytorch torch".'
    )
from pylops.utils.typing import TensorTypeLike


[docs] class TorchOperator: """Wrap a PyLops operator into a Torch function. This class can be used to wrap a pylops operator into a torch function. Doing so, users can mix native torch functions (e.g. basic linear algebra operations, neural networks, etc.) and pylops operators. Since all operators in PyLops are linear operators, a Torch function is simply implemented by using the forward operator for its forward pass and the adjoint operator for its backward (gradient) pass. Parameters ---------- Op : :obj:`pylops.LinearOperator` PyLops operator batch : :obj:`bool`, optional Input has single sample (``False``) or batch of samples (``True``). If ``batch=False``, the input must be a 1-d Torch tensor of size ``Op.shape[1]`` for ``flatten=True`` or a tensor of size ``Op.dims`` for ``flatten=False``; if ``batch=True``, the input must be a 2-d Torch tensor of size ``[nbatch, Op.shape[1]]`` for ``flatten=True`` or a tensor of size ``[nbatch, *Op.dims]`` for ``flatten=False``, where ``nbatch`` is the size of the batch. The output follows the same convention with ``Op.shape[0]`` and ``Op.dimsd``. When using :meth:`adjoint`, the roles of ``Op.shape[1]``/``Op.dims`` and ``Op.shape[0]``/``Op.dimsd`` are swapped. flatten : :obj:`bool`, optional Input beyond the batch dimension is flattened (``True``) or not (``False``). See ``batch`` for more details. device : :obj:`str`, optional Device to be used when applying operator (``cpu`` or ``gpu``) devicetorch : :obj:`str`, optional Device to be assigned the output of the operator to (any Torch-compatible device) Attributes ---------- transpf : :obj:`numpy.ndarray` *Deprecated*, will be removed in v3.0.0. Indices to transpose batch to last dimension before applying ``Op`` transpb : :obj:`numpy.ndarray` *Deprecated*, will be removed in v3.0.0. Indices to transpose back batch to first dimension after applying ``Op`` """ def __init__( self, Op: LinearOperator, batch: bool = False, flatten: bool | None = True, device: str = "cpu", devicetorch: str = "cpu", ) -> None: if not deps.torch_enabled: raise NotImplementedError(torch_message) self.device = device self.devicetorch = devicetorch self.dtype = np.dtype(Op.dtype) self.dims, self.dimsd = Op.dims, Op.dimsd self.name = Op.name # define transpose indices to move the batch axis before and after # applying the forward operator (deprecated, kept only for backward # compatibility of the transpf and transpb attributes) self.transpf = np.roll(np.arange(2 if flatten else len(self.dims) + 1), -1) self.transpb = np.roll(np.arange(2 if flatten else len(self.dimsd) + 1), 1) if batch and not flatten: # batched and unflattened # flatten inputs of size ``[nbatch, *Op.dims]`` (or # ``[nbatch, *Op.dimsd]``), transpose, apply matmat (or rmatmat), # transpose back, and reshape outputs to ``[nbatch, *Op.dimsd]`` # (or ``[nbatch, *Op.dims]``) self.matvec = lambda x: Op.matmat(x.reshape(x.shape[0], -1).T).T.reshape( x.shape[0], *self.dimsd ) self.rmatvec = lambda x: Op.rmatmat(x.reshape(x.shape[0], -1).T).T.reshape( x.shape[0], *self.dims ) elif batch: # batched and flattened # transpose, apply matmat (or rmatmat), and transpose back self.matvec = lambda x: Op.matmat(x.T).T self.rmatvec = lambda x: Op.rmatmat(x.T).T elif not flatten: # unbatched and unflattened # flatten inputs of size ``Op.dims`` (or ``Op.dimsd``), apply # matvec (or rmatvec), and reshape outputs to ``Op.dimsd`` # (or ``Op.dims``) self.matvec = lambda x: Op.matvec(x.ravel()).reshape(self.dimsd) self.rmatvec = lambda x: Op.rmatvec(x.ravel()).reshape(self.dims) else: # unbatched and flattened # use directly matvec/rmatvec self.matvec = Op.matvec self.rmatvec = Op.rmatvec self.Top = _TorchOperator.apply def __call__(self, x): return self.apply(x) def __repr__(self): M, N = prod(self.dimsd), prod(self.dims) if self.dtype is None: dt = "unspecified dtype" else: dt = "dtype=" + str(self.dtype) return "<%dx%d %s with %s>" % (M, N, self.__class__.__name__, dt) def apply(self, x: TensorTypeLike) -> TensorTypeLike: """Apply forward pass to input vector Parameters ---------- x : :obj:`torch.Tensor` Input array Returns ------- y : :obj:`torch.Tensor` Output array resulting from the application of the operator to ``x``. """ return self.Top(x, self.matvec, self.rmatvec, self.device, self.devicetorch) # alias for forward pass forward = apply def adjoint(self, x: TensorTypeLike) -> TensorTypeLike: """Apply adjoint pass to input vector Parameters ---------- x : :obj:`torch.Tensor` Input array Returns ------- y : :obj:`torch.Tensor` Output array resulting from the application of the adjoint operator to ``x``. """ return self.Top(x, self.rmatvec, self.matvec, self.device, self.devicetorch)