OSCR

Fixation-related potentials reveal that confusing program code elicits a late frontal positivity.

Code ↔ Paper

5 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 5 matches · 1 of them tie a paragraph to a whole file, not to given lines: a weak match, whose lines are not tinted
  1. [1] § Methods › Recording and preprocessing › EEG preprocessing ↔ 09-Task-Evaluate_Data/utils/eeg_helpers.py, lines 572–686 · score 0.72 · Brain Vision, EEG channels, weight, segments, algorithm
  2. [2] § Methods › Recording and preprocessing › Eye-movement recording ↔ 06-Task-Study_Presentation_Software/Experiment_lastrun.py, lines 81–97 · score 0.71 · Tobii Pro Spectrum, Tobii Eye Tracker
  3. [3] § Methods › Recording and preprocessing › Eye-movement preprocessing ↔ 09-Task-Evaluate_Data/utils/I2MC_settings.py, the whole file · a weak match · score 0.70 · I2MC, fixation duration, eye tracking, noise, saccades, algorithm
  4. [4] § Methods › Data analysis › FRP analysis ↔ 09-Task-Evaluate_Data/utils/eeg_helpers.py, lines 755–829 · score 0.67 · baseline corrected, stimulus onset, voltage, segment, amplitude, absolute
  5. [5] § Methods › Recording and preprocessing › EEG preprocessing ↔ 09-Task-Evaluate_Data/utils/path_helpers.py, lines 309–317 · score 0.64 · Brain Vision Analyzer, EEG

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Python · 1,133 lines · 65 KB · CC-BY-4.0 · 2 matches

  1. import gc
  2. import json
  3. import re
  4. from math import log10
  5. from pathlib import Path
  6. from typing import Union
  7. import mne
  8. import numpy as np
  9. import pandas as pd
  10. import seaborn as sns
  11. from matplotlib import pyplot as plt
  12. import matplotlib as mpl
  13. from mne.io import Raw
  14. from tqdm.notebook import tqdm
  15. from utils.eeg_settings import (ACCEPTED_SYNCHRONIZATION_OFFSET, EEG_CHANNELS,
  16. EEG_FREQUENCY, EEG_LONG_BUFFER,
  17. EEG_MEAN_BUFFER, EEG_SHORT_BUFFER,
  18. EEG_STIMULUS, EEG_STIMULUS_FIXATION_CROSS,
  19. EEG_STIMULUS_SNIPPET_END,
  20. EEG_STIMULUS_SNIPPET_START,
  21. EEG_VOLTAGE_OVERALL, EEG_VOLTAGE_STEP,
  22. EEG_VOLTAGE_WINDOW, EOG_CHANNELS, ERP_PARAMETER_CORRECT_TRIALS_ONLY, ERP_PARAMETER_EPOCH_INTERVAL,
  23. FRP_EEG_STIMULUS_SNIPPET_START,
  24. IMPEDANCE_UPPER_BOUND, IMPEDANCE_VALUE, MNE_KEY_FREQUENCY,
  25. STIMULUS_EVENT_NAMES)
  26. from utils.file_helpers import (get_exclusions,
  27. get_participant_folder_per_participant)
  28. from utils.file_settings import (ANNOTATION_COLUMN_DESCRIPTION,
  29. ANNOTATION_COLUMN_ONSET,
  30. ANNOTATION_COLUMN_ONSET_FLOAT,
  31. BEHAVIORAL_COLUMN_CORRECTNESS,
  32. BEHAVIORAL_COLUMN_END,
  33. BEHAVIORAL_COLUMN_FIXATION_START,
  34. BEHAVIORAL_COLUMN_START, COLUMN_TIME,
  35. EEG_COLUMN_STIMULUS, FIXATION_COLUMN_START,
  36. HDF_INDEX, SEPARATOR)
  37. from utils.path_helpers import (get_all_erp_epoch_paths, get_behavioral_data_path, get_erp_average_path,
  38. get_eeg_trial_path, get_erp_epoch_path,
  39. get_erp_fixation_analysis_path,
  40. get_erp_nave_path, get_erp_status_path)
  41. from utils.path_settings import (EEG_FILE_DATA_ENDING, EEG_FILE_HEADER_ENDING,
  42. EEG_FILE_MARKER_ENDING, PROCESSED_PATH)
  43. from utils.snippet_helpers import get_snippet_number, get_snippet_variant
  44. from utils.snippet_settings import (CONDITION, CONDITION_CLEAN,
  45. CONDITION_COLORS, CONDITION_CONFUSING,
  46. CONDITION_DIFF, CONDITION_VARIANT_MATCH,
  47. PANDAS_DESCRIPTION_AGG_FUNCTIONS,
  48. PANDAS_DESCRIPTION_AGG_NAMES, SNIPPET_GROUP_ALL, SNIPPET_NUMBERS)
  49. from utils.textconstants import (BEHAVIORAL, EEG, EEG_ERP, FIXATIONS,
  50. PARTICIPANT, SNIPPET, TIME, TOTAL, VISUAL)
  51. from utils.visual_settings import (FIXATION_SELECTION_ALGORITHM,
  52. FIXATION_SELECTION_ALGORITHMS,
  53. FIXATION_SELECTION_SHORT_VERSION)
  54. def check_file_existence(files: dict[str, Path], file: Path, file_ending: str, participant: str):
  55. '''check whether file has the given extension and there already exists one.
  56. Arguments:
  57. * files: where to add suitable files per ending
  58. * file: the path to check
  59. * file_endings: the file ending to check for
  60. * participant: the participant to name when problems arise
  61. raises: Exception if file with given ending has already been identified
  62. '''
  63. if file.suffix == file_ending:
  64. if file_ending in files:
  65. print(
  66. f'Multiple \'{file_ending}\' files found for participant {participant}: {files[file_ending].name}, new: {file.name}')
  67. raise Exception()
  68. files[file_ending] = file
  69. def get_eeg_files_per_participant() -> dict[str, dict[str, Path]]:
  70. f'''get eeg files (3 files as dictionary) per participant folders identified in the base path
  71. requirement: exactly one eeg file of each file ending per participant (or none at all, then participant is ignored)
  72. returns: the participant numbers, and per each the three eeg files
  73. {{'{EEG_FILE_HEADER_ENDING}': path to the eeg header file,
  74. '{EEG_FILE_MARKER_ENDING}': path to the eeg marker file,
  75. '{EEG_FILE_DATA_ENDING}': path to the eeg data file}}
  76. '''
  77. eeg_files = {}
  78. for participant, participant_folder in get_participant_folder_per_participant().items():
  79. files = {}
  80. for file in participant_folder.iterdir():
  81. check_file_existence(
  82. files, file, EEG_FILE_DATA_ENDING, participant)
  83. check_file_existence(
  84. files, file, EEG_FILE_HEADER_ENDING, participant)
  85. check_file_existence(
  86. files, file, EEG_FILE_MARKER_ENDING, participant)
  87. # check that either all three files exits, or none at all
  88. if ((EEG_FILE_DATA_ENDING in files) ^ (EEG_FILE_HEADER_ENDING in files)) or ((EEG_FILE_DATA_ENDING in files) ^ (EEG_FILE_MARKER_ENDING in files)):
  89. print(f'Not all files found for {participant}: {files}')
  90. return None
  91. # only add if all files exist
  92. if files:
  93. eeg_files[participant] = files
  94. return eeg_files
  95. EEG_HEADER_DATE = re.compile('Impedance \[kOhm\] at (\d\d:\d\d:\d\d) :')
  96. EEG_MARKER_DATE = re.compile(r'New Segment,,(\d+),1,0,(\d+)')
  97. def anonymize_eeg_data(eeg_file: dict[str, Path]):
  98. '''anonymizes eeg files in-place by removing all traces of timestamps in the marker and header files
  99. Argument: eeg_file: a dictionary mapping the eeg file keys to the respective paths of the files
  100. '''
  101. with open(eeg_file[EEG_FILE_HEADER_ENDING], 'r+') as f:
  102. eeg_header_content = f.read()
  103. f.seek(0)
  104. eeg_header_content = EEG_HEADER_DATE.sub(
  105. 'Impedance [kOhm] at the beginning of the experiment :', eeg_header_content)
  106. f.write(eeg_header_content)
  107. f.truncate()
  108. with open(eeg_file[EEG_FILE_MARKER_ENDING], 'r+') as f:
  109. eeg_marker_content = f.read()
  110. f.seek(0)
  111. eeg_marker_content = EEG_MARKER_DATE.sub(
  112. r'New Segment,,\1,1,0,0', eeg_marker_content, count=0)
  113. f.write(eeg_marker_content)
  114. f.truncate()
  115. def load_eeg_data(filepath: Path, preload: bool = True) -> tuple[Raw, float]:
  116. raw_eeg = mne.io.read_raw_brainvision(filepath, eog=tuple(EOG_CHANNELS),
  117. preload=preload)
  118. frequency = raw_eeg.info[MNE_KEY_FREQUENCY]
  119. return raw_eeg, frequency
  120. def check_impedances(impedance_data: pd.DataFrame, log: bool = True) -> bool:
  121. impedance_data[f'{IMPEDANCE_UPPER_BOUND}_check'] = impedance_data.apply(lambda row: max(
  122. 0, row[IMPEDANCE_VALUE]-row[IMPEDANCE_UPPER_BOUND]), 1)
  123. # display(impedance_data)
  124. impedance_errors = ((
  125. impedance_data[f'{IMPEDANCE_UPPER_BOUND}_check'] > 0)*1).sum(), \
  126. impedance_data[f'{IMPEDANCE_UPPER_BOUND}_check'].max(), \
  127. impedance_data[f'{IMPEDANCE_UPPER_BOUND}_check'].sum()
  128. impedance_okay = not (impedance_errors[0] > 2 or
  129. impedance_errors[1] > 5 or
  130. impedance_errors[2] >= 7)
  131. if log:
  132. print(
  133. f'{impedance_errors[0]} errors with impedances, maximum breach of {impedance_errors[1]}, sum of breaches in total {impedance_errors[2]}.\n\tImpedances accepted: {impedance_okay}')
  134. return impedance_okay
  135. def prepare_annotation_information(eeg_data: Raw) -> tuple[pd.DataFrame, float]:
  136. '''extracts and prepares annotation information from the given EEG data
  137. Arguments: eeg_data: the eeg data
  138. returns: a tuple of
  139. * the annotations as DataFrame with columns onset, duration and description extracted from the eeg data, as well as an additional column describing the onset as a float value to be used for cropping, reinsert, ...
  140. * the duration of the eeg data in seconds
  141. '''
  142. # get recording information required+
  143. # offset in seconds between start of the file counter and start of samples
  144. recording_offset = eeg_data.first_time
  145. recording_duration = (eeg_data.n_times-1) / \
  146. eeg_data.info[MNE_KEY_FREQUENCY] # duration of recording
  147. annotation_data = eeg_data.annotations.to_data_frame()
  148. # transform onset of an annotation into a float based on recording start, erasing offset, to use for cropping
  149. annotation_data[ANNOTATION_COLUMN_ONSET_FLOAT] = annotation_data[ANNOTATION_COLUMN_ONSET].apply(
  150. lambda onset: (onset-pd.Timestamp(year=1970, month=1, day=1)).total_seconds()-recording_offset)
  151. return annotation_data, recording_duration
  152. def crop_to_complete_annotation_range(eeg_data: Raw) -> None:
  153. '''crop given Raw object by the annotations present in the object.
  154. The new sequence starts shortly before first fixation cross,
  155. and end shortly after last snippet end, or longer after the last fixation cross
  156. Arguments:
  157. * eeg_data: the eeg data
  158. * time_before_first_snippet: the time buffer to add before the first fixation cross
  159. * time_after_last_snippet: the time buffer to add after the last snippet end if found
  160. * time_constant_without_ending: the time buffer to add after the last fixation cross if no end found
  161. '''
  162. # get required information from annotations
  163. annotation_data, recording_duration = prepare_annotation_information(
  164. eeg_data)
  165. # get first fixation cross or start
  166. start_buffer = {EEG_STIMULUS_FIXATION_CROSS: EEG_SHORT_BUFFER} | {
  167. stimuli: EEG_MEAN_BUFFER for stimuli in EEG_STIMULUS_SNIPPET_START.values()}
  168. snippets_start_row = annotation_data[annotation_data[ANNOTATION_COLUMN_DESCRIPTION].isin(
  169. start_buffer.keys())].iloc[0]
  170. snippets_start_time = snippets_start_row[ANNOTATION_COLUMN_ONSET_FLOAT] - \
  171. start_buffer[snippets_start_row[ANNOTATION_COLUMN_DESCRIPTION]]
  172. snippets_start_time = max(0.0, snippets_start_time)
  173. # get snippet ends and use them to calculate the end of the cropped recording
  174. end_buffer = {EEG_STIMULUS_SNIPPET_END: EEG_SHORT_BUFFER} | {
  175. stimuli: EEG_LONG_BUFFER for stimuli in EEG_STIMULUS_SNIPPET_START.values()}
  176. snippets_end_row = annotation_data[annotation_data[ANNOTATION_COLUMN_DESCRIPTION].isin([
  177. EEG_STIMULUS_FIXATION_CROSS, *EEG_STIMULUS_SNIPPET_START.values(), EEG_STIMULUS_SNIPPET_END])]
  178. if not snippets_end_row.empty:
  179. snippets_end_time = snippets_end_row.iloc[-1][ANNOTATION_COLUMN_ONSET_FLOAT] + \
  180. EEG_MEAN_BUFFER
  181. # otherwise, add a longer buffer to the last fixation
  182. else:
  183. snippets_end_time = min(
  184. recording_duration, snippets_end_row[ANNOTATION_COLUMN_ONSET_FLOAT] +
  185. end_buffer[snippets_end_row[ANNOTATION_COLUMN_DESCRIPTION]])
  186. # crop
  187. assert (recording_duration >= snippets_end_time-snippets_start_time)
  188. eeg_data.crop(tmin=snippets_start_time,
  189. tmax=snippets_end_time, include_tmax=True)
  190. def export_eeg_brainvision(eeg_data: Raw, eeg_path: Path):
  191. assert (eeg_path.suffix == EEG_FILE_HEADER_ENDING)
  192. eeg_data.export(eeg_path, overwrite=True, verbose=False)
  193. eeg_marker_path = eeg_path.with_suffix(EEG_FILE_MARKER_ENDING)
  194. with open(eeg_marker_path, 'r+') as f:
  195. eeg_marker_content = f.read()
  196. f.seek(0)
  197. eeg_marker_content = eeg_marker_content.replace(
  198. 'Comment,Bad Interval/', 'Bad Interval,')
  199. eeg_marker_content = eeg_marker_content.replace(
  200. 'Comment,Bad', 'Bad Interval,Bad')
  201. f.write(eeg_marker_content)
  202. f.truncate()
  203. def check_manual_ICA_reasoning(artifact_reasoning: pd.DataFrame):
  204. # print(artifact_reasoning)
  205. # check that all components are still there
  206. expected_components = set(f'F{str(i).zfill(2)}' for i in range(32))
  207. given_components = set(artifact_reasoning['Component'].values)
  208. assert given_components == expected_components, f'''The components are not correct, \n\twanted {
  209. expected_components},\n\tgiven {given_components}'''
  210. # check that description & topology are filled
  211. assert (artifact_reasoning['Description'].apply(lambda v: not pd.isna(
  212. v)).all()), 'Description must be filled for each component.'
  213. assert (artifact_reasoning['Topology'].apply(lambda v: not pd.isna(
  214. v)).all()), 'Topology must be filled for each component.'
  215. # check that reason is given if artifact... is not false
  216. assert artifact_reasoning.apply(lambda row: not pd.isna(row['Reason']) if row['Artifact or Channel related'] != False else True, axis=1).all(
  217. ), 'Each component identified as possibly being artifact or channel related must have a reason towards the choice of inclusion or not.'
  218. # check that included is not false if artifact... is false
  219. assert artifact_reasoning.apply(lambda row: row['Included'] != False if row['Artifact or Channel related'] == False else True, axis=1).all(
  220. ), 'Each component not identified as possibly being artifact or channel related must be included.'
  221. def assign_trials_to_annotations(eeg_data: Raw, behavioral_events: pd.DataFrame) -> tuple[bool, pd.DataFrame]:
  222. '''Split given Raw object to receive eeq splits per snippet.
  223. Each split starts shortly before first fixation cross,
  224. and end shortly after next snippet end, or until the next fixation cross.
  225. If there is no other way of determination, a long buffer is added to the current fixation crops of the split.
  226. Arguments:
  227. * eeg_data: the eeg data
  228. * sequence_order: the snippet sequence order to assign eeg splits to each snippet
  229. * time_before_first_snippet: the time buffer to add before the first fixation cross
  230. * time_after_last_snippet: the time buffer to add after the last snippet end if found
  231. * time_constant_without_ending: the time buffer to add after the last fixation cross if no end found
  232. returns: eeg split per snippet
  233. '''
  234. annotation_data, _ = prepare_annotation_information(
  235. eeg_data)
  236. annotation_data = annotation_data[annotation_data[ANNOTATION_COLUMN_DESCRIPTION].isin([EEG_STIMULUS_FIXATION_CROSS,
  237. EEG_STIMULUS_SNIPPET_END,
  238. *EEG_STIMULUS_SNIPPET_START.values()])]
  239. def annotation_synchronization_check(data: pd.DataFrame, time_check: bool = False) -> bool:
  240. data['stimuli_check'] = data[ANNOTATION_COLUMN_DESCRIPTION] == data[EEG_COLUMN_STIMULUS]
  241. if time_check:
  242. data['time_check'] = data.apply(lambda row: (row['onset_e'] < 0) or
  243. ((row['onset_a'] - row['onset_e']) < ACCEPTED_SYNCHRONIZATION_OFFSET), axis=1)
  244. if not data['stimuli_check'].all():
  245. return False
  246. if time_check and not data['time_check'].all():
  247. return False
  248. data = data.drop(columns=[c for c in data if (c == 'onset_a') or (
  249. (c != SNIPPET) and (not c in annotation_data.columns))])
  250. return True
  251. print('annotations:', annotation_data.shape[0],
  252. 'behavioral_events:', behavioral_events.shape[0])
  253. annotation_data.reset_index(drop=True, inplace=True)
  254. behavioral_events.reset_index(drop=True, inplace=True)
  255. # possibility 1: match via index if both are the same length
  256. if annotation_data.shape[0] == behavioral_events.shape[0]:
  257. annotation_data['onset_a'] = 0
  258. behavioral_events['onset_e'] = 0
  259. for hdf_index in behavioral_events[HDF_INDEX].unique():
  260. hdf_data = behavioral_events[behavioral_events[HDF_INDEX] == hdf_index]
  261. anno_data = annotation_data[behavioral_events[HDF_INDEX] == hdf_index]
  262. behavioral_events.loc[hdf_data.index,
  263. 'onset_e'] = hdf_data['Time'] - hdf_data['Time'].iloc[0]
  264. annotation_data.loc[anno_data.index, 'onset_a'] = anno_data[ANNOTATION_COLUMN_ONSET_FLOAT] - \
  265. anno_data[ANNOTATION_COLUMN_ONSET_FLOAT].iloc[0]
  266. com_data = pd.concat([behavioral_events, annotation_data], axis=1)
  267. if annotation_synchronization_check(com_data):
  268. return True, com_data
  269. test_data = pd.concat([behavioral_events, annotation_data], axis=1)
  270. # possibility 2: match via time based on first event / annotation
  271. # com_data = pd.DataFrame([], columns = [c for c in test_data.columns])
  272. # annotations_index, behavioral_index=0, 0
  273. # annotation_data['onset_a'] = 0
  274. # behavioral_events['onset_e'] = 0
  275. # for hdf_index in behavioral_events[HDF_INDEX].unique():
  276. # hdf_data = behavioral_events[behavioral_events[HDF_INDEX]==hdf_index]
  277. # anno_data = annotation_data[behavioral_events[HDF_INDEX]==hdf_index]
  278. # behavioral_events.loc[hdf_data.index, 'onset_e'] = hdf_data['Time'] - hdf_data['Time'].iloc[0]
  279. # annotation_data.loc[anno_data.index, 'onset_a'] = anno_data[ANNOTATION_COLUMN_ONSET_FLOAT] - anno_data[ANNOTATION_COLUMN_ONSET_FLOAT].iloc[0]
  280. # while (annotations_index<annotation_data.shape[0] and behavioral_index<behavioral_events.shape[0]):
  281. # pass
  282. # if (not com_data.empty) and annotation_synchronization_check(com_data):
  283. # return True, com_data
  284. # possibility 3: match via time based on last event / annotation
  285. # com_data = pd.DataFrame([], columns = [c for c in test_data.columns])
  286. # annotations_index, behavioral_index=annotation_data.shape[0]-1, behavioral_events.shape[0]-1
  287. # while (annotations_index>=0 and behavioral_index<behavioral_events>=0):
  288. # if (not com_data.empty) and annotation_synchronization_check(com_data):
  289. # return True, com_data
  290. # possibility 4: match via time diff based on events / annotation
  291. # com_data = pd.DataFrame([], columns = [c for c in test_data.columns])
  292. # if (not com_data.empty) and annotation_synchronization_check(com_data):
  293. # return True, com_data
  294. return False, test_data
  295. def check_trial_annotations(trial_annotation_data: pd.DataFrame, eeg_data: Raw):
  296. # print(trial_annotation_data)
  297. annotation_data, _ = prepare_annotation_information(eeg_data)
  298. annotation_data = annotation_data[annotation_data[ANNOTATION_COLUMN_DESCRIPTION].isin([EEG_STIMULUS_FIXATION_CROSS,
  299. EEG_STIMULUS_SNIPPET_END,
  300. *EEG_STIMULUS_SNIPPET_START.values()])]
  301. data1 = (trial_annotation_data[[ANNOTATION_COLUMN_DESCRIPTION, ANNOTATION_COLUMN_ONSET_FLOAT]].dropna(axis='index')
  302. .sort_values([ANNOTATION_COLUMN_ONSET_FLOAT]).reset_index(drop=True))
  303. data2 = (annotation_data[[ANNOTATION_COLUMN_DESCRIPTION, ANNOTATION_COLUMN_ONSET_FLOAT]]
  304. .sort_values([ANNOTATION_COLUMN_ONSET_FLOAT]).reset_index(drop=True))
  305. # check that all annotations covered by a line
  306. assert data1[ANNOTATION_COLUMN_DESCRIPTION].equals(
  307. data2[ANNOTATION_COLUMN_DESCRIPTION]), 'All annotations must be present.'
  308. data1[ANNOTATION_COLUMN_ONSET_FLOAT] = data1[ANNOTATION_COLUMN_ONSET_FLOAT].round(
  309. 3)
  310. data2[ANNOTATION_COLUMN_ONSET_FLOAT] = data2[ANNOTATION_COLUMN_ONSET_FLOAT].round(
  311. 3)
  312. time_delta = (data2[ANNOTATION_COLUMN_ONSET_FLOAT] -
  313. data1[ANNOTATION_COLUMN_ONSET_FLOAT]).abs().ge(1.5/EEG_FREQUENCY)
  314. assert not time_delta.any(), f'All annotations must be present with their given frame'
  315. # check that all annotations, that are not fixation crosses, have an assigned snippet
  316. assert (trial_annotation_data.apply(lambda row: (pd.isna(row[ANNOTATION_COLUMN_DESCRIPTION]) or row[ANNOTATION_COLUMN_DESCRIPTION] == EEG_STIMULUS_FIXATION_CROSS) or pd.notna(
  317. row[SNIPPET]), axis=1).all()), "All annotations that are not fixation crosses require an assigned event."
  318. # check that stimuli identical
  319. assert (trial_annotation_data.dropna(axis='index').apply(lambda row: row[EEG_COLUMN_STIMULUS] == row[ANNOTATION_COLUMN_DESCRIPTION], axis=1).all(
  320. )), 'The stimuli of event and annotation must be identical'
  321. # check time synchronization between events and annotations work within a hdf file index
  322. for hdf_index in trial_annotation_data[HDF_INDEX].unique():
  323. if pd.isna(hdf_index):
  324. continue
  325. hdf_trials = trial_annotation_data[trial_annotation_data[HDF_INDEX] == hdf_index].dropna(
  326. axis='index')
  327. if hdf_trials.empty:
  328. continue
  329. hdf_trials['Time delta beh eeg'] = hdf_trials[ANNOTATION_COLUMN_ONSET_FLOAT] - hdf_trials[TIME]
  330. assert (hdf_trials['Time delta beh eeg'].max() - hdf_trials['Time delta beh eeg'].min() < 0.3), \
  331. f'''The difference between eeg annotation timestamp and behavioral event timestamp should remain within a second of time.\n {
  332. hdf_trials["Time delta beh eeg"]}'''
  333. def get_synchronized_annotations(trial_annotation_data: pd.DataFrame, behavioral_data: pd.DataFrame) -> pd.DataFrame:
  334. annotations_to_delete = []
  335. # check isna and print na lines to delete
  336. na_rows = trial_annotation_data[trial_annotation_data.isna().any(axis=1)]
  337. if not na_rows.empty:
  338. print(f'\tThese rows will be deleted (ignored) due to nas in the rows')
  339. print(na_rows)
  340. relevant_trial_annotation_data = trial_annotation_data.dropna(axis='index')
  341. for i, row in relevant_trial_annotation_data.iterrows():
  342. behavioral_row = behavioral_data[behavioral_data[SNIPPET] == row[SNIPPET]].squeeze(
  343. )
  344. if behavioral_row.empty:
  345. annotations_to_delete.append(i)
  346. continue
  347. if row[ANNOTATION_COLUMN_DESCRIPTION] == EEG_STIMULUS_FIXATION_CROSS:
  348. behavioral_time = behavioral_row[BEHAVIORAL_COLUMN_FIXATION_START]
  349. elif row[ANNOTATION_COLUMN_DESCRIPTION] in EEG_STIMULUS_SNIPPET_START.values():
  350. behavioral_time = behavioral_row[BEHAVIORAL_COLUMN_START]
  351. elif row[ANNOTATION_COLUMN_DESCRIPTION] == EEG_STIMULUS_SNIPPET_END:
  352. behavioral_time = behavioral_row[BEHAVIORAL_COLUMN_END]
  353. if abs(row[TIME] - behavioral_time) > 0.0001:
  354. annotations_to_delete.append(i)
  355. print(
  356. f'\tAnnotation {row} ignored even though assigned behavioral event, as the behavioral time is expected to be {behavioral_time} from {behavioral_row[[SNIPPET, PARTICIPANT, BEHAVIORAL_COLUMN_FIXATION_START, BEHAVIORAL_COLUMN_START, BEHAVIORAL_COLUMN_END]]}.')
  357. relevant_trial_annotation_data = relevant_trial_annotation_data[~relevant_trial_annotation_data.index.isin(
  358. annotations_to_delete)]
  359. return relevant_trial_annotation_data
  360. def transform_synchronized_annotations(trial_annotation_data: pd.DataFrame) -> pd.DataFrame:
  361. eeg_snippet_data = trial_annotation_data.pivot(
  362. columns=EEG_COLUMN_STIMULUS, index=SNIPPET, values=ANNOTATION_COLUMN_ONSET_FLOAT)
  363. eeg_snippet_data[BEHAVIORAL_COLUMN_FIXATION_START] = eeg_snippet_data[EEG_STIMULUS_FIXATION_CROSS]
  364. eeg_snippet_data[BEHAVIORAL_COLUMN_START] = eeg_snippet_data.apply(
  365. lambda row: [row[stimulus] for stimulus in EEG_STIMULUS_SNIPPET_START.values() if not pd.isna(row[stimulus])][0], axis=1)
  366. eeg_snippet_data[BEHAVIORAL_COLUMN_END] = eeg_snippet_data[EEG_STIMULUS_SNIPPET_END]
  367. eeg_snippet_data = eeg_snippet_data[[
  368. BEHAVIORAL_COLUMN_FIXATION_START, BEHAVIORAL_COLUMN_START, BEHAVIORAL_COLUMN_END]]
  369. return eeg_snippet_data
  370. def split_eeg_segments(eeg_data: Raw, trial_annotations: pd.DataFrame) -> dict[str, Raw]:
  371. '''Split given Raw object to receive eeg segments per snippet.
  372. Each split starts shortly before first fixation cross,
  373. and end shortly after next snippet end, or until the next fixation cross.
  374. If there is no other way of determination, a long buffer is added to the current fixation crops of the split.
  375. Arguments:
  376. * eeg_data: the eeg data
  377. * sequence_order: the snippet sequence order to assign eeg segments to each snippet
  378. * time_before_first_snippet: the time buffer to add before the first fixation cross
  379. * time_after_last_snippet: the time buffer to add after the last snippet end if found
  380. * time_constant_without_ending: the time buffer to add after the last fixation cross if no end found
  381. returns: eeg split per snippet
  382. '''
  383. eeg_segments = {}
  384. for snippet, row in trial_annotations.iterrows():
  385. start = 0
  386. if not pd.isna(row[BEHAVIORAL_COLUMN_FIXATION_START]):
  387. start = row[BEHAVIORAL_COLUMN_FIXATION_START]
  388. elif not pd.isna(row[BEHAVIORAL_COLUMN_START]):
  389. start = row[BEHAVIORAL_COLUMN_START]-5.0
  390. else:
  391. print(
  392. f'ignored snippet {snippet} of row {row} due to missing start')
  393. continue
  394. start = max(0, start)
  395. end = 0
  396. if not pd.isna(row[BEHAVIORAL_COLUMN_END]):
  397. end = row[BEHAVIORAL_COLUMN_END]
  398. else:
  399. end = start+EEG_LONG_BUFFER
  400. end = min(end, eeg_data.times[-1])
  401. if start >= end:
  402. print(start, end)
  403. eeg_segment_data: Raw = eeg_data.copy().crop(
  404. tmin=start, tmax=end, include_tmax=True)
  405. # remove unneeded annotations
  406. segment_annotations = eeg_segment_data.annotations.to_data_frame()
  407. segment_annotations['unnecessary'] = ~segment_annotations[ANNOTATION_COLUMN_DESCRIPTION].isin(
  408. list(EEG_STIMULUS_SNIPPET_START.values())+[EEG_STIMULUS_FIXATION_CROSS, EEG_STIMULUS_SNIPPET_END])
  409. eeg_segment_data.annotations.delete(
  410. segment_annotations[segment_annotations['unnecessary']].index)
  411. # add begin annotation to add snippet name to file
  412. eeg_segment_data.annotations.append(
  413. start, 1/EEG_FREQUENCY, f'SNIPPET {snippet}')
  414. eeg_segments[snippet] = eeg_segment_data
  415. return eeg_segments
  416. def check_voltage_per_segment(eeg_segments: dict[str, Raw]) -> pd.DataFrame:
  417. snippet_violation_data = pd.DataFrame(index=[snippet for snippet in eeg_segments],
  418. columns=['Voltage Step Count', 'Voltage Step Channels',
  419. 'Voltage Step Frames', 'Voltage Difference Count',
  420. 'Voltage Difference Channels', 'Voltage Difference Frames'], dtype=object)
  421. for snippet in eeg_segments:
  422. eeg_segment = eeg_segments[snippet]
  423. snippet_violations = check_voltage_in_segment(eeg_segment)
  424. for key in snippet_violations:
  425. snippet_violation_data.at[snippet, key] = snippet_violations[key]
  426. return snippet_violation_data
  427. def check_voltage_in_segment(eeg_segment: Raw, is_epoch: bool = False) -> pd.DataFrame:
  428. snippet_violations = {}
  429. assert eeg_segment.info[MNE_KEY_FREQUENCY] == EEG_FREQUENCY
  430. # check voltage steps
  431. eeg_content_data: np.ndarray = eeg_segment.get_data(
  432. picks=EEG_CHANNELS,units='uV')
  433. if is_epoch:
  434. eeg_content_data = eeg_content_data[0]
  435. # * voltage steps >= 30µV/1ms --> (or 60µV/2ms ?)
  436. voltage_step = np.diff(eeg_content_data, 1)
  437. abs_voltage_step = np.abs(voltage_step)
  438. high_voltage_step = abs_voltage_step >= EEG_VOLTAGE_STEP
  439. has_high_voltage_step = np.any(high_voltage_step)
  440. if has_high_voltage_step:
  441. count_high_voltage_step = np.sum(high_voltage_step*1)
  442. snippet_violations['Voltage Step Count'] = count_high_voltage_step
  443. channel_high_voltage_step = np.sum(high_voltage_step*1, -1)
  444. snippet_violations['Voltage Step Channels'] = [
  445. {channel: channel_high_voltage_step[i] for i, channel in enumerate(EEG_CHANNELS) if channel_high_voltage_step[i] > 0}]
  446. time_high_voltage_step = np.sum(high_voltage_step*1, 0)
  447. frame_high_voltage_step = np.nonzero(time_high_voltage_step)[0]
  448. snippet_violations['Voltage Step Frames'] = [
  449. {frame: time_high_voltage_step[frame] for frame in frame_high_voltage_step}]
  450. starts = [eeg_segment.first_time + f /
  451. EEG_FREQUENCY for f in frame_high_voltage_step]
  452. eeg_segment.annotations.append(
  453. starts, 1/EEG_FREQUENCY, 'BAD Voltage step')
  454. # plot_eeg(eeg_segment, True, True)
  455. del count_high_voltage_step
  456. del time_high_voltage_step
  457. if is_epoch:
  458. return snippet_violations
  459. del voltage_step
  460. del abs_voltage_step
  461. del high_voltage_step
  462. # * voltage difference > 100 µV within 0.2 s
  463. voltage_windows = np.lib.stride_tricks.sliding_window_view(
  464. eeg_content_data, 101, -1)
  465. min_voltage_windows = np.min(voltage_windows, 2)
  466. max_voltage_windows = np.max(voltage_windows, 2)
  467. high_difference_voltage = (
  468. max_voltage_windows-min_voltage_windows) > EEG_VOLTAGE_WINDOW
  469. has_high_difference_voltage = np.any(high_difference_voltage)
  470. if has_high_difference_voltage:
  471. count_high_difference_voltage = np.sum(high_difference_voltage*1)
  472. snippet_violations['Voltage Difference Count'] = count_high_difference_voltage
  473. channel_high_difference_voltage = np.sum(
  474. high_difference_voltage*1, -1)
  475. snippet_violations['Voltage Difference Channels'] = [
  476. {channel: channel_high_difference_voltage[i] for i, channel in enumerate(EEG_CHANNELS) if channel_high_difference_voltage[i] > 0}]
  477. time_high_difference_voltage = np.sum(high_difference_voltage*1, 0)
  478. frame_high_difference_voltage = np.nonzero(
  479. time_high_difference_voltage)[0]
  480. # snippet_violation_data.loc[snippet,'Voltage Difference Frames'] =[{frame:time_high_difference_voltage[frame] for frame in frame_high_difference_voltage.flat}]
  481. first_time = eeg_segment.tmin if is_epoch else eeg_segment.first_time
  482. starts = [first_time + f /
  483. EEG_FREQUENCY for f in frame_high_difference_voltage]
  484. last_time = eeg_segment.tmax if is_epoch else eeg_segment._last_time
  485. durations = [
  486. min(s+100/EEG_FREQUENCY, last_time)-s for s in starts]
  487. eeg_segment.annotations.append(
  488. starts, durations, 'BAD Voltage difference')
  489. # print(eeg_segment.annotations.to_data_frame())
  490. # plot_eeg(eeg_segment, True, True)
  491. del count_high_difference_voltage
  492. del time_high_difference_voltage
  493. del voltage_windows
  494. del min_voltage_windows
  495. del max_voltage_windows
  496. del high_difference_voltage
  497. return snippet_violations
  498. def check_voltage_amplitude(epochs) -> bool:
  499. # * greater absolute amplitude difference than 140 µV --> or if baseline corrected, then within +/-70 µV
  500. eeg_content_data = epochs[0].get_data(picks=EEG_CHANNELS,units='uV')[0]
  501. min_overall_voltage = np.min(eeg_content_data, (0, 1))
  502. max_overall_voltage = np.max(eeg_content_data, (0, 1))
  503. if (min_overall_voltage < EEG_VOLTAGE_OVERALL[0]) or (max_overall_voltage > EEG_VOLTAGE_OVERALL[1]):
  504. return True
  505. return False
  506. def perform_eeg_erp_averaging(participants: list[str], erp_frp: bool | str = True, correct_data_only: bool = False, epoch_interval: tuple[int, int] = (-0.2, 1),
  507. conditional_stimuli: dict[str, str] = EEG_STIMULUS_SNIPPET_START, topomap_times: list[float] = [0.2, 0.3, 0.4, 0.5, 0.6, 0.8, 1],
  508. plot: bool = False, snippet_group: str = SNIPPET_GROUP_ALL, snippet_numbers: list[int] = SNIPPET_NUMBERS) -> tuple[dict[str, dict[str, mne.Evoked], dict[str, mne.Evoked]]]:
  509. assert (erp_frp is True or erp_frp in FIXATION_SELECTION_ALGORITHMS), erp_frp
  510. assert epoch_interval[0] < epoch_interval[1]
  511. assert all([t >= epoch_interval[0] and t <= epoch_interval[1]
  512. for t in topomap_times])
  513. description = get_erp_description(
  514. erp_frp, correct_data_only, epoch_interval)
  515. subjectwise_nave = {}
  516. subjectwise_averages = {}
  517. subjectwise_frp_offsets = {}
  518. for participant in tqdm(participants):
  519. print('----------------------------------------------')
  520. print(PARTICIPANT, participant)
  521. # skip participant if excluded
  522. exclusions = get_exclusions(participant, [PARTICIPANT], [
  523. BEHAVIORAL, EEG, VISUAL])[PARTICIPANT]
  524. if any(exclusions.values()):
  525. print('excluded')
  526. continue
  527. # Load behavioral data
  528. if correct_data_only:
  529. behavioral_data = pd.read_csv(get_behavioral_data_path(
  530. participant, final_data_exclusion=True), index_col=False, sep=SEPARATOR, dtype={PARTICIPANT: str})
  531. correct_snippets = behavioral_data[behavioral_data[BEHAVIORAL_COLUMN_CORRECTNESS]][SNIPPET].to_list()
  532. else:
  533. correct_snippets = None
  534. # Load raw data for all snippets
  535. snippet_segments = get_all_eeg_trial_segments(
  536. erp_frp == True, participant, correct_trials=correct_snippets, accepted_snippet_numbers=snippet_numbers)
  537. # Transform annotations to event to epoch and apply baseline correction
  538. snippet_epochs, frp_fixation_offsets = extract_epochs_from_snippet_segments(erp_frp, description, participant, snippet_group,
  539. snippet_segments, conditional_stimuli.values(), epoch_interval, (epoch_interval[0], 0), True, True)
  540. frp_fixation_offsets[PARTICIPANT] = participant
  541. subjectwise_frp_offsets[participant] = frp_fixation_offsets
  542. # Concatenate epochs
  543. snippet_groups = [list(snippet_epochs.keys())]
  544. group_epochs = mne.concatenate_epochs([snippet_epochs[snippet] for snippet in snippet_groups[0]], add_offset=True).pick(
  545. picks=EEG_CHANNELS).set_montage("easycap-M1")
  546. # Calculate and plot subjectwise average per condition
  547. averaged_evoked: mne.Evoked = group_epochs.average(by_event_type=True)
  548. averaged_evoked = {ev.comment: ev for ev in averaged_evoked}
  549. averaged_evoked = {
  550. condition: averaged_evoked[conditional_stimuli[condition]] for condition in conditional_stimuli}
  551. assert (averaged_evoked[CONDITION_CLEAN].comment == conditional_stimuli[CONDITION_CLEAN]) and (averaged_evoked[CONDITION_CONFUSING].comment == conditional_stimuli[CONDITION_CONFUSING]), \
  552. f'''{averaged_evoked[CONDITION_CLEAN].comment} should be {conditional_stimuli[CONDITION_CLEAN]} and {
  553. averaged_evoked[CONDITION_CONFUSING].comment} should be {conditional_stimuli[CONDITION_CONFUSING]}'''
  554. subjectwise_averages[participant] = averaged_evoked
  555. subjectwise_nave[participant] = {
  556. condition: averaged_evoked[condition].nave for condition in averaged_evoked}
  557. if plot:
  558. plot_all_evoked_low_frequency(erp_frp, description, averaged_evoked,
  559. participant, topomap_times, False)
  560. # comparison to BrainVision results
  561. # ae_c,_ = load_eeg_data(f'E:/PHD/Studies/aoc-frp-main-studies/Main_Study_Part/08-Data-Trial_Recordings/prepared_EEG_files/ERP subjectwise averages/AoCfrp_{participant}__averaged_confusing.vhdr', preload=True)
  562. # ae_c_events, ae_c_event_dict = mne.events_from_annotations(ae_c, {'Time 0/':11})
  563. # ae_c_epochs = mne.Epochs(ae_c, ae_c_events, tmin=ERP_INTERVAL[0]+1/EEG_FREQUENCY, tmax=ERP_INTERVAL[1]-1/EEG_FREQUENCY, event_id=ae_c_event_dict, preload=True, baseline=(ERP_INTERVAL[0]+1/EEG_FREQUENCY,0))
  564. # ae_c_averaged_epochs =ae_c_epochs.average()
  565. # plot_evoked(ae_c_averaged_epochs)
  566. # ae_nc,_ = load_eeg_data(f'E:/PHD/Studies/aoc-frp-main-studies/Main_Study_Part/08-Data-Trial_Recordings/prepared_EEG_files/ERP subjectwise averages/AoCfrp_{participant}__averaged_non_confusing.vhdr', preload=True)
  567. # ae_nc_events, ae_nc_event_dict = mne.events_from_annotations(ae_nc, {'Time 0/':12})
  568. # ae_nc_epochs = mne.Epochs(ae_nc, ae_nc_events, tmin=ERP_INTERVAL[0]+1/EEG_FREQUENCY, tmax=ERP_INTERVAL[1]-1/EEG_FREQUENCY, event_id=ae_nc_event_dict, preload=True, baseline=(ERP_INTERVAL[0]+1/EEG_FREQUENCY,0))
  569. # ae_nc_averaged_epochs =ae_nc_epochs.average()
  570. # plot_evoked(ae_nc_averaged_epochs)
  571. # Calculate and plot subjectwise difference wave
  572. diff_wave = mne.combine_evoked(
  573. [averaged_evoked[CONDITION_CONFUSING], averaged_evoked[CONDITION_CLEAN]], weights=[1, -1])
  574. subjectwise_averages[participant][CONDITION_DIFF] = diff_wave
  575. if plot:
  576. plot_all_evoked_low_frequency(erp_frp, description, {
  577. CONDITION_DIFF: diff_wave}, participant, topomap_times, False)
  578. # Save subjectwise averages
  579. for condition in averaged_evoked:
  580. averaged_evoked[condition].save(get_erp_average_path(
  581. erp_frp, snippet_group, description, participant, condition), overwrite=True)
  582. # save included snippets, offsets
  583. frp_offset_data = pd.concat(subjectwise_frp_offsets.values())
  584. frp_offset_data[ERP_PARAMETER_EPOCH_INTERVAL] = [
  585. epoch_interval for _ in range(frp_offset_data.shape[0])]
  586. frp_offset_data[ERP_PARAMETER_CORRECT_TRIALS_ONLY] = correct_data_only
  587. if erp_frp != True:
  588. frp_offset_data[FIXATION_SELECTION_ALGORITHM] = erp_frp
  589. frp_offset_data.to_csv(get_erp_fixation_analysis_path(
  590. erp_frp, snippet_group, description, 'erp frp offset'), sep=SEPARATOR, index=False)
  591. # statistics and plot distribution (best in other method)
  592. if erp_frp != True:
  593. statistics_distribution(erp_frp, snippet_group, description,
  594. frp_offset_data, 'erp frp offset', 'Delay to stimulus onset')
  595. # Save subjectwise naves
  596. nave_data = pd.DataFrame.from_dict(subjectwise_nave, 'index')
  597. nave_data.to_csv(get_erp_nave_path(
  598. erp_frp, snippet_group, description), sep=SEPARATOR)
  599. # Calculate and plot grand averages per condition
  600. grand_averages = {}
  601. for condition in [CONDITION_CONFUSING, CONDITION_CLEAN]:
  602. grand_average = mne.grand_average(
  603. [subjectwise_averages[participant][condition] for participant in subjectwise_averages])
  604. grand_averages[condition] = grand_average
  605. if plot:
  606. plot_all_evoked_low_frequency(erp_frp, description, grand_averages,
  607. TOTAL, topomap_times, False)
  608. # Calculate and plot grand averages difference wave
  609. diff_wave = mne.combine_evoked(
  610. [grand_averages[CONDITION_CONFUSING], grand_averages[CONDITION_CLEAN]], weights=[1, -1])
  611. grand_averages[CONDITION_DIFF] = diff_wave
  612. if plot:
  613. plot_all_evoked_low_frequency(erp_frp, description, {
  614. CONDITION_DIFF: diff_wave}, TOTAL, topomap_times, False)
  615. # Save grand averages
  616. for condition in grand_averages:
  617. grand_averages[condition].save(get_erp_average_path(
  618. erp_frp, snippet_group, description, TOTAL, condition=condition), overwrite=True)
  619. return subjectwise_averages, grand_averages, subjectwise_nave
  620. def get_erp_description(erp_frp: str, correct_data_only: bool, epoch_interval: tuple[int, int]) -> str:
  621. description = f'{"erp" if erp_frp is True else FIXATION_SELECTION_SHORT_VERSION[erp_frp]}_{int(epoch_interval[0]*1000)}_{int(epoch_interval[1]*1000)}_{"correct" if correct_data_only else "all"}'
  622. return description
  623. def get_stimulus_number(stimuli: list[str] = EEG_STIMULUS) -> dict[str, int]:
  624. '''get stimulus number (event number) per recognized stimulus (used in description of annotations)
  625. returns: stimulus text and number per recognized stimulus'''
  626. return {stimulus: int(stimulus[-3:]) for stimulus in stimuli}
  627. def get_event_name_numbers(given_stimuli: list[str]) -> dict[str, int]:
  628. '''get event name per event number for each recognized stimulus (used in description of annotations)
  629. Arguments: given_stimuli: the stimuli to return
  630. returns: event name and event number per recognized stimulus'''
  631. given_stimuli = [
  632. stimulus for stimulus in given_stimuli if stimulus in EEG_STIMULUS]
  633. stimulus_numbers = get_stimulus_number()
  634. return {STIMULUS_EVENT_NAMES[stimulus]: stimulus_numbers[stimulus] for stimulus in given_stimuli}
  635. def get_all_eeg_trial_segments(erp_frp: bool | str, participant: str, correct_trials: list[str] = None, accepted_snippet_numbers: list[int] = None) -> dict[str, Raw]:
  636. '''load eeg trial segments for participant
  637. Arguments:
  638. * erp_frp: whether it is erp or a certain type of frp
  639. * participant: the participant to get segments for
  640. * visual_exclude: whether to exclude based on visual as well (required for FRP)
  641. returns: the non-excluded trial segments for this participant
  642. '''
  643. assert (erp_frp in [True, False]), erp_frp
  644. modes = [BEHAVIORAL, EEG]
  645. if not erp_frp:
  646. modes.append(VISUAL)
  647. exclusions = get_exclusions(participant, [SNIPPET], modes)[SNIPPET]
  648. snippet_segments: dict[str, Raw] = {}
  649. for snippet in exclusions:
  650. if any(exclusions[snippet].values()):
  651. # print(f'\t{snippet} was excluded due to {exclusions[snippet]}')
  652. continue
  653. # if correct only (correct trials given) and snippet not correctly answered
  654. if not (correct_trials is None) and not (snippet in correct_trials):
  655. # print(f'\t{snippet} was excluded due to being answered incorrectly')
  656. continue
  657. # if correct only (correct trials given) and snippet not correctly answered
  658. if not (accepted_snippet_numbers is None) and not (get_snippet_number(snippet) in accepted_snippet_numbers):
  659. # print(f'\t{snippet} was excluded due to not being in the group')
  660. continue
  661. eeg_data, _ = load_eeg_data(get_eeg_trial_path(
  662. erp_frp is True, participant, snippet))
  663. snippet_segments[snippet] = eeg_data
  664. return snippet_segments
  665. def plot_epoch(eeg_data: mne.Epochs):
  666. return eeg_data.plot(EEG_CHANNELS, n_epochs=1, events=eeg_data.events)
  667. def round_time_EEG(time: float, frequency=EEG_FREQUENCY):
  668. return round(time, int(round(log10(frequency), 0))+1)
  669. def extract_epochs_from_snippet_segments(erp_frp: bool | str, description: str, participant: str, snippet_group: str, snippet_segments: dict[str, Raw], condition_stimuli: list[str],
  670. epoch_interval: tuple[int, int], baseline_interval: tuple[int, int] = None, perform_voltage_checks: bool = True, save_epoch_data: bool = True):
  671. regarded_stimuli = get_stimulus_number(condition_stimuli)
  672. snippet_epochs = {}
  673. frp_fixation_offsets = pd.DataFrame([], columns=[
  674. PARTICIPANT, CONDITION, SNIPPET, 'Stimulus Onset', 'Fixation Onset', 'Delay to stimulus onset'])
  675. snippet_status = {}
  676. # calculate epoch
  677. for snippet in snippet_segments:
  678. try:
  679. events, event_dict = mne.events_from_annotations(
  680. snippet_segments[snippet], regarded_stimuli)
  681. except ValueError:
  682. if erp_frp != True:
  683. print(
  684. f'''{snippet}: No stimulus of {list(condition_stimuli)} found in annotations in {snippet_segments[snippet].annotations.to_data_frame()[ANNOTATION_COLUMN_DESCRIPTION].values}''')
  685. snippet_status[snippet] = 'No stimulus found, fixation data of this trials likely did not contain any fixation fulfilling the requirements for this FRP calculation.'
  686. continue
  687. else:
  688. raise ValueError(
  689. f'''In ERP, all stimuli must be found. No stimulus of {list(condition_stimuli)} found in annotations for {snippet} {snippet_segments[snippet].annotations.to_data_frame()[ANNOTATION_COLUMN_DESCRIPTION].values}''')
  690. # create epoch and perform baseline correction if specified
  691. if baseline_interval is None:
  692. epochs = mne.Epochs(snippet_segments[snippet], events, tmin=epoch_interval[0],
  693. tmax=epoch_interval[1], event_id=event_dict, preload=True)
  694. else:
  695. epochs = mne.Epochs(snippet_segments[snippet], events, tmin=epoch_interval[0], tmax=epoch_interval[1],
  696. event_id=event_dict, preload=True, baseline=(baseline_interval[0], baseline_interval[1]))
  697. # check whether epoch really exists (not too short)
  698. if erp_frp != True and len(epochs) < 1 and 'TOO_SHORT' in epochs.drop_log[0]:
  699. anno = snippet_segments[snippet].annotations.to_data_frame()
  700. relevant_stimuli = anno[anno[ANNOTATION_COLUMN_DESCRIPTION].isin([EEG_STIMULUS_SNIPPET_END, *EEG_STIMULUS_SNIPPET_START.values(), *condition_stimuli])]
  701. snippet_status[snippet] = f'Data for existing stimuli too short, {relevant_stimuli}'
  702. print(
  703. f'''{snippet}: Data for existing stimuli {list(event_dict.keys())[0]} too short {epochs.drop_log[0]} {relevant_stimuli}''')
  704. continue
  705. assert len(epochs) == 1
  706. # exclude based on all previously marked violations
  707. if perform_voltage_checks:
  708. if check_voltage_amplitude(epochs):
  709. print(f'\t{snippet} excluded due to overall voltage violation')
  710. # plot_epoch(epochs)
  711. snippet_segments[snippet].close()
  712. snippet_status[snippet] = 'Voltage violation absolute of segment'
  713. continue
  714. if check_voltage_in_segment(epochs, True):
  715. print(
  716. f'\t{snippet} excluded due to voltage violation inside epoch')
  717. # plot_epoch(epochs)+-
  718. snippet_segments[snippet].close()
  719. snippet_status[snippet] = 'Voltage violation in interval of segment'
  720. continue
  721. snippet_status[snippet] = 'Included'
  722. snippet_epochs[snippet] = epochs
  723. # calculate frp offset to erp
  724. annotation_data, _ = prepare_annotation_information(
  725. snippet_segments[snippet])
  726. erp_onset = annotation_data[annotation_data[ANNOTATION_COLUMN_DESCRIPTION].isin(
  727. EEG_STIMULUS_SNIPPET_START.values())][ANNOTATION_COLUMN_ONSET_FLOAT].values[0]
  728. if erp_frp != True:
  729. frp_onset = annotation_data[annotation_data[ANNOTATION_COLUMN_DESCRIPTION].isin(
  730. condition_stimuli)][ANNOTATION_COLUMN_ONSET_FLOAT].values[0]
  731. frp_fixation_offsets.loc[frp_fixation_offsets.shape[0]] = [None, CONDITION_VARIANT_MATCH[get_snippet_variant(
  732. snippet)], snippet, round_time_EEG(erp_onset), round_time_EEG(frp_onset), round_time_EEG(frp_onset-erp_onset)]
  733. else:
  734. frp_fixation_offsets.loc[frp_fixation_offsets.shape[0]] = [None, CONDITION_VARIANT_MATCH[get_snippet_variant(
  735. snippet)], snippet, round_time_EEG(erp_onset), round_time_EEG(erp_onset), .0]
  736. with open(get_erp_status_path(erp_frp, snippet_group, description, participant), 'w') as f:
  737. json.dump(snippet_status, f, indent=4, sort_keys=True)
  738. if save_epoch_data:
  739. # save all epochs
  740. for snippet, epoch in snippet_epochs.items():
  741. epoch.save(get_erp_epoch_path(erp_frp, snippet_group, description,
  742. participant, snippet), fmt='double', overwrite=True)
  743. return snippet_epochs, frp_fixation_offsets
  744. def plot_all_evoked(erp_frp: bool | str, snippet_group, description: str, conditional_evoked: dict[str, mne.Evoked], participant: str = TOTAL, topomap_times: list[float] = [0.2, 0.3, 0.4, 0.5, 0.6, 0.8, 1], show: bool = True):
  745. assert (erp_frp is True or erp_frp in FIXATION_SELECTION_ALGORITHMS), erp_frp
  746. for condition, evoked in conditional_evoked.items():
  747. fig = evoked.plot(picks='eeg', show=show,
  748. window_title=condition, time_unit='ms')
  749. fig.savefig(get_erp_average_path(erp_frp, snippet_group, description,
  750. participant, condition, 'butterfly'))
  751. plt.close()
  752. for condition, evoked in conditional_evoked.items():
  753. fig1 = evoked.plot_topomap(times=[min(
  754. time, evoked.tmax) for time in topomap_times], time_unit='ms', show=False)
  755. # TODO: hier Daten für Topoplots abgreifen
  756. fig1.suptitle(f'Topomap {description}')
  757. fig1.savefig(get_erp_average_path(erp_frp, snippet_group, description,
  758. participant, condition, 'topomap'))
  759. fig1.show()
  760. if erp_frp!=True:
  761. fig2 = evoked.plot_topomap(times=[min(
  762. round_time_EEG(time+0.0255, 10000), evoked.tmax) for time in topomap_times],average=0.05199, time_unit='ms', show=False)
  763. fig2_path = get_erp_average_path(erp_frp, snippet_group, description,
  764. participant, condition, 'topomap_averaged_50ms')
  765. for i, ax in enumerate(fig2.get_axes()[:-1]):
  766. title:str = ax.get_title()[:-3]
  767. start, end = [int(i)/1000 for i in title.split(' – ')]
  768. new_title = fig1.axes[i].get_title()
  769. data = evoked.copy().crop(tmin=start, tmax=end).to_data_frame(index='time', time_format='ms')
  770. data.index.name='Time (ms)'
  771. data.to_csv(fig2_path.with_name(f'Data Figure2b amplitudes {new_title} interval.csv'), sep=SEPARATOR)
  772. ax.set_title(new_title)
  773. fig2.suptitle(f'Topomap {description}')
  774. fig2.savefig(fig2_path)
  775. fig2.show()
  776. else:
  777. fig2 = evoked.plot_topomap(times=[min(
  778. round_time_EEG(time+0.1015, 10000), evoked.tmax) for time in topomap_times],average=0.201, time_unit='ms', show=False)
  779. fig2_path = get_erp_average_path(erp_frp, snippet_group, description,
  780. participant, condition, 'topomap_averaged_200ms')
  781. for i, ax in enumerate(fig2.get_axes()[:-1]):
  782. title:str = ax.get_title()[:-3]
  783. start, end = [int(i)/1000 for i in title.split(' – ')]
  784. new_title = fig1.axes[i].get_title()
  785. data = evoked.copy().crop(tmin=start, tmax=end).to_data_frame(index='time', time_format='ms')
  786. data.index.name='Time (ms)'
  787. data.to_csv(fig2_path.with_name(f'Data Figure3b amplitudes {new_title} interval.csv'), sep=SEPARATOR)
  788. ax.set_title(new_title)
  789. fig2.suptitle(f'Topomap {description}')
  790. fig2.savefig(fig2_path)
  791. fig2.show()
  792. plt.close('all')
  793. if all([condition in CONDITION_COLORS for condition in conditional_evoked]):
  794. colors = {condition: CONDITION_COLORS[condition]
  795. for condition in conditional_evoked}
  796. else:
  797. colors = None
  798. fig = mne.viz.plot_compare_evokeds(
  799. conditional_evoked, show_sensors=True, title=f'Topographic comparison {description}', axes='topo', show=show, colors=colors, time_unit='ms')
  800. fig[0].savefig(get_erp_average_path(erp_frp, snippet_group, description, participant, (condition if len(
  801. conditional_evoked) == 1 else TOTAL), f'topo_channels'))
  802. plt.close()
  803. minimum, maximum = [], []
  804. for condition, evoked in conditional_evoked.items():
  805. eeg_data = evoked.get_data(EEG_CHANNELS,units='uV')
  806. minimum.append(np.min(eeg_data))
  807. maximum.append(np.max(eeg_data))
  808. minimum, maximum = min(minimum), max(maximum) # from volt to microvolt scale
  809. for channel in EEG_CHANNELS:
  810. fig = mne.viz.plot_compare_evokeds(conditional_evoked, picks=channel, title=f'Electrode {channel}', show=show, colors=colors,
  811. show_sensors=False,
  812. # for identical scaling
  813. ylim={'eeg': (minimum, maximum)}, time_unit='ms', )
  814. fig[0].get_axes()[0].get_legend().remove()
  815. fig_path = get_erp_average_path(erp_frp, snippet_group, description, participant, (condition if len(
  816. conditional_evoked) == 1 else TOTAL), f'channel_{channel}')
  817. fig[0].savefig(fig_path)
  818. plt.close()
  819. if CONDITION_DIFF in conditional_evoked:
  820. return
  821. fig_data_path = get_erp_average_path(erp_frp, snippet_group, description, participant, (condition if len(
  822. conditional_evoked) == 1 else TOTAL), f'data').with_suffix('.csv')
  823. for channel in EEG_CHANNELS:
  824. if channel[0] in 'FCP' and channel[1] in '34z':
  825. data = []
  826. for condition in conditional_evoked:
  827. cond_data = conditional_evoked[condition].to_data_frame(channel, index='time', time_format='ms')
  828. cond_data.columns = [condition]
  829. data.append(cond_data)
  830. data = pd.concat(data, axis=1)
  831. data.index.name = 'Time (ms)'
  832. data.to_csv(fig_data_path.with_stem(f'Data Figure{3 if erp_frp==True else 2}a {channel} conditional amplitude'), sep=SEPARATOR)
  833. def plot_all_evoked_low_frequency(erp_frp: bool | str, snippet_group: str, description: str, conditional_evoked: dict[str, mne.Evoked], participant: str = TOTAL, topomap_times: list[float] = [0.2, 0.3, 0.4, 0.5, 0.6, 0.8, 1], show: bool = False):
  834. # plot_all_evoked(erp_frp, description, conditional_evoked, participant, topomap_times, show)
  835. conditional_evoked = {condition: evoked.copy().resample(
  836. 20) for condition, evoked in conditional_evoked.items()}
  837. plot_all_evoked(erp_frp, snippet_group,
  838. f'{description}_20Hz', conditional_evoked, participant, topomap_times, show)
  839. def statistics_distribution(erp_frp: bool | str, snippet_group: str, description: str, fixation_analysis_data: pd.DataFrame, analysis_topic: str, analysis_column: str):
  840. fixation_analysis_data[f'{analysis_column} (ms)'] = fixation_analysis_data[analysis_column]*1000
  841. analysis_column = f'{analysis_column} (ms)'
  842. conditional_offset = fixation_analysis_data[[CONDITION, analysis_column]].groupby(
  843. [CONDITION]).agg({analysis_column: PANDAS_DESCRIPTION_AGG_FUNCTIONS})
  844. conditional_offset.columns = PANDAS_DESCRIPTION_AGG_NAMES
  845. conditional_offset.to_csv(get_erp_fixation_analysis_path(
  846. erp_frp, snippet_group, f'{description}_statistics', analysis_topic), sep=SEPARATOR, decimal=',')
  847. fig, axis = plt.subplots(1, 1, figsize=(8, 3))
  848. plt.rcParams.update({'font.size': 12})
  849. # , palette=[CONDITION_COLORS[CONDITION_CLEAN], CONDITION_COLORS[CONDITION_CONFUSING]])
  850. sns.violinplot(fixation_analysis_data, x=analysis_column, y=CONDITION, legend=False, inner="box", cut=0, ax=axis)
  851. plt.tight_layout()
  852. plt.savefig(get_erp_fixation_analysis_path(erp_frp, snippet_group,
  853. f'{description}_statistics', analysis_topic).with_suffix('.pdf'), bbox_inches='tight', pad_inches=0)
  854. plt.savefig(get_erp_fixation_analysis_path(erp_frp, snippet_group,
  855. f'{description}_statistics', analysis_topic).with_suffix('.png'), bbox_inches='tight', pad_inches=0)
  856. plt.close()
  857. participant_conditional_offset = fixation_analysis_data[[PARTICIPANT, CONDITION, analysis_column]].groupby(
  858. [PARTICIPANT, CONDITION]).agg({analysis_column: PANDAS_DESCRIPTION_AGG_FUNCTIONS})
  859. participant_conditional_offset.columns = PANDAS_DESCRIPTION_AGG_NAMES
  860. participant_conditional_offset.to_csv(get_erp_fixation_analysis_path(
  861. erp_frp, snippet_group, f'{description}_participant_statistics', analysis_topic), sep=SEPARATOR, decimal=',')
  862. fig, axis = plt.subplots(1, 1, figsize=(8, 24))
  863. plt.rcParams.update({'font.size': 12})
  864. sns.violinplot(fixation_analysis_data, x=analysis_column,
  865. y=PARTICIPANT, hue=CONDITION, inner="stick", cut=0, ax=axis)
  866. plt.tight_layout()
  867. plt.savefig(get_erp_fixation_analysis_path(erp_frp, snippet_group,
  868. f'{description}_participant_statistics', analysis_topic).with_suffix('.pdf'), bbox_inches='tight', pad_inches=0)
  869. plt.savefig(get_erp_fixation_analysis_path(erp_frp, snippet_group,
  870. f'{description}_participant_statistics', analysis_topic).with_suffix('.png'), bbox_inches='tight', pad_inches=0)
  871. plt.close()
  872. return conditional_offset, participant_conditional_offset
  873. def load_all_erp_averages(erp_frp: bool | str, snippet_group: str, correct_data_only: bool, epoch_interval: tuple[int, int], subjectwise: bool,
  874. participants: list[str], grand: bool, conditional: bool, diff: bool) -> Union[tuple[dict[str, dict[str, mne.Evoked], dict[str, mne.Evoked]]], dict[str, dict[str, mne.Evoked]], dict[str, mne.Evoked]]:
  875. assert (erp_frp is True or erp_frp in FIXATION_SELECTION_ALGORITHMS), erp_frp
  876. assert ((not subjectwise) or (len(participants) > 0)
  877. ), "if subjectwise participants are required, send with the participants to use"
  878. assert (subjectwise or grand), "Subjectwise or grand or both must be chosen"
  879. description = get_erp_description(
  880. erp_frp, correct_data_only, epoch_interval)
  881. conditions = []
  882. if conditional:
  883. conditions.extend([CONDITION_CONFUSING, CONDITION_CLEAN])
  884. if diff:
  885. conditions.append(CONDITION_DIFF)
  886. if grand:
  887. grand_averages = {}
  888. for condition in conditions:
  889. path = get_erp_average_path(
  890. erp_frp, snippet_group, description, condition=condition)
  891. grand_averages[condition] = mne.read_evokeds(path)[0]
  892. if not subjectwise:
  893. return grand_averages
  894. if subjectwise:
  895. subjectwise_averages = {}
  896. for participant in participants:
  897. averages = {}
  898. for condition in conditions:
  899. path = get_erp_average_path(
  900. erp_frp, snippet_group, description, participant, condition=condition)
  901. averages[condition] = mne.read_evokeds(path)[0]
  902. subjectwise_averages[participant] = averages
  903. if not grand:
  904. return subjectwise_averages
  905. return subjectwise_averages, grand_averages
  906. def load_all_erp_epochs(erp_frp: bool | str, snippet_group: str, description: str, participants: list[str]) -> dict[str, dict[str, mne.Epochs]]:
  907. assert (erp_frp is True or erp_frp in FIXATION_SELECTION_ALGORITHMS), erp_frp
  908. subjectwise_epochs = {}
  909. for participant in participants:
  910. snippet_epochs = {}
  911. snippet_epoch_paths = get_all_erp_epoch_paths(
  912. erp_frp, snippet_group, description, participant)
  913. for snippet, epoch_path in snippet_epoch_paths.items():
  914. snippet_epochs[snippet] = mne.read_epochs(
  915. epoch_path, proj=True, preload=True, verbose=None)
  916. subjectwise_epochs[participant] = snippet_epochs
  917. return subjectwise_epochs
  918. def plot_eeg(eeg_data: Raw, plot_annotations: bool, plot_data: bool) -> None:
  919. '''plot eeg data or its events.
  920. Each split starts shortly before first fixation cross,
  921. and end shortly after next snippet end, or until the next fixation cross.
  922. If there is no other way of determination, a long buffer is added to the current fixation crops of the split.
  923. Arguments:
  924. * eeg_data: the eeg data
  925. * plot_annotations: whether to plot the annotations as events
  926. * plot_data: plot the eeg data
  927. '''
  928. events, event_id = mne.events_from_annotations(
  929. eeg_data, get_stimulus_number())
  930. event_dict = get_event_name_numbers(event_id.keys())
  931. event_color = {3: 'r', 4: 'b', 11: 'g', 12: 'y', }
  932. print('Events ID:', event_id, event_dict)
  933. if plot_data:
  934. eeg_data.plot(events=events, start=0, duration=30, color='gray', event_color={k: event_color[k] for k in event_color if k in event_id.values()},
  935. )
  936. # prepare data
  937. # for erp_parameters in tqdm(all_erp_parameter_combinations):
  938. def plot_waveforms(erp_frp: bool | str, snippet_group: str, correct_data_only: bool, epoch_interval: tuple[float, float], subjectwise: bool, grand: bool, participants: list[str], topomap_times: list[float]):
  939. assert (erp_frp is True or erp_frp in FIXATION_SELECTION_ALGORITHMS), erp_frp
  940. description = get_erp_description(
  941. erp_frp, correct_data_only, epoch_interval)
  942. print(description)
  943. data = load_all_erp_averages(erp_frp, snippet_group, correct_data_only, epoch_interval,
  944. subjectwise=subjectwise, participants=participants, grand=grand, diff=True, conditional=True)
  945. if subjectwise and grand:
  946. subjectwise_data, total_data = data
  947. elif subjectwise:
  948. subjectwise_data = data
  949. elif grand:
  950. total_data = data
  951. if subjectwise:
  952. for participant in tqdm(participants):
  953. plot_all_evoked(erp_frp, snippet_group, description, {condition: subjectwise_data[participant][condition] for condition in [CONDITION_CLEAN, CONDITION_CONFUSING]},
  954. participant, topomap_times, False)
  955. plot_all_evoked(erp_frp, snippet_group, description, {CONDITION_DIFF: subjectwise_data[participant][CONDITION_DIFF]},
  956. participant, topomap_times, False)
  957. del subjectwise_data
  958. if grand:
  959. plot_all_evoked(erp_frp, snippet_group, description, {condition: total_data[condition] for condition in [CONDITION_CLEAN, CONDITION_CONFUSING]},
  960. TOTAL, topomap_times, False)
  961. plot_all_evoked(erp_frp, snippet_group, description, {CONDITION_DIFF: total_data[CONDITION_DIFF]},
  962. TOTAL, topomap_times, False)
  963. for channel in EEG_CHANNELS:
  964. fig, axis = plt.subplots(1, 1, figsize=(8, 3))
  965. plt.rcParams.update({'font.size': 12})
  966. mne.viz.plot_compare_evokeds(total_data, picks=channel, # show_sensors=True,
  967. title=f'Electrode {channel}', show=False, colors=CONDITION_COLORS, time_unit='ms', axes=axis, truncate_yaxis=False)
  968. fig.savefig(get_erp_average_path(erp_frp, snippet_group, description, TOTAL, 'all',
  969. f'channel_{channel}').with_suffix('.pdf'), bbox_inches='tight', pad_inches=0)
  970. plt.clf()
  971. plt.close()
  972. total_data[CONDITION_DIFF].plot_topomap(times=[0.400, 0.450, 0.500, 0.550, 0.600, 0.650, 0.700], show=False, time_unit='ms')
  973. fig.savefig(get_erp_average_path(erp_frp, snippet_group, description,
  974. TOTAL, CONDITION_DIFF, 'topomap').with_suffix('.pdf'), bbox_inches='tight', pad_inches=0)
  975. plt.close()
  976. total_data.pop(CONDITION_DIFF)
  977. # plt.figure(figsize=(16, 16))
  978. fig = mne.viz.plot_compare_evokeds(
  979. {condition:data.copy().crop(tmax=min(data.tmax, 1.0), include_tmax=True) for condition, data in total_data.items()}, picks=['F3','Fz', 'F4','C3','Cz', 'C4','P3','Pz', 'P4', 'F7','F8'],
  980. show_sensors=True, title=f'Topographic comparison with 9 crucial electrodes {description}', axes='topo',
  981. colors={condition:CONDITION_COLORS[condition] for condition in total_data}, time_unit='ms', )
  982. # fig[0].tight_layout()
  983. fig[0].savefig(get_erp_average_path(erp_frp, snippet_group, description, TOTAL, 'all', 'topo_9_channels_1sec').with_suffix('.pdf'))#, bbox_inches='tight', pad_inches=0)
  984. fig = mne.viz.plot_compare_evokeds(
  985. total_data, picks=['F3','Fz', 'F4','C3','Cz', 'C4','P3','Pz', 'P4', 'F7','F8'],
  986. show_sensors=True, title=f'Topographic comparison with 9 crucial electrodes {description}', axes='topo', show=False,
  987. colors={condition:CONDITION_COLORS[condition] for condition in total_data}, time_unit='ms', )
  988. # fig[0].tight_layout()
  989. fig[0].savefig(get_erp_average_path(erp_frp, snippet_group, description, TOTAL, 'all', 'topo_9_channels').with_suffix('.pdf'))#, bbox_inches='tight', pad_inches=0)
  990. plt.close()
  991. del total_data
  992. gc.collect()
  993. def add_frp_marker_by_special_fixations(participant: str, snippets: list[str], behavioral_data: pd.DataFrame, special_fixation_data: pd.DataFrame, eeg_trials: dict[str, mne.io.Raw]) -> float:
  994. for snippet in snippets:
  995. snippet_behavioral_start = behavioral_data[behavioral_data[SNIPPET] == snippet].squeeze(
  996. )[BEHAVIORAL_COLUMN_START]
  997. eeg_segment_data: mne.io.Raw = eeg_trials[snippet]
  998. snippet_condition = CONDITION_VARIANT_MATCH[get_snippet_variant(
  999. snippet)]
  1000. eeg_annotations, _ = prepare_annotation_information(eeg_segment_data)
  1001. eeg_start = eeg_annotations[eeg_annotations[ANNOTATION_COLUMN_DESCRIPTION]
  1002. == EEG_STIMULUS_SNIPPET_START[snippet_condition]].squeeze()[ANNOTATION_COLUMN_ONSET_FLOAT]
  1003. snippet_special_fixation_data: pd.DataFrame = special_fixation_data[
  1004. special_fixation_data[SNIPPET] == snippet]
  1005. for f_a in snippet_special_fixation_data[FIXATION_SELECTION_ALGORITHM].unique():
  1006. fixation_start = snippet_special_fixation_data[snippet_special_fixation_data[FIXATION_SELECTION_ALGORITHM] == f_a].squeeze(
  1007. )[FIXATION_COLUMN_START]
  1008. eeg_fixation_start = transform_eye_to_eeg(fixation_start,
  1009. snippet_behavioral_start, eeg_start)
  1010. eeg_segment_data.annotations.append(eeg_fixation_start, 1/EEG_FREQUENCY,
  1011. FRP_EEG_STIMULUS_SNIPPET_START[f_a][snippet_condition])
  1012. export_eeg_brainvision(
  1013. eeg_segment_data, get_eeg_trial_path(False, participant, snippet))
  1014. # transform eye-tracking timestamp to eeg frame
  1015. def transform_eye_to_eeg(eye_timestamp: float, eye_start_time: float, eeg_start_time: float) -> float:
  1016. '''transforms the eye-tracking timestamp into an eeg frame
  1017. Arguments:
  1018. * eye_timestamp: timestamp of eye-tracking to transform (in seconds with milliseconds floating precision)
  1019. * eye_start_time: timestamp of eye-tracking marking the starting point (in seconds with milliseconds floating precision)
  1020. * eeg_start_time: frame that corresponds to the eye_start timestamp
  1021. * eeg_sampling_rate: the frequency of frames logged in the data (frames per second)
  1022. returns: the frame corresponding to the eye_timestamp
  1023. '''
  1024. eye_offset = eye_timestamp-eye_start_time
  1025. eeg_timestamp = eeg_start_time+eye_offset
  1026. return eeg_timestamp

