measure op• Data kinds: depth → pairs
• Call: import fullseye as fs; fs.ledger.surface_psd(z, dx, kind='areal', dy=None) (to call the implementation directly, import roughness; roughness.surface_psd(z, dx, kind='areal', dy=None); from the registry, opsroughness.get("surface_psd"))
• Return value through the ledger: fullseye.ledger.surface_psd(...) returns only the declared out type pairs (the underlying function also returns auxiliary values). When you need what was dropped, use fullseye.ledger.surface_psd.raw(...) or call roughness.surface_psd directly.
• Underlying return: (q, C) -> (N,2)
Power spectral density of the height field. Returns (q, C). The conventions are made explicit through the arguments.
Arguments
z Height field (the mean is subtracted internally; the DC bin is not returned).
dx, dy Sample spacing. dy=None means square pixels.
kind "areal" ... ring average of the 2-D PSD C_2D(q).
"radial" ... 1-D radial PSD C_1D(q) = 2π q C_2D(q).
Conventions (without writing these down it cannot be read without calibration ―― the existing radial_power_spectrum does not have them in its docstring)
• q is in cycles / length (not the angular frequency 2π/λ). q = 1/λ.
• C_2D(q) = dx dy |F|² / (ny nx). Normalisation matches Parseval: ∫ C_2D d²q = <h²> and ∫ C_1D dq = <h²>.
• The slope for a self-affine surface H is -2(H+1) for the areal PSD and -2H-1 for the radial PSD (they differ by exactly 1).
★ Measured —— H is recovered without calibration under either convention
H was injected with surface_synth_psd, a straight line fitted in log-log over q ∈ [2/λ_hi, 0.4/dx],
and H recovered (512², band 2-64 µm):
injected H areal PSD slope recovered H radial slope recovered H slope diff
0.30 -2.600 0.300 -1.600 0.300 1.0000
0.50 -3.000 0.500 -2.000 0.500 1.0000
0.80 -3.600 0.800 -2.600 0.800 1.0000
0.95 -3.901 0.950 -2.901 0.950 1.0000
The error is within ±0.0003 in all 4 cases, and the slope difference between the two conventions is
exactly 1.0000 for every H. Readable without calibration.
★ This is where re-measuring the PoC changed the conclusion. The PoC found, with the existing
radial_power_spectrum, a uniform systematic error of -0.013 (1.6 % of H) and
concluded it was "a systematic error from discretising the ring average". Doing the same cross-check with this
implementation makes the error vanish. Isolating the cause showed it was the difference between using the bin index i·Δq as the representative q of a ring, or the mean of q within the ring
—— with the bin index a negative offset of -0.0007 to -0.0010 appears (measured, same surface, same
fitting band). The direction matches the PoC's observation but the size is 1/15, so
the rest of the -0.013 comes not from ring-average discretisation but from other implementation differences in that function.
This function returns the mean of q within the ring.
★ Pitfall —— rings beyond Nyquist consist of "corners only"
The maximum |q| of a square grid is √2/(2 dx), but rings with q > 1/(2 dx)
contain only the four corners of the frequency plane. Measured (512²): of all 362 bins, the top
107 bins (29.6 %) are beyond Nyquist. How far the point count in a ring falls short of the ideal 2πq/Δq
drops to at least 0.838x even below Nyquist
(grid discretisation), but beyond Nyquist it drops to 0.
Do not use beyond-Nyquist bins for the slope fit.
★ How far the Parseval cross-check can be trusted (measured, 512², Sq²=6.400e-03)
• Taking Σ C_2D Δqx Δqy over the full 2-D grid gives +0.0000 % (exact).
• Trapezoidal integration of the returned radial PSD gives -0.55 %. This amount is lost the moment the data are averaged per ring and collapsed to 1-D —— the ring average is a summary and not invertible. For uses needing accuracy better than 0.5 %, integrate the 2-D C_2D yourself.
• This surface's band tops out at Nyquist, so truncating at Nyquist leaves it at -0.55 % unchanged (the corners have no content). For surfaces whose corners have content (band beyond Nyquist), truncation loses it.
fail-closed
• Not 2-D / smaller than 8x8 / contains NaN or Inf / dx, dy not positive.
• kind other than "areal" or "radial".
• 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_surface_roughness — py -3.11 examples/poc_surface_roughness.py
pairs as input)—
measure)surface_params · profile_params
*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.