Trial-level sequence modeling reveals hidden dynamics of dual-task interference.
The 6 matches
- [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. 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] § 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] § 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] § 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] § 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
- # %% [markdown]
- # # An example of EEG preprocessing with MNE python
- # %% [markdown]
- # 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.
- #
- # 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.
- # %%
- name_subj = "VP2_copy"#Data is dynamically read and saved based on this string, adapt to the number of your participant
- #Necessary library
- import mne #preprocessing and plotting
- import os #file and path handling
- import numpy as np #Numerical computations
- import pandas as pd #data frame handling
- import matplotlib.pyplot as plt #plotting
- print(f'MNE version is {mne.__version__}')
- %matplotlib inline
- #plot within notebook, useful for later sharing
- EEG_data_path = os.path.join('C:/Users/annad/Documents/Thesis/Experiment 1/Data/Raw Data/')
- #Path where the EEG data, windows user should probably replace the way path are declared using os.path.join()
- # %% [markdown]
- # # Reading raw BDF
- # %% [markdown]
- # This data was recorded using a biosemi 32 channel system.
- # %%
- raw = mne.io.read_raw_bdf(EEG_data_path+'%s.bdf'%name_subj,preload=True, verbose=True)
- raw.plot_psd(tmin=500, picks='eeg'); #First glimpse at the coarse frequnecy composition of the data
- plt.show()
- raw.info #Info structure about the data
- # %% [markdown]
- # # Setting up bipolar references and montage
- # %% [markdown]
- # 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
- # %%
- # raw.drop_channels(['EXG6','EXG7','EXG8'])#Dropping empty external channels (shouldn't have been recorded
- # Creating the bipolar montage, the 3 first are the EOG
- # 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
- #Drop refs in this case should be false as we don't want to drop Fp1
- # raw.set_channel_types({'EOGH':'eog','EOGV':'eog','EXG4':'misc','EXG5':'misc','Erg1':'misc',
- # 'EXG1':'misc','EXG2':'misc','EXG3':'misc','EXG4':'misc'})#declare type to avoid confusion with EEG channels
- #Renaming electrodes
- # 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
- # raw.rename_channels(dict_to_biosemi)
- # raw.set_montage('biosemi64')#Get electrode positions of a standard montage of the electrodes, MNE also allows to provide positions if they are recorded
- # %% [markdown]
- # We see that the info object is now updated according to the modifications done
- # %% [markdown]
- # # Checking for bad electrodes
- # %% [markdown]
- # 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
- # %%
- # %matplotlib qt
- raw.set_eeg_reference('average')#For electrode check only
- 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
- # %% [markdown]
- # 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
- # %%
- raw.info['bads']
- # %% [markdown]
- # ## REST reference
- # %% [markdown]
- # Then we apply a REST reference (see https://iopscience.iop.org/article/10.1088/0967-3334/22/4/305/meta)
- # %%
- # Setting the reference, REST
- print(raw.info)
- sphere = mne.make_sphere_model('auto', 'auto', raw.info, verbose=False)
- src = mne.setup_volume_source_space(sphere=sphere, exclude=30., pos=15., verbose=False)
- forward = mne.make_forward_solution(raw.info, trans=None, src=src, bem=sphere, verbose=False)
- raw.set_eeg_reference('REST', forward=forward);
- # %% [markdown]
- # # Filtering
- # %% [markdown]
- # Low, high pass filter + notchfilter at 50Hz
- # %%
- %matplotlib inline
- raw.filter(0.1, 40, None,l_trans_bandwidth='auto',filter_length='auto',phase='zero') #Filtering between 1 and 100 Hz
- raw.notch_filter(freqs=50)#In this experiment 50Hz for all participants so notch filter needed
- raw.plot_psd(tmin=500, fmax=50, picks='eeg')#rechecking frequency decomposition
- plt.show()
- # %% [markdown]
- # I save the data, hence saving the montage, the low-pass filtered data, the (eventual) annotation of the bad channel
- # %%
- raw.save(EEG_data_path+"preprocessing/pre_rejection_%s.fif"%name_subj,overwrite=True) #save data in .fif format
- # %% [markdown]
- # # Rejecting artefacts
- # %% [markdown]
- # First we recover the events from the trigger channel
- # %%
- events = mne.find_events(raw)#In MNE for Biosemi the trigger is automatically found
- # %% [markdown]
- # We can display every events in the experiment found in the trigger channel
- # %%
- mne.viz.plot_events(events);
- # %% [markdown]
- # Events have the following structure [sample, offset of the trigger channel, value of the trigger]
- # %%
- events[1:11]
- # %% [markdown]
- # We then parse the codes according to how we coded the experiment (hence function on how you defined your triggers)
- # %%
- all_events = np.array(np.unique(events[:,2]))
- stim_trigger = all_events[all_events<99]
- conditions_to_trigger = {"acc":101, "spd":201}
- side_to_trigger = {"left":99, "right":199}
- resp_trigger = [100,200]
- # %% [markdown]
- # Just for visualisation purposes we define separate colors for stimulus vs response triggers vs others
- # %%
- stim = np.array([x for x in events if x[-1] in stim_trigger])
- resp = np.array([x for x in events if x[-1] in resp_trigger])
- color_dict = {k:'b' for k in stim_trigger}
- color_dict.update({k:'g' for k in resp_trigger})
- color_dict.update({k:'gray' for k in set.difference(set(all_events),set(stim_trigger), set(resp_trigger))})
- # %% [markdown]
- # ## Correcting stim onset based on photodiode
- # %% [markdown]
- # 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
- # %%
- def find_onset_pd(data, event_sample, baseline=500):
- '''
- Detects onset of stim through std deviation of photodiode signal
- '''
- try:
- index = np.where(data <0)[0][0]
- except:
- return(event_sample)
- if event_sample - (index-baseline+event_sample) >0:
- print('inconsistent photodiode detected onset')
- return(event_sample)
- else:
- return(index-baseline+event_sample)
- len([x[0] for x in events if x[2] in stim_trigger])#Print the number of total events having a stimulus code
- # %%
- photodiode = raw.copy().pick(['Erg1']).get_data()[0]
- baseline = 500
- new_events = events.copy()
- onsets = []
- for i in np.arange(len(events)):
- if events[i,2] in stim_trigger:
- onsets.append(find_onset_pd(photodiode[events[i,0]-baseline:events[i,0]+150], events[i,0], baseline=baseline))
- new_events[i,0] = onsets[-1]
- %matplotlib inline
- diffs = events[:,0] - new_events[:,0]
- diffs = diffs[diffs != 0]
- plt.hist(diffs,bins=50);
- plt.show()
- print(sum(events[:,0] != new_events[:,0]))#check how many events changed, should be the same as above
- # %% [markdown]
- # 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
- # %%
- plt.plot(photodiode[:5000])
- plt.show()
- # %% [markdown]
- # We now replace the photodiode corrected events
- # %%
- events = new_events
- raw.add_events(events, replace=True)
- # %% [markdown]
- # # ICA
- # %% [markdown]
- # 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)
- # %% [markdown]
- # ## Preparing for ICA
- # %% [markdown]
- # 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.
- # %% [markdown]
- # ### Annotation of very long RTs/breaks
- # %%
- stim_events = np.array([list(x) for x in events if x[2] in stim_trigger])
- estimated_duration_breaks = 6 #seconds
- 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
- 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']
- onset_breaks = onset_breaks + 3 #add 3 sec after last stimulus trigger
- offset_breaks = offset_breaks - .6 #removes 600 msec before next stimulus trigger
- onset_breaks = np.insert(onset_breaks,0,0)#just adding start of the recording to the breaks
- onset_breaks = np.insert(onset_breaks,-1, stim_events[-1,0]/raw.info['sfreq']+3)#just adding end of the recording to the breaks
- offset_breaks = np.insert(offset_breaks,0,stim_events[0,0]/raw.info['sfreq']-.6)#just adding start of the recording to the breaks
- offset_breaks = np.insert(offset_breaks,-1, raw.times.max())#just adding end of the recording to the breaks
- duration_breaks = np.asarray(offset_breaks) - np.asarray(onset_breaks)
- print(len(duration_breaks))
- break_annot = mne.Annotations(onset= np.insert(onset_breaks,0,0),#just adding start of the recording to the breaks
- duration=np.insert(duration_breaks,0, stim_events[0,0]/raw.info['sfreq']-1),
- description=['BAD_breaks'])
- raw.set_annotations(break_annot);
- # %% [markdown]
- # 9 of such periods where found and annotated so excluded from future data processing
- # %% [markdown]
- # ## Adding annotations
- #
- # 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
- # %%
- %matplotlib qt
- if 'saved_annotations_%s.csv'%(name_subj) in os.listdir('preprocessing'):
- annot_from_file = mne.read_annotations('preprocessing/saved_annotations_%s.csv'%(name_subj))
- raw.set_annotations(annot_from_file)
- raw.plot(events=events, event_color=color_dict, remove_dc=True, n_channels=len(raw.ch_names), use_opengl=True, block=True)
- raw.annotations.save('preprocessing/saved_annotations_%s.csv'%(name_subj),overwrite=True)
- else:
- raw.plot(events=events, event_color=color_dict, remove_dc=True, n_channels=len(raw.ch_names), use_opengl=True, block=True)
- raw.annotations.save('preprocessing/saved_annotations_%s.csv'%(name_subj),overwrite=True)
- # %% [markdown]
- # Save every step up to now and before the actual fit of the ICA
- # %%
- raw.save("preprocessing/pre_ica_%s.fif"%name_subj,overwrite=True)
- # %% [markdown]
- # # Fitting ICA
- # %% [markdown]
- # ### Resampling
- # %%
- # 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
- # %% [markdown]
- # 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
- # %%
- events = mne.find_events(raw)
- raw, events = raw.resample(500, events=events)#passing the events avoids generating incorrect timing in events
- # %% [markdown]
- # We rebuild the same event structure (in case you needed to rerun the notebook)
- # %%
- all_events = np.array(np.unique(events[:,2]))
- stim_trigger = all_events[all_events<99]
- conditions_to_trigger = {"acc":101, "spd":201}
- side_to_trigger = {"left":99, "right":199}
- resp_trigger = [100,200]
- # %% [markdown]
- # Fit using the fastICA algorithm implemented by MNE. Add -1 to avoid rank deficiency linked to REST reference
- # %%
- ica = mne.preprocessing.ICA(n_components = len(raw.pick_types(eeg=True).ch_names)-1, method='fastica', max_iter='auto')
- ica.fit(raw)
- # %% [markdown]
- # Save the fitted ICA
- # %%
- ica.save("preprocessing/ICA_object_%s.fif"%name_subj, overwrite=True);
- # %% [markdown]
- # ### Visualizing on epochs
- #
- # 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
- # %%
- 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
- # %% [markdown]
- # ### Inspection
- # 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]])
- # %%
- %matplotlib qt
- #%matplotlib qt opens a separate window
- ica.plot_components(inst=epochs);
- # %% [markdown]
- # ### ICs epoch timecourse
- # %% [markdown]
- # Here we inspect each ICs time course during the whole experiment, very usefull to understand eventual functional properties of the identified ICs.
- # %%
- ica.plot_sources(epochs, block=True)# opens a dynamic window when run
- # %% [markdown]
- # ### zoom on suspsicious ICs
- # %% [markdown]
- # 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
- # %%
- %matplotlib inline
- fig, ax = plt.subplots(len(ica.exclude), 5, figsize = (25, len(ica.exclude)*5))
- i = 0
- for comp in ica.exclude:
- ica.plot_properties(epochs, picks=comp, axes=ax[i,:], show=False);
- i += 1
- plt.show()
- ica.exclude = [int(x) for x in input([]).split(',')]
- # %% [markdown]
- # 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.
- # %% [markdown]
- # ## Final exclusions of ICA components :
- # %% [markdown]
- # Visualize the consequence of the correction on the data without yet applying it
- # %%
- %matplotlib inline
- print(ica.exclude)
- ica.plot_overlay(epochs.average(), exclude=ica.exclude, picks='eeg');
- # %% [markdown]
- # ### Reloading original pre-ica data
- # %%
- raw = mne.io.read_raw_fif(EEG_data_path+"preprocessing/pre_ica_%s.fif"%name_subj, preload=True)
- # %% [markdown]
- # You see here that we apply the ICA correction on the original raw data that wasn't downsampled
- # %%
- ica.apply(raw)
- # %% [markdown]
- # ## Interpolating the bad electrodes after ICA
- # %% [markdown]
- # Once we have done all that we can interpolate the bad channel we had
- # %%
- print(raw.info['bads'])
- raw = raw.interpolate_bads()
- raw.info['bads']
- # %% [markdown]
- # And take a look at the new frequency decomposition
- # %%
- raw.plot_psd(fmax=80);
- # %% [markdown]
- # Save the post ICA correction
- # %%
- raw.save("preprocessing/post_ica_%s_raw.fif"%name_subj,overwrite=True)
- # %% [markdown]
- # # Auto-reject remaining artifacts
- # %% [markdown]
- # 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)
- # %% [markdown]
- # ## Creating epochs
- # %% [markdown]
- # The method works on epoched data, hence we first decompose the continuous data based on the stimulus triggers that were sent in the experiment
- # %%
- raw = mne.io.read_raw_fif(EEG_data_path+"preprocessing/post_ica_%s_raw.fif"%name_subj, preload=True)#reloading last step
- events = mne.find_events(raw)
- #In this decision-making experiment, triggers are condition (speed or accurate), side of the correct response (L/R), stimulus intensity, and response
- #A description of a related design can be found here: Chapter 5 of https://thesiscommons.org/342zp
- all_events = np.array(np.unique(events[:,2]))
- stim_trigger_values = all_events[all_events<99]#stimulus intensity values
- stim_id = {'stimulus/'+str(k):k for k in stim_trigger_values}#building dict on those
- condition_id = {"condition/accuracy":101, "condition/speed":201}#condition trigger
- side_id = {"side/left":99, "side/right":199}#Expected response side (correct answer)
- resp_id = {"response/left":100, "response/right":200}#Given esponse side events
- event_id = condition_id | stim_id | side_id | resp_id #all retained events
- tmin = -0.25 #tmin is how much data (in s) needs to be used for baseline correction
- tmax = 2 #tmax is how much far in time from stim should we look for a response
- stim = list(stim_id.keys())#values to center the epochs on
- #here we create a metadata structure, very useful to use triggers outside of thos used for centering (here condition, side and response)
- metadata, events, event_id = mne.epochs.make_metadata(
- events=events, event_id=event_id, tmin=tmin, tmax=tmax,
- sfreq=raw.info["sfreq"], row_events=stim, keep_first=["condition","side","stimulus","response"])
- #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
- keep_cols = ['event_name', 'response', 'first_condition', 'first_side','first_stimulus','first_response']
- metadata = metadata[keep_cols]
- metadata.reset_index(drop=True, inplace=True)#This allows to preserve the trial number after the rejections performed by the mne.Epochs function
- metadata.columns = ['event_name', 'rt', 'condition', 'side','stimulus','response']#More convenient names
- #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)
- epochs = mne.Epochs(raw, events, event_id, tmin, tmax, proj=False,
- baseline=(None, 0), preload=True,
- verbose=True, detrend=1, on_missing = 'warn', event_repeated='drop',
- metadata=metadata, reject_by_annotation=True, reject=None)
- # %% [markdown]
- # 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()```
- # %%
- epochs.plot_drop_log();
- # %% [markdown]
- # ## Auto-reject on remaining artifacts
- # %% [markdown]
- # Takes some time (and resources) to run
- # %%
- import autoreject #version 0.3.1 https://autoreject.github.io/
- 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
- ar.fit(epochs) # fit on the first 20 epochs to save time
- epochs_ar, reject_log = ar.transform(epochs, return_log=True)
- # %% [markdown]
- # The following plots summarize what was done by autoreject
- # %%
- %matplotlib inline
- fig, ax = plt.subplots(1,1, figsize=(10,20), dpi=300)
- reject_log.plot('horizontal', ax=ax)
- plt.show()
- evoked_bad = epochs[reject_log.bad_epochs].average()
- plt.figure()
- plt.plot(evoked_bad.times, evoked_bad.data.T * 1e6, 'r', zorder=-1)
- epochs_ar.average().plot(axes=plt.gca());#Compare ERP of rejected vs not rejected
- # %% [markdown]
- # 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
- # %% [markdown]
- # 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.
- # %%
- %matplotlib qt
- reject_log.plot_epochs(epochs,scalings=dict(eeg=100e-6))
- # %% [markdown]
- # 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
- # %%
- epochs_ar.plot_drop_log()
- # %% [markdown]
- # Then we save the data and can use them in further processing
- # %%
- epochs_ar.save('preprocessed/%s_epo.fif'%name_subj, overwrite=True)
Template_preprocessing.ipynb at commit 54727dc, no license · at the source
Overview
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
54727dc4cc246faae2f2e7e3470a0bbe1b81bf00, 17 February 2026Availability: 1 check, the latest on 28 September 2026: the link answers
- 28 September 2026: the link answers
35 files
- docker/
install_docker.sh , Shell, 15 lines - docker/
install_gh.sh , Shell, 7 lines - docker/
install_nvidia_container , Shell, 11 lines_toolkit.sh - docker/
install_r.sh , Shell, 12 lines - docker/
install_vscode.sh , Shell, 9 lines - preprocessing/
1_epoching.ipynb , Jupyter, 702 lines - preprocessing/
1_epoching_static.ipynb , Jupyter, 696 lines - preprocessing/
old/ , Jupyter, 506 lines, 1 matchTemplate_preprocessing.i pynb - preprocessing/
old/ , Jupyter, 194 linesevent_signif_crop.ipynb - preprocessing/
old/ , Jupyter, 112 linesevent_signif_visu.ipynb - preprocessing/
old/ , Jupyter, 148 lineshmp_analysis_t1.ipynb - preprocessing/
old/ , Jupyter, 148 lineshmp_analysis_t2.ipynb - preprocessing/
old/ , Jupyter, 484 lines, 1 matchpreprocessing.ipynb - preprocessing/
old/ , Jupyter, 328 linessplit_epochs_resplocked. ipynb - preprocessing/
old/ , Jupyter, 222 linessplit_epochs_stimlocked. ipynb - s4/
1_loading.ipynb , Jupyter, 211 lines - s4/
1_loading_static.ipynb , Jupyter, 191 lines - s4/
2_estimation static.ipynb , Jupyter, 242 lines - s4/
2_estimation.ipynb , Jupyter, 215 lines - s4/
3_training.ipynb , Jupyter, 165 lines - s4/
4_analysis_combined.ipyn , Jupyter, 896 lines, 1 matchb - s4/
4_analysis_prep.ipynb , Jupyter, 279 lines - s4/
4_analysis_r.ipynb , Jupyter, 231 lines, 1 match - s4/
7_embeddings_cluster.ipy , Jupyter, 308 linesnb - s4/
7_embeddings_prep.ipynb , Jupyter, 283 lines - s4/
7_embeddings_prep_combin , Jupyter, 166 linesed.ipynb - s4/
7_embeddings_visu.ipynb , Jupyter, 297 lines, 1 match - s4/
combine_tasks.ipynb , Jupyter, 113 lines - s4/
depr/ , Jupyter, 312 lines5_analysis_single.ipynb - s4/
prp2/ , Jupyter, 358 lines1_loading.ipynb - s4/
prp2/ , Jupyter, 143 lines, 1 match2_estimation.ipynb - s4/
prp2/ , Jupyter, 141 lines3_training.ipynb - s4/
prp2/ , Jupyter, 85 linestest.ipynb - s4/
prp_visu.py , Python, 187 lines - s4/
test.ipynb , Jupyter, 25 lines
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://
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://
BibTeX
@article{denotter2026tri
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/
url = {https://
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/
VL - 22
IS - 5
SP - e1014302
SN - 1553-734X
PB - PLOS
DO - 10.1371/
UR - https://
LA - en
ER -
CSL-JSON
{
"id": "10.1371/
"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":
"volume": "22",
"issue": "5",
"page": "e1014302",
"DOI": "10.1371/
"PMID": "42160393",
"PMCID": "PMC13215611",
"ISSN": "1553-734X",
"publisher": "PLOS",
"URL": "https://
"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 consciousnessIn 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 dataIn 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 dataIn 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 reportsIn 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 adaptationJournal: 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: eLifeIn 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 reportsIn 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 journalIn 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.
Claim this paper
Correct its record
Say what each link of this record is, remove the ones that are not the paper's, add the ones that are missing. The correction becomes a new version of the record, in its Versions section.
Validate its tracing map
You validate the map as this page shows it: 1 repository of the authors' code, each at its verified commit and with its license, 35 scripts, and 6 matches between paragraphs and code (see the Code and Map sections). It then receives a DOI on Zenodo, with you (your ORCID iD) and OSCR as its creators; the code itself is not deposited.
The map's fingerprint: sha256:c60c021a3f809824…
Add the badge to its README
The badge links the code to this page. Copy one of these into the README of the paper's code: only you decide where it goes, and nothing is changed for you.
Markdown
[, paste the snippet at the top, then “Commit changes…” and, to review it first, “Create a new branch and start a pull request”. You open the pull request; OSCR asks for no permission.
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.
