Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: Add copy-and-skip for EEG-only data #348

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions doc/overview.rst
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ subject_run_indices : list of array-like | dict | None
(must be same length as ``params.subjects``) or a dict (keys are subject
strings, values are the run indices) where missing subjects get all runs.
None is an alias for "all runs".
fix_eeg_order : bool | dict
Whether or not to fix the EEG order for a given subject (dict) or all
subjects (bool). Default is True, which is appropriate for use with the
mis-ordered Neuromag caps.

2. do_score
-----------
Expand Down Expand Up @@ -318,6 +322,13 @@ fir_window : str
See :func:`mne.filter.create_filter`.
phase : str
See :func:`mne.filter.create_filter`.
notch_filter : None | dict
If None (default), perform no FIR-based notch filtering (MEG data will
by default still have a notch filter applied during the Maxwell filtering
step). If ``dict``, arguments are passed directly to
:meth:`mne.io.Raw.notch_filter`, e.g.::

params.notch_filter = dict(freqs=[60], notch_widths=1, trans_bandwidth=0.5)

.. _preprocessing_auto_bads:

Expand Down Expand Up @@ -383,6 +394,10 @@ ssp_eog_reject : dict | None
ssp_ecg_reject : dict | None
Amplitude rejection criteria for ECG SSP computation. None will
use the mne-python default.
ssp_eog_baseline : None | tuple
The baseline to use when constructing EOG epochs (default None).
ssp_ecg_baseline : None | tuple
The baseline to use when constructing ECG epochs (default None).
eog_channel : str | dict | None
The channel to use to detect blink events. None will use EOG* channels.
In lieu of an EOG recording, MEG1411 may work.
Expand Down Expand Up @@ -564,7 +579,10 @@ force_erm_cov_rank_full : bool
If True, force the ERM cov to be full rank.
Usually not needed, but might help when the empty-room data
is short and/or there are a lot of head movements.

erm_cov_from_task : bool
If True (default False), use the task runs to compute the "empty-room"
covariance rather than any empty-room run(s). This should only be used
with EEG data, and should be interpreted with caution.

9. gen_fwd
----------
Expand Down Expand Up @@ -625,8 +643,11 @@ good_hpi_count : bool
Number of good HPI coils (default True).
head_movement : bool
Head movement (default True).
raw_segments : bool
10 evenly spaced raw data segments (default True).
raw_segments : bool | dict
10 evenly spaced raw data segments (default True). Can also be a dict
of keyword arguments to pass to :meth:`mne.Epochs.plot`, e.g.::

raw_segments=dict(scalings=dict(eeg=100e-6))
psd : bool
Raw PSDs, often slow (default True).
ssp_topomaps : bool
Expand Down
37 changes: 19 additions & 18 deletions mnefun/_cov.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
from mne.viz import plot_cov

from ._epoching import _concat_resamp_raws
from ._paths import get_epochs_evokeds_fnames, get_raw_fnames, safe_inserter
from ._paths import (get_epochs_evokeds_fnames, get_raw_fnames, safe_inserter,
get_erm_cov_fname)
from ._scoring import _read_events
from ._utils import (get_args, _get_baseline, _restrict_reject_flat,
_handle_dict, _handle_decim)
Expand Down Expand Up @@ -72,11 +73,9 @@ def gen_covariances(p, subjects, run_indices, decim):
for si, subj in enumerate(subjects):
print(' Subject %2d/%2d...' % (si + 1, len(subjects)), end='')
cov_dir = op.join(p.work_dir, subj, p.cov_dir)
if not op.isdir(cov_dir):
os.mkdir(cov_dir)
os.makedirs(cov_dir, exist_ok=True)
has_rank_arg = 'rank' in get_args(compute_covariance)
kwargs = dict()
kwargs_erm = dict()
kwargs = dict(method=p.cov_method)
if p.cov_rank == 'full': # backward compat
if has_rank_arg:
kwargs['rank'] = 'full'
Expand All @@ -90,7 +89,7 @@ def gen_covariances(p, subjects, run_indices, decim):
kwargs['rank'] = _compute_rank(p, subj, run_indices[si])
else:
kwargs['rank'] = p.cov_rank
kwargs_erm['rank'] = kwargs['rank']
kwargs_erm = kwargs.copy()
if p.force_erm_cov_rank_full and has_rank_arg:
kwargs_erm['rank'] = 'full'
# Use the same thresholds we used for primary Epochs
Expand All @@ -103,22 +102,25 @@ def gen_covariances(p, subjects, run_indices, decim):
flat = _handle_dict(p.flat, subj)

