Source code for pylops.basicoperators.restriction

__all__ = ["Restriction"]

import logging
from collections.abc import Sequence

import numpy as np
import numpy.ma as np_ma

from pylops import LinearOperator
from pylops.utils._internal import _value_or_sized_to_tuple
from pylops.utils.backend import (
    get_array_module,
    get_module_name,
    get_normalize_axis_index,
    inplace_add,
)
from pylops.utils.typing import DTypeLike, InputDimsLike, IntNDArray, NDArray

logger = logging.getLogger(__name__)


[docs] class Restriction(LinearOperator): r"""Restriction (or sampling) operator. Extract subset of values from input vector at locations ``iava`` in forward mode and place those values at locations ``iava`` in an otherwise zero vector in adjoint mode. Parameters ---------- dims : :obj:`list` or :obj:`int` Number of samples for each dimension iava : :obj:`list` or :obj:`numpy.ndarray` Integer indices of available samples for data selection. axis : :obj:`int`, optional .. versionadded:: 2.0.0 Axis along which restriction is applied to model. inplace : :obj:`bool`, optional Work inplace (``True``) or make a new copy (``False``). By default, data is a reference to the model (in forward) and model is a reference to the data (in adjoint). forceflat : :obj:`bool`, optional .. versionadded:: 2.2.0 Force an array to be flattened after rmatvec. Note that this is only required when `len(dims)=2`, otherwise pylops will detect whether to return a 1d or nd array. dtype : :obj:`str`, optional Type of elements in input array. name : :obj:`str`, optional .. versionadded:: 2.0.0 Name of operator (to be used by :func:`pylops.utils.describe.describe`) Attributes ---------- dims : :obj:`tuple` Shape of the array after the adjoint, but before flattening. For example, ``x_reshaped = (Op.H * y.ravel()).reshape(Op.dims)``. dimsd : :obj:`tuple` Shape of the array after the forward, but before flattening. For example, ``y_reshaped = (Op * x.ravel()).reshape(Op.dimsd)``. repeated : :obj:`bool` Indicates whether there are repeated indices in ``iava``. shape : :obj:`tuple` Operator shape. See Also -------- pylops.signalprocessing.Interp : Interpolation operator Notes ----- Extraction (or *sampling*) of a subset of :math:`N` values at locations ``iava`` from an input (or model) vector :math:`\mathbf{x}` of size :math:`M` can be expressed as: .. math:: y_i = x_{l_i} \quad \forall i=0,1,\ldots,N-1 where :math:`\mathbf{l}=[l_0, l_1,\ldots, l_{N-1}]` is a vector containing the indices of the original array at which samples are taken. Conversely, in adjoint mode the available values in the data vector :math:`\mathbf{y}` are placed at locations :math:`\mathbf{l}=[l_0, l_1,\ldots, l_{M-1}]` in the model vector: .. math:: x_{l_i} = y_i \quad \forall i=0,1,\ldots,N-1 and :math:`x_{j}=0` for :math:`j \neq l_i` (i.e., at all other locations in input vector). """ def __init__( self, dims: int | InputDimsLike, iava: IntNDArray | Sequence[int], axis: int = -1, inplace: bool = True, forceflat: bool | None = None, dtype: DTypeLike = "float64", name: str = "R", ) -> None: ncp = get_array_module(iava) dims = _value_or_sized_to_tuple(dims) axis = get_normalize_axis_index()(axis, len(dims)) dimsd = list(dims) # data dimensions dimsd[axis] = len(iava) # check if forceflat is needed and set it back to None otherwise if len(dims) > 2: if forceflat is not None: logger.warning( f"Setting forceflat=None since len(dims)={len(dims)}>2. " f"PyLops will automatically detect whether to return " f"a 1d or nd array based on the shape of the input" f"array." ) forceflat = None super().__init__( dtype=np.dtype(dtype), dims=dims, dimsd=dimsd, forceflat=forceflat, name=name, ) self.inplace = inplace self.axis = axis self.iava = ncp.asarray(iava) # check whether any index in iava is repeated self.repeated = np.unique(self.iava).size != self.iava.size def _matvec(self, x: NDArray) -> NDArray: ncp = get_array_module(x) if not self.inplace: x = x.copy() x = ncp.reshape(x, self.dims) y = ncp.take(x, self.iava, axis=self.axis) y = y.ravel() return y def _rmatvec(self, x: NDArray) -> NDArray: ncp = get_array_module(x) if not self.inplace: x = x.copy() x = ncp.reshape(x, self.dimsd) indices = tuple( self.iava if ax == self.axis else slice(None) for ax in range(x.ndim) ) if get_module_name(ncp) == "cupy" and self.dtype.kind == "c": # work on real/imag separately for cupy arrays as cp.add.at does # not support complex dtype rdtype = ncp.real(ncp.ones(1, self.dtype)).dtype y_real = ncp.zeros(self.dims, dtype=rdtype) y_imag = ncp.zeros(self.dims, dtype=rdtype) ncp.add.at( y_real, indices, x.real, ) ncp.add.at( y_imag, indices, x.imag, ) y = y_real + 1j * y_imag else: y = ncp.zeros(self.dims, dtype=self.dtype) y = inplace_add(x, y, indices, accumulate=self.repeated) y = y.ravel() return y def mask(self, x: NDArray) -> NDArray: """Apply mask to input signal returning a signal of same size with values at ``iava`` locations and ``0`` at other locations If any location is repeated in ``iava``, an error is raised. Parameters ---------- x : :obj:`numpy.ndarray` or :obj:`cupy.ndarray` Input array (can be either flattened or not) Returns ------- y : :obj:`numpy.ma.core.MaskedArray` Masked array. Raises ------ ValueError If any index is repeated in iava """ ncp = get_array_module(x) if ncp != np: iava = ncp.asnumpy(self.iava) else: iava = self.iava.copy() if np.unique(iava).size != iava.size: msg = "At least one index in iava is repeated, mask is not enabled..." raise ValueError(msg) y = np_ma.array(np.zeros(self.dims), mask=np.ones(self.dims), dtype=self.dtype) x = np.reshape(x, self.dims) x = np.swapaxes(x, self.axis, -1) y = np.swapaxes(y, self.axis, -1) y.mask[..., iava] = False if ncp == np: y[..., iava] = x[..., self.iava] else: y[..., iava] = ncp.asnumpy(x)[..., iava] y = np.swapaxes(y, -1, self.axis) return y