From 4edcc2ccc5dcbd7ab244a0c28736b6295e5fa9d7 Mon Sep 17 00:00:00 2001 From: phandangthoai <82931854+phandangthoai@users.noreply.github.com> Date: Mon, 26 May 2025 12:42:58 +0200 Subject: [PATCH 01/20] Enhance _cwt.py by introducing a configurable hop size parameter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generating a scalogram from the full output of the Continuous Wavelet Transform (CWT) entails high computational cost while providing limited performance gains in acoustic recognition models based on deep learning. Therefore, this update proposes reducing the output size during the intermediate processing stage—rather than after CWT generation—to improve computational efficiency of CWT. This pull request reflects the research findings presented in the following paper. Phan, D. T., Huynh, T. A., Pham, V. T., Tran, C. M., Mai, V. T., & Tran, N. Q. (2025). Optimal Scalogram for Computational Complexity Reduction in Acoustic Recognition Using Deep Learning. arXiv preprint arXiv:2505.13017. --- pywt/_cwt.py | 56 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/pywt/_cwt.py b/pywt/_cwt.py index 0d69095b..569bea26 100644 --- a/pywt/_cwt.py +++ b/pywt/_cwt.py @@ -14,17 +14,27 @@ import numpy as np - -def next_fast_len(n): - """Round up size to the nearest power of two. - - Given a number of samples `n`, returns the next power of two - following this number to take advantage of FFT speedup. - """ - return 2**ceil(np.log2(n)) - - -def cwt(data, scales, wavelet, sampling_period=1., method='conv', axis=-1): +try: + import scipy + fftmodule = scipy.fft + next_fast_len = fftmodule.next_fast_len +except ImportError: + fftmodule = np.fft + + # provide a fallback so scipy is an optional requirement + # note: numpy.fft in numpy 2.0 is as fast as scipy.fft, so could be used + # unconditionally once the minimum supported numpy version is >=2.0 + def next_fast_len(n): + """Round up size to the nearest power of two. + + Given a number of samples `n`, returns the next power of two + following this number to take advantage of FFT speedup. + This fallback is less efficient than `scipy.fftpack.next_fast_len` + """ + return 2**ceil(np.log2(n)) + + +def cwt(data, scales, wavelet, hop_size=1, sampling_period=1., method='conv', axis=-1): """ cwt(data, scales, wavelet) @@ -114,7 +124,12 @@ def cwt(data, scales, wavelet, sampling_period=1., method='conv', axis=-1): raise AxisError("axis must be a scalar.") dt_out = dt_cplx if wavelet.complex_cwt else dt - out = np.empty((np.size(scales),) + data.shape, dtype=dt_out) + + # out length of transform when applying down sampling + downsampled_length = len(data) // hop_size + data_sampled = np.empty((1, downsampled_length)) + out = np.empty((np.size(scales), downsampled_length), dtype=dt_out) + precision = 10 int_psi, x = integrate_wavelet(wavelet, precision=precision) int_psi = np.conj(int_psi) if wavelet.complex_cwt else int_psi @@ -167,17 +182,22 @@ def cwt(data, scales, wavelet, sampling_period=1., method='conv', axis=-1): ) if size_scale != size_scale0: # Must recompute fft_data when the padding size changes. - fft_data = np.fft.fft(data, size_scale, axis=-1) + fft_data = fftmodule.fft(data, size_scale, axis=-1) size_scale0 = size_scale - fft_wav = np.fft.fft(int_psi_scale, size_scale, axis=-1) - conv = np.fft.ifft(fft_wav * fft_data, axis=-1) + fft_wav = fftmodule.fft(int_psi_scale, size_scale, axis=-1) + conv = fftmodule.ifft(fft_wav * fft_data, axis=-1) conv = conv[..., :data.shape[-1] + int_psi_scale.size - 1] - coef = - np.sqrt(scale) * np.diff(conv, axis=-1) + coef_temp = - np.sqrt(scale) * np.diff(conv, axis=-1) + + # Apply time downsampling + coef = coef_temp[::hop_size] # Selecting every `hop_size`-th sample + if out.dtype.kind != 'c': coef = coef.real + # transform axis is always -1 due to the data reshape above - d = (coef.shape[-1] - data.shape[-1]) / 2. + d = (coef.shape[-1] - data_sampled.shape[-1]) / 2. if d > 0: coef = coef[..., floor(d):-ceil(d)] elif d < 0: @@ -185,7 +205,7 @@ def cwt(data, scales, wavelet, sampling_period=1., method='conv', axis=-1): f"Selected scale of {scale} too small.") if data.ndim > 1: # restore original data shape and axis position - coef = coef.reshape(data_shape_pre) + coef = coef.reshape(data_sampled) coef = coef.swapaxes(axis, -1) out[i, ...] = coef From 9a8bf1a249aa3b6fe0ef1040c3d38e7af3575b44 Mon Sep 17 00:00:00 2001 From: phandangthoai <82931854+phandangthoai@users.noreply.github.com> Date: Sun, 1 Jun 2025 12:39:11 +0200 Subject: [PATCH 02/20] Update document for hop_size in _cwt.py --- pywt/_cwt.py | 42 ++++++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/pywt/_cwt.py b/pywt/_cwt.py index 569bea26..6b345192 100644 --- a/pywt/_cwt.py +++ b/pywt/_cwt.py @@ -14,29 +14,19 @@ import numpy as np -try: - import scipy - fftmodule = scipy.fft - next_fast_len = fftmodule.next_fast_len -except ImportError: - fftmodule = np.fft - - # provide a fallback so scipy is an optional requirement - # note: numpy.fft in numpy 2.0 is as fast as scipy.fft, so could be used - # unconditionally once the minimum supported numpy version is >=2.0 - def next_fast_len(n): - """Round up size to the nearest power of two. - - Given a number of samples `n`, returns the next power of two - following this number to take advantage of FFT speedup. - This fallback is less efficient than `scipy.fftpack.next_fast_len` - """ - return 2**ceil(np.log2(n)) + +def next_fast_len(n): + """Round up size to the nearest power of two. + + Given a number of samples `n`, returns the next power of two + following this number to take advantage of FFT speedup. + """ + return 2**ceil(np.log2(n)) def cwt(data, scales, wavelet, hop_size=1, sampling_period=1., method='conv', axis=-1): """ - cwt(data, scales, wavelet) + cwt(data, scales, wavelet, hop_size) One dimensional Continuous Wavelet Transform. @@ -51,6 +41,14 @@ def cwt(data, scales, wavelet, hop_size=1, sampling_period=1., method='conv', ax ``sampling_period`` is given in seconds. wavelet : Wavelet object or name Wavelet to use + hop_size : int + Specifies the down-sampling factor applied on temporal axis during the transform. + The output is sampled every hop_size samples, rather than at every consecutive sample. + For example: + A signal of length 1024 yields 1024 output samples when hop_size=1; + 512 output samples when hop_size=2; + 256 output samples when hop_size=4. + hop_size must be a positive integer (≥1). sampling_period : float Sampling period for the frequencies output (optional). The values computed for ``coefs`` are independent of the choice of @@ -83,7 +81,7 @@ def cwt(data, scales, wavelet, hop_size=1, sampling_period=1., method='conv', ax Notes ----- - Size of coefficients arrays depends on the length of the input array and + Size of coefficients arrays depends on the length of the input array, the given hop_size and the length of given scales. Examples @@ -93,7 +91,7 @@ def cwt(data, scales, wavelet, hop_size=1, sampling_period=1., method='conv', ax >>> import matplotlib.pyplot as plt >>> x = np.arange(512) >>> y = np.sin(2*np.pi*x/32) - >>> coef, freqs=pywt.cwt(y,np.arange(1,129),'gaus1') + >>> coef, freqs=pywt.cwt(y,np.arange(1,129),1, 'gaus1') >>> plt.matshow(coef) >>> plt.show() @@ -103,7 +101,7 @@ def cwt(data, scales, wavelet, hop_size=1, sampling_period=1., method='conv', ax >>> t = np.linspace(-1, 1, 200, endpoint=False) >>> sig = np.cos(2 * np.pi * 7 * t) + np.real(np.exp(-7*(t-0.4)**2)*np.exp(1j*2*np.pi*2*(t-0.4))) >>> widths = np.arange(1, 31) - >>> cwtmatr, freqs = pywt.cwt(sig, widths, 'mexh') + >>> cwtmatr, freqs = pywt.cwt(sig, widths,2, 'mexh') >>> plt.imshow(cwtmatr, extent=[-1, 1, 1, 31], cmap='PRGn', aspect='auto', ... vmax=abs(cwtmatr).max(), vmin=-abs(cwtmatr).max()) >>> plt.show() From ad4152165b1d43620480729b426f69c5327d1823 Mon Sep 17 00:00:00 2001 From: phandangthoai <82931854+phandangthoai@users.noreply.github.com> Date: Sun, 1 Jun 2025 14:56:36 +0200 Subject: [PATCH 03/20] Update _cwt.py to fix downsampled_length error --- pywt/_cwt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pywt/_cwt.py b/pywt/_cwt.py index 6b345192..17eb412d 100644 --- a/pywt/_cwt.py +++ b/pywt/_cwt.py @@ -124,7 +124,7 @@ def cwt(data, scales, wavelet, hop_size=1, sampling_period=1., method='conv', ax dt_out = dt_cplx if wavelet.complex_cwt else dt # out length of transform when applying down sampling - downsampled_length = len(data) // hop_size + downsampled_length = int(len(data) // hop_size) data_sampled = np.empty((1, downsampled_length)) out = np.empty((np.size(scales), downsampled_length), dtype=dt_out) From 6b7d7f1bfe3f04fc8548823fb1bd91e1591883a5 Mon Sep 17 00:00:00 2001 From: phandangthoai <82931854+phandangthoai@users.noreply.github.com> Date: Sun, 1 Jun 2025 15:16:48 +0200 Subject: [PATCH 04/20] Update _cwt.py with hop_size as integer --- pywt/_cwt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pywt/_cwt.py b/pywt/_cwt.py index 17eb412d..ce58c2e1 100644 --- a/pywt/_cwt.py +++ b/pywt/_cwt.py @@ -189,7 +189,7 @@ def cwt(data, scales, wavelet, hop_size=1, sampling_period=1., method='conv', ax coef_temp = - np.sqrt(scale) * np.diff(conv, axis=-1) # Apply time downsampling - coef = coef_temp[::hop_size] # Selecting every `hop_size`-th sample + coef = coef_temp[::int(hop_size)] # Selecting every `hop_size`-th sample if out.dtype.kind != 'c': coef = coef.real From 3c9289b01c656c74739bd6c2ae0947ccb3012d62 Mon Sep 17 00:00:00 2001 From: phandangthoai <82931854+phandangthoai@users.noreply.github.com> Date: Thu, 5 Jun 2025 10:01:43 +0200 Subject: [PATCH 05/20] Fix fft lib for _cwt --- pywt/_cwt.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pywt/_cwt.py b/pywt/_cwt.py index ce58c2e1..1b13e978 100644 --- a/pywt/_cwt.py +++ b/pywt/_cwt.py @@ -180,10 +180,10 @@ def cwt(data, scales, wavelet, hop_size=1, sampling_period=1., method='conv', ax ) if size_scale != size_scale0: # Must recompute fft_data when the padding size changes. - fft_data = fftmodule.fft(data, size_scale, axis=-1) + fft_data = np.fft.fft(data, size_scale, axis=-1) size_scale0 = size_scale - fft_wav = fftmodule.fft(int_psi_scale, size_scale, axis=-1) - conv = fftmodule.ifft(fft_wav * fft_data, axis=-1) + fft_wav = np.fft.fft(int_psi_scale, size_scale, axis=-1) + conv = np.fft.ifft(fft_wav * fft_data, axis=-1) conv = conv[..., :data.shape[-1] + int_psi_scale.size - 1] coef_temp = - np.sqrt(scale) * np.diff(conv, axis=-1) From 0a53e697bc1efdabc83c99000af00d1e516e9cb3 Mon Sep 17 00:00:00 2001 From: phandangthoai <82931854+phandangthoai@users.noreply.github.com> Date: Thu, 5 Jun 2025 10:14:29 +0200 Subject: [PATCH 06/20] Validation for hop_size in _cwt.py --- pywt/_cwt.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pywt/_cwt.py b/pywt/_cwt.py index 1b13e978..ce16cc2d 100644 --- a/pywt/_cwt.py +++ b/pywt/_cwt.py @@ -120,6 +120,9 @@ def cwt(data, scales, wavelet, hop_size=1, sampling_period=1., method='conv', ax if not np.isscalar(axis): raise AxisError("axis must be a scalar.") + # Ensure hop_size is a positive integer + if not isinstance(hop_size, int) or hop_size <= 0: + raise ValueError(f"Invalid hop_size: {hop_size}. It must be a positive integer.") dt_out = dt_cplx if wavelet.complex_cwt else dt From 6776f9883e9a686ab5405c5e6085e374ae60fb4b Mon Sep 17 00:00:00 2001 From: phandangthoai <82931854+phandangthoai@users.noreply.github.com> Date: Thu, 5 Jun 2025 10:38:48 +0200 Subject: [PATCH 07/20] Add hop_size to test cases of _cwt --- pywt/tests/test_cwt_wavelets.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pywt/tests/test_cwt_wavelets.py b/pywt/tests/test_cwt_wavelets.py index f142404c..9067ecc7 100644 --- a/pywt/tests/test_cwt_wavelets.py +++ b/pywt/tests/test_cwt_wavelets.py @@ -379,9 +379,10 @@ def test_cwt_complex(dtype, tol, method): dt = time[1] - time[0] wavelet = 'cmor1.5-1.0' scales = np.arange(1, 32) + hop_size = 1 # real-valued tranfsorm as a reference - [cfs, f] = pywt.cwt(sst, scales, wavelet, dt, method=method) + [cfs, f] = pywt.cwt(sst, scales, wavelet, hop_size, dt, method=method) # verify same precision assert_equal(cfs.real.dtype, sst.dtype) @@ -389,7 +390,7 @@ def test_cwt_complex(dtype, tol, method): # complex-valued transform equals sum of the transforms of the real # and imaginary components sst_complex = sst + 1j*sst - [cfs_complex, f] = pywt.cwt(sst_complex, scales, wavelet, dt, + [cfs_complex, f] = pywt.cwt(sst_complex, scales, wavelet, hop_size, dt, method=method) assert_allclose(cfs + 1j*cfs, cfs_complex, atol=tol, rtol=tol) # verify dtype is preserved @@ -409,10 +410,10 @@ def test_cwt_batch(axis, method): scales = np.arange(1, 32) # non-batch transform as reference - [cfs1, f] = pywt.cwt(sst1, scales, wavelet, dt, method=method, axis=axis) + [cfs1, f] = pywt.cwt(sst1, scales, wavelet, hop_size, dt, method=method, axis=axis) shape_in = sst.shape - [cfs, f] = pywt.cwt(sst, scales, wavelet, dt, method=method, axis=axis) + [cfs, f] = pywt.cwt(sst, scales, wavelet, hop_size, dt, method=method, axis=axis) # shape of input is not modified assert_equal(shape_in, sst.shape) From 82a4ef01826ee1839648a35adba64b90aa1eebb1 Mon Sep 17 00:00:00 2001 From: phandangthoai <82931854+phandangthoai@users.noreply.github.com> Date: Thu, 5 Jun 2025 10:48:04 +0200 Subject: [PATCH 08/20] Update test_cwt_wavelets.py --- pywt/tests/test_cwt_wavelets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pywt/tests/test_cwt_wavelets.py b/pywt/tests/test_cwt_wavelets.py index 9067ecc7..5e959e38 100644 --- a/pywt/tests/test_cwt_wavelets.py +++ b/pywt/tests/test_cwt_wavelets.py @@ -408,6 +408,7 @@ def test_cwt_batch(axis, method): dt = time[1] - time[0] wavelet = 'cmor1.5-1.0' scales = np.arange(1, 32) + hop_size = 1 # non-batch transform as reference [cfs1, f] = pywt.cwt(sst1, scales, wavelet, hop_size, dt, method=method, axis=axis) From 81cd1bb55eb633c1bfbb1638d83c828454d23103 Mon Sep 17 00:00:00 2001 From: phandangthoai <82931854+phandangthoai@users.noreply.github.com> Date: Thu, 5 Jun 2025 14:59:50 +0200 Subject: [PATCH 09/20] Fix shape of output if data in n-dim Update _cwt.py --- pywt/_cwt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pywt/_cwt.py b/pywt/_cwt.py index ce16cc2d..5ee407e6 100644 --- a/pywt/_cwt.py +++ b/pywt/_cwt.py @@ -128,7 +128,7 @@ def cwt(data, scales, wavelet, hop_size=1, sampling_period=1., method='conv', ax # out length of transform when applying down sampling downsampled_length = int(len(data) // hop_size) - data_sampled = np.empty((1, downsampled_length)) + data_sampled = np.empty((data.shape[0], downsampled_length)) out = np.empty((np.size(scales), downsampled_length), dtype=dt_out) precision = 10 From dc93c83bbb4e98186f7b50a203acd263a7680e11 Mon Sep 17 00:00:00 2001 From: phandangthoai <82931854+phandangthoai@users.noreply.github.com> Date: Fri, 6 Jun 2025 11:12:04 +0200 Subject: [PATCH 10/20] Update _cwt.py with reshape the coef --- pywt/_cwt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pywt/_cwt.py b/pywt/_cwt.py index 5ee407e6..8afaf002 100644 --- a/pywt/_cwt.py +++ b/pywt/_cwt.py @@ -206,7 +206,7 @@ def cwt(data, scales, wavelet, hop_size=1, sampling_period=1., method='conv', ax f"Selected scale of {scale} too small.") if data.ndim > 1: # restore original data shape and axis position - coef = coef.reshape(data_sampled) + coef = coef.reshape(data_sampled.shape) coef = coef.swapaxes(axis, -1) out[i, ...] = coef From a8a9aee9c425c3c042230e03b48d8f69036c6ed7 Mon Sep 17 00:00:00 2001 From: phandangthoai <82931854+phandangthoai@users.noreply.github.com> Date: Fri, 6 Jun 2025 12:22:58 +0200 Subject: [PATCH 11/20] Update _cwt.py to fix coef shape --- pywt/_cwt.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pywt/_cwt.py b/pywt/_cwt.py index 8afaf002..6dfd2e36 100644 --- a/pywt/_cwt.py +++ b/pywt/_cwt.py @@ -149,9 +149,11 @@ def cwt(data, scales, wavelet, hop_size=1, sampling_period=1., method='conv', ax if data.ndim > 1: # move axis to be transformed last (so it is contiguous) data = data.swapaxes(-1, axis) + data_sampled = data_sampled.swapaxes(-1, axis) # reshape to (n_batch, data.shape[-1]) data_shape_pre = data.shape + data_sampled_shape_pre = data_sampled.shape data = data.reshape((-1, data.shape[-1])) for i, scale in enumerate(scales): From f607aa03bdcfab2a71f938a1d42c0b9aa34055cd Mon Sep 17 00:00:00 2001 From: phandangthoai <82931854+phandangthoai@users.noreply.github.com> Date: Fri, 6 Jun 2025 12:32:43 +0200 Subject: [PATCH 12/20] Update _cwt.py for fixing coef size --- pywt/_cwt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pywt/_cwt.py b/pywt/_cwt.py index 6dfd2e36..89dc28a9 100644 --- a/pywt/_cwt.py +++ b/pywt/_cwt.py @@ -208,7 +208,7 @@ def cwt(data, scales, wavelet, hop_size=1, sampling_period=1., method='conv', ax f"Selected scale of {scale} too small.") if data.ndim > 1: # restore original data shape and axis position - coef = coef.reshape(data_sampled.shape) + coef = coef.reshape(data_sampled_shape_pre.shape) coef = coef.swapaxes(axis, -1) out[i, ...] = coef From 5cda18e0767061914a29a1fd8b356c66292c5db6 Mon Sep 17 00:00:00 2001 From: phandangthoai <82931854+phandangthoai@users.noreply.github.com> Date: Fri, 6 Jun 2025 12:51:42 +0200 Subject: [PATCH 13/20] Update _cwt.py --- pywt/_cwt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pywt/_cwt.py b/pywt/_cwt.py index 89dc28a9..3d28a58e 100644 --- a/pywt/_cwt.py +++ b/pywt/_cwt.py @@ -208,7 +208,7 @@ def cwt(data, scales, wavelet, hop_size=1, sampling_period=1., method='conv', ax f"Selected scale of {scale} too small.") if data.ndim > 1: # restore original data shape and axis position - coef = coef.reshape(data_sampled_shape_pre.shape) + coef = coef.reshape(data_sampled_shape_pre) coef = coef.swapaxes(axis, -1) out[i, ...] = coef From 6f4e57a7cf8d162a184bb36d6677839d6b446e48 Mon Sep 17 00:00:00 2001 From: phandangthoai <82931854+phandangthoai@users.noreply.github.com> Date: Fri, 6 Jun 2025 15:39:31 +0200 Subject: [PATCH 14/20] Update _cwt.py --- pywt/_cwt.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/pywt/_cwt.py b/pywt/_cwt.py index 3d28a58e..ff057df2 100644 --- a/pywt/_cwt.py +++ b/pywt/_cwt.py @@ -127,9 +127,11 @@ def cwt(data, scales, wavelet, hop_size=1, sampling_period=1., method='conv', ax dt_out = dt_cplx if wavelet.complex_cwt else dt # out length of transform when applying down sampling - downsampled_length = int(len(data) // hop_size) - data_sampled = np.empty((data.shape[0], downsampled_length)) - out = np.empty((np.size(scales), downsampled_length), dtype=dt_out) + # hop_size is only applied for 1D data + if data.ndim == 1: + downsampled_length = int(len(data) // hop_size) + data_sampled = np.empty((data.shape[0], downsampled_length)) + out = np.empty((np.size(scales), downsampled_length), dtype=dt_out) precision = 10 int_psi, x = integrate_wavelet(wavelet, precision=precision) @@ -149,12 +151,12 @@ def cwt(data, scales, wavelet, hop_size=1, sampling_period=1., method='conv', ax if data.ndim > 1: # move axis to be transformed last (so it is contiguous) data = data.swapaxes(-1, axis) - data_sampled = data_sampled.swapaxes(-1, axis) # reshape to (n_batch, data.shape[-1]) data_shape_pre = data.shape - data_sampled_shape_pre = data_sampled.shape data = data.reshape((-1, data.shape[-1])) + if data.ndim == 1: + data_sampled_shape_pre = data_sampled.shape for i, scale in enumerate(scales): step = x[1] - x[0] @@ -194,8 +196,10 @@ def cwt(data, scales, wavelet, hop_size=1, sampling_period=1., method='conv', ax coef_temp = - np.sqrt(scale) * np.diff(conv, axis=-1) # Apply time downsampling - coef = coef_temp[::int(hop_size)] # Selecting every `hop_size`-th sample - + if data.ndim == 1: + coef = coef_temp[::int(hop_size)] # Selecting every `hop_size`-th sample + if data.ndim > 1: + coef = coef_temp if out.dtype.kind != 'c': coef = coef.real @@ -208,8 +212,12 @@ def cwt(data, scales, wavelet, hop_size=1, sampling_period=1., method='conv', ax f"Selected scale of {scale} too small.") if data.ndim > 1: # restore original data shape and axis position - coef = coef.reshape(data_sampled_shape_pre) + coef = coef.reshape(data_shape_pre) coef = coef.swapaxes(axis, -1) + # if data.ndim == 1: + # # restore original data shape and axis position + # coef = coef.reshape(data_sampled_shape_pre) + # coef = coef.swapaxes(axis, -1) out[i, ...] = coef frequencies = scale2frequency(wavelet, scales, precision) From 099f895b88cf17952e005198e65eb9b3ccc46f49 Mon Sep 17 00:00:00 2001 From: phandangthoai <82931854+phandangthoai@users.noreply.github.com> Date: Mon, 16 Jun 2025 15:46:37 +0200 Subject: [PATCH 15/20] Update _cwt.py with correct sampling --- pywt/_cwt.py | 36 +++++++----------------------------- 1 file changed, 7 insertions(+), 29 deletions(-) diff --git a/pywt/_cwt.py b/pywt/_cwt.py index ff057df2..692740f1 100644 --- a/pywt/_cwt.py +++ b/pywt/_cwt.py @@ -91,7 +91,7 @@ def cwt(data, scales, wavelet, hop_size=1, sampling_period=1., method='conv', ax >>> import matplotlib.pyplot as plt >>> x = np.arange(512) >>> y = np.sin(2*np.pi*x/32) - >>> coef, freqs=pywt.cwt(y,np.arange(1,129),1, 'gaus1') + >>> coef, freqs=pywt.cwt(y,np.arange(1,129),1,'gaus1') >>> plt.matshow(coef) >>> plt.show() @@ -101,7 +101,7 @@ def cwt(data, scales, wavelet, hop_size=1, sampling_period=1., method='conv', ax >>> t = np.linspace(-1, 1, 200, endpoint=False) >>> sig = np.cos(2 * np.pi * 7 * t) + np.real(np.exp(-7*(t-0.4)**2)*np.exp(1j*2*np.pi*2*(t-0.4))) >>> widths = np.arange(1, 31) - >>> cwtmatr, freqs = pywt.cwt(sig, widths,2, 'mexh') + >>> cwtmatr, freqs = pywt.cwt(sig, widths, 2, 'mexh') >>> plt.imshow(cwtmatr, extent=[-1, 1, 1, 31], cmap='PRGn', aspect='auto', ... vmax=abs(cwtmatr).max(), vmin=-abs(cwtmatr).max()) >>> plt.show() @@ -120,19 +120,10 @@ def cwt(data, scales, wavelet, hop_size=1, sampling_period=1., method='conv', ax if not np.isscalar(axis): raise AxisError("axis must be a scalar.") - # Ensure hop_size is a positive integer - if not isinstance(hop_size, int) or hop_size <= 0: - raise ValueError(f"Invalid hop_size: {hop_size}. It must be a positive integer.") dt_out = dt_cplx if wavelet.complex_cwt else dt - - # out length of transform when applying down sampling - # hop_size is only applied for 1D data - if data.ndim == 1: - downsampled_length = int(len(data) // hop_size) - data_sampled = np.empty((data.shape[0], downsampled_length)) - out = np.empty((np.size(scales), downsampled_length), dtype=dt_out) - + ata_sampled = data[..., ::hop_size] + out = np.empty((np.size(scales),) + data_sampled.shape, dtype=dt_out) precision = 10 int_psi, x = integrate_wavelet(wavelet, precision=precision) int_psi = np.conj(int_psi) if wavelet.complex_cwt else int_psi @@ -155,8 +146,6 @@ def cwt(data, scales, wavelet, hop_size=1, sampling_period=1., method='conv', ax # reshape to (n_batch, data.shape[-1]) data_shape_pre = data.shape data = data.reshape((-1, data.shape[-1])) - if data.ndim == 1: - data_sampled_shape_pre = data_sampled.shape for i, scale in enumerate(scales): step = x[1] - x[0] @@ -193,18 +182,11 @@ def cwt(data, scales, wavelet, hop_size=1, sampling_period=1., method='conv', ax conv = np.fft.ifft(fft_wav * fft_data, axis=-1) conv = conv[..., :data.shape[-1] + int_psi_scale.size - 1] - coef_temp = - np.sqrt(scale) * np.diff(conv, axis=-1) - - # Apply time downsampling - if data.ndim == 1: - coef = coef_temp[::int(hop_size)] # Selecting every `hop_size`-th sample - if data.ndim > 1: - coef = coef_temp + coef = - np.sqrt(scale) * np.diff(conv, axis=-1) if out.dtype.kind != 'c': coef = coef.real - # transform axis is always -1 due to the data reshape above - d = (coef.shape[-1] - data_sampled.shape[-1]) / 2. + d = (coef.shape[-1] - data.shape[-1]) / 2. if d > 0: coef = coef[..., floor(d):-ceil(d)] elif d < 0: @@ -214,11 +196,7 @@ def cwt(data, scales, wavelet, hop_size=1, sampling_period=1., method='conv', ax # restore original data shape and axis position coef = coef.reshape(data_shape_pre) coef = coef.swapaxes(axis, -1) - # if data.ndim == 1: - # # restore original data shape and axis position - # coef = coef.reshape(data_sampled_shape_pre) - # coef = coef.swapaxes(axis, -1) - out[i, ...] = coef + out[i, ...] = coef[..., ::hop_size] frequencies = scale2frequency(wavelet, scales, precision) if np.isscalar(frequencies): From 7282d9f47976ca880ed8c7be79c0d308fa6a6608 Mon Sep 17 00:00:00 2001 From: phandangthoai <82931854+phandangthoai@users.noreply.github.com> Date: Mon, 16 Jun 2025 15:54:52 +0200 Subject: [PATCH 16/20] Update _cwt.py --- pywt/_cwt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pywt/_cwt.py b/pywt/_cwt.py index 692740f1..a38627f5 100644 --- a/pywt/_cwt.py +++ b/pywt/_cwt.py @@ -122,7 +122,7 @@ def cwt(data, scales, wavelet, hop_size=1, sampling_period=1., method='conv', ax raise AxisError("axis must be a scalar.") dt_out = dt_cplx if wavelet.complex_cwt else dt - ata_sampled = data[..., ::hop_size] + data_sampled = data[..., ::hop_size] out = np.empty((np.size(scales),) + data_sampled.shape, dtype=dt_out) precision = 10 int_psi, x = integrate_wavelet(wavelet, precision=precision) From 20838a775186095cff23ae35114a4aaacfe517c3 Mon Sep 17 00:00:00 2001 From: Dang Thoai Phan <82931854+phandangthoai@users.noreply.github.com> Date: Sat, 19 Jul 2025 10:05:30 +0200 Subject: [PATCH 17/20] Update _cwt.py Move hop_size to the end --- pywt/_cwt.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/pywt/_cwt.py b/pywt/_cwt.py index a38627f5..2fd7d779 100644 --- a/pywt/_cwt.py +++ b/pywt/_cwt.py @@ -24,9 +24,8 @@ def next_fast_len(n): return 2**ceil(np.log2(n)) -def cwt(data, scales, wavelet, hop_size=1, sampling_period=1., method='conv', axis=-1): +def cwt(data, scales, wavelet, sampling_period=1., method='conv', axis=-1, *, hop_size=1): """ - cwt(data, scales, wavelet, hop_size) One dimensional Continuous Wavelet Transform. @@ -41,14 +40,6 @@ def cwt(data, scales, wavelet, hop_size=1, sampling_period=1., method='conv', ax ``sampling_period`` is given in seconds. wavelet : Wavelet object or name Wavelet to use - hop_size : int - Specifies the down-sampling factor applied on temporal axis during the transform. - The output is sampled every hop_size samples, rather than at every consecutive sample. - For example: - A signal of length 1024 yields 1024 output samples when hop_size=1; - 512 output samples when hop_size=2; - 256 output samples when hop_size=4. - hop_size must be a positive integer (≥1). sampling_period : float Sampling period for the frequencies output (optional). The values computed for ``coefs`` are independent of the choice of @@ -68,6 +59,14 @@ def cwt(data, scales, wavelet, hop_size=1, sampling_period=1., method='conv', ax axis: int, optional Axis over which to compute the CWT. If not given, the last axis is used. + hop_size : int + Specifies the down-sampling factor applied on temporal axis during the transform. + The output is sampled every hop size samples, rather than at every consecutive sample. + For example: + A signal of length 1024 yields 1024 output samples when ``hop_size=1``; + 512 output samples when ``hop_size=2``; + 256 output samples when ``hop_size=4``. + ``hop_size`` must be a positive integer (≥1). Returns ------- @@ -81,7 +80,7 @@ def cwt(data, scales, wavelet, hop_size=1, sampling_period=1., method='conv', ax Notes ----- - Size of coefficients arrays depends on the length of the input array, the given hop_size and + Size of coefficients arrays depends on the length of the input array, the given hop size and the length of given scales. Examples @@ -91,7 +90,7 @@ def cwt(data, scales, wavelet, hop_size=1, sampling_period=1., method='conv', ax >>> import matplotlib.pyplot as plt >>> x = np.arange(512) >>> y = np.sin(2*np.pi*x/32) - >>> coef, freqs=pywt.cwt(y,np.arange(1,129),1,'gaus1') + >>> coef, freqs=pywt.cwt(y,np.arange(1,129),'gaus1',hop_size=1) >>> plt.matshow(coef) >>> plt.show() @@ -101,7 +100,7 @@ def cwt(data, scales, wavelet, hop_size=1, sampling_period=1., method='conv', ax >>> t = np.linspace(-1, 1, 200, endpoint=False) >>> sig = np.cos(2 * np.pi * 7 * t) + np.real(np.exp(-7*(t-0.4)**2)*np.exp(1j*2*np.pi*2*(t-0.4))) >>> widths = np.arange(1, 31) - >>> cwtmatr, freqs = pywt.cwt(sig, widths, 2, 'mexh') + >>> cwtmatr, freqs = pywt.cwt(sig, widths, 'mexh', hop_size=2) >>> plt.imshow(cwtmatr, extent=[-1, 1, 1, 31], cmap='PRGn', aspect='auto', ... vmax=abs(cwtmatr).max(), vmin=-abs(cwtmatr).max()) >>> plt.show() From d9867336cdf14b148890a3fb6bac4f44b50f9e2d Mon Sep 17 00:00:00 2001 From: Dang Thoai Phan <82931854+phandangthoai@users.noreply.github.com> Date: Sat, 19 Jul 2025 10:08:36 +0200 Subject: [PATCH 18/20] Update test_cwt_wavelets.py Move hop_size to the end --- pywt/tests/test_cwt_wavelets.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/pywt/tests/test_cwt_wavelets.py b/pywt/tests/test_cwt_wavelets.py index 5e959e38..0e0d7f7b 100644 --- a/pywt/tests/test_cwt_wavelets.py +++ b/pywt/tests/test_cwt_wavelets.py @@ -379,10 +379,9 @@ def test_cwt_complex(dtype, tol, method): dt = time[1] - time[0] wavelet = 'cmor1.5-1.0' scales = np.arange(1, 32) - hop_size = 1 # real-valued tranfsorm as a reference - [cfs, f] = pywt.cwt(sst, scales, wavelet, hop_size, dt, method=method) + [cfs, f] = pywt.cwt(sst, scales, wavelet, dt, method=method, hop_size=1) # verify same precision assert_equal(cfs.real.dtype, sst.dtype) @@ -390,8 +389,8 @@ def test_cwt_complex(dtype, tol, method): # complex-valued transform equals sum of the transforms of the real # and imaginary components sst_complex = sst + 1j*sst - [cfs_complex, f] = pywt.cwt(sst_complex, scales, wavelet, hop_size, dt, - method=method) + [cfs_complex, f] = pywt.cwt(sst_complex, scales, wavelet, dt, + method=method, hop_size=1) assert_allclose(cfs + 1j*cfs, cfs_complex, atol=tol, rtol=tol) # verify dtype is preserved assert_equal(cfs_complex.dtype, sst_complex.dtype) @@ -408,13 +407,12 @@ def test_cwt_batch(axis, method): dt = time[1] - time[0] wavelet = 'cmor1.5-1.0' scales = np.arange(1, 32) - hop_size = 1 # non-batch transform as reference - [cfs1, f] = pywt.cwt(sst1, scales, wavelet, hop_size, dt, method=method, axis=axis) + [cfs1, f] = pywt.cwt(sst1, scales, wavelet, dt, method=method, axis=axis, hop_size=1) shape_in = sst.shape - [cfs, f] = pywt.cwt(sst, scales, wavelet, hop_size, dt, method=method, axis=axis) + [cfs, f] = pywt.cwt(sst, scales, wavelet, dt, method=method, axis=axis, hop_size=1) # shape of input is not modified assert_equal(shape_in, sst.shape) From 5eed6aac6f2bc11c68feb080601d1db276a3e5e1 Mon Sep 17 00:00:00 2001 From: Dang Thoai Phan <82931854+phandangthoai@users.noreply.github.com> Date: Sat, 19 Jul 2025 10:37:51 +0200 Subject: [PATCH 19/20] Update _cwt.py Add References to the research paper. --- pywt/_cwt.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pywt/_cwt.py b/pywt/_cwt.py index 2fd7d779..e240cb6e 100644 --- a/pywt/_cwt.py +++ b/pywt/_cwt.py @@ -82,6 +82,12 @@ def cwt(data, scales, wavelet, sampling_period=1., method='conv', axis=-1, *, ho ----- Size of coefficients arrays depends on the length of the input array, the given hop size and the length of given scales. + + References + ---------- + .. [1] Phan, D. T., Huynh, T. A., Pham, V. T., Tran, C. M., Mai, V. T., & Tran, N. Q. (2025). + Optimal Scalogram for Computational Complexity Reduction in Acoustic Recognition Using Deep Learning. + *arXiv preprint arXiv:2505.13017*. https://arxiv.org/abs/2505.13017 Examples -------- From 8189d35f90ea25c6f6168b594d89bd6d0f7d64de Mon Sep 17 00:00:00 2001 From: Dang Thoai Phan <82931854+phandangthoai@users.noreply.github.com> Date: Sat, 19 Jul 2025 10:39:50 +0200 Subject: [PATCH 20/20] Update _cwt.py Revise the notes --- pywt/_cwt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pywt/_cwt.py b/pywt/_cwt.py index e240cb6e..ed69504a 100644 --- a/pywt/_cwt.py +++ b/pywt/_cwt.py @@ -80,8 +80,8 @@ def cwt(data, scales, wavelet, sampling_period=1., method='conv', axis=-1, *, ho Notes ----- - Size of coefficients arrays depends on the length of the input array, the given hop size and - the length of given scales. + Size of coefficients arrays depends on the length of the input array, + the length of given scales and the given hop size. References ----------