diff --git a/pywt/_dwt.py b/pywt/_dwt.py index 1a5a1ffa..cf01b61a 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:] = \ @@ -479,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 e0c59645..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) @@ -310,6 +323,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]