Skip to content

DEP: signal: remove support for object arrays and longdoubles in {correlate,convolve,lfilter,sosfilter} #23212

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Jul 5, 2025

Conversation

j-bowhay
Copy link
Member

As part of array api conversion support for object arrays is scheduled for removal in this release.

@j-bowhay j-bowhay requested a review from ev-br June 22, 2025 20:36
@j-bowhay j-bowhay requested review from larsoner and ilayn as code owners June 22, 2025 20:36
@j-bowhay j-bowhay removed request for ilayn and larsoner June 22, 2025 20:36
@github-actions github-actions bot added scipy.signal deprecated Items related to behavior that has been deprecated labels Jun 22, 2025
Copy link
Member

@ev-br ev-br left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

Can also remove some C code, too: https://github.com/scipy/scipy/blob/main/scipy/signal/_lfilter.cc#L24, https://github.com/scipy/scipy/blob/main/scipy/signal/_correlate_nd.cc#L181 and maybe some more from the _sigtools extension.

Can be done either here or in a follow-up though.

@lucascolley lucascolley added this to the 1.17.0 milestone Jun 22, 2025
@lucascolley lucascolley changed the title DEP: signal: remove support for object arrays and longdoubles in signal.{correlate,convolve,lfilter,sosfilter} DEP: signal: remove support for object arrays and longdoubles in {correlate,convolve,lfilter,sosfilter} Jun 22, 2025
@j-bowhay
Copy link
Member Author

Done thanks, that made me spot a few areas I'd missed

@ev-br
Copy link
Member

ev-br commented Jun 22, 2025

In fact, also _sosfilt.pyx can be cleaned up, if you're up to it.

@j-bowhay j-bowhay force-pushed the signal_object_array branch from 139831b to b17bf7d Compare June 23, 2025 06:55
Co-authored-by: Evgeni Burovski <evgeny.burovskiy@gmail.com>
@j-bowhay j-bowhay mentioned this pull request Jun 23, 2025
32 tasks
@ev-br
Copy link
Member

ev-br commented Jun 23, 2025

I think the CI failure is due to this line: https://github.com/scipy/scipy/blob/main/scipy/signal/tests/test_signaltools.py#L2951: butter returns numpy arrays for python scalars. A low-tech solution (used also elsewhere) is to wrap it first or second argument into an xp.asarray/

@j-bowhay j-bowhay force-pushed the signal_object_array branch from 8836a50 to 5ed36db Compare June 23, 2025 22:03
@ev-br
Copy link
Member

ev-br commented Jun 24, 2025

This patch seems to fix the dask failure:

