From 5340bbdbe54d65e8e9c4538148ae276de35d9596 Mon Sep 17 00:00:00 2001 From: Achraf Ez Date: Tue, 4 Aug 2026 15:53:24 +0000 Subject: [PATCH] BUG: fix pywt.pad for a zero pad width in 'smooth' and 'antisymmetric' (gh-589) The two modes that numpy.pad cannot express directly are implemented with callbacks that index the vector from the end using the trailing pad width. When that width is 0, `vector[-pad_width[1]:]` selects the whole vector instead of an empty slice, and `vector[pad_width[0]:-pad_width[-1]]` selects an empty one, so pad() raised a ValueError instead of returning the array unchanged. Every other extension mode already treats a zero pad width as a no-op. Index from the start of the vector instead, and correct the message of the negative-pad-width check, which reported that 0 is invalid. --- pywt/_dwt.py | 13 ++++++++----- pywt/tests/test_dwt_idwt.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/pywt/_dwt.py b/pywt/_dwt.py index 1af28d945..1a5a1ffaa 100644 --- a/pywt/_dwt.py +++ b/pywt/_dwt.py @@ -442,7 +442,7 @@ def pad(x, pad_widths, mode): pad_widths = np.array(pad_widths) pad_widths = np.round(pad_widths).astype(np.intp, copy=False) if pad_widths.min() < 0: - raise ValueError("pad_widths must be > 0") + raise ValueError("pad_widths must be >= 0") pad_widths = np.broadcast_to(pad_widths, (x.ndim, 2)).tolist() if mode in ['symmetric', 'reflect']: @@ -468,9 +468,12 @@ def pad_smooth(vector, pad_width, iaxis, kwargs): left + np.arange(pad_width[0], 0, -1) * slope_left # smooth extension to right - right = vector[-pad_width[1] - 1] - slope_right = (right - vector[-pad_width[1] - 2]) - vector[-pad_width[1]:] = \ + # Note: indices are measured from the start of the vector so that + # a pad width of 0 gives an empty slice rather than the full one. + iright = vector.size - pad_width[1] - 1 + right = vector[iright] + slope_right = (right - vector[iright - 1]) + vector[iright + 1:] = \ right + np.arange(1, pad_width[1] + 1) * slope_right return vector xp = np.pad(x, pad_widths, pad_smooth) @@ -481,7 +484,7 @@ def pad_antisymmetric(vector, pad_width, iaxis, kwargs): npad_l, npad_r = pad_width vsize_nonpad = vector.size - npad_l - npad_r # Note: must modify vector in-place - vector[:] = np.pad(vector[pad_width[0]:-pad_width[-1]], + vector[:] = np.pad(vector[npad_l:vector.size - npad_r], pad_width, mode='symmetric') vp = vector r_edge = npad_l + vsize_nonpad - 1 diff --git a/pywt/tests/test_dwt_idwt.py b/pywt/tests/test_dwt_idwt.py index 5e1349b77..e0c59645a 100644 --- a/pywt/tests/test_dwt_idwt.py +++ b/pywt/tests/test_dwt_idwt.py @@ -279,6 +279,37 @@ def test_pad_1d(): pywt.pad(x, (4, 4), 'periodic')) +def test_pad_zero_width(): + # zero pad width is a no-op for all modes except 'periodization', which + # also promotes odd-length axes to even length (gh-589) + for ndim in [1, 2, 3]: + x = np.arange(3.0**ndim).reshape((3, ) * ndim) + for mode in pywt.Modes.modes: + if mode == 'periodization': + continue + assert_array_equal(pywt.pad(x, 0, mode), x, + err_msg=f"mode={mode}, ndim={ndim}") + + +def test_pad_one_sided(): + # a zero pad width on only one side of the axis (gh-589) + x = [1, 2, 3] + assert_array_equal(pywt.pad(x, (2, 0), 'smooth'), [-1, 0, 1, 2, 3]) + assert_array_equal(pywt.pad(x, (0, 2), 'smooth'), [1, 2, 3, 4, 5]) + assert_array_equal(pywt.pad(x, (2, 0), 'antisymmetric'), [-2, -1, 1, 2, 3]) + assert_array_equal(pywt.pad(x, (0, 2), 'antisymmetric'), [1, 2, 3, -3, -2]) + + # one-sided padding matches the corresponding slice of two-sided padding + for mode in pywt.Modes.modes: + if mode == 'periodization': + continue + two_sided = pywt.pad(x, (4, 6), mode) + assert_array_equal(pywt.pad(x, (4, 0), mode), two_sided[:-6], + err_msg=f"mode={mode}") + assert_array_equal(pywt.pad(x, (0, 6), mode), two_sided[4:], + err_msg=f"mode={mode}") + + def test_pad_errors(): # negative pad width x = [1, 2, 3]