Skip to content

Commit df59045

Browse files
committed
Added re-referencing per headbox; additional settings in config file; BIDS validation off by default; Late re-referencing in CLI
1 parent 711007e commit df59045

4 files changed

Lines changed: 237 additions & 39 deletions

File tree

erdetect/_erdetect.py

Lines changed: 112 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -102,13 +102,18 @@ def process(bids_subset_data_path, output_dir, preproc_prioritize_speed=False):
102102

103103
channels_measured_incl = [] # the channels that are used as measured electrodes
104104
channels_stim_incl = [] # the channels which stim-pairs should be included (actual filtering of stim-pairs happens at the reading of the events)
105-
channels_early_reref_incl = [] #
105+
channels_early_reref_incl_names = [] # the names of the channels that are included for early re-referencing
106+
channels_early_reref_incl_headbox = [] # the headbox that each of the included early re-referencing channels belong to
107+
channels_late_reref_incl_names = [] # the names of the channels that are included for late re-referencing
108+
channels_late_reref_incl_headbox = [] # the headbox that each of the included late re-referencing channels belong to
106109

107110
channels_measured_excl_by_type = [] # channels that were excluded as measured electrodes (by type)
108111
channels_stim_excl_by_type = [] # channels that were excluded (and as a result exclude stim-pairs)
109112
channels_early_reref_excl_by_type = [] #
113+
channels_late_reref_excl_by_type = [] #
110114

111115
channels_have_status = 'status' in channel_tsv.columns
116+
channels_have_headbox = 'headbox' in channel_tsv.columns
112117
for index, row in channel_tsv.iterrows():
113118

114119
# check if bad channel
@@ -135,10 +140,12 @@ def process(bids_subset_data_path, output_dir, preproc_prioritize_speed=False):
135140

136141
# determine if included or excluded from early re-referencing electrodes (by type)
137142
if cfg('preprocess', 'early_re_referencing', 'enabled'):
138-
if row['type'].upper() in cfg('preprocess', 'early_re_referencing', 'types'):
143+
if row['type'].upper() in cfg('preprocess', 'early_re_referencing', 'channel_types'):
139144

140145
# save for log output and the early-referencing (structure)
141-
channels_early_reref_incl.append(row['name'])
146+
channels_early_reref_incl_names.append(row['name'])
147+
if channels_have_status:
148+
channels_early_reref_incl_headbox.append(row['headbox'])
142149

143150
# save for data reading (no duplicates)
144151
if not row['name'] in channels_incl:
@@ -147,6 +154,23 @@ def process(bids_subset_data_path, output_dir, preproc_prioritize_speed=False):
147154
else:
148155
channels_early_reref_excl_by_type.append(row['name']) # save for log output
149156

157+
# determine if included or excluded from late re-referencing electrodes (by type)
158+
if cfg('preprocess', 'late_re_referencing', 'enabled'):
159+
if row['type'].upper() in cfg('preprocess', 'late_re_referencing', 'channel_types'):
160+
161+
# save for log output and the late-referencing (structure)
162+
channels_late_reref_incl_names.append(row['name'])
163+
if channels_have_status:
164+
channels_late_reref_incl_headbox.append(row['headbox'])
165+
# TODO: what if nan or not a number
166+
167+
# save for data reading (no duplicates)
168+
if not row['name'] in channels_incl:
169+
channels_incl.append(row['name'])
170+
171+
else:
172+
channels_late_reref_excl_by_type.append(row['name']) # save for log output
173+
150174
# print channel information
151175
logging.info(multi_line_list(channels_excl_bad, LOGGING_CAPTION_INDENT_LENGTH, 'Bad channels (excluded):', 25, ' '))
152176
if channels_measured_excl_by_type == channels_stim_excl_by_type:
@@ -161,19 +185,95 @@ def process(bids_subset_data_path, output_dir, preproc_prioritize_speed=False):
161185
logging.info(multi_line_list(channels_measured_incl, LOGGING_CAPTION_INDENT_LENGTH, 'Channels incl. as measured electrodes:', 25, ' ', str(len(channels_measured_incl))))
162186
logging.info(multi_line_list(channels_stim_incl, LOGGING_CAPTION_INDENT_LENGTH, 'Channels incl. as stim electrodes:', 25, ' ', str(len(channels_stim_incl))))
163187

