Repository: nipy/nitime

Issue:
numpy 2.0 compatibility
https://github.com/conda-forge/nitime-feedstock/pull/32 indicates (unsurprisingly) that there are numpy 2.0 compatibilities.

With numpy 2.0 in RC phase, it's probably time to try to fix these.

I don't have the time at the moment, unfortunately.

Discussion:

Patch:
diff --git a/nitime/algorithms/event_related.py b/nitime/algorithms/event_related.py
index acf7d34..e0141a3 100644
--- a/nitime/algorithms/event_related.py
+++ b/nitime/algorithms/event_related.py
@@ -10,7 +10,7 @@ from nitime.lazy import scipy_fftpack as fftpack
 
 
 def fir(timeseries, design):
-    """
+    r"""
     Calculate the FIR (finite impulse response) HRF, according to [Burock2000]_
 
     Parameters
diff --git a/nitime/index_utils.py b/nitime/index_utils.py
index be5de81..e845c60 100644
--- a/nitime/index_utils.py
+++ b/nitime/index_utils.py
@@ -6,11 +6,11 @@ __all__ = ['tri', 'triu', 'tril', 'mask_indices', 'tril_indices',
            'tril_indices_from', 'triu_indices', 'triu_indices_from',
            ]
 
-from numpy.core.numeric import asanyarray, subtract, arange, \
+from numpy import asanyarray, subtract, arange, \
      greater_equal, multiply, ones, asarray, where
 
