From cb9bd9ea8e329fcef49b303be671bb30d9652068 Mon Sep 17 00:00:00 2001 From: Ralf Gommers Date: Thu, 13 Aug 2026 15:57:49 +0000 Subject: [PATCH 1/3] TST: cover WaveletPacketND.reconstruct for odd-shaped input Every existing WaveletPacketND test uses even-length axes, so the trim back to the original data shape was never exercised - which is how the IndexError in reconstruct() went unnoticed. --- pywt/tests/test_wpnd.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/pywt/tests/test_wpnd.py b/pywt/tests/test_wpnd.py index 4473cb09f..93abbeffd 100644 --- a/pywt/tests/test_wpnd.py +++ b/pywt/tests/test_wpnd.py @@ -168,3 +168,37 @@ def test_wavelet_packet_axes(): # must have non-duplicate axes assert_raises(ValueError, pywt.WaveletPacketND, data=x, wavelet='db1', axes=(0, 0)) + + +def test_wavelet_packet_odd_shape(): + # the reconstruction has to be trimmed back to the original shape when a + # transformed axis has an odd length + x = np.arange(2 * 2 * 3, dtype=np.float64).reshape(2, 2, 3) + wp = pywt.WaveletPacketND(data=x, wavelet='haar', mode='symmetric', + axes=(0, 1, 2)) + wp.get_level(1) + r = wp.reconstruct(update=False) + assert_equal(r.shape, x.shape) + assert_allclose(r, x, atol=1e-12, rtol=1e-12) + + rstate = np.random.RandomState(0) + y = rstate.standard_normal((9, 11)) + for level in [1, 2, 3]: + wp = pywt.WaveletPacketND(data=y, wavelet='haar', mode='symmetric') + wp.get_level(level) + r = wp.reconstruct(update=False) + assert_equal(r.shape, y.shape) + assert_allclose(r, y, rtol=1e-12) + + # only some of the axes transformed + z = rstate.standard_normal((3, 5, 7)) + wp = pywt.WaveletPacketND(data=z, wavelet='haar', mode='symmetric', + axes=(1, 2)) + wp.get_level(1) + assert_allclose(wp.reconstruct(update=False), z, rtol=1e-12) + + # reconstructing again after an update gives the same result + wp = pywt.WaveletPacketND(data=y, wavelet='haar', mode='symmetric') + wp.get_level(2) + assert_allclose(wp.reconstruct(update=True), y, rtol=1e-12) + assert_allclose(wp.reconstruct(update=True), y, rtol=1e-12) From a39f48ed6c1af9ffdd72cadcd95cda9955e904f1 Mon Sep 17 00:00:00 2001 From: Ralf Gommers Date: Thu, 13 Aug 2026 15:58:44 +0000 Subject: [PATCH 2/3] BUG: trim the reconstruction of an ND wavelet packet node to its data shape Node._reconstruct and Node2D._reconstruct both trim the inverse transform back to the shape of the coefficients stored in the node, but NodeND._reconstruct did not. For odd-length axes this made a subnode reconstruct one sample too long along each transformed axis, and with update=True the data held by the node silently grew. It is also why only the ND class reached the trim in WaveletPacketND.reconstruct, where the IndexError was hit. --- pywt/_wavelet_packets.py | 3 +++ pywt/tests/test_wpnd.py | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/pywt/_wavelet_packets.py b/pywt/_wavelet_packets.py index 65fc3ac73..461befb0a 100644 --- a/pywt/_wavelet_packets.py +++ b/pywt/_wavelet_packets.py @@ -675,6 +675,9 @@ def _reconstruct(self, update): ) else: rec = idwtn(coeffs, self.wavelet, self.mode, axes=self.axes) + if self._data_shape is not None and ( + rec.shape != self._data_shape): + rec = rec[tuple([slice(sz) for sz in self._data_shape])] if update: self.data = rec return rec diff --git a/pywt/tests/test_wpnd.py b/pywt/tests/test_wpnd.py index 93abbeffd..d04d03416 100644 --- a/pywt/tests/test_wpnd.py +++ b/pywt/tests/test_wpnd.py @@ -202,3 +202,22 @@ def test_wavelet_packet_odd_shape(): wp.get_level(2) assert_allclose(wp.reconstruct(update=True), y, rtol=1e-12) assert_allclose(wp.reconstruct(update=True), y, rtol=1e-12) + + +def test_wavelet_packet_odd_shape_subnode(): + # a subnode reconstructs to the shape of its own coefficients, so an + # update does not grow the data stored in the node + rstate = np.random.RandomState(0) + y = rstate.standard_normal((9, 11)) + wp = pywt.WaveletPacketND(data=y, wavelet='haar', mode='symmetric') + wp.get_level(2) + + shape = wp['aa'].data.shape + assert_equal(wp['aa'].reconstruct(update=False).shape, shape) + wp['aa'].reconstruct(update=True) + assert_equal(wp['aa'].data.shape, shape) + + # WaveletPacket2D already behaves this way + wp2 = pywt.WaveletPacket2D(data=y, wavelet='haar', mode='symmetric') + wp2.get_level(2) + assert_equal(wp2['a'].reconstruct(update=False).shape, shape) From b3b650c6cb9f6ed090769ec6b957d153ec615144 Mon Sep 17 00:00:00 2001 From: Ralf Gommers Date: Thu, 13 Aug 2026 15:59:28 +0000 Subject: [PATCH 3/3] MAINT: use tuple indices in the 1D and 2D wavelet packet reconstruct Same defect as the one fixed in WaveletPacketND.reconstruct: numpy no longer accepts a list of slices as an index. These two are not reachable today, as the node level trim in Node._reconstruct and Node2D._reconstruct already resizes the data, so there is nothing to add a test for. --- pywt/_wavelet_packets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pywt/_wavelet_packets.py b/pywt/_wavelet_packets.py index 461befb0a..787df4431 100644 --- a/pywt/_wavelet_packets.py +++ b/pywt/_wavelet_packets.py @@ -744,7 +744,7 @@ def reconstruct(self, update=True): if self.has_any_subnode: data = super().reconstruct(update) if self.data_size is not None and (data.shape != self.data_size): - data = data[[slice(sz) for sz in self.data_size]] + data = data[tuple(slice(sz) for sz in self.data_size)] if update: self.data = data return data @@ -870,7 +870,7 @@ def reconstruct(self, update=True): if self.has_any_subnode: data = super().reconstruct(update) if self.data_size is not None and (data.shape != self.data_size): - data = data[[slice(sz) for sz in self.data_size]] + data = data[tuple(slice(sz) for sz in self.data_size)] if update: self.data = data return data