188+
164189
# check if there are any channels (as measured electrodes, or to re-reference on)
165190
if len(channels_measured_incl) == 0:
166191
logging.error('No channels were found (after filtering by type), exiting...')
167192
raise RuntimeError('No channels were found')
193+
194+
# check early re-referencing settings and prepare reref struct
195+
early_reref = None
168196
if cfg('preprocess', 'early_re_referencing', 'enabled'):
169-
if len(channels_early_reref_incl) == 0:
170-
logging.info(multi_line_list(channels_early_reref_incl, LOGGING_CAPTION_INDENT_LENGTH, 'Channels included (by type) for early re-ref:', 25, ' '))
197+
198+
if cfg('preprocess', 'early_re_referencing', 'method') == 'CAR_headbox' and not channels_have_headbox:
199+
logging.error('Early re-referencing is set to CAR per headbox, but the _channels.tsv file does not have a \'headbox\' column, exiting...')
200+
raise RuntimeError('No \'headbox\' column in _channels.tsv file, needed to perform early re-referencing per headbox')
201+
202+
if len(channels_early_reref_incl_names) == 0:
203+
logging.info(multi_line_list(channels_early_reref_incl_names, LOGGING_CAPTION_INDENT_LENGTH, 'Channels included (by type) for early re-ref:', 25, ' '))
171204
logging.info(multi_line_list(channels_early_reref_excl_by_type, LOGGING_CAPTION_INDENT_LENGTH, 'Channels excluded by type for early re-ref:', 25, ' '))
172205
logging.error('Early re-referencing is enabled but (after filtering by type) no channels were found, exiting...')
173206
raise RuntimeError('No channels were found for early re-referencing')
207+
208+
# generate an early re-referencing object
209+
if cfg('preprocess', 'early_re_referencing', 'method') == 'CAR':
210+
early_reref = RerefStruct.generate_car(channels_early_reref_incl_names)
211+
elif cfg('preprocess', 'early_re_referencing', 'method') == 'CAR_headbox':
212+
early_reref = RerefStruct.generate_car_per_headbox(channels_early_reref_incl_names, channels_early_reref_incl_headbox)
213+
214+
# print CAR headbox info
215+
logging.info('')
216+
log_indented_line('Early re-referencing groups:', '')
217+
for ind, group in enumerate(early_reref.groups):
218+
logging.info(multi_line_list(group, LOGGING_CAPTION_INDENT_LENGTH, ' CAR group ' + str(ind) + ':', 25, ' '))
219+
220+
# check to make sure all included channels are also included in early re-referencing
221+
missing_channels = []
222+
for channel in channels_measured_incl:
223+
if channel not in early_reref.channel_group.keys():
224+
missing_channels.append(channel)
225+
if len(missing_channels) == 1:
226+
logging.error('Channel \'' + missing_channels[0] + '\' is included but cannot be found in any early re-referencing group, make sure the channel has a valid headbox value in the _channels.tsv')
227+
raise RuntimeError('Included channel not in re-referencing group')
228+
elif len(missing_channels) > 1:
229+
logging.error('Channels \'' + ', '.join(missing_channels) + '\' are included but cannot be found in any early re-referencing group, make sure the channels have valid headbox values in the _channels.tsv')
230+
raise RuntimeError('Included channel not in re-referencing group')
231+
232+
233+
# check late re-referencing settings and prepare reref struct
234+
late_reref = None
235+
if cfg('preprocess', 'late_re_referencing', 'enabled'):
236+
237+
if cfg('preprocess', 'late_re_referencing', 'method') == 'CAR_headbox' and not channels_have_headbox:
238+
logging.error('Late re-referencing is set to CAR per headbox, but the _channels.tsv file does not have a \'headbox\' column, exiting...')
239+
raise RuntimeError('No \'headbox\' column in _channels.tsv file, needed to perform late re-referencing per headbox')
240+
241+
if len(channels_late_reref_incl_names) == 0:
242+
logging.info(multi_line_list(channels_late_reref_incl_names, LOGGING_CAPTION_INDENT_LENGTH, 'Channels included (by type) for late re-ref:', 25, ' '))
243+
logging.info(multi_line_list(channels_late_reref_excl_by_type, LOGGING_CAPTION_INDENT_LENGTH, 'Channels excluded by type for late re-ref:', 25, ' '))
244+
logging.error('Late re-referencing is enabled but (after filtering by type) no channels were found, exiting...')
245+
raise RuntimeError('No channels were found for late re-referencing')
246+
247+
# generate a late re-referencing object
248+
if cfg('preprocess', 'late_re_referencing', 'method') == 'CAR':
249+
late_reref = RerefStruct.generate_car(channels_late_reref_incl_names)
250+
elif cfg('preprocess', 'late_re_referencing', 'method') == 'CAR_headbox':
251+
late_reref = RerefStruct.generate_car_per_headbox(channels_late_reref_incl_names, channels_late_reref_incl_headbox)
252+
253+
# print CAR headbox info
254+
logging.info('')
255+
log_indented_line('Late re-referencing groups:', '')
256+
for ind, group in enumerate(late_reref.groups):
257+
logging.info(multi_line_list(group, LOGGING_CAPTION_INDENT_LENGTH, ' CAR group ' + str(ind) + ':', 25, ' '))
258+
259+
# check to make sure all included channels are also included in late re-referencing
260+
missing_channels = []
261+
for channel in channels_measured_incl:
262+
if channel not in late_reref.channel_group.keys():
263+
missing_channels.append(channel)
264+
if len(missing_channels) == 1:
265+
logging.error('Channel \'' + missing_channels[0] + '\' is included but cannot be found in any late re-referencing group, make sure the channel has a valid headbox value in the _channels.tsv')
266+
raise RuntimeError('Included channel not in re-referencing group')
267+
elif len(missing_channels) > 1:
268+
logging.error('Channels \'' + ', '.join(missing_channels) + '\' are included but cannot be found in any late re-referencing group, make sure the channels have valid headbox values in the _channels.tsv')
269+
raise RuntimeError('Included channel not in re-referencing group')
270+
271+
272+
174273
logging.info('')
175274