eeg_helpers.py at commit d84bf83, under CC-BY-4.0 · at the source

Overview

Authors: Annabelle Bergum1, Anna-Maria Maurer1, Norman Peitek1, Regine Bader2, Axel Mecklinger2, Vera Demberg1,3, Janet Siegmund4, Sven Apel1
  1. Computer Science, Saarland University, Saarbrücken, Germany
  2. Psychology, Saarland University, Saarbrücken, Germany
  3. Language Science and Technology, Saarland University, Saarbrücken, Germany
  4. Computer Science, University of Technology Chemnitz, Chemnitz, Germany
Institutions: Saarland University (Germany); Chemnitz University of Technology (Germany)
Journal: Scientific reports, volume 16, issue 1, article 16833
Dates: received 23 January 2025; accepted 24 April 2026; published online 1 June 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1038/s41598-026-50946-9 · PMID 42225689 · PMCID PMC13226682 · OpenAlex W4405433354
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: EEG (modality), human (organism), methods / tools (subfield)
Methods: Spectral & time-frequency, Preprocessing, Statistics, Smoothing, state filtering, decompositions, Evoked potentials, Physiology & signal measures
Keywords: Program comprehension, Natural language comprehension, Atoms of confusion, EEG, Fixation-related potentials, Late frontal positivity, Software, Computer science, Psychology, Human behaviour
MeSH: Evoked Potentials*, Software*, Comprehension, Electroencephalography, Humans, Natural Language Processing (* major topic)
Topic: Software Engineering Research (Information Systems, Computer Science), according to OpenAlex
Funding: European Research Council (101052182)
Citations: not cited yet (Europe PMC); 70 references in the paper

