diff --git a/.gitattributes b/.gitattributes index 5b22c54a..4d1b53ff 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ pyprep/_version.py export-subst +* text=auto diff --git a/.github/workflows/python_build.yml b/.github/workflows/python_build.yml index b14c5ce7..c31ca52e 100644 --- a/.github/workflows/python_build.yml +++ b/.github/workflows/python_build.yml @@ -5,9 +5,6 @@ on: branches: [ main ] pull_request: branches: [ main ] - create: - branches: [ main ] - tags: [ '**' ] schedule: - cron: "0 4 * * MON" @@ -18,7 +15,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - python-version: ["3.11"] + python-version: ["3.12"] env: TZ: Europe/Berlin FORCE_COLOR: true diff --git a/.github/workflows/python_publish.yml b/.github/workflows/python_publish.yml index 3831d5ad..01fcd18a 100644 --- a/.github/workflows/python_publish.yml +++ b/.github/workflows/python_publish.yml @@ -20,7 +20,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: "3.11" + python-version: "3.12" - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/.github/workflows/python_tests.yml b/.github/workflows/python_tests.yml index a22f5960..78ca71d6 100644 --- a/.github/workflows/python_tests.yml +++ b/.github/workflows/python_tests.yml @@ -5,9 +5,6 @@ on: branches: [ main ] pull_request: branches: [ main ] - create: - branches: [ main ] - tags: [ '**' ] schedule: - cron: "0 4 * * MON" @@ -17,13 +14,13 @@ jobs: fail-fast: false matrix: platform: [ubuntu-latest, macos-latest, windows-latest] - python-version: ["3.11"] + python-version: ["3.12"] mne-version: [mne-stable] include: # Test mne development version only on ubuntu - platform: ubuntu-latest - python-version: "3.11" + python-version: "3.12" mne-version: mne-main run-as-extra: true diff --git a/.readthedocs.yml b/.readthedocs.yml index 9dcf6125..fbcc0676 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -8,7 +8,7 @@ version: 2 build: os: ubuntu-22.04 tools: - python: "3.11" + python: "3.12" # Build documentation in the docs/ directory with Sphinx sphinx: diff --git a/README.rst b/README.rst index a27cb61a..9d6633f0 100644 --- a/README.rst +++ b/README.rst @@ -16,7 +16,7 @@ .. image:: https://readthedocs.org/projects/pyprep/badge/?version=latest - :target: http://pyprep.readthedocs.io/en/latest/?badge=latest + :target: https://pyprep.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status @@ -37,8 +37,8 @@ pyprep For documentation, see the: -- `stable documentation `_ -- `latest (development) documentation `_ +- `stable documentation `_ +- `latest (development) documentation `_ .. docs_readme_include_label diff --git a/docs/whats_new.rst b/docs/whats_new.rst index 4bc0696e..f3491c85 100644 --- a/docs/whats_new.rst +++ b/docs/whats_new.rst @@ -219,7 +219,7 @@ Changelog - Initial commit: 2018-04-12 - Miscellaneous changes -.. _Stefan Appelhoff: http://stefanappelhoff.com/ +.. _Stefan Appelhoff: https://stefanappelhoff.com/ .. _Aamna Lawrence: https://github.com/AamnaLawrence .. _Adam Li: https://github.com/adam2392/ .. _Christian O'Reilly: https://github.com/christian-oreilly diff --git a/pyprep/find_noisy_channels.py b/pyprep/find_noisy_channels.py index d849fd22..4dd7e8a4 100644 --- a/pyprep/find_noisy_channels.py +++ b/pyprep/find_noisy_channels.py @@ -1,6 +1,4 @@ """finds bad channels.""" -from copy import copy - import mne import numpy as np from mne.utils import check_random_state, logger @@ -579,7 +577,14 @@ def find_bad_by_ransac( exclude_from_ransac = ( self.bad_by_correlation + self.bad_by_deviation + self.bad_by_dropout ) - rng = copy(self.random_state) if self.matlab_strict else self.random_state + + if self.matlab_strict: + random_state = self.random_state.get_state() + rng = np.random.RandomState() + rng.set_state(random_state) + else: + rng = self.random_state + self.bad_by_ransac, ch_correlations_usable = find_bad_by_ransac( self.EEGFiltered, self.sample_rate, diff --git a/pyprep/removeTrend.py b/pyprep/removeTrend.py index f75af617..1d106781 100644 --- a/pyprep/removeTrend.py +++ b/pyprep/removeTrend.py @@ -170,5 +170,5 @@ def runline(y, n, dn): (np.multiply(np.arange(n + 1, n + npts + 1), a) + b), (npts, 1) ) for i in range(0, len(y_line)): - y[i] = y[i] - y_line[i] + y[i] = y[i] - y_line[i, 0] return y diff --git a/pyprep/utils.py b/pyprep/utils.py index e32bad5c..ae296789 100644 --- a/pyprep/utils.py +++ b/pyprep/utils.py @@ -76,7 +76,7 @@ def _mat_quantile(arr, q, axis=None): # Sort the array in ascending order along the given axis (any NaNs go to the end) # Return NaN if array is empty. if len(arr) == 0: - return np.NaN + return np.nan arr_sorted = np.sort(arr, axis=axis) # Ensure array is either 1D or 2D @@ -182,7 +182,7 @@ def _eeglab_create_highpass(cutoff, srate): N = order + 1 filt = np.zeros(N) filt[N // 2] = 1 - filt -= firwin(N, transition, window="hamming", nyq=1) + filt -= firwin(N, transition, window="hamming") return filt @@ -215,7 +215,7 @@ def _eeglab_fir_filter(data, filt): pad_len = min(group_delay, n_samples) # Prepare initial state of filter, using padding at start of data - start_pad_idx = np.zeros(pad_len, dtype=np.uint8) + start_pad_idx = np.zeros(pad_len, dtype=np.uint) start_padded = np.concatenate((data[:, start_pad_idx], data[:, :pad_len]), axis=1) zi_init = lfilter_zi(filt, 1) * np.take(start_padded, [0], axis=0) _, zi = lfilter(filt, 1, start_padded, axis=1, zi=zi_init) @@ -232,7 +232,7 @@ def _eeglab_fir_filter(data, filt): ) # Finish filtering data, using padding at end to calculate final values - end_pad_idx = np.zeros(pad_len, dtype=np.uint8) + (n_samples - 1) + end_pad_idx = np.zeros(pad_len, dtype=np.uint) + (n_samples - 1) end, _ = lfilter(filt, 1, data[:, end_pad_idx], axis=1, zi=zi) out[:, (n_samples - pad_len) :] = end[:, (group_delay - pad_len) :] diff --git a/tests/conftest.py b/tests/conftest.py index 131691bd..5edbf334 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -77,7 +77,7 @@ def make_random_mne_object( n_freq_comps=5, freq_range=[10, 60], scale=1e-6, - RNG=np.random.RandomState(1337), + rng=np.random.default_rng(1337), ): """Make a random MNE object to use for testing. @@ -98,8 +98,9 @@ def make_random_mne_object( scale : float Scaling factor applied to the signal in volts. For example 1e-6 to get microvolts. - RNG : np.random.RandomState - Random state seed. + rng : np.random.Generator + The random number generator object. Must be created with + ``np.random.default_rng``. Returns ------- @@ -120,8 +121,8 @@ def make_random_mne_object( high = freq_range[1] for chan in range(n_chans): # Each channel signal is a sum of random freq sine waves - for freq_i in range(n_freq_comps): - freq = RNG.randint(low, high, signal_len) + for _ in range(n_freq_comps): + freq = rng.integers(low, high, signal_len) signal[chan, :] += np.sin(2 * np.pi * times * freq) signal *= scale # scale diff --git a/tests/test_find_noisy_channels.py b/tests/test_find_noisy_channels.py index 315aa421..e29b9f16 100644 --- a/tests/test_find_noisy_channels.py +++ b/tests/test_find_noisy_channels.py @@ -1,7 +1,6 @@ """Test the find_noisy_channels module.""" import numpy as np import pytest -from numpy.random import RandomState from pyprep.find_noisy_channels import NoisyChannels from pyprep.ransac import find_bad_by_ransac @@ -9,7 +8,7 @@ # Set a fixed random seed for reproducible test results -RNG = RandomState(30) +rng = np.random.default_rng(30) # Define some fixtures and utility functions for use across multiple tests @@ -47,7 +46,7 @@ def raw_tmp(raw_clean_detrend): def _generate_signal(fmin, fmax, timepoints, fcount=1): """Generate an EEG signal from one or more sine waves in a frequency range.""" signal = np.zeros_like(timepoints) - for freq in RNG.randint(fmin, fmax + 1, fcount): + for freq in rng.integers(fmin, fmax + 1, fcount): signal += np.sin(2 * np.pi * timepoints * freq) return signal * 1e-6 @@ -59,7 +58,7 @@ def test_bad_by_nan(raw_tmp): """Test the detection of channels containing any NaN values.""" # Insert a NaN value into a random channel n_chans = raw_tmp.get_data().shape[0] - nan_idx = int(RNG.randint(0, n_chans, 1)) + nan_idx = int(rng.integers(0, n_chans, 1)[0]) raw_tmp._data[nan_idx, 3] = np.nan # Test automatic detection of NaN channels on NoisyChannels init @@ -75,7 +74,7 @@ def test_bad_by_flat(raw_tmp): """Test the detection of channels with flat or very weak signals.""" # Make the signal for a random channel extremely weak n_chans = raw_tmp.get_data().shape[0] - flat_idx = int(RNG.randint(0, n_chans, 1)) + flat_idx = int(rng.integers(0, n_chans, 1)[0]) raw_tmp._data[flat_idx, :] = raw_tmp.get_data()[flat_idx, :] * 1e-12 # Test automatic detection of flat channels on NoisyChannels init @@ -100,7 +99,7 @@ def test_bad_by_deviation(raw_tmp): # Make the signal for a random channel have a very high amplitude n_chans = raw_tmp.get_data().shape[0] - high_dev_idx = int(RNG.randint(0, n_chans, 1)) + high_dev_idx = int(rng.integers(0, n_chans, 1)[0]) raw_tmp._data[high_dev_idx, :] *= high_dev_factor # Test detection of abnormally high-amplitude channels @@ -126,7 +125,7 @@ def test_bad_by_hf_noise(raw_tmp): """Test detection of channels with high-frequency noise.""" # Add some noise between 70 & 80 Hz to the signal of a random channel n_chans = raw_tmp.get_data().shape[0] - hf_noise_idx = int(RNG.randint(0, n_chans, 1)) + hf_noise_idx = int(rng.integers(0, n_chans, 1)[0]) hf_noise = _generate_signal(70, 80, raw_tmp.times, 5) * 10 raw_tmp._data[hf_noise_idx, :] += hf_noise @@ -148,7 +147,7 @@ def test_bad_by_dropout(raw_tmp): """Test detection of channels with excessive portions of flat signal.""" # Add large dropout portions to the signal of a random channel n_chans, n_samples = raw_tmp.get_data().shape - dropout_idx = int(RNG.randint(0, n_chans, 1)) + dropout_idx = int(rng.integers(0, n_chans, 1)[0]) x1, x2 = (int(n_samples / 10), int(2 * n_samples / 10)) raw_tmp._data[dropout_idx, x1:x2] = 0 # flatten 10% of signal @@ -162,7 +161,7 @@ def test_bad_by_correlation(raw_tmp): """Test detection of channels that correlate poorly with others.""" # Replace a random channel's signal with uncorrelated values n_chans, n_samples = raw_tmp.get_data().shape - low_corr_idx = int(RNG.randint(0, n_chans, 1)) + low_corr_idx = int(rng.integers(0, n_chans, 1)[0]) raw_tmp._data[low_corr_idx, :] = _generate_signal(10, 30, raw_tmp.times, 5) # Test detection of channels that correlate poorly with others @@ -187,7 +186,7 @@ def test_bad_by_SNR(raw_tmp): """Test detection of channels that have low signal-to-noise ratios.""" # Replace a random channel's signal with uncorrelated values n_chans = raw_tmp.get_data().shape[0] - low_snr_idx = int(RNG.randint(0, n_chans, 1)) + low_snr_idx = int(rng.integers(0, n_chans, 1)[0]) raw_tmp._data[low_snr_idx, :] = _generate_signal(10, 30, raw_tmp.times, 5) # Add some high-frequency noise to the uncorrelated channel @@ -203,7 +202,7 @@ def test_bad_by_SNR(raw_tmp): def test_find_bad_by_ransac(raw_tmp): """Test the RANSAC component of NoisyChannels.""" # Set a consistent random seed for all RANSAC runs - RANSAC_RNG = 435656 + ransac_rng = 435656 # RANSAC identifies channels that go bad together and are highly correlated. # Inserting highly correlated signal in channels 0 through 6 at 30 Hz @@ -222,7 +221,7 @@ def test_find_bad_by_ransac(raw_tmp): corr = {} for name, args in test_matrix.items(): nd = NoisyChannels( - raw_tmp, do_detrend=False, random_state=RANSAC_RNG, matlab_strict=args[0] + raw_tmp, do_detrend=False, random_state=ransac_rng, matlab_strict=args[0] ) nd.find_bad_by_ransac(channel_wise=args[1], max_chunk_size=args[2]) # Save bad channels and RANSAC correlation matrix for later comparison @@ -247,7 +246,7 @@ def test_find_bad_by_ransac(raw_tmp): assert not np.allclose(corr["by_window"], corr["by_window_strict"]) # Ensure that RANSAC doesn't change random state if in MATLAB-strict mode - rng = RandomState(RANSAC_RNG) + rng = np.random.RandomState(ransac_rng) init_state = rng.get_state()[2] nd = NoisyChannels(raw_tmp, do_detrend=False, random_state=rng, matlab_strict=True) nd.find_bad_by_ransac() diff --git a/tests/test_prep_pipeline.py b/tests/test_prep_pipeline.py index b3f854fa..25acfc88 100644 --- a/tests/test_prep_pipeline.py +++ b/tests/test_prep_pipeline.py @@ -223,7 +223,7 @@ def test_prep_pipeline_non_eeg(raw, montage): ch_types_non_eeg, times, sfreq, - RNG=np.random.RandomState(1337), + rng=np.random.default_rng(1337), ) raw_copy.add_channels([raw_non_eeg], force_update_info=True) diff --git a/tests/test_utils.py b/tests/test_utils.py index 65039f6d..03ba2eea 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,7 +1,6 @@ """Test various helper functions.""" import numpy as np import pytest -from numpy.random import RandomState from pyprep.utils import ( _correlate_arrays, @@ -64,7 +63,7 @@ def test_mat_quantile_iqr(): # Add NaNs to test data tst_nan = tst.copy() - tst_nan[0, :] = np.NaN + tst_nan[0, :] = np.nan # Create arrays containing MATLAB results for NaN test case quantile_expected = np.asarray([0.9712, 0.9880, 0.9807]) @@ -91,7 +90,7 @@ def test_mat_quantile_iqr(): def test_get_random_subset(): """Test the function for getting random channel subsets.""" # Generate test data - rng = RandomState(435656) + rng = np.random.RandomState(435656) chans = range(1, 61) # Compare random subset equivalence with MATLAB