Quickstart#

This walks through generating a Gaussian random field with a prescribed power spectrum, then recovering its power spectrum from the realization to check that the two match.

Define a power spectrum#

from kspace import PowerLawBetaModel

# A power-law power spectrum with large- and small-scale cutoffs:
#   l_min, l_max set the cutoff scales; alpha is the power-law index.
power_spec = PowerLawBetaModel(l_min=10.0, l_max=200.0, alpha=-11.0 / 3.0)

# Normalize so the field has a given RMS amplitude
power_spec.renormalize(f_rms=10.0)

l_min/l_max and the grid coordinates below are in whatever length unit you choose (kpc is a common choice for the astrophysical use case this package targets, but nothing enforces that).

Generate a field realization#

import numpy as np
from kspace import GaussianRandomField

left_edge = np.array([0.0, 0.0, 0.0])
right_edge = np.array([750.0, 750.0, 750.0])
ddims = [256, 256, 256]

grf = GaussianRandomField(left_edge, right_edge, ddims, power_spec, seed=10)
field = grf.generate_scalar_field_realization()

field is a real-valued NumPy array of shape (256, 256, 256).

Recover and check the power spectrum#

from kspace import FourierAnalysis

fa = FourierAnalysis(right_edge - left_edge, ddims)
kbins, pk = fa.make_binned_powerspec(field, nbins=60)

# Compare to the input spectrum at the bin centers
k_centers = np.sqrt(kbins[1:] * kbins[:-1])
measured = pk
expected = power_spec(k_centers)

Plotting measured and expected against k_centers on a log-log axis should show good agreement away from the largest and smallest scales (where the finite grid and box size limit the achievable dynamic range).

Forward and inverse FFTs#

fa above already wraps the FFT machinery used internally by make_binned_powerspec; you can also call it directly. fftn/ifftn aren’t drop-in replacements for scipy.fft’s functions of the same name — they use the \(k = 2\pi f\) convention and normalize by the cell volume dV (see Conventions), and ifftn requires its input to be the FFTArray that fftn returns, not a plain array:

# Forward transform: a real-space field -> an FFTArray in Fourier space
field_hat = fa.fftn(field)

# Inverse transform: exact inverse of fftn, back to real space
field_recovered = fa.ifftn(field_hat)

np.allclose(field, field_recovered)  # True

fftn also accepts vector fields, of shape (ndim, *ddims), transforming each component along the spatial axes.

Vector fields#

# A 3-component vector field, e.g. a synthetic turbulent velocity field
vfield = grf.generate_vector_field_realization()

# Or a divergence-free ("solenoidal") one
vfield_solenoidal = grf.generate_vector_field_realization(divergence_free=True)

Divergence and curl#

FourierAnalysis.divergence_of_field/curl_of_field compute these in real space via finite differences. Since vfield came from an FFT-based realization, its domain is periodic, so pass periodic=True to use wraparound (rather than one-sided) differences at the box edges:

div_v = fa.divergence_of_field(vfield, periodic=True)
curl_v = fa.curl_of_field(vfield, periodic=True)

# vfield_solenoidal was constructed to be divergence-free, so this should
# be close to zero everywhere (up to finite-difference truncation error)
div_v_solenoidal = fa.divergence_of_field(vfield_solenoidal, periodic=True)

There are also spectral counterparts, divergence_component/ solenoidal_component, which project a vector field into its compressional and solenoidal parts in Fourier space rather than differentiating in real space — see Conventions for how they relate to divergence_of_field/ curl_of_field and the choice of wavenumber (diff_type) they use.

See Conventions for the Fourier conventions this package uses, and Tutorials for complete, runnable examples.