_czt.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575
  1. # This program is public domain
  2. # Authors: Paul Kienzle, Nadav Horesh
  3. """
  4. Chirp z-transform.
  5. We provide two interfaces to the chirp z-transform: an object interface
  6. which precalculates part of the transform and can be applied efficiently
  7. to many different data sets, and a functional interface which is applied
  8. only to the given data set.
  9. Transforms
  10. ----------
  11. CZT : callable (x, axis=-1) -> array
  12. Define a chirp z-transform that can be applied to different signals.
  13. ZoomFFT : callable (x, axis=-1) -> array
  14. Define a Fourier transform on a range of frequencies.
  15. Functions
  16. ---------
  17. czt : array
  18. Compute the chirp z-transform for a signal.
  19. zoom_fft : array
  20. Compute the Fourier transform on a range of frequencies.
  21. """
  22. import cmath
  23. import numbers
  24. import numpy as np
  25. from numpy import pi, arange
  26. from scipy.fft import fft, ifft, next_fast_len
  27. __all__ = ['czt', 'zoom_fft', 'CZT', 'ZoomFFT', 'czt_points']
  28. def _validate_sizes(n, m):
  29. if n < 1 or not isinstance(n, numbers.Integral):
  30. raise ValueError('Invalid number of CZT data '
  31. f'points ({n}) specified. '
  32. 'n must be positive and integer type.')
  33. if m is None:
  34. m = n
  35. elif m < 1 or not isinstance(m, numbers.Integral):
  36. raise ValueError('Invalid number of CZT output '
  37. f'points ({m}) specified. '
  38. 'm must be positive and integer type.')
  39. return m
  40. def czt_points(m, w=None, a=1+0j):
  41. """
  42. Return the points at which the chirp z-transform is computed.
  43. Parameters
  44. ----------
  45. m : int
  46. The number of points desired.
  47. w : complex, optional
  48. The ratio between points in each step.
  49. Defaults to equally spaced points around the entire unit circle.
  50. a : complex, optional
  51. The starting point in the complex plane. Default is 1+0j.
  52. Returns
  53. -------
  54. out : ndarray
  55. The points in the Z plane at which `CZT` samples the z-transform,
  56. when called with arguments `m`, `w`, and `a`, as complex numbers.
  57. See Also
  58. --------
  59. CZT : Class that creates a callable chirp z-transform function.
  60. czt : Convenience function for quickly calculating CZT.
  61. Examples
  62. --------
  63. Plot the points of a 16-point FFT:
  64. >>> import numpy as np
  65. >>> from scipy.signal import czt_points
  66. >>> points = czt_points(16)
  67. >>> import matplotlib.pyplot as plt
  68. >>> plt.plot(points.real, points.imag, 'o')
  69. >>> plt.gca().add_patch(plt.Circle((0,0), radius=1, fill=False, alpha=.3))
  70. >>> plt.axis('equal')
  71. >>> plt.show()
  72. and a 91-point logarithmic spiral that crosses the unit circle:
  73. >>> m, w, a = 91, 0.995*np.exp(-1j*np.pi*.05), 0.8*np.exp(1j*np.pi/6)
  74. >>> points = czt_points(m, w, a)
  75. >>> plt.plot(points.real, points.imag, 'o')
  76. >>> plt.gca().add_patch(plt.Circle((0,0), radius=1, fill=False, alpha=.3))
  77. >>> plt.axis('equal')
  78. >>> plt.show()
  79. """
  80. m = _validate_sizes(1, m)
  81. k = arange(m)
  82. a = 1.0 * a # at least float
  83. if w is None:
  84. # Nothing specified, default to FFT
  85. return a * np.exp(2j * pi * k / m)
  86. else:
  87. # w specified
  88. w = 1.0 * w # at least float
  89. return a * w**-k
  90. class CZT:
  91. """
  92. Create a callable chirp z-transform function.
  93. Transform to compute the frequency response around a spiral.
  94. Objects of this class are callables which can compute the
  95. chirp z-transform on their inputs. This object precalculates the constant
  96. chirps used in the given transform.
  97. Parameters
  98. ----------
  99. n : int
  100. The size of the signal.
  101. m : int, optional
  102. The number of output points desired. Default is `n`.
  103. w : complex, optional
  104. The ratio between points in each step. This must be precise or the
  105. accumulated error will degrade the tail of the output sequence.
  106. Defaults to equally spaced points around the entire unit circle.
  107. a : complex, optional
  108. The starting point in the complex plane. Default is 1+0j.
  109. Returns
  110. -------
  111. f : CZT
  112. Callable object ``f(x, axis=-1)`` for computing the chirp z-transform
  113. on `x`.
  114. See Also
  115. --------
  116. czt : Convenience function for quickly calculating CZT.
  117. ZoomFFT : Class that creates a callable partial FFT function.
  118. Notes
  119. -----
  120. The defaults are chosen such that ``f(x)`` is equivalent to
  121. ``fft.fft(x)`` and, if ``m > len(x)``, that ``f(x, m)`` is equivalent to
  122. ``fft.fft(x, m)``.
  123. If `w` does not lie on the unit circle, then the transform will be
  124. around a spiral with exponentially-increasing radius. Regardless,
  125. angle will increase linearly.
  126. For transforms that do lie on the unit circle, accuracy is better when
  127. using `ZoomFFT`, since any numerical error in `w` is
  128. accumulated for long data lengths, drifting away from the unit circle.
  129. The chirp z-transform can be faster than an equivalent FFT with
  130. zero padding. Try it with your own array sizes to see.
  131. However, the chirp z-transform is considerably less precise than the
  132. equivalent zero-padded FFT.
  133. As this CZT is implemented using the Bluestein algorithm, it can compute
  134. large prime-length Fourier transforms in O(N log N) time, rather than the
  135. O(N**2) time required by the direct DFT calculation. (`scipy.fft` also
  136. uses Bluestein's algorithm'.)
  137. (The name "chirp z-transform" comes from the use of a chirp in the
  138. Bluestein algorithm. It does not decompose signals into chirps, like
  139. other transforms with "chirp" in the name.)
  140. References
  141. ----------
  142. .. [1] Leo I. Bluestein, "A linear filtering approach to the computation
  143. of the discrete Fourier transform," Northeast Electronics Research
  144. and Engineering Meeting Record 10, 218-219 (1968).
  145. .. [2] Rabiner, Schafer, and Rader, "The chirp z-transform algorithm and
  146. its application," Bell Syst. Tech. J. 48, 1249-1292 (1969).
  147. Examples
  148. --------
  149. Compute multiple prime-length FFTs:
  150. >>> from scipy.signal import CZT
  151. >>> import numpy as np
  152. >>> a = np.random.rand(7)
  153. >>> b = np.random.rand(7)
  154. >>> c = np.random.rand(7)
  155. >>> czt_7 = CZT(n=7)
  156. >>> A = czt_7(a)
  157. >>> B = czt_7(b)
  158. >>> C = czt_7(c)
  159. Display the points at which the FFT is calculated:
  160. >>> czt_7.points()
  161. array([ 1.00000000+0.j , 0.62348980+0.78183148j,
  162. -0.22252093+0.97492791j, -0.90096887+0.43388374j,
  163. -0.90096887-0.43388374j, -0.22252093-0.97492791j,
  164. 0.62348980-0.78183148j])
  165. >>> import matplotlib.pyplot as plt
  166. >>> plt.plot(czt_7.points().real, czt_7.points().imag, 'o')
  167. >>> plt.gca().add_patch(plt.Circle((0,0), radius=1, fill=False, alpha=.3))
  168. >>> plt.axis('equal')
  169. >>> plt.show()
  170. """
  171. def __init__(self, n, m=None, w=None, a=1+0j):
  172. m = _validate_sizes(n, m)
  173. k = arange(max(m, n), dtype=np.min_scalar_type(-max(m, n)**2))
  174. if w is None:
  175. # Nothing specified, default to FFT-like
  176. w = cmath.exp(-2j*pi/m)
  177. wk2 = np.exp(-(1j * pi * ((k**2) % (2*m))) / m)
  178. else:
  179. # w specified
  180. wk2 = w**(k**2/2.)
  181. a = 1.0 * a # at least float
  182. self.w, self.a = w, a
  183. self.m, self.n = m, n
  184. nfft = next_fast_len(n + m - 1)
  185. self._Awk2 = a**-k[:n] * wk2[:n]
  186. self._nfft = nfft
  187. self._Fwk2 = fft(1/np.hstack((wk2[n-1:0:-1], wk2[:m])), nfft)
  188. self._wk2 = wk2[:m]
  189. self._yidx = slice(n-1, n+m-1)
  190. def __call__(self, x, *, axis=-1):
  191. """
  192. Calculate the chirp z-transform of a signal.
  193. Parameters
  194. ----------
  195. x : array
  196. The signal to transform.
  197. axis : int, optional
  198. Axis over which to compute the FFT. If not given, the last axis is
  199. used.
  200. Returns
  201. -------
  202. out : ndarray
  203. An array of the same dimensions as `x`, but with the length of the
  204. transformed axis set to `m`.
  205. """
  206. x = np.asarray(x)
  207. if x.shape[axis] != self.n:
  208. raise ValueError(f"CZT defined for length {self.n}, not "
  209. f"{x.shape[axis]}")
  210. # Calculate transpose coordinates, to allow operation on any given axis
  211. trnsp = np.arange(x.ndim)
  212. trnsp[[axis, -1]] = [-1, axis]
  213. x = x.transpose(*trnsp)
  214. y = ifft(self._Fwk2 * fft(x*self._Awk2, self._nfft))
  215. y = y[..., self._yidx] * self._wk2
  216. return y.transpose(*trnsp)
  217. def points(self):
  218. """
  219. Return the points at which the chirp z-transform is computed.
  220. """
  221. return czt_points(self.m, self.w, self.a)
  222. class ZoomFFT(CZT):
  223. """
  224. Create a callable zoom FFT transform function.
  225. This is a specialization of the chirp z-transform (`CZT`) for a set of
  226. equally-spaced frequencies around the unit circle, used to calculate a
  227. section of the FFT more efficiently than calculating the entire FFT and
  228. truncating.
  229. Parameters
  230. ----------
  231. n : int
  232. The size of the signal.
  233. fn : array_like
  234. A length-2 sequence [`f1`, `f2`] giving the frequency range, or a
  235. scalar, for which the range [0, `fn`] is assumed.
  236. m : int, optional
  237. The number of points to evaluate. Default is `n`.
  238. fs : float, optional
  239. The sampling frequency. If ``fs=10`` represented 10 kHz, for example,
  240. then `f1` and `f2` would also be given in kHz.
  241. The default sampling frequency is 2, so `f1` and `f2` should be
  242. in the range [0, 1] to keep the transform below the Nyquist
  243. frequency.
  244. endpoint : bool, optional
  245. If True, `f2` is the last sample. Otherwise, it is not included.
  246. Default is False.
  247. Returns
  248. -------
  249. f : ZoomFFT
  250. Callable object ``f(x, axis=-1)`` for computing the zoom FFT on `x`.
  251. See Also
  252. --------
  253. zoom_fft : Convenience function for calculating a zoom FFT.
  254. Notes
  255. -----
  256. The defaults are chosen such that ``f(x, 2)`` is equivalent to
  257. ``fft.fft(x)`` and, if ``m > len(x)``, that ``f(x, 2, m)`` is equivalent to
  258. ``fft.fft(x, m)``.
  259. Sampling frequency is 1/dt, the time step between samples in the
  260. signal `x`. The unit circle corresponds to frequencies from 0 up
  261. to the sampling frequency. The default sampling frequency of 2
  262. means that `f1`, `f2` values up to the Nyquist frequency are in the
  263. range [0, 1). For `f1`, `f2` values expressed in radians, a sampling
  264. frequency of 2*pi should be used.
  265. Remember that a zoom FFT can only interpolate the points of the existing
  266. FFT. It cannot help to resolve two separate nearby frequencies.
  267. Frequency resolution can only be increased by increasing acquisition
  268. time.
  269. These functions are implemented using Bluestein's algorithm (as is
  270. `scipy.fft`). [2]_
  271. References
  272. ----------
  273. .. [1] Steve Alan Shilling, "A study of the chirp z-transform and its
  274. applications", pg 29 (1970)
  275. https://krex.k-state.edu/dspace/bitstream/handle/2097/7844/LD2668R41972S43.pdf
  276. .. [2] Leo I. Bluestein, "A linear filtering approach to the computation
  277. of the discrete Fourier transform," Northeast Electronics Research
  278. and Engineering Meeting Record 10, 218-219 (1968).
  279. Examples
  280. --------
  281. To plot the transform results use something like the following:
  282. >>> import numpy as np
  283. >>> from scipy.signal import ZoomFFT
  284. >>> t = np.linspace(0, 1, 1021)
  285. >>> x = np.cos(2*np.pi*15*t) + np.sin(2*np.pi*17*t)
  286. >>> f1, f2 = 5, 27
  287. >>> transform = ZoomFFT(len(x), [f1, f2], len(x), fs=1021)
  288. >>> X = transform(x)
  289. >>> f = np.linspace(f1, f2, len(x))
  290. >>> import matplotlib.pyplot as plt
  291. >>> plt.plot(f, 20*np.log10(np.abs(X)))
  292. >>> plt.show()
  293. """
  294. def __init__(self, n, fn, m=None, *, fs=2, endpoint=False):
  295. m = _validate_sizes(n, m)
  296. k = arange(max(m, n), dtype=np.min_scalar_type(-max(m, n)**2))
  297. if np.size(fn) == 2:
  298. f1, f2 = fn
  299. elif np.size(fn) == 1:
  300. f1, f2 = 0.0, fn
  301. else:
  302. raise ValueError('fn must be a scalar or 2-length sequence')
  303. self.f1, self.f2, self.fs = f1, f2, fs
  304. if endpoint:
  305. scale = ((f2 - f1) * m) / (fs * (m - 1))
  306. else:
  307. scale = (f2 - f1) / fs
  308. a = cmath.exp(2j * pi * f1/fs)
  309. wk2 = np.exp(-(1j * pi * scale * k**2) / m)
  310. self.w = cmath.exp(-2j*pi/m * scale)
  311. self.a = a
  312. self.m, self.n = m, n
  313. ak = np.exp(-2j * pi * f1/fs * k[:n])
  314. self._Awk2 = ak * wk2[:n]
  315. nfft = next_fast_len(n + m - 1)
  316. self._nfft = nfft
  317. self._Fwk2 = fft(1/np.hstack((wk2[n-1:0:-1], wk2[:m])), nfft)
  318. self._wk2 = wk2[:m]
  319. self._yidx = slice(n-1, n+m-1)
  320. def czt(x, m=None, w=None, a=1+0j, *, axis=-1):
  321. """
  322. Compute the frequency response around a spiral in the Z plane.
  323. Parameters
  324. ----------
  325. x : array
  326. The signal to transform.
  327. m : int, optional
  328. The number of output points desired. Default is the length of the
  329. input data.
  330. w : complex, optional
  331. The ratio between points in each step. This must be precise or the
  332. accumulated error will degrade the tail of the output sequence.
  333. Defaults to equally spaced points around the entire unit circle.
  334. a : complex, optional
  335. The starting point in the complex plane. Default is 1+0j.
  336. axis : int, optional
  337. Axis over which to compute the FFT. If not given, the last axis is
  338. used.
  339. Returns
  340. -------
  341. out : ndarray
  342. An array of the same dimensions as `x`, but with the length of the
  343. transformed axis set to `m`.
  344. See Also
  345. --------
  346. CZT : Class that creates a callable chirp z-transform function.
  347. zoom_fft : Convenience function for partial FFT calculations.
  348. Notes
  349. -----
  350. The defaults are chosen such that ``signal.czt(x)`` is equivalent to
  351. ``fft.fft(x)`` and, if ``m > len(x)``, that ``signal.czt(x, m)`` is
  352. equivalent to ``fft.fft(x, m)``.
  353. If the transform needs to be repeated, use `CZT` to construct a
  354. specialized transform function which can be reused without
  355. recomputing constants.
  356. An example application is in system identification, repeatedly evaluating
  357. small slices of the z-transform of a system, around where a pole is
  358. expected to exist, to refine the estimate of the pole's true location. [1]_
  359. References
  360. ----------
  361. .. [1] Steve Alan Shilling, "A study of the chirp z-transform and its
  362. applications", pg 20 (1970)
  363. https://krex.k-state.edu/dspace/bitstream/handle/2097/7844/LD2668R41972S43.pdf
  364. Examples
  365. --------
  366. Generate a sinusoid:
  367. >>> import numpy as np
  368. >>> f1, f2, fs = 8, 10, 200 # Hz
  369. >>> t = np.linspace(0, 1, fs, endpoint=False)
  370. >>> x = np.sin(2*np.pi*t*f2)
  371. >>> import matplotlib.pyplot as plt
  372. >>> plt.plot(t, x)
  373. >>> plt.axis([0, 1, -1.1, 1.1])
  374. >>> plt.show()
  375. Its discrete Fourier transform has all of its energy in a single frequency
  376. bin:
  377. >>> from scipy.fft import rfft, rfftfreq
  378. >>> from scipy.signal import czt, czt_points
  379. >>> plt.plot(rfftfreq(fs, 1/fs), abs(rfft(x)))
  380. >>> plt.margins(0, 0.1)
  381. >>> plt.show()
  382. However, if the sinusoid is logarithmically-decaying:
  383. >>> x = np.exp(-t*f1) * np.sin(2*np.pi*t*f2)
  384. >>> plt.plot(t, x)
  385. >>> plt.axis([0, 1, -1.1, 1.1])
  386. >>> plt.show()
  387. the DFT will have spectral leakage:
  388. >>> plt.plot(rfftfreq(fs, 1/fs), abs(rfft(x)))
  389. >>> plt.margins(0, 0.1)
  390. >>> plt.show()
  391. While the DFT always samples the z-transform around the unit circle, the
  392. chirp z-transform allows us to sample the Z-transform along any
  393. logarithmic spiral, such as a circle with radius smaller than unity:
  394. >>> M = fs // 2 # Just positive frequencies, like rfft
  395. >>> a = np.exp(-f1/fs) # Starting point of the circle, radius < 1
  396. >>> w = np.exp(-1j*np.pi/M) # "Step size" of circle
  397. >>> points = czt_points(M + 1, w, a) # M + 1 to include Nyquist
  398. >>> plt.plot(points.real, points.imag, '.')
  399. >>> plt.gca().add_patch(plt.Circle((0,0), radius=1, fill=False, alpha=.3))
  400. >>> plt.axis('equal'); plt.axis([-1.05, 1.05, -0.05, 1.05])
  401. >>> plt.show()
  402. With the correct radius, this transforms the decaying sinusoid (and others
  403. with the same decay rate) without spectral leakage:
  404. >>> z_vals = czt(x, M + 1, w, a) # Include Nyquist for comparison to rfft
  405. >>> freqs = np.angle(points)*fs/(2*np.pi) # angle = omega, radius = sigma
  406. >>> plt.plot(freqs, abs(z_vals))
  407. >>> plt.margins(0, 0.1)
  408. >>> plt.show()
  409. """
  410. x = np.asarray(x)
  411. transform = CZT(x.shape[axis], m=m, w=w, a=a)
  412. return transform(x, axis=axis)
  413. def zoom_fft(x, fn, m=None, *, fs=2, endpoint=False, axis=-1):
  414. """
  415. Compute the DFT of `x` only for frequencies in range `fn`.
  416. Parameters
  417. ----------
  418. x : array
  419. The signal to transform.
  420. fn : array_like
  421. A length-2 sequence [`f1`, `f2`] giving the frequency range, or a
  422. scalar, for which the range [0, `fn`] is assumed.
  423. m : int, optional
  424. The number of points to evaluate. The default is the length of `x`.
  425. fs : float, optional
  426. The sampling frequency. If ``fs=10`` represented 10 kHz, for example,
  427. then `f1` and `f2` would also be given in kHz.
  428. The default sampling frequency is 2, so `f1` and `f2` should be
  429. in the range [0, 1] to keep the transform below the Nyquist
  430. frequency.
  431. endpoint : bool, optional
  432. If True, `f2` is the last sample. Otherwise, it is not included.
  433. Default is False.
  434. axis : int, optional
  435. Axis over which to compute the FFT. If not given, the last axis is
  436. used.
  437. Returns
  438. -------
  439. out : ndarray
  440. The transformed signal. The Fourier transform will be calculated
  441. at the points f1, f1+df, f1+2df, ..., f2, where df=(f2-f1)/m.
  442. See Also
  443. --------
  444. ZoomFFT : Class that creates a callable partial FFT function.
  445. Notes
  446. -----
  447. The defaults are chosen such that ``signal.zoom_fft(x, 2)`` is equivalent
  448. to ``fft.fft(x)`` and, if ``m > len(x)``, that ``signal.zoom_fft(x, 2, m)``
  449. is equivalent to ``fft.fft(x, m)``.
  450. To graph the magnitude of the resulting transform, use::
  451. plot(linspace(f1, f2, m, endpoint=False), abs(zoom_fft(x, [f1, f2], m)))
  452. If the transform needs to be repeated, use `ZoomFFT` to construct
  453. a specialized transform function which can be reused without
  454. recomputing constants.
  455. Examples
  456. --------
  457. To plot the transform results use something like the following:
  458. >>> import numpy as np
  459. >>> from scipy.signal import zoom_fft
  460. >>> t = np.linspace(0, 1, 1021)
  461. >>> x = np.cos(2*np.pi*15*t) + np.sin(2*np.pi*17*t)
  462. >>> f1, f2 = 5, 27
  463. >>> X = zoom_fft(x, [f1, f2], len(x), fs=1021)
  464. >>> f = np.linspace(f1, f2, len(x))
  465. >>> import matplotlib.pyplot as plt
  466. >>> plt.plot(f, 20*np.log10(np.abs(X)))
  467. >>> plt.show()
  468. """
  469. x = np.asarray(x)
  470. transform = ZoomFFT(x.shape[axis], fn, m=m, fs=fs, endpoint=endpoint)
  471. return transform(x, axis=axis)