def _initialize_impl(impl):
        """Internal method to verify the validity of the `impl` kwarg."""
        impl_instance = None

        if impl is None:  # User didn't specify a backend
            if not RAY_TRAFO_IMPLS:
                raise RuntimeError(
                    'No `RayTransform` back-end available; this requires '
                    '3rd party packages, please check the install docs.')

            # Select fastest available
            impl_type = next(reversed(RAY_TRAFO_IMPLS.values()))

        else:
            # User did specify `impl`
            if is_string(impl):
                if impl.lower() not in RAY_TRAFO_IMPLS.keys():
                    raise ValueError(
                        'The {!r} `impl` is not found. This `impl` is either '
                        'not supported, it may be misspelled, or external '
                        'packages required are not available. Consult '
                        '`RAY_TRAFO_IMPLS` to find the run-time available '
                        'implementations.'.format(impl))

                impl_type = RAY_TRAFO_IMPLS[impl.lower()]
            elif isinstance(impl, type) or isinstance(impl, object):
                # User gave the type and leaves instantiation to us
                forward = getattr(impl, "call_forward", None)
                backward = getattr(impl, "call_backward", None)

                if not callable(forward) and not callable(backward):
                    raise TypeError('Type {!r} must have a `call_forward()` '
                                    'and/or `call_backward()`.'.format(impl))

                if isinstance(impl, type):
                    impl_type = impl
                else:
                    # User gave an object for `impl`, meaning to set the
                    # backend cache to an already initiated object
                    impl_type = type(impl)
                    impl_instance = impl
            else:
                raise TypeError(
                    '`impl` {!r} should be a string, or an object or type '
                    'having a `call_forward()` and/or `call_backward()`. '
                    ''.format(type(impl)))

        return impl_type, impl_instance