$ git diff
diff --git a/scipy/signal/_signaltools.py b/scipy/signal/_signaltools.py
index 2cdb6c45c4..b085f758f4 100644
--- a/scipy/signal/_signaltools.py
+++ b/scipy/signal/_signaltools.py
@@ -4755,6 +4755,7 @@ def filtfilt(b, a, x, axis=-1, padtype='odd', padlen=None, method='pad',
     # method == "pad"
     edge, ext = _validate_pad(padtype, padlen, x, axis,
                               ntaps=max(len(a), len(b)))
+    ext = xp.asarray(ext)
 
     # Get the steady state of the filter's step response.
     zi = lfilter_zi(b, a)

EDIT: Sorry, no. The patch above is wrong, and causes other issues elsewhere. A correct patch:

$ git diff
diff --git a/scipy/signal/_signaltools.py b/scipy/signal/_signaltools.py
index 2cdb6c45c4..61460f999b 100644
--- a/scipy/signal/_signaltools.py
+++ b/scipy/signal/_signaltools.py
@@ -4741,8 +4741,8 @@ def filtfilt(b, a, x, axis=-1, padtype='odd', padlen=None, method='pad',
     """
     xp = array_namespace(b, a, x)
 
-    b = np.atleast_1d(b)
-    a = np.atleast_1d(a)
+    b = np.atleast_1d(np.asarray(b))
+    a = np.atleast_1d(np.asarray(a))
     x = np.asarray(x)
 
     if method not in ["pad", "gust"]:
diff --git a/scipy/signal/tests/test_signaltools.py b/scipy/signal/tests/test_signaltools.py
index 8d6f82bedd..f922e5c606 100644
--- a/scipy/signal/tests/test_signaltools.py
+++ b/scipy/signal/tests/test_signaltools.py
@@ -1649,8 +1649,7 @@ class TestResample:
             x[0] = 0
             x[-1] = 0
 
-            h = signal.firwin(31, 1. / down_factor, window='hamming')
-            h = xp.asarray(h)   # XXX: convert firwin
+            h = signal.firwin(31, xp.asarray(1. / down_factor), window='hamming')
             yf = filtfilt(h, 1.0, x, padtype='constant')[::down_factor]
 
             # Need to pass convolved version of filter to resample_poly,

The underlying reason is that np.atleast_1d calls np.asanyarray not np.asarray

@ev-br
Copy link
Member

ev-br commented Jun 24, 2025

Array-api-strict test failure is fixed by this:

@@ -2803,7 +2802,7 @@ class TestFiltFilt:
         if is_jax(xp) and self.filtfilt_kind == 'sos':
             pytest.skip(reason='sosfilt works in-place')
 
-        zpk = tf2zpk(xp.asarray([1, 2, 3]), xp.asarray([1, 2, 3]))
+        zpk = tf2zpk(xp.asarray([1.0, 2, 3]), xp.asarray([1.0, 2, 3]))
         out = self.filtfilt(zpk, xp.arange(12), xp=xp)
         atol= 3e-9 if is_cupy(xp) else 5.28e-11
         xp_assert_close(out, xp.arange(12, dtype=xp.float64), atol=atol)

The what remains is a small tolerance bump on CuPy. Phew.

@j-bowhay
Copy link
Member Author

j-bowhay commented Jul 4, 2025

CI finally green, hopefully this is ready to go!

@lucascolley lucascolley added the needs-release-note a maintainer should add a release note written by a reviewer/author to the wiki label Jul 5, 2025
Copy link
Member

@lucascolley lucascolley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks Jake, great cleanup!

@lucascolley lucascolley merged commit 3b51780 into scipy:main Jul 5, 2025
38 checks passed
@ev-br
Copy link
Member

ev-br commented Jul 5, 2025

Minus 500 LOC of obscure C code, yay!

@j-bowhay
Copy link
Member Author

j-bowhay commented Jul 5, 2025

Is there anywhere else we're hiding compiled code just for handling object arrays?

@ev-br
Copy link
Member

ev-br commented Jul 5, 2025

is there anywhere else we're hiding compiled code just for handling object arrays?

Let's see:

$ git grep NPY_OBJECT     # None found anymoe, yay!
$ git grep "object\["
scipy/sparse/_csparsetools.pyx.in:cpdef lil_get1(cnp.npy_intp M, cnp.npy_intp N, object[:] rows, object[:] datas,
scipy/sparse/_csparsetools.pyx.in:cpdef int lil_insert(cnp.npy_intp M, cnp.npy_intp N, object[:] rows,
scipy/sparse/_csparsetools.pyx.in:                     object[:] datas, cnp.npy_intp i, cnp.npy_intp j,
scipy/sparse/_csparsetools.pyx.in:def lil_get_lengths(object[:] input,
scipy/sparse/_csparsetools.pyx.in:def _lil_get_lengths_{{NAME}}(object[:] input,
scipy/sparse/_csparsetools.pyx.in:def _lil_flatten_to_array_{{NAME}}(object[:] input not None, cnp.ndarray[{{T}}] output not None):
scipy/sparse/_csparsetools.pyx.in:                  object[:] rows,
scipy/sparse/_csparsetools.pyx.in:                  object[:] datas,
scipy/sparse/_csparsetools.pyx.in:                  object[:] new_rows,
scipy/sparse/_csparsetools.pyx.in:                  object[:] new_datas,
scipy/sparse/_csparsetools.pyx.in:                            object[:] rows,
scipy/sparse/_csparsetools.pyx.in:                            object[:] datas,
scipy/sparse/_csparsetools.pyx.in:                            object[:] new_rows,
scipy/sparse/_csparsetools.pyx.in:                            object[:] new_datas,
scipy/sparse/_csparsetools.pyx.in:                  object[:] rows,
scipy/sparse/_csparsetools.pyx.in:                  object[:] data,
scipy/sparse/_csparsetools.pyx.in:                                         object[:] rows,
scipy/sparse/_csparsetools.pyx.in:                                         object[:] data,
scipy/sparse/_csparsetools.pyx.in:                       object[:] new_rows, object[:] new_datas,
scipy/spatial/_ckdtree.pyx:            object[::1] vout

Don't know about sparsetools usage; there's something in KDTree.

Let's see about object arrays in pure python:

$ git grep "dtype=object"
doc/source/release/0.7.0-notes.rst:* string arrays have ``dtype='U...'`` instead of ``dtype=object``
doc/source/release/1.3.0-notes.rst:* `#9645 <https://github.com/scipy/scipy/issues/9645>`__: \`scipy.stats.mode\` crashes with variable length arrays (\`dtype=object\`)
doc/source/tutorial/io.rst:   >>> obj_arr = np.zeros((2,), dtype=object)
doc/source/tutorial/io.rst:   array([1, 'a string'], dtype=object)
scipy/cluster/tests/test_disjoint_set.py:    tokens = np.array(tokens, dtype=object)
scipy/interpolate/_interpolate.py:            r2 = np.empty(prod(self.c.shape[2:]), dtype=object)
scipy/interpolate/tests/test_interpolate.py:            assert_raises(ValueError, p, np.array([[0.1, 0.2], [0.4]], dtype=object))
scipy/io/matlab/_mio.py:       old-style NumPy arrays with dtype=object. Setting this flag to
scipy/io/matlab/_mio5.py:        narr = np.asanyarray(source, dtype=object)
scipy/io/matlab/_mio5_utils.pyx:        result = np.empty(length, dtype=object)
scipy/io/matlab/_mio5_utils.pyx:                return np.empty(tupdims, dtype=object).T
scipy/io/matlab/_mio5_utils.pyx:        result = np.empty(length, dtype=object)
scipy/io/matlab/_miobase.py:   old-style NumPy arrays with dtype=object. Setting this flag to
scipy/io/matlab/tests/test_mio.py:        mlarr([[1,2,3]])), dtype=object).reshape(1,-1)
scipy/io/matlab/tests/test_mio.py:    mlarr(3)), dtype=object).reshape(1,-1)
scipy/io/matlab/tests/test_mio.py:objarr = np.empty((1,1),dtype=object)
scipy/io/matlab/tests/test_mio.py:CN = np.zeros((1,2), dtype=object)
scipy/io/matlab/tests/test_mio.py:CN[0,1] = np.zeros((1,3), dtype=object)
scipy/io/matlab/tests/test_mio.py:CN[0,1][0,2] = np.zeros((1,2), dtype=object)
scipy/io/matlab/tests/test_mio.py:    cell = np.ndarray((1,2),dtype=object)
scipy/io/matlab/tests/test_mio.py:    cells = np.ndarray((1,2),dtype=object)
scipy/io/matlab/tests/test_mio.py:    cells = np.ndarray((1,1),dtype=object)
scipy/io/matlab/tests/test_mio.py:    savemat(stream, {'A':np.array(1, dtype=object)})
scipy/io/matlab/tests/test_mio.py:                                     ['world']], dtype=object))
scipy/io/tests/test_idl.py:                         np.array([b"cheese", b"bacon", b"spam"], dtype=object))
scipy/io/tests/test_idl.py:        assert_identical(s.scalars.e, np.array([b"spam"], dtype=object))
scipy/io/tests/test_idl.py:                               np.array([b"cheese", b"bacon", b"spam"], dtype=object))
scipy/io/tests/test_idl.py:                                            dtype=object))
scipy/io/tests/test_idl.py:                                                    dtype=object))
scipy/linalg/_special_matrices.py:            L_n = np.empty((n, n), dtype=object)
scipy/signal/tests/test_ltisys.py:        tf = (np.array([1, [2, 3]], dtype=object), [1, 6])
scipy/signal/tests/test_ltisys.py:        tf = (np.array([[1, -3], [1, 2, 3]], dtype=object), [1, 6, 5])
scipy/sparse/_lil.py:                self.rows = np.empty((M,), dtype=object)
scipy/sparse/_lil.py:                self.data = np.empty((M,), dtype=object)
scipy/spatial/_ckdtree.pyx:            result = np.empty(retshape, dtype=object)
scipy/spatial/tests/test_distance.py:        data = np.array([[myclass()]], dtype=object)
scipy/spatial/tests/test_distance.py:        data = np.array([[myclass()], [myclass()]], dtype=object)
scipy/spatial/tests/test_kdtree.py:    y = np.empty(shape=(10, ), dtype=object)
scipy/spatial/transform/tests/test_rotation.py:    array = np.array([single, multiple], dtype=object)
scipy/special/tests/test_basic.py:            exp = np.array(_nest_me(nucleus, k=levels), dtype=object)
scipy/special/tests/test_basic.py:        n = asarray([4, 4, 4, 4], dtype=object)
scipy/special/tests/test_basic.py:        k = asarray([1, 2, 3, 4], dtype=object)
scipy/stats/_levy_stable/__init__.py:    [[], [1]] + [v[2] for v in _builtincoeffs.values()], dtype=object
scipy/stats/_stats_py.py:                return np.array(arrays, dtype=object)
scipy/stats/tests/test_contingency.py:                  np.array([[1, 2], ["dd", 4]], dtype=object), 'cramer')
scipy/stats/tests/test_stats.py:            stats.mode(np.arange(3, dtype=object))
scipy/stats/tests/test_stats.py:            stats.lmoment(np.array(self.data, dtype=object))

ISTM we can discount scipy/io; the rest would be nice to audit and remove (potentially with deprecations if needs be).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
deprecated Items related to behavior that has been deprecated needs-release-note a maintainer should add a release note written by a reviewer/author to the wiki scipy.signal
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants