123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575 |
- # This program is public domain
- # Authors: Paul Kienzle, Nadav Horesh
- """
- Chirp z-transform.
- We provide two interfaces to the chirp z-transform: an object interface
- which precalculates part of the transform and can be applied efficiently
- to many different data sets, and a functional interface which is applied
- only to the given data set.
- Transforms
- ----------
- CZT : callable (x, axis=-1) -> array
- Define a chirp z-transform that can be applied to different signals.
- ZoomFFT : callable (x, axis=-1) -> array
- Define a Fourier transform on a range of frequencies.
- Functions
- ---------
- czt : array
- Compute the chirp z-transform for a signal.
- zoom_fft : array
- Compute the Fourier transform on a range of frequencies.
- """
- import cmath
- import numbers
- import numpy as np
- from numpy import pi, arange
- from scipy.fft import fft, ifft, next_fast_len
- __all__ = ['czt', 'zoom_fft', 'CZT', 'ZoomFFT', 'czt_points']
- def _validate_sizes(n, m):
- if n < 1 or not isinstance(n, numbers.Integral):
- raise ValueError('Invalid number of CZT data '
- f'points ({n}) specified. '
- 'n must be positive and integer type.')
- if m is None:
- m = n
- elif m < 1 or not isinstance(m, numbers.Integral):
- raise ValueError('Invalid number of CZT output '
- f'points ({m}) specified. '
- 'm must be positive and integer type.')
- return m
- def czt_points(m, w=None, a=1+0j):
- """
- Return the points at which the chirp z-transform is computed.
- Parameters
- ----------
- m : int
- The number of points desired.
- w : complex, optional
- The ratio between points in each step.
- Defaults to equally spaced points around the entire unit circle.
- a : complex, optional
- The starting point in the complex plane. Default is 1+0j.
- Returns
- -------
- out : ndarray
- The points in the Z plane at which `CZT` samples the z-transform,
- when called with arguments `m`, `w`, and `a`, as complex numbers.
- See Also
- --------
- CZT : Class that creates a callable chirp z-transform function.
- czt : Convenience function for quickly calculating CZT.
- Examples
- --------
- Plot the points of a 16-point FFT:
- >>> import numpy as np
- >>> from scipy.signal import czt_points
- >>> points = czt_points(16)
- >>> import matplotlib.pyplot as plt
- >>> plt.plot(points.real, points.imag, 'o')
- >>> plt.gca().add_patch(plt.Circle((0,0), radius=1, fill=False, alpha=.3))
- >>> plt.axis('equal')
- >>> plt.show()
- and a 91-point logarithmic spiral that crosses the unit circle:
- >>> m, w, a = 91, 0.995*np.exp(-1j*np.pi*.05), 0.8*np.exp(1j*np.pi/6)
- >>> points = czt_points(m, w, a)
- >>> plt.plot(points.real, points.imag, 'o')
- >>> plt.gca().add_patch(plt.Circle((0,0), radius=1, fill=False, alpha=.3))
- >>> plt.axis('equal')
- >>> plt.show()
- """
- m = _validate_sizes(1, m)
- k = arange(m)
- a = 1.0 * a # at least float
- if w is None:
- # Nothing specified, default to FFT
- return a * np.exp(2j * pi * k / m)
- else:
- # w specified
- w = 1.0 * w # at least float
- return a * w**-k
- class CZT:
- """
- Create a callable chirp z-transform function.
- Transform to compute the frequency response around a spiral.
- Objects of this class are callables which can compute the
- chirp z-transform on their inputs. This object precalculates the constant
- chirps used in the given transform.
- Parameters
- ----------
- n : int
- The size of the signal.
- m : int, optional
- The number of output points desired. Default is `n`.
- w : complex, optional
- The ratio between points in each step. This must be precise or the
- accumulated error will degrade the tail of the output sequence.
- Defaults to equally spaced points around the entire unit circle.
- a : complex, optional
- The starting point in the complex plane. Default is 1+0j.
- Returns
- -------
- f : CZT
- Callable object ``f(x, axis=-1)`` for computing the chirp z-transform
- on `x`.
- See Also
- --------
- czt : Convenience function for quickly calculating CZT.
- ZoomFFT : Class that creates a callable partial FFT function.
- Notes
- -----
- The defaults are chosen such that ``f(x)`` is equivalent to
- ``fft.fft(x)`` and, if ``m > len(x)``, that ``f(x, m)`` is equivalent to
- ``fft.fft(x, m)``.
- If `w` does not lie on the unit circle, then the transform will be
- around a spiral with exponentially-increasing radius. Regardless,
- angle will increase linearly.
- For transforms that do lie on the unit circle, accuracy is better when
- using `ZoomFFT`, since any numerical error in `w` is
- accumulated for long data lengths, drifting away from the unit circle.
- The chirp z-transform can be faster than an equivalent FFT with
- zero padding. Try it with your own array sizes to see.
- However, the chirp z-transform is considerably less precise than the
- equivalent zero-padded FFT.
- As this CZT is implemented using the Bluestein algorithm, it can compute
- large prime-length Fourier transforms in O(N log N) time, rather than the
- O(N**2) time required by the direct DFT calculation. (`scipy.fft` also
- uses Bluestein's algorithm'.)
- (The name "chirp z-transform" comes from the use of a chirp in the
- Bluestein algorithm. It does not decompose signals into chirps, like
- other transforms with "chirp" in the name.)
- References
- ----------
- .. [1] Leo I. Bluestein, "A linear filtering approach to the computation
- of the discrete Fourier transform," Northeast Electronics Research
- and Engineering Meeting Record 10, 218-219 (1968).
- .. [2] Rabiner, Schafer, and Rader, "The chirp z-transform algorithm and
- its application," Bell Syst. Tech. J. 48, 1249-1292 (1969).
- Examples
- --------
- Compute multiple prime-length FFTs:
- >>> from scipy.signal import CZT
- >>> import numpy as np
- >>> a = np.random.rand(7)
- >>> b = np.random.rand(7)
- >>> c = np.random.rand(7)
- >>> czt_7 = CZT(n=7)
- >>> A = czt_7(a)
- >>> B = czt_7(b)
- >>> C = czt_7(c)
- Display the points at which the FFT is calculated:
- >>> czt_7.points()
- array([ 1.00000000+0.j , 0.62348980+0.78183148j,
- -0.22252093+0.97492791j, -0.90096887+0.43388374j,
- -0.90096887-0.43388374j, -0.22252093-0.97492791j,
- 0.62348980-0.78183148j])
- >>> import matplotlib.pyplot as plt
- >>> plt.plot(czt_7.points().real, czt_7.points().imag, 'o')
- >>> plt.gca().add_patch(plt.Circle((0,0), radius=1, fill=False, alpha=.3))
- >>> plt.axis('equal')
- >>> plt.show()
- """
- def __init__(self, n, m=None, w=None, a=1+0j):
- m = _validate_sizes(n, m)
- k = arange(max(m, n), dtype=np.min_scalar_type(-max(m, n)**2))
- if w is None:
- # Nothing specified, default to FFT-like
- w = cmath.exp(-2j*pi/m)
- wk2 = np.exp(-(1j * pi * ((k**2) % (2*m))) / m)
- else:
- # w specified
- wk2 = w**(k**2/2.)
- a = 1.0 * a # at least float
- self.w, self.a = w, a
- self.m, self.n = m, n
- nfft = next_fast_len(n + m - 1)
- self._Awk2 = a**-k[:n] * wk2[:n]
- self._nfft = nfft
- self._Fwk2 = fft(1/np.hstack((wk2[n-1:0:-1], wk2[:m])), nfft)
- self._wk2 = wk2[:m]
- self._yidx = slice(n-1, n+m-1)
- def __call__(self, x, *, axis=-1):
- """
- Calculate the chirp z-transform of a signal.
- Parameters
- ----------
- x : array
- The signal to transform.
- axis : int, optional
- Axis over which to compute the FFT. If not given, the last axis is
- used.
- Returns
- -------
- out : ndarray
- An array of the same dimensions as `x`, but with the length of the
- transformed axis set to `m`.
- """
- x = np.asarray(x)
- if x.shape[axis] != self.n:
- raise ValueError(f"CZT defined for length {self.n}, not "
- f"{x.shape[axis]}")
- # Calculate transpose coordinates, to allow operation on any given axis
- trnsp = np.arange(x.ndim)
- trnsp[[axis, -1]] = [-1, axis]
- x = x.transpose(*trnsp)
- y = ifft(self._Fwk2 * fft(x*self._Awk2, self._nfft))
- y = y[..., self._yidx] * self._wk2
- return y.transpose(*trnsp)
- def points(self):
- """
- Return the points at which the chirp z-transform is computed.
- """
- return czt_points(self.m, self.w, self.a)
- class ZoomFFT(CZT):
- """
- Create a callable zoom FFT transform function.
- This is a specialization of the chirp z-transform (`CZT`) for a set of
- equally-spaced frequencies around the unit circle, used to calculate a
- section of the FFT more efficiently than calculating the entire FFT and
- truncating.
- Parameters
- ----------
- n : int
- The size of the signal.
- fn : array_like
- A length-2 sequence [`f1`, `f2`] giving the frequency range, or a
- scalar, for which the range [0, `fn`] is assumed.
- m : int, optional
- The number of points to evaluate. Default is `n`.
- fs : float, optional
- The sampling frequency. If ``fs=10`` represented 10 kHz, for example,
- then `f1` and `f2` would also be given in kHz.
- The default sampling frequency is 2, so `f1` and `f2` should be
- in the range [0, 1] to keep the transform below the Nyquist
- frequency.
- endpoint : bool, optional
- If True, `f2` is the last sample. Otherwise, it is not included.
- Default is False.
- Returns
- -------
- f : ZoomFFT
- Callable object ``f(x, axis=-1)`` for computing the zoom FFT on `x`.
- See Also
- --------
- zoom_fft : Convenience function for calculating a zoom FFT.
- Notes
- -----
- The defaults are chosen such that ``f(x, 2)`` is equivalent to
- ``fft.fft(x)`` and, if ``m > len(x)``, that ``f(x, 2, m)`` is equivalent to
- ``fft.fft(x, m)``.
- Sampling frequency is 1/dt, the time step between samples in the
- signal `x`. The unit circle corresponds to frequencies from 0 up
- to the sampling frequency. The default sampling frequency of 2
- means that `f1`, `f2` values up to the Nyquist frequency are in the
- range [0, 1). For `f1`, `f2` values expressed in radians, a sampling
- frequency of 2*pi should be used.
- Remember that a zoom FFT can only interpolate the points of the existing
- FFT. It cannot help to resolve two separate nearby frequencies.
- Frequency resolution can only be increased by increasing acquisition
- time.
- These functions are implemented using Bluestein's algorithm (as is
- `scipy.fft`). [2]_
- References
- ----------
- .. [1] Steve Alan Shilling, "A study of the chirp z-transform and its
- applications", pg 29 (1970)
- https://krex.k-state.edu/dspace/bitstream/handle/2097/7844/LD2668R41972S43.pdf
- .. [2] Leo I. Bluestein, "A linear filtering approach to the computation
- of the discrete Fourier transform," Northeast Electronics Research
- and Engineering Meeting Record 10, 218-219 (1968).
- Examples
- --------
- To plot the transform results use something like the following:
- >>> import numpy as np
- >>> from scipy.signal import ZoomFFT
- >>> t = np.linspace(0, 1, 1021)
- >>> x = np.cos(2*np.pi*15*t) + np.sin(2*np.pi*17*t)
- >>> f1, f2 = 5, 27
- >>> transform = ZoomFFT(len(x), [f1, f2], len(x), fs=1021)
- >>> X = transform(x)
- >>> f = np.linspace(f1, f2, len(x))
- >>> import matplotlib.pyplot as plt
- >>> plt.plot(f, 20*np.log10(np.abs(X)))
- >>> plt.show()
- """
- def __init__(self, n, fn, m=None, *, fs=2, endpoint=False):
- m = _validate_sizes(n, m)
- k = arange(max(m, n), dtype=np.min_scalar_type(-max(m, n)**2))
- if np.size(fn) == 2:
- f1, f2 = fn
- elif np.size(fn) == 1:
- f1, f2 = 0.0, fn
- else:
- raise ValueError('fn must be a scalar or 2-length sequence')
- self.f1, self.f2, self.fs = f1, f2, fs
- if endpoint:
- scale = ((f2 - f1) * m) / (fs * (m - 1))
- else:
- scale = (f2 - f1) / fs
- a = cmath.exp(2j * pi * f1/fs)
- wk2 = np.exp(-(1j * pi * scale * k**2) / m)
- self.w = cmath.exp(-2j*pi/m * scale)
- self.a = a
- self.m, self.n = m, n
- ak = np.exp(-2j * pi * f1/fs * k[:n])
- self._Awk2 = ak * wk2[:n]
- nfft = next_fast_len(n + m - 1)
- self._nfft = nfft
- self._Fwk2 = fft(1/np.hstack((wk2[n-1:0:-1], wk2[:m])), nfft)
- self._wk2 = wk2[:m]
- self._yidx = slice(n-1, n+m-1)
- def czt(x, m=None, w=None, a=1+0j, *, axis=-1):
- """
- Compute the frequency response around a spiral in the Z plane.
- Parameters
- ----------
- x : array
- The signal to transform.
- m : int, optional
- The number of output points desired. Default is the length of the
- input data.
- w : complex, optional
- The ratio between points in each step. This must be precise or the
- accumulated error will degrade the tail of the output sequence.
- Defaults to equally spaced points around the entire unit circle.
- a : complex, optional
- The starting point in the complex plane. Default is 1+0j.
- axis : int, optional
- Axis over which to compute the FFT. If not given, the last axis is
- used.
- Returns
- -------
- out : ndarray
- An array of the same dimensions as `x`, but with the length of the
- transformed axis set to `m`.
- See Also
- --------
- CZT : Class that creates a callable chirp z-transform function.
- zoom_fft : Convenience function for partial FFT calculations.
- Notes
- -----
- The defaults are chosen such that ``signal.czt(x)`` is equivalent to
- ``fft.fft(x)`` and, if ``m > len(x)``, that ``signal.czt(x, m)`` is
- equivalent to ``fft.fft(x, m)``.
- If the transform needs to be repeated, use `CZT` to construct a
- specialized transform function which can be reused without
- recomputing constants.
- An example application is in system identification, repeatedly evaluating
- small slices of the z-transform of a system, around where a pole is
- expected to exist, to refine the estimate of the pole's true location. [1]_
- References
- ----------
- .. [1] Steve Alan Shilling, "A study of the chirp z-transform and its
- applications", pg 20 (1970)
- https://krex.k-state.edu/dspace/bitstream/handle/2097/7844/LD2668R41972S43.pdf
- Examples
- --------
- Generate a sinusoid:
- >>> import numpy as np
- >>> f1, f2, fs = 8, 10, 200 # Hz
- >>> t = np.linspace(0, 1, fs, endpoint=False)
- >>> x = np.sin(2*np.pi*t*f2)
- >>> import matplotlib.pyplot as plt
- >>> plt.plot(t, x)
- >>> plt.axis([0, 1, -1.1, 1.1])
- >>> plt.show()
- Its discrete Fourier transform has all of its energy in a single frequency
- bin:
- >>> from scipy.fft import rfft, rfftfreq
- >>> from scipy.signal import czt, czt_points
- >>> plt.plot(rfftfreq(fs, 1/fs), abs(rfft(x)))
- >>> plt.margins(0, 0.1)
- >>> plt.show()
- However, if the sinusoid is logarithmically-decaying:
- >>> x = np.exp(-t*f1) * np.sin(2*np.pi*t*f2)
- >>> plt.plot(t, x)
- >>> plt.axis([0, 1, -1.1, 1.1])
- >>> plt.show()
- the DFT will have spectral leakage:
- >>> plt.plot(rfftfreq(fs, 1/fs), abs(rfft(x)))
- >>> plt.margins(0, 0.1)
- >>> plt.show()
- While the DFT always samples the z-transform around the unit circle, the
- chirp z-transform allows us to sample the Z-transform along any
- logarithmic spiral, such as a circle with radius smaller than unity:
- >>> M = fs // 2 # Just positive frequencies, like rfft
- >>> a = np.exp(-f1/fs) # Starting point of the circle, radius < 1
- >>> w = np.exp(-1j*np.pi/M) # "Step size" of circle
- >>> points = czt_points(M + 1, w, a) # M + 1 to include Nyquist
- >>> plt.plot(points.real, points.imag, '.')
- >>> plt.gca().add_patch(plt.Circle((0,0), radius=1, fill=False, alpha=.3))
- >>> plt.axis('equal'); plt.axis([-1.05, 1.05, -0.05, 1.05])
- >>> plt.show()
- With the correct radius, this transforms the decaying sinusoid (and others
- with the same decay rate) without spectral leakage:
- >>> z_vals = czt(x, M + 1, w, a) # Include Nyquist for comparison to rfft
- >>> freqs = np.angle(points)*fs/(2*np.pi) # angle = omega, radius = sigma
- >>> plt.plot(freqs, abs(z_vals))
- >>> plt.margins(0, 0.1)
- >>> plt.show()
- """
- x = np.asarray(x)
- transform = CZT(x.shape[axis], m=m, w=w, a=a)
- return transform(x, axis=axis)
- def zoom_fft(x, fn, m=None, *, fs=2, endpoint=False, axis=-1):
- """
- Compute the DFT of `x` only for frequencies in range `fn`.
- Parameters
- ----------
- x : array
- The signal to transform.
- fn : array_like
- A length-2 sequence [`f1`, `f2`] giving the frequency range, or a
- scalar, for which the range [0, `fn`] is assumed.
- m : int, optional
- The number of points to evaluate. The default is the length of `x`.
- fs : float, optional
- The sampling frequency. If ``fs=10`` represented 10 kHz, for example,
- then `f1` and `f2` would also be given in kHz.
- The default sampling frequency is 2, so `f1` and `f2` should be
- in the range [0, 1] to keep the transform below the Nyquist
- frequency.
- endpoint : bool, optional
- If True, `f2` is the last sample. Otherwise, it is not included.
- Default is False.
- axis : int, optional
- Axis over which to compute the FFT. If not given, the last axis is
- used.
- Returns
- -------
- out : ndarray
- The transformed signal. The Fourier transform will be calculated
- at the points f1, f1+df, f1+2df, ..., f2, where df=(f2-f1)/m.
- See Also
- --------
- ZoomFFT : Class that creates a callable partial FFT function.
- Notes
- -----
- The defaults are chosen such that ``signal.zoom_fft(x, 2)`` is equivalent
- to ``fft.fft(x)`` and, if ``m > len(x)``, that ``signal.zoom_fft(x, 2, m)``
- is equivalent to ``fft.fft(x, m)``.
- To graph the magnitude of the resulting transform, use::
- plot(linspace(f1, f2, m, endpoint=False), abs(zoom_fft(x, [f1, f2], m)))
- If the transform needs to be repeated, use `ZoomFFT` to construct
- a specialized transform function which can be reused without
- recomputing constants.
- Examples
- --------
- To plot the transform results use something like the following:
- >>> import numpy as np
- >>> from scipy.signal import zoom_fft
- >>> t = np.linspace(0, 1, 1021)
- >>> x = np.cos(2*np.pi*15*t) + np.sin(2*np.pi*17*t)
- >>> f1, f2 = 5, 27
- >>> X = zoom_fft(x, [f1, f2], len(x), fs=1021)
- >>> f = np.linspace(f1, f2, len(x))
- >>> import matplotlib.pyplot as plt
- >>> plt.plot(f, 20*np.log10(np.abs(X)))
- >>> plt.show()
- """
- x = np.asarray(x)
- transform = ZoomFFT(x.shape[axis], fn, m=m, fs=fs, endpoint=endpoint)
- return transform(x, axis=axis)
|