Coverage for dynasor/tools/acfs.py: 98%

80 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-10 08:27 +0000

1""" 

2A number of utility functions, for example for dealing with 

3autocorrelation functions, Fourier transforms, and smoothing. 

4""" 

5 

6from typing import Optional 

7import numpy as np 

8from scipy.signal import correlate 

9from numpy.typing import NDArray 

10import pandas as pd 

11 

12 

13def psd_from_acf( 

14 acf: NDArray[float], 

15 dt: Optional[float] = 1, 

16 even: Optional[bool] = True, 

17) -> tuple[NDArray[float], NDArray[float]]: 

18 """Computes the power spectral density (PSD) from an auto-correlation function (ACF). 

19 

20 Let x(t) be a time signal and define its auto-correlation function 

21 

22 C(τ) = ⟨ x(t) x(t+τ) ⟩ 

23 

24 where ⟨·⟩ denotes a time or ensemble average. According to the 

25 Wiener–Khinchin theorem, the power spectral density (PSD) of x(t) is 

26 the Fourier transform of its auto-correlation function: 

27 

28 S(ω) = ∫ C(τ) e^{-i ω τ} dτ 

29 

30 This function computes the PSD by performing a discrete Fourier 

31 transform of the provided ACF. 

32 

33 Parameters 

34 ---------- 

35 acf 

36 The auto-correlation function C(τ) evaluated at discrete time lags. 

37 dt 

38 Time spacing between consecutive samples in the ACF. 

39 even 

40 Whether the ACF is assumed to be even in time. If True, the ACF 

41 is mirrored to construct a two-sided correlation function before 

42 computing the Fourier transform. 

43 

44 Returns 

45 ------- 

46 freqs, psd 

47 Angular frequencies ω and corresponding PSD values. 

48 """ 

49 if dt <= 0: 

50 raise ValueError('dt must be positive.') 

51 assert acf.ndim == 1 

52 if np.iscomplexobj(acf): 

53 raise ValueError('acf must be real.') 

54 

55 signal = np.asarray(acf) 

56 

57 if even: 

58 signal = np.hstack((signal, signal[:0:-1])) 

59 

60 fft = dt * np.fft.rfft(signal) 

61 psd = fft.real if even else fft 

62 

63 freqs = np.fft.rfftfreq(len(signal), dt) 

64 freqs = 2 * np.pi * freqs 

65 

66 return freqs, psd 

67 

68 

69def psd_from_acf_2d( 

70 acf: NDArray[float], 

71 dt: Optional[float] = 1, 

72 even: Optional[bool] = True, 

73) -> tuple[NDArray[float], NDArray[float]]: 

74 """Computes the power spectral density (PSD) from an auto-correlation function (ACF). 

75 

76 This is the 2D analogue of `psd_from_acf`, where the ACF is provided for 

77 multiple signals, e.g. F(q, t). The Fourier transform is performed along 

78 the last axis. 

79 

80 Parameters 

81 ---------- 

82 acf 

83 The ACF as a 2D array with shape (N, Nt), where the last axis corresponds 

84 to time lags. 

85 dt 

86 The time step between samples. 

87 even 

88 Whether the ACF is even in time and should be mirrored before computing 

89 the Fourier transform. 

90 

91 Returns 

92 ------- 

93 freqs, psd 

94 Angular frequencies ω and corresponding PSD values with shape (N, Nω). 

95 """ 

96 if dt <= 0: 

97 raise ValueError('dt must be positive.') 

98 assert acf.ndim == 2 

99 if np.iscomplexobj(acf): 

100 raise ValueError('acf must be real.') 

101 

102 signal = np.asarray(acf) 

103 

104 if even: 

105 signal = np.hstack((signal, signal[:, :0:-1])) 

106 

107 fft = dt * np.fft.rfft(signal, axis=1) 

108 psd = fft.real if even else fft 

109 

110 freqs = np.fft.rfftfreq(signal.shape[1], dt) 

111 freqs = 2 * np.pi * freqs 

112 

113 return freqs, psd 

114 

115 

116def psd_from_time_signal( 

117 x: NDArray[float], 

118 dt: Optional[float] = 1, 

119 complex: Optional[bool] = False, 

120) -> tuple[NDArray[float], NDArray[float]]: 

121 """Computes the power spectral density (PSD) directly from a time signal. 

122 

123 Let x(t) be a time signal. The power spectral density (PSD) can be 

124 computed directly from the Fourier transform of the signal: 

125 

126 ``S(ω) = |F[x(t)]|²`` 

127 

128 where F denotes the Fourier transform. 

129 

130 This function computes the PSD by performing a discrete Fourier 

131 transform of the time signal and taking the squared magnitude of 

132 the transform. 

133 

134 This is equivalent to computing the PSD from the Fourier transform of the 

135 auto-correlation function. 

136 

137 Parameters 

138 ---------- 

139 x 

140 Time signal as a 1D array. 

141 dt 

142 Time spacing between consecutive samples. 

143 complex 

144 Whether the time signal is complex. If False (default), the signal is 

145 assumed to be real and the PSD is computed using a real FFT (`rfft`), 

146 returning only non-negative frequencies. If True, the full FFT (`fft`) 

147 is used and both positive and negative frequencies are returned. 

148 

149 Returns 

150 ------- 

151 freqs, psd 

152 Angular frequencies ω and corresponding PSD values. 

153 """ 

154 if dt <= 0: 

155 raise ValueError('dt must be positive.') 

156 assert x.ndim == 1 

157 

