|
|
|
|
|
""" |
|
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 |
|
|
|
if w is None: |
|
|
|
return a * np.exp(2j * pi * k / m) |
|
else: |
|
|
|
w = 1.0 * w |
|
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: |
|
|
|
w = cmath.exp(-2j*pi/m) |
|
wk2 = np.exp(-(1j * pi * ((k**2) % (2*m))) / m) |
|
else: |
|
|
|
wk2 = w**(k**2/2.) |
|
|
|
a = 1.0 * a |
|
|
|
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]}") |
|
|
|
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) |
|
|