This guide documents the Python interface of structure-factor 1.0.0. It is
intended both as an API reference and as an explanation of the numerical
choices behind the returned arrays.
The library analyzes particle coordinates in a periodic, orthorhombic, two-dimensional box. It provides:
- a direct microscopic particle structure factor;
- FFT spectra of occupancy and number-density grids;
- an FFT spectrum of a thresholded binary phase field;
- FFT spectra of fields smoothed by Gaussian, square top-hat, or disk kernels;
- circular radial averages in physical wave-number coordinates;
- publication-style Matplotlib figures.
The implementation uses float64 real arrays and complex128 Fourier work
arrays. It does not perform file output, plotting, or display as a side effect
of the numerical calculation.
The package root exports the five symbols needed by most programs:
from structure_factor import (
StructureFactorConfig,
StructureFactorResult,
compute_structure_factor,
load_positions,
occupancy_grid,
)These names are also available from their defining modules:
from structure_factor.core import (
StructureFactorConfig,
StructureFactorResult,
compute_structure_factor,
occupancy_grid,
)
from structure_factor.io import load_positionsTwo additional helpers are intentionally imported from their modules:
from structure_factor.core import radial_average
from structure_factor.plotting import create_figuresradial_average is useful when rebinning a computed spectrum or averaging an
independently produced two-dimensional spectrum. create_figures is kept in
the plotting module so numerical users can import the package root without
importing Matplotlib.
The installed version is available as:
import structure_factor
print(structure_factor.__version__)Names beginning with an underscore are implementation details and should not be used as application APIs.
The following example loads a coordinate file, computes the microscopic particle structure factor, and inspects the radial result:
import numpy as np
from structure_factor import (
StructureFactorConfig,
compute_structure_factor,
load_positions,
)
positions = load_positions("positions.dat")
config = StructureFactorConfig(
box_size=(128.0, 128.0),
method="particle",
max_mode=16,
)
result = compute_structure_factor(positions, config)
print(f"N = {result.particle_count}")
print(f"2D spectrum shape = {result.spectrum.shape}")
assert result.radial_wave_numbers is not None
assert result.radial_spectrum is not None
peak = int(np.argmax(result.radial_spectrum))
print(f"strongest radial bin: k = {result.radial_wave_numbers[peak]:.6g}")The assert statements are useful to type checkers because radial attributes
are optional. They are present by default, but are None if
compute_radial_average=False was requested.
Understanding these conventions prevents the most common interpretation errors.
Coordinates are supplied as an array with shape (N, 2):
positions[:, 0] -> x coordinates
positions[:, 1] -> y coordinates
For box_size=(Lx, Ly), every point must already satisfy:
The upper boundary is excluded because it is periodically identical to zero. The library validates these bounds but deliberately does not wrap coordinates. If an upstream simulation can emit unwrapped positions, wrap them explicitly:
positions = positions % np.asarray((Lx, Ly))Do this only when periodic wrapping is scientifically appropriate for the input data.
Every numerical two-dimensional array is stored with x first:
value = result.spectrum[ix, iy]
k_vector = (result.kx[ix], result.ky[iy])Likewise, a real-space field uses:
value = result.source_field[ix, iy]This is different from Matplotlib's image convention, in which the first
array index is the vertical row. The package plotting functions transpose an
array exactly once before displaying it. If making a custom image, normally
use field.T or spectrum.T.
The library does not prescribe a particular length unit. It only requires consistent inputs.
| Quantity | Unit |
|---|---|
positions, box_size, grid_size |
caller's length unit |
particle_diameter, kernel_length |
caller's length unit |
kx, ky, radial_wave_numbers |
inverse length |
| occupancy source field | particles per cell |
| density source field | particles per length squared |
| kernel source field | smoothed particle mass per cell |
| binary source field | dimensionless, exactly -1 or +1 |
| computed spectra | dimensionless |
| radial mode counts | number of Fourier modes |
Wave numbers are angular wave numbers: reciprocal-lattice spacings contain
2*pi, not just 1/L.
Every estimator returns a connected spectrum. Grid methods subtract the
spatial mean before the FFT, and the direct method discards the particle
amplitude at
The unconnected microscopic definition would instead have
load_positions(path: str | pathlib.Path) -> numpy.ndarray
load_positions reads a whitespace-delimited numeric text file with
numpy.loadtxt. Each nonempty row must contain exactly two values:
0.25 1.50
2.75 3.10
4.00 0.20
The returned array:
- has shape
(N, 2), including the single-particle case; - has
float64dtype; - is C-contiguous;
- contains at least one row;
- contains only finite values.
The loader does not know the box dimensions, so it does not check periodic
bounds. Bound checking occurs in occupancy_grid or
compute_structure_factor.
Example:
from pathlib import Path
from structure_factor import load_positions
positions = load_positions(Path("examples") / "xy.dat")
print(positions.shape)Errors:
OSErroris raised when the file cannot be opened or read;ValueErroris raised for an empty file, nonnumeric content, a column count other than two, or NaN/infinite coordinates.
StructureFactorConfig(
box_size,
method="particle",
grid_size=2.0,
density_threshold=0.7,
particle_diameter=1.0,
max_mode=8,
kernel="gaussian",
kernel_length=None,
direct_chunk_size=4096,
)The configuration is a frozen dataclass. Construction validates and normalizes every field, including fields unused by the selected method. This gives one consistent configuration contract and catches invalid values early.
| Field | Default | Meaning |
|---|---|---|
box_size |
required | Positive periodic lengths (Lx, Ly) |
method |
"particle" |
"particle", "occupancy", "density", "binary", or "kernel" |
grid_size |
2.0 |
Requested maximum grid-cell spacing |
density_threshold |
0.7 |
Binary local packing-fraction threshold |
particle_diameter |
1.0 |
Diameter used to calculate local packing fraction |
max_mode |
8 |
Direct reciprocal cutoff, scalar or (mx, my) |
kernel |
"gaussian" |
"gaussian", "top-hat", or "disk" |
kernel_length |
None |
Gaussian standard deviation, square half-width, or disk radius |
direct_chunk_size |
4096 |
Number of particles in each direct-sum work chunk |
All lengths must be finite and positive. density_threshold may be zero but
not negative. Mode cutoffs and direct_chunk_size must be positive integers;
booleans are rejected rather than being interpreted as zero or one.
If kernel_length=None, the constructor resolves it to the current
grid_size and stores that positive float:
config = StructureFactorConfig((10.0, 10.0), grid_size=1.25)
assert config.kernel_length == 1.25This is a construction-time default. The configuration is immutable after it has been created.
grid_size is a target maximum spacing, not necessarily the exact cell
width. For direction
where grid_size.
The configuration exposes both derived quantities:
config = StructureFactorConfig(
box_size=(5.0, 7.0),
method="occupancy",
grid_size=2.0,
)
assert config.grid_shape == (3, 4)
assert config.effective_cell_size == (5.0 / 3.0, 7.0 / 4.0)config.grid_shapereturns(nx, ny);config.effective_cell_sizereturns(hx, hy)in length units;config.mode_cutoffsreturns an explicit(mx, my)pair.
For example:
isotropic = StructureFactorConfig((8.0, 8.0), max_mode=12)
anisotropic = StructureFactorConfig((8.0, 12.0), max_mode=(12, 18))
assert isotropic.mode_cutoffs == (12, 12)
assert anisotropic.mode_cutoffs == (12, 18)The grid properties are available even for the particle method, although that method does not use a real-space grid.
occupancy_grid(
positions,
config: StructureFactorConfig,
) -> numpy.ndarray
This helper assigns each particle to a regular cell:
It returns an x-first float64 array with shape config.grid_shape. Values
are numerically integer counts and their sum equals N.
import numpy as np
from structure_factor import StructureFactorConfig, occupancy_grid
positions = np.array(
[
[0.1, 0.1],
[0.8, 0.3],
[2.9, 1.9],
]
)
config = StructureFactorConfig(
box_size=(3.0, 2.0),
method="occupancy",
grid_size=1.0,
)
counts = occupancy_grid(positions, config)
assert counts.shape == (3, 2)
assert counts.sum() == positions.shape[0]The configured method does not change this helper; only box and grid
geometry matter. Assignment is cell membership without interpolation.
Cloud-in-cell, triangular-shaped-cloud, and assignment-window corrections are
not implemented in this release.
occupancy_grid accepts a single coordinate pair with shape (2,) and
promotes it to (1, 2). It rejects empty, malformed, nonfinite, and
out-of-box coordinates.
compute_structure_factor(
positions,
config: StructureFactorConfig,
*,
compute_radial_average: bool = True,
) -> StructureFactorResult
positions may be any numeric array-like object with shape (N, 2). A single
pair with shape (2,) is accepted. The function validates the coordinates,
dispatches to the configured estimator, and optionally computes a radial
average.
Every method produces a two-dimensional spectrum with matching kx and
ky axes. Grid methods additionally retain both the raw occupancy and the
real-space source_field that was Fourier transformed.
| Method | Fourier amplitude | Power normalization |
|---|---|---|
particle |
|
|
occupancy |
FFT of |
|
density |
cell area times FFT of |
|
binary |
FFT of |
|
kernel |
FFT of centered, smoothed cell mass |
The different normalizations are part of the scientific definitions. In particular, the binary spectrum is an order-parameter spectrum and its absolute amplitude should not be compared directly to a particle-normalized spectrum.
config = StructureFactorConfig(
box_size=(64.0, 48.0),
method="particle",
max_mode=(16, 12),
direct_chunk_size=4096,
)
result = compute_structure_factor(positions, config)The sampled reciprocal vectors are:
\left( \frac{2\pi m}{L_x}, \frac{2\pi n}{L_y} \right), $$
with
\frac{1}{N} \left| \sum_{j=1}^{N} e^{-i\mathbf{k}\cdot\mathbf{r}_j} \right|^2. $$
The spectrum has shape (2*mx + 1, 2*my + 1). It is computed directly from
the coordinates without grid quantization. direct_chunk_size changes
temporary phase-matrix memory, but not modes, normalization, or results apart
from possible floating-point summation-order differences.
config = StructureFactorConfig(
box_size=(64.0, 48.0),
method="occupancy",
grid_size=1.0,
)
result = compute_structure_factor(positions, config)Particles are binned into counts N:
\frac{ \left|\operatorname{FFT}(n-\langle n\rangle)\right|^2 }{N}. $$
The returned source_field and occupancy both contain cell counts.
config = StructureFactorConfig(
box_size=(64.0, 48.0),
method="density",
grid_size=1.0,
)
result = compute_structure_factor(positions, config)The real-space source field is:
The density Fourier amplitude is multiplied by the cell area before its power
is divided by N. Therefore:
\widehat{\delta n}, $$
and density and occupancy return the same spectrum up to floating-point
round-off. They retain different source_field arrays: one is number density
and the other is particles per cell.
occupancy_result = compute_structure_factor(
positions,
StructureFactorConfig((64.0, 48.0), method="occupancy", grid_size=1.0),
)
density_result = compute_structure_factor(
positions,
StructureFactorConfig((64.0, 48.0), method="density", grid_size=1.0),
)
np.testing.assert_allclose(
density_result.spectrum,
occupancy_result.spectrum,
rtol=1e-12,
atol=1e-12,
)config = StructureFactorConfig(
box_size=(64.0, 48.0),
method="binary",
grid_size=2.0,
particle_diameter=1.0,
density_threshold=0.7,
)
result = compute_structure_factor(positions, config)For particle diameter
\frac{n_{ij}\pi\sigma^2}{4h_xh_y}. $$
It is mapped to:
\begin{cases} -1, & \phi_{ij}<\phi_c,\ +1, & \phi_{ij}\geq\phi_c. \end{cases} $$
A cell exactly equal to the threshold is dense (+1). The connected phase
field is transformed and squared amplitudes are divided by the total number
of cells. This estimator is useful for domain morphology and coarsening. It is
not the microscopic particle structure factor.
config = StructureFactorConfig(
box_size=(64.0, 48.0),
method="kernel",
grid_size=1.0,
kernel="gaussian",
kernel_length=2.0,
)
result = compute_structure_factor(positions, config)The method samples a nonnegative kernel on the periodic grid, normalizes its discrete weights to sum to one, and circularly convolves it with occupancy. Consequently, the smoothed source field conserves particle mass:
np.testing.assert_allclose(
result.source_field.sum(),
positions.shape[0],
)kernel_length has a shape-dependent meaning:
- Gaussian: standard deviation
$\ell$ in$K(r)\propto\exp[-r^2/(2\ell^2)]$ ; - square top-hat: half-width
$\ell$ , with support$|x|\leq\ell$ and$|y|\leq\ell$ ; - disk: radius
$\ell$ , with support$r\leq\ell$ .
The output is the filtered occupancy spectrum:
|W_{\mathrm{grid}}(\mathbf{k})|^2 S_{\mathrm{occupancy}}(\mathbf{k}). $$
No kernel deconvolution is applied. In particular, correction would be unstable around transform zeros of top-hat and disk windows and near the Nyquist limit.
Each calculation receives a complete configuration. This makes method comparisons explicit and reproducible:
from structure_factor import (
StructureFactorConfig,
compute_structure_factor,
load_positions,
)
box = (1284.0, 1284.0)
grid_size = 2.0
positions = load_positions("examples/xy.dat")
configs = {
"particle": StructureFactorConfig(
box,
method="particle",
max_mode=8,
),
"occupancy": StructureFactorConfig(
box,
method="occupancy",
grid_size=grid_size,
),
"density": StructureFactorConfig(
box,
method="density",
grid_size=grid_size,
),
"binary": StructureFactorConfig(
box,
method="binary",
grid_size=grid_size,
density_threshold=0.7,
particle_diameter=1.0,
),
"gaussian": StructureFactorConfig(
box,
method="kernel",
grid_size=grid_size,
kernel="gaussian",
kernel_length=2.0,
),
"top-hat": StructureFactorConfig(
box,
method="kernel",
grid_size=grid_size,
kernel="top-hat",
kernel_length=2.0,
),
"disk": StructureFactorConfig(
box,
method="kernel",
grid_size=grid_size,
kernel="disk",
kernel_length=2.0,
),
}
results = {
name: compute_structure_factor(positions, config)
for name, config in configs.items()
}The complete executable comparison is available in
examples/xy_all_methods.ipynb.
compute_structure_factor returns a frozen StructureFactorResult dataclass.
Freezing prevents attribute rebinding, but it does not make the contained
NumPy arrays read-only. Treat result arrays as calculation outputs; copy an
array before intentionally modifying it.
Its complete constructor is:
StructureFactorResult(
config,
particle_count,
spectrum,
kx,
ky,
radial_wave_numbers=None,
radial_spectrum=None,
radial_mode_counts=None,
source_field=None,
occupancy=None,
)
Applications normally receive this object from compute_structure_factor
rather than constructing it directly. The dataclass constructor does not
revalidate array shapes, axis alignment, or method-dependent fields.
| Attribute | Shape | Meaning and units |
|---|---|---|
config |
scalar object | Validated configuration used by the calculation |
particle_count |
integer | Number of input particles, N |
spectrum |
(len(kx), len(ky)) |
Connected dimensionless 2D spectrum |
kx |
(len(kx),) |
First-dimension angular wave numbers, inverse length |
ky |
(len(ky),) |
Second-dimension angular wave numbers, inverse length |
radial_wave_numbers |
(B,) or None |
Mean actual radius in each radial bin, inverse length |
radial_spectrum |
(B,) or None |
Mean spectrum in each radial bin |
radial_mode_counts |
(B,) or None |
Number of 2D modes in each radial bin |
source_field |
grid shape or None |
Real-space field transformed by a grid estimator |
occupancy |
grid shape or None |
Raw particles-per-cell grid |
Method-dependent shapes are:
| Method | Spectrum shape | source_field |
occupancy |
|---|---|---|---|
particle |
(2*mx + 1, 2*my + 1) |
None |
None |
| any grid method | config.grid_shape |
present | present |
For a grid method, shifted FFT axes are constructed as:
kx = 2 * np.pi * np.fft.fftshift(np.fft.fftfreq(nx, d=hx))
ky = 2 * np.pi * np.fft.fftshift(np.fft.fftfreq(ny, d=hy))Thus spectrum[i, j] is always aligned with (kx[i], ky[j]). On an even FFT
grid there is one negative Nyquist mode and no separate positive partner. The
direct particle grid is instead explicitly symmetric because it contains
integer modes from -max_mode to +max_mode, inclusive.
The DC location can be found robustly from the axes:
dc = (
int(np.argmin(np.abs(result.kx))),
int(np.argmin(np.abs(result.ky))),
)
assert result.spectrum[dc] == 0.0Use the convenience property when code accepts results computed with or without radial averaging:
if result.has_radial_average:
assert result.radial_wave_numbers is not None
assert result.radial_spectrum is not None
assert result.radial_mode_counts is not None
print(result.radial_wave_numbers)The property tests that all three attributes are non-None. A valid radial
calculation can still return three empty arrays if the sampled spectrum
contains no retained nonzero modes.
To avoid radial work:
result = compute_structure_factor(
positions,
config,
compute_radial_average=False,
)
assert result.radial_wave_numbers is None
assert result.radial_spectrum is None
assert result.radial_mode_counts is NoneBy default, compute_structure_factor calls radial_average with:
\min\left(\frac{2\pi}{L_x},\frac{2\pi}{L_y}\right). $$
For every Fourier sample:
The algorithm:
- retains the largest origin-centered circle sampled symmetrically by both axes;
- thereby excludes unmatched even-grid Nyquist rows or columns and reciprocal-space corners with anisotropic angular sampling;
- excludes the zero mode;
- assigns each retained mode to the nearest integer-width bin using
floor(k / bin_width + 0.5); - omits bin zero and all empty bins;
- returns the mean actual wave number, mean power, and contributing mode count for every retained bin.
Using the actual mean radius matters for rectangular boxes, where a bin can contain modes at several nearby but unequal radii.
radial_average(
spectrum,
kx,
ky,
*,
bin_width,
) -> tuple[radial_wave_numbers, radial_spectrum, radial_mode_counts]
The function accepts any finite x-first two-dimensional spectrum whose shape
is (len(kx), len(ky)), not only a StructureFactorResult.
To rebin a result more coarsely:
from structure_factor.core import radial_average
fundamental = min(
2.0 * np.pi / result.config.box_size[0],
2.0 * np.pi / result.config.box_size[1],
)
radial_k, radial_s, counts = radial_average(
result.spectrum,
result.kx,
result.ky,
bin_width=2.0 * fundamental,
)bin_width must be finite and positive. A very large custom width can place
small positive radii into bin zero; because bin zero is omitted, those modes
will not appear in the result. If no valid modes remain, the function returns
three empty one-dimensional arrays.
No characteristic length, integral moment, uncertainty estimate, or error bar is imposed by the library. Those choices depend on the physical application. For a visibly isolated positive peak, one possible diagnostic is:
positive = (
(radial_k > 0.0)
& np.isfinite(radial_s)
& (radial_s > 0.0)
)
if np.any(positive):
shown_k = radial_k[positive]
shown_s = radial_s[positive]
peak_k = shown_k[np.argmax(shown_s)]
wavelength = 2.0 * np.pi / peak_kThis reports a wavelength associated with the largest sampled radial bin; it does not establish the physical origin or statistical significance of that peak.
from structure_factor.plotting import create_figures
figures = create_figures(result)The return value is a mapping from semantic names to newly created Matplotlib
Figure objects:
| Key | When present | Content |
|---|---|---|
"source_field" |
result.source_field is not None |
Real-space grid field |
"spectrum" |
always | Two-dimensional reciprocal-space spectrum |
"radial_spectrum" |
radial wave numbers and powers are not None |
Log-log radial curve |
No window is opened and no file is written by create_figures. Figure
ownership passes to the caller.
The current plotting behavior is:
- binary source fields use two categorical colors for dilute
-1and dense+1cells; - occupancy, density, and kernel fields use a continuous color scale;
- 2D spectra use logarithmic normalization, mask nonpositive values and the connected DC cell, and can add an adaptive low-$k$ inset;
- the spectrum color floor uses the 0.5th percentile of positive, non-DC power so isolated numerical speckles do not flatten the visible range;
- radial figures use logarithmic axes and annotate the largest displayed
positive bin with both
$k_\mathrm{peak}$ and$2\pi/k_\mathrm{peak}$ ; - plotting style is applied in a local Matplotlib context and does not change the application's global style settings.
Because Matplotlib uses row-first images, the plotting functions transpose x-first arrays internally. Custom plotting code should follow the same rule.
from pathlib import Path
import matplotlib.pyplot as plt
from structure_factor.plotting import create_figures
output_dir = Path("results") / "figures"
output_dir.mkdir(parents=True, exist_ok=True)
figures = create_figures(result)
try:
for name, figure in figures.items():
figure.savefig(
output_dir / f"{name}.png",
dpi=200,
bbox_inches="tight",
)
finally:
for figure in figures.values():
plt.close(figure)Closing figures is important in loops, notebooks that recompute many results, and long-running services.
For a noninteractive machine, select a headless backend before importing
pyplot or structure_factor.plotting:
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from structure_factor.plotting import create_figuresAlternatively, set the backend in the shell:
MPLBACKEND=Agg python3 analysis.pyThe full-data plotting workflow is implemented in
examples/xy_plotting.py.
Version 1.0.0 does not expose a separate Python result-serialization function. NumPy can save the arrays and essential metadata directly:
payload = {
"spectrum": result.spectrum,
"kx": result.kx,
"ky": result.ky,
"particle_count": np.asarray(result.particle_count),
"box_size": np.asarray(result.config.box_size),
"method": np.asarray(result.config.method),
}
if result.radial_wave_numbers is not None:
payload["radial_wave_numbers"] = result.radial_wave_numbers
if result.radial_spectrum is not None:
payload["radial_spectrum"] = result.radial_spectrum
if result.radial_mode_counts is not None:
payload["radial_mode_counts"] = result.radial_mode_counts
if result.source_field is not None:
payload["source_field"] = result.source_field
if result.occupancy is not None:
payload["occupancy"] = result.occupancy
np.savez_compressed("structure_factor_result.npz", **payload)For reproducibility, also save all configuration fields used by the analysis, especially grid size, threshold, particle diameter, mode cutoffs, and kernel parameters.
StructureFactorConfig rejects:
- a
box_sizethat does not contain exactly two finite positive lengths; - a method outside
particle,occupancy,density,binary, andkernel; - nonfinite or nonpositive
grid_size,particle_diameter, orkernel_length; - a nonfinite or negative
density_threshold; - a
max_modethat is not a positive integer or a two-positive-integer tuple; - an unknown kernel name;
- a non-positive or non-integral
direct_chunk_size; - lengths so large that squaring them would overflow.
Every field is validated even if the selected method does not use it.
occupancy_grid and compute_structure_factor raise:
TypeErrorwhenconfigis not aStructureFactorConfig;ValueErrorfor an empty position set, an incompatible shape, nonfinite coordinates, or a coordinate outside the half-open box.
Nonnumeric objects can also fail during conversion to float64.
radial_average raises TypeError when an input cannot be converted to a
numeric NumPy array. It raises ValueError when:
bin_widthis not finite and positive;kxorkyis not one-dimensional;spectrum.shape != (len(kx), len(ky));- spectrum or axes contain NaN or infinite values.
For normal results returned by compute_structure_factor, plotting inputs are
already consistent. If a StructureFactorResult is constructed or modified
manually, create_figures can raise ValueError for:
- a source field that is not a finite, nonempty two-dimensional array;
- a spectrum shape not aligned with one-dimensional
kxandky; - nonfinite wave-number axes;
- radial wave-number and power arrays with incompatible dimensions.
Nonfinite or nonpositive spectrum values are masked in the logarithmic display. Nonfinite or nonpositive radial values are omitted from the log-log curve.
Grid estimators emit RuntimeWarning in two scientifically uninformative or
under-resolved cases:
- If
config.grid_shape == (1, 1), subtracting the only cell's mean leaves an identically zero connected spectrum. - For the kernel method, if
kernel_lengthis smaller than the larger effective cell spacing, the sampled kernel may be under-resolved and can collapse to the occupancy estimator.
Warnings are not errors: a result is still returned. They indicate that the chosen discretization should be reconsidered.
For cutoffs (mx, my), the computational cost scales as:
The default max_mode=8 is deliberately a low-wave-number calculation.
Doubling an isotropic mode cutoff approximately quadruples the number of
sampled mode pairs.
Particles are processed in chunks. Temporary phase matrices scale roughly with:
Decrease direct_chunk_size when temporary memory is constrained. Increase
it only after measuring performance on the target machine; it does not change
reciprocal resolution.
Occupancy construction is linear in particle count plus grid storage. The FFT work scales approximately as:
Grid memory scales with grid_size in
both directions produces roughly four times as many cells when box lengths
are large compared with the spacing.
The kernel method performs additional FFT convolution work and constructs a
full periodic kernel array. compute_structure_factor also retains both
occupancy and source_field, so account for both arrays in memory planning.
Radial averaging constructs a full array of wave-number radii comparable in
shape to the spectrum. Set compute_radial_average=False when only the
anisotropic 2D spectrum is needed.
Defaults make the package immediately runnable; they are not universal convergence parameters.
- Increase
max_modeuntil the direct spectrum covers the scientifically relevant wave-number range. - Refine
grid_sizeand confirm that conclusions are stable. Grid assignment aliases high-wave-number power, and version 1.0.0 applies no assignment-window correction. - Vary
kernel_lengthand ensure the kernel is resolved by several grid cells when a smooth continuum-like field is intended. - Treat Gaussian, top-hat, and disk kernels as different filters, not interchangeable numerical implementations.
- Use
densityandoccupancyas an internal normalization cross-check: their spectra should agree at the same grid resolution. - Do not compare the absolute binary phase-spectrum amplitude directly with particle-normalized estimators.
- Inspect the full 2D spectrum before relying only on a radial average; radial averaging intentionally removes directional information.
For the million-particle examples/xy.dat dataset, start with the provided
defaults and examples before increasing direct mode cutoffs or refining the
grid:
examples/xy_particle.pyexamples/xy_grid_methods.pyexamples/xy_all_methods.ipynbexamples/xy_plotting.py
For the estimator definitions and first-release scope, see
Scientific methods. The
command-line guide documents batch execution and the CLI archive
schema, while Examples collects the complete xy.dat
workflows.