diff --git a/.github/actions/get-testing-dataset/action.yaml b/.github/actions/get-testing-dataset/action.yaml new file mode 100644 index 000000000..523239a02 --- /dev/null +++ b/.github/actions/get-testing-dataset/action.yaml @@ -0,0 +1,44 @@ +name: "Get MNE-LSL testing dataset" +description: "A composite action to get MNE-LSL testing dataset from cache or remote." +inputs: + sample: + description: "If True, retrieve the sample dataset." + required: false + default: "false" + testing: + description: "If True, retrieve the testing dataset." + required: false + default: "true" + +runs: + using: "composite" + steps: + - name: Get dataset version file + shell: bash + run: | + curl https://raw.githubusercontent.com/mscheltienne/mne-lsl-datasets/main/version.txt -o mne_lsl_dataset_version.txt + - name: Cache testing dataset + if: ${{ inputs.testing == 'true' }} + id: cache_testing + uses: actions/cache@v4 + with: + key: mne-lsl-testing-${{ runner.os }}-${{ hashFiles('mne_lsl_dataset_version.txt') }} + path: ~/mne_data/MNE-LSL-data/testing + - name: Download testing dataset + if: ${{ inputs.testing == 'true' && steps.cache_testing.outputs.cache-hit != 'true' }} + shell: bash + run: python -c "import mne_lsl; mne_lsl.datasets.testing.data_path()" + - name: Cache sample dataset + if: ${{ inputs.sample == 'true' }} + id: cache_sample + uses: actions/cache@v4 + with: + key: mne-lsl-sample-${{ runner.os }}-${{ hashFiles('mne_lsl_dataset_version.txt') }} + path: ~/mne_data/MNE-LSL-data/sample + - name: Download sample dataset + if: ${{ inputs.sample == 'true' && steps.cache_sample.outputs.cache-hit != 'true' }} + shell: bash + run: python -c "import mne_lsl; mne_lsl.datasets.sample.data_path()" + - name: Remove dataset version file + shell: bash + run: rm mne_lsl_dataset_version.txt diff --git a/.github/actions/install-system-dependencies/action.yaml b/.github/actions/install-system-dependencies/action.yaml new file mode 100644 index 000000000..edb569ec5 --- /dev/null +++ b/.github/actions/install-system-dependencies/action.yaml @@ -0,0 +1,37 @@ +name: "Install system dependencies" +description: "A composite action to install liblsl and system dependencies on different operating systems." + +runs: + using: "composite" + steps: + - name: Install linux dependencies + if: ${{ runner.os == 'Linux' }} + shell: bash + run: | + sudo apt update + sudo apt install -y binutils libpugixml-dev qtbase5-dev qt5-qmake + - name: Install liblsl (linux) + if: ${{ runner.os == 'Linux' }} + shell: bash + run: | + curl -L https://github.com/sccn/liblsl/releases/download/v1.16.2/liblsl-1.16.2-jammy_amd64.deb -o liblsl-1.16.2-jammy_amd64.deb + sudo apt install -y ./liblsl-1.16.2-jammy_amd64.deb + rm liblsl-1.16.2-jammy_amd64.deb + - name: Install liblsl (macOS) + if: ${{ runner.os == 'macOS' }} + shell: bash + run: | + curl -L https://github.com/sccn/liblsl/releases/download/v1.16.0/liblsl-1.16.0-OSX_arm64.tar.bz2 -o liblsl-1.16.0-OSX_arm64.tar.bz2 + tar -xf liblsl-1.16.0-OSX_arm64.tar.bz2 + mv lib/liblsl.1.16.0.dylib . + echo "MNE_LSL_LIB=$PWD/liblsl.1.16.0.dylib" >> $GITHUB_ENV + rm -R lib include bin + rm liblsl-1.16.0-OSX_arm64.tar.bz2 + - name: Install liblsl (windows) + if: ${{ runner.os == 'Windows' }} + shell: bash + run: | + curl -L https://github.com/sccn/liblsl/releases/download/v1.16.2/liblsl-1.16.2-Win_amd64.zip -o liblsl-1.16.2-Win_amd64.zip + 7z x -oliblsl liblsl-1.16.2-Win_amd64.zip + echo "MNE_LSL_LIB=$PWD/liblsl/bin/lsl.dll" >> $GITHUB_ENV + rm liblsl-1.16.2-Win_amd64.zip diff --git a/.github/workflows/doc.yaml b/.github/workflows/doc.yaml index d8542c15d..909362fe7 100644 --- a/.github/workflows/doc.yaml +++ b/.github/workflows/doc.yaml @@ -22,25 +22,23 @@ jobs: uses: actions/setup-python@v5 with: python-version: 3.11 - - name: Install linux dependencies - run: | - curl -L https://github.com/sccn/liblsl/releases/download/v1.16.2/liblsl-1.16.2-jammy_amd64.deb -o liblsl-1.16.2-jammy_amd64.deb - sudo apt update - sudo apt install -y libpugixml-dev qtbase5-dev qt5-qmake optipng - sudo apt install -y ./liblsl-1.16.2-jammy_amd64.deb - rm liblsl-1.16.2-jammy_amd64.deb + - name: Install system dependencies + uses: ./.github/actions/install-system-dependencies + - name: Install optipng (for sphinx-gallery) + run: sudo apt install optipng - name: Install package run: | python -m pip install --progress-bar off --upgrade pip setuptools python -m pip install --progress-bar off .[doc] - name: Display system information run: mne_lsl-sys_info --developer - - name: Build doc - uses: nick-fields/retry@v3 + - name: Get dataset + uses: ./.github/actions/get-testing-dataset with: - timeout_minutes: 10 - max_attempts: 3 - command: make -C doc html + sample: "true" + testing: "false" + - name: Build doc + run: make -C doc html - name: Prune sphinx environment run: rm -R ./doc/_build/html/.doctrees - name: Upload documentation diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 9a3865571..2f265cccc 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -15,14 +15,9 @@ jobs: uses: actions/setup-python@v5 with: python-version: 3.11 - - name: Install linux dependencies - run: | - curl -L https://github.com/sccn/liblsl/releases/download/v1.16.2/liblsl-1.16.2-jammy_amd64.deb -o liblsl-1.16.2-jammy_amd64.deb - sudo apt update - sudo apt install -y libpugixml-dev qtbase5-dev qt5-qmake optipng - sudo apt install -y ./liblsl-1.16.2-jammy_amd64.deb - rm liblsl-1.16.2-jammy_amd64.deb - - name: Install dependencies + - name: Install system dependencies + uses: ./.github/actions/install-system-dependencies + - name: Install package run: | python -m pip install --progress-bar off --upgrade pip setuptools python -m pip install --progress-bar off -e .[build,stubs] diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index 9fe6f7bf3..1d19ce688 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -28,36 +28,16 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - name: Install liblsl (linux) - if: ${{ matrix.os == 'ubuntu' }} - run: | - curl -L https://github.com/sccn/liblsl/releases/download/v1.16.2/liblsl-1.16.2-jammy_amd64.deb -o liblsl-1.16.2-jammy_amd64.deb - sudo apt update - sudo apt install -y binutils libpugixml-dev qtbase5-dev qt5-qmake - sudo apt install -y ./liblsl-1.16.2-jammy_amd64.deb - rm liblsl-1.16.2-jammy_amd64.deb - - name: Install liblsl (macOS) - if: ${{ matrix.os == 'macos' }} - run: | - curl -L https://github.com/sccn/liblsl/releases/download/v1.16.0/liblsl-1.16.0-OSX_arm64.tar.bz2 -o liblsl-1.16.0-OSX_arm64.tar.bz2 - tar -xf liblsl-1.16.0-OSX_arm64.tar.bz2 - mv lib/liblsl.1.16.0.dylib . - echo "MNE_LSL_LIB=$PWD/liblsl.1.16.0.dylib" >> $GITHUB_ENV - rm -R lib include bin - rm liblsl-1.16.0-OSX_arm64.tar.bz2 - - name: Install liblsl (windows) - if: ${{ matrix.os == 'windows' }} - run: | - curl -L https://github.com/sccn/liblsl/releases/download/v1.16.2/liblsl-1.16.2-Win_amd64.zip -o liblsl-1.16.2-Win_amd64.zip - 7z x -oliblsl liblsl-1.16.2-Win_amd64.zip - echo "MNE_LSL_LIB=$PWD/liblsl/bin/lsl.dll" >> $GITHUB_ENV - rm liblsl-1.16.2-Win_amd64.zip - - name: Install dependencies + - name: Install system dependencies + uses: ./.github/actions/install-system-dependencies + - name: Install package run: | python -m pip install --progress-bar off --upgrade pip setuptools python -m pip install --progress-bar off .[test] - name: Display system information run: mne_lsl-sys_info --developer + - name: Get testing dataset + uses: ./.github/actions/get-testing-dataset - name: Run pytest run: pytest mne_lsl --cov=mne_lsl --cov-report=xml --cov-config=pyproject.toml -s env: @@ -90,13 +70,9 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - name: Install liblsl & linux dependencies - run: | - curl -L https://github.com/sccn/liblsl/releases/download/v1.16.2/liblsl-1.16.2-jammy_amd64.deb -o liblsl-1.16.2-jammy_amd64.deb - sudo apt update - sudo apt install -y binutils libpugixml-dev qtbase5-dev qt5-qmake - sudo apt install -y ./liblsl-1.16.2-jammy_amd64.deb - - name: Install dependencies + - name: Install system dependencies + uses: ./.github/actions/install-system-dependencies + - name: Install package run: | python -m pip install --progress-bar off --upgrade pip setuptools python -m pip install --progress-bar off .[test] @@ -106,6 +82,8 @@ jobs: python -m pip install --progress-bar off --upgrade --pre --only-binary :all: -i https://pypi.anaconda.org/scientific-python-nightly-wheels/simple --timeout=180 numpy scipy - name: Display system information run: mne_lsl-sys_info --developer + - name: Get testing dataset + uses: ./.github/actions/get-testing-dataset - name: Run pytest run: pytest mne_lsl --cov=mne_lsl --cov-report=xml --cov-config=pyproject.toml -s env: @@ -139,14 +117,9 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - name: Install liblsl (linux) - run: | - curl -L https://github.com/sccn/liblsl/releases/download/v1.16.2/liblsl-1.16.2-jammy_amd64.deb -o liblsl-1.16.2-jammy_amd64.deb - sudo apt update - sudo apt install -y binutils libpugixml-dev qtbase5-dev qt5-qmake - sudo apt install -y ./liblsl-1.16.2-jammy_amd64.deb - rm liblsl-1.16.2-jammy_amd64.deb - - name: Install dependencies + - name: Install system dependencies + uses: ./.github/actions/install-system-dependencies + - name: Install package run: | python -m pip install --progress-bar off --upgrade pip setuptools python -m pip install --progress-bar off .[test] @@ -154,6 +127,8 @@ jobs: python -m pip install --progress-bar off mne==${{ matrix.mne-version }} - name: Display system information run: mne_lsl-sys_info --developer + - name: Get testing dataset + uses: ./.github/actions/get-testing-dataset - name: Run pytest run: pytest mne_lsl --cov=mne_lsl --cov-report=xml --cov-config=pyproject.toml -s env: diff --git a/.github/workflows/stubs.yaml b/.github/workflows/stubs.yaml index 07e48a621..18105fc96 100644 --- a/.github/workflows/stubs.yaml +++ b/.github/workflows/stubs.yaml @@ -22,13 +22,8 @@ jobs: uses: actions/setup-python@v5 with: python-version: 3.11 - - name: Install linux dependencies - run: | - curl -L https://github.com/sccn/liblsl/releases/download/v1.16.2/liblsl-1.16.2-jammy_amd64.deb -o liblsl-1.16.2-jammy_amd64.deb - sudo apt update - sudo apt install -y libpugixml-dev qtbase5-dev qt5-qmake - sudo apt install -y ./liblsl-1.16.2-jammy_amd64.deb - rm liblsl-1.16.2-jammy_amd64.deb + - name: Install system dependencies + uses: ./.github/actions/install-system-dependencies - name: Install package run: | python -m pip install --progress-bar off --upgrade pip setuptools diff --git a/doc/_static/tutorials/qrs-detector-performance.png b/doc/_static/tutorials/qrs-detector-performance.png index 845ffd7c5..82e3b4ed5 100644 Binary files a/doc/_static/tutorials/qrs-detector-performance.png and b/doc/_static/tutorials/qrs-detector-performance.png differ diff --git a/examples/10_player_separate_process.py b/examples/00_player_separate_process.py similarity index 100% rename from examples/10_player_separate_process.py rename to examples/00_player_separate_process.py diff --git a/examples/00_peak_detection.py b/examples/10_peak_detection.py similarity index 98% rename from examples/00_peak_detection.py rename to examples/10_peak_detection.py index 8fa0e46ff..9f0d5fed4 100644 --- a/examples/00_peak_detection.py +++ b/examples/10_peak_detection.py @@ -288,6 +288,7 @@ def __init__( self._stream.notch_filter(100, picks=ch_name) sleep(bufsize) # prefill an entire buffer # peak detection settings + self._last_acq_time = None self._last_peak = None self._peak_candidates = None self._peak_candidates_count = None @@ -301,6 +302,11 @@ def detect_peaks(self) -> NDArray[np.float64]: The timestamps of all detected peaks. """ data, ts = self._stream.get_data() # we have a single channel in the stream + if self._last_acq_time is None: + self._last_acq_time = ts[-1] + elif self._last_acq_time == ts[-1]: + self._last_acq_time = ts[-1] + return np.array([]) # nothing new to do data = data.squeeze() peaks, _ = find_peaks( data, diff --git a/mne_lsl/datasets/testing.py b/mne_lsl/datasets/testing.py index f245f026b..28f031bf0 100644 --- a/mne_lsl/datasets/testing.py +++ b/mne_lsl/datasets/testing.py @@ -44,7 +44,7 @@ def data_path() -> Path: path = ( Path(get_config("MNE_DATA", Path.home())).expanduser() / "mne_data" - / "MNE-LSL" + / "MNE-LSL-data" / "testing" ) base_url = "https://github.com/mscheltienne/mne-lsl-datasets/raw/main/testing" diff --git a/mne_lsl/player/player_lsl.py b/mne_lsl/player/player_lsl.py index 982929db0..1290f9370 100644 --- a/mne_lsl/player/player_lsl.py +++ b/mne_lsl/player/player_lsl.py @@ -279,7 +279,7 @@ def _stream(self) -> None: else: self._outlet.push_chunk(data, timestamp=self._target_timestamp) self._stream_annotations(start, stop, start_timestamp) - except Exception as exc: + except Exception as exc: # pragma: no cover logger.error("%s: Stopping due to exception: %s", self._name, exc) self._del_outlets() self._reset_variables() @@ -296,8 +296,8 @@ def _stream(self) -> None: sleep(delay) try: self._executor.submit(self._stream) - except RuntimeError: - assert self._executor._shutdown # pragma: no cover + except RuntimeError: # pragma: no cover + pass # shutdown def _stream_annotations( self, start: int, stop: int, start_timestamp: float diff --git a/mne_lsl/player/tests/test_player_lsl.py b/mne_lsl/player/tests/test_player_lsl.py index 41a8ecfb4..2127c6c4f 100644 --- a/mne_lsl/player/tests/test_player_lsl.py +++ b/mne_lsl/player/tests/test_player_lsl.py @@ -114,6 +114,7 @@ def test_player_context_manager_raw_annotations(raw_annotations): with Player( raw_annotations, chunk_size=200, name=name, annotations=False ) as player: + assert player.running streams = resolve_streams(timeout=2) assert len(streams) == 1 assert streams[0].name == name @@ -122,6 +123,7 @@ def test_player_context_manager_raw_annotations(raw_annotations): assert len(streams) == 0 with Player(raw_annotations, chunk_size=200, name=name) as player: + assert player.running streams = resolve_streams(timeout=2) assert len(streams) == 2 assert any(stream.name == name for stream in streams) @@ -336,6 +338,7 @@ def test_player_annotations(raw_annotations, close_io): name = "Player-test_player_annotations" annotations = sorted(set(raw_annotations.annotations.description)) player = Player(raw_annotations, chunk_size=200, name=name) + assert f"Player: {name}" in repr(player) assert player.name == name assert player.fname == Path(raw_annotations.filenames[0]) streams = resolve_streams(timeout=0.1) @@ -410,7 +413,10 @@ def test_player_n_repeat(raw): player = Player( raw, chunk_size=200, n_repeat=4, name="Player-test_player_n_repeat-2" ) + assert player.n_repeat == 4 player.start() + assert player.n_repeat == 4 + assert player.running time.sleep((raw.times.size / raw.info["sfreq"]) * 1.8) assert player._executor is not None streams = resolve_streams(timeout=2) diff --git a/mne_lsl/stream/_base.py b/mne_lsl/stream/_base.py index e18ba2bf1..41c93a704 100644 --- a/mne_lsl/stream/_base.py +++ b/mne_lsl/stream/_base.py @@ -567,7 +567,7 @@ def get_data( "The Stream is not connected. Please connect to the stream before " "retrieving data from the buffer." ) - else: + else: # pragma: no cover logger.error( "Something went wrong while retrieving data from a connected " "stream. Please open an issue on GitHub and provide the error " diff --git a/mne_lsl/stream/stream_lsl.py b/mne_lsl/stream/stream_lsl.py index 323c03ac0..4c710b948 100644 --- a/mne_lsl/stream/stream_lsl.py +++ b/mne_lsl/stream/stream_lsl.py @@ -230,8 +230,8 @@ def _acquire(self) -> None: sleep(self._acquisition_delay) try: self._executor.submit(self._acquire) - except RuntimeError: - assert self._executor._shutdown # pragma: no cover + except RuntimeError: # pragma: no cover + pass # shutdown return # interrupt early # process acquisition window @@ -249,8 +249,8 @@ def _acquire(self) -> None: sleep(self._acquisition_delay) try: self._executor.submit(self._acquire) - except RuntimeError: - assert self._executor._shutdown # pragma: no cover + except RuntimeError: # pragma: no cover + pass # shutdown return # interrupt early if len(self._added_channels) != 0: refs = np.zeros( @@ -296,7 +296,7 @@ def _acquire(self) -> None: "argument or consider retrieving new samples more often with " "Stream.get_data()." ) - except Exception as error: + except Exception as error: # pragma: no cover logger.exception(error) self._reset_variables() # disconnects from the stream if os.getenv("MNE_LSL_RAISE_STREAM_ERRORS", "false").lower() == "true": @@ -305,8 +305,8 @@ def _acquire(self) -> None: try: sleep(self._acquisition_delay) self._executor.submit(self._acquire) - except RuntimeError: - assert self._executor._shutdown # pragma: no cover + except RuntimeError: # pragma: no cover + pass # shutdown def _reset_variables(self) -> None: """Reset variables define after connection."""