Signal module#
This module provides (mostly) orthonormal lazy linear operators associated to fast linear transforms commonly used in signal processing.
List of transforms#
lazylinop.signal.convolve()The lazy convolution operator is obtained asL = convolve(N, filter, ...)withNthe dimension of the input signal andfilterthe filter (given as a vector), so thaty = L @ xyields the prescribed convolution. Whenever neededz = L.H @ ycomputes the adjoint of the convolution operator, appropriately taking into account all boundary conditions that you may have specified using the “mode” option ofconvolve().lazylinop.signal.dct()Discrete Cosine Transform of types I to IVlazylinop.signal.dst()Discrete Sine Transform of types I to IVlazylinop.signal.mdct()Modified DCTlazylinop.signal.imdct()Inverse of the Modified DCTlazylinop.signal.dft()Discrete Fourier Transformlazylinop.signal.wht()Walsh-Hadamard Transformlazylinop.signal.dwt()Discrete Wavelet Transformlazylinop.signal.idwt()Inverse of the Discrete Wavelet Transformlazylinop.signal.dzt()Zak Transformlazylinop.signal.stft()Short-Time-Fourier-Transform with a given windowlazylinop.signal.istft()Inverse of Short-Time-Fourier-Transformlazylinop.signal.nufft()Nonuniform Fast-Fourier-Transform
Padding or cropping#
Traditional implementations of signal processing transforms offer the possibility to either crop or zero-pad the analyzed signal.
We chose not to include this in our implementation, as mimicing this feature is simple with the generic lazylinop interface.
Consider for the example F = dft(N), the operator associated to the NxN DFT matrix.
To apply it to a signal x of length n, we can define G = eye(N, n), and observe that F @ G does exactly what we need:
padding: if \(n<N\),
G @ xis a zero padded version ofx, of size \(N\), so thatH = F @ Gis the lazy linear operator that computes the DFT after zero padding. An even simpler implementation isH = F[:,:n].cropping: if \(n>N\),
G @ xis a cropped version ofxof size \(N\), andH = F @ Gis again exactly what you need.
Inverse transforms#
The lazy linear operator associated to a given transform (e.g., the DFT) is obtained as F = dft(N) with \(N\) the dimension of the input signal, and y = F @ x yields the DFT of x, a vector of size \(N\).
You may be surprised that we generally do not provide any implementation for inverse transforms (i.e., matching pairs of functions fft/ifft, dct/idct etc.).
This is simply due to the fact that since most of the transforms we provide (to the exception of the STFT, MDCT, NUFFT and DWT when non-orthogonal wavelet is used) are orthonormal, their inverse is simply their adjoint, e.g. F.H is the lazy linear operator associated to the inverse DFT: z = F.H @ y recovers x.
Orthonormality corresponds to the fact that both F.H @ F and F @ F.H are lazy linear operators that act as the identity matrix (up to numerical precision).
When F is a DCT/DST/WHT operator, its array version F.toarray() is real-valued, so you can also use F.T instead of F.H, and both F @ F.T and F.T @ F act as the identity.
The main exception to orthonormality are the MDCT, the NUFFT and the STFT: for example, the operator M = mdct(N) acts on signals of size N, and is only defined when N is even, with y = M @ x yielding a signal of size N/2.
The matrix version of M is (real-valued and) rectangular of size \(\left[N/2,N\right]\) so that M.T @ M cannot correspond to an identity, however we chose to normalize M in such a way that both M @ M.T and M @ M.H correspond to the identity.
The other exception is the DWT: see its documentation (dwt()) for details on parameters ensuring that L = dwt(...) satisfies L @ L.T = L.T @ L = Id (or only L.T @ L = Id when the shape of L is rectangular).
Other normalizations#
Traditional implemementations of signal processing transforms offer various normalizations (e.g., with division by N, sqrt(N), or no division, or slightly more subtle operations for the DCT/DST).
They can all be mimicked (if really needed) by pre- or post-composing the transform F.
As an illustration, SciPy’s fft and ifft with the default normalization is mimicked as follows.
>>> import numpy as np
>>> from scipy.fft import fft as sp_fft
>>> from scipy.fft import ifft as sp_ifft
>>> from lazylinop.signal import dft as lz_dft
>>> N = 32
>>> x = np.random.randn(N)
>>> F = lz_dft(N)
>>> scale = sqrt(N)
>>> y = scale * F @ x
>>> np.allclose(y, sp_fft(x))
True
>>> x_ = F.H @ y / scale
>>> np.allclose(x_, sp_ifft(y))
True
To mimick SciPy’s DCT/DST called with orthogonalize=True, the same trick holds where scale depends on the transform’s type (I,II,III,IV) and the choice of norm ('backward' or 'forward'; NB: scale = 1 if norm = 'ortho').
Mimicking the DCT/DST with orthogonalize=False requires pre- and/or post-composing by diagonal operators that depend on the type.
For example, the default DCT-II behavior (norm = 'ortho'):
>>> from lazylinop.signal import dct as lz_dct
>>> from scipy.fft import dct as sp_dct
>>> from lazylinop.basicops import diag
>>> import numpy as np
>>> N = 32
>>> x = np.random.randn(N)
>>> F = lz_dct(N)
>>> v = np.full(N, 1.0)
>>> v[0] = np.sqrt(2.0)
>>> y = diag(v) @ F @ x
>>> z = sp_dct(x, 2, N, 0, 'ortho', False, 1, orthogonalize=False)
>>> np.allclose(y, z)
Note: to be coherent with other transforms, our implementation of the Walsh-Hadamard Transform is orthonormal, unlike the Hadamard function of Scipy.
Transforms#
- lazylinop.signal.convolve(N, filter, mode='full', backend='scipy_convolve')#
Returns a
LazyLinOpLfor the 1d convolution of signal(s) of sizeNwith a kernelfilter.Shape of
Lis \((M,~N)\). Seemodefor output size \(M\).- Args:
- N:
int Size of the signal to be convolved.
- filter:
np.ndarray,cp.ndarrayortorch.Tensor Filter to be applied to the signal.
filtermust be a 1d-array.- mode:
str, optional 'full': compute full convolution including at points of non-complete overlapping of inputs. Output sizeMisN + filter.size - 1.'valid': compute only fully overlapping part of the convolution. This is the ‘full’ center part of (output) sizeM = max(N, K) - min(N, K) + 1whereK = filter.size.'same': compute only the center part of'full'to obtain an output sizeMequal to the input sizeN.'circ': compute circular convolution (filter.size <= Nmust be satisfied). Output sizeMisN.
- backend:
str, optional 'auto': use bestbackendavailable according to the namespace of filter.for
mode='circ'usebackend='fft'.for any other mode use
backend='direct'iffilteris a NumPy array andfilter.size < log2(M),backend='scipy_convolve'iffilteris a NumPy array andfilter.size >= log2(M),backend='scipy_convolve'iffilteris a CuPy or NumPy array,backend='torch'iffilteris a torch tensor.
'scipy_convolve': (default) encapsulates(cupyx).scipy.signal.convolve.It uses internally the best SciPy backend between
'fft'and'direct'(see scipy.signal.choose_conv_method). If the filter and the input are CuPy arrays the function usescupyx.scipy.signal.convolve.'direct': direct computation using nested for-loops with Numba and parallelization. It does not work if the input is CuPy array or torch tensor.'fft': (only formode='circ') compute circular convolution using(cupyx).scipy.fft.fft.'toeplitz': encapsulate(cupyx).scipy.linalg.toeplitzifN < 2048,(cupyx).scipy.linalg.matmul_toeplitzotherwise. Of note, there is no torch implementation for Toeplitz matrix.'oa': use Lazylinop implementation of overlap-add backend.'torch': usetorchaudio.functional.convolve.
- N:
- Returns:
- Examples:
>>> import numpy as np >>> import scipy.signal as sps >>> import lazylinop.signal as lzs >>> N = 1024 >>> x = np.random.randn(N) >>> kernel = np.random.randn(32) >>> L = lzs.convolve(N, kernel) >>> c1 = L @ x >>> c2 = sps.convolve(x, kernel) >>> np.allclose(c1, c2) True >>> N = 32768 >>> x = np.random.randn(N) >>> kernel = np.random.randn(48) >>> L = lzs.convolve(N, kernel, mode='circ', backend='fft') >>> c1 = L @ x >>> L = lzs.convolve(N, kernel, mode='circ', backend='direct') >>> c2 = L @ x >>> np.allclose(c1, c2) True
See also
- lazylinop.signal.dct(N, type=2, backend='scipy')#
Returns a
LazyLinOp`Lfor the Direct Cosine Transform (DCT).Shape of
Lis \((N,~N)\).Lis orthonormal, and theLazyLinOpfor the inverse DCT isL.T.The function provides two backends: SciPy and Lazylinop.
- Args:
- N:
int Size of the input (N > 0).
- type:
int, optional 1, 2, 3, 4 (I, II, III, IV). Defaut is 2. See SciPy DCT and CuPy DCT for more details.
- backend: str, optional
'scipy'(default) uses(cupyx).scipy.fft.dctencapsulation for the underlying computation of the DCT.
'lazylinop'uses a composition of basic Lazylinop operators to compute the DCT (fft(),vstack()etc.).
- N:
- Returns:
- Example:
>>> from lazylinop.signal import dct as lz_dct >>> from scipy.fft import dct as sp_dct >>> import numpy as np >>> N = 32 >>> x = np.random.randn(N) >>> F = lz_dct(N) >>> y = F @ x >>> np.allclose(y, sp_dct(x, norm='ortho')) True >>> # compute the inverse DCT >>> x_ = F.T @ y >>> np.allclose(x_, x) True >>> # To mimick SciPy DCT II norm='ortho' and orthogonalize=False >>> from lazylinop.basicops import diag >>> v = np.full(N, 1.0) >>> v[0] = np.sqrt(2.0) >>> y = diag(v) @ F @ x >>> z = sp_dct(x, 2, N, 0, 'ortho', False, 1, orthogonalize=False) >>> np.allclose(y, z) True
- References:
- [1] A Fast Cosine Transform in One and Two Dimensions,
by J. Makhoul, IEEE Transactions on acoustics, speech and signal processing vol. 28(1), pp. 27-34, :doi:`10.1109/TASSP.1980.1163351` (1980).
See also
- lazylinop.signal.dst(N, type=2, backend='scipy')#
Returns a
LazyLinOpLfor the Direct Sine Transform (DST).Shape of
Lis \((N,~N)\).Lis orthonormal, and theLazyLinOpfor the inverse DST isL.T.The function provides two backends: SciPy and Lazylinop.
- Args:
- N:
int Size of the input (N > 0).
- type:
int, optional 1, 2, 3, 4 (I, II, III, IV). Defaut is 2. See SciPy DST and CuPy DST for more details.
- backend: str, optional
'scipy'(default) uses(cupyx).scipy.fft.dstto compute the DST depending on the input array.'lazylinop'Uses a composition of basic Lazylinop operators to compute the DST (fft(),vstack()etc.).
- N:
- Returns:
- Example:
>>> from lazylinop.signal import dst as lz_dst >>> from scipy.fft import dst as sp_dst >>> import numpy as np >>> x = np.random.randn(32) >>> F = lz_dst(32) >>> y = F @ x >>> np.allclose(y, sp_dst(x, norm='ortho')) True >>> # compute the inverse DST >>> x_ = F.T @ y >>> np.allclose(x_, x) True
See also
- lazylinop.signal.mdct(N, window=('vorbis', 128), backend='scipy')#
Returns a
LazyLinOpLfor the Modified Direct Cosine Transform (MDCT).Shape of
Lis \((M,~N)\) with \(M=n\frac{W}{2}\).\(n\) is the number of chunks and \(W\) is the window size.
Our implementation matches the one from TensorFlow,
L @ xwhereL = mdct(N, window=('vorbis', 128))is equivalent to:import tensorflow as tf tf.signal.mdct(x, frame_length=128, window_fn=tf.signal.vorbis_window, norm='ortho')
If we consider MDCT using a rectangular window with a scale \(\frac{1}{\sqrt{2}}\),
L = mdct(N, window=('None', N)),X = L @ xyiels a vectorXof size \(H=\frac{N}{2}\) such that:\[\begin{equation} X_k=\frac{1}{\sqrt{H}}\sum_{n=0}^{N-1}x_n\cos\left(\frac{\pi}{H}\left(n+\frac{1}{2}+\frac{H}{2}\right)\left(k+\frac{1}{2}\right)\right) \end{equation}\]y = L.T @ Xyields a vectoryof size \(N\) such that:\[\begin{equation} y_n=\frac{1}{\sqrt{H}}\sum_{k=0}^{H-1}X_k\cos\left(\frac{\pi}{H}\left(n+\frac{1}{2}+\frac{H}{2}\right)\left(k+\frac{1}{2}\right)\right) \end{equation}\]The operator
Lis rectangular and is not left invertible. It is however right-invertible asL @ L.T = Id. Thus,L.Tcan be used as a right-inverse.After removing some details the code looks like:
# Consecutive windows are slided by hop samples. hop = win_size // 2 # Number of chunks. n = 1 + (N - win_size) // hop # Lazy linear operator that extracts and # concatenates chunks from a signal of length N. G = chunk(N, win_size, hop)
and using the mixed Kronecker product property \((I^T\otimes A)\mathtt{vec}(X)=\mathtt{vec}(AXI)=\mathtt{vec}(AX)\):
K = kron(eye(n), _helper(win_size, backend) @ diag(win)) L = K @ G
where
_helper(...)encapsulates underlying implementation using DCT of type IV (see Ref [1] for more details).The function provides two backends: SciPy and Lazylinop for the underlying computation of the DCT of type IV.
- Args:
- N:
int Length of the input array.
- window:
(str, int)or(str, int, float), optional Window, a tuple
(name: str, win_size: int)or(name: str, win_size: int, beta: float). Window size must be a mutliple of 4. Default is('vorbis', 128).betahas no effect excepts for'kaiser_bessel_derived'window. Possible windows are:'None'corresponds to a rectangular window with a scale \(\frac{1}{\sqrt{2}}\).'kaiser_bessel_derived'see scipy.signal.window.kaiser_bessel_derived for more details.'vorbis'(default) or'sin'see https://en.wikipedia.org/wiki/Modified_discrete_cosine_transform for more details.
- backend: str, optional
'scipy'(default) usesscipy.fft.dctencapsulation for the underlying computation of the DCT of type IV.backend='scipy'does not work with CuPy input array becausecupyx.signal.fft.dctonly implements type II and III.'lazylinop'uses pre-built Lazylinop operators (Lazylinopdct(),eye(),kron(),vstack()etc.) to build the pipeline that will compute the MDCT and the underlying DCT of type IV.
- N:
- Returns:
- Example:
>>> from lazylinop.signal import mdct >>> import numpy as np >>> x = np.random.randn(64) >>> # MDCT with a rectangular window >>> # of size equal to the size of the input. >>> L = mdct(64, ('None', 64)) >>> y = L @ x >>> y.shape[0] == 32 True >>> x_ = L.T @ y >>> x_.shape[0] == 64 True
- References:
[1] Xuancheng Shao, Steven G. Johnson, Type-IV DCT, DST, and MDCT algorithms with reduced numbers of arithmetic operations, Signal Processing, Volume 88, Issue 6, 2008, Pages 1313-1326, ISSN 0165-1684, https://doi.org/10.1016/j.sigpro.2007.11.024.
- lazylinop.signal.imdct(N, window=('vorbis', 128), backend='scipy')#
Returns a
LazyLinOpiLfor the inverse Modified Direct Cosine Transform (iMDCT).Shape of
iLis \((P,~M)\) with \(M=n\frac{W}{2}\) and \(P=(n-1)\frac{W}{2}+W\).\(n\) is the number of chunks and \(W\) is the window size.
Nis the size of the input signal of the MDCT LazyLinop, not of the iMDCT.Our implementation matches the one from TensorFlow,
iL @ ywhereiL = imdct(N, window=('vorbis', N))is equivalent to:import tensorflow as tf tf.signal.inverse_mdct(y, window_fn=tf.signal.vorbis_window, norm='ortho')
The operator
iLis rectangular and is not right invertible. It is however left-invertible asiL.T @ iL. Thus,iL.Tcan be used as a left-inverse.After removing some details the code looks like:
# Consecutive windows are slided by hop samples. hop = win_size // 2 # Number of chunks. n = 1 + (N - win_size) // hop if N == win_size: return diag(win) @ _helper(N, backend) else: # Apply inverse MDCT per chunk followed by windowing. K = kron(eye(n), diag(win) @ _helper(win_size, backend)) # Overlap and add. A = overlap_add(K.shape[0], size=win_size, overlap=win_size - hop) # Restrict to get the output of length N. iL = eye(N, A.shape[0]) @ A @ K
where
_helper(...)encapsulates underlying implementation using DCT of type IV (see Ref [1] for more details).The function provides two backends: SciPy and Lazylinop for the underlying computation of the DCT of type IV.
- Args:
- N:
int Length of the output array (see above).
- window:
(str, int)or(str, int, float), optional Window, a tuple
(name: str, win_size: int)or(name: str, win_size: int, beta: float). Window size must be a mutliple of 4. Default is('vorbis', 128).betahas no effect excepts for'kaiser_bessel_derived'window. Possible windows are:'None'corresponds to a rectangular window with a scale \(\frac{1}{\sqrt{2}}\).'kaiser_bessel_derived'see scipy.signal.window.kaiser_bessel_derived for more details.'vorbis'(default) or'sin'see https://en.wikipedia.org/wiki/Modified_discrete_cosine_transform for more details.
- backend: str, optional
'scipy'(default) usesscipy.fft.dctencapsulation for the underlying computation of the DCT of type IV.backend='scipy'does not work with CuPy input array becausecupyx.signal.fft.dctonly implements type II and III.'lazylinop'uses pre-built Lazylinop operators (Lazylinopdct(),eye(),kron(),vstack()etc.) to build the pipeline that will compute the MDCT and the underlying DCT of type IV.
- N:
- Returns:
- Example:
>>> from lazylinop.signal import mdct, imdct >>> import numpy as np >>> N = 64 >>> x = np.random.randn(N) >>> # MDCT with a rectangular window >>> # of size equal to the size of the input. >>> L = imdct(N, window=('None', N)) >>> L.shape[0] == 64 True >>> L.shape[1] == 32 True
- References:
[1] Xuancheng Shao, Steven G. Johnson, Type-IV DCT, DST, and MDCT algorithms with reduced numbers of arithmetic operations, Signal Processing, Volume 88, Issue 6, 2008, Pages 1313-1326, ISSN 0165-1684, https://doi.org/10.1016/j.sigpro.2007.11.024.
- lazylinop.signal.dft(N)#
Returns a
LazyLinOp`Lfor the Discrete Fourier Transform (DFT).Shape of
Lis \((N,~N)\).Lis orthonormal, and theLazyLinOpfor the inverse DFT isL.H.SciPy FFT is used as underlying implementation.
Of note, we provide an alias
fftof thedftfunction.- Args:
- N:
int Size of the input (\(N > 0\)).
- N:
- Returns:
LazyLinOpDFT- Examples:
>>> from lazylinop.signal import dft as lz_dft >>> from scipy.fft import fft as sp_fft >>> from scipy.fft import ifft as sp_ifft >>> import numpy as np >>> N = 32 >>> x = np.random.randn(N) >>> F = lz_dft(N) >>> y = F @ x >>> np.allclose(y, sp_fft(x, norm='ortho')) True >>> # easy inverse >>> x_ = F.H @ y >>> np.allclose(x_, x) True >>> # To mimick SciPy FFT norm='backward' >>> scale = np.sqrt(N) >>> y = scale * F @ x >>> np.allclose(y, sp_fft(x)) True >>> x_ = F.H @ y / scale >>> np.allclose(x_, sp_ifft(y)) True
See also
scipy.fft.fft, scipy.fft.ifft,
rfft()
- lazylinop.signal.wht(N, backend='auto')#
Returns a
LazyLinOp`Lfor the Fast-Walsh-Hadamard-Transform (WHT).Shape of
Lis \((N,~N)\). N must be a power of 2.Lis orthonormal and symmetric and the inverse WHT operator isL.T = L.y = L @ xreturns a vector of size \(N\) that satisfiesy = scipy.linalg.hadamard(N) @ x / np.sqrt(N). The implementation is however more efficient for large values of \(N\).Of note, we provide an alias
fwhtof thewhtfunction.- Args:
- N:
int Size of the input (N > 0).
- backend:
str, optional Either ‘auto’ (default), ‘direct’, ‘kronecker’, ‘pyfaust’ or ‘scipy’.
‘auto’ uses ‘pyfaust’ for
N<=32768, ‘direct’ otherwise. Of note, this condition depends on your hardware. It may change in the near future.‘direct’ uses
N * log(N)algorithm and Numba jit to compute FWHT. Because of Numba jit, computation of the firstF @ xwill be longer than the next one.Larger the size
Nof the input is better the performances are.Larger the batch is better the performances are.
‘kronecker’ uses kron from lazylinop to compute FWHT.
‘pyfaust’ (default) uses
whtfrom pyfaust.‘scipy’ uses
scipy.linalg.hadamardmatrix. It could be memory consuming for largeN.‘cupy’ uses
cupyx.scipy.linalg.hadamardmatrix. It could be memory consuming for largeN.
- N:
- Returns:
LazyLinOp
- Examples:
>>> import numpy as np >>> import scipy.linalg as spl >>> import lazylinop.signal as lzs >>> N = 16 >>> x = np.random.randn(N) >>> H = lzs.fwht(N) >>> y = np.sqrt(N) * (H @ x) >>> np.allclose(y, spl.hadamard(N) @ x) True >>> np.allclose(H.T @ (H @ x), x) True >>> X = np.random.randn(N, 3) >>> Y = np.sqrt(N) * (H @ X) >>> np.allclose(Y, spl.hadamard(N) @ X) True
- lazylinop.signal.dwt(N, wavelet='haar', mode='zero', level=None, backend='pywavelets')#
Returns a
LazyLinOpLfor the Discrete Wavelet Transform (DWT).L @ xwill return a 1D numpy array as the concatenation of the DWT coefficients in the form[cAₙ, cDₙ, cDₙ₋₁, …, cD₂, cD₁], wherenis the decomposition level,cAₙis the approximation coefficients for levelnandcDᵢis the detail coefficients at decomposition leveli.Shape of
Lis \((M,~N)\) where \(M\) depends on thewavelet, input sizeNand decompositionlevel.In general,
Lis not orthogonal. However, whenwaveletis either the name of an orthogonal wavelet (see Pywavelets documentation), or a tuple of Quadrature-Mirrors-Filters, the following properties holds: - ifmode='zero'thenL.Tis a left-inverse toL,i.e.
L.T @ L = Id(but sinceL.shape = (M, N)with \(M>N\),Ldoes not satisfyL @ L.T = Id.if
mode='periodization'and if the signal sizeNis a power of two, then \(M=N\) andLis orthogonal so thatL @ L.T = L.T @ L = Id
See technical notes for more details.
After removing some details the code looks like (for the first level of decomposition and without extension mode):
# Convolution operators with low-pass and # high-pass decomposition filters. G = convolve(N, dec_lo, mode='same') H = convolve(N, dec_hi, mode='same') # Downsampling operator. DG = downsample(G.shape[0], 2, 1) DH = downsample(H.shape[0], 2, 1) # Decomposition operator. L = vstack((DG @ G, DH @ H))
- Args:
- N:
int Size of the input array.
wavelet:
stror tuple of(dec_lo, dec_hi), optionalIf a
stris provided, the wavelet name from Pywavelets library. In that case, the namespace of the low and high-pass filters depends on the namespace of the input array.If a tuple
(dec_lo, dec_hi)of two NumPy, CuPy arrays or torch tensors. is provided, the low and high-pass filters (for decomposition) used to define the wavelet.The
dwt()function does not test whether these two filters are actually Quadrature-Mirror-Filters.
mode:
str, optional'zero', signal is padded with zeros (default).'periodic', signal is treated as periodic signal.'symmetric', use mirroring to pad the signal.'antisymmetric', signal is extended by mirroring and multiplying elements by minus one.'reflect', signal is extended by reflecting elements.'periodization', signal is extended like'periodic'extension mode. Only the smallest possible number of coefficients is returned. Odd-length signal is extended first by replicating the last value.
- level:
int, optional Decomposition level, by default (None) the maximum level is used.
- backend:
str, optional 'pywavelets'(default) or'lazylinop'for the underlying computation of the DWT.backend='pywavelets'does not work for CuPy array and torch tensor inputs.
- N:
- Returns:
LazyLinOpDWT.- Examples:
>>> from lazylinop.signal import dwt >>> import numpy as np >>> import pywt >>> N = 8 >>> x = np.arange(1, N + 1, 1) >>> L = dwt(N, wavelet='haar', mode='periodic', level=1) >>> y = L @ x >>> z = pywt.wavedec(x, wavelet='haar', mode='periodic', level=1) >>> np.allclose(y, np.concatenate(z)) True
- lazylinop.signal.idwt(N, wavelet='haar', mode='zero', level=None, backend='pywavelets')#
Returns a
LazyLinOpiLfor the inverse Discrete Wavelet Transform (iDWT).If
L = dwt(N, wavelet, mode, level, backend)is the DWT operator of shape \((M,~N)\) with \(M\ge~N\), theniL = idwt(N, wavelet, mode, level, backend)is the iDWT operator such thatiL @ L = Id. As a result, ify = L @ xis the coefficients at level decompositionlevel, then the N-dimensionnal signalxcan be reconstructed from the M-dimensionnal vectoryasiL @ y.Shape of
iLis \((N,~M)\).Nis the size of the input signal of the DWT LazyLinop, not of the iDWT.The order is not a typo since
Nis the input length of the associated DWT operatorL, which is of shape \((M,~N)\), andiLis of the same shape asL.H.
In general,
iLis not orthogonal. See technical notes for more details.The implementation uses pre-built
Lazylinopoperators to build iDWT operator.After removing some details the code looks like (for the first level of decomposition):
# Wavelet length. W = pywt.Wavelet(wavelet).dec_len # Number of detail and approximation coefficients. m = (N + W - 1) // 2 # Upsampling operator. U = downsample(2 * m, step=2, start=0).H # Convolution operators with low-pass and # high-pass reconstruction filters. Cl = convolve(U.shape[0], rec_lo, mode='same') Ch = convolve(U.shape[0], rec_hi, mode='same') C = hstack((Cl @ U, Ch @ U)) # Restrict. R = eye(N, C.shape[0], k=(C.shape[0] - N) // 2) # Reconstruction operator. iL = R @ C
Reconstruction using
'dmey'wavelet does not ensurenp.allclose(x, L @ y) == True.import numpy as np import pywt N = 133 x = np.random.randn(N) y = pywt.wavedec(x, wavelet='dmey', level=1, mode='periodic') z = pywt.waverec(y, wavelet='dmey', mode='periodic') np.allclose(x, z[:N]) False y = pywt.wavedec(x, wavelet='coif11', level=1, mode='periodic') z = pywt.waverec(y, wavelet='coif11', mode='periodic') np.allclose(x, z[:N]) True
- Args:
- N:
int Length of the output array (i.e., length of the input array of the associated DWT
LazyLinOp, see above).
wavelet:
stror tuple of(rec_lo, rec_hi), optionalIf a
stris provided, the wavelet name from Pywavelets libraryIf a tuple
(rec_lo, rec_hi)of two NumPy, CuPy arrays or torch tensors is provided, the low and high-pass filters (for reconstruction) used to define the wavelet.The
idwt()function does not test whether these two filters are actually Quadrature-Mirror-Filters.
mode:
str, optional'zero', signal is padded with zeros (default).'periodic', signal is treated as periodic signal.'periodization', signal is extended like'periodic'extension mode. Only the smallest possible number of coefficients is returned. Odd-length signal is extended first by replicating the last value.'symmetric', use mirroring to pad the signal.'antisymmetric', signal is extended by mirroring and multiplying elements by minus one.'reflect', signal is extended by reflecting elements.
- level:
int, optional Decomposition level, by default (None) the maximum level is used.
- backend:
str, optional ‘pywavelets’ (default) or ‘lazylinop’ for the underlying computation of the iDWT.
backend='pywavelets'does not work for CuPy array and torch tensor inputs.
- N:
- Returns:
LazyLinOpiDWT.- Examples:
>>> from lazylinop.signal import dwt, idwt >>> import numpy as np >>> N = 8 >>> x = np.arange(1, N + 1, 1) >>> W = dwt(N, mode='periodic', level=2) >>> y = W @ x >>> L = idwt(N, mode='periodic', level=2) >>> x_ = L @ y >>> np.allclose(x, x_) True
- lazylinop.signal.dzt(N, a=1)#
Returns a
LazyLinOpLfor the Discrete Zak Transform (DZT). The parameteramust divide input lengthN.\[\begin{equation} Z_x^a\left(n, k\right)=\frac{1}{\sqrt{a}}\sum_{l=0}^ax\left(n+lL\right)e^{-2j\pi\frac{kl}{a}} \end{equation}\]Shape of
Lis \((N,~N)\).Lis orthonormal, and theLazyLinOpfor the inverse DZT isL.H.- Args:
- N:
int Size of the input (\(N > 0\)).
- a:
int, optional The parameter
amust divide input lengthN. Default is 1.
- N:
- Returns:
LazyLinOpDZT- Examples:
>>> from lazylinop.signal import dzt >>> import numpy as np >>> Z = dzt(32, 1) >>> x = np.random.randn(32) >>> y = Z @ x >>> x_ = Z.H @ y >>> np.allclose(x_, x) True
- References:
- [1] H. Bölcskei and F. Hlawatsch. Discrete Zak transforms, polyphase
transforms, and applications. IEEE Trans. Signal Process., 45(4):851–866, april 1997.
See also
- lazylinop.signal.stft(N, window=('hann', 256), hop=None, fft_size=None)#
Returns a
LazyLinOpfor the Short-Time-Fourier-Transform (STFT), so thatL @ xis equivalent to:import scipy as sp win_size = window[1] STFT = sp.signal.ShortTimeFFT( win=sp.signal.windows.get_window(window, win_size), hop=hop, fs=1.0, fft_mode='twosided', fft_size=None, phase_shift=None ) STFT.stft(x)
The STFT computes and concatenates Fourier transforms of windowed signal slices where consecutive windows are slided by
hopsamples.The implementation uses pre-built
Lazylinopoperators to build the STFT operator.After removing some details the code looks like:
# For a 1d real valued array win = sp.signal.get_window(win_name, win_size) # Number of chunks. n = 1 + (N - win_size) // hop # Lazy linear operator that extracts and # concatenates chunks from a signal of length N. G = chunk(N, win_size, hop) # Lazy operator that multiplies a (batch of) # chunk(s) of length win_len by the window. W = diag(win) # Lazy operator computing the DFT of a (batch of) # windowed chunk(s) of length win_len with appropriate padding. FW = dft(fft_size)[:win_size, :] @ W
and using the mixed Kronecker product property \((I^T\otimes A)\mathtt{vec}(X)=vec(AXI)=vec(AX)\):
# Final lazy operator that extract chunks, applies F on each chunk. L = kron(eye(n), FW) @ G
Shape of
Lis \((M,~N)\). In general \(M\ge~N\).- Args:
- N:
int Length of the input array.
- window: NumPy/CuPy array, torch tensor or
(str, int), optional Window, either directly provided as an 1d (real or complex valued) array, or as a pair
(name: str, win_size: int)which is equivalent to usewindow=scipy.signal.get_window(name, length). Default is('hann', 256). See scipy.signal.get_window for a list of available windows.Be aware that if
windowdoes not have the same namespacexp = array_api_compat.array_namespace(x)thanx, computation ofL @ xuses an extraxp.asarray(window, device=array_api_compat.device(x), copy=True). This incur a loss of performance.- hop:
int, optional The increment in sample location between two consecutives slices.
None(default) corresponds tolen(window) // 2.
- fft_size:
int, optional Size of the FFT, if zero-padding is required. Default value is
None. It corresponds tofft_size=len(window).
- N:
- Returns:
LazyLinOpL for the STFT- Examples:
>>> import lazylinop as lz >>> import numpy as np >>> import scipy as sp >>> x = np.random.randn(1024) >>> L = lz.signal.stft(x.shape[0]) >>> win = sp.signal.windows.get_window('hann', 256) >>> STFT = sp.signal.ShortTimeFFT(win=win, hop=128, fs=1.0, fft_mode="twosided", phase_shift=None) >>> np.allclose(STFT.stft(x).ravel(order='F'), L @ x) True
- lazylinop.signal.istft(N, window=('hann', 256), hop=None, fft_size=None)#
Returns a LazyLinOp
LazyLinOpiLfor the inverse Short-Time-Fourier-Transform (iSTFT), so that ifLis the LazyLinop of the STFT with the same arguments,iL @ Lis the identity.Shape of
iLis \((N,~M)\) with \(M\ge~N\).Nis the size of the input signal of the STFT LazyLinop, not of the iSTFT.The order is not a typo since
Nis the input length of the associated STFT operatorL, which is of shape \((M,~N)\), andiLis of the same shape asL.H.
- Args:
- N:
int Length of the output array (i.e., length of the input array of the associated STFT
LazyLinOp, see above).- window: NumPy array, CuPy array, torch tensor or
(str, int), optional Window, either directly provided as an 1d (real or complex valued) array , or as a pair
(name: str, win_size: int)which is equivalent to usewindow=scipy.signal.get_window(name, length). Default is('hann', 256). See scipy.signal.get_window for a list of available windows.Be aware that if
windowdoes not have the same namespacexp = array_api_compat.array_namespace(x)thanx, computation ofL @ xuses an extraxp.asarray(window, device=array_api_compat.device(x), copy=True). This can incur a loss of performance.- hop:
int, optional The increment in sample location between two consecutives slices.
None(default) corresponds tolen(window) // 2.
- fft_size:
int, optional See
stft()for more details.
- N:
- Returns:
LazyLinOpiLfor the inverse STFT- Examples:
>>> import lazylinop as lz >>> import numpy as np >>> N = 1024 >>> x = np.random.randn(N) >>> L = lz.signal.stft(N) >>> y = L @ x >>> iL = lz.signal.istft(N) >>> np.allclose(x, iL @ y) True
- lazylinop.signal.nufft(N, samples=None, frequencies=None, backend='lazylinop', eps=1e-06)#
Returns a
LazyLinOpLfor the Non-Uniform Discrete Fourier Transform (NUFFT).Shape of
Lis \((N,~N)\).In general,
L.His not the inverse NUFFT (as the generic NUFFT is not orthonormal).L = nufft(N, None, None)(default) is functionally equivalent to the Fast-Fourier-Transform operator and returnsF = fft(N).The nonuniform Discrete Fourier Transform is defined (for possibly nonuniform samples \(s_n\) and possibly nonuniform frequencies \(\omega_k\)) as:
\[\begin{equation} y_k\equiv\left(\omega_k\right)~\text{where}~y\left(\omega\right)=\frac{1}{\sqrt{N}}\sum_{n=0}^{N-1}x_ne^{-2\pi is_n\omega}\text{,}~\forall\omega\in\left[0,~N\right] \end{equation}\]- Args:
- N:
int Size of the input (\(N > 0\)).
- samples: 1d array
Samples \(s_n\in\left[0,~1\right[\) must be real. The size of
samplesmust be \(N\). Default value isNone(uniform samples \(s_n=\frac{n}{N}\) with \(0\le n\lt N\)).- frequencies: 1d array
Frequencies \(\omega_k\in\left[0,~N\right[\) must be real. The size of
frequenciesmust be \(N\). Default value isNone(uniform frequencies \(\omega_k=k\) with \(0\le k\lt N\)).- backend:
str, optional Three different implementations are available:
'lazylinop'is the default value. The underlying implementation is based on [1] andLazyLinOp’s pre-built operators.'finuftt'uses encapsulation of fiNUFFT Python package.'direct'uses direct computation of NUFFT. In the case of small \(N\) there is no need for a fast algorithm.epsargument has no effect forbackend='direct'.
- eps:
float, optional Precision to achieve (see [1] and fiNUFFT Python package for more details). Default value is
1e-6. It has not effect forbackend='direct'.
- N:
- Returns:
LazyLinOpNUFFT- Examples:
>>> from lazylinop.signal import nufft >>> import numpy as np >>> N = 32 >>> x = np.random.randn(N) >>> samples = np.random.rand(N) >>> k = 4 >>> F = nufft(N, samples, None) >>> y = F @ x >>> # Naive implementation of NUFFT-II. >>> z = np.sum(x * np.exp(-2j * np.pi * k * samples)) / np.sqrt(N) >>> np.allclose(y[k], z) True
References:
[1] A Nonuniform Fast Fourier Transform Based on Low Rank Approximation. Diego Ruiz-Antolin and Alex Townsend SIAM Journal on Scientific Computing 2018 40:1, A529-A547
Utility functions#
- lazylinop.signal.chunk(N, size, hop=1, start=0, stop=None)#
Returns a
LazyLinOpthat extracts chunks from a signal of size N.This operator returns the concatenation of regularly spaced chunks of
sizeelements from the provided signal. The spacing between consecutive chunks is ofhopelements of the original signal. Ifstartand/orstopis provided, the first chunk starts at thestartelement of the signal (and subsequents chunks are also shifted) and stops at thestopelement.- Args:
- size:
int Size of each chunk.
- hop:
int, optional Number of elements in the original signal between two chunks. Default is 1.
- start:
int, optional Index of the element in the original signal where chunking starts. Default is 0.
- stop:
int, optional Index of the element (excluded) in the original signal where chunking stops. Default is None, meaning that chunkins stops at the end of the signal.
- size:
- Returns:
The chunk
LazyLinOpof shape \((M,N)\), where \(M=size imes Nchunks\) with \(Nchunks\) the number of chunks.
Chunk description#
- Examples:
>>> import numpy as np >>> import lazylinop as lz >>> N = 10 >>> x = np.arange(N) >>> x array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) >>> # extract chunks of 5 elements every 2 element >>> lz.signal.chunk(N, 5, 2) @ x array([0, 1, 2, 3, 4, 2, 3, 4, 5, 6, 4, 5, 6, 7, 8])
- lazylinop.signal.decimate(N, step, start=0, stop=None)#
Returns a
LazyLinOpthat decimates/subsamples a signal x of size N.This operator performs decimation on provided signal, where the result contains elements selected every
stepelements in the original signal, starting atstartup tostop, if provided.When
L = decimate(N, step, start, stop), computingy = L@xis equivalent toy = x[start:stop:step].- Args:
- N:
int Length of the input vector.
- step:
int The stride to use for selecting elements in the original signal.
- start:
int, optional Index of the element in the original signal where decimation starts. Default is 0.
- stop:
int, optional Index of the element (excluded) in the original signal where decimation stops. Default is None, meaning that chunkins stops at the end of the signal.
- N:
- Returns:
The decimate
LazyLinOp.- Examples:
>>> import numpy as np >>> import lazylinop as lz >>> N = 10 >>> x = np.arange(N) >>> x array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) >>> # decimate every 2 element, starting at the third >>> lz.signal.decimate(N, 2, 3) @ x array([3, 5, 7, 9])
- lazylinop.signal.downsample(N, step, start=0, stop=None)#
Returns a
LazyLinOpthat downsamples a signal x of size N.This operator performs decimation on provided signal, where the result contains elements selected every
stepelements in the original signal, starting atstartup tostop, if provided.When
L = downsample(N, step, start, stop), computingy = L @ xis equivalent toy = x[start:stop:step].- Args:
- N:
int Length of the input vector.
- step:
int The stride to use for selecting elements in the original signal.
- start:
int, optional Index of the element in the original signal where decimation starts. Default is 0.
- stop:
int, optional Index of the element (excluded) in the original signal where decimation stops. Default is None, meaning that chunkins stops at the end of the signal.
- N:
- Returns:
The downsample
LazyLinOp.- Examples:
>>> import numpy as np >>> import lazylinop as lz >>> N = 10 >>> x = np.arange(N) >>> x array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) >>> # Downsample every 2 element, starting at the third >>> lz.signal.downsample(N, 2, 3) @ x array([3, 5, 7, 9])
- lazylinop.signal.overlap_add(N, size, overlap=1)#
Returns a
LazyLinOpLfor overlap-add linear operator.Such an operator L applies to an input signal x with
Nelements, whereNmust be a multiple ofsize, and is considered as the concatenation of \(N/size\) chunks ofsizeelements.The output
y = L@xresults from the addition of these chunks with overlap: The firstoverlapelements of a block \(i+1\) with lastoverlapelements of block \(i\). Blocks size is given bysize.Shape of L is \((M, N)\) with \(M = N - overlap imes ( N/size - 1)\)
- Args:
- N:
int Size of the input.
- size:
int Block size. Must divide N.
- overlap:
int Size of the overlap between blocks. Must be strictly positive, and smaller than the block size
size.
- N:
- Returns:
Overlap-add description#
- Examples:
>>> from lazylinop.signal import overlap_add >>> import numpy as np >>> x = np.full(5, 1.0) >>> # Do nothing because input size is equal to window >>> L = overlap_add(5, 5, overlap=1) >>> np.allclose(L @ x, x) True >>> x = np.full(6, 1.0) >>> L = overlap_add(6, 2, overlap=1) >>> L @ x array([1., 2., 2., 1.])
- lazylinop.signal.dwt_coeffs_sizes(N, wavelet='haar', level=None, mode='zero')#
Return a
listofintthat gives the size of the coefficients[cAn, cDn, ..., cD2, cD1].- Args:
- N, wavelet, level, mode:
See
dwt()for more details.
- Returns:
listofint.- Examples:
>>> from lazylinop.signal.dwt import dwt_coeffs_sizes >>> import pywt >>> W = pywt.Wavelet('haar').dec_len >>> dwt_coeffs_sizes(5, 'haar', level=2) [2, 2, 3]
- lazylinop.signal.dwt_to_pywt_coeffs(x, N, wavelet='haar', level=None, mode='zero')#
Returns Pywavelets compatible
[cAn, cDn, ..., cD2, cD1]computed fromxwherenis the decomposition level.- Args:
- x:
np.ndarray List of coefficients
[cAn, cDn, ..., cD2, cD1].- N, wavelet, level, mode:
See
dwt()for more details.
- x:
- Returns:
Pywavelets compatible
list[cAn, cDn, ..., cD2, cD1].- Examples:
>>> import numpy as np >>> from lazylinop.signal import dwt, dwt_to_pywt_coeffs >>> import pywt >>> N = 11 >>> x = np.arange(N) >>> L = dwt(N, wavelet='haar', level=2, mode='zero') >>> y = dwt_to_pywt_coeffs(L @ x, N, 'haar', level=2, mode='zero') >>> z = pywt.wavedec(x, wavelet='haar', level=2, mode='zero') >>> np.allclose(y[0], z[0]) True >>> np.allclose(y[1], z[1]) True
version 1.20.1 documentation