176275

276+
177277
#
178278
# retrieve trials
179279
#
@@ -283,18 +383,16 @@ def process(bids_subset_data_path, output_dir, preproc_prioritize_speed=False):
283383
logging.error('No stimulus-pairs were found, exiting...')
284384
raise RuntimeError('No stimulus-pairs found')
285385

286-
# prepare some preprocessing variables
287-
early_reref = None
288-
late_reref = None
289-
if cfg('preprocess', 'early_re_referencing', 'enabled'):
290-
291-
# set referencing
292-
early_reref = RerefStruct.generate_car(channels_early_reref_incl)
293-
294-
# set the parts of stimulation (of specific channels) to exclude
386+
# set the parts of stimulation (of specific channels) to exclude from early or late re-referencing
387+
if early_reref is not None:
295388
early_reref.set_exclude_reref_epochs(stim_pairs_onsets,
296389
(cfg('preprocess', 'early_re_referencing', 'stim_excl_epoch')[0], cfg('preprocess', 'early_re_referencing', 'stim_excl_epoch')[1]),
297390
'-')
391+
if late_reref is not None:
392+
late_reref.set_exclude_reref_epochs(stim_pairs_onsets,
393+
(cfg('preprocess', 'late_re_referencing', 'stim_excl_epoch')[0], cfg('preprocess', 'late_re_referencing', 'stim_excl_epoch')[1]),
394+
'-')
395+
logging.info('')
298396

299397

300398
#

erdetect/core/config.py

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
CONFIG_DETECTION_WAVEFORM_PROJ_THRESHOLD = 1000
3030

3131

32-
def __create_default_config():
32+
def create_default_config():
3333
"""
3434
Create and return a config dictionary with default values
3535
@@ -41,12 +41,17 @@ def __create_default_config():
4141

4242
config['preprocess'] = dict()
4343
config['preprocess']['high_pass'] = False #
44-
config['preprocess']['line_noise_removal'] = 'off' #
4544
config['preprocess']['early_re_referencing'] = dict()
4645
config['preprocess']['early_re_referencing']['enabled'] = False #
4746
config['preprocess']['early_re_referencing']['method'] = 'CAR' #
4847
config['preprocess']['early_re_referencing']['stim_excl_epoch'] = (-1.0, 2.0)
49-
config['preprocess']['early_re_referencing']['types'] = ('ECOG', 'SEEG', 'DBS') # the type of channels that will be included for early re-referencing
48+
config['preprocess']['early_re_referencing']['channel_types'] = ('ECOG', 'SEEG', 'DBS') # the type of channels that will be included for early re-referencing
49+
config['preprocess']['line_noise_removal'] = 'off' # TODO: off, json, 60, 50, 60Hz 50Hz
50+
config['preprocess']['late_re_referencing'] = dict()
51+
config['preprocess']['late_re_referencing']['enabled'] = False #
52+
config['preprocess']['late_re_referencing']['method'] = 'CAR' #
53+
config['preprocess']['late_re_referencing']['stim_excl_epoch'] = (-1.0, 2.0)
54+
config['preprocess']['late_re_referencing']['channel_types'] = ('ECOG', 'SEEG', 'DBS') # the type of channels that will be included for late re-referencing
5055

5156
config['trials'] = dict()
5257
config['trials']['trial_epoch'] = (-1.0, 2.0) # the time-span (in seconds) relative to the stimulus onset that will be used to extract the signal for each trial
@@ -175,7 +180,7 @@ def load_config(filepath):
175180
"""
176181

177182
# first retrieve a default config
178-
config = __create_default_config()
183+
config = create_default_config()
179184