Beispiel #2
0
def _normalize_interp(interp, ndim):
    """Turn interpolation type into a tuple with one entry per axis."""
    interp_in = interp

    if is_string(interp):
        interp = str(interp).lower()
        interp_byaxis = (interp,) * ndim
    else:
        interp_byaxis = tuple(str(itp).lower() for itp in interp)
        if len(interp_byaxis) != ndim:
            raise ValueError(
                'length of `interp` ({}) does not match number of axes ({})'
                ''.format(len(interp_byaxis, ndim))
            )

    if not all(
        interp in SUPPORTED_INTERP for interp in interp_byaxis
    ):
        raise ValueError(
            'invalid `interp` {!r}; supported are: {}'
            ''.format(interp_in, SUPPORTED_INTERP)
        )

    return interp_byaxis
    def __init__(self, vol_space, geometry, **kwargs):
        """Initialize a new instance.

        Parameters
        ----------
        vol_space : `DiscretizedSpace`
            Discretized reconstruction space, the domain of the forward
            operator or the range of the adjoint (back-projection).
        geometry : `Geometry`
            Geometry of the transform that contains information about
            the data structure.

        Other Parameters
        ----------------
        impl : {`None`, 'astra_cuda', 'astra_cpu', 'skimage'}, optional
            Implementation back-end for the transform. Supported back-ends:

            - ``'astra_cuda'``: ASTRA toolbox, using CUDA, 2D or 3D
            - ``'astra_cpu'``: ASTRA toolbox using CPU, only 2D
            - ``'skimage'``: scikit-image, only 2D parallel with square
              reconstruction space.

            For the default ``None``, the fastest available back-end is
            used.

        proj_space : `DiscretizedSpace`, optional
            Discretized projection (sinogram) space, the range of the forward
            operator or the domain of the adjoint (back-projection).
            Default: Inferred from parameters.
        use_cache : bool, optional
            If ``True``, data is cached. This gives a significant speed-up
            at the expense of a notable memory overhead, both on the GPU
            and on the CPU, since a full volume and a projection dataset
            are stored. That may be prohibitive in 3D.
            Default: True
        kwargs
            Further keyword arguments passed to the projector backend.

        Notes
        -----
        The ASTRA backend is faster if data are given with
        ``dtype='float32'`` and storage order 'C'. Otherwise copies will be
        needed.
        """
        if not isinstance(vol_space, DiscretizedSpace):
            raise TypeError(
                '`vol_space` must be a `DiscretizedSpace` instance, got '
                '{!r}'.format(vol_space))

        if not isinstance(geometry, Geometry):
            raise TypeError(
                '`geometry` must be a `Geometry` instance, got {!r}'
                ''.format(geometry))

        # Generate or check projection space
        proj_space = kwargs.pop('proj_space', None)
        if proj_space is None:
            dtype = vol_space.dtype

            if not vol_space.is_weighted:
                weighting = None
            elif (isinstance(vol_space.weighting, ConstWeighting)
                  and np.isclose(vol_space.weighting.const,
                                 vol_space.cell_volume)):
                # Approximate cell volume
                # TODO: find a way to treat angles and detector differently
                # regarding weighting. While the detector should be uniformly
                # discretized, the angles do not have to and often are not.
                # The needed partition property is available since
                # commit a551190d, but weighting is not adapted yet.
                # See also issue #286
                extent = float(geometry.partition.extent.prod())
                size = float(geometry.partition.size)
                weighting = extent / size
            else:
                raise NotImplementedError('unknown weighting of domain')

            proj_tspace = vol_space.tspace_type(
                geometry.partition.shape,
                weighting=weighting,
                dtype=dtype,
            )

            if geometry.motion_partition.ndim == 0:
                angle_labels = []
            elif geometry.motion_partition.ndim == 1:
                angle_labels = ['$\\varphi$']
            elif geometry.motion_partition.ndim == 2:
                # TODO: check order
                angle_labels = ['$\\vartheta$', '$\\varphi$']
            elif geometry.motion_partition.ndim == 3:
                # TODO: check order
                angle_labels = ['$\\vartheta$', '$\\varphi$', '$\\psi$']
            else:
                angle_labels = None

            if geometry.det_partition.ndim == 1:
                det_labels = ['$s$']
            elif geometry.det_partition.ndim == 2:
                det_labels = ['$u$', '$v$']
            else:
                det_labels = None

            if angle_labels is None or det_labels is None:
                # Fallback for unknown configuration
                axis_labels = None
            else:
                axis_labels = angle_labels + det_labels

            proj_space = DiscretizedSpace(geometry.partition,
                                          proj_tspace,
                                          axis_labels=axis_labels)

        else:
            # proj_space was given, checking some stuff
            if not isinstance(proj_space, DiscretizedSpace):
                raise TypeError(
                    '`proj_space` must be a `DiscretizedSpace` instance, '
                    'got {!r}'.format(proj_space))
            if proj_space.shape != geometry.partition.shape:
                raise ValueError(
                    '`proj_space.shape` not equal to `geometry.shape`: '
                    '{} != {}'
                    ''.format(proj_space.shape, geometry.partition.shape))
            if proj_space.dtype != vol_space.dtype:
                raise ValueError(
                    '`proj_space.dtype` not equal to `vol_space.dtype`: '
                    '{} != {}'.format(proj_space.dtype, vol_space.dtype))

        if vol_space.ndim != geometry.ndim:
            raise ValueError('`vol_space.ndim` not equal to `geometry.ndim`: '
                             '{} != {}'.format(vol_space.ndim, geometry.ndim))

        # Cache for input/output arrays of transforms
        self.use_cache = kwargs.pop('use_cache', True)

        # Check `impl`
        impl = kwargs.pop('impl', None)
        impl_type, self.__cached_impl = self._initialize_impl(impl)
        self._impl_type = impl_type
        if is_string(impl):
            self.__impl = impl.lower()
        else:
            self.__impl = impl_type.__name__

        self._geometry = geometry

        # Reserve name for cached properties (used for efficiency reasons)
        self._adjoint = None

        # Extra kwargs that can be reused for adjoint etc. These must
        # be retrieved with `get` instead of `pop` above.
        self._extra_kwargs = kwargs

        # Finally, initialize the Operator structure
        super(RayTransform, self).__init__(domain=vol_space,
                                           range=proj_space,
                                           linear=True)
