def _call(self, f, out=None): """Evaluate the gradient in the point ``f``.""" if functional.impl != 'numpy': raise NotImplementedError( 'gradient not implemented for `impl` {!r}' ''.format(functional.impl)) if isinstance(self.domain, ProductSpace): tmp = self.domain[0].element() pwnorm = PointwiseNorm(self.domain) pwnorm(f, out=tmp) else: tmp = out f.ufunc.absolute(out=tmp) tmp.ufunc.power(self.exponent - 2, out=tmp) with writable_array(tmp) as tmp_arr: tmp_arr[np.isnan(tmp_arr)] = 0 # Handle NaN if out is None: out = f.copy() else: out.assign(f) out *= self.exponent return out
def _call(self, x, out): """Implement ``self(x, out)``.""" with writable_array(out) as out_arr: resize_array(x.asarray(), op.domain.shape, offset=op.offset, pad_mode=op.pad_mode, pad_const=0, direction='adjoint', out=out_arr)
def _call(self, func, out=None, **kwargs): """Return ``self(func[, out, **kwargs])``.""" mesh = self.grid.meshgrid if out is None: out = func(mesh, **kwargs) else: with writable_array(out) as out_arr: func(mesh, out=out_arr, **kwargs) return out
def _call(self, x, out): """Implement ``self(x, out)``.""" with writable_array(out) as out_arr: resize_array(x.asarray(), self.range.shape, offset=self.offset, pad_mode=self.pad_mode, pad_const=self.pad_const, direction='forward', out=out_arr)
def _call(self, x, out=None): """Raw apply method on input, writing to given output.""" if out is None: return self.range.element(self.matrix.dot(x)) else: if self.matrix_issparse: # Unfortunately, there is no native in-place dot product for # sparse matrices out[:] = self.matrix.dot(x) else: with writable_array(out) as out_arr: self.matrix.dot(x, out=out_arr)
def _call(self, x, out=None): """Return ``self(x[, out])``.""" if out is None: return self.range.element(self.matrix.dot(x)) else: if self.matrix_issparse: # Unfortunately, there is no native in-place dot product for # sparse matrices out[:] = self.matrix.dot(x) else: with writable_array(out) as out_arr: self.matrix.dot(x, out=out_arr)
def _call(self, x, out): """Mask ``x`` and store the result in ``out`` if given.""" # Find the indices of the mask min and max. The floating point # versions are also required for the linear transition. idx_min_flt = np.array(self.domain.partition.index(self.min_pt, floating=True), ndmin=1) idx_max_flt = np.array(self.domain.partition.index(self.max_pt, floating=True), ndmin=1) # To deal with coinciding boundaries we introduce an epsilon tolerance epsilon = 1e-6 idx_min = np.floor(idx_min_flt - epsilon).astype(int) idx_max = np.ceil(idx_max_flt + epsilon).astype(int) def coeffs(d): return (1.0 - (idx_min_flt[d] - idx_min[d]), 1.0 - (idx_max[d] - idx_max_flt[d])) # Need an extra level of indirection in order to capture `d` inside # the lambda expressions def fn_pair(d): return (lambda x: x * coeffs(d)[0], lambda x: x * coeffs(d)[1]) boundary_scale_fns = [fn_pair(d) for d in range(x.ndim)] slc = tuple(slice(imin, imax) for imin, imax in zip(idx_min, idx_max)) slc_inner = tuple( slice(imin + 1, imax - 1) for imin, imax in zip(idx_min, idx_max)) # Make a mask that is 1 outside the masking region, 0 inside # and has a linear transition where the region boundary does not # coincide with a cell boundary mask = np.ones_like(x) mask[slc_inner] = 0 apply_on_boundary(mask[slc], boundary_scale_fns, only_once=False, out=mask[slc]) apply_on_boundary(mask[slc], lambda x: 1.0 - x, only_once=True, out=mask[slc]) # out = masked version of x out.assign(x) with writable_array(out) as out_arr: out_arr[slc] = mask[slc] * out_arr[slc]
def skimage_radon_forward_projector(volume, geometry, proj_space, out=None): """Calculate forward projection using skimage. Parameters ---------- volume : `DiscreteLpElement` The volume to project. geometry : `Geometry` The projection geometry to use. proj_space : `DiscreteLp` Space in which the projections (sinograms) live. out : ``proj_space`` element, optional Element to which the result should be written. Returns ------- sinogram : ``proj_space`` element Result of the forward projection. If ``out`` was given, the returned object is a reference to it. """ # Lazy import due to significant import time from skimage.transform import radon # Check basic requirements. Fully checking should be in wrapper assert volume.shape[0] == volume.shape[1] theta = np.degrees(geometry.angles) skimage_range = skimage_proj_space(geometry, volume.space, proj_space) # Rotate volume from (x, y) to (rows, cols), then project sino_arr = radon( np.rot90(volume.asarray(), 1), theta=theta, circle=False ) sinogram = skimage_range.element(sino_arr.T) if out is None: out = proj_space.element() with writable_array(out) as out_arr: point_collocation( clamped_interpolation(skimage_range, sinogram), proj_space.grid.meshgrid, out=out_arr, ) scale = volume.space.cell_sides[0] out *= scale return out
def _call(self, x, out=None): """Apply resampling operator. The element ``x`` is resampled using the sampling and interpolation operators of the underlying spaces. """ interpolator = per_axis_interpolator( x, self.domain.grid.coord_vectors, self.interp ) out_ctx = nullcontext() if out is None else writable_array(out) with out_ctx as out_arr: return point_collocation( interpolator, self.range.meshgrid, out=out_arr )
def _call(self, x, out=None): """Calculate partial derivative of ``x``.""" if out is None: out = self.range.element() # TODO: this pipes CUDA arrays through NumPy. Write native operator. with writable_array(out) as out_arr: finite_diff(x.asarray(), axis=self.axis, dx=self.dx, method=self.method, pad_mode=self.pad_mode, pad_const=self.pad_const, out=out_arr) return out
def _call(self, x, out=None): """Calculate the spatial gradient of ``x``.""" if out is None: out = self.range.element() x_arr = x.asarray() ndim = self.domain.ndim dx = self.domain.cell_sides for axis in range(ndim): with writable_array(out[axis]) as out_arr: finite_diff(x_arr, axis=axis, dx=dx[axis], method=self.method, pad_mode=self.pad_mode, pad_const=self.pad_const, out=out_arr) return out
def _call(self, x, out=None): """Calculate the spatial Laplacian of ``x``.""" if out is None: out = self.range.zero() else: out.set_zero() x_arr = x.asarray() out_arr = out.asarray() tmp = np.empty(out.shape, out.dtype, order=out.space.default_order) ndim = self.domain.ndim dx = self.domain.cell_sides with writable_array(out) as out_arr: for axis in range(ndim): # TODO: this can be optimized finite_diff(x_arr, axis=axis, dx=dx[axis]**2, method='forward', pad_mode=self.pad_mode, pad_const=self.pad_const, out=tmp) out_arr += tmp finite_diff(x_arr, axis=axis, dx=dx[axis]**2, method='backward', pad_mode=self.pad_mode, pad_const=self.pad_const, out=tmp) out_arr -= tmp return out
def _call(self, x, out=None): """Calculate the divergence of ``x``.""" if out is None: out = self.range.element() ndim = self.range.ndim dx = self.range.cell_sides tmp = np.empty(out.shape, out.dtype, order=out.space.default_order) with writable_array(out) as out_arr: for axis in range(ndim): finite_diff(x[axis], axis=axis, dx=dx[axis], method=self.method, pad_mode=self.pad_mode, pad_const=self.pad_const, out=tmp) if axis == 0: out_arr[:] = tmp else: out_arr += tmp return out
def astra_cpu_forward_projector(vol_data, geometry, proj_space, out=None): """Run an ASTRA forward projection on the given data using the CPU. Parameters ---------- vol_data : `DiscreteLpElement` Volume data to which the forward projector is applied geometry : `Geometry` Geometry defining the tomographic setup proj_space : `DiscreteLp` Space to which the calling operator maps out : ``proj_space`` element, optional Element of the projection space to which the result is written. If ``None``, an element in ``proj_space`` is created. Returns ------- out : ``proj_space`` element Projection data resulting from the application of the projector. If ``out`` was provided, the returned object is a reference to it. """ if not isinstance(vol_data, DiscreteLpElement): raise TypeError('volume data {!r} is not a `DiscreteLpElement` ' 'instance.'.format(vol_data)) if vol_data.space.impl != 'numpy': raise TypeError('dspace {!r} of the volume is not an ' 'instance of `NumpyNtuples`' ''.format(vol_data.space.dspace)) if not isinstance(geometry, Geometry): raise TypeError('geometry {!r} is not a Geometry instance' ''.format(geometry)) if not isinstance(proj_space, DiscreteLp): raise TypeError('projection space {!r} is not a DiscreteLp ' 'instance.'.format(proj_space)) if proj_space.impl != 'numpy': raise TypeError('data type {!r} of the reconstruction space is not an ' 'instance of NumpyNtuples'.format(proj_space.dspace)) if vol_data.ndim != geometry.ndim: raise ValueError('dimensions {} of volume data and {} of geometry ' 'do not match' ''.format(vol_data.ndim, geometry.ndim)) if out is None: out = proj_space.element() else: if out not in proj_space: raise TypeError('`out` {} is neither None nor a ' 'DiscreteLpElement instance'.format(out)) ndim = vol_data.ndim # Create astra geometries vol_geom = astra_volume_geometry(vol_data.space) proj_geom = astra_projection_geometry(geometry) # Create projector if not all(s == vol_data.space.interp_byaxis[0] for s in vol_data.space.interp_byaxis): raise ValueError('volume interpolation must be the same in each ' 'dimension, got {}'.format(vol_data.space.interp)) vol_interp = vol_data.space.interp proj_id = astra_projector(vol_interp, vol_geom, proj_geom, ndim, impl='cpu') # Create ASTRA data structures vol_id = astra_data(vol_geom, datatype='volume', data=vol_data, allow_copy=True) with writable_array(out, dtype='float32', order='C') as arr: sino_id = astra_data(proj_geom, datatype='projection', data=arr, ndim=proj_space.ndim) # Create algorithm algo_id = astra_algorithm('forward', ndim, vol_id, sino_id, proj_id, impl='cpu') # Run algorithm astra.algorithm.run(algo_id) # Delete ASTRA objects astra.algorithm.delete(algo_id) astra.data2d.delete((vol_id, sino_id)) astra.projector.delete(proj_id) return out
def astra_cpu_back_projector(proj_data, geometry, reco_space, out=None): """Run an ASTRA back-projection on the given data using the CPU. Parameters ---------- proj_data : `DiscreteLpElement` Projection data to which the back-projector is applied geometry : `Geometry` Geometry defining the tomographic setup reco_space : `DiscreteLp` Space to which the calling operator maps out : ``reco_space`` element, optional Element of the reconstruction space to which the result is written. If ``None``, an element in ``reco_space`` is created. Returns ------- out : ``reco_space`` element Reconstruction data resulting from the application of the backward projector. If ``out`` was provided, the returned object is a reference to it. """ if not isinstance(proj_data, DiscreteLpElement): raise TypeError('projection data {!r} is not a DiscreteLpElement ' 'instance'.format(proj_data)) if proj_data.space.impl != 'numpy': raise TypeError('data type {!r} of the projection space is not an ' 'instance of NumpyNtuples' ''.format(proj_data.shape.dspace)) if not isinstance(geometry, Geometry): raise TypeError('geometry {!r} is not a Geometry instance' ''.format(geometry)) if not isinstance(reco_space, DiscreteLp): raise TypeError('reconstruction space {!r} is not a DiscreteLp ' 'instance'.format(reco_space)) if reco_space.impl != 'numpy': raise TypeError('data type {!r} of the reconstruction space is not an ' 'instance of NumpyNtuples'.format(reco_space.dspace)) if reco_space.ndim != geometry.ndim: raise ValueError('dimensions {} of reconstruction space and {} of ' 'geometry do not match'.format( reco_space.ndim, geometry.ndim)) if out is None: out = reco_space.element() else: if out not in reco_space: raise TypeError('`out` {} is neither None nor a ' 'DiscreteLpElement instance'.format(out)) ndim = proj_data.ndim # Create astra geometries vol_geom = astra_volume_geometry(reco_space) proj_geom = astra_projection_geometry(geometry) # Create ASTRA data structure sino_id = astra_data(proj_geom, datatype='projection', data=proj_data, allow_copy=True) # Create projector # TODO: implement with different schemes for angles and detector if not all(s == proj_data.space.interp_byaxis[0] for s in proj_data.space.interp_byaxis): raise ValueError('data interpolation must be the same in each ' 'dimension, got {}' ''.format(proj_data.space.interp_byaxis)) proj_interp = proj_data.space.interp proj_id = astra_projector(proj_interp, vol_geom, proj_geom, ndim, impl='cpu') # convert out to correct dtype and order if needed with writable_array(out, dtype='float32', order='C') as arr: vol_id = astra_data(vol_geom, datatype='volume', data=arr, ndim=reco_space.ndim) # Create algorithm algo_id = astra_algorithm('backward', ndim, vol_id, sino_id, proj_id, impl='cpu') # Run algorithm and delete it astra.algorithm.run(algo_id) # Weight the adjoint by appropriate weights scaling_factor = float(proj_data.space.weighting.const) scaling_factor /= float(reco_space.weighting.const) out *= scaling_factor # Delete ASTRA objects astra.algorithm.delete(algo_id) astra.data2d.delete((vol_id, sino_id)) astra.projector.delete(proj_id) return out
def astra_cpu_back_projector(proj_data, geometry, vol_space, out=None, astra_proj_type=None): """Run an ASTRA back-projection on the given data using the CPU. Parameters ---------- proj_data : `DiscreteLpElement` Projection data to which the back-projector is applied. geometry : `Geometry` Geometry defining the tomographic setup. vol_space : `DiscreteLp` Space to which the calling operator maps. out : ``vol_space`` element, optional Element of the reconstruction space to which the result is written. If ``None``, an element in ``vol_space`` is created. astra_proj_type : str, optional Type of projector that should be used. See `the ASTRA documentation <http://www.astra-toolbox.com/docs/proj2d.html>`_ for details. By default, a suitable projector type for the given geometry is selected, see `default_astra_proj_type`. Returns ------- out : ``vol_space`` element Reconstruction data resulting from the application of the backward projector. If ``out`` was provided, the returned object is a reference to it. """ if not isinstance(proj_data, DiscreteLpElement): raise TypeError('projection data {!r} is not a DiscreteLpElement ' 'instance'.format(proj_data)) if proj_data.space.impl != 'numpy': raise TypeError('`proj_data` must be a `numpy.ndarray` based, ' "container got `impl` {!r}" "".format(proj_data.space.impl)) if not isinstance(geometry, Geometry): raise TypeError('geometry {!r} is not a Geometry instance' ''.format(geometry)) if not isinstance(vol_space, DiscreteLp): raise TypeError('volume space {!r} is not a DiscreteLp ' 'instance'.format(vol_space)) if vol_space.impl != 'numpy': raise TypeError("`vol_space.impl` must be 'numpy', got {!r}" "".format(vol_space.impl)) if vol_space.ndim != geometry.ndim: raise ValueError('dimensions {} of reconstruction space and {} of ' 'geometry do not match'.format( vol_space.ndim, geometry.ndim)) if out is None: out = vol_space.element() else: if out not in vol_space: raise TypeError('`out` {} is neither None nor a ' 'DiscreteLpElement instance'.format(out)) ndim = proj_data.ndim # Create astra geometries vol_geom = astra_volume_geometry(vol_space) proj_geom = astra_projection_geometry(geometry) # Create ASTRA data structure sino_id = astra_data(proj_geom, datatype='projection', data=proj_data, allow_copy=True) # Create projector if astra_proj_type is None: astra_proj_type = default_astra_proj_type(geometry) proj_id = astra_projector(astra_proj_type, vol_geom, proj_geom, ndim) # Convert out to correct dtype and order if needed. with writable_array(out, dtype='float32', order='C') as out_arr: vol_id = astra_data(vol_geom, datatype='volume', data=out_arr, ndim=vol_space.ndim) # Create algorithm algo_id = astra_algorithm('backward', ndim, vol_id, sino_id, proj_id, impl='cpu') # Run algorithm astra.algorithm.run(algo_id) # Weight the adjoint by appropriate weights scaling_factor = float(proj_data.space.weighting.const) scaling_factor /= float(vol_space.weighting.const) out *= scaling_factor # Delete ASTRA objects astra.algorithm.delete(algo_id) astra.data2d.delete((vol_id, sino_id)) astra.projector.delete(proj_id) return out
def astra_cpu_forward_projector(vol_data, geometry, proj_space, out=None, astra_proj_type=None): """Run an ASTRA forward projection on the given data using the CPU. Parameters ---------- vol_data : `DiscretizedSpaceElement` Volume data to which the forward projector is applied. geometry : `Geometry` Geometry defining the tomographic setup. proj_space : `DiscretizedSpace` Space to which the calling operator maps. out : ``proj_space`` element, optional Element of the projection space to which the result is written. If ``None``, an element in ``proj_space`` is created. astra_proj_type : str, optional Type of projector that should be used. See `the ASTRA documentation <http://www.astra-toolbox.com/docs/proj2d.html>`_ for details. By default, a suitable projector type for the given geometry is selected, see `default_astra_proj_type`. Returns ------- out : ``proj_space`` element Projection data resulting from the application of the projector. If ``out`` was provided, the returned object is a reference to it. """ if not isinstance(vol_data, DiscretizedSpaceElement): raise TypeError( 'volume data {!r} is not a `DiscretizedSpaceElement` instance' ''.format(vol_data) ) if vol_data.space.impl != 'numpy': raise TypeError( "`vol_data.space.impl` must be 'numpy', got {!r}" "".format(vol_data.space.impl) ) if not isinstance(geometry, Geometry): raise TypeError( 'geometry {!r} is not a Geometry instance'.format(geometry) ) if not isinstance(proj_space, DiscretizedSpace): raise TypeError( '`proj_space` {!r} is not a DiscretizedSpace instance.' ''.format(proj_space) ) if proj_space.impl != 'numpy': raise TypeError( "`proj_space.impl` must be 'numpy', got {!r}" "".format(proj_space.impl) ) if vol_data.ndim != geometry.ndim: raise ValueError( 'dimensions {} of volume data and {} of geometry do not match' ''.format(vol_data.ndim, geometry.ndim) ) if out is None: out = proj_space.element() else: if out not in proj_space: raise TypeError( '`out` {} is neither None nor a `DiscretizedSpaceElement` ' 'instance'.format(out) ) ndim = vol_data.ndim # Create astra geometries vol_geom = astra_volume_geometry(vol_data.space) proj_geom = astra_projection_geometry(geometry) # Create projector if astra_proj_type is None: astra_proj_type = default_astra_proj_type(geometry) proj_id = astra_projector(astra_proj_type, vol_geom, proj_geom, ndim) # Create ASTRA data structures vol_data_arr = np.asarray(vol_data) vol_id = astra_data(vol_geom, datatype='volume', data=vol_data_arr, allow_copy=True) with writable_array(out, dtype='float32', order='C') as out_arr: sino_id = astra_data(proj_geom, datatype='projection', data=out_arr, ndim=proj_space.ndim) # Create algorithm algo_id = astra_algorithm('forward', ndim, vol_id, sino_id, proj_id, impl='cpu') # Run algorithm astra.algorithm.run(algo_id) # Delete ASTRA objects astra.algorithm.delete(algo_id) astra.data2d.delete((vol_id, sino_id)) astra.projector.delete(proj_id) return out
def _varlp_prox_call(f, out, exponent, sigma, impl, num_newton_iter, conj): """Implementation of the proximals of the modular and its conjugate. Parameters ---------- f : domain element Element at which to evaluate the operator. out : range element Element to which the result is written. exponent : base space element Variable exponent used in the modular. sigma : positive float Scaling parameter in the proximal operator. impl : string Implementation back-end for the proximal operator. num_newton_iter : int, optional Number of Newton iterations. conj : bool Apply the proximal of the convex conjugate if ``True``, otherwise the proximal of the variable Lp modular. """ num_newton_iter = int(num_newton_iter) sigma = float(sigma) # Compute the magnitude of the input function if isinstance(f.space, ProductSpace): pwnorm = PointwiseNorm(f.space) prox_fac = pwnorm(f) else: prox_fac = out f.ufuncs.absolute(out=prox_fac) if conj: prox_npy = _numpy_impl.varlp_cc_prox_factor_npy prox_numba = _numba_impl.varlp_cc_prox_factor_numba prox_cython = _cython_impl.varlp_cc_prox_factor_cython prox_gpuary = _gpuarray_impl.varlp_cc_prox_factor_gpuary else: prox_npy = _numpy_impl.varlp_prox_factor_npy prox_numba = _numba_impl.varlp_prox_factor_numba prox_cython = _cython_impl.varlp_prox_factor_cython prox_gpuary = _gpuarray_impl.varlp_prox_factor_gpuary # Compute the multiplicative factor if impl == 'numpy': with writable_array(prox_fac) as out_arr: prox_npy(prox_fac.asarray(), exponent.asarray(), sigma, num_newton_iter, out=out_arr) elif impl.startswith('numba'): _, target = impl.split('_') prox_fac[:] = prox_numba(prox_fac.asarray(), exponent.asarray(), sigma, num_newton_iter, target=target) elif impl == 'cython': if not _cython_impl.CYTHON_EXTENSION_BUILT: raise RuntimeError( 'Cython extension has not been built. ' 'Run `python setup.py build_ext` to build the extension.' 'For development installations of this package ' '(`pip install -e`), add the `--inplace` option to the ' 'build command.') with writable_array(prox_fac) as out_arr: prox_cython(prox_fac.asarray(), exponent.asarray(), sigma, num_newton_iter, out=out_arr) elif impl == 'gpuarray': if not _gpuarray_impl.PYGPU_AVAILABLE: raise RuntimeError( '`pygpu` package not installed. You need Anaconda ' '(or Miniconda) to install it via `conda install pygpu`. ' 'Alternatively, you can build it from source. See ' 'https://github.com/Theano/libgpuarray for more information.') # TODO: context manager for GPU arrays if isinstance(f.space, ProductSpace): dom_impl = f.space[0].impl else: dom_impl = f.space.impl if dom_impl == 'gpuarray': out_arr = prox_fac.tensor.data prox_fac_arr = prox_fac.tensor.data else: out_arr = None prox_fac_arr = prox_fac.asarray() result = prox_gpuary(prox_fac_arr, exponent.asarray(), sigma, num_newton_iter, out=out_arr) if out_arr is None: prox_fac[:] = result else: raise RuntimeError('bad impl {!r}'.format(impl)) # Perform the multiplication if isinstance(f.space, ProductSpace): # out is empty out.assign(f) out *= prox_fac else: # out already stores the factor out *= f return f
def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): """Interface to Numpy's ufunc machinery. This method is called by Numpy version 1.13 and higher as a single point for the ufunc dispatch logic. An object implementing ``__array_ufunc__`` takes over control when a `numpy.ufunc` is called on it, allowing it to use custom implementations and output types. This includes handling of in-place arithmetic like ``npy_array += custom_obj``. In this case, the custom object's ``__array_ufunc__`` takes precedence over the baseline `numpy.ndarray` implementation. It will be called with ``npy_array`` as ``out`` argument, which ensures that the returned object is a Numpy array. For this to work properly, ``__array_ufunc__`` has to accept Numpy arrays as ``out`` arguments. See the `corresponding NEP`_ and the `interface documentation`_ for further details. See also the `general documentation on Numpy ufuncs`_. .. note:: This basic implementation casts inputs and outputs to Numpy arrays and evaluates ``ufunc`` on those. For `numpy.ndarray` based data storage, this incurs no significant overhead compared to direct usage of Numpy arrays. For other (in particular non-local) implementations, e.g., GPU arrays or distributed memory, overhead is significant due to copies to CPU main memory. In those classes, the ``__array_ufunc__`` mechanism should be overridden in favor of a native implementations if possible. .. note:: If no ``out`` parameter is provided, this implementation just returns the raw array and does not attempt to wrap the result in any kind of space. Parameters ---------- ufunc : `numpy.ufunc` Ufunc that should be called on ``self``. method : str Method on ``ufunc`` that should be called on ``self``. Possible values: ``'__call__'``, ``'accumulate'``, ``'at'``, ``'outer'``, ``'reduce'``, ``'reduceat'`` input1, ..., inputN: Positional arguments to ``ufunc.method``. kwargs: Keyword arguments to ``ufunc.method``. Returns ------- ufunc_result : `Tensor`, `numpy.ndarray` or tuple Result of the ufunc evaluation. If no ``out`` keyword argument was given, the result is a `Tensor` or a tuple of such, depending on the number of outputs of ``ufunc``. If ``out`` was provided, the returned object or tuple entries refer(s) to ``out``. References ---------- .. _corresponding NEP: https://docs.scipy.org/doc/numpy/neps/ufunc-overrides.html .. _interface documentation: https://docs.scipy.org/doc/numpy/reference/arrays.classes.html\ #numpy.class.__array_ufunc__ .. _general documentation on Numpy ufuncs: https://docs.scipy.org/doc/numpy/reference/ufuncs.html .. _reduceat documentation: https://docs.scipy.org/doc/numpy/reference/generated/\ numpy.ufunc.reduceat.html """ # --- Process `out` --- # # Unwrap out if provided. The output parameters are all wrapped # in one tuple, even if there is only one. out_tuple = kwargs.pop('out', ()) # Check number of `out` args, depending on `method` if method == '__call__' and len(out_tuple) not in (0, ufunc.nout): raise ValueError("ufunc {}: need 0 or {} `out` arguments for " "`method='__call__'`, got {}" ''.format(ufunc.__name__, ufunc.nout, len(out_tuple))) elif method != '__call__' and len(out_tuple) not in (0, 1): raise ValueError( 'ufunc {}: need 0 or 1 `out` arguments for `method={!r}`, ' 'got {}'.format(ufunc.__name__, method, len(out_tuple))) # We allow our own tensors, the data container type and # `numpy.ndarray` objects as `out` (see docs for reason for the # latter) valid_types = (type(self), type(self.data), np.ndarray) if not all(isinstance(o, valid_types) or o is None for o in out_tuple): return NotImplemented # Assign to `out` or `out1` and `out2`, respectively out = out1 = out2 = None if len(out_tuple) == 1: out = out_tuple[0] elif len(out_tuple) == 2: out1 = out_tuple[0] out2 = out_tuple[1] # --- Process `inputs` --- # # Convert inputs that are ODL tensors or their data containers to # Numpy arrays so that the native Numpy ufunc is called later inputs = tuple( np.asarray(inp) if isinstance(inp, (type(self), type(self.data))) else inp for inp in inputs) # --- Get some parameters for later --- # # Arguments for `writable_array` and/or space constructors out_dtype = kwargs.get('dtype', None) if out_dtype is None: array_kwargs = {} else: array_kwargs = {'dtype': out_dtype} # --- Evaluate ufunc --- # if method == '__call__': if ufunc.nout == 1: # Make context for output (trivial one returns `None`) if out is None: out_ctx = nullcontext() else: out_ctx = writable_array(out, **array_kwargs) # Evaluate ufunc with out_ctx as out_arr: kwargs['out'] = out_arr res = ufunc(*inputs, **kwargs) # Return result (may be a raw array or a space element) return res elif ufunc.nout == 2: # Make contexts for outputs (trivial ones return `None`) if out1 is not None: out1_ctx = writable_array(out1, **array_kwargs) else: out1_ctx = nullcontext() if out2 is not None: out2_ctx = writable_array(out2, **array_kwargs) else: out2_ctx = nullcontext() # Evaluate ufunc with out1_ctx as out1_arr, out2_ctx as out2_arr: kwargs['out'] = (out1_arr, out2_arr) res1, res2 = ufunc(*inputs, **kwargs) # Return results (may be raw arrays or space elements) return res1, res2 else: raise NotImplementedError('nout = {} not supported' ''.format(ufunc.nout)) else: # method != '__call__' # Make context for output (trivial one returns `None`) if out is None: out_ctx = nullcontext() else: out_ctx = writable_array(out, **array_kwargs) # Evaluate ufunc method if method == 'at': with writable_array(inputs[0]) as inp_arr: res = ufunc.at(inp_arr, *inputs[1:], **kwargs) else: with out_ctx as out_arr: kwargs['out'] = out_arr res = getattr(ufunc, method)(*inputs, **kwargs) # Return result (may be scalar, raw array or space element) return res
def astra_cpu_back_projector(proj_data, geometry, reco_space, out=None): """Run an ASTRA back-projection on the given data using the CPU. Parameters ---------- proj_data : `DiscreteLpElement` Projection data to which the back-projector is applied geometry : `Geometry` Geometry defining the tomographic setup reco_space : `DiscreteLp` Space to which the calling operator maps out : ``reco_space`` element, optional Element of the reconstruction space to which the result is written. If ``None``, an element in ``reco_space`` is created. Returns ------- out : ``reco_space`` element Reconstruction data resulting from the application of the backward projector. If ``out`` was provided, the returned object is a reference to it. """ if not isinstance(proj_data, DiscreteLpElement): raise TypeError('projection data {!r} is not a DiscreteLpElement ' 'instance'.format(proj_data)) if proj_data.space.impl != 'numpy': raise TypeError('data type {!r} of the projection space is not an ' 'instance of NumpyNtuples' ''.format(proj_data.shape.dspace)) if not isinstance(geometry, Geometry): raise TypeError('geometry {!r} is not a Geometry instance' ''.format(geometry)) if not isinstance(reco_space, DiscreteLp): raise TypeError('reconstruction space {!r} is not a DiscreteLp ' 'instance'.format(reco_space)) if reco_space.impl != 'numpy': raise TypeError('data type {!r} of the reconstruction space is not an ' 'instance of NumpyNtuples'.format(reco_space.dspace)) if reco_space.ndim != geometry.ndim: raise ValueError('dimensions {} of reconstruction space and {} of ' 'geometry do not match'.format( reco_space.ndim, geometry.ndim)) if out is None: out = reco_space.element() else: if out not in reco_space: raise TypeError('`out` {} is neither None nor a ' 'DiscreteLpElement instance'.format(out)) ndim = proj_data.ndim # Create astra geometries vol_geom = astra_volume_geometry(reco_space) proj_geom = astra_projection_geometry(geometry) # Create ASTRA data structure sino_id = astra_data(proj_geom, datatype='projection', data=proj_data, allow_copy=True) # Create projector # TODO: implement with different schemes for angles and detector if not all(s == proj_data.space.interp_by_axis[0] for s in proj_data.space.interp_by_axis): raise ValueError('data interpolation must be the same in each ' 'dimension, got {}' ''.format(proj_data.space.interp_by_axis)) proj_interp = proj_data.space.interp proj_id = astra_projector(proj_interp, vol_geom, proj_geom, ndim, impl='cpu') # convert out to correct dtype and order if needed with writable_array(out, dtype='float32', order='C') as arr: vol_id = astra_data(vol_geom, datatype='volume', data=arr, ndim=reco_space.ndim) # Create algorithm algo_id = astra_algorithm('backward', ndim, vol_id, sino_id, proj_id, impl='cpu') # Run algorithm and delete it astra.algorithm.run(algo_id) # Angular integration weighting factor # angle interval weight by approximate cell volume extent = float(geometry.motion_partition.extent) size = float(geometry.motion_partition.size) scaling_factor = extent / size # Fix inconsistent scaling: parallel2d & fanflat scale with (voxel # stride)**2 / (pixel stride), currently only square voxels are supported scaling_factor *= float(geometry.det_partition.cell_sides[0]) scaling_factor /= float(reco_space.partition.cell_sides[0]) ** 2 out *= scaling_factor # Delete ASTRA objects astra.algorithm.delete(algo_id) astra.data2d.delete((vol_id, sino_id)) astra.projector.delete(proj_id) return out
def skimage_radon_back_projector(sinogram, geometry, vol_space, out=None): """Calculate forward projection using skimage. Parameters ---------- sinogram : `DiscreteLpElement` Sinogram (projections) to backproject. geometry : `Geometry` The projection geometry to use. vol_space : `DiscreteLp` Space in which reconstructed volumes live. out : ``vol_space`` element, optional An element to which the result should be written. Returns ------- volume : ``vol_space`` element Result of the back-projection. If ``out`` was given, the returned object is a reference to it. """ # Lazy import due to significant import time from skimage.transform import iradon theta = np.degrees(geometry.angles) skimage_range = skimage_proj_space(geometry, vol_space, sinogram.space) skimage_sinogram = skimage_range.element() with writable_array(skimage_sinogram) as sino_arr: point_collocation( clamped_interpolation(sinogram.space, sinogram), skimage_range.grid.meshgrid, out=sino_arr, ) if out is None: out = vol_space.element() else: # Only do asserts here since these are backend functions assert out in vol_space # Rotate back from (rows, cols) to (x, y), then back-project (no filter) backproj = iradon( skimage_sinogram.asarray().T, theta, output_size=vol_space.shape[0], filter=None, circle=False, ) out[:] = np.rot90(backproj, -1) # Empirically determined value, gives correct scaling scaling_factor = 4 * geometry.motion_params.length / (2 * np.pi) # Correct in case of non-weighted spaces proj_volume = np.prod(sinogram.space.partition.extent) proj_size = sinogram.space.partition.size proj_weighting = proj_volume / proj_size scaling_factor *= sinogram.space.weighting.const / proj_weighting scaling_factor /= vol_space.weighting.const / vol_space.cell_volume # Correctly scale the output out *= scaling_factor return out
def astra_cpu_forward_projector(vol_data, geometry, proj_space, out=None): """Run an ASTRA forward projection on the given data using the CPU. Parameters ---------- vol_data : `DiscreteLpElement` Volume data to which the forward projector is applied geometry : `Geometry` Geometry defining the tomographic setup proj_space : `DiscreteLp` Space to which the calling operator maps out : ``proj_space`` element, optional Element of the projection space to which the result is written. If ``None``, an element in ``proj_space`` is created. Returns ------- out : ``proj_space`` element Projection data resulting from the application of the projector. If ``out`` was provided, the returned object is a reference to it. """ if not isinstance(vol_data, DiscreteLpElement): raise TypeError('volume data {!r} is not a `DiscreteLpElement` ' 'instance.'.format(vol_data)) if vol_data.space.impl != 'numpy': raise TypeError('dspace {!r} of the volume is not an ' 'instance of `NumpyNtuples`' ''.format(vol_data.space.dspace)) if not isinstance(geometry, Geometry): raise TypeError('geometry {!r} is not a Geometry instance' ''.format(geometry)) if not isinstance(proj_space, DiscreteLp): raise TypeError('projection space {!r} is not a DiscreteLp ' 'instance.'.format(proj_space)) if proj_space.impl != 'numpy': raise TypeError('data type {!r} of the reconstruction space is not an ' 'instance of NumpyNtuples'.format(proj_space.dspace)) if vol_data.ndim != geometry.ndim: raise ValueError('dimensions {} of volume data and {} of geometry ' 'do not match' ''.format(vol_data.ndim, geometry.ndim)) if out is None: out = proj_space.element() else: if out not in proj_space: raise TypeError('`out` {} is neither None nor a ' 'DiscreteLpElement instance'.format(out)) ndim = vol_data.ndim # Create astra geometries vol_geom = astra_volume_geometry(vol_data.space) proj_geom = astra_projection_geometry(geometry) # Create projector if not all(s == vol_data.space.interp_by_axis[0] for s in vol_data.space.interp_by_axis): raise ValueError('volume interpolation must be the same in each ' 'dimension, got {}'.format(vol_data.space.interp)) vol_interp = vol_data.space.interp proj_id = astra_projector(vol_interp, vol_geom, proj_geom, ndim, impl='cpu') # Create ASTRA data structures vol_id = astra_data(vol_geom, datatype='volume', data=vol_data, allow_copy=True) with writable_array(out, dtype='float32', order='C') as arr: sino_id = astra_data(proj_geom, datatype='projection', data=arr, ndim=proj_space.ndim) # Create algorithm algo_id = astra_algorithm('forward', ndim, vol_id, sino_id, proj_id, impl='cpu') # Run algorithm astra.algorithm.run(algo_id) # Delete ASTRA objects astra.algorithm.delete(algo_id) astra.data2d.delete((vol_id, sino_id)) astra.projector.delete(proj_id) return out
def _call(self, f): """Return ``self(f)``.""" # Compute the magnitude of the input function if isinstance(self.domain, ProductSpace): pwnorm = PointwiseNorm(self.domain) integrand = pwnorm(f) else: integrand = f.ufuncs.absolute() # Compute the integrand if self.impl == 'numpy': with writable_array(integrand) as out_arr: _numpy_impl.varlp_cc_integrand_npy(integrand.asarray(), self.exponent.asarray(), out=out_arr) elif self.impl.startswith('numba'): _, target = self.impl.split('_') integrand[:] = _numba_impl.varlp_cc_integrand_numba( integrand.asarray(), self.exponent.asarray(), target=target) elif self.impl == 'cython': if not _cython_impl.CYTHON_EXTENSION_BUILT: raise RuntimeError( 'Cython extension has not been built. ' 'Run `python setup.py build_ext` to build the extension.' 'For development installations of this package ' '(`pip install -e`), add the `--inplace` option to the ' 'build command.') with writable_array(integrand) as out_arr: _cython_impl.varlp_cc_integrand_cython(integrand.asarray(), self.exponent.asarray(), out=out_arr) elif self.impl == 'gpuarray': if not _gpuarray_impl.PYGPU_AVAILABLE: raise RuntimeError( '`pygpu` package not installed. You need Anaconda ' '(or Miniconda) to install it via `conda install pygpu`. ' 'Alternatively, you can build it from source. See ' 'https://github.com/Theano/libgpuarray for more ' 'information.') # TODO: context manager for GPU arrays if isinstance(self.domain, ProductSpace): dom_impl = self.domain[0].impl else: dom_impl = self.domain.impl if dom_impl == 'gpuarray': out_arr = integrand.data integrand_arr = integrand.data else: out_arr = None integrand_arr = integrand.asarray() result = _gpuarray_impl.varlp_cc_integrand_gpuary( integrand_arr, self.exponent.asarray(), out=out_arr) if out_arr is None: integrand[:] = result else: raise RuntimeError('bad impl {!r}'.format(self.impl)) # Integrate if isinstance(self.domain, ProductSpace): return integrand.inner(self.domain[0].one()) else: return integrand.inner(self.domain.one())