# Make empty room cov
if p.runs_empty:
if len(p.runs_empty) > 1:
raise ValueError('Too many empty rooms; undefined output!')
new_run = safe_inserter(p.runs_empty[0], subj)
empty_cov_name = op.join(cov_dir, new_run + p.pca_extra +
p.inv_tag + '-cov.fif')
empty_fif = get_raw_fnames(p, subj, 'pca', 'only', False)[0]
raw = read_raw_fif(empty_fif, preload=True)
raw.pick_types(meg=True, eog=True, exclude='bads')
empty_cov_name = get_erm_cov_fname(p, subj)
if empty_cov_name is not None:
if p.erm_cov_from_task:
# read in raw files
raw_fnames = get_raw_fnames(
p, subj, 'pca', False, False, run_indices[si])
raw, ratios = _concat_resamp_raws(p, subj, raw_fnames)
raw.pick_types(meg=True, eeg=True, eog=True, exclude='bads')
else:
empty_fif = get_raw_fnames(p, subj, 'pca', 'only', False)[0]
raw = read_raw_fif(empty_fif, preload=True)
raw.pick_types(meg=True, eog=True, exclude='bads')
use_reject, use_flat = _restrict_reject_flat(reject, flat, raw)
if 'eeg' in use_reject:
del use_reject['eeg']
if 'eeg' in use_flat:
del use_flat['eeg']
cov = compute_raw_covariance(raw, reject=use_reject, flat=use_flat,
method=p.cov_method, **kwargs_erm)
**kwargs_erm)
write_cov(empty_cov_name, cov)

