up follow livre
This commit is contained in:
parent
70a5c3465c
commit
cffb31c1ef
12198 changed files with 2562132 additions and 35 deletions
316
venv/lib/python3.13/site-packages/scipy/signal/__init__.py
Normal file
316
venv/lib/python3.13/site-packages/scipy/signal/__init__.py
Normal file
|
|
@ -0,0 +1,316 @@
|
|||
"""
|
||||
=======================================
|
||||
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.
|
||||
envelope -- Compute the envelope of a real- or complex-valued signal.
|
||||
|
||||
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.
|
||||
firwin_2d -- Windowed FIR filter design, with frequency response for
|
||||
-- 2D using 1D design.
|
||||
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.
|
||||
sosfreqz -- Digital filter frequency response for SOS format filter (legacy).
|
||||
freqz_sos -- Digital filter frequency response for SOS format filter.
|
||||
freqz_zpk -- Digital filter frequency response from ZPK coefficients.
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
closest_STFT_dual_window -- Calculate the STFT dual window of a given window \
|
||||
closest to a desired dual window.
|
||||
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 (legacy).
|
||||
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.
|
||||
|
||||
"""
|
||||
# bring in the public functionality from private namespaces
|
||||
|
||||
# mypy: ignore-errors
|
||||
|
||||
from ._support_alternative_backends import *
|
||||
from . import _support_alternative_backends
|
||||
__all__ = _support_alternative_backends.__all__
|
||||
del _support_alternative_backends, _signal_api, _delegators # noqa: F821
|
||||
|
||||
|
||||
# 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
|
||||
)
|
||||
|
||||
|
||||
from scipy._lib._testutils import PytestTester
|
||||
test = PytestTester(__name__)
|
||||
del PytestTester
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
264
venv/lib/python3.13/site-packages/scipy/signal/_arraytools.py
Normal file
264
venv/lib/python3.13/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
|
||||
575
venv/lib/python3.13/site-packages/scipy/signal/_czt.py
Normal file
575
venv/lib/python3.13/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)
|
||||
568
venv/lib/python3.13/site-packages/scipy/signal/_delegators.py
Normal file
568
venv/lib/python3.13/site-packages/scipy/signal/_delegators.py
Normal file
|
|
@ -0,0 +1,568 @@
|
|||
"""Delegators for alternative backends in scipy.signal.
|
||||
|
||||
The signature of `func_signature` must match the signature of signal.func.
|
||||
The job of a `func_signature` is to know which arguments of `signal.func`
|
||||
are arrays.
|
||||
|
||||
* signatures are generated by
|
||||
|
||||
--------------
|
||||
import inspect
|
||||
from scipy import signal
|
||||
|
||||
names = [x for x in dir(signal) if not x.startswith('_')]
|
||||
objs = [getattr(signal, name) for name in names]
|
||||
funcs = [obj for obj in objs if inspect.isroutine(obj)]
|
||||
|
||||
for func in funcs:
|
||||
try:
|
||||
sig = inspect.signature(func)
|
||||
except ValueError:
|
||||
sig = "( FIXME )"
|
||||
print(f"def {func.__name__}_signature{sig}:\n\treturn array_namespace(...
|
||||
)\n\n")
|
||||
---------------
|
||||
|
||||
* which arguments to delegate on: manually trawled the documentation for
|
||||
array-like and array arguments
|
||||
|
||||
"""
|
||||
import numpy as np
|
||||
from scipy._lib._array_api import array_namespace, np_compat
|
||||
|
||||
|
||||
def _skip_if_lti(arg):
|
||||
"""Handle `system` arg overloads.
|
||||
|
||||
ATM, only pass tuples through. Consider updating when cupyx.lti class
|
||||
is supported.
|
||||
"""
|
||||
if isinstance(arg, tuple):
|
||||
return arg
|
||||
else:
|
||||
return (None,)
|
||||
|
||||
|
||||
def _skip_if_str_or_tuple(window):
|
||||
"""Handle `window` being a str or a tuple or an array-like.
|
||||
"""
|
||||
if isinstance(window, str) or isinstance(window, tuple) or callable(window):
|
||||
return None
|
||||
else:
|
||||
return window
|
||||
|
||||
|
||||
def _skip_if_poly1d(arg):
|
||||
return None if isinstance(arg, np.poly1d) else arg
|
||||
|
||||
|
||||
###################
|
||||
|
||||
def abcd_normalize_signature(A=None, B=None, C=None, D=None):
|
||||
return array_namespace(A, B, C, D)
|
||||
|
||||
|
||||
def argrelextrema_signature(data, *args, **kwds):
|
||||
return array_namespace(data)
|
||||
|
||||
argrelmax_signature = argrelextrema_signature
|
||||
argrelmin_signature = argrelextrema_signature
|
||||
|
||||
|
||||
def band_stop_obj_signature(wp, ind, passb, stopb, gpass, gstop, type):
|
||||
return array_namespace(passb, stopb)
|
||||
|
||||
|
||||
def bessel_signature(N, Wn, *args, **kwds):
|
||||
return array_namespace(Wn)
|
||||
|
||||
butter_signature = bessel_signature
|
||||
|
||||
|
||||
def cheby2_signature(N, rs, Wn, *args, **kwds):
|
||||
return array_namespace(Wn)
|
||||
|
||||
|
||||
def cheby1_signature(N, rp, Wn, *args, **kwds):
|
||||
return array_namespace(Wn)
|
||||
|
||||
|
||||
def ellip_signature(N, rp, rs, Wn, *args, **kwds):
|
||||
return array_namespace(Wn)
|
||||
|
||||
|
||||
########################## XXX: no arrays in, arrays out
|
||||
def besselap_signature(N, norm='phase'):
|
||||
return np
|
||||
|
||||
def buttap_signature(N):
|
||||
return np
|
||||
|
||||
def cheb1ap_signature(N, rp):
|
||||
return np
|
||||
|
||||
|
||||
def cheb2ap_signature(N, rs):
|
||||
return np
|
||||
|
||||
def ellipap_signature(N, rp, rs):
|
||||
return np
|
||||
|
||||
def correlation_lags_signature(in1_len, in2_len, mode='full'):
|
||||
return np
|
||||
|
||||
|
||||
def czt_points_signature(m, w=None, a=(1+0j)):
|
||||
return np
|
||||
|
||||
|
||||
def gammatone_signature(freq, ftype, order=None, numtaps=None, fs=None):
|
||||
return np
|
||||
|
||||
|
||||
def iircomb_signature(w0, Q, ftype='notch', fs=2.0, *, pass_zero=False):
|
||||
return np
|
||||
|
||||
|
||||
def iirnotch_signature(w0, Q, fs=2.0):
|
||||
return np
|
||||
|
||||
|
||||
def iirpeak_signature(w0, Q, fs=2.0):
|
||||
return np
|
||||
|
||||
|
||||
def savgol_coeffs_signature(
|
||||
window_length, polyorder, deriv=0, delta=1.0, pos=None, use='conv'
|
||||
):
|
||||
return np
|
||||
|
||||
|
||||
def unit_impulse_signature(shape, idx=None, dtype=float):
|
||||
return np
|
||||
############################
|
||||
|
||||
|
||||
####################### XXX: no arrays, maybe arrays out
|
||||
def buttord_signature(wp, ws, gpass, gstop, analog=False, fs=None):
|
||||
return np
|
||||
|
||||
def cheb1ord_signature(wp, ws, gpass, gstop, analog=False, fs=None):
|
||||
return np
|
||||
|
||||
def cheb2ord_signature(wp, ws, gpass, gstop, analog=False, fs=None):
|
||||
return np
|
||||
|
||||
def ellipord_signature(wp, ws, gpass, gstop, analog=False, fs=None):
|
||||
return np
|
||||
###########################################
|
||||
|
||||
|
||||
########### NB: scalars in, scalars out
|
||||
def kaiser_atten_signature(numtaps, width):
|
||||
return np
|
||||
|
||||
def kaiser_beta_signature(a):
|
||||
return np
|
||||
|
||||
def kaiserord_signature(ripple, width):
|
||||
return np
|
||||
|
||||
def get_window_signature(window, Nx, fftbins=True, *, xp=None, device=None):
|
||||
return np if xp is None else xp
|
||||
#################################
|
||||
|
||||
|
||||
def bode_signature(system, w=None, n=100):
|
||||
return array_namespace(*_skip_if_lti(system), w)
|
||||
|
||||
dbode_signature = bode_signature
|
||||
|
||||
|
||||
def freqresp_signature(system, w=None, n=10000):
|
||||
return array_namespace(*_skip_if_lti(system), w)
|
||||
|
||||
dfreqresp_signature = freqresp_signature
|
||||
|
||||
|
||||
def impulse_signature(system, X0=None, T=None, N=None):
|
||||
return array_namespace(*_skip_if_lti(system), X0, T)
|
||||
|
||||
|
||||
def dimpulse_signature(system, x0=None, t=None, n=None):
|
||||
return array_namespace(*_skip_if_lti(system), x0, t)
|
||||
|
||||
|
||||
def lsim_signature(system, U, T, X0=None, interp=True):
|
||||
return array_namespace(*_skip_if_lti(system), U, T, X0)
|
||||
|
||||
|
||||
def dlsim_signature(system, u, t=None, x0=None):
|
||||
return array_namespace(*_skip_if_lti(system), u, t, x0)
|
||||
|
||||
|
||||
def step_signature(system, X0=None, T=None, N=None):
|
||||
return array_namespace(*_skip_if_lti(system), X0, T)
|
||||
|
||||
def dstep_signature(system, x0=None, t=None, n=None):
|
||||
return array_namespace(*_skip_if_lti(system), x0, t)
|
||||
|
||||
|
||||
def cont2discrete_signature(system, dt, method='zoh', alpha=None):
|
||||
return array_namespace(*_skip_if_lti(system))
|
||||
|
||||
|
||||
def bilinear_signature(b, a, fs=1.0):
|
||||
return array_namespace(b, a)
|
||||
|
||||
|
||||
def bilinear_zpk_signature(z, p, k, fs):
|
||||
return array_namespace(z, p)
|
||||
|
||||
|
||||
def chirp_signature(t,*args, **kwds):
|
||||
return array_namespace(t)
|
||||
|
||||
|
||||
############## XXX: array-likes in, str out
|
||||
def choose_conv_method_signature(in1, in2, *args, **kwds):
|
||||
return array_namespace(in1, in2)
|
||||
############################################
|
||||
|
||||
|
||||
def convolve_signature(in1, in2, *args, **kwds):
|
||||
return array_namespace(in1, in2)
|
||||
|
||||
fftconvolve_signature = convolve_signature
|
||||
oaconvolve_signature = convolve_signature
|
||||
correlate_signature = convolve_signature
|
||||
correlate_signature = convolve_signature
|
||||
convolve2d_signature = convolve_signature
|
||||
correlate2d_signature = convolve_signature
|
||||
|
||||
|
||||
def coherence_signature(x, y, fs=1.0, window='hann', *args, **kwds):
|
||||
return array_namespace(x, y, _skip_if_str_or_tuple(window))
|
||||
|
||||
|
||||
def csd_signature(x, y, fs=1.0, window='hann', *args, **kwds):
|
||||
return array_namespace(x, y, _skip_if_str_or_tuple(window))
|
||||
|
||||
|
||||
def periodogram_signature(x, fs=1.0, window='boxcar'):
|
||||
return array_namespace(x, _skip_if_str_or_tuple(window))
|
||||
|
||||
|
||||
def welch_signature(x, fs=1.0, window='hann', *args, **kwds):
|
||||
return array_namespace(x, _skip_if_str_or_tuple(window))
|
||||
|
||||
|
||||
def spectrogram_signature(x, fs=1.0, window=('tukey', 0.25), *args, **kwds):
|
||||
return array_namespace(x, _skip_if_str_or_tuple(window))
|
||||
|
||||
|
||||
def stft_signature(x, fs=1.0, window='hann', *args, **kwds):
|
||||
return array_namespace(x, _skip_if_str_or_tuple(window))
|
||||
|
||||
|
||||
def istft_signature(Zxx, fs=1.0, window='hann', *args, **kwds):
|
||||
return array_namespace(Zxx, _skip_if_str_or_tuple(window))
|
||||
|
||||
|
||||
def resample_signature(x, num, t=None, axis=0, window=None, domain='time'):
|
||||
return array_namespace(x, t, _skip_if_str_or_tuple(window))
|
||||
|
||||
|
||||
def resample_poly_signature(x, up, down, axis=0, window=('kaiser', 5.0), *args, **kwds):
|
||||
return array_namespace(x, _skip_if_str_or_tuple(window))
|
||||
|
||||
|
||||
def check_COLA_signature(window, nperseg, noverlap, tol=1e-10):
|
||||
return array_namespace(_skip_if_str_or_tuple(window))
|
||||
|
||||
|
||||
def check_NOLA_signature(window, nperseg, noverlap, tol=1e-10):
|
||||
return array_namespace(_skip_if_str_or_tuple(window))
|
||||
|
||||
|
||||
def czt_signature(x, *args, **kwds):
|
||||
return array_namespace(x)
|
||||
|
||||
decimate_signature = czt_signature
|
||||
gauss_spline_signature = czt_signature
|
||||
|
||||
|
||||
def deconvolve_signature(signal, divisor):
|
||||
return array_namespace(signal, divisor)
|
||||
|
||||
|
||||
def detrend_signature(data, axis=1, type='linear', bp=0, *args, **kwds):
|
||||
return array_namespace(data, bp)
|
||||
|
||||
|
||||
def filtfilt_signature(b, a, x, *args, **kwds):
|
||||
return array_namespace(b, a, x)
|
||||
|
||||
|
||||
def lfilter_signature(b, a, x, axis=-1, zi=None):
|
||||
return array_namespace(b, a, x, zi)
|
||||
|
||||
|
||||
def envelope_signature(z, *args, **kwds):
|
||||
return array_namespace(z)
|
||||
|
||||
|
||||
def find_peaks_signature(
|
||||
x, height=None, threshold=None, distance=None, prominence=None, width=None,
|
||||
wlen=None, rel_height=0.5, plateau_size=None
|
||||
):
|
||||
return array_namespace(x, height, threshold, prominence, width, plateau_size)
|
||||
|
||||
|
||||
def find_peaks_cwt_signature(
|
||||
vector, widths, wavelet=None, max_distances=None, *args, **kwds
|
||||
):
|
||||
return array_namespace(vector, widths, max_distances)
|
||||
|
||||
|
||||
def findfreqs_signature(num, den, N, kind='ba'):
|
||||
return array_namespace(num, den)
|
||||
|
||||
|
||||
def firls_signature(numtaps, bands, desired, *, weight=None, fs=None):
|
||||
return array_namespace(bands, desired, weight)
|
||||
|
||||
|
||||
def firwin_signature(numtaps, cutoff, *args, **kwds):
|
||||
if isinstance(cutoff, int | float):
|
||||
xp = np_compat
|
||||
else:
|
||||
xp = array_namespace(cutoff)
|
||||
return xp
|
||||
|
||||
|
||||
def firwin2_signature(numtaps, freq, gain, *args, **kwds):
|
||||
return array_namespace(freq, gain)
|
||||
|
||||
|
||||
def freqs_zpk_signature(z, p, k, worN, *args, **kwds):
|
||||
return array_namespace(z, p, worN)
|
||||
|
||||
freqz_zpk_signature = freqs_zpk_signature
|
||||
|
||||
|
||||
def freqs_signature(b, a, worN=200, *args, **kwds):
|
||||
return array_namespace(b, a, worN)
|
||||
|
||||
freqz_signature = freqs_signature
|
||||
|
||||
|
||||
def freqz_sos_signature(sos, worN=512, *args, **kwds):
|
||||
return array_namespace(sos, worN)
|
||||
|
||||
sosfreqz_signature = freqz_sos_signature
|
||||
|
||||
|
||||
def gausspulse_signature(t, *args, **kwds):
|
||||
arr_t = None if isinstance(t, str) else t
|
||||
return array_namespace(arr_t)
|
||||
|
||||
|
||||
def group_delay_signature(system, w=512, whole=False, fs=6.283185307179586):
|
||||
return array_namespace(_skip_if_str_or_tuple(system), w)
|
||||
|
||||
|
||||
def hilbert_signature(x, N=None, axis=-1):
|
||||
return array_namespace(x)
|
||||
|
||||
hilbert2_signature = hilbert_signature
|
||||
|
||||
|
||||
def iirdesign_signature(wp, ws, *args, **kwds):
|
||||
return array_namespace(wp, ws)
|
||||
|
||||
|
||||
def iirfilter_signature(N, Wn, *args, **kwds):
|
||||
return array_namespace(Wn)
|
||||
|
||||
|
||||
def invres_signature(r, p, k, tol=0.001, rtype='avg'):
|
||||
return array_namespace(r, p, k)
|
||||
|
||||
invresz_signature = invres_signature
|
||||
|
||||
|
||||
############################### XXX: excluded, blacklisted on CuPy (mismatched API)
|
||||
def lfilter_zi_signature(b, a):
|
||||
return array_namespace(b, a)
|
||||
|
||||
def sosfilt_zi_signature(sos):
|
||||
return array_namespace(sos)
|
||||
|
||||
# needs to be blacklisted on CuPy (is not implemented)
|
||||
def remez_signature(numtaps, bands, desired, *, weight=None, **kwds):
|
||||
return array_namespace(bands, desired, weight)
|
||||
#############################################
|
||||
|
||||
def lfiltic_signature(b, a, y, x=None):
|
||||
return array_namespace(b, a, y, x)
|
||||
|
||||
|
||||
def lombscargle_signature(
|
||||
x, y, freqs, precenter=False, normalize=False, *,
|
||||
weights=None, floating_mean=False
|
||||
):
|
||||
return array_namespace(x, y, freqs, weights)
|
||||
|
||||
|
||||
def lp2bp_signature(b, a, *args, **kwds):
|
||||
return array_namespace(b, a)
|
||||
|
||||
lp2bs_signature = lp2bp_signature
|
||||
lp2hp_signature = lp2bp_signature
|
||||
lp2lp_signature = lp2bp_signature
|
||||
|
||||
tf2zpk_signature = lp2bp_signature
|
||||
tf2sos_signature = lp2bp_signature
|
||||
|
||||
normalize_signature = lp2bp_signature
|
||||
residue_signature = lp2bp_signature
|
||||
residuez_signature = residue_signature
|
||||
|
||||
|
||||
def lp2bp_zpk_signature(z, p, k, *args, **kwds):
|
||||
return array_namespace(z, p)
|
||||
|
||||
lp2bs_zpk_signature = lp2bp_zpk_signature
|
||||
lp2hp_zpk_signature = lp2bs_zpk_signature
|
||||
lp2lp_zpk_signature = lp2bs_zpk_signature
|
||||
|
||||
|
||||
def zpk2sos_signature(z, p, k, *args, **kwds):
|
||||
return array_namespace(z, p)
|
||||
|
||||
zpk2ss_signature = zpk2sos_signature
|
||||
zpk2tf_signature = zpk2sos_signature
|
||||
|
||||
|
||||
def max_len_seq_signature(nbits, state=None, length=None, taps=None):
|
||||
return array_namespace(state, taps)
|
||||
|
||||
|
||||
def medfilt_signature(volume, kernel_size=None):
|
||||
return array_namespace(volume)
|
||||
|
||||
|
||||
def medfilt2d_signature(input, kernel_size=3):
|
||||
return array_namespace(input)
|
||||
|
||||
|
||||
def minimum_phase_signature(h, *args, **kwds):
|
||||
return array_namespace(h)
|
||||
|
||||
|
||||
def order_filter_signature(a, domain, rank):
|
||||
return array_namespace(a, domain)
|
||||
|
||||
|
||||
def peak_prominences_signature(x, peaks, *args, **kwds):
|
||||
return array_namespace(x, peaks)
|
||||
|
||||
|
||||
peak_widths_signature = peak_prominences_signature
|
||||
|
||||
|
||||
def place_poles_signature(A, B, poles, method='YT', rtol=0.001, maxiter=30):
|
||||
return array_namespace(A, B, poles)
|
||||
|
||||
|
||||
def savgol_filter_signature(x, *args, **kwds):
|
||||
return array_namespace(x)
|
||||
|
||||
|
||||
def sawtooth_signature(t, width=1):
|
||||
return array_namespace(t)
|
||||
|
||||
|
||||
def sepfir2d_signature(input, hrow, hcol):
|
||||
return array_namespace(input, hrow, hcol)
|
||||
|
||||
|
||||
def sos2tf_signature(sos):
|
||||
return array_namespace(sos)
|
||||
|
||||
|
||||
sos2zpk_signature = sos2tf_signature
|
||||
|
||||
|
||||
def sosfilt_signature(sos, x, axis=-1, zi=None):
|
||||
return array_namespace(sos, x, zi)
|
||||
|
||||
|
||||
def sosfiltfilt_signature(sos, x, *args, **kwds):
|
||||
return array_namespace(sos, x)
|
||||
|
||||
|
||||
def spline_filter_signature(Iin, lmbda=5.0):
|
||||
return array_namespace(Iin)
|
||||
|
||||
|
||||
def square_signature(t, duty=0.5):
|
||||
return array_namespace(t)
|
||||
|
||||
|
||||
def ss2tf_signature(A, B, C, D, input=0):
|
||||
return array_namespace(A, B, C, D)
|
||||
|
||||
ss2zpk_signature = ss2tf_signature
|
||||
|
||||
|
||||
def sweep_poly_signature(t, poly, phi=0):
|
||||
return array_namespace(t, _skip_if_poly1d(poly))
|
||||
|
||||
|
||||
def symiirorder1_signature(signal, c0, z1, precision=-1.0):
|
||||
return array_namespace(signal)
|
||||
|
||||
|
||||
def symiirorder2_signature(input, r, omega, precision=-1.0):
|
||||
return array_namespace(input, r, omega)
|
||||
|
||||
|
||||
def cspline1d_signature(signal, *args, **kwds):
|
||||
return array_namespace(signal)
|
||||
|
||||
qspline1d_signature = cspline1d_signature
|
||||
cspline2d_signature = cspline1d_signature
|
||||
qspline2d_signature = qspline1d_signature
|
||||
|
||||
|
||||
def cspline1d_eval_signature(cj, newx, *args, **kwds):
|
||||
return array_namespace(cj, newx)
|
||||
|
||||
qspline1d_eval_signature = cspline1d_eval_signature
|
||||
|
||||
|
||||
def tf2ss_signature(num, den):
|
||||
return array_namespace(num, den)
|
||||
|
||||
|
||||
def unique_roots_signature(p, tol=0.001, rtype='min'):
|
||||
return array_namespace(p)
|
||||
|
||||
|
||||
def upfirdn_signature(h, x, up=1, down=1, axis=-1, mode='constant', cval=0):
|
||||
return array_namespace(h, x)
|
||||
|
||||
|
||||
def vectorstrength_signature(events, period):
|
||||
return array_namespace(events, period)
|
||||
|
||||
|
||||
def wiener_signature(im, mysize=None, noise=None):
|
||||
return array_namespace(im)
|
||||
|
||||
|
||||
def zoom_fft_signature(x, fn, m=None, *, fs=2, endpoint=False, axis=-1):
|
||||
return array_namespace(x, fn)
|
||||
|
||||
5893
venv/lib/python3.13/site-packages/scipy/signal/_filter_design.py
Normal file
5893
venv/lib/python3.13/site-packages/scipy/signal/_filter_design.py
Normal file
File diff suppressed because it is too large
Load diff
1458
venv/lib/python3.13/site-packages/scipy/signal/_fir_filter_design.py
Normal file
1458
venv/lib/python3.13/site-packages/scipy/signal/_fir_filter_design.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,534 @@
|
|||
"""
|
||||
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 hasattr(system, 'to_discrete') and callable(system.to_discrete):
|
||||
return system.to_discrete(dt=dt, method=method, alpha=alpha)
|
||||
|
||||
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(f"Unknown transformation method '{method}'")
|
||||
|
||||
return ad, bd, cd, dd, dt
|
||||
3546
venv/lib/python3.13/site-packages/scipy/signal/_ltisys.py
Normal file
3546
venv/lib/python3.13/site-packages/scipy/signal/_ltisys.py
Normal file
File diff suppressed because it is too large
Load diff
139
venv/lib/python3.13/site-packages/scipy/signal/_max_len_seq.py
Normal file
139
venv/lib/python3.13/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.
1310
venv/lib/python3.13/site-packages/scipy/signal/_peak_finding.py
Normal file
1310
venv/lib/python3.13/site-packages/scipy/signal/_peak_finding.py
Normal file
File diff suppressed because it is too large
Load diff
Binary file not shown.
172
venv/lib/python3.13/site-packages/scipy/signal/_polyutils.py
Normal file
172
venv/lib/python3.13/site-packages/scipy/signal/_polyutils.py
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
"""Partial replacements for numpy polynomial routines, with Array API compatibility.
|
||||
|
||||
This module contains both "old-style", np.poly1d, routines from the main numpy
|
||||
namespace, and "new-style", np.polynomial.polynomial, routines.
|
||||
|
||||
To distinguish the two sets, the "new-style" routine names start with `npp_`
|
||||
"""
|
||||
import scipy._lib.array_api_extra as xpx
|
||||
from scipy._lib._array_api import xp_promote, xp_default_dtype
|
||||
|
||||
|
||||
def _sort_cmplx(arr, xp):
|
||||
# xp.sort is undefined for complex dtypes. Here we only need some
|
||||
# consistent way to sort a complex array, including equal magnitude elements.
|
||||
arr = xp.asarray(arr)
|
||||
if xp.isdtype(arr.dtype, 'complex floating'):
|
||||
sorter = abs(arr) + xp.real(arr) + xp.imag(arr)**3
|
||||
else:
|
||||
sorter = arr
|
||||
|
||||
idxs = xp.argsort(sorter)
|
||||
return arr[idxs]
|
||||
|
||||
|
||||
def polyroots(coef, *, xp):
|
||||
"""numpy.roots, best-effor replacement
|
||||
"""
|
||||
if coef.shape[0] < 2:
|
||||
return xp.asarray([], dtype=coef.dtype)
|
||||
|
||||
root_func = getattr(xp, 'roots', None)
|
||||
if root_func:
|
||||
# NB: cupy.roots is broken in CuPy 13.x, but CuPy is handled via delegation
|
||||
# so we never hit this code path with xp being cupy
|
||||
return root_func(coef)
|
||||
|
||||
# companion matrix
|
||||
n = coef.shape[0]
|
||||
a = xp.eye(n - 1, n - 1, k=-1, dtype=coef.dtype)
|
||||
a[:, -1] = -xp.flip(coef[1:]) / coef[0]
|
||||
|
||||
# non-symmetric eigenvalue problem is not in the spec but is available on e.g. torch
|
||||
if hasattr(xp.linalg, 'eigvals'):
|
||||
return xp.linalg.eigvals(a)
|
||||
else:
|
||||
import numpy as np
|
||||
return xp.asarray(np.linalg.eigvals(np.asarray(a)))
|
||||
|
||||
|
||||
# https://github.com/numpy/numpy/blob/v2.1.0/numpy/lib/_function_base_impl.py#L1874-L1925
|
||||
def _trim_zeros(filt, trim='fb'):
|
||||
first = 0
|
||||
trim = trim.upper()
|
||||
if 'F' in trim:
|
||||
for i in filt:
|
||||
if i != 0.:
|
||||
break
|
||||
else:
|
||||
first = first + 1
|
||||
last = filt.shape[0]
|
||||
if 'B' in trim:
|
||||
for i in filt[::-1]:
|
||||
if i != 0.:
|
||||
break
|
||||
else:
|
||||
last = last - 1
|
||||
return filt[first:last]
|
||||
|
||||
|
||||
# ### Old-style routines ###
|
||||
|
||||
|
||||
# https://github.com/numpy/numpy/blob/v2.2.0/numpy/lib/_polynomial_impl.py#L1232
|
||||
def _poly1d(c_or_r, *, xp):
|
||||
""" Constructor of np.poly1d object from an array of coefficients (r=False)
|
||||
"""
|
||||
c_or_r = xpx.atleast_nd(c_or_r, ndim=1, xp=xp)
|
||||
if c_or_r.ndim > 1:
|
||||
raise ValueError("Polynomial must be 1d only.")
|
||||
c_or_r = _trim_zeros(c_or_r, trim='f')
|
||||
if c_or_r.shape[0] == 0:
|
||||
c_or_r = xp.asarray([0], dtype=c_or_r.dtype)
|
||||
return c_or_r
|
||||
|
||||
|
||||
# https://github.com/numpy/numpy/blob/v2.2.0/numpy/lib/_polynomial_impl.py#L702-L779
|
||||
def polyval(p, x, *, xp):
|
||||
""" Old-style polynomial, `np.polyval`
|
||||
"""
|
||||
y = xp.zeros_like(x)
|
||||
|
||||
for pv in p:
|
||||
y = y * x + pv
|
||||
return y
|
||||
|
||||
|
||||
# https://github.com/numpy/numpy/blob/v2.2.0/numpy/lib/_polynomial_impl.py#L34-L157
|
||||
def poly(seq_of_zeros, *, xp):
|
||||
# Only reproduce the 1D variant of np.poly
|
||||
seq_of_zeros = xp.asarray(seq_of_zeros)
|
||||
seq_of_zeros = xpx.atleast_nd(seq_of_zeros, ndim=1, xp=xp)
|
||||
|
||||
if seq_of_zeros.shape[0] == 0:
|
||||
return 1.0
|
||||
|
||||
# prefer np.convolve etc, if available
|
||||
convolve_func = getattr(xp, 'convolve', None)
|
||||
if convolve_func is None:
|
||||
from scipy.signal import convolve as convolve_func
|
||||
|
||||
dt = seq_of_zeros.dtype
|
||||
a = xp.ones((1,), dtype=dt)
|
||||
one = xp.ones_like(seq_of_zeros[0])
|
||||
for zero in seq_of_zeros:
|
||||
a = convolve_func(a, xp.stack((one, -zero)), mode='full')
|
||||
|
||||
if xp.isdtype(a.dtype, 'complex floating'):
|
||||
# if complex roots are all complex conjugates, the roots are real.
|
||||
roots = xp.asarray(seq_of_zeros, dtype=xp.complex128)
|
||||
if xp.all(xp.sort(xp.imag(roots)) == xp.sort(xp.imag(xp.conj(roots)))):
|
||||
a = xp.asarray(xp.real(a), copy=True)
|
||||
|
||||
return a
|
||||
|
||||
|
||||
# https://github.com/numpy/numpy/blob/v2.2.0/numpy/lib/_polynomial_impl.py#L912
|
||||
def polymul(a1, a2, *, xp):
|
||||
a1, a2 = _poly1d(a1, xp=xp), _poly1d(a2, xp=xp)
|
||||
|
||||
# prefer np.convolve etc, if available
|
||||
convolve_func = getattr(xp, 'convolve', None)
|
||||
if convolve_func is None:
|
||||
from scipy.signal import convolve as convolve_func
|
||||
|
||||
val = convolve_func(a1, a2)
|
||||
return val
|
||||
|
||||
|
||||
# ### New-style routines ###
|
||||
|
||||
|
||||
# https://github.com/numpy/numpy/blob/v2.2.0/numpy/polynomial/polynomial.py#L663
|
||||
def npp_polyval(x, c, *, xp, tensor=True):
|
||||
if xp.isdtype(c.dtype, 'integral'):
|
||||
c = xp.astype(c, xp_default_dtype(xp))
|
||||
|
||||
c = xpx.atleast_nd(c, ndim=1, xp=xp)
|
||||
if isinstance(x, tuple | list):
|
||||
x = xp.asarray(x)
|
||||
if tensor:
|
||||
c = xp.reshape(c, (c.shape + (1,)*x.ndim))
|
||||
|
||||
c0, _ = xp_promote(c[-1, ...], x, broadcast=True, xp=xp)
|
||||
for i in range(2, c.shape[0] + 1):
|
||||
c0 = c[-i, ...] + c0*x
|
||||
return c0
|
||||
|
||||
|
||||
# https://github.com/numpy/numpy/blob/v2.2.0/numpy/polynomial/polynomial.py#L758-L842
|
||||
def npp_polyvalfromroots(x, r, *, xp, tensor=True):
|
||||
r = xpx.atleast_nd(r, ndim=1, xp=xp)
|
||||
# if r.dtype.char in '?bBhHiIlLqQpP':
|
||||
# r = r.astype(np.double)
|
||||
|
||||
if isinstance(x, tuple | list):
|
||||
x = xp.asarray(x)
|
||||
|
||||
if tensor:
|
||||
r = xp.reshape(r, r.shape + (1,) * x.ndim)
|
||||
elif x.ndim >= r.ndim:
|
||||
raise ValueError("x.ndim must be < r.ndim when tensor == False")
|
||||
return xp.prod(x - r, axis=0)
|
||||
|
|
@ -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 # type: ignore[attr-defined]
|
||||
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
|
||||
2187
venv/lib/python3.13/site-packages/scipy/signal/_short_time_fft.py
Normal file
2187
venv/lib/python3.13/site-packages/scipy/signal/_short_time_fft.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,30 @@
|
|||
"""This is the 'bare' scipy.signal API.
|
||||
|
||||
This --- private! --- module only collects implementations of public API
|
||||
for _support_alternative_backends.
|
||||
The latter --- also private! --- module adds delegation to CuPy etc and
|
||||
re-exports decorated names to __init__.py
|
||||
"""
|
||||
|
||||
from . import _sigtools, windows # noqa: F401
|
||||
from ._waveforms import * # noqa: F403
|
||||
from ._max_len_seq import max_len_seq # noqa: F401
|
||||
from ._upfirdn import upfirdn # noqa: F401
|
||||
|
||||
from ._spline import sepfir2d # noqa: F401
|
||||
|
||||
from ._spline_filters import * # noqa: F403
|
||||
from ._filter_design import * # noqa: F403
|
||||
from ._fir_filter_design import * # noqa: F403
|
||||
from ._ltisys import * # noqa: F403
|
||||
from ._lti_conversion import * # noqa: F403
|
||||
from ._signaltools import * # noqa: F403
|
||||
from ._savitzky_golay import savgol_coeffs, savgol_filter # noqa: F401
|
||||
from ._spectral_py import * # noqa: F403
|
||||
from ._short_time_fft import * # noqa: F403
|
||||
from ._peak_finding import * # noqa: F403
|
||||
from ._czt import * # noqa: F403
|
||||
from .windows import get_window # keep this one in signal namespace # noqa: F401
|
||||
|
||||
|
||||
__all__ = [s for s in dir() if not s.startswith('_')]
|
||||
5309
venv/lib/python3.13/site-packages/scipy/signal/_signaltools.py
Normal file
5309
venv/lib/python3.13/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.
2471
venv/lib/python3.13/site-packages/scipy/signal/_spectral_py.py
Normal file
2471
venv/lib/python3.13/site-packages/scipy/signal/_spectral_py.py
Normal file
File diff suppressed because it is too large
Load diff
Binary file not shown.
34
venv/lib/python3.13/site-packages/scipy/signal/_spline.pyi
Normal file
34
venv/lib/python3.13/site-packages/scipy/signal/_spline.pyi
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
|
||||
import numpy as np
|
||||
from numpy.typing import NDArray
|
||||
|
||||
FloatingArray = NDArray[np.float32] | NDArray[np.float64]
|
||||
ComplexArray = NDArray[np.complex64] | NDArray[np.complex128]
|
||||
FloatingComplexArray = FloatingArray | ComplexArray
|
||||
|
||||
|
||||
def symiirorder1_ic(signal: FloatingComplexArray,
|
||||
c0: float,
|
||||
z1: float,
|
||||
precision: float) -> FloatingComplexArray:
|
||||
...
|
||||
|
||||
|
||||
def symiirorder2_ic_fwd(signal: FloatingArray,
|
||||
r: float,
|
||||
omega: float,
|
||||
precision: float) -> FloatingArray:
|
||||
...
|
||||
|
||||
|
||||
def symiirorder2_ic_bwd(signal: FloatingArray,
|
||||
r: float,
|
||||
omega: float,
|
||||
precision: float) -> FloatingArray:
|
||||
...
|
||||
|
||||
|
||||
def sepfir2d(input: FloatingComplexArray,
|
||||
hrow: FloatingComplexArray,
|
||||
hcol: FloatingComplexArray) -> FloatingComplexArray:
|
||||
...
|
||||
|
|
@ -0,0 +1,848 @@
|
|||
import math
|
||||
|
||||
from numpy import (zeros_like, array, tan, arange, floor,
|
||||
r_, atleast_1d, greater, cos, add, sin,
|
||||
moveaxis, abs, complex64, float32)
|
||||
import numpy as np
|
||||
|
||||
from scipy._lib._array_api import array_namespace, xp_promote
|
||||
|
||||
from scipy._lib._util import normalize_axis_index
|
||||
|
||||
# From splinemodule.c
|
||||
from ._spline import sepfir2d, symiirorder1_ic, symiirorder2_ic_fwd, symiirorder2_ic_bwd
|
||||
from ._signaltools import lfilter, sosfilt, lfiltic
|
||||
from ._arraytools import axis_slice, axis_reverse
|
||||
|
||||
from scipy.interpolate import BSpline
|
||||
|
||||
|
||||
__all__ = ['spline_filter', 'gauss_spline',
|
||||
'cspline1d', 'qspline1d', 'qspline2d', 'cspline2d',
|
||||
'cspline1d_eval', 'qspline1d_eval', 'symiirorder1', 'symiirorder2']
|
||||
|
||||
|
||||
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 smoothing 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()
|
||||
|
||||
"""
|
||||
xp = array_namespace(Iin)
|
||||
Iin = np.asarray(Iin)
|
||||
|
||||
if Iin.dtype not in [np.float32, np.float64, np.complex64, np.complex128]:
|
||||
raise TypeError(f"Invalid data type for Iin: {Iin.dtype = }")
|
||||
|
||||
# XXX: note that complex-valued computations are done in single precision
|
||||
# this is historic, and the root reason is unclear,
|
||||
# see https://github.com/scipy/scipy/issues/9209
|
||||
# Attempting to work in complex double precision leads to symiirorder1
|
||||
# failing to converge for the boundary conditions.
|
||||
intype = Iin.dtype
|
||||
hcol = array([1.0, 4.0, 1.0], np.float32) / 6.0
|
||||
if intype == np.complex128:
|
||||
Iin = Iin.astype(np.complex64)
|
||||
|
||||
ck = cspline2d(Iin, lmbda)
|
||||
out = sepfir2d(ck, hcol, hcol)
|
||||
out = out.astype(intype)
|
||||
return xp.asarray(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
|
||||
|
||||
"""
|
||||
xp = array_namespace(x)
|
||||
x = xp.asarray(x)
|
||||
signsq = (n + 1) / 12.0
|
||||
return 1 / math.sqrt(2 * math.pi * signsq) * xp.exp(-x ** 2 / 2 / signsq)
|
||||
|
||||
|
||||
def _cubic(x):
|
||||
xp = array_namespace(x)
|
||||
|
||||
x = np.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 xp.asarray(out)
|
||||
|
||||
|
||||
def _quadratic(x):
|
||||
xp = array_namespace(x)
|
||||
|
||||
x = abs(np.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 xp.asarray(out)
|
||||
|
||||
|
||||
def _coeff_smooth(lam):
|
||||
xi = 1 - 96 * lam + 24 * lam * math.sqrt(3 + 144 * lam)
|
||||
omeg = math.atan2(math.sqrt(144 * lam - 1), math.sqrt(xi))
|
||||
rho = (24 * lam - 1 - math.sqrt(xi)) / (24 * lam)
|
||||
rho = rho * math.sqrt((48 * lam + 24 * lam * math.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):
|
||||
signal = np.asarray(signal)
|
||||
|
||||
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):
|
||||
signal = np.asarray(signal)
|
||||
|
||||
zi = -2 + math.sqrt(3)
|
||||
K = len(signal)
|
||||
powers = zi ** np.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, np.r_[1, -zi], np.atleast_1d(add.reduce(powers * signal)))
|
||||
|
||||
b = np.ones(1)
|
||||
a = np.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], np.atleast_1d(out_last))
|
||||
|
||||
b = np.asarray([-zi])
|
||||
output, _ = lfilter(b, a, yplus[-2::-1], zi=state)
|
||||
output = np.r_[output[::-1], out_last]
|
||||
return output * 6.0
|
||||
|
||||
|
||||
def _quadratic_coeff(signal):
|
||||
signal = np.asarray(signal)
|
||||
|
||||
zi = -3 + 2 * math.sqrt(2.0)
|
||||
K = len(signal)
|
||||
powers = zi ** np.arange(K)
|
||||
|
||||
if K == 1:
|
||||
yplus = signal[0] + zi * np.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, np.r_[1, -zi], np.atleast_1d(np.add.reduce(powers * signal)))
|
||||
|
||||
b = np.ones(1)
|
||||
a = np.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], np.atleast_1d(out_last))
|
||||
|
||||
b = np.asarray([-zi])
|
||||
output, _ = lfilter(b, a, yplus[-2::-1], zi=state)
|
||||
output = np.r_[output[::-1], out_last]
|
||||
return output * 8.0
|
||||
|
||||
|
||||
def compute_root_from_lambda(lamb):
|
||||
tmp = math.sqrt(3 + 144 * lamb)
|
||||
xi = 1 - 96 * lamb + 24 * lamb * tmp
|
||||
omega = math.atan(math.sqrt((144 * lamb - 1.0) / xi))
|
||||
tmp2 = math.sqrt(xi)
|
||||
r = ((24 * lamb - 1 - tmp2) / (24 * lamb) *
|
||||
math.sqrt(48*lamb + 24 * lamb * tmp) / tmp2)
|
||||
return r, omega
|
||||
|
||||
|
||||
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()
|
||||
|
||||
"""
|
||||
xp = array_namespace(signal)
|
||||
|
||||
if lamb != 0.0:
|
||||
ret = _cubic_smooth_coeff(signal, lamb)
|
||||
else:
|
||||
ret = _cubic_coeff(signal)
|
||||
return xp.asarray(ret)
|
||||
|
||||
|
||||
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()
|
||||
|
||||
"""
|
||||
xp = array_namespace(signal)
|
||||
|
||||
if lamb != 0.0:
|
||||
raise ValueError("Smoothing quadratic splines not supported yet.")
|
||||
else:
|
||||
return xp.asarray(_quadratic_coeff(signal))
|
||||
|
||||
|
||||
def collapse_2d(x, axis):
|
||||
x = moveaxis(x, axis, -1)
|
||||
x_shape = x.shape
|
||||
x = x.reshape(-1, x.shape[-1])
|
||||
if not x.flags.c_contiguous:
|
||||
x = x.copy()
|
||||
return x, x_shape
|
||||
|
||||
|
||||
def symiirorder_nd(func, input, *args, axis=-1, **kwargs):
|
||||
axis = normalize_axis_index(axis, input.ndim)
|
||||
input_shape = input.shape
|
||||
input_ndim = input.ndim
|
||||
if input.ndim > 1:
|
||||
input, input_shape = collapse_2d(input, axis)
|
||||
|
||||
out = func(input, *args, **kwargs)
|
||||
|
||||
if input_ndim > 1:
|
||||
out = out.reshape(input_shape)
|
||||
out = moveaxis(out, -1, axis)
|
||||
if not out.flags.c_contiguous:
|
||||
out = out.copy()
|
||||
return out
|
||||
|
||||
|
||||
def qspline2d(signal, lamb=0.0, precision=-1.0):
|
||||
"""
|
||||
Coefficients for 2-D quadratic (2nd order) B-spline.
|
||||
|
||||
Return the second-order B-spline coefficients over a regularly spaced
|
||||
input grid for the two-dimensional input image.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
input : ndarray
|
||||
The input signal.
|
||||
lamb : float
|
||||
Specifies the amount of smoothing in the transfer function.
|
||||
precision : float
|
||||
Specifies the precision for computing the infinite sum needed to apply
|
||||
mirror-symmetric boundary conditions.
|
||||
|
||||
Returns
|
||||
-------
|
||||
output : ndarray
|
||||
The filtered signal.
|
||||
"""
|
||||
if precision < 0.0 or precision >= 1.0:
|
||||
if signal.dtype in [float32, complex64]:
|
||||
precision = 1e-3
|
||||
else:
|
||||
precision = 1e-6
|
||||
|
||||
if lamb > 0:
|
||||
raise ValueError('lambda must be negative or zero')
|
||||
|
||||
# normal quadratic spline
|
||||
r = -3 + 2 * math.sqrt(2.0)
|
||||
c0 = -r * 8.0
|
||||
z1 = r
|
||||
|
||||
out = symiirorder_nd(symiirorder1, signal, c0, z1, precision, axis=-1)
|
||||
out = symiirorder_nd(symiirorder1, out, c0, z1, precision, axis=0)
|
||||
return out
|
||||
|
||||
|
||||
def cspline2d(signal, lamb=0.0, precision=-1.0):
|
||||
"""
|
||||
Coefficients for 2-D cubic (3rd order) B-spline.
|
||||
|
||||
Return the third-order B-spline coefficients over a regularly spaced
|
||||
input grid for the two-dimensional input image.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
input : ndarray
|
||||
The input signal.
|
||||
lamb : float
|
||||
Specifies the amount of smoothing in the transfer function.
|
||||
precision : float
|
||||
Specifies the precision for computing the infinite sum needed to apply
|
||||
mirror-symmetric boundary conditions.
|
||||
|
||||
Returns
|
||||
-------
|
||||
output : ndarray
|
||||
The filtered signal.
|
||||
"""
|
||||
xp = array_namespace(signal)
|
||||
signal = np.asarray(signal)
|
||||
|
||||
if precision < 0.0 or precision >= 1.0:
|
||||
if signal.dtype in [np.float32, np.complex64]:
|
||||
precision = 1e-3
|
||||
else:
|
||||
precision = 1e-6
|
||||
|
||||
if lamb <= 1 / 144.0:
|
||||
# Normal cubic spline
|
||||
r = -2 + math.sqrt(3.0)
|
||||
out = symiirorder_nd(
|
||||
symiirorder1, signal, -r * 6.0, r, precision=precision, axis=-1)
|
||||
out = symiirorder_nd(
|
||||
symiirorder1, out, -r * 6.0, r, precision=precision, axis=0)
|
||||
return out
|
||||
|
||||
r, omega = compute_root_from_lambda(lamb)
|
||||
out = symiirorder_nd(symiirorder2, signal, r, omega,
|
||||
precision=precision, axis=-1)
|
||||
out = symiirorder_nd(symiirorder2, out, r, omega,
|
||||
precision=precision, axis=0)
|
||||
return xp.asarray(out)
|
||||
|
||||
|
||||
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()
|
||||
|
||||
"""
|
||||
xp = array_namespace(cj, newx)
|
||||
|
||||
newx = (np.asarray(newx) - x0) / float(dx)
|
||||
cj = np.asarray(cj)
|
||||
|
||||
if cj.size == 0:
|
||||
raise ValueError("Spline coefficients 'cj' must not be empty.")
|
||||
|
||||
res = zeros_like(newx, dtype=cj.dtype)
|
||||
if res.size == 0:
|
||||
return xp.asarray(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 xp.asarray(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 xp.asarray(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()
|
||||
|
||||
"""
|
||||
xp = array_namespace(newx, cj)
|
||||
|
||||
newx = (np.asarray(newx) - x0) / dx
|
||||
res = np.zeros_like(newx)
|
||||
if res.size == 0:
|
||||
return xp.asarray(res)
|
||||
|
||||
cj = np.asarray(cj)
|
||||
if cj.size == 0:
|
||||
raise ValueError("Spline coefficients 'cj' must not be empty.")
|
||||
|
||||
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 xp.asarray(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 xp.asarray(res)
|
||||
|
||||
|
||||
def symiirorder1(signal, c0, z1, precision=-1.0):
|
||||
"""
|
||||
Implement a smoothing IIR filter with mirror-symmetric boundary conditions
|
||||
using a cascade of first-order sections.
|
||||
|
||||
The second section uses a reversed sequence. This implements a system with
|
||||
the following transfer function and mirror-symmetric boundary conditions::
|
||||
|
||||
c0
|
||||
H(z) = ---------------------
|
||||
(1-z1/z) (1 - z1 z)
|
||||
|
||||
The resulting signal will have mirror symmetric boundary conditions
|
||||
as well.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
signal : ndarray
|
||||
The input signal. If 2D, then the filter will be applied in a batched
|
||||
fashion across the last axis.
|
||||
c0, z1 : scalar
|
||||
Parameters in the transfer function.
|
||||
precision :
|
||||
Specifies the precision for calculating initial conditions
|
||||
of the recursive filter based on mirror-symmetric input.
|
||||
|
||||
Returns
|
||||
-------
|
||||
output : ndarray
|
||||
The filtered signal.
|
||||
"""
|
||||
xp = array_namespace(signal)
|
||||
signal = xp_promote(signal, force_floating=True, xp=xp)
|
||||
# This function uses C internals
|
||||
signal = np.asarray(signal)
|
||||
|
||||
if abs(z1) >= 1:
|
||||
raise ValueError('|z1| must be less than 1.0')
|
||||
|
||||
if signal.ndim > 2:
|
||||
raise ValueError('Input must be 1D or 2D')
|
||||
|
||||
squeeze_dim = False
|
||||
if signal.ndim == 1:
|
||||
signal = signal[None, :]
|
||||
squeeze_dim = True
|
||||
|
||||
y0 = symiirorder1_ic(signal, z1, precision)
|
||||
|
||||
# Apply first the system 1 / (1 - z1 * z^-1)
|
||||
b = np.ones(1, dtype=signal.dtype)
|
||||
a = np.r_[1, -z1]
|
||||
a = a.astype(signal.dtype)
|
||||
|
||||
# Compute the initial state for lfilter.
|
||||
zii = y0 * z1
|
||||
|
||||
y1, _ = lfilter(b, a, axis_slice(signal, 1), zi=zii)
|
||||
y1 = np.c_[y0, y1]
|
||||
|
||||
# Compute backward symmetric condition and apply the system
|
||||
# c0 / (1 - z1 * z)
|
||||
b = np.asarray([c0], dtype=signal.dtype)
|
||||
out_last = -c0 / (z1 - 1.0) * axis_slice(y1, -1)
|
||||
|
||||
# Compute the initial state for lfilter.
|
||||
zii = out_last * z1
|
||||
|
||||
# Apply the system c0 / (1 - z1 * z) by reversing the output of the previous stage
|
||||
out, _ = lfilter(b, a, axis_slice(y1, -2, step=-1), zi=zii)
|
||||
out = np.c_[axis_reverse(out), out_last]
|
||||
|
||||
if squeeze_dim:
|
||||
out = out[0]
|
||||
|
||||
return xp.asarray(out)
|
||||
|
||||
|
||||
def symiirorder2(input, r, omega, precision=-1.0):
|
||||
"""
|
||||
Implement a smoothing IIR filter with mirror-symmetric boundary conditions
|
||||
using a cascade of second-order sections.
|
||||
|
||||
The second section uses a reversed sequence. This implements the following
|
||||
transfer function::
|
||||
|
||||
cs^2
|
||||
H(z) = ---------------------------------------
|
||||
(1 - a2/z - a3/z^2) (1 - a2 z - a3 z^2 )
|
||||
|
||||
where::
|
||||
|
||||
a2 = 2 * r * cos(omega)
|
||||
a3 = - r ** 2
|
||||
cs = 1 - 2 * r * cos(omega) + r ** 2
|
||||
|
||||
Parameters
|
||||
----------
|
||||
input : ndarray
|
||||
The input signal.
|
||||
r, omega : float
|
||||
Parameters in the transfer function.
|
||||
precision : float
|
||||
Specifies the precision for calculating initial conditions
|
||||
of the recursive filter based on mirror-symmetric input.
|
||||
|
||||
Returns
|
||||
-------
|
||||
output : ndarray
|
||||
The filtered signal.
|
||||
"""
|
||||
xp = array_namespace(input)
|
||||
input = xp_promote(input, force_floating=True, xp=xp)
|
||||
# This function uses C internals
|
||||
input = np.ascontiguousarray(input)
|
||||
|
||||
if r >= 1.0:
|
||||
raise ValueError('r must be less than 1.0')
|
||||
|
||||
if input.ndim > 2:
|
||||
raise ValueError('Input must be 1D or 2D')
|
||||
|
||||
squeeze_dim = False
|
||||
if input.ndim == 1:
|
||||
input = input[None, :]
|
||||
squeeze_dim = True
|
||||
|
||||
rsq = r * r
|
||||
a2 = 2 * r * math.cos(omega)
|
||||
a3 = -rsq
|
||||
cs = 1 - 2 * r * math.cos(omega) + rsq
|
||||
sos = np.asarray([cs, 0, 0, 1, -a2, -a3], dtype=input.dtype)
|
||||
|
||||
# Find the starting (forward) conditions.
|
||||
ic_fwd = symiirorder2_ic_fwd(input, r, omega, precision)
|
||||
|
||||
# Apply first the system cs / (1 - a2 * z^-1 - a3 * z^-2)
|
||||
# Compute the initial conditions in the form expected by sosfilt
|
||||
# coef = np.asarray([[a3, a2], [0, a3]], dtype=input.dtype)
|
||||
coef = np.asarray([[a3, a2], [0, a3]], dtype=input.dtype)
|
||||
zi = np.matmul(coef, ic_fwd[:, :, None])[:, :, 0]
|
||||
|
||||
y_fwd, _ = sosfilt(sos, axis_slice(input, 2), zi=zi[None])
|
||||
y_fwd = np.c_[ic_fwd, y_fwd]
|
||||
|
||||
# Then compute the symmetric backward starting conditions
|
||||
ic_bwd = symiirorder2_ic_bwd(input, r, omega, precision)
|
||||
|
||||
# Apply the system cs / (1 - a2 * z^1 - a3 * z^2)
|
||||
# Compute the initial conditions in the form expected by sosfilt
|
||||
zi = np.matmul(coef, ic_bwd[:, :, None])[:, :, 0]
|
||||
y, _ = sosfilt(sos, axis_slice(y_fwd, -3, step=-1), zi=zi[None])
|
||||
out = np.c_[axis_reverse(y), axis_reverse(ic_bwd)]
|
||||
|
||||
if squeeze_dim:
|
||||
out = out[0]
|
||||
|
||||
return xp.asarray(out)
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
import functools
|
||||
from scipy._lib._array_api import (
|
||||
is_cupy, is_jax, scipy_namespace_for, SCIPY_ARRAY_API
|
||||
)
|
||||
|
||||
from ._signal_api import * # noqa: F403
|
||||
from . import _signal_api
|
||||
from . import _delegators
|
||||
__all__ = _signal_api.__all__
|
||||
|
||||
|
||||
MODULE_NAME = 'signal'
|
||||
|
||||
# jax.scipy.signal has only partial coverage of scipy.signal, so we keep the list
|
||||
# of functions we can delegate to JAX
|
||||
# https://jax.readthedocs.io/en/latest/jax.scipy.html
|
||||
JAX_SIGNAL_FUNCS = [
|
||||
'fftconvolve', 'convolve', 'convolve2d', 'correlate', 'correlate2d',
|
||||
'csd', 'detrend', 'istft', 'welch'
|
||||
]
|
||||
|
||||
# some cupyx.scipy.signal functions are incompatible with their scipy counterparts
|
||||
CUPY_BLACKLIST = ['lfilter_zi', 'sosfilt_zi', 'get_window', 'envelope', 'remez']
|
||||
|
||||
# freqz_sos is a sosfreqz rename, and cupy does not have the new name yet (in v13.x)
|
||||
CUPY_RENAMES = {'freqz_sos': 'sosfreqz'}
|
||||
|
||||
|
||||
def delegate_xp(delegator, module_name):
|
||||
def inner(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwds):
|
||||
try:
|
||||
xp = delegator(*args, **kwds)
|
||||
except TypeError:
|
||||
# object arrays
|
||||
import numpy as np
|
||||
xp = np
|
||||
|
||||
# try delegating to a cupyx/jax namesake
|
||||
if is_cupy(xp) and func.__name__ not in CUPY_BLACKLIST:
|
||||
func_name = CUPY_RENAMES.get(func.__name__, func.__name__)
|
||||
|
||||
# https://github.com/cupy/cupy/issues/8336
|
||||
import importlib
|
||||
cupyx_module = importlib.import_module(f"cupyx.scipy.{module_name}")
|
||||
cupyx_func = getattr(cupyx_module, func_name)
|
||||
return cupyx_func(*args, **kwds)
|
||||
elif is_jax(xp) and func.__name__ in JAX_SIGNAL_FUNCS:
|
||||
spx = scipy_namespace_for(xp)
|
||||
jax_module = getattr(spx, module_name)
|
||||
jax_func = getattr(jax_module, func.__name__)
|
||||
return jax_func(*args, **kwds)
|
||||
else:
|
||||
# the original function
|
||||
return func(*args, **kwds)
|
||||
return wrapper
|
||||
return inner
|
||||
|
||||
|
||||
|
||||
# ### decorate ###
|
||||
for obj_name in _signal_api.__all__:
|
||||
bare_obj = getattr(_signal_api, obj_name)
|
||||
delegator = getattr(_delegators, obj_name + "_signature", None)
|
||||
|
||||
if SCIPY_ARRAY_API and delegator is not None:
|
||||
f = delegate_xp(delegator, MODULE_NAME)(bare_obj)
|
||||
else:
|
||||
f = bare_obj
|
||||
|
||||
# add the decorated function to the namespace, to be imported in __init__.py
|
||||
vars()[obj_name] = f
|
||||
219
venv/lib/python3.13/site-packages/scipy/signal/_upfirdn.py
Normal file
219
venv/lib/python3.13/site-packages/scipy/signal/_upfirdn.py
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
# 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 scipy._lib._array_api import array_namespace
|
||||
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.]])
|
||||
"""
|
||||
xp = array_namespace(h, x)
|
||||
|
||||
x = np.asarray(x)
|
||||
ufd = _UpFIRDn(h, x.dtype, up, down)
|
||||
# This is equivalent to (but faster than) using np.apply_along_axis
|
||||
return xp.asarray(ufd.apply_filter(x, axis, mode, cval))
|
||||
Binary file not shown.
687
venv/lib/python3.13/site-packages/scipy/signal/_waveforms.py
Normal file
687
venv/lib/python3.13/site-packages/scipy/signal/_waveforms.py
Normal file
|
|
@ -0,0 +1,687 @@
|
|||
# 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))
|
||||
y = zeros(t.shape, dtype="d")
|
||||
|
||||
# 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))
|
||||
y = zeros(t.shape, dtype="d")
|
||||
|
||||
# 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.
|
||||
|
||||
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(f"Center frequency (fc={fc:.2f}) must be >=0.")
|
||||
if bw <= 0:
|
||||
raise ValueError(f"Fractional bandwidth (bw={bw:.2f}) must be > 0.")
|
||||
if bwr >= 0:
|
||||
raise ValueError(f"Reference level for bandwidth (bwr={bwr:.2f}) "
|
||||
"must be < 0 dB")
|
||||
|
||||
# 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, *,
|
||||
complex=False):
|
||||
r"""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.
|
||||
complex : bool, optional
|
||||
This parameter creates a complex-valued analytic signal instead of a
|
||||
real-valued signal. It allows the use of complex baseband (in communications
|
||||
domain). Default is False.
|
||||
|
||||
.. versionadded:: 1.15.0
|
||||
|
||||
Returns
|
||||
-------
|
||||
y : ndarray
|
||||
A numpy array containing the signal evaluated at `t` with the requested
|
||||
time-varying frequency. More precisely, the function returns
|
||||
``exp(1j*phase + 1j*(pi/180)*phi) if complex else cos(phase + (pi/180)*phi)``
|
||||
where `phase` is the integral (from 0 to `t`) of ``2*pi*f(t)``.
|
||||
The instantaneous frequency ``f(t)`` is defined below.
|
||||
|
||||
See Also
|
||||
--------
|
||||
sweep_poly
|
||||
|
||||
Notes
|
||||
-----
|
||||
There are four possible options for the parameter `method`, which have a (long)
|
||||
standard form and some allowed abbreviations. The formulas for the instantaneous
|
||||
frequency :math:`f(t)` of the generated signal are as follows:
|
||||
|
||||
1. Parameter `method` in ``('linear', 'lin', 'li')``:
|
||||
|
||||
.. math::
|
||||
f(t) = f_0 + \beta\, t \quad\text{with}\quad
|
||||
\beta = \frac{f_1 - f_0}{t_1}
|
||||
|
||||
Frequency :math:`f(t)` varies linearly over time with a constant rate
|
||||
:math:`\beta`.
|
||||
|
||||
2. Parameter `method` in ``('quadratic', 'quad', 'q')``:
|
||||
|
||||
.. math::
|
||||
f(t) =
|
||||
\begin{cases}
|
||||
f_0 + \beta\, t^2 & \text{if vertex_zero is True,}\\
|
||||
f_1 + \beta\, (t_1 - t)^2 & \text{otherwise,}
|
||||
\end{cases}
|
||||
\quad\text{with}\quad
|
||||
\beta = \frac{f_1 - f_0}{t_1^2}
|
||||
|
||||
The graph of the frequency f(t) is a parabola through :math:`(0, f_0)` and
|
||||
:math:`(t_1, f_1)`. By default, the vertex of the parabola is at
|
||||
:math:`(0, f_0)`. If `vertex_zero` is ``False``, then the vertex is at
|
||||
:math:`(t_1, f_1)`.
|
||||
To use a more general quadratic function, or an arbitrary
|
||||
polynomial, use the function `scipy.signal.sweep_poly`.
|
||||
|
||||
3. Parameter `method` in ``('logarithmic', 'log', 'lo')``:
|
||||
|
||||
.. math::
|
||||
f(t) = f_0 \left(\frac{f_1}{f_0}\right)^{t/t_1}
|
||||
|
||||
:math:`f_0` and :math:`f_1` must be nonzero and have the same sign.
|
||||
This signal is also known as a geometric or exponential chirp.
|
||||
|
||||
4. Parameter `method` in ``('hyperbolic', 'hyp')``:
|
||||
|
||||
.. math::
|
||||
f(t) = \frac{\alpha}{\beta\, t + \gamma} \quad\text{with}\quad
|
||||
\alpha = f_0 f_1 t_1, \ \beta = f_0 - f_1, \ \gamma = f_1 t_1
|
||||
|
||||
:math:`f_0` and :math:`f_1` must be nonzero.
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
For the first example, a linear chirp ranging from 6 Hz to 1 Hz over 10 seconds is
|
||||
plotted:
|
||||
|
||||
>>> import numpy as np
|
||||
>>> from matplotlib.pyplot import tight_layout
|
||||
>>> from scipy.signal import chirp, square, ShortTimeFFT
|
||||
>>> from scipy.signal.windows import gaussian
|
||||
>>> import matplotlib.pyplot as plt
|
||||
...
|
||||
>>> N, T = 1000, 0.01 # number of samples and sampling interval for 10 s signal
|
||||
>>> t = np.arange(N) * T # timestamps
|
||||
...
|
||||
>>> x_lin = chirp(t, f0=6, f1=1, t1=10, method='linear')
|
||||
...
|
||||
>>> fg0, ax0 = plt.subplots()
|
||||
>>> ax0.set_title(r"Linear Chirp from $f(0)=6\,$Hz to $f(10)=1\,$Hz")
|
||||
>>> ax0.set(xlabel="Time $t$ in Seconds", ylabel=r"Amplitude $x_\text{lin}(t)$")
|
||||
>>> ax0.plot(t, x_lin)
|
||||
>>> plt.show()
|
||||
|
||||
The following four plots each show the short-time Fourier transform of a chirp
|
||||
ranging from 45 Hz to 5 Hz with different values for the parameter `method`
|
||||
(and `vertex_zero`):
|
||||
|
||||
>>> x_qu0 = chirp(t, f0=45, f1=5, t1=N*T, method='quadratic', vertex_zero=True)
|
||||
>>> x_qu1 = chirp(t, f0=45, f1=5, t1=N*T, method='quadratic', vertex_zero=False)
|
||||
>>> x_log = chirp(t, f0=45, f1=5, t1=N*T, method='logarithmic')
|
||||
>>> x_hyp = chirp(t, f0=45, f1=5, t1=N*T, method='hyperbolic')
|
||||
...
|
||||
>>> win = gaussian(50, std=12, sym=True)
|
||||
>>> SFT = ShortTimeFFT(win, hop=2, fs=1/T, mfft=800, scale_to='magnitude')
|
||||
>>> ts = ("'quadratic', vertex_zero=True", "'quadratic', vertex_zero=False",
|
||||
... "'logarithmic'", "'hyperbolic'")
|
||||
>>> fg1, ax1s = plt.subplots(2, 2, sharex='all', sharey='all',
|
||||
... figsize=(6, 5), layout="constrained")
|
||||
>>> for x_, ax_, t_ in zip([x_qu0, x_qu1, x_log, x_hyp], ax1s.ravel(), ts):
|
||||
... aSx = abs(SFT.stft(x_))
|
||||
... im_ = ax_.imshow(aSx, origin='lower', aspect='auto', extent=SFT.extent(N),
|
||||
... cmap='plasma')
|
||||
... ax_.set_title(t_)
|
||||
... if t_ == "'hyperbolic'":
|
||||
... fg1.colorbar(im_, ax=ax1s, label='Magnitude $|S_z(t,f)|$')
|
||||
>>> _ = fg1.supxlabel("Time $t$ in Seconds") # `_ =` is needed to pass doctests
|
||||
>>> _ = fg1.supylabel("Frequency $f$ in Hertz")
|
||||
>>> plt.show()
|
||||
|
||||
Finally, the short-time Fourier transform of a complex-valued linear chirp
|
||||
ranging from -30 Hz to 30 Hz is depicted:
|
||||
|
||||
>>> z_lin = chirp(t, f0=-30, f1=30, t1=N*T, method="linear", complex=True)
|
||||
>>> SFT.fft_mode = 'centered' # needed to work with complex signals
|
||||
>>> aSz = abs(SFT.stft(z_lin))
|
||||
...
|
||||
>>> fg2, ax2 = plt.subplots()
|
||||
>>> ax2.set_title(r"Linear Chirp from $-30\,$Hz to $30\,$Hz")
|
||||
>>> ax2.set(xlabel="Time $t$ in Seconds", ylabel="Frequency $f$ in Hertz")
|
||||
>>> im2 = ax2.imshow(aSz, origin='lower', aspect='auto',
|
||||
... extent=SFT.extent(N), cmap='viridis')
|
||||
>>> fg2.colorbar(im2, label='Magnitude $|S_z(t,f)|$')
|
||||
>>> plt.show()
|
||||
|
||||
Note that using negative frequencies makes only sense with complex-valued signals.
|
||||
Furthermore, the magnitude of the complex exponential function is one whereas the
|
||||
magnitude of the real-valued cosine function is only 1/2.
|
||||
"""
|
||||
# 'phase' is computed in _chirp_phase, to make testing easier.
|
||||
phase = _chirp_phase(t, f0, t1, f1, method, vertex_zero) + np.deg2rad(phi)
|
||||
return np.exp(1j*phase) if complex else np.cos(phase)
|
||||
|
||||
|
||||
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', "
|
||||
f"or 'hyperbolic', but a value of {method!r} was given.")
|
||||
|
||||
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):
|
||||
r"""
|
||||
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
|
||||
-----
|
||||
In digital signal processing literature the unit impulse signal is often
|
||||
represented by the Kronecker delta. [1]_ I.e., a signal :math:`u_k[n]`,
|
||||
which is zero everywhere except being one at the :math:`k`-th sample,
|
||||
can be expressed as
|
||||
|
||||
.. math::
|
||||
|
||||
u_k[n] = \delta[n-k] \equiv \delta_{n,k}\ .
|
||||
|
||||
Furthermore, the unit impulse is frequently interpreted as the discrete-time
|
||||
version of the continuous-time Dirac distribution. [2]_
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] "Kronecker delta", *Wikipedia*,
|
||||
https://en.wikipedia.org/wiki/Kronecker_delta#Digital_signal_processing
|
||||
.. [2] "Dirac delta function" *Wikipedia*,
|
||||
https://en.wikipedia.org/wiki/Dirac_delta_function#Relationship_to_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
|
||||
29
venv/lib/python3.13/site-packages/scipy/signal/_wavelets.py
Normal file
29
venv/lib/python3.13/site-packages/scipy/signal/_wavelets.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import numpy as np
|
||||
from scipy.signal._signaltools import convolve
|
||||
|
||||
|
||||
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 _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.13/site-packages/scipy/signal/bsplines.py
Normal file
21
venv/lib/python3.13/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=["_spline_filters"], 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', 'freqz_sos', '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,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
|
||||
'kaiser_beta', 'kaiser_atten', 'kaiserord',
|
||||
'firwin', 'firwin2', 'remez', 'firls', 'minimum_phase',
|
||||
'firwin_2d',
|
||||
]
|
||||
|
||||
|
||||
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.13/site-packages/scipy/signal/ltisys.py
Normal file
25
venv/lib/python3.13/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',
|
||||
'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.13/site-packages/scipy/signal/spectral.py
Normal file
21
venv/lib/python3.13/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)
|
||||
18
venv/lib/python3.13/site-packages/scipy/signal/spline.py
Normal file
18
venv/lib/python3.13/site-packages/scipy/signal/spline.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# 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.
|
||||
|
||||
from scipy._lib.deprecation import _sub_module_deprecation
|
||||
|
||||
|
||||
__all__ = ['sepfir2d'] # noqa: F822
|
||||
|
||||
|
||||
def __dir__():
|
||||
return __all__
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
return _sub_module_deprecation(sub_package="signal", module="spline",
|
||||
private_modules=["_spline"], all=__all__,
|
||||
attribute=name)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,311 @@
|
|||
"""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 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 _triage_segments
|
||||
|
||||
|
||||
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[..., : -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
|
||||
# number of frames akin to legacy stft computation
|
||||
p1 = (x.shape[axis] - nperseg) // nstep + 1
|
||||
|
||||
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)
|
||||
|
||||
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 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
|
||||
122
venv/lib/python3.13/site-packages/scipy/signal/tests/mpsig.py
Normal file
122
venv/lib/python3.13/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 scipy._lib._array_api import xp_assert_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)
|
||||
xp_assert_equal(s, a[0:1, :])
|
||||
|
||||
s = axis_slice(a, start=-1, axis=0)
|
||||
xp_assert_equal(s, a[-1:, :])
|
||||
|
||||
s = axis_slice(a, start=0, stop=1, axis=1)
|
||||
xp_assert_equal(s, a[:, 0:1])
|
||||
|
||||
s = axis_slice(a, start=-1, axis=1)
|
||||
xp_assert_equal(s, a[:, -1:])
|
||||
|
||||
s = axis_slice(a, start=0, step=2, axis=0)
|
||||
xp_assert_equal(s, a[::2, :])
|
||||
|
||||
s = axis_slice(a, start=0, step=2, axis=1)
|
||||
xp_assert_equal(s, a[:, ::2])
|
||||
|
||||
def test_axis_reverse(self):
|
||||
a = np.arange(12).reshape(3, 4)
|
||||
|
||||
r = axis_reverse(a, axis=0)
|
||||
xp_assert_equal(r, a[::-1, :])
|
||||
|
||||
r = axis_reverse(a, axis=1)
|
||||
xp_assert_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]])
|
||||
xp_assert_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]])
|
||||
xp_assert_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]])
|
||||
xp_assert_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]])
|
||||
xp_assert_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]])
|
||||
xp_assert_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]])
|
||||
xp_assert_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]])
|
||||
xp_assert_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]])
|
||||
xp_assert_equal(zero, expected)
|
||||
|
||||
|
|
@ -0,0 +1,365 @@
|
|||
# pylint: disable=missing-docstring
|
||||
import math
|
||||
import numpy as np
|
||||
|
||||
from scipy._lib._array_api import (
|
||||
assert_almost_equal, xp_assert_close, xp_assert_equal
|
||||
)
|
||||
import pytest
|
||||
from pytest import raises
|
||||
|
||||
from scipy import signal
|
||||
|
||||
skip_xp_backends = pytest.mark.skip_xp_backends
|
||||
xfail_xp_backends = pytest.mark.xfail_xp_backends
|
||||
|
||||
|
||||
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)."""
|
||||
|
||||
@skip_xp_backends(cpu_only=True, exceptions=["cupy"])
|
||||
def test_spline_filter(self, xp):
|
||||
rng = np.random.RandomState(12457)
|
||||
# Test the type-error branch
|
||||
raises(TypeError, signal.spline_filter, xp.asarray([0]), 0)
|
||||
# Test the real branch
|
||||
data_array_real = rng.rand(12, 12)
|
||||
# make the magnitude exceed 1, and make some negative
|
||||
data_array_real = 10*(1-2*data_array_real)
|
||||
data_array_real = xp.asarray(data_array_real)
|
||||
result_array_real = xp.asarray(
|
||||
[[-.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]], dtype=xp.float64)
|
||||
xp_assert_close(signal.spline_filter(data_array_real, 0),
|
||||
result_array_real)
|
||||
|
||||
@skip_xp_backends(cpu_only=True, exceptions=["cupy"])
|
||||
def test_spline_filter_complex(self, xp):
|
||||
rng = np.random.RandomState(12457)
|
||||
data_array_complex = rng.rand(7, 7) + rng.rand(7, 7)*1j
|
||||
# make the magnitude exceed 1, and make some negative
|
||||
data_array_complex = 10*(1+1j-2*data_array_complex)
|
||||
data_array_complex = xp.asarray(data_array_complex)
|
||||
|
||||
result_array_complex = xp.asarray(
|
||||
[[-4.61489230e-01-1.92994022j, 8.33332443+6.25519943j,
|
||||
6.96300745e-01-9.05576038j, 5.28294849+3.97541356j,
|
||||
5.92165565+7.68240595j, 6.59493160-1.04542804j,
|
||||
9.84503460-5.85946894j],
|
||||
[-8.78262329-8.4295969j, 7.20675516+5.47528982j,
|
||||
-8.17223072+2.06330729j, -4.38633347-8.65968037j,
|
||||
9.89916801-8.91720295j, 2.67755103+8.8706522j,
|
||||
6.24192142+3.76879835j],
|
||||
[-3.15627527+2.56303072j, 9.87658501-0.82838702j,
|
||||
-9.96930313+8.72288895j, 3.17193985+6.42474651j,
|
||||
-4.50919819-6.84576082j, 5.75423431+9.94723988j,
|
||||
9.65979767+6.90665293j],
|
||||
[-8.28993416-6.61064005j, 9.71416473e-01-9.44907284j,
|
||||
-2.38331890+9.25196648j, -7.08868170-0.77403212j,
|
||||
4.89887714+7.05371094j, -1.37062311-2.73505688j,
|
||||
7.70705748+2.5395329j],
|
||||
[2.51528406-1.82964492j, 3.65885472+2.95454836j,
|
||||
5.16786575-1.66362023j, -8.77737999e-03+5.72478867j,
|
||||
4.10533333-3.10287571j, 9.04761887+1.54017115j,
|
||||
-5.77960968e-01-7.87758923j],
|
||||
[9.86398506-3.98528528j, -4.71444130-2.44316983j,
|
||||
-1.68038976-1.12708664j, 2.84695053+1.01725709j,
|
||||
1.14315915-8.89294529j, -3.17127085-5.42145538j,
|
||||
1.91830420-6.16370344j],
|
||||
[7.13875294+2.91851187j, -5.35737514+9.64132309j,
|
||||
-9.66586399+0.70250005j, -9.87717438-2.0262239j,
|
||||
9.93160629+1.5630846j, 4.71948051-2.22050714j,
|
||||
9.49550819+7.8995142j]], dtype=xp.complex128)
|
||||
# FIXME: for complex types, the computations are done in
|
||||
# single precision (reason unclear). When this is changed,
|
||||
# this test needs updating.
|
||||
xp_assert_close(signal.spline_filter(data_array_complex, 0),
|
||||
result_array_complex, rtol=1e-6)
|
||||
|
||||
def test_gauss_spline(self, xp):
|
||||
assert math.isclose(signal.gauss_spline(0, 0), 1.381976597885342)
|
||||
|
||||
xp_assert_close(signal.gauss_spline(xp.asarray([1.]), 1),
|
||||
xp.asarray([0.04865217]), atol=1e-9
|
||||
)
|
||||
|
||||
@skip_xp_backends(np_only=True, reason="deliberate: array-likes are accepted")
|
||||
def test_gauss_spline_list(self, xp):
|
||||
# regression test for gh-12152 (accept array_like)
|
||||
knots = [-1.0, 0.0, -1.0]
|
||||
assert_almost_equal(signal.gauss_spline(knots, 3),
|
||||
np.asarray([0.15418033, 0.6909883, 0.15418033])
|
||||
)
|
||||
|
||||
@skip_xp_backends(cpu_only=True)
|
||||
def test_cspline1d(self, xp):
|
||||
xp_assert_equal(signal.cspline1d(xp.asarray([0])),
|
||||
xp.asarray([0.], dtype=xp.float64))
|
||||
c1d = xp.asarray([1.21037185, 1.86293902, 2.98834059, 4.11660378,
|
||||
4.78893826], dtype=xp.float64)
|
||||
# test lamda != 0
|
||||
xp_assert_close(signal.cspline1d(xp.asarray([1., 2, 3, 4, 5]), 1), c1d)
|
||||
c1d0 = xp.asarray([0.78683946, 2.05333735, 2.99981113, 3.94741812,
|
||||
5.21051638], dtype=xp.float64)
|
||||
xp_assert_close(signal.cspline1d(xp.asarray([1., 2, 3, 4, 5])), c1d0)
|
||||
|
||||
@skip_xp_backends(cpu_only=True)
|
||||
def test_qspline1d(self, xp):
|
||||
xp_assert_equal(signal.qspline1d(xp.asarray([0])),
|
||||
xp.asarray([0.], dtype=xp.float64))
|
||||
# test lamda != 0
|
||||
raises(ValueError, signal.qspline1d, xp.asarray([1., 2, 3, 4, 5]), 1.)
|
||||
raises(ValueError, signal.qspline1d, xp.asarray([1., 2, 3, 4, 5]), -1.)
|
||||
q1d0 = xp.asarray([0.85350007, 2.02441743, 2.99999534, 3.97561055,
|
||||
5.14634135], dtype=xp.float64)
|
||||
xp_assert_close(
|
||||
signal.qspline1d(xp.asarray([1., 2, 3, 4, 5], dtype=xp.float64)), q1d0
|
||||
)
|
||||
|
||||
@skip_xp_backends(cpu_only=True)
|
||||
def test_cspline1d_eval(self, xp):
|
||||
r = signal.cspline1d_eval(xp.asarray([0., 0], dtype=xp.float64),
|
||||
xp.asarray([0.], dtype=xp.float64))
|
||||
xp_assert_close(r, xp.asarray([0.], dtype=xp.float64))
|
||||
|
||||
r = signal.cspline1d_eval(xp.asarray([1., 0, 1], dtype=xp.float64),
|
||||
xp.asarray([], dtype=xp.float64))
|
||||
xp_assert_equal(r, xp.asarray([], dtype=xp.float64))
|
||||
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 = xp.asarray([4.216, 6.864, 3.514, 6.203, 6.759, 7.433, 7.874, 5.879,
|
||||
1.396, 4.094])
|
||||
cj = signal.cspline1d(y)
|
||||
newy = xp.asarray([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], dtype=xp.float64)
|
||||
xp_assert_close(
|
||||
signal.cspline1d_eval(cj, xp.asarray(newx), dx=dx, x0=x[0]), newy
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError,
|
||||
match="Spline coefficients 'cj' must not be empty."):
|
||||
signal.cspline1d_eval(xp.asarray([], dtype=xp.float64),
|
||||
xp.asarray([0.0], dtype=xp.float64))
|
||||
|
||||
@skip_xp_backends(cpu_only=True)
|
||||
def test_qspline1d_eval(self, xp):
|
||||
xp_assert_close(signal.qspline1d_eval(xp.asarray([0., 0]), xp.asarray([0.])),
|
||||
xp.asarray([0.])
|
||||
)
|
||||
xp_assert_equal(signal.qspline1d_eval(xp.asarray([1., 0, 1]), xp.asarray([])),
|
||||
xp.asarray([])
|
||||
)
|
||||
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 = xp.asarray([4.216, 6.864, 3.514, 6.203, 6.759, 7.433, 7.874, 5.879,
|
||||
1.396, 4.094])
|
||||
cj = signal.qspline1d(y)
|
||||
newy = xp.asarray([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], dtype=xp.float64)
|
||||
r = signal.qspline1d_eval(
|
||||
cj, xp.asarray(newx, dtype=xp.float64), dx=dx, x0=x[0]
|
||||
)
|
||||
xp_assert_close(r, newy)
|
||||
|
||||
with pytest.raises(ValueError,
|
||||
match="Spline coefficients 'cj' must not be empty."):
|
||||
signal.qspline1d_eval(xp.asarray([], dtype=xp.float64),
|
||||
xp.asarray([0.0], dtype=xp.float64))
|
||||
|
||||
|
||||
# i/o dtypes with scipy 1.9.1, likely fixed by backwards compat
|
||||
sepfir_dtype_map = {np.uint8: np.float32, int: np.float64,
|
||||
np.float32: np.float32, float: float,
|
||||
np.complex64: np.complex64, complex: complex}
|
||||
|
||||
|
||||
@skip_xp_backends(np_only=True)
|
||||
class TestSepfir2d:
|
||||
def test_sepfir2d_invalid_filter(self, xp):
|
||||
filt = xp.asarray([1.0, 2.0, 4.0, 2.0, 1.0])
|
||||
image = np.random.rand(7, 9)
|
||||
image = xp.asarray(image)
|
||||
# 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, xp.reshape(filt, (1, -1)), filt)
|
||||
with pytest.raises(ValueError, match="object too deep"):
|
||||
signal.sepfir2d(image, filt, xp.reshape(filt, (1, -1)))
|
||||
|
||||
def test_sepfir2d_invalid_image(self, xp):
|
||||
filt = xp.asarray([1.0, 2.0, 4.0, 2.0, 1.0])
|
||||
image = np.random.rand(8, 8)
|
||||
image = xp.asarray(image)
|
||||
|
||||
# Image must be 2 dimensional
|
||||
with pytest.raises(ValueError, match="object too deep"):
|
||||
signal.sepfir2d(xp.reshape(image, (4, 4, 4)), filt, filt)
|
||||
|
||||
with pytest.raises(ValueError, match="object of too small depth"):
|
||||
signal.sepfir2d(image[0, :], filt, filt)
|
||||
|
||||
@pytest.mark.parametrize('dtyp',
|
||||
[np.uint8, int, np.float32, float, np.complex64, complex]
|
||||
)
|
||||
def test_simple(self, dtyp, xp):
|
||||
# test values on a paper-and-pencil example
|
||||
a = np.array([[1, 2, 3, 3, 2, 1],
|
||||
[1, 2, 3, 3, 2, 1],
|
||||
[1, 2, 3, 3, 2, 1],
|
||||
[1, 2, 3, 3, 2, 1]], dtype=dtyp)
|
||||
h1 = [0.5, 1, 0.5]
|
||||
h2 = [1]
|
||||
result = signal.sepfir2d(a, h1, h2)
|
||||
dt = sepfir_dtype_map[dtyp]
|
||||
expected = np.asarray([[2.5, 4. , 5.5, 5.5, 4. , 2.5],
|
||||
[2.5, 4. , 5.5, 5.5, 4. , 2.5],
|
||||
[2.5, 4. , 5.5, 5.5, 4. , 2.5],
|
||||
[2.5, 4. , 5.5, 5.5, 4. , 2.5]], dtype=dt)
|
||||
xp_assert_close(result, expected, atol=1e-16)
|
||||
|
||||
result = signal.sepfir2d(a, h2, h1)
|
||||
expected = np.asarray([[2., 4., 6., 6., 4., 2.],
|
||||
[2., 4., 6., 6., 4., 2.],
|
||||
[2., 4., 6., 6., 4., 2.],
|
||||
[2., 4., 6., 6., 4., 2.]], dtype=dt)
|
||||
xp_assert_close(result, expected, atol=1e-16)
|
||||
|
||||
@skip_xp_backends(np_only=True, reason="TODO: convert this test")
|
||||
@pytest.mark.parametrize('dtyp',
|
||||
[np.uint8, int, np.float32, float, np.complex64, complex]
|
||||
)
|
||||
def test_strided(self, dtyp, xp):
|
||||
a = np.array([[1, 2, 3, 3, 2, 1, 1, 2, 3],
|
||||
[1, 2, 3, 3, 2, 1, 1, 2, 3],
|
||||
[1, 2, 3, 3, 2, 1, 1, 2, 3],
|
||||
[1, 2, 3, 3, 2, 1, 1, 2, 3]])
|
||||
h1, h2 = [0.5, 1, 0.5], [1]
|
||||
result_strided = signal.sepfir2d(a[:, ::2], h1, h2)
|
||||
result_contig = signal.sepfir2d(a[:, ::2].copy(), h1, h2)
|
||||
xp_assert_close(result_strided, result_contig, atol=1e-15)
|
||||
assert result_strided.dtype == result_contig.dtype
|
||||
|
||||
@skip_xp_backends(np_only=True, reason="TODO: convert this test")
|
||||
@pytest.mark.xfail(reason="XXX: filt.size > image.shape: flaky")
|
||||
def test_sepfir2d_strided_2(self, xp):
|
||||
# XXX: this test is flaky: fails on some reruns, with
|
||||
# result[0, 1] and result[1, 1] being ~1e+224.
|
||||
filt = np.array([1.0, 2.0, 4.0, 2.0, 1.0, 3.0, 2.0])
|
||||
image = np.random.rand(4, 4)
|
||||
|
||||
expected = np.asarray([[36.018162, 30.239061, 38.71187 , 43.878183],
|
||||
[38.180999, 35.824583, 43.525247, 43.874945],
|
||||
[43.269533, 40.834018, 46.757772, 44.276423],
|
||||
[49.120928, 39.681844, 43.596067, 45.085854]])
|
||||
xp_assert_close(signal.sepfir2d(image, filt, filt[::3]), expected)
|
||||
|
||||
@skip_xp_backends(np_only=True, reason="TODO: convert this test")
|
||||
@pytest.mark.xfail(reason="XXX: flaky. pointers OOB on some platforms")
|
||||
@pytest.mark.parametrize('dtyp',
|
||||
[np.uint8, int, np.float32, float, np.complex64, complex]
|
||||
)
|
||||
def test_sepfir2d_strided_3(self, dtyp, xp):
|
||||
# NB: 'image' and 'filt' dtypes match here. Otherwise we can run into
|
||||
# unsafe casting errors for many combinations. Historically, dtype handling
|
||||
# in `sepfir2d` is a tad baroque; fixing it is an enhancement.
|
||||
filt = np.array([1, 2, 4, 2, 1, 3, 2], dtype=dtyp)
|
||||
image = np.asarray([[0, 3, 0, 1, 2],
|
||||
[2, 2, 3, 3, 3],
|
||||
[0, 1, 3, 0, 3],
|
||||
[2, 3, 0, 1, 3],
|
||||
[3, 3, 2, 1, 2]], dtype=dtyp)
|
||||
|
||||
expected = [[123., 101., 91., 136., 127.],
|
||||
[133., 125., 126., 152., 160.],
|
||||
[136., 137., 150., 162., 177.],
|
||||
[133., 124., 132., 148., 147.],
|
||||
[173., 158., 152., 164., 141.]]
|
||||
expected = np.asarray(expected)
|
||||
result = signal.sepfir2d(image, filt, filt[::3])
|
||||
xp_assert_close(result, expected, atol=1e-15)
|
||||
assert result.dtype == sepfir_dtype_map[dtyp]
|
||||
|
||||
expected = [[22., 35., 41., 31., 47.],
|
||||
[27., 39., 48., 47., 55.],
|
||||
[33., 42., 49., 53., 59.],
|
||||
[39., 44., 41., 36., 48.],
|
||||
[67., 62., 47., 34., 46.]]
|
||||
expected = np.asarray(expected)
|
||||
result = signal.sepfir2d(image, filt[::3], filt[::3])
|
||||
xp_assert_close(result, expected, atol=1e-15)
|
||||
assert result.dtype == sepfir_dtype_map[dtyp]
|
||||
|
||||
|
||||
def test_cspline2d(xp):
|
||||
rng = np.random.RandomState(181819142)
|
||||
image = rng.rand(71, 73)
|
||||
signal.cspline2d(image, 8.0)
|
||||
|
||||
|
||||
def test_qspline2d(xp):
|
||||
rng = np.random.RandomState(181819143)
|
||||
image = rng.rand(71, 73)
|
||||
signal.qspline2d(image)
|
||||
|
|
@ -0,0 +1,424 @@
|
|||
import numpy as np
|
||||
from scipy._lib._array_api import (
|
||||
assert_array_almost_equal, assert_almost_equal, xp_assert_close
|
||||
)
|
||||
|
||||
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, dtype=np.float64)
|
||||
bc = np.full((2, 1), 0.5, dtype=np.float64)
|
||||
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)
|
||||
|
||||
xp_assert_close(dnum, c2dnum)
|
||||
xp_assert_close(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)
|
||||
|
||||
xp_assert_close(dz, c2dz)
|
||||
xp_assert_close(dp, c2dp)
|
||||
xp_assert_close(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:])
|
||||
|
||||
xp_assert_close(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 dt == 0.01 # sanity check
|
||||
xp_assert_close(den, [1, -0.990404983], rtol=1e-3)
|
||||
xp_assert_close(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 dt == dt1
|
||||
assert dt == dt2
|
||||
|
||||
# Check that we get the same results
|
||||
xp_assert_close(num, np.vstack((num1, num2)), rtol=1e-13)
|
||||
|
||||
# Single input, so the denominator should
|
||||
# not be multidimensional like the numerator
|
||||
xp_assert_close(den, den1, rtol=1e-13)
|
||||
xp_assert_close(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
|
||||
dt = 0.05
|
||||
|
||||
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(dt=dt)
|
||||
|
||||
xp_assert_close(sys_ssd.A, A_res)
|
||||
xp_assert_close(sys_ssd.B, B_res)
|
||||
xp_assert_close(sys_ssd.C, C)
|
||||
xp_assert_close(sys_ssd.D, np.zeros_like(sys_ssd.D))
|
||||
|
||||
sys_ssd2 = c2d(sys_ssc, dt=dt)
|
||||
|
||||
xp_assert_close(sys_ssd2.A, A_res)
|
||||
xp_assert_close(sys_ssd2.B, B_res)
|
||||
xp_assert_close(sys_ssd2.C, C)
|
||||
xp_assert_close(sys_ssd2.D, np.zeros_like(sys_ssd2.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
|
||||
xp_assert_close(sys.den, den_res, atol=0.02)
|
||||
xp_assert_close(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))
|
||||
xp_assert_close(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))
|
||||
xp_assert_close(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)
|
||||
xp_assert_close(yout_cont.ravel(), yout_disc.ravel())
|
||||
221
venv/lib/python3.13/site-packages/scipy/signal/tests/test_czt.py
Normal file
221
venv/lib/python3.13/site-packages/scipy/signal/tests/test_czt.py
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
# This program is public domain
|
||||
# Authors: Paul Kienzle, Nadav Horesh
|
||||
'''
|
||||
A unit test module for czt.py
|
||||
'''
|
||||
import pytest
|
||||
from scipy._lib._array_api import xp_assert_close
|
||||
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)
|
||||
xp_assert_close(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))
|
||||
xp_assert_close(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)
|
||||
xp_assert_close(y1, y, rtol=1e-11, atol=1e-14)
|
||||
y1 = zoom_fft(x, [0, 2])
|
||||
xp_assert_close(y1, y, rtol=1e-11, atol=1e-14)
|
||||
|
||||
# Test fn scalar
|
||||
y1 = zoom_fft(x, 2-2./len(y), endpoint=True)
|
||||
xp_assert_close(y1, y, rtol=1e-11, atol=1e-14)
|
||||
y1 = zoom_fft(x, 2)
|
||||
xp_assert_close(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)
|
||||
xp_assert_close(y2, yover, rtol=1e-12, atol=1e-10)
|
||||
y2 = zoom_fft(x, [0, 2], m=len(yover))
|
||||
xp_assert_close(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)
|
||||
xp_assert_close(y3, yover[idx3], rtol=1e-13)
|
||||
|
||||
|
||||
def test_1D():
|
||||
# Test of 1D version of the transforms
|
||||
|
||||
rng = np.random.RandomState(0) # Deterministic randomness
|
||||
|
||||
# Random signals
|
||||
lengths = rng.randint(8, 200, 20)
|
||||
np.append(lengths, 1)
|
||||
for length in lengths:
|
||||
x = rng.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])
|
||||
xp_assert_close(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)
|
||||
xp_assert_close(y1[2, 0], y2, rtol=1e-13, atol=1e-12)
|
||||
|
||||
# Random (not a test condition)
|
||||
x = rng.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():
|
||||
rng = np.random.RandomState(0) # Deterministic randomness
|
||||
for N in (101, 1009, 10007):
|
||||
x = rng.rand(N)
|
||||
y = fft(x)
|
||||
y1 = czt(x)
|
||||
xp_assert_close(y, y1, rtol=1e-12)
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_czt_vs_fft():
|
||||
rng = np.random.RandomState(123) # Deterministic randomness
|
||||
random_lengths = rng.exponential(100000, size=10).astype('int')
|
||||
for n in random_lengths:
|
||||
a = rng.randn(n)
|
||||
xp_assert_close(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
|
||||
xp_assert_close(czt(impulse[2:], m=m, w=w, a=a),
|
||||
np.ones(m, dtype=np.complex128), rtol=1e-10)
|
||||
|
||||
# z-transform of a delayed impulse is z**-1
|
||||
xp_assert_close(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
|
||||
xp_assert_close(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
|
||||
xp_assert_close(abs(czt([0, 1], m=10, a=2)), 0.5*np.ones(10), rtol=1e-15)
|
||||
xp_assert_close(czt_points(11, w=2),
|
||||
1/(2**np.arange(11, dtype=np.complex128)), rtol=1e-30)
|
||||
|
||||
|
||||
def test_czt_points():
|
||||
for N in (1, 2, 3, 8, 11, 100, 101, 10007):
|
||||
xp_assert_close(czt_points(N), np.exp(2j*np.pi*np.arange(N)/N),
|
||||
rtol=1e-30)
|
||||
|
||||
xp_assert_close(czt_points(7, w=1), np.ones(7, dtype=np.complex128), rtol=1e-30)
|
||||
xp_assert_close(czt_points(11, w=2.),
|
||||
1/(2**np.arange(11, dtype=np.complex128)), rtol=1e-30)
|
||||
|
||||
func = CZT(12, m=11, w=2., a=1)
|
||||
xp_assert_close(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,599 @@
|
|||
# Author: Jeffrey Armstrong <jeff@approximatrix.com>
|
||||
# April 4, 2011
|
||||
|
||||
import numpy as np
|
||||
from numpy.testing import suppress_warnings
|
||||
from pytest import raises as assert_raises
|
||||
from scipy._lib._array_api import (
|
||||
assert_array_almost_equal, assert_almost_equal, xp_assert_close, xp_assert_equal,
|
||||
)
|
||||
|
||||
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 len(tout) == len(yout)
|
||||
|
||||
# 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 len(yout) == 3
|
||||
|
||||
for i in range(0, len(yout)):
|
||||
assert 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 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 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 len(yout) == 3
|
||||
|
||||
for i in range(0, len(yout)):
|
||||
assert 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 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 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)
|
||||
xp_assert_equal(tout, np.arange(float(n)))
|
||||
xp_assert_equal(yout, np.zeros((n, 1)))
|
||||
xp_assert_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)
|
||||
xp_assert_equal(tout, np.arange(float(n)))
|
||||
expected = (0.5 ** np.arange(float(n))).reshape(-1, 1)
|
||||
xp_assert_equal(yout, expected)
|
||||
xp_assert_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)
|
||||
xp_assert_equal(tout, np.arange(float(n)))
|
||||
# The analytical solution:
|
||||
expected = (np.array([lambda1, lambda2]) **
|
||||
np.arange(float(n)).reshape(-1, 1))
|
||||
xp_assert_equal(yout, expected)
|
||||
xp_assert_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))
|
||||
|
||||
xp_assert_close(ys[0][:, 0], stp0)
|
||||
xp_assert_close(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)
|
||||
|
||||
xp_assert_close(yi[0][:, 0], y0)
|
||||
xp_assert_close(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)
|
||||
xp_assert_close(t, [0, 0.1, 0.2])
|
||||
xp_assert_equal(y.T, [[0, 1.0, 1.5]])
|
||||
t, (y,) = dimpulse(system, n=3)
|
||||
xp_assert_close(t, [0, 0.1, 0.2])
|
||||
xp_assert_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 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 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 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)
|
||||
xp_assert_equal(s.poles, [1.])
|
||||
xp_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)
|
||||
xp_assert_equal(s.poles, [1.])
|
||||
xp_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 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
|
||||
xp_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 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 = np.asarray([2.0, 3, 4])
|
||||
den = np.asarray([5.0, 6, 7])
|
||||
num2, den2 = TransferFunction._z_to_zinv(num, den)
|
||||
xp_assert_equal(num, num2)
|
||||
xp_assert_equal(den, den2)
|
||||
|
||||
num2, den2 = TransferFunction._zinv_to_z(num, den)
|
||||
xp_assert_equal(num, num2)
|
||||
xp_assert_equal(den, den2)
|
||||
|
||||
def test_numerator(self):
|
||||
# Numerator lower order than denominator
|
||||
num = np.asarray([2.0, 3])
|
||||
den = np.asarray([50, 6, 7])
|
||||
num2, den2 = TransferFunction._z_to_zinv(num, den)
|
||||
xp_assert_equal([0.0, 2, 3], num2)
|
||||
xp_assert_equal(den, den2)
|
||||
|
||||
num2, den2 = TransferFunction._zinv_to_z(num, den)
|
||||
xp_assert_equal([2.0, 3, 0], num2)
|
||||
xp_assert_equal(den, den2)
|
||||
|
||||
def test_denominator(self):
|
||||
# Numerator higher order than denominator
|
||||
num = np.asarray([2., 3, 4])
|
||||
den = np.asarray([5.0, 6])
|
||||
num2, den2 = TransferFunction._z_to_zinv(num, den)
|
||||
xp_assert_equal(num, num2)
|
||||
xp_assert_equal([0.0, 5, 6], den2)
|
||||
|
||||
num2, den2 = TransferFunction._zinv_to_z(num, den)
|
||||
xp_assert_equal(num, num2)
|
||||
xp_assert_equal([5.0, 6, 0], den2)
|
||||
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,851 @@
|
|||
import math
|
||||
import numpy as np
|
||||
|
||||
from numpy.testing import assert_warns
|
||||
from pytest import raises as assert_raises
|
||||
import pytest
|
||||
|
||||
import scipy._lib.array_api_extra as xpx
|
||||
from scipy._lib._array_api import (
|
||||
xp_assert_close, xp_assert_equal, assert_almost_equal, assert_array_almost_equal,
|
||||
array_namespace, xp_default_dtype
|
||||
)
|
||||
from scipy.fft import fft, fft2
|
||||
from scipy.signal import (kaiser_beta, kaiser_atten, kaiserord,
|
||||
firwin, firwin2, freqz, remez, firls, minimum_phase, convolve2d, firwin_2d
|
||||
)
|
||||
|
||||
skip_xp_backends = pytest.mark.skip_xp_backends
|
||||
xfail_xp_backends = pytest.mark.xfail_xp_backends
|
||||
|
||||
|
||||
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 b == 0.0
|
||||
b = kaiser_beta(10.0)
|
||||
assert b == 0.0
|
||||
|
||||
|
||||
def test_kaiser_atten():
|
||||
a = kaiser_atten(1, 1.0)
|
||||
assert a == 7.95
|
||||
a = kaiser_atten(2, 1/np.pi)
|
||||
assert 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 (numtaps, beta) == (2, 0.0)
|
||||
|
||||
|
||||
class TestFirwin:
|
||||
|
||||
def check_response(self, h, expected_response, tol=.05):
|
||||
xp = array_namespace(h)
|
||||
N = h.shape[0]
|
||||
alpha = 0.5 * (N-1)
|
||||
m = xp.arange(0, N) - alpha # time indices of taps
|
||||
for freq, expected in expected_response:
|
||||
actual = abs(xp.sum(h * xp.exp(-1j * xp.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, xp):
|
||||
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, xp):
|
||||
"""
|
||||
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]
|
||||
msg = 'least squares violation'
|
||||
assert self.mse(h, [cutoff]) < self.mse(hs, [cutoff]), msg
|
||||
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, xp):
|
||||
width = 0.04
|
||||
ntaps, beta = kaiserord(120, width)
|
||||
cutoff = xp.asarray(0.5)
|
||||
kwargs = dict(cutoff=cutoff, window=('kaiser', beta), scale=False)
|
||||
taps = firwin(ntaps, **kwargs)
|
||||
|
||||
# Check the symmetry of taps.
|
||||
assert_array_almost_equal(taps[:ntaps//2], xp.flip(taps)[:ntaps//2])
|
||||
|
||||
# Check the gain at a few samples where
|
||||
# we know it should be approximately 0 or 1.
|
||||
freq_samples = xp.asarray([0.0, 0.25, 0.5-width/2, 0.5+width/2, 0.75, 1.0])
|
||||
freqs, response = freqz(taps, worN=xp.pi*freq_samples)
|
||||
|
||||
assert_array_almost_equal(
|
||||
xp.abs(response),
|
||||
xp.asarray([1.0, 1.0, 1.0, 0.0, 0.0, 0.0]), decimal=5
|
||||
)
|
||||
|
||||
taps_str = firwin(ntaps, pass_zero='lowpass', **kwargs)
|
||||
xp_assert_close(taps, taps_str)
|
||||
|
||||
def test_highpass(self, xp):
|
||||
width = 0.04
|
||||
ntaps, beta = kaiserord(120, width)
|
||||
|
||||
# Ensure that ntaps is odd.
|
||||
ntaps |= 1
|
||||
|
||||
cutoff = xp.asarray(0.5)
|
||||
kwargs = dict(cutoff=cutoff, window=('kaiser', beta), scale=False)
|
||||
taps = firwin(ntaps, pass_zero=False, **kwargs)
|
||||
|
||||
# Check the symmetry of taps.
|
||||
assert_array_almost_equal(taps[:ntaps//2], xp.flip(taps)[:ntaps//2])
|
||||
|
||||
# Check the gain at a few samples where
|
||||
# we know it should be approximately 0 or 1.
|
||||
freq_samples = xp.asarray([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(xp.abs(response),
|
||||
xp.asarray([0.0, 0.0, 0.0, 1.0, 1.0, 1.0]), decimal=5)
|
||||
|
||||
taps_str = firwin(ntaps, pass_zero='highpass', **kwargs)
|
||||
xp_assert_close(taps, taps_str)
|
||||
|
||||
def test_bandpass(self, xp):
|
||||
width = 0.04
|
||||
ntaps, beta = kaiserord(120, width)
|
||||
kwargs = dict(
|
||||
cutoff=xp.asarray([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], xp.flip(taps)[:ntaps//2])
|
||||
|
||||
# Check the gain at a few samples where
|
||||
# we know it should be approximately 0 or 1.
|
||||
freq_samples = xp.asarray([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(xp.abs(response),
|
||||
xp.asarray([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)
|
||||
xp_assert_close(taps, taps_str)
|
||||
|
||||
def test_bandstop_multi(self, xp):
|
||||
width = 0.04
|
||||
ntaps, beta = kaiserord(120, width)
|
||||
kwargs = dict(cutoff=xp.asarray([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], xp.flip(taps)[:ntaps//2])
|
||||
|
||||
# Check the gain at a few samples where
|
||||
# we know it should be approximately 0 or 1.
|
||||
freq_samples = xp.asarray([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(
|
||||
xp.abs(response),
|
||||
xp.asarray([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)
|
||||
xp_assert_close(taps, taps_str)
|
||||
|
||||
def test_fs_nyq(self, xp):
|
||||
"""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=xp.asarray([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], xp.flip(taps)[:ntaps//2])
|
||||
|
||||
# Check the gain at a few samples where
|
||||
# we know it should be approximately 0 or 1.
|
||||
freq_samples = xp.asarray([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(xp.abs(response),
|
||||
xp.asarray([0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0]), decimal=5)
|
||||
|
||||
def test_array_cutoff(self, xp):
|
||||
taps = firwin(3, xp.asarray([.1, .2]))
|
||||
# smoke test against the value computed by scipy==1.5.2
|
||||
xp_assert_close(
|
||||
taps, xp.asarray([-0.00801395, 1.0160279, -0.00801395]), atol=1e-8
|
||||
)
|
||||
|
||||
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="^Parameter pass_zero='foo' not in "):
|
||||
firwin(41, 0.5, pass_zero='foo')
|
||||
with assert_raises(ValueError, match="^Parameter pass_zero=1.0 not in "):
|
||||
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]))
|
||||
|
||||
|
||||
@skip_xp_backends(cpu_only=True, reason="firwin2 uses np.interp")
|
||||
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, xp):
|
||||
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 = xp.asarray([0.0, 0.5, 1.0])
|
||||
gain = xp.asarray([1.0, 1.0, 0.0])
|
||||
taps = firwin2(ntaps, freq, gain, window=('kaiser', beta))
|
||||
freq_samples = xp.asarray([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)
|
||||
freqs, response = xp.asarray(freqs), xp.asarray(response)
|
||||
assert_array_almost_equal(
|
||||
xp.abs(response),
|
||||
xp.asarray([1.0, 1.0, 1.0, 1.0 - width, 0.5, width]), decimal=5
|
||||
)
|
||||
|
||||
@skip_xp_backends("jax.numpy", reason="immutable arrays")
|
||||
def test02(self, xp):
|
||||
width = 0.04
|
||||
beta = 12.0
|
||||
# ntaps must be odd for positive gain at Nyquist.
|
||||
ntaps = 401
|
||||
# An ideal highpass filter.
|
||||
freq = xp.asarray([0.0, 0.5, 0.5, 1.0])
|
||||
gain = xp.asarray([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)
|
||||
freqs, response = xp.asarray(freqs), xp.asarray(response)
|
||||
assert_array_almost_equal(
|
||||
xp.abs(response),
|
||||
xp.asarray([0.0, 0.0, 0.0, 1.0, 1.0, 1.0]), decimal=5
|
||||
)
|
||||
|
||||
@skip_xp_backends("jax.numpy", reason="immutable arrays")
|
||||
def test03(self, xp):
|
||||
width = 0.02
|
||||
ntaps, beta = kaiserord(120, width)
|
||||
# ntaps must be odd for positive gain at Nyquist.
|
||||
ntaps = int(ntaps) | 1
|
||||
freq = xp.asarray([0.0, 0.4, 0.4, 0.5, 0.5, 1.0])
|
||||
gain = xp.asarray([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)
|
||||
freqs, response = xp.asarray(freqs), xp.asarray(response)
|
||||
assert_array_almost_equal(
|
||||
xp.abs(response),
|
||||
xp.asarray([1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0]), decimal=5
|
||||
)
|
||||
|
||||
@skip_xp_backends("jax.numpy", reason="immutable arrays")
|
||||
def test04(self, xp):
|
||||
"""Test firwin2 when window=None."""
|
||||
ntaps = 5
|
||||
# Ideal lowpass: gain is 1 on [0,0.5], and 0 on [0.5, 1.0]
|
||||
freq = xp.asarray([0.0, 0.5, 0.5, 1.0])
|
||||
gain = xp.asarray([1.0, 1.0, 0.0, 0.0])
|
||||
|
||||
taps = firwin2(ntaps, freq, gain, window=None, nfreqs=8193)
|
||||
alpha = 0.5 * (ntaps - 1)
|
||||
m = xp.arange(0, ntaps, dtype=freq.dtype) - alpha
|
||||
h = 0.5 * xpx.sinc(0.5 * m)
|
||||
assert_array_almost_equal(h, taps)
|
||||
|
||||
def test05(self, xp):
|
||||
"""Test firwin2 for calculating Type IV filters"""
|
||||
ntaps = 1500
|
||||
|
||||
freq = xp.asarray([0.0, 1.0])
|
||||
gain = xp.asarray([0.0, 1.0])
|
||||
taps = firwin2(ntaps, freq, gain, window=None, antisymmetric=True)
|
||||
|
||||
flip = array_namespace(freq).flip
|
||||
dec = {'decimal': 4.5} if xp_default_dtype(xp) == xp.float32 else {}
|
||||
assert_array_almost_equal(taps[: ntaps // 2], flip(-taps[ntaps // 2:]), **dec)
|
||||
|
||||
freqs, response = freqz(np.asarray(taps), worN=2048) # XXX convert freqz
|
||||
assert_array_almost_equal(abs(xp.asarray(response)),
|
||||
xp.asarray(freqs / np.pi), decimal=4)
|
||||
|
||||
@skip_xp_backends("jax.numpy", reason="immutable arrays")
|
||||
def test06(self, xp):
|
||||
"""Test firwin2 for calculating Type III filters"""
|
||||
ntaps = 1501
|
||||
|
||||
freq = xp.asarray([0.0, 0.5, 0.55, 1.0])
|
||||
gain = xp.asarray([0.0, 0.5, 0.0, 0.0])
|
||||
taps = firwin2(ntaps, freq, gain, window=None, antisymmetric=True)
|
||||
assert taps[ntaps // 2] == 0.0
|
||||
|
||||
flip = array_namespace(freq).flip
|
||||
dec = {'decimal': 4.5} if xp_default_dtype(xp) == xp.float32 else {}
|
||||
assert_array_almost_equal(taps[: ntaps // 2],
|
||||
flip(-taps[ntaps // 2 + 1:]), **dec
|
||||
)
|
||||
|
||||
freqs, response1 = freqz(np.asarray(taps), worN=2048) # XXX convert freqz
|
||||
response1 = xp.asarray(response1)
|
||||
response2 = xp.asarray(
|
||||
np.interp(np.asarray(freqs) / np.pi, np.asarray(freq), np.asarray(gain))
|
||||
)
|
||||
assert_array_almost_equal(abs(response1), response2, decimal=3)
|
||||
|
||||
def test_fs_nyq(self, xp):
|
||||
taps1 = firwin2(80, xp.asarray([0.0, 0.5, 1.0]), xp.asarray([1.0, 1.0, 0.0]))
|
||||
taps2 = firwin2(80, xp.asarray([0.0, 30.0, 60.0]), xp.asarray([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)
|
||||
|
||||
@skip_xp_backends("jax.numpy", reason="immutable arrays")
|
||||
def test_input_modyfication(self, xp):
|
||||
freq1 = xp.asarray([0.0, 0.5, 0.5, 1.0])
|
||||
freq2 = xp.asarray(freq1)
|
||||
firwin2(80, freq1, xp.asarray([1.0, 1.0, 0.0, 0.0]))
|
||||
xp_assert_equal(freq1, freq2)
|
||||
|
||||
|
||||
@skip_xp_backends(cpu_only=True)
|
||||
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, xp):
|
||||
# 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, xp.asarray([0, 0.3, 0.5, 1]), xp.asarray([1, 0]), fs=2.)
|
||||
atol_arg = {'atol': 1e-8} if xp_default_dtype(xp) == xp.float32 else {}
|
||||
xp_assert_close(h, xp.asarray(k, dtype=xp.float64), **atol_arg)
|
||||
|
||||
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]
|
||||
atol_arg = {'atol': 3e-8} if xp_default_dtype(xp) == xp.float32 else {}
|
||||
xp_assert_close(
|
||||
remez(21, xp.asarray([0, 0.8, 0.9, 1]), xp.asarray([0, 1]), fs=2.),
|
||||
xp.asarray(h, dtype=xp.float64), **atol_arg
|
||||
)
|
||||
|
||||
def test_fs_validation(self):
|
||||
with pytest.raises(ValueError, match="Sampling.*single scalar"):
|
||||
remez(11, .1, 1, fs=np.array([10, 20]))
|
||||
|
||||
def test_gh_23266(self, xp):
|
||||
bands = xp.asarray([0.0, 0.2, 0.3, 0.5])
|
||||
desired = xp.asarray([1.0, 0.0])
|
||||
weight = xp.asarray([1.0, 2.0])
|
||||
remez(21, bands, desired, weight=weight)
|
||||
|
||||
|
||||
@skip_xp_backends(cpu_only=True, reason="lstsq")
|
||||
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])
|
||||
|
||||
@skip_xp_backends("dask.array", reason="dask fancy indexing shape=(nan,)")
|
||||
def test_firls(self, xp):
|
||||
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, xp.asarray([0, a, 0.5 - a, 0.5]), xp.asarray([1, 1, 0, 0]),
|
||||
fs=1.0)
|
||||
|
||||
# make sure the filter has correct # of taps
|
||||
assert h.shape[0] == N
|
||||
|
||||
# make sure it is symmetric
|
||||
midx = (N-1) // 2
|
||||
flip = array_namespace(h).flip
|
||||
assert_array_almost_equal(h[:midx], flip(h[midx+1:])) # h[:-midx-1:-1])
|
||||
|
||||
# make sure the center tap is 0.5
|
||||
assert math.isclose(h[midx], 0.5, abs_tol=1e-8)
|
||||
|
||||
# For halfband symmetric, odd coefficients (except the center)
|
||||
# should be zero (really small)
|
||||
hodd = xp.stack((h[1:midx:2], h[-midx+1::2]))
|
||||
assert_array_almost_equal(hodd, xp.zeros_like(hodd))
|
||||
|
||||
# now check the frequency response
|
||||
w, H = freqz(np.asarray(h), 1)
|
||||
w, H = xp.asarray(w), xp.asarray(H)
|
||||
f = w/2/xp.pi
|
||||
Hmag = xp.abs(H)
|
||||
|
||||
# check that the pass band is close to unity
|
||||
idx = xp.logical_and(f > 0, f < a)
|
||||
assert_array_almost_equal(Hmag[idx], xp.ones_like(Hmag[idx]), decimal=3)
|
||||
|
||||
# check that the stop band is close to zero
|
||||
idx = xp.logical_and(f > 0.5 - a, f < 0.5)
|
||||
assert_array_almost_equal(Hmag[idx], xp.zeros_like(Hmag[idx]), decimal=3)
|
||||
|
||||
def test_compare(self, xp):
|
||||
# compare to OCTAVE output
|
||||
taps = firls(9, xp.asarray([0, 0.5, 0.55, 1]),
|
||||
xp.asarray([1, 1, 0, 0]), weight=xp.asarray([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]
|
||||
atol_arg = {'atol': 5e-8} if xp_default_dtype(xp) == xp.float32 else {}
|
||||
known_taps = xp.asarray(known_taps, dtype=xp.float64)
|
||||
xp_assert_close(taps, known_taps, **atol_arg)
|
||||
|
||||
# compare to MATLAB output
|
||||
taps = firls(11, xp.asarray([0, 0.5, 0.5, 1]),
|
||||
xp.asarray([1, 1, 0, 0]), weight=xp.asarray([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]
|
||||
known_taps = xp.asarray(known_taps, dtype=xp.float64)
|
||||
atol_arg = {'atol': 3e-8} if xp_default_dtype(xp) == xp.float32 else {}
|
||||
xp_assert_close(taps, known_taps, **atol_arg)
|
||||
|
||||
# With linear changes:
|
||||
taps = firls(7, xp.asarray((0, 1, 2, 3, 4, 5)),
|
||||
xp.asarray([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]
|
||||
known_taps = xp.asarray(known_taps, dtype=xp.float64)
|
||||
xp_assert_close(taps, known_taps)
|
||||
|
||||
def test_rank_deficient(self, xp):
|
||||
# solve() runs but warns (only sometimes, so here we don't use match)
|
||||
x = firls(21, xp.asarray([0, 0.1, 0.9, 1]), xp.asarray([1, 1, 0, 0]))
|
||||
w, h = freqz(np.asarray(x), fs=2.)
|
||||
w, h = map(xp.asarray, (w, h)) # XXX convert freqz
|
||||
absh2 = xp.abs(h[:2])
|
||||
xp_assert_close(absh2, xp.ones_like(absh2), atol=1e-5)
|
||||
absh2 = xp.abs(h[-2:])
|
||||
xp_assert_close(absh2, xp.zeros_like(absh2), atol=1e-6, rtol=1e-7)
|
||||
# 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, xp.asarray([0, 0.01, 0.99, 1]), xp.asarray([1, 1, 0, 0]))
|
||||
w, h = freqz(np.asarray(x), fs=2.)
|
||||
w, h = map(xp.asarray, (w, h)) # XXX convert freqz
|
||||
mask = xp.asarray(w < 0.01)
|
||||
h = xp.asarray(h)
|
||||
assert xp.sum(xp.astype(mask, xp.int64)) > 3
|
||||
habs = xp.abs(h[mask])
|
||||
xp_assert_close(habs, xp.ones_like(habs), atol=1e-4)
|
||||
mask = xp.asarray(w > 0.99)
|
||||
assert xp.sum(xp.astype(mask, xp.int64)) > 3
|
||||
habs = xp.abs(h[mask])
|
||||
xp_assert_close(habs, xp.zeros_like(habs), 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:
|
||||
|
||||
@pytest.mark.thread_unsafe
|
||||
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, TypeError), 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]))
|
||||
xp_assert_close(h_new, np.asarray(h, dtype=np.float64), 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)
|
||||
xp_assert_close(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)
|
||||
xp_assert_close(np.abs(fft(h_new)), np.abs(fft(h_linear)), rtol=1e-4)
|
||||
|
||||
@skip_xp_backends("dask.array", reason="too slow")
|
||||
@skip_xp_backends("jax.numpy", reason="immutable arrays")
|
||||
def test_hilbert(self, xp):
|
||||
# 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]
|
||||
h = xp.asarray(h)
|
||||
k = xp.asarray(k, dtype=xp.float64)
|
||||
m = minimum_phase(h, 'hilbert')
|
||||
xp_assert_close(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]
|
||||
h = xp.asarray(h)
|
||||
k = xp.asarray(k, dtype=xp.float64)
|
||||
m = minimum_phase(h, 'hilbert', n_fft=2**19)
|
||||
xp_assert_close(m, k, rtol=2e-3)
|
||||
|
||||
|
||||
class Testfirwin_2d:
|
||||
def test_invalid_args(self):
|
||||
with pytest.raises(ValueError,
|
||||
match="hsize must be a 2-element tuple or list"):
|
||||
firwin_2d((50,), window=(("kaiser", 5.0), "boxcar"), fc=0.4)
|
||||
|
||||
with pytest.raises(ValueError,
|
||||
match="window must be a 2-element tuple or list"):
|
||||
firwin_2d((51, 51), window=("hamming",), fc=0.5)
|
||||
|
||||
with pytest.raises(ValueError,
|
||||
match="window must be a 2-element tuple or list"):
|
||||
firwin_2d((51, 51), window="invalid_window", fc=0.5)
|
||||
|
||||
def test_filter_design(self):
|
||||
hsize = (51, 51)
|
||||
window = (("kaiser", 8.0), ("kaiser", 8.0))
|
||||
fc = 0.4
|
||||
taps_kaiser = firwin_2d(hsize, window, fc=fc)
|
||||
assert taps_kaiser.shape == (51, 51)
|
||||
|
||||
window = ("hamming", "hamming")
|
||||
taps_hamming = firwin_2d(hsize, window, fc=fc)
|
||||
assert taps_hamming.shape == (51, 51)
|
||||
|
||||
def test_impulse_response(self):
|
||||
hsize = (31, 31)
|
||||
window = ("hamming", "hamming")
|
||||
fc = 0.4
|
||||
taps = firwin_2d(hsize, window, fc=fc)
|
||||
|
||||
impulse = np.zeros((63, 63))
|
||||
impulse[31, 31] = 1
|
||||
|
||||
response = convolve2d(impulse, taps, mode='same')
|
||||
|
||||
expected_response = taps
|
||||
xp_assert_close(response[16:47, 16:47], expected_response, rtol=1e-5)
|
||||
|
||||
def test_frequency_response(self):
|
||||
"""Compare 1d and 2d frequency response. """
|
||||
hsize = (31, 31)
|
||||
windows = ("hamming", "hamming")
|
||||
fc = 0.4
|
||||
taps_1d = firwin(numtaps=hsize[0], cutoff=fc, window=windows[0])
|
||||
taps_2d = firwin_2d(hsize, windows, fc=fc)
|
||||
|
||||
f_resp_1d = fft(taps_1d)
|
||||
f_resp_2d = fft2(taps_2d)
|
||||
|
||||
xp_assert_close(f_resp_2d[0, :], f_resp_1d,
|
||||
err_msg='DC Gain at (0, f1) is not unity!')
|
||||
xp_assert_close(f_resp_2d[:, 0], f_resp_1d,
|
||||
err_msg='DC Gain at (f0, 0) is not unity!')
|
||||
xp_assert_close(f_resp_2d, np.outer(f_resp_1d, f_resp_1d),
|
||||
atol=np.finfo(f_resp_2d.dtype).resolution,
|
||||
err_msg='2d frequency response is not product of 1d responses')
|
||||
|
||||
def test_symmetry(self):
|
||||
hsize = (51, 51)
|
||||
window = ("hamming", "hamming")
|
||||
fc = 0.4
|
||||
taps = firwin_2d(hsize, window, fc=fc)
|
||||
xp_assert_close(taps, np.flip(taps), rtol=1e-5)
|
||||
|
||||
def test_circular_symmetry(self):
|
||||
hsize = (51, 51)
|
||||
window = "hamming"
|
||||
taps = firwin_2d(hsize, window, circular=True, fc=0.5)
|
||||
center = hsize[0] // 2
|
||||
for i in range(hsize[0]):
|
||||
for j in range(hsize[1]):
|
||||
xp_assert_close(taps[i, j],
|
||||
taps[center - (i - center), center - (j - center)],
|
||||
rtol=1e-5)
|
||||
|
||||
def test_edge_case_circular(self):
|
||||
hsize = (3, 3)
|
||||
window = "hamming"
|
||||
taps_small = firwin_2d(hsize, window, circular=True, fc=0.5)
|
||||
assert taps_small.shape == (3, 3)
|
||||
|
||||
hsize = (101, 101)
|
||||
taps_large = firwin_2d(hsize, window, circular=True, fc=0.5)
|
||||
assert taps_large.shape == (101, 101)
|
||||
|
||||
def test_known_result(self):
|
||||
hsize = (5, 5)
|
||||
window = ('kaiser', 8.0)
|
||||
fc = 0.1
|
||||
fs = 2
|
||||
|
||||
row_filter = firwin(hsize[0], cutoff=fc, window=window, fs=fs)
|
||||
col_filter = firwin(hsize[1], cutoff=fc, window=window, fs=fs)
|
||||
known_result = np.outer(row_filter, col_filter)
|
||||
|
||||
taps = firwin_2d(hsize, (window, window), fc=fc)
|
||||
assert taps.shape == known_result.shape, (
|
||||
f"Shape mismatch: {taps.shape} vs {known_result.shape}"
|
||||
)
|
||||
assert np.allclose(taps, known_result, rtol=1e-1), (
|
||||
f"Filter shape mismatch: {taps} vs {known_result}"
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue