test_upfirdn.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. # Code adapted from "upfirdn" python library with permission:
  2. #
  3. # Copyright (c) 2009, Motorola, Inc
  4. #
  5. # All Rights Reserved.
  6. #
  7. # Redistribution and use in source and binary forms, with or without
  8. # modification, are permitted provided that the following conditions are
  9. # met:
  10. #
  11. # * Redistributions of source code must retain the above copyright notice,
  12. # this list of conditions and the following disclaimer.
  13. #
  14. # * Redistributions in binary form must reproduce the above copyright
  15. # notice, this list of conditions and the following disclaimer in the
  16. # documentation and/or other materials provided with the distribution.
  17. #
  18. # * Neither the name of Motorola nor the names of its contributors may be
  19. # used to endorse or promote products derived from this software without
  20. # specific prior written permission.
  21. #
  22. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
  23. # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
  24. # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
  25. # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
  26. # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
  27. # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  28. # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
  29. # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
  30. # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  31. # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  32. # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  33. import numpy as np
  34. from itertools import product
  35. from numpy.testing import assert_equal, assert_allclose
  36. from pytest import raises as assert_raises
  37. import pytest
  38. from scipy.signal import upfirdn, firwin
  39. from scipy.signal._upfirdn import _output_len, _upfirdn_modes
  40. from scipy.signal._upfirdn_apply import _pad_test
  41. def upfirdn_naive(x, h, up=1, down=1):
  42. """Naive upfirdn processing in Python.
  43. Note: arg order (x, h) differs to facilitate apply_along_axis use.
  44. """
  45. h = np.asarray(h)
  46. out = np.zeros(len(x) * up, x.dtype)
  47. out[::up] = x
  48. out = np.convolve(h, out)[::down][:_output_len(len(h), len(x), up, down)]
  49. return out
  50. class UpFIRDnCase:
  51. """Test _UpFIRDn object"""
  52. def __init__(self, up, down, h, x_dtype):
  53. self.up = up
  54. self.down = down
  55. self.h = np.atleast_1d(h)
  56. self.x_dtype = x_dtype
  57. self.rng = np.random.RandomState(17)
  58. def __call__(self):
  59. # tiny signal
  60. self.scrub(np.ones(1, self.x_dtype))
  61. # ones
  62. self.scrub(np.ones(10, self.x_dtype)) # ones
  63. # randn
  64. x = self.rng.randn(10).astype(self.x_dtype)
  65. if self.x_dtype in (np.complex64, np.complex128):
  66. x += 1j * self.rng.randn(10)
  67. self.scrub(x)
  68. # ramp
  69. self.scrub(np.arange(10).astype(self.x_dtype))
  70. # 3D, random
  71. size = (2, 3, 5)
  72. x = self.rng.randn(*size).astype(self.x_dtype)
  73. if self.x_dtype in (np.complex64, np.complex128):
  74. x += 1j * self.rng.randn(*size)
  75. for axis in range(len(size)):
  76. self.scrub(x, axis=axis)
  77. x = x[:, ::2, 1::3].T
  78. for axis in range(len(size)):
  79. self.scrub(x, axis=axis)
  80. def scrub(self, x, axis=-1):
  81. yr = np.apply_along_axis(upfirdn_naive, axis, x,
  82. self.h, self.up, self.down)
  83. want_len = _output_len(len(self.h), x.shape[axis], self.up, self.down)
  84. assert yr.shape[axis] == want_len
  85. y = upfirdn(self.h, x, self.up, self.down, axis=axis)
  86. assert y.shape[axis] == want_len
  87. assert y.shape == yr.shape
  88. dtypes = (self.h.dtype, x.dtype)
  89. if all(d == np.complex64 for d in dtypes):
  90. assert_equal(y.dtype, np.complex64)
  91. elif np.complex64 in dtypes and np.float32 in dtypes:
  92. assert_equal(y.dtype, np.complex64)
  93. elif all(d == np.float32 for d in dtypes):
  94. assert_equal(y.dtype, np.float32)
  95. elif np.complex128 in dtypes or np.complex64 in dtypes:
  96. assert_equal(y.dtype, np.complex128)
  97. else:
  98. assert_equal(y.dtype, np.float64)
  99. assert_allclose(yr, y)
  100. _UPFIRDN_TYPES = (int, np.float32, np.complex64, float, complex)
  101. class TestUpfirdn:
  102. def test_valid_input(self):
  103. assert_raises(ValueError, upfirdn, [1], [1], 1, 0) # up or down < 1
  104. assert_raises(ValueError, upfirdn, [], [1], 1, 1) # h.ndim != 1
  105. assert_raises(ValueError, upfirdn, [[1]], [1], 1, 1)
  106. @pytest.mark.parametrize('len_h', [1, 2, 3, 4, 5])
  107. @pytest.mark.parametrize('len_x', [1, 2, 3, 4, 5])
  108. def test_singleton(self, len_h, len_x):
  109. # gh-9844: lengths producing expected outputs
  110. h = np.zeros(len_h)
  111. h[len_h // 2] = 1. # make h a delta
  112. x = np.ones(len_x)
  113. y = upfirdn(h, x, 1, 1)
  114. want = np.pad(x, (len_h // 2, (len_h - 1) // 2), 'constant')
  115. assert_allclose(y, want)
  116. def test_shift_x(self):
  117. # gh-9844: shifted x can change values?
  118. y = upfirdn([1, 1], [1.], 1, 1)
  119. assert_allclose(y, [1, 1]) # was [0, 1] in the issue
  120. y = upfirdn([1, 1], [0., 1.], 1, 1)
  121. assert_allclose(y, [0, 1, 1])
  122. # A bunch of lengths/factors chosen because they exposed differences
  123. # between the "old way" and new way of computing length, and then
  124. # got `expected` from MATLAB
  125. @pytest.mark.parametrize('len_h, len_x, up, down, expected', [
  126. (2, 2, 5, 2, [1, 0, 0, 0]),
  127. (2, 3, 6, 3, [1, 0, 1, 0, 1]),
  128. (2, 4, 4, 3, [1, 0, 0, 0, 1]),
  129. (3, 2, 6, 2, [1, 0, 0, 1, 0]),
  130. (4, 11, 3, 5, [1, 0, 0, 1, 0, 0, 1]),
  131. ])
  132. def test_length_factors(self, len_h, len_x, up, down, expected):
  133. # gh-9844: weird factors
  134. h = np.zeros(len_h)
  135. h[0] = 1.
  136. x = np.ones(len_x)
  137. y = upfirdn(h, x, up, down)
  138. assert_allclose(y, expected)
  139. @pytest.mark.parametrize('down, want_len', [ # lengths from MATLAB
  140. (2, 5015),
  141. (11, 912),
  142. (79, 127),
  143. ])
  144. def test_vs_convolve(self, down, want_len):
  145. # Check that up=1.0 gives same answer as convolve + slicing
  146. random_state = np.random.RandomState(17)
  147. try_types = (int, np.float32, np.complex64, float, complex)
  148. size = 10000
  149. for dtype in try_types:
  150. x = random_state.randn(size).astype(dtype)
  151. if dtype in (np.complex64, np.complex128):
  152. x += 1j * random_state.randn(size)
  153. h = firwin(31, 1. / down, window='hamming')
  154. yl = upfirdn_naive(x, h, 1, down)
  155. y = upfirdn(h, x, up=1, down=down)
  156. assert y.shape == (want_len,)
  157. assert yl.shape[0] == y.shape[0]
  158. assert_allclose(yl, y, atol=1e-7, rtol=1e-7)
  159. @pytest.mark.parametrize('x_dtype', _UPFIRDN_TYPES)
  160. @pytest.mark.parametrize('h', (1., 1j))
  161. @pytest.mark.parametrize('up, down', [(1, 1), (2, 2), (3, 2), (2, 3)])
  162. def test_vs_naive_delta(self, x_dtype, h, up, down):
  163. UpFIRDnCase(up, down, h, x_dtype)()
  164. @pytest.mark.parametrize('x_dtype', _UPFIRDN_TYPES)
  165. @pytest.mark.parametrize('h_dtype', _UPFIRDN_TYPES)
  166. @pytest.mark.parametrize('p_max, q_max',
  167. list(product((10, 100), (10, 100))))
  168. def test_vs_naive(self, x_dtype, h_dtype, p_max, q_max):
  169. tests = self._random_factors(p_max, q_max, h_dtype, x_dtype)
  170. for test in tests:
  171. test()
  172. def _random_factors(self, p_max, q_max, h_dtype, x_dtype):
  173. n_rep = 3
  174. longest_h = 25
  175. random_state = np.random.RandomState(17)
  176. tests = []
  177. for _ in range(n_rep):
  178. # Randomize the up/down factors somewhat
  179. p_add = q_max if p_max > q_max else 1
  180. q_add = p_max if q_max > p_max else 1
  181. p = random_state.randint(p_max) + p_add
  182. q = random_state.randint(q_max) + q_add
  183. # Generate random FIR coefficients
  184. len_h = random_state.randint(longest_h) + 1
  185. h = np.atleast_1d(random_state.randint(len_h))
  186. h = h.astype(h_dtype)
  187. if h_dtype == complex:
  188. h += 1j * random_state.randint(len_h)
  189. tests.append(UpFIRDnCase(p, q, h, x_dtype))
  190. return tests
  191. @pytest.mark.parametrize('mode', _upfirdn_modes)
  192. def test_extensions(self, mode):
  193. """Test vs. manually computed results for modes not in numpy's pad."""
  194. x = np.array([1, 2, 3, 1], dtype=float)
  195. npre, npost = 6, 6
  196. y = _pad_test(x, npre=npre, npost=npost, mode=mode)
  197. if mode == 'antisymmetric':
  198. y_expected = np.asarray(
  199. [3, 1, -1, -3, -2, -1, 1, 2, 3, 1, -1, -3, -2, -1, 1, 2])
  200. elif mode == 'antireflect':
  201. y_expected = np.asarray(
  202. [1, 2, 3, 1, -1, 0, 1, 2, 3, 1, -1, 0, 1, 2, 3, 1])
  203. elif mode == 'smooth':
  204. y_expected = np.asarray(
  205. [-5, -4, -3, -2, -1, 0, 1, 2, 3, 1, -1, -3, -5, -7, -9, -11])
  206. elif mode == "line":
  207. lin_slope = (x[-1] - x[0]) / (len(x) - 1)
  208. left = x[0] + np.arange(-npre, 0, 1) * lin_slope
  209. right = x[-1] + np.arange(1, npost + 1) * lin_slope
  210. y_expected = np.concatenate((left, x, right))
  211. else:
  212. y_expected = np.pad(x, (npre, npost), mode=mode)
  213. assert_allclose(y, y_expected)
  214. @pytest.mark.parametrize(
  215. 'size, h_len, mode, dtype',
  216. product(
  217. [8],
  218. [4, 5, 26], # include cases with h_len > 2*size
  219. _upfirdn_modes,
  220. [np.float32, np.float64, np.complex64, np.complex128],
  221. )
  222. )
  223. def test_modes(self, size, h_len, mode, dtype):
  224. random_state = np.random.RandomState(5)
  225. x = random_state.randn(size).astype(dtype)
  226. if dtype in (np.complex64, np.complex128):
  227. x += 1j * random_state.randn(size)
  228. h = np.arange(1, 1 + h_len, dtype=x.real.dtype)
  229. y = upfirdn(h, x, up=1, down=1, mode=mode)
  230. # expected result: pad the input, filter with zero padding, then crop
  231. npad = h_len - 1
  232. if mode in ['antisymmetric', 'antireflect', 'smooth', 'line']:
  233. # use _pad_test test function for modes not supported by np.pad.
  234. xpad = _pad_test(x, npre=npad, npost=npad, mode=mode)
  235. else:
  236. xpad = np.pad(x, npad, mode=mode)
  237. ypad = upfirdn(h, xpad, up=1, down=1, mode='constant')
  238. y_expected = ypad[npad:-npad]
  239. atol = rtol = np.finfo(dtype).eps * 1e2
  240. assert_allclose(y, y_expected, atol=atol, rtol=rtol)
  241. def test_output_len_long_input():
  242. # Regression test for gh-17375. On Windows, a large enough input
  243. # that should have been well within the capabilities of 64 bit integers
  244. # would result in a 32 bit overflow because of a bug in Cython 0.29.32.
  245. len_h = 1001
  246. in_len = 10**8
  247. up = 320
  248. down = 441
  249. out_len = _output_len(len_h, in_len, up, down)
  250. # The expected value was computed "by hand" from the formula
  251. # (((in_len - 1) * up + len_h) - 1) // down + 1
  252. assert out_len == 72562360