diff --git a/.circleci/config.yml b/.circleci/config.yml index 049b271ef..b824df64b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -49,7 +49,7 @@ jobs: command: | pip install --upgrade --progress-bar off pip # TODO: Restore https://api.github.com/repos/mne-tools/mne-bids/zipball/main pending https://github.com/mne-tools/mne-bids/pull/1349/files#r1885104885 - pip install --upgrade --progress-bar off "autoreject @ https://api.github.com/repos/autoreject/autoreject/zipball/master" "mne[hdf5] @ git+https://github.com/mne-tools/mne-python@main" "mne-bids[full]" numba + pip install --upgrade --progress-bar off "autoreject @ https://api.github.com/repos/autoreject/autoreject/zipball/master" "mne[hdf5] @ git+https://github.com/mne-tools/mne-python@main" "mne-bids[full] @ git+https://github.com/mne-tools/mne-bids@main" numba pip install -ve .[tests] pip install "PyQt6!=6.6.1" "PyQt6-Qt6!=6.6.1,!=6.6.2,!=6.6.3,!=6.7.0" - run: diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 703c980ef..d4570e352 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -22,7 +22,7 @@ jobs: with: python-version: "3.12" - run: pip install --upgrade pip - - run: pip install -ve .[tests] codespell tomli --only-binary="numpy,scipy,pandas,matplotlib,pyarrow,numexpr" + - run: pip install -ve .[tests] "mne-bids[full] @ git+https://github.com/mne-tools/mne-bids@main" codespell tomli --only-binary="numpy,scipy,pandas,matplotlib,pyarrow,numexpr" - run: make codespell-error - run: pytest mne_bids_pipeline -m "not dataset_test" - uses: codecov/codecov-action@v5 diff --git a/mne_bids_pipeline/_config_import.py b/mne_bids_pipeline/_config_import.py index 2b34f7894..a480c1e25 100644 --- a/mne_bids_pipeline/_config_import.py +++ b/mne_bids_pipeline/_config_import.py @@ -6,12 +6,14 @@ import pathlib from dataclasses import field from functools import partial +from inspect import signature from types import SimpleNamespace from typing import Any import matplotlib import mne import numpy as np +from mne_bids import get_entity_vals from pydantic import BaseModel, ConfigDict, ValidationError from ._logging import gen_log_kwargs, logger @@ -324,6 +326,16 @@ def _check_config(config: SimpleNamespace, config_path: PathLike | None) -> None "Please set process_empty_room = True" ) + if ( + config.allow_missing_sessions + and "ignore_suffixes" not in signature(get_entity_vals).parameters + ): + raise ConfigError( + "You've requested to `allow_missing_sessions`, but this functionality " + "requires a newer version of `mne_bids` than you have available. Please " + "update MNE-BIDS (or if on the latest version, install the dev version)." + ) + bl = config.baseline if bl is not None: if (bl[0] is not None and bl[0] < config.epochs_tmin) or ( diff --git a/mne_bids_pipeline/_config_utils.py b/mne_bids_pipeline/_config_utils.py index 9c2776908..1c4a9a235 100644 --- a/mne_bids_pipeline/_config_utils.py +++ b/mne_bids_pipeline/_config_utils.py @@ -4,6 +4,7 @@ import functools import pathlib from collections.abc import Iterable, Sized +from inspect import signature from types import ModuleType, SimpleNamespace from typing import Any, Literal, TypeVar @@ -142,26 +143,52 @@ def _get_sessions(config: SimpleNamespace) -> tuple[str, ...]: return tuple(str(x) for x in sessions) -def get_subjects_sessions(config: SimpleNamespace) -> dict[str, list[str] | list[None]]: - subj_sessions: dict[str, list[str] | list[None]] = dict() +def get_subjects_sessions( + config: SimpleNamespace, +) -> dict[str, tuple[None] | tuple[str, ...]]: + subjects = get_subjects(config) cfg_sessions = _get_sessions(config) - for subject in get_subjects(config): - # Only traverse through the current subject's directory + # easy case first: datasets that don't have (named) sessions + if not cfg_sessions: + return {subj: (None,) for subj in subjects} + + # find which tasks to ignore when deciding if a subj has data for a session + ignore_datatypes = _get_ignore_datatypes(config) + if config.task == "": + ignore_tasks = None + else: + all_tasks = _get_entity_vals_cached( + root=config.bids_root, + entity_key="task", + ignore_datatypes=ignore_datatypes, + ) + ignore_tasks = tuple(set(all_tasks) - set([config.task])) + + # loop over subjs and check for available sessions + subj_sessions: dict[str, tuple[None] | tuple[str, ...]] = dict() + kwargs = ( + dict(ignore_suffixes=("scans", "coordsystem")) + if "ignore_suffixes" in signature(mne_bids.get_entity_vals).parameters + else dict() + ) + for subject in subjects: valid_sessions_subj = _get_entity_vals_cached( config.bids_root / f"sub-{subject}", entity_key="session", - ignore_datatypes=_get_ignore_datatypes(config), + ignore_tasks=ignore_tasks, + ignore_acquisitions=("calibration", "crosstalk"), + ignore_datatypes=ignore_datatypes, + **kwargs, ) - missing_sessions = set(cfg_sessions) - set(valid_sessions_subj) + missing_sessions = sorted(set(cfg_sessions) - set(valid_sessions_subj)) if missing_sessions and not config.allow_missing_sessions: raise RuntimeError( f"Subject {subject} is missing session{_pl(missing_sessions)} " - f"{tuple(sorted(missing_sessions))}, and " - "`config.allow_missing_sessions` is False" + f"{missing_sessions}, and `config.allow_missing_sessions` is False" ) - subj_sessions[subject] = sorted(set(cfg_sessions) & set(valid_sessions_subj)) - if subj_sessions[subject] == []: - subj_sessions[subject] = [None] + keep_sessions = tuple(sorted(set(cfg_sessions) & set(valid_sessions_subj))) + if len(keep_sessions): + subj_sessions[subject] = keep_sessions return subj_sessions diff --git a/mne_bids_pipeline/tests/test_run.py b/mne_bids_pipeline/tests/test_run.py index b0f34e62d..ee9ceac73 100644 --- a/mne_bids_pipeline/tests/test_run.py +++ b/mne_bids_pipeline/tests/test_run.py @@ -258,7 +258,7 @@ def test_missing_sessions( context = ( nullcontext() if allow_missing_sessions - else pytest.raises(RuntimeError, match=r"Subject 1 is missing session \('b',\)") + else pytest.raises(RuntimeError, match=r"Subject 1 is missing session \['b'\]") ) # run command = [