Beispiel #4
0
    def __init__(self, fspace, partition, dspace, schemes, **kwargs):
        """Initialize a new instance.

        Parameters
        ----------
        fspace : `FunctionSpace`
            The undiscretized (abstract) space of functions to be
            discretized. Its field must be the same as that of data
            space. The function domain must provide a
            `Set.contains_set` method.
        partition : `RectPartition`
            Partition of (a subset of) ``fspace.domain`` based on a
            `RectGrid`
        dspace : `FnBase`
            Data space providing containers for the values of a
            discretized object. Its `NtuplesBase.size` must be equal
            to the total number of grid points, and its `FnBase.field`
            must be the same as that of the function space.
        schemes : string or sequence of strings
            Indicates which interpolation scheme to use for which axis.
            A single string is interpreted as a global scheme for all
            axes.
        nn_variants : string or sequence of strings, optional
            Which variant ('left' or 'right') to use in nearest neighbor
            interpolation for which axis. A single string is interpreted
            as a global variant for all axes.
            This option has no effect for schemes other than nearest
            neighbor.
            Default: 'left'
        order : {'C', 'F'}, optional
            Ordering of the axes in the data storage. 'C' means the
            first axis varies slowest, the last axis fastest;
            vice versa for 'F'.
            Default: 'C'
        """
        if not isinstance(fspace, FunctionSpace):
            raise TypeError('`fspace` {!r} is not a `FunctionSpace` '
                            'instance'.format(fspace))

        super(PerAxisInterpolation, self).__init__('interpolation',
                                                   fspace,
                                                   partition,
                                                   dspace,
                                                   linear=True,
                                                   **kwargs)

        schemes_in = schemes
        if is_string(schemes):
            scheme = str(schemes).lower()
            if scheme not in _SUPPORTED_INTERP_SCHEMES:
                raise ValueError('`schemes` {!r} not understood'
                                 ''.format(schemes_in))
            schemes = [scheme] * self.grid.ndim
        else:
            schemes = [
                str(scm).lower() if scm is not None else None
                for scm in schemes
            ]

        nn_variants = kwargs.pop('nn_variants', None)
        nn_variants_in = nn_variants
        if nn_variants is None:
            nn_variants = [
                'left' if scm == 'nearest' else None for scm in schemes
            ]
        else:
            if is_string(nn_variants):
                # Make list with `nn_variants` where `schemes == 'nearest'`,
                # else `None` (variants only applies to axes with nn
                # interpolation)
                nn_variants = [
                    nn_variants if scm == 'nearest' else None
                    for scm in schemes
                ]
                if str(nn_variants_in).lower() not in ('left', 'right'):
                    raise ValueError('`nn_variants` {!r} not understood'
                                     ''.format(nn_variants_in))
            else:
                nn_variants = [
                    str(var).lower() if var is not None else None
                    for var in nn_variants
                ]

        for i in range(self.grid.ndim):
            # Reaching a raise condition here only happens for invalid
            # sequences of inputs, single-input case has been checked above
            if schemes[i] not in _SUPPORTED_INTERP_SCHEMES:
                raise ValueError('`interp[{}]={!r}` not understood'
                                 ''.format(schemes_in[i], i))
            if (schemes[i] == 'nearest'
                    and nn_variants[i] not in ('left', 'right')):
                raise ValueError('`nn_variants[{}]={!r}` not understood'
                                 ''.format(nn_variants_in[i], i))
            elif schemes[i] != 'nearest' and nn_variants[i] is not None:
                raise ValueError('in axis {}: `nn_variants` cannot be used '
                                 'with `interp={!r}'
                                 ''.format(i, schemes_in[i]))

        self.__schemes = schemes
        self.__nn_variants = nn_variants
