OSCR

Trial-level sequence modeling reveals hidden dynamics of dual-task interference.

Code ↔ Paper

6 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 6 matches
  1. [1] § 2. Methods › 2.3. Data collection and preprocessing ↔ preprocessing/old/Template_preprocessing.ipynb, lines 91–102 · score 0.66 · notch filter, 1–100 Hz, pass filtered, preprocessing, EEG
  2. [2] § 2. Methods › 2.3. Data collection and preprocessing ↔ preprocessing/old/preprocessing.ipynb, lines 96–107 · score 0.66 · notch filter, 1–100 Hz, pass filtered, preprocessing, EEG
  3. [3] § 2. Methods › 2.7. Sequence analysis ↔ s4/4_analysis_combined.ipynb, lines 560–642 · score 0.64 · sampled sequences, predicted probabilities, probability distributions, posterior, weighted
  4. [4] § 3. Results › 3.2 Cross-decoding cognitive operations across condition ↔ s4/7_embeddings_visu.ipynb, lines 156–257 · score 0.54 · absolute cosine, error bars, CI, distance, embedding
  5. [5] § 2. Methods › 2.4. Hidden multivariate pattern analysis ↔ s4/prp2/2_estimation.ipynb, lines 44–71 · score 0.53 · PCA weights, event width, 50 Hz, training, fit, SOA
  6. [6] § 3. Results › 3.3. Decoding the sequence of cognitive operations across tasks ↔ s4/4_analysis_r.ipynb, lines 205–231 · score 0.50 · Holm correction, pairwise contrasts, accuracy, RT

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

Jupyter notebook · 506 lines · 21 KB · no license · 1 match

  1. # %% [markdown]
  2. # # An example of EEG preprocessing with MNE python
  3. # %% [markdown]
  4. # This is an example on how to preprocess EEG data with [MNE](https://mne.tools/stable/index.html). Obviously there are many ways to preprocess EEG data, this template is intended for people using [Hidden semi-Markov Models with pattern analysis (HMP)](https://github.com/GWeindel/hsmm_mvpy) but others might find it useful.
  5. #
  6. # I usually preprocess EEG in a notebook using MNE and save one notebook per participant I prepreocessed. This allows me to later retrieve the information on what particular steps were taken for each individual and also eventually change some steps without having to run all the notebook again.
  7. # %%
  8. name_subj = "VP2_copy"#Data is dynamically read and saved based on this string, adapt to the number of your participant
  9. #Necessary library
  10. import mne #preprocessing and plotting
  11. import os #file and path handling
  12. import numpy as np #Numerical computations
  13. import pandas as pd #data frame handling
  14. import matplotlib.pyplot as plt #plotting
  15. print(f'MNE version is {mne.__version__}')
  16. %matplotlib inline
  17. #plot within notebook, useful for later sharing
  18. EEG_data_path = os.path.join('C:/Users/annad/Documents/Thesis/Experiment 1/Data/Raw Data/')
  19. #Path where the EEG data, windows user should probably replace the way path are declared using os.path.join()
  20. # %% [markdown]
  21. # # Reading raw BDF
  22. # %% [markdown]
  23. # This data was recorded using a biosemi 32 channel system.
  24. # %%
  25. raw = mne.io.read_raw_bdf(EEG_data_path+'%s.bdf'%name_subj,preload=True, verbose=True)
  26. raw.plot_psd(tmin=500, picks='eeg'); #First glimpse at the coarse frequnecy composition of the data
  27. plt.show()
  28. raw.info #Info structure about the data
  29. # %% [markdown]
  30. # # Setting up bipolar references and montage
  31. # %% [markdown]
  32. # Note that everything is done inplace, meaning that e.g. if you drop channels they will no longer be present in the original ```raw``` variable
  33. # %%
  34. # raw.drop_channels(['EXG6','EXG7','EXG8'])#Dropping empty external channels (shouldn't have been recorded
  35. # Creating the bipolar montage, the 3 first are the EOG
  36. # mne.set_bipolar_reference(raw,anode=['EXG1','EXG3'],cathode=['EXG2','A1'],ch_name=['EOGH','EOGV'],copy=False, drop_refs=False) #creating horizontal and vertical EOGs
  37. #Drop refs in this case should be false as we don't want to drop Fp1
  38. # raw.set_channel_types({'EOGH':'eog','EOGV':'eog','EXG4':'misc','EXG5':'misc','Erg1':'misc',
  39. # 'EXG1':'misc','EXG2':'misc','EXG3':'misc','EXG4':'misc'})#declare type to avoid confusion with EEG channels
  40. #Renaming electrodes
  41. # dict_to_biosemi = dict(zip(raw.copy().pick_types(eeg=True).ch_names, mne.channels.make_standard_montage('biosemi64').ch_names))#convert names to biosemi 32 frame
  42. # raw.rename_channels(dict_to_biosemi)
  43. # raw.set_montage('biosemi64')#Get electrode positions of a standard montage of the electrodes, MNE also allows to provide positions if they are recorded
  44. # %% [markdown]
  45. # We see that the info object is now updated according to the modifications done
  46. # %% [markdown]
  47. # # Checking for bad electrodes
  48. # %% [markdown]
  49. # Visual inspection to detect fault electrodes (flat, very noisy, unstable for long period of times). Do not worry about occasional glitches as these will be fixed at the last stage of the pipeline
  50. # %%
  51. # %matplotlib qt
  52. raw.set_eeg_reference('average')#For electrode check only
  53. raw.plot(block=False, n_channels=64, scalings=100e-6);#plotting function, block argument stops notebook execution, useful if you just want to run the whole notebook
  54. # %% [markdown]
  55. # Additionally it is possible to check bridged electrode (https://mne.tools/dev/auto_examples/preprocessing/eeg_bridging.html) but with 32 electrodes that is unlikely
  56. # %%
  57. raw.info['bads']
  58. # %% [markdown]
  59. # ## REST reference
  60. # %% [markdown]
  61. # Then we apply a REST reference (see https://iopscience.iop.org/article/10.1088/0967-3334/22/4/305/meta)
  62. # %%
  63. # Setting the reference, REST
  64. print(raw.info)
  65. sphere = mne.make_sphere_model('auto', 'auto', raw.info, verbose=False)
  66. src = mne.setup_volume_source_space(sphere=sphere, exclude=30., pos=15., verbose=False)
  67. forward = mne.make_forward_solution(raw.info, trans=None, src=src, bem=sphere, verbose=False)
  68. raw.set_eeg_reference('REST', forward=forward);
  69. # %% [markdown]
  70. # # Filtering
  71. # %% [markdown]
  72. # Low, high pass filter + notchfilter at 50Hz
  73. # %%
  74. %matplotlib inline
  75. raw.filter(0.1, 40, None,l_trans_bandwidth='auto',filter_length='auto',phase='zero') #Filtering between 1 and 100 Hz
  76. raw.notch_filter(freqs=50)#In this experiment 50Hz for all participants so notch filter needed
  77. raw.plot_psd(tmin=500, fmax=50, picks='eeg')#rechecking frequency decomposition
  78. plt.show()
  79. # %% [markdown]
  80. # I save the data, hence saving the montage, the low-pass filtered data, the (eventual) annotation of the bad channel
  81. # %%
  82. raw.save(EEG_data_path+"preprocessing/pre_rejection_%s.fif"%name_subj,overwrite=True) #save data in .fif format
  83. # %% [markdown]
  84. # # Rejecting artefacts
  85. # %% [markdown]
  86. # First we recover the events from the trigger channel
  87. # %%
  88. events = mne.find_events(raw)#In MNE for Biosemi the trigger is automatically found
  89. # %% [markdown]
  90. # We can display every events in the experiment found in the trigger channel
  91. # %%
  92. mne.viz.plot_events(events);
  93. # %% [markdown]
  94. # Events have the following structure [sample, offset of the trigger channel, value of the trigger]
  95. # %%
  96. events[1:11]
  97. # %% [markdown]
  98. # We then parse the codes according to how we coded the experiment (hence function on how you defined your triggers)
  99. # %%
  100. all_events = np.array(np.unique(events[:,2]))
  101. stim_trigger = all_events[all_events<99]
  102. conditions_to_trigger = {"acc":101, "spd":201}
  103. side_to_trigger = {"left":99, "right":199}
  104. resp_trigger = [100,200]
  105. # %% [markdown]
  106. # Just for visualisation purposes we define separate colors for stimulus vs response triggers vs others
  107. # %%
  108. stim = np.array([x for x in events if x[-1] in stim_trigger])
  109. resp = np.array([x for x in events if x[-1] in resp_trigger])
  110. color_dict = {k:'b' for k in stim_trigger}
  111. color_dict.update({k:'g' for k in resp_trigger})
  112. color_dict.update({k:'gray' for k in set.difference(set(all_events),set(stim_trigger), set(resp_trigger))})
  113. # %% [markdown]
  114. # ## Correcting stim onset based on photodiode
  115. # %% [markdown]
  116. # In this (specific) case we have the recording of a photodiode so we'll use that info to know when the screen was actually refreshed
  117. # %%
  118. def find_onset_pd(data, event_sample, baseline=500):
  119. '''
  120. Detects onset of stim through std deviation of photodiode signal
  121. '''
  122. try:
  123. index = np.where(data <0)[0][0]
  124. except:
  125. return(event_sample)
  126. if event_sample - (index-baseline+event_sample) >0:
  127. print('inconsistent photodiode detected onset')
  128. return(event_sample)
  129. else:
  130. return(index-baseline+event_sample)
  131. len([x[0] for x in events if x[2] in stim_trigger])#Print the number of total events having a stimulus code
  132. # %%
  133. photodiode = raw.copy().pick(['Erg1']).get_data()[0]
  134. baseline = 500
  135. new_events = events.copy()
  136. onsets = []
  137. for i in np.arange(len(events)):
  138. if events[i,2] in stim_trigger:
  139. onsets.append(find_onset_pd(photodiode[events[i,0]-baseline:events[i,0]+150], events[i,0], baseline=baseline))
  140. new_events[i,0] = onsets[-1]
  141. %matplotlib inline
  142. diffs = events[:,0] - new_events[:,0]
  143. diffs = diffs[diffs != 0]
  144. plt.hist(diffs,bins=50);
  145. plt.show()
  146. print(sum(events[:,0] != new_events[:,0]))#check how many events changed, should be the same as above
  147. # %% [markdown]
  148. # 1116 stimulus events had their location (in samples) corrected according to the value of the photodiode. For the record, the photodiode is just a channel with data (see below) so we can just use plain python to find where the onset of the stimulus is located
  149. # %%
  150. plt.plot(photodiode[:5000])
  151. plt.show()
  152. # %% [markdown]
  153. # We now replace the photodiode corrected events
  154. # %%
  155. events = new_events
  156. raw.add_events(events, replace=True)
  157. # %% [markdown]
  158. # # ICA
  159. # %% [markdown]
  160. # To remove artifacts we'll use independent component analysis (see MNE's tutorials, e.g. https://mne.tools/stable/auto_tutorials/preprocessing/40_artifact_correction_ica.html#sphx-glr-auto-tutorials-preprocessing-40-artifact-correction-ica-py)
  161. # %% [markdown]
  162. # ## Preparing for ICA
  163. # %% [markdown]
  164. # We fit the ICA on the continuous EEG signal but to avoid taking beginning and end of the recording as well as (eventual) breaks in the experiment, which are often very noisy, we annotate the long sections without stimulus triggers.
  165. # %% [markdown]
  166. # ### Annotation of very long RTs/breaks
  167. # %%
  168. stim_events = np.array([list(x) for x in events if x[2] in stim_trigger])
  169. estimated_duration_breaks = 6 #seconds
  170. onset_breaks = stim_events[np.where(np.diff(stim_events[:,0], n=1) > (raw.info['sfreq']*estimated_duration_breaks))][:,0]/raw.info['sfreq'] #detecting latencies between triggers > x sec
  171. offset_breaks = np.flip(np.flip(stim_events)[np.where(np.diff(np.flip(stim_events[:,0]), n=1) < -(raw.info['sfreq']*estimated_duration_breaks))])[:,0]/raw.info['sfreq']
  172. onset_breaks = onset_breaks + 3 #add 3 sec after last stimulus trigger
  173. offset_breaks = offset_breaks - .6 #removes 600 msec before next stimulus trigger
  174. onset_breaks = np.insert(onset_breaks,0,0)#just adding start of the recording to the breaks
  175. onset_breaks = np.insert(onset_breaks,-1, stim_events[-1,0]/raw.info['sfreq']+3)#just adding end of the recording to the breaks
  176. offset_breaks = np.insert(offset_breaks,0,stim_events[0,0]/raw.info['sfreq']-.6)#just adding start of the recording to the breaks
  177. offset_breaks = np.insert(offset_breaks,-1, raw.times.max())#just adding end of the recording to the breaks
  178. duration_breaks = np.asarray(offset_breaks) - np.asarray(onset_breaks)
  179. print(len(duration_breaks))
  180. break_annot = mne.Annotations(onset= np.insert(onset_breaks,0,0),#just adding start of the recording to the breaks
  181. duration=np.insert(duration_breaks,0, stim_events[0,0]/raw.info['sfreq']-1),
  182. description=['BAD_breaks'])
  183. raw.set_annotations(break_annot);
  184. # %% [markdown]
  185. # 9 of such periods where found and annotated so excluded from future data processing
  186. # %% [markdown]
  187. # ## Adding annotations
  188. #
  189. # First enter in annotation mode with ctrl+A and add a description "BAD" then remove portions where participant was clearly doing something else (scratching, blinking during stimulus presentation, weird artifacts), also remoe trials with unrecoverable noise and spread across all electrodes
  190. # %%
  191. %matplotlib qt
  192. if 'saved_annotations_%s.csv'%(name_subj) in os.listdir('preprocessing'):
  193. annot_from_file = mne.read_annotations('preprocessing/saved_annotations_%s.csv'%(name_subj))
  194. raw.set_annotations(annot_from_file)
  195. raw.plot(events=events, event_color=color_dict, remove_dc=True, n_channels=len(raw.ch_names), use_opengl=True, block=True)
  196. raw.annotations.save('preprocessing/saved_annotations_%s.csv'%(name_subj),overwrite=True)
  197. else:
  198. raw.plot(events=events, event_color=color_dict, remove_dc=True, n_channels=len(raw.ch_names), use_opengl=True, block=True)
  199. raw.annotations.save('preprocessing/saved_annotations_%s.csv'%(name_subj),overwrite=True)
  200. # %% [markdown]
  201. # Save every step up to now and before the actual fit of the ICA
  202. # %%
  203. raw.save("preprocessing/pre_ica_%s.fif"%name_subj,overwrite=True)
  204. # %% [markdown]
  205. # # Fitting ICA
  206. # %% [markdown]
  207. # ### Resampling
  208. # %%
  209. # raw = mne.io.read_raw_fif(EEG_data_path+"preprocessing/pre_ica_%s.fif"%name_subj, preload=True)#needed just in case of restart at this point
  210. # %% [markdown]
  211. # Downsampling to 500 Hz to reduce computational load. Note that while we fit the ICA on resampled data, the ICA based correction will be done on the original sampling frequency by reloading the data pre-ica before correction
  212. # %%
  213. events = mne.find_events(raw)
  214. raw, events = raw.resample(500, events=events)#passing the events avoids generating incorrect timing in events
  215. # %% [markdown]
  216. # We rebuild the same event structure (in case you needed to rerun the notebook)
  217. # %%
  218. all_events = np.array(np.unique(events[:,2]))
  219. stim_trigger = all_events[all_events<99]
  220. conditions_to_trigger = {"acc":101, "spd":201}
  221. side_to_trigger = {"left":99, "right":199}
  222. resp_trigger = [100,200]
  223. # %% [markdown]
  224. # Fit using the fastICA algorithm implemented by MNE. Add -1 to avoid rank deficiency linked to REST reference
  225. # %%
  226. ica = mne.preprocessing.ICA(n_components = len(raw.pick_types(eeg=True).ch_names)-1, method='fastica', max_iter='auto')
  227. ica.fit(raw)
  228. # %% [markdown]
  229. # Save the fitted ICA
  230. # %%
  231. ica.save("preprocessing/ICA_object_%s.fif"%name_subj, overwrite=True);
  232. # %% [markdown]
  233. # ### Visualizing on epochs
  234. #
  235. # Now we fitted on the continuous data (minus breaks and bad portions), but we want to inspect how ICs behave at the epoch level. Hence we first epoch the data in order to take alook at those ICs
  236. # %%
  237. epochs = mne.Epochs(raw, stim, event_id=[int(x) for x in stim_trigger], tmin=-0.5, tmax=2, preload=True)#Epoch is -500ms up to 2000ms after stim, only for IC visualization
  238. # %% [markdown]
  239. # ### Inspection
  240. # Here we open an interactive view to inspect components based on their topologies on the scalp. Clicking on their number marks them for exclusion, clicking on the topologies opens a view of the IC on different measures (see also [[zoom on suspicious ICs]])
  241. # %%
  242. %matplotlib qt
  243. #%matplotlib qt opens a separate window
  244. ica.plot_components(inst=epochs);
  245. # %% [markdown]
  246. # ### ICs epoch timecourse
  247. # %% [markdown]
  248. # Here we inspect each ICs time course during the whole experiment, very usefull to understand eventual functional properties of the identified ICs.
  249. # %%
  250. ica.plot_sources(epochs, block=True)# opens a dynamic window when run
  251. # %% [markdown]
  252. # ### zoom on suspsicious ICs
  253. # %% [markdown]
  254. # Before exclusions I take a look at those IC that I marked to be sure that those are the ones I want to exclude. A prompt at the end of the cell takes as input the number (separated by commas) of the ICs I really want to remove
  255. # %%
  256. %matplotlib inline
  257. fig, ax = plt.subplots(len(ica.exclude), 5, figsize = (25, len(ica.exclude)*5))
  258. i = 0
  259. for comp in ica.exclude:
  260. ica.plot_properties(epochs, picks=comp, axes=ax[i,:], show=False);
  261. i += 1
  262. plt.show()
  263. ica.exclude = [int(x) for x in input([]).split(',')]
  264. # %% [markdown]
  265. # In this example IC0, 1 and 3 are clearly related to eye activities and, because I do not care much about frontal electrodes, can be removed. ICA019 for example is a, very localized, brain source as it looks based on the centrality of the activity and the alpha/beta like frequency.
  266. # %% [markdown]
  267. # ## Final exclusions of ICA components :
  268. # %% [markdown]
  269. # Visualize the consequence of the correction on the data without yet applying it
  270. # %%
  271. %matplotlib inline
  272. print(ica.exclude)
  273. ica.plot_overlay(epochs.average(), exclude=ica.exclude, picks='eeg');
  274. # %% [markdown]
  275. # ### Reloading original pre-ica data
  276. # %%
  277. raw = mne.io.read_raw_fif(EEG_data_path+"preprocessing/pre_ica_%s.fif"%name_subj, preload=True)
  278. # %% [markdown]
  279. # You see here that we apply the ICA correction on the original raw data that wasn't downsampled
  280. # %%
  281. ica.apply(raw)
  282. # %% [markdown]
  283. # ## Interpolating the bad electrodes after ICA
  284. # %% [markdown]
  285. # Once we have done all that we can interpolate the bad channel we had
  286. # %%
  287. print(raw.info['bads'])
  288. raw = raw.interpolate_bads()
  289. raw.info['bads']
  290. # %% [markdown]
  291. # And take a look at the new frequency decomposition
  292. # %%
  293. raw.plot_psd(fmax=80);
  294. # %% [markdown]
  295. # Save the post ICA correction
  296. # %%
  297. raw.save("preprocessing/post_ica_%s_raw.fif"%name_subj,overwrite=True)
  298. # %% [markdown]
  299. # # Auto-reject remaining artifacts
  300. # %% [markdown]
  301. # Autoreject (https://autoreject.github.io/v0.2/index.html) is a very useful tool for post-ICA preprocessing (but some also like to use it before, see the website). We first epoch the data and then try to find electrode-wise optimal thresholds based on a cross-validation method. The resulting solution is a way to interpolate electrodes that exceed their respective thresholds as long as no ttoo many of them have to be interpolated within a given trial (otherwise the trial is rejected)
  302. # %% [markdown]
  303. # ## Creating epochs
  304. # %% [markdown]
  305. # The method works on epoched data, hence we first decompose the continuous data based on the stimulus triggers that were sent in the experiment
  306. # %%
  307. raw = mne.io.read_raw_fif(EEG_data_path+"preprocessing/post_ica_%s_raw.fif"%name_subj, preload=True)#reloading last step
  308. events = mne.find_events(raw)
  309. #In this decision-making experiment, triggers are condition (speed or accurate), side of the correct response (L/R), stimulus intensity, and response
  310. #A description of a related design can be found here: Chapter 5 of https://thesiscommons.org/342zp
  311. all_events = np.array(np.unique(events[:,2]))
  312. stim_trigger_values = all_events[all_events<99]#stimulus intensity values
  313. stim_id = {'stimulus/'+str(k):k for k in stim_trigger_values}#building dict on those
  314. condition_id = {"condition/accuracy":101, "condition/speed":201}#condition trigger
  315. side_id = {"side/left":99, "side/right":199}#Expected response side (correct answer)
  316. resp_id = {"response/left":100, "response/right":200}#Given esponse side events
  317. event_id = condition_id | stim_id | side_id | resp_id #all retained events
  318. tmin = -0.25 #tmin is how much data (in s) needs to be used for baseline correction
  319. tmax = 2 #tmax is how much far in time from stim should we look for a response
  320. stim = list(stim_id.keys())#values to center the epochs on
  321. #here we create a metadata structure, very useful to use triggers outside of thos used for centering (here condition, side and response)
  322. metadata, events, event_id = mne.epochs.make_metadata(
  323. events=events, event_id=event_id, tmin=tmin, tmax=tmax,
  324. sfreq=raw.info["sfreq"], row_events=stim, keep_first=["condition","side","stimulus","response"])
  325. #In our case we are not interested in the specific timing of those events, just the nature of the first occurence of those so we can subselect the generated pandas dataframe
  326. keep_cols = ['event_name', 'response', 'first_condition', 'first_side','first_stimulus','first_response']
  327. metadata = metadata[keep_cols]
  328. metadata.reset_index(drop=True, inplace=True)#This allows to preserve the trial number after the rejections performed by the mne.Epochs function
  329. metadata.columns = ['event_name', 'rt', 'condition', 'side','stimulus','response']#More convenient names
  330. #If you get a RuntimeWarning about no matching event found this is normal as sometime a few combinations are absent (e.g. no trial with accuracy condition, left expected response and contrast of 92)
  331. epochs = mne.Epochs(raw, events, event_id, tmin, tmax, proj=False,
  332. baseline=(None, 0), preload=True,
  333. verbose=True, detrend=1, on_missing = 'warn', event_repeated='drop',
  334. metadata=metadata, reject_by_annotation=True, reject=None)
  335. # %% [markdown]
  336. # Epoch are dropped during the epoching because an artifact (or a break) was present between tmin and tmax. The following plot shows you how many, but more details can be found in ```epochs.drop_log()```
  337. # %%
  338. epochs.plot_drop_log();
  339. # %% [markdown]
  340. # ## Auto-reject on remaining artifacts
  341. # %% [markdown]
  342. # Takes some time (and resources) to run
  343. # %%
  344. import autoreject #version 0.3.1 https://autoreject.github.io/
  345. ar = autoreject.AutoReject(consensus=np.linspace(0, .5, 11), n_jobs=-1) #I constrain the consensus parameter to be <=.5 as trials with more than half bad chan should be rejected
  346. ar.fit(epochs) # fit on the first 20 epochs to save time
  347. epochs_ar, reject_log = ar.transform(epochs, return_log=True)
  348. # %% [markdown]
  349. # The following plots summarize what was done by autoreject
  350. # %%
  351. %matplotlib inline
  352. fig, ax = plt.subplots(1,1, figsize=(10,20), dpi=300)
  353. reject_log.plot('horizontal', ax=ax)
  354. plt.show()
  355. evoked_bad = epochs[reject_log.bad_epochs].average()
  356. plt.figure()
  357. plt.plot(evoked_bad.times, evoked_bad.data.T * 1e6, 'r', zorder=-1)
  358. epochs_ar.average().plot(axes=plt.gca());#Compare ERP of rejected vs not rejected
  359. # %% [markdown]
  360. # Now we can review what the algorithm did. If we don't agree with, e.g. when the trial was rejected rather than interpolated or the rejection threshold we can adjust the parameters in the ```AutoReject``` function
  361. # %% [markdown]
  362. # I would rather recommend to constrain the algortihm in such a way that it remains conservative so that eventually we mark some more trials as bad (as in the next cell) rather than automatically rejecting a lot of epochs.
  363. # %%
  364. %matplotlib qt
  365. reject_log.plot_epochs(epochs,scalings=dict(eeg=100e-6))
  366. # %% [markdown]
  367. # The ```plot_drop_log()``` now shows the total amount of dropped epochs and those that come from the previous visual rejection and those rejected based on autoreject
  368. # %%
  369. epochs_ar.plot_drop_log()
  370. # %% [markdown]
  371. # Then we save the data and can use them in further processing
  372. # %%
  373. epochs_ar.save('preprocessed/%s_epo.fif'%name_subj, overwrite=True)

