SurfClustSim - native surface cluster simulation and alpha tables
========================================================================
PURPOSE
-------
SurfClustSim estimates surface-cluster area thresholds under a Gaussian
noise null hypothesis. It is a native replacement for the simulation,
smoothing, SurfClust, and quick.alpha.vals.py stages generated by
slow_surf_clustsim.py. Surfaces, smoothing weights, and cluster topology
are loaded once; simulations run in memory and may use OpenMP.
For each simulated field the program generates noise, applies HEAT_07
surface smoothing, rescales the field to unit standard deviation, and saves
the largest cluster area at each nodewise p threshold. Quantiles of those
maxima form a C(p,alpha) table: a row is an uncorrected nodewise p threshold
and a column is a whole-surface corrected alpha level.
SYNOPSIS
--------
Surface-domain noise:
SurfClustSim -spec SPEC -surf_A SURF -on_surface \
SMOOTHING_OPTIONS -niter N -prefix PREFIX [options]
Volume-domain noise mapped between two surfaces:
SurfClustSim -spec SPEC -surf_A SURF_A -surf_B SURF_B -sv SURFVOL \
-vol_mask MASK SMOOTHING_OPTIONS -niter N -prefix PREFIX [options]
QUICK RECIPE: REPEAT A slow_surf_clustsim.py ANALYSIS
----------------------------------------------------
The closest native equivalent to an old surface-domain run with blur=10,
itersize=10, and pthr_list=(0.05 0.01 0.001) is:
mkdir -p clust.lh.results.10000
SurfClustSim \
-spec fsaverage_SUMA/fsaverage_lh.spec \
-surf_A smoothwm -on_surface \
-compat -target_fwhm 10 -itersize 10 \
-niter 10000 -pthr 0.05 0.01 0.001 \
-legacy_sided -athr 0.10 0.05 0.02 0.01 \
-prefix clust.lh.results.10000/surf.lh.clustsim -niml
The historical surf_vol input is not needed when noise is generated directly
on the surface. The output of interest is surf.lh.clustsim.legacy.1D
(and .legacy.niml with -niml).
This is statistical compatibility, not iteration-for-iteration identity.
The native program uses a different Gaussian random stream. Also, the old
quick.alpha.vals.py rounded simulated areas to integers and printed an entire
area/alpha curve; this program retains continuous areas and directly reports
the requested alpha quantiles. Results should agree within Monte Carlo and
small tabulation differences, not necessarily character for character.
SMOOTHING MODES
---------------
Both modes use the same SUMA HEAT_07 weights and iterative smoothing kernel.
They differ in how the kernel bandwidth and number of passes are selected.
-compat
Adaptive, block-level target-FWHM smoothing. This reproduces the path
used by slow_surf_clustsim.py and SurfSmooth with -target_fwhm,
-blurmaster, and -detrend_master.
For every block of B=-itersize fields, the program:
1. copies the B noise fields to make a smoothing master;
2. detrends the master across its B columns;
3. repeatedly estimates mean master FWHM and applies one HEAT_07 pass
until that mean exceeds -target_fwhm; and
4. applies the selected number of passes to the original fields.
One Niter is therefore selected for the entire block, not independently
for every field. Different blocks can select different values.
-target_fwhm is the requested FINAL smoothness, not additional blur.
If -sigma is omitted, a mesh-aware bandwidth is estimated from target
FWHM and mean edge length, as SurfSmooth did. Supplying -sigma fixes the
bandwidth of each pass, but the number of passes remains adaptive.
Do not give -Niter with -compat.
Use -compat when comparing with old slow_surf_clustsim.py results, or
whenever reaching a measured target FWHM is more important than speed.
It is slower because FWHM is repeatedly re-estimated and the master is
smoothed before the original fields are smoothed.
-fixed
Fixed-parameter smoothing. Every field receives exactly K HEAT_07
passes with bandwidth S:
-fixed -sigma S -Niter K
-target_fwhm is not used in this mode. Fixed mode does not promise that
the final measured smoothness equals a particular value unless S and K
have first been calibrated for the same surface, mask, and noise domain.
It is faster and its results are independent of -itersize.
A practical calibration workflow is to run a smaller -compat pilot using
the intended surface and mask, inspect the reported sigma and block Niter
values, choose a representative Niter (often the median), and then use
that sigma/Niter pair in a large -fixed run. Validate the achieved FWHM
before treating the fixed run as interchangeable with adaptive smoothing.
-sigma S
Bandwidth of the HEAT_07 kernel for ONE iteration, in surface coordinate
units (normally mm). It is not the final FWHM. Repeated graph smoothing
and surface geometry make the relation between sigma, Niter, and final
FWHM nontrivial. Very small sigma can require thousands of passes and
can cause numerical precision warnings on a coarse mesh.
-target_fwhm F, -blur F
Requested final FWHM for -compat (default 4). FWHM is an equivalent
Gaussian width in surface coordinate units. On a curved mesh it is an
estimate based on neighboring-node differences.
-Niter K
Nonnegative number of HEAT_07 passes for -fixed. If neither smoothing
mode is named, supplying -Niter implies -fixed; otherwise the default is
-compat. -compat and -fixed are mutually exclusive.
-max_Niter K, -max_smooth_niter K
Safety ceiling for an adaptive -compat search (default 3000). A warning
is issued if a block reaches this ceiling below the target FWHM. Raising
sigma is often more useful than merely raising this ceiling.
-acf a b c (or -ACF)
-acf_file FILE
A third smoothing mode, alternative to -compat and -fixed. Instead of
smoothing white noise to a target FWHM, generate noise whose spatial
AUTOCORRELATION matches a requested curve:
ACF(r) = a*exp(-r*r/(2*b*b)) + (1-a)*exp(-r/c)
with r a geodesic distance. -acf_file reads a, b and c from the output
of 'SurfFWHM -acf' run on your own data, which is the intended workflow.
WHY: a FWHM pins the WIDTH of the autocorrelation, not its SHAPE, and
cluster extent at high thresholds is governed by the tail. Smoothing
white noise gives a nearly Gaussian tail (a near 1); real data has a
much heavier one (a nearer 0.5). A too-light tail makes simulated
clusters too small, so the area threshold comes out too lenient. This
is the same failure that inflated cluster false positives in the volume
and the reason 3dClustSim gained -acf. Measure your data first: if it
reports a near 0.9, the ordinary -fixed/-compat noise is already close
and this mode buys little.
HOW: a single heat kernel cannot produce an exponential tail at all, so
the noise is built as a weighted sum of INDEPENDENT fields smoothed by
differing numbers of passes. Independent unit-variance components make
the mixture's ACF the weighted sum of theirs, so the weights follow from
an ordinary least-squares fit to the requested curve, solved once at
startup. The program then GENERATES a few fields, measures their ACF,
and reports what it actually achieved -- read that line rather than
trusting the fit. It warns if the generated 'a' misses the request by
more than 0.10.
ACCURACY: expect the generated a within roughly 0.15 of the request, and
b and c looser -- both are weakly identified whenever their component
carries little weight, so do not read much into them on their own. The
achieved value is printed on the 'ACF verify' line every run: READ IT
rather than assuming the request was met, and treat the request as a
target the program aims at, not a guarantee. The generated shape tends
to land on the heavy-tailed side of what was asked for (a lower than
requested), which is the conservative direction for cluster inference:
a heavier tail yields larger null clusters and so a stricter threshold.
The
generated curve tends to sit slightly below the requested one at large
r, because an empirical autocorrelation is biased low at long lags while
the target is an exact analytic curve; the true ACF of the noise is
likely closer to the request than the report suggests.
COST: calibration is a fixed startup cost, dominated by measuring the
basis autocorrelations, and takes roughly half a minute on a 10k-node
surface. After that the per-simulation cost is small, so this mode
suits large -niter: 1000 simulations added under a second to a 27-second
calibration in testing.
-acf_nbasis K
Number of basis fields in the mixture (default 5, range 2-16). Raise it
if the achieved 'a' misses the request; more components can follow a
more awkward curve, at proportionally more smoothing per simulation.
-acf_radius R
Largest geodesic distance used when fitting and verifying the ACF. The
default reaches a couple of decay lengths of c, which is what lets the
fit see the tail at all, capped to keep startup affordable.
-itersize B
Number of fields per processing block (default 10). In -compat this is
also the blurmaster length, matching the old script default; changing it
can change detrending, selected Niter, and results. In -fixed it is only
a memory/parallel-work batching parameter and does not change results.
Blocks are distributed across threads, so -niter/B (the block COUNT) is
what bounds parallelism -- a larger B means fewer, bigger blocks and less
parallelism, not more. The default suits both roles; prefer to leave it.
NOISE DOMAIN AND MASKING
------------------------
-on_surface [yes|no]
Generate independent N(0,1) noise at surface nodes before smoothing.
A bare -on_surface means yes. This matches '-on_surface yes' in the old
script and does not require -sv or a surface volume.
-surf_mask DSET
Restrict generated noise, FWHM estimation, smoothing, rescaling, and
clustering to nonzero surface nodes. Use the same analysis mask that
will be used on real data. This option can also restrict nodes after
volume-to-surface mapping.
-vol_mask MASK
Select volume-domain simulation and use MASK both as the noise grid and
nonzero voxel mask. Volume noise is mapped between -surf_A and -surf_B
using the coordinate system supplied by -sv SURFVOL.
-map_func FUNC 3dVol2Surf mapping function (default ave).
-f_steps N Samples between surfaces (default 10).
-nsteps is an alias.
SIMULATION AND CLUSTER OPTIONS
------------------------------
-niter N
Total number of independent simulated fields (default 1000). Unlike the
generated legacy script, this is not converted to a number of blocks and
is not rounded upward. For a final alpha=0.01 table, 10000 or more runs
are advisable: 10000 simulations provide only about 100 observations in
a 1% tail. More simulations reduce Monte Carlo variation.
-pthr p1 ... pn, -pval p1 ... pn
Uncorrected nodewise probability thresholds. Values must be in (0,.5].
Defaults are .05 .02 .01 .005 .002 .001 .0005 .0002 .0001. Each value
is converted internally to a standard-normal threshold according to the
selected sidedness.
-athr a1 ... an
Corrected whole-surface alpha levels to report. Defaults are
.10 .05 .02 .01. A table entry C(p,alpha) is the cluster area threshold
obtained from the empirical distribution of maximum cluster area.
-LOTS
Use dense 3dClustSim-style pthr and athr grids (29 p thresholds and
10 alpha levels). Explicit -pthr or -athr options appearing later can
replace the corresponding list.
-rmm R
Cluster connection rule (default -1). A negative integer -N connects
nodes within N mesh-edge steps; -1 means immediate neighbors. A positive
R uses SUMA's surface-offset radius in surface coordinate units. Use the
same rule when clustering the real statistical map.
-seed S Nonnegative deterministic seed (default 123456789).
-nthreads N OpenMP thread limit; 0 means the runtime maximum.
Results are invariant to thread count.
-quiet Suppress routine information and progress messages.
Warnings and errors are still printed.
-verbose, -verb Show per-block smoothing messages and progress
pacifiers. By default, routine setup/output messages
and one global progress update every 10 blocks are
printed.
SIDEDNESS: WHAT pthr MEANS
-------------------------
By default the program writes 1-sided, 2-sided, and bi-sided tables. In
-compat it additionally writes a legacy table. If any selector below is
given, only explicitly selected modes are written; selectors may be repeated.
-1sided, -onesided
Positive clusters only. Threshold z=Phi^-1(1-pthr), so pthr is the
positive upper-tail probability.
-2sided, -twosided
Threshold |z| at Phi^-1(1-pthr/2), so total nodewise probability is pthr.
Positive and negative active nodes belong to one binary excursion set and
may join the same connected component. This follows 3dClustSim's
2-sided convention.
-bisided
Use the same pthr/2 threshold in each tail, but cluster positive and
negative nodes separately. The simulation records the larger maximum
from the two signs.
-legacy_sided
Match slow_surf_clustsim.py/SurfClust thresholding: threshold |z| at
Phi^-1(1-pthr). Thus EACH tail has probability pthr and the total
nodewise false-positive probability is approximately 2*pthr. Opposite
signs can join. This is intentionally not the modern meaning of a
2-sided pthr; use it only for historical compatibility.
OUTPUT
------
-prefix P
Write P.1sided.1D, P.2sided.1D, P.bisided.1D, and/or P.legacy.1D.
The parent directory must already exist. Each file records the command,
surface/mask sizes, smoothing parameters, seed, threads, pthr rows, and
athr columns.
-niml Also write matching .niml table files.
-niml_only Write only NIML tables.
-both Write both 1D and NIML tables (same as -niml).
-maxarea_1D
Also write the raw per-simulation maximum cluster areas, one file per
p-value, named PREFIX.MODE.max.area.PTHR. Each file holds -niter lines,
one area per line, unsorted -- the same layout slow_surf_clustsim.py
wrote as z.max.area.PTHR. Use it to compare against the old pipeline:
quick.alpha.vals.py -niter N PREFIX.legacy.max.area.0.001
The built-in alpha table is NOT directly comparable to that tool, which
rounds areas to whole units and builds an empirical survival curve; the
table here uses an interpolated quantile instead.
One format difference from slow_surf_clustsim.py: when a simulation has
no suprathreshold cluster, that file wrote nothing, so it held FEWER
than niter lines. This writes an explicit 0, always niter lines. Alpha
values still agree because quick.alpha.vals.py normalizes by its -niter
argument -- but pass -niter, or it will normalize by the line count and
the two pipelines will disagree by the fraction of empty simulations.
-multithresh (or -mthresh)
Also write a jointly calibrated multi-threshold table, named
PREFIX.MODE.mthresh.1D.
THE IDEA, AND WHERE IT COMES FROM
This option is inspired by ETAC (Equitable Thresholding And Clustering),
available in the volume via 3dttest++ -ETAC and 3dXClustSim. ETAC's
insight is that picking a single per-node p-threshold before you know
what the signal looks like is arbitrary, and the choice changes the
answer: a low threshold favours large diffuse clusters, a high one
favours small focal ones. ETAC's answer is to stop choosing. It tests
several thresholds at once and calibrates them together, 'equitably' --
meaning each member of the family is tuned to contribute about equally
to the overall false positive rate, so no single one dominates.
That equitable multi-threshold principle is what this option borrows.
A result counts as significant if a cluster survives at ANY of the
p-thresholds, and all the cutoffs are chosen together so the probability
of that happening under the null is alpha. Concretely, one shared tail
probability q sets every threshold's cutoff, which is what makes the
contributions equitable, and q is solved for by bisection. The
arbitrary choice of threshold goes away; the cost is larger cutoffs,
typically 15-25% above the per-threshold table, which is the honest
price of testing a whole family.
HOW TO READ THE TABLE
Each COLUMN is one calibrated family, not a menu of alternatives. To
use the alpha column, test EVERY p-threshold listed in it and accept a
cluster surviving at any of them; the family then carries the stated
false positive rate. Using a single row on its own is still valid, just
conservative -- that cutoff is larger than the per-threshold table's.
The achieved null FPR is written into the header so you can see how well
the calibration converged. It needs a few thousand simulations to mean
much, and a warning is issued if it lands far from what was requested.
WHAT THIS DOES *NOT* INHERIT FROM ETAC
Real ETAC is substantially more than multi-threshold calibration, and
this option should not be mistaken for it. ETAC additionally offers:
* a null built by randomizing/permuting the residuals of your ACTUAL
data, rather than from simulated noise. This is the big one: it is
where most of ETAC's advantage over a simulated null comes from,
because it assumes nothing about the shape of the spatial
autocorrelation. This program calibrates over the noise it
SIMULATES, and so inherits every assumption in that noise model --
in particular that smoothing white noise to a target FWHM yields a
realistic autocorrelation, which is exactly the assumption that
could inflate cluster false positives in the volume literature.
* a more general cluster figure of merit than area alone; ETAC can
also use sums of powers of |z| within a cluster. Here it is area.
* simultaneous calibration across multiple blur levels. Here there is
one smoothing level per run.
* spatially varying thresholds, so that ETAC produces a MAP of cutoffs
adapted to local smoothness. This produces one number per
(p, alpha) pair, applied everywhere, and so still assumes the
surface is statistically stationary.
In short: this borrows ETAC's equitable multi-threshold idea, and with
it the benefit of not having to pick a threshold. It does not deliver
ETAC's data-driven null, its generalized merit, its multi-blur
calibration, or its spatial adaptivity. If you can run a permutation
test on your data, that remains the better tool.
-selfcheck
Recompute every maximum cluster area with an independent breadth-first
search and abort if it disagrees with the threshold-sweep result. The
sweep activates nodes in descending order and merges components with
union-find to get every threshold from one pass; the BFS simply floods
each thresholded field separately. They must agree exactly. This is a
correctness check, not a sampling option -- it is roughly an order of
magnitude slower, so use it on a small -niter, not a production run.
Table entries are areas in the SQUARED coordinate units of the surface.
They are mm^2 only when the surface coordinates are expressed in mm. Apply
a table only to the same surface geometry, mask, smoothing procedure, noise
domain, rmm, and sidedness used to generate it. A cluster is significant at
the requested (pthr,alpha) when its area meets the corresponding threshold.
The empirical quantiles retain continuous node areas and are monotonized so
that stricter pthr or alpha settings cannot produce a smaller threshold merely
from Monte Carlo jitter.
REPRODUCIBILITY AND PERFORMANCE
-------------------------------
The random stream is indexed by global simulation number, so results do not
depend on -nthreads. Fixed-mode results also do not depend on -itersize.
Compatibility-mode results can depend on -itersize because the block is the
adaptive smoothing master.
OpenMP distributes whole blocks across threads, so parallelism is limited by
the BLOCK COUNT (-niter / -itersize), not by -itersize itself. Raising
-itersize therefore REDUCES available parallelism, and in -compat it also
changes the results; leave it alone unless you mean to. Memory scales as
-nthreads * -itersize * nodes, since each thread holds a whole block.
Volume-mode noise generation is serialized (the vol2surf routines beneath it
are not reentrant); everything after it runs in parallel. Final table output
is serial. With -verbose, pacifiers are capped at 50 marks per phase.
EXAMPLES
--------
1. Historical surface-domain compatibility, one old-style table:
SurfClustSim -spec std.lh.spec -surf_A smoothwm -on_surface \
-compat -target_fwhm 10 -itersize 10 -niter 10000 \
-pthr .05 .01 .001 -legacy_sided -prefix lh.old -niml
2. New analysis with adaptive FWHM and modern sidedness tables:
SurfClustSim -spec std.lh.spec -surf_A smoothwm -on_surface \
-compat -target_fwhm 10 -niter 10000 \
-1sided -2sided -bisided -prefix lh.adaptive -niml
3. Calibrated fixed smoothing:
SurfClustSim -spec std.lh.spec -surf_A smoothwm -on_surface \
-fixed -sigma S -Niter K -niter 10000 -itersize 32 \
-nthreads 16 -prefix lh.fixed -niml
4. Volume noise mapped between white and pial surfaces:
SurfClustSim -spec std.lh.spec -surf_A smoothwm -surf_B pial \
-sv SurfVol.nii -vol_mask epi.mask.nii \
-compat -target_fwhm 10 -niter 10000 -prefix lh.volume
See also: SurfSmooth, SurfClust, 3dClustSim, slow_surf_clustsim.py
Author: P. Molfese
Date: 20 Aug 2026
Version: 0.3.0
=========================================================================
* This binary version of SurfClustSim is compiled using OpenMP, a semi-
automatic parallelizer software toolkit, which splits the work across
multiple CPUs/cores on the same shared memory computer.
* OpenMP is NOT like MPI -- it does not work with CPUs connected only
by a network (e.g., OpenMP doesn't work across cluster nodes).
* For some implementation and compilation details, please see
https://afni.nimh.nih.gov/pub/dist/doc/misc/OpenMP.html
* The number of CPU threads used will default to the maximum number on
your system. You can control this value by setting environment variable
OMP_NUM_THREADS to some smaller value (including 1).
* Un-setting OMP_NUM_THREADS resets OpenMP back to its default state of
using all CPUs available.
++ However, on some systems, it seems to be necessary to set variable
OMP_NUM_THREADS explicitly, or you only get one CPU.
++ On other systems with many CPUS, you probably want to limit the CPU
count, since using more than (say) 16 threads is probably useless.
* You must set OMP_NUM_THREADS in the shell BEFORE running the program,
since OpenMP queries this variable BEFORE the program actually starts.
++ You can't usefully set this variable in your ~/.afnirc file or on the
command line with the '-D' option.
* How many threads are useful? That varies with the program, and how well
it was coded. You'll have to experiment on your own systems!
* The number of CPUs on this particular computer system is ...... 1.
* The maximum number of CPUs that will be used is now set to .... 1.
* SurfClustSim also accepts -nthreads N to set a smaller thread limit
for one invocation. Parallel simulation workers are also limited by
-itersize (default 10).