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 +98 -0
- rfgen/analysis/__init__.py +56 -0
- rfgen/analysis/autocorrelation.py +230 -0
- rfgen/analysis/moments.py +330 -0
- rfgen/analysis/spectrum.py +340 -0
- rfgen/generators/__init__.py +22 -0
- rfgen/generators/matern.py +283 -0
- rfgen/generators/selfaffine.py +221 -0
- rfgen/py.typed +0 -0
- rfgen-0.1.0.dist-info/METADATA +167 -0
- rfgen-0.1.0.dist-info/RECORD +14 -0
- rfgen-0.1.0.dist-info/WHEEL +5 -0
- rfgen-0.1.0.dist-info/licenses/LICENSE +30 -0
- rfgen-0.1.0.dist-info/top_level.txt +1 -0
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
|
+
|