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
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)
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
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
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