CLI reference: lsdtt-spectral-analysis
Spectral analysis of a raster after Perron, Kirchner and Dietrich (2008): finds characteristic spatial scales, such as ridge-valley spacing, dune or vegetation-pattern spacing, and their orientations. It computes the 2D power spectrum, compares it with the spectrum of synthetic fractal surfaces, and reports the frequencies and bearings that stand out. It is analysis only: it does not filter or write a filtered raster. Requires FFTW3. The method, its equations and its tests are described in Spectral analysis.
Usage
lsdtt-spectral-analysis <path> <parameter_file>
lsdtt-spectral-analysis <parameter_file> # path defaults to ./
lsdtt-spectral-analysis -h # print help and exit
lsdtt-spectral-analysis -v # print version and exit
lsdtt-spectral-analysis -c # print citation and exit
# Lines beginning with # are comments.
key: value
Keys use snake_case. The full parameter list is in Parameters,
generated from the CLI's own help.
Requirements
Requires FFTW3 library. If FFTW3 is not available at compile time, this CLI is not built. Verify availability: lsdtt-spectral-analysis -h should execute without error.
Pipeline overview
Steps run in this order:
| Step |
Description |
| 1. Prepare |
Load the raster, remove a best-fit plane, apply a Hann window, zero-pad to a power of two in each direction |
| 2. Periodogram |
2D FFT and periodogram; collapse to a radial spectrum |
| 3. Rollover |
Log-bin the radial spectrum, find the rollover and the power-law exponent beta |
| 4. Background |
Average the periodograms of n_background_iterations synthetic fractal surfaces with that exponent |
| 5. Normalise and test |
Observed / background; flag binned frequencies above 2.9955 and 2D cells above -ln(1 - confidence) |
| 6. Output |
Write the 2D spectrum rasters and the CSVs below |
| Parameter |
Type |
Default |
Description |
read_path |
string |
directory of parameter file |
Directory containing the input DEM. |
read_fname |
string |
parameter file stem |
Input DEM filename. Any GDAL-supported format. |
write_path |
string |
same as read_path |
Directory for output files. |
write_fname |
string |
same as read_fname |
Output filename stem. |
Spectral analysis
n_threads defaults to 0, which uses all available hardware threads. Use
1 for serial execution, or set an explicit limit no greater than the allocated
physical cores on a shared machine.
| Parameter |
Type |
Default |
Description |
n_threads |
int |
0 |
OpenMP threads; 0 uses all available hardware threads. |
spectral_log_bin_width |
float |
0.05 |
Log10 bin width for the radial spectrum. |
n_background_iterations |
int |
100 |
Number of synthetic fractal surfaces averaged into the background. More iterations bring the 2D test's false-alarm rate closer to nominal: at 9.21 it is about 1.5 in 10,000 cells at 100, 5 in 10,000 at 20 (the default before 2026-10-21). |
background_seed |
int |
-1 |
Background seed; -1 selects the reproducible built-in sequence (base 1000). For a given seed every output is bit-identical, whatever n_threads is. |
significant_frequencies_2d_confidence |
float |
0.9999 |
Confidence level for _significant_frequencies_2d.csv; the normalised-PSD threshold is -ln(1 - confidence) (9.2103 at 0.9999). |
Valley spacing
| Parameter |
Type |
Default |
Description |
run_valley_spacing |
bool |
false |
Measure characteristic valley spacing. Former compute_valley_spacing is an unchanged-value alias. |
valley_spacing_axis |
string |
rows |
Axis along which spacing is measured: rows or columns. |
valley_spacing_margin |
int |
20 |
Flow-direction boundary cells omitted at each end. |
valley_spacing_K |
float |
0 |
Erodibility for the channelization index; 0 skips it. |
valley_spacing_D |
float |
0 |
Hillslope diffusivity for the channelization index. |
valley_spacing_U |
float |
0 |
Uplift rate for the channelization index. |
valley_spacing_area_exponent |
float |
0.45 |
Dimensionless drainage-area exponent m. Former valley_spacing_m is an unchanged-value alias. |
valley_spacing_n |
float |
1.0 |
Dimensionless slope exponent n. |
Outputs
| Parameter |
Type |
Default |
Description |
write_2d_spectrum_rasters |
bool |
true |
Write observed and background 2D PSD rasters. Former write_2d_spectrum is an unchanged-value alias. |
write_radial_spectrum |
bool |
false |
Write the log-binned radial spectrum CSV. |
write_significant_frequencies |
bool |
true |
Write significant frequencies and rollover summaries. |
write_significant_frequencies_2d |
bool |
true |
Write the individual significant 2D spectrum cells, with bearings. |
Significant 2D frequencies with bearings
_significant_frequencies_2d.csv lists every cell of the 2D spectrum whose
normalised PSD (observed / background) exceeds -ln(1 - confidence), the
χ²(2 DOF) quantile divided by 2 (Perron et al. 2008, eq. 9). It replaces the
legacy LSDTopoTools _significant_freq.csv, at the same default 99.99% level.
Where 9.21 comes from. For a surface with no characteristic scale, each
periodogram cell divided by its expected value (the normalised PSD) follows an
exponential distribution with mean 1: it is a sum of two squared Gaussians (the
real and imaginary parts of the Fourier coefficient), i.e. χ² with 2 degrees of
freedom, divided by 2. The chance that such a cell exceeds t is exp(-t).
Setting that chance to 1 in 10,000 (confidence 0.9999) gives
t = -ln(0.0001) = 9.2103. The legacy code wrote the same value as
18.42185 / 2, half the χ²₂ value at 99.99 %. The binned test's 2.9955 is the
same construction at 95 % (5.991 / 2). See
Spectral analysis, section 2.11
for the derivation and for the effect of a finite number of background
iterations.
| Column |
Units |
Meaning |
bearing_rad, bearing_deg |
rad, ° |
Direction of the wave vector, clockwise from north, in [0, 2π) / [0, 360). The crests run perpendicular to it. |
frequency_cycles_per_m |
cycles/m |
Radial frequency of the cell. |
wavelength_m |
m |
1 / frequency. |
normalised_psd |
- |
Observed PSD / background PSD. |
Five # comment lines at the top record the confidence, threshold,
effective background seed, number of background iterations and
n_significant_frequencies. When nothing passes the threshold, a further #
line under the column header says so in words:
# n_significant_frequencies,0
bearing_rad,bearing_deg,frequency_cycles_per_m,wavelength_m,normalised_psd
# No significant frequencies were found in this raster: no cell of the 2D spectrum exceeded the normalised PSD threshold of 9.21034 (confidence 0.9999). This is a valid result, not an error.
_significant_frequencies.csv does the same, with its threshold (2.9955)
and n_significant_frequencies added to its rollover lines. Read either file
with pandas.read_csv(path, comment="#").
- The spectrum of a real surface is point-symmetric, so a significant
frequency normally appears twice, at θ and θ + 180°. Use
bearing_deg % 180
for an axial (orientation) analysis, or keep one of each pair.
- The exception is the Nyquist edges of the padded grid, i.e. wavelengths near
two cells. +Nyquist and −Nyquist are the same frequency, so the twin of a cell
on the north Nyquist row is on that row too, at 360° − θ, and the twin of a
cell on the east Nyquist column is on that column, at 180° − θ. Three cells
(bearings 0°, 270° and 315° at the Nyquist corners) are their own twins and
appear once.
- The bearing uses the physical frequency components, so it is correct when
the padded grid is not square. The legacy code used index offsets, which
agree only for square grids.
- The 2D spectrum has thousands of cells, so a few pass the threshold by
chance alone. 1 in 10,000 at 0.9999 is the limit for infinitely many
background iterations; with the default 100 the rate is about 1.5 in 10,000,
i.e. about 2.4 entries (one or two pairs) on a 128 × 128 padded grid. Repeat
a run with another
background_seed and keep detections that persist.
| Parameter |
Type |
Default |
Description |
write_output_format |
string |
cog |
Raster output format: cog, gtiff, same_as_input, png. png writes an 8-bit scaled image with a worldfile and PAM sidecar for quick visualisation only, not for analysis reuse. Output files take the extension of the chosen format. |
Notes
- The DFT treats the raster as periodic. To limit the leakage from the jump
between opposite edges, the tile is detrended and multiplied by a Hann window
that falls to zero at the edges, then zero-padded, before the FFT.
- PSD values are variance per DFT cell, in the input units squared (m² for an
elevation raster); they are not divided by the frequency-cell area.
- The tile must hold several wavelengths of a pattern for its peak to stand out
from the lowest-frequency bins.
- The 2D spectrum rasters are in frequency space, with the zero frequency at
(0, 0) and axes in cycles/m. Each axis is padded to its own power of two, so
when the padded row and column counts differ (e.g. a 200 × 500 input pads to
256 × 512) the north and east frequency spacings differ and the rasters are
written with non-square pixels. This is deliberate: both axes are exact. They
are the only lsdtt3 outputs with non-square pixels.
Parameters
This section is generated from the executable's -h json registry. Do not edit it manually.
Status: stable.
File I/O
| Parameter |
Type |
Default |
Role |
Parent |
Description |
Guidance |
read_path |
string |
./ |
io |
- |
Directory from which input files are read. |
Defaults to the directory holding the parameter file. |
read_fname |
string |
- |
io |
- |
Input filename, with or without extension (e.g. my_dem.tif). |
Read from read_path. lsdtt3 uses GDAL, so any GDAL-readable raster format works. If empty, defaults to the parameter filename stem. |
write_path |
string |
- |
io |
- |
Directory to which output files are written. |
Defaults to read_path when not set. |
write_fname |
string |
- |
io |
- |
Output filename prefix (no path, no extension). |
All outputs are named <write_fname>_<descriptor>.<ext>. Defaults to the stem of read_fname when not set. |
Threading
| Parameter |
Type |
Default |
Role |
Parent |
Description |
Guidance |
n_threads |
int |
0 |
io |
- |
Number of OpenMP threads. |
0 = use all hardware threads (default). 1 = serial. In shared environments, set an explicit limit no greater than the physical-core allocation. |
| Parameter |
Type |
Default |
Role |
Parent |
Description |
Guidance |
write_output_format |
string |
cog |
io |
- |
Output raster format. |
Allowed values: cog, gtiff, same_as_input, png. cog: Cloud Optimised GeoTIFF (default). gtiff: plain compressed GeoTIFF. same_as_input: match the input raster format. png: 8-bit scaled image with a worldfile and PAM sidecar for georeferencing - for quick visualisation only, not for analysis reuse. Output files take the extension of the chosen format. |
Spectral options
| Parameter |
Type |
Default |
Role |
Parent |
Description |
Guidance |
spectral_log_bin_width |
float |
0.05 |
param |
- |
Log10 bin width for the radial spectrum. |
Controls the width of each frequency bin in log10 space. Smaller values give finer frequency resolution. Default is 0.05. |
n_background_iterations |
int |
100 |
param |
- |
Number of synthetic fractal surfaces averaged into the background spectrum. |
Each iteration makes a synthetic surface (white noise filtered to a power-law spectrum with the fitted exponent beta, scaled to the tile's standard deviation) and takes its periodogram with the same window and padding; the background is their mean. Its scatter in each cell is about 1/sqrt(N), and the 2D test's threshold assumes the background is exact: at 9.21 the per-cell false-alarm rate is (1 + 9.21/N)^-N, about 1.5e-4 at the default N = 100 (nominal 1e-4) and 5e-4 at N = 20. Default is 100. |
background_seed |
int |
-1 |
param |
- |
Base seed for the Monte Carlo background spectrum. |
Iteration i uses seed = background_seed + i. Default (-1) uses the built-in sequence (base 1000), which is always reproducible. Set an explicit non-negative integer to pin a specific realisation - record the value alongside results to reproduce them exactly. |
significant_frequencies_2d_confidence |
float |
0.9999 |
param |
- |
Confidence level for a 2D spectrum cell to be written as significant. |
A cell is significant when its normalised PSD exceeds -ln(1 - confidence), the chi-squared (2 DOF) quantile divided by 2 (Perron et al. 2008, eq. 9). 0.9999 gives 9.2103, as in the legacy LSDTopoTools output; 0.95 gives 2.9957. Must lie strictly between 0 and 1. The spectrum has thousands of cells, so at 0.95 hundreds pass by chance alone. |
Output toggles
| Parameter |
Type |
Default |
Role |
Parent |
Description |
Guidance |
write_2d_spectrum_rasters |
bool |
true |
switch |
- |
Write the 2D observed and background PSD rasters. |
Writes _psd_2d.<ext> and _background_psd_2d.<ext> in frequency space; write_output_format selects the extension. |
write_radial_spectrum |
bool |
false |
switch |
- |
Write the log-binned radial spectrum as a CSV. |
Writes _radial_spectrum.csv with columns: frequency, wavelength, psd, background_psd, normalised_psd, beta_by_bin, r_sq_by_bin. |
write_significant_frequencies |
bool |
true |
switch |
- |
Write a CSV of frequencies with significant spectral power. |
Writes _significant_frequencies.csv. A frequency is significant if its normalised PSD exceeds the chi-squared 95% CI threshold (2.9955). Lines starting with # give the rollover summary, the threshold and n_significant_frequencies; when that is 0 a line under the column header says no significant frequencies were found. |
write_significant_frequencies_2d |
bool |
true |
switch |
- |
Write a CSV of individual 2D spectrum cells with significant power, with bearings. |
Writes _significant_frequencies_2d.csv with columns bearing_rad, bearing_deg, frequency_cycles_per_m, wavelength_m, normalised_psd. The bearing is the direction of the wave vector, clockwise from grid north (from true north with bearings_relative_to_true_north, converted with the convergence at the grid centre, since the spectrum belongs to the whole raster); the crests run perpendicular to it. The spectrum of a real surface is point-symmetric, so each significant frequency appears twice, at bearings theta and theta + 180, except cells on the Nyquist edges of the padded grid (wavelengths near two cells), whose twin is at 360 - theta (first row) or 180 - theta (first column), or which appear once. Equivalent to the legacy _significant_freq.csv. The threshold is set by significant_frequencies_2d_confidence. Lines starting with # record the confidence, threshold, seed, iterations and n_significant_frequencies; when that is 0 a line under the column header says no significant frequencies were found, so an empty table is never mistaken for a failed run. Read with pandas.read_csv(path, comment='#'). |
Valley spacing
| Parameter |
Type |
Default |
Role |
Parent |
Description |
Guidance |
run_valley_spacing |
bool |
false |
switch |
- |
Measure characteristic valley spacing (Perron 2008 / Bonetti 2020). |
Reports three independent measures because each fails differently: the POWER-WEIGHTED mean of spectral peaks (Perron et al. 2008 -- weighted, not the argmax, because spacing occupies a band); the CONVEX HALF-WIDTH, the mean run of convex cells across the valleys, which uses thousands of samples and so survives a domain too small for a spectral peak; and the small-scale spectral exponent alpha, predicted to be 2m-3 (Bonetti et al. 2020, Hooshyar et al. 2021). Writes _valley_spacing.csv and, with the spectrum, _valley_spacing_spectrum.csv. |
valley_spacing_axis |
string |
rows |
param |
- |
Axis the spacing is measured ALONG: rows or columns. |
Valleys run in the flow direction, so a landscape draining north-south has its spacing measured east-west, i.e. along rows. |
valley_spacing_margin |
int |
20 |
param |
- |
Cells dropped at each end of the FLOW direction before measuring. |
Base level pins elevation there, so those lines carry the boundary's imprint rather than the landscape's own scale. |
valley_spacing_K |
float |
0 |
param |
- |
Erodibility K, for the Bonetti channelization index. 0 = skip CI. |
CI = K l^(m+n) / (D^n U^(1-n)); for n = 1 it is independent of U. CI_c ~ 32-37 marks first channelization, so reporting CI beside the measured spacing is what makes a result comparable to the literature. |
valley_spacing_D |
float |
0 |
param |
- |
Hillslope diffusivity D, for the channelization index. |
See valley_spacing_K. |
valley_spacing_U |
float |
0 |
param |
- |
Uplift rate U, for the channelization index (unused when n = 1). |
See valley_spacing_K. |
valley_spacing_area_exponent |
float |
0.45 |
param |
- |
Drainage-area exponent m, for CI and for the alpha = 2m-3 prediction. |
See valley_spacing_K. |
valley_spacing_n |
float |
1.0 |
param |
- |
Slope exponent n, for the channelization index. |
See valley_spacing_K. |
Orientations
| Parameter |
Type |
Default |
Role |
Parent |
Description |
Guidance |
bearings_relative_to_true_north |
bool |
false |
param |
- |
Write bearings, aspects and orientations clockwise from true north. |
Off (default): they are clockwise from grid north (grid up), so in an oblique frame with the feature along +x, 90 degrees means along the feature. On: each value is converted with the grid convergence at its own location (each cell of a raster, each row of a table), which also removes the up to about 3 degree difference between UTM grid north and true north. Angles between two directions (junction angles, deflections, contrasts) are unchanged, and hillshade illumination always stays relative to grid up. Needs a raster with a CRS. |