158 signal = np.asarray(x) 

159 N = len(signal) 

160 

161 if complex: 

162 fft = np.fft.fft(signal) 

163 freqs = np.fft.fftfreq(N, dt) 

164 else: 

165 fft = np.fft.rfft(signal) 

166 freqs = np.fft.rfftfreq(N, dt) 

167 

168 psd = dt * np.abs(fft) ** 2 / N 

169 freqs = 2 * np.pi * freqs 

170 

171 return freqs, psd 

172 

173 

174def compute_acf( 

175 Z: NDArray[float], 

176 delta_t: Optional[float] = 1.0, 

177 method: Optional[str] = 'scipy', 

178) -> tuple[NDArray[float], NDArray[float]]: 

179 r""" 

180 Computes the autocorrelation function (ACF) for a one-dimensional signal :math:`Z` in time as 

181 

182 .. math:: 

183 

184 \text{ACF}(\tau) = \frac{\left < Z(t) Z^*(t+\tau) \right >}{\left < Z(t) Z^*(t) \right >} 

185 

186 Here, only the real part of the ACF is returned since if :math:`Z` is complex 

187 the imaginary part should average out to zero for any stationary signal. 

188 

189 Parameters 

190 ---------- 

191 Z 

192 Complex time signal. 

193 delta_t 

194 Spacing in time between two consecutive values in :math:`Z`. 

195 method 

196 Implementation to use; possible values: `numpy` and `scipy` (default and usually faster). 

197 

198 Returns 

199 ------- 

200 time_lags, acf 

201 Time lags τ and the corresponding normalized ACF values. 

202 """ 

203 

204 if delta_t <= 0: 

205 raise ValueError('delta_t must be positive.') 

206 

207 # keep only real part and normalize 

208 acf = _compute_correlation_function(Z, Z, method) 

209 acf = np.real(acf) 

210 if acf[0] == 0: 

211 raise ValueError('Cannot normalize ACF; zero-lag autocorrelation (acf[0]) is zero') 

212 

213 acf /= acf[0] 

214 

215 time_lags = delta_t * np.arange(0, len(acf), 1) 

216 return time_lags, acf 

217 

218 

219def _compute_correlation_function(Z1, Z2, method: Optional[str] = 'scipy'): 

220 N = len(Z1) 

221 assert len(Z1) == len(Z2) 

222 if method == 'scipy': 

223 cf = correlate(Z1, Z2, mode='full')[N - 1:] / np.arange(N, 0, -1) 

224 elif method == 'numpy': 

225 cf = np.correlate(Z1, Z2, mode='full')[N - 1:] / np.arange(N, 0, -1) 

226 else: 

227 raise ValueError('method must be either numpy or scipy') 

228 return cf 

229 

230 

231# smoothing functions / FFT filters 

232# ------------------------------------- 

233def gaussian_decay(t: NDArray[float], t_sigma: float) -> NDArray[float]: 

234 r""" 

235 Evaluates a gaussian distribution in time :math:`f(t)`, which can be applied to an ACF in time 

236 to artificially damp it, i.e., forcing it to go to zero for long times. 

237 

238 .. math:: 

239 

240 f(t) = \exp{\left [-\frac{1}{2} \left (\frac{t}{t_\mathrm{sigma}}\right )^2 \right ] } 

241 

242 Parameters 

243 ---------- 

244 t 

245 Time array. 

246 t_sigma 

247 Width (standard deviation of the gaussian) of the decay. 

248 """ 

249 

250 if t_sigma <= 0: 250 ↛ 252line 250 didn't jump to line 252 because the condition on line 250 was always true

251 raise ValueError('t_sigma must be positive.') 

252 return np.exp(- 1 / 2 * (t / t_sigma) ** 2) 

253 

254 

255def fermi_dirac(t: NDArray[float], t_0: float, t_width: float) -> NDArray[float]: 

256 r""" 

257 Evaluates a Fermi-Dirac-like function in time :math:`f(t)`, which can be applied to an 

258 auto-correlation function (ACF) in time to artificially dampen it, i.e., forcing it to 

259 go to zero for long times without affecting the short-time correlations too much. 

260 

261 .. math:: 

262 

263 f(t) = \frac{1}{\exp{[(t-t_0)/t_\mathrm{width}}] + 1} 

264 

265 Parameters 

266 ---------- 

267 t 

268 Time array. 

269 t_0 

270 Starting time for decay. 

271 t_width 

272 Width of the decay. 

273 

274 """ 

275 if t_width <= 0: 

276 raise ValueError('t_width must be positive.') 

277 return 1.0 / (np.exp((t - t_0) / t_width) + 1) 

278 

279 

280def smoothing_function( 

281 data: NDArray[float], 

282 window_size: int, 

283 window_type: Optional[str] = 'hamming', 

284) -> NDArray[float]: 

285 """ 

286 Smoothing function for 1D arrays. 

287 This functions employs the pandas rolling window average function. 

288 

289 Parameters 

290 ---------- 

291 data 

292 1D data array. 

293 window_size 

294 The size of smoothing/smearing window. 

295 window_type 

296 What type of window-shape to use, e.g. ``'blackman'``, ``'hamming'``, ``'boxcar'`` 

297 (see pandas and SciPy documentation for more details). 

298 

299 """ 

300 if window_size <= 0: 

301 raise ValueError('window_size must be a positive integer.') 

302 series = pd.Series(data) 

303 new_data = series.rolling(window_size, win_type=window_type, center=True, min_periods=1).mean() 

304 return np.array(new_data)