surface_synth_psd — ROUGHNESS synth op

• Data kinds: none → depth (an op determined by its arguments alone — it takes no image or data input)

• Call: import fullseye as fs; fs.ledger.surface_synth_psd(n, dx, hurst, lambda_lo, lambda_hi, sq, seed=0) (to call the implementation directly, import roughness; roughness.surface_synth_psd(n, dx, hurst, lambda_lo, lambda_hi, sq, seed=0); from the registry, opsroughness.get("surface_synth_psd"))

• Return value through the ledger: fullseye.ledger.surface_synth_psd(...) returns only the declared out type depth (the underlying function also returns auxiliary values). When you need what was dropped, use fullseye.ledger.surface_synth_psd.raw(...) or call roughness.surface_synth_psd directly.

• Underlying return: (z, sq_analytic)

Usage

Synthesizes a height field from a specified PSD. Returns (z, sq_analytic).

Places on every mode in the band lambda_lo <= λ <= lambda_hi an amplitude A(q) ∝ q^-(H+1) (= areal PSD C(q) ∝ q^-2(H+1), self-affine) and randomizes only the phases. Normalizes the whole to sq and returns it.

Arguments

n grid size. An integer gives n x n; (ny, nx) is also accepted.

dx sampling interval (square pixels).

hurst Hurst exponent H (0 < H < 1). Larger means smoother.

lambda_lo wavelength at the short end of the band.

lambda_hi wavelength at the long end of the band.

sq target Sq (rms height within the band).

seed random seed for the phases.

★ Returning the analytic Sq alongside is the key point

Since the amplitudes are fixed and only the phases vary, Parseval gives

<h²> = (1/(ny nx))² Σ_k A_k² fixed regardless of how the phases are drawn.

In other words, the ground truth of Sq does not depend on the random numbers. Measured: the relative difference between z.std() and

sq_analytic, even when varying seed, H, band and size, is

at most 2.2e-16 (table below). If this is settled by "vary the random numbers and measure the rms",

the ground truth itself scatters per realization and mixes with the downstream errors.

n H λ band sq |std/analytic-1| kurtosis PTV/rms

128 0.30 4-32 0.5000 2.2e-16 3.084 7.97

256 0.50 4-64 0.0800 0.0e+00 3.005 7.85

256 0.80 2-64 0.0800 2.2e-16 2.893 6.92

512 0.80 2-64 0.0800 0.0e+00 3.113 8.25

512 0.95 8-256 2.0000 0.0e+00 2.542 5.69

★ The pitfall the PoC stepped into is locked inside the implementation

If, to make the coefficients Hermitian, the phase is made antisymmetric with phi = (u - u[-k]) / 2,

it does become antisymmetric, but the phase is no longer uniform

(it becomes a triangular distribution that clusters near 0). Then all modes add up in phase at the origin

and 1 spike rises in the height field. Measured with the same seed and the same band

(512², H=0.8, band 2-64):

phase construction PTV/rms kurtosis |std/analytic-1|

antisymmetrized by (u - u[-k])/2 55.00 71.81 0.0e+00

uniform random on one of each pair 8.47 2.95 0.0e+00 ← correct

Both match the analytic Sq exactly. This kind of breakage is invisible if you look

only at the variance, so this function is fixed to the implementation that draws uniform random numbers

for only one of each conjugate pair, and the tests also check kurtosis and PTV/rms. Self-conjugate modes

(k = -k, 4 points) must have real coefficients, so instead of a phase

a sign ±1 is randomized (fixing the phase to 0 would bias all of them to +A).

fail-closed

• n less than 8 / dx non-positive / hurst outside (0, 1).

• lambda_lo >= lambda_hi.

• lambda_lo below the Nyquist wavelength 2*dx (unrealizable band).

• lambda_hi exceeds the evaluation length (not even 1 period fits).

• sq non-positive.

• Not even 1 mode falls in the band (grid too coarse / band too narrow).

Detailed usage guide

• surface_roughness family guide

References (sample data, literature)

• Sample-data catalog (download URLs / licences) — 2-D uses skimage.data (BSD/public domain) plus synthetic images; 3-D lists download URLs for real data sources (Stanford, PDS, …).

• Operator provenance and references — the sources of the research/methods this op family came from.

• The canonical algorithm (author, year) and its uses are named in the family usage guide above.

Runnable examples (verified samples that actually call this op)

• poc_polish_wipe_measure — py -3.11 examples/poc_polish_wipe_measure.py

• poc_surface_roughness — py -3.11 examples/poc_surface_roughness.py

Ops the type connects to (they accept depth as input)

surface_form_remove · surface_filter · surface_params · surface_psd

Same category (synth)

—


*Provenance: roughness.py — ROUGHNESS operator registry. This per-op note is generated by tools/opdocs.py md (do not hand-edit).*

© 2026 Kazufumi Furuse — Fullseye operator documentation. Licensed under Apache-2.0.