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)
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).
• surface_roughness family guide
• 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.
• poc_polish_wipe_measure — py -3.11 examples/poc_polish_wipe_measure.py
• poc_surface_roughness — py -3.11 examples/poc_surface_roughness.py
depth as input)surface_form_remove · surface_filter · surface_params · surface_psd
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.