diff --git a/README.md b/README.md index e24e492..e19ac51 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,38 @@ # EEGprep Standardized EEG preprocessing +[![https://www.singularity-hub.org/static/img/hosted-singularity--hub-%23e32929.svg](https://www.singularity-hub.org/static/img/hosted-singularity--hub-%23e32929.svg)](https://singularity-hub.org/collections/3833) + ## Singularity -Build the EEGprep singularity image: +Download the EEGprep singularity image: ``` -sudo singularity build eegprep.simg Singularity +singularity pull --name eegprep.simg shub://Charestlab/eegprep ``` Run EEGprep on your data: ``` singularity run -c -e --bind /your/data/dir/:/data eegprep.simg ``` -where /your/data/dir/ contains a *BIDS* folder. + +## Commandline + +You can run eegprep on the commandline. Start by running `eegprep -h` and you'll see: +``` +usage: eegprep [-h] [-s SUBJECT_INDEX] [-l SUBJECT_LABEL] [data_directory] + +positional arguments: + data_directory root data directory + +optional arguments: + -h, --help show this help message and exit + -s SUBJECT_INDEX, --subject-index SUBJECT_INDEX + index of subject to work on when sorted alphabetically + -l SUBJECT_LABEL, --subject-label SUBJECT_LABEL + label of subject to work on + +``` ## Configuration diff --git a/Singularity b/Singularity index c6aad3b..4a28a5d 100644 --- a/Singularity +++ b/Singularity @@ -11,10 +11,10 @@ From: python:3.7 dist/eegprep-0.1.tar.gz . %post - pip install numpy ipython - pip install --no-cache-dir -U https://api.github.com/repos/mne-tools/mne-python/zipball/master#egg=mne - pip install --no-cache-dir -U https://api.github.com/repos/autoreject/autoreject/zipball/master#egg=autoreject pip install eegprep-0.1.tar.gz %runscript exec eegprep + +%labels + Version 0.1 diff --git a/eegprep/args.py b/eegprep/args.py new file mode 100644 index 0000000..ca6a3f3 --- /dev/null +++ b/eegprep/args.py @@ -0,0 +1,24 @@ +from argparse import ArgumentParser + + +def parse_arguments(args=None): + """Parse commandline parameters + + Args: + args (str, optional): String of arguments, for testing purposes only. + Defaults to None. + + Returns: + Namespace: Object with parsed arguments as properties + """ + parser = ArgumentParser() + parser.add_argument('data_directory', type=str, nargs='?', default='/data', + help='root data directory') + parser.add_argument('--dry-run', action='store_true', + help='rshow assembled pipeline but do not run analyses or store files') + subject = parser.add_mutually_exclusive_group() + subject.add_argument('-s', '--subject-index', type=int, + help='index of subject to work on, when sorted alphabetically') + subject.add_argument('-l', '--subject-label', type=str, + help='label of subject to work on') + return parser.parse_args(args) diff --git a/eegprep/bids/naming.py b/eegprep/bids/naming.py deleted file mode 100644 index b64e3a5..0000000 --- a/eegprep/bids/naming.py +++ /dev/null @@ -1,19 +0,0 @@ -BIDS_SEGMENTS = ['sub', 'ses', 'task', 'run'] - - -def filename2tuple(fname): - segments = fname.split('_') - vals = [None] * len(BIDS_SEGMENTS) - for seg in segments: - for l in [3, 4]: - if seg[:l] in BIDS_SEGMENTS: - vals[BIDS_SEGMENTS.index(seg[:l])] = seg[l+1:] - return tuple(vals) - - -def args2filename(**kwargs): - fname = '' - for seg in BIDS_SEGMENTS: - if seg in kwargs: - fname += seg + '-' + kwargs[seg] - return fname \ No newline at end of file diff --git a/eegprep/configuration.py b/eegprep/configuration.py index 1dc13a7..a14a9f1 100644 --- a/eegprep/configuration.py +++ b/eegprep/configuration.py @@ -1,4 +1,18 @@ +"""[summary] +Previously used like: + + # print('data directory: {}'.format(datadir)) + # conf_file_path = join(datadir, 'eegprep.conf') + # config = Configuration() + # config.setDefaults(defaults) + # if os.path.isfile(conf_file_path): + # with open(conf_file_path) as fh: + # conf_string = fh.read() + # config.updateFromString(conf_string) + # print('configuration:') + # print(config) +""" class Configuration(object): diff --git a/eegprep/input_output.py b/eegprep/input_output.py new file mode 100644 index 0000000..ac2297a --- /dev/null +++ b/eegprep/input_output.py @@ -0,0 +1,130 @@ +from bids import BIDSLayout +import copy +from os.path import join, dirname, isdir +from os import makedirs + + +class InputOutput(object): + + def __init__(self, log, memory, root_dir, scope=None, layout=None): + self.log = log + self.memory = memory + self.root_dir = root_dir + self._layout = layout or None + self.scope = scope or dict() + + @property + def layout(self): + """pyBIDS layout object, lazily loaded. + + If the layout has not been created yet, it will + be done here, which takes time (<1min) for larger datasets. + + Returns: + bids.BIDSLayout: The BIDS layout object + """ + if self._layout is None: + self.log.discovering_data() + self._layout = BIDSLayout(self.root_dir) + return self._layout + + def describe_scope(self): + return ' '.join([f'{k[:3]}={v}' for k, v in self.scope.items()]) + + def for_(self, subject=None, session=None, run=None): + new_scope = copy.copy(self.scope) + filters = dict(subject=subject, session=session, run=run) + for spec, val in filters.items(): + if val is not None: + new_scope[spec] = val + return InputOutput( + self.log, + self.memory, + self.root_dir, + new_scope, + self.layout + ) + + def get_subject_labels(self): + subjects = self.layout.get( + return_type='id', + target='subject', + datatype='eeg' + ) + self.log.found_subjects(subjects) + return subjects + + def get_session_labels(self): + return self.layout.get( + return_type='id', + target='session', + datatype='eeg', + **self.scope + ) + + def get_run_labels(self): + return self.layout.get( + return_type='id', + target='run', + datatype='eeg', + **self.scope + ) + + def get_filepath(self, suffix): + fpaths = self.layout.get( + return_type='filename', + suffix=suffix, + datatype='eeg', + **self.scope + ) + fpaths = [f for f in fpaths if '.json' not in f] + assert len(fpaths) == 1 + return fpaths[0] + + def store_object(self, obj, name, job): + # first delete existing copies (overwriting) + self.memory.delete(name=name, **self.scope) + identifiers = dict(name=name, job=job.get_id(), **self.scope) + self.memory.store(obj, **identifiers) + + def retrieve_objects(self, name): + filters = dict(name=name, **self.scope) + return self.memory.find(**filters) + + def retrieve_object(self, name): + objects = self.retrieve_objects(name) + assert len(objects) == 1 + return objects[0] + + def expire_output_of(self, job): + self.memory.delete(job=job.get_id(), **self.scope) + + def write_output_of(self, job): + keys = self.memory.find_matching_keys(job=job.get_id(), **self.scope) + for key in keys: + name = dict(key)['name'] + self.write_object(name, self.memory.get(key)) + + def write_object(self, name, obj): + fpath = self.build_fpath(suffix=name, ext='fif') + self.ensure_dir(dirname(fpath)) + self.log.writing_object(obj, fpath) + obj.save(fpath) + + def ensure_dir(self, dirpath): + if not isdir(dirpath): + makedirs(dirpath) + + def build_fpath(self, suffix, ext): + outdir = join(self.root_dir, 'derivatives', 'eegprep') + for entity in ('subject', 'session'): + if entity in self.scope: + label = self.scope[entity] + outdir = join(outdir, f'{entity[:3]}-{label}') + fname = '' + for entity in ('subject', 'session'): + if entity in self.scope: + label = self.scope[entity] + fname += f'{entity[:3]}-{label}_' + fname += f'{suffix}.{ext}' + return join(outdir, fname) diff --git a/eegprep/bids/__init__.py b/eegprep/jobs/__init__.py similarity index 100% rename from eegprep/bids/__init__.py rename to eegprep/jobs/__init__.py diff --git a/eegprep/jobs/base.py b/eegprep/jobs/base.py new file mode 100644 index 0000000..24c578d --- /dev/null +++ b/eegprep/jobs/base.py @@ -0,0 +1,43 @@ + + +class BaseJob(object): + + def __init__(self, io, log): + self.io = io + self.log = log + self.jobs_to_expire = [] + self.jobs_to_write = [] + + def get_id(self): + return self.__class__.__name__.replace('Job', '') + + def describe(self): + """Return a string that describes this job + + Returns: + str: one-line string describing this job and it's scope + """ + scope = self.io.describe_scope() + return scope + ' ' + self.get_id() + + def add_to(self, pipeline): + self.add_children_to(pipeline) + pipeline.add(self) + + def add_children_to(self, pipeline): + pass + + def run(self): + pass + + def cleanup(self): + for job in self.jobs_to_write: + self.io.write_output_of(job) + for job in self.jobs_to_expire: + self.io.expire_output_of(job) + + def expire_output_on_cleanup(self, job): + self.jobs_to_expire.append(job) + + def write_output_on_cleanup(self, job): + self.jobs_to_write.append(job) diff --git a/eegprep/jobs/concat_epochs.py b/eegprep/jobs/concat_epochs.py new file mode 100644 index 0000000..502abe6 --- /dev/null +++ b/eegprep/jobs/concat_epochs.py @@ -0,0 +1,10 @@ +from eegprep.jobs.base import BaseJob +import mne + + +class ConcatEpochsJob(BaseJob): + + def run(self): + epochs_per_run = self.io.retrieve_objects('epo') + epochs = mne.epochs.concatenate_epochs(epochs_per_run) + self.io.store_object(epochs, name='epo', job=self) diff --git a/eegprep/jobs/epoch.py b/eegprep/jobs/epoch.py new file mode 100644 index 0000000..8b564f2 --- /dev/null +++ b/eegprep/jobs/epoch.py @@ -0,0 +1,20 @@ +from eegprep.jobs.base import BaseJob +import mne + + +class EpochJob(BaseJob): + + def run(self): + raw = self.io.retrieve_object('raw') + # additional options: consecutive=False, min_duration=0.005) + events = mne.find_events(raw, verbose=False) + picks = mne.pick_types(raw.info, eeg=True) + epochs_params = dict( + events=events, + tmin=-0.2, + tmax=3.1, + picks=picks, + verbose=False + ) + epochs = mne.Epochs(raw, preload=True, **epochs_params) + self.io.store_object(epochs, name='epo', job=self) diff --git a/eegprep/jobs/filter.py b/eegprep/jobs/filter.py new file mode 100644 index 0000000..c3f57cb --- /dev/null +++ b/eegprep/jobs/filter.py @@ -0,0 +1,9 @@ +from eegprep.jobs.base import BaseJob + + +class FilterJob(BaseJob): + + def run(self): + raw = self.io.retrieve_object('raw') + raw.filter(l_freq=0.05, h_freq=45, fir_design='firwin') + self.io.store_object(raw, name='raw', job=self) diff --git a/eegprep/jobs/read.py b/eegprep/jobs/read.py new file mode 100644 index 0000000..07a3999 --- /dev/null +++ b/eegprep/jobs/read.py @@ -0,0 +1,46 @@ +import mne, pandas +from eegprep.jobs.base import BaseJob +from eegprep.guess import guess_montage + + +class ReadJob(BaseJob): + + def run(self): + + fpath_raw = self.io.get_filepath(suffix='eeg') + ext = fpath_raw[-3:] + raw_funcs = { + 'bdf': mne.io.read_raw_bdf, + 'edf': mne.io.read_raw_edf + } + raw = raw_funcs[ext](fpath_raw, preload=True, verbose=False) + + # Set channel types and select reference channels + fpath_channels = self.io.get_filepath(suffix='channels') + channels = pandas.read_csv(fpath_channels, index_col='name', sep='\t') + bids2mne = { + 'MISC': 'misc', + 'EEG': 'eeg', + 'EOG': 'eog', + 'VEOG': 'eog', + 'TRIG': 'stim', + 'REF': 'eeg', + } + channels['mne'] = channels.type.replace(bids2mne) + raw.set_channel_types(channels.mne.to_dict()) + + # set bad channels + # raw.info['bads'] = channels[channels.status=='bad'].index.tolist() + + # Set reference + refChannels = channels[channels.type=='REF'].index.tolist() + raw.set_eeg_reference(ref_channels=refChannels) + # can now drop reference electrodes + raw.set_channel_types({k: 'misc' for k in refChannels}) + + # tell MNE about electrode locations + montageName = guess_montage(raw.ch_names) + montage = mne.channels.make_standard_montage(kind=montageName) + raw.set_montage(montage, verbose=False) + + self.io.store_object(raw, name='raw', job=self) diff --git a/eegprep/jobs/run.py b/eegprep/jobs/run.py new file mode 100644 index 0000000..4372035 --- /dev/null +++ b/eegprep/jobs/run.py @@ -0,0 +1,38 @@ +from eegprep.jobs.base import BaseJob +from eegprep.jobs.read import ReadJob +from eegprep.jobs.filter import FilterJob +from eegprep.jobs.epoch import EpochJob + + +class RunJob(BaseJob): + """Represents preprocessing of one raw data file. + """ + + def add_children_to(self, pipeline): + job = ReadJob(self.io, self.log) + job.add_to(pipeline) + self.expire_output_on_cleanup(job) + job = FilterJob(self.io, self.log) + job.add_to(pipeline) + self.expire_output_on_cleanup(job) + job = EpochJob(self.io, self.log) + job.add_to(pipeline) + # io.store_output_of(job) + + # plot raw data + # nchans = len(raw.ch_names) + # pick_channels = numpy.arange(0, nchans, numpy.floor(nchans/20)).astype(int) + # start = numpy.round(raw.times.max()/2) + # fig = raw.plot(start=start, order=pick_channels) + # fname_plot = 'sub-{}_ses-{}_task-{}_run-{}_raw.png'.format(sub, ses, task, run) + # fig.savefig(join(reportsdir, fname_plot)) + + # # create evoked plots + # conds = clean_epochs.event_id.keys() + # selected_conds = random.sample(conds, min(len(conds), 6)) + # picks = mne.pick_types(clean_epochs.info, eeg=True) + # for cond in selected_conds: + # evoked = clean_epochs[cond].average() + # fname_plot = 'sub-{}_ses-{}_task-{}_run-{}_evoked-{}.png'.format(sub, ses, task, run, cond) + # fig = evoked.plot_joint(picks=picks) + # fig.savefig(join(reportsdir, fname_plot)) diff --git a/eegprep/jobs/subject.py b/eegprep/jobs/subject.py new file mode 100644 index 0000000..896b607 --- /dev/null +++ b/eegprep/jobs/subject.py @@ -0,0 +1,22 @@ +from eegprep.jobs.base import BaseJob +from eegprep.jobs.run import RunJob +from eegprep.jobs.concat_epochs import ConcatEpochsJob + + +class SubjectJob(BaseJob): + + def add_children_to(self, pipeline): + found_data = False + sessions = self.io.get_session_labels() + for session_label in sessions: + session_io = self.io.for_(session=session_label) + for run_label in session_io.get_run_labels(): + found_data = True + job = RunJob(session_io.for_(run=run_label), self.log) + job.add_to(pipeline) + self.expire_output_on_cleanup(job) + if found_data: + job = ConcatEpochsJob(self.io, self.log) + job.add_to(pipeline) + self.write_output_on_cleanup(job) + self.expire_output_on_cleanup(job) diff --git a/eegprep/log.py b/eegprep/log.py new file mode 100644 index 0000000..c702cef --- /dev/null +++ b/eegprep/log.py @@ -0,0 +1,51 @@ +from mne.utils.misc import sizeof_fmt as hsize + + +class Log(object): + + def write(self, message): + print(message) + + def new_partial_log(self): + return self + + def received_arguments(self, args): + m = 'Command arguments:\n\t' + m += '\n\t'.join([f'{k}: {v}' for k, v in vars(args).items()]) + self.write(m) + + def found_subjects(self, subjects): + listed = ', '.join(subjects) + self.write(f'Found {len(subjects)} subjects: {listed}') + + def started_pipeline(self, jobs): + m = f'Starting pipeline with {len(jobs)} jobs:\n' + job_lines = [f'{j+1}: {job.describe()}' for j, job in enumerate(jobs)] + m += '\n'.join(job_lines) + self.write(m) + + def starting_job(self, job): + self.write(f'Starting job: ' + job.describe()) + + def cleaning_up_after_job(self, job): + self.write(f'Cleaning up after job: ' + job.describe()) + + def discovering_data(self): + self.write('Discovering data..') + + def storing_object_in_memory(self, key, obj, size, total_size): + self.write( + f'Storing object ({hsize(size)}) ' + f'in memory store ({hsize(total_size)})' + ) + + def removing_object_from_memory(self, key, obj, size, total_size): + self.write( + f'Removing object ({hsize(size)}) ' + f'from memory ({hsize(total_size)})' + ) + + def writing_object(self, obj, fpath): + self.write(f'Writing object to disk at {fpath}') + + # TODO: job can flush log after done: log.flush(io) (io.write_text(log.xyz)) \ No newline at end of file diff --git a/eegprep/main.py b/eegprep/main.py new file mode 100644 index 0000000..b13c3a8 --- /dev/null +++ b/eegprep/main.py @@ -0,0 +1,35 @@ +from eegprep.args import parse_arguments +from eegprep.pipeline import Pipeline +from eegprep.log import Log +from eegprep.memory import Memory +from eegprep.input_output import InputOutput +from eegprep.jobs.subject import SubjectJob + + +def run(args=None): + """Parses commandline arguments and runs EEGprep for the specified scope + + Args: + args (Namespace, optional): Object with eeg prep parameters + as attributes. Defaults to None. + """ + log = Log() + args = args or parse_arguments() + log.received_arguments(args) + + io = InputOutput(log, Memory(log), args.data_directory) + pipeline = Pipeline(log, args.dry_run) + + subjects = io.get_subject_labels() + if args.subject_index: + subjects = [subjects[args.subject_index-1]] + + if args.subject_label: + subjects = [args.subject_label] + for subject_label in subjects: + job = SubjectJob( + io.for_(subject=subject_label), + log.new_partial_log() + ) + job.add_to(pipeline) + pipeline.run() diff --git a/eegprep/memory.py b/eegprep/memory.py new file mode 100644 index 0000000..c5c6a41 --- /dev/null +++ b/eegprep/memory.py @@ -0,0 +1,45 @@ +from copy import copy + + +class Memory(object): + """Stores arbitrary objects by a set of key/value pairs. + """ + + def __init__(self, log): + self.objects = {} + self.log = log + self.total_size = 0 + + def get(self, key): + return self.objects[key] + + def store(self, obj, **filters): + key = frozenset(filters.items()) + size = obj._size + self.total_size += size + self.log.storing_object_in_memory(key, obj, size, self.total_size) + self.objects[key] = obj + + def find(self, **filters): + selection = self.find_matching_keys(**filters) + return [self.objects[k] for k in selection] + + def delete(self, **filters): + selection = self.find_matching_keys(**filters) + for k in selection: + obj = self.objects[k] + size = obj._size + self.total_size -= size + self.log.removing_object_from_memory( + k, obj, size, self.total_size) + del self.objects[k] + + def find_matching_keys(self, **filters): + selection = [] + for object_key in self.objects.keys(): + for name, val in object_key: + if (name in filters) and (filters.get(name) != val): + break + else: + selection.append(object_key) + return selection diff --git a/eegprep/pipeline.py b/eegprep/pipeline.py new file mode 100644 index 0000000..930c9d9 --- /dev/null +++ b/eegprep/pipeline.py @@ -0,0 +1,20 @@ + +class Pipeline(object): + + def __init__(self, log, dry): + self.log = log + self.dry = dry + self.jobs = [] + + def add(self, job): + self.jobs.append(job) + + def run(self): + self.log.started_pipeline(self.jobs) + if self.dry: + return + for job in self.jobs: + self.log.starting_job(job) + job.run() + self.log.cleaning_up_after_job(job) + job.cleanup() diff --git a/eegprep/preproc.py b/eegprep/preproc.py deleted file mode 100644 index d68e3e7..0000000 --- a/eegprep/preproc.py +++ /dev/null @@ -1,173 +0,0 @@ -from os.path import join, basename, splitext -import os, glob, random -import numpy -import scipy.io -import mne -import pandas -from autoreject import AutoReject -from eegprep.bids.naming import filename2tuple -from eegprep.guess import guess_montage -from eegprep.util import ( - resample_events_on_resampled_epochs, - plot_rejectlog, - save_rejectlog -) -from eegprep.configuration import Configuration -from eegprep.defaults import defaults - - -def run_preproc(datadir='/data'): - - print('data directory: {}'.format(datadir)) - conf_file_path = join(datadir, 'eegprep.conf') - config = Configuration() - config.setDefaults(defaults) - if os.path.isfile(conf_file_path): - with open(conf_file_path) as fh: - conf_string = fh.read() - config.updateFromString(conf_string) - print('configuration:') - print(config) - - bidsdir = join(datadir, 'BIDS') - eegprepdir = join(bidsdir, 'derivatives', 'eegprep') - - - subjectdirs = sorted(glob.glob(join(bidsdir, 'sub-*'))) - for subjectdir in subjectdirs: - assert os.path.isdir(subjectdir) - - sub = basename(subjectdir)[4:] - - # prepare derivatives directory - derivdir = join(eegprepdir, 'sub-' + sub) - os.makedirs(derivdir, exist_ok=True) - reportsdir = join(eegprepdir, 'reports', 'sub-' + sub) - os.makedirs(reportsdir, exist_ok=True) - - - subject_epochs = {} - rawtypes = {'.set': mne.io.read_raw_eeglab, '.bdf': mne.io.read_raw_edf} - for fname in sorted(glob.glob(join(subjectdir, 'eeg', '*'))): - _, ext = splitext(fname) - if ext not in rawtypes.keys(): - continue - sub, ses, task, run = filename2tuple(basename(fname)) - - print('\nProcessing raw file: ' + basename(fname)) - - # read data - raw = rawtypes[ext](fname, preload=True, verbose=False) - events = mne.find_events(raw) #raw, consecutive=False, min_duration=0.005) - - # Set channel types and select reference channels - channelFile = fname.replace('eeg' + ext, 'channels.tsv') - channels = pandas.read_csv(channelFile, index_col='name', sep='\t') - bids2mne = { - 'MISC': 'misc', - 'EEG': 'eeg', - 'VEOG': 'eog', - 'TRIG': 'stim', - 'REF': 'eeg', - } - channels['mne'] = channels.type.replace(bids2mne) - - # the below fails if the specified channels are not in the data - raw.set_channel_types(channels.mne.to_dict()) - - # set bad channels - raw.info['bads'] = channels[channels.status=='bad'].index.tolist() - - # pick channels to use for epoching - epoching_picks = mne.pick_types(raw.info, eeg=True, eog=False, stim=False, exclude='bads') - - - # Filtering - #raw.filter(l_freq=0.05, h_freq=40, fir_design='firwin') - - montage = mne.channels.read_montage(guess_montage(raw.ch_names)) - print(montage) - raw.set_montage(montage) - - # plot raw data - nchans = len(raw.ch_names) - pick_channels = numpy.arange(0, nchans, numpy.floor(nchans/20)).astype(int) - start = numpy.round(raw.times.max()/2) - fig = raw.plot(start=start, order=pick_channels) - fname_plot = 'sub-{}_ses-{}_task-{}_run-{}_raw.png'.format(sub, ses, task, run) - fig.savefig(join(reportsdir, fname_plot)) - - # Set reference - refChannels = channels[channels.type=='REF'].index.tolist() - raw.set_eeg_reference(ref_channels=refChannels) - - ## epoching - epochs_params = dict( - events=events, - tmin=-0.1, - tmax=0.8, - reject=None, # dict(eeg=250e-6, eog=150e-6) - picks=epoching_picks, - detrend=0, - ) - file_epochs = mne.Epochs(raw, preload=True, **epochs_params) - file_epochs.drop_channels(refChannels) - - # autoreject (under development) - ar = AutoReject(n_jobs=4) - clean_epochs = ar.fit_transform(file_epochs) - - rejectlog = ar.get_reject_log(clean_epochs) - fname_log = 'sub-{}_ses-{}_task-{}_run-{}_reject-log.npz'.format(sub, ses, task, run) - save_rejectlog(join(reportsdir, fname_log), rejectlog) - fig = plot_rejectlog(rejectlog) - fname_plot = 'sub-{}_ses-{}_task-{}_run-{}_bad-epochs.png'.format(sub, ses, task, run) - fig.savefig(join(reportsdir, fname_plot)) - - - # store for now - subject_epochs[(ses, task, run)] = clean_epochs - - # create evoked plots - conds = clean_epochs.event_id.keys() - selected_conds = random.sample(conds, min(len(conds), 6)) - picks = mne.pick_types(clean_epochs.info, eeg=True) - for cond in selected_conds: - evoked = clean_epochs[cond].average() - fname_plot = 'sub-{}_ses-{}_task-{}_run-{}_evoked-{}.png'.format(sub, ses, task, run, cond) - fig = evoked.plot_joint(picks=picks) - fig.savefig(join(reportsdir, fname_plot)) - - - - sessSeg = 0 - sessions = sorted(list(set([k[sessSeg] for k in subject_epochs.keys()]))) - for session in sessions: - taskSeg = 1 - tasks = list(set([k[taskSeg] for k in subject_epochs.keys() if k[sessSeg]==session])) - for task in tasks: - print('\nGathering epochs for session {} task {}'.format(session, task)) - epochs_selection = [v for (k, v) in subject_epochs.items() if k[:2]==(session, task)] - - task_epochs = mne.epochs.concatenate_epochs(epochs_selection) - - # downsample if configured to do so - # important to do this after concatenation because - # downsampling may cause rejection for 'TOOSHORT' - if config['downsample'] < task_epochs.info['sfreq']: - task_epochs = task_epochs.copy().resample(config['downsample'], npad='auto') - - ext = config['out_file_format'] - fname = join(derivdir, 'sub-{}_ses-{}_task-{}_epo.{}'.format(sub, session, task, ext)) - variables = { - 'epochs': task_epochs.get_data(), - 'events': task_epochs.events, - 'timepoints': task_epochs.times - } - if ext == 'fif': - task_epochs.save(fname) - elif ext == 'mat': - scipy.io.savemat(fname, mdict=variables) - elif ext == 'npy': - numpy.savez(fname, **variables) - diff --git a/requirements.txt b/requirements.txt index 9bd4e40..b97f841 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ numpy scipy -scikit-learn>=0.18 +scikit-learn matplotlib -mne>0.16.2 -autoreject +mne==0.19.2 +autoreject==0.2.1 ipython pandas setuptools diff --git a/scripts/eegprep b/scripts/eegprep deleted file mode 100644 index dde2952..0000000 --- a/scripts/eegprep +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env python3 -import sys -from eegprep.preproc import run_preproc -datadir = sys.argv[1] if len(sys.argv) > 1 else '/data' -run_preproc(datadir) \ No newline at end of file diff --git a/setup.py b/setup.py index ba16d02..b392826 100644 --- a/setup.py +++ b/setup.py @@ -20,11 +20,15 @@ author='', author_email='', keywords='analysis eeg BIDS', - packages=['eegprep', 'eegprep.bids'], + packages=['eegprep'], include_package_data=True, zip_safe=False, install_requires=requires, - scripts=['scripts/eegprep'], + entry_points={ + 'console_scripts': [ + 'eegprep = eegprep.main:run', + ], + }, tests_require=requires, test_suite="tests" ) diff --git a/tests/input_output_tests.py b/tests/input_output_tests.py new file mode 100644 index 0000000..166b0e1 --- /dev/null +++ b/tests/input_output_tests.py @@ -0,0 +1,18 @@ +from unittest import TestCase +from unittest.mock import Mock + + +class InputOutputTests(TestCase): + + def test_build_fpath(self): + from eegprep.input_output import InputOutput + log, memory, layout = Mock(), Mock(), Mock() + scope = { + 'subject': '02', + 'session': '05' + } + io = InputOutput(log, memory, '/data', scope, layout) + self.assertEqual( + io.build_fpath(suffix='hello', ext='foo'), + '/data/derivatives/eegprep/sub-02/ses-05/sub-02_ses-05_hello.foo' + ) diff --git a/tests/log_tests.py b/tests/log_tests.py new file mode 100644 index 0000000..a199e0d --- /dev/null +++ b/tests/log_tests.py @@ -0,0 +1,30 @@ +from unittest import TestCase +from unittest.mock import Mock + + +class LogTests(TestCase): + + def test_received_arguments(self): + from eegprep.log import Log + class MockNamespace(object): + pass + args = MockNamespace() + args.foo = 'abc' + args.bar = 1 + log = Log() + log.write = Mock() + log.received_arguments(args) + log.write.assert_called_with( + 'Command arguments:\n' + '\tfoo: abc\n' + '\tbar: 1' + ) + + def test_found_subjects(self): + from eegprep.log import Log + log = Log() + log.write = Mock() + log.found_subjects(['pilot1', '03', 'pilot2', '02']) + log.write.assert_called_with( + 'Found 4 subjects: pilot1, 03, pilot2, 02' + ) diff --git a/tests/memory_tests.py b/tests/memory_tests.py new file mode 100644 index 0000000..abf9e47 --- /dev/null +++ b/tests/memory_tests.py @@ -0,0 +1,16 @@ +from unittest import TestCase +from unittest.mock import Mock + + +class MemoryTests(TestCase): + + def test_store_retrieve(self): + from eegprep.memory import Memory + obj1, obj2, obj3, obj4 = Mock(), Mock(), Mock(), Mock() + log = Mock() + ram = Memory(log) + ram.store(obj1, foo='a') + ram.store(obj2, foo='a', bar=1) + ram.store(obj3, foo='a', bar=2, baz=0.5) + ram.store(obj4, foo='b', bar=2) + self.assertEqual(ram.find(foo='a', bar=2), [obj1, obj3]) diff --git a/tests/naming_tests.py b/tests/naming_tests.py deleted file mode 100644 index 6069df2..0000000 --- a/tests/naming_tests.py +++ /dev/null @@ -1,17 +0,0 @@ -from unittest import TestCase - - -class TestNaming(TestCase): - - def test_filename2tuple(self): - from eegprep.bids.naming import filename2tuple - fname = 'sub-03_ses-01_task-irsa_run-15_eeg.set' - sub, ses, task, run = filename2tuple(fname) - self.assertEqual(sub, '03') - self.assertEqual(ses, '01') - self.assertEqual(task, 'irsa') - self.assertEqual(run, '15') - - def test_args2filename(self): - from eegprep.bids.naming import args2filename - self.assertEqual(args2filename(sub='02'), 'sub-02')