Template_preprocessing.ipynb at commit 54727dc, no license · at the source

Overview

Authors: Rick den Otter1, Anna Dame1, Sjoerd Stuit1, Leendert van Maanen1
  1. Helmholtz Institute, Department of Experimental Psychology, Utrecht University, Utrecht, The Netherlands
Institutions: Utrecht University (Netherlands)
Journal: PLoS computational biology, volume 22, issue 5, article e1014302
Dates: received 12 November 2025; accepted 7 May 2026; published online 20 May 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1371/journal.pcbi.1014302 · PMID 42160393 · PMCID PMC13215611 · OpenAlex W4416336917
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: EEG (modality), human (organism)
Methods: Spectral & time-frequency, Preprocessing, Connectivity, Smoothing, state filtering, decompositions, Statistics, Machine learning
MeSH: Models, Neurological*, Cognition, Computational Biology, Dual-Task Tests, Electroencephalography, Humans, Reaction Time, Refractory Period, Psychological (* major topic)
Topic: Neural and Behavioral Psychology Studies (Cognitive Neuroscience, Neuroscience), according to OpenAlex
Citations: not cited yet (Europe PMC); 70 references in the paper

Abstract

Theories of dual-task interference assume that the same cognitive operations underlie multitasking regardless of stimulus timing, yet this core assumption has remained untested due to methodological limitations of behavioral averaging. Here, we combine hidden multivariate pattern (HMP) analysis with deep spatiotemporal sequence modeling of single-trial EEG to uncover the neural dynamics of multitasking in the psychological refractory period (PRP) paradigm. Using a deep spatiotemporal sequence model trained on Long stimulus-onset asynchrony (SOA) trials, we identify Encoding, Central, and Response operations and show that these same operations occur in the Short SOA condition, demonstrating shared cognitive processes across interference conditions. Additionally, trial-level decoding reveals multiple distinct sequences of cognitive operations across both tasks during interference, varying both within and across individuals. These sequences predict behavioral differences in reaction time and accuracy, revealing how interference timing within the cognitive operation sequence influences performance. In other words, we found trial-by-trial variability related to individual strategies directly affecting accuracy and reaction time (RT). Our findings challenge static bottleneck accounts and establish trial-level sequence modeling as a powerful tool to investigate the hidden dynamics of multitasking.

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