180185
# try to read the JSON configuration file
181186
try:
@@ -268,7 +273,7 @@ def retrieve_config_string(json_dict, ref_config, level1, level2, level3=None, o
268273
else:
269274
value_cased = json_dict[level1][level2]
270275
if not case_sensitive:
271-
options = (option.lower() for option in options)
276+
options = [option.lower() for option in options]
272277
value_cased = value_cased.lower()
273278
if value_cased in options:
274279
ref_config[level1][level2] = json_dict[level1][level2]
@@ -287,7 +292,7 @@ def retrieve_config_string(json_dict, ref_config, level1, level2, level3=None, o
287292
else:
288293
value_cased = json_dict[level1][level2][level3]
289294
if not case_sensitive:
290-
options = (option.lower() for option in options)
295+
options = [option.lower() for option in options]
291296
value_cased = value_cased.lower()
292297
if value_cased in options:
293298
ref_config[level1][level2][level3] = json_dict[level1][level2][level3]
@@ -358,6 +363,26 @@ def retrieve_config_tuple(json_dict, ref_config, level1, level2, level3=None, op
358363
# preprocessing settings
359364
if not retrieve_config_bool(json_config, config, 'preprocess', 'high_pass'):
360365
return False
366+
if not retrieve_config_bool(json_config, config, 'preprocess', 'early_re_referencing', 'enabled'):
367+
return False
368+
if not retrieve_config_string(json_config, config, 'preprocess', 'early_re_referencing', 'method', options=('CAR', 'CAR_headbox')):
369+
return False
370+
371+
if not retrieve_config_string(json_config, config, 'preprocess', 'line_noise_removal', options=('off', 'json', '50', '60', '50hz', '60hz')):
372+
return False
373+
if config['preprocess']['line_noise_removal'].lower() == '50hz':
374+
config['preprocess']['line_noise_removal'] = '50'
375+
if config['preprocess']['line_noise_removal'].lower() == '60hz':
376+
config['preprocess']['line_noise_removal'] = '60'
377+
# TODO: load line noise removal, try also to accept a number instead of a string
378+
379+
if not retrieve_config_bool(json_config, config, 'preprocess', 'late_re_referencing', 'enabled'):
380+
return False
381+
if not retrieve_config_string(json_config, config, 'preprocess', 'late_re_referencing', 'method', options=('CAR', 'CAR_headbox')):
382+
return False
383+
# TODO: load early re-referencing channels
384+
# TODO: load late re-referencing channels
385+
361386

362387
# trials settings
363388
if not retrieve_config_range(json_config, config, 'trials', 'trial_epoch'):
@@ -486,11 +511,18 @@ def write_config(filepath):
486511
config_str = '{\n' \
487512
' "preprocess": {\n' \
488513
' "high_pass": ' + ('true' if _config['preprocess']['high_pass'] else 'false') + ',\n' \
489-
' "line_noise_removal": "' + _config['preprocess']['line_noise_removal'] + '",\n' \
490514
' "early_re_referencing": {\n' \
491515
' "enabled": ' + ('true' if _config['preprocess']['early_re_referencing']['enabled'] else 'false') + ',\n' \
492516
' "method": "' + _config['preprocess']['early_re_referencing']['method'] + '",\n' \
493-
' "stim_excl_epoch": [' + numbers_to_padded_string(_config['preprocess']['early_re_referencing']['stim_excl_epoch'], 16) + ']\n' \
517+
' "stim_excl_epoch": [' + numbers_to_padded_string(_config['preprocess']['early_re_referencing']['stim_excl_epoch'], 16) + '],\n' \
518+
' "channel_types": ' + json.dumps(_config['preprocess']['early_re_referencing']['channel_types']) + '\n' \
519+
' },\n' \
520+
' "line_noise_removal": "' + _config['preprocess']['line_noise_removal'] + '",\n' \
521+
' "late_re_referencing": {\n' \
522+
' "enabled": ' + ('true' if _config['preprocess']['late_re_referencing']['enabled'] else 'false') + ',\n' \
523+
' "method": "' + _config['preprocess']['late_re_referencing']['method'] + '",\n' \
524+
' "stim_excl_epoch": [' + numbers_to_padded_string(_config['preprocess']['late_re_referencing']['stim_excl_epoch'], 16) + '],\n' \
525+
' "channel_types": ' + json.dumps(_config['preprocess']['late_re_referencing']['channel_types']) + '\n' \
494526
' }\n' \
495527
' },\n\n' \
496528
' "trials": {\n' \
@@ -719,4 +751,4 @@ def check_range_order(ref_config, level1, level2, level3=None):
719751

720752

721753
# initialize a variable with a default configuration dictionary for this module
722-
_config = __create_default_config()
754+
_config = create_default_config()

0 commit comments

Comments
 (0)