Fast Fourier Transform ###################### *µ*\Grid includes a built-in Fast Fourier Transform (FFT) engine that operates on *µ*\Grid fields. The FFT engine uses `pocketfft `_ for efficient FFT computation. The FFTEngine class ******************* Instantiating an FFT engine is straightforward: .. code-block:: python import muGrid # Create a 2D FFT engine engine = muGrid.FFTEngine([nx, ny]) # Create a 3D FFT engine engine = muGrid.FFTEngine([nx, ny, nz]) The FFT engine provides properties for querying the grid dimensions: .. code-block:: python engine = muGrid.FFTEngine([16, 20]) print(f"Real-space grid: {engine.nb_domain_grid_pts}") # (16, 20) print(f"Fourier-space grid: {engine.nb_fourier_grid_pts}") # (9, 20) print(f"Backend: {engine.backend_name}") # "pocketfft" The Fourier-space grid has reduced dimensions due to the real-to-complex (r2c) transform exploiting Hermitian symmetry. Creating FFT fields ******************* The FFT engine manages two field collections: one for real-space fields and one for Fourier-space fields. Use the engine's methods to create fields: .. code-block:: python import muGrid engine = muGrid.FFTEngine([16, 20]) # Create real-space field (real-valued) real_field = engine.real_space_field("real") print(f"Real field shape: {real_field.s.shape}") # (1, 1, 16, 20) # Create Fourier-space field (complex-valued) fourier_field = engine.fourier_space_field("fourier") print(f"Fourier field shape: {fourier_field.s.shape}") # (1, 1, 9, 20) Forward and inverse transforms ****************************** The ``fft`` method performs the forward transform (real to Fourier space) and ``ifft`` performs the inverse transform (Fourier to real space): .. literalinclude:: ../../examples/fft_roundtrip.py :language: python Normalization ************* *µ*\Grid FFT transforms are **not normalized** by default. A forward-inverse roundtrip picks up a global factor equal to the total number of grid points. This factor is available as the ``normalisation`` property: .. code-block:: python engine = muGrid.FFTEngine([8, 10]) print(f"Normalization factor: {engine.normalisation}") # 1/80 = 0.0125 To get the original values after a roundtrip, multiply by the normalization factor after the inverse transform. The reason for leaving normalization to the user is that there are multiple valid conventions (normalize forward, normalize inverse, or split between both), and different applications may prefer different choices. Frequency and coordinate arrays ******************************* The FFT engine provides properties for accessing frequency and coordinate arrays that are correctly shaped for the local subdomain. This is particularly useful for MPI-parallel computations where each rank only handles a portion of the grid. Frequency arrays ================ The ``fftfreq`` and ``ifftfreq`` properties provide FFT frequency arrays: .. code-block:: python import muGrid import numpy as np engine = muGrid.FFTEngine([7, 4]) # Normalized frequencies (range [-0.5, 0.5)) qx, qy = engine.fftfreq print(f"fftfreq shape: {engine.fftfreq.shape}") # (2, 4, 4) for r2c transform # Integer frequency indices iqx, iqy = engine.ifftfreq print(f"ifftfreq dtype: {iqx.dtype}") # integer type # These match numpy's frequency arrays (sliced for r2c transform) freq_ref = np.array(np.meshgrid( *(np.fft.fftfreq(n) for n in [7, 4]), indexing="ij" )) freq_ref = freq_ref[:, :4, :] # Slice for half-complex np.testing.assert_allclose(engine.fftfreq, freq_ref) The frequency arrays have shape ``[dim, local_fx, local_fy, ...]`` where the first axis indexes the spatial dimension and the remaining axes match the local Fourier subdomain dimensions. Coordinate arrays ================= The ``coords`` and ``icoords`` properties provide real-space coordinate arrays: .. code-block:: python import muGrid engine = muGrid.FFTEngine([7, 4]) # Normalized coordinates (range [0, 1)) x, y = engine.coords print(f"coords shape: {engine.coords.shape}") # (2, 7, 4) # Integer coordinate indices ix, iy = engine.icoords print(f"icoords dtype: {ix.dtype}") # integer type # Verify: integer coords = fractional * n np.testing.assert_allclose(ix, x * 7) np.testing.assert_allclose(iy, y * 4) For fields with ghost regions, use ``coordsg`` and ``icoordsg`` to get coordinates including the ghost cells: .. code-block:: python engine = muGrid.FFTEngine( [64, 64], nb_ghosts_left=[1, 1], nb_ghosts_right=[1, 1] ) # Coordinates without ghosts x, y = engine.coords # Coordinates with ghosts (larger array) xg, yg = engine.coordsg print(f"Without ghosts: {engine.coords.shape}") # (2, local_nx, local_ny) print(f"With ghosts: {engine.coordsg.shape}") # (2, local_nx+2, local_ny+2) MPI-parallel frequency arrays ============================= In MPI-parallel runs, frequency and coordinate arrays return only the data for the local subdomain owned by each rank: .. code-block:: python from mpi4py import MPI import muGrid comm = muGrid.Communicator(MPI.COMM_WORLD) engine = muGrid.FFTEngine([128, 128], comm) # Each rank gets frequencies for its local Fourier subdomain qx, qy = engine.fftfreq print(f"Rank {MPI.COMM_WORLD.rank}: fftfreq shape = {engine.fftfreq.shape}") # Coordinates for local real-space subdomain x, y = engine.coords print(f"Rank {MPI.COMM_WORLD.rank}: coords shape = {engine.coords.shape}") Example: Fourier-space operations ================================= A common pattern is using frequency arrays to construct Fourier-space operators: .. code-block:: python import numpy as np import muGrid engine = muGrid.FFTEngine([64, 64]) qx, qy = engine.fftfreq real_field = engine.real_space_field("u") fourier_field = engine.fourier_space_field("u_hat") # Initialize with a cosine wave x, y = engine.coords real_field.p[0] = np.cos(2 * np.pi * x) # Forward FFT engine.fft(real_field, fourier_field) # Compute Laplacian in Fourier space: -4π²(qx² + qy²) * u_hat laplacian_hat = -4 * np.pi**2 * (qx**2 + qy**2) * fourier_field.p[0] # Or use integer frequencies for identifying specific modes iqx, iqy = engine.ifftfreq # Find the (1, 0) mode mode_mask = (np.abs(iqx) == 1) & (iqy == 0) Hermitian grid dimensions ************************* For real-to-complex transforms, the output has reduced dimensions due to Hermitian symmetry. Use ``get_hermitian_grid_pts`` to compute the Fourier-space grid size: .. code-block:: python import muGrid real_grid = [64, 64, 64] fourier_grid = muGrid.get_hermitian_grid_pts(real_grid) print(f"Fourier grid: {fourier_grid}") # [33, 64, 64] The first dimension is reduced to ``n//2 + 1``. FFT normalization factor ************************ The ``fft_normalization`` function returns the normalization factor for a given grid size: .. code-block:: python import muGrid norm = muGrid.fft_normalization([16, 20]) print(f"Normalization: {norm}") # 1/320 Example: Fourier derivative *************************** A common use of FFTs is computing derivatives in Fourier space. Here's an example computing the gradient of a 2D field: .. literalinclude:: ../../examples/fourier_derivative.py :language: python Multi-component fields ********************** FFT fields can have multiple components: .. code-block:: python import muGrid engine = muGrid.FFTEngine([16, 20]) # Create field with 3 components (e.g., velocity vector) velocity = engine.real_space_field("velocity", nb_components=3) velocity_hat = engine.fourier_space_field("velocity_hat", nb_components=3) print(f"Velocity shape: {velocity.s.shape}") # (3, 1, 16, 20) print(f"Velocity_hat shape: {velocity_hat.s.shape}") # (3, 1, 9, 20) # FFT transforms all components - fields are passed directly engine.fft(velocity, velocity_hat) engine.ifft(velocity_hat, velocity) MPI-parallel FFT **************** The ``FFTEngine`` class supports MPI parallelization using pencil (2D) decomposition, which allows efficient scaling to large numbers of ranks. Basic parallel usage ==================== .. code-block:: python import numpy as np from mpi4py import MPI import muGrid from muGrid import Communicator # Create parallel FFT engine nb_grid_pts = [128, 128, 128] comm = Communicator(MPI.COMM_WORLD) engine = muGrid.FFTEngine(nb_grid_pts, comm) # Each rank has a subdomain print(f"Rank {MPI.COMM_WORLD.rank}:") print(f" Global grid: {engine.nb_domain_grid_pts}") print(f" Local subdomain: {engine.nb_subdomain_grid_pts}") print(f" Process grid: {engine.process_grid}") print(f" Process coords: {engine.process_coords}") The FFT engine uses pencil decomposition, distributing the grid across a 2D process grid for efficient scaling. FFT with ghost regions ====================== For computations requiring ghost cells (e.g., stencil operations), specify the ghost buffer sizes: .. code-block:: python import muGrid from muGrid import Communicator from mpi4py import MPI comm = Communicator(MPI.COMM_WORLD) # Create FFT engine with ghost regions engine = muGrid.FFTEngine( nb_domain_grid_pts=[64, 64, 64], comm=comm, nb_ghosts_left=[1, 1, 1], nb_ghosts_right=[1, 1, 1] ) # Real-space fields include ghost regions real_field = engine.real_space_field("displacement", nb_components=3) # Fourier-space fields have no ghosts (hard assumption) fourier_field = engine.fourier_space_field("displacement_k", nb_components=3) Run with MPI: .. code-block:: sh $ mpirun -np 4 python parallel_fft.py GPU support *********** When *µ*\Grid is compiled with CUDA or HIP support, FFT operations can run on GPU. See the :doc:`GPU` documentation for details on building with GPU support and working with GPU fields. Currently, the FFT engine operates on host (CPU) memory. For GPU-accelerated FFT, you can transfer data between host and device fields as needed.