asd
This commit is contained in:
346
venv/lib/python3.12/site-packages/scipy/signal/__init__.py
Normal file
346
venv/lib/python3.12/site-packages/scipy/signal/__init__.py
Normal file
@ -0,0 +1,346 @@
|
||||
"""
|
||||
=======================================
|
||||
Signal processing (:mod:`scipy.signal`)
|
||||
=======================================
|
||||
|
||||
Convolution
|
||||
===========
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
convolve -- N-D convolution.
|
||||
correlate -- N-D correlation.
|
||||
fftconvolve -- N-D convolution using the FFT.
|
||||
oaconvolve -- N-D convolution using the overlap-add method.
|
||||
convolve2d -- 2-D convolution (more options).
|
||||
correlate2d -- 2-D correlation (more options).
|
||||
sepfir2d -- Convolve with a 2-D separable FIR filter.
|
||||
choose_conv_method -- Chooses faster of FFT and direct convolution methods.
|
||||
correlation_lags -- Determines lag indices for 1D cross-correlation.
|
||||
|
||||
B-splines
|
||||
=========
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
gauss_spline -- Gaussian approximation to the B-spline basis function.
|
||||
cspline1d -- Coefficients for 1-D cubic (3rd order) B-spline.
|
||||
qspline1d -- Coefficients for 1-D quadratic (2nd order) B-spline.
|
||||
cspline2d -- Coefficients for 2-D cubic (3rd order) B-spline.
|
||||
qspline2d -- Coefficients for 2-D quadratic (2nd order) B-spline.
|
||||
cspline1d_eval -- Evaluate a cubic spline at the given points.
|
||||
qspline1d_eval -- Evaluate a quadratic spline at the given points.
|
||||
spline_filter -- Smoothing spline (cubic) filtering of a rank-2 array.
|
||||
|
||||
Filtering
|
||||
=========
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
order_filter -- N-D order filter.
|
||||
medfilt -- N-D median filter.
|
||||
medfilt2d -- 2-D median filter (faster).
|
||||
wiener -- N-D Wiener filter.
|
||||
|
||||
symiirorder1 -- 2nd-order IIR filter (cascade of first-order systems).
|
||||
symiirorder2 -- 4th-order IIR filter (cascade of second-order systems).
|
||||
lfilter -- 1-D FIR and IIR digital linear filtering.
|
||||
lfiltic -- Construct initial conditions for `lfilter`.
|
||||
lfilter_zi -- Compute an initial state zi for the lfilter function that
|
||||
-- corresponds to the steady state of the step response.
|
||||
filtfilt -- A forward-backward filter.
|
||||
savgol_filter -- Filter a signal using the Savitzky-Golay filter.
|
||||
|
||||
deconvolve -- 1-D deconvolution using lfilter.
|
||||
|
||||
sosfilt -- 1-D IIR digital linear filtering using
|
||||
-- a second-order sections filter representation.
|
||||
sosfilt_zi -- Compute an initial state zi for the sosfilt function that
|
||||
-- corresponds to the steady state of the step response.
|
||||
sosfiltfilt -- A forward-backward filter for second-order sections.
|
||||
hilbert -- Compute 1-D analytic signal, using the Hilbert transform.
|
||||
hilbert2 -- Compute 2-D analytic signal, using the Hilbert transform.
|
||||
|
||||
decimate -- Downsample a signal.
|
||||
detrend -- Remove linear and/or constant trends from data.
|
||||
resample -- Resample using Fourier method.
|
||||
resample_poly -- Resample using polyphase filtering method.
|
||||
upfirdn -- Upsample, apply FIR filter, downsample.
|
||||
|
||||
Filter design
|
||||
=============
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
bilinear -- Digital filter from an analog filter using
|
||||
-- the bilinear transform.
|
||||
bilinear_zpk -- Digital filter from an analog filter using
|
||||
-- the bilinear transform.
|
||||
findfreqs -- Find array of frequencies for computing filter response.
|
||||
firls -- FIR filter design using least-squares error minimization.
|
||||
firwin -- Windowed FIR filter design, with frequency response
|
||||
-- defined as pass and stop bands.
|
||||
firwin2 -- Windowed FIR filter design, with arbitrary frequency
|
||||
-- response.
|
||||
freqs -- Analog filter frequency response from TF coefficients.
|
||||
freqs_zpk -- Analog filter frequency response from ZPK coefficients.
|
||||
freqz -- Digital filter frequency response from TF coefficients.
|
||||
freqz_zpk -- Digital filter frequency response from ZPK coefficients.
|
||||
sosfreqz -- Digital filter frequency response for SOS format filter.
|
||||
gammatone -- FIR and IIR gammatone filter design.
|
||||
group_delay -- Digital filter group delay.
|
||||
iirdesign -- IIR filter design given bands and gains.
|
||||
iirfilter -- IIR filter design given order and critical frequencies.
|
||||
kaiser_atten -- Compute the attenuation of a Kaiser FIR filter, given
|
||||
-- the number of taps and the transition width at
|
||||
-- discontinuities in the frequency response.
|
||||
kaiser_beta -- Compute the Kaiser parameter beta, given the desired
|
||||
-- FIR filter attenuation.
|
||||
kaiserord -- Design a Kaiser window to limit ripple and width of
|
||||
-- transition region.
|
||||
minimum_phase -- Convert a linear phase FIR filter to minimum phase.
|
||||
savgol_coeffs -- Compute the FIR filter coefficients for a Savitzky-Golay
|
||||
-- filter.
|
||||
remez -- Optimal FIR filter design.
|
||||
|
||||
unique_roots -- Unique roots and their multiplicities.
|
||||
residue -- Partial fraction expansion of b(s) / a(s).
|
||||
residuez -- Partial fraction expansion of b(z) / a(z).
|
||||
invres -- Inverse partial fraction expansion for analog filter.
|
||||
invresz -- Inverse partial fraction expansion for digital filter.
|
||||
BadCoefficients -- Warning on badly conditioned filter coefficients.
|
||||
|
||||
Lower-level filter design functions:
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
abcd_normalize -- Check state-space matrices and ensure they are rank-2.
|
||||
band_stop_obj -- Band Stop Objective Function for order minimization.
|
||||
besselap -- Return (z,p,k) for analog prototype of Bessel filter.
|
||||
buttap -- Return (z,p,k) for analog prototype of Butterworth filter.
|
||||
cheb1ap -- Return (z,p,k) for type I Chebyshev filter.
|
||||
cheb2ap -- Return (z,p,k) for type II Chebyshev filter.
|
||||
cmplx_sort -- Sort roots based on magnitude.
|
||||
ellipap -- Return (z,p,k) for analog prototype of elliptic filter.
|
||||
lp2bp -- Transform a lowpass filter prototype to a bandpass filter.
|
||||
lp2bp_zpk -- Transform a lowpass filter prototype to a bandpass filter.
|
||||
lp2bs -- Transform a lowpass filter prototype to a bandstop filter.
|
||||
lp2bs_zpk -- Transform a lowpass filter prototype to a bandstop filter.
|
||||
lp2hp -- Transform a lowpass filter prototype to a highpass filter.
|
||||
lp2hp_zpk -- Transform a lowpass filter prototype to a highpass filter.
|
||||
lp2lp -- Transform a lowpass filter prototype to a lowpass filter.
|
||||
lp2lp_zpk -- Transform a lowpass filter prototype to a lowpass filter.
|
||||
normalize -- Normalize polynomial representation of a transfer function.
|
||||
|
||||
|
||||
|
||||
Matlab-style IIR filter design
|
||||
==============================
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
butter -- Butterworth
|
||||
buttord
|
||||
cheby1 -- Chebyshev Type I
|
||||
cheb1ord
|
||||
cheby2 -- Chebyshev Type II
|
||||
cheb2ord
|
||||
ellip -- Elliptic (Cauer)
|
||||
ellipord
|
||||
bessel -- Bessel (no order selection available -- try butterod)
|
||||
iirnotch -- Design second-order IIR notch digital filter.
|
||||
iirpeak -- Design second-order IIR peak (resonant) digital filter.
|
||||
iircomb -- Design IIR comb filter.
|
||||
|
||||
Continuous-time linear systems
|
||||
==============================
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
lti -- Continuous-time linear time invariant system base class.
|
||||
StateSpace -- Linear time invariant system in state space form.
|
||||
TransferFunction -- Linear time invariant system in transfer function form.
|
||||
ZerosPolesGain -- Linear time invariant system in zeros, poles, gain form.
|
||||
lsim -- Continuous-time simulation of output to linear system.
|
||||
impulse -- Impulse response of linear, time-invariant (LTI) system.
|
||||
step -- Step response of continuous-time LTI system.
|
||||
freqresp -- Frequency response of a continuous-time LTI system.
|
||||
bode -- Bode magnitude and phase data (continuous-time LTI).
|
||||
|
||||
Discrete-time linear systems
|
||||
============================
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
dlti -- Discrete-time linear time invariant system base class.
|
||||
StateSpace -- Linear time invariant system in state space form.
|
||||
TransferFunction -- Linear time invariant system in transfer function form.
|
||||
ZerosPolesGain -- Linear time invariant system in zeros, poles, gain form.
|
||||
dlsim -- Simulation of output to a discrete-time linear system.
|
||||
dimpulse -- Impulse response of a discrete-time LTI system.
|
||||
dstep -- Step response of a discrete-time LTI system.
|
||||
dfreqresp -- Frequency response of a discrete-time LTI system.
|
||||
dbode -- Bode magnitude and phase data (discrete-time LTI).
|
||||
|
||||
LTI representations
|
||||
===================
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
tf2zpk -- Transfer function to zero-pole-gain.
|
||||
tf2sos -- Transfer function to second-order sections.
|
||||
tf2ss -- Transfer function to state-space.
|
||||
zpk2tf -- Zero-pole-gain to transfer function.
|
||||
zpk2sos -- Zero-pole-gain to second-order sections.
|
||||
zpk2ss -- Zero-pole-gain to state-space.
|
||||
ss2tf -- State-pace to transfer function.
|
||||
ss2zpk -- State-space to pole-zero-gain.
|
||||
sos2zpk -- Second-order sections to zero-pole-gain.
|
||||
sos2tf -- Second-order sections to transfer function.
|
||||
cont2discrete -- Continuous-time to discrete-time LTI conversion.
|
||||
place_poles -- Pole placement.
|
||||
|
||||
Waveforms
|
||||
=========
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
chirp -- Frequency swept cosine signal, with several freq functions.
|
||||
gausspulse -- Gaussian modulated sinusoid.
|
||||
max_len_seq -- Maximum length sequence.
|
||||
sawtooth -- Periodic sawtooth.
|
||||
square -- Square wave.
|
||||
sweep_poly -- Frequency swept cosine signal; freq is arbitrary polynomial.
|
||||
unit_impulse -- Discrete unit impulse.
|
||||
|
||||
Window functions
|
||||
================
|
||||
|
||||
For window functions, see the `scipy.signal.windows` namespace.
|
||||
|
||||
In the `scipy.signal` namespace, there is a convenience function to
|
||||
obtain these windows by name:
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
get_window -- Return a window of a given length and type.
|
||||
|
||||
Wavelets
|
||||
========
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
cascade -- Compute scaling function and wavelet from coefficients.
|
||||
daub -- Return low-pass.
|
||||
morlet -- Complex Morlet wavelet.
|
||||
qmf -- Return quadrature mirror filter from low-pass.
|
||||
ricker -- Return ricker wavelet.
|
||||
morlet2 -- Return Morlet wavelet, compatible with cwt.
|
||||
cwt -- Perform continuous wavelet transform.
|
||||
|
||||
Peak finding
|
||||
============
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
argrelmin -- Calculate the relative minima of data.
|
||||
argrelmax -- Calculate the relative maxima of data.
|
||||
argrelextrema -- Calculate the relative extrema of data.
|
||||
find_peaks -- Find a subset of peaks inside a signal.
|
||||
find_peaks_cwt -- Find peaks in a 1-D array with wavelet transformation.
|
||||
peak_prominences -- Calculate the prominence of each peak in a signal.
|
||||
peak_widths -- Calculate the width of each peak in a signal.
|
||||
|
||||
Spectral analysis
|
||||
=================
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
periodogram -- Compute a (modified) periodogram.
|
||||
welch -- Compute a periodogram using Welch's method.
|
||||
csd -- Compute the cross spectral density, using Welch's method.
|
||||
coherence -- Compute the magnitude squared coherence, using Welch's method.
|
||||
spectrogram -- Compute the spectrogram (legacy).
|
||||
lombscargle -- Computes the Lomb-Scargle periodogram.
|
||||
vectorstrength -- Computes the vector strength.
|
||||
ShortTimeFFT -- Interface for calculating the \
|
||||
:ref:`Short Time Fourier Transform <tutorial_stft>` and \
|
||||
its inverse.
|
||||
stft -- Compute the Short Time Fourier Transform (legacy).
|
||||
istft -- Compute the Inverse Short Time Fourier Transform (legacy).
|
||||
check_COLA -- Check the COLA constraint for iSTFT reconstruction.
|
||||
check_NOLA -- Check the NOLA constraint for iSTFT reconstruction.
|
||||
|
||||
Chirp Z-transform and Zoom FFT
|
||||
============================================
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
czt - Chirp z-transform convenience function
|
||||
zoom_fft - Zoom FFT convenience function
|
||||
CZT - Chirp z-transform function generator
|
||||
ZoomFFT - Zoom FFT function generator
|
||||
czt_points - Output the z-plane points sampled by a chirp z-transform
|
||||
|
||||
The functions are simpler to use than the classes, but are less efficient when
|
||||
using the same transform on many arrays of the same length, since they
|
||||
repeatedly generate the same chirp signal with every call. In these cases,
|
||||
use the classes to create a reusable function instead.
|
||||
|
||||
"""
|
||||
|
||||
from . import _sigtools, windows
|
||||
from ._waveforms import *
|
||||
from ._max_len_seq import max_len_seq
|
||||
from ._upfirdn import upfirdn
|
||||
|
||||
from ._spline import (
|
||||
cspline2d,
|
||||
qspline2d,
|
||||
sepfir2d,
|
||||
symiirorder1,
|
||||
symiirorder2,
|
||||
)
|
||||
|
||||
from ._bsplines import *
|
||||
from ._filter_design import *
|
||||
from ._fir_filter_design import *
|
||||
from ._ltisys import *
|
||||
from ._lti_conversion import *
|
||||
from ._signaltools import *
|
||||
from ._savitzky_golay import savgol_coeffs, savgol_filter
|
||||
from ._spectral_py import *
|
||||
from ._short_time_fft import *
|
||||
from ._wavelets import *
|
||||
from ._peak_finding import *
|
||||
from ._czt import *
|
||||
from .windows import get_window # keep this one in signal namespace
|
||||
|
||||
# Deprecated namespaces, to be removed in v2.0.0
|
||||
from . import (
|
||||
bsplines, filter_design, fir_filter_design, lti_conversion, ltisys,
|
||||
spectral, signaltools, waveforms, wavelets, spline
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
s for s in dir() if not s.startswith("_")
|
||||
]
|
||||
|
||||
from scipy._lib._testutils import PytestTester
|
||||
test = PytestTester(__name__)
|
||||
del PytestTester
|
||||
264
venv/lib/python3.12/site-packages/scipy/signal/_arraytools.py
Normal file
264
venv/lib/python3.12/site-packages/scipy/signal/_arraytools.py
Normal file
@ -0,0 +1,264 @@
|
||||
"""
|
||||
Functions for acting on a axis of an array.
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
|
||||
def axis_slice(a, start=None, stop=None, step=None, axis=-1):
|
||||
"""Take a slice along axis 'axis' from 'a'.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
a : numpy.ndarray
|
||||
The array to be sliced.
|
||||
start, stop, step : int or None
|
||||
The slice parameters.
|
||||
axis : int, optional
|
||||
The axis of `a` to be sliced.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> from scipy.signal._arraytools import axis_slice
|
||||
>>> a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
|
||||
>>> axis_slice(a, start=0, stop=1, axis=1)
|
||||
array([[1],
|
||||
[4],
|
||||
[7]])
|
||||
>>> axis_slice(a, start=1, axis=0)
|
||||
array([[4, 5, 6],
|
||||
[7, 8, 9]])
|
||||
|
||||
Notes
|
||||
-----
|
||||
The keyword arguments start, stop and step are used by calling
|
||||
slice(start, stop, step). This implies axis_slice() does not
|
||||
handle its arguments the exactly the same as indexing. To select
|
||||
a single index k, for example, use
|
||||
axis_slice(a, start=k, stop=k+1)
|
||||
In this case, the length of the axis 'axis' in the result will
|
||||
be 1; the trivial dimension is not removed. (Use numpy.squeeze()
|
||||
to remove trivial axes.)
|
||||
"""
|
||||
a_slice = [slice(None)] * a.ndim
|
||||
a_slice[axis] = slice(start, stop, step)
|
||||
b = a[tuple(a_slice)]
|
||||
return b
|
||||
|
||||
|
||||
def axis_reverse(a, axis=-1):
|
||||
"""Reverse the 1-D slices of `a` along axis `axis`.
|
||||
|
||||
Returns axis_slice(a, step=-1, axis=axis).
|
||||
"""
|
||||
return axis_slice(a, step=-1, axis=axis)
|
||||
|
||||
|
||||
def odd_ext(x, n, axis=-1):
|
||||
"""
|
||||
Odd extension at the boundaries of an array
|
||||
|
||||
Generate a new ndarray by making an odd extension of `x` along an axis.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x : ndarray
|
||||
The array to be extended.
|
||||
n : int
|
||||
The number of elements by which to extend `x` at each end of the axis.
|
||||
axis : int, optional
|
||||
The axis along which to extend `x`. Default is -1.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> from scipy.signal._arraytools import odd_ext
|
||||
>>> a = np.array([[1, 2, 3, 4, 5], [0, 1, 4, 9, 16]])
|
||||
>>> odd_ext(a, 2)
|
||||
array([[-1, 0, 1, 2, 3, 4, 5, 6, 7],
|
||||
[-4, -1, 0, 1, 4, 9, 16, 23, 28]])
|
||||
|
||||
Odd extension is a "180 degree rotation" at the endpoints of the original
|
||||
array:
|
||||
|
||||
>>> t = np.linspace(0, 1.5, 100)
|
||||
>>> a = 0.9 * np.sin(2 * np.pi * t**2)
|
||||
>>> b = odd_ext(a, 40)
|
||||
>>> import matplotlib.pyplot as plt
|
||||
>>> plt.plot(np.arange(-40, 140), b, 'b', lw=1, label='odd extension')
|
||||
>>> plt.plot(np.arange(100), a, 'r', lw=2, label='original')
|
||||
>>> plt.legend(loc='best')
|
||||
>>> plt.show()
|
||||
"""
|
||||
if n < 1:
|
||||
return x
|
||||
if n > x.shape[axis] - 1:
|
||||
raise ValueError(("The extension length n (%d) is too big. " +
|
||||
"It must not exceed x.shape[axis]-1, which is %d.")
|
||||
% (n, x.shape[axis] - 1))
|
||||
left_end = axis_slice(x, start=0, stop=1, axis=axis)
|
||||
left_ext = axis_slice(x, start=n, stop=0, step=-1, axis=axis)
|
||||
right_end = axis_slice(x, start=-1, axis=axis)
|
||||
right_ext = axis_slice(x, start=-2, stop=-(n + 2), step=-1, axis=axis)
|
||||
ext = np.concatenate((2 * left_end - left_ext,
|
||||
x,
|
||||
2 * right_end - right_ext),
|
||||
axis=axis)
|
||||
return ext
|
||||
|
||||
|
||||
def even_ext(x, n, axis=-1):
|
||||
"""
|
||||
Even extension at the boundaries of an array
|
||||
|
||||
Generate a new ndarray by making an even extension of `x` along an axis.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x : ndarray
|
||||
The array to be extended.
|
||||
n : int
|
||||
The number of elements by which to extend `x` at each end of the axis.
|
||||
axis : int, optional
|
||||
The axis along which to extend `x`. Default is -1.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> from scipy.signal._arraytools import even_ext
|
||||
>>> a = np.array([[1, 2, 3, 4, 5], [0, 1, 4, 9, 16]])
|
||||
>>> even_ext(a, 2)
|
||||
array([[ 3, 2, 1, 2, 3, 4, 5, 4, 3],
|
||||
[ 4, 1, 0, 1, 4, 9, 16, 9, 4]])
|
||||
|
||||
Even extension is a "mirror image" at the boundaries of the original array:
|
||||
|
||||
>>> t = np.linspace(0, 1.5, 100)
|
||||
>>> a = 0.9 * np.sin(2 * np.pi * t**2)
|
||||
>>> b = even_ext(a, 40)
|
||||
>>> import matplotlib.pyplot as plt
|
||||
>>> plt.plot(np.arange(-40, 140), b, 'b', lw=1, label='even extension')
|
||||
>>> plt.plot(np.arange(100), a, 'r', lw=2, label='original')
|
||||
>>> plt.legend(loc='best')
|
||||
>>> plt.show()
|
||||
"""
|
||||
if n < 1:
|
||||
return x
|
||||
if n > x.shape[axis] - 1:
|
||||
raise ValueError(("The extension length n (%d) is too big. " +
|
||||
"It must not exceed x.shape[axis]-1, which is %d.")
|
||||
% (n, x.shape[axis] - 1))
|
||||
left_ext = axis_slice(x, start=n, stop=0, step=-1, axis=axis)
|
||||
right_ext = axis_slice(x, start=-2, stop=-(n + 2), step=-1, axis=axis)
|
||||
ext = np.concatenate((left_ext,
|
||||
x,
|
||||
right_ext),
|
||||
axis=axis)
|
||||
return ext
|
||||
|
||||
|
||||
def const_ext(x, n, axis=-1):
|
||||
"""
|
||||
Constant extension at the boundaries of an array
|
||||
|
||||
Generate a new ndarray that is a constant extension of `x` along an axis.
|
||||
|
||||
The extension repeats the values at the first and last element of
|
||||
the axis.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x : ndarray
|
||||
The array to be extended.
|
||||
n : int
|
||||
The number of elements by which to extend `x` at each end of the axis.
|
||||
axis : int, optional
|
||||
The axis along which to extend `x`. Default is -1.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> from scipy.signal._arraytools import const_ext
|
||||
>>> a = np.array([[1, 2, 3, 4, 5], [0, 1, 4, 9, 16]])
|
||||
>>> const_ext(a, 2)
|
||||
array([[ 1, 1, 1, 2, 3, 4, 5, 5, 5],
|
||||
[ 0, 0, 0, 1, 4, 9, 16, 16, 16]])
|
||||
|
||||
Constant extension continues with the same values as the endpoints of the
|
||||
array:
|
||||
|
||||
>>> t = np.linspace(0, 1.5, 100)
|
||||
>>> a = 0.9 * np.sin(2 * np.pi * t**2)
|
||||
>>> b = const_ext(a, 40)
|
||||
>>> import matplotlib.pyplot as plt
|
||||
>>> plt.plot(np.arange(-40, 140), b, 'b', lw=1, label='constant extension')
|
||||
>>> plt.plot(np.arange(100), a, 'r', lw=2, label='original')
|
||||
>>> plt.legend(loc='best')
|
||||
>>> plt.show()
|
||||
"""
|
||||
if n < 1:
|
||||
return x
|
||||
left_end = axis_slice(x, start=0, stop=1, axis=axis)
|
||||
ones_shape = [1] * x.ndim
|
||||
ones_shape[axis] = n
|
||||
ones = np.ones(ones_shape, dtype=x.dtype)
|
||||
left_ext = ones * left_end
|
||||
right_end = axis_slice(x, start=-1, axis=axis)
|
||||
right_ext = ones * right_end
|
||||
ext = np.concatenate((left_ext,
|
||||
x,
|
||||
right_ext),
|
||||
axis=axis)
|
||||
return ext
|
||||
|
||||
|
||||
def zero_ext(x, n, axis=-1):
|
||||
"""
|
||||
Zero padding at the boundaries of an array
|
||||
|
||||
Generate a new ndarray that is a zero-padded extension of `x` along
|
||||
an axis.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x : ndarray
|
||||
The array to be extended.
|
||||
n : int
|
||||
The number of elements by which to extend `x` at each end of the
|
||||
axis.
|
||||
axis : int, optional
|
||||
The axis along which to extend `x`. Default is -1.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> from scipy.signal._arraytools import zero_ext
|
||||
>>> a = np.array([[1, 2, 3, 4, 5], [0, 1, 4, 9, 16]])
|
||||
>>> zero_ext(a, 2)
|
||||
array([[ 0, 0, 1, 2, 3, 4, 5, 0, 0],
|
||||
[ 0, 0, 0, 1, 4, 9, 16, 0, 0]])
|
||||
"""
|
||||
if n < 1:
|
||||
return x
|
||||
zeros_shape = list(x.shape)
|
||||
zeros_shape[axis] = n
|
||||
zeros = np.zeros(zeros_shape, dtype=x.dtype)
|
||||
ext = np.concatenate((zeros, x, zeros), axis=axis)
|
||||
return ext
|
||||
|
||||
|
||||
def _validate_fs(fs, allow_none=True):
|
||||
"""
|
||||
Check if the given sampling frequency is a scalar and raises an exception
|
||||
otherwise. If allow_none is False, also raises an exception for none
|
||||
sampling rates. Returns the sampling frequency as float or none if the
|
||||
input is none.
|
||||
"""
|
||||
if fs is None:
|
||||
if not allow_none:
|
||||
raise ValueError("Sampling frequency can not be none.")
|
||||
else: # should be float
|
||||
if not np.isscalar(fs):
|
||||
raise ValueError("Sampling frequency fs must be a single scalar.")
|
||||
fs = float(fs)
|
||||
return fs
|
||||
519
venv/lib/python3.12/site-packages/scipy/signal/_bsplines.py
Normal file
519
venv/lib/python3.12/site-packages/scipy/signal/_bsplines.py
Normal file
@ -0,0 +1,519 @@
|
||||
from numpy import (asarray, pi, zeros_like,
|
||||
array, arctan2, tan, ones, arange, floor,
|
||||
r_, atleast_1d, sqrt, exp, greater, cos, add, sin)
|
||||
|
||||
# From splinemodule.c
|
||||
from ._spline import cspline2d, sepfir2d
|
||||
from ._signaltools import lfilter, sosfilt, lfiltic
|
||||
|
||||
from scipy.interpolate import BSpline
|
||||
|
||||
__all__ = ['spline_filter', 'gauss_spline',
|
||||
'cspline1d', 'qspline1d', 'cspline1d_eval', 'qspline1d_eval']
|
||||
|
||||
|
||||
def spline_filter(Iin, lmbda=5.0):
|
||||
"""Smoothing spline (cubic) filtering of a rank-2 array.
|
||||
|
||||
Filter an input data set, `Iin`, using a (cubic) smoothing spline of
|
||||
fall-off `lmbda`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
Iin : array_like
|
||||
input data set
|
||||
lmbda : float, optional
|
||||
spline smooghing fall-off value, default is `5.0`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
res : ndarray
|
||||
filtered input data
|
||||
|
||||
Examples
|
||||
--------
|
||||
We can filter an multi dimensional signal (ex: 2D image) using cubic
|
||||
B-spline filter:
|
||||
|
||||
>>> import numpy as np
|
||||
>>> from scipy.signal import spline_filter
|
||||
>>> import matplotlib.pyplot as plt
|
||||
>>> orig_img = np.eye(20) # create an image
|
||||
>>> orig_img[10, :] = 1.0
|
||||
>>> sp_filter = spline_filter(orig_img, lmbda=0.1)
|
||||
>>> f, ax = plt.subplots(1, 2, sharex=True)
|
||||
>>> for ind, data in enumerate([[orig_img, "original image"],
|
||||
... [sp_filter, "spline filter"]]):
|
||||
... ax[ind].imshow(data[0], cmap='gray_r')
|
||||
... ax[ind].set_title(data[1])
|
||||
>>> plt.tight_layout()
|
||||
>>> plt.show()
|
||||
|
||||
"""
|
||||
intype = Iin.dtype.char
|
||||
hcol = array([1.0, 4.0, 1.0], 'f') / 6.0
|
||||
if intype in ['F', 'D']:
|
||||
Iin = Iin.astype('F')
|
||||
ckr = cspline2d(Iin.real, lmbda)
|
||||
cki = cspline2d(Iin.imag, lmbda)
|
||||
outr = sepfir2d(ckr, hcol, hcol)
|
||||
outi = sepfir2d(cki, hcol, hcol)
|
||||
out = (outr + 1j * outi).astype(intype)
|
||||
elif intype in ['f', 'd']:
|
||||
ckr = cspline2d(Iin, lmbda)
|
||||
out = sepfir2d(ckr, hcol, hcol)
|
||||
out = out.astype(intype)
|
||||
else:
|
||||
raise TypeError("Invalid data type for Iin")
|
||||
return out
|
||||
|
||||
|
||||
_splinefunc_cache = {}
|
||||
|
||||
|
||||
def gauss_spline(x, n):
|
||||
r"""Gaussian approximation to B-spline basis function of order n.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x : array_like
|
||||
a knot vector
|
||||
n : int
|
||||
The order of the spline. Must be non-negative, i.e., n >= 0
|
||||
|
||||
Returns
|
||||
-------
|
||||
res : ndarray
|
||||
B-spline basis function values approximated by a zero-mean Gaussian
|
||||
function.
|
||||
|
||||
Notes
|
||||
-----
|
||||
The B-spline basis function can be approximated well by a zero-mean
|
||||
Gaussian function with standard-deviation equal to :math:`\sigma=(n+1)/12`
|
||||
for large `n` :
|
||||
|
||||
.. math:: \frac{1}{\sqrt {2\pi\sigma^2}}exp(-\frac{x^2}{2\sigma})
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Bouma H., Vilanova A., Bescos J.O., ter Haar Romeny B.M., Gerritsen
|
||||
F.A. (2007) Fast and Accurate Gaussian Derivatives Based on B-Splines. In:
|
||||
Sgallari F., Murli A., Paragios N. (eds) Scale Space and Variational
|
||||
Methods in Computer Vision. SSVM 2007. Lecture Notes in Computer
|
||||
Science, vol 4485. Springer, Berlin, Heidelberg
|
||||
.. [2] http://folk.uio.no/inf3330/scripting/doc/python/SciPy/tutorial/old/node24.html
|
||||
|
||||
Examples
|
||||
--------
|
||||
We can calculate B-Spline basis functions approximated by a gaussian
|
||||
distribution:
|
||||
|
||||
>>> import numpy as np
|
||||
>>> from scipy.signal import gauss_spline
|
||||
>>> knots = np.array([-1.0, 0.0, -1.0])
|
||||
>>> gauss_spline(knots, 3)
|
||||
array([0.15418033, 0.6909883, 0.15418033]) # may vary
|
||||
|
||||
"""
|
||||
x = asarray(x)
|
||||
signsq = (n + 1) / 12.0
|
||||
return 1 / sqrt(2 * pi * signsq) * exp(-x ** 2 / 2 / signsq)
|
||||
|
||||
|
||||
def _cubic(x):
|
||||
x = asarray(x, dtype=float)
|
||||
b = BSpline.basis_element([-2, -1, 0, 1, 2], extrapolate=False)
|
||||
out = b(x)
|
||||
out[(x < -2) | (x > 2)] = 0
|
||||
return out
|
||||
|
||||
|
||||
def _quadratic(x):
|
||||
x = abs(asarray(x, dtype=float))
|
||||
b = BSpline.basis_element([-1.5, -0.5, 0.5, 1.5], extrapolate=False)
|
||||
out = b(x)
|
||||
out[(x < -1.5) | (x > 1.5)] = 0
|
||||
return out
|
||||
|
||||
|
||||
def _coeff_smooth(lam):
|
||||
xi = 1 - 96 * lam + 24 * lam * sqrt(3 + 144 * lam)
|
||||
omeg = arctan2(sqrt(144 * lam - 1), sqrt(xi))
|
||||
rho = (24 * lam - 1 - sqrt(xi)) / (24 * lam)
|
||||
rho = rho * sqrt((48 * lam + 24 * lam * sqrt(3 + 144 * lam)) / xi)
|
||||
return rho, omeg
|
||||
|
||||
|
||||
def _hc(k, cs, rho, omega):
|
||||
return (cs / sin(omega) * (rho ** k) * sin(omega * (k + 1)) *
|
||||
greater(k, -1))
|
||||
|
||||
|
||||
def _hs(k, cs, rho, omega):
|
||||
c0 = (cs * cs * (1 + rho * rho) / (1 - rho * rho) /
|
||||
(1 - 2 * rho * rho * cos(2 * omega) + rho ** 4))
|
||||
gamma = (1 - rho * rho) / (1 + rho * rho) / tan(omega)
|
||||
ak = abs(k)
|
||||
return c0 * rho ** ak * (cos(omega * ak) + gamma * sin(omega * ak))
|
||||
|
||||
|
||||
def _cubic_smooth_coeff(signal, lamb):
|
||||
rho, omega = _coeff_smooth(lamb)
|
||||
cs = 1 - 2 * rho * cos(omega) + rho * rho
|
||||
K = len(signal)
|
||||
k = arange(K)
|
||||
|
||||
zi_2 = (_hc(0, cs, rho, omega) * signal[0] +
|
||||
add.reduce(_hc(k + 1, cs, rho, omega) * signal))
|
||||
zi_1 = (_hc(0, cs, rho, omega) * signal[0] +
|
||||
_hc(1, cs, rho, omega) * signal[1] +
|
||||
add.reduce(_hc(k + 2, cs, rho, omega) * signal))
|
||||
|
||||
# Forward filter:
|
||||
# for n in range(2, K):
|
||||
# yp[n] = (cs * signal[n] + 2 * rho * cos(omega) * yp[n - 1] -
|
||||
# rho * rho * yp[n - 2])
|
||||
zi = lfiltic(cs, r_[1, -2 * rho * cos(omega), rho * rho], r_[zi_1, zi_2])
|
||||
zi = zi.reshape(1, -1)
|
||||
|
||||
sos = r_[cs, 0, 0, 1, -2 * rho * cos(omega), rho * rho]
|
||||
sos = sos.reshape(1, -1)
|
||||
|
||||
yp, _ = sosfilt(sos, signal[2:], zi=zi)
|
||||
yp = r_[zi_2, zi_1, yp]
|
||||
|
||||
# Reverse filter:
|
||||
# for n in range(K - 3, -1, -1):
|
||||
# y[n] = (cs * yp[n] + 2 * rho * cos(omega) * y[n + 1] -
|
||||
# rho * rho * y[n + 2])
|
||||
|
||||
zi_2 = add.reduce((_hs(k, cs, rho, omega) +
|
||||
_hs(k + 1, cs, rho, omega)) * signal[::-1])
|
||||
zi_1 = add.reduce((_hs(k - 1, cs, rho, omega) +
|
||||
_hs(k + 2, cs, rho, omega)) * signal[::-1])
|
||||
|
||||
zi = lfiltic(cs, r_[1, -2 * rho * cos(omega), rho * rho], r_[zi_1, zi_2])
|
||||
zi = zi.reshape(1, -1)
|
||||
y, _ = sosfilt(sos, yp[-3::-1], zi=zi)
|
||||
y = r_[y[::-1], zi_1, zi_2]
|
||||
return y
|
||||
|
||||
|
||||
def _cubic_coeff(signal):
|
||||
zi = -2 + sqrt(3)
|
||||
K = len(signal)
|
||||
powers = zi ** arange(K)
|
||||
|
||||
if K == 1:
|
||||
yplus = signal[0] + zi * add.reduce(powers * signal)
|
||||
output = zi / (zi - 1) * yplus
|
||||
return atleast_1d(output)
|
||||
|
||||
# Forward filter:
|
||||
# yplus[0] = signal[0] + zi * add.reduce(powers * signal)
|
||||
# for k in range(1, K):
|
||||
# yplus[k] = signal[k] + zi * yplus[k - 1]
|
||||
|
||||
state = lfiltic(1, r_[1, -zi], atleast_1d(add.reduce(powers * signal)))
|
||||
|
||||
b = ones(1)
|
||||
a = r_[1, -zi]
|
||||
yplus, _ = lfilter(b, a, signal, zi=state)
|
||||
|
||||
# Reverse filter:
|
||||
# output[K - 1] = zi / (zi - 1) * yplus[K - 1]
|
||||
# for k in range(K - 2, -1, -1):
|
||||
# output[k] = zi * (output[k + 1] - yplus[k])
|
||||
out_last = zi / (zi - 1) * yplus[K - 1]
|
||||
state = lfiltic(-zi, r_[1, -zi], atleast_1d(out_last))
|
||||
|
||||
b = asarray([-zi])
|
||||
output, _ = lfilter(b, a, yplus[-2::-1], zi=state)
|
||||
output = r_[output[::-1], out_last]
|
||||
return output * 6.0
|
||||
|
||||
|
||||
def _quadratic_coeff(signal):
|
||||
zi = -3 + 2 * sqrt(2.0)
|
||||
K = len(signal)
|
||||
powers = zi ** arange(K)
|
||||
|
||||
if K == 1:
|
||||
yplus = signal[0] + zi * add.reduce(powers * signal)
|
||||
output = zi / (zi - 1) * yplus
|
||||
return atleast_1d(output)
|
||||
|
||||
# Forward filter:
|
||||
# yplus[0] = signal[0] + zi * add.reduce(powers * signal)
|
||||
# for k in range(1, K):
|
||||
# yplus[k] = signal[k] + zi * yplus[k - 1]
|
||||
|
||||
state = lfiltic(1, r_[1, -zi], atleast_1d(add.reduce(powers * signal)))
|
||||
|
||||
b = ones(1)
|
||||
a = r_[1, -zi]
|
||||
yplus, _ = lfilter(b, a, signal, zi=state)
|
||||
|
||||
# Reverse filter:
|
||||
# output[K - 1] = zi / (zi - 1) * yplus[K - 1]
|
||||
# for k in range(K - 2, -1, -1):
|
||||
# output[k] = zi * (output[k + 1] - yplus[k])
|
||||
out_last = zi / (zi - 1) * yplus[K - 1]
|
||||
state = lfiltic(-zi, r_[1, -zi], atleast_1d(out_last))
|
||||
|
||||
b = asarray([-zi])
|
||||
output, _ = lfilter(b, a, yplus[-2::-1], zi=state)
|
||||
output = r_[output[::-1], out_last]
|
||||
return output * 8.0
|
||||
|
||||
|
||||
def cspline1d(signal, lamb=0.0):
|
||||
"""
|
||||
Compute cubic spline coefficients for rank-1 array.
|
||||
|
||||
Find the cubic spline coefficients for a 1-D signal assuming
|
||||
mirror-symmetric boundary conditions. To obtain the signal back from the
|
||||
spline representation mirror-symmetric-convolve these coefficients with a
|
||||
length 3 FIR window [1.0, 4.0, 1.0]/ 6.0 .
|
||||
|
||||
Parameters
|
||||
----------
|
||||
signal : ndarray
|
||||
A rank-1 array representing samples of a signal.
|
||||
lamb : float, optional
|
||||
Smoothing coefficient, default is 0.0.
|
||||
|
||||
Returns
|
||||
-------
|
||||
c : ndarray
|
||||
Cubic spline coefficients.
|
||||
|
||||
See Also
|
||||
--------
|
||||
cspline1d_eval : Evaluate a cubic spline at the new set of points.
|
||||
|
||||
Examples
|
||||
--------
|
||||
We can filter a signal to reduce and smooth out high-frequency noise with
|
||||
a cubic spline:
|
||||
|
||||
>>> import numpy as np
|
||||
>>> import matplotlib.pyplot as plt
|
||||
>>> from scipy.signal import cspline1d, cspline1d_eval
|
||||
>>> rng = np.random.default_rng()
|
||||
>>> sig = np.repeat([0., 1., 0.], 100)
|
||||
>>> sig += rng.standard_normal(len(sig))*0.05 # add noise
|
||||
>>> time = np.linspace(0, len(sig))
|
||||
>>> filtered = cspline1d_eval(cspline1d(sig), time)
|
||||
>>> plt.plot(sig, label="signal")
|
||||
>>> plt.plot(time, filtered, label="filtered")
|
||||
>>> plt.legend()
|
||||
>>> plt.show()
|
||||
|
||||
"""
|
||||
if lamb != 0.0:
|
||||
return _cubic_smooth_coeff(signal, lamb)
|
||||
else:
|
||||
return _cubic_coeff(signal)
|
||||
|
||||
|
||||
def qspline1d(signal, lamb=0.0):
|
||||
"""Compute quadratic spline coefficients for rank-1 array.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
signal : ndarray
|
||||
A rank-1 array representing samples of a signal.
|
||||
lamb : float, optional
|
||||
Smoothing coefficient (must be zero for now).
|
||||
|
||||
Returns
|
||||
-------
|
||||
c : ndarray
|
||||
Quadratic spline coefficients.
|
||||
|
||||
See Also
|
||||
--------
|
||||
qspline1d_eval : Evaluate a quadratic spline at the new set of points.
|
||||
|
||||
Notes
|
||||
-----
|
||||
Find the quadratic spline coefficients for a 1-D signal assuming
|
||||
mirror-symmetric boundary conditions. To obtain the signal back from the
|
||||
spline representation mirror-symmetric-convolve these coefficients with a
|
||||
length 3 FIR window [1.0, 6.0, 1.0]/ 8.0 .
|
||||
|
||||
Examples
|
||||
--------
|
||||
We can filter a signal to reduce and smooth out high-frequency noise with
|
||||
a quadratic spline:
|
||||
|
||||
>>> import numpy as np
|
||||
>>> import matplotlib.pyplot as plt
|
||||
>>> from scipy.signal import qspline1d, qspline1d_eval
|
||||
>>> rng = np.random.default_rng()
|
||||
>>> sig = np.repeat([0., 1., 0.], 100)
|
||||
>>> sig += rng.standard_normal(len(sig))*0.05 # add noise
|
||||
>>> time = np.linspace(0, len(sig))
|
||||
>>> filtered = qspline1d_eval(qspline1d(sig), time)
|
||||
>>> plt.plot(sig, label="signal")
|
||||
>>> plt.plot(time, filtered, label="filtered")
|
||||
>>> plt.legend()
|
||||
>>> plt.show()
|
||||
|
||||
"""
|
||||
if lamb != 0.0:
|
||||
raise ValueError("Smoothing quadratic splines not supported yet.")
|
||||
else:
|
||||
return _quadratic_coeff(signal)
|
||||
|
||||
|
||||
def cspline1d_eval(cj, newx, dx=1.0, x0=0):
|
||||
"""Evaluate a cubic spline at the new set of points.
|
||||
|
||||
`dx` is the old sample-spacing while `x0` was the old origin. In
|
||||
other-words the old-sample points (knot-points) for which the `cj`
|
||||
represent spline coefficients were at equally-spaced points of:
|
||||
|
||||
oldx = x0 + j*dx j=0...N-1, with N=len(cj)
|
||||
|
||||
Edges are handled using mirror-symmetric boundary conditions.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cj : ndarray
|
||||
cublic spline coefficients
|
||||
newx : ndarray
|
||||
New set of points.
|
||||
dx : float, optional
|
||||
Old sample-spacing, the default value is 1.0.
|
||||
x0 : int, optional
|
||||
Old origin, the default value is 0.
|
||||
|
||||
Returns
|
||||
-------
|
||||
res : ndarray
|
||||
Evaluated a cubic spline points.
|
||||
|
||||
See Also
|
||||
--------
|
||||
cspline1d : Compute cubic spline coefficients for rank-1 array.
|
||||
|
||||
Examples
|
||||
--------
|
||||
We can filter a signal to reduce and smooth out high-frequency noise with
|
||||
a cubic spline:
|
||||
|
||||
>>> import numpy as np
|
||||
>>> import matplotlib.pyplot as plt
|
||||
>>> from scipy.signal import cspline1d, cspline1d_eval
|
||||
>>> rng = np.random.default_rng()
|
||||
>>> sig = np.repeat([0., 1., 0.], 100)
|
||||
>>> sig += rng.standard_normal(len(sig))*0.05 # add noise
|
||||
>>> time = np.linspace(0, len(sig))
|
||||
>>> filtered = cspline1d_eval(cspline1d(sig), time)
|
||||
>>> plt.plot(sig, label="signal")
|
||||
>>> plt.plot(time, filtered, label="filtered")
|
||||
>>> plt.legend()
|
||||
>>> plt.show()
|
||||
|
||||
"""
|
||||
newx = (asarray(newx) - x0) / float(dx)
|
||||
res = zeros_like(newx, dtype=cj.dtype)
|
||||
if res.size == 0:
|
||||
return res
|
||||
N = len(cj)
|
||||
cond1 = newx < 0
|
||||
cond2 = newx > (N - 1)
|
||||
cond3 = ~(cond1 | cond2)
|
||||
# handle general mirror-symmetry
|
||||
res[cond1] = cspline1d_eval(cj, -newx[cond1])
|
||||
res[cond2] = cspline1d_eval(cj, 2 * (N - 1) - newx[cond2])
|
||||
newx = newx[cond3]
|
||||
if newx.size == 0:
|
||||
return res
|
||||
result = zeros_like(newx, dtype=cj.dtype)
|
||||
jlower = floor(newx - 2).astype(int) + 1
|
||||
for i in range(4):
|
||||
thisj = jlower + i
|
||||
indj = thisj.clip(0, N - 1) # handle edge cases
|
||||
result += cj[indj] * _cubic(newx - thisj)
|
||||
res[cond3] = result
|
||||
return res
|
||||
|
||||
|
||||
def qspline1d_eval(cj, newx, dx=1.0, x0=0):
|
||||
"""Evaluate a quadratic spline at the new set of points.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cj : ndarray
|
||||
Quadratic spline coefficients
|
||||
newx : ndarray
|
||||
New set of points.
|
||||
dx : float, optional
|
||||
Old sample-spacing, the default value is 1.0.
|
||||
x0 : int, optional
|
||||
Old origin, the default value is 0.
|
||||
|
||||
Returns
|
||||
-------
|
||||
res : ndarray
|
||||
Evaluated a quadratic spline points.
|
||||
|
||||
See Also
|
||||
--------
|
||||
qspline1d : Compute quadratic spline coefficients for rank-1 array.
|
||||
|
||||
Notes
|
||||
-----
|
||||
`dx` is the old sample-spacing while `x0` was the old origin. In
|
||||
other-words the old-sample points (knot-points) for which the `cj`
|
||||
represent spline coefficients were at equally-spaced points of::
|
||||
|
||||
oldx = x0 + j*dx j=0...N-1, with N=len(cj)
|
||||
|
||||
Edges are handled using mirror-symmetric boundary conditions.
|
||||
|
||||
Examples
|
||||
--------
|
||||
We can filter a signal to reduce and smooth out high-frequency noise with
|
||||
a quadratic spline:
|
||||
|
||||
>>> import numpy as np
|
||||
>>> import matplotlib.pyplot as plt
|
||||
>>> from scipy.signal import qspline1d, qspline1d_eval
|
||||
>>> rng = np.random.default_rng()
|
||||
>>> sig = np.repeat([0., 1., 0.], 100)
|
||||
>>> sig += rng.standard_normal(len(sig))*0.05 # add noise
|
||||
>>> time = np.linspace(0, len(sig))
|
||||
>>> filtered = qspline1d_eval(qspline1d(sig), time)
|
||||
>>> plt.plot(sig, label="signal")
|
||||
>>> plt.plot(time, filtered, label="filtered")
|
||||
>>> plt.legend()
|
||||
>>> plt.show()
|
||||
|
||||
"""
|
||||
newx = (asarray(newx) - x0) / dx
|
||||
res = zeros_like(newx)
|
||||
if res.size == 0:
|
||||
return res
|
||||
N = len(cj)
|
||||
cond1 = newx < 0
|
||||
cond2 = newx > (N - 1)
|
||||
cond3 = ~(cond1 | cond2)
|
||||
# handle general mirror-symmetry
|
||||
res[cond1] = qspline1d_eval(cj, -newx[cond1])
|
||||
res[cond2] = qspline1d_eval(cj, 2 * (N - 1) - newx[cond2])
|
||||
newx = newx[cond3]
|
||||
if newx.size == 0:
|
||||
return res
|
||||
result = zeros_like(newx)
|
||||
jlower = floor(newx - 1.5).astype(int) + 1
|
||||
for i in range(3):
|
||||
thisj = jlower + i
|
||||
indj = thisj.clip(0, N - 1) # handle edge cases
|
||||
result += cj[indj] * _quadratic(newx - thisj)
|
||||
res[cond3] = result
|
||||
return res
|
||||
575
venv/lib/python3.12/site-packages/scipy/signal/_czt.py
Normal file
575
venv/lib/python3.12/site-packages/scipy/signal/_czt.py
Normal file
@ -0,0 +1,575 @@
|
||||
# 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)
|
||||
5623
venv/lib/python3.12/site-packages/scipy/signal/_filter_design.py
Normal file
5623
venv/lib/python3.12/site-packages/scipy/signal/_filter_design.py
Normal file
File diff suppressed because it is too large
Load Diff
1282
venv/lib/python3.12/site-packages/scipy/signal/_fir_filter_design.py
Normal file
1282
venv/lib/python3.12/site-packages/scipy/signal/_fir_filter_design.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,533 @@
|
||||
"""
|
||||
ltisys -- a collection of functions to convert linear time invariant systems
|
||||
from one representation to another.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
from numpy import (r_, eye, atleast_2d, poly, dot,
|
||||
asarray, zeros, array, outer)
|
||||
from scipy import linalg
|
||||
|
||||
from ._filter_design import tf2zpk, zpk2tf, normalize
|
||||
|
||||
|
||||
__all__ = ['tf2ss', 'abcd_normalize', 'ss2tf', 'zpk2ss', 'ss2zpk',
|
||||
'cont2discrete']
|
||||
|
||||
|
||||
def tf2ss(num, den):
|
||||
r"""Transfer function to state-space representation.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
num, den : array_like
|
||||
Sequences representing the coefficients of the numerator and
|
||||
denominator polynomials, in order of descending degree. The
|
||||
denominator needs to be at least as long as the numerator.
|
||||
|
||||
Returns
|
||||
-------
|
||||
A, B, C, D : ndarray
|
||||
State space representation of the system, in controller canonical
|
||||
form.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Convert the transfer function:
|
||||
|
||||
.. math:: H(s) = \frac{s^2 + 3s + 3}{s^2 + 2s + 1}
|
||||
|
||||
>>> num = [1, 3, 3]
|
||||
>>> den = [1, 2, 1]
|
||||
|
||||
to the state-space representation:
|
||||
|
||||
.. math::
|
||||
|
||||
\dot{\textbf{x}}(t) =
|
||||
\begin{bmatrix} -2 & -1 \\ 1 & 0 \end{bmatrix} \textbf{x}(t) +
|
||||
\begin{bmatrix} 1 \\ 0 \end{bmatrix} \textbf{u}(t) \\
|
||||
|
||||
\textbf{y}(t) = \begin{bmatrix} 1 & 2 \end{bmatrix} \textbf{x}(t) +
|
||||
\begin{bmatrix} 1 \end{bmatrix} \textbf{u}(t)
|
||||
|
||||
>>> from scipy.signal import tf2ss
|
||||
>>> A, B, C, D = tf2ss(num, den)
|
||||
>>> A
|
||||
array([[-2., -1.],
|
||||
[ 1., 0.]])
|
||||
>>> B
|
||||
array([[ 1.],
|
||||
[ 0.]])
|
||||
>>> C
|
||||
array([[ 1., 2.]])
|
||||
>>> D
|
||||
array([[ 1.]])
|
||||
"""
|
||||
# Controller canonical state-space representation.
|
||||
# if M+1 = len(num) and K+1 = len(den) then we must have M <= K
|
||||
# states are found by asserting that X(s) = U(s) / D(s)
|
||||
# then Y(s) = N(s) * X(s)
|
||||
#
|
||||
# A, B, C, and D follow quite naturally.
|
||||
#
|
||||
num, den = normalize(num, den) # Strips zeros, checks arrays
|
||||
nn = len(num.shape)
|
||||
if nn == 1:
|
||||
num = asarray([num], num.dtype)
|
||||
M = num.shape[1]
|
||||
K = len(den)
|
||||
if M > K:
|
||||
msg = "Improper transfer function. `num` is longer than `den`."
|
||||
raise ValueError(msg)
|
||||
if M == 0 or K == 0: # Null system
|
||||
return (array([], float), array([], float), array([], float),
|
||||
array([], float))
|
||||
|
||||
# pad numerator to have same number of columns has denominator
|
||||
num = np.hstack((np.zeros((num.shape[0], K - M), dtype=num.dtype), num))
|
||||
|
||||
if num.shape[-1] > 0:
|
||||
D = atleast_2d(num[:, 0])
|
||||
|
||||
else:
|
||||
# We don't assign it an empty array because this system
|
||||
# is not 'null'. It just doesn't have a non-zero D
|
||||
# matrix. Thus, it should have a non-zero shape so that
|
||||
# it can be operated on by functions like 'ss2tf'
|
||||
D = array([[0]], float)
|
||||
|
||||
if K == 1:
|
||||
D = D.reshape(num.shape)
|
||||
|
||||
return (zeros((1, 1)), zeros((1, D.shape[1])),
|
||||
zeros((D.shape[0], 1)), D)
|
||||
|
||||
frow = -array([den[1:]])
|
||||
A = r_[frow, eye(K - 2, K - 1)]
|
||||
B = eye(K - 1, 1)
|
||||
C = num[:, 1:] - outer(num[:, 0], den[1:])
|
||||
D = D.reshape((C.shape[0], B.shape[1]))
|
||||
|
||||
return A, B, C, D
|
||||
|
||||
|
||||
def _none_to_empty_2d(arg):
|
||||
if arg is None:
|
||||
return zeros((0, 0))
|
||||
else:
|
||||
return arg
|
||||
|
||||
|
||||
def _atleast_2d_or_none(arg):
|
||||
if arg is not None:
|
||||
return atleast_2d(arg)
|
||||
|
||||
|
||||
def _shape_or_none(M):
|
||||
if M is not None:
|
||||
return M.shape
|
||||
else:
|
||||
return (None,) * 2
|
||||
|
||||
|
||||
def _choice_not_none(*args):
|
||||
for arg in args:
|
||||
if arg is not None:
|
||||
return arg
|
||||
|
||||
|
||||
def _restore(M, shape):
|
||||
if M.shape == (0, 0):
|
||||
return zeros(shape)
|
||||
else:
|
||||
if M.shape != shape:
|
||||
raise ValueError("The input arrays have incompatible shapes.")
|
||||
return M
|
||||
|
||||
|
||||
def abcd_normalize(A=None, B=None, C=None, D=None):
|
||||
"""Check state-space matrices and ensure they are 2-D.
|
||||
|
||||
If enough information on the system is provided, that is, enough
|
||||
properly-shaped arrays are passed to the function, the missing ones
|
||||
are built from this information, ensuring the correct number of
|
||||
rows and columns. Otherwise a ValueError is raised.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A, B, C, D : array_like, optional
|
||||
State-space matrices. All of them are None (missing) by default.
|
||||
See `ss2tf` for format.
|
||||
|
||||
Returns
|
||||
-------
|
||||
A, B, C, D : array
|
||||
Properly shaped state-space matrices.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If not enough information on the system was provided.
|
||||
|
||||
"""
|
||||
A, B, C, D = map(_atleast_2d_or_none, (A, B, C, D))
|
||||
|
||||
MA, NA = _shape_or_none(A)
|
||||
MB, NB = _shape_or_none(B)
|
||||
MC, NC = _shape_or_none(C)
|
||||
MD, ND = _shape_or_none(D)
|
||||
|
||||
p = _choice_not_none(MA, MB, NC)
|
||||
q = _choice_not_none(NB, ND)
|
||||
r = _choice_not_none(MC, MD)
|
||||
if p is None or q is None or r is None:
|
||||
raise ValueError("Not enough information on the system.")
|
||||
|
||||
A, B, C, D = map(_none_to_empty_2d, (A, B, C, D))
|
||||
A = _restore(A, (p, p))
|
||||
B = _restore(B, (p, q))
|
||||
C = _restore(C, (r, p))
|
||||
D = _restore(D, (r, q))
|
||||
|
||||
return A, B, C, D
|
||||
|
||||
|
||||
def ss2tf(A, B, C, D, input=0):
|
||||
r"""State-space to transfer function.
|
||||
|
||||
A, B, C, D defines a linear state-space system with `p` inputs,
|
||||
`q` outputs, and `n` state variables.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : array_like
|
||||
State (or system) matrix of shape ``(n, n)``
|
||||
B : array_like
|
||||
Input matrix of shape ``(n, p)``
|
||||
C : array_like
|
||||
Output matrix of shape ``(q, n)``
|
||||
D : array_like
|
||||
Feedthrough (or feedforward) matrix of shape ``(q, p)``
|
||||
input : int, optional
|
||||
For multiple-input systems, the index of the input to use.
|
||||
|
||||
Returns
|
||||
-------
|
||||
num : 2-D ndarray
|
||||
Numerator(s) of the resulting transfer function(s). `num` has one row
|
||||
for each of the system's outputs. Each row is a sequence representation
|
||||
of the numerator polynomial.
|
||||
den : 1-D ndarray
|
||||
Denominator of the resulting transfer function(s). `den` is a sequence
|
||||
representation of the denominator polynomial.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Convert the state-space representation:
|
||||
|
||||
.. math::
|
||||
|
||||
\dot{\textbf{x}}(t) =
|
||||
\begin{bmatrix} -2 & -1 \\ 1 & 0 \end{bmatrix} \textbf{x}(t) +
|
||||
\begin{bmatrix} 1 \\ 0 \end{bmatrix} \textbf{u}(t) \\
|
||||
|
||||
\textbf{y}(t) = \begin{bmatrix} 1 & 2 \end{bmatrix} \textbf{x}(t) +
|
||||
\begin{bmatrix} 1 \end{bmatrix} \textbf{u}(t)
|
||||
|
||||
>>> A = [[-2, -1], [1, 0]]
|
||||
>>> B = [[1], [0]] # 2-D column vector
|
||||
>>> C = [[1, 2]] # 2-D row vector
|
||||
>>> D = 1
|
||||
|
||||
to the transfer function:
|
||||
|
||||
.. math:: H(s) = \frac{s^2 + 3s + 3}{s^2 + 2s + 1}
|
||||
|
||||
>>> from scipy.signal import ss2tf
|
||||
>>> ss2tf(A, B, C, D)
|
||||
(array([[1., 3., 3.]]), array([ 1., 2., 1.]))
|
||||
"""
|
||||
# transfer function is C (sI - A)**(-1) B + D
|
||||
|
||||
# Check consistency and make them all rank-2 arrays
|
||||
A, B, C, D = abcd_normalize(A, B, C, D)
|
||||
|
||||
nout, nin = D.shape
|
||||
if input >= nin:
|
||||
raise ValueError("System does not have the input specified.")
|
||||
|
||||
# make SIMO from possibly MIMO system.
|
||||
B = B[:, input:input + 1]
|
||||
D = D[:, input:input + 1]
|
||||
|
||||
try:
|
||||
den = poly(A)
|
||||
except ValueError:
|
||||
den = 1
|
||||
|
||||
if (B.size == 0) and (C.size == 0):
|
||||
num = np.ravel(D)
|
||||
if (D.size == 0) and (A.size == 0):
|
||||
den = []
|
||||
return num, den
|
||||
|
||||
num_states = A.shape[0]
|
||||
type_test = A[:, 0] + B[:, 0] + C[0, :] + D + 0.0
|
||||
num = np.empty((nout, num_states + 1), type_test.dtype)
|
||||
for k in range(nout):
|
||||
Ck = atleast_2d(C[k, :])
|
||||
num[k] = poly(A - dot(B, Ck)) + (D[k] - 1) * den
|
||||
|
||||
return num, den
|
||||
|
||||
|
||||
def zpk2ss(z, p, k):
|
||||
"""Zero-pole-gain representation to state-space representation
|
||||
|
||||
Parameters
|
||||
----------
|
||||
z, p : sequence
|
||||
Zeros and poles.
|
||||
k : float
|
||||
System gain.
|
||||
|
||||
Returns
|
||||
-------
|
||||
A, B, C, D : ndarray
|
||||
State space representation of the system, in controller canonical
|
||||
form.
|
||||
|
||||
"""
|
||||
return tf2ss(*zpk2tf(z, p, k))
|
||||
|
||||
|
||||
def ss2zpk(A, B, C, D, input=0):
|
||||
"""State-space representation to zero-pole-gain representation.
|
||||
|
||||
A, B, C, D defines a linear state-space system with `p` inputs,
|
||||
`q` outputs, and `n` state variables.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : array_like
|
||||
State (or system) matrix of shape ``(n, n)``
|
||||
B : array_like
|
||||
Input matrix of shape ``(n, p)``
|
||||
C : array_like
|
||||
Output matrix of shape ``(q, n)``
|
||||
D : array_like
|
||||
Feedthrough (or feedforward) matrix of shape ``(q, p)``
|
||||
input : int, optional
|
||||
For multiple-input systems, the index of the input to use.
|
||||
|
||||
Returns
|
||||
-------
|
||||
z, p : sequence
|
||||
Zeros and poles.
|
||||
k : float
|
||||
System gain.
|
||||
|
||||
"""
|
||||
return tf2zpk(*ss2tf(A, B, C, D, input=input))
|
||||
|
||||
|
||||
def cont2discrete(system, dt, method="zoh", alpha=None):
|
||||
"""
|
||||
Transform a continuous to a discrete state-space system.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
system : a tuple describing the system or an instance of `lti`
|
||||
The following gives the number of elements in the tuple and
|
||||
the interpretation:
|
||||
|
||||
* 1: (instance of `lti`)
|
||||
* 2: (num, den)
|
||||
* 3: (zeros, poles, gain)
|
||||
* 4: (A, B, C, D)
|
||||
|
||||
dt : float
|
||||
The discretization time step.
|
||||
method : str, optional
|
||||
Which method to use:
|
||||
|
||||
* gbt: generalized bilinear transformation
|
||||
* bilinear: Tustin's approximation ("gbt" with alpha=0.5)
|
||||
* euler: Euler (or forward differencing) method ("gbt" with alpha=0)
|
||||
* backward_diff: Backwards differencing ("gbt" with alpha=1.0)
|
||||
* zoh: zero-order hold (default)
|
||||
* foh: first-order hold (*versionadded: 1.3.0*)
|
||||
* impulse: equivalent impulse response (*versionadded: 1.3.0*)
|
||||
|
||||
alpha : float within [0, 1], optional
|
||||
The generalized bilinear transformation weighting parameter, which
|
||||
should only be specified with method="gbt", and is ignored otherwise
|
||||
|
||||
Returns
|
||||
-------
|
||||
sysd : tuple containing the discrete system
|
||||
Based on the input type, the output will be of the form
|
||||
|
||||
* (num, den, dt) for transfer function input
|
||||
* (zeros, poles, gain, dt) for zeros-poles-gain input
|
||||
* (A, B, C, D, dt) for state-space system input
|
||||
|
||||
Notes
|
||||
-----
|
||||
By default, the routine uses a Zero-Order Hold (zoh) method to perform
|
||||
the transformation. Alternatively, a generalized bilinear transformation
|
||||
may be used, which includes the common Tustin's bilinear approximation,
|
||||
an Euler's method technique, or a backwards differencing technique.
|
||||
|
||||
The Zero-Order Hold (zoh) method is based on [1]_, the generalized bilinear
|
||||
approximation is based on [2]_ and [3]_, the First-Order Hold (foh) method
|
||||
is based on [4]_.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] https://en.wikipedia.org/wiki/Discretization#Discretization_of_linear_state_space_models
|
||||
|
||||
.. [2] http://techteach.no/publications/discretetime_signals_systems/discrete.pdf
|
||||
|
||||
.. [3] G. Zhang, X. Chen, and T. Chen, Digital redesign via the generalized
|
||||
bilinear transformation, Int. J. Control, vol. 82, no. 4, pp. 741-754,
|
||||
2009.
|
||||
(https://www.mypolyuweb.hk/~magzhang/Research/ZCC09_IJC.pdf)
|
||||
|
||||
.. [4] G. F. Franklin, J. D. Powell, and M. L. Workman, Digital control
|
||||
of dynamic systems, 3rd ed. Menlo Park, Calif: Addison-Wesley,
|
||||
pp. 204-206, 1998.
|
||||
|
||||
Examples
|
||||
--------
|
||||
We can transform a continuous state-space system to a discrete one:
|
||||
|
||||
>>> import numpy as np
|
||||
>>> import matplotlib.pyplot as plt
|
||||
>>> from scipy.signal import cont2discrete, lti, dlti, dstep
|
||||
|
||||
Define a continuous state-space system.
|
||||
|
||||
>>> A = np.array([[0, 1],[-10., -3]])
|
||||
>>> B = np.array([[0],[10.]])
|
||||
>>> C = np.array([[1., 0]])
|
||||
>>> D = np.array([[0.]])
|
||||
>>> l_system = lti(A, B, C, D)
|
||||
>>> t, x = l_system.step(T=np.linspace(0, 5, 100))
|
||||
>>> fig, ax = plt.subplots()
|
||||
>>> ax.plot(t, x, label='Continuous', linewidth=3)
|
||||
|
||||
Transform it to a discrete state-space system using several methods.
|
||||
|
||||
>>> dt = 0.1
|
||||
>>> for method in ['zoh', 'bilinear', 'euler', 'backward_diff', 'foh', 'impulse']:
|
||||
... d_system = cont2discrete((A, B, C, D), dt, method=method)
|
||||
... s, x_d = dstep(d_system)
|
||||
... ax.step(s, np.squeeze(x_d), label=method, where='post')
|
||||
>>> ax.axis([t[0], t[-1], x[0], 1.4])
|
||||
>>> ax.legend(loc='best')
|
||||
>>> fig.tight_layout()
|
||||
>>> plt.show()
|
||||
|
||||
"""
|
||||
if len(system) == 1:
|
||||
return system.to_discrete()
|
||||
if len(system) == 2:
|
||||
sysd = cont2discrete(tf2ss(system[0], system[1]), dt, method=method,
|
||||
alpha=alpha)
|
||||
return ss2tf(sysd[0], sysd[1], sysd[2], sysd[3]) + (dt,)
|
||||
elif len(system) == 3:
|
||||
sysd = cont2discrete(zpk2ss(system[0], system[1], system[2]), dt,
|
||||
method=method, alpha=alpha)
|
||||
return ss2zpk(sysd[0], sysd[1], sysd[2], sysd[3]) + (dt,)
|
||||
elif len(system) == 4:
|
||||
a, b, c, d = system
|
||||
else:
|
||||
raise ValueError("First argument must either be a tuple of 2 (tf), "
|
||||
"3 (zpk), or 4 (ss) arrays.")
|
||||
|
||||
if method == 'gbt':
|
||||
if alpha is None:
|
||||
raise ValueError("Alpha parameter must be specified for the "
|
||||
"generalized bilinear transform (gbt) method")
|
||||
elif alpha < 0 or alpha > 1:
|
||||
raise ValueError("Alpha parameter must be within the interval "
|
||||
"[0,1] for the gbt method")
|
||||
|
||||
if method == 'gbt':
|
||||
# This parameter is used repeatedly - compute once here
|
||||
ima = np.eye(a.shape[0]) - alpha*dt*a
|
||||
ad = linalg.solve(ima, np.eye(a.shape[0]) + (1.0-alpha)*dt*a)
|
||||
bd = linalg.solve(ima, dt*b)
|
||||
|
||||
# Similarly solve for the output equation matrices
|
||||
cd = linalg.solve(ima.transpose(), c.transpose())
|
||||
cd = cd.transpose()
|
||||
dd = d + alpha*np.dot(c, bd)
|
||||
|
||||
elif method == 'bilinear' or method == 'tustin':
|
||||
return cont2discrete(system, dt, method="gbt", alpha=0.5)
|
||||
|
||||
elif method == 'euler' or method == 'forward_diff':
|
||||
return cont2discrete(system, dt, method="gbt", alpha=0.0)
|
||||
|
||||
elif method == 'backward_diff':
|
||||
return cont2discrete(system, dt, method="gbt", alpha=1.0)
|
||||
|
||||
elif method == 'zoh':
|
||||
# Build an exponential matrix
|
||||
em_upper = np.hstack((a, b))
|
||||
|
||||
# Need to stack zeros under the a and b matrices
|
||||
em_lower = np.hstack((np.zeros((b.shape[1], a.shape[0])),
|
||||
np.zeros((b.shape[1], b.shape[1]))))
|
||||
|
||||
em = np.vstack((em_upper, em_lower))
|
||||
ms = linalg.expm(dt * em)
|
||||
|
||||
# Dispose of the lower rows
|
||||
ms = ms[:a.shape[0], :]
|
||||
|
||||
ad = ms[:, 0:a.shape[1]]
|
||||
bd = ms[:, a.shape[1]:]
|
||||
|
||||
cd = c
|
||||
dd = d
|
||||
|
||||
elif method == 'foh':
|
||||
# Size parameters for convenience
|
||||
n = a.shape[0]
|
||||
m = b.shape[1]
|
||||
|
||||
# Build an exponential matrix similar to 'zoh' method
|
||||
em_upper = linalg.block_diag(np.block([a, b]) * dt, np.eye(m))
|
||||
em_lower = zeros((m, n + 2 * m))
|
||||
em = np.block([[em_upper], [em_lower]])
|
||||
|
||||
ms = linalg.expm(em)
|
||||
|
||||
# Get the three blocks from upper rows
|
||||
ms11 = ms[:n, 0:n]
|
||||
ms12 = ms[:n, n:n + m]
|
||||
ms13 = ms[:n, n + m:]
|
||||
|
||||
ad = ms11
|
||||
bd = ms12 - ms13 + ms11 @ ms13
|
||||
cd = c
|
||||
dd = d + c @ ms13
|
||||
|
||||
elif method == 'impulse':
|
||||
if not np.allclose(d, 0):
|
||||
raise ValueError("Impulse method is only applicable "
|
||||
"to strictly proper systems")
|
||||
|
||||
ad = linalg.expm(a * dt)
|
||||
bd = ad @ b * dt
|
||||
cd = c
|
||||
dd = c @ b * dt
|
||||
|
||||
else:
|
||||
raise ValueError("Unknown transformation method '%s'" % method)
|
||||
|
||||
return ad, bd, cd, dd, dt
|
||||
3495
venv/lib/python3.12/site-packages/scipy/signal/_ltisys.py
Normal file
3495
venv/lib/python3.12/site-packages/scipy/signal/_ltisys.py
Normal file
File diff suppressed because it is too large
Load Diff
139
venv/lib/python3.12/site-packages/scipy/signal/_max_len_seq.py
Normal file
139
venv/lib/python3.12/site-packages/scipy/signal/_max_len_seq.py
Normal file
@ -0,0 +1,139 @@
|
||||
# Author: Eric Larson
|
||||
# 2014
|
||||
|
||||
"""Tools for MLS generation"""
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ._max_len_seq_inner import _max_len_seq_inner
|
||||
|
||||
__all__ = ['max_len_seq']
|
||||
|
||||
|
||||
# These are definitions of linear shift register taps for use in max_len_seq()
|
||||
_mls_taps = {2: [1], 3: [2], 4: [3], 5: [3], 6: [5], 7: [6], 8: [7, 6, 1],
|
||||
9: [5], 10: [7], 11: [9], 12: [11, 10, 4], 13: [12, 11, 8],
|
||||
14: [13, 12, 2], 15: [14], 16: [15, 13, 4], 17: [14],
|
||||
18: [11], 19: [18, 17, 14], 20: [17], 21: [19], 22: [21],
|
||||
23: [18], 24: [23, 22, 17], 25: [22], 26: [25, 24, 20],
|
||||
27: [26, 25, 22], 28: [25], 29: [27], 30: [29, 28, 7],
|
||||
31: [28], 32: [31, 30, 10]}
|
||||
|
||||
def max_len_seq(nbits, state=None, length=None, taps=None):
|
||||
"""
|
||||
Maximum length sequence (MLS) generator.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
nbits : int
|
||||
Number of bits to use. Length of the resulting sequence will
|
||||
be ``(2**nbits) - 1``. Note that generating long sequences
|
||||
(e.g., greater than ``nbits == 16``) can take a long time.
|
||||
state : array_like, optional
|
||||
If array, must be of length ``nbits``, and will be cast to binary
|
||||
(bool) representation. If None, a seed of ones will be used,
|
||||
producing a repeatable representation. If ``state`` is all
|
||||
zeros, an error is raised as this is invalid. Default: None.
|
||||
length : int, optional
|
||||
Number of samples to compute. If None, the entire length
|
||||
``(2**nbits) - 1`` is computed.
|
||||
taps : array_like, optional
|
||||
Polynomial taps to use (e.g., ``[7, 6, 1]`` for an 8-bit sequence).
|
||||
If None, taps will be automatically selected (for up to
|
||||
``nbits == 32``).
|
||||
|
||||
Returns
|
||||
-------
|
||||
seq : array
|
||||
Resulting MLS sequence of 0's and 1's.
|
||||
state : array
|
||||
The final state of the shift register.
|
||||
|
||||
Notes
|
||||
-----
|
||||
The algorithm for MLS generation is generically described in:
|
||||
|
||||
https://en.wikipedia.org/wiki/Maximum_length_sequence
|
||||
|
||||
The default values for taps are specifically taken from the first
|
||||
option listed for each value of ``nbits`` in:
|
||||
|
||||
https://web.archive.org/web/20181001062252/http://www.newwaveinstruments.com/resources/articles/m_sequence_linear_feedback_shift_register_lfsr.htm
|
||||
|
||||
.. versionadded:: 0.15.0
|
||||
|
||||
Examples
|
||||
--------
|
||||
MLS uses binary convention:
|
||||
|
||||
>>> from scipy.signal import max_len_seq
|
||||
>>> max_len_seq(4)[0]
|
||||
array([1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0], dtype=int8)
|
||||
|
||||
MLS has a white spectrum (except for DC):
|
||||
|
||||
>>> import numpy as np
|
||||
>>> import matplotlib.pyplot as plt
|
||||
>>> from numpy.fft import fft, ifft, fftshift, fftfreq
|
||||
>>> seq = max_len_seq(6)[0]*2-1 # +1 and -1
|
||||
>>> spec = fft(seq)
|
||||
>>> N = len(seq)
|
||||
>>> plt.plot(fftshift(fftfreq(N)), fftshift(np.abs(spec)), '.-')
|
||||
>>> plt.margins(0.1, 0.1)
|
||||
>>> plt.grid(True)
|
||||
>>> plt.show()
|
||||
|
||||
Circular autocorrelation of MLS is an impulse:
|
||||
|
||||
>>> acorrcirc = ifft(spec * np.conj(spec)).real
|
||||
>>> plt.figure()
|
||||
>>> plt.plot(np.arange(-N/2+1, N/2+1), fftshift(acorrcirc), '.-')
|
||||
>>> plt.margins(0.1, 0.1)
|
||||
>>> plt.grid(True)
|
||||
>>> plt.show()
|
||||
|
||||
Linear autocorrelation of MLS is approximately an impulse:
|
||||
|
||||
>>> acorr = np.correlate(seq, seq, 'full')
|
||||
>>> plt.figure()
|
||||
>>> plt.plot(np.arange(-N+1, N), acorr, '.-')
|
||||
>>> plt.margins(0.1, 0.1)
|
||||
>>> plt.grid(True)
|
||||
>>> plt.show()
|
||||
|
||||
"""
|
||||
taps_dtype = np.int32 if np.intp().itemsize == 4 else np.int64
|
||||
if taps is None:
|
||||
if nbits not in _mls_taps:
|
||||
known_taps = np.array(list(_mls_taps.keys()))
|
||||
raise ValueError(f'nbits must be between {known_taps.min()} and '
|
||||
f'{known_taps.max()} if taps is None')
|
||||
taps = np.array(_mls_taps[nbits], taps_dtype)
|
||||
else:
|
||||
taps = np.unique(np.array(taps, taps_dtype))[::-1]
|
||||
if np.any(taps < 0) or np.any(taps > nbits) or taps.size < 1:
|
||||
raise ValueError('taps must be non-empty with values between '
|
||||
'zero and nbits (inclusive)')
|
||||
taps = np.array(taps) # needed for Cython and Pythran
|
||||
n_max = (2**nbits) - 1
|
||||
if length is None:
|
||||
length = n_max
|
||||
else:
|
||||
length = int(length)
|
||||
if length < 0:
|
||||
raise ValueError('length must be greater than or equal to 0')
|
||||
# We use int8 instead of bool here because NumPy arrays of bools
|
||||
# don't seem to work nicely with Cython
|
||||
if state is None:
|
||||
state = np.ones(nbits, dtype=np.int8, order='c')
|
||||
else:
|
||||
# makes a copy if need be, ensuring it's 0's and 1's
|
||||
state = np.array(state, dtype=bool, order='c').astype(np.int8)
|
||||
if state.ndim != 1 or state.size != nbits:
|
||||
raise ValueError('state must be a 1-D array of size nbits')
|
||||
if np.all(state == 0):
|
||||
raise ValueError('state must not be all zeros')
|
||||
|
||||
seq = np.empty(length, dtype=np.int8, order='c')
|
||||
state = _max_len_seq_inner(taps, state, nbits, length, seq)
|
||||
return seq, state
|
||||
Binary file not shown.
1312
venv/lib/python3.12/site-packages/scipy/signal/_peak_finding.py
Normal file
1312
venv/lib/python3.12/site-packages/scipy/signal/_peak_finding.py
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -0,0 +1,357 @@
|
||||
import numpy as np
|
||||
from scipy.linalg import lstsq
|
||||
from scipy._lib._util import float_factorial
|
||||
from scipy.ndimage import convolve1d
|
||||
from ._arraytools import axis_slice
|
||||
|
||||
|
||||
def savgol_coeffs(window_length, polyorder, deriv=0, delta=1.0, pos=None,
|
||||
use="conv"):
|
||||
"""Compute the coefficients for a 1-D Savitzky-Golay FIR filter.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
window_length : int
|
||||
The length of the filter window (i.e., the number of coefficients).
|
||||
polyorder : int
|
||||
The order of the polynomial used to fit the samples.
|
||||
`polyorder` must be less than `window_length`.
|
||||
deriv : int, optional
|
||||
The order of the derivative to compute. This must be a
|
||||
nonnegative integer. The default is 0, which means to filter
|
||||
the data without differentiating.
|
||||
delta : float, optional
|
||||
The spacing of the samples to which the filter will be applied.
|
||||
This is only used if deriv > 0.
|
||||
pos : int or None, optional
|
||||
If pos is not None, it specifies evaluation position within the
|
||||
window. The default is the middle of the window.
|
||||
use : str, optional
|
||||
Either 'conv' or 'dot'. This argument chooses the order of the
|
||||
coefficients. The default is 'conv', which means that the
|
||||
coefficients are ordered to be used in a convolution. With
|
||||
use='dot', the order is reversed, so the filter is applied by
|
||||
dotting the coefficients with the data set.
|
||||
|
||||
Returns
|
||||
-------
|
||||
coeffs : 1-D ndarray
|
||||
The filter coefficients.
|
||||
|
||||
See Also
|
||||
--------
|
||||
savgol_filter
|
||||
|
||||
Notes
|
||||
-----
|
||||
.. versionadded:: 0.14.0
|
||||
|
||||
References
|
||||
----------
|
||||
A. Savitzky, M. J. E. Golay, Smoothing and Differentiation of Data by
|
||||
Simplified Least Squares Procedures. Analytical Chemistry, 1964, 36 (8),
|
||||
pp 1627-1639.
|
||||
Jianwen Luo, Kui Ying, and Jing Bai. 2005. Savitzky-Golay smoothing and
|
||||
differentiation filter for even number data. Signal Process.
|
||||
85, 7 (July 2005), 1429-1434.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> from scipy.signal import savgol_coeffs
|
||||
>>> savgol_coeffs(5, 2)
|
||||
array([-0.08571429, 0.34285714, 0.48571429, 0.34285714, -0.08571429])
|
||||
>>> savgol_coeffs(5, 2, deriv=1)
|
||||
array([ 2.00000000e-01, 1.00000000e-01, 2.07548111e-16, -1.00000000e-01,
|
||||
-2.00000000e-01])
|
||||
|
||||
Note that use='dot' simply reverses the coefficients.
|
||||
|
||||
>>> savgol_coeffs(5, 2, pos=3)
|
||||
array([ 0.25714286, 0.37142857, 0.34285714, 0.17142857, -0.14285714])
|
||||
>>> savgol_coeffs(5, 2, pos=3, use='dot')
|
||||
array([-0.14285714, 0.17142857, 0.34285714, 0.37142857, 0.25714286])
|
||||
>>> savgol_coeffs(4, 2, pos=3, deriv=1, use='dot')
|
||||
array([0.45, -0.85, -0.65, 1.05])
|
||||
|
||||
`x` contains data from the parabola x = t**2, sampled at
|
||||
t = -1, 0, 1, 2, 3. `c` holds the coefficients that will compute the
|
||||
derivative at the last position. When dotted with `x` the result should
|
||||
be 6.
|
||||
|
||||
>>> x = np.array([1, 0, 1, 4, 9])
|
||||
>>> c = savgol_coeffs(5, 2, pos=4, deriv=1, use='dot')
|
||||
>>> c.dot(x)
|
||||
6.0
|
||||
"""
|
||||
|
||||
# An alternative method for finding the coefficients when deriv=0 is
|
||||
# t = np.arange(window_length)
|
||||
# unit = (t == pos).astype(int)
|
||||
# coeffs = np.polyval(np.polyfit(t, unit, polyorder), t)
|
||||
# The method implemented here is faster.
|
||||
|
||||
# To recreate the table of sample coefficients shown in the chapter on
|
||||
# the Savitzy-Golay filter in the Numerical Recipes book, use
|
||||
# window_length = nL + nR + 1
|
||||
# pos = nL + 1
|
||||
# c = savgol_coeffs(window_length, M, pos=pos, use='dot')
|
||||
|
||||
if polyorder >= window_length:
|
||||
raise ValueError("polyorder must be less than window_length.")
|
||||
|
||||
halflen, rem = divmod(window_length, 2)
|
||||
|
||||
if pos is None:
|
||||
if rem == 0:
|
||||
pos = halflen - 0.5
|
||||
else:
|
||||
pos = halflen
|
||||
|
||||
if not (0 <= pos < window_length):
|
||||
raise ValueError("pos must be nonnegative and less than "
|
||||
"window_length.")
|
||||
|
||||
if use not in ['conv', 'dot']:
|
||||
raise ValueError("`use` must be 'conv' or 'dot'")
|
||||
|
||||
if deriv > polyorder:
|
||||
coeffs = np.zeros(window_length)
|
||||
return coeffs
|
||||
|
||||
# Form the design matrix A. The columns of A are powers of the integers
|
||||
# from -pos to window_length - pos - 1. The powers (i.e., rows) range
|
||||
# from 0 to polyorder. (That is, A is a vandermonde matrix, but not
|
||||
# necessarily square.)
|
||||
x = np.arange(-pos, window_length - pos, dtype=float)
|
||||
|
||||
if use == "conv":
|
||||
# Reverse so that result can be used in a convolution.
|
||||
x = x[::-1]
|
||||
|
||||
order = np.arange(polyorder + 1).reshape(-1, 1)
|
||||
A = x ** order
|
||||
|
||||
# y determines which order derivative is returned.
|
||||
y = np.zeros(polyorder + 1)
|
||||
# The coefficient assigned to y[deriv] scales the result to take into
|
||||
# account the order of the derivative and the sample spacing.
|
||||
y[deriv] = float_factorial(deriv) / (delta ** deriv)
|
||||
|
||||
# Find the least-squares solution of A*c = y
|
||||
coeffs, _, _, _ = lstsq(A, y)
|
||||
|
||||
return coeffs
|
||||
|
||||
|
||||
def _polyder(p, m):
|
||||
"""Differentiate polynomials represented with coefficients.
|
||||
|
||||
p must be a 1-D or 2-D array. In the 2-D case, each column gives
|
||||
the coefficients of a polynomial; the first row holds the coefficients
|
||||
associated with the highest power. m must be a nonnegative integer.
|
||||
(numpy.polyder doesn't handle the 2-D case.)
|
||||
"""
|
||||
|
||||
if m == 0:
|
||||
result = p
|
||||
else:
|
||||
n = len(p)
|
||||
if n <= m:
|
||||
result = np.zeros_like(p[:1, ...])
|
||||
else:
|
||||
dp = p[:-m].copy()
|
||||
for k in range(m):
|
||||
rng = np.arange(n - k - 1, m - k - 1, -1)
|
||||
dp *= rng.reshape((n - m,) + (1,) * (p.ndim - 1))
|
||||
result = dp
|
||||
return result
|
||||
|
||||
|
||||
def _fit_edge(x, window_start, window_stop, interp_start, interp_stop,
|
||||
axis, polyorder, deriv, delta, y):
|
||||
"""
|
||||
Given an N-d array `x` and the specification of a slice of `x` from
|
||||
`window_start` to `window_stop` along `axis`, create an interpolating
|
||||
polynomial of each 1-D slice, and evaluate that polynomial in the slice
|
||||
from `interp_start` to `interp_stop`. Put the result into the
|
||||
corresponding slice of `y`.
|
||||
"""
|
||||
|
||||
# Get the edge into a (window_length, -1) array.
|
||||
x_edge = axis_slice(x, start=window_start, stop=window_stop, axis=axis)
|
||||
if axis == 0 or axis == -x.ndim:
|
||||
xx_edge = x_edge
|
||||
swapped = False
|
||||
else:
|
||||
xx_edge = x_edge.swapaxes(axis, 0)
|
||||
swapped = True
|
||||
xx_edge = xx_edge.reshape(xx_edge.shape[0], -1)
|
||||
|
||||
# Fit the edges. poly_coeffs has shape (polyorder + 1, -1),
|
||||
# where '-1' is the same as in xx_edge.
|
||||
poly_coeffs = np.polyfit(np.arange(0, window_stop - window_start),
|
||||
xx_edge, polyorder)
|
||||
|
||||
if deriv > 0:
|
||||
poly_coeffs = _polyder(poly_coeffs, deriv)
|
||||
|
||||
# Compute the interpolated values for the edge.
|
||||
i = np.arange(interp_start - window_start, interp_stop - window_start)
|
||||
values = np.polyval(poly_coeffs, i.reshape(-1, 1)) / (delta ** deriv)
|
||||
|
||||
# Now put the values into the appropriate slice of y.
|
||||
# First reshape values to match y.
|
||||
shp = list(y.shape)
|
||||
shp[0], shp[axis] = shp[axis], shp[0]
|
||||
values = values.reshape(interp_stop - interp_start, *shp[1:])
|
||||
if swapped:
|
||||
values = values.swapaxes(0, axis)
|
||||
# Get a view of the data to be replaced by values.
|
||||
y_edge = axis_slice(y, start=interp_start, stop=interp_stop, axis=axis)
|
||||
y_edge[...] = values
|
||||
|
||||
|
||||
def _fit_edges_polyfit(x, window_length, polyorder, deriv, delta, axis, y):
|
||||
"""
|
||||
Use polynomial interpolation of x at the low and high ends of the axis
|
||||
to fill in the halflen values in y.
|
||||
|
||||
This function just calls _fit_edge twice, once for each end of the axis.
|
||||
"""
|
||||
halflen = window_length // 2
|
||||
_fit_edge(x, 0, window_length, 0, halflen, axis,
|
||||
polyorder, deriv, delta, y)
|
||||
n = x.shape[axis]
|
||||
_fit_edge(x, n - window_length, n, n - halflen, n, axis,
|
||||
polyorder, deriv, delta, y)
|
||||
|
||||
|
||||
def savgol_filter(x, window_length, polyorder, deriv=0, delta=1.0,
|
||||
axis=-1, mode='interp', cval=0.0):
|
||||
""" Apply a Savitzky-Golay filter to an array.
|
||||
|
||||
This is a 1-D filter. If `x` has dimension greater than 1, `axis`
|
||||
determines the axis along which the filter is applied.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x : array_like
|
||||
The data to be filtered. If `x` is not a single or double precision
|
||||
floating point array, it will be converted to type ``numpy.float64``
|
||||
before filtering.
|
||||
window_length : int
|
||||
The length of the filter window (i.e., the number of coefficients).
|
||||
If `mode` is 'interp', `window_length` must be less than or equal
|
||||
to the size of `x`.
|
||||
polyorder : int
|
||||
The order of the polynomial used to fit the samples.
|
||||
`polyorder` must be less than `window_length`.
|
||||
deriv : int, optional
|
||||
The order of the derivative to compute. This must be a
|
||||
nonnegative integer. The default is 0, which means to filter
|
||||
the data without differentiating.
|
||||
delta : float, optional
|
||||
The spacing of the samples to which the filter will be applied.
|
||||
This is only used if deriv > 0. Default is 1.0.
|
||||
axis : int, optional
|
||||
The axis of the array `x` along which the filter is to be applied.
|
||||
Default is -1.
|
||||
mode : str, optional
|
||||
Must be 'mirror', 'constant', 'nearest', 'wrap' or 'interp'. This
|
||||
determines the type of extension to use for the padded signal to
|
||||
which the filter is applied. When `mode` is 'constant', the padding
|
||||
value is given by `cval`. See the Notes for more details on 'mirror',
|
||||
'constant', 'wrap', and 'nearest'.
|
||||
When the 'interp' mode is selected (the default), no extension
|
||||
is used. Instead, a degree `polyorder` polynomial is fit to the
|
||||
last `window_length` values of the edges, and this polynomial is
|
||||
used to evaluate the last `window_length // 2` output values.
|
||||
cval : scalar, optional
|
||||
Value to fill past the edges of the input if `mode` is 'constant'.
|
||||
Default is 0.0.
|
||||
|
||||
Returns
|
||||
-------
|
||||
y : ndarray, same shape as `x`
|
||||
The filtered data.
|
||||
|
||||
See Also
|
||||
--------
|
||||
savgol_coeffs
|
||||
|
||||
Notes
|
||||
-----
|
||||
Details on the `mode` options:
|
||||
|
||||
'mirror':
|
||||
Repeats the values at the edges in reverse order. The value
|
||||
closest to the edge is not included.
|
||||
'nearest':
|
||||
The extension contains the nearest input value.
|
||||
'constant':
|
||||
The extension contains the value given by the `cval` argument.
|
||||
'wrap':
|
||||
The extension contains the values from the other end of the array.
|
||||
|
||||
For example, if the input is [1, 2, 3, 4, 5, 6, 7, 8], and
|
||||
`window_length` is 7, the following shows the extended data for
|
||||
the various `mode` options (assuming `cval` is 0)::
|
||||
|
||||
mode | Ext | Input | Ext
|
||||
-----------+---------+------------------------+---------
|
||||
'mirror' | 4 3 2 | 1 2 3 4 5 6 7 8 | 7 6 5
|
||||
'nearest' | 1 1 1 | 1 2 3 4 5 6 7 8 | 8 8 8
|
||||
'constant' | 0 0 0 | 1 2 3 4 5 6 7 8 | 0 0 0
|
||||
'wrap' | 6 7 8 | 1 2 3 4 5 6 7 8 | 1 2 3
|
||||
|
||||
.. versionadded:: 0.14.0
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> from scipy.signal import savgol_filter
|
||||
>>> np.set_printoptions(precision=2) # For compact display.
|
||||
>>> x = np.array([2, 2, 5, 2, 1, 0, 1, 4, 9])
|
||||
|
||||
Filter with a window length of 5 and a degree 2 polynomial. Use
|
||||
the defaults for all other parameters.
|
||||
|
||||
>>> savgol_filter(x, 5, 2)
|
||||
array([1.66, 3.17, 3.54, 2.86, 0.66, 0.17, 1. , 4. , 9. ])
|
||||
|
||||
Note that the last five values in x are samples of a parabola, so
|
||||
when mode='interp' (the default) is used with polyorder=2, the last
|
||||
three values are unchanged. Compare that to, for example,
|
||||
`mode='nearest'`:
|
||||
|
||||
>>> savgol_filter(x, 5, 2, mode='nearest')
|
||||
array([1.74, 3.03, 3.54, 2.86, 0.66, 0.17, 1. , 4.6 , 7.97])
|
||||
|
||||
"""
|
||||
if mode not in ["mirror", "constant", "nearest", "interp", "wrap"]:
|
||||
raise ValueError("mode must be 'mirror', 'constant', 'nearest' "
|
||||
"'wrap' or 'interp'.")
|
||||
|
||||
x = np.asarray(x)
|
||||
# Ensure that x is either single or double precision floating point.
|
||||
if x.dtype != np.float64 and x.dtype != np.float32:
|
||||
x = x.astype(np.float64)
|
||||
|
||||
coeffs = savgol_coeffs(window_length, polyorder, deriv=deriv, delta=delta)
|
||||
|
||||
if mode == "interp":
|
||||
if window_length > x.shape[axis]:
|
||||
raise ValueError("If mode is 'interp', window_length must be less "
|
||||
"than or equal to the size of x.")
|
||||
|
||||
# Do not pad. Instead, for the elements within `window_length // 2`
|
||||
# of the ends of the sequence, use the polynomial that is fitted to
|
||||
# the last `window_length` elements.
|
||||
y = convolve1d(x, coeffs, axis=axis, mode="constant")
|
||||
_fit_edges_polyfit(x, window_length, polyorder, deriv, delta, axis, y)
|
||||
else:
|
||||
# Any mode other than 'interp' is passed on to ndimage.convolve1d.
|
||||
y = convolve1d(x, coeffs, axis=axis, mode=mode, cval=cval)
|
||||
|
||||
return y
|
||||
1676
venv/lib/python3.12/site-packages/scipy/signal/_short_time_fft.py
Normal file
1676
venv/lib/python3.12/site-packages/scipy/signal/_short_time_fft.py
Normal file
File diff suppressed because it is too large
Load Diff
4655
venv/lib/python3.12/site-packages/scipy/signal/_signaltools.py
Normal file
4655
venv/lib/python3.12/site-packages/scipy/signal/_signaltools.py
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
2101
venv/lib/python3.12/site-packages/scipy/signal/_spectral_py.py
Normal file
2101
venv/lib/python3.12/site-packages/scipy/signal/_spectral_py.py
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
216
venv/lib/python3.12/site-packages/scipy/signal/_upfirdn.py
Normal file
216
venv/lib/python3.12/site-packages/scipy/signal/_upfirdn.py
Normal file
@ -0,0 +1,216 @@
|
||||
# Code adapted from "upfirdn" python library with permission:
|
||||
#
|
||||
# Copyright (c) 2009, Motorola, Inc
|
||||
#
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
#
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
#
|
||||
# * Neither the name of Motorola nor the names of its contributors may be
|
||||
# used to endorse or promote products derived from this software without
|
||||
# specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
||||
# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
||||
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ._upfirdn_apply import _output_len, _apply, mode_enum
|
||||
|
||||
__all__ = ['upfirdn', '_output_len']
|
||||
|
||||
_upfirdn_modes = [
|
||||
'constant', 'wrap', 'edge', 'smooth', 'symmetric', 'reflect',
|
||||
'antisymmetric', 'antireflect', 'line',
|
||||
]
|
||||
|
||||
|
||||
def _pad_h(h, up):
|
||||
"""Store coefficients in a transposed, flipped arrangement.
|
||||
|
||||
For example, suppose upRate is 3, and the
|
||||
input number of coefficients is 10, represented as h[0], ..., h[9].
|
||||
|
||||
Then the internal buffer will look like this::
|
||||
|
||||
h[9], h[6], h[3], h[0], // flipped phase 0 coefs
|
||||
0, h[7], h[4], h[1], // flipped phase 1 coefs (zero-padded)
|
||||
0, h[8], h[5], h[2], // flipped phase 2 coefs (zero-padded)
|
||||
|
||||
"""
|
||||
h_padlen = len(h) + (-len(h) % up)
|
||||
h_full = np.zeros(h_padlen, h.dtype)
|
||||
h_full[:len(h)] = h
|
||||
h_full = h_full.reshape(-1, up).T[:, ::-1].ravel()
|
||||
return h_full
|
||||
|
||||
|
||||
def _check_mode(mode):
|
||||
mode = mode.lower()
|
||||
enum = mode_enum(mode)
|
||||
return enum
|
||||
|
||||
|
||||
class _UpFIRDn:
|
||||
"""Helper for resampling."""
|
||||
|
||||
def __init__(self, h, x_dtype, up, down):
|
||||
h = np.asarray(h)
|
||||
if h.ndim != 1 or h.size == 0:
|
||||
raise ValueError('h must be 1-D with non-zero length')
|
||||
self._output_type = np.result_type(h.dtype, x_dtype, np.float32)
|
||||
h = np.asarray(h, self._output_type)
|
||||
self._up = int(up)
|
||||
self._down = int(down)
|
||||
if self._up < 1 or self._down < 1:
|
||||
raise ValueError('Both up and down must be >= 1')
|
||||
# This both transposes, and "flips" each phase for filtering
|
||||
self._h_trans_flip = _pad_h(h, self._up)
|
||||
self._h_trans_flip = np.ascontiguousarray(self._h_trans_flip)
|
||||
self._h_len_orig = len(h)
|
||||
|
||||
def apply_filter(self, x, axis=-1, mode='constant', cval=0):
|
||||
"""Apply the prepared filter to the specified axis of N-D signal x."""
|
||||
output_len = _output_len(self._h_len_orig, x.shape[axis],
|
||||
self._up, self._down)
|
||||
# Explicit use of np.int64 for output_shape dtype avoids OverflowError
|
||||
# when allocating large array on platforms where intp is 32 bits.
|
||||
output_shape = np.asarray(x.shape, dtype=np.int64)
|
||||
output_shape[axis] = output_len
|
||||
out = np.zeros(output_shape, dtype=self._output_type, order='C')
|
||||
axis = axis % x.ndim
|
||||
mode = _check_mode(mode)
|
||||
_apply(np.asarray(x, self._output_type),
|
||||
self._h_trans_flip, out,
|
||||
self._up, self._down, axis, mode, cval)
|
||||
return out
|
||||
|
||||
|
||||
def upfirdn(h, x, up=1, down=1, axis=-1, mode='constant', cval=0):
|
||||
"""Upsample, FIR filter, and downsample.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
h : array_like
|
||||
1-D FIR (finite-impulse response) filter coefficients.
|
||||
x : array_like
|
||||
Input signal array.
|
||||
up : int, optional
|
||||
Upsampling rate. Default is 1.
|
||||
down : int, optional
|
||||
Downsampling rate. Default is 1.
|
||||
axis : int, optional
|
||||
The axis of the input data array along which to apply the
|
||||
linear filter. The filter is applied to each subarray along
|
||||
this axis. Default is -1.
|
||||
mode : str, optional
|
||||
The signal extension mode to use. The set
|
||||
``{"constant", "symmetric", "reflect", "edge", "wrap"}`` correspond to
|
||||
modes provided by `numpy.pad`. ``"smooth"`` implements a smooth
|
||||
extension by extending based on the slope of the last 2 points at each
|
||||
end of the array. ``"antireflect"`` and ``"antisymmetric"`` are
|
||||
anti-symmetric versions of ``"reflect"`` and ``"symmetric"``. The mode
|
||||
`"line"` extends the signal based on a linear trend defined by the
|
||||
first and last points along the ``axis``.
|
||||
|
||||
.. versionadded:: 1.4.0
|
||||
cval : float, optional
|
||||
The constant value to use when ``mode == "constant"``.
|
||||
|
||||
.. versionadded:: 1.4.0
|
||||
|
||||
Returns
|
||||
-------
|
||||
y : ndarray
|
||||
The output signal array. Dimensions will be the same as `x` except
|
||||
for along `axis`, which will change size according to the `h`,
|
||||
`up`, and `down` parameters.
|
||||
|
||||
Notes
|
||||
-----
|
||||
The algorithm is an implementation of the block diagram shown on page 129
|
||||
of the Vaidyanathan text [1]_ (Figure 4.3-8d).
|
||||
|
||||
The direct approach of upsampling by factor of P with zero insertion,
|
||||
FIR filtering of length ``N``, and downsampling by factor of Q is
|
||||
O(N*Q) per output sample. The polyphase implementation used here is
|
||||
O(N/P).
|
||||
|
||||
.. versionadded:: 0.18
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] P. P. Vaidyanathan, Multirate Systems and Filter Banks,
|
||||
Prentice Hall, 1993.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Simple operations:
|
||||
|
||||
>>> import numpy as np
|
||||
>>> from scipy.signal import upfirdn
|
||||
>>> upfirdn([1, 1, 1], [1, 1, 1]) # FIR filter
|
||||
array([ 1., 2., 3., 2., 1.])
|
||||
>>> upfirdn([1], [1, 2, 3], 3) # upsampling with zeros insertion
|
||||
array([ 1., 0., 0., 2., 0., 0., 3.])
|
||||
>>> upfirdn([1, 1, 1], [1, 2, 3], 3) # upsampling with sample-and-hold
|
||||
array([ 1., 1., 1., 2., 2., 2., 3., 3., 3.])
|
||||
>>> upfirdn([.5, 1, .5], [1, 1, 1], 2) # linear interpolation
|
||||
array([ 0.5, 1. , 1. , 1. , 1. , 1. , 0.5])
|
||||
>>> upfirdn([1], np.arange(10), 1, 3) # decimation by 3
|
||||
array([ 0., 3., 6., 9.])
|
||||
>>> upfirdn([.5, 1, .5], np.arange(10), 2, 3) # linear interp, rate 2/3
|
||||
array([ 0. , 1. , 2.5, 4. , 5.5, 7. , 8.5])
|
||||
|
||||
Apply a single filter to multiple signals:
|
||||
|
||||
>>> x = np.reshape(np.arange(8), (4, 2))
|
||||
>>> x
|
||||
array([[0, 1],
|
||||
[2, 3],
|
||||
[4, 5],
|
||||
[6, 7]])
|
||||
|
||||
Apply along the last dimension of ``x``:
|
||||
|
||||
>>> h = [1, 1]
|
||||
>>> upfirdn(h, x, 2)
|
||||
array([[ 0., 0., 1., 1.],
|
||||
[ 2., 2., 3., 3.],
|
||||
[ 4., 4., 5., 5.],
|
||||
[ 6., 6., 7., 7.]])
|
||||
|
||||
Apply along the 0th dimension of ``x``:
|
||||
|
||||
>>> upfirdn(h, x, 2, axis=0)
|
||||
array([[ 0., 1.],
|
||||
[ 0., 1.],
|
||||
[ 2., 3.],
|
||||
[ 2., 3.],
|
||||
[ 4., 5.],
|
||||
[ 4., 5.],
|
||||
[ 6., 7.],
|
||||
[ 6., 7.]])
|
||||
"""
|
||||
x = np.asarray(x)
|
||||
ufd = _UpFIRDn(h, x.dtype, up, down)
|
||||
# This is equivalent to (but faster than) using np.apply_along_axis
|
||||
return ufd.apply_filter(x, axis, mode, cval)
|
||||
Binary file not shown.
672
venv/lib/python3.12/site-packages/scipy/signal/_waveforms.py
Normal file
672
venv/lib/python3.12/site-packages/scipy/signal/_waveforms.py
Normal file
@ -0,0 +1,672 @@
|
||||
# Author: Travis Oliphant
|
||||
# 2003
|
||||
#
|
||||
# Feb. 2010: Updated by Warren Weckesser:
|
||||
# Rewrote much of chirp()
|
||||
# Added sweep_poly()
|
||||
import numpy as np
|
||||
from numpy import asarray, zeros, place, nan, mod, pi, extract, log, sqrt, \
|
||||
exp, cos, sin, polyval, polyint
|
||||
|
||||
|
||||
__all__ = ['sawtooth', 'square', 'gausspulse', 'chirp', 'sweep_poly',
|
||||
'unit_impulse']
|
||||
|
||||
|
||||
def sawtooth(t, width=1):
|
||||
"""
|
||||
Return a periodic sawtooth or triangle waveform.
|
||||
|
||||
The sawtooth waveform has a period ``2*pi``, rises from -1 to 1 on the
|
||||
interval 0 to ``width*2*pi``, then drops from 1 to -1 on the interval
|
||||
``width*2*pi`` to ``2*pi``. `width` must be in the interval [0, 1].
|
||||
|
||||
Note that this is not band-limited. It produces an infinite number
|
||||
of harmonics, which are aliased back and forth across the frequency
|
||||
spectrum.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
t : array_like
|
||||
Time.
|
||||
width : array_like, optional
|
||||
Width of the rising ramp as a proportion of the total cycle.
|
||||
Default is 1, producing a rising ramp, while 0 produces a falling
|
||||
ramp. `width` = 0.5 produces a triangle wave.
|
||||
If an array, causes wave shape to change over time, and must be the
|
||||
same length as t.
|
||||
|
||||
Returns
|
||||
-------
|
||||
y : ndarray
|
||||
Output array containing the sawtooth waveform.
|
||||
|
||||
Examples
|
||||
--------
|
||||
A 5 Hz waveform sampled at 500 Hz for 1 second:
|
||||
|
||||
>>> import numpy as np
|
||||
>>> from scipy import signal
|
||||
>>> import matplotlib.pyplot as plt
|
||||
>>> t = np.linspace(0, 1, 500)
|
||||
>>> plt.plot(t, signal.sawtooth(2 * np.pi * 5 * t))
|
||||
|
||||
"""
|
||||
t, w = asarray(t), asarray(width)
|
||||
w = asarray(w + (t - t))
|
||||
t = asarray(t + (w - w))
|
||||
if t.dtype.char in ['fFdD']:
|
||||
ytype = t.dtype.char
|
||||
else:
|
||||
ytype = 'd'
|
||||
y = zeros(t.shape, ytype)
|
||||
|
||||
# width must be between 0 and 1 inclusive
|
||||
mask1 = (w > 1) | (w < 0)
|
||||
place(y, mask1, nan)
|
||||
|
||||
# take t modulo 2*pi
|
||||
tmod = mod(t, 2 * pi)
|
||||
|
||||
# on the interval 0 to width*2*pi function is
|
||||
# tmod / (pi*w) - 1
|
||||
mask2 = (1 - mask1) & (tmod < w * 2 * pi)
|
||||
tsub = extract(mask2, tmod)
|
||||
wsub = extract(mask2, w)
|
||||
place(y, mask2, tsub / (pi * wsub) - 1)
|
||||
|
||||
# on the interval width*2*pi to 2*pi function is
|
||||
# (pi*(w+1)-tmod) / (pi*(1-w))
|
||||
|
||||
mask3 = (1 - mask1) & (1 - mask2)
|
||||
tsub = extract(mask3, tmod)
|
||||
wsub = extract(mask3, w)
|
||||
place(y, mask3, (pi * (wsub + 1) - tsub) / (pi * (1 - wsub)))
|
||||
return y
|
||||
|
||||
|
||||
def square(t, duty=0.5):
|
||||
"""
|
||||
Return a periodic square-wave waveform.
|
||||
|
||||
The square wave has a period ``2*pi``, has value +1 from 0 to
|
||||
``2*pi*duty`` and -1 from ``2*pi*duty`` to ``2*pi``. `duty` must be in
|
||||
the interval [0,1].
|
||||
|
||||
Note that this is not band-limited. It produces an infinite number
|
||||
of harmonics, which are aliased back and forth across the frequency
|
||||
spectrum.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
t : array_like
|
||||
The input time array.
|
||||
duty : array_like, optional
|
||||
Duty cycle. Default is 0.5 (50% duty cycle).
|
||||
If an array, causes wave shape to change over time, and must be the
|
||||
same length as t.
|
||||
|
||||
Returns
|
||||
-------
|
||||
y : ndarray
|
||||
Output array containing the square waveform.
|
||||
|
||||
Examples
|
||||
--------
|
||||
A 5 Hz waveform sampled at 500 Hz for 1 second:
|
||||
|
||||
>>> import numpy as np
|
||||
>>> from scipy import signal
|
||||
>>> import matplotlib.pyplot as plt
|
||||
>>> t = np.linspace(0, 1, 500, endpoint=False)
|
||||
>>> plt.plot(t, signal.square(2 * np.pi * 5 * t))
|
||||
>>> plt.ylim(-2, 2)
|
||||
|
||||
A pulse-width modulated sine wave:
|
||||
|
||||
>>> plt.figure()
|
||||
>>> sig = np.sin(2 * np.pi * t)
|
||||
>>> pwm = signal.square(2 * np.pi * 30 * t, duty=(sig + 1)/2)
|
||||
>>> plt.subplot(2, 1, 1)
|
||||
>>> plt.plot(t, sig)
|
||||
>>> plt.subplot(2, 1, 2)
|
||||
>>> plt.plot(t, pwm)
|
||||
>>> plt.ylim(-1.5, 1.5)
|
||||
|
||||
"""
|
||||
t, w = asarray(t), asarray(duty)
|
||||
w = asarray(w + (t - t))
|
||||
t = asarray(t + (w - w))
|
||||
if t.dtype.char in ['fFdD']:
|
||||
ytype = t.dtype.char
|
||||
else:
|
||||
ytype = 'd'
|
||||
|
||||
y = zeros(t.shape, ytype)
|
||||
|
||||
# width must be between 0 and 1 inclusive
|
||||
mask1 = (w > 1) | (w < 0)
|
||||
place(y, mask1, nan)
|
||||
|
||||
# on the interval 0 to duty*2*pi function is 1
|
||||
tmod = mod(t, 2 * pi)
|
||||
mask2 = (1 - mask1) & (tmod < w * 2 * pi)
|
||||
place(y, mask2, 1)
|
||||
|
||||
# on the interval duty*2*pi to 2*pi function is
|
||||
# (pi*(w+1)-tmod) / (pi*(1-w))
|
||||
mask3 = (1 - mask1) & (1 - mask2)
|
||||
place(y, mask3, -1)
|
||||
return y
|
||||
|
||||
|
||||
def gausspulse(t, fc=1000, bw=0.5, bwr=-6, tpr=-60, retquad=False,
|
||||
retenv=False):
|
||||
"""
|
||||
Return a Gaussian modulated sinusoid:
|
||||
|
||||
``exp(-a t^2) exp(1j*2*pi*fc*t).``
|
||||
|
||||
If `retquad` is True, then return the real and imaginary parts
|
||||
(in-phase and quadrature).
|
||||
If `retenv` is True, then return the envelope (unmodulated signal).
|
||||
Otherwise, return the real part of the modulated sinusoid.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
t : ndarray or the string 'cutoff'
|
||||
Input array.
|
||||
fc : float, optional
|
||||
Center frequency (e.g. Hz). Default is 1000.
|
||||
bw : float, optional
|
||||
Fractional bandwidth in frequency domain of pulse (e.g. Hz).
|
||||
Default is 0.5.
|
||||
bwr : float, optional
|
||||
Reference level at which fractional bandwidth is calculated (dB).
|
||||
Default is -6.
|
||||
tpr : float, optional
|
||||
If `t` is 'cutoff', then the function returns the cutoff
|
||||
time for when the pulse amplitude falls below `tpr` (in dB).
|
||||
Default is -60.
|
||||
retquad : bool, optional
|
||||
If True, return the quadrature (imaginary) as well as the real part
|
||||
of the signal. Default is False.
|
||||
retenv : bool, optional
|
||||
If True, return the envelope of the signal. Default is False.
|
||||
|
||||
Returns
|
||||
-------
|
||||
yI : ndarray
|
||||
Real part of signal. Always returned.
|
||||
yQ : ndarray
|
||||
Imaginary part of signal. Only returned if `retquad` is True.
|
||||
yenv : ndarray
|
||||
Envelope of signal. Only returned if `retenv` is True.
|
||||
|
||||
See Also
|
||||
--------
|
||||
scipy.signal.morlet
|
||||
|
||||
Examples
|
||||
--------
|
||||
Plot real component, imaginary component, and envelope for a 5 Hz pulse,
|
||||
sampled at 100 Hz for 2 seconds:
|
||||
|
||||
>>> import numpy as np
|
||||
>>> from scipy import signal
|
||||
>>> import matplotlib.pyplot as plt
|
||||
>>> t = np.linspace(-1, 1, 2 * 100, endpoint=False)
|
||||
>>> i, q, e = signal.gausspulse(t, fc=5, retquad=True, retenv=True)
|
||||
>>> plt.plot(t, i, t, q, t, e, '--')
|
||||
|
||||
"""
|
||||
if fc < 0:
|
||||
raise ValueError("Center frequency (fc=%.2f) must be >=0." % fc)
|
||||
if bw <= 0:
|
||||
raise ValueError("Fractional bandwidth (bw=%.2f) must be > 0." % bw)
|
||||
if bwr >= 0:
|
||||
raise ValueError("Reference level for bandwidth (bwr=%.2f) must "
|
||||
"be < 0 dB" % bwr)
|
||||
|
||||
# exp(-a t^2) <-> sqrt(pi/a) exp(-pi^2/a * f^2) = g(f)
|
||||
|
||||
ref = pow(10.0, bwr / 20.0)
|
||||
# fdel = fc*bw/2: g(fdel) = ref --- solve this for a
|
||||
#
|
||||
# pi^2/a * fc^2 * bw^2 /4=-log(ref)
|
||||
a = -(pi * fc * bw) ** 2 / (4.0 * log(ref))
|
||||
|
||||
if isinstance(t, str):
|
||||
if t == 'cutoff': # compute cut_off point
|
||||
# Solve exp(-a tc**2) = tref for tc
|
||||
# tc = sqrt(-log(tref) / a) where tref = 10^(tpr/20)
|
||||
if tpr >= 0:
|
||||
raise ValueError("Reference level for time cutoff must "
|
||||
"be < 0 dB")
|
||||
tref = pow(10.0, tpr / 20.0)
|
||||
return sqrt(-log(tref) / a)
|
||||
else:
|
||||
raise ValueError("If `t` is a string, it must be 'cutoff'")
|
||||
|
||||
yenv = exp(-a * t * t)
|
||||
yI = yenv * cos(2 * pi * fc * t)
|
||||
yQ = yenv * sin(2 * pi * fc * t)
|
||||
if not retquad and not retenv:
|
||||
return yI
|
||||
if not retquad and retenv:
|
||||
return yI, yenv
|
||||
if retquad and not retenv:
|
||||
return yI, yQ
|
||||
if retquad and retenv:
|
||||
return yI, yQ, yenv
|
||||
|
||||
|
||||
def chirp(t, f0, t1, f1, method='linear', phi=0, vertex_zero=True):
|
||||
"""Frequency-swept cosine generator.
|
||||
|
||||
In the following, 'Hz' should be interpreted as 'cycles per unit';
|
||||
there is no requirement here that the unit is one second. The
|
||||
important distinction is that the units of rotation are cycles, not
|
||||
radians. Likewise, `t` could be a measurement of space instead of time.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
t : array_like
|
||||
Times at which to evaluate the waveform.
|
||||
f0 : float
|
||||
Frequency (e.g. Hz) at time t=0.
|
||||
t1 : float
|
||||
Time at which `f1` is specified.
|
||||
f1 : float
|
||||
Frequency (e.g. Hz) of the waveform at time `t1`.
|
||||
method : {'linear', 'quadratic', 'logarithmic', 'hyperbolic'}, optional
|
||||
Kind of frequency sweep. If not given, `linear` is assumed. See
|
||||
Notes below for more details.
|
||||
phi : float, optional
|
||||
Phase offset, in degrees. Default is 0.
|
||||
vertex_zero : bool, optional
|
||||
This parameter is only used when `method` is 'quadratic'.
|
||||
It determines whether the vertex of the parabola that is the graph
|
||||
of the frequency is at t=0 or t=t1.
|
||||
|
||||
Returns
|
||||
-------
|
||||
y : ndarray
|
||||
A numpy array containing the signal evaluated at `t` with the
|
||||
requested time-varying frequency. More precisely, the function
|
||||
returns ``cos(phase + (pi/180)*phi)`` where `phase` is the integral
|
||||
(from 0 to `t`) of ``2*pi*f(t)``. ``f(t)`` is defined below.
|
||||
|
||||
See Also
|
||||
--------
|
||||
sweep_poly
|
||||
|
||||
Notes
|
||||
-----
|
||||
There are four options for the `method`. The following formulas give
|
||||
the instantaneous frequency (in Hz) of the signal generated by
|
||||
`chirp()`. For convenience, the shorter names shown below may also be
|
||||
used.
|
||||
|
||||
linear, lin, li:
|
||||
|
||||
``f(t) = f0 + (f1 - f0) * t / t1``
|
||||
|
||||
quadratic, quad, q:
|
||||
|
||||
The graph of the frequency f(t) is a parabola through (0, f0) and
|
||||
(t1, f1). By default, the vertex of the parabola is at (0, f0).
|
||||
If `vertex_zero` is False, then the vertex is at (t1, f1). The
|
||||
formula is:
|
||||
|
||||
if vertex_zero is True:
|
||||
|
||||
``f(t) = f0 + (f1 - f0) * t**2 / t1**2``
|
||||
|
||||
else:
|
||||
|
||||
``f(t) = f1 - (f1 - f0) * (t1 - t)**2 / t1**2``
|
||||
|
||||
To use a more general quadratic function, or an arbitrary
|
||||
polynomial, use the function `scipy.signal.sweep_poly`.
|
||||
|
||||
logarithmic, log, lo:
|
||||
|
||||
``f(t) = f0 * (f1/f0)**(t/t1)``
|
||||
|
||||
f0 and f1 must be nonzero and have the same sign.
|
||||
|
||||
This signal is also known as a geometric or exponential chirp.
|
||||
|
||||
hyperbolic, hyp:
|
||||
|
||||
``f(t) = f0*f1*t1 / ((f0 - f1)*t + f1*t1)``
|
||||
|
||||
f0 and f1 must be nonzero.
|
||||
|
||||
Examples
|
||||
--------
|
||||
The following will be used in the examples:
|
||||
|
||||
>>> import numpy as np
|
||||
>>> from scipy.signal import chirp, spectrogram
|
||||
>>> import matplotlib.pyplot as plt
|
||||
|
||||
For the first example, we'll plot the waveform for a linear chirp
|
||||
from 6 Hz to 1 Hz over 10 seconds:
|
||||
|
||||
>>> t = np.linspace(0, 10, 1500)
|
||||
>>> w = chirp(t, f0=6, f1=1, t1=10, method='linear')
|
||||
>>> plt.plot(t, w)
|
||||
>>> plt.title("Linear Chirp, f(0)=6, f(10)=1")
|
||||
>>> plt.xlabel('t (sec)')
|
||||
>>> plt.show()
|
||||
|
||||
For the remaining examples, we'll use higher frequency ranges,
|
||||
and demonstrate the result using `scipy.signal.spectrogram`.
|
||||
We'll use a 4 second interval sampled at 7200 Hz.
|
||||
|
||||
>>> fs = 7200
|
||||
>>> T = 4
|
||||
>>> t = np.arange(0, int(T*fs)) / fs
|
||||
|
||||
We'll use this function to plot the spectrogram in each example.
|
||||
|
||||
>>> def plot_spectrogram(title, w, fs):
|
||||
... ff, tt, Sxx = spectrogram(w, fs=fs, nperseg=256, nfft=576)
|
||||
... fig, ax = plt.subplots()
|
||||
... ax.pcolormesh(tt, ff[:145], Sxx[:145], cmap='gray_r',
|
||||
... shading='gouraud')
|
||||
... ax.set_title(title)
|
||||
... ax.set_xlabel('t (sec)')
|
||||
... ax.set_ylabel('Frequency (Hz)')
|
||||
... ax.grid(True)
|
||||
...
|
||||
|
||||
Quadratic chirp from 1500 Hz to 250 Hz
|
||||
(vertex of the parabolic curve of the frequency is at t=0):
|
||||
|
||||
>>> w = chirp(t, f0=1500, f1=250, t1=T, method='quadratic')
|
||||
>>> plot_spectrogram(f'Quadratic Chirp, f(0)=1500, f({T})=250', w, fs)
|
||||
>>> plt.show()
|
||||
|
||||
Quadratic chirp from 1500 Hz to 250 Hz
|
||||
(vertex of the parabolic curve of the frequency is at t=T):
|
||||
|
||||
>>> w = chirp(t, f0=1500, f1=250, t1=T, method='quadratic',
|
||||
... vertex_zero=False)
|
||||
>>> plot_spectrogram(f'Quadratic Chirp, f(0)=1500, f({T})=250\\n' +
|
||||
... '(vertex_zero=False)', w, fs)
|
||||
>>> plt.show()
|
||||
|
||||
Logarithmic chirp from 1500 Hz to 250 Hz:
|
||||
|
||||
>>> w = chirp(t, f0=1500, f1=250, t1=T, method='logarithmic')
|
||||
>>> plot_spectrogram(f'Logarithmic Chirp, f(0)=1500, f({T})=250', w, fs)
|
||||
>>> plt.show()
|
||||
|
||||
Hyperbolic chirp from 1500 Hz to 250 Hz:
|
||||
|
||||
>>> w = chirp(t, f0=1500, f1=250, t1=T, method='hyperbolic')
|
||||
>>> plot_spectrogram(f'Hyperbolic Chirp, f(0)=1500, f({T})=250', w, fs)
|
||||
>>> plt.show()
|
||||
|
||||
"""
|
||||
# 'phase' is computed in _chirp_phase, to make testing easier.
|
||||
phase = _chirp_phase(t, f0, t1, f1, method, vertex_zero)
|
||||
# Convert phi to radians.
|
||||
phi *= pi / 180
|
||||
return cos(phase + phi)
|
||||
|
||||
|
||||
def _chirp_phase(t, f0, t1, f1, method='linear', vertex_zero=True):
|
||||
"""
|
||||
Calculate the phase used by `chirp` to generate its output.
|
||||
|
||||
See `chirp` for a description of the arguments.
|
||||
|
||||
"""
|
||||
t = asarray(t)
|
||||
f0 = float(f0)
|
||||
t1 = float(t1)
|
||||
f1 = float(f1)
|
||||
if method in ['linear', 'lin', 'li']:
|
||||
beta = (f1 - f0) / t1
|
||||
phase = 2 * pi * (f0 * t + 0.5 * beta * t * t)
|
||||
|
||||
elif method in ['quadratic', 'quad', 'q']:
|
||||
beta = (f1 - f0) / (t1 ** 2)
|
||||
if vertex_zero:
|
||||
phase = 2 * pi * (f0 * t + beta * t ** 3 / 3)
|
||||
else:
|
||||
phase = 2 * pi * (f1 * t + beta * ((t1 - t) ** 3 - t1 ** 3) / 3)
|
||||
|
||||
elif method in ['logarithmic', 'log', 'lo']:
|
||||
if f0 * f1 <= 0.0:
|
||||
raise ValueError("For a logarithmic chirp, f0 and f1 must be "
|
||||
"nonzero and have the same sign.")
|
||||
if f0 == f1:
|
||||
phase = 2 * pi * f0 * t
|
||||
else:
|
||||
beta = t1 / log(f1 / f0)
|
||||
phase = 2 * pi * beta * f0 * (pow(f1 / f0, t / t1) - 1.0)
|
||||
|
||||
elif method in ['hyperbolic', 'hyp']:
|
||||
if f0 == 0 or f1 == 0:
|
||||
raise ValueError("For a hyperbolic chirp, f0 and f1 must be "
|
||||
"nonzero.")
|
||||
if f0 == f1:
|
||||
# Degenerate case: constant frequency.
|
||||
phase = 2 * pi * f0 * t
|
||||
else:
|
||||
# Singular point: the instantaneous frequency blows up
|
||||
# when t == sing.
|
||||
sing = -f1 * t1 / (f0 - f1)
|
||||
phase = 2 * pi * (-sing * f0) * log(np.abs(1 - t/sing))
|
||||
|
||||
else:
|
||||
raise ValueError("method must be 'linear', 'quadratic', 'logarithmic',"
|
||||
" or 'hyperbolic', but a value of %r was given."
|
||||
% method)
|
||||
|
||||
return phase
|
||||
|
||||
|
||||
def sweep_poly(t, poly, phi=0):
|
||||
"""
|
||||
Frequency-swept cosine generator, with a time-dependent frequency.
|
||||
|
||||
This function generates a sinusoidal function whose instantaneous
|
||||
frequency varies with time. The frequency at time `t` is given by
|
||||
the polynomial `poly`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
t : ndarray
|
||||
Times at which to evaluate the waveform.
|
||||
poly : 1-D array_like or instance of numpy.poly1d
|
||||
The desired frequency expressed as a polynomial. If `poly` is
|
||||
a list or ndarray of length n, then the elements of `poly` are
|
||||
the coefficients of the polynomial, and the instantaneous
|
||||
frequency is
|
||||
|
||||
``f(t) = poly[0]*t**(n-1) + poly[1]*t**(n-2) + ... + poly[n-1]``
|
||||
|
||||
If `poly` is an instance of numpy.poly1d, then the
|
||||
instantaneous frequency is
|
||||
|
||||
``f(t) = poly(t)``
|
||||
|
||||
phi : float, optional
|
||||
Phase offset, in degrees, Default: 0.
|
||||
|
||||
Returns
|
||||
-------
|
||||
sweep_poly : ndarray
|
||||
A numpy array containing the signal evaluated at `t` with the
|
||||
requested time-varying frequency. More precisely, the function
|
||||
returns ``cos(phase + (pi/180)*phi)``, where `phase` is the integral
|
||||
(from 0 to t) of ``2 * pi * f(t)``; ``f(t)`` is defined above.
|
||||
|
||||
See Also
|
||||
--------
|
||||
chirp
|
||||
|
||||
Notes
|
||||
-----
|
||||
.. versionadded:: 0.8.0
|
||||
|
||||
If `poly` is a list or ndarray of length `n`, then the elements of
|
||||
`poly` are the coefficients of the polynomial, and the instantaneous
|
||||
frequency is:
|
||||
|
||||
``f(t) = poly[0]*t**(n-1) + poly[1]*t**(n-2) + ... + poly[n-1]``
|
||||
|
||||
If `poly` is an instance of `numpy.poly1d`, then the instantaneous
|
||||
frequency is:
|
||||
|
||||
``f(t) = poly(t)``
|
||||
|
||||
Finally, the output `s` is:
|
||||
|
||||
``cos(phase + (pi/180)*phi)``
|
||||
|
||||
where `phase` is the integral from 0 to `t` of ``2 * pi * f(t)``,
|
||||
``f(t)`` as defined above.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Compute the waveform with instantaneous frequency::
|
||||
|
||||
f(t) = 0.025*t**3 - 0.36*t**2 + 1.25*t + 2
|
||||
|
||||
over the interval 0 <= t <= 10.
|
||||
|
||||
>>> import numpy as np
|
||||
>>> from scipy.signal import sweep_poly
|
||||
>>> p = np.poly1d([0.025, -0.36, 1.25, 2.0])
|
||||
>>> t = np.linspace(0, 10, 5001)
|
||||
>>> w = sweep_poly(t, p)
|
||||
|
||||
Plot it:
|
||||
|
||||
>>> import matplotlib.pyplot as plt
|
||||
>>> plt.subplot(2, 1, 1)
|
||||
>>> plt.plot(t, w)
|
||||
>>> plt.title("Sweep Poly\\nwith frequency " +
|
||||
... "$f(t) = 0.025t^3 - 0.36t^2 + 1.25t + 2$")
|
||||
>>> plt.subplot(2, 1, 2)
|
||||
>>> plt.plot(t, p(t), 'r', label='f(t)')
|
||||
>>> plt.legend()
|
||||
>>> plt.xlabel('t')
|
||||
>>> plt.tight_layout()
|
||||
>>> plt.show()
|
||||
|
||||
"""
|
||||
# 'phase' is computed in _sweep_poly_phase, to make testing easier.
|
||||
phase = _sweep_poly_phase(t, poly)
|
||||
# Convert to radians.
|
||||
phi *= pi / 180
|
||||
return cos(phase + phi)
|
||||
|
||||
|
||||
def _sweep_poly_phase(t, poly):
|
||||
"""
|
||||
Calculate the phase used by sweep_poly to generate its output.
|
||||
|
||||
See `sweep_poly` for a description of the arguments.
|
||||
|
||||
"""
|
||||
# polyint handles lists, ndarrays and instances of poly1d automatically.
|
||||
intpoly = polyint(poly)
|
||||
phase = 2 * pi * polyval(intpoly, t)
|
||||
return phase
|
||||
|
||||
|
||||
def unit_impulse(shape, idx=None, dtype=float):
|
||||
"""
|
||||
Unit impulse signal (discrete delta function) or unit basis vector.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
shape : int or tuple of int
|
||||
Number of samples in the output (1-D), or a tuple that represents the
|
||||
shape of the output (N-D).
|
||||
idx : None or int or tuple of int or 'mid', optional
|
||||
Index at which the value is 1. If None, defaults to the 0th element.
|
||||
If ``idx='mid'``, the impulse will be centered at ``shape // 2`` in
|
||||
all dimensions. If an int, the impulse will be at `idx` in all
|
||||
dimensions.
|
||||
dtype : data-type, optional
|
||||
The desired data-type for the array, e.g., ``numpy.int8``. Default is
|
||||
``numpy.float64``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
y : ndarray
|
||||
Output array containing an impulse signal.
|
||||
|
||||
Notes
|
||||
-----
|
||||
The 1D case is also known as the Kronecker delta.
|
||||
|
||||
.. versionadded:: 0.19.0
|
||||
|
||||
Examples
|
||||
--------
|
||||
An impulse at the 0th element (:math:`\\delta[n]`):
|
||||
|
||||
>>> from scipy import signal
|
||||
>>> signal.unit_impulse(8)
|
||||
array([ 1., 0., 0., 0., 0., 0., 0., 0.])
|
||||
|
||||
Impulse offset by 2 samples (:math:`\\delta[n-2]`):
|
||||
|
||||
>>> signal.unit_impulse(7, 2)
|
||||
array([ 0., 0., 1., 0., 0., 0., 0.])
|
||||
|
||||
2-dimensional impulse, centered:
|
||||
|
||||
>>> signal.unit_impulse((3, 3), 'mid')
|
||||
array([[ 0., 0., 0.],
|
||||
[ 0., 1., 0.],
|
||||
[ 0., 0., 0.]])
|
||||
|
||||
Impulse at (2, 2), using broadcasting:
|
||||
|
||||
>>> signal.unit_impulse((4, 4), 2)
|
||||
array([[ 0., 0., 0., 0.],
|
||||
[ 0., 0., 0., 0.],
|
||||
[ 0., 0., 1., 0.],
|
||||
[ 0., 0., 0., 0.]])
|
||||
|
||||
Plot the impulse response of a 4th-order Butterworth lowpass filter:
|
||||
|
||||
>>> imp = signal.unit_impulse(100, 'mid')
|
||||
>>> b, a = signal.butter(4, 0.2)
|
||||
>>> response = signal.lfilter(b, a, imp)
|
||||
|
||||
>>> import numpy as np
|
||||
>>> import matplotlib.pyplot as plt
|
||||
>>> plt.plot(np.arange(-50, 50), imp)
|
||||
>>> plt.plot(np.arange(-50, 50), response)
|
||||
>>> plt.margins(0.1, 0.1)
|
||||
>>> plt.xlabel('Time [samples]')
|
||||
>>> plt.ylabel('Amplitude')
|
||||
>>> plt.grid(True)
|
||||
>>> plt.show()
|
||||
|
||||
"""
|
||||
out = zeros(shape, dtype)
|
||||
|
||||
shape = np.atleast_1d(shape)
|
||||
|
||||
if idx is None:
|
||||
idx = (0,) * len(shape)
|
||||
elif idx == 'mid':
|
||||
idx = tuple(shape // 2)
|
||||
elif not hasattr(idx, "__iter__"):
|
||||
idx = (idx,) * len(shape)
|
||||
|
||||
out[idx] = 1
|
||||
return out
|
||||
556
venv/lib/python3.12/site-packages/scipy/signal/_wavelets.py
Normal file
556
venv/lib/python3.12/site-packages/scipy/signal/_wavelets.py
Normal file
@ -0,0 +1,556 @@
|
||||
import warnings
|
||||
|
||||
import numpy as np
|
||||
from scipy.linalg import eig
|
||||
from scipy.special import comb
|
||||
from scipy.signal import convolve
|
||||
|
||||
__all__ = ['daub', 'qmf', 'cascade', 'morlet', 'ricker', 'morlet2', 'cwt']
|
||||
|
||||
|
||||
_msg="""scipy.signal.%s is deprecated in SciPy 1.12 and will be removed
|
||||
in SciPy 1.15. We recommend using PyWavelets instead.
|
||||
"""
|
||||
|
||||
|
||||
def daub(p):
|
||||
"""
|
||||
The coefficients for the FIR low-pass filter producing Daubechies wavelets.
|
||||
|
||||
.. deprecated:: 1.12.0
|
||||
|
||||
scipy.signal.daub is deprecated in SciPy 1.12 and will be removed
|
||||
in SciPy 1.15. We recommend using PyWavelets instead.
|
||||
|
||||
p>=1 gives the order of the zero at f=1/2.
|
||||
There are 2p filter coefficients.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
p : int
|
||||
Order of the zero at f=1/2, can have values from 1 to 34.
|
||||
|
||||
Returns
|
||||
-------
|
||||
daub : ndarray
|
||||
Return
|
||||
|
||||
"""
|
||||
warnings.warn(_msg % 'daub', DeprecationWarning, stacklevel=2)
|
||||
|
||||
sqrt = np.sqrt
|
||||
if p < 1:
|
||||
raise ValueError("p must be at least 1.")
|
||||
if p == 1:
|
||||
c = 1 / sqrt(2)
|
||||
return np.array([c, c])
|
||||
elif p == 2:
|
||||
f = sqrt(2) / 8
|
||||
c = sqrt(3)
|
||||
return f * np.array([1 + c, 3 + c, 3 - c, 1 - c])
|
||||
elif p == 3:
|
||||
tmp = 12 * sqrt(10)
|
||||
z1 = 1.5 + sqrt(15 + tmp) / 6 - 1j * (sqrt(15) + sqrt(tmp - 15)) / 6
|
||||
z1c = np.conj(z1)
|
||||
f = sqrt(2) / 8
|
||||
d0 = np.real((1 - z1) * (1 - z1c))
|
||||
a0 = np.real(z1 * z1c)
|
||||
a1 = 2 * np.real(z1)
|
||||
return f / d0 * np.array([a0, 3 * a0 - a1, 3 * a0 - 3 * a1 + 1,
|
||||
a0 - 3 * a1 + 3, 3 - a1, 1])
|
||||
elif p < 35:
|
||||
# construct polynomial and factor it
|
||||
if p < 35:
|
||||
P = [comb(p - 1 + k, k, exact=1) for k in range(p)][::-1]
|
||||
yj = np.roots(P)
|
||||
else: # try different polynomial --- needs work
|
||||
P = [comb(p - 1 + k, k, exact=1) / 4.0**k
|
||||
for k in range(p)][::-1]
|
||||
yj = np.roots(P) / 4
|
||||
# for each root, compute two z roots, select the one with |z|>1
|
||||
# Build up final polynomial
|
||||
c = np.poly1d([1, 1])**p
|
||||
q = np.poly1d([1])
|
||||
for k in range(p - 1):
|
||||
yval = yj[k]
|
||||
part = 2 * sqrt(yval * (yval - 1))
|
||||
const = 1 - 2 * yval
|
||||
z1 = const + part
|
||||
if (abs(z1)) < 1:
|
||||
z1 = const - part
|
||||
q = q * [1, -z1]
|
||||
|
||||
q = c * np.real(q)
|
||||
# Normalize result
|
||||
q = q / np.sum(q) * sqrt(2)
|
||||
return q.c[::-1]
|
||||
else:
|
||||
raise ValueError("Polynomial factorization does not work "
|
||||
"well for p too large.")
|
||||
|
||||
|
||||
def qmf(hk):
|
||||
"""
|
||||
Return high-pass qmf filter from low-pass
|
||||
|
||||
.. deprecated:: 1.12.0
|
||||
|
||||
scipy.signal.qmf is deprecated in SciPy 1.12 and will be removed
|
||||
in SciPy 1.15. We recommend using PyWavelets instead.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
hk : array_like
|
||||
Coefficients of high-pass filter.
|
||||
|
||||
Returns
|
||||
-------
|
||||
array_like
|
||||
High-pass filter coefficients.
|
||||
|
||||
"""
|
||||
warnings.warn(_msg % 'qmf', DeprecationWarning, stacklevel=2)
|
||||
|
||||
N = len(hk) - 1
|
||||
asgn = [{0: 1, 1: -1}[k % 2] for k in range(N + 1)]
|
||||
return hk[::-1] * np.array(asgn)
|
||||
|
||||
|
||||
def cascade(hk, J=7):
|
||||
"""
|
||||
Return (x, phi, psi) at dyadic points ``K/2**J`` from filter coefficients.
|
||||
|
||||
.. deprecated:: 1.12.0
|
||||
|
||||
scipy.signal.cascade is deprecated in SciPy 1.12 and will be removed
|
||||
in SciPy 1.15. We recommend using PyWavelets instead.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
hk : array_like
|
||||
Coefficients of low-pass filter.
|
||||
J : int, optional
|
||||
Values will be computed at grid points ``K/2**J``. Default is 7.
|
||||
|
||||
Returns
|
||||
-------
|
||||
x : ndarray
|
||||
The dyadic points ``K/2**J`` for ``K=0...N * (2**J)-1`` where
|
||||
``len(hk) = len(gk) = N+1``.
|
||||
phi : ndarray
|
||||
The scaling function ``phi(x)`` at `x`:
|
||||
``phi(x) = sum(hk * phi(2x-k))``, where k is from 0 to N.
|
||||
psi : ndarray, optional
|
||||
The wavelet function ``psi(x)`` at `x`:
|
||||
``phi(x) = sum(gk * phi(2x-k))``, where k is from 0 to N.
|
||||
`psi` is only returned if `gk` is not None.
|
||||
|
||||
Notes
|
||||
-----
|
||||
The algorithm uses the vector cascade algorithm described by Strang and
|
||||
Nguyen in "Wavelets and Filter Banks". It builds a dictionary of values
|
||||
and slices for quick reuse. Then inserts vectors into final vector at the
|
||||
end.
|
||||
|
||||
"""
|
||||
warnings.warn(_msg % 'cascade', DeprecationWarning, stacklevel=2)
|
||||
|
||||
N = len(hk) - 1
|
||||
|
||||
if (J > 30 - np.log2(N + 1)):
|
||||
raise ValueError("Too many levels.")
|
||||
if (J < 1):
|
||||
raise ValueError("Too few levels.")
|
||||
|
||||
# construct matrices needed
|
||||
nn, kk = np.ogrid[:N, :N]
|
||||
s2 = np.sqrt(2)
|
||||
# append a zero so that take works
|
||||
thk = np.r_[hk, 0]
|
||||
gk = qmf(hk)
|
||||
tgk = np.r_[gk, 0]
|
||||
|
||||
indx1 = np.clip(2 * nn - kk, -1, N + 1)
|
||||
indx2 = np.clip(2 * nn - kk + 1, -1, N + 1)
|
||||
m = np.empty((2, 2, N, N), 'd')
|
||||
m[0, 0] = np.take(thk, indx1, 0)
|
||||
m[0, 1] = np.take(thk, indx2, 0)
|
||||
m[1, 0] = np.take(tgk, indx1, 0)
|
||||
m[1, 1] = np.take(tgk, indx2, 0)
|
||||
m *= s2
|
||||
|
||||
# construct the grid of points
|
||||
x = np.arange(0, N * (1 << J), dtype=float) / (1 << J)
|
||||
phi = 0 * x
|
||||
|
||||
psi = 0 * x
|
||||
|
||||
# find phi0, and phi1
|
||||
lam, v = eig(m[0, 0])
|
||||
ind = np.argmin(np.absolute(lam - 1))
|
||||
# a dictionary with a binary representation of the
|
||||
# evaluation points x < 1 -- i.e. position is 0.xxxx
|
||||
v = np.real(v[:, ind])
|
||||
# need scaling function to integrate to 1 so find
|
||||
# eigenvector normalized to sum(v,axis=0)=1
|
||||
sm = np.sum(v)
|
||||
if sm < 0: # need scaling function to integrate to 1
|
||||
v = -v
|
||||
sm = -sm
|
||||
bitdic = {'0': v / sm}
|
||||
bitdic['1'] = np.dot(m[0, 1], bitdic['0'])
|
||||
step = 1 << J
|
||||
phi[::step] = bitdic['0']
|
||||
phi[(1 << (J - 1))::step] = bitdic['1']
|
||||
psi[::step] = np.dot(m[1, 0], bitdic['0'])
|
||||
psi[(1 << (J - 1))::step] = np.dot(m[1, 1], bitdic['0'])
|
||||
# descend down the levels inserting more and more values
|
||||
# into bitdic -- store the values in the correct location once we
|
||||
# have computed them -- stored in the dictionary
|
||||
# for quicker use later.
|
||||
prevkeys = ['1']
|
||||
for level in range(2, J + 1):
|
||||
newkeys = ['%d%s' % (xx, yy) for xx in [0, 1] for yy in prevkeys]
|
||||
fac = 1 << (J - level)
|
||||
for key in newkeys:
|
||||
# convert key to number
|
||||
num = 0
|
||||
for pos in range(level):
|
||||
if key[pos] == '1':
|
||||
num += (1 << (level - 1 - pos))
|
||||
pastphi = bitdic[key[1:]]
|
||||
ii = int(key[0])
|
||||
temp = np.dot(m[0, ii], pastphi)
|
||||
bitdic[key] = temp
|
||||
phi[num * fac::step] = temp
|
||||
psi[num * fac::step] = np.dot(m[1, ii], pastphi)
|
||||
prevkeys = newkeys
|
||||
|
||||
return x, phi, psi
|
||||
|
||||
|
||||
def morlet(M, w=5.0, s=1.0, complete=True):
|
||||
"""
|
||||
Complex Morlet wavelet.
|
||||
|
||||
.. deprecated:: 1.12.0
|
||||
|
||||
scipy.signal.morlet is deprecated in SciPy 1.12 and will be removed
|
||||
in SciPy 1.15. We recommend using PyWavelets instead.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
M : int
|
||||
Length of the wavelet.
|
||||
w : float, optional
|
||||
Omega0. Default is 5
|
||||
s : float, optional
|
||||
Scaling factor, windowed from ``-s*2*pi`` to ``+s*2*pi``. Default is 1.
|
||||
complete : bool, optional
|
||||
Whether to use the complete or the standard version.
|
||||
|
||||
Returns
|
||||
-------
|
||||
morlet : (M,) ndarray
|
||||
|
||||
See Also
|
||||
--------
|
||||
morlet2 : Implementation of Morlet wavelet, compatible with `cwt`.
|
||||
scipy.signal.gausspulse
|
||||
|
||||
Notes
|
||||
-----
|
||||
The standard version::
|
||||
|
||||
pi**-0.25 * exp(1j*w*x) * exp(-0.5*(x**2))
|
||||
|
||||
This commonly used wavelet is often referred to simply as the
|
||||
Morlet wavelet. Note that this simplified version can cause
|
||||
admissibility problems at low values of `w`.
|
||||
|
||||
The complete version::
|
||||
|
||||
pi**-0.25 * (exp(1j*w*x) - exp(-0.5*(w**2))) * exp(-0.5*(x**2))
|
||||
|
||||
This version has a correction
|
||||
term to improve admissibility. For `w` greater than 5, the
|
||||
correction term is negligible.
|
||||
|
||||
Note that the energy of the return wavelet is not normalised
|
||||
according to `s`.
|
||||
|
||||
The fundamental frequency of this wavelet in Hz is given
|
||||
by ``f = 2*s*w*r / M`` where `r` is the sampling rate.
|
||||
|
||||
Note: This function was created before `cwt` and is not compatible
|
||||
with it.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from scipy import signal
|
||||
>>> import matplotlib.pyplot as plt
|
||||
|
||||
>>> M = 100
|
||||
>>> s = 4.0
|
||||
>>> w = 2.0
|
||||
>>> wavelet = signal.morlet(M, s, w)
|
||||
>>> plt.plot(wavelet.real, label="real")
|
||||
>>> plt.plot(wavelet.imag, label="imag")
|
||||
>>> plt.legend()
|
||||
>>> plt.show()
|
||||
|
||||
"""
|
||||
warnings.warn(_msg % 'morlet', DeprecationWarning, stacklevel=2)
|
||||
|
||||
x = np.linspace(-s * 2 * np.pi, s * 2 * np.pi, M)
|
||||
output = np.exp(1j * w * x)
|
||||
|
||||
if complete:
|
||||
output -= np.exp(-0.5 * (w**2))
|
||||
|
||||
output *= np.exp(-0.5 * (x**2)) * np.pi**(-0.25)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def ricker(points, a):
|
||||
"""
|
||||
Return a Ricker wavelet, also known as the "Mexican hat wavelet".
|
||||
|
||||
.. deprecated:: 1.12.0
|
||||
|
||||
scipy.signal.ricker is deprecated in SciPy 1.12 and will be removed
|
||||
in SciPy 1.15. We recommend using PyWavelets instead.
|
||||
|
||||
It models the function:
|
||||
|
||||
``A * (1 - (x/a)**2) * exp(-0.5*(x/a)**2)``,
|
||||
|
||||
where ``A = 2/(sqrt(3*a)*(pi**0.25))``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
points : int
|
||||
Number of points in `vector`.
|
||||
Will be centered around 0.
|
||||
a : scalar
|
||||
Width parameter of the wavelet.
|
||||
|
||||
Returns
|
||||
-------
|
||||
vector : (N,) ndarray
|
||||
Array of length `points` in shape of ricker curve.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from scipy import signal
|
||||
>>> import matplotlib.pyplot as plt
|
||||
|
||||
>>> points = 100
|
||||
>>> a = 4.0
|
||||
>>> vec2 = signal.ricker(points, a)
|
||||
>>> print(len(vec2))
|
||||
100
|
||||
>>> plt.plot(vec2)
|
||||
>>> plt.show()
|
||||
|
||||
"""
|
||||
warnings.warn(_msg % 'ricker', DeprecationWarning, stacklevel=2)
|
||||
return _ricker(points, a)
|
||||
|
||||
|
||||
def _ricker(points, a):
|
||||
A = 2 / (np.sqrt(3 * a) * (np.pi**0.25))
|
||||
wsq = a**2
|
||||
vec = np.arange(0, points) - (points - 1.0) / 2
|
||||
xsq = vec**2
|
||||
mod = (1 - xsq / wsq)
|
||||
gauss = np.exp(-xsq / (2 * wsq))
|
||||
total = A * mod * gauss
|
||||
return total
|
||||
|
||||
|
||||
def morlet2(M, s, w=5):
|
||||
"""
|
||||
Complex Morlet wavelet, designed to work with `cwt`.
|
||||
|
||||
.. deprecated:: 1.12.0
|
||||
|
||||
scipy.signal.morlet2 is deprecated in SciPy 1.12 and will be removed
|
||||
in SciPy 1.15. We recommend using PyWavelets instead.
|
||||
|
||||
Returns the complete version of morlet wavelet, normalised
|
||||
according to `s`::
|
||||
|
||||
exp(1j*w*x/s) * exp(-0.5*(x/s)**2) * pi**(-0.25) * sqrt(1/s)
|
||||
|
||||
Parameters
|
||||
----------
|
||||
M : int
|
||||
Length of the wavelet.
|
||||
s : float
|
||||
Width parameter of the wavelet.
|
||||
w : float, optional
|
||||
Omega0. Default is 5
|
||||
|
||||
Returns
|
||||
-------
|
||||
morlet : (M,) ndarray
|
||||
|
||||
See Also
|
||||
--------
|
||||
morlet : Implementation of Morlet wavelet, incompatible with `cwt`
|
||||
|
||||
Notes
|
||||
-----
|
||||
|
||||
.. versionadded:: 1.4.0
|
||||
|
||||
This function was designed to work with `cwt`. Because `morlet2`
|
||||
returns an array of complex numbers, the `dtype` argument of `cwt`
|
||||
should be set to `complex128` for best results.
|
||||
|
||||
Note the difference in implementation with `morlet`.
|
||||
The fundamental frequency of this wavelet in Hz is given by::
|
||||
|
||||
f = w*fs / (2*s*np.pi)
|
||||
|
||||
where ``fs`` is the sampling rate and `s` is the wavelet width parameter.
|
||||
Similarly we can get the wavelet width parameter at ``f``::
|
||||
|
||||
s = w*fs / (2*f*np.pi)
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> from scipy import signal
|
||||
>>> import matplotlib.pyplot as plt
|
||||
|
||||
>>> M = 100
|
||||
>>> s = 4.0
|
||||
>>> w = 2.0
|
||||
>>> wavelet = signal.morlet2(M, s, w)
|
||||
>>> plt.plot(abs(wavelet))
|
||||
>>> plt.show()
|
||||
|
||||
This example shows basic use of `morlet2` with `cwt` in time-frequency
|
||||
analysis:
|
||||
|
||||
>>> t, dt = np.linspace(0, 1, 200, retstep=True)
|
||||
>>> fs = 1/dt
|
||||
>>> w = 6.
|
||||
>>> sig = np.cos(2*np.pi*(50 + 10*t)*t) + np.sin(40*np.pi*t)
|
||||
>>> freq = np.linspace(1, fs/2, 100)
|
||||
>>> widths = w*fs / (2*freq*np.pi)
|
||||
>>> cwtm = signal.cwt(sig, signal.morlet2, widths, w=w)
|
||||
>>> plt.pcolormesh(t, freq, np.abs(cwtm), cmap='viridis', shading='gouraud')
|
||||
>>> plt.show()
|
||||
|
||||
"""
|
||||
warnings.warn(_msg % 'morlet2', DeprecationWarning, stacklevel=2)
|
||||
|
||||
x = np.arange(0, M) - (M - 1.0) / 2
|
||||
x = x / s
|
||||
wavelet = np.exp(1j * w * x) * np.exp(-0.5 * x**2) * np.pi**(-0.25)
|
||||
output = np.sqrt(1/s) * wavelet
|
||||
return output
|
||||
|
||||
|
||||
def cwt(data, wavelet, widths, dtype=None, **kwargs):
|
||||
"""
|
||||
Continuous wavelet transform.
|
||||
|
||||
.. deprecated:: 1.12.0
|
||||
|
||||
scipy.signal.cwt is deprecated in SciPy 1.12 and will be removed
|
||||
in SciPy 1.15. We recommend using PyWavelets instead.
|
||||
|
||||
Performs a continuous wavelet transform on `data`,
|
||||
using the `wavelet` function. A CWT performs a convolution
|
||||
with `data` using the `wavelet` function, which is characterized
|
||||
by a width parameter and length parameter. The `wavelet` function
|
||||
is allowed to be complex.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : (N,) ndarray
|
||||
data on which to perform the transform.
|
||||
wavelet : function
|
||||
Wavelet function, which should take 2 arguments.
|
||||
The first argument is the number of points that the returned vector
|
||||
will have (len(wavelet(length,width)) == length).
|
||||
The second is a width parameter, defining the size of the wavelet
|
||||
(e.g. standard deviation of a gaussian). See `ricker`, which
|
||||
satisfies these requirements.
|
||||
widths : (M,) sequence
|
||||
Widths to use for transform.
|
||||
dtype : data-type, optional
|
||||
The desired data type of output. Defaults to ``float64`` if the
|
||||
output of `wavelet` is real and ``complex128`` if it is complex.
|
||||
|
||||
.. versionadded:: 1.4.0
|
||||
|
||||
kwargs
|
||||
Keyword arguments passed to wavelet function.
|
||||
|
||||
.. versionadded:: 1.4.0
|
||||
|
||||
Returns
|
||||
-------
|
||||
cwt: (M, N) ndarray
|
||||
Will have shape of (len(widths), len(data)).
|
||||
|
||||
Notes
|
||||
-----
|
||||
|
||||
.. versionadded:: 1.4.0
|
||||
|
||||
For non-symmetric, complex-valued wavelets, the input signal is convolved
|
||||
with the time-reversed complex-conjugate of the wavelet data [1].
|
||||
|
||||
::
|
||||
|
||||
length = min(10 * width[ii], len(data))
|
||||
cwt[ii,:] = signal.convolve(data, np.conj(wavelet(length, width[ii],
|
||||
**kwargs))[::-1], mode='same')
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] S. Mallat, "A Wavelet Tour of Signal Processing (3rd Edition)",
|
||||
Academic Press, 2009.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> from scipy import signal
|
||||
>>> import matplotlib.pyplot as plt
|
||||
>>> t = np.linspace(-1, 1, 200, endpoint=False)
|
||||
>>> sig = np.cos(2 * np.pi * 7 * t) + signal.gausspulse(t - 0.4, fc=2)
|
||||
>>> widths = np.arange(1, 31)
|
||||
>>> cwtmatr = signal.cwt(sig, signal.ricker, widths)
|
||||
|
||||
.. note:: For cwt matrix plotting it is advisable to flip the y-axis
|
||||
|
||||
>>> cwtmatr_yflip = np.flipud(cwtmatr)
|
||||
>>> plt.imshow(cwtmatr_yflip, extent=[-1, 1, 1, 31], cmap='PRGn', aspect='auto',
|
||||
... vmax=abs(cwtmatr).max(), vmin=-abs(cwtmatr).max())
|
||||
>>> plt.show()
|
||||
"""
|
||||
warnings.warn(_msg % 'cwt', DeprecationWarning, stacklevel=2)
|
||||
return _cwt(data, wavelet, widths, dtype, **kwargs)
|
||||
|
||||
|
||||
def _cwt(data, wavelet, widths, dtype=None, **kwargs):
|
||||
# Determine output type
|
||||
if dtype is None:
|
||||
if np.asarray(wavelet(1, widths[0], **kwargs)).dtype.char in 'FDG':
|
||||
dtype = np.complex128
|
||||
else:
|
||||
dtype = np.float64
|
||||
|
||||
output = np.empty((len(widths), len(data)), dtype=dtype)
|
||||
for ind, width in enumerate(widths):
|
||||
N = np.min([10 * width, len(data)])
|
||||
wavelet_data = np.conj(wavelet(N, width, **kwargs)[::-1])
|
||||
output[ind] = convolve(data, wavelet_data, mode='same')
|
||||
return output
|
||||
21
venv/lib/python3.12/site-packages/scipy/signal/bsplines.py
Normal file
21
venv/lib/python3.12/site-packages/scipy/signal/bsplines.py
Normal file
@ -0,0 +1,21 @@
|
||||
# This file is not meant for public use and will be removed in SciPy v2.0.0.
|
||||
# Use the `scipy.signal` namespace for importing the functions
|
||||
# included below.
|
||||
|
||||
from scipy._lib.deprecation import _sub_module_deprecation
|
||||
|
||||
__all__ = [ # noqa: F822
|
||||
'spline_filter', 'gauss_spline',
|
||||
'cspline1d', 'qspline1d', 'cspline1d_eval', 'qspline1d_eval',
|
||||
'cspline2d', 'sepfir2d'
|
||||
]
|
||||
|
||||
|
||||
def __dir__():
|
||||
return __all__
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
return _sub_module_deprecation(sub_package="signal", module="bsplines",
|
||||
private_modules=["_bsplines"], all=__all__,
|
||||
attribute=name)
|
||||
@ -0,0 +1,28 @@
|
||||
# This file is not meant for public use and will be removed in SciPy v2.0.0.
|
||||
# Use the `scipy.signal` namespace for importing the functions
|
||||
# included below.
|
||||
|
||||
from scipy._lib.deprecation import _sub_module_deprecation
|
||||
|
||||
__all__ = [ # noqa: F822
|
||||
'findfreqs', 'freqs', 'freqz', 'tf2zpk', 'zpk2tf', 'normalize',
|
||||
'lp2lp', 'lp2hp', 'lp2bp', 'lp2bs', 'bilinear', 'iirdesign',
|
||||
'iirfilter', 'butter', 'cheby1', 'cheby2', 'ellip', 'bessel',
|
||||
'band_stop_obj', 'buttord', 'cheb1ord', 'cheb2ord', 'ellipord',
|
||||
'buttap', 'cheb1ap', 'cheb2ap', 'ellipap', 'besselap',
|
||||
'BadCoefficients', 'freqs_zpk', 'freqz_zpk',
|
||||
'tf2sos', 'sos2tf', 'zpk2sos', 'sos2zpk', 'group_delay',
|
||||
'sosfreqz', 'iirnotch', 'iirpeak', 'bilinear_zpk',
|
||||
'lp2lp_zpk', 'lp2hp_zpk', 'lp2bp_zpk', 'lp2bs_zpk',
|
||||
'gammatone', 'iircomb',
|
||||
]
|
||||
|
||||
|
||||
def __dir__():
|
||||
return __all__
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
return _sub_module_deprecation(sub_package="signal", module="filter_design",
|
||||
private_modules=["_filter_design"], all=__all__,
|
||||
attribute=name)
|
||||
@ -0,0 +1,20 @@
|
||||
# This file is not meant for public use and will be removed in SciPy v2.0.0.
|
||||
# Use the `scipy.signal` namespace for importing the functions
|
||||
# included below.
|
||||
|
||||
from scipy._lib.deprecation import _sub_module_deprecation
|
||||
|
||||
__all__ = [ # noqa: F822
|
||||
'kaiser_beta', 'kaiser_atten', 'kaiserord',
|
||||
'firwin', 'firwin2', 'remez', 'firls', 'minimum_phase',
|
||||
]
|
||||
|
||||
|
||||
def __dir__():
|
||||
return __all__
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
return _sub_module_deprecation(sub_package="signal", module="fir_filter_design",
|
||||
private_modules=["_fir_filter_design"], all=__all__,
|
||||
attribute=name)
|
||||
@ -0,0 +1,20 @@
|
||||
# This file is not meant for public use and will be removed in SciPy v2.0.0.
|
||||
# Use the `scipy.signal` namespace for importing the functions
|
||||
# included below.
|
||||
|
||||
from scipy._lib.deprecation import _sub_module_deprecation
|
||||
|
||||
__all__ = [ # noqa: F822
|
||||
'tf2ss', 'abcd_normalize', 'ss2tf', 'zpk2ss', 'ss2zpk',
|
||||
'cont2discrete', 'tf2zpk', 'zpk2tf', 'normalize'
|
||||
]
|
||||
|
||||
|
||||
def __dir__():
|
||||
return __all__
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
return _sub_module_deprecation(sub_package="signal", module="lti_conversion",
|
||||
private_modules=["_lti_conversion"], all=__all__,
|
||||
attribute=name)
|
||||
25
venv/lib/python3.12/site-packages/scipy/signal/ltisys.py
Normal file
25
venv/lib/python3.12/site-packages/scipy/signal/ltisys.py
Normal file
@ -0,0 +1,25 @@
|
||||
# This file is not meant for public use and will be removed in SciPy v2.0.0.
|
||||
# Use the `scipy.signal` namespace for importing the functions
|
||||
# included below.
|
||||
|
||||
from scipy._lib.deprecation import _sub_module_deprecation
|
||||
|
||||
__all__ = [ # noqa: F822
|
||||
'lti', 'dlti', 'TransferFunction', 'ZerosPolesGain', 'StateSpace',
|
||||
'lsim', 'impulse', 'step', 'bode',
|
||||
'freqresp', 'place_poles', 'dlsim', 'dstep', 'dimpulse',
|
||||
'dfreqresp', 'dbode',
|
||||
'tf2zpk', 'zpk2tf', 'normalize', 'freqs',
|
||||
'freqz', 'freqs_zpk', 'freqz_zpk', 'tf2ss', 'abcd_normalize',
|
||||
'ss2tf', 'zpk2ss', 'ss2zpk', 'cont2discrete',
|
||||
]
|
||||
|
||||
|
||||
def __dir__():
|
||||
return __all__
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
return _sub_module_deprecation(sub_package="signal", module="ltisys",
|
||||
private_modules=["_ltisys"], all=__all__,
|
||||
attribute=name)
|
||||
@ -0,0 +1,27 @@
|
||||
# This file is not meant for public use and will be removed in SciPy v2.0.0.
|
||||
# Use the `scipy.signal` namespace for importing the functions
|
||||
# included below.
|
||||
|
||||
from scipy._lib.deprecation import _sub_module_deprecation
|
||||
|
||||
__all__ = [ # noqa: F822
|
||||
'correlate', 'correlation_lags', 'correlate2d',
|
||||
'convolve', 'convolve2d', 'fftconvolve', 'oaconvolve',
|
||||
'order_filter', 'medfilt', 'medfilt2d', 'wiener', 'lfilter',
|
||||
'lfiltic', 'sosfilt', 'deconvolve', 'hilbert', 'hilbert2',
|
||||
'cmplx_sort', 'unique_roots', 'invres', 'invresz', 'residue',
|
||||
'residuez', 'resample', 'resample_poly', 'detrend',
|
||||
'lfilter_zi', 'sosfilt_zi', 'sosfiltfilt', 'choose_conv_method',
|
||||
'filtfilt', 'decimate', 'vectorstrength',
|
||||
'dlti', 'upfirdn', 'get_window', 'cheby1', 'firwin'
|
||||
]
|
||||
|
||||
|
||||
def __dir__():
|
||||
return __all__
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
return _sub_module_deprecation(sub_package="signal", module="signaltools",
|
||||
private_modules=["_signaltools"], all=__all__,
|
||||
attribute=name)
|
||||
21
venv/lib/python3.12/site-packages/scipy/signal/spectral.py
Normal file
21
venv/lib/python3.12/site-packages/scipy/signal/spectral.py
Normal file
@ -0,0 +1,21 @@
|
||||
# This file is not meant for public use and will be removed in SciPy v2.0.0.
|
||||
# Use the `scipy.signal` namespace for importing the functions
|
||||
# included below.
|
||||
|
||||
from scipy._lib.deprecation import _sub_module_deprecation
|
||||
|
||||
__all__ = [ # noqa: F822
|
||||
'periodogram', 'welch', 'lombscargle', 'csd', 'coherence',
|
||||
'spectrogram', 'stft', 'istft', 'check_COLA', 'check_NOLA',
|
||||
'get_window',
|
||||
]
|
||||
|
||||
|
||||
def __dir__():
|
||||
return __all__
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
return _sub_module_deprecation(sub_package="signal", module="spectral",
|
||||
private_modules=["_spectral_py"], all=__all__,
|
||||
attribute=name)
|
||||
26
venv/lib/python3.12/site-packages/scipy/signal/spline.py
Normal file
26
venv/lib/python3.12/site-packages/scipy/signal/spline.py
Normal file
@ -0,0 +1,26 @@
|
||||
# This file is not meant for public use and will be removed in the future
|
||||
# versions of SciPy. Use the `scipy.signal` namespace for importing the
|
||||
# functions included below.
|
||||
|
||||
import warnings
|
||||
|
||||
from . import _spline
|
||||
|
||||
__all__ = [ # noqa: F822
|
||||
'cspline2d', 'qspline2d', 'sepfir2d', 'symiirorder1', 'symiirorder2']
|
||||
|
||||
|
||||
def __dir__():
|
||||
return __all__
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
if name not in __all__:
|
||||
raise AttributeError(
|
||||
f"scipy.signal.spline is deprecated and has no attribute {name}. "
|
||||
"Try looking in scipy.signal instead.")
|
||||
|
||||
warnings.warn(f"Please use `{name}` from the `scipy.signal` namespace, "
|
||||
"the `scipy.signal.spline` namespace is deprecated.",
|
||||
category=DeprecationWarning, stacklevel=2)
|
||||
return getattr(_spline, name)
|
||||
@ -0,0 +1,488 @@
|
||||
"""Helpers to utilize existing stft / istft tests for testing `ShortTimeFFT`.
|
||||
|
||||
This module provides the functions stft_compare() and istft_compare(), which,
|
||||
compares the output between the existing (i)stft() and the shortTimeFFT based
|
||||
_(i)stft_wrapper() implementations in this module.
|
||||
|
||||
For testing add the following imports to the file ``tests/test_spectral.py``::
|
||||
|
||||
from ._scipy_spectral_test_shim import stft_compare as stft
|
||||
from ._scipy_spectral_test_shim import istft_compare as istft
|
||||
|
||||
and remove the existing imports of stft and istft.
|
||||
|
||||
The idea of these wrappers is not to provide a backward-compatible interface
|
||||
but to demonstrate that the ShortTimeFFT implementation is at least as capable
|
||||
as the existing one and delivers comparable results. Furthermore, the
|
||||
wrappers highlight the different philosophies of the implementations,
|
||||
especially in the border handling.
|
||||
"""
|
||||
import platform
|
||||
from typing import cast, Literal
|
||||
|
||||
import numpy as np
|
||||
from numpy.testing import assert_allclose
|
||||
|
||||
from scipy.signal import ShortTimeFFT
|
||||
from scipy.signal import csd, get_window, stft, istft
|
||||
from scipy.signal._arraytools import const_ext, even_ext, odd_ext, zero_ext
|
||||
from scipy.signal._short_time_fft import FFT_MODE_TYPE
|
||||
from scipy.signal._spectral_py import _spectral_helper, _triage_segments, \
|
||||
_median_bias
|
||||
|
||||
|
||||
def _stft_wrapper(x, fs=1.0, window='hann', nperseg=256, noverlap=None,
|
||||
nfft=None, detrend=False, return_onesided=True,
|
||||
boundary='zeros', padded=True, axis=-1, scaling='spectrum'):
|
||||
"""Wrapper for the SciPy `stft()` function based on `ShortTimeFFT` for
|
||||
unit testing.
|
||||
|
||||
Handling the boundary and padding is where `ShortTimeFFT` and `stft()`
|
||||
differ in behavior. Parts of `_spectral_helper()` were copied to mimic
|
||||
the` stft()` behavior.
|
||||
|
||||
This function is meant to be solely used by `stft_compare()`.
|
||||
"""
|
||||
if scaling not in ('psd', 'spectrum'): # same errors as in original stft:
|
||||
raise ValueError(f"Parameter {scaling=} not in ['spectrum', 'psd']!")
|
||||
|
||||
# The following lines are taken from the original _spectral_helper():
|
||||
boundary_funcs = {'even': even_ext,
|
||||
'odd': odd_ext,
|
||||
'constant': const_ext,
|
||||
'zeros': zero_ext,
|
||||
None: None}
|
||||
|
||||
if boundary not in boundary_funcs:
|
||||
raise ValueError(f"Unknown boundary option '{boundary}', must be one" +
|
||||
f" of: {list(boundary_funcs.keys())}")
|
||||
if x.size == 0:
|
||||
return np.empty(x.shape), np.empty(x.shape), np.empty(x.shape)
|
||||
|
||||
if nperseg is not None: # if specified by user
|
||||
nperseg = int(nperseg)
|
||||
if nperseg < 1:
|
||||
raise ValueError('nperseg must be a positive integer')
|
||||
|
||||
# parse window; if array like, then set nperseg = win.shape
|
||||
win, nperseg = _triage_segments(window, nperseg,
|
||||
input_length=x.shape[axis])
|
||||
|
||||
if nfft is None:
|
||||
nfft = nperseg
|
||||
elif nfft < nperseg:
|
||||
raise ValueError('nfft must be greater than or equal to nperseg.')
|
||||
else:
|
||||
nfft = int(nfft)
|
||||
|
||||
if noverlap is None:
|
||||
noverlap = nperseg//2
|
||||
else:
|
||||
noverlap = int(noverlap)
|
||||
if noverlap >= nperseg:
|
||||
raise ValueError('noverlap must be less than nperseg.')
|
||||
nstep = nperseg - noverlap
|
||||
n = x.shape[axis]
|
||||
|
||||
# Padding occurs after boundary extension, so that the extended signal ends
|
||||
# in zeros, instead of introducing an impulse at the end.
|
||||
# I.e. if x = [..., 3, 2]
|
||||
# extend then pad -> [..., 3, 2, 2, 3, 0, 0, 0]
|
||||
# pad then extend -> [..., 3, 2, 0, 0, 0, 2, 3]
|
||||
|
||||
if boundary is not None:
|
||||
ext_func = boundary_funcs[boundary]
|
||||
# Extend by nperseg//2 in front and back:
|
||||
x = ext_func(x, nperseg//2, axis=axis)
|
||||
|
||||
if padded:
|
||||
# Pad to integer number of windowed segments
|
||||
# I.e make x.shape[-1] = nperseg + (nseg-1)*nstep, with integer nseg
|
||||
x = np.moveaxis(x, axis, -1)
|
||||
|
||||
# This is an edge case where shortTimeFFT returns one more time slice
|
||||
# than the Scipy stft() shorten to remove last time slice:
|
||||
if n % 2 == 1 and nperseg % 2 == 1 and noverlap % 2 == 1:
|
||||
x = x[..., :axis - 1]
|
||||
|
||||
nadd = (-(x.shape[-1]-nperseg) % nstep) % nperseg
|
||||
zeros_shape = list(x.shape[:-1]) + [nadd]
|
||||
x = np.concatenate((x, np.zeros(zeros_shape)), axis=-1)
|
||||
x = np.moveaxis(x, -1, axis)
|
||||
|
||||
# ... end original _spectral_helper() code.
|
||||
scale_to = {'spectrum': 'magnitude', 'psd': 'psd'}[scaling]
|
||||
|
||||
if np.iscomplexobj(x) and return_onesided:
|
||||
return_onesided = False
|
||||
# using cast() to make mypy happy:
|
||||
fft_mode = cast(FFT_MODE_TYPE, 'onesided' if return_onesided else 'twosided')
|
||||
|
||||
ST = ShortTimeFFT(win, nstep, fs, fft_mode=fft_mode, mfft=nfft,
|
||||
scale_to=scale_to, phase_shift=None)
|
||||
|
||||
k_off = nperseg // 2
|
||||
p0 = 0 # ST.lower_border_end[1] + 1
|
||||
nn = x.shape[axis] if padded else n+k_off+1
|
||||
p1 = ST.upper_border_begin(nn)[1] # ST.p_max(n) + 1
|
||||
|
||||
# This is bad hack to pass the test test_roundtrip_boundary_extension():
|
||||
if padded is True and nperseg - noverlap == 1:
|
||||
p1 -= nperseg // 2 - 1 # the reasoning behind this is not clear to me
|
||||
|
||||
detr = None if detrend is False else detrend
|
||||
Sxx = ST.stft_detrend(x, detr, p0, p1, k_offset=k_off, axis=axis)
|
||||
t = ST.t(nn, 0, p1 - p0, k_offset=0 if boundary is not None else k_off)
|
||||
if x.dtype in (np.float32, np.complex64):
|
||||
Sxx = Sxx.astype(np.complex64)
|
||||
|
||||
# workaround for test_average_all_segments() - seems to be buggy behavior:
|
||||
if boundary is None and padded is False:
|
||||
t, Sxx = t[1:-1], Sxx[..., :-2]
|
||||
t -= k_off / fs
|
||||
|
||||
return ST.f, t, Sxx
|
||||
|
||||
|
||||
def _istft_wrapper(Zxx, fs=1.0, window='hann', nperseg=None, noverlap=None,
|
||||
nfft=None, input_onesided=True, boundary=True, time_axis=-1,
|
||||
freq_axis=-2, scaling='spectrum') -> \
|
||||
tuple[np.ndarray, np.ndarray, tuple[int, int]]:
|
||||
"""Wrapper for the SciPy `istft()` function based on `ShortTimeFFT` for
|
||||
unit testing.
|
||||
|
||||
Note that only option handling is implemented as far as to handle the unit
|
||||
tests. E.g., the case ``nperseg=None`` is not handled.
|
||||
|
||||
This function is meant to be solely used by `istft_compare()`.
|
||||
"""
|
||||
# *** Lines are taken from _spectral_py.istft() ***:
|
||||
if Zxx.ndim < 2:
|
||||
raise ValueError('Input stft must be at least 2d!')
|
||||
|
||||
if freq_axis == time_axis:
|
||||
raise ValueError('Must specify differing time and frequency axes!')
|
||||
|
||||
nseg = Zxx.shape[time_axis]
|
||||
|
||||
if input_onesided:
|
||||
# Assume even segment length
|
||||
n_default = 2*(Zxx.shape[freq_axis] - 1)
|
||||
else:
|
||||
n_default = Zxx.shape[freq_axis]
|
||||
|
||||
# Check windowing parameters
|
||||
if nperseg is None:
|
||||
nperseg = n_default
|
||||
else:
|
||||
nperseg = int(nperseg)
|
||||
if nperseg < 1:
|
||||
raise ValueError('nperseg must be a positive integer')
|
||||
|
||||
if nfft is None:
|
||||
if input_onesided and (nperseg == n_default + 1):
|
||||
# Odd nperseg, no FFT padding
|
||||
nfft = nperseg
|
||||
else:
|
||||
nfft = n_default
|
||||
elif nfft < nperseg:
|
||||
raise ValueError('nfft must be greater than or equal to nperseg.')
|
||||
else:
|
||||
nfft = int(nfft)
|
||||
|
||||
if noverlap is None:
|
||||
noverlap = nperseg//2
|
||||
else:
|
||||
noverlap = int(noverlap)
|
||||
if noverlap >= nperseg:
|
||||
raise ValueError('noverlap must be less than nperseg.')
|
||||
nstep = nperseg - noverlap
|
||||
|
||||
# Get window as array
|
||||
if isinstance(window, str) or type(window) is tuple:
|
||||
win = get_window(window, nperseg)
|
||||
else:
|
||||
win = np.asarray(window)
|
||||
if len(win.shape) != 1:
|
||||
raise ValueError('window must be 1-D')
|
||||
if win.shape[0] != nperseg:
|
||||
raise ValueError(f'window must have length of {nperseg}')
|
||||
|
||||
outputlength = nperseg + (nseg-1)*nstep
|
||||
# *** End block of: Taken from _spectral_py.istft() ***
|
||||
|
||||
# Using cast() to make mypy happy:
|
||||
fft_mode = cast(FFT_MODE_TYPE, 'onesided' if input_onesided else 'twosided')
|
||||
scale_to = cast(Literal['magnitude', 'psd'],
|
||||
{'spectrum': 'magnitude', 'psd': 'psd'}[scaling])
|
||||
|
||||
ST = ShortTimeFFT(win, nstep, fs, fft_mode=fft_mode, mfft=nfft,
|
||||
scale_to=scale_to, phase_shift=None)
|
||||
|
||||
if boundary:
|
||||
j = nperseg if nperseg % 2 == 0 else nperseg - 1
|
||||
k0 = ST.k_min + nperseg // 2
|
||||
k1 = outputlength - j + k0
|
||||
else:
|
||||
raise NotImplementedError("boundary=False does not make sense with" +
|
||||
"ShortTimeFFT.istft()!")
|
||||
|
||||
x = ST.istft(Zxx, k0=k0, k1=k1, f_axis=freq_axis, t_axis=time_axis)
|
||||
t = np.arange(k1 - k0) * ST.T
|
||||
k_hi = ST.upper_border_begin(k1 - k0)[0]
|
||||
# using cast() to make mypy happy:
|
||||
return t, x, (ST.lower_border_end[0], k_hi)
|
||||
|
||||
|
||||
def _csd_wrapper(x, y, fs=1.0, window='hann', nperseg=None, noverlap=None,
|
||||
nfft=None, detrend='constant', return_onesided=True,
|
||||
scaling='density', axis=-1, average='mean'):
|
||||
"""Wrapper for the `csd()` function based on `ShortTimeFFT` for
|
||||
unit testing.
|
||||
"""
|
||||
freqs, _, Pxy = _csd_test_shim(x, y, fs, window, nperseg, noverlap, nfft,
|
||||
detrend, return_onesided, scaling, axis)
|
||||
|
||||
# The following code is taken from csd():
|
||||
if len(Pxy.shape) >= 2 and Pxy.size > 0:
|
||||
if Pxy.shape[-1] > 1:
|
||||
if average == 'median':
|
||||
# np.median must be passed real arrays for the desired result
|
||||
bias = _median_bias(Pxy.shape[-1])
|
||||
if np.iscomplexobj(Pxy):
|
||||
Pxy = (np.median(np.real(Pxy), axis=-1)
|
||||
+ 1j * np.median(np.imag(Pxy), axis=-1))
|
||||
else:
|
||||
Pxy = np.median(Pxy, axis=-1)
|
||||
Pxy /= bias
|
||||
elif average == 'mean':
|
||||
Pxy = Pxy.mean(axis=-1)
|
||||
else:
|
||||
raise ValueError(f'average must be "median" or "mean", got {average}')
|
||||
else:
|
||||
Pxy = np.reshape(Pxy, Pxy.shape[:-1])
|
||||
|
||||
return freqs, Pxy
|
||||
|
||||
|
||||
def _csd_test_shim(x, y, fs=1.0, window='hann', nperseg=None, noverlap=None,
|
||||
nfft=None, detrend='constant', return_onesided=True,
|
||||
scaling='density', axis=-1):
|
||||
"""Compare output of _spectral_helper() and ShortTimeFFT, more
|
||||
precisely _spect_helper_csd() for used in csd_wrapper().
|
||||
|
||||
The motivation of this function is to test if the ShortTimeFFT-based
|
||||
wrapper `_spect_helper_csd()` returns the same values as `_spectral_helper`.
|
||||
This function should only be usd by csd() in (unit) testing.
|
||||
"""
|
||||
freqs, t, Pxy = _spectral_helper(x, y, fs, window, nperseg, noverlap, nfft,
|
||||
detrend, return_onesided, scaling, axis,
|
||||
mode='psd')
|
||||
freqs1, Pxy1 = _spect_helper_csd(x, y, fs, window, nperseg, noverlap, nfft,
|
||||
detrend, return_onesided, scaling, axis)
|
||||
|
||||
np.testing.assert_allclose(freqs1, freqs)
|
||||
amax_Pxy = max(np.abs(Pxy).max(), 1) if Pxy.size else 1
|
||||
atol = np.finfo(Pxy.dtype).resolution * amax_Pxy # needed for large Pxy
|
||||
# for c_ in range(Pxy.shape[-1]):
|
||||
# np.testing.assert_allclose(Pxy1[:, c_], Pxy[:, c_], atol=atol)
|
||||
np.testing.assert_allclose(Pxy1, Pxy, atol=atol)
|
||||
return freqs, t, Pxy
|
||||
|
||||
|
||||
def _spect_helper_csd(x, y, fs=1.0, window='hann', nperseg=None, noverlap=None,
|
||||
nfft=None, detrend='constant', return_onesided=True,
|
||||
scaling='density', axis=-1):
|
||||
"""Wrapper for replacing _spectral_helper() by using the ShortTimeFFT
|
||||
for use by csd().
|
||||
|
||||
This function should be only used by _csd_test_shim() and is only useful
|
||||
for testing the ShortTimeFFT implementation.
|
||||
"""
|
||||
|
||||
# The following lines are taken from the original _spectral_helper():
|
||||
same_data = y is x
|
||||
axis = int(axis)
|
||||
|
||||
# Ensure we have np.arrays, get outdtype
|
||||
x = np.asarray(x)
|
||||
if not same_data:
|
||||
y = np.asarray(y)
|
||||
# outdtype = np.result_type(x, y, np.complex64)
|
||||
# else:
|
||||
# outdtype = np.result_type(x, np.complex64)
|
||||
|
||||
if not same_data:
|
||||
# Check if we can broadcast the outer axes together
|
||||
xouter = list(x.shape)
|
||||
youter = list(y.shape)
|
||||
xouter.pop(axis)
|
||||
youter.pop(axis)
|
||||
try:
|
||||
outershape = np.broadcast(np.empty(xouter), np.empty(youter)).shape
|
||||
except ValueError as e:
|
||||
raise ValueError('x and y cannot be broadcast together.') from e
|
||||
|
||||
if same_data:
|
||||
if x.size == 0:
|
||||
return np.empty(x.shape), np.empty(x.shape)
|
||||
else:
|
||||
if x.size == 0 or y.size == 0:
|
||||
outshape = outershape + (min([x.shape[axis], y.shape[axis]]),)
|
||||
emptyout = np.moveaxis(np.empty(outshape), -1, axis)
|
||||
return emptyout, emptyout
|
||||
|
||||
if nperseg is not None: # if specified by user
|
||||
nperseg = int(nperseg)
|
||||
if nperseg < 1:
|
||||
raise ValueError('nperseg must be a positive integer')
|
||||
|
||||
# parse window; if array like, then set nperseg = win.shape
|
||||
n = x.shape[axis] if same_data else max(x.shape[axis], y.shape[axis])
|
||||
win, nperseg = _triage_segments(window, nperseg, input_length=n)
|
||||
|
||||
if nfft is None:
|
||||
nfft = nperseg
|
||||
elif nfft < nperseg:
|
||||
raise ValueError('nfft must be greater than or equal to nperseg.')
|
||||
else:
|
||||
nfft = int(nfft)
|
||||
|
||||
if noverlap is None:
|
||||
noverlap = nperseg // 2
|
||||
else:
|
||||
noverlap = int(noverlap)
|
||||
if noverlap >= nperseg:
|
||||
raise ValueError('noverlap must be less than nperseg.')
|
||||
nstep = nperseg - noverlap
|
||||
|
||||
if np.iscomplexobj(x) and return_onesided:
|
||||
return_onesided = False
|
||||
|
||||
# using cast() to make mypy happy:
|
||||
fft_mode = cast(FFT_MODE_TYPE, 'onesided' if return_onesided
|
||||
else 'twosided')
|
||||
scale = {'spectrum': 'magnitude', 'density': 'psd'}[scaling]
|
||||
SFT = ShortTimeFFT(win, nstep, fs, fft_mode=fft_mode, mfft=nfft,
|
||||
scale_to=scale, phase_shift=None)
|
||||
|
||||
# _spectral_helper() calculates X.conj()*Y instead of X*Y.conj():
|
||||
Pxy = SFT.spectrogram(y, x, detr=None if detrend is False else detrend,
|
||||
p0=0, p1=(n-noverlap)//SFT.hop, k_offset=nperseg//2,
|
||||
axis=axis).conj()
|
||||
# Note:
|
||||
# 'onesided2X' scaling of ShortTimeFFT conflicts with the
|
||||
# scaling='spectrum' parameter, since it doubles the squared magnitude,
|
||||
# which in the view of the ShortTimeFFT implementation does not make sense.
|
||||
# Hence, the doubling of the square is implemented here:
|
||||
if return_onesided:
|
||||
f_axis = Pxy.ndim - 1 + axis if axis < 0 else axis
|
||||
Pxy = np.moveaxis(Pxy, f_axis, -1)
|
||||
Pxy[..., 1:-1 if SFT.mfft % 2 == 0 else None] *= 2
|
||||
Pxy = np.moveaxis(Pxy, -1, f_axis)
|
||||
|
||||
return SFT.f, Pxy
|
||||
|
||||
|
||||
def stft_compare(x, fs=1.0, window='hann', nperseg=256, noverlap=None,
|
||||
nfft=None, detrend=False, return_onesided=True,
|
||||
boundary='zeros', padded=True, axis=-1, scaling='spectrum'):
|
||||
"""Assert that the results from the existing `stft()` and `_stft_wrapper()`
|
||||
are close to each other.
|
||||
|
||||
For comparing the STFT values an absolute tolerance of the floating point
|
||||
resolution was added to circumvent problems with the following tests:
|
||||
* For float32 the tolerances are much higher in
|
||||
TestSTFT.test_roundtrip_float32()).
|
||||
* The TestSTFT.test_roundtrip_scaling() has a high relative deviation.
|
||||
Interestingly this did not appear in Scipy 1.9.1 but only in the current
|
||||
development version.
|
||||
"""
|
||||
kw = dict(x=x, fs=fs, window=window, nperseg=nperseg, noverlap=noverlap,
|
||||
nfft=nfft, detrend=detrend, return_onesided=return_onesided,
|
||||
boundary=boundary, padded=padded, axis=axis, scaling=scaling)
|
||||
f, t, Zxx = stft(**kw)
|
||||
f_wrapper, t_wrapper, Zxx_wrapper = _stft_wrapper(**kw)
|
||||
|
||||
e_msg_part = " of `stft_wrapper()` differ from `stft()`."
|
||||
assert_allclose(f_wrapper, f, err_msg=f"Frequencies {e_msg_part}")
|
||||
assert_allclose(t_wrapper, t, err_msg=f"Time slices {e_msg_part}")
|
||||
|
||||
# Adapted tolerances to account for:
|
||||
atol = np.finfo(Zxx.dtype).resolution * 2
|
||||
assert_allclose(Zxx_wrapper, Zxx, atol=atol,
|
||||
err_msg=f"STFT values {e_msg_part}")
|
||||
return f, t, Zxx
|
||||
|
||||
|
||||
def istft_compare(Zxx, fs=1.0, window='hann', nperseg=None, noverlap=None,
|
||||
nfft=None, input_onesided=True, boundary=True, time_axis=-1,
|
||||
freq_axis=-2, scaling='spectrum'):
|
||||
"""Assert that the results from the existing `istft()` and
|
||||
`_istft_wrapper()` are close to each other.
|
||||
|
||||
Quirks:
|
||||
* If ``boundary=False`` the comparison is skipped, since it does not
|
||||
make sense with ShortTimeFFT.istft(). Only used in test
|
||||
TestSTFT.test_roundtrip_boundary_extension().
|
||||
* If ShortTimeFFT.istft() decides the STFT is not invertible, the
|
||||
comparison is skipped, since istft() only emits a warning and does not
|
||||
return a correct result. Only used in
|
||||
ShortTimeFFT.test_roundtrip_not_nola().
|
||||
* For comparing the signals an absolute tolerance of the floating point
|
||||
resolution was added to account for the low accuracy of float32 (Occurs
|
||||
only in TestSTFT.test_roundtrip_float32()).
|
||||
"""
|
||||
kw = dict(Zxx=Zxx, fs=fs, window=window, nperseg=nperseg,
|
||||
noverlap=noverlap, nfft=nfft, input_onesided=input_onesided,
|
||||
boundary=boundary, time_axis=time_axis, freq_axis=freq_axis,
|
||||
scaling=scaling)
|
||||
|
||||
t, x = istft(**kw)
|
||||
if not boundary: # skip test_roundtrip_boundary_extension():
|
||||
return t, x # _istft_wrapper does() not implement this case
|
||||
try: # if inversion fails, istft() only emits a warning:
|
||||
t_wrapper, x_wrapper, (k_lo, k_hi) = _istft_wrapper(**kw)
|
||||
except ValueError as v: # Do nothing if inversion fails:
|
||||
if v.args[0] == "Short-time Fourier Transform not invertible!":
|
||||
return t, x
|
||||
raise v
|
||||
|
||||
e_msg_part = " of `istft_wrapper()` differ from `istft()`"
|
||||
assert_allclose(t, t_wrapper, err_msg=f"Sample times {e_msg_part}")
|
||||
|
||||
# Adapted tolerances to account for resolution loss:
|
||||
atol = np.finfo(x.dtype).resolution*2 # instead of default atol = 0
|
||||
rtol = 1e-7 # default for np.allclose()
|
||||
|
||||
# Relax atol on 32-Bit platforms a bit to pass CI tests.
|
||||
# - Not clear why there are discrepancies (in the FFT maybe?)
|
||||
# - Not sure what changed on 'i686' since earlier on those test passed
|
||||
if x.dtype == np.float32 and platform.machine() == 'i686':
|
||||
# float32 gets only used by TestSTFT.test_roundtrip_float32() so
|
||||
# we are using the tolerances from there to circumvent CI problems
|
||||
atol, rtol = 1e-4, 1e-5
|
||||
elif platform.machine() in ('aarch64', 'i386', 'i686'):
|
||||
atol = max(atol, 1e-12) # 2e-15 seems too tight for 32-Bit platforms
|
||||
|
||||
assert_allclose(x_wrapper[k_lo:k_hi], x[k_lo:k_hi], atol=atol, rtol=rtol,
|
||||
err_msg=f"Signal values {e_msg_part}")
|
||||
return t, x
|
||||
|
||||
|
||||
def csd_compare(x, y, fs=1.0, window='hann', nperseg=None, noverlap=None,
|
||||
nfft=None, detrend='constant', return_onesided=True,
|
||||
scaling='density', axis=-1, average='mean'):
|
||||
"""Assert that the results from the existing `csd()` and `_csd_wrapper()`
|
||||
are close to each other. """
|
||||
kw = dict(x=x, y=y, fs=fs, window=window, nperseg=nperseg,
|
||||
noverlap=noverlap, nfft=nfft, detrend=detrend,
|
||||
return_onesided=return_onesided, scaling=scaling, axis=axis,
|
||||
average=average)
|
||||
freqs0, Pxy0 = csd(**kw)
|
||||
freqs1, Pxy1 = _csd_wrapper(**kw)
|
||||
|
||||
assert_allclose(freqs1, freqs0)
|
||||
assert_allclose(Pxy1, Pxy0)
|
||||
assert_allclose(freqs1, freqs0)
|
||||
return freqs0, Pxy0
|
||||
122
venv/lib/python3.12/site-packages/scipy/signal/tests/mpsig.py
Normal file
122
venv/lib/python3.12/site-packages/scipy/signal/tests/mpsig.py
Normal file
@ -0,0 +1,122 @@
|
||||
"""
|
||||
Some signal functions implemented using mpmath.
|
||||
"""
|
||||
|
||||
try:
|
||||
import mpmath
|
||||
except ImportError:
|
||||
mpmath = None
|
||||
|
||||
|
||||
def _prod(seq):
|
||||
"""Returns the product of the elements in the sequence `seq`."""
|
||||
p = 1
|
||||
for elem in seq:
|
||||
p *= elem
|
||||
return p
|
||||
|
||||
|
||||
def _relative_degree(z, p):
|
||||
"""
|
||||
Return relative degree of transfer function from zeros and poles.
|
||||
|
||||
This is simply len(p) - len(z), which must be nonnegative.
|
||||
A ValueError is raised if len(p) < len(z).
|
||||
"""
|
||||
degree = len(p) - len(z)
|
||||
if degree < 0:
|
||||
raise ValueError("Improper transfer function. "
|
||||
"Must have at least as many poles as zeros.")
|
||||
return degree
|
||||
|
||||
|
||||
def _zpkbilinear(z, p, k, fs):
|
||||
"""Bilinear transformation to convert a filter from analog to digital."""
|
||||
|
||||
degree = _relative_degree(z, p)
|
||||
|
||||
fs2 = 2*fs
|
||||
|
||||
# Bilinear transform the poles and zeros
|
||||
z_z = [(fs2 + z1) / (fs2 - z1) for z1 in z]
|
||||
p_z = [(fs2 + p1) / (fs2 - p1) for p1 in p]
|
||||
|
||||
# Any zeros that were at infinity get moved to the Nyquist frequency
|
||||
z_z.extend([-1] * degree)
|
||||
|
||||
# Compensate for gain change
|
||||
numer = _prod(fs2 - z1 for z1 in z)
|
||||
denom = _prod(fs2 - p1 for p1 in p)
|
||||
k_z = k * numer / denom
|
||||
|
||||
return z_z, p_z, k_z.real
|
||||
|
||||
|
||||
def _zpklp2lp(z, p, k, wo=1):
|
||||
"""Transform a lowpass filter to a different cutoff frequency."""
|
||||
|
||||
degree = _relative_degree(z, p)
|
||||
|
||||
# Scale all points radially from origin to shift cutoff frequency
|
||||
z_lp = [wo * z1 for z1 in z]
|
||||
p_lp = [wo * p1 for p1 in p]
|
||||
|
||||
# Each shifted pole decreases gain by wo, each shifted zero increases it.
|
||||
# Cancel out the net change to keep overall gain the same
|
||||
k_lp = k * wo**degree
|
||||
|
||||
return z_lp, p_lp, k_lp
|
||||
|
||||
|
||||
def _butter_analog_poles(n):
|
||||
"""
|
||||
Poles of an analog Butterworth lowpass filter.
|
||||
|
||||
This is the same calculation as scipy.signal.buttap(n) or
|
||||
scipy.signal.butter(n, 1, analog=True, output='zpk'), but mpmath is used,
|
||||
and only the poles are returned.
|
||||
"""
|
||||
poles = [-mpmath.exp(1j*mpmath.pi*k/(2*n)) for k in range(-n+1, n, 2)]
|
||||
return poles
|
||||
|
||||
|
||||
def butter_lp(n, Wn):
|
||||
"""
|
||||
Lowpass Butterworth digital filter design.
|
||||
|
||||
This computes the same result as scipy.signal.butter(n, Wn, output='zpk'),
|
||||
but it uses mpmath, and the results are returned in lists instead of NumPy
|
||||
arrays.
|
||||
"""
|
||||
zeros = []
|
||||
poles = _butter_analog_poles(n)
|
||||
k = 1
|
||||
fs = 2
|
||||
warped = 2 * fs * mpmath.tan(mpmath.pi * Wn / fs)
|
||||
z, p, k = _zpklp2lp(zeros, poles, k, wo=warped)
|
||||
z, p, k = _zpkbilinear(z, p, k, fs=fs)
|
||||
return z, p, k
|
||||
|
||||
|
||||
def zpkfreqz(z, p, k, worN=None):
|
||||
"""
|
||||
Frequency response of a filter in zpk format, using mpmath.
|
||||
|
||||
This is the same calculation as scipy.signal.freqz, but the input is in
|
||||
zpk format, the calculation is performed using mpath, and the results are
|
||||
returned in lists instead of NumPy arrays.
|
||||
"""
|
||||
if worN is None or isinstance(worN, int):
|
||||
N = worN or 512
|
||||
ws = [mpmath.pi * mpmath.mpf(j) / N for j in range(N)]
|
||||
else:
|
||||
ws = worN
|
||||
|
||||
h = []
|
||||
for wk in ws:
|
||||
zm1 = mpmath.exp(1j * wk)
|
||||
numer = _prod([zm1 - t for t in z])
|
||||
denom = _prod([zm1 - t for t in p])
|
||||
hk = k * numer / denom
|
||||
h.append(hk)
|
||||
return ws, h
|
||||
@ -0,0 +1,111 @@
|
||||
import numpy as np
|
||||
|
||||
from numpy.testing import assert_array_equal
|
||||
from pytest import raises as assert_raises
|
||||
|
||||
from scipy.signal._arraytools import (axis_slice, axis_reverse,
|
||||
odd_ext, even_ext, const_ext, zero_ext)
|
||||
|
||||
|
||||
class TestArrayTools:
|
||||
|
||||
def test_axis_slice(self):
|
||||
a = np.arange(12).reshape(3, 4)
|
||||
|
||||
s = axis_slice(a, start=0, stop=1, axis=0)
|
||||
assert_array_equal(s, a[0:1, :])
|
||||
|
||||
s = axis_slice(a, start=-1, axis=0)
|
||||
assert_array_equal(s, a[-1:, :])
|
||||
|
||||
s = axis_slice(a, start=0, stop=1, axis=1)
|
||||
assert_array_equal(s, a[:, 0:1])
|
||||
|
||||
s = axis_slice(a, start=-1, axis=1)
|
||||
assert_array_equal(s, a[:, -1:])
|
||||
|
||||
s = axis_slice(a, start=0, step=2, axis=0)
|
||||
assert_array_equal(s, a[::2, :])
|
||||
|
||||
s = axis_slice(a, start=0, step=2, axis=1)
|
||||
assert_array_equal(s, a[:, ::2])
|
||||
|
||||
def test_axis_reverse(self):
|
||||
a = np.arange(12).reshape(3, 4)
|
||||
|
||||
r = axis_reverse(a, axis=0)
|
||||
assert_array_equal(r, a[::-1, :])
|
||||
|
||||
r = axis_reverse(a, axis=1)
|
||||
assert_array_equal(r, a[:, ::-1])
|
||||
|
||||
def test_odd_ext(self):
|
||||
a = np.array([[1, 2, 3, 4, 5],
|
||||
[9, 8, 7, 6, 5]])
|
||||
|
||||
odd = odd_ext(a, 2, axis=1)
|
||||
expected = np.array([[-1, 0, 1, 2, 3, 4, 5, 6, 7],
|
||||
[11, 10, 9, 8, 7, 6, 5, 4, 3]])
|
||||
assert_array_equal(odd, expected)
|
||||
|
||||
odd = odd_ext(a, 1, axis=0)
|
||||
expected = np.array([[-7, -4, -1, 2, 5],
|
||||
[1, 2, 3, 4, 5],
|
||||
[9, 8, 7, 6, 5],
|
||||
[17, 14, 11, 8, 5]])
|
||||
assert_array_equal(odd, expected)
|
||||
|
||||
assert_raises(ValueError, odd_ext, a, 2, axis=0)
|
||||
assert_raises(ValueError, odd_ext, a, 5, axis=1)
|
||||
|
||||
def test_even_ext(self):
|
||||
a = np.array([[1, 2, 3, 4, 5],
|
||||
[9, 8, 7, 6, 5]])
|
||||
|
||||
even = even_ext(a, 2, axis=1)
|
||||
expected = np.array([[3, 2, 1, 2, 3, 4, 5, 4, 3],
|
||||
[7, 8, 9, 8, 7, 6, 5, 6, 7]])
|
||||
assert_array_equal(even, expected)
|
||||
|
||||
even = even_ext(a, 1, axis=0)
|
||||
expected = np.array([[9, 8, 7, 6, 5],
|
||||
[1, 2, 3, 4, 5],
|
||||
[9, 8, 7, 6, 5],
|
||||
[1, 2, 3, 4, 5]])
|
||||
assert_array_equal(even, expected)
|
||||
|
||||
assert_raises(ValueError, even_ext, a, 2, axis=0)
|
||||
assert_raises(ValueError, even_ext, a, 5, axis=1)
|
||||
|
||||
def test_const_ext(self):
|
||||
a = np.array([[1, 2, 3, 4, 5],
|
||||
[9, 8, 7, 6, 5]])
|
||||
|
||||
const = const_ext(a, 2, axis=1)
|
||||
expected = np.array([[1, 1, 1, 2, 3, 4, 5, 5, 5],
|
||||
[9, 9, 9, 8, 7, 6, 5, 5, 5]])
|
||||
assert_array_equal(const, expected)
|
||||
|
||||
const = const_ext(a, 1, axis=0)
|
||||
expected = np.array([[1, 2, 3, 4, 5],
|
||||
[1, 2, 3, 4, 5],
|
||||
[9, 8, 7, 6, 5],
|
||||
[9, 8, 7, 6, 5]])
|
||||
assert_array_equal(const, expected)
|
||||
|
||||
def test_zero_ext(self):
|
||||
a = np.array([[1, 2, 3, 4, 5],
|
||||
[9, 8, 7, 6, 5]])
|
||||
|
||||
zero = zero_ext(a, 2, axis=1)
|
||||
expected = np.array([[0, 0, 1, 2, 3, 4, 5, 0, 0],
|
||||
[0, 0, 9, 8, 7, 6, 5, 0, 0]])
|
||||
assert_array_equal(zero, expected)
|
||||
|
||||
zero = zero_ext(a, 1, axis=0)
|
||||
expected = np.array([[0, 0, 0, 0, 0],
|
||||
[1, 2, 3, 4, 5],
|
||||
[9, 8, 7, 6, 5],
|
||||
[0, 0, 0, 0, 0]])
|
||||
assert_array_equal(zero, expected)
|
||||
|
||||
@ -0,0 +1,186 @@
|
||||
# pylint: disable=missing-docstring
|
||||
import numpy as np
|
||||
from numpy import array
|
||||
from numpy.testing import (assert_allclose, assert_array_equal,
|
||||
assert_almost_equal)
|
||||
import pytest
|
||||
from pytest import raises
|
||||
|
||||
import scipy.signal._bsplines as bsp
|
||||
from scipy import signal
|
||||
|
||||
|
||||
class TestBSplines:
|
||||
"""Test behaviors of B-splines. Some of the values tested against were
|
||||
returned as of SciPy 1.1.0 and are included for regression testing
|
||||
purposes. Others (at integer points) are compared to theoretical
|
||||
expressions (cf. Unser, Aldroubi, Eden, IEEE TSP 1993, Table 1)."""
|
||||
|
||||
def test_spline_filter(self):
|
||||
np.random.seed(12457)
|
||||
# Test the type-error branch
|
||||
raises(TypeError, bsp.spline_filter, array([0]), 0)
|
||||
# Test the real branch
|
||||
np.random.seed(12457)
|
||||
data_array_real = np.random.rand(12, 12)
|
||||
# make the magnitude exceed 1, and make some negative
|
||||
data_array_real = 10*(1-2*data_array_real)
|
||||
result_array_real = array(
|
||||
[[-.463312621, 8.33391222, .697290949, 5.28390836,
|
||||
5.92066474, 6.59452137, 9.84406950, -8.78324188,
|
||||
7.20675750, -8.17222994, -4.38633345, 9.89917069],
|
||||
[2.67755154, 6.24192170, -3.15730578, 9.87658581,
|
||||
-9.96930425, 3.17194115, -4.50919947, 5.75423446,
|
||||
9.65979824, -8.29066885, .971416087, -2.38331897],
|
||||
[-7.08868346, 4.89887705, -1.37062289, 7.70705838,
|
||||
2.51526461, 3.65885497, 5.16786604, -8.77715342e-03,
|
||||
4.10533325, 9.04761993, -.577960351, 9.86382519],
|
||||
[-4.71444301, -1.68038985, 2.84695116, 1.14315938,
|
||||
-3.17127091, 1.91830461, 7.13779687, -5.35737482,
|
||||
-9.66586425, -9.87717456, 9.93160672, 4.71948144],
|
||||
[9.49551194, -1.92958436, 6.25427993, -9.05582911,
|
||||
3.97562282, 7.68232426, -1.04514824, -5.86021443,
|
||||
-8.43007451, 5.47528997, 2.06330736, -8.65968112],
|
||||
[-8.91720100, 8.87065356, 3.76879937, 2.56222894,
|
||||
-.828387146, 8.72288903, 6.42474741, -6.84576083,
|
||||
9.94724115, 6.90665380, -6.61084494, -9.44907391],
|
||||
[9.25196790, -.774032030, 7.05371046, -2.73505725,
|
||||
2.53953305, -1.82889155, 2.95454824, -1.66362046,
|
||||
5.72478916, -3.10287679, 1.54017123, -7.87759020],
|
||||
[-3.98464539, -2.44316992, -1.12708657, 1.01725672,
|
||||
-8.89294671, -5.42145629, -6.16370321, 2.91775492,
|
||||
9.64132208, .702499998, -2.02622392, 1.56308431],
|
||||
[-2.22050773, 7.89951554, 5.98970713, -7.35861835,
|
||||
5.45459283, -7.76427957, 3.67280490, -4.05521315,
|
||||
4.51967507, -3.22738749, -3.65080177, 3.05630155],
|
||||
[-6.21240584, -.296796126, -8.34800163, 9.21564563,
|
||||
-3.61958784, -4.77120006, -3.99454057, 1.05021988e-03,
|
||||
-6.95982829, 6.04380797, 8.43181250, -2.71653339],
|
||||
[1.19638037, 6.99718842e-02, 6.72020394, -2.13963198,
|
||||
3.75309875, -5.70076744, 5.92143551, -7.22150575,
|
||||
-3.77114594, -1.11903194, -5.39151466, 3.06620093],
|
||||
[9.86326886, 1.05134482, -7.75950607, -3.64429655,
|
||||
7.81848957, -9.02270373, 3.73399754, -4.71962549,
|
||||
-7.71144306, 3.78263161, 6.46034818, -4.43444731]])
|
||||
assert_allclose(bsp.spline_filter(data_array_real, 0),
|
||||
result_array_real)
|
||||
|
||||
def test_gauss_spline(self):
|
||||
np.random.seed(12459)
|
||||
assert_almost_equal(bsp.gauss_spline(0, 0), 1.381976597885342)
|
||||
assert_allclose(bsp.gauss_spline(array([1.]), 1), array([0.04865217]))
|
||||
|
||||
def test_gauss_spline_list(self):
|
||||
# regression test for gh-12152 (accept array_like)
|
||||
knots = [-1.0, 0.0, -1.0]
|
||||
assert_almost_equal(bsp.gauss_spline(knots, 3),
|
||||
array([0.15418033, 0.6909883, 0.15418033]))
|
||||
|
||||
def test_cspline1d(self):
|
||||
np.random.seed(12462)
|
||||
assert_array_equal(bsp.cspline1d(array([0])), [0.])
|
||||
c1d = array([1.21037185, 1.86293902, 2.98834059, 4.11660378,
|
||||
4.78893826])
|
||||
# test lamda != 0
|
||||
assert_allclose(bsp.cspline1d(array([1., 2, 3, 4, 5]), 1), c1d)
|
||||
c1d0 = array([0.78683946, 2.05333735, 2.99981113, 3.94741812,
|
||||
5.21051638])
|
||||
assert_allclose(bsp.cspline1d(array([1., 2, 3, 4, 5])), c1d0)
|
||||
|
||||
def test_qspline1d(self):
|
||||
np.random.seed(12463)
|
||||
assert_array_equal(bsp.qspline1d(array([0])), [0.])
|
||||
# test lamda != 0
|
||||
raises(ValueError, bsp.qspline1d, array([1., 2, 3, 4, 5]), 1.)
|
||||
raises(ValueError, bsp.qspline1d, array([1., 2, 3, 4, 5]), -1.)
|
||||
q1d0 = array([0.85350007, 2.02441743, 2.99999534, 3.97561055,
|
||||
5.14634135])
|
||||
assert_allclose(bsp.qspline1d(array([1., 2, 3, 4, 5])), q1d0)
|
||||
|
||||
def test_cspline1d_eval(self):
|
||||
np.random.seed(12464)
|
||||
assert_allclose(bsp.cspline1d_eval(array([0., 0]), [0.]), array([0.]))
|
||||
assert_array_equal(bsp.cspline1d_eval(array([1., 0, 1]), []),
|
||||
array([]))
|
||||
x = [-3, -2, -1, 0, 1, 2, 3, 4, 5, 6]
|
||||
dx = x[1]-x[0]
|
||||
newx = [-6., -5.5, -5., -4.5, -4., -3.5, -3., -2.5, -2., -1.5, -1.,
|
||||
-0.5, 0., 0.5, 1., 1.5, 2., 2.5, 3., 3.5, 4., 4.5, 5., 5.5, 6.,
|
||||
6.5, 7., 7.5, 8., 8.5, 9., 9.5, 10., 10.5, 11., 11.5, 12.,
|
||||
12.5]
|
||||
y = array([4.216, 6.864, 3.514, 6.203, 6.759, 7.433, 7.874, 5.879,
|
||||
1.396, 4.094])
|
||||
cj = bsp.cspline1d(y)
|
||||
newy = array([6.203, 4.41570658, 3.514, 5.16924703, 6.864, 6.04643068,
|
||||
4.21600281, 6.04643068, 6.864, 5.16924703, 3.514,
|
||||
4.41570658, 6.203, 6.80717667, 6.759, 6.98971173, 7.433,
|
||||
7.79560142, 7.874, 7.41525761, 5.879, 3.18686814, 1.396,
|
||||
2.24889482, 4.094, 2.24889482, 1.396, 3.18686814, 5.879,
|
||||
7.41525761, 7.874, 7.79560142, 7.433, 6.98971173, 6.759,
|
||||
6.80717667, 6.203, 4.41570658])
|
||||
assert_allclose(bsp.cspline1d_eval(cj, newx, dx=dx, x0=x[0]), newy)
|
||||
|
||||
def test_qspline1d_eval(self):
|
||||
np.random.seed(12465)
|
||||
assert_allclose(bsp.qspline1d_eval(array([0., 0]), [0.]), array([0.]))
|
||||
assert_array_equal(bsp.qspline1d_eval(array([1., 0, 1]), []),
|
||||
array([]))
|
||||
x = [-3, -2, -1, 0, 1, 2, 3, 4, 5, 6]
|
||||
dx = x[1]-x[0]
|
||||
newx = [-6., -5.5, -5., -4.5, -4., -3.5, -3., -2.5, -2., -1.5, -1.,
|
||||
-0.5, 0., 0.5, 1., 1.5, 2., 2.5, 3., 3.5, 4., 4.5, 5., 5.5, 6.,
|
||||
6.5, 7., 7.5, 8., 8.5, 9., 9.5, 10., 10.5, 11., 11.5, 12.,
|
||||
12.5]
|
||||
y = array([4.216, 6.864, 3.514, 6.203, 6.759, 7.433, 7.874, 5.879,
|
||||
1.396, 4.094])
|
||||
cj = bsp.qspline1d(y)
|
||||
newy = array([6.203, 4.49418159, 3.514, 5.18390821, 6.864, 5.91436915,
|
||||
4.21600002, 5.91436915, 6.864, 5.18390821, 3.514,
|
||||
4.49418159, 6.203, 6.71900226, 6.759, 7.03980488, 7.433,
|
||||
7.81016848, 7.874, 7.32718426, 5.879, 3.23872593, 1.396,
|
||||
2.34046013, 4.094, 2.34046013, 1.396, 3.23872593, 5.879,
|
||||
7.32718426, 7.874, 7.81016848, 7.433, 7.03980488, 6.759,
|
||||
6.71900226, 6.203, 4.49418159])
|
||||
assert_allclose(bsp.qspline1d_eval(cj, newx, dx=dx, x0=x[0]), newy)
|
||||
|
||||
|
||||
def test_sepfir2d_invalid_filter():
|
||||
filt = np.array([1.0, 2.0, 4.0, 2.0, 1.0])
|
||||
image = np.random.rand(7, 9)
|
||||
# No error for odd lengths
|
||||
signal.sepfir2d(image, filt, filt[2:])
|
||||
|
||||
# Row or column filter must be odd
|
||||
with pytest.raises(ValueError, match="odd length"):
|
||||
signal.sepfir2d(image, filt, filt[1:])
|
||||
with pytest.raises(ValueError, match="odd length"):
|
||||
signal.sepfir2d(image, filt[1:], filt)
|
||||
|
||||
# Filters must be 1-dimensional
|
||||
with pytest.raises(ValueError, match="object too deep"):
|
||||
signal.sepfir2d(image, filt.reshape(1, -1), filt)
|
||||
with pytest.raises(ValueError, match="object too deep"):
|
||||
signal.sepfir2d(image, filt, filt.reshape(1, -1))
|
||||
|
||||
def test_sepfir2d_invalid_image():
|
||||
filt = np.array([1.0, 2.0, 4.0, 2.0, 1.0])
|
||||
image = np.random.rand(8, 8)
|
||||
|
||||
# Image must be 2 dimensional
|
||||
with pytest.raises(ValueError, match="object too deep"):
|
||||
signal.sepfir2d(image.reshape(4, 4, 4), filt, filt)
|
||||
|
||||
with pytest.raises(ValueError, match="object of too small depth"):
|
||||
signal.sepfir2d(image[0], filt, filt)
|
||||
|
||||
|
||||
def test_cspline2d():
|
||||
np.random.seed(181819142)
|
||||
image = np.random.rand(71, 73)
|
||||
signal.cspline2d(image, 8.0)
|
||||
|
||||
|
||||
def test_qspline2d():
|
||||
np.random.seed(181819143)
|
||||
image = np.random.rand(71, 73)
|
||||
signal.qspline2d(image)
|
||||
@ -0,0 +1,416 @@
|
||||
import numpy as np
|
||||
from numpy.testing import \
|
||||
assert_array_almost_equal, assert_almost_equal, \
|
||||
assert_allclose, assert_equal
|
||||
|
||||
import pytest
|
||||
from scipy.signal import cont2discrete as c2d
|
||||
from scipy.signal import dlsim, ss2tf, ss2zpk, lsim, lti
|
||||
from scipy.signal import tf2ss, impulse, dimpulse, step, dstep
|
||||
|
||||
# Author: Jeffrey Armstrong <jeff@approximatrix.com>
|
||||
# March 29, 2011
|
||||
|
||||
|
||||
class TestC2D:
|
||||
def test_zoh(self):
|
||||
ac = np.eye(2)
|
||||
bc = np.full((2, 1), 0.5)
|
||||
cc = np.array([[0.75, 1.0], [1.0, 1.0], [1.0, 0.25]])
|
||||
dc = np.array([[0.0], [0.0], [-0.33]])
|
||||
|
||||
ad_truth = 1.648721270700128 * np.eye(2)
|
||||
bd_truth = np.full((2, 1), 0.324360635350064)
|
||||
# c and d in discrete should be equal to their continuous counterparts
|
||||
dt_requested = 0.5
|
||||
|
||||
ad, bd, cd, dd, dt = c2d((ac, bc, cc, dc), dt_requested, method='zoh')
|
||||
|
||||
assert_array_almost_equal(ad_truth, ad)
|
||||
assert_array_almost_equal(bd_truth, bd)
|
||||
assert_array_almost_equal(cc, cd)
|
||||
assert_array_almost_equal(dc, dd)
|
||||
assert_almost_equal(dt_requested, dt)
|
||||
|
||||
def test_foh(self):
|
||||
ac = np.eye(2)
|
||||
bc = np.full((2, 1), 0.5)
|
||||
cc = np.array([[0.75, 1.0], [1.0, 1.0], [1.0, 0.25]])
|
||||
dc = np.array([[0.0], [0.0], [-0.33]])
|
||||
|
||||
# True values are verified with Matlab
|
||||
ad_truth = 1.648721270700128 * np.eye(2)
|
||||
bd_truth = np.full((2, 1), 0.420839287058789)
|
||||
cd_truth = cc
|
||||
dd_truth = np.array([[0.260262223725224],
|
||||
[0.297442541400256],
|
||||
[-0.144098411624840]])
|
||||
dt_requested = 0.5
|
||||
|
||||
ad, bd, cd, dd, dt = c2d((ac, bc, cc, dc), dt_requested, method='foh')
|
||||
|
||||
assert_array_almost_equal(ad_truth, ad)
|
||||
assert_array_almost_equal(bd_truth, bd)
|
||||
assert_array_almost_equal(cd_truth, cd)
|
||||
assert_array_almost_equal(dd_truth, dd)
|
||||
assert_almost_equal(dt_requested, dt)
|
||||
|
||||
def test_impulse(self):
|
||||
ac = np.eye(2)
|
||||
bc = np.full((2, 1), 0.5)
|
||||
cc = np.array([[0.75, 1.0], [1.0, 1.0], [1.0, 0.25]])
|
||||
dc = np.array([[0.0], [0.0], [0.0]])
|
||||
|
||||
# True values are verified with Matlab
|
||||
ad_truth = 1.648721270700128 * np.eye(2)
|
||||
bd_truth = np.full((2, 1), 0.412180317675032)
|
||||
cd_truth = cc
|
||||
dd_truth = np.array([[0.4375], [0.5], [0.3125]])
|
||||
dt_requested = 0.5
|
||||
|
||||
ad, bd, cd, dd, dt = c2d((ac, bc, cc, dc), dt_requested,
|
||||
method='impulse')
|
||||
|
||||
assert_array_almost_equal(ad_truth, ad)
|
||||
assert_array_almost_equal(bd_truth, bd)
|
||||
assert_array_almost_equal(cd_truth, cd)
|
||||
assert_array_almost_equal(dd_truth, dd)
|
||||
assert_almost_equal(dt_requested, dt)
|
||||
|
||||
def test_gbt(self):
|
||||
ac = np.eye(2)
|
||||
bc = np.full((2, 1), 0.5)
|
||||
cc = np.array([[0.75, 1.0], [1.0, 1.0], [1.0, 0.25]])
|
||||
dc = np.array([[0.0], [0.0], [-0.33]])
|
||||
|
||||
dt_requested = 0.5
|
||||
alpha = 1.0 / 3.0
|
||||
|
||||
ad_truth = 1.6 * np.eye(2)
|
||||
bd_truth = np.full((2, 1), 0.3)
|
||||
cd_truth = np.array([[0.9, 1.2],
|
||||
[1.2, 1.2],
|
||||
[1.2, 0.3]])
|
||||
dd_truth = np.array([[0.175],
|
||||
[0.2],
|
||||
[-0.205]])
|
||||
|
||||
ad, bd, cd, dd, dt = c2d((ac, bc, cc, dc), dt_requested,
|
||||
method='gbt', alpha=alpha)
|
||||
|
||||
assert_array_almost_equal(ad_truth, ad)
|
||||
assert_array_almost_equal(bd_truth, bd)
|
||||
assert_array_almost_equal(cd_truth, cd)
|
||||
assert_array_almost_equal(dd_truth, dd)
|
||||
|
||||
def test_euler(self):
|
||||
ac = np.eye(2)
|
||||
bc = np.full((2, 1), 0.5)
|
||||
cc = np.array([[0.75, 1.0], [1.0, 1.0], [1.0, 0.25]])
|
||||
dc = np.array([[0.0], [0.0], [-0.33]])
|
||||
|
||||
dt_requested = 0.5
|
||||
|
||||
ad_truth = 1.5 * np.eye(2)
|
||||
bd_truth = np.full((2, 1), 0.25)
|
||||
cd_truth = np.array([[0.75, 1.0],
|
||||
[1.0, 1.0],
|
||||
[1.0, 0.25]])
|
||||
dd_truth = dc
|
||||
|
||||
ad, bd, cd, dd, dt = c2d((ac, bc, cc, dc), dt_requested,
|
||||
method='euler')
|
||||
|
||||
assert_array_almost_equal(ad_truth, ad)
|
||||
assert_array_almost_equal(bd_truth, bd)
|
||||
assert_array_almost_equal(cd_truth, cd)
|
||||
assert_array_almost_equal(dd_truth, dd)
|
||||
assert_almost_equal(dt_requested, dt)
|
||||
|
||||
def test_backward_diff(self):
|
||||
ac = np.eye(2)
|
||||
bc = np.full((2, 1), 0.5)
|
||||
cc = np.array([[0.75, 1.0], [1.0, 1.0], [1.0, 0.25]])
|
||||
dc = np.array([[0.0], [0.0], [-0.33]])
|
||||
|
||||
dt_requested = 0.5
|
||||
|
||||
ad_truth = 2.0 * np.eye(2)
|
||||
bd_truth = np.full((2, 1), 0.5)
|
||||
cd_truth = np.array([[1.5, 2.0],
|
||||
[2.0, 2.0],
|
||||
[2.0, 0.5]])
|
||||
dd_truth = np.array([[0.875],
|
||||
[1.0],
|
||||
[0.295]])
|
||||
|
||||
ad, bd, cd, dd, dt = c2d((ac, bc, cc, dc), dt_requested,
|
||||
method='backward_diff')
|
||||
|
||||
assert_array_almost_equal(ad_truth, ad)
|
||||
assert_array_almost_equal(bd_truth, bd)
|
||||
assert_array_almost_equal(cd_truth, cd)
|
||||
assert_array_almost_equal(dd_truth, dd)
|
||||
|
||||
def test_bilinear(self):
|
||||
ac = np.eye(2)
|
||||
bc = np.full((2, 1), 0.5)
|
||||
cc = np.array([[0.75, 1.0], [1.0, 1.0], [1.0, 0.25]])
|
||||
dc = np.array([[0.0], [0.0], [-0.33]])
|
||||
|
||||
dt_requested = 0.5
|
||||
|
||||
ad_truth = (5.0 / 3.0) * np.eye(2)
|
||||
bd_truth = np.full((2, 1), 1.0 / 3.0)
|
||||
cd_truth = np.array([[1.0, 4.0 / 3.0],
|
||||
[4.0 / 3.0, 4.0 / 3.0],
|
||||
[4.0 / 3.0, 1.0 / 3.0]])
|
||||
dd_truth = np.array([[0.291666666666667],
|
||||
[1.0 / 3.0],
|
||||
[-0.121666666666667]])
|
||||
|
||||
ad, bd, cd, dd, dt = c2d((ac, bc, cc, dc), dt_requested,
|
||||
method='bilinear')
|
||||
|
||||
assert_array_almost_equal(ad_truth, ad)
|
||||
assert_array_almost_equal(bd_truth, bd)
|
||||
assert_array_almost_equal(cd_truth, cd)
|
||||
assert_array_almost_equal(dd_truth, dd)
|
||||
assert_almost_equal(dt_requested, dt)
|
||||
|
||||
# Same continuous system again, but change sampling rate
|
||||
|
||||
ad_truth = 1.4 * np.eye(2)
|
||||
bd_truth = np.full((2, 1), 0.2)
|
||||
cd_truth = np.array([[0.9, 1.2], [1.2, 1.2], [1.2, 0.3]])
|
||||
dd_truth = np.array([[0.175], [0.2], [-0.205]])
|
||||
|
||||
dt_requested = 1.0 / 3.0
|
||||
|
||||
ad, bd, cd, dd, dt = c2d((ac, bc, cc, dc), dt_requested,
|
||||
method='bilinear')
|
||||
|
||||
assert_array_almost_equal(ad_truth, ad)
|
||||
assert_array_almost_equal(bd_truth, bd)
|
||||
assert_array_almost_equal(cd_truth, cd)
|
||||
assert_array_almost_equal(dd_truth, dd)
|
||||
assert_almost_equal(dt_requested, dt)
|
||||
|
||||
def test_transferfunction(self):
|
||||
numc = np.array([0.25, 0.25, 0.5])
|
||||
denc = np.array([0.75, 0.75, 1.0])
|
||||
|
||||
numd = np.array([[1.0 / 3.0, -0.427419169438754, 0.221654141101125]])
|
||||
dend = np.array([1.0, -1.351394049721225, 0.606530659712634])
|
||||
|
||||
dt_requested = 0.5
|
||||
|
||||
num, den, dt = c2d((numc, denc), dt_requested, method='zoh')
|
||||
|
||||
assert_array_almost_equal(numd, num)
|
||||
assert_array_almost_equal(dend, den)
|
||||
assert_almost_equal(dt_requested, dt)
|
||||
|
||||
def test_zerospolesgain(self):
|
||||
zeros_c = np.array([0.5, -0.5])
|
||||
poles_c = np.array([1.j / np.sqrt(2), -1.j / np.sqrt(2)])
|
||||
k_c = 1.0
|
||||
|
||||
zeros_d = [1.23371727305860, 0.735356894461267]
|
||||
polls_d = [0.938148335039729 + 0.346233593780536j,
|
||||
0.938148335039729 - 0.346233593780536j]
|
||||
k_d = 1.0
|
||||
|
||||
dt_requested = 0.5
|
||||
|
||||
zeros, poles, k, dt = c2d((zeros_c, poles_c, k_c), dt_requested,
|
||||
method='zoh')
|
||||
|
||||
assert_array_almost_equal(zeros_d, zeros)
|
||||
assert_array_almost_equal(polls_d, poles)
|
||||
assert_almost_equal(k_d, k)
|
||||
assert_almost_equal(dt_requested, dt)
|
||||
|
||||
def test_gbt_with_sio_tf_and_zpk(self):
|
||||
"""Test method='gbt' with alpha=0.25 for tf and zpk cases."""
|
||||
# State space coefficients for the continuous SIO system.
|
||||
A = -1.0
|
||||
B = 1.0
|
||||
C = 1.0
|
||||
D = 0.5
|
||||
|
||||
# The continuous transfer function coefficients.
|
||||
cnum, cden = ss2tf(A, B, C, D)
|
||||
|
||||
# Continuous zpk representation
|
||||
cz, cp, ck = ss2zpk(A, B, C, D)
|
||||
|
||||
h = 1.0
|
||||
alpha = 0.25
|
||||
|
||||
# Explicit formulas, in the scalar case.
|
||||
Ad = (1 + (1 - alpha) * h * A) / (1 - alpha * h * A)
|
||||
Bd = h * B / (1 - alpha * h * A)
|
||||
Cd = C / (1 - alpha * h * A)
|
||||
Dd = D + alpha * C * Bd
|
||||
|
||||
# Convert the explicit solution to tf
|
||||
dnum, dden = ss2tf(Ad, Bd, Cd, Dd)
|
||||
|
||||
# Compute the discrete tf using cont2discrete.
|
||||
c2dnum, c2dden, dt = c2d((cnum, cden), h, method='gbt', alpha=alpha)
|
||||
|
||||
assert_allclose(dnum, c2dnum)
|
||||
assert_allclose(dden, c2dden)
|
||||
|
||||
# Convert explicit solution to zpk.
|
||||
dz, dp, dk = ss2zpk(Ad, Bd, Cd, Dd)
|
||||
|
||||
# Compute the discrete zpk using cont2discrete.
|
||||
c2dz, c2dp, c2dk, dt = c2d((cz, cp, ck), h, method='gbt', alpha=alpha)
|
||||
|
||||
assert_allclose(dz, c2dz)
|
||||
assert_allclose(dp, c2dp)
|
||||
assert_allclose(dk, c2dk)
|
||||
|
||||
def test_discrete_approx(self):
|
||||
"""
|
||||
Test that the solution to the discrete approximation of a continuous
|
||||
system actually approximates the solution to the continuous system.
|
||||
This is an indirect test of the correctness of the implementation
|
||||
of cont2discrete.
|
||||
"""
|
||||
|
||||
def u(t):
|
||||
return np.sin(2.5 * t)
|
||||
|
||||
a = np.array([[-0.01]])
|
||||
b = np.array([[1.0]])
|
||||
c = np.array([[1.0]])
|
||||
d = np.array([[0.2]])
|
||||
x0 = 1.0
|
||||
|
||||
t = np.linspace(0, 10.0, 101)
|
||||
dt = t[1] - t[0]
|
||||
u1 = u(t)
|
||||
|
||||
# Use lsim to compute the solution to the continuous system.
|
||||
t, yout, xout = lsim((a, b, c, d), T=t, U=u1, X0=x0)
|
||||
|
||||
# Convert the continuous system to a discrete approximation.
|
||||
dsys = c2d((a, b, c, d), dt, method='bilinear')
|
||||
|
||||
# Use dlsim with the pairwise averaged input to compute the output
|
||||
# of the discrete system.
|
||||
u2 = 0.5 * (u1[:-1] + u1[1:])
|
||||
t2 = t[:-1]
|
||||
td2, yd2, xd2 = dlsim(dsys, u=u2.reshape(-1, 1), t=t2, x0=x0)
|
||||
|
||||
# ymid is the average of consecutive terms of the "exact" output
|
||||
# computed by lsim2. This is what the discrete approximation
|
||||
# actually approximates.
|
||||
ymid = 0.5 * (yout[:-1] + yout[1:])
|
||||
|
||||
assert_allclose(yd2.ravel(), ymid, rtol=1e-4)
|
||||
|
||||
def test_simo_tf(self):
|
||||
# See gh-5753
|
||||
tf = ([[1, 0], [1, 1]], [1, 1])
|
||||
num, den, dt = c2d(tf, 0.01)
|
||||
|
||||
assert_equal(dt, 0.01) # sanity check
|
||||
assert_allclose(den, [1, -0.990404983], rtol=1e-3)
|
||||
assert_allclose(num, [[1, -1], [1, -0.99004983]], rtol=1e-3)
|
||||
|
||||
def test_multioutput(self):
|
||||
ts = 0.01 # time step
|
||||
|
||||
tf = ([[1, -3], [1, 5]], [1, 1])
|
||||
num, den, dt = c2d(tf, ts)
|
||||
|
||||
tf1 = (tf[0][0], tf[1])
|
||||
num1, den1, dt1 = c2d(tf1, ts)
|
||||
|
||||
tf2 = (tf[0][1], tf[1])
|
||||
num2, den2, dt2 = c2d(tf2, ts)
|
||||
|
||||
# Sanity checks
|
||||
assert_equal(dt, dt1)
|
||||
assert_equal(dt, dt2)
|
||||
|
||||
# Check that we get the same results
|
||||
assert_allclose(num, np.vstack((num1, num2)), rtol=1e-13)
|
||||
|
||||
# Single input, so the denominator should
|
||||
# not be multidimensional like the numerator
|
||||
assert_allclose(den, den1, rtol=1e-13)
|
||||
assert_allclose(den, den2, rtol=1e-13)
|
||||
|
||||
class TestC2dLti:
|
||||
def test_c2d_ss(self):
|
||||
# StateSpace
|
||||
A = np.array([[-0.3, 0.1], [0.2, -0.7]])
|
||||
B = np.array([[0], [1]])
|
||||
C = np.array([[1, 0]])
|
||||
D = 0
|
||||
|
||||
A_res = np.array([[0.985136404135682, 0.004876671474795],
|
||||
[0.009753342949590, 0.965629718236502]])
|
||||
B_res = np.array([[0.000122937599964], [0.049135527547844]])
|
||||
|
||||
sys_ssc = lti(A, B, C, D)
|
||||
sys_ssd = sys_ssc.to_discrete(0.05)
|
||||
|
||||
assert_allclose(sys_ssd.A, A_res)
|
||||
assert_allclose(sys_ssd.B, B_res)
|
||||
assert_allclose(sys_ssd.C, C)
|
||||
assert_allclose(sys_ssd.D, D)
|
||||
|
||||
def test_c2d_tf(self):
|
||||
|
||||
sys = lti([0.5, 0.3], [1.0, 0.4])
|
||||
sys = sys.to_discrete(0.005)
|
||||
|
||||
# Matlab results
|
||||
num_res = np.array([0.5, -0.485149004980066])
|
||||
den_res = np.array([1.0, -0.980198673306755])
|
||||
|
||||
# Somehow a lot of numerical errors
|
||||
assert_allclose(sys.den, den_res, atol=0.02)
|
||||
assert_allclose(sys.num, num_res, atol=0.02)
|
||||
|
||||
|
||||
class TestC2dInvariants:
|
||||
# Some test cases for checking the invariances.
|
||||
# Array of triplets: (system, sample time, number of samples)
|
||||
cases = [
|
||||
(tf2ss([1, 1], [1, 1.5, 1]), 0.25, 10),
|
||||
(tf2ss([1, 2], [1, 1.5, 3, 1]), 0.5, 10),
|
||||
(tf2ss(0.1, [1, 1, 2, 1]), 0.5, 10),
|
||||
]
|
||||
|
||||
# Check that systems discretized with the impulse-invariant
|
||||
# method really hold the invariant
|
||||
@pytest.mark.parametrize("sys,sample_time,samples_number", cases)
|
||||
def test_impulse_invariant(self, sys, sample_time, samples_number):
|
||||
time = np.arange(samples_number) * sample_time
|
||||
_, yout_cont = impulse(sys, T=time)
|
||||
_, yout_disc = dimpulse(c2d(sys, sample_time, method='impulse'),
|
||||
n=len(time))
|
||||
assert_allclose(sample_time * yout_cont.ravel(), yout_disc[0].ravel())
|
||||
|
||||
# Step invariant should hold for ZOH discretized systems
|
||||
@pytest.mark.parametrize("sys,sample_time,samples_number", cases)
|
||||
def test_step_invariant(self, sys, sample_time, samples_number):
|
||||
time = np.arange(samples_number) * sample_time
|
||||
_, yout_cont = step(sys, T=time)
|
||||
_, yout_disc = dstep(c2d(sys, sample_time, method='zoh'), n=len(time))
|
||||
assert_allclose(yout_cont.ravel(), yout_disc[0].ravel())
|
||||
|
||||
# Linear invariant should hold for FOH discretized systems
|
||||
@pytest.mark.parametrize("sys,sample_time,samples_number", cases)
|
||||
def test_linear_invariant(self, sys, sample_time, samples_number):
|
||||
time = np.arange(samples_number) * sample_time
|
||||
_, yout_cont, _ = lsim(sys, T=time, U=time)
|
||||
_, yout_disc, _ = dlsim(c2d(sys, sample_time, method='foh'), u=time)
|
||||
assert_allclose(yout_cont.ravel(), yout_disc.ravel())
|
||||
219
venv/lib/python3.12/site-packages/scipy/signal/tests/test_czt.py
Normal file
219
venv/lib/python3.12/site-packages/scipy/signal/tests/test_czt.py
Normal file
@ -0,0 +1,219 @@
|
||||
# This program is public domain
|
||||
# Authors: Paul Kienzle, Nadav Horesh
|
||||
'''
|
||||
A unit test module for czt.py
|
||||
'''
|
||||
import pytest
|
||||
from numpy.testing import assert_allclose
|
||||
from scipy.fft import fft
|
||||
from scipy.signal import (czt, zoom_fft, czt_points, CZT, ZoomFFT)
|
||||
import numpy as np
|
||||
|
||||
|
||||
def check_czt(x):
|
||||
# Check that czt is the equivalent of normal fft
|
||||
y = fft(x)
|
||||
y1 = czt(x)
|
||||
assert_allclose(y1, y, rtol=1e-13)
|
||||
|
||||
# Check that interpolated czt is the equivalent of normal fft
|
||||
y = fft(x, 100*len(x))
|
||||
y1 = czt(x, 100*len(x))
|
||||
assert_allclose(y1, y, rtol=1e-12)
|
||||
|
||||
|
||||
def check_zoom_fft(x):
|
||||
# Check that zoom_fft is the equivalent of normal fft
|
||||
y = fft(x)
|
||||
y1 = zoom_fft(x, [0, 2-2./len(y)], endpoint=True)
|
||||
assert_allclose(y1, y, rtol=1e-11, atol=1e-14)
|
||||
y1 = zoom_fft(x, [0, 2])
|
||||
assert_allclose(y1, y, rtol=1e-11, atol=1e-14)
|
||||
|
||||
# Test fn scalar
|
||||
y1 = zoom_fft(x, 2-2./len(y), endpoint=True)
|
||||
assert_allclose(y1, y, rtol=1e-11, atol=1e-14)
|
||||
y1 = zoom_fft(x, 2)
|
||||
assert_allclose(y1, y, rtol=1e-11, atol=1e-14)
|
||||
|
||||
# Check that zoom_fft with oversampling is equivalent to zero padding
|
||||
over = 10
|
||||
yover = fft(x, over*len(x))
|
||||
y2 = zoom_fft(x, [0, 2-2./len(yover)], m=len(yover), endpoint=True)
|
||||
assert_allclose(y2, yover, rtol=1e-12, atol=1e-10)
|
||||
y2 = zoom_fft(x, [0, 2], m=len(yover))
|
||||
assert_allclose(y2, yover, rtol=1e-12, atol=1e-10)
|
||||
|
||||
# Check that zoom_fft works on a subrange
|
||||
w = np.linspace(0, 2-2./len(x), len(x))
|
||||
f1, f2 = w[3], w[6]
|
||||
y3 = zoom_fft(x, [f1, f2], m=3*over+1, endpoint=True)
|
||||
idx3 = slice(3*over, 6*over+1)
|
||||
assert_allclose(y3, yover[idx3], rtol=1e-13)
|
||||
|
||||
|
||||
def test_1D():
|
||||
# Test of 1D version of the transforms
|
||||
|
||||
np.random.seed(0) # Deterministic randomness
|
||||
|
||||
# Random signals
|
||||
lengths = np.random.randint(8, 200, 20)
|
||||
np.append(lengths, 1)
|
||||
for length in lengths:
|
||||
x = np.random.random(length)
|
||||
check_zoom_fft(x)
|
||||
check_czt(x)
|
||||
|
||||
# Gauss
|
||||
t = np.linspace(-2, 2, 128)
|
||||
x = np.exp(-t**2/0.01)
|
||||
check_zoom_fft(x)
|
||||
|
||||
# Linear
|
||||
x = [1, 2, 3, 4, 5, 6, 7]
|
||||
check_zoom_fft(x)
|
||||
|
||||
# Check near powers of two
|
||||
check_zoom_fft(range(126-31))
|
||||
check_zoom_fft(range(127-31))
|
||||
check_zoom_fft(range(128-31))
|
||||
check_zoom_fft(range(129-31))
|
||||
check_zoom_fft(range(130-31))
|
||||
|
||||
# Check transform on n-D array input
|
||||
x = np.reshape(np.arange(3*2*28), (3, 2, 28))
|
||||
y1 = zoom_fft(x, [0, 2-2./28])
|
||||
y2 = zoom_fft(x[2, 0, :], [0, 2-2./28])
|
||||
assert_allclose(y1[2, 0], y2, rtol=1e-13, atol=1e-12)
|
||||
|
||||
y1 = zoom_fft(x, [0, 2], endpoint=False)
|
||||
y2 = zoom_fft(x[2, 0, :], [0, 2], endpoint=False)
|
||||
assert_allclose(y1[2, 0], y2, rtol=1e-13, atol=1e-12)
|
||||
|
||||
# Random (not a test condition)
|
||||
x = np.random.rand(101)
|
||||
check_zoom_fft(x)
|
||||
|
||||
# Spikes
|
||||
t = np.linspace(0, 1, 128)
|
||||
x = np.sin(2*np.pi*t*5)+np.sin(2*np.pi*t*13)
|
||||
check_zoom_fft(x)
|
||||
|
||||
# Sines
|
||||
x = np.zeros(100, dtype=complex)
|
||||
x[[1, 5, 21]] = 1
|
||||
check_zoom_fft(x)
|
||||
|
||||
# Sines plus complex component
|
||||
x += 1j*np.linspace(0, 0.5, x.shape[0])
|
||||
check_zoom_fft(x)
|
||||
|
||||
|
||||
def test_large_prime_lengths():
|
||||
np.random.seed(0) # Deterministic randomness
|
||||
for N in (101, 1009, 10007):
|
||||
x = np.random.rand(N)
|
||||
y = fft(x)
|
||||
y1 = czt(x)
|
||||
assert_allclose(y, y1, rtol=1e-12)
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_czt_vs_fft():
|
||||
np.random.seed(123)
|
||||
random_lengths = np.random.exponential(100000, size=10).astype('int')
|
||||
for n in random_lengths:
|
||||
a = np.random.randn(n)
|
||||
assert_allclose(czt(a), fft(a), rtol=1e-11)
|
||||
|
||||
|
||||
def test_empty_input():
|
||||
with pytest.raises(ValueError, match='Invalid number of CZT'):
|
||||
czt([])
|
||||
with pytest.raises(ValueError, match='Invalid number of CZT'):
|
||||
zoom_fft([], 0.5)
|
||||
|
||||
|
||||
def test_0_rank_input():
|
||||
with pytest.raises(IndexError, match='tuple index out of range'):
|
||||
czt(5)
|
||||
with pytest.raises(IndexError, match='tuple index out of range'):
|
||||
zoom_fft(5, 0.5)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('impulse', ([0, 0, 1], [0, 0, 1, 0, 0],
|
||||
np.concatenate((np.array([0, 0, 1]),
|
||||
np.zeros(100)))))
|
||||
@pytest.mark.parametrize('m', (1, 3, 5, 8, 101, 1021))
|
||||
@pytest.mark.parametrize('a', (1, 2, 0.5, 1.1))
|
||||
# Step that tests away from the unit circle, but not so far it explodes from
|
||||
# numerical error
|
||||
@pytest.mark.parametrize('w', (None, 0.98534 + 0.17055j))
|
||||
def test_czt_math(impulse, m, w, a):
|
||||
# z-transform of an impulse is 1 everywhere
|
||||
assert_allclose(czt(impulse[2:], m=m, w=w, a=a),
|
||||
np.ones(m), rtol=1e-10)
|
||||
|
||||
# z-transform of a delayed impulse is z**-1
|
||||
assert_allclose(czt(impulse[1:], m=m, w=w, a=a),
|
||||
czt_points(m=m, w=w, a=a)**-1, rtol=1e-10)
|
||||
|
||||
# z-transform of a 2-delayed impulse is z**-2
|
||||
assert_allclose(czt(impulse, m=m, w=w, a=a),
|
||||
czt_points(m=m, w=w, a=a)**-2, rtol=1e-10)
|
||||
|
||||
|
||||
def test_int_args():
|
||||
# Integer argument `a` was producing all 0s
|
||||
assert_allclose(abs(czt([0, 1], m=10, a=2)), 0.5*np.ones(10), rtol=1e-15)
|
||||
assert_allclose(czt_points(11, w=2), 1/(2**np.arange(11)), rtol=1e-30)
|
||||
|
||||
|
||||
def test_czt_points():
|
||||
for N in (1, 2, 3, 8, 11, 100, 101, 10007):
|
||||
assert_allclose(czt_points(N), np.exp(2j*np.pi*np.arange(N)/N),
|
||||
rtol=1e-30)
|
||||
|
||||
assert_allclose(czt_points(7, w=1), np.ones(7), rtol=1e-30)
|
||||
assert_allclose(czt_points(11, w=2.), 1/(2**np.arange(11)), rtol=1e-30)
|
||||
|
||||
func = CZT(12, m=11, w=2., a=1)
|
||||
assert_allclose(func.points(), 1/(2**np.arange(11)), rtol=1e-30)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('cls, args', [(CZT, (100,)), (ZoomFFT, (100, 0.2))])
|
||||
def test_CZT_size_mismatch(cls, args):
|
||||
# Data size doesn't match function's expected size
|
||||
myfunc = cls(*args)
|
||||
with pytest.raises(ValueError, match='CZT defined for'):
|
||||
myfunc(np.arange(5))
|
||||
|
||||
|
||||
def test_invalid_range():
|
||||
with pytest.raises(ValueError, match='2-length sequence'):
|
||||
ZoomFFT(100, [1, 2, 3])
|
||||
|
||||
|
||||
@pytest.mark.parametrize('m', [0, -11, 5.5, 4.0])
|
||||
def test_czt_points_errors(m):
|
||||
# Invalid number of points
|
||||
with pytest.raises(ValueError, match='Invalid number of CZT'):
|
||||
czt_points(m)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('size', [0, -5, 3.5, 4.0])
|
||||
def test_nonsense_size(size):
|
||||
# Numpy and Scipy fft() give ValueError for 0 output size, so we do, too
|
||||
with pytest.raises(ValueError, match='Invalid number of CZT'):
|
||||
CZT(size, 3)
|
||||
with pytest.raises(ValueError, match='Invalid number of CZT'):
|
||||
ZoomFFT(size, 0.2, 3)
|
||||
with pytest.raises(ValueError, match='Invalid number of CZT'):
|
||||
CZT(3, size)
|
||||
with pytest.raises(ValueError, match='Invalid number of CZT'):
|
||||
ZoomFFT(3, 0.2, size)
|
||||
with pytest.raises(ValueError, match='Invalid number of CZT'):
|
||||
czt([1, 2, 3], size)
|
||||
with pytest.raises(ValueError, match='Invalid number of CZT'):
|
||||
zoom_fft([1, 2, 3], 0.2, size)
|
||||
@ -0,0 +1,598 @@
|
||||
# Author: Jeffrey Armstrong <jeff@approximatrix.com>
|
||||
# April 4, 2011
|
||||
|
||||
import numpy as np
|
||||
from numpy.testing import (assert_equal,
|
||||
assert_array_almost_equal, assert_array_equal,
|
||||
assert_allclose, assert_, assert_almost_equal,
|
||||
suppress_warnings)
|
||||
from pytest import raises as assert_raises
|
||||
from scipy.signal import (dlsim, dstep, dimpulse, tf2zpk, lti, dlti,
|
||||
StateSpace, TransferFunction, ZerosPolesGain,
|
||||
dfreqresp, dbode, BadCoefficients)
|
||||
|
||||
|
||||
class TestDLTI:
|
||||
|
||||
def test_dlsim(self):
|
||||
|
||||
a = np.asarray([[0.9, 0.1], [-0.2, 0.9]])
|
||||
b = np.asarray([[0.4, 0.1, -0.1], [0.0, 0.05, 0.0]])
|
||||
c = np.asarray([[0.1, 0.3]])
|
||||
d = np.asarray([[0.0, -0.1, 0.0]])
|
||||
dt = 0.5
|
||||
|
||||
# Create an input matrix with inputs down the columns (3 cols) and its
|
||||
# respective time input vector
|
||||
u = np.hstack((np.linspace(0, 4.0, num=5)[:, np.newaxis],
|
||||
np.full((5, 1), 0.01),
|
||||
np.full((5, 1), -0.002)))
|
||||
t_in = np.linspace(0, 2.0, num=5)
|
||||
|
||||
# Define the known result
|
||||
yout_truth = np.array([[-0.001,
|
||||
-0.00073,
|
||||
0.039446,
|
||||
0.0915387,
|
||||
0.13195948]]).T
|
||||
xout_truth = np.asarray([[0, 0],
|
||||
[0.0012, 0.0005],
|
||||
[0.40233, 0.00071],
|
||||
[1.163368, -0.079327],
|
||||
[2.2402985, -0.3035679]])
|
||||
|
||||
tout, yout, xout = dlsim((a, b, c, d, dt), u, t_in)
|
||||
|
||||
assert_array_almost_equal(yout_truth, yout)
|
||||
assert_array_almost_equal(xout_truth, xout)
|
||||
assert_array_almost_equal(t_in, tout)
|
||||
|
||||
# Make sure input with single-dimension doesn't raise error
|
||||
dlsim((1, 2, 3), 4)
|
||||
|
||||
# Interpolated control - inputs should have different time steps
|
||||
# than the discrete model uses internally
|
||||
u_sparse = u[[0, 4], :]
|
||||
t_sparse = np.asarray([0.0, 2.0])
|
||||
|
||||
tout, yout, xout = dlsim((a, b, c, d, dt), u_sparse, t_sparse)
|
||||
|
||||
assert_array_almost_equal(yout_truth, yout)
|
||||
assert_array_almost_equal(xout_truth, xout)
|
||||
assert_equal(len(tout), yout.shape[0])
|
||||
|
||||
# Transfer functions (assume dt = 0.5)
|
||||
num = np.asarray([1.0, -0.1])
|
||||
den = np.asarray([0.3, 1.0, 0.2])
|
||||
yout_truth = np.array([[0.0,
|
||||
0.0,
|
||||
3.33333333333333,
|
||||
-4.77777777777778,
|
||||
23.0370370370370]]).T
|
||||
|
||||
# Assume use of the first column of the control input built earlier
|
||||
tout, yout = dlsim((num, den, 0.5), u[:, 0], t_in)
|
||||
|
||||
assert_array_almost_equal(yout, yout_truth)
|
||||
assert_array_almost_equal(t_in, tout)
|
||||
|
||||
# Retest the same with a 1-D input vector
|
||||
uflat = np.asarray(u[:, 0])
|
||||
uflat = uflat.reshape((5,))
|
||||
tout, yout = dlsim((num, den, 0.5), uflat, t_in)
|
||||
|
||||
assert_array_almost_equal(yout, yout_truth)
|
||||
assert_array_almost_equal(t_in, tout)
|
||||
|
||||
# zeros-poles-gain representation
|
||||
zd = np.array([0.5, -0.5])
|
||||
pd = np.array([1.j / np.sqrt(2), -1.j / np.sqrt(2)])
|
||||
k = 1.0
|
||||
yout_truth = np.array([[0.0, 1.0, 2.0, 2.25, 2.5]]).T
|
||||
|
||||
tout, yout = dlsim((zd, pd, k, 0.5), u[:, 0], t_in)
|
||||
|
||||
assert_array_almost_equal(yout, yout_truth)
|
||||
assert_array_almost_equal(t_in, tout)
|
||||
|
||||
# Raise an error for continuous-time systems
|
||||
system = lti([1], [1, 1])
|
||||
assert_raises(AttributeError, dlsim, system, u)
|
||||
|
||||
def test_dstep(self):
|
||||
|
||||
a = np.asarray([[0.9, 0.1], [-0.2, 0.9]])
|
||||
b = np.asarray([[0.4, 0.1, -0.1], [0.0, 0.05, 0.0]])
|
||||
c = np.asarray([[0.1, 0.3]])
|
||||
d = np.asarray([[0.0, -0.1, 0.0]])
|
||||
dt = 0.5
|
||||
|
||||
# Because b.shape[1] == 3, dstep should result in a tuple of three
|
||||
# result vectors
|
||||
yout_step_truth = (np.asarray([0.0, 0.04, 0.052, 0.0404, 0.00956,
|
||||
-0.036324, -0.093318, -0.15782348,
|
||||
-0.226628324, -0.2969374948]),
|
||||
np.asarray([-0.1, -0.075, -0.058, -0.04815,
|
||||
-0.04453, -0.0461895, -0.0521812,
|
||||
-0.061588875, -0.073549579,
|
||||
-0.08727047595]),
|
||||
np.asarray([0.0, -0.01, -0.013, -0.0101, -0.00239,
|
||||
0.009081, 0.0233295, 0.03945587,
|
||||
0.056657081, 0.0742343737]))
|
||||
|
||||
tout, yout = dstep((a, b, c, d, dt), n=10)
|
||||
|
||||
assert_equal(len(yout), 3)
|
||||
|
||||
for i in range(0, len(yout)):
|
||||
assert_equal(yout[i].shape[0], 10)
|
||||
assert_array_almost_equal(yout[i].flatten(), yout_step_truth[i])
|
||||
|
||||
# Check that the other two inputs (tf, zpk) will work as well
|
||||
tfin = ([1.0], [1.0, 1.0], 0.5)
|
||||
yout_tfstep = np.asarray([0.0, 1.0, 0.0])
|
||||
tout, yout = dstep(tfin, n=3)
|
||||
assert_equal(len(yout), 1)
|
||||
assert_array_almost_equal(yout[0].flatten(), yout_tfstep)
|
||||
|
||||
zpkin = tf2zpk(tfin[0], tfin[1]) + (0.5,)
|
||||
tout, yout = dstep(zpkin, n=3)
|
||||
assert_equal(len(yout), 1)
|
||||
assert_array_almost_equal(yout[0].flatten(), yout_tfstep)
|
||||
|
||||
# Raise an error for continuous-time systems
|
||||
system = lti([1], [1, 1])
|
||||
assert_raises(AttributeError, dstep, system)
|
||||
|
||||
def test_dimpulse(self):
|
||||
|
||||
a = np.asarray([[0.9, 0.1], [-0.2, 0.9]])
|
||||
b = np.asarray([[0.4, 0.1, -0.1], [0.0, 0.05, 0.0]])
|
||||
c = np.asarray([[0.1, 0.3]])
|
||||
d = np.asarray([[0.0, -0.1, 0.0]])
|
||||
dt = 0.5
|
||||
|
||||
# Because b.shape[1] == 3, dimpulse should result in a tuple of three
|
||||
# result vectors
|
||||
yout_imp_truth = (np.asarray([0.0, 0.04, 0.012, -0.0116, -0.03084,
|
||||
-0.045884, -0.056994, -0.06450548,
|
||||
-0.068804844, -0.0703091708]),
|
||||
np.asarray([-0.1, 0.025, 0.017, 0.00985, 0.00362,
|
||||
-0.0016595, -0.0059917, -0.009407675,
|
||||
-0.011960704, -0.01372089695]),
|
||||
np.asarray([0.0, -0.01, -0.003, 0.0029, 0.00771,
|
||||
0.011471, 0.0142485, 0.01612637,
|
||||
0.017201211, 0.0175772927]))
|
||||
|
||||
tout, yout = dimpulse((a, b, c, d, dt), n=10)
|
||||
|
||||
assert_equal(len(yout), 3)
|
||||
|
||||
for i in range(0, len(yout)):
|
||||
assert_equal(yout[i].shape[0], 10)
|
||||
assert_array_almost_equal(yout[i].flatten(), yout_imp_truth[i])
|
||||
|
||||
# Check that the other two inputs (tf, zpk) will work as well
|
||||
tfin = ([1.0], [1.0, 1.0], 0.5)
|
||||
yout_tfimpulse = np.asarray([0.0, 1.0, -1.0])
|
||||
tout, yout = dimpulse(tfin, n=3)
|
||||
assert_equal(len(yout), 1)
|
||||
assert_array_almost_equal(yout[0].flatten(), yout_tfimpulse)
|
||||
|
||||
zpkin = tf2zpk(tfin[0], tfin[1]) + (0.5,)
|
||||
tout, yout = dimpulse(zpkin, n=3)
|
||||
assert_equal(len(yout), 1)
|
||||
assert_array_almost_equal(yout[0].flatten(), yout_tfimpulse)
|
||||
|
||||
# Raise an error for continuous-time systems
|
||||
system = lti([1], [1, 1])
|
||||
assert_raises(AttributeError, dimpulse, system)
|
||||
|
||||
def test_dlsim_trivial(self):
|
||||
a = np.array([[0.0]])
|
||||
b = np.array([[0.0]])
|
||||
c = np.array([[0.0]])
|
||||
d = np.array([[0.0]])
|
||||
n = 5
|
||||
u = np.zeros(n).reshape(-1, 1)
|
||||
tout, yout, xout = dlsim((a, b, c, d, 1), u)
|
||||
assert_array_equal(tout, np.arange(float(n)))
|
||||
assert_array_equal(yout, np.zeros((n, 1)))
|
||||
assert_array_equal(xout, np.zeros((n, 1)))
|
||||
|
||||
def test_dlsim_simple1d(self):
|
||||
a = np.array([[0.5]])
|
||||
b = np.array([[0.0]])
|
||||
c = np.array([[1.0]])
|
||||
d = np.array([[0.0]])
|
||||
n = 5
|
||||
u = np.zeros(n).reshape(-1, 1)
|
||||
tout, yout, xout = dlsim((a, b, c, d, 1), u, x0=1)
|
||||
assert_array_equal(tout, np.arange(float(n)))
|
||||
expected = (0.5 ** np.arange(float(n))).reshape(-1, 1)
|
||||
assert_array_equal(yout, expected)
|
||||
assert_array_equal(xout, expected)
|
||||
|
||||
def test_dlsim_simple2d(self):
|
||||
lambda1 = 0.5
|
||||
lambda2 = 0.25
|
||||
a = np.array([[lambda1, 0.0],
|
||||
[0.0, lambda2]])
|
||||
b = np.array([[0.0],
|
||||
[0.0]])
|
||||
c = np.array([[1.0, 0.0],
|
||||
[0.0, 1.0]])
|
||||
d = np.array([[0.0],
|
||||
[0.0]])
|
||||
n = 5
|
||||
u = np.zeros(n).reshape(-1, 1)
|
||||
tout, yout, xout = dlsim((a, b, c, d, 1), u, x0=1)
|
||||
assert_array_equal(tout, np.arange(float(n)))
|
||||
# The analytical solution:
|
||||
expected = (np.array([lambda1, lambda2]) **
|
||||
np.arange(float(n)).reshape(-1, 1))
|
||||
assert_array_equal(yout, expected)
|
||||
assert_array_equal(xout, expected)
|
||||
|
||||
def test_more_step_and_impulse(self):
|
||||
lambda1 = 0.5
|
||||
lambda2 = 0.75
|
||||
a = np.array([[lambda1, 0.0],
|
||||
[0.0, lambda2]])
|
||||
b = np.array([[1.0, 0.0],
|
||||
[0.0, 1.0]])
|
||||
c = np.array([[1.0, 1.0]])
|
||||
d = np.array([[0.0, 0.0]])
|
||||
|
||||
n = 10
|
||||
|
||||
# Check a step response.
|
||||
ts, ys = dstep((a, b, c, d, 1), n=n)
|
||||
|
||||
# Create the exact step response.
|
||||
stp0 = (1.0 / (1 - lambda1)) * (1.0 - lambda1 ** np.arange(n))
|
||||
stp1 = (1.0 / (1 - lambda2)) * (1.0 - lambda2 ** np.arange(n))
|
||||
|
||||
assert_allclose(ys[0][:, 0], stp0)
|
||||
assert_allclose(ys[1][:, 0], stp1)
|
||||
|
||||
# Check an impulse response with an initial condition.
|
||||
x0 = np.array([1.0, 1.0])
|
||||
ti, yi = dimpulse((a, b, c, d, 1), n=n, x0=x0)
|
||||
|
||||
# Create the exact impulse response.
|
||||
imp = (np.array([lambda1, lambda2]) **
|
||||
np.arange(-1, n + 1).reshape(-1, 1))
|
||||
imp[0, :] = 0.0
|
||||
# Analytical solution to impulse response
|
||||
y0 = imp[:n, 0] + np.dot(imp[1:n + 1, :], x0)
|
||||
y1 = imp[:n, 1] + np.dot(imp[1:n + 1, :], x0)
|
||||
|
||||
assert_allclose(yi[0][:, 0], y0)
|
||||
assert_allclose(yi[1][:, 0], y1)
|
||||
|
||||
# Check that dt=0.1, n=3 gives 3 time values.
|
||||
system = ([1.0], [1.0, -0.5], 0.1)
|
||||
t, (y,) = dstep(system, n=3)
|
||||
assert_allclose(t, [0, 0.1, 0.2])
|
||||
assert_array_equal(y.T, [[0, 1.0, 1.5]])
|
||||
t, (y,) = dimpulse(system, n=3)
|
||||
assert_allclose(t, [0, 0.1, 0.2])
|
||||
assert_array_equal(y.T, [[0, 1, 0.5]])
|
||||
|
||||
|
||||
class TestDlti:
|
||||
def test_dlti_instantiation(self):
|
||||
# Test that lti can be instantiated.
|
||||
|
||||
dt = 0.05
|
||||
# TransferFunction
|
||||
s = dlti([1], [-1], dt=dt)
|
||||
assert_(isinstance(s, TransferFunction))
|
||||
assert_(isinstance(s, dlti))
|
||||
assert_(not isinstance(s, lti))
|
||||
assert_equal(s.dt, dt)
|
||||
|
||||
# ZerosPolesGain
|
||||
s = dlti(np.array([]), np.array([-1]), 1, dt=dt)
|
||||
assert_(isinstance(s, ZerosPolesGain))
|
||||
assert_(isinstance(s, dlti))
|
||||
assert_(not isinstance(s, lti))
|
||||
assert_equal(s.dt, dt)
|
||||
|
||||
# StateSpace
|
||||
s = dlti([1], [-1], 1, 3, dt=dt)
|
||||
assert_(isinstance(s, StateSpace))
|
||||
assert_(isinstance(s, dlti))
|
||||
assert_(not isinstance(s, lti))
|
||||
assert_equal(s.dt, dt)
|
||||
|
||||
# Number of inputs
|
||||
assert_raises(ValueError, dlti, 1)
|
||||
assert_raises(ValueError, dlti, 1, 1, 1, 1, 1)
|
||||
|
||||
|
||||
class TestStateSpaceDisc:
|
||||
def test_initialization(self):
|
||||
# Check that all initializations work
|
||||
dt = 0.05
|
||||
StateSpace(1, 1, 1, 1, dt=dt)
|
||||
StateSpace([1], [2], [3], [4], dt=dt)
|
||||
StateSpace(np.array([[1, 2], [3, 4]]), np.array([[1], [2]]),
|
||||
np.array([[1, 0]]), np.array([[0]]), dt=dt)
|
||||
StateSpace(1, 1, 1, 1, dt=True)
|
||||
|
||||
def test_conversion(self):
|
||||
# Check the conversion functions
|
||||
s = StateSpace(1, 2, 3, 4, dt=0.05)
|
||||
assert_(isinstance(s.to_ss(), StateSpace))
|
||||
assert_(isinstance(s.to_tf(), TransferFunction))
|
||||
assert_(isinstance(s.to_zpk(), ZerosPolesGain))
|
||||
|
||||
# Make sure copies work
|
||||
assert_(StateSpace(s) is not s)
|
||||
assert_(s.to_ss() is not s)
|
||||
|
||||
def test_properties(self):
|
||||
# Test setters/getters for cross class properties.
|
||||
# This implicitly tests to_tf() and to_zpk()
|
||||
|
||||
# Getters
|
||||
s = StateSpace(1, 1, 1, 1, dt=0.05)
|
||||
assert_equal(s.poles, [1])
|
||||
assert_equal(s.zeros, [0])
|
||||
|
||||
|
||||
class TestTransferFunction:
|
||||
def test_initialization(self):
|
||||
# Check that all initializations work
|
||||
dt = 0.05
|
||||
TransferFunction(1, 1, dt=dt)
|
||||
TransferFunction([1], [2], dt=dt)
|
||||
TransferFunction(np.array([1]), np.array([2]), dt=dt)
|
||||
TransferFunction(1, 1, dt=True)
|
||||
|
||||
def test_conversion(self):
|
||||
# Check the conversion functions
|
||||
s = TransferFunction([1, 0], [1, -1], dt=0.05)
|
||||
assert_(isinstance(s.to_ss(), StateSpace))
|
||||
assert_(isinstance(s.to_tf(), TransferFunction))
|
||||
assert_(isinstance(s.to_zpk(), ZerosPolesGain))
|
||||
|
||||
# Make sure copies work
|
||||
assert_(TransferFunction(s) is not s)
|
||||
assert_(s.to_tf() is not s)
|
||||
|
||||
def test_properties(self):
|
||||
# Test setters/getters for cross class properties.
|
||||
# This implicitly tests to_ss() and to_zpk()
|
||||
|
||||
# Getters
|
||||
s = TransferFunction([1, 0], [1, -1], dt=0.05)
|
||||
assert_equal(s.poles, [1])
|
||||
assert_equal(s.zeros, [0])
|
||||
|
||||
|
||||
class TestZerosPolesGain:
|
||||
def test_initialization(self):
|
||||
# Check that all initializations work
|
||||
dt = 0.05
|
||||
ZerosPolesGain(1, 1, 1, dt=dt)
|
||||
ZerosPolesGain([1], [2], 1, dt=dt)
|
||||
ZerosPolesGain(np.array([1]), np.array([2]), 1, dt=dt)
|
||||
ZerosPolesGain(1, 1, 1, dt=True)
|
||||
|
||||
def test_conversion(self):
|
||||
# Check the conversion functions
|
||||
s = ZerosPolesGain(1, 2, 3, dt=0.05)
|
||||
assert_(isinstance(s.to_ss(), StateSpace))
|
||||
assert_(isinstance(s.to_tf(), TransferFunction))
|
||||
assert_(isinstance(s.to_zpk(), ZerosPolesGain))
|
||||
|
||||
# Make sure copies work
|
||||
assert_(ZerosPolesGain(s) is not s)
|
||||
assert_(s.to_zpk() is not s)
|
||||
|
||||
|
||||
class Test_dfreqresp:
|
||||
|
||||
def test_manual(self):
|
||||
# Test dfreqresp() real part calculation (manual sanity check).
|
||||
# 1st order low-pass filter: H(z) = 1 / (z - 0.2),
|
||||
system = TransferFunction(1, [1, -0.2], dt=0.1)
|
||||
w = [0.1, 1, 10]
|
||||
w, H = dfreqresp(system, w=w)
|
||||
|
||||
# test real
|
||||
expected_re = [1.2383, 0.4130, -0.7553]
|
||||
assert_almost_equal(H.real, expected_re, decimal=4)
|
||||
|
||||
# test imag
|
||||
expected_im = [-0.1555, -1.0214, 0.3955]
|
||||
assert_almost_equal(H.imag, expected_im, decimal=4)
|
||||
|
||||
def test_auto(self):
|
||||
# Test dfreqresp() real part calculation.
|
||||
# 1st order low-pass filter: H(z) = 1 / (z - 0.2),
|
||||
system = TransferFunction(1, [1, -0.2], dt=0.1)
|
||||
w = [0.1, 1, 10, 100]
|
||||
w, H = dfreqresp(system, w=w)
|
||||
jw = np.exp(w * 1j)
|
||||
y = np.polyval(system.num, jw) / np.polyval(system.den, jw)
|
||||
|
||||
# test real
|
||||
expected_re = y.real
|
||||
assert_almost_equal(H.real, expected_re)
|
||||
|
||||
# test imag
|
||||
expected_im = y.imag
|
||||
assert_almost_equal(H.imag, expected_im)
|
||||
|
||||
def test_freq_range(self):
|
||||
# Test that freqresp() finds a reasonable frequency range.
|
||||
# 1st order low-pass filter: H(z) = 1 / (z - 0.2),
|
||||
# Expected range is from 0.01 to 10.
|
||||
system = TransferFunction(1, [1, -0.2], dt=0.1)
|
||||
n = 10
|
||||
expected_w = np.linspace(0, np.pi, 10, endpoint=False)
|
||||
w, H = dfreqresp(system, n=n)
|
||||
assert_almost_equal(w, expected_w)
|
||||
|
||||
def test_pole_one(self):
|
||||
# Test that freqresp() doesn't fail on a system with a pole at 0.
|
||||
# integrator, pole at zero: H(s) = 1 / s
|
||||
system = TransferFunction([1], [1, -1], dt=0.1)
|
||||
|
||||
with suppress_warnings() as sup:
|
||||
sup.filter(RuntimeWarning, message="divide by zero")
|
||||
sup.filter(RuntimeWarning, message="invalid value encountered")
|
||||
w, H = dfreqresp(system, n=2)
|
||||
assert_equal(w[0], 0.) # a fail would give not-a-number
|
||||
|
||||
def test_error(self):
|
||||
# Raise an error for continuous-time systems
|
||||
system = lti([1], [1, 1])
|
||||
assert_raises(AttributeError, dfreqresp, system)
|
||||
|
||||
def test_from_state_space(self):
|
||||
# H(z) = 2 / z^3 - 0.5 * z^2
|
||||
|
||||
system_TF = dlti([2], [1, -0.5, 0, 0])
|
||||
|
||||
A = np.array([[0.5, 0, 0],
|
||||
[1, 0, 0],
|
||||
[0, 1, 0]])
|
||||
B = np.array([[1, 0, 0]]).T
|
||||
C = np.array([[0, 0, 2]])
|
||||
D = 0
|
||||
|
||||
system_SS = dlti(A, B, C, D)
|
||||
w = 10.0**np.arange(-3,0,.5)
|
||||
with suppress_warnings() as sup:
|
||||
sup.filter(BadCoefficients)
|
||||
w1, H1 = dfreqresp(system_TF, w=w)
|
||||
w2, H2 = dfreqresp(system_SS, w=w)
|
||||
|
||||
assert_almost_equal(H1, H2)
|
||||
|
||||
def test_from_zpk(self):
|
||||
# 1st order low-pass filter: H(s) = 0.3 / (z - 0.2),
|
||||
system_ZPK = dlti([],[0.2],0.3)
|
||||
system_TF = dlti(0.3, [1, -0.2])
|
||||
w = [0.1, 1, 10, 100]
|
||||
w1, H1 = dfreqresp(system_ZPK, w=w)
|
||||
w2, H2 = dfreqresp(system_TF, w=w)
|
||||
assert_almost_equal(H1, H2)
|
||||
|
||||
|
||||
class Test_bode:
|
||||
|
||||
def test_manual(self):
|
||||
# Test bode() magnitude calculation (manual sanity check).
|
||||
# 1st order low-pass filter: H(s) = 0.3 / (z - 0.2),
|
||||
dt = 0.1
|
||||
system = TransferFunction(0.3, [1, -0.2], dt=dt)
|
||||
w = [0.1, 0.5, 1, np.pi]
|
||||
w2, mag, phase = dbode(system, w=w)
|
||||
|
||||
# Test mag
|
||||
expected_mag = [-8.5329, -8.8396, -9.6162, -12.0412]
|
||||
assert_almost_equal(mag, expected_mag, decimal=4)
|
||||
|
||||
# Test phase
|
||||
expected_phase = [-7.1575, -35.2814, -67.9809, -180.0000]
|
||||
assert_almost_equal(phase, expected_phase, decimal=4)
|
||||
|
||||
# Test frequency
|
||||
assert_equal(np.array(w) / dt, w2)
|
||||
|
||||
def test_auto(self):
|
||||
# Test bode() magnitude calculation.
|
||||
# 1st order low-pass filter: H(s) = 0.3 / (z - 0.2),
|
||||
system = TransferFunction(0.3, [1, -0.2], dt=0.1)
|
||||
w = np.array([0.1, 0.5, 1, np.pi])
|
||||
w2, mag, phase = dbode(system, w=w)
|
||||
jw = np.exp(w * 1j)
|
||||
y = np.polyval(system.num, jw) / np.polyval(system.den, jw)
|
||||
|
||||
# Test mag
|
||||
expected_mag = 20.0 * np.log10(abs(y))
|
||||
assert_almost_equal(mag, expected_mag)
|
||||
|
||||
# Test phase
|
||||
expected_phase = np.rad2deg(np.angle(y))
|
||||
assert_almost_equal(phase, expected_phase)
|
||||
|
||||
def test_range(self):
|
||||
# Test that bode() finds a reasonable frequency range.
|
||||
# 1st order low-pass filter: H(s) = 0.3 / (z - 0.2),
|
||||
dt = 0.1
|
||||
system = TransferFunction(0.3, [1, -0.2], dt=0.1)
|
||||
n = 10
|
||||
# Expected range is from 0.01 to 10.
|
||||
expected_w = np.linspace(0, np.pi, n, endpoint=False) / dt
|
||||
w, mag, phase = dbode(system, n=n)
|
||||
assert_almost_equal(w, expected_w)
|
||||
|
||||
def test_pole_one(self):
|
||||
# Test that freqresp() doesn't fail on a system with a pole at 0.
|
||||
# integrator, pole at zero: H(s) = 1 / s
|
||||
system = TransferFunction([1], [1, -1], dt=0.1)
|
||||
|
||||
with suppress_warnings() as sup:
|
||||
sup.filter(RuntimeWarning, message="divide by zero")
|
||||
sup.filter(RuntimeWarning, message="invalid value encountered")
|
||||
w, mag, phase = dbode(system, n=2)
|
||||
assert_equal(w[0], 0.) # a fail would give not-a-number
|
||||
|
||||
def test_imaginary(self):
|
||||
# bode() should not fail on a system with pure imaginary poles.
|
||||
# The test passes if bode doesn't raise an exception.
|
||||
system = TransferFunction([1], [1, 0, 100], dt=0.1)
|
||||
dbode(system, n=2)
|
||||
|
||||
def test_error(self):
|
||||
# Raise an error for continuous-time systems
|
||||
system = lti([1], [1, 1])
|
||||
assert_raises(AttributeError, dbode, system)
|
||||
|
||||
|
||||
class TestTransferFunctionZConversion:
|
||||
"""Test private conversions between 'z' and 'z**-1' polynomials."""
|
||||
|
||||
def test_full(self):
|
||||
# Numerator and denominator same order
|
||||
num = [2, 3, 4]
|
||||
den = [5, 6, 7]
|
||||
num2, den2 = TransferFunction._z_to_zinv(num, den)
|
||||
assert_equal(num, num2)
|
||||
assert_equal(den, den2)
|
||||
|
||||
num2, den2 = TransferFunction._zinv_to_z(num, den)
|
||||
assert_equal(num, num2)
|
||||
assert_equal(den, den2)
|
||||
|
||||
def test_numerator(self):
|
||||
# Numerator lower order than denominator
|
||||
num = [2, 3]
|
||||
den = [5, 6, 7]
|
||||
num2, den2 = TransferFunction._z_to_zinv(num, den)
|
||||
assert_equal([0, 2, 3], num2)
|
||||
assert_equal(den, den2)
|
||||
|
||||
num2, den2 = TransferFunction._zinv_to_z(num, den)
|
||||
assert_equal([2, 3, 0], num2)
|
||||
assert_equal(den, den2)
|
||||
|
||||
def test_denominator(self):
|
||||
# Numerator higher order than denominator
|
||||
num = [2, 3, 4]
|
||||
den = [5, 6]
|
||||
num2, den2 = TransferFunction._z_to_zinv(num, den)
|
||||
assert_equal(num, num2)
|
||||
assert_equal([0, 5, 6], den2)
|
||||
|
||||
num2, den2 = TransferFunction._zinv_to_z(num, den)
|
||||
assert_equal(num, num2)
|
||||
assert_equal([5, 6, 0], den2)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,647 @@
|
||||
import numpy as np
|
||||
from numpy.testing import (assert_almost_equal, assert_array_almost_equal,
|
||||
assert_equal, assert_,
|
||||
assert_allclose, assert_warns)
|
||||
from pytest import raises as assert_raises
|
||||
import pytest
|
||||
|
||||
from scipy.fft import fft
|
||||
from scipy.special import sinc
|
||||
from scipy.signal import kaiser_beta, kaiser_atten, kaiserord, \
|
||||
firwin, firwin2, freqz, remez, firls, minimum_phase
|
||||
|
||||
|
||||
def test_kaiser_beta():
|
||||
b = kaiser_beta(58.7)
|
||||
assert_almost_equal(b, 0.1102 * 50.0)
|
||||
b = kaiser_beta(22.0)
|
||||
assert_almost_equal(b, 0.5842 + 0.07886)
|
||||
b = kaiser_beta(21.0)
|
||||
assert_equal(b, 0.0)
|
||||
b = kaiser_beta(10.0)
|
||||
assert_equal(b, 0.0)
|
||||
|
||||
|
||||
def test_kaiser_atten():
|
||||
a = kaiser_atten(1, 1.0)
|
||||
assert_equal(a, 7.95)
|
||||
a = kaiser_atten(2, 1/np.pi)
|
||||
assert_equal(a, 2.285 + 7.95)
|
||||
|
||||
|
||||
def test_kaiserord():
|
||||
assert_raises(ValueError, kaiserord, 1.0, 1.0)
|
||||
numtaps, beta = kaiserord(2.285 + 7.95 - 0.001, 1/np.pi)
|
||||
assert_equal((numtaps, beta), (2, 0.0))
|
||||
|
||||
|
||||
class TestFirwin:
|
||||
|
||||
def check_response(self, h, expected_response, tol=.05):
|
||||
N = len(h)
|
||||
alpha = 0.5 * (N-1)
|
||||
m = np.arange(0,N) - alpha # time indices of taps
|
||||
for freq, expected in expected_response:
|
||||
actual = abs(np.sum(h*np.exp(-1.j*np.pi*m*freq)))
|
||||
mse = abs(actual-expected)**2
|
||||
assert_(mse < tol, f'response not as expected, mse={mse:g} > {tol:g}')
|
||||
|
||||
def test_response(self):
|
||||
N = 51
|
||||
f = .5
|
||||
# increase length just to try even/odd
|
||||
h = firwin(N, f) # low-pass from 0 to f
|
||||
self.check_response(h, [(.25,1), (.75,0)])
|
||||
|
||||
h = firwin(N+1, f, window='nuttall') # specific window
|
||||
self.check_response(h, [(.25,1), (.75,0)])
|
||||
|
||||
h = firwin(N+2, f, pass_zero=False) # stop from 0 to f --> high-pass
|
||||
self.check_response(h, [(.25,0), (.75,1)])
|
||||
|
||||
f1, f2, f3, f4 = .2, .4, .6, .8
|
||||
h = firwin(N+3, [f1, f2], pass_zero=False) # band-pass filter
|
||||
self.check_response(h, [(.1,0), (.3,1), (.5,0)])
|
||||
|
||||
h = firwin(N+4, [f1, f2]) # band-stop filter
|
||||
self.check_response(h, [(.1,1), (.3,0), (.5,1)])
|
||||
|
||||
h = firwin(N+5, [f1, f2, f3, f4], pass_zero=False, scale=False)
|
||||
self.check_response(h, [(.1,0), (.3,1), (.5,0), (.7,1), (.9,0)])
|
||||
|
||||
h = firwin(N+6, [f1, f2, f3, f4]) # multiband filter
|
||||
self.check_response(h, [(.1,1), (.3,0), (.5,1), (.7,0), (.9,1)])
|
||||
|
||||
h = firwin(N+7, 0.1, width=.03) # low-pass
|
||||
self.check_response(h, [(.05,1), (.75,0)])
|
||||
|
||||
h = firwin(N+8, 0.1, pass_zero=False) # high-pass
|
||||
self.check_response(h, [(.05,0), (.75,1)])
|
||||
|
||||
def mse(self, h, bands):
|
||||
"""Compute mean squared error versus ideal response across frequency
|
||||
band.
|
||||
h -- coefficients
|
||||
bands -- list of (left, right) tuples relative to 1==Nyquist of
|
||||
passbands
|
||||
"""
|
||||
w, H = freqz(h, worN=1024)
|
||||
f = w/np.pi
|
||||
passIndicator = np.zeros(len(w), bool)
|
||||
for left, right in bands:
|
||||
passIndicator |= (f >= left) & (f < right)
|
||||
Hideal = np.where(passIndicator, 1, 0)
|
||||
mse = np.mean(abs(abs(H)-Hideal)**2)
|
||||
return mse
|
||||
|
||||
def test_scaling(self):
|
||||
"""
|
||||
For one lowpass, bandpass, and highpass example filter, this test
|
||||
checks two things:
|
||||
- the mean squared error over the frequency domain of the unscaled
|
||||
filter is smaller than the scaled filter (true for rectangular
|
||||
window)
|
||||
- the response of the scaled filter is exactly unity at the center
|
||||
of the first passband
|
||||
"""
|
||||
N = 11
|
||||
cases = [
|
||||
([.5], True, (0, 1)),
|
||||
([0.2, .6], False, (.4, 1)),
|
||||
([.5], False, (1, 1)),
|
||||
]
|
||||
for cutoff, pass_zero, expected_response in cases:
|
||||
h = firwin(N, cutoff, scale=False, pass_zero=pass_zero, window='ones')
|
||||
hs = firwin(N, cutoff, scale=True, pass_zero=pass_zero, window='ones')
|
||||
if len(cutoff) == 1:
|
||||
if pass_zero:
|
||||
cutoff = [0] + cutoff
|
||||
else:
|
||||
cutoff = cutoff + [1]
|
||||
assert_(self.mse(h, [cutoff]) < self.mse(hs, [cutoff]),
|
||||
'least squares violation')
|
||||
self.check_response(hs, [expected_response], 1e-12)
|
||||
|
||||
def test_fs_validation(self):
|
||||
with pytest.raises(ValueError, match="Sampling.*single scalar"):
|
||||
firwin(51, .5, fs=np.array([10, 20]))
|
||||
|
||||
|
||||
class TestFirWinMore:
|
||||
"""Different author, different style, different tests..."""
|
||||
|
||||
def test_lowpass(self):
|
||||
width = 0.04
|
||||
ntaps, beta = kaiserord(120, width)
|
||||
kwargs = dict(cutoff=0.5, window=('kaiser', beta), scale=False)
|
||||
taps = firwin(ntaps, **kwargs)
|
||||
|
||||
# Check the symmetry of taps.
|
||||
assert_array_almost_equal(taps[:ntaps//2], taps[ntaps:ntaps-ntaps//2-1:-1])
|
||||
|
||||
# Check the gain at a few samples where
|
||||
# we know it should be approximately 0 or 1.
|
||||
freq_samples = np.array([0.0, 0.25, 0.5-width/2, 0.5+width/2, 0.75, 1.0])
|
||||
freqs, response = freqz(taps, worN=np.pi*freq_samples)
|
||||
assert_array_almost_equal(np.abs(response),
|
||||
[1.0, 1.0, 1.0, 0.0, 0.0, 0.0], decimal=5)
|
||||
|
||||
taps_str = firwin(ntaps, pass_zero='lowpass', **kwargs)
|
||||
assert_allclose(taps, taps_str)
|
||||
|
||||
def test_highpass(self):
|
||||
width = 0.04
|
||||
ntaps, beta = kaiserord(120, width)
|
||||
|
||||
# Ensure that ntaps is odd.
|
||||
ntaps |= 1
|
||||
|
||||
kwargs = dict(cutoff=0.5, window=('kaiser', beta), scale=False)
|
||||
taps = firwin(ntaps, pass_zero=False, **kwargs)
|
||||
|
||||
# Check the symmetry of taps.
|
||||
assert_array_almost_equal(taps[:ntaps//2], taps[ntaps:ntaps-ntaps//2-1:-1])
|
||||
|
||||
# Check the gain at a few samples where
|
||||
# we know it should be approximately 0 or 1.
|
||||
freq_samples = np.array([0.0, 0.25, 0.5-width/2, 0.5+width/2, 0.75, 1.0])
|
||||
freqs, response = freqz(taps, worN=np.pi*freq_samples)
|
||||
assert_array_almost_equal(np.abs(response),
|
||||
[0.0, 0.0, 0.0, 1.0, 1.0, 1.0], decimal=5)
|
||||
|
||||
taps_str = firwin(ntaps, pass_zero='highpass', **kwargs)
|
||||
assert_allclose(taps, taps_str)
|
||||
|
||||
def test_bandpass(self):
|
||||
width = 0.04
|
||||
ntaps, beta = kaiserord(120, width)
|
||||
kwargs = dict(cutoff=[0.3, 0.7], window=('kaiser', beta), scale=False)
|
||||
taps = firwin(ntaps, pass_zero=False, **kwargs)
|
||||
|
||||
# Check the symmetry of taps.
|
||||
assert_array_almost_equal(taps[:ntaps//2], taps[ntaps:ntaps-ntaps//2-1:-1])
|
||||
|
||||
# Check the gain at a few samples where
|
||||
# we know it should be approximately 0 or 1.
|
||||
freq_samples = np.array([0.0, 0.2, 0.3-width/2, 0.3+width/2, 0.5,
|
||||
0.7-width/2, 0.7+width/2, 0.8, 1.0])
|
||||
freqs, response = freqz(taps, worN=np.pi*freq_samples)
|
||||
assert_array_almost_equal(np.abs(response),
|
||||
[0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0], decimal=5)
|
||||
|
||||
taps_str = firwin(ntaps, pass_zero='bandpass', **kwargs)
|
||||
assert_allclose(taps, taps_str)
|
||||
|
||||
def test_bandstop_multi(self):
|
||||
width = 0.04
|
||||
ntaps, beta = kaiserord(120, width)
|
||||
kwargs = dict(cutoff=[0.2, 0.5, 0.8], window=('kaiser', beta),
|
||||
scale=False)
|
||||
taps = firwin(ntaps, **kwargs)
|
||||
|
||||
# Check the symmetry of taps.
|
||||
assert_array_almost_equal(taps[:ntaps//2], taps[ntaps:ntaps-ntaps//2-1:-1])
|
||||
|
||||
# Check the gain at a few samples where
|
||||
# we know it should be approximately 0 or 1.
|
||||
freq_samples = np.array([0.0, 0.1, 0.2-width/2, 0.2+width/2, 0.35,
|
||||
0.5-width/2, 0.5+width/2, 0.65,
|
||||
0.8-width/2, 0.8+width/2, 0.9, 1.0])
|
||||
freqs, response = freqz(taps, worN=np.pi*freq_samples)
|
||||
assert_array_almost_equal(np.abs(response),
|
||||
[1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0],
|
||||
decimal=5)
|
||||
|
||||
taps_str = firwin(ntaps, pass_zero='bandstop', **kwargs)
|
||||
assert_allclose(taps, taps_str)
|
||||
|
||||
def test_fs_nyq(self):
|
||||
"""Test the fs and nyq keywords."""
|
||||
nyquist = 1000
|
||||
width = 40.0
|
||||
relative_width = width/nyquist
|
||||
ntaps, beta = kaiserord(120, relative_width)
|
||||
taps = firwin(ntaps, cutoff=[300, 700], window=('kaiser', beta),
|
||||
pass_zero=False, scale=False, fs=2*nyquist)
|
||||
|
||||
# Check the symmetry of taps.
|
||||
assert_array_almost_equal(taps[:ntaps//2], taps[ntaps:ntaps-ntaps//2-1:-1])
|
||||
|
||||
# Check the gain at a few samples where
|
||||
# we know it should be approximately 0 or 1.
|
||||
freq_samples = np.array([0.0, 200, 300-width/2, 300+width/2, 500,
|
||||
700-width/2, 700+width/2, 800, 1000])
|
||||
freqs, response = freqz(taps, worN=np.pi*freq_samples/nyquist)
|
||||
assert_array_almost_equal(np.abs(response),
|
||||
[0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0], decimal=5)
|
||||
|
||||
def test_bad_cutoff(self):
|
||||
"""Test that invalid cutoff argument raises ValueError."""
|
||||
# cutoff values must be greater than 0 and less than 1.
|
||||
assert_raises(ValueError, firwin, 99, -0.5)
|
||||
assert_raises(ValueError, firwin, 99, 1.5)
|
||||
# Don't allow 0 or 1 in cutoff.
|
||||
assert_raises(ValueError, firwin, 99, [0, 0.5])
|
||||
assert_raises(ValueError, firwin, 99, [0.5, 1])
|
||||
# cutoff values must be strictly increasing.
|
||||
assert_raises(ValueError, firwin, 99, [0.1, 0.5, 0.2])
|
||||
assert_raises(ValueError, firwin, 99, [0.1, 0.5, 0.5])
|
||||
# Must have at least one cutoff value.
|
||||
assert_raises(ValueError, firwin, 99, [])
|
||||
# 2D array not allowed.
|
||||
assert_raises(ValueError, firwin, 99, [[0.1, 0.2],[0.3, 0.4]])
|
||||
# cutoff values must be less than nyq.
|
||||
assert_raises(ValueError, firwin, 99, 50.0, fs=80)
|
||||
assert_raises(ValueError, firwin, 99, [10, 20, 30], fs=50)
|
||||
|
||||
def test_even_highpass_raises_value_error(self):
|
||||
"""Test that attempt to create a highpass filter with an even number
|
||||
of taps raises a ValueError exception."""
|
||||
assert_raises(ValueError, firwin, 40, 0.5, pass_zero=False)
|
||||
assert_raises(ValueError, firwin, 40, [.25, 0.5])
|
||||
|
||||
def test_bad_pass_zero(self):
|
||||
"""Test degenerate pass_zero cases."""
|
||||
with assert_raises(ValueError, match='pass_zero must be'):
|
||||
firwin(41, 0.5, pass_zero='foo')
|
||||
with assert_raises(TypeError, match='cannot be interpreted'):
|
||||
firwin(41, 0.5, pass_zero=1.)
|
||||
for pass_zero in ('lowpass', 'highpass'):
|
||||
with assert_raises(ValueError, match='cutoff must have one'):
|
||||
firwin(41, [0.5, 0.6], pass_zero=pass_zero)
|
||||
for pass_zero in ('bandpass', 'bandstop'):
|
||||
with assert_raises(ValueError, match='must have at least two'):
|
||||
firwin(41, [0.5], pass_zero=pass_zero)
|
||||
|
||||
def test_fs_validation(self):
|
||||
with pytest.raises(ValueError, match="Sampling.*single scalar"):
|
||||
firwin2(51, .5, 1, fs=np.array([10, 20]))
|
||||
|
||||
|
||||
class TestFirwin2:
|
||||
|
||||
def test_invalid_args(self):
|
||||
# `freq` and `gain` have different lengths.
|
||||
with assert_raises(ValueError, match='must be of same length'):
|
||||
firwin2(50, [0, 0.5, 1], [0.0, 1.0])
|
||||
# `nfreqs` is less than `ntaps`.
|
||||
with assert_raises(ValueError, match='ntaps must be less than nfreqs'):
|
||||
firwin2(50, [0, 0.5, 1], [0.0, 1.0, 1.0], nfreqs=33)
|
||||
# Decreasing value in `freq`
|
||||
with assert_raises(ValueError, match='must be nondecreasing'):
|
||||
firwin2(50, [0, 0.5, 0.4, 1.0], [0, .25, .5, 1.0])
|
||||
# Value in `freq` repeated more than once.
|
||||
with assert_raises(ValueError, match='must not occur more than twice'):
|
||||
firwin2(50, [0, .1, .1, .1, 1.0], [0.0, 0.5, 0.75, 1.0, 1.0])
|
||||
# `freq` does not start at 0.0.
|
||||
with assert_raises(ValueError, match='start with 0'):
|
||||
firwin2(50, [0.5, 1.0], [0.0, 1.0])
|
||||
# `freq` does not end at fs/2.
|
||||
with assert_raises(ValueError, match='end with fs/2'):
|
||||
firwin2(50, [0.0, 0.5], [0.0, 1.0])
|
||||
# Value 0 is repeated in `freq`
|
||||
with assert_raises(ValueError, match='0 must not be repeated'):
|
||||
firwin2(50, [0.0, 0.0, 0.5, 1.0], [1.0, 1.0, 0.0, 0.0])
|
||||
# Value fs/2 is repeated in `freq`
|
||||
with assert_raises(ValueError, match='fs/2 must not be repeated'):
|
||||
firwin2(50, [0.0, 0.5, 1.0, 1.0], [1.0, 1.0, 0.0, 0.0])
|
||||
# Value in `freq` that is too close to a repeated number
|
||||
with assert_raises(ValueError, match='cannot contain numbers '
|
||||
'that are too close'):
|
||||
firwin2(50, [0.0, 0.5 - np.finfo(float).eps * 0.5, 0.5, 0.5, 1.0],
|
||||
[1.0, 1.0, 1.0, 0.0, 0.0])
|
||||
|
||||
# Type II filter, but the gain at nyquist frequency is not zero.
|
||||
with assert_raises(ValueError, match='Type II filter'):
|
||||
firwin2(16, [0.0, 0.5, 1.0], [0.0, 1.0, 1.0])
|
||||
|
||||
# Type III filter, but the gains at nyquist and zero rate are not zero.
|
||||
with assert_raises(ValueError, match='Type III filter'):
|
||||
firwin2(17, [0.0, 0.5, 1.0], [0.0, 1.0, 1.0], antisymmetric=True)
|
||||
with assert_raises(ValueError, match='Type III filter'):
|
||||
firwin2(17, [0.0, 0.5, 1.0], [1.0, 1.0, 0.0], antisymmetric=True)
|
||||
with assert_raises(ValueError, match='Type III filter'):
|
||||
firwin2(17, [0.0, 0.5, 1.0], [1.0, 1.0, 1.0], antisymmetric=True)
|
||||
|
||||
# Type IV filter, but the gain at zero rate is not zero.
|
||||
with assert_raises(ValueError, match='Type IV filter'):
|
||||
firwin2(16, [0.0, 0.5, 1.0], [1.0, 1.0, 0.0], antisymmetric=True)
|
||||
|
||||
def test01(self):
|
||||
width = 0.04
|
||||
beta = 12.0
|
||||
ntaps = 400
|
||||
# Filter is 1 from w=0 to w=0.5, then decreases linearly from 1 to 0 as w
|
||||
# increases from w=0.5 to w=1 (w=1 is the Nyquist frequency).
|
||||
freq = [0.0, 0.5, 1.0]
|
||||
gain = [1.0, 1.0, 0.0]
|
||||
taps = firwin2(ntaps, freq, gain, window=('kaiser', beta))
|
||||
freq_samples = np.array([0.0, 0.25, 0.5-width/2, 0.5+width/2,
|
||||
0.75, 1.0-width/2])
|
||||
freqs, response = freqz(taps, worN=np.pi*freq_samples)
|
||||
assert_array_almost_equal(np.abs(response),
|
||||
[1.0, 1.0, 1.0, 1.0-width, 0.5, width], decimal=5)
|
||||
|
||||
def test02(self):
|
||||
width = 0.04
|
||||
beta = 12.0
|
||||
# ntaps must be odd for positive gain at Nyquist.
|
||||
ntaps = 401
|
||||
# An ideal highpass filter.
|
||||
freq = [0.0, 0.5, 0.5, 1.0]
|
||||
gain = [0.0, 0.0, 1.0, 1.0]
|
||||
taps = firwin2(ntaps, freq, gain, window=('kaiser', beta))
|
||||
freq_samples = np.array([0.0, 0.25, 0.5-width, 0.5+width, 0.75, 1.0])
|
||||
freqs, response = freqz(taps, worN=np.pi*freq_samples)
|
||||
assert_array_almost_equal(np.abs(response),
|
||||
[0.0, 0.0, 0.0, 1.0, 1.0, 1.0], decimal=5)
|
||||
|
||||
def test03(self):
|
||||
width = 0.02
|
||||
ntaps, beta = kaiserord(120, width)
|
||||
# ntaps must be odd for positive gain at Nyquist.
|
||||
ntaps = int(ntaps) | 1
|
||||
freq = [0.0, 0.4, 0.4, 0.5, 0.5, 1.0]
|
||||
gain = [1.0, 1.0, 0.0, 0.0, 1.0, 1.0]
|
||||
taps = firwin2(ntaps, freq, gain, window=('kaiser', beta))
|
||||
freq_samples = np.array([0.0, 0.4-width, 0.4+width, 0.45,
|
||||
0.5-width, 0.5+width, 0.75, 1.0])
|
||||
freqs, response = freqz(taps, worN=np.pi*freq_samples)
|
||||
assert_array_almost_equal(np.abs(response),
|
||||
[1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0], decimal=5)
|
||||
|
||||
def test04(self):
|
||||
"""Test firwin2 when window=None."""
|
||||
ntaps = 5
|
||||
# Ideal lowpass: gain is 1 on [0,0.5], and 0 on [0.5, 1.0]
|
||||
freq = [0.0, 0.5, 0.5, 1.0]
|
||||
gain = [1.0, 1.0, 0.0, 0.0]
|
||||
taps = firwin2(ntaps, freq, gain, window=None, nfreqs=8193)
|
||||
alpha = 0.5 * (ntaps - 1)
|
||||
m = np.arange(0, ntaps) - alpha
|
||||
h = 0.5 * sinc(0.5 * m)
|
||||
assert_array_almost_equal(h, taps)
|
||||
|
||||
def test05(self):
|
||||
"""Test firwin2 for calculating Type IV filters"""
|
||||
ntaps = 1500
|
||||
|
||||
freq = [0.0, 1.0]
|
||||
gain = [0.0, 1.0]
|
||||
taps = firwin2(ntaps, freq, gain, window=None, antisymmetric=True)
|
||||
assert_array_almost_equal(taps[: ntaps // 2], -taps[ntaps // 2:][::-1])
|
||||
|
||||
freqs, response = freqz(taps, worN=2048)
|
||||
assert_array_almost_equal(abs(response), freqs / np.pi, decimal=4)
|
||||
|
||||
def test06(self):
|
||||
"""Test firwin2 for calculating Type III filters"""
|
||||
ntaps = 1501
|
||||
|
||||
freq = [0.0, 0.5, 0.55, 1.0]
|
||||
gain = [0.0, 0.5, 0.0, 0.0]
|
||||
taps = firwin2(ntaps, freq, gain, window=None, antisymmetric=True)
|
||||
assert_equal(taps[ntaps // 2], 0.0)
|
||||
assert_array_almost_equal(taps[: ntaps // 2], -taps[ntaps // 2 + 1:][::-1])
|
||||
|
||||
freqs, response1 = freqz(taps, worN=2048)
|
||||
response2 = np.interp(freqs / np.pi, freq, gain)
|
||||
assert_array_almost_equal(abs(response1), response2, decimal=3)
|
||||
|
||||
def test_fs_nyq(self):
|
||||
taps1 = firwin2(80, [0.0, 0.5, 1.0], [1.0, 1.0, 0.0])
|
||||
taps2 = firwin2(80, [0.0, 30.0, 60.0], [1.0, 1.0, 0.0], fs=120.0)
|
||||
assert_array_almost_equal(taps1, taps2)
|
||||
|
||||
def test_tuple(self):
|
||||
taps1 = firwin2(150, (0.0, 0.5, 0.5, 1.0), (1.0, 1.0, 0.0, 0.0))
|
||||
taps2 = firwin2(150, [0.0, 0.5, 0.5, 1.0], [1.0, 1.0, 0.0, 0.0])
|
||||
assert_array_almost_equal(taps1, taps2)
|
||||
|
||||
def test_input_modyfication(self):
|
||||
freq1 = np.array([0.0, 0.5, 0.5, 1.0])
|
||||
freq2 = np.array(freq1)
|
||||
firwin2(80, freq1, [1.0, 1.0, 0.0, 0.0])
|
||||
assert_equal(freq1, freq2)
|
||||
|
||||
|
||||
class TestRemez:
|
||||
|
||||
def test_bad_args(self):
|
||||
assert_raises(ValueError, remez, 11, [0.1, 0.4], [1], type='pooka')
|
||||
|
||||
def test_hilbert(self):
|
||||
N = 11 # number of taps in the filter
|
||||
a = 0.1 # width of the transition band
|
||||
|
||||
# design an unity gain hilbert bandpass filter from w to 0.5-w
|
||||
h = remez(11, [a, 0.5-a], [1], type='hilbert')
|
||||
|
||||
# make sure the filter has correct # of taps
|
||||
assert_(len(h) == N, "Number of Taps")
|
||||
|
||||
# make sure it is type III (anti-symmetric tap coefficients)
|
||||
assert_array_almost_equal(h[:(N-1)//2], -h[:-(N-1)//2-1:-1])
|
||||
|
||||
# Since the requested response is symmetric, all even coefficients
|
||||
# should be zero (or in this case really small)
|
||||
assert_((abs(h[1::2]) < 1e-15).all(), "Even Coefficients Equal Zero")
|
||||
|
||||
# now check the frequency response
|
||||
w, H = freqz(h, 1)
|
||||
f = w/2/np.pi
|
||||
Hmag = abs(H)
|
||||
|
||||
# should have a zero at 0 and pi (in this case close to zero)
|
||||
assert_((Hmag[[0, -1]] < 0.02).all(), "Zero at zero and pi")
|
||||
|
||||
# check that the pass band is close to unity
|
||||
idx = np.logical_and(f > a, f < 0.5-a)
|
||||
assert_((abs(Hmag[idx] - 1) < 0.015).all(), "Pass Band Close To Unity")
|
||||
|
||||
def test_compare(self):
|
||||
# test comparison to MATLAB
|
||||
k = [0.024590270518440, -0.041314581814658, -0.075943803756711,
|
||||
-0.003530911231040, 0.193140296954975, 0.373400753484939,
|
||||
0.373400753484939, 0.193140296954975, -0.003530911231040,
|
||||
-0.075943803756711, -0.041314581814658, 0.024590270518440]
|
||||
h = remez(12, [0, 0.3, 0.5, 1], [1, 0], fs=2.)
|
||||
assert_allclose(h, k)
|
||||
|
||||
h = [-0.038976016082299, 0.018704846485491, -0.014644062687875,
|
||||
0.002879152556419, 0.016849978528150, -0.043276706138248,
|
||||
0.073641298245579, -0.103908158578635, 0.129770906801075,
|
||||
-0.147163447297124, 0.153302248456347, -0.147163447297124,
|
||||
0.129770906801075, -0.103908158578635, 0.073641298245579,
|
||||
-0.043276706138248, 0.016849978528150, 0.002879152556419,
|
||||
-0.014644062687875, 0.018704846485491, -0.038976016082299]
|
||||
assert_allclose(remez(21, [0, 0.8, 0.9, 1], [0, 1], fs=2.), h)
|
||||
|
||||
def test_fs_validation(self):
|
||||
with pytest.raises(ValueError, match="Sampling.*single scalar"):
|
||||
remez(11, .1, 1, fs=np.array([10, 20]))
|
||||
|
||||
class TestFirls:
|
||||
|
||||
def test_bad_args(self):
|
||||
# even numtaps
|
||||
assert_raises(ValueError, firls, 10, [0.1, 0.2], [0, 0])
|
||||
# odd bands
|
||||
assert_raises(ValueError, firls, 11, [0.1, 0.2, 0.4], [0, 0, 0])
|
||||
# len(bands) != len(desired)
|
||||
assert_raises(ValueError, firls, 11, [0.1, 0.2, 0.3, 0.4], [0, 0, 0])
|
||||
# non-monotonic bands
|
||||
assert_raises(ValueError, firls, 11, [0.2, 0.1], [0, 0])
|
||||
assert_raises(ValueError, firls, 11, [0.1, 0.2, 0.3, 0.3], [0] * 4)
|
||||
assert_raises(ValueError, firls, 11, [0.3, 0.4, 0.1, 0.2], [0] * 4)
|
||||
assert_raises(ValueError, firls, 11, [0.1, 0.3, 0.2, 0.4], [0] * 4)
|
||||
# negative desired
|
||||
assert_raises(ValueError, firls, 11, [0.1, 0.2], [-1, 1])
|
||||
# len(weight) != len(pairs)
|
||||
assert_raises(ValueError, firls, 11, [0.1, 0.2], [0, 0], weight=[1, 2])
|
||||
# negative weight
|
||||
assert_raises(ValueError, firls, 11, [0.1, 0.2], [0, 0], weight=[-1])
|
||||
|
||||
def test_firls(self):
|
||||
N = 11 # number of taps in the filter
|
||||
a = 0.1 # width of the transition band
|
||||
|
||||
# design a halfband symmetric low-pass filter
|
||||
h = firls(11, [0, a, 0.5-a, 0.5], [1, 1, 0, 0], fs=1.0)
|
||||
|
||||
# make sure the filter has correct # of taps
|
||||
assert_equal(len(h), N)
|
||||
|
||||
# make sure it is symmetric
|
||||
midx = (N-1) // 2
|
||||
assert_array_almost_equal(h[:midx], h[:-midx-1:-1])
|
||||
|
||||
# make sure the center tap is 0.5
|
||||
assert_almost_equal(h[midx], 0.5)
|
||||
|
||||
# For halfband symmetric, odd coefficients (except the center)
|
||||
# should be zero (really small)
|
||||
hodd = np.hstack((h[1:midx:2], h[-midx+1::2]))
|
||||
assert_array_almost_equal(hodd, 0)
|
||||
|
||||
# now check the frequency response
|
||||
w, H = freqz(h, 1)
|
||||
f = w/2/np.pi
|
||||
Hmag = np.abs(H)
|
||||
|
||||
# check that the pass band is close to unity
|
||||
idx = np.logical_and(f > 0, f < a)
|
||||
assert_array_almost_equal(Hmag[idx], 1, decimal=3)
|
||||
|
||||
# check that the stop band is close to zero
|
||||
idx = np.logical_and(f > 0.5-a, f < 0.5)
|
||||
assert_array_almost_equal(Hmag[idx], 0, decimal=3)
|
||||
|
||||
def test_compare(self):
|
||||
# compare to OCTAVE output
|
||||
taps = firls(9, [0, 0.5, 0.55, 1], [1, 1, 0, 0], weight=[1, 2])
|
||||
# >> taps = firls(8, [0 0.5 0.55 1], [1 1 0 0], [1, 2]);
|
||||
known_taps = [-6.26930101730182e-04, -1.03354450635036e-01,
|
||||
-9.81576747564301e-03, 3.17271686090449e-01,
|
||||
5.11409425599933e-01, 3.17271686090449e-01,
|
||||
-9.81576747564301e-03, -1.03354450635036e-01,
|
||||
-6.26930101730182e-04]
|
||||
assert_allclose(taps, known_taps)
|
||||
|
||||
# compare to MATLAB output
|
||||
taps = firls(11, [0, 0.5, 0.5, 1], [1, 1, 0, 0], weight=[1, 2])
|
||||
# >> taps = firls(10, [0 0.5 0.5 1], [1 1 0 0], [1, 2]);
|
||||
known_taps = [
|
||||
0.058545300496815, -0.014233383714318, -0.104688258464392,
|
||||
0.012403323025279, 0.317930861136062, 0.488047220029700,
|
||||
0.317930861136062, 0.012403323025279, -0.104688258464392,
|
||||
-0.014233383714318, 0.058545300496815]
|
||||
assert_allclose(taps, known_taps)
|
||||
|
||||
# With linear changes:
|
||||
taps = firls(7, (0, 1, 2, 3, 4, 5), [1, 0, 0, 1, 1, 0], fs=20)
|
||||
# >> taps = firls(6, [0, 0.1, 0.2, 0.3, 0.4, 0.5], [1, 0, 0, 1, 1, 0])
|
||||
known_taps = [
|
||||
1.156090832768218, -4.1385894727395849, 7.5288619164321826,
|
||||
-8.5530572592947856, 7.5288619164321826, -4.1385894727395849,
|
||||
1.156090832768218]
|
||||
assert_allclose(taps, known_taps)
|
||||
|
||||
def test_rank_deficient(self):
|
||||
# solve() runs but warns (only sometimes, so here we don't use match)
|
||||
x = firls(21, [0, 0.1, 0.9, 1], [1, 1, 0, 0])
|
||||
w, h = freqz(x, fs=2.)
|
||||
assert_allclose(np.abs(h[:2]), 1., atol=1e-5)
|
||||
assert_allclose(np.abs(h[-2:]), 0., atol=1e-6)
|
||||
# switch to pinvh (tolerances could be higher with longer
|
||||
# filters, but using shorter ones is faster computationally and
|
||||
# the idea is the same)
|
||||
x = firls(101, [0, 0.01, 0.99, 1], [1, 1, 0, 0])
|
||||
w, h = freqz(x, fs=2.)
|
||||
mask = w < 0.01
|
||||
assert mask.sum() > 3
|
||||
assert_allclose(np.abs(h[mask]), 1., atol=1e-4)
|
||||
mask = w > 0.99
|
||||
assert mask.sum() > 3
|
||||
assert_allclose(np.abs(h[mask]), 0., atol=1e-4)
|
||||
|
||||
def test_fs_validation(self):
|
||||
with pytest.raises(ValueError, match="Sampling.*single scalar"):
|
||||
firls(11, .1, 1, fs=np.array([10, 20]))
|
||||
|
||||
class TestMinimumPhase:
|
||||
|
||||
def test_bad_args(self):
|
||||
# not enough taps
|
||||
assert_raises(ValueError, minimum_phase, [1.])
|
||||
assert_raises(ValueError, minimum_phase, [1., 1.])
|
||||
assert_raises(ValueError, minimum_phase, np.full(10, 1j))
|
||||
assert_raises(ValueError, minimum_phase, 'foo')
|
||||
assert_raises(ValueError, minimum_phase, np.ones(10), n_fft=8)
|
||||
assert_raises(ValueError, minimum_phase, np.ones(10), method='foo')
|
||||
assert_warns(RuntimeWarning, minimum_phase, np.arange(3))
|
||||
with pytest.raises(ValueError, match="is only supported when"):
|
||||
minimum_phase(np.ones(3), method='hilbert', half=False)
|
||||
|
||||
def test_homomorphic(self):
|
||||
# check that it can recover frequency responses of arbitrary
|
||||
# linear-phase filters
|
||||
|
||||
# for some cases we can get the actual filter back
|
||||
h = [1, -1]
|
||||
h_new = minimum_phase(np.convolve(h, h[::-1]))
|
||||
assert_allclose(h_new, h, rtol=0.05)
|
||||
|
||||
# but in general we only guarantee we get the magnitude back
|
||||
rng = np.random.RandomState(0)
|
||||
for n in (2, 3, 10, 11, 15, 16, 17, 20, 21, 100, 101):
|
||||
h = rng.randn(n)
|
||||
h_linear = np.convolve(h, h[::-1])
|
||||
h_new = minimum_phase(h_linear)
|
||||
assert_allclose(np.abs(fft(h_new)), np.abs(fft(h)), rtol=1e-4)
|
||||
h_new = minimum_phase(h_linear, half=False)
|
||||
assert len(h_linear) == len(h_new)
|
||||
assert_allclose(np.abs(fft(h_new)), np.abs(fft(h_linear)), rtol=1e-4)
|
||||
|
||||
def test_hilbert(self):
|
||||
# compare to MATLAB output of reference implementation
|
||||
|
||||
# f=[0 0.3 0.5 1];
|
||||
# a=[1 1 0 0];
|
||||
# h=remez(11,f,a);
|
||||
h = remez(12, [0, 0.3, 0.5, 1], [1, 0], fs=2.)
|
||||
k = [0.349585548646686, 0.373552164395447, 0.326082685363438,
|
||||
0.077152207480935, -0.129943946349364, -0.059355880509749]
|
||||
m = minimum_phase(h, 'hilbert')
|
||||
assert_allclose(m, k, rtol=5e-3)
|
||||
|
||||
# f=[0 0.8 0.9 1];
|
||||
# a=[0 0 1 1];
|
||||
# h=remez(20,f,a);
|
||||
h = remez(21, [0, 0.8, 0.9, 1], [0, 1], fs=2.)
|
||||
k = [0.232486803906329, -0.133551833687071, 0.151871456867244,
|
||||
-0.157957283165866, 0.151739294892963, -0.129293146705090,
|
||||
0.100787844523204, -0.065832656741252, 0.035361328741024,
|
||||
-0.014977068692269, -0.158416139047557]
|
||||
m = minimum_phase(h, 'hilbert', n_fft=2**19)
|
||||
assert_allclose(m, k, rtol=2e-3)
|
||||
1221
venv/lib/python3.12/site-packages/scipy/signal/tests/test_ltisys.py
Normal file
1221
venv/lib/python3.12/site-packages/scipy/signal/tests/test_ltisys.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,65 @@
|
||||
import numpy as np
|
||||
from numpy.testing import assert_allclose, assert_array_equal
|
||||
from pytest import raises as assert_raises
|
||||
|
||||
from numpy.fft import fft, ifft
|
||||
|
||||
from scipy.signal import max_len_seq
|
||||
|
||||
|
||||
class TestMLS:
|
||||
|
||||
def test_mls_inputs(self):
|
||||
# can't all be zero state
|
||||
assert_raises(ValueError, max_len_seq,
|
||||
10, state=np.zeros(10))
|
||||
# wrong size state
|
||||
assert_raises(ValueError, max_len_seq, 10,
|
||||
state=np.ones(3))
|
||||
# wrong length
|
||||
assert_raises(ValueError, max_len_seq, 10, length=-1)
|
||||
assert_array_equal(max_len_seq(10, length=0)[0], [])
|
||||
# unknown taps
|
||||
assert_raises(ValueError, max_len_seq, 64)
|
||||
# bad taps
|
||||
assert_raises(ValueError, max_len_seq, 10, taps=[-1, 1])
|
||||
|
||||
def test_mls_output(self):
|
||||
# define some alternate working taps
|
||||
alt_taps = {2: [1], 3: [2], 4: [3], 5: [4, 3, 2], 6: [5, 4, 1], 7: [4],
|
||||
8: [7, 5, 3]}
|
||||
# assume the other bit levels work, too slow to test higher orders...
|
||||
for nbits in range(2, 8):
|
||||
for state in [None, np.round(np.random.rand(nbits))]:
|
||||
for taps in [None, alt_taps[nbits]]:
|
||||
if state is not None and np.all(state == 0):
|
||||
state[0] = 1 # they can't all be zero
|
||||
orig_m = max_len_seq(nbits, state=state,
|
||||
taps=taps)[0]
|
||||
m = 2. * orig_m - 1. # convert to +/- 1 representation
|
||||
# First, make sure we got all 1's or -1
|
||||
err_msg = "mls had non binary terms"
|
||||
assert_array_equal(np.abs(m), np.ones_like(m),
|
||||
err_msg=err_msg)
|
||||
# Test via circular cross-correlation, which is just mult.
|
||||
# in the frequency domain with one signal conjugated
|
||||
tester = np.real(ifft(fft(m) * np.conj(fft(m))))
|
||||
out_len = 2**nbits - 1
|
||||
# impulse amplitude == test_len
|
||||
err_msg = "mls impulse has incorrect value"
|
||||
assert_allclose(tester[0], out_len, err_msg=err_msg)
|
||||
# steady-state is -1
|
||||
err_msg = "mls steady-state has incorrect value"
|
||||
assert_allclose(tester[1:], np.full(out_len - 1, -1),
|
||||
err_msg=err_msg)
|
||||
# let's do the split thing using a couple options
|
||||
for n in (1, 2**(nbits - 1)):
|
||||
m1, s1 = max_len_seq(nbits, state=state, taps=taps,
|
||||
length=n)
|
||||
m2, s2 = max_len_seq(nbits, state=s1, taps=taps,
|
||||
length=1)
|
||||
m3, s3 = max_len_seq(nbits, state=s2, taps=taps,
|
||||
length=out_len - n - 1)
|
||||
new_m = np.concatenate((m1, m2, m3))
|
||||
assert_array_equal(orig_m, new_m)
|
||||
|
||||
@ -0,0 +1,891 @@
|
||||
import copy
|
||||
|
||||
import numpy as np
|
||||
from numpy.testing import (
|
||||
assert_,
|
||||
assert_equal,
|
||||
assert_allclose,
|
||||
assert_array_equal
|
||||
)
|
||||
import pytest
|
||||
from pytest import raises, warns
|
||||
|
||||
from scipy.signal._peak_finding import (
|
||||
argrelmax,
|
||||
argrelmin,
|
||||
peak_prominences,
|
||||
peak_widths,
|
||||
_unpack_condition_args,
|
||||
find_peaks,
|
||||
find_peaks_cwt,
|
||||
_identify_ridge_lines
|
||||
)
|
||||
from scipy.signal.windows import gaussian
|
||||
from scipy.signal._peak_finding_utils import _local_maxima_1d, PeakPropertyWarning
|
||||
|
||||
|
||||
def _gen_gaussians(center_locs, sigmas, total_length):
|
||||
xdata = np.arange(0, total_length).astype(float)
|
||||
out_data = np.zeros(total_length, dtype=float)
|
||||
for ind, sigma in enumerate(sigmas):
|
||||
tmp = (xdata - center_locs[ind]) / sigma
|
||||
out_data += np.exp(-(tmp**2))
|
||||
return out_data
|
||||
|
||||
|
||||
def _gen_gaussians_even(sigmas, total_length):
|
||||
num_peaks = len(sigmas)
|
||||
delta = total_length / (num_peaks + 1)
|
||||
center_locs = np.linspace(delta, total_length - delta, num=num_peaks).astype(int)
|
||||
out_data = _gen_gaussians(center_locs, sigmas, total_length)
|
||||
return out_data, center_locs
|
||||
|
||||
|
||||
def _gen_ridge_line(start_locs, max_locs, length, distances, gaps):
|
||||
"""
|
||||
Generate coordinates for a ridge line.
|
||||
|
||||
Will be a series of coordinates, starting a start_loc (length 2).
|
||||
The maximum distance between any adjacent columns will be
|
||||
`max_distance`, the max distance between adjacent rows
|
||||
will be `map_gap'.
|
||||
|
||||
`max_locs` should be the size of the intended matrix. The
|
||||
ending coordinates are guaranteed to be less than `max_locs`,
|
||||
although they may not approach `max_locs` at all.
|
||||
"""
|
||||
|
||||
def keep_bounds(num, max_val):
|
||||
out = max(num, 0)
|
||||
out = min(out, max_val)
|
||||
return out
|
||||
|
||||
gaps = copy.deepcopy(gaps)
|
||||
distances = copy.deepcopy(distances)
|
||||
|
||||
locs = np.zeros([length, 2], dtype=int)
|
||||
locs[0, :] = start_locs
|
||||
total_length = max_locs[0] - start_locs[0] - sum(gaps)
|
||||
if total_length < length:
|
||||
raise ValueError('Cannot generate ridge line according to constraints')
|
||||
dist_int = length / len(distances) - 1
|
||||
gap_int = length / len(gaps) - 1
|
||||
for ind in range(1, length):
|
||||
nextcol = locs[ind - 1, 1]
|
||||
nextrow = locs[ind - 1, 0] + 1
|
||||
if (ind % dist_int == 0) and (len(distances) > 0):
|
||||
nextcol += ((-1)**ind)*distances.pop()
|
||||
if (ind % gap_int == 0) and (len(gaps) > 0):
|
||||
nextrow += gaps.pop()
|
||||
nextrow = keep_bounds(nextrow, max_locs[0])
|
||||
nextcol = keep_bounds(nextcol, max_locs[1])
|
||||
locs[ind, :] = [nextrow, nextcol]
|
||||
|
||||
return [locs[:, 0], locs[:, 1]]
|
||||
|
||||
|
||||
class TestLocalMaxima1d:
|
||||
|
||||
def test_empty(self):
|
||||
"""Test with empty signal."""
|
||||
x = np.array([], dtype=np.float64)
|
||||
for array in _local_maxima_1d(x):
|
||||
assert_equal(array, np.array([]))
|
||||
assert_(array.base is None)
|
||||
|
||||
def test_linear(self):
|
||||
"""Test with linear signal."""
|
||||
x = np.linspace(0, 100)
|
||||
for array in _local_maxima_1d(x):
|
||||
assert_equal(array, np.array([]))
|
||||
assert_(array.base is None)
|
||||
|
||||
def test_simple(self):
|
||||
"""Test with simple signal."""
|
||||
x = np.linspace(-10, 10, 50)
|
||||
x[2::3] += 1
|
||||
expected = np.arange(2, 50, 3)
|
||||
for array in _local_maxima_1d(x):
|
||||
# For plateaus of size 1, the edges are identical with the
|
||||
# midpoints
|
||||
assert_equal(array, expected)
|
||||
assert_(array.base is None)
|
||||
|
||||
def test_flat_maxima(self):
|
||||
"""Test if flat maxima are detected correctly."""
|
||||
x = np.array([-1.3, 0, 1, 0, 2, 2, 0, 3, 3, 3, 2.99, 4, 4, 4, 4, -10,
|
||||
-5, -5, -5, -5, -5, -10])
|
||||
midpoints, left_edges, right_edges = _local_maxima_1d(x)
|
||||
assert_equal(midpoints, np.array([2, 4, 8, 12, 18]))
|
||||
assert_equal(left_edges, np.array([2, 4, 7, 11, 16]))
|
||||
assert_equal(right_edges, np.array([2, 5, 9, 14, 20]))
|
||||
|
||||
@pytest.mark.parametrize('x', [
|
||||
np.array([1., 0, 2]),
|
||||
np.array([3., 3, 0, 4, 4]),
|
||||
np.array([5., 5, 5, 0, 6, 6, 6]),
|
||||
])
|
||||
def test_signal_edges(self, x):
|
||||
"""Test if behavior on signal edges is correct."""
|
||||
for array in _local_maxima_1d(x):
|
||||
assert_equal(array, np.array([]))
|
||||
assert_(array.base is None)
|
||||
|
||||
def test_exceptions(self):
|
||||
"""Test input validation and raised exceptions."""
|
||||
with raises(ValueError, match="wrong number of dimensions"):
|
||||
_local_maxima_1d(np.ones((1, 1)))
|
||||
with raises(ValueError, match="expected 'const float64_t'"):
|
||||
_local_maxima_1d(np.ones(1, dtype=int))
|
||||
with raises(TypeError, match="list"):
|
||||
_local_maxima_1d([1., 2.])
|
||||
with raises(TypeError, match="'x' must not be None"):
|
||||
_local_maxima_1d(None)
|
||||
|
||||
|
||||
class TestRidgeLines:
|
||||
|
||||
def test_empty(self):
|
||||
test_matr = np.zeros([20, 100])
|
||||
lines = _identify_ridge_lines(test_matr, np.full(20, 2), 1)
|
||||
assert_(len(lines) == 0)
|
||||
|
||||
def test_minimal(self):
|
||||
test_matr = np.zeros([20, 100])
|
||||
test_matr[0, 10] = 1
|
||||
lines = _identify_ridge_lines(test_matr, np.full(20, 2), 1)
|
||||
assert_(len(lines) == 1)
|
||||
|
||||
test_matr = np.zeros([20, 100])
|
||||
test_matr[0:2, 10] = 1
|
||||
lines = _identify_ridge_lines(test_matr, np.full(20, 2), 1)
|
||||
assert_(len(lines) == 1)
|
||||
|
||||
def test_single_pass(self):
|
||||
distances = [0, 1, 2, 5]
|
||||
gaps = [0, 1, 2, 0, 1]
|
||||
test_matr = np.zeros([20, 50]) + 1e-12
|
||||
length = 12
|
||||
line = _gen_ridge_line([0, 25], test_matr.shape, length, distances, gaps)
|
||||
test_matr[line[0], line[1]] = 1
|
||||
max_distances = np.full(20, max(distances))
|
||||
identified_lines = _identify_ridge_lines(test_matr,
|
||||
max_distances,
|
||||
max(gaps) + 1)
|
||||
assert_array_equal(identified_lines, [line])
|
||||
|
||||
def test_single_bigdist(self):
|
||||
distances = [0, 1, 2, 5]
|
||||
gaps = [0, 1, 2, 4]
|
||||
test_matr = np.zeros([20, 50])
|
||||
length = 12
|
||||
line = _gen_ridge_line([0, 25], test_matr.shape, length, distances, gaps)
|
||||
test_matr[line[0], line[1]] = 1
|
||||
max_dist = 3
|
||||
max_distances = np.full(20, max_dist)
|
||||
#This should get 2 lines, since the distance is too large
|
||||
identified_lines = _identify_ridge_lines(test_matr,
|
||||
max_distances,
|
||||
max(gaps) + 1)
|
||||
assert_(len(identified_lines) == 2)
|
||||
|
||||
for iline in identified_lines:
|
||||
adists = np.diff(iline[1])
|
||||
np.testing.assert_array_less(np.abs(adists), max_dist)
|
||||
|
||||
agaps = np.diff(iline[0])
|
||||
np.testing.assert_array_less(np.abs(agaps), max(gaps) + 0.1)
|
||||
|
||||
def test_single_biggap(self):
|
||||
distances = [0, 1, 2, 5]
|
||||
max_gap = 3
|
||||
gaps = [0, 4, 2, 1]
|
||||
test_matr = np.zeros([20, 50])
|
||||
length = 12
|
||||
line = _gen_ridge_line([0, 25], test_matr.shape, length, distances, gaps)
|
||||
test_matr[line[0], line[1]] = 1
|
||||
max_dist = 6
|
||||
max_distances = np.full(20, max_dist)
|
||||
#This should get 2 lines, since the gap is too large
|
||||
identified_lines = _identify_ridge_lines(test_matr, max_distances, max_gap)
|
||||
assert_(len(identified_lines) == 2)
|
||||
|
||||
for iline in identified_lines:
|
||||
adists = np.diff(iline[1])
|
||||
np.testing.assert_array_less(np.abs(adists), max_dist)
|
||||
|
||||
agaps = np.diff(iline[0])
|
||||
np.testing.assert_array_less(np.abs(agaps), max(gaps) + 0.1)
|
||||
|
||||
def test_single_biggaps(self):
|
||||
distances = [0]
|
||||
max_gap = 1
|
||||
gaps = [3, 6]
|
||||
test_matr = np.zeros([50, 50])
|
||||
length = 30
|
||||
line = _gen_ridge_line([0, 25], test_matr.shape, length, distances, gaps)
|
||||
test_matr[line[0], line[1]] = 1
|
||||
max_dist = 1
|
||||
max_distances = np.full(50, max_dist)
|
||||
#This should get 3 lines, since the gaps are too large
|
||||
identified_lines = _identify_ridge_lines(test_matr, max_distances, max_gap)
|
||||
assert_(len(identified_lines) == 3)
|
||||
|
||||
for iline in identified_lines:
|
||||
adists = np.diff(iline[1])
|
||||
np.testing.assert_array_less(np.abs(adists), max_dist)
|
||||
|
||||
agaps = np.diff(iline[0])
|
||||
np.testing.assert_array_less(np.abs(agaps), max(gaps) + 0.1)
|
||||
|
||||
|
||||
class TestArgrel:
|
||||
|
||||
def test_empty(self):
|
||||
# Regression test for gh-2832.
|
||||
# When there are no relative extrema, make sure that
|
||||
# the number of empty arrays returned matches the
|
||||
# dimension of the input.
|
||||
|
||||
empty_array = np.array([], dtype=int)
|
||||
|
||||
z1 = np.zeros(5)
|
||||
|
||||
i = argrelmin(z1)
|
||||
assert_equal(len(i), 1)
|
||||
assert_array_equal(i[0], empty_array)
|
||||
|
||||
z2 = np.zeros((3,5))
|
||||
|
||||
row, col = argrelmin(z2, axis=0)
|
||||
assert_array_equal(row, empty_array)
|
||||
assert_array_equal(col, empty_array)
|
||||
|
||||
row, col = argrelmin(z2, axis=1)
|
||||
assert_array_equal(row, empty_array)
|
||||
assert_array_equal(col, empty_array)
|
||||
|
||||
def test_basic(self):
|
||||
# Note: the docstrings for the argrel{min,max,extrema} functions
|
||||
# do not give a guarantee of the order of the indices, so we'll
|
||||
# sort them before testing.
|
||||
|
||||
x = np.array([[1, 2, 2, 3, 2],
|
||||
[2, 1, 2, 2, 3],
|
||||
[3, 2, 1, 2, 2],
|
||||
[2, 3, 2, 1, 2],
|
||||
[1, 2, 3, 2, 1]])
|
||||
|
||||
row, col = argrelmax(x, axis=0)
|
||||
order = np.argsort(row)
|
||||
assert_equal(row[order], [1, 2, 3])
|
||||
assert_equal(col[order], [4, 0, 1])
|
||||
|
||||
row, col = argrelmax(x, axis=1)
|
||||
order = np.argsort(row)
|
||||
assert_equal(row[order], [0, 3, 4])
|
||||
assert_equal(col[order], [3, 1, 2])
|
||||
|
||||
row, col = argrelmin(x, axis=0)
|
||||
order = np.argsort(row)
|
||||
assert_equal(row[order], [1, 2, 3])
|
||||
assert_equal(col[order], [1, 2, 3])
|
||||
|
||||
row, col = argrelmin(x, axis=1)
|
||||
order = np.argsort(row)
|
||||
assert_equal(row[order], [1, 2, 3])
|
||||
assert_equal(col[order], [1, 2, 3])
|
||||
|
||||
def test_highorder(self):
|
||||
order = 2
|
||||
sigmas = [1.0, 2.0, 10.0, 5.0, 15.0]
|
||||
test_data, act_locs = _gen_gaussians_even(sigmas, 500)
|
||||
test_data[act_locs + order] = test_data[act_locs]*0.99999
|
||||
test_data[act_locs - order] = test_data[act_locs]*0.99999
|
||||
rel_max_locs = argrelmax(test_data, order=order, mode='clip')[0]
|
||||
|
||||
assert_(len(rel_max_locs) == len(act_locs))
|
||||
assert_((rel_max_locs == act_locs).all())
|
||||
|
||||
def test_2d_gaussians(self):
|
||||
sigmas = [1.0, 2.0, 10.0]
|
||||
test_data, act_locs = _gen_gaussians_even(sigmas, 100)
|
||||
rot_factor = 20
|
||||
rot_range = np.arange(0, len(test_data)) - rot_factor
|
||||
test_data_2 = np.vstack([test_data, test_data[rot_range]])
|
||||
rel_max_rows, rel_max_cols = argrelmax(test_data_2, axis=1, order=1)
|
||||
|
||||
for rw in range(0, test_data_2.shape[0]):
|
||||
inds = (rel_max_rows == rw)
|
||||
|
||||
assert_(len(rel_max_cols[inds]) == len(act_locs))
|
||||
assert_((act_locs == (rel_max_cols[inds] - rot_factor*rw)).all())
|
||||
|
||||
|
||||
class TestPeakProminences:
|
||||
|
||||
def test_empty(self):
|
||||
"""
|
||||
Test if an empty array is returned if no peaks are provided.
|
||||
"""
|
||||
out = peak_prominences([1, 2, 3], [])
|
||||
for arr, dtype in zip(out, [np.float64, np.intp, np.intp]):
|
||||
assert_(arr.size == 0)
|
||||
assert_(arr.dtype == dtype)
|
||||
|
||||
out = peak_prominences([], [])
|
||||
for arr, dtype in zip(out, [np.float64, np.intp, np.intp]):
|
||||
assert_(arr.size == 0)
|
||||
assert_(arr.dtype == dtype)
|
||||
|
||||
def test_basic(self):
|
||||
"""
|
||||
Test if height of prominences is correctly calculated in signal with
|
||||
rising baseline (peak widths are 1 sample).
|
||||
"""
|
||||
# Prepare basic signal
|
||||
x = np.array([-1, 1.2, 1.2, 1, 3.2, 1.3, 2.88, 2.1])
|
||||
peaks = np.array([1, 2, 4, 6])
|
||||
lbases = np.array([0, 0, 0, 5])
|
||||
rbases = np.array([3, 3, 5, 7])
|
||||
proms = x[peaks] - np.max([x[lbases], x[rbases]], axis=0)
|
||||
# Test if calculation matches handcrafted result
|
||||
out = peak_prominences(x, peaks)
|
||||
assert_equal(out[0], proms)
|
||||
assert_equal(out[1], lbases)
|
||||
assert_equal(out[2], rbases)
|
||||
|
||||
def test_edge_cases(self):
|
||||
"""
|
||||
Test edge cases.
|
||||
"""
|
||||
# Peaks have same height, prominence and bases
|
||||
x = [0, 2, 1, 2, 1, 2, 0]
|
||||
peaks = [1, 3, 5]
|
||||
proms, lbases, rbases = peak_prominences(x, peaks)
|
||||
assert_equal(proms, [2, 2, 2])
|
||||
assert_equal(lbases, [0, 0, 0])
|
||||
assert_equal(rbases, [6, 6, 6])
|
||||
|
||||
# Peaks have same height & prominence but different bases
|
||||
x = [0, 1, 0, 1, 0, 1, 0]
|
||||
peaks = np.array([1, 3, 5])
|
||||
proms, lbases, rbases = peak_prominences(x, peaks)
|
||||
assert_equal(proms, [1, 1, 1])
|
||||
assert_equal(lbases, peaks - 1)
|
||||
assert_equal(rbases, peaks + 1)
|
||||
|
||||
def test_non_contiguous(self):
|
||||
"""
|
||||
Test with non-C-contiguous input arrays.
|
||||
"""
|
||||
x = np.repeat([-9, 9, 9, 0, 3, 1], 2)
|
||||
peaks = np.repeat([1, 2, 4], 2)
|
||||
proms, lbases, rbases = peak_prominences(x[::2], peaks[::2])
|
||||
assert_equal(proms, [9, 9, 2])
|
||||
assert_equal(lbases, [0, 0, 3])
|
||||
assert_equal(rbases, [3, 3, 5])
|
||||
|
||||
def test_wlen(self):
|
||||
"""
|
||||
Test if wlen actually shrinks the evaluation range correctly.
|
||||
"""
|
||||
x = [0, 1, 2, 3, 1, 0, -1]
|
||||
peak = [3]
|
||||
# Test rounding behavior of wlen
|
||||
assert_equal(peak_prominences(x, peak), [3., 0, 6])
|
||||
for wlen, i in [(8, 0), (7, 0), (6, 0), (5, 1), (3.2, 1), (3, 2), (1.1, 2)]:
|
||||
assert_equal(peak_prominences(x, peak, wlen), [3. - i, 0 + i, 6 - i])
|
||||
|
||||
def test_exceptions(self):
|
||||
"""
|
||||
Verify that exceptions and warnings are raised.
|
||||
"""
|
||||
# x with dimension > 1
|
||||
with raises(ValueError, match='1-D array'):
|
||||
peak_prominences([[0, 1, 1, 0]], [1, 2])
|
||||
# peaks with dimension > 1
|
||||
with raises(ValueError, match='1-D array'):
|
||||
peak_prominences([0, 1, 1, 0], [[1, 2]])
|
||||
# x with dimension < 1
|
||||
with raises(ValueError, match='1-D array'):
|
||||
peak_prominences(3, [0,])
|
||||
|
||||
# empty x with supplied
|
||||
with raises(ValueError, match='not a valid index'):
|
||||
peak_prominences([], [0])
|
||||
# invalid indices with non-empty x
|
||||
for p in [-100, -1, 3, 1000]:
|
||||
with raises(ValueError, match='not a valid index'):
|
||||
peak_prominences([1, 0, 2], [p])
|
||||
|
||||
# peaks is not cast-able to np.intp
|
||||
with raises(TypeError, match='cannot safely cast'):
|
||||
peak_prominences([0, 1, 1, 0], [1.1, 2.3])
|
||||
|
||||
# wlen < 3
|
||||
with raises(ValueError, match='wlen'):
|
||||
peak_prominences(np.arange(10), [3, 5], wlen=1)
|
||||
|
||||
def test_warnings(self):
|
||||
"""
|
||||
Verify that appropriate warnings are raised.
|
||||
"""
|
||||
msg = "some peaks have a prominence of 0"
|
||||
for p in [0, 1, 2]:
|
||||
with warns(PeakPropertyWarning, match=msg):
|
||||
peak_prominences([1, 0, 2], [p,])
|
||||
with warns(PeakPropertyWarning, match=msg):
|
||||
peak_prominences([0, 1, 1, 1, 0], [2], wlen=2)
|
||||
|
||||
|
||||
class TestPeakWidths:
|
||||
|
||||
def test_empty(self):
|
||||
"""
|
||||
Test if an empty array is returned if no peaks are provided.
|
||||
"""
|
||||
widths = peak_widths([], [])[0]
|
||||
assert_(isinstance(widths, np.ndarray))
|
||||
assert_equal(widths.size, 0)
|
||||
widths = peak_widths([1, 2, 3], [])[0]
|
||||
assert_(isinstance(widths, np.ndarray))
|
||||
assert_equal(widths.size, 0)
|
||||
out = peak_widths([], [])
|
||||
for arr in out:
|
||||
assert_(isinstance(arr, np.ndarray))
|
||||
assert_equal(arr.size, 0)
|
||||
|
||||
@pytest.mark.filterwarnings("ignore:some peaks have a width of 0")
|
||||
def test_basic(self):
|
||||
"""
|
||||
Test a simple use case with easy to verify results at different relative
|
||||
heights.
|
||||
"""
|
||||
x = np.array([1, 0, 1, 2, 1, 0, -1])
|
||||
prominence = 2
|
||||
for rel_height, width_true, lip_true, rip_true in [
|
||||
(0., 0., 3., 3.), # raises warning
|
||||
(0.25, 1., 2.5, 3.5),
|
||||
(0.5, 2., 2., 4.),
|
||||
(0.75, 3., 1.5, 4.5),
|
||||
(1., 4., 1., 5.),
|
||||
(2., 5., 1., 6.),
|
||||
(3., 5., 1., 6.)
|
||||
]:
|
||||
width_calc, height, lip_calc, rip_calc = peak_widths(
|
||||
x, [3], rel_height)
|
||||
assert_allclose(width_calc, width_true)
|
||||
assert_allclose(height, 2 - rel_height * prominence)
|
||||
assert_allclose(lip_calc, lip_true)
|
||||
assert_allclose(rip_calc, rip_true)
|
||||
|
||||
def test_non_contiguous(self):
|
||||
"""
|
||||
Test with non-C-contiguous input arrays.
|
||||
"""
|
||||
x = np.repeat([0, 100, 50], 4)
|
||||
peaks = np.repeat([1], 3)
|
||||
result = peak_widths(x[::4], peaks[::3])
|
||||
assert_equal(result, [0.75, 75, 0.75, 1.5])
|
||||
|
||||
def test_exceptions(self):
|
||||
"""
|
||||
Verify that argument validation works as intended.
|
||||
"""
|
||||
with raises(ValueError, match='1-D array'):
|
||||
# x with dimension > 1
|
||||
peak_widths(np.zeros((3, 4)), np.ones(3))
|
||||
with raises(ValueError, match='1-D array'):
|
||||
# x with dimension < 1
|
||||
peak_widths(3, [0])
|
||||
with raises(ValueError, match='1-D array'):
|
||||
# peaks with dimension > 1
|
||||
peak_widths(np.arange(10), np.ones((3, 2), dtype=np.intp))
|
||||
with raises(ValueError, match='1-D array'):
|
||||
# peaks with dimension < 1
|
||||
peak_widths(np.arange(10), 3)
|
||||
with raises(ValueError, match='not a valid index'):
|
||||
# peak pos exceeds x.size
|
||||
peak_widths(np.arange(10), [8, 11])
|
||||
with raises(ValueError, match='not a valid index'):
|
||||
# empty x with peaks supplied
|
||||
peak_widths([], [1, 2])
|
||||
with raises(TypeError, match='cannot safely cast'):
|
||||
# peak cannot be safely casted to intp
|
||||
peak_widths(np.arange(10), [1.1, 2.3])
|
||||
with raises(ValueError, match='rel_height'):
|
||||
# rel_height is < 0
|
||||
peak_widths([0, 1, 0, 1, 0], [1, 3], rel_height=-1)
|
||||
with raises(TypeError, match='None'):
|
||||
# prominence data contains None
|
||||
peak_widths([1, 2, 1], [1], prominence_data=(None, None, None))
|
||||
|
||||
def test_warnings(self):
|
||||
"""
|
||||
Verify that appropriate warnings are raised.
|
||||
"""
|
||||
msg = "some peaks have a width of 0"
|
||||
with warns(PeakPropertyWarning, match=msg):
|
||||
# Case: rel_height is 0
|
||||
peak_widths([0, 1, 0], [1], rel_height=0)
|
||||
with warns(PeakPropertyWarning, match=msg):
|
||||
# Case: prominence is 0 and bases are identical
|
||||
peak_widths(
|
||||
[0, 1, 1, 1, 0], [2],
|
||||
prominence_data=(np.array([0.], np.float64),
|
||||
np.array([2], np.intp),
|
||||
np.array([2], np.intp))
|
||||
)
|
||||
|
||||
def test_mismatching_prominence_data(self):
|
||||
"""Test with mismatching peak and / or prominence data."""
|
||||
x = [0, 1, 0]
|
||||
peak = [1]
|
||||
for i, (prominences, left_bases, right_bases) in enumerate([
|
||||
((1.,), (-1,), (2,)), # left base not in x
|
||||
((1.,), (0,), (3,)), # right base not in x
|
||||
((1.,), (2,), (0,)), # swapped bases same as peak
|
||||
((1., 1.), (0, 0), (2, 2)), # array shapes don't match peaks
|
||||
((1., 1.), (0,), (2,)), # arrays with different shapes
|
||||
((1.,), (0, 0), (2,)), # arrays with different shapes
|
||||
((1.,), (0,), (2, 2)) # arrays with different shapes
|
||||
]):
|
||||
# Make sure input is matches output of signal.peak_prominences
|
||||
prominence_data = (np.array(prominences, dtype=np.float64),
|
||||
np.array(left_bases, dtype=np.intp),
|
||||
np.array(right_bases, dtype=np.intp))
|
||||
# Test for correct exception
|
||||
if i < 3:
|
||||
match = "prominence data is invalid for peak"
|
||||
else:
|
||||
match = "arrays in `prominence_data` must have the same shape"
|
||||
with raises(ValueError, match=match):
|
||||
peak_widths(x, peak, prominence_data=prominence_data)
|
||||
|
||||
@pytest.mark.filterwarnings("ignore:some peaks have a width of 0")
|
||||
def test_intersection_rules(self):
|
||||
"""Test if x == eval_height counts as an intersection."""
|
||||
# Flatt peak with two possible intersection points if evaluated at 1
|
||||
x = [0, 1, 2, 1, 3, 3, 3, 1, 2, 1, 0]
|
||||
# relative height is 0 -> width is 0 as well, raises warning
|
||||
assert_allclose(peak_widths(x, peaks=[5], rel_height=0),
|
||||
[(0.,), (3.,), (5.,), (5.,)])
|
||||
# width_height == x counts as intersection -> nearest 1 is chosen
|
||||
assert_allclose(peak_widths(x, peaks=[5], rel_height=2/3),
|
||||
[(4.,), (1.,), (3.,), (7.,)])
|
||||
|
||||
|
||||
def test_unpack_condition_args():
|
||||
"""
|
||||
Verify parsing of condition arguments for `scipy.signal.find_peaks` function.
|
||||
"""
|
||||
x = np.arange(10)
|
||||
amin_true = x
|
||||
amax_true = amin_true + 10
|
||||
peaks = amin_true[1::2]
|
||||
|
||||
# Test unpacking with None or interval
|
||||
assert_((None, None) == _unpack_condition_args((None, None), x, peaks))
|
||||
assert_((1, None) == _unpack_condition_args(1, x, peaks))
|
||||
assert_((1, None) == _unpack_condition_args((1, None), x, peaks))
|
||||
assert_((None, 2) == _unpack_condition_args((None, 2), x, peaks))
|
||||
assert_((3., 4.5) == _unpack_condition_args((3., 4.5), x, peaks))
|
||||
|
||||
# Test if borders are correctly reduced with `peaks`
|
||||
amin_calc, amax_calc = _unpack_condition_args((amin_true, amax_true), x, peaks)
|
||||
assert_equal(amin_calc, amin_true[peaks])
|
||||
assert_equal(amax_calc, amax_true[peaks])
|
||||
|
||||
# Test raises if array borders don't match x
|
||||
with raises(ValueError, match="array size of lower"):
|
||||
_unpack_condition_args(amin_true, np.arange(11), peaks)
|
||||
with raises(ValueError, match="array size of upper"):
|
||||
_unpack_condition_args((None, amin_true), np.arange(11), peaks)
|
||||
|
||||
|
||||
class TestFindPeaks:
|
||||
|
||||
# Keys of optionally returned properties
|
||||
property_keys = {'peak_heights', 'left_thresholds', 'right_thresholds',
|
||||
'prominences', 'left_bases', 'right_bases', 'widths',
|
||||
'width_heights', 'left_ips', 'right_ips'}
|
||||
|
||||
def test_constant(self):
|
||||
"""
|
||||
Test behavior for signal without local maxima.
|
||||
"""
|
||||
open_interval = (None, None)
|
||||
peaks, props = find_peaks(np.ones(10),
|
||||
height=open_interval, threshold=open_interval,
|
||||
prominence=open_interval, width=open_interval)
|
||||
assert_(peaks.size == 0)
|
||||
for key in self.property_keys:
|
||||
assert_(props[key].size == 0)
|
||||
|
||||
def test_plateau_size(self):
|
||||
"""
|
||||
Test plateau size condition for peaks.
|
||||
"""
|
||||
# Prepare signal with peaks with peak_height == plateau_size
|
||||
plateau_sizes = np.array([1, 2, 3, 4, 8, 20, 111])
|
||||
x = np.zeros(plateau_sizes.size * 2 + 1)
|
||||
x[1::2] = plateau_sizes
|
||||
repeats = np.ones(x.size, dtype=int)
|
||||
repeats[1::2] = x[1::2]
|
||||
x = np.repeat(x, repeats)
|
||||
|
||||
# Test full output
|
||||
peaks, props = find_peaks(x, plateau_size=(None, None))
|
||||
assert_equal(peaks, [1, 3, 7, 11, 18, 33, 100])
|
||||
assert_equal(props["plateau_sizes"], plateau_sizes)
|
||||
assert_equal(props["left_edges"], peaks - (plateau_sizes - 1) // 2)
|
||||
assert_equal(props["right_edges"], peaks + plateau_sizes // 2)
|
||||
|
||||
# Test conditions
|
||||
assert_equal(find_peaks(x, plateau_size=4)[0], [11, 18, 33, 100])
|
||||
assert_equal(find_peaks(x, plateau_size=(None, 3.5))[0], [1, 3, 7])
|
||||
assert_equal(find_peaks(x, plateau_size=(5, 50))[0], [18, 33])
|
||||
|
||||
def test_height_condition(self):
|
||||
"""
|
||||
Test height condition for peaks.
|
||||
"""
|
||||
x = (0., 1/3, 0., 2.5, 0, 4., 0)
|
||||
peaks, props = find_peaks(x, height=(None, None))
|
||||
assert_equal(peaks, np.array([1, 3, 5]))
|
||||
assert_equal(props['peak_heights'], np.array([1/3, 2.5, 4.]))
|
||||
assert_equal(find_peaks(x, height=0.5)[0], np.array([3, 5]))
|
||||
assert_equal(find_peaks(x, height=(None, 3))[0], np.array([1, 3]))
|
||||
assert_equal(find_peaks(x, height=(2, 3))[0], np.array([3]))
|
||||
|
||||
def test_threshold_condition(self):
|
||||
"""
|
||||
Test threshold condition for peaks.
|
||||
"""
|
||||
x = (0, 2, 1, 4, -1)
|
||||
peaks, props = find_peaks(x, threshold=(None, None))
|
||||
assert_equal(peaks, np.array([1, 3]))
|
||||
assert_equal(props['left_thresholds'], np.array([2, 3]))
|
||||
assert_equal(props['right_thresholds'], np.array([1, 5]))
|
||||
assert_equal(find_peaks(x, threshold=2)[0], np.array([3]))
|
||||
assert_equal(find_peaks(x, threshold=3.5)[0], np.array([]))
|
||||
assert_equal(find_peaks(x, threshold=(None, 5))[0], np.array([1, 3]))
|
||||
assert_equal(find_peaks(x, threshold=(None, 4))[0], np.array([1]))
|
||||
assert_equal(find_peaks(x, threshold=(2, 4))[0], np.array([]))
|
||||
|
||||
def test_distance_condition(self):
|
||||
"""
|
||||
Test distance condition for peaks.
|
||||
"""
|
||||
# Peaks of different height with constant distance 3
|
||||
peaks_all = np.arange(1, 21, 3)
|
||||
x = np.zeros(21)
|
||||
x[peaks_all] += np.linspace(1, 2, peaks_all.size)
|
||||
|
||||
# Test if peaks with "minimal" distance are still selected (distance = 3)
|
||||
assert_equal(find_peaks(x, distance=3)[0], peaks_all)
|
||||
|
||||
# Select every second peak (distance > 3)
|
||||
peaks_subset = find_peaks(x, distance=3.0001)[0]
|
||||
# Test if peaks_subset is subset of peaks_all
|
||||
assert_(
|
||||
np.setdiff1d(peaks_subset, peaks_all, assume_unique=True).size == 0
|
||||
)
|
||||
# Test if every second peak was removed
|
||||
assert_equal(np.diff(peaks_subset), 6)
|
||||
|
||||
# Test priority of peak removal
|
||||
x = [-2, 1, -1, 0, -3]
|
||||
peaks_subset = find_peaks(x, distance=10)[0] # use distance > x size
|
||||
assert_(peaks_subset.size == 1 and peaks_subset[0] == 1)
|
||||
|
||||
def test_prominence_condition(self):
|
||||
"""
|
||||
Test prominence condition for peaks.
|
||||
"""
|
||||
x = np.linspace(0, 10, 100)
|
||||
peaks_true = np.arange(1, 99, 2)
|
||||
offset = np.linspace(1, 10, peaks_true.size)
|
||||
x[peaks_true] += offset
|
||||
prominences = x[peaks_true] - x[peaks_true + 1]
|
||||
interval = (3, 9)
|
||||
keep = np.nonzero(
|
||||
(interval[0] <= prominences) & (prominences <= interval[1]))
|
||||
|
||||
peaks_calc, properties = find_peaks(x, prominence=interval)
|
||||
assert_equal(peaks_calc, peaks_true[keep])
|
||||
assert_equal(properties['prominences'], prominences[keep])
|
||||
assert_equal(properties['left_bases'], 0)
|
||||
assert_equal(properties['right_bases'], peaks_true[keep] + 1)
|
||||
|
||||
def test_width_condition(self):
|
||||
"""
|
||||
Test width condition for peaks.
|
||||
"""
|
||||
x = np.array([1, 0, 1, 2, 1, 0, -1, 4, 0])
|
||||
peaks, props = find_peaks(x, width=(None, 2), rel_height=0.75)
|
||||
assert_equal(peaks.size, 1)
|
||||
assert_equal(peaks, 7)
|
||||
assert_allclose(props['widths'], 1.35)
|
||||
assert_allclose(props['width_heights'], 1.)
|
||||
assert_allclose(props['left_ips'], 6.4)
|
||||
assert_allclose(props['right_ips'], 7.75)
|
||||
|
||||
def test_properties(self):
|
||||
"""
|
||||
Test returned properties.
|
||||
"""
|
||||
open_interval = (None, None)
|
||||
x = [0, 1, 0, 2, 1.5, 0, 3, 0, 5, 9]
|
||||
peaks, props = find_peaks(x,
|
||||
height=open_interval, threshold=open_interval,
|
||||
prominence=open_interval, width=open_interval)
|
||||
assert_(len(props) == len(self.property_keys))
|
||||
for key in self.property_keys:
|
||||
assert_(peaks.size == props[key].size)
|
||||
|
||||
def test_raises(self):
|
||||
"""
|
||||
Test exceptions raised by function.
|
||||
"""
|
||||
with raises(ValueError, match="1-D array"):
|
||||
find_peaks(np.array(1))
|
||||
with raises(ValueError, match="1-D array"):
|
||||
find_peaks(np.ones((2, 2)))
|
||||
with raises(ValueError, match="distance"):
|
||||
find_peaks(np.arange(10), distance=-1)
|
||||
|
||||
@pytest.mark.filterwarnings("ignore:some peaks have a prominence of 0",
|
||||
"ignore:some peaks have a width of 0")
|
||||
def test_wlen_smaller_plateau(self):
|
||||
"""
|
||||
Test behavior of prominence and width calculation if the given window
|
||||
length is smaller than a peak's plateau size.
|
||||
|
||||
Regression test for gh-9110.
|
||||
"""
|
||||
peaks, props = find_peaks([0, 1, 1, 1, 0], prominence=(None, None),
|
||||
width=(None, None), wlen=2)
|
||||
assert_equal(peaks, 2)
|
||||
assert_equal(props["prominences"], 0)
|
||||
assert_equal(props["widths"], 0)
|
||||
assert_equal(props["width_heights"], 1)
|
||||
for key in ("left_bases", "right_bases", "left_ips", "right_ips"):
|
||||
assert_equal(props[key], peaks)
|
||||
|
||||
@pytest.mark.parametrize("kwargs", [
|
||||
{},
|
||||
{"distance": 3.0},
|
||||
{"prominence": (None, None)},
|
||||
{"width": (None, 2)},
|
||||
|
||||
])
|
||||
def test_readonly_array(self, kwargs):
|
||||
"""
|
||||
Test readonly arrays are accepted.
|
||||
"""
|
||||
x = np.linspace(0, 10, 15)
|
||||
x_readonly = x.copy()
|
||||
x_readonly.flags.writeable = False
|
||||
|
||||
peaks, _ = find_peaks(x)
|
||||
peaks_readonly, _ = find_peaks(x_readonly, **kwargs)
|
||||
|
||||
assert_allclose(peaks, peaks_readonly)
|
||||
|
||||
|
||||
class TestFindPeaksCwt:
|
||||
|
||||
def test_find_peaks_exact(self):
|
||||
"""
|
||||
Generate a series of gaussians and attempt to find the peak locations.
|
||||
"""
|
||||
sigmas = [5.0, 3.0, 10.0, 20.0, 10.0, 50.0]
|
||||
num_points = 500
|
||||
test_data, act_locs = _gen_gaussians_even(sigmas, num_points)
|
||||
widths = np.arange(0.1, max(sigmas))
|
||||
found_locs = find_peaks_cwt(test_data, widths, gap_thresh=2, min_snr=0,
|
||||
min_length=None)
|
||||
np.testing.assert_array_equal(found_locs, act_locs,
|
||||
"Found maximum locations did not equal those expected")
|
||||
|
||||
def test_find_peaks_withnoise(self):
|
||||
"""
|
||||
Verify that peak locations are (approximately) found
|
||||
for a series of gaussians with added noise.
|
||||
"""
|
||||
sigmas = [5.0, 3.0, 10.0, 20.0, 10.0, 50.0]
|
||||
num_points = 500
|
||||
test_data, act_locs = _gen_gaussians_even(sigmas, num_points)
|
||||
widths = np.arange(0.1, max(sigmas))
|
||||
noise_amp = 0.07
|
||||
np.random.seed(18181911)
|
||||
test_data += (np.random.rand(num_points) - 0.5)*(2*noise_amp)
|
||||
found_locs = find_peaks_cwt(test_data, widths, min_length=15,
|
||||
gap_thresh=1, min_snr=noise_amp / 5)
|
||||
|
||||
np.testing.assert_equal(len(found_locs), len(act_locs), 'Different number' +
|
||||
'of peaks found than expected')
|
||||
diffs = np.abs(found_locs - act_locs)
|
||||
max_diffs = np.array(sigmas) / 5
|
||||
np.testing.assert_array_less(diffs, max_diffs, 'Maximum location differed' +
|
||||
'by more than %s' % (max_diffs))
|
||||
|
||||
def test_find_peaks_nopeak(self):
|
||||
"""
|
||||
Verify that no peak is found in
|
||||
data that's just noise.
|
||||
"""
|
||||
noise_amp = 1.0
|
||||
num_points = 100
|
||||
np.random.seed(181819141)
|
||||
test_data = (np.random.rand(num_points) - 0.5)*(2*noise_amp)
|
||||
widths = np.arange(10, 50)
|
||||
found_locs = find_peaks_cwt(test_data, widths, min_snr=5, noise_perc=30)
|
||||
np.testing.assert_equal(len(found_locs), 0)
|
||||
|
||||
def test_find_peaks_with_non_default_wavelets(self):
|
||||
x = gaussian(200, 2)
|
||||
widths = np.array([1, 2, 3, 4])
|
||||
a = find_peaks_cwt(x, widths, wavelet=gaussian)
|
||||
|
||||
np.testing.assert_equal(np.array([100]), a)
|
||||
|
||||
def test_find_peaks_window_size(self):
|
||||
"""
|
||||
Verify that window_size is passed correctly to private function and
|
||||
affects the result.
|
||||
"""
|
||||
sigmas = [2.0, 2.0]
|
||||
num_points = 1000
|
||||
test_data, act_locs = _gen_gaussians_even(sigmas, num_points)
|
||||
widths = np.arange(0.1, max(sigmas), 0.2)
|
||||
noise_amp = 0.05
|
||||
np.random.seed(18181911)
|
||||
test_data += (np.random.rand(num_points) - 0.5)*(2*noise_amp)
|
||||
|
||||
# Possibly contrived negative region to throw off peak finding
|
||||
# when window_size is too large
|
||||
test_data[250:320] -= 1
|
||||
|
||||
found_locs = find_peaks_cwt(test_data, widths, gap_thresh=2, min_snr=3,
|
||||
min_length=None, window_size=None)
|
||||
with pytest.raises(AssertionError):
|
||||
assert found_locs.size == act_locs.size
|
||||
|
||||
found_locs = find_peaks_cwt(test_data, widths, gap_thresh=2, min_snr=3,
|
||||
min_length=None, window_size=20)
|
||||
assert found_locs.size == act_locs.size
|
||||
|
||||
def test_find_peaks_with_one_width(self):
|
||||
"""
|
||||
Verify that the `width` argument
|
||||
in `find_peaks_cwt` can be a float
|
||||
"""
|
||||
xs = np.arange(0, np.pi, 0.05)
|
||||
test_data = np.sin(xs)
|
||||
widths = 1
|
||||
found_locs = find_peaks_cwt(test_data, widths)
|
||||
|
||||
np.testing.assert_equal(found_locs, 32)
|
||||
@ -0,0 +1,52 @@
|
||||
# Regressions tests on result types of some signal functions
|
||||
|
||||
import numpy as np
|
||||
from numpy.testing import assert_
|
||||
|
||||
from scipy.signal import (decimate,
|
||||
lfilter_zi,
|
||||
lfiltic,
|
||||
sos2tf,
|
||||
sosfilt_zi)
|
||||
|
||||
|
||||
def test_decimate():
|
||||
ones_f32 = np.ones(32, dtype=np.float32)
|
||||
assert_(decimate(ones_f32, 2).dtype == np.float32)
|
||||
|
||||
ones_i64 = np.ones(32, dtype=np.int64)
|
||||
assert_(decimate(ones_i64, 2).dtype == np.float64)
|
||||
|
||||
|
||||
def test_lfilter_zi():
|
||||
b_f32 = np.array([1, 2, 3], dtype=np.float32)
|
||||
a_f32 = np.array([4, 5, 6], dtype=np.float32)
|
||||
assert_(lfilter_zi(b_f32, a_f32).dtype == np.float32)
|
||||
|
||||
|
||||
def test_lfiltic():
|
||||
# this would return f32 when given a mix of f32 / f64 args
|
||||
b_f32 = np.array([1, 2, 3], dtype=np.float32)
|
||||
a_f32 = np.array([4, 5, 6], dtype=np.float32)
|
||||
x_f32 = np.ones(32, dtype=np.float32)
|
||||
|
||||
b_f64 = b_f32.astype(np.float64)
|
||||
a_f64 = a_f32.astype(np.float64)
|
||||
x_f64 = x_f32.astype(np.float64)
|
||||
|
||||
assert_(lfiltic(b_f64, a_f32, x_f32).dtype == np.float64)
|
||||
assert_(lfiltic(b_f32, a_f64, x_f32).dtype == np.float64)
|
||||
assert_(lfiltic(b_f32, a_f32, x_f64).dtype == np.float64)
|
||||
assert_(lfiltic(b_f32, a_f32, x_f32, x_f64).dtype == np.float64)
|
||||
|
||||
|
||||
def test_sos2tf():
|
||||
sos_f32 = np.array([[4, 5, 6, 1, 2, 3]], dtype=np.float32)
|
||||
b, a = sos2tf(sos_f32)
|
||||
assert_(b.dtype == np.float32)
|
||||
assert_(a.dtype == np.float32)
|
||||
|
||||
|
||||
def test_sosfilt_zi():
|
||||
sos_f32 = np.array([[4, 5, 6, 1, 2, 3]], dtype=np.float32)
|
||||
assert_(sosfilt_zi(sos_f32).dtype == np.float32)
|
||||
@ -0,0 +1,358 @@
|
||||
import pytest
|
||||
import numpy as np
|
||||
from numpy.testing import (assert_allclose, assert_equal,
|
||||
assert_almost_equal, assert_array_equal,
|
||||
assert_array_almost_equal)
|
||||
|
||||
from scipy.ndimage import convolve1d
|
||||
|
||||
from scipy.signal import savgol_coeffs, savgol_filter
|
||||
from scipy.signal._savitzky_golay import _polyder
|
||||
|
||||
|
||||
def check_polyder(p, m, expected):
|
||||
dp = _polyder(p, m)
|
||||
assert_array_equal(dp, expected)
|
||||
|
||||
|
||||
def test_polyder():
|
||||
cases = [
|
||||
([5], 0, [5]),
|
||||
([5], 1, [0]),
|
||||
([3, 2, 1], 0, [3, 2, 1]),
|
||||
([3, 2, 1], 1, [6, 2]),
|
||||
([3, 2, 1], 2, [6]),
|
||||
([3, 2, 1], 3, [0]),
|
||||
([[3, 2, 1], [5, 6, 7]], 0, [[3, 2, 1], [5, 6, 7]]),
|
||||
([[3, 2, 1], [5, 6, 7]], 1, [[6, 2], [10, 6]]),
|
||||
([[3, 2, 1], [5, 6, 7]], 2, [[6], [10]]),
|
||||
([[3, 2, 1], [5, 6, 7]], 3, [[0], [0]]),
|
||||
]
|
||||
for p, m, expected in cases:
|
||||
check_polyder(np.array(p).T, m, np.array(expected).T)
|
||||
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
# savgol_coeffs tests
|
||||
#--------------------------------------------------------------------
|
||||
|
||||
def alt_sg_coeffs(window_length, polyorder, pos):
|
||||
"""This is an alternative implementation of the SG coefficients.
|
||||
|
||||
It uses numpy.polyfit and numpy.polyval. The results should be
|
||||
equivalent to those of savgol_coeffs(), but this implementation
|
||||
is slower.
|
||||
|
||||
window_length should be odd.
|
||||
|
||||
"""
|
||||
if pos is None:
|
||||
pos = window_length // 2
|
||||
t = np.arange(window_length)
|
||||
unit = (t == pos).astype(int)
|
||||
h = np.polyval(np.polyfit(t, unit, polyorder), t)
|
||||
return h
|
||||
|
||||
|
||||
def test_sg_coeffs_trivial():
|
||||
# Test a trivial case of savgol_coeffs: polyorder = window_length - 1
|
||||
h = savgol_coeffs(1, 0)
|
||||
assert_allclose(h, [1])
|
||||
|
||||
h = savgol_coeffs(3, 2)
|
||||
assert_allclose(h, [0, 1, 0], atol=1e-10)
|
||||
|
||||
h = savgol_coeffs(5, 4)
|
||||
assert_allclose(h, [0, 0, 1, 0, 0], atol=1e-10)
|
||||
|
||||
h = savgol_coeffs(5, 4, pos=1)
|
||||
assert_allclose(h, [0, 0, 0, 1, 0], atol=1e-10)
|
||||
|
||||
h = savgol_coeffs(5, 4, pos=1, use='dot')
|
||||
assert_allclose(h, [0, 1, 0, 0, 0], atol=1e-10)
|
||||
|
||||
|
||||
def compare_coeffs_to_alt(window_length, order):
|
||||
# For the given window_length and order, compare the results
|
||||
# of savgol_coeffs and alt_sg_coeffs for pos from 0 to window_length - 1.
|
||||
# Also include pos=None.
|
||||
for pos in [None] + list(range(window_length)):
|
||||
h1 = savgol_coeffs(window_length, order, pos=pos, use='dot')
|
||||
h2 = alt_sg_coeffs(window_length, order, pos=pos)
|
||||
assert_allclose(h1, h2, atol=1e-10,
|
||||
err_msg=("window_length = %d, order = %d, pos = %s" %
|
||||
(window_length, order, pos)))
|
||||
|
||||
|
||||
def test_sg_coeffs_compare():
|
||||
# Compare savgol_coeffs() to alt_sg_coeffs().
|
||||
for window_length in range(1, 8, 2):
|
||||
for order in range(window_length):
|
||||
compare_coeffs_to_alt(window_length, order)
|
||||
|
||||
|
||||
def test_sg_coeffs_exact():
|
||||
polyorder = 4
|
||||
window_length = 9
|
||||
halflen = window_length // 2
|
||||
|
||||
x = np.linspace(0, 21, 43)
|
||||
delta = x[1] - x[0]
|
||||
|
||||
# The data is a cubic polynomial. We'll use an order 4
|
||||
# SG filter, so the filtered values should equal the input data
|
||||
# (except within half window_length of the edges).
|
||||
y = 0.5 * x ** 3 - x
|
||||
h = savgol_coeffs(window_length, polyorder)
|
||||
y0 = convolve1d(y, h)
|
||||
assert_allclose(y0[halflen:-halflen], y[halflen:-halflen])
|
||||
|
||||
# Check the same input, but use deriv=1. dy is the exact result.
|
||||
dy = 1.5 * x ** 2 - 1
|
||||
h = savgol_coeffs(window_length, polyorder, deriv=1, delta=delta)
|
||||
y1 = convolve1d(y, h)
|
||||
assert_allclose(y1[halflen:-halflen], dy[halflen:-halflen])
|
||||
|
||||
# Check the same input, but use deriv=2. d2y is the exact result.
|
||||
d2y = 3.0 * x
|
||||
h = savgol_coeffs(window_length, polyorder, deriv=2, delta=delta)
|
||||
y2 = convolve1d(y, h)
|
||||
assert_allclose(y2[halflen:-halflen], d2y[halflen:-halflen])
|
||||
|
||||
|
||||
def test_sg_coeffs_deriv():
|
||||
# The data in `x` is a sampled parabola, so using savgol_coeffs with an
|
||||
# order 2 or higher polynomial should give exact results.
|
||||
i = np.array([-2.0, 0.0, 2.0, 4.0, 6.0])
|
||||
x = i ** 2 / 4
|
||||
dx = i / 2
|
||||
d2x = np.full_like(i, 0.5)
|
||||
for pos in range(x.size):
|
||||
coeffs0 = savgol_coeffs(5, 3, pos=pos, delta=2.0, use='dot')
|
||||
assert_allclose(coeffs0.dot(x), x[pos], atol=1e-10)
|
||||
coeffs1 = savgol_coeffs(5, 3, pos=pos, delta=2.0, use='dot', deriv=1)
|
||||
assert_allclose(coeffs1.dot(x), dx[pos], atol=1e-10)
|
||||
coeffs2 = savgol_coeffs(5, 3, pos=pos, delta=2.0, use='dot', deriv=2)
|
||||
assert_allclose(coeffs2.dot(x), d2x[pos], atol=1e-10)
|
||||
|
||||
|
||||
def test_sg_coeffs_deriv_gt_polyorder():
|
||||
"""
|
||||
If deriv > polyorder, the coefficients should be all 0.
|
||||
This is a regression test for a bug where, e.g.,
|
||||
savgol_coeffs(5, polyorder=1, deriv=2)
|
||||
raised an error.
|
||||
"""
|
||||
coeffs = savgol_coeffs(5, polyorder=1, deriv=2)
|
||||
assert_array_equal(coeffs, np.zeros(5))
|
||||
coeffs = savgol_coeffs(7, polyorder=4, deriv=6)
|
||||
assert_array_equal(coeffs, np.zeros(7))
|
||||
|
||||
|
||||
def test_sg_coeffs_large():
|
||||
# Test that for large values of window_length and polyorder the array of
|
||||
# coefficients returned is symmetric. The aim is to ensure that
|
||||
# no potential numeric overflow occurs.
|
||||
coeffs0 = savgol_coeffs(31, 9)
|
||||
assert_array_almost_equal(coeffs0, coeffs0[::-1])
|
||||
coeffs1 = savgol_coeffs(31, 9, deriv=1)
|
||||
assert_array_almost_equal(coeffs1, -coeffs1[::-1])
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# savgol_coeffs tests for even window length
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_sg_coeffs_even_window_length():
|
||||
# Simple case - deriv=0, polyorder=0, 1
|
||||
window_lengths = [4, 6, 8, 10, 12, 14, 16]
|
||||
for length in window_lengths:
|
||||
h_p_d = savgol_coeffs(length, 0, 0)
|
||||
assert_allclose(h_p_d, 1/length)
|
||||
|
||||
# Verify with closed forms
|
||||
# deriv=1, polyorder=1, 2
|
||||
def h_p_d_closed_form_1(k, m):
|
||||
return 6*(k - 0.5)/((2*m + 1)*m*(2*m - 1))
|
||||
|
||||
# deriv=2, polyorder=2
|
||||
def h_p_d_closed_form_2(k, m):
|
||||
numer = 15*(-4*m**2 + 1 + 12*(k - 0.5)**2)
|
||||
denom = 4*(2*m + 1)*(m + 1)*m*(m - 1)*(2*m - 1)
|
||||
return numer/denom
|
||||
|
||||
for length in window_lengths:
|
||||
m = length//2
|
||||
expected_output = [h_p_d_closed_form_1(k, m)
|
||||
for k in range(-m + 1, m + 1)][::-1]
|
||||
actual_output = savgol_coeffs(length, 1, 1)
|
||||
assert_allclose(expected_output, actual_output)
|
||||
actual_output = savgol_coeffs(length, 2, 1)
|
||||
assert_allclose(expected_output, actual_output)
|
||||
|
||||
expected_output = [h_p_d_closed_form_2(k, m)
|
||||
for k in range(-m + 1, m + 1)][::-1]
|
||||
actual_output = savgol_coeffs(length, 2, 2)
|
||||
assert_allclose(expected_output, actual_output)
|
||||
actual_output = savgol_coeffs(length, 3, 2)
|
||||
assert_allclose(expected_output, actual_output)
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
# savgol_filter tests
|
||||
#--------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_sg_filter_trivial():
|
||||
""" Test some trivial edge cases for savgol_filter()."""
|
||||
x = np.array([1.0])
|
||||
y = savgol_filter(x, 1, 0)
|
||||
assert_equal(y, [1.0])
|
||||
|
||||
# Input is a single value. With a window length of 3 and polyorder 1,
|
||||
# the value in y is from the straight-line fit of (-1,0), (0,3) and
|
||||
# (1, 0) at 0. This is just the average of the three values, hence 1.0.
|
||||
x = np.array([3.0])
|
||||
y = savgol_filter(x, 3, 1, mode='constant')
|
||||
assert_almost_equal(y, [1.0], decimal=15)
|
||||
|
||||
x = np.array([3.0])
|
||||
y = savgol_filter(x, 3, 1, mode='nearest')
|
||||
assert_almost_equal(y, [3.0], decimal=15)
|
||||
|
||||
x = np.array([1.0] * 3)
|
||||
y = savgol_filter(x, 3, 1, mode='wrap')
|
||||
assert_almost_equal(y, [1.0, 1.0, 1.0], decimal=15)
|
||||
|
||||
|
||||
def test_sg_filter_basic():
|
||||
# Some basic test cases for savgol_filter().
|
||||
x = np.array([1.0, 2.0, 1.0])
|
||||
y = savgol_filter(x, 3, 1, mode='constant')
|
||||
assert_allclose(y, [1.0, 4.0 / 3, 1.0])
|
||||
|
||||
y = savgol_filter(x, 3, 1, mode='mirror')
|
||||
assert_allclose(y, [5.0 / 3, 4.0 / 3, 5.0 / 3])
|
||||
|
||||
y = savgol_filter(x, 3, 1, mode='wrap')
|
||||
assert_allclose(y, [4.0 / 3, 4.0 / 3, 4.0 / 3])
|
||||
|
||||
|
||||
def test_sg_filter_2d():
|
||||
x = np.array([[1.0, 2.0, 1.0],
|
||||
[2.0, 4.0, 2.0]])
|
||||
expected = np.array([[1.0, 4.0 / 3, 1.0],
|
||||
[2.0, 8.0 / 3, 2.0]])
|
||||
y = savgol_filter(x, 3, 1, mode='constant')
|
||||
assert_allclose(y, expected)
|
||||
|
||||
y = savgol_filter(x.T, 3, 1, mode='constant', axis=0)
|
||||
assert_allclose(y, expected.T)
|
||||
|
||||
|
||||
def test_sg_filter_interp_edges():
|
||||
# Another test with low degree polynomial data, for which we can easily
|
||||
# give the exact results. In this test, we use mode='interp', so
|
||||
# savgol_filter should match the exact solution for the entire data set,
|
||||
# including the edges.
|
||||
t = np.linspace(-5, 5, 21)
|
||||
delta = t[1] - t[0]
|
||||
# Polynomial test data.
|
||||
x = np.array([t,
|
||||
3 * t ** 2,
|
||||
t ** 3 - t])
|
||||
dx = np.array([np.ones_like(t),
|
||||
6 * t,
|
||||
3 * t ** 2 - 1.0])
|
||||
d2x = np.array([np.zeros_like(t),
|
||||
np.full_like(t, 6),
|
||||
6 * t])
|
||||
|
||||
window_length = 7
|
||||
|
||||
y = savgol_filter(x, window_length, 3, axis=-1, mode='interp')
|
||||
assert_allclose(y, x, atol=1e-12)
|
||||
|
||||
y1 = savgol_filter(x, window_length, 3, axis=-1, mode='interp',
|
||||
deriv=1, delta=delta)
|
||||
assert_allclose(y1, dx, atol=1e-12)
|
||||
|
||||
y2 = savgol_filter(x, window_length, 3, axis=-1, mode='interp',
|
||||
deriv=2, delta=delta)
|
||||
assert_allclose(y2, d2x, atol=1e-12)
|
||||
|
||||
# Transpose everything, and test again with axis=0.
|
||||
|
||||
x = x.T
|
||||
dx = dx.T
|
||||
d2x = d2x.T
|
||||
|
||||
y = savgol_filter(x, window_length, 3, axis=0, mode='interp')
|
||||
assert_allclose(y, x, atol=1e-12)
|
||||
|
||||
y1 = savgol_filter(x, window_length, 3, axis=0, mode='interp',
|
||||
deriv=1, delta=delta)
|
||||
assert_allclose(y1, dx, atol=1e-12)
|
||||
|
||||
y2 = savgol_filter(x, window_length, 3, axis=0, mode='interp',
|
||||
deriv=2, delta=delta)
|
||||
assert_allclose(y2, d2x, atol=1e-12)
|
||||
|
||||
|
||||
def test_sg_filter_interp_edges_3d():
|
||||
# Test mode='interp' with a 3-D array.
|
||||
t = np.linspace(-5, 5, 21)
|
||||
delta = t[1] - t[0]
|
||||
x1 = np.array([t, -t])
|
||||
x2 = np.array([t ** 2, 3 * t ** 2 + 5])
|
||||
x3 = np.array([t ** 3, 2 * t ** 3 + t ** 2 - 0.5 * t])
|
||||
dx1 = np.array([np.ones_like(t), -np.ones_like(t)])
|
||||
dx2 = np.array([2 * t, 6 * t])
|
||||
dx3 = np.array([3 * t ** 2, 6 * t ** 2 + 2 * t - 0.5])
|
||||
|
||||
# z has shape (3, 2, 21)
|
||||
z = np.array([x1, x2, x3])
|
||||
dz = np.array([dx1, dx2, dx3])
|
||||
|
||||
y = savgol_filter(z, 7, 3, axis=-1, mode='interp', delta=delta)
|
||||
assert_allclose(y, z, atol=1e-10)
|
||||
|
||||
dy = savgol_filter(z, 7, 3, axis=-1, mode='interp', deriv=1, delta=delta)
|
||||
assert_allclose(dy, dz, atol=1e-10)
|
||||
|
||||
# z has shape (3, 21, 2)
|
||||
z = np.array([x1.T, x2.T, x3.T])
|
||||
dz = np.array([dx1.T, dx2.T, dx3.T])
|
||||
|
||||
y = savgol_filter(z, 7, 3, axis=1, mode='interp', delta=delta)
|
||||
assert_allclose(y, z, atol=1e-10)
|
||||
|
||||
dy = savgol_filter(z, 7, 3, axis=1, mode='interp', deriv=1, delta=delta)
|
||||
assert_allclose(dy, dz, atol=1e-10)
|
||||
|
||||
# z has shape (21, 3, 2)
|
||||
z = z.swapaxes(0, 1).copy()
|
||||
dz = dz.swapaxes(0, 1).copy()
|
||||
|
||||
y = savgol_filter(z, 7, 3, axis=0, mode='interp', delta=delta)
|
||||
assert_allclose(y, z, atol=1e-10)
|
||||
|
||||
dy = savgol_filter(z, 7, 3, axis=0, mode='interp', deriv=1, delta=delta)
|
||||
assert_allclose(dy, dz, atol=1e-10)
|
||||
|
||||
|
||||
def test_sg_filter_valid_window_length_3d():
|
||||
"""Tests that the window_length check is using the correct axis."""
|
||||
|
||||
x = np.ones((10, 20, 30))
|
||||
|
||||
savgol_filter(x, window_length=29, polyorder=3, mode='interp')
|
||||
|
||||
with pytest.raises(ValueError, match='window_length must be less than'):
|
||||
# window_length is more than x.shape[-1].
|
||||
savgol_filter(x, window_length=31, polyorder=3, mode='interp')
|
||||
|
||||
savgol_filter(x, window_length=9, polyorder=3, axis=0, mode='interp')
|
||||
|
||||
with pytest.raises(ValueError, match='window_length must be less than'):
|
||||
# window_length is more than x.shape[0].
|
||||
savgol_filter(x, window_length=11, polyorder=3, axis=0, mode='interp')
|
||||
@ -0,0 +1,840 @@
|
||||
"""Unit tests for module `_short_time_fft`.
|
||||
|
||||
This file's structure loosely groups the tests into the following sequential
|
||||
categories:
|
||||
|
||||
1. Test function `_calc_dual_canonical_window`.
|
||||
2. Test for invalid parameters and exceptions in `ShortTimeFFT` (until the
|
||||
`test_from_window` function).
|
||||
3. Test algorithmic properties of STFT/ISTFT. Some tests were ported from
|
||||
``test_spectral.py``.
|
||||
|
||||
Notes
|
||||
-----
|
||||
* Mypy 0.990 does interpret the line::
|
||||
|
||||
from scipy.stats import norm as normal_distribution
|
||||
|
||||
incorrectly (but the code works), hence a ``type: ignore`` was appended.
|
||||
"""
|
||||
import math
|
||||
from itertools import product
|
||||
from typing import cast, get_args, Literal
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from numpy.testing import assert_allclose, assert_equal
|
||||
from scipy.fft import fftshift
|
||||
from scipy.stats import norm as normal_distribution # type: ignore
|
||||
from scipy.signal import get_window, welch, stft, istft, spectrogram
|
||||
|
||||
from scipy.signal._short_time_fft import FFT_MODE_TYPE, \
|
||||
_calc_dual_canonical_window, ShortTimeFFT, PAD_TYPE
|
||||
from scipy.signal.windows import gaussian
|
||||
|
||||
|
||||
def test__calc_dual_canonical_window_roundtrip():
|
||||
"""Test dual window calculation with a round trip to verify duality.
|
||||
|
||||
Note that this works only for canonical window pairs (having minimal
|
||||
energy) like a Gaussian.
|
||||
|
||||
The window is the same as in the example of `from ShortTimeFFT.from_dual`.
|
||||
"""
|
||||
win = gaussian(51, std=10, sym=True)
|
||||
d_win = _calc_dual_canonical_window(win, 10)
|
||||
win2 = _calc_dual_canonical_window(d_win, 10)
|
||||
assert_allclose(win2, win)
|
||||
|
||||
|
||||
def test__calc_dual_canonical_window_exceptions():
|
||||
"""Raise all exceptions in `_calc_dual_canonical_window`."""
|
||||
# Verify that calculation can fail:
|
||||
with pytest.raises(ValueError, match="hop=5 is larger than window len.*"):
|
||||
_calc_dual_canonical_window(np.ones(4), 5)
|
||||
with pytest.raises(ValueError, match=".* Transform not invertible!"):
|
||||
_calc_dual_canonical_window(np.array([.1, .2, .3, 0]), 4)
|
||||
|
||||
# Verify that parameter `win` may not be integers:
|
||||
with pytest.raises(ValueError, match="Parameter 'win' cannot be of int.*"):
|
||||
_calc_dual_canonical_window(np.ones(4, dtype=int), 1)
|
||||
|
||||
|
||||
def test_invalid_initializer_parameters():
|
||||
"""Verify that exceptions get raised on invalid parameters when
|
||||
instantiating ShortTimeFFT. """
|
||||
with pytest.raises(ValueError, match=r"Parameter win must be 1d, " +
|
||||
r"but win.shape=\(2, 2\)!"):
|
||||
ShortTimeFFT(np.ones((2, 2)), hop=4, fs=1)
|
||||
with pytest.raises(ValueError, match="Parameter win must have " +
|
||||
"finite entries"):
|
||||
ShortTimeFFT(np.array([1, np.inf, 2, 3]), hop=4, fs=1)
|
||||
with pytest.raises(ValueError, match="Parameter hop=0 is not " +
|
||||
"an integer >= 1!"):
|
||||
ShortTimeFFT(np.ones(4), hop=0, fs=1)
|
||||
with pytest.raises(ValueError, match="Parameter hop=2.0 is not " +
|
||||
"an integer >= 1!"):
|
||||
# noinspection PyTypeChecker
|
||||
ShortTimeFFT(np.ones(4), hop=2.0, fs=1)
|
||||
with pytest.raises(ValueError, match=r"dual_win.shape=\(5,\) must equal " +
|
||||
r"win.shape=\(4,\)!"):
|
||||
ShortTimeFFT(np.ones(4), hop=2, fs=1, dual_win=np.ones(5))
|
||||
with pytest.raises(ValueError, match="Parameter dual_win must be " +
|
||||
"a finite array!"):
|
||||
ShortTimeFFT(np.ones(3), hop=2, fs=1,
|
||||
dual_win=np.array([np.nan, 2, 3]))
|
||||
|
||||
|
||||
def test_exceptions_properties_methods():
|
||||
"""Verify that exceptions get raised when setting properties or calling
|
||||
method of ShortTimeFFT to/with invalid values."""
|
||||
SFT = ShortTimeFFT(np.ones(8), hop=4, fs=1)
|
||||
with pytest.raises(ValueError, match="Sampling interval T=-1 must be " +
|
||||
"positive!"):
|
||||
SFT.T = -1
|
||||
with pytest.raises(ValueError, match="Sampling frequency fs=-1 must be " +
|
||||
"positive!"):
|
||||
SFT.fs = -1
|
||||
with pytest.raises(ValueError, match="fft_mode='invalid_typ' not in " +
|
||||
r"\('twosided', 'centered', " +
|
||||
r"'onesided', 'onesided2X'\)!"):
|
||||
SFT.fft_mode = 'invalid_typ'
|
||||
with pytest.raises(ValueError, match="For scaling is None, " +
|
||||
"fft_mode='onesided2X' is invalid.*"):
|
||||
SFT.fft_mode = 'onesided2X'
|
||||
with pytest.raises(ValueError, match="Attribute mfft=7 needs to be " +
|
||||
"at least the window length.*"):
|
||||
SFT.mfft = 7
|
||||
with pytest.raises(ValueError, match="scaling='invalid' not in.*"):
|
||||
# noinspection PyTypeChecker
|
||||
SFT.scale_to('invalid')
|
||||
with pytest.raises(ValueError, match="phase_shift=3.0 has the unit .*"):
|
||||
SFT.phase_shift = 3.0
|
||||
with pytest.raises(ValueError, match="-mfft < phase_shift < mfft " +
|
||||
"does not hold.*"):
|
||||
SFT.phase_shift = 2*SFT.mfft
|
||||
with pytest.raises(ValueError, match="Parameter padding='invalid' not.*"):
|
||||
# noinspection PyTypeChecker
|
||||
g = SFT._x_slices(np.zeros(16), k_off=0, p0=0, p1=1, padding='invalid')
|
||||
next(g) # execute generator
|
||||
with pytest.raises(ValueError, match="Trend type must be 'linear' " +
|
||||
"or 'constant'"):
|
||||
# noinspection PyTypeChecker
|
||||
SFT.stft_detrend(np.zeros(16), detr='invalid')
|
||||
with pytest.raises(ValueError, match="Parameter detr=nan is not a str, " +
|
||||
"function or None!"):
|
||||
# noinspection PyTypeChecker
|
||||
SFT.stft_detrend(np.zeros(16), detr=np.nan)
|
||||
with pytest.raises(ValueError, match="Invalid Parameter p0=0, p1=200.*"):
|
||||
SFT.p_range(100, 0, 200)
|
||||
|
||||
with pytest.raises(ValueError, match="f_axis=0 may not be equal to " +
|
||||
"t_axis=0!"):
|
||||
SFT.istft(np.zeros((SFT.f_pts, 2)), t_axis=0, f_axis=0)
|
||||
with pytest.raises(ValueError, match=r"S.shape\[f_axis\]=2 must be equal" +
|
||||
" to self.f_pts=5.*"):
|
||||
SFT.istft(np.zeros((2, 2)))
|
||||
with pytest.raises(ValueError, match=r"S.shape\[t_axis\]=1 needs to have" +
|
||||
" at least 2 slices.*"):
|
||||
SFT.istft(np.zeros((SFT.f_pts, 1)))
|
||||
with pytest.raises(ValueError, match=r".*\(k1=100\) <= \(k_max=12\) " +
|
||||
"is false!$"):
|
||||
SFT.istft(np.zeros((SFT.f_pts, 3)), k1=100)
|
||||
with pytest.raises(ValueError, match=r"\(k1=1\) - \(k0=0\) = 1 has to " +
|
||||
"be at least.* length 4!"):
|
||||
SFT.istft(np.zeros((SFT.f_pts, 3)), k0=0, k1=1)
|
||||
|
||||
with pytest.raises(ValueError, match=r"Parameter axes_seq='invalid' " +
|
||||
r"not in \['tf', 'ft'\]!"):
|
||||
# noinspection PyTypeChecker
|
||||
SFT.extent(n=100, axes_seq='invalid')
|
||||
with pytest.raises(ValueError, match="Attribute fft_mode=twosided must.*"):
|
||||
SFT.fft_mode = 'twosided'
|
||||
SFT.extent(n=100)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('m', ('onesided', 'onesided2X'))
|
||||
def test_exceptions_fft_mode_complex_win(m: FFT_MODE_TYPE):
|
||||
"""Verify that one-sided spectra are not allowed with complex-valued
|
||||
windows or with complex-valued signals.
|
||||
|
||||
The reason being, the `rfft` function only accepts real-valued input.
|
||||
"""
|
||||
with pytest.raises(ValueError,
|
||||
match=f"One-sided spectra, i.e., fft_mode='{m}'.*"):
|
||||
ShortTimeFFT(np.ones(8)*1j, hop=4, fs=1, fft_mode=m)
|
||||
|
||||
SFT = ShortTimeFFT(np.ones(8)*1j, hop=4, fs=1, fft_mode='twosided')
|
||||
with pytest.raises(ValueError,
|
||||
match=f"One-sided spectra, i.e., fft_mode='{m}'.*"):
|
||||
SFT.fft_mode = m
|
||||
|
||||
SFT = ShortTimeFFT(np.ones(8), hop=4, fs=1, scale_to='psd', fft_mode='onesided')
|
||||
with pytest.raises(ValueError, match="Complex-valued `x` not allowed for self.*"):
|
||||
SFT.stft(np.ones(8)*1j)
|
||||
SFT.fft_mode = 'onesided2X'
|
||||
with pytest.raises(ValueError, match="Complex-valued `x` not allowed for self.*"):
|
||||
SFT.stft(np.ones(8)*1j)
|
||||
|
||||
|
||||
def test_invalid_fft_mode_RuntimeError():
|
||||
"""Ensure exception gets raised when property `fft_mode` is invalid. """
|
||||
SFT = ShortTimeFFT(np.ones(8), hop=4, fs=1)
|
||||
SFT._fft_mode = 'invalid_typ'
|
||||
|
||||
with pytest.raises(RuntimeError):
|
||||
_ = SFT.f
|
||||
with pytest.raises(RuntimeError):
|
||||
SFT._fft_func(np.ones(8))
|
||||
with pytest.raises(RuntimeError):
|
||||
SFT._ifft_func(np.ones(8))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('win_params, Nx', [(('gaussian', 2.), 9), # in docstr
|
||||
('triang', 7),
|
||||
(('kaiser', 4.0), 9),
|
||||
(('exponential', None, 1.), 9),
|
||||
(4.0, 9)])
|
||||
def test_from_window(win_params, Nx: int):
|
||||
"""Verify that `from_window()` handles parameters correctly.
|
||||
|
||||
The window parameterizations are documented in the `get_window` docstring.
|
||||
"""
|
||||
w_sym, fs = get_window(win_params, Nx, fftbins=False), 16.
|
||||
w_per = get_window(win_params, Nx, fftbins=True)
|
||||
SFT0 = ShortTimeFFT(w_sym, hop=3, fs=fs, fft_mode='twosided',
|
||||
scale_to='psd', phase_shift=1)
|
||||
nperseg = len(w_sym)
|
||||
noverlap = nperseg - SFT0.hop
|
||||
SFT1 = ShortTimeFFT.from_window(win_params, fs, nperseg, noverlap,
|
||||
symmetric_win=True, fft_mode='twosided',
|
||||
scale_to='psd', phase_shift=1)
|
||||
# periodic window:
|
||||
SFT2 = ShortTimeFFT.from_window(win_params, fs, nperseg, noverlap,
|
||||
symmetric_win=False, fft_mode='twosided',
|
||||
scale_to='psd', phase_shift=1)
|
||||
# Be informative when comparing instances:
|
||||
assert_equal(SFT1.win, SFT0.win)
|
||||
assert_allclose(SFT2.win, w_per / np.sqrt(sum(w_per**2) * fs))
|
||||
for n_ in ('hop', 'T', 'fft_mode', 'mfft', 'scaling', 'phase_shift'):
|
||||
v0, v1, v2 = (getattr(SFT_, n_) for SFT_ in (SFT0, SFT1, SFT2))
|
||||
assert v1 == v0, f"SFT1.{n_}={v1} does not equal SFT0.{n_}={v0}"
|
||||
assert v2 == v0, f"SFT2.{n_}={v2} does not equal SFT0.{n_}={v0}"
|
||||
|
||||
|
||||
def test_dual_win_roundtrip():
|
||||
"""Verify the duality of `win` and `dual_win`.
|
||||
|
||||
Note that this test does not work for arbitrary windows, since dual windows
|
||||
are not unique. It always works for invertible STFTs if the windows do not
|
||||
overlap.
|
||||
"""
|
||||
# Non-standard values for keyword arguments (except for `scale_to`):
|
||||
kw = dict(hop=4, fs=1, fft_mode='twosided', mfft=8, scale_to=None,
|
||||
phase_shift=2)
|
||||
SFT0 = ShortTimeFFT(np.ones(4), **kw)
|
||||
SFT1 = ShortTimeFFT.from_dual(SFT0.dual_win, **kw)
|
||||
assert_allclose(SFT1.dual_win, SFT0.win)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('scale_to, fac_psd, fac_mag',
|
||||
[(None, 0.25, 0.125),
|
||||
('magnitude', 2.0, 1),
|
||||
('psd', 1, 0.5)])
|
||||
def test_scaling(scale_to: Literal['magnitude', 'psd'], fac_psd, fac_mag):
|
||||
"""Verify scaling calculations.
|
||||
|
||||
* Verify passing `scale_to`parameter to ``__init__().
|
||||
* Roundtrip while changing scaling factor.
|
||||
"""
|
||||
SFT = ShortTimeFFT(np.ones(4) * 2, hop=4, fs=1, scale_to=scale_to)
|
||||
assert SFT.fac_psd == fac_psd
|
||||
assert SFT.fac_magnitude == fac_mag
|
||||
# increase coverage by accessing properties twice:
|
||||
assert SFT.fac_psd == fac_psd
|
||||
assert SFT.fac_magnitude == fac_mag
|
||||
|
||||
x = np.fft.irfft([0, 0, 7, 0, 0, 0, 0]) # periodic signal
|
||||
Sx = SFT.stft(x)
|
||||
Sx_mag, Sx_psd = Sx * SFT.fac_magnitude, Sx * SFT.fac_psd
|
||||
|
||||
SFT.scale_to('magnitude')
|
||||
x_mag = SFT.istft(Sx_mag, k1=len(x))
|
||||
assert_allclose(x_mag, x)
|
||||
|
||||
SFT.scale_to('psd')
|
||||
x_psd = SFT.istft(Sx_psd, k1=len(x))
|
||||
assert_allclose(x_psd, x)
|
||||
|
||||
|
||||
def test_scale_to():
|
||||
"""Verify `scale_to()` method."""
|
||||
SFT = ShortTimeFFT(np.ones(4) * 2, hop=4, fs=1, scale_to=None)
|
||||
|
||||
SFT.scale_to('magnitude')
|
||||
assert SFT.scaling == 'magnitude'
|
||||
assert SFT.fac_psd == 2.0
|
||||
assert SFT.fac_magnitude == 1
|
||||
|
||||
SFT.scale_to('psd')
|
||||
assert SFT.scaling == 'psd'
|
||||
assert SFT.fac_psd == 1
|
||||
assert SFT.fac_magnitude == 0.5
|
||||
|
||||
SFT.scale_to('psd') # needed for coverage
|
||||
|
||||
for scale, s_fac in zip(('magnitude', 'psd'), (8, 4)):
|
||||
SFT = ShortTimeFFT(np.ones(4) * 2, hop=4, fs=1, scale_to=None)
|
||||
dual_win = SFT.dual_win.copy()
|
||||
|
||||
SFT.scale_to(cast(Literal['magnitude', 'psd'], scale))
|
||||
assert_allclose(SFT.dual_win, dual_win * s_fac)
|
||||
|
||||
|
||||
def test_x_slices_padding():
|
||||
"""Verify padding.
|
||||
|
||||
The reference arrays were taken from the docstrings of `zero_ext`,
|
||||
`const_ext`, `odd_ext()`, and `even_ext()` from the _array_tools module.
|
||||
"""
|
||||
SFT = ShortTimeFFT(np.ones(5), hop=4, fs=1)
|
||||
x = np.array([[1, 2, 3, 4, 5], [0, 1, 4, 9, 16]], dtype=float)
|
||||
d = {'zeros': [[[0, 0, 1, 2, 3], [0, 0, 0, 1, 4]],
|
||||
[[3, 4, 5, 0, 0], [4, 9, 16, 0, 0]]],
|
||||
'edge': [[[1, 1, 1, 2, 3], [0, 0, 0, 1, 4]],
|
||||
[[3, 4, 5, 5, 5], [4, 9, 16, 16, 16]]],
|
||||
'even': [[[3, 2, 1, 2, 3], [4, 1, 0, 1, 4]],
|
||||
[[3, 4, 5, 4, 3], [4, 9, 16, 9, 4]]],
|
||||
'odd': [[[-1, 0, 1, 2, 3], [-4, -1, 0, 1, 4]],
|
||||
[[3, 4, 5, 6, 7], [4, 9, 16, 23, 28]]]}
|
||||
for p_, xx in d.items():
|
||||
gen = SFT._x_slices(np.array(x), 0, 0, 2, padding=cast(PAD_TYPE, p_))
|
||||
yy = np.array([y_.copy() for y_ in gen]) # due to inplace copying
|
||||
assert_equal(yy, xx, err_msg=f"Failed '{p_}' padding.")
|
||||
|
||||
|
||||
def test_invertible():
|
||||
"""Verify `invertible` property. """
|
||||
SFT = ShortTimeFFT(np.ones(8), hop=4, fs=1)
|
||||
assert SFT.invertible
|
||||
SFT = ShortTimeFFT(np.ones(8), hop=9, fs=1)
|
||||
assert not SFT.invertible
|
||||
|
||||
|
||||
def test_border_values():
|
||||
"""Ensure that minimum and maximum values of slices are correct."""
|
||||
SFT = ShortTimeFFT(np.ones(8), hop=4, fs=1)
|
||||
assert SFT.p_min == 0
|
||||
assert SFT.k_min == -4
|
||||
assert SFT.lower_border_end == (4, 1)
|
||||
assert SFT.lower_border_end == (4, 1) # needed to test caching
|
||||
assert SFT.p_max(10) == 4
|
||||
assert SFT.k_max(10) == 16
|
||||
assert SFT.upper_border_begin(10) == (4, 2)
|
||||
|
||||
|
||||
def test_border_values_exotic():
|
||||
"""Ensure that the border calculations are correct for windows with
|
||||
zeros. """
|
||||
w = np.array([0, 0, 0, 0, 0, 0, 0, 1.])
|
||||
SFT = ShortTimeFFT(w, hop=1, fs=1)
|
||||
assert SFT.lower_border_end == (0, 0)
|
||||
|
||||
SFT = ShortTimeFFT(np.flip(w), hop=20, fs=1)
|
||||
assert SFT.upper_border_begin(4) == (0, 0)
|
||||
|
||||
SFT._hop = -1 # provoke unreachable line
|
||||
with pytest.raises(RuntimeError):
|
||||
_ = SFT.k_max(4)
|
||||
with pytest.raises(RuntimeError):
|
||||
_ = SFT.k_min
|
||||
|
||||
|
||||
def test_t():
|
||||
"""Verify that the times of the slices are correct. """
|
||||
SFT = ShortTimeFFT(np.ones(8), hop=4, fs=2)
|
||||
assert SFT.T == 1/2
|
||||
assert SFT.fs == 2.
|
||||
assert SFT.delta_t == 4 * 1/2
|
||||
t_stft = np.arange(0, SFT.p_max(10)) * SFT.delta_t
|
||||
assert_equal(SFT.t(10), t_stft)
|
||||
assert_equal(SFT.t(10, 1, 3), t_stft[1:3])
|
||||
SFT.T = 1/4
|
||||
assert SFT.T == 1/4
|
||||
assert SFT.fs == 4
|
||||
SFT.fs = 1/8
|
||||
assert SFT.fs == 1/8
|
||||
assert SFT.T == 8
|
||||
|
||||
|
||||
@pytest.mark.parametrize('fft_mode, f',
|
||||
[('onesided', [0., 1., 2.]),
|
||||
('onesided2X', [0., 1., 2.]),
|
||||
('twosided', [0., 1., 2., -2., -1.]),
|
||||
('centered', [-2., -1., 0., 1., 2.])])
|
||||
def test_f(fft_mode: FFT_MODE_TYPE, f):
|
||||
"""Verify the frequency values property `f`."""
|
||||
SFT = ShortTimeFFT(np.ones(5), hop=4, fs=5, fft_mode=fft_mode,
|
||||
scale_to='psd')
|
||||
assert_equal(SFT.f, f)
|
||||
|
||||
|
||||
def test_extent():
|
||||
"""Ensure that the `extent()` method is correct. """
|
||||
SFT = ShortTimeFFT(np.ones(32), hop=4, fs=32, fft_mode='onesided')
|
||||
assert SFT.extent(100, 'tf', False) == (-0.375, 3.625, 0.0, 17.0)
|
||||
assert SFT.extent(100, 'ft', False) == (0.0, 17.0, -0.375, 3.625)
|
||||
assert SFT.extent(100, 'tf', True) == (-0.4375, 3.5625, -0.5, 16.5)
|
||||
assert SFT.extent(100, 'ft', True) == (-0.5, 16.5, -0.4375, 3.5625)
|
||||
|
||||
SFT = ShortTimeFFT(np.ones(32), hop=4, fs=32, fft_mode='centered')
|
||||
assert SFT.extent(100, 'tf', False) == (-0.375, 3.625, -16.0, 15.0)
|
||||
|
||||
|
||||
def test_spectrogram():
|
||||
"""Verify spectrogram and cross-spectrogram methods. """
|
||||
SFT = ShortTimeFFT(np.ones(8), hop=4, fs=1)
|
||||
x, y = np.ones(10), np.arange(10)
|
||||
X, Y = SFT.stft(x), SFT.stft(y)
|
||||
assert_allclose(SFT.spectrogram(x), X.real**2+X.imag**2)
|
||||
assert_allclose(SFT.spectrogram(x, y), X * Y.conj())
|
||||
|
||||
|
||||
@pytest.mark.parametrize('n', [8, 9])
|
||||
def test_fft_func_roundtrip(n: int):
|
||||
"""Test roundtrip `ifft_func(fft_func(x)) == x` for all permutations of
|
||||
relevant parameters. """
|
||||
np.random.seed(2394795)
|
||||
x0 = np.random.rand(n)
|
||||
w, h_n = np.ones(n), 4
|
||||
|
||||
pp = dict(
|
||||
fft_mode=get_args(FFT_MODE_TYPE),
|
||||
mfft=[None, n, n+1, n+2],
|
||||
scaling=[None, 'magnitude', 'psd'],
|
||||
phase_shift=[None, -n+1, 0, n // 2, n-1])
|
||||
for f_typ, mfft, scaling, phase_shift in product(*pp.values()):
|
||||
if f_typ == 'onesided2X' and scaling is None:
|
||||
continue # this combination is forbidden
|
||||
SFT = ShortTimeFFT(w, h_n, fs=n, fft_mode=f_typ, mfft=mfft,
|
||||
scale_to=scaling, phase_shift=phase_shift)
|
||||
X0 = SFT._fft_func(x0)
|
||||
x1 = SFT._ifft_func(X0)
|
||||
assert_allclose(x0, x1, err_msg="_fft_func() roundtrip failed for " +
|
||||
f"{f_typ=}, {mfft=}, {scaling=}, {phase_shift=}")
|
||||
|
||||
SFT = ShortTimeFFT(w, h_n, fs=1)
|
||||
SFT._fft_mode = 'invalid_fft' # type: ignore
|
||||
with pytest.raises(RuntimeError):
|
||||
SFT._fft_func(x0)
|
||||
with pytest.raises(RuntimeError):
|
||||
SFT._ifft_func(x0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('i', range(19))
|
||||
def test_impulse_roundtrip(i):
|
||||
"""Roundtrip for an impulse being at different positions `i`."""
|
||||
n = 19
|
||||
w, h_n = np.ones(8), 3
|
||||
x = np.zeros(n)
|
||||
x[i] = 1
|
||||
|
||||
SFT = ShortTimeFFT(w, hop=h_n, fs=1, scale_to=None, phase_shift=None)
|
||||
Sx = SFT.stft(x)
|
||||
# test slicing the input signal into two parts:
|
||||
n_q = SFT.nearest_k_p(n // 2)
|
||||
Sx0 = SFT.stft(x[:n_q], padding='zeros')
|
||||
Sx1 = SFT.stft(x[n_q:], padding='zeros')
|
||||
q0_ub = SFT.upper_border_begin(n_q)[1] - SFT.p_min
|
||||
q1_le = SFT.lower_border_end[1] - SFT.p_min
|
||||
assert_allclose(Sx0[:, :q0_ub], Sx[:, :q0_ub], err_msg=f"{i=}")
|
||||
assert_allclose(Sx1[:, q1_le:], Sx[:, q1_le-Sx1.shape[1]:],
|
||||
err_msg=f"{i=}")
|
||||
|
||||
Sx01 = np.hstack((Sx0[:, :q0_ub],
|
||||
Sx0[:, q0_ub:] + Sx1[:, :q1_le],
|
||||
Sx1[:, q1_le:]))
|
||||
assert_allclose(Sx, Sx01, atol=1e-8, err_msg=f"{i=}")
|
||||
|
||||
y = SFT.istft(Sx, 0, n)
|
||||
assert_allclose(y, x, atol=1e-8, err_msg=f"{i=}")
|
||||
y0 = SFT.istft(Sx, 0, n//2)
|
||||
assert_allclose(x[:n//2], y0, atol=1e-8, err_msg=f"{i=}")
|
||||
y1 = SFT.istft(Sx, n // 2, n)
|
||||
assert_allclose(x[n // 2:], y1, atol=1e-8, err_msg=f"{i=}")
|
||||
|
||||
|
||||
@pytest.mark.parametrize('hop', [1, 7, 8])
|
||||
def test_asymmetric_window_roundtrip(hop: int):
|
||||
"""An asymmetric window could uncover indexing problems. """
|
||||
np.random.seed(23371)
|
||||
|
||||
w = np.arange(16) / 8 # must be of type float
|
||||
w[len(w)//2:] = 1
|
||||
SFT = ShortTimeFFT(w, hop, fs=1)
|
||||
|
||||
x = 10 * np.random.randn(64)
|
||||
Sx = SFT.stft(x)
|
||||
x1 = SFT.istft(Sx, k1=len(x))
|
||||
assert_allclose(x1, x1, err_msg="Roundtrip for asymmetric window with " +
|
||||
f" {hop=} failed!")
|
||||
|
||||
|
||||
@pytest.mark.parametrize('m_num', [6, 7])
|
||||
def test_minimal_length_signal(m_num):
|
||||
"""Verify that the shortest allowed signal works. """
|
||||
SFT = ShortTimeFFT(np.ones(m_num), m_num//2, fs=1)
|
||||
n = math.ceil(m_num/2)
|
||||
x = np.ones(n)
|
||||
Sx = SFT.stft(x)
|
||||
x1 = SFT.istft(Sx, k1=n)
|
||||
assert_allclose(x1, x, err_msg=f"Roundtrip minimal length signal ({n=})" +
|
||||
f" for {m_num} sample window failed!")
|
||||
with pytest.raises(ValueError, match=rf"len\(x\)={n-1} must be >= ceil.*"):
|
||||
SFT.stft(x[:-1])
|
||||
with pytest.raises(ValueError, match=rf"S.shape\[t_axis\]={Sx.shape[1]-1}"
|
||||
f" needs to have at least {Sx.shape[1]} slices"):
|
||||
SFT.istft(Sx[:, :-1], k1=n)
|
||||
|
||||
|
||||
def test_tutorial_stft_sliding_win():
|
||||
"""Verify example in "Sliding Windows" subsection from the "User Guide".
|
||||
|
||||
In :ref:`tutorial_stft_sliding_win` (file ``signal.rst``) of the
|
||||
:ref:`user_guide` the behavior the border behavior of
|
||||
``ShortTimeFFT(np.ones(6), 2, fs=1)`` with a 50 sample signal is discussed.
|
||||
This test verifies the presented indexes.
|
||||
"""
|
||||
SFT = ShortTimeFFT(np.ones(6), 2, fs=1)
|
||||
|
||||
# Lower border:
|
||||
assert SFT.m_num_mid == 3, f"Slice middle is not 3 but {SFT.m_num_mid=}"
|
||||
assert SFT.p_min == -1, f"Lowest slice {SFT.p_min=} is not -1"
|
||||
assert SFT.k_min == -5, f"Lowest slice sample {SFT.p_min=} is not -5"
|
||||
k_lb, p_lb = SFT.lower_border_end
|
||||
assert p_lb == 2, f"First unaffected slice {p_lb=} is not 2"
|
||||
assert k_lb == 5, f"First unaffected sample {k_lb=} is not 5"
|
||||
|
||||
n = 50 # upper signal border
|
||||
assert (p_max := SFT.p_max(n)) == 27, f"Last slice {p_max=} must be 27"
|
||||
assert (k_max := SFT.k_max(n)) == 55, f"Last sample {k_max=} must be 55"
|
||||
k_ub, p_ub = SFT.upper_border_begin(n)
|
||||
assert p_ub == 24, f"First upper border slice {p_ub=} must be 24"
|
||||
assert k_ub == 45, f"First upper border slice {k_ub=} must be 45"
|
||||
|
||||
|
||||
def test_tutorial_stft_legacy_stft():
|
||||
"""Verify STFT example in "Comparison with Legacy Implementation" from the
|
||||
"User Guide".
|
||||
|
||||
In :ref:`tutorial_stft_legacy_stft` (file ``signal.rst``) of the
|
||||
:ref:`user_guide` the legacy and the new implementation are compared.
|
||||
"""
|
||||
fs, N = 200, 1001 # # 200 Hz sampling rate for 5 s signal
|
||||
t_z = np.arange(N) / fs # time indexes for signal
|
||||
z = np.exp(2j*np.pi * 70 * (t_z - 0.2 * t_z ** 2)) # complex-valued chirp
|
||||
|
||||
nperseg, noverlap = 50, 40
|
||||
win = ('gaussian', 1e-2 * fs) # Gaussian with 0.01 s standard deviation
|
||||
|
||||
# Legacy STFT:
|
||||
f0_u, t0, Sz0_u = stft(z, fs, win, nperseg, noverlap,
|
||||
return_onesided=False, scaling='spectrum')
|
||||
Sz0 = fftshift(Sz0_u, axes=0)
|
||||
|
||||
# New STFT:
|
||||
SFT = ShortTimeFFT.from_window(win, fs, nperseg, noverlap,
|
||||
fft_mode='centered',
|
||||
scale_to='magnitude', phase_shift=None)
|
||||
Sz1 = SFT.stft(z)
|
||||
|
||||
assert_allclose(Sz0, Sz1[:, 2:-1])
|
||||
|
||||
assert_allclose((abs(Sz1[:, 1]).min(), abs(Sz1[:, 1]).max()),
|
||||
(6.925060911593139e-07, 8.00271269218721e-07))
|
||||
|
||||
t0_r, z0_r = istft(Sz0_u, fs, win, nperseg, noverlap, input_onesided=False,
|
||||
scaling='spectrum')
|
||||
z1_r = SFT.istft(Sz1, k1=N)
|
||||
assert len(z0_r) == N + 9
|
||||
assert_allclose(z0_r[:N], z)
|
||||
assert_allclose(z1_r, z)
|
||||
|
||||
# Spectrogram is just the absolute square of th STFT:
|
||||
assert_allclose(SFT.spectrogram(z), abs(Sz1) ** 2)
|
||||
|
||||
|
||||
def test_tutorial_stft_legacy_spectrogram():
|
||||
"""Verify spectrogram example in "Comparison with Legacy Implementation"
|
||||
from the "User Guide".
|
||||
|
||||
In :ref:`tutorial_stft_legacy_stft` (file ``signal.rst``) of the
|
||||
:ref:`user_guide` the legacy and the new implementation are compared.
|
||||
"""
|
||||
fs, N = 200, 1001 # 200 Hz sampling rate for almost 5 s signal
|
||||
t_z = np.arange(N) / fs # time indexes for signal
|
||||
z = np.exp(2j*np.pi*70 * (t_z - 0.2*t_z**2)) # complex-valued sweep
|
||||
|
||||
nperseg, noverlap = 50, 40
|
||||
win = ('gaussian', 1e-2 * fs) # Gaussian with 0.01 s standard dev.
|
||||
|
||||
# Legacy spectrogram:
|
||||
f2_u, t2, Sz2_u = spectrogram(z, fs, win, nperseg, noverlap, detrend=None,
|
||||
return_onesided=False, scaling='spectrum',
|
||||
mode='complex')
|
||||
|
||||
f2, Sz2 = fftshift(f2_u), fftshift(Sz2_u, axes=0)
|
||||
|
||||
# New STFT:
|
||||
SFT = ShortTimeFFT.from_window(win, fs, nperseg, noverlap,
|
||||
fft_mode='centered', scale_to='magnitude',
|
||||
phase_shift=None)
|
||||
Sz3 = SFT.stft(z, p0=0, p1=(N-noverlap) // SFT.hop, k_offset=nperseg // 2)
|
||||
t3 = SFT.t(N, p0=0, p1=(N-noverlap) // SFT.hop, k_offset=nperseg // 2)
|
||||
|
||||
assert_allclose(t2, t3)
|
||||
assert_allclose(f2, SFT.f)
|
||||
assert_allclose(Sz2, Sz3)
|
||||
|
||||
|
||||
def test_permute_axes():
|
||||
"""Verify correctness of four-dimensional signal by permuting its
|
||||
shape. """
|
||||
n = 25
|
||||
SFT = ShortTimeFFT(np.ones(8)/8, hop=3, fs=n)
|
||||
x0 = np.arange(n)
|
||||
Sx0 = SFT.stft(x0)
|
||||
Sx0 = Sx0.reshape((Sx0.shape[0], 1, 1, 1, Sx0.shape[-1]))
|
||||
SxT = np.moveaxis(Sx0, (0, -1), (-1, 0))
|
||||
|
||||
atol = 2 * np.finfo(SFT.win.dtype).resolution
|
||||
for i in range(4):
|
||||
y = np.reshape(x0, np.roll((n, 1, 1, 1), i))
|
||||
Sy = SFT.stft(y, axis=i)
|
||||
assert_allclose(Sy, np.moveaxis(Sx0, 0, i))
|
||||
|
||||
yb0 = SFT.istft(Sy, k1=n, f_axis=i)
|
||||
assert_allclose(yb0, y, atol=atol)
|
||||
# explicit t-axis parameter (for coverage):
|
||||
yb1 = SFT.istft(Sy, k1=n, f_axis=i, t_axis=Sy.ndim-1)
|
||||
assert_allclose(yb1, y, atol=atol)
|
||||
|
||||
SyT = np.moveaxis(Sy, (i, -1), (-1, i))
|
||||
assert_allclose(SyT, np.moveaxis(SxT, 0, i))
|
||||
|
||||
ybT = SFT.istft(SyT, k1=n, t_axis=i, f_axis=-1)
|
||||
assert_allclose(ybT, y, atol=atol)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("fft_mode",
|
||||
('twosided', 'centered', 'onesided', 'onesided2X'))
|
||||
def test_roundtrip_multidimensional(fft_mode: FFT_MODE_TYPE):
|
||||
"""Test roundtrip of a multidimensional input signal versus its components.
|
||||
|
||||
This test can uncover potential problems with `fftshift()`.
|
||||
"""
|
||||
n = 9
|
||||
x = np.arange(4*n*2).reshape(4, n, 2)
|
||||
SFT = ShortTimeFFT(get_window('hann', 4), hop=2, fs=1,
|
||||
scale_to='magnitude', fft_mode=fft_mode)
|
||||
Sx = SFT.stft(x, axis=1)
|
||||
y = SFT.istft(Sx, k1=n, f_axis=1, t_axis=-1)
|
||||
assert_allclose(y, x, err_msg='Multidim. roundtrip failed!')
|
||||
|
||||
for i, j in product(range(x.shape[0]), range(x.shape[2])):
|
||||
y_ = SFT.istft(Sx[i, :, j, :], k1=n)
|
||||
assert_allclose(y_, x[i, :, j], err_msg="Multidim. roundtrip for component " +
|
||||
f"x[{i}, :, {j}] and {fft_mode=} failed!")
|
||||
|
||||
|
||||
@pytest.mark.parametrize('window, n, nperseg, noverlap',
|
||||
[('boxcar', 100, 10, 0), # Test no overlap
|
||||
('boxcar', 100, 10, 9), # Test high overlap
|
||||
('bartlett', 101, 51, 26), # Test odd nperseg
|
||||
('hann', 1024, 256, 128), # Test defaults
|
||||
(('tukey', 0.5), 1152, 256, 64), # Test Tukey
|
||||
('hann', 1024, 256, 255), # Test overlapped hann
|
||||
('boxcar', 100, 10, 3), # NOLA True, COLA False
|
||||
('bartlett', 101, 51, 37), # NOLA True, COLA False
|
||||
('hann', 1024, 256, 127), # NOLA True, COLA False
|
||||
# NOLA True, COLA False:
|
||||
(('tukey', 0.5), 1152, 256, 14),
|
||||
('hann', 1024, 256, 5)]) # NOLA True, COLA False
|
||||
def test_roundtrip_windows(window, n: int, nperseg: int, noverlap: int):
|
||||
"""Roundtrip test adapted from `test_spectral.TestSTFT`.
|
||||
|
||||
The parameters are taken from the methods test_roundtrip_real(),
|
||||
test_roundtrip_nola_not_cola(), test_roundtrip_float32(),
|
||||
test_roundtrip_complex().
|
||||
"""
|
||||
np.random.seed(2394655)
|
||||
|
||||
w = get_window(window, nperseg)
|
||||
SFT = ShortTimeFFT(w, nperseg - noverlap, fs=1, fft_mode='twosided',
|
||||
phase_shift=None)
|
||||
|
||||
z = 10 * np.random.randn(n) + 10j * np.random.randn(n)
|
||||
Sz = SFT.stft(z)
|
||||
z1 = SFT.istft(Sz, k1=len(z))
|
||||
assert_allclose(z, z1, err_msg="Roundtrip for complex values failed")
|
||||
|
||||
x = 10 * np.random.randn(n)
|
||||
Sx = SFT.stft(x)
|
||||
x1 = SFT.istft(Sx, k1=len(z))
|
||||
assert_allclose(x, x1, err_msg="Roundtrip for float values failed")
|
||||
|
||||
x32 = x.astype(np.float32)
|
||||
Sx32 = SFT.stft(x32)
|
||||
x32_1 = SFT.istft(Sx32, k1=len(x32))
|
||||
assert_allclose(x32, x32_1,
|
||||
err_msg="Roundtrip for 32 Bit float values failed")
|
||||
|
||||
|
||||
@pytest.mark.parametrize('signal_type', ('real', 'complex'))
|
||||
def test_roundtrip_complex_window(signal_type):
|
||||
"""Test roundtrip for complex-valued window function
|
||||
|
||||
The purpose of this test is to check if the dual window is calculated
|
||||
correctly for complex-valued windows.
|
||||
"""
|
||||
np.random.seed(1354654)
|
||||
win = np.exp(2j*np.linspace(0, np.pi, 8))
|
||||
SFT = ShortTimeFFT(win, 3, fs=1, fft_mode='twosided')
|
||||
|
||||
z = 10 * np.random.randn(11)
|
||||
if signal_type == 'complex':
|
||||
z = z + 2j * z
|
||||
Sz = SFT.stft(z)
|
||||
z1 = SFT.istft(Sz, k1=len(z))
|
||||
assert_allclose(z, z1,
|
||||
err_msg="Roundtrip for complex-valued window failed")
|
||||
|
||||
|
||||
def test_average_all_segments():
|
||||
"""Compare `welch` function with stft mean.
|
||||
|
||||
Ported from `TestSpectrogram.test_average_all_segments` from file
|
||||
``test__spectral.py``.
|
||||
"""
|
||||
x = np.random.randn(1024)
|
||||
|
||||
fs = 1.0
|
||||
window = ('tukey', 0.25)
|
||||
nperseg, noverlap = 16, 2
|
||||
fw, Pw = welch(x, fs, window, nperseg, noverlap)
|
||||
SFT = ShortTimeFFT.from_window(window, fs, nperseg, noverlap,
|
||||
fft_mode='onesided2X', scale_to='psd',
|
||||
phase_shift=None)
|
||||
# `welch` positions the window differently than the STFT:
|
||||
P = SFT.spectrogram(x, detr='constant', p0=0,
|
||||
p1=(len(x)-noverlap)//SFT.hop, k_offset=nperseg//2)
|
||||
|
||||
assert_allclose(SFT.f, fw)
|
||||
assert_allclose(np.mean(P, axis=-1), Pw)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('window, N, nperseg, noverlap, mfft',
|
||||
# from test_roundtrip_padded_FFT:
|
||||
[('hann', 1024, 256, 128, 512),
|
||||
('hann', 1024, 256, 128, 501),
|
||||
('boxcar', 100, 10, 0, 33),
|
||||
(('tukey', 0.5), 1152, 256, 64, 1024),
|
||||
# from test_roundtrip_padded_signal:
|
||||
('boxcar', 101, 10, 0, None),
|
||||
('hann', 1000, 256, 128, None),
|
||||
# from test_roundtrip_boundary_extension:
|
||||
('boxcar', 100, 10, 0, None),
|
||||
('boxcar', 100, 10, 9, None)])
|
||||
@pytest.mark.parametrize('padding', get_args(PAD_TYPE))
|
||||
def test_stft_padding_roundtrip(window, N: int, nperseg: int, noverlap: int,
|
||||
mfft: int, padding):
|
||||
"""Test the parameter 'padding' of `stft` with roundtrips.
|
||||
|
||||
The STFT parametrizations were taken from the methods
|
||||
`test_roundtrip_padded_FFT`, `test_roundtrip_padded_signal` and
|
||||
`test_roundtrip_boundary_extension` from class `TestSTFT` in file
|
||||
``test_spectral.py``. Note that the ShortTimeFFT does not need the
|
||||
concept of "boundary extension".
|
||||
"""
|
||||
x = normal_distribution.rvs(size=N, random_state=2909) # real signal
|
||||
z = x * np.exp(1j * np.pi / 4) # complex signal
|
||||
|
||||
SFT = ShortTimeFFT.from_window(window, 1, nperseg, noverlap,
|
||||
fft_mode='twosided', mfft=mfft)
|
||||
Sx = SFT.stft(x, padding=padding)
|
||||
x1 = SFT.istft(Sx, k1=N)
|
||||
assert_allclose(x1, x,
|
||||
err_msg=f"Failed real roundtrip with '{padding}' padding")
|
||||
|
||||
Sz = SFT.stft(z, padding=padding)
|
||||
z1 = SFT.istft(Sz, k1=N)
|
||||
assert_allclose(z1, z, err_msg="Failed complex roundtrip with " +
|
||||
f" '{padding}' padding")
|
||||
|
||||
|
||||
@pytest.mark.parametrize('N_x', (128, 129, 255, 256, 1337)) # signal length
|
||||
@pytest.mark.parametrize('w_size', (128, 256)) # window length
|
||||
@pytest.mark.parametrize('t_step', (4, 64)) # SFT time hop
|
||||
@pytest.mark.parametrize('f_c', (7., 23.)) # frequency of input sine
|
||||
def test_energy_conservation(N_x: int, w_size: int, t_step: int, f_c: float):
|
||||
"""Test if a `psd`-scaled STFT conserves the L2 norm.
|
||||
|
||||
This test is adapted from MNE-Python [1]_. Besides being battle-tested,
|
||||
this test has the benefit of using non-standard window including
|
||||
non-positive values and a 2d input signal.
|
||||
|
||||
Since `ShortTimeFFT` requires the signal length `N_x` to be at least the
|
||||
window length `w_size`, the parameter `N_x` was changed from
|
||||
``(127, 128, 255, 256, 1337)`` to ``(128, 129, 255, 256, 1337)`` to be
|
||||
more useful.
|
||||
|
||||
.. [1] File ``test_stft.py`` of MNE-Python
|
||||
https://github.com/mne-tools/mne-python/blob/main/mne/time_frequency/tests/test_stft.py
|
||||
"""
|
||||
window = np.sin(np.arange(.5, w_size + .5) / w_size * np.pi)
|
||||
SFT = ShortTimeFFT(window, t_step, fs=1000, fft_mode='onesided2X',
|
||||
scale_to='psd')
|
||||
atol = 2*np.finfo(window.dtype).resolution
|
||||
N_x = max(N_x, w_size) # minimal sing
|
||||
# Test with low frequency signal
|
||||
t = np.arange(N_x).astype(np.float64)
|
||||
x = np.sin(2 * np.pi * f_c * t * SFT.T)
|
||||
x = np.array([x, x + 1.])
|
||||
X = SFT.stft(x)
|
||||
xp = SFT.istft(X, k1=N_x)
|
||||
|
||||
max_freq = SFT.f[np.argmax(np.sum(np.abs(X[0]) ** 2, axis=1))]
|
||||
|
||||
assert X.shape[1] == SFT.f_pts
|
||||
assert np.all(SFT.f >= 0.)
|
||||
assert np.abs(max_freq - f_c) < 1.
|
||||
assert_allclose(x, xp, atol=atol)
|
||||
|
||||
# check L2-norm squared (i.e., energy) conservation:
|
||||
E_x = np.sum(x**2, axis=-1) * SFT.T # numerical integration
|
||||
aX2 = X.real**2 + X.imag.real**2
|
||||
E_X = np.sum(np.sum(aX2, axis=-1) * SFT.delta_t, axis=-1) * SFT.delta_f
|
||||
assert_allclose(E_X, E_x, atol=atol)
|
||||
|
||||
# Test with random signal
|
||||
np.random.seed(2392795)
|
||||
x = np.random.randn(2, N_x)
|
||||
X = SFT.stft(x)
|
||||
xp = SFT.istft(X, k1=N_x)
|
||||
|
||||
assert X.shape[1] == SFT.f_pts
|
||||
assert np.all(SFT.f >= 0.)
|
||||
assert np.abs(max_freq - f_c) < 1.
|
||||
assert_allclose(x, xp, atol=atol)
|
||||
|
||||
# check L2-norm squared (i.e., energy) conservation:
|
||||
E_x = np.sum(x**2, axis=-1) * SFT.T # numeric integration
|
||||
aX2 = X.real ** 2 + X.imag.real ** 2
|
||||
E_X = np.sum(np.sum(aX2, axis=-1) * SFT.delta_t, axis=-1) * SFT.delta_f
|
||||
assert_allclose(E_X, E_x, atol=atol)
|
||||
|
||||
# Try with empty array
|
||||
x = np.zeros((0, N_x))
|
||||
X = SFT.stft(x)
|
||||
xp = SFT.istft(X, k1=N_x)
|
||||
assert xp.shape == x.shape
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,287 @@
|
||||
# Code adapted from "upfirdn" python library with permission:
|
||||
#
|
||||
# Copyright (c) 2009, Motorola, Inc
|
||||
#
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
#
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
#
|
||||
# * Neither the name of Motorola nor the names of its contributors may be
|
||||
# used to endorse or promote products derived from this software without
|
||||
# specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
||||
# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
||||
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
import numpy as np
|
||||
from itertools import product
|
||||
|
||||
from numpy.testing import assert_equal, assert_allclose
|
||||
from pytest import raises as assert_raises
|
||||
import pytest
|
||||
|
||||
from scipy.signal import upfirdn, firwin
|
||||
from scipy.signal._upfirdn import _output_len, _upfirdn_modes
|
||||
from scipy.signal._upfirdn_apply import _pad_test
|
||||
|
||||
|
||||
def upfirdn_naive(x, h, up=1, down=1):
|
||||
"""Naive upfirdn processing in Python.
|
||||
|
||||
Note: arg order (x, h) differs to facilitate apply_along_axis use.
|
||||
"""
|
||||
h = np.asarray(h)
|
||||
out = np.zeros(len(x) * up, x.dtype)
|
||||
out[::up] = x
|
||||
out = np.convolve(h, out)[::down][:_output_len(len(h), len(x), up, down)]
|
||||
return out
|
||||
|
||||
|
||||
class UpFIRDnCase:
|
||||
"""Test _UpFIRDn object"""
|
||||
def __init__(self, up, down, h, x_dtype):
|
||||
self.up = up
|
||||
self.down = down
|
||||
self.h = np.atleast_1d(h)
|
||||
self.x_dtype = x_dtype
|
||||
self.rng = np.random.RandomState(17)
|
||||
|
||||
def __call__(self):
|
||||
# tiny signal
|
||||
self.scrub(np.ones(1, self.x_dtype))
|
||||
# ones
|
||||
self.scrub(np.ones(10, self.x_dtype)) # ones
|
||||
# randn
|
||||
x = self.rng.randn(10).astype(self.x_dtype)
|
||||
if self.x_dtype in (np.complex64, np.complex128):
|
||||
x += 1j * self.rng.randn(10)
|
||||
self.scrub(x)
|
||||
# ramp
|
||||
self.scrub(np.arange(10).astype(self.x_dtype))
|
||||
# 3D, random
|
||||
size = (2, 3, 5)
|
||||
x = self.rng.randn(*size).astype(self.x_dtype)
|
||||
if self.x_dtype in (np.complex64, np.complex128):
|
||||
x += 1j * self.rng.randn(*size)
|
||||
for axis in range(len(size)):
|
||||
self.scrub(x, axis=axis)
|
||||
x = x[:, ::2, 1::3].T
|
||||
for axis in range(len(size)):
|
||||
self.scrub(x, axis=axis)
|
||||
|
||||
def scrub(self, x, axis=-1):
|
||||
yr = np.apply_along_axis(upfirdn_naive, axis, x,
|
||||
self.h, self.up, self.down)
|
||||
want_len = _output_len(len(self.h), x.shape[axis], self.up, self.down)
|
||||
assert yr.shape[axis] == want_len
|
||||
y = upfirdn(self.h, x, self.up, self.down, axis=axis)
|
||||
assert y.shape[axis] == want_len
|
||||
assert y.shape == yr.shape
|
||||
dtypes = (self.h.dtype, x.dtype)
|
||||
if all(d == np.complex64 for d in dtypes):
|
||||
assert_equal(y.dtype, np.complex64)
|
||||
elif np.complex64 in dtypes and np.float32 in dtypes:
|
||||
assert_equal(y.dtype, np.complex64)
|
||||
elif all(d == np.float32 for d in dtypes):
|
||||
assert_equal(y.dtype, np.float32)
|
||||
elif np.complex128 in dtypes or np.complex64 in dtypes:
|
||||
assert_equal(y.dtype, np.complex128)
|
||||
else:
|
||||
assert_equal(y.dtype, np.float64)
|
||||
assert_allclose(yr, y)
|
||||
|
||||
|
||||
_UPFIRDN_TYPES = (int, np.float32, np.complex64, float, complex)
|
||||
|
||||
|
||||
class TestUpfirdn:
|
||||
|
||||
def test_valid_input(self):
|
||||
assert_raises(ValueError, upfirdn, [1], [1], 1, 0) # up or down < 1
|
||||
assert_raises(ValueError, upfirdn, [], [1], 1, 1) # h.ndim != 1
|
||||
assert_raises(ValueError, upfirdn, [[1]], [1], 1, 1)
|
||||
|
||||
@pytest.mark.parametrize('len_h', [1, 2, 3, 4, 5])
|
||||
@pytest.mark.parametrize('len_x', [1, 2, 3, 4, 5])
|
||||
def test_singleton(self, len_h, len_x):
|
||||
# gh-9844: lengths producing expected outputs
|
||||
h = np.zeros(len_h)
|
||||
h[len_h // 2] = 1. # make h a delta
|
||||
x = np.ones(len_x)
|
||||
y = upfirdn(h, x, 1, 1)
|
||||
want = np.pad(x, (len_h // 2, (len_h - 1) // 2), 'constant')
|
||||
assert_allclose(y, want)
|
||||
|
||||
def test_shift_x(self):
|
||||
# gh-9844: shifted x can change values?
|
||||
y = upfirdn([1, 1], [1.], 1, 1)
|
||||
assert_allclose(y, [1, 1]) # was [0, 1] in the issue
|
||||
y = upfirdn([1, 1], [0., 1.], 1, 1)
|
||||
assert_allclose(y, [0, 1, 1])
|
||||
|
||||
# A bunch of lengths/factors chosen because they exposed differences
|
||||
# between the "old way" and new way of computing length, and then
|
||||
# got `expected` from MATLAB
|
||||
@pytest.mark.parametrize('len_h, len_x, up, down, expected', [
|
||||
(2, 2, 5, 2, [1, 0, 0, 0]),
|
||||
(2, 3, 6, 3, [1, 0, 1, 0, 1]),
|
||||
(2, 4, 4, 3, [1, 0, 0, 0, 1]),
|
||||
(3, 2, 6, 2, [1, 0, 0, 1, 0]),
|
||||
(4, 11, 3, 5, [1, 0, 0, 1, 0, 0, 1]),
|
||||
])
|
||||
def test_length_factors(self, len_h, len_x, up, down, expected):
|
||||
# gh-9844: weird factors
|
||||
h = np.zeros(len_h)
|
||||
h[0] = 1.
|
||||
x = np.ones(len_x)
|
||||
y = upfirdn(h, x, up, down)
|
||||
assert_allclose(y, expected)
|
||||
|
||||
@pytest.mark.parametrize('down, want_len', [ # lengths from MATLAB
|
||||
(2, 5015),
|
||||
(11, 912),
|
||||
(79, 127),
|
||||
])
|
||||
def test_vs_convolve(self, down, want_len):
|
||||
# Check that up=1.0 gives same answer as convolve + slicing
|
||||
random_state = np.random.RandomState(17)
|
||||
try_types = (int, np.float32, np.complex64, float, complex)
|
||||
size = 10000
|
||||
|
||||
for dtype in try_types:
|
||||
x = random_state.randn(size).astype(dtype)
|
||||
if dtype in (np.complex64, np.complex128):
|
||||
x += 1j * random_state.randn(size)
|
||||
|
||||
h = firwin(31, 1. / down, window='hamming')
|
||||
yl = upfirdn_naive(x, h, 1, down)
|
||||
y = upfirdn(h, x, up=1, down=down)
|
||||
assert y.shape == (want_len,)
|
||||
assert yl.shape[0] == y.shape[0]
|
||||
assert_allclose(yl, y, atol=1e-7, rtol=1e-7)
|
||||
|
||||
@pytest.mark.parametrize('x_dtype', _UPFIRDN_TYPES)
|
||||
@pytest.mark.parametrize('h', (1., 1j))
|
||||
@pytest.mark.parametrize('up, down', [(1, 1), (2, 2), (3, 2), (2, 3)])
|
||||
def test_vs_naive_delta(self, x_dtype, h, up, down):
|
||||
UpFIRDnCase(up, down, h, x_dtype)()
|
||||
|
||||
@pytest.mark.parametrize('x_dtype', _UPFIRDN_TYPES)
|
||||
@pytest.mark.parametrize('h_dtype', _UPFIRDN_TYPES)
|
||||
@pytest.mark.parametrize('p_max, q_max',
|
||||
list(product((10, 100), (10, 100))))
|
||||
def test_vs_naive(self, x_dtype, h_dtype, p_max, q_max):
|
||||
tests = self._random_factors(p_max, q_max, h_dtype, x_dtype)
|
||||
for test in tests:
|
||||
test()
|
||||
|
||||
def _random_factors(self, p_max, q_max, h_dtype, x_dtype):
|
||||
n_rep = 3
|
||||
longest_h = 25
|
||||
random_state = np.random.RandomState(17)
|
||||
tests = []
|
||||
|
||||
for _ in range(n_rep):
|
||||
# Randomize the up/down factors somewhat
|
||||
p_add = q_max if p_max > q_max else 1
|
||||
q_add = p_max if q_max > p_max else 1
|
||||
p = random_state.randint(p_max) + p_add
|
||||
q = random_state.randint(q_max) + q_add
|
||||
|
||||
# Generate random FIR coefficients
|
||||
len_h = random_state.randint(longest_h) + 1
|
||||
h = np.atleast_1d(random_state.randint(len_h))
|
||||
h = h.astype(h_dtype)
|
||||
if h_dtype == complex:
|
||||
h += 1j * random_state.randint(len_h)
|
||||
|
||||
tests.append(UpFIRDnCase(p, q, h, x_dtype))
|
||||
|
||||
return tests
|
||||
|
||||
@pytest.mark.parametrize('mode', _upfirdn_modes)
|
||||
def test_extensions(self, mode):
|
||||
"""Test vs. manually computed results for modes not in numpy's pad."""
|
||||
x = np.array([1, 2, 3, 1], dtype=float)
|
||||
npre, npost = 6, 6
|
||||
y = _pad_test(x, npre=npre, npost=npost, mode=mode)
|
||||
if mode == 'antisymmetric':
|
||||
y_expected = np.asarray(
|
||||
[3, 1, -1, -3, -2, -1, 1, 2, 3, 1, -1, -3, -2, -1, 1, 2])
|
||||
elif mode == 'antireflect':
|
||||
y_expected = np.asarray(
|
||||
[1, 2, 3, 1, -1, 0, 1, 2, 3, 1, -1, 0, 1, 2, 3, 1])
|
||||
elif mode == 'smooth':
|
||||
y_expected = np.asarray(
|
||||
[-5, -4, -3, -2, -1, 0, 1, 2, 3, 1, -1, -3, -5, -7, -9, -11])
|
||||
elif mode == "line":
|
||||
lin_slope = (x[-1] - x[0]) / (len(x) - 1)
|
||||
left = x[0] + np.arange(-npre, 0, 1) * lin_slope
|
||||
right = x[-1] + np.arange(1, npost + 1) * lin_slope
|
||||
y_expected = np.concatenate((left, x, right))
|
||||
else:
|
||||
y_expected = np.pad(x, (npre, npost), mode=mode)
|
||||
assert_allclose(y, y_expected)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'size, h_len, mode, dtype',
|
||||
product(
|
||||
[8],
|
||||
[4, 5, 26], # include cases with h_len > 2*size
|
||||
_upfirdn_modes,
|
||||
[np.float32, np.float64, np.complex64, np.complex128],
|
||||
)
|
||||
)
|
||||
def test_modes(self, size, h_len, mode, dtype):
|
||||
random_state = np.random.RandomState(5)
|
||||
x = random_state.randn(size).astype(dtype)
|
||||
if dtype in (np.complex64, np.complex128):
|
||||
x += 1j * random_state.randn(size)
|
||||
h = np.arange(1, 1 + h_len, dtype=x.real.dtype)
|
||||
|
||||
y = upfirdn(h, x, up=1, down=1, mode=mode)
|
||||
# expected result: pad the input, filter with zero padding, then crop
|
||||
npad = h_len - 1
|
||||
if mode in ['antisymmetric', 'antireflect', 'smooth', 'line']:
|
||||
# use _pad_test test function for modes not supported by np.pad.
|
||||
xpad = _pad_test(x, npre=npad, npost=npad, mode=mode)
|
||||
else:
|
||||
xpad = np.pad(x, npad, mode=mode)
|
||||
ypad = upfirdn(h, xpad, up=1, down=1, mode='constant')
|
||||
y_expected = ypad[npad:-npad]
|
||||
|
||||
atol = rtol = np.finfo(dtype).eps * 1e2
|
||||
assert_allclose(y, y_expected, atol=atol, rtol=rtol)
|
||||
|
||||
|
||||
def test_output_len_long_input():
|
||||
# Regression test for gh-17375. On Windows, a large enough input
|
||||
# that should have been well within the capabilities of 64 bit integers
|
||||
# would result in a 32 bit overflow because of a bug in Cython 0.29.32.
|
||||
len_h = 1001
|
||||
in_len = 10**8
|
||||
up = 320
|
||||
down = 441
|
||||
out_len = _output_len(len_h, in_len, up, down)
|
||||
# The expected value was computed "by hand" from the formula
|
||||
# (((in_len - 1) * up + len_h) - 1) // down + 1
|
||||
assert out_len == 72562360
|
||||
@ -0,0 +1,351 @@
|
||||
import numpy as np
|
||||
from numpy.testing import (assert_almost_equal, assert_equal,
|
||||
assert_, assert_allclose, assert_array_equal)
|
||||
from pytest import raises as assert_raises
|
||||
|
||||
import scipy.signal._waveforms as waveforms
|
||||
|
||||
|
||||
# These chirp_* functions are the instantaneous frequencies of the signals
|
||||
# returned by chirp().
|
||||
|
||||
def chirp_linear(t, f0, f1, t1):
|
||||
f = f0 + (f1 - f0) * t / t1
|
||||
return f
|
||||
|
||||
|
||||
def chirp_quadratic(t, f0, f1, t1, vertex_zero=True):
|
||||
if vertex_zero:
|
||||
f = f0 + (f1 - f0) * t**2 / t1**2
|
||||
else:
|
||||
f = f1 - (f1 - f0) * (t1 - t)**2 / t1**2
|
||||
return f
|
||||
|
||||
|
||||
def chirp_geometric(t, f0, f1, t1):
|
||||
f = f0 * (f1/f0)**(t/t1)
|
||||
return f
|
||||
|
||||
|
||||
def chirp_hyperbolic(t, f0, f1, t1):
|
||||
f = f0*f1*t1 / ((f0 - f1)*t + f1*t1)
|
||||
return f
|
||||
|
||||
|
||||
def compute_frequency(t, theta):
|
||||
"""
|
||||
Compute theta'(t)/(2*pi), where theta'(t) is the derivative of theta(t).
|
||||
"""
|
||||
# Assume theta and t are 1-D NumPy arrays.
|
||||
# Assume that t is uniformly spaced.
|
||||
dt = t[1] - t[0]
|
||||
f = np.diff(theta)/(2*np.pi) / dt
|
||||
tf = 0.5*(t[1:] + t[:-1])
|
||||
return tf, f
|
||||
|
||||
|
||||
class TestChirp:
|
||||
|
||||
def test_linear_at_zero(self):
|
||||
w = waveforms.chirp(t=0, f0=1.0, f1=2.0, t1=1.0, method='linear')
|
||||
assert_almost_equal(w, 1.0)
|
||||
|
||||
def test_linear_freq_01(self):
|
||||
method = 'linear'
|
||||
f0 = 1.0
|
||||
f1 = 2.0
|
||||
t1 = 1.0
|
||||
t = np.linspace(0, t1, 100)
|
||||
phase = waveforms._chirp_phase(t, f0, t1, f1, method)
|
||||
tf, f = compute_frequency(t, phase)
|
||||
abserr = np.max(np.abs(f - chirp_linear(tf, f0, f1, t1)))
|
||||
assert_(abserr < 1e-6)
|
||||
|
||||
def test_linear_freq_02(self):
|
||||
method = 'linear'
|
||||
f0 = 200.0
|
||||
f1 = 100.0
|
||||
t1 = 10.0
|
||||
t = np.linspace(0, t1, 100)
|
||||
phase = waveforms._chirp_phase(t, f0, t1, f1, method)
|
||||
tf, f = compute_frequency(t, phase)
|
||||
abserr = np.max(np.abs(f - chirp_linear(tf, f0, f1, t1)))
|
||||
assert_(abserr < 1e-6)
|
||||
|
||||
def test_quadratic_at_zero(self):
|
||||
w = waveforms.chirp(t=0, f0=1.0, f1=2.0, t1=1.0, method='quadratic')
|
||||
assert_almost_equal(w, 1.0)
|
||||
|
||||
def test_quadratic_at_zero2(self):
|
||||
w = waveforms.chirp(t=0, f0=1.0, f1=2.0, t1=1.0, method='quadratic',
|
||||
vertex_zero=False)
|
||||
assert_almost_equal(w, 1.0)
|
||||
|
||||
def test_quadratic_freq_01(self):
|
||||
method = 'quadratic'
|
||||
f0 = 1.0
|
||||
f1 = 2.0
|
||||
t1 = 1.0
|
||||
t = np.linspace(0, t1, 2000)
|
||||
phase = waveforms._chirp_phase(t, f0, t1, f1, method)
|
||||
tf, f = compute_frequency(t, phase)
|
||||
abserr = np.max(np.abs(f - chirp_quadratic(tf, f0, f1, t1)))
|
||||
assert_(abserr < 1e-6)
|
||||
|
||||
def test_quadratic_freq_02(self):
|
||||
method = 'quadratic'
|
||||
f0 = 20.0
|
||||
f1 = 10.0
|
||||
t1 = 10.0
|
||||
t = np.linspace(0, t1, 2000)
|
||||
phase = waveforms._chirp_phase(t, f0, t1, f1, method)
|
||||
tf, f = compute_frequency(t, phase)
|
||||
abserr = np.max(np.abs(f - chirp_quadratic(tf, f0, f1, t1)))
|
||||
assert_(abserr < 1e-6)
|
||||
|
||||
def test_logarithmic_at_zero(self):
|
||||
w = waveforms.chirp(t=0, f0=1.0, f1=2.0, t1=1.0, method='logarithmic')
|
||||
assert_almost_equal(w, 1.0)
|
||||
|
||||
def test_logarithmic_freq_01(self):
|
||||
method = 'logarithmic'
|
||||
f0 = 1.0
|
||||
f1 = 2.0
|
||||
t1 = 1.0
|
||||
t = np.linspace(0, t1, 10000)
|
||||
phase = waveforms._chirp_phase(t, f0, t1, f1, method)
|
||||
tf, f = compute_frequency(t, phase)
|
||||
abserr = np.max(np.abs(f - chirp_geometric(tf, f0, f1, t1)))
|
||||
assert_(abserr < 1e-6)
|
||||
|
||||
def test_logarithmic_freq_02(self):
|
||||
method = 'logarithmic'
|
||||
f0 = 200.0
|
||||
f1 = 100.0
|
||||
t1 = 10.0
|
||||
t = np.linspace(0, t1, 10000)
|
||||
phase = waveforms._chirp_phase(t, f0, t1, f1, method)
|
||||
tf, f = compute_frequency(t, phase)
|
||||
abserr = np.max(np.abs(f - chirp_geometric(tf, f0, f1, t1)))
|
||||
assert_(abserr < 1e-6)
|
||||
|
||||
def test_logarithmic_freq_03(self):
|
||||
method = 'logarithmic'
|
||||
f0 = 100.0
|
||||
f1 = 100.0
|
||||
t1 = 10.0
|
||||
t = np.linspace(0, t1, 10000)
|
||||
phase = waveforms._chirp_phase(t, f0, t1, f1, method)
|
||||
tf, f = compute_frequency(t, phase)
|
||||
abserr = np.max(np.abs(f - chirp_geometric(tf, f0, f1, t1)))
|
||||
assert_(abserr < 1e-6)
|
||||
|
||||
def test_hyperbolic_at_zero(self):
|
||||
w = waveforms.chirp(t=0, f0=10.0, f1=1.0, t1=1.0, method='hyperbolic')
|
||||
assert_almost_equal(w, 1.0)
|
||||
|
||||
def test_hyperbolic_freq_01(self):
|
||||
method = 'hyperbolic'
|
||||
t1 = 1.0
|
||||
t = np.linspace(0, t1, 10000)
|
||||
# f0 f1
|
||||
cases = [[10.0, 1.0],
|
||||
[1.0, 10.0],
|
||||
[-10.0, -1.0],
|
||||
[-1.0, -10.0]]
|
||||
for f0, f1 in cases:
|
||||
phase = waveforms._chirp_phase(t, f0, t1, f1, method)
|
||||
tf, f = compute_frequency(t, phase)
|
||||
expected = chirp_hyperbolic(tf, f0, f1, t1)
|
||||
assert_allclose(f, expected)
|
||||
|
||||
def test_hyperbolic_zero_freq(self):
|
||||
# f0=0 or f1=0 must raise a ValueError.
|
||||
method = 'hyperbolic'
|
||||
t1 = 1.0
|
||||
t = np.linspace(0, t1, 5)
|
||||
assert_raises(ValueError, waveforms.chirp, t, 0, t1, 1, method)
|
||||
assert_raises(ValueError, waveforms.chirp, t, 1, t1, 0, method)
|
||||
|
||||
def test_unknown_method(self):
|
||||
method = "foo"
|
||||
f0 = 10.0
|
||||
f1 = 20.0
|
||||
t1 = 1.0
|
||||
t = np.linspace(0, t1, 10)
|
||||
assert_raises(ValueError, waveforms.chirp, t, f0, t1, f1, method)
|
||||
|
||||
def test_integer_t1(self):
|
||||
f0 = 10.0
|
||||
f1 = 20.0
|
||||
t = np.linspace(-1, 1, 11)
|
||||
t1 = 3.0
|
||||
float_result = waveforms.chirp(t, f0, t1, f1)
|
||||
t1 = 3
|
||||
int_result = waveforms.chirp(t, f0, t1, f1)
|
||||
err_msg = "Integer input 't1=3' gives wrong result"
|
||||
assert_equal(int_result, float_result, err_msg=err_msg)
|
||||
|
||||
def test_integer_f0(self):
|
||||
f1 = 20.0
|
||||
t1 = 3.0
|
||||
t = np.linspace(-1, 1, 11)
|
||||
f0 = 10.0
|
||||
float_result = waveforms.chirp(t, f0, t1, f1)
|
||||
f0 = 10
|
||||
int_result = waveforms.chirp(t, f0, t1, f1)
|
||||
err_msg = "Integer input 'f0=10' gives wrong result"
|
||||
assert_equal(int_result, float_result, err_msg=err_msg)
|
||||
|
||||
def test_integer_f1(self):
|
||||
f0 = 10.0
|
||||
t1 = 3.0
|
||||
t = np.linspace(-1, 1, 11)
|
||||
f1 = 20.0
|
||||
float_result = waveforms.chirp(t, f0, t1, f1)
|
||||
f1 = 20
|
||||
int_result = waveforms.chirp(t, f0, t1, f1)
|
||||
err_msg = "Integer input 'f1=20' gives wrong result"
|
||||
assert_equal(int_result, float_result, err_msg=err_msg)
|
||||
|
||||
def test_integer_all(self):
|
||||
f0 = 10
|
||||
t1 = 3
|
||||
f1 = 20
|
||||
t = np.linspace(-1, 1, 11)
|
||||
float_result = waveforms.chirp(t, float(f0), float(t1), float(f1))
|
||||
int_result = waveforms.chirp(t, f0, t1, f1)
|
||||
err_msg = "Integer input 'f0=10, t1=3, f1=20' gives wrong result"
|
||||
assert_equal(int_result, float_result, err_msg=err_msg)
|
||||
|
||||
|
||||
class TestSweepPoly:
|
||||
|
||||
def test_sweep_poly_quad1(self):
|
||||
p = np.poly1d([1.0, 0.0, 1.0])
|
||||
t = np.linspace(0, 3.0, 10000)
|
||||
phase = waveforms._sweep_poly_phase(t, p)
|
||||
tf, f = compute_frequency(t, phase)
|
||||
expected = p(tf)
|
||||
abserr = np.max(np.abs(f - expected))
|
||||
assert_(abserr < 1e-6)
|
||||
|
||||
def test_sweep_poly_const(self):
|
||||
p = np.poly1d(2.0)
|
||||
t = np.linspace(0, 3.0, 10000)
|
||||
phase = waveforms._sweep_poly_phase(t, p)
|
||||
tf, f = compute_frequency(t, phase)
|
||||
expected = p(tf)
|
||||
abserr = np.max(np.abs(f - expected))
|
||||
assert_(abserr < 1e-6)
|
||||
|
||||
def test_sweep_poly_linear(self):
|
||||
p = np.poly1d([-1.0, 10.0])
|
||||
t = np.linspace(0, 3.0, 10000)
|
||||
phase = waveforms._sweep_poly_phase(t, p)
|
||||
tf, f = compute_frequency(t, phase)
|
||||
expected = p(tf)
|
||||
abserr = np.max(np.abs(f - expected))
|
||||
assert_(abserr < 1e-6)
|
||||
|
||||
def test_sweep_poly_quad2(self):
|
||||
p = np.poly1d([1.0, 0.0, -2.0])
|
||||
t = np.linspace(0, 3.0, 10000)
|
||||
phase = waveforms._sweep_poly_phase(t, p)
|
||||
tf, f = compute_frequency(t, phase)
|
||||
expected = p(tf)
|
||||
abserr = np.max(np.abs(f - expected))
|
||||
assert_(abserr < 1e-6)
|
||||
|
||||
def test_sweep_poly_cubic(self):
|
||||
p = np.poly1d([2.0, 1.0, 0.0, -2.0])
|
||||
t = np.linspace(0, 2.0, 10000)
|
||||
phase = waveforms._sweep_poly_phase(t, p)
|
||||
tf, f = compute_frequency(t, phase)
|
||||
expected = p(tf)
|
||||
abserr = np.max(np.abs(f - expected))
|
||||
assert_(abserr < 1e-6)
|
||||
|
||||
def test_sweep_poly_cubic2(self):
|
||||
"""Use an array of coefficients instead of a poly1d."""
|
||||
p = np.array([2.0, 1.0, 0.0, -2.0])
|
||||
t = np.linspace(0, 2.0, 10000)
|
||||
phase = waveforms._sweep_poly_phase(t, p)
|
||||
tf, f = compute_frequency(t, phase)
|
||||
expected = np.poly1d(p)(tf)
|
||||
abserr = np.max(np.abs(f - expected))
|
||||
assert_(abserr < 1e-6)
|
||||
|
||||
def test_sweep_poly_cubic3(self):
|
||||
"""Use a list of coefficients instead of a poly1d."""
|
||||
p = [2.0, 1.0, 0.0, -2.0]
|
||||
t = np.linspace(0, 2.0, 10000)
|
||||
phase = waveforms._sweep_poly_phase(t, p)
|
||||
tf, f = compute_frequency(t, phase)
|
||||
expected = np.poly1d(p)(tf)
|
||||
abserr = np.max(np.abs(f - expected))
|
||||
assert_(abserr < 1e-6)
|
||||
|
||||
|
||||
class TestGaussPulse:
|
||||
|
||||
def test_integer_fc(self):
|
||||
float_result = waveforms.gausspulse('cutoff', fc=1000.0)
|
||||
int_result = waveforms.gausspulse('cutoff', fc=1000)
|
||||
err_msg = "Integer input 'fc=1000' gives wrong result"
|
||||
assert_equal(int_result, float_result, err_msg=err_msg)
|
||||
|
||||
def test_integer_bw(self):
|
||||
float_result = waveforms.gausspulse('cutoff', bw=1.0)
|
||||
int_result = waveforms.gausspulse('cutoff', bw=1)
|
||||
err_msg = "Integer input 'bw=1' gives wrong result"
|
||||
assert_equal(int_result, float_result, err_msg=err_msg)
|
||||
|
||||
def test_integer_bwr(self):
|
||||
float_result = waveforms.gausspulse('cutoff', bwr=-6.0)
|
||||
int_result = waveforms.gausspulse('cutoff', bwr=-6)
|
||||
err_msg = "Integer input 'bwr=-6' gives wrong result"
|
||||
assert_equal(int_result, float_result, err_msg=err_msg)
|
||||
|
||||
def test_integer_tpr(self):
|
||||
float_result = waveforms.gausspulse('cutoff', tpr=-60.0)
|
||||
int_result = waveforms.gausspulse('cutoff', tpr=-60)
|
||||
err_msg = "Integer input 'tpr=-60' gives wrong result"
|
||||
assert_equal(int_result, float_result, err_msg=err_msg)
|
||||
|
||||
|
||||
class TestUnitImpulse:
|
||||
|
||||
def test_no_index(self):
|
||||
assert_array_equal(waveforms.unit_impulse(7), [1, 0, 0, 0, 0, 0, 0])
|
||||
assert_array_equal(waveforms.unit_impulse((3, 3)),
|
||||
[[1, 0, 0], [0, 0, 0], [0, 0, 0]])
|
||||
|
||||
def test_index(self):
|
||||
assert_array_equal(waveforms.unit_impulse(10, 3),
|
||||
[0, 0, 0, 1, 0, 0, 0, 0, 0, 0])
|
||||
assert_array_equal(waveforms.unit_impulse((3, 3), (1, 1)),
|
||||
[[0, 0, 0], [0, 1, 0], [0, 0, 0]])
|
||||
|
||||
# Broadcasting
|
||||
imp = waveforms.unit_impulse((4, 4), 2)
|
||||
assert_array_equal(imp, np.array([[0, 0, 0, 0],
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 1, 0],
|
||||
[0, 0, 0, 0]]))
|
||||
|
||||
def test_mid(self):
|
||||
assert_array_equal(waveforms.unit_impulse((3, 3), 'mid'),
|
||||
[[0, 0, 0], [0, 1, 0], [0, 0, 0]])
|
||||
assert_array_equal(waveforms.unit_impulse(9, 'mid'),
|
||||
[0, 0, 0, 0, 1, 0, 0, 0, 0])
|
||||
|
||||
def test_dtype(self):
|
||||
imp = waveforms.unit_impulse(7)
|
||||
assert_(np.issubdtype(imp.dtype, np.floating))
|
||||
|
||||
imp = waveforms.unit_impulse(5, 3, dtype=int)
|
||||
assert_(np.issubdtype(imp.dtype, np.integer))
|
||||
|
||||
imp = waveforms.unit_impulse((5, 2), (3, 1), dtype=complex)
|
||||
assert_(np.issubdtype(imp.dtype, np.complexfloating))
|
||||
@ -0,0 +1,161 @@
|
||||
import numpy as np
|
||||
from numpy.testing import (assert_equal,
|
||||
assert_array_equal, assert_array_almost_equal, assert_array_less, assert_,)
|
||||
import pytest
|
||||
|
||||
import scipy.signal._wavelets as wavelets
|
||||
|
||||
|
||||
class TestWavelets:
|
||||
def test_qmf(self):
|
||||
with pytest.deprecated_call():
|
||||
assert_array_equal(wavelets.qmf([1, 1]), [1, -1])
|
||||
|
||||
def test_daub(self):
|
||||
with pytest.deprecated_call():
|
||||
for i in range(1, 15):
|
||||
assert_equal(len(wavelets.daub(i)), i * 2)
|
||||
|
||||
def test_cascade(self):
|
||||
with pytest.deprecated_call():
|
||||
for J in range(1, 7):
|
||||
for i in range(1, 5):
|
||||
lpcoef = wavelets.daub(i)
|
||||
k = len(lpcoef)
|
||||
x, phi, psi = wavelets.cascade(lpcoef, J)
|
||||
assert_(len(x) == len(phi) == len(psi))
|
||||
assert_equal(len(x), (k - 1) * 2 ** J)
|
||||
|
||||
def test_morlet(self):
|
||||
with pytest.deprecated_call():
|
||||
x = wavelets.morlet(50, 4.1, complete=True)
|
||||
y = wavelets.morlet(50, 4.1, complete=False)
|
||||
# Test if complete and incomplete wavelet have same lengths:
|
||||
assert_equal(len(x), len(y))
|
||||
# Test if complete wavelet is less than incomplete wavelet:
|
||||
assert_array_less(x, y)
|
||||
|
||||
x = wavelets.morlet(10, 50, complete=False)
|
||||
y = wavelets.morlet(10, 50, complete=True)
|
||||
# For large widths complete and incomplete wavelets should be
|
||||
# identical within numerical precision:
|
||||
assert_equal(x, y)
|
||||
|
||||
# miscellaneous tests:
|
||||
x = np.array([1.73752399e-09 + 9.84327394e-25j,
|
||||
6.49471756e-01 + 0.00000000e+00j,
|
||||
1.73752399e-09 - 9.84327394e-25j])
|
||||
y = wavelets.morlet(3, w=2, complete=True)
|
||||
assert_array_almost_equal(x, y)
|
||||
|
||||
x = np.array([2.00947715e-09 + 9.84327394e-25j,
|
||||
7.51125544e-01 + 0.00000000e+00j,
|
||||
2.00947715e-09 - 9.84327394e-25j])
|
||||
y = wavelets.morlet(3, w=2, complete=False)
|
||||
assert_array_almost_equal(x, y, decimal=2)
|
||||
|
||||
x = wavelets.morlet(10000, s=4, complete=True)
|
||||
y = wavelets.morlet(20000, s=8, complete=True)[5000:15000]
|
||||
assert_array_almost_equal(x, y, decimal=2)
|
||||
|
||||
x = wavelets.morlet(10000, s=4, complete=False)
|
||||
assert_array_almost_equal(y, x, decimal=2)
|
||||
y = wavelets.morlet(20000, s=8, complete=False)[5000:15000]
|
||||
assert_array_almost_equal(x, y, decimal=2)
|
||||
|
||||
x = wavelets.morlet(10000, w=3, s=5, complete=True)
|
||||
y = wavelets.morlet(20000, w=3, s=10, complete=True)[5000:15000]
|
||||
assert_array_almost_equal(x, y, decimal=2)
|
||||
|
||||
x = wavelets.morlet(10000, w=3, s=5, complete=False)
|
||||
assert_array_almost_equal(y, x, decimal=2)
|
||||
y = wavelets.morlet(20000, w=3, s=10, complete=False)[5000:15000]
|
||||
assert_array_almost_equal(x, y, decimal=2)
|
||||
|
||||
x = wavelets.morlet(10000, w=7, s=10, complete=True)
|
||||
y = wavelets.morlet(20000, w=7, s=20, complete=True)[5000:15000]
|
||||
assert_array_almost_equal(x, y, decimal=2)
|
||||
|
||||
x = wavelets.morlet(10000, w=7, s=10, complete=False)
|
||||
assert_array_almost_equal(x, y, decimal=2)
|
||||
y = wavelets.morlet(20000, w=7, s=20, complete=False)[5000:15000]
|
||||
assert_array_almost_equal(x, y, decimal=2)
|
||||
|
||||
def test_morlet2(self):
|
||||
with pytest.deprecated_call():
|
||||
w = wavelets.morlet2(1.0, 0.5)
|
||||
expected = (np.pi**(-0.25) * np.sqrt(1/0.5)).astype(complex)
|
||||
assert_array_equal(w, expected)
|
||||
|
||||
lengths = [5, 11, 15, 51, 101]
|
||||
for length in lengths:
|
||||
w = wavelets.morlet2(length, 1.0)
|
||||
assert_(len(w) == length)
|
||||
max_loc = np.argmax(w)
|
||||
assert_(max_loc == (length // 2))
|
||||
|
||||
points = 100
|
||||
w = abs(wavelets.morlet2(points, 2.0))
|
||||
half_vec = np.arange(0, points // 2)
|
||||
assert_array_almost_equal(w[half_vec], w[-(half_vec + 1)])
|
||||
|
||||
x = np.array([5.03701224e-09 + 2.46742437e-24j,
|
||||
1.88279253e+00 + 0.00000000e+00j,
|
||||
5.03701224e-09 - 2.46742437e-24j])
|
||||
y = wavelets.morlet2(3, s=1/(2*np.pi), w=2)
|
||||
assert_array_almost_equal(x, y)
|
||||
|
||||
def test_ricker(self):
|
||||
with pytest.deprecated_call():
|
||||
w = wavelets.ricker(1.0, 1)
|
||||
expected = 2 / (np.sqrt(3 * 1.0) * (np.pi ** 0.25))
|
||||
assert_array_equal(w, expected)
|
||||
|
||||
lengths = [5, 11, 15, 51, 101]
|
||||
for length in lengths:
|
||||
w = wavelets.ricker(length, 1.0)
|
||||
assert_(len(w) == length)
|
||||
max_loc = np.argmax(w)
|
||||
assert_(max_loc == (length // 2))
|
||||
|
||||
points = 100
|
||||
w = wavelets.ricker(points, 2.0)
|
||||
half_vec = np.arange(0, points // 2)
|
||||
#Wavelet should be symmetric
|
||||
assert_array_almost_equal(w[half_vec], w[-(half_vec + 1)])
|
||||
|
||||
#Check zeros
|
||||
aas = [5, 10, 15, 20, 30]
|
||||
points = 99
|
||||
for a in aas:
|
||||
w = wavelets.ricker(points, a)
|
||||
vec = np.arange(0, points) - (points - 1.0) / 2
|
||||
exp_zero1 = np.argmin(np.abs(vec - a))
|
||||
exp_zero2 = np.argmin(np.abs(vec + a))
|
||||
assert_array_almost_equal(w[exp_zero1], 0)
|
||||
assert_array_almost_equal(w[exp_zero2], 0)
|
||||
|
||||
def test_cwt(self):
|
||||
with pytest.deprecated_call():
|
||||
widths = [1.0]
|
||||
def delta_wavelet(s, t):
|
||||
return np.array([1])
|
||||
len_data = 100
|
||||
test_data = np.sin(np.pi * np.arange(0, len_data) / 10.0)
|
||||
|
||||
#Test delta function input gives same data as output
|
||||
cwt_dat = wavelets.cwt(test_data, delta_wavelet, widths)
|
||||
assert_(cwt_dat.shape == (len(widths), len_data))
|
||||
assert_array_almost_equal(test_data, cwt_dat.flatten())
|
||||
|
||||
#Check proper shape on output
|
||||
widths = [1, 3, 4, 5, 10]
|
||||
cwt_dat = wavelets.cwt(test_data, wavelets.ricker, widths)
|
||||
assert_(cwt_dat.shape == (len(widths), len_data))
|
||||
|
||||
widths = [len_data * 10]
|
||||
#Note: this wavelet isn't defined quite right, but is fine for this test
|
||||
def flat_wavelet(l, w):
|
||||
return np.full(w, 1 / w)
|
||||
cwt_dat = wavelets.cwt(test_data, flat_wavelet, widths)
|
||||
assert_array_almost_equal(cwt_dat, np.mean(test_data))
|
||||
File diff suppressed because one or more lines are too long
20
venv/lib/python3.12/site-packages/scipy/signal/waveforms.py
Normal file
20
venv/lib/python3.12/site-packages/scipy/signal/waveforms.py
Normal file
@ -0,0 +1,20 @@
|
||||
# This file is not meant for public use and will be removed in SciPy v2.0.0.
|
||||
# Use the `scipy.signal` namespace for importing the functions
|
||||
# included below.
|
||||
|
||||
from scipy._lib.deprecation import _sub_module_deprecation
|
||||
|
||||
__all__ = [ # noqa: F822
|
||||
'sawtooth', 'square', 'gausspulse', 'chirp', 'sweep_poly',
|
||||
'unit_impulse',
|
||||
]
|
||||
|
||||
|
||||
def __dir__():
|
||||
return __all__
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
return _sub_module_deprecation(sub_package="signal", module="waveforms",
|
||||
private_modules=["_waveforms"], all=__all__,
|
||||
attribute=name)
|
||||
20
venv/lib/python3.12/site-packages/scipy/signal/wavelets.py
Normal file
20
venv/lib/python3.12/site-packages/scipy/signal/wavelets.py
Normal file
@ -0,0 +1,20 @@
|
||||
# This file is not meant for public use and will be removed in SciPy v2.0.0.
|
||||
# Use the `scipy.signal` namespace for importing the functions
|
||||
# included below.
|
||||
|
||||
from scipy._lib.deprecation import _sub_module_deprecation
|
||||
|
||||
__all__ = [ # noqa: F822
|
||||
'daub', 'qmf', 'cascade', 'morlet', 'ricker', 'morlet2', 'cwt',
|
||||
'convolve'
|
||||
]
|
||||
|
||||
|
||||
def __dir__():
|
||||
return __all__
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
return _sub_module_deprecation(sub_package="signal", module="wavelets",
|
||||
private_modules=["_wavelets"], all=__all__,
|
||||
attribute=name)
|
||||
@ -0,0 +1,52 @@
|
||||
"""
|
||||
Window functions (:mod:`scipy.signal.windows`)
|
||||
==============================================
|
||||
|
||||
The suite of window functions for filtering and spectral estimation.
|
||||
|
||||
.. currentmodule:: scipy.signal.windows
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
get_window -- Return a window of a given length and type.
|
||||
|
||||
barthann -- Bartlett-Hann window
|
||||
bartlett -- Bartlett window
|
||||
blackman -- Blackman window
|
||||
blackmanharris -- Minimum 4-term Blackman-Harris window
|
||||
bohman -- Bohman window
|
||||
boxcar -- Boxcar window
|
||||
chebwin -- Dolph-Chebyshev window
|
||||
cosine -- Cosine window
|
||||
dpss -- Discrete prolate spheroidal sequences
|
||||
exponential -- Exponential window
|
||||
flattop -- Flat top window
|
||||
gaussian -- Gaussian window
|
||||
general_cosine -- Generalized Cosine window
|
||||
general_gaussian -- Generalized Gaussian window
|
||||
general_hamming -- Generalized Hamming window
|
||||
hamming -- Hamming window
|
||||
hann -- Hann window
|
||||
kaiser -- Kaiser window
|
||||
kaiser_bessel_derived -- Kaiser-Bessel derived window
|
||||
lanczos -- Lanczos window also known as a sinc window
|
||||
nuttall -- Nuttall's minimum 4-term Blackman-Harris window
|
||||
parzen -- Parzen window
|
||||
taylor -- Taylor window
|
||||
triang -- Triangular window
|
||||
tukey -- Tukey window
|
||||
|
||||
"""
|
||||
|
||||
from ._windows import *
|
||||
|
||||
# Deprecated namespaces, to be removed in v2.0.0
|
||||
from . import windows
|
||||
|
||||
__all__ = ['boxcar', 'triang', 'parzen', 'bohman', 'blackman', 'nuttall',
|
||||
'blackmanharris', 'flattop', 'bartlett', 'barthann',
|
||||
'hamming', 'kaiser', 'kaiser_bessel_derived', 'gaussian',
|
||||
'general_gaussian', 'general_cosine', 'general_hamming',
|
||||
'chebwin', 'cosine', 'hann', 'exponential', 'tukey', 'taylor',
|
||||
'get_window', 'dpss', 'lanczos']
|
||||
2374
venv/lib/python3.12/site-packages/scipy/signal/windows/_windows.py
Normal file
2374
venv/lib/python3.12/site-packages/scipy/signal/windows/_windows.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,23 @@
|
||||
# This file is not meant for public use and will be removed in SciPy v2.0.0.
|
||||
# Use the `scipy.signal.windows` namespace for importing the functions
|
||||
# included below.
|
||||
|
||||
from scipy._lib.deprecation import _sub_module_deprecation
|
||||
|
||||
__all__ = [ # noqa: F822
|
||||
'boxcar', 'triang', 'parzen', 'bohman', 'blackman', 'nuttall',
|
||||
'blackmanharris', 'flattop', 'bartlett', 'barthann',
|
||||
'hamming', 'kaiser', 'gaussian', 'general_cosine',
|
||||
'general_gaussian', 'general_hamming', 'chebwin', 'cosine',
|
||||
'hann', 'exponential', 'tukey', 'taylor', 'dpss', 'get_window',
|
||||
]
|
||||
|
||||
|
||||
def __dir__():
|
||||
return __all__
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
return _sub_module_deprecation(sub_package="signal.windows", module="windows",
|
||||
private_modules=["_windows"], all=__all__,
|
||||
attribute=name)
|
||||
Reference in New Issue
Block a user