Module xelo2.bids.ephys
Expand source code
from logging import getLogger
from datetime import timedelta
from json import dump
from numpy import isin, ones, array
from numpy.lib.recfunctions import append_fields
from re import sub
from .utils import rename_task, make_bids_name, find_one_file, make_taskdescription
from ..io.tsv import save_tsv
from ..io.ephys import localize_blackrock
CHAN_TYPES = {
'ECOGChannelCount': 'ECOG',
'SEEGChannelCount': 'SEEG',
'EEGChannelCount': 'EEG',
'EOGChannelCount': 'EOG',
'ECGChannelCount': 'ECG',
'EMGChannelCount': 'EMG',
'MiscChannelCount': 'MISC',
'TriggerChannelCount': 'TRIG',
}
lg = getLogger(__name__)
EXCLUDE_CHAN_TYPES = ['OTHER', ]
def convert_ephys(run, rec, dest_path, name, intendedfor):
start_time = run.start_time + timedelta(seconds=rec.offset)
end_time = start_time + timedelta(seconds=run.duration)
file = find_one_file(rec, ('blackrock', 'micromed', 'bci2000'))
if file is None:
return
electrodes = rec.electrodes
if electrodes is not None:
elec_name = electrodes.name
if elec_name is None:
elec_name = 'unspecified'
name['acq'] = 'acq-' + sub('[\s()]', '', elec_name)
electrodes_tsv = dest_path / make_bids_name(name, 'electrodes')
save_tsv(electrodes_tsv, electrodes.data, ['name', 'x', 'y', 'z', 'size'])
electrodes_json = dest_path / make_bids_name(name, 'coordsystem')
save_coordsystem(electrodes_json, electrodes, intendedfor)
else:
lg.warning(f'No electrodes for {run} / {rec}, so no _acq-<label>_ field')
name['acq'] = None
d = localize_blackrock(file.path)
n_chan = len(d.header['chan_name'])
channels = rec.channels
chan_to_include = None
if channels is None:
lg.warning(f'No channel information for {str(rec)}')
else:
chan_data = channels.data
if n_chan == chan_data.shape[0]:
i_chan = ~isin(chan_data['type'], EXCLUDE_CHAN_TYPES)
chan_data = chan_data[i_chan]
chan_data = append_fields(chan_data, ['sampling_frequency', ], [d.header['s_freq'] * ones(chan_data.shape[0]), ], usemask=False)
channels_tsv = dest_path / make_bids_name(name, 'channels')
save_tsv(channels_tsv, chan_data, ['name', 'type', 'units', 'low_cutoff', 'high_cutoff'])
replace_micro(channels_tsv)
chan_to_include = array(d.header['chan_name'])[i_chan]
else:
lg.warning(f'{str(rec)}: actual recording has {n_chan} channels, while the channels.tsv has {chan_data.shape[0]} channels. The labels will not be correct')
data = d.read_data(begtime=start_time, endtime=end_time, chan=chan_to_include)
if chan_to_include is not None:
data.chan[0] = chan_data['name']
output_ephys = dest_path / make_bids_name(name, rec.modality)
markers = convert_events_to_wonambi(run.events)
data.export(output_ephys, 'brainvision', markers=markers, anonymize=True)
sidecar = _convert_sidecar(run, rec, d)
sidecar_file = output_ephys.with_suffix('.json')
with sidecar_file.open('w') as f:
dump(sidecar, f, indent=2)
return output_ephys
def replace_micro(channels_tsv):
"""delete this when the PR is accepted
https://github.com/bids-standard/bids-validator/pull/923"""
with channels_tsv.open() as f:
x = f.read()
x = x.replace('μ', 'µ') # 'GREEK SMALL LETTER MU' -> 'MICRO SIGN'
with channels_tsv.open('w') as f:
f.write(x)
def _convert_sidecar(run, rec, d):
D = {
'InstitutionName': 'University Medical Center Utrecht',
'InstitutionAddress': 'Heidelberglaan 100, 3584 CX Utrecht, the Netherlands',
'Manufacturer': rec.Manufacturer,
'TaskName': rename_task(run.task_name),
'TaskDescription': make_taskdescription(run),
'SamplingFrequency': int(d.header['s_freq']),
'PowerLineFrequency': 50,
'SoftwareFilters': 'n/a',
}
channels = rec.channels
reference = 'n/a'
if channels is not None:
if channels.Reference is not None:
reference = channels.Reference
chans = channels.data
for field, chan_type in CHAN_TYPES.items():
n_chan = int(sum(chans['type'] == chan_type))
if n_chan > 0:
D[field] = n_chan
if rec.modality == 'ieeg':
D['iEEGReference'] = reference
elif rec.modality == 'eeg':
D['EEGReference'] = reference
D['RecordingDuration'] = run.duration
D['RecordingType'] = 'continuous'
if rec.modality == 'ieeg':
D['ElectricalStimulation'] = False
return D
def save_coordsystem(electrodes_json, electrodes, intendedfor):
D = {
"iEEGCoordinateSystem": "ACPC", # T1w should be an option https://github.com/bids-standard/bids-specification/issues/661
"iEEGCoordinateUnits": "mm",
"iEEGCoordinateProcessingDescription": "surface_projection",
"iEEGCoordinateProcessingReference": "PMID: 19836416",
}
if electrodes.IntendedFor is not None:
if electrodes.IntendedFor in intendedfor:
path_to_t1 = intendedfor[electrodes.IntendedFor]
# assume we have to go back three folders
path_to_t1 = path_to_t1.relative_to(path_to_t1.parents[3])
D['IntendedFor'] = '/' + str(path_to_t1)
else:
lg.warning(f'Could not find the intended-for t1w for electrodes {electrodes}')
with electrodes_json.open('w') as f:
dump(D, f, indent=2)
def convert_events_to_wonambi(events):
"""This function should be in wonambi, once the bids format is more stable
in wonambi"""
mrk = []
for ev in events:
mrk.append({
'name': ev['trial_type'],
'start': ev['onset'],
'end': ev['onset'] + ev['duration']
})
return mrk
Functions
def convert_ephys(run, rec, dest_path, name, intendedfor)
-
Expand source code
def convert_ephys(run, rec, dest_path, name, intendedfor): start_time = run.start_time + timedelta(seconds=rec.offset) end_time = start_time + timedelta(seconds=run.duration) file = find_one_file(rec, ('blackrock', 'micromed', 'bci2000')) if file is None: return electrodes = rec.electrodes if electrodes is not None: elec_name = electrodes.name if elec_name is None: elec_name = 'unspecified' name['acq'] = 'acq-' + sub('[\s()]', '', elec_name) electrodes_tsv = dest_path / make_bids_name(name, 'electrodes') save_tsv(electrodes_tsv, electrodes.data, ['name', 'x', 'y', 'z', 'size']) electrodes_json = dest_path / make_bids_name(name, 'coordsystem') save_coordsystem(electrodes_json, electrodes, intendedfor) else: lg.warning(f'No electrodes for {run} / {rec}, so no _acq-<label>_ field') name['acq'] = None d = localize_blackrock(file.path) n_chan = len(d.header['chan_name']) channels = rec.channels chan_to_include = None if channels is None: lg.warning(f'No channel information for {str(rec)}') else: chan_data = channels.data if n_chan == chan_data.shape[0]: i_chan = ~isin(chan_data['type'], EXCLUDE_CHAN_TYPES) chan_data = chan_data[i_chan] chan_data = append_fields(chan_data, ['sampling_frequency', ], [d.header['s_freq'] * ones(chan_data.shape[0]), ], usemask=False) channels_tsv = dest_path / make_bids_name(name, 'channels') save_tsv(channels_tsv, chan_data, ['name', 'type', 'units', 'low_cutoff', 'high_cutoff']) replace_micro(channels_tsv) chan_to_include = array(d.header['chan_name'])[i_chan] else: lg.warning(f'{str(rec)}: actual recording has {n_chan} channels, while the channels.tsv has {chan_data.shape[0]} channels. The labels will not be correct') data = d.read_data(begtime=start_time, endtime=end_time, chan=chan_to_include) if chan_to_include is not None: data.chan[0] = chan_data['name'] output_ephys = dest_path / make_bids_name(name, rec.modality) markers = convert_events_to_wonambi(run.events) data.export(output_ephys, 'brainvision', markers=markers, anonymize=True) sidecar = _convert_sidecar(run, rec, d) sidecar_file = output_ephys.with_suffix('.json') with sidecar_file.open('w') as f: dump(sidecar, f, indent=2) return output_ephys
def convert_events_to_wonambi(events)
-
This function should be in wonambi, once the bids format is more stable in wonambi
Expand source code
def convert_events_to_wonambi(events): """This function should be in wonambi, once the bids format is more stable in wonambi""" mrk = [] for ev in events: mrk.append({ 'name': ev['trial_type'], 'start': ev['onset'], 'end': ev['onset'] + ev['duration'] }) return mrk
def replace_micro(channels_tsv)
-
delete this when the PR is accepted https://github.com/bids-standard/bids-validator/pull/923
Expand source code
def replace_micro(channels_tsv): """delete this when the PR is accepted https://github.com/bids-standard/bids-validator/pull/923""" with channels_tsv.open() as f: x = f.read() x = x.replace('μ', 'µ') # 'GREEK SMALL LETTER MU' -> 'MICRO SIGN' with channels_tsv.open('w') as f: f.write(x)
def save_coordsystem(electrodes_json, electrodes, intendedfor)
-
Expand source code
def save_coordsystem(electrodes_json, electrodes, intendedfor): D = { "iEEGCoordinateSystem": "ACPC", # T1w should be an option https://github.com/bids-standard/bids-specification/issues/661 "iEEGCoordinateUnits": "mm", "iEEGCoordinateProcessingDescription": "surface_projection", "iEEGCoordinateProcessingReference": "PMID: 19836416", } if electrodes.IntendedFor is not None: if electrodes.IntendedFor in intendedfor: path_to_t1 = intendedfor[electrodes.IntendedFor] # assume we have to go back three folders path_to_t1 = path_to_t1.relative_to(path_to_t1.parents[3]) D['IntendedFor'] = '/' + str(path_to_t1) else: lg.warning(f'Could not find the intended-for t1w for electrodes {electrodes}') with electrodes_json.open('w') as f: dump(D, f, indent=2)