Repository

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

rickdott/prp

License: none: the authors keep all their rights
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Commit: 54727dc4cc246faae2f2e7e3470a0bbe1b81bf00, 17 February 2026
Languages: Jupyter (29), Shell (5), Python (1)
Size: 90 files, 35 scripts
Software Heritage: not archived
Found in: the text, “2.5.1. Model training.”
Holds: environment (docker/docker-compose.yaml, docker/Dockerfile, docker/requirements.txt), 29 notebooks
Not found: README, license file, CITATION.cff, tests, continuous integration, documentation
Tools: MNE-Python (20 files), pandas (19 files), Matplotlib (17 files), NumPy (17 files), xarray (13 files), seaborn (10 files), PyTorch (9 files), autoreject (8 files), SciPy (4 files), scikit-learn (2 files), statsmodels (2 files), emmeans (1 file), lme4 (1 file), tidyverse (1 file)
Availability: 1 check, the latest on 28 September 2026: the link answers
  • 28 September 2026: the link answers
35 files

The paper's code and data availability statement is in the Data section.

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:

  • 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 35 scripts, each with its path and the digest of its content;
  • 6 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

The data is made available openly at https://osf.io/5ub8z [34] by the original authors, the code is available at https://github.com/rickdott/prp.

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, 28 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 4 authors, 8 MeSH terms, 65 references.

