diff --git a/docs/source/v1.9.md.inc b/docs/source/v1.9.md.inc index 39103483c..dbd33e27c 100644 --- a/docs/source/v1.9.md.inc +++ b/docs/source/v1.9.md.inc @@ -2,15 +2,22 @@ ### :new: New features & enhancements -- Added number of subject to `sub-average` report (#902, #910 by @SophieHerbst) +- Added number of subjects to `sub-average` report (#902, #910 by @SophieHerbst) - The type annotations in the default configuration file are now easier to read: We replaced `Union[X, Y]` with `X | Y` and `Optional[X]` with `X | None`. (#908, #911 by @hoechenberger) +- Added a new configuration option [`spatial_filter_raw`][mne_bids_pipeline._config.spatial_filter_raw] + to control whether to create cleaned raw data via ICA or SSP. Previously, we'd only clean epochs. + (#840, #926 by @larsoner and @hoechenberger) ### :warning: Behavior changes - All ICA HTML reports have been consolidated in the standard subject `*_report.html` file instead of producing separate files (#899 by @larsoner). -- Changed default for `source_info_path_update` to `None`. In `_04_make_forward.py` +- When using ICA or SSP with resting-state data, we now automatically produce cleaned raw data files. This + behavior can be controlled via the new [`spatial_filter_raw`][mne_bids_pipeline._config.spatial_filter_raw] + configuration option. (#840, #926 by @larsoner and @hoechenberger) +- Changed default for [`source_info_path_update`][mne_bids_pipeline._config.source_info_path_update] + to `None`. In `_04_make_forward.py` and `_05_make_inverse.py`, we retrieve the info from the file from which the `noise_cov` is computed (#919 by @SophieHerbst) - The [`depth`][mne_bids_pipeline._config.depth] parameter doesn't accept `None` diff --git a/mne_bids_pipeline/_config.py b/mne_bids_pipeline/_config.py index 912d257f0..ee43fff95 100644 --- a/mne_bids_pipeline/_config.py +++ b/mne_bids_pipeline/_config.py @@ -1138,6 +1138,13 @@ ICA fitting – this file will need to be updated! """ +spatial_filter_raw: bool | None = None +""" +Whether to clean the raw data with the spatial filter. If `True`, creates cleaned data +for all raw data. If `False`, no raw data is written. If `None`, creates +cleaned raw data for resting-state and empty-room runs if present. +""" + min_ecg_epochs: Annotated[int, Ge(1)] = 5 """ Minimal number of ECG epochs needed to compute SSP projectors. diff --git a/mne_bids_pipeline/_config_utils.py b/mne_bids_pipeline/_config_utils.py index 46990a623..70b0f1b5f 100644 --- a/mne_bids_pipeline/_config_utils.py +++ b/mne_bids_pipeline/_config_utils.py @@ -658,3 +658,12 @@ def _proj_path( suffix="proj", check=False, ) + + +def _which_spatial_filter_raw(*, config: SimpleNamespace) -> tuple[str]: + if config.spatial_filter_raw: + return ("runs", "noise", "rest") + elif config.spatial_filter_raw is None: + return ("noise", "rest") + else: + return () diff --git a/mne_bids_pipeline/_docs.py b/mne_bids_pipeline/_docs.py index 440b34690..759d5e2aa 100644 --- a/mne_bids_pipeline/_docs.py +++ b/mne_bids_pipeline/_docs.py @@ -100,6 +100,7 @@ "_bids_kwargs": ("get_task",), "_import_data_kwargs": ("get_mf_reference_run",), "get_runs": ("get_runs_all_subjects",), + "_which_spatial_filter_raw": ("spatial_filter_raw",), } @@ -144,6 +145,10 @@ def __init__(self, force_empty=None): for call in ast.walk(func): if not isinstance(call, ast.Call): continue + # e.g., which = _which_spatial_filter_raw(config=config) + if isinstance(call.func, ast.Name): + for attr in _EXTRA_FUNCS.get(call.func.id, ()): + self._add_step_option(step, attr) for keyword in call.keywords: if not isinstance(keyword.value, ast.Attribute): continue @@ -226,7 +231,7 @@ def __init__(self, force_empty=None): if isinstance(keyword.value, ast.Name): key = f"{where}:{keyword.value.id}" if key in _MANUAL_KWS: - for option in _MANUAL_KWS[f"{where}:{keyword.value.id}"]: + for option in _MANUAL_KWS[key]: self._add_step_option(step, option) continue raise RuntimeError(f"{where} cannot handle Name {key=}") diff --git a/mne_bids_pipeline/steps/preprocessing/_08a_apply_ica.py b/mne_bids_pipeline/steps/preprocessing/_08a_apply_ica.py index 5b097c106..0e6ab9b0a 100644 --- a/mne_bids_pipeline/steps/preprocessing/_08a_apply_ica.py +++ b/mne_bids_pipeline/steps/preprocessing/_08a_apply_ica.py @@ -13,6 +13,7 @@ from mne_bids import BIDSPath from ..._config_utils import ( + _which_spatial_filter_raw, get_runs_tasks, get_sessions, get_subjects, @@ -276,6 +277,7 @@ def main(*, config: SimpleNamespace) -> None: parallel, run_func = parallel_func( apply_ica_raw, exec_params=config.exec_params ) + which = _which_spatial_filter_raw(config=config) logs += parallel( run_func( cfg=get_config( @@ -294,6 +296,7 @@ def main(*, config: SimpleNamespace) -> None: config=config, subject=subject, session=session, + which=which, ) ) save_logs(config=config, logs=logs) diff --git a/mne_bids_pipeline/steps/preprocessing/_08b_apply_ssp.py b/mne_bids_pipeline/steps/preprocessing/_08b_apply_ssp.py index ee2c56cb9..38ae2ca4f 100644 --- a/mne_bids_pipeline/steps/preprocessing/_08b_apply_ssp.py +++ b/mne_bids_pipeline/steps/preprocessing/_08b_apply_ssp.py @@ -10,6 +10,7 @@ from ..._config_utils import ( _proj_path, + _which_spatial_filter_raw, get_runs_tasks, get_sessions, get_subjects, @@ -181,6 +182,7 @@ def main(*, config: SimpleNamespace) -> None: parallel, run_func = parallel_func( apply_ssp_raw, exec_params=config.exec_params ) + which = _which_spatial_filter_raw(config=config) logs += parallel( run_func( cfg=get_config( @@ -199,6 +201,7 @@ def main(*, config: SimpleNamespace) -> None: config=config, subject=subject, session=session, + which=which, ) ) save_logs(config=config, logs=logs)