rfgen 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
rfgen/__init__.py ADDED
@@ -0,0 +1,98 @@
1
+ """
2
+ rfgen - Periodic Gaussian Random Field Generation and Analysis.
3
+
4
+ A Python package for generating and analyzing periodic Gaussian random fields
5
+ using spectral (Fourier) methods.
6
+
7
+ Features
8
+ --------
9
+ - Self-affine (power-law) spectrum generation
10
+ - Matérn covariance spectrum generation
11
+ - Both filtered white noise and ideal spectrum methods
12
+ - Fast autocorrelation and PSD computation
13
+ - Spectral moment analysis
14
+
15
+ Quick Start
16
+ -----------
17
+ >>> import numpy as np
18
+ >>> from rfgen import selfaffine_field
19
+ >>> rng = np.random.default_rng(42)
20
+ >>> field = selfaffine_field(dim=2, N=256, Hurst=0.8, rng=rng)
21
+
22
+ References
23
+ ----------
24
+ Hu, Y.Z. and Tonder, K., 1992. Simulation of 3-D random rough surface by 2-D
25
+ digital filter and Fourier analysis. International Journal of Machine Tools
26
+ and Manufacture, 32(1-2), pp.83-90. DOI: 10.1016/0890-6955(92)90064-N
27
+
28
+ Author
29
+ ------
30
+ Vladislav Yastrebov, CNRS, Mines Paris - PSL, Centre des matériaux
31
+
32
+ License
33
+ -------
34
+ BSD-3-Clause
35
+ """
36
+
37
+ __version__ = "0.1.0"
38
+ __author__ = "Vladislav Yastrebov"
39
+
40
+ # Main generators
41
+ from .generators import (
42
+ selfaffine_field,
43
+ matern_field,
44
+ matern_spectrum,
45
+ )
46
+
47
+ # Analysis tools
48
+ from .analysis import (
49
+ # Autocorrelation
50
+ autocorrelation_1d,
51
+ autocorrelation_2d,
52
+ autocorrelation_nd,
53
+ correlation_length,
54
+ integral_correlation_length,
55
+ # Spectrum
56
+ psd_1d,
57
+ psd_2d,
58
+ psd_radial_average,
59
+ psd_along_axis,
60
+ fit_power_law,
61
+ estimate_hurst_exponent,
62
+ # Moments
63
+ spectral_moment,
64
+ spectral_moment_1d,
65
+ compute_standard_moments,
66
+ nayak_parameter,
67
+ rms_quantities,
68
+ summit_density_estimate,
69
+ )
70
+
71
+ __all__ = [
72
+ # Version
73
+ "__version__",
74
+ # Generators
75
+ "selfaffine_field",
76
+ "matern_field",
77
+ "matern_spectrum",
78
+ # Autocorrelation
79
+ "autocorrelation_1d",
80
+ "autocorrelation_2d",
81
+ "autocorrelation_nd",
82
+ "correlation_length",
83
+ "integral_correlation_length",
84
+ # Spectrum
85
+ "psd_1d",
86
+ "psd_2d",
87
+ "psd_radial_average",
88
+ "psd_along_axis",
89
+ "fit_power_law",
90
+ "estimate_hurst_exponent",
91
+ # Moments
92
+ "spectral_moment",
93
+ "spectral_moment_1d",
94
+ "compute_standard_moments",
95
+ "nayak_parameter",
96
+ "rms_quantities",
97
+ "summit_density_estimate",
98
+ ]
@@ -0,0 +1,56 @@
1
+ """
2
+ Analysis tools for random fields.
3
+
4
+ This module provides functions for analyzing random fields:
5
+
6
+ - Autocorrelation functions (1D, 2D, N-D)
7
+ - Power spectral density computation
8
+ - Spectral moments and derived quantities
9
+ """
10
+
11
+ from .autocorrelation import (
12
+ autocorrelation_1d,
13
+ autocorrelation_2d,
14
+ autocorrelation_nd,
15
+ correlation_length,
16
+ integral_correlation_length,
17
+ )
18
+ from .spectrum import (
19
+ psd_1d,
20
+ psd_2d,
21
+ psd_radial_average,
22
+ psd_along_axis,
23
+ fit_power_law,
24
+ estimate_hurst_exponent,
25
+ )
26
+ from .moments import (
27
+ spectral_moment,
28
+ spectral_moment_1d,
29
+ compute_standard_moments,
30
+ nayak_parameter,
31
+ rms_quantities,
32
+ summit_density_estimate,
33
+ )
34
+
35
+ __all__ = [
36
+ # Autocorrelation
37
+ "autocorrelation_1d",
38
+ "autocorrelation_2d",
39
+ "autocorrelation_nd",
40
+ "correlation_length",
41
+ "integral_correlation_length",
42
+ # Spectrum
43
+ "psd_1d",
44
+ "psd_2d",
45
+ "psd_radial_average",
46
+ "psd_along_axis",
47
+ "fit_power_law",
48
+ "estimate_hurst_exponent",
49
+ # Moments
50
+ "spectral_moment",
51
+ "spectral_moment_1d",
52
+ "compute_standard_moments",
53
+ "nayak_parameter",
54
+ "rms_quantities",
55
+ "summit_density_estimate",
56
+ ]
@@ -0,0 +1,230 @@
1
+ """
2
+ Autocorrelation function computation for random fields.
3
+
4
+ Provides fast FFT-based computation of autocorrelation functions
5
+ for 1D profiles and 2D/3D fields.
6
+
7
+ Author: Vladislav Yastrebov, CNRS, Mines Paris - PSL, Centre des matériaux
8
+ License: BSD-3-Clause
9
+ """
10
+
11
+ import numpy as np
12
+ from numpy.fft import fft, ifft, fft2, ifft2, fftn, ifftn
13
+
14
+
15
+ def autocorrelation_1d(signal: np.ndarray, normalize: bool = True) -> np.ndarray:
16
+ """
17
+ Compute the autocorrelation function of a 1D signal using FFT.
18
+
19
+ Uses the Wiener-Khinchin theorem: the autocorrelation is the inverse
20
+ Fourier transform of the power spectral density.
21
+
22
+ Parameters
23
+ ----------
24
+ signal : ndarray
25
+ 1D input signal.
26
+ normalize : bool, optional
27
+ If True, normalize so R(0) = 1. Default is True.
28
+
29
+ Returns
30
+ -------
31
+ R : ndarray
32
+ Autocorrelation function. Same length as input.
33
+
34
+ Examples
35
+ --------
36
+ >>> import numpy as np
37
+ >>> from randomfield.analysis import autocorrelation_1d
38
+ >>> signal = np.random.randn(1024)
39
+ >>> R = autocorrelation_1d(signal)
40
+ >>> R[0] # Should be 1.0 if normalized
41
+ 1.0
42
+ """
43
+ signal = np.asarray(signal)
44
+ if signal.ndim != 1:
45
+ raise ValueError(f"Expected 1D array, got shape {signal.shape}")
46
+
47
+ N = len(signal)
48
+ zhat = fft(signal)
49
+ psd = zhat * np.conj(zhat) / N**2
50
+ R = np.real(ifft(psd))
51
+
52
+ if normalize and R[0] != 0:
53
+ R = R / R[0]
54
+
55
+ return R
56
+
57
+
58
+ def autocorrelation_2d(field: np.ndarray, normalize: bool = True) -> np.ndarray:
59
+ """
60
+ Compute the 2D autocorrelation function of a field using FFT.
61
+
62
+ Uses the Wiener-Khinchin theorem: the autocorrelation is the inverse
63
+ Fourier transform of the power spectral density.
64
+
65
+ Parameters
66
+ ----------
67
+ field : ndarray
68
+ 2D input field with shape (Ny, Nx).
69
+ normalize : bool, optional
70
+ If True, normalize so R(0,0) = 1. Default is True.
71
+
72
+ Returns
73
+ -------
74
+ R : ndarray
75
+ 2D autocorrelation function. Same shape as input.
76
+
77
+ Examples
78
+ --------
79
+ >>> import numpy as np
80
+ >>> from randomfield.analysis import autocorrelation_2d
81
+ >>> field = np.random.randn(128, 128)
82
+ >>> R = autocorrelation_2d(field)
83
+ >>> R[0, 0] # Should be 1.0 if normalized
84
+ 1.0
85
+ """
86
+ field = np.asarray(field)
87
+ if field.ndim != 2:
88
+ raise ValueError(f"Expected 2D array, got shape {field.shape}")
89
+
90
+ Ny, Nx = field.shape
91
+ zhat = fft2(field)
92
+ psd = zhat * np.conj(zhat) / (Nx * Ny) ** 2
93
+ R = np.real(ifft2(psd))
94
+
95
+ if normalize and R[0, 0] != 0:
96
+ R = R / R[0, 0]
97
+
98
+ return R
99
+
100
+
101
+ def autocorrelation_nd(field: np.ndarray, normalize: bool = True) -> np.ndarray:
102
+ """
103
+ Compute the N-dimensional autocorrelation function using FFT.
104
+
105
+ Parameters
106
+ ----------
107
+ field : ndarray
108
+ Input field of any dimension.
109
+ normalize : bool, optional
110
+ If True, normalize so R at origin = 1. Default is True.
111
+
112
+ Returns
113
+ -------
114
+ R : ndarray
115
+ Autocorrelation function. Same shape as input.
116
+
117
+ Examples
118
+ --------
119
+ >>> import numpy as np
120
+ >>> from randomfield.analysis import autocorrelation_nd
121
+ >>> field = np.random.randn(32, 32, 32)
122
+ >>> R = autocorrelation_nd(field)
123
+ >>> R[0, 0, 0] # Should be 1.0 if normalized
124
+ 1.0
125
+ """
126
+ field = np.asarray(field)
127
+ N_total = field.size
128
+ zhat = fftn(field)
129
+ psd = zhat * np.conj(zhat) / N_total**2
130
+ R = np.real(ifftn(psd))
131
+
132
+ if normalize:
133
+ origin = tuple(0 for _ in range(field.ndim))
134
+ if R[origin] != 0:
135
+ R = R / R[origin]
136
+
137
+ return R
138
+
139
+
140
+ def correlation_length(
141
+ R: np.ndarray,
142
+ threshold: float = 0.0,
143
+ spacing: float = 1.0,
144
+ ) -> float:
145
+ """
146
+ Estimate the correlation length from an autocorrelation function.
147
+
148
+ The correlation length is defined as the distance at which the
149
+ autocorrelation first drops below the threshold value.
150
+
151
+ Parameters
152
+ ----------
153
+ R : ndarray
154
+ 1D autocorrelation function (assumed normalized, R[0] = 1).
155
+ threshold : float, optional
156
+ Threshold value for determining correlation length. Default is 0.0
157
+ (first zero crossing).
158
+ spacing : float, optional
159
+ Grid spacing for converting index to physical distance. Default is 1.0.
160
+
161
+ Returns
162
+ -------
163
+ l_corr : float
164
+ Estimated correlation length.
165
+
166
+ Examples
167
+ --------
168
+ >>> import numpy as np
169
+ >>> from randomfield.analysis import autocorrelation_1d, correlation_length
170
+ >>> from randomfield.generators import periodic_gaussian_random_field
171
+ >>> field = periodic_gaussian_random_field(dim=1, N=1024, k_low=0.01, k_high=0.3)
172
+ >>> R = autocorrelation_1d(field)
173
+ >>> l_corr = correlation_length(R, spacing=1.0/1024)
174
+ """
175
+ R = np.asarray(R).flatten()
176
+
177
+ # Find first crossing below threshold
178
+ for i, r in enumerate(R):
179
+ if r < threshold:
180
+ # Linear interpolation for better estimate
181
+ if i > 0:
182
+ r_prev = R[i - 1]
183
+ # Interpolate: find x where R(x) = threshold
184
+ frac = (r_prev - threshold) / (r_prev - r + 1e-30)
185
+ return (i - 1 + frac) * spacing
186
+ return i * spacing
187
+
188
+ # If never crosses, return half the domain
189
+ return len(R) * spacing / 2
190
+
191
+
192
+ def integral_correlation_length(
193
+ R: np.ndarray,
194
+ spacing: float = 1.0,
195
+ ) -> float:
196
+ """
197
+ Compute the integral correlation length.
198
+
199
+ The integral correlation length is defined as:
200
+ L = ∫₀^∞ R(r) dr
201
+
202
+ For discrete data, we integrate up to the first zero crossing
203
+ to avoid issues with oscillating tails.
204
+
205
+ Parameters
206
+ ----------
207
+ R : ndarray
208
+ 1D autocorrelation function (assumed normalized, R[0] = 1).
209
+ spacing : float, optional
210
+ Grid spacing. Default is 1.0.
211
+
212
+ Returns
213
+ -------
214
+ L : float
215
+ Integral correlation length.
216
+ """
217
+ R = np.asarray(R).flatten()
218
+
219
+ # Find first zero crossing
220
+ n_integrate = len(R)
221
+ for i, r in enumerate(R):
222
+ if r < 0:
223
+ n_integrate = i
224
+ break
225
+
226
+ # Integrate using trapezoidal rule
227
+ L = np.trapz(R[:n_integrate], dx=spacing)
228
+
229
+ return L
230
+
@@ -0,0 +1,330 @@
1
+ """
2
+ Spectral moments computation for random fields.
3
+
4
+ Spectral moments are important statistical descriptors of random surfaces
5
+ and fields, used in tribology, contact mechanics, and surface metrology.
6
+
7
+ The spectral moments m_{ij} are defined as:
8
+ m_{ij} = ∫∫ kx^i * ky^j * Φ(kx, ky) dkx dky
9
+
10
+ where Φ is the power spectral density.
11
+
12
+ References:
13
+ Nayak, P.R., 1971. Random process model of rough surfaces.
14
+ Journal of Lubrication Technology, 93(3), pp.398-407.
15
+
16
+ Greenwood, J.A., 1984. A unified theory of surface roughness.
17
+ Proceedings of the Royal Society A, 393(1804), pp.133-157.
18
+
19
+ Author: Vladislav Yastrebov, CNRS, Mines Paris - PSL, Centre des matériaux
20
+ License: BSD-3-Clause
21
+ """
22
+
23
+ import numpy as np
24
+ from numpy.fft import fft2, fftfreq
25
+
26
+
27
+ def spectral_moment(
28
+ field: np.ndarray,
29
+ i: int,
30
+ j: int,
31
+ spacing: float = 1.0,
32
+ ) -> float:
33
+ """
34
+ Compute the spectral moment m_{ij} of a 2D field.
35
+
36
+ The spectral moment is defined as:
37
+ m_{ij} = ∫∫ |kx|^i * |ky|^j * Φ(kx, ky) dkx dky
38
+
39
+ where Φ is the power spectral density.
40
+
41
+ Parameters
42
+ ----------
43
+ field : ndarray
44
+ 2D input field.
45
+ i : int
46
+ Power of kx (non-negative integer).
47
+ j : int
48
+ Power of ky (non-negative integer).
49
+ spacing : float, optional
50
+ Grid spacing. Default is 1.0.
51
+
52
+ Returns
53
+ -------
54
+ m_ij : float
55
+ The spectral moment m_{ij}.
56
+
57
+ Examples
58
+ --------
59
+ >>> import numpy as np
60
+ >>> from randomfield.analysis import spectral_moment
61
+ >>> from randomfield.generators import periodic_gaussian_random_field
62
+ >>> field = periodic_gaussian_random_field(N=256, Hurst=0.8)
63
+ >>> m00 = spectral_moment(field, 0, 0) # Variance
64
+ >>> m20 = spectral_moment(field, 2, 0) # Related to mean square slope in x
65
+ """
66
+ field = np.asarray(field)
67
+ if field.ndim != 2:
68
+ raise ValueError(f"Expected 2D array, got shape {field.shape}")
69
+ if i < 0 or j < 0:
70
+ raise ValueError("Moment indices must be non-negative")
71
+
72
+ Ny, Nx = field.shape
73
+ dk = 1.0 / spacing # Frequency resolution
74
+
75
+ # Compute PSD
76
+ zhat = fft2(field)
77
+ psd = np.abs(zhat) ** 2 / (Nx * Ny) ** 2
78
+
79
+ # Frequency arrays
80
+ kx = fftfreq(Nx, d=spacing)
81
+ ky = fftfreq(Ny, d=spacing)
82
+ kx_grid, ky_grid = np.meshgrid(kx, ky)
83
+
84
+ # Use absolute values for moments (symmetric about origin)
85
+ kx_power = np.abs(kx_grid) ** i if i > 0 else np.ones_like(kx_grid)
86
+ ky_power = np.abs(ky_grid) ** j if j > 0 else np.ones_like(ky_grid)
87
+
88
+ # Integrate
89
+ m_ij = np.sum(kx_power * ky_power * psd) * (dk / Nx) * (dk / Ny)
90
+
91
+ return float(m_ij)
92
+
93
+
94
+ def spectral_moment_1d(
95
+ signal: np.ndarray,
96
+ n: int,
97
+ spacing: float = 1.0,
98
+ ) -> float:
99
+ """
100
+ Compute the n-th spectral moment of a 1D signal.
101
+
102
+ The spectral moment is defined as:
103
+ m_n = ∫ |k|^n * Φ(k) dk
104
+
105
+ Parameters
106
+ ----------
107
+ signal : ndarray
108
+ 1D input signal.
109
+ n : int
110
+ Order of the moment (non-negative integer).
111
+ spacing : float, optional
112
+ Grid spacing. Default is 1.0.
113
+
114
+ Returns
115
+ -------
116
+ m_n : float
117
+ The n-th spectral moment.
118
+ """
119
+ signal = np.asarray(signal)
120
+ if signal.ndim != 1:
121
+ raise ValueError(f"Expected 1D array, got shape {signal.shape}")
122
+ if n < 0:
123
+ raise ValueError("Moment order must be non-negative")
124
+
125
+ N = len(signal)
126
+ dk = 1.0 / (N * spacing)
127
+
128
+ # Compute PSD
129
+ zhat = np.fft.fft(signal)
130
+ psd = np.abs(zhat) ** 2 / N**2
131
+
132
+ # Frequencies
133
+ k = fftfreq(N, d=spacing)
134
+
135
+ # Use absolute values
136
+ k_power = np.abs(k) ** n if n > 0 else np.ones_like(k)
137
+
138
+ # Integrate
139
+ m_n = np.sum(k_power * psd) * dk
140
+
141
+ return float(m_n)
142
+
143
+
144
+ def compute_standard_moments(
145
+ field: np.ndarray,
146
+ spacing: float = 1.0,
147
+ ) -> dict[str, float]:
148
+ """
149
+ Compute standard spectral moments for a 2D surface.
150
+
151
+ Computes m00, m10, m01, m20, m02, m11, m40, m04, m22 which are
152
+ commonly used in surface roughness characterization.
153
+
154
+ Parameters
155
+ ----------
156
+ field : ndarray
157
+ 2D input field.
158
+ spacing : float, optional
159
+ Grid spacing. Default is 1.0.
160
+
161
+ Returns
162
+ -------
163
+ moments : dict
164
+ Dictionary with moment names as keys and values.
165
+
166
+ Notes
167
+ -----
168
+ Physical interpretations:
169
+ - m00: Variance of the surface heights
170
+ - m20, m02: Related to mean square slopes
171
+ - m40, m04: Related to mean square curvatures
172
+ - m11: Cross-correlation of slopes
173
+
174
+ For isotropic surfaces: m20 ≈ m02, m40 ≈ m04.
175
+ """
176
+ moments = {}
177
+
178
+ # Zero-order moment (variance)
179
+ moments["m00"] = spectral_moment(field, 0, 0, spacing)
180
+
181
+ # First-order moments
182
+ moments["m10"] = spectral_moment(field, 1, 0, spacing)
183
+ moments["m01"] = spectral_moment(field, 0, 1, spacing)
184
+
185
+ # Second-order moments
186
+ moments["m20"] = spectral_moment(field, 2, 0, spacing)
187
+ moments["m02"] = spectral_moment(field, 0, 2, spacing)
188
+ moments["m11"] = spectral_moment(field, 1, 1, spacing)
189
+
190
+ # Fourth-order moments
191
+ moments["m40"] = spectral_moment(field, 4, 0, spacing)
192
+ moments["m04"] = spectral_moment(field, 0, 4, spacing)
193
+ moments["m22"] = spectral_moment(field, 2, 2, spacing)
194
+
195
+ return moments
196
+
197
+
198
+ def nayak_parameter(field: np.ndarray, spacing: float = 1.0) -> float:
199
+ """
200
+ Compute Nayak's bandwidth parameter α.
201
+
202
+ The bandwidth parameter is defined as:
203
+ α = m0 * m4 / m2²
204
+
205
+ where m0, m2, m4 are isotropic spectral moments.
206
+
207
+ For Gaussian surfaces: α ≥ 1, with α = 1 for a narrow-band surface.
208
+
209
+ Parameters
210
+ ----------
211
+ field : ndarray
212
+ 2D input field.
213
+ spacing : float, optional
214
+ Grid spacing. Default is 1.0.
215
+
216
+ Returns
217
+ -------
218
+ alpha : float
219
+ Nayak's bandwidth parameter.
220
+
221
+ References
222
+ ----------
223
+ Nayak, P.R., 1971. Random process model of rough surfaces.
224
+ Journal of Lubrication Technology, 93(3), pp.398-407.
225
+ """
226
+ m00 = spectral_moment(field, 0, 0, spacing)
227
+ m20 = spectral_moment(field, 2, 0, spacing)
228
+ m02 = spectral_moment(field, 0, 2, spacing)
229
+ m40 = spectral_moment(field, 4, 0, spacing)
230
+ m04 = spectral_moment(field, 0, 4, spacing)
231
+
232
+ # Isotropic averages
233
+ m2 = 0.5 * (m20 + m02)
234
+ m4 = 0.5 * (m40 + m04)
235
+
236
+ if m2 == 0:
237
+ return np.inf
238
+
239
+ alpha = m00 * m4 / m2**2
240
+
241
+ return alpha
242
+
243
+
244
+ def rms_quantities(field: np.ndarray, spacing: float = 1.0) -> dict[str, float]:
245
+ """
246
+ Compute RMS surface quantities from spectral moments.
247
+
248
+ Parameters
249
+ ----------
250
+ field : ndarray
251
+ 2D input field.
252
+ spacing : float, optional
253
+ Grid spacing. Default is 1.0.
254
+
255
+ Returns
256
+ -------
257
+ quantities : dict
258
+ Dictionary containing:
259
+ - 'rms_height': RMS height (σ)
260
+ - 'rms_slope_x': RMS slope in x direction
261
+ - 'rms_slope_y': RMS slope in y direction
262
+ - 'rms_slope': Isotropic RMS slope
263
+ - 'rms_curvature_x': RMS curvature in x direction
264
+ - 'rms_curvature_y': RMS curvature in y direction
265
+ - 'rms_curvature': Isotropic RMS curvature
266
+ """
267
+ m00 = spectral_moment(field, 0, 0, spacing)
268
+ m20 = spectral_moment(field, 2, 0, spacing)
269
+ m02 = spectral_moment(field, 0, 2, spacing)
270
+ m40 = spectral_moment(field, 4, 0, spacing)
271
+ m04 = spectral_moment(field, 0, 4, spacing)
272
+
273
+ # Prefactor for slopes and curvatures from spectral moments
274
+ # Slope: ∂z/∂x has PSD = (2πkx)² * Φ(kx,ky)
275
+ # Curvature: ∂²z/∂x² has PSD = (2πkx)⁴ * Φ(kx,ky)
276
+ two_pi_sq = (2 * np.pi) ** 2
277
+ two_pi_4 = (2 * np.pi) ** 4
278
+
279
+ quantities = {
280
+ "rms_height": np.sqrt(m00),
281
+ "rms_slope_x": np.sqrt(two_pi_sq * m20),
282
+ "rms_slope_y": np.sqrt(two_pi_sq * m02),
283
+ "rms_slope": np.sqrt(two_pi_sq * 0.5 * (m20 + m02)),
284
+ "rms_curvature_x": np.sqrt(two_pi_4 * m40),
285
+ "rms_curvature_y": np.sqrt(two_pi_4 * m04),
286
+ "rms_curvature": np.sqrt(two_pi_4 * 0.5 * (m40 + m04)),
287
+ }
288
+
289
+ return quantities
290
+
291
+
292
+ def summit_density_estimate(field: np.ndarray, spacing: float = 1.0) -> float:
293
+ """
294
+ Estimate the density of summits (local maxima) per unit area.
295
+
296
+ Based on random process theory for Gaussian surfaces:
297
+ D_s = (1 / 6π√3) * √(m4/m2)
298
+
299
+ Parameters
300
+ ----------
301
+ field : ndarray
302
+ 2D input field.
303
+ spacing : float, optional
304
+ Grid spacing. Default is 1.0.
305
+
306
+ Returns
307
+ -------
308
+ D_s : float
309
+ Summit density (number per unit area).
310
+
311
+ Notes
312
+ -----
313
+ This is a statistical estimate valid for isotropic Gaussian surfaces.
314
+ The actual count of local maxima may differ.
315
+ """
316
+ m20 = spectral_moment(field, 2, 0, spacing)
317
+ m02 = spectral_moment(field, 0, 2, spacing)
318
+ m40 = spectral_moment(field, 4, 0, spacing)
319
+ m04 = spectral_moment(field, 0, 4, spacing)
320
+
321
+ m2 = 0.5 * (m20 + m02)
322
+ m4 = 0.5 * (m40 + m04)
323
+
324
+ if m2 <= 0:
325
+ return 0.0
326
+
327
+ D_s = (1.0 / (6 * np.pi * np.sqrt(3))) * np.sqrt(m4 / m2)
328
+
329
+ return D_s
330
+