Cite

This paper

den Otter, R., Dame, A., Stuit, S., & van Maanen, L. (2026). Trial-level sequence modeling reveals hidden dynamics of dual-task interference. PLoS computational biology, 22(5), e1014302. https://doi.org/10.1371/journal.pcbi.1014302

BibTeX

@article{denotter2026trial,
author = {den Otter, Rick and Dame, Anna and Stuit, Sjoerd and van Maanen, Leendert},
title = {{Trial-level sequence modeling reveals hidden dynamics of dual-task interference}},
journal = {PLoS computational biology},
year = {2026},
month = may,
volume = {22},
number = {5},
pages = {e1014302},
publisher = {PLOS},
issn = {1553-734X},
doi = {10.1371/journal.pcbi.1014302},
url = {https://doi.org/10.1371/journal.pcbi.1014302},
pmid = {42160393},
pmcid = {PMC13215611}
}

RIS

TY - JOUR
AU - den Otter, Rick
AU - Dame, Anna
AU - Stuit, Sjoerd
AU - van Maanen, Leendert
TI - Trial-level sequence modeling reveals hidden dynamics of dual-task interference
T2 - PLoS computational biology
J2 - PLoS Comput Biol
PY - 2026
DA - 2026/05/20
VL - 22
IS - 5
SP - e1014302
SN - 1553-734X
PB - PLOS
DO - 10.1371/journal.pcbi.1014302
UR - https://doi.org/10.1371/journal.pcbi.1014302
LA - en
ER -

CSL-JSON

{
"id": "10.1371/journal.pcbi.1014302",
"type": "article-journal",
"title": "Trial-level sequence modeling reveals hidden dynamics of dual-task interference",
"container-title": "PLoS computational biology",
"author": [
{
"family": "den Otter",
"given": "Rick"
},
{
"family": "Dame",
"given": "Anna"
},
{
"family": "Stuit",
"given": "Sjoerd"
},
{
"family": "van Maanen",
"given": "Leendert"
}
],
"container-title-short": "PLoS Comput Biol",
"volume": "22",
"issue": "5",
"page": "e1014302",
"DOI": "10.1371/journal.pcbi.1014302",
"PMID": "42160393",
"PMCID": "PMC13215611",
"ISSN": "1553-734X",
"publisher": "PLOS",
"URL": "https://doi.org/10.1371/journal.pcbi.1014302",
"language": "en",
"issued": {
"date-parts": [
[
2026,
5,
20
]
]
}
}

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.1093/nc/niag029 [code]
A data-driven approach to identifying and evaluating connectivity-based neural correlates of conscious visual perception.
Journal: Neuroscience of consciousness
In common: autoreject, xarray, emmeans, 9 other tools, 2 references
[2] doi:10.1038/s41597-026-07350-9 [code]
An open multi-center MEG-EEG dataset for studying conscious visual perception.
Journal: Scientific data
In common: autoreject, xarray, emmeans, 9 other tools, EEG
[3] doi:10.1162/imag.a.1321 [code]
Phase similarity between similar objects indicates representational merging across retrieval training but not sleep.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: autoreject, emmeans, MNE-Python, 8 other tools, EEG, 2 references
[4] doi:10.1038/s41597-026-07377-y [code]
An open-access multi-site fMRI dataset for investigating conscious visual perception.
Journal: Scientific data
In common: autoreject, xarray, emmeans, 9 other tools
[5] 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: autoreject, emmeans, MNE-Python, 7 other tools, EEG, 2 references
[6] doi:10.1162/imag.a.1269 [code]
From early to contemporary normative modeling: Mapping individual differences in neurophysiological signals.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: autoreject, xarray, MNE-Python, 7 other tools, EEG
[7] doi:10.64898/2026.03.06.710026 [code]
Distinct beta burst motifs exhibit opposing error relationships during motor adaptation
Journal: bioRxiv (preprint)
In common: emmeans, MNE-Python, lme4, 8 other tools, 1 reference
[8] doi:10.7554/elife.108023 [code]
Challenges in replay detection by TDLM in post-encoding resting state.
Journal: eLife
In common: autoreject, MNE-Python, statsmodels, 6 other tools, 2 references
[9] doi:10.1038/s41598-026-58505-y [code]
Remoteness sensitive theta network dynamics during early autobiographical memory access.
Journal: Scientific reports
In common: autoreject, MNE-Python, lme4, 6 other tools, EEG, 1 reference
[10] 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: emmeans, MNE-Python, lme4, 8 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.