-# Need to import numpy for the doctests! 
-import numpy as np  
+# Need to import numpy for the doctests!
+import numpy as np
 
 def tri(N, M=None, k=0, dtype=float):
     """
diff --git a/nitime/timeseries.py b/nitime/timeseries.py
index 4fd44a9..c13ced2 100644
--- a/nitime/timeseries.py
+++ b/nitime/timeseries.py
@@ -33,6 +33,11 @@ import numpy as np
 # Our own
 from nitime import descriptors as desc
 
+try:
+    _NP_2 = int(np.__version__.split(".")[0]) >= 2
+except Exception:
+    _NP_2 = True
+
 #-----------------------------------------------------------------------------
 # Module globals
 #-----------------------------------------------------------------------------
@@ -112,7 +117,9 @@ class TimeArray(np.ndarray, TimeInterface):
             which are SI units of time. Default: 's'
 
         copy : bool, optional
-            Whether to create this instance by  copy of a
+            Whether to create this instance by copy of a. If False,
+            a copy will not be forced but might still be required depending
+            on the data array.
 
         Note
         ----
@@ -152,7 +159,7 @@ class TimeArray(np.ndarray, TimeInterface):
                 e_s += 'TimeArray in object, or int64 times, in %s' % base_unit
                 raise ValueError(e_s)
 
-            time = np.array(data, copy=False)
+            time = np.asarray(data)
         else:
             if isinstance(data, TimeInterface):
                 time = data.copy()
@@ -309,7 +316,11 @@ class TimeArray(np.ndarray, TimeInterface):
         return ret
 
     def ptp(self, *args, **kwargs):
-        ret = TimeArray(np.ndarray.ptp(self, *args, **kwargs),
+        if _NP_2:
+            ptp = np.ptp
+        else:
+            ptp = np.ndarray.ptp
+        ret = TimeArray(ptp(self, *args, **kwargs),
                         time_unit=base_unit)
         ret.convert_unit(self.time_unit)
         return ret
diff --git a/pyproject.toml b/pyproject.toml
index 2b60557..0ba485b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -62,6 +62,12 @@ skip = "pp* cp38-*_aarch64 cp38-musllinux_*"
 # don't bother unless someone asks
 archs = ["native"]
 
+test-requires = [
+    "pytest",
+    "nitime[full]",  # Enable all optional behavior
+]
+test-command = "pytest -rsx --pyargs nitime"
+
 [tool.cibuildwheel.linux]
 archs = ["x86_64", "aarch64"]
 


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index a91458d..ade1112 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -19,8 +19,10 @@ jobs:
   test:
 
     runs-on: ubuntu-latest
+    continue-on-error: true
     strategy:
       matrix:
+        # We test NumPy dev on 3.11
         python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
         requires: ['requirements.txt']
         include:
@@ -37,9 +39,13 @@ jobs:
         allow-prereleases: true
     - name: Install
       run: |
+        set -eo pipefail
         python -m pip install --upgrade pip
         python -m pip install -r ${{ matrix.requires }}
         python -m pip install -r requirements-dev.txt
+        if [[ "${{ matrix.python-version }}" == "3.11" ]]; then
+          python -m pip install --only-binary numpy --pre --extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple "numpy>=2.1.0.dev0"
+        fi
         python -m pip install .
     - name: Lint
       run: |
diff --git a/nitime/algorithms/tests/test_spectral.py b/nitime/algorithms/tests/test_spectral.py
index 6c00a58..20d95df 100644
--- a/nitime/algorithms/tests/test_spectral.py
+++ b/nitime/algorithms/tests/test_spectral.py
@@ -246,8 +246,7 @@ def test_mtm_lin_combo():
         mtm_cross = tsa.mtm_cross_spectrum(
             spec1, spec2, (weights[0], weights[1]), sides=sides
             )
-        npt.assert_(mtm_cross.dtype in np.sctypes['complex'],
-               'Wrong dtype for crossspectrum')
+        assert mtm_cross.dtype == np.complex128, 'Wrong dtype for crossspectrum'
         npt.assert_(len(mtm_cross) == 51,
                'Wrong length for halfband spectrum')
         sides = 'twosided'
@@ -260,8 +259,7 @@ def test_mtm_lin_combo():
         mtm_auto = tsa.mtm_cross_spectrum(
             spec1, spec1, weights[0], sides=sides
             )
-        npt.assert_(mtm_auto.dtype in np.sctypes['float'],
-               'Wrong dtype for autospectrum')
+        assert mtm_auto.dtype == np.float64, 'Wrong dtype for autospectrum'
         npt.assert_(len(mtm_auto) == 51,
                'Wrong length for halfband spectrum')
         sides = 'twosided'
diff --git a/nitime/tests/test_timeseries.py b/nitime/tests/test_timeseries.py
index 7aaeac9..edf05ea 100644
--- a/nitime/tests/test_timeseries.py
+++ b/nitime/tests/test_timeseries.py
@@ -916,16 +916,20 @@ def test_index_int64():
     assert repr(b[0]) == repr(b[np.int32(0)])
 
 
-def test_timearray_math_functions():
+@pytest.mark.parametrize('f', ['min', 'max', 'mean', 'ptp', 'sum'])
+@pytest.mark.parametrize('tu', ['s', 'ms', 'ps', 'D'])
+def test_timearray_math_functions(f, tu):
     "Calling TimeArray.min() .max(), mean() should return TimeArrays"
     a = np.arange(2, 11)
-    for f in ['min', 'max', 'mean', 'ptp', 'sum']:
-        for tu in ['s', 'ms', 'ps', 'D']:
-            b = ts.TimeArray(a, time_unit=tu)
-            npt.assert_(getattr(b, f)().__class__ == ts.TimeArray)
-            npt.assert_(getattr(b, f)().time_unit == b.time_unit)
-            # comparison with unitless should convert to the TimeArray's units
-            npt.assert_(getattr(b, f)() == getattr(a, f)())
+    b = ts.TimeArray(a, time_unit=tu)
+    if f == "ptp" and ts._NP_2:
+        want = np.ptp(a)
+    else:
+        want = getattr(a, f)()
+    npt.assert_(getattr(b, f)().__class__ == ts.TimeArray)
+    npt.assert_(getattr(b, f)().time_unit == b.time_unit)
+    # comparison with unitless should convert to the TimeArray's units
+    npt.assert_(getattr(b, f)() == want)
 
 
 def test_timearray_var_prod():
diff --git a/nitime/tests/test_utils.py b/nitime/tests/test_utils.py
index 7770227..d31c749 100644
--- a/nitime/tests/test_utils.py
+++ b/nitime/tests/test_utils.py
@@ -230,6 +230,8 @@ def test_detect_lines():
     """
     Tests detect_lines utility in the reliable low-SNR scenario.
     """
+    np.random.seed(0)
+
     N = 1000
     fft_pow = int( np.ceil(np.log2(N) + 2) )
     NW = 4
@@ -286,19 +288,21 @@ def test_detect_lines_2dmode():
     Test multi-sequence operation
     """
 
+    # This seed affects not just the signal we generate below, but then also
+    # detect_lines->dpss_windows->tridi_inverse_iteration
+    np.random.seed(0)
+
     N = 1000
 
     sig = np.cos( 2*np.pi*np.arange(N) * 20./N ) + np.random.randn(N) * .01
 
-    sig2d = np.row_stack( (sig, sig, sig) )
+    sig2d = np.vstack( (sig, sig, sig) )
 
     lines = utils.detect_lines(sig2d, (4, 8), low_bias=True, NFFT=2**12)
 
     npt.assert_(len(lines)==3, 'Detect lines failed multi-sequence mode')
 
-    consistent1 = (lines[0][0] == lines[1][0]).all() and \
-      (lines[1][0] == lines[2][0]).all()
-    consistent2 = (lines[0][1] == lines[1][1]).all() and \
-      (lines[1][1] == lines[2][1]).all()
-
-    npt.assert_(consistent1 and consistent2, 'Inconsistent results')
+    npt.assert_allclose(lines[0][0], lines[1][0])
+    npt.assert_allclose(lines[0][0], lines[2][0])
+    npt.assert_allclose(lines[0][1], lines[1][1])
+    npt.assert_allclose(lines[0][1], lines[2][1])