# Make evoked covariances
Expand Down Expand Up @@ -153,8 +155,7 @@ def gen_covariances(p, subjects, run_indices, decim):
on_missing=p.on_missing,
reject_by_annotation=p.reject_epochs_by_annot)
epochs.pick_types(meg=True, eeg=True, exclude=[])
cov = compute_covariance(epochs, method=p.cov_method,
**kwargs)
cov = compute_covariance(epochs, **kwargs)
if kwargs.get('rank', None) not in (None, 'full'):
want_rank = sum(kwargs['rank'].values())
out_rank = compute_whitener(
Expand Down
21 changes: 12 additions & 9 deletions mnefun/_fix.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from mne import pick_types

from ._paths import get_raw_fnames
from ._utils import _handle_dict


_1_ORDER = (1, 2, 3, 5, 6, 7, 9, 10,
Expand Down Expand Up @@ -46,6 +47,7 @@ def fix_eeg_files(p, subjects, structurals=None, dates=None, run_indices=None):
if run_indices is None:
run_indices = [None] * len(subjects)
for si, subj in enumerate(subjects):
fix_eeg = _handle_dict(p.fix_eeg_order, subj)
if p.disp_files:
print(' Fixing subject %g/%g.' % (si + 1, len(subjects)))
raw_names = get_raw_fnames(p, subj, 'sss', True, False,
Expand All @@ -64,10 +66,10 @@ def fix_eeg_files(p, subjects, structurals=None, dates=None, run_indices=None):
birthday=dates[si])
else:
anon = None
fix_eeg_channels(names, anon)
fix_eeg_channels(names, anon, fix_eeg=fix_eeg)


def fix_eeg_channels(raw_files, anon=None, verbose=True):
def fix_eeg_channels(raw_files, anon=None, fix_eeg=True, verbose=True):
"""Reorder EEG channels based on UW cap setup

Parameters
Expand All @@ -88,7 +90,7 @@ def fix_eeg_channels(raw_files, anon=None, verbose=True):
# actually do the reordering
for ri, raw_file in enumerate(raw_files):
need_reorder, need_anon, write_key, anon_key, picks, order = \
_is_file_unfixed(raw_file, anon)
_is_file_unfixed(raw_file, anon, fix_eeg)
if need_anon or need_reorder:
to_do = []
if need_reorder:
Expand Down Expand Up @@ -123,13 +125,14 @@ def fix_eeg_channels(raw_files, anon=None, verbose=True):
print(' File %i already corrected' % (ri + 1))


def _all_files_fixed(p, subj, type_='pca'):
"""Determine if all files have been fixed for a subject"""
return all(op.isfile(fname) and not any(_is_file_unfixed(fname)[:2])
def _all_files_fixed(p, subj, type_='pca', fix_eeg=True):
"""Determine if all files have been fixed for a subject."""
kw = dict(fix_eeg=_handle_dict(p.fix_eeg_order, subj))
return all(op.isfile(fname) and not any(_is_file_unfixed(fname, **kw)[:2])
for fname in get_raw_fnames(p, subj, type_))


def _is_file_unfixed(fname, anon=None):
def _is_file_unfixed(fname, anon=None, fix_eeg=True):
"""Determine if a file needs reordering or anonymization."""
order = np.array(_1_ORDER) - 1
assert len(order) == 60
Expand All @@ -142,7 +145,7 @@ def _is_file_unfixed(fname, anon=None):
raw = read_raw_fif(fname, preload=False, allow_maxshield='yes')
picks = pick_types(raw.info, meg=False, eeg=True, exclude=[])
need_reorder = False
if len(picks) > 0:
if len(picks) > 0 and fix_eeg:
if len(picks) == len(order):
need_reorder = (write_key not in raw.info['description'])
else:
Expand All @@ -152,7 +155,7 @@ def _is_file_unfixed(fname, anon=None):
warnings.warn(msg)
else:
raise RuntimeError(msg)
need_anon = (anon_key not in raw.info['description'])
need_anon = (anon_key not in (raw.info['description'] or ''))
return need_reorder, need_anon, write_key, anon_key, picks, order


Expand Down
8 changes: 4 additions & 4 deletions mnefun/_forward.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ def gen_forwards(p, subjects, structurals, run_indices):
if not getattr(p, 'translate_positions', True):
raise RuntimeError('Not translating positions is no longer '
'supported')
print(' Creating forward solution(s) using a %s for %s...'
% (bem_type, subj), end='')
print(f' Creating forward solution(s) using a {bem_type} for '
f'{subj} ({struc})...', end='')
# XXX Don't actually need to generate a different fwd for each inv
# anymore, since all runs are included, but changing the filename
# would break a lot of existing pipelines :(
Expand Down Expand Up @@ -110,8 +110,8 @@ def _get_bem_src_trans(p, info, subj, struc):
if op.isfile(src_space_file):
break
else: # if neither exists, use last filename
print(' Creating %s%s source space for %s...'
% (kind, num, subj))
print(f' Creating {kind}{num} source space for {subj} '
f'({struc})...')
if kind == 'oct':
src = setup_source_space(
struc, spacing='%s%s' % (kind, num),
Expand Down
13 changes: 8 additions & 5 deletions mnefun/_inverse.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

from ._cov import _compute_rank
from ._paths import (get_epochs_evokeds_fnames, safe_inserter,
get_cov_fwd_inv_fnames)
get_cov_fwd_inv_fnames, get_erm_cov_fname)

try:
from mne import spatial_src_adjacency
Expand Down Expand Up @@ -46,7 +46,6 @@ def gen_inverses(p, subjects, run_indices):
cov_dir = op.join(p.work_dir, subj, p.cov_dir)
if not op.isdir(inv_dir):
os.mkdir(inv_dir)
make_erm_inv = len(p.runs_empty) > 0

epochs_fnames, _ = get_epochs_evokeds_fnames(p, subj, p.analyses)
_, fif_file = epochs_fnames
Expand All @@ -71,11 +70,12 @@ def gen_inverses(p, subjects, run_indices):
rank = _compute_rank(p, subj, run_indices[si])
else:
rank = None # should be safe from our gen_covariances step
erm_name = get_erm_cov_fname(p, subj)
empty_cov = None
make_erm_inv = erm_name is not None
if make_erm_inv:
# We now process the empty room with "movement
# compensation" so it should get the same rank!
erm_name = op.join(cov_dir, safe_inserter(p.runs_empty[0], subj) +
p.pca_extra + p.inv_tag + '-cov.fif')
empty_cov = read_cov(erm_name)
if p.force_erm_cov_rank_full and p.cov_method == 'empirical':
empty_cov = regularize(
Expand All @@ -100,6 +100,7 @@ def gen_inverses(p, subjects, run_indices):
temp_name = subj
cov = empty_cov
tag = p.inv_erm_tag
assert cov is not None
else:
s_name = safe_inserter(name, subj)
temp_name = s_name + ('-%d' % p.lp_cut) + p.inv_tag
Expand All @@ -117,7 +118,9 @@ def gen_inverses(p, subjects, run_indices):
inv_dir, temp_name + f + tag + s + '-inv.fif')
kwargs = dict(loose=l_, depth=d, fixed=x, use_cps=True,
verbose='error')
if name is not True or not e:
if (name is not True or
(not e) or
p.erm_cov_from_task):
inv = make_inverse_operator(
epochs.info, fwd_restricted, cov, rank=rank,
**kwargs)
Expand Down
5 changes: 5 additions & 0 deletions mnefun/_mnefun.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,11 @@ def __init__(self, tmin=None, tmax=None, t_adjust=0, bmin=-0.2, bmax=0.0,
self.cont_as_esss = False
self.cont_reject = None
self.allow_resample = False
self.fix_eeg_order = True
self.erm_cov_from_task = False
self.notch_filter = None
self.ssp_ecg_baseline = None
self.ssp_eog_baseline = None
self.freeze()
# Read static-able paraws from config file
_set_static(self)
Expand Down
23 changes: 19 additions & 4 deletions mnefun/_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ def get_cov_fwd_inv_fnames(p, subj, run_indices):
inv_dir = op.join(p.work_dir, subj, p.inverse_dir)
fwd_dir = op.join(p.work_dir, subj, p.forward_dir)
cov_dir = op.join(p.work_dir, subj, p.cov_dir)
make_erm_inv = len(p.runs_empty) > 0
erm_cov_fname = get_erm_cov_fname(p, subj)
make_erm_inv = erm_cov_fname is not None

# Shouldn't matter which raw file we use
raw_fname = get_raw_fnames(p, subj, 'pca', True, False, run_indices)[0]
Expand Down Expand Up @@ -169,11 +170,10 @@ def get_cov_fwd_inv_fnames(p, subj, run_indices):
inv_fnames += [op.join(inv_dir,
temp_name + f + s + '-inv.fif')]
if make_erm_inv:
cov_fnames += [op.join(cov_dir, safe_inserter(p.runs_empty[0], subj) +
p.pca_extra + p.inv_tag + '-cov.fif')]
cov_fnames += [erm_cov_fname]
for f, m, e in zip(out_flags, meg_bools, eeg_bools):
for s in [p.inv_fixed_tag, '']:
if (not e):
if (p.erm_cov_from_task or not e):
inv_fnames += [op.join(inv_dir, subj + f +
p.inv_erm_tag + s + '-inv.fif')]
return cov_fnames, fwd_fnames, inv_fnames
Expand Down Expand Up @@ -242,3 +242,18 @@ def _is_dir(d):

def _get_config_file():
return op.expanduser(op.join('~', '.mnefun', 'mnefun.json'))


def get_erm_cov_fname(p, subj):
"""Get the continuous (empty-room) covariance filename."""
cov_dir = op.join(p.work_dir, subj, p.cov_dir)
fname = None
if p.erm_cov_from_task:
fname = f'{subj}-cont'
elif len(p.runs_empty):
if len(p.runs_empty) > 1:
raise ValueError('Too many empty rooms; undefined output!')
fname = safe_inserter(p.runs_empty[0], subj)
if fname is not None:
fname = op.join(cov_dir, f'{fname}{p.pca_extra}{p.inv_tag}-cov.fif')
return fname
Loading