Beispiel #5
0
def dft_postprocess_data(arr,
                         real_grid,
                         recip_grid,
                         shift,
                         axes,
                         interp,
                         sign='-',
                         op='multiply',
                         out=None):
    """Post-process the Fourier-space data after DFT.

    This function multiplies the given data with the separable
    function::

        q(xi) = exp(+- 1j * dot(x[0], xi)) * s * phi_hat(xi_bar)

    where ``x[0]`` and ``s`` are the minimum point and the stride of
    the real-space grid, respectively, and ``phi_hat(xi_bar)`` is the FT
    of the interpolation kernel. The sign of the exponent depends on the
    choice of ``sign``. Note that for ``op='divide'`` the
    multiplication with ``s * phi_hat(xi_bar)`` is replaced by a
    division with the same array.

    In discretized form on the reciprocal grid, the exponential part
    of this function becomes an array::

        q[k] = exp(+- 1j * dot(x[0], xi[k]))

    and the arguments ``xi_bar`` to the interpolation kernel
    are the normalized frequencies::

        for 'shift=True'  : xi_bar[k] = -pi + pi * (2*k) / N
        for 'shift=False' : xi_bar[k] = -pi + pi * (2*k+1) / N

    See [Pre+2007], Section 13.9 "Computing Fourier Integrals Using
    the FFT" for a similar approach.

    Parameters
    ----------
    arr : `array-like`
        Array to be pre-processed. An array with real data type is
        converted to its complex counterpart.
    real_grid : uniform `RectGrid`
        Real space grid in the transform.
    recip_grid : uniform `RectGrid`
        Reciprocal grid in the transform
    shift : bool or sequence of bools
        If ``True``, the grid is shifted by half a stride in the negative
        direction in the corresponding axes. The sequence must have the
        same length as ``axes``.
    axes : int or sequence of ints
        Dimensions along which to take the transform. The sequence must
        have the same length as ``shifts``.
    interp : string or sequence of strings
        Interpolation scheme used in the real-space.
    sign : {'-', '+'}, optional
        Sign of the complex exponent.
    op : {'multiply', 'divide'}, optional
        Operation to perform with the stride times the interpolation
        kernel FT
    out : `numpy.ndarray`, optional
        Array in which the result is stored. If ``out is arr``, an
        in-place modification is performed.

    Returns
    -------
    out : `numpy.ndarray`
        Result of the post-processing. If ``out`` was given, the returned
        object is a reference to it.

    References
    ----------
    [Pre+2007] Press, W H, Teukolsky, S A, Vetterling, W T, and Flannery, B P.
    *Numerical Recipes in C - The Art of Scientific Computing* (Volume 3).
    Cambridge University Press, 2007.
    """
    arr = np.asarray(arr)
    if is_real_floating_dtype(arr.dtype):
        arr = arr.astype(complex_dtype(arr.dtype))
    elif not is_complex_floating_dtype(arr.dtype):
        raise ValueError('array data type {} is not a complex floating point '
                         'data type'.format(dtype_repr(arr.dtype)))

    if out is None:
        out = arr.copy()
    elif out is not arr:
        out[:] = arr

    if axes is None:
        axes = list(range(arr.ndim))
    else:
        try:
            axes = [int(axes)]
        except TypeError:
            axes = list(axes)

    shift_list = normalized_scalar_param_list(shift,
                                              length=len(axes),
                                              param_conv=bool)

    if sign == '-':
        imag = -1j
    elif sign == '+':
        imag = 1j
    else:
        raise ValueError("`sign` '{}' not understood".format(sign))

    op, op_in = str(op).lower(), op
    if op not in ('multiply', 'divide'):
        raise ValueError("kernel `op` '{}' not understood".format(op_in))

    # Make a list from interp if that's not the case already
    if is_string(interp):
        interp = [str(interp).lower()] * arr.ndim

    onedim_arrs = []
    for ax, shift, intp in zip(axes, shift_list, interp):
        x = real_grid.min_pt[ax]
        xi = recip_grid.coord_vectors[ax]

        # First part: exponential array
        onedim_arr = np.exp(imag * x * xi)

        # Second part: interpolation kernel
        len_dft = recip_grid.shape[ax]
        len_orig = real_grid.shape[ax]
        halfcomplex = (len_dft < len_orig)
        odd = len_orig % 2

        fmin = -0.5 if shift else -0.5 + 1.0 / (2 * len_orig)
        if halfcomplex:
            # maximum lies around 0, possibly half a cell left or right of it
            if shift and odd:
                fmax = -1.0 / (2 * len_orig)
            elif not shift and not odd:
                fmax = 1.0 / (2 * len_orig)
            else:
                fmax = 0.0

        else:  # not halfcomplex
            # maximum lies close to 0.5, half or full cell left of it
            if shift:
                # -0.5 + (N-1)/N = 0.5 - 1/N
                fmax = 0.5 - 1.0 / len_orig
            else:
                # -0.5 + 1/(2*N) + (N-1)/N = 0.5 - 1/(2*N)
                fmax = 0.5 - 1.0 / (2 * len_orig)

        freqs = np.linspace(fmin, fmax, num=len_dft)
        stride = real_grid.stride[ax]

        interp_kernel = _interp_kernel_ft(freqs, intp)
        interp_kernel *= stride

        if op == 'multiply':
            onedim_arr *= interp_kernel
        else:
            onedim_arr /= interp_kernel

        onedim_arrs.append(onedim_arr.astype(out.dtype, copy=False))

    fast_1d_tensor_mult(out, onedim_arrs, axes=axes, out=out)
    return out