Abstract

As software pervades more and more areas of our professional and personal lives, there is an ever-increasing need to maintain software and for programmers to efficiently write and understand program code. In the first study of its kind, we analyze fixation-related potentials (FRPs) to explore the online processing of program code patterns that are confusing to programmers, but not to the computer (so-called atoms of confusion), and their underlying neurocognitive mechanisms in an ecologically valid setting. Relative to clean counterparts in program code without an atom of confusion, confusing code elicits a late frontal positivity of about 400 to 700 ms after first looking at the atom of confusion. This frontal positivity resembles an event-related potential (ERP) component found during natural language processing that is elicited by unexpected but plausible words in sentence context. Thus, we suggest that the brain engages similar neurocognitive mechanisms in response to unexpected and informative inputs in program code and in natural language. In both domains, these inputs update a comprehender’s situation model, which is essential for information extraction from a quickly unfolding input. Our results have far-reaching implications for programming and pave the way for interdisciplinary collaborations between software engineering and psycholinguistics.

Reproduced under the paper's license (CC BY), from the paper cited above.

Repositories

Its files are read in the Code ↔ Paper reader above, with 5 matches between paragraphs and lines of code.

Zenodo 14229848

License: CC-BY-4.0
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Size: 8 files
Software Heritage: not checked
Found in: “Data availability”
Holds: README
Not found: license file, CITATION.cff, environment file, tests, continuous integration, documentation
Availability: 1 check, the latest on 27 September 2026: the link answers (HTTP 200)
  • 27 September 2026: the link answers (HTTP 200)
