From 849819a827661c9955c0f9a958f837b36f552d6b Mon Sep 17 00:00:00 2001 From: Ralf Gommers Date: Thu, 13 Aug 2026 13:44:38 +0000 Subject: [PATCH 1/2] BUG: use constant extension for 'smooth' on signals shorter than 2 samples A slope cannot be determined from a single sample, and pywt.pad computed one anyway from whatever numpy.pad had left in the padding buffer, giving a ramp away from the only known value. With a pad width of 0 there was no buffer to read from and it raised an IndexError instead. The transforms already handle this: convolution.template.c demotes MODE_SMOOTH to MODE_CONSTANT_EDGE when the input has fewer than 2 samples. Do the same in pad(), and leave a zero-length axis untouched as there is nothing to extend from. --- pywt/_dwt.py | 17 ++++++++++++++--- pywt/tests/test_dwt_idwt.py | 19 +++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/pywt/_dwt.py b/pywt/_dwt.py index 1a5a1ffa..d0b6a75f 100644 --- a/pywt/_dwt.py +++ b/pywt/_dwt.py @@ -461,6 +461,20 @@ def pad(x, pad_widths, mode): xp = np.pad(x, pad_widths, mode='edge') elif mode == 'smooth': def pad_smooth(vector, pad_width, iaxis, kwargs): + # 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 # last unpadded sample + vsize_nonpad = iright + 1 - pad_width[0] + if vsize_nonpad < 2: + # The slope is undefined for a signal shorter than two + # samples, so use constant edge extension instead. This is + # what the transforms do as well (see the MODE_SMOOTH case in + # convolution.template.c). + if vsize_nonpad == 1: + vector[:pad_width[0]] = vector[iright] + vector[iright + 1:] = vector[iright] + return vector + # smooth extension to left left = vector[pad_width[0]] slope_left = (left - vector[pad_width[0] + 1]) @@ -468,9 +482,6 @@ def pad_smooth(vector, pad_width, iaxis, kwargs): left + np.arange(pad_width[0], 0, -1) * slope_left # smooth extension to right - # 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:] = \ diff --git a/pywt/tests/test_dwt_idwt.py b/pywt/tests/test_dwt_idwt.py index e0c59645..919b3299 100644 --- a/pywt/tests/test_dwt_idwt.py +++ b/pywt/tests/test_dwt_idwt.py @@ -310,6 +310,25 @@ def test_pad_one_sided(): err_msg=f"mode={mode}") +def test_pad_smooth_short_signal(): + # the slope used by 'smooth' is undefined for a signal shorter than two + # samples, so constant edge extension is used instead (gh-589) + assert_array_equal(pywt.pad([1.], 0, 'smooth'), [1.]) + assert_array_equal(pywt.pad([1.], (2, 3), 'smooth'), + pywt.pad([1.], (2, 3), 'constant')) + + # only the length 1 axis of an nd array falls back + x = np.arange(3.0).reshape(1, 3) + assert_array_equal(pywt.pad(x, ((0, 0), (2, 2)), 'smooth'), + [[-2, -1, 0, 1, 2, 3, 4]]) + assert_array_equal(pywt.pad(x, ((2, 2), (0, 0)), 'smooth'), + np.tile(x, (5, 1))) + + # matches the fallback used by the transforms + assert_allclose(pywt.dwt([1.], 'db2', 'smooth'), + pywt.dwt([1.], 'db2', 'constant')) + + def test_pad_errors(): # negative pad width x = [1, 2, 3] From af6c4e0f923dd628b3870447f022e8cd3dfd6054 Mon Sep 17 00:00:00 2001 From: Ralf Gommers Date: Thu, 13 Aug 2026 13:45:29 +0000 Subject: [PATCH 2/2] BUG: fix a hang in pywt.pad for 'antisymmetric' on a zero-size axis The reflected segments that the mode flips the sign of are as wide as the unpadded signal, so for a zero-size axis `r_edge += seg_width` never advanced and the loop spun forever. Only reachable with a pad width of 0, as numpy.pad rejects reflecting an empty axis otherwise. There is nothing to reflect in that case, so return early. Also assert what 'periodization' does for a zero pad width instead of skipping it, since it is the one mode that is deliberately not the identity there. --- pywt/_dwt.py | 7 +++++-- pywt/tests/test_dwt_idwt.py | 13 +++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/pywt/_dwt.py b/pywt/_dwt.py index d0b6a75f..cf01b61a 100644 --- a/pywt/_dwt.py +++ b/pywt/_dwt.py @@ -490,10 +490,13 @@ def pad_smooth(vector, pad_width, iaxis, kwargs): xp = np.pad(x, pad_widths, pad_smooth) elif mode == 'antisymmetric': def pad_antisymmetric(vector, pad_width, iaxis, kwargs): - # smooth extension to left - # implement by flipping portions symmetric padding + # implement by flipping portions of symmetric padding npad_l, npad_r = pad_width vsize_nonpad = vector.size - npad_l - npad_r + if vsize_nonpad == 0: + # Nothing to reflect. The reflected segments below would have + # zero width, so the loops over them would never terminate. + return vector # Note: must modify vector in-place vector[:] = np.pad(vector[npad_l:vector.size - npad_r], pad_width, mode='symmetric') diff --git a/pywt/tests/test_dwt_idwt.py b/pywt/tests/test_dwt_idwt.py index 919b3299..411e4c8e 100644 --- a/pywt/tests/test_dwt_idwt.py +++ b/pywt/tests/test_dwt_idwt.py @@ -290,6 +290,19 @@ def test_pad_zero_width(): assert_array_equal(pywt.pad(x, 0, mode), x, err_msg=f"mode={mode}, ndim={ndim}") + assert_array_equal(pywt.pad([1, 2, 3], 0, 'periodization'), [1, 2, 3, 3]) + + +def test_pad_zero_size_axis(): + # a zero-size axis has nothing to extend from, so a zero pad width leaves + # it alone rather than raising or (for 'antisymmetric') hanging (gh-589) + x = np.ones((0, 4)) + for mode in pywt.Modes.modes: + assert_array_equal(pywt.pad(x, 0, mode), x, err_msg=f"mode={mode}") + + # padding along the other axis only + assert_(pywt.pad(x, ((0, 0), (2, 2)), 'antisymmetric').shape == (0, 8)) + def test_pad_one_sided(): # a zero pad width on only one side of the axis (gh-589)