Beispiel #6
0
    def __init__(self, fspace, partition, tspace, schemes, nn_variants='left'):
        """Initialize a new instance.

        Parameters
        ----------
        fspace : `FunctionSpace`
            Non-discretized (abstract) space of functions to be
            discretized. ``fspace.domain`` must provide a
            `Set.contains_set` method.
        partition : `RectPartition`
            Partition of (a subset of) ``fspace.domain`` based on a
            `RectGrid`
        tspace : `TensorSpace`
            Space providing containers for the values/coefficients of a
            discretized object. Its `TensorSpace.shape` must be equal
            to ``partition.shape``, and its `TensorSpace.field` must
            match ``fspace.field``.
        schemes : string or sequence of strings
            Indicates which interpolation scheme to use for which axis.
            A single string is interpreted as a global scheme for all
            axes.
        nn_variants : string or sequence of strings, optional
            Which variant ('left' or 'right') to use in nearest neighbor
            interpolation for which axis. A single string is interpreted
            as a global variant for all axes.
            This option has no effect for schemes other than nearest
            neighbor.
        """
        if getattr(fspace, 'field', None) is None:
            raise TypeError('`fspace.field` cannot be `None`')

        super(PerAxisInterpolation, self).__init__('interpolation',
                                                   fspace,
                                                   partition,
                                                   tspace,
                                                   linear=True)

        schemes_in = schemes
        if is_string(schemes):
            scheme = str(schemes).lower()
            if scheme not in _SUPPORTED_INTERP_SCHEMES:
                raise ValueError('`schemes` {!r} not understood'
                                 ''.format(schemes_in))
            schemes = [scheme] * self.grid.ndim
        else:
            schemes = [
                str(scm).lower() if scm is not None else None
                for scm in schemes
            ]

        nn_variants_in = nn_variants
        if nn_variants is None:
            nn_variants = [
                'left' if scm == 'nearest' else None for scm in schemes
            ]
        else:
            if is_string(nn_variants):
                # Make list with `nn_variants` where `schemes == 'nearest'`,
                # else `None` (variants only applies to axes with nn
                # interpolation)
                nn_variants = [
                    nn_variants if scm == 'nearest' else None
                    for scm in schemes
                ]
                if str(nn_variants_in).lower() not in ('left', 'right'):
                    raise ValueError('`nn_variants` {!r} not understood'
                                     ''.format(nn_variants_in))
            else:
                nn_variants = [
                    str(var).lower() if var is not None else None
                    for var in nn_variants
                ]

        for i in range(self.grid.ndim):
            # Reaching a raise condition here only happens for invalid
            # sequences of inputs, single-input case has been checked above
            if schemes[i] not in _SUPPORTED_INTERP_SCHEMES:
                raise ValueError('`interp[{}]={!r}` not understood'
                                 ''.format(schemes_in[i], i))
            if (schemes[i] == 'nearest'
                    and nn_variants[i] not in ('left', 'right')):
                raise ValueError('`nn_variants[{}]={!r}` not understood'
                                 ''.format(nn_variants_in[i], i))
            elif schemes[i] != 'nearest' and nn_variants[i] is not None:
                raise ValueError('in axis {}: `nn_variants` cannot be used '
                                 'with `interp={!r}'
                                 ''.format(i, schemes_in[i]))

        self.__schemes = schemes
        self.__nn_variants = nn_variants