def __init__(self, size, dtype): """Initialize a new instance. Parameters ---------- size : non-negative int Number of entries in a tuple. dtype : Data type for each tuple entry. Can be provided in any way the `numpy.dtype` function understands, most notably as built-in type, as one of NumPy's internal datatype objects or as string. Only scalar data types (numbers) are allowed. """ NtuplesBase.__init__(self, size, dtype) if not is_scalar_dtype(self.dtype): raise TypeError('{!r} is not a scalar data type'.format(dtype)) if is_real_dtype(self.dtype): field = RealNumbers() self.__is_real = True self.__real_dtype = self.dtype self.__real_space = self try: self.__complex_dtype = complex_dtype(self.dtype) except ValueError: self.__complex_dtype = None self.__complex_space = None # Set in first call of astype else: field = ComplexNumbers() self.__is_real = False try: self.__real_dtype = real_dtype(self.dtype) except ValueError: self.__real_dtype = None self.__real_space = None # Set in first call of astype self.__complex_dtype = self.dtype self.__complex_space = self self.__is_floating = is_floating_dtype(self.dtype) LinearSpace.__init__(self, field)
def _params_from_dtype(dtype): if is_real_dtype(dtype): halfcomplex = True else: halfcomplex = False return halfcomplex, complex_dtype(dtype)
def pyfftw_call(array_in, array_out, direction='forward', axes=None, halfcomplex=False, **kwargs): """Calculate the DFT with pyfftw. The discrete Fourier (forward) transform calcuates the sum:: f_hat[k] = sum_j( f[j] * exp(-2*pi*1j * j*k/N) ) where the summation is taken over all indices ``j = (j[0], ..., j[d-1])`` in the range ``0 <= j < N`` (component-wise), with ``N`` being the shape of the input array. The output indices ``k`` lie in the same range, except for half-complex transforms, where the last axis ``i`` in ``axes`` is shortened to ``0 <= k[i] < floor(N[i]/2) + 1``. In the backward transform, sign of the the exponential argument is flipped. Parameters ---------- array_in : `numpy.ndarray` Array to be transformed array_out : `numpy.ndarray` Output array storing the transformed values, may be aliased with ``array_in``. direction : {'forward', 'backward'}, optional Direction of the transform axes : int or sequence of ints, optional Dimensions along which to take the transform. ``None`` means using all axes and is equivalent to ``np.arange(ndim)``. halfcomplex : bool, optional If ``True``, calculate only the negative frequency part along the last axis. If ``False``, calculate the full complex FFT. This option can only be used with real input data. Other Parameters ---------------- fftw_plan : ``pyfftw.FFTW``, optional Use this plan instead of calculating a new one. If specified, the options ``planning_effort``, ``planning_timelimit`` and ``threads`` have no effect. planning_effort : str, optional Flag for the amount of effort put into finding an optimal FFTW plan. See the `FFTW doc on planner flags <http://www.fftw.org/fftw3_doc/Planner-Flags.html>`_. Available options: {'estimate', 'measure', 'patient', 'exhaustive'} Default: 'estimate' planning_timelimit : float or ``None``, optional Limit planning time to roughly this many seconds. Default: ``None`` (no limit) threads : int, optional Number of threads to use. Default: Number of CPUs if the number of data points is larger than 4096, else 1. normalise_idft : bool, optional If ``True``, the result of the backward transform is divided by ``1 / N``, where ``N`` is the total number of points in ``array_in[axes]``. This ensures that the IDFT is the true inverse of the forward DFT. Default: ``False`` import_wisdom : filename or file handle, optional File to load FFTW wisdom from. If the file does not exist, it is ignored. export_wisdom : filename or file handle, optional File to append the accumulated FFTW wisdom to Returns ------- fftw_plan : ``pyfftw.FFTW`` The plan object created from the input arguments. It can be reused for transforms of the same size with the same data types. Note that reuse only gives a speedup if the initial plan used a planner flag other than ``'estimate'``. If ``fftw_plan`` was specified, the returned object is a reference to it. Notes ----- * The planning and direction flags can also be specified as capitalized and prepended by ``'FFTW_'``, i.e. in the original FFTW form. * For a ``halfcomplex`` forward transform, the arrays must fulfill ``array_out.shape[axes[-1]] == array_in.shape[axes[-1]] // 2 + 1``, and vice versa for backward transforms. * All planning schemes except ``'estimate'`` require an internal copy of the input array but are often several times faster after the first call (measuring results are cached). Typically, 'measure' is a good compromise. If you cannot afford the copy, use ``'estimate'``. * If a plan is provided via the ``fftw_plan`` parameter, no copy is needed internally. """ import pickle if not array_in.flags.aligned: raise ValueError('input array not aligned') if not array_out.flags.aligned: raise ValueError('output array not aligned') if axes is None: axes = tuple(range(array_in.ndim)) axes = normalized_axes_tuple(axes, array_in.ndim) direction = _flag_pyfftw_to_odl(direction) fftw_plan_in = kwargs.pop('fftw_plan', None) planning_effort = _flag_pyfftw_to_odl( kwargs.pop('planning_effort', 'estimate') ) planning_timelimit = kwargs.pop('planning_timelimit', None) threads = kwargs.pop('threads', None) normalise_idft = kwargs.pop('normalise_idft', False) wimport = kwargs.pop('import_wisdom', '') wexport = kwargs.pop('export_wisdom', '') # Cast input to complex if necessary array_in_copied = False if is_real_dtype(array_in.dtype) and not halfcomplex: # Need to cast array_in to complex dtype array_in = array_in.astype(complex_dtype(array_in.dtype)) array_in_copied = True # Do consistency checks on the arguments _pyfftw_check_args(array_in, array_out, axes, halfcomplex, direction) # Import wisdom if possible if wimport: try: with open(wimport, 'rb') as wfile: wisdom = pickle.load(wfile) except IOError: wisdom = [] except TypeError: # Got file handle wisdom = pickle.load(wimport) if wisdom: pyfftw.import_wisdom(wisdom) # Copy input array if it hasn't been done yet and the planner is likely # to destroy it. If we already have a plan, we don't have to worry. planner_destroys = _pyfftw_destroys_input( [planning_effort], direction, halfcomplex, array_in.ndim) must_copy_array_in = fftw_plan_in is None and planner_destroys if must_copy_array_in and not array_in_copied: plan_arr_in = np.empty_like(array_in) flags = [_flag_odl_to_pyfftw(planning_effort), 'FFTW_DESTROY_INPUT'] else: plan_arr_in = array_in flags = [_flag_odl_to_pyfftw(planning_effort)] if fftw_plan_in is None: if threads is None: if plan_arr_in.size <= 4096: # Trade-off wrt threading overhead threads = 1 else: threads = cpu_count() fftw_plan = pyfftw.FFTW( plan_arr_in, array_out, direction=_flag_odl_to_pyfftw(direction), flags=flags, planning_timelimit=planning_timelimit, threads=threads, axes=axes) else: fftw_plan = fftw_plan_in fftw_plan(array_in, array_out, normalise_idft=normalise_idft) if wexport: try: with open(wexport, 'ab') as wfile: pickle.dump(pyfftw.export_wisdom(), wfile) except TypeError: # Got file handle pickle.dump(pyfftw.export_wisdom(), wexport) return fftw_plan
def _pyfftw_check_args(arr_in, arr_out, axes, halfcomplex, direction): """Raise an error if anything is not ok with in and out.""" if len(set(axes)) != len(axes): raise ValueError('duplicate axes are not allowed') if direction == 'forward': out_shape = list(arr_in.shape) if halfcomplex: try: out_shape[axes[-1]] = arr_in.shape[axes[-1]] // 2 + 1 except IndexError: raise IndexError('axis index {} out of range for array ' 'with {} axes' ''.format(axes[-1], arr_in.ndim)) if arr_out.shape != tuple(out_shape): raise ValueError('expected output shape {}, got {}' ''.format(tuple(out_shape), arr_out.shape)) if is_real_dtype(arr_in.dtype): out_dtype = complex_dtype(arr_in.dtype) elif halfcomplex: raise ValueError('cannot combine halfcomplex forward transform ' 'with complex input') else: out_dtype = arr_in.dtype if arr_out.dtype != out_dtype: raise ValueError('expected output dtype {}, got {}' ''.format(dtype_repr(out_dtype), dtype_repr(arr_out.dtype))) elif direction == 'backward': in_shape = list(arr_out.shape) if halfcomplex: try: in_shape[axes[-1]] = arr_out.shape[axes[-1]] // 2 + 1 except IndexError as err: raise IndexError('axis index {} out of range for array ' 'with {} axes' ''.format(axes[-1], arr_out.ndim)) if arr_in.shape != tuple(in_shape): raise ValueError('expected input shape {}, got {}' ''.format(tuple(in_shape), arr_in.shape)) if is_real_dtype(arr_out.dtype): in_dtype = complex_dtype(arr_out.dtype) elif halfcomplex: raise ValueError('cannot combine halfcomplex backward transform ' 'with complex output') else: in_dtype = arr_out.dtype if arr_in.dtype != in_dtype: raise ValueError('expected input dtype {}, got {}' ''.format(dtype_repr(in_dtype), dtype_repr(arr_in.dtype))) else: # Shouldn't happen raise RuntimeError
def __init__(self, domain, field=None, out_dtype=None): """Initialize a new instance. Parameters ---------- domain : `Set` The domain of the functions field : `Field`, optional The range of the functions, usually the `RealNumbers` or `ComplexNumbers`. If not given, the field is either inferred from ``out_dtype``, or, if the latter is also ``None``, set to ``RealNumbers()``. out_dtype : optional Data type of the return value of a function in this space. Can be given in any way `numpy.dtype` understands, e.g. as string (``'float64'``) or data type (``float``). By default, ``'float64'`` is used for real and ``'complex128'`` for complex spaces. """ if not isinstance(domain, Set): raise TypeError('`domain` {!r} not a Set instance'.format(domain)) if field is not None and not isinstance(field, Field): raise TypeError('`field` {!r} not a `Field` instance' ''.format(field)) # Data type: check if consistent with field, take default for None dtype, dtype_in = np.dtype(out_dtype), out_dtype # Default for both None if field is None and out_dtype is None: field = RealNumbers() out_dtype = np.dtype('float64') # field None, dtype given -> infer field elif field is None: if is_real_dtype(dtype): field = RealNumbers() elif is_complex_floating_dtype(dtype): field = ComplexNumbers() else: raise ValueError('{} is not a scalar data type' ''.format(dtype_in)) # field given -> infer dtype if not given, else check consistency elif field == RealNumbers(): if out_dtype is None: out_dtype = np.dtype('float64') elif not is_real_dtype(dtype): raise ValueError('{} is not a real data type' ''.format(dtype_in)) elif field == ComplexNumbers(): if out_dtype is None: out_dtype = np.dtype('complex128') elif not is_complex_floating_dtype(dtype): raise ValueError('{} is not a complex data type' ''.format(dtype_in)) # Else: keep out_dtype=None, which results in lazy dtype determination LinearSpace.__init__(self, field) FunctionSet.__init__(self, domain, field, out_dtype) # Init cache attributes for real / complex variants if self.field == RealNumbers(): self.__real_out_dtype = self.out_dtype self.__real_space = self self.__complex_out_dtype = complex_dtype(self.out_dtype, default=np.dtype(object)) self.__complex_space = None elif self.field == ComplexNumbers(): self.__real_out_dtype = real_dtype(self.out_dtype) self.__real_space = None self.__complex_out_dtype = self.out_dtype self.__complex_space = self else: self.__real_out_dtype = None self.__real_space = None self.__complex_out_dtype = None self.__complex_space = None
def pyfftw_call(array_in, array_out, direction='forward', axes=None, halfcomplex=False, **kwargs): """Calculate the DFT with pyfftw. The discrete Fourier (forward) transform calcuates the sum:: f_hat[k] = sum_j( f[j] * exp(-2*pi*1j * j*k/N) ) where the summation is taken over all indices ``j = (j[0], ..., j[d-1])`` in the range ``0 <= j < N`` (component-wise), with ``N`` being the shape of the input array. The output indices ``k`` lie in the same range, except for half-complex transforms, where the last axis ``i`` in ``axes`` is shortened to ``0 <= k[i] < floor(N[i]/2) + 1``. In the backward transform, sign of the the exponential argument is flipped. Parameters ---------- array_in : `numpy.ndarray` Array to be transformed array_out : `numpy.ndarray` Output array storing the transformed values, may be aliased with ``array_in``. direction : {'forward', 'backward'} Direction of the transform axes : int or sequence of ints, optional Dimensions along which to take the transform. ``None`` means using all axes and is equivalent to ``np.arange(ndim)``. halfcomplex : bool, optional If ``True``, calculate only the negative frequency part along the last axis. If ``False``, calculate the full complex FFT. This option can only be used with real input data. Other Parameters ---------------- fftw_plan : ``pyfftw.FFTW``, optional Use this plan instead of calculating a new one. If specified, the options ``planning_effort``, ``planning_timelimit`` and ``threads`` have no effect. planning_effort : {'estimate', 'measure', 'patient', 'exhaustive'} Flag for the amount of effort put into finding an optimal FFTW plan. See the `FFTW doc on planner flags <http://www.fftw.org/fftw3_doc/Planner-Flags.html>`_. Default: 'estimate' planning_timelimit : float or ``None``, optional Limit planning time to roughly this many seconds. Default: ``None`` (no limit) threads : int, optional Number of threads to use. Default: Number of CPUs if the number of data points is larger than 4096, else 1. normalise_idft : bool, optional If ``True``, the result of the backward transform is divided by ``1 / N``, where ``N`` is the total number of points in ``array_in[axes]``. This ensures that the IDFT is the true inverse of the forward DFT. Default: ``False`` import_wisdom : filename or file handle, optional File to load FFTW wisdom from. If the file does not exist, it is ignored. export_wisdom : filename or file handle, optional File to append the accumulated FFTW wisdom to Returns ------- fftw_plan : ``pyfftw.FFTW`` The plan object created from the input arguments. It can be reused for transforms of the same size with the same data types. Note that reuse only gives a speedup if the initial plan used a planner flag other than ``'estimate'``. If ``fftw_plan`` was specified, the returned object is a reference to it. Notes ----- * The planning and direction flags can also be specified as capitalized and prepended by ``'FFTW_'``, i.e. in the original FFTW form. * For a ``halfcomplex`` forward transform, the arrays must fulfill ``array_out.shape[axes[-1]] == array_in.shape[axes[-1]] // 2 + 1``, and vice versa for backward transforms. * All planning schemes except ``'estimate'`` require an internal copy of the input array but are often several times faster after the first call (measuring results are cached). Typically, 'measure' is a good compromise. If you cannot afford the copy, use ``'estimate'``. * If a plan is provided via the ``fftw_plan`` parameter, no copy is needed internally. """ import pickle if not array_in.flags.aligned: raise ValueError('input array not aligned') if not array_out.flags.aligned: raise ValueError('output array not aligned') if axes is None: axes = tuple(range(array_in.ndim)) axes = normalized_axes_tuple(axes, array_in.ndim) direction = _pyfftw_to_local(direction) fftw_plan_in = kwargs.pop('fftw_plan', None) planning_effort = _pyfftw_to_local(kwargs.pop('planning_effort', 'estimate')) planning_timelimit = kwargs.pop('planning_timelimit', None) threads = kwargs.pop('threads', None) normalise_idft = kwargs.pop('normalise_idft', False) wimport = kwargs.pop('import_wisdom', '') wexport = kwargs.pop('export_wisdom', '') # Cast input to complex if necessary array_in_copied = False if is_real_dtype(array_in.dtype) and not halfcomplex: # Need to cast array_in to complex dtype array_in = array_in.astype(complex_dtype(array_in.dtype)) array_in_copied = True # Do consistency checks on the arguments _pyfftw_check_args(array_in, array_out, axes, halfcomplex, direction) # Import wisdom if possible if wimport: try: with open(wimport, 'rb') as wfile: wisdom = pickle.load(wfile) except IOError: wisdom = [] except TypeError: # Got file handle wisdom = pickle.load(wimport) if wisdom: pyfftw.import_wisdom(wisdom) # Copy input array if it hasn't been done yet and the planner is likely # to destroy it. If we already have a plan, we don't have to worry. planner_destroys = _pyfftw_destroys_input( [planning_effort], direction, halfcomplex, array_in.ndim) must_copy_array_in = fftw_plan_in is None and planner_destroys if must_copy_array_in and not array_in_copied: plan_arr_in = np.empty_like(array_in) flags = [_local_to_pyfftw(planning_effort), 'FFTW_DESTROY_INPUT'] else: plan_arr_in = array_in flags = [_local_to_pyfftw(planning_effort)] if fftw_plan_in is None: if threads is None: if plan_arr_in.size <= 4096: # Trade-off wrt threading overhead threads = 1 else: threads = cpu_count() fftw_plan = pyfftw.FFTW( plan_arr_in, array_out, direction=_local_to_pyfftw(direction), flags=flags, planning_timelimit=planning_timelimit, threads=threads, axes=axes) else: fftw_plan = fftw_plan_in fftw_plan(array_in, array_out, normalise_idft=normalise_idft) if wexport: try: with open(wexport, 'ab') as wfile: pickle.dump(pyfftw.export_wisdom(), wfile) except TypeError: # Got file handle pickle.dump(pyfftw.export_wisdom(), wexport) return fftw_plan
def _pyfftw_check_args(arr_in, arr_out, axes, halfcomplex, direction): """Raise an error if anything is not ok with in and out.""" if len(set(axes)) != len(axes): raise ValueError('duplicate axes are not allowed') if direction == 'forward': out_shape = list(arr_in.shape) if halfcomplex: try: out_shape[axes[-1]] = arr_in.shape[axes[-1]] // 2 + 1 except IndexError as err: raise_from(IndexError('axis index {} out of range for array ' 'with {} axes' ''.format(axes[-1], arr_in.ndim)), err) if arr_out.shape != tuple(out_shape): raise ValueError('expected output shape {}, got {}' ''.format(tuple(out_shape), arr_out.shape)) if is_real_dtype(arr_in.dtype): out_dtype = complex_dtype(arr_in.dtype) elif halfcomplex: raise ValueError('cannot combine halfcomplex forward transform ' 'with complex input') else: out_dtype = arr_in.dtype if arr_out.dtype != out_dtype: raise ValueError('expected output dtype {}, got {}' ''.format(dtype_repr(out_dtype), dtype_repr(arr_out.dtype))) elif direction == 'backward': in_shape = list(arr_out.shape) if halfcomplex: try: in_shape[axes[-1]] = arr_out.shape[axes[-1]] // 2 + 1 except IndexError as err: raise_from(IndexError('axis index {} out of range for array ' 'with {} axes' ''.format(axes[-1], arr_out.ndim)), err) if arr_in.shape != tuple(in_shape): raise ValueError('expected input shape {}, got {}' ''.format(tuple(in_shape), arr_in.shape)) if is_real_dtype(arr_out.dtype): in_dtype = complex_dtype(arr_out.dtype) elif halfcomplex: raise ValueError('cannot combine halfcomplex backward transform ' 'with complex output') else: in_dtype = arr_out.dtype if arr_in.dtype != in_dtype: raise ValueError('expected input dtype {}, got {}' ''.format(dtype_repr(in_dtype), dtype_repr(arr_in.dtype))) else: # Shouldn't happen raise RuntimeError
def reciprocal_space(space, axes=None, halfcomplex=False, shift=True, **kwargs): """Return the range of the Fourier transform on ``space``. Parameters ---------- space : `DiscreteLp` Real space whose reciprocal is calculated. It must be uniformly discretized. axes : sequence of ints, optional Dimensions along which the Fourier transform is taken. Default: all axes halfcomplex : bool, optional If ``True``, take only the negative frequency part along the last axis for. For ``False``, use the full frequency space. This option can only be used if ``space`` is a space of real-valued functions. shift : bool or sequence of bools, optional If ``True``, the reciprocal grid is shifted by half a stride in the negative direction. With a boolean sequence, this option is applied separately to each axis. If a sequence is provided, it must have the same length as ``axes`` if supplied. Note that this must be set to ``True`` in the halved axis in half-complex transforms. Default: ``True`` impl : string, optional Implementation back-end for the created space. Default: ``'numpy'`` exponent : float, optional Create a space with this exponent. By default, the conjugate exponent ``q = p / (p - 1)`` of the exponent of ``space`` is used, where ``q = inf`` for ``p = 1`` and vice versa. dtype : optional Complex data type of the created space. By default, the complex counterpart of ``space.dtype`` is used. Returns ------- rspace : `DiscreteLp` Reciprocal of the input ``space``. If ``halfcomplex=True``, the upper end of the domain (where the half space ends) is chosen to coincide with the grid node. """ if not isinstance(space, DiscreteLp): raise TypeError('`space` {!r} is not a `DiscreteLp` instance' ''.format(space)) if axes is None: axes = tuple(range(space.ndim)) axes = normalized_axes_tuple(axes, space.ndim) if not all(space.is_uniform_byaxis[axis] for axis in axes): raise ValueError('`space` is not uniformly discretized in the ' '`axes` of the transform') if halfcomplex and space.field != RealNumbers(): raise ValueError('`halfcomplex` option can only be used with real ' 'spaces') exponent = kwargs.pop('exponent', None) if exponent is None: exponent = conj_exponent(space.exponent) dtype = kwargs.pop('dtype', None) if dtype is None: dtype = complex_dtype(space.dtype) else: if not is_complex_floating_dtype(dtype): raise ValueError('{} is not a complex data type' ''.format(dtype_repr(dtype))) impl = kwargs.pop('impl', 'numpy') # Calculate range recip_grid = reciprocal_grid(space.grid, shift=shift, halfcomplex=halfcomplex, axes=axes) # Make a partition with nodes on the boundary in the last transform axis # if `halfcomplex == True`, otherwise a standard partition. if halfcomplex: max_pt = {axes[-1]: recip_grid.max_pt[axes[-1]]} part = uniform_partition_fromgrid(recip_grid, max_pt=max_pt) else: part = uniform_partition_fromgrid(recip_grid) # Use convention of adding a hat to represent fourier transform of variable axis_labels = list(space.axis_labels) for i in axes: # Avoid double math label = axis_labels[i].replace('$', '') axis_labels[i] = '$\^{{{}}}$'.format(label) recip_spc = uniform_discr_frompartition(part, exponent=exponent, dtype=dtype, impl=impl, axis_labels=axis_labels) return recip_spc
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 try: # Duck-typed string check interp + '' except TypeError: pass else: 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 dft_preprocess_data(arr, shift=True, axes=None, sign='-', out=None): """Pre-process the real-space data before DFT. This function multiplies the given data with the separable function:: p(x) = exp(+- 1j * dot(x - x[0], xi[0])) where ``x[0]`` and ``xi[0]`` are the minimum coodinates of the real-space and reciprocal grids, respectively. The sign of the exponent depends on the choice of ``sign``. In discretized form, this function becomes an array:: p[k] = exp(+- 1j * k * s * xi[0]) If the reciprocal grid is not shifted, i.e. symmetric around 0, it is ``xi[0] = pi/s * (-1 + 1/N)``, hence:: p[k] = exp(-+ 1j * pi * k * (1 - 1/N)) For a shifted grid, we have :math:``xi[0] = -pi/s``, thus the array is given by:: p[k] = (-1)**k Parameters ---------- arr : `array-like` Array to be pre-processed. If its data type is a real non-floating type, it is converted to 'float64'. shift : bool or or sequence of bools, optional If ``True``, the grid is shifted by half a stride in the negative direction. With a sequence, this option is applied separately on each axis. axes : int or sequence of ints, optional Dimensions in which to calculate the reciprocal. The sequence must have the same length as ``shift`` if the latter is given as a sequence. Default: all axes. sign : {'-', '+'}, optional Sign of the complex exponent. out : `numpy.ndarray`, optional Array in which the result is stored. If ``out is arr``, an in-place modification is performed. For real data type, this is only possible for ``shift=True`` since the factors are complex otherwise. Returns ------- out : `numpy.ndarray` Result of the pre-processing. If ``out`` was given, the returned object is a reference to it. Notes ----- If ``out`` is not specified, the data type of the returned array is the same as that of ``arr`` except when ``arr`` has real data type and ``shift`` is not ``True``. In this case, the return type is the complex counterpart of ``arr.dtype``. """ arr = np.asarray(arr) if not is_scalar_dtype(arr.dtype): raise ValueError('array has non-scalar data type {}' ''.format(dtype_repr(arr.dtype))) elif is_real_dtype(arr.dtype) and not is_real_floating_dtype(arr.dtype): arr = arr.astype('float64') if axes is None: axes = list(range(arr.ndim)) else: try: axes = [int(axes)] except TypeError: axes = list(axes) shape = arr.shape shift_list = normalized_scalar_param_list(shift, length=len(axes), param_conv=bool) # Make a copy of arr with correct data type if necessary, or copy values. if out is None: if is_real_dtype(arr.dtype) and not all(shift_list): out = np.array(arr, dtype=complex_dtype(arr.dtype), copy=True) else: out = arr.copy() else: out[:] = arr if is_real_dtype(out.dtype) and not shift: raise ValueError('cannot pre-process real input in-place without ' 'shift') if sign == '-': imag = -1j elif sign == '+': imag = 1j else: raise ValueError("`sign` '{}' not understood".format(sign)) def _onedim_arr(length, shift): if shift: # (-1)^indices factor = np.ones(length, dtype=out.dtype) factor[1::2] = -1 else: factor = np.arange(length, dtype=out.dtype) factor *= -imag * np.pi * (1 - 1.0 / length) np.exp(factor, out=factor) return factor.astype(out.dtype, copy=False) onedim_arrs = [] for axis, shift in zip(axes, shift_list): length = shape[axis] onedim_arrs.append(_onedim_arr(length, shift)) fast_1d_tensor_mult(out, onedim_arrs, axes=axes, out=out) return out
def reciprocal_space(space, axes=None, halfcomplex=False, shift=True, **kwargs): """Return the range of the Fourier transform on ``space``. Parameters ---------- space : `DiscreteLp` Real space whose reciprocal is calculated. It must be uniformly discretized. axes : sequence of ints, optional Dimensions along which the Fourier transform is taken. Default: all axes halfcomplex : bool, optional If ``True``, take only the negative frequency part along the last axis for. For ``False``, use the full frequency space. This option can only be used if ``space`` is a space of real-valued functions. shift : bool or sequence of bools, optional If ``True``, the reciprocal grid is shifted by half a stride in the negative direction. With a boolean sequence, this option is applied separately to each axis. If a sequence is provided, it must have the same length as ``axes`` if supplied. Note that this must be set to ``True`` in the halved axis in half-complex transforms. Default: ``True`` impl : string, optional Implementation back-end for the created space. Default: ``'numpy'`` exponent : float, optional Create a space with this exponent. By default, the conjugate exponent ``q = p / (p - 1)`` of the exponent of ``space`` is used, where ``q = inf`` for ``p = 1`` and vice versa. dtype : optional Complex data type of the created space. By default, the complex counterpart of ``space.dtype`` is used. Returns ------- rspace : `DiscreteLp` Reciprocal of the input ``space``. If ``halfcomplex=True``, the upper end of the domain (where the half space ends) is chosen to coincide with the grid node. """ if not isinstance(space, DiscreteLp): raise TypeError('`space` {!r} is not a `DiscreteLp` instance' ''.format(space)) if not space.is_uniform: raise ValueError('`space` is not uniformly discretized') if axes is None: axes = tuple(range(space.ndim)) axes = normalized_axes_tuple(axes, space.ndim) if halfcomplex and space.field != RealNumbers(): raise ValueError('`halfcomplex` option can only be used with real ' 'spaces') exponent = kwargs.pop('exponent', None) if exponent is None: exponent = conj_exponent(space.exponent) dtype = kwargs.pop('dtype', None) if dtype is None: dtype = complex_dtype(space.dtype) else: if not is_complex_floating_dtype(dtype): raise ValueError('{} is not a complex data type' ''.format(dtype_repr(dtype))) impl = kwargs.pop('impl', 'numpy') # Calculate range recip_grid = reciprocal_grid(space.grid, shift=shift, halfcomplex=halfcomplex, axes=axes) # Make a partition with nodes on the boundary in the last transform axis # if `halfcomplex == True`, otherwise a standard partition. if halfcomplex: max_pt = {axes[-1]: recip_grid.max_pt[axes[-1]]} part = uniform_partition_fromgrid(recip_grid, max_pt=max_pt) else: part = uniform_partition_fromgrid(recip_grid) # Use convention of adding a hat to represent fourier transform of variable axis_labels = list(space.axis_labels) for i in axes: # Avoid double math label = axis_labels[i].replace('$', '') axis_labels[i] = '$\^{{{}}}$'.format(label) recip_spc = uniform_discr_frompartition(part, exponent=exponent, dtype=dtype, impl=impl, axis_labels=axis_labels) return recip_spc
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 : `RegularGrid` Real space grid in the transform recip_grid : `RegularGrid` 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'} 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. """ 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 try: # Duck-typed string check interp + '' except TypeError: pass else: 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] if op == 'multiply': onedim_arr *= stride * _interp_kernel_ft(freqs, intp) else: onedim_arr /= stride * _interp_kernel_ft(freqs, intp) onedim_arrs.append(onedim_arr.astype(out.dtype, copy=False)) fast_1d_tensor_mult(out, onedim_arrs, axes=axes, out=out) return out