1 file
At the source:

brains-on-code/AoC-FRP-Code

License: CC-BY-4.0
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: d84bf83e386dc631437efe80e7d5ac75098a51c8, 8 January 2025
Languages: Java (145), Jupyter (39), Python (26)
Size: 611 files, 210 scripts
Software Heritage: not checked
Found in: “Materials availability”
Holds: README, license file, CITATION.cff, environment (requirements.txt), 39 notebooks
Not found: tests, continuous integration, documentation
Tools: pandas (30 files), NumPy (17 files), Matplotlib (9 files), SciPy (9 files), MNE-Python (7 files), seaborn (3 files), h5py (2 files), OpenCV (2 files), lme4 (1 file), lmerTest (1 file), nlme (1 file), Pillow (1 file), PsychoPy (1 file), rpy2 (1 file), scikit-learn (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
212 files

Materials availability

This experiment used code available in Python (v. 3.11.5), as well as R (v. 4.3.2) for linear mixed effect models. Additionally, we used other open-source (PsychoPy, v. 2021.2.3) and commercial applications (Tobii Eye-Tracker Manager (v. 2.6.0), BrainVision Recorder (v. 1.20.0801), and BrainVision Analyzer (v. 2.3.0.8300)) to perform the experiment. We deposited the scripts and content in a GitHub Repository https://github.com/brains-on-code/AoC-FRP-Code and they are available for unrestricted open access under a CC-BY license.

Reproduced under the paper's license (CC BY), from the paper cited above.

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 2 repositories of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 210 scripts, each with its path and the digest of its content;
  • 5 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

Datasets cited

Data Availability Statement

Data availability We provide all relevant data in line with open data principles under a CC-BY license, respecting our local privacy laws (GDPR). Specifically, the datasets collected during the experiment and generated for the analysis are long-term archived in the Zenodo repository Dataset for “Fixation-related potentials reveal that confusing program code elicits a late frontal positivity”: 10.5281/zenodo.14229848

This experiment used code available in Python (v. 3.11.5), as well as R (v. 4.3.2) for linear mixed effect models. Additionally, we used other open-source (PsychoPy, v. 2021.2.3) and commercial applications (Tobii Eye-Tracker Manager (v. 2.6.0), BrainVision Recorder (v. 1.20.0801), and BrainVision Analyzer (v. 2.3.0.8300)) to perform the experiment. We deposited the scripts and content in a GitHub Repository https://github.com/brains-on-code/AoC-FRP-Code and they are available for unrestricted open access under a CC-BY license.

Reproduced under the paper's license (CC BY), from the paper cited above.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 1, 27 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 8 authors, 10 keywords, 6 MeSH terms, 1 funder, 32 references.

Cite

This paper

Bergum, A., Maurer, A.-M., Peitek, N., Bader, R., Mecklinger, A., Demberg, V., Siegmund, J., & Apel, S. (2026). Fixation-related potentials reveal that confusing program code elicits a late frontal positivity. Scientific reports, 16(1), 16833. https://doi.org/10.1038/s41598-026-50946-9

BibTeX

@article{bergum2026fixation,
author = {Bergum, Annabelle and Maurer, Anna-Maria and Peitek, Norman and Bader, Regine and Mecklinger, Axel and Demberg, Vera and Siegmund, Janet and Apel, Sven},
title = {{Fixation-related potentials reveal that confusing program code elicits a late frontal positivity}},
journal = {Scientific reports},
year = {2026},
month = jun,
volume = {16},
number = {1},
pages = {16833},
publisher = {Nature Publishing Group},
issn = {2045-2322},
doi = {10.1038/s41598-026-50946-9},
url = {https://doi.org/10.1038/s41598-026-50946-9},
pmid = {42225689},
pmcid = {PMC13226682}
}

RIS

TY - JOUR
AU - Bergum, Annabelle
AU - Maurer, Anna-Maria
AU - Peitek, Norman
AU - Bader, Regine
AU - Mecklinger, Axel
AU - Demberg, Vera
AU - Siegmund, Janet
AU - Apel, Sven
TI - Fixation-related potentials reveal that confusing program code elicits a late frontal positivity
T2 - Scientific reports
J2 - Sci Rep
PY - 2026
DA - 2026/06/01
VL - 16
IS - 1
SP - 16833
SN - 2045-2322
PB - Nature Publishing Group
DO - 10.1038/s41598-026-50946-9
UR - https://doi.org/10.1038/s41598-026-50946-9
LA - en
ER -

CSL-JSON

{
"id": "10.1038/s41598-026-50946-9",
"type": "article-journal",
"title": "Fixation-related potentials reveal that confusing program code elicits a late frontal positivity",
"container-title": "Scientific reports",
"author": [
{
"family": "Bergum",
"given": "Annabelle"
},
{
"family": "Maurer",
"given": "Anna-Maria"
},
{
"family": "Peitek",
"given": "Norman"
},
{
"family": "Bader",
"given": "Regine"
},
{
"family": "Mecklinger",
"given": "Axel"
},
{
"family": "Demberg",
"given": "Vera"
},
{
"family": "Siegmund",
"given": "Janet"
},
{
"family": "Apel",
"given": "Sven"
}
],
"container-title-short": "Sci Rep",
"volume": "16",
"issue": "1",
"page": "16833",
"DOI": "10.1038/s41598-026-50946-9",
"PMID": "42225689",
"PMCID": "PMC13226682",
"ISSN": "2045-2322",
"publisher": "Nature Publishing Group",
"URL": "https://doi.org/10.1038/s41598-026-50946-9",
"language": "en",
"issued": {
"date-parts": [
[
2026,
6,
1
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1016/j.xcrm.2026.102766 [code]
A longitudinal single-cell and spatial multiomic atlas of pediatric high-grade glioma.
Journal: Cell reports. Medicine
In common: rpy2, lmerTest, lme4, 9 other tools
[2] doi:10.34133/csbj.0042 [code]
Using Steady-State Visual Evoked Potentials to Characterize Wide-Ranging Retinopathy Linked to &lt;i&gt;CRB1&lt;/i&gt;: Implications for Clinical Trials.
Journal: Computational and structural biotechnology journal
In common: PsychoPy, MNE-Python, lmerTest, 8 other tools, EEG
[3] doi:10.1038/s41598-026-41129-7 [code]
Non-linear relationships between auditory mismatch responses and the inharmonicity of complex sounds.
Journal: Scientific reports
In common: PsychoPy, nlme, MNE-Python, 7 other tools, EEG
[4] doi:10.1038/s41597-025-05174-7 [code]
A large-scale MEG and EEG dataset for object recognition in naturalistic scenes
Journal: n/a
In common: MNE-Python, OpenCV, h5py, 7 other tools, methods / tools, EEG
[5] doi:10.21203/rs.3.rs-9676637/v1 [code]
A Comprehensive Benchmarking of Spatial Deconvolution and Domain Detection Methods across Diverse Tissues and Spatial Transcriptomic Technologies
Journal: Research Square (preprint)
In common: rpy2, OpenCV, h5py, 7 other tools, methods / tools
[6] doi:10.1038/s42003-026-10957-8 [code]
Brain defence by the extracellular matrix protein Cochlin.
Journal: Communications biology
In common: nlme, lme4, OpenCV, 7 other tools
[7] doi:10.7554/elife.108673 [code]
Adaptive behavior is guided by integrated representations of controlled and non-controlled information.
Journal: eLife
In common: rpy2, MNE-Python, lme4, 6 other tools, EEG
[8] doi:10.1117/1.nph.13.2.025001 [code]
Surface-based image reconstruction optimization for high-density functional near-infrared spectroscopy.
Journal: Neurophotonics
In common: MNE-Python, OpenCV, h5py, 7 other tools
[9] doi:10.1093/cercor/bhag113 [code]
Long-term reliability and stability of parameterized resting state EEG: evidence from a five-year follow-up.
Journal: Cerebral cortex (New York, N.Y. : 1991)
In common: MNE-Python, lmerTest, lme4, 6 other tools, methods / tools, EEG
[10] doi:10.1371/journal.pone.0343722 [code]
Comprehensive methodology for sample enrichment in EEG biomarker studies for Alzheimer's risk classification.
Journal: PloS one
In common: rpy2, MNE-Python, Pillow, 6 other tools, EEG

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.