Inzynierka/Lib/site-packages/scipy/signal/_czt.py
2023-06-02 12:51:02 +02:00

576 lines
19 KiB
Python

# 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)