Learning regularities in noise engages both neural predictive activity and representational changes.
The 10 matches · 2 of them tie a paragraph to a whole file, not to given lines: weak matches, whose lines are not tinted
- [1] § Methods › Representational similarity analysis ↔ base.py, lines 114–170 · score 0.81 · Ledoit Wolf, covariance matrix, Mahalanobis distance, OLS, shrinkage, residuals
- [2] § Methods › MEG recordings and preprocessing ↔ 02_preprocessing/save_epochs.py, lines 56–115 · score 0.74 · filtered raw, unfiltered, component, ECG, preprocessing, EOG
- [3] § Methods › MEG recordings and preprocessing ↔ 02_preprocessing/save_reordered_epochs_practice.py, lines 74–154 · score 0.74 · filtered raw, artifacts, unfiltered, component, ECG, preprocessing
- [4] § Methods › Decoding ↔ 04_source/time_gen/decode_htc.py, lines 42–171 · score 0.68 · standard scaler, logistic regression, source space, pipeline, decoding, classifier
- [5] § Methods › Decoding ↔ base.py, lines 193–208 · score 0.64 · standard scaler, logistic regression, stratified, folding, pipeline, classifier
- [6] § Methods › Representational similarity analysis ↔ base.py, lines 446–526 · score 0.62 · Ledoit Wolf, Cross validation, class, covariance, distance, training
- [7] § Methods › Source reconstruction ↔ config.py, the whole file · a weak match · score 0.58 · central executive, dorsal attention, limbic, salience, Freesurfer, thalamus
- [8] § Results › Distributed predictive activity in the sensorimotor, dorsal attention, central executive, and default mode networks ↔ config.py, the whole file · a weak match · score 0.57 · central executive, dorsal attention, limbic, salience, thalamus, sensorimotor
- [9] § Methods › Source reconstruction ↔ 04_source/schaefer_to_destrieux.py, lines 1–22 · score 0.57 · FreeSurfer, Schaefer, parcellate, atlas, brain, networks
- [10] § Methods › Non-linear temporal modeling ↔ 05_gam/rsa_tables.qmd, lines 199–240 · score 0.54 · random intercept, random smooth, linear, models, block
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 · 924 lines · 35 KB · no license · 3 matches
- import os
- import os.path as op
- import numpy as np
- from mne.stats import permutation_cluster_1samp_test
- from sklearn.pipeline import make_pipeline
- from mne.decoding import SlidingEstimator
- from sklearn.model_selection import StratifiedKFold
- from sklearn.preprocessing import StandardScaler
- from sklearn.linear_model import LogisticRegressionCV
- def ensure_dir(path):
- if not os.path.exists(path):
- os.makedirs(path, exist_ok=True)
- def ensured(path):
- if not os.path.exists(path):
- os.makedirs(path, exist_ok=True)
- return path
- def decod_stats(X, jobs):
- """Statistical test applied across subjects for Decoding"""
- # check input
- if not isinstance(X, np.ndarray):
- X = np.array(X)
- X = X.astype(np.float64)
- # stats function report p_value for each cluster
- T_obs_, clusters, p_values, _ = permutation_cluster_1samp_test(
- X, out_type='mask', n_permutations=2**12, n_jobs=jobs,
- verbose=False)
- # format p_values to get same dimensionality as X
- p_values_ = np.ones_like(X[0]).T
- for cluster, pval in zip(clusters, p_values):
- p_values_[cluster] = pval
- return np.squeeze(p_values_)
- def gat_stats(X, jobs):
- """Statistical test applied across subjects for Temporal Generalization"""
- from mne.stats import spatio_temporal_cluster_1samp_test
- # check input
- X = np.array(X)
- X = X[:, :, None] if X.ndim == 2 else X
- # stats function report p_value for each cluster
- T_obs_, clusters, p_values, _ = spatio_temporal_cluster_1samp_test(
- X, out_type='mask',
- n_permutations=2**10, n_jobs=jobs, verbose=True)
- # format p_values to get same dimensionality as X
- p_values_ = np.ones_like(X[0]).T
- for cluster, pval in zip(clusters, p_values):
- p_values_[cluster.T] = pval
- return np.squeeze(p_values_).T
- def gat_t1samp(X):
- from scipy.stats import ttest_1samp
- X = np.array(X)
- X = X[:, :, None] if X.ndim == 2 else X
- t_values = np.zeros_like(X[0])
- for itime in range(X.shape[1]):
- t_values[itime] = ttest_1samp(X[:, itime], 0)[0]
- return t_values
- def do_pca(epochs):
- import mne
- from mne.decoding import UnsupervisedSpatialFilter
- from sklearn.decomposition import PCA
- n_component = 30
- pca = UnsupervisedSpatialFilter(PCA(n_component), average=False)
- pca_data = pca.fit_transform(epochs.get_data())
- sampling_freq = epochs.info['sfreq']
- info = mne.create_info(n_component, ch_types='mag', sfreq=sampling_freq)
- all_epochs = mne.EpochsArray(pca_data, info = info, events=epochs.events, event_id=epochs.event_id)
- return all_epochs
- def get_sequence(behav_dir):
- """Get the sequence of a subject from the behavioral file."""
- behav_files = [f for f in os.listdir(behav_dir) if (not f.startswith('.') and ('_eASRT_Epoch_' in f))]
- behav = open(op.join(behav_dir, behav_files[0]), 'r')
- lines = behav.readlines()
- column_names = lines[0].split()
- sequence = list()
- for line in lines[1:]:
- trialtype = int(line.split()[column_names.index('trialtype')])
- if trialtype == 1:
- sequence.append(int(line.split()[column_names.index('position')]))
- if len(sequence) == 4:
- break
- return sequence
- def get_random_low(behav_dir):
- behav_files = [f for f in os.listdir(behav_dir) if (not f.startswith('.') and ('_eASRT_Epoch_' in f))]
- behav = open(op.join(behav_dir, behav_files[0]), 'r')
- lines = behav.readlines()
- column_names = lines[0].split()
- rdm_low = list()
- for iline, line in enumerate(lines[1:]):
- triplet = int(line.split()[column_names.index('triplet')])
- if triplet == 34:
- first = line.split()[column_names.index('position')]
- second = lines[iline-2].split()[column_names.index('position')]
- pair = first + second
- if pair not in rdm_low:
- # print(pair)
- rdm_low.append(pair)
- # if len(rdm_low) == 3:
- # break
- return rdm_low
- def get_rdm(epoch, behav):
- from scipy.spatial.distance import pdist, squareform
- import scipy.stats
- import statsmodels.api as sm
- # from tqdm.auto import tqdm
- from sklearn.covariance import LedoitWolf
- import pandas as pd
- # Prepare the design matrix
- ntrials = len(epoch)
- nconditions = 4
- design_matrix = np.zeros((ntrials, nconditions))
- if type(behav) == pd.core.frame.DataFrame:
- y = behav["positions"]
- else:
- y = behav
- for icondi, condi in enumerate(y):
- # assert isinstance(condi, np.int64)
- design_matrix[icondi, condi-1] = 1
- assert np.sum(design_matrix.sum(axis=1) == 1) == len(epoch)
- meg_data_V = epoch
- _, nchs, ntimes = meg_data_V.shape
- meg_data_V = scipy.stats.zscore(meg_data_V, axis=0)
- coefs = np.zeros((nconditions, nchs, ntimes))
- resids = np.zeros_like(meg_data_V)
- # for ich in tqdm(range(nchs)):
- for ich in range(nchs):
- for itime in range(ntimes):
- y = meg_data_V[:, ich, itime]
- model = sm.OLS(endog=y, exog=design_matrix, missing="raise")
- results = model.fit()
- coefs[:, ich, itime] = results.params # (4, 248, 163)
- resids[:, ich, itime] = results.resid # (ntrials, 248, 163)
- # Calculate pairwise mahalanobis distance between regression coefficients
- rdm_times = np.zeros((nconditions, nconditions, ntimes))
- for itime in range(ntimes):
- response = coefs[:, :, itime] # (4, 248)
- residuals = resids[:, :, itime] # (51, 248)
- # Estimate covariance from residuals
- lw_shrinkage = LedoitWolf(assume_centered=True)
- cov = lw_shrinkage.fit(residuals)
- # Compute pairwise mahalanobis distances
- VI = np.linalg.inv(cov.covariance_) # inverse of covariance matrix needed for mahalonobis
- rdm = squareform(pdist(response, metric="mahalanobis", VI=VI))
- # rdm = squareform(pdist(response, metric="cosine"))
- assert ~np.isnan(rdm).any()
- rdm_times[:, :, itime] = rdm # rdm_times (4, 4, 163), rdm (4, 4)
- return rdm_times
- def get_inseq(sequence):
- """Get the pairs in the sequence."""
- # create list of possible pairs
- pairs_in_sequence = list()
- pairs_in_sequence.append(str(sequence[0]) + str(sequence[1]))
- pairs_in_sequence.append(str(sequence[1]) + str(sequence[2]))
- pairs_in_sequence.append(str(sequence[2]) + str(sequence[3]))
- pairs_in_sequence.append(str(sequence[3]) + str(sequence[0]))
- return pairs_in_sequence
- def print_proportions(subject, all_beh):
- #### get stimuli proportions
- print(f"############### {subject}")
- for i, sess in zip(range(5), ['prac', 'b1', 'b2', 'b3', 'b4']):
- print(f"{sess} ----------------------")
- unique, values = np.unique(all_beh[i].positions, return_counts=True)
- for un, val in zip(unique, values):
- print(un, round((val/np.sum(values)*100), 2))
- def make_predictions(X, y, folds, jobs, scoring, verbose):
- # set-up the classifier and cv structure
- clf = make_pipeline(StandardScaler(), LogisticRegressionCV(multi_class="ovr", max_iter=100000, solver='saga', random_state=42)) # use JAX maybe
- # clf = make_pipeline(StandardScaler(), SGDRegressor(loss="squared_error", max_iter=100000, random_state=42)) # use JAX maybe
- clf = SlidingEstimator(clf, scoring=scoring, n_jobs=jobs, verbose=verbose) # get time of one sample (slide), try with less jobs maybe ?
- cv = StratifiedKFold(folds, shuffle=True)
- pred = np.zeros((len(y), X.shape[-1]))
- pred_rock = np.zeros((len(y), X.shape[-1], len(set(y))))
- # there is only randoms in practice sessions
- for train, test in cv.split(X, y):
- clf.fit(X[train], y[train])
- pred[test] = np.array(clf.predict(X[test]))
- pred_rock[test] = np.array(clf.predict_proba(X[test]))
- return test, pred, pred_rock
- def get_volume_estimate_time_course(stcs, fwd, subject, subjects_dir):
- """Extracts time courses for each label from volume source estimates.
- Args:
- stcs (list of mne.VolSourceEstimate): List of volume source estimates.
- fwd (dict): Forward solution.
- subject (str): Subject name.
- subjects_dir (str): Path to SUBJECTS_DIR.
- Returns:
- dict: A dictionary with label names as keys and arrays of shape
- (n_epochs, n_vertices_in_label, n_times) as values.
- """
- import numpy as np
- from mne import get_volume_labels_from_src
- from tqdm.auto import tqdm
- labels = get_volume_labels_from_src(fwd['src'], subject, subjects_dir)
- vertices_info = dict()
- for label in labels:
- vertices_info[label.name] = len(label.vertices)
- # Initialize a dictionary to hold time courses for each label
- label_time_courses = {}
- # Loop through each STC (source time course) for each epoch
- for stc in tqdm(stcs):
- # Extract data from the STC
- stc_data = stc.data # shape: (n_vertices, n_times)
- # Loop through each label to extract the time course
- for ilabel, label in enumerate(labels):
- if ilabel >= len(stc.vertices):
- # If ilabel exceeds the number of vertex arrays, break the loop
- break
- # Get the vertices in the label
- label_vertices = np.intersect1d(stc.vertices[ilabel+2], label.vertices)
- if label_vertices.size == 0:
- continue
- # Get indices of these vertices in the STC data
- indices = np.searchsorted(stc.vertices[ilabel+2], label_vertices)
- # Extract the time courses for these vertices
- vertices_time_courses = stc_data[indices, :]
- # Store the time courses in the dictionary
- if label.name not in label_time_courses:
- label_time_courses[label.name] = []
- label_time_courses[label.name].append(vertices_time_courses)
- # Convert to numpy arrays
- for label in label_time_courses:
- label_time_courses[label] = np.array(label_time_courses[label]) # shape: (n_trials, n_vertices_in_label, n_times)
- return label_time_courses, vertices_info
- def get_labels_from_vol_src(src, subject, subjects_dir):
- from mne import Label
- from mne import get_volume_labels_from_aseg
- """Return a list of Label of segmented volumes included in the src space.
- Parameters
- ----------
- src : instance of SourceSpaces
- The source space containing the volume regions.
- %(subject)s
- subjects_dir : str
- Freesurfer folder of the subjects.
- Returns
- -------
- labels_aseg : list of Label
- List of Label of segmented volumes included in src space.
- """
- # from ..label import Label
- # Read the aseg file
- aseg_fname = op.join(subjects_dir, subject, "mri", "aseg.mgz")
- all_labels_aseg = get_volume_labels_from_aseg(aseg_fname, return_colors=True)
- if any(np.any(s["type"] != "vol") for s in src):
- raise ValueError("source spaces have to be of vol type")
- labels_aseg = list()
- for nr in range(len(src)):
- vertices = src[nr]["vertno"]
- pos = src[nr]["rr"][src[nr]["vertno"], :]
- roi_str = src[nr]["seg_name"]
- try:
- ind = all_labels_aseg[0].index(roi_str)
- color = np.array(all_labels_aseg[1][ind]) / 255
- except ValueError:
- pass
- if "left" in roi_str.lower():
- hemi = "lh"
- roi_str = roi_str.replace("Left-", "") + "-lh"
- elif "right" in roi_str.lower():
- hemi = "rh"
- roi_str = roi_str.replace("Right-", "") + "-rh"
- else:
- hemi = "both"
- label = Label(
- vertices=vertices,
- pos=pos,
- hemi=hemi,
- name=roi_str,
- color=color,
- subject=subject,
- )
- labels_aseg.append(label)
- return labels_aseg
- def get_volume_estimate_tc(stcs, fwd, offsets, subject, subjects_dir):
- """Extracts time courses for each label from volume source estimates."""
- import numpy as np
- from mne import get_volume_labels_from_src
- labels = get_volume_labels_from_src(fwd['src'], subject, subjects_dir)
- vertices_info = dict()
- for label in labels:
- vertices_info[label.name] = len(label.vertices)
- # Initialize a dictionary to hold time courses for each label
- label_time_courses = {}
- for stc in stcs:
- stc_data = stc.data # shape: (n_vertices, n_times)
- for ilabel, label in enumerate(labels):
- tc = stc_data[offsets[ilabel]:offsets[ilabel+1]]
- if label.name not in label_time_courses:
- label_time_courses[label.name] = []
- label_time_courses[label.name].append(tc)
- # Convert to numpy arrays
- for label in label_time_courses:
- label_time_courses[label] = np.array(label_time_courses[label]) # shape: (n_trials, n_vertices_in_label, n_times)
- return label_time_courses, vertices_info
- def rsync_files(source, destination, options=""):
- """Rsync files from source to destination with given options."""
- import subprocess
- try:
- # Construct the rsync command
- command = f"rsync {options} --progress --ignore-existing {source} {destination}"
- # Execute the command
- result = subprocess.run(command, shell=True, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
- # Decode and print the output and errors (if any)
- stdout = result.stdout.decode()
- stderr = result.stderr.decode()
- print(stdout)
- if stderr:
- print(f"Errors during rsync: {stderr}")
- print("Rsync operation completed successfully.")
- return stdout
- except subprocess.CalledProcessError as e:
- print(f"An error occurred: {e.stderr.decode()}")
- return None
- def get_in_out_seq(sequence, similarities, random_lows, analysis):
- """Get in-sequence and out-sequence similarity values based on the analysis type."""
- import numpy as np
- # create list of possible pairs
- pairs_in_sequence = list()
- pairs_in_sequence.append(str(sequence[0]) + str(sequence[1]))
- pairs_in_sequence.append(str(sequence[1]) + str(sequence[2]))
- pairs_in_sequence.append(str(sequence[2]) + str(sequence[3]))
- pairs_in_sequence.append(str(sequence[3]) + str(sequence[0]))
- in_seq, out_seq = [], []
- pairs = ['12', '13', '14', '23', '24', '34']
- rev_pairs = ['21', '31', '41', '32', '42', '43']
- if analysis == 'pat_high_rdm_high':
- for pair, rev_pair, pat_sim, rand_sim in zip(pairs, rev_pairs, similarities, random_lows):
- if ((pair in pairs_in_sequence) or (rev_pair in pairs_in_sequence)):
- in_seq.append(pat_sim)
- out_seq.append(rand_sim)
- elif analysis == 'pat_high_rdm_low':
- for pair, rev_pair, pat_sim, rand_sim in zip(pairs, rev_pairs, similarities, random_lows):
- if ((pair in pairs_in_sequence) or (rev_pair in pairs_in_sequence)):
- in_seq.append(pat_sim)
- else:
- out_seq.append(rand_sim)
- else:
- for pair, rev_pair, pat_sim, rand_sim in zip(pairs, rev_pairs, similarities, random_lows):
- if ((pair in pairs_in_sequence) or (rev_pair in pairs_in_sequence)):
- in_seq.append(pat_sim)
- else:
- out_seq.append(pat_sim)
- return np.array(in_seq), np.array(out_seq)
- def get_all_high_low(pattern_data, random_data, sequence, block=True, step=1):
- """
- Extracts high and low similarity sets from RDMs based on a stimulus sequence.
- Parameters:
- pattern_data: np.ndarray
- RDM for patterned stimuli:
- - block=True: (n_blocks, n_timepoints, n_items, n_items)
- - block=False: (n_epochs, n_timepoints, n_items, n_items)
- random_data: np.ndarray
- RDM for random stimuli (same shape as pattern_data).
- sequence: list[int]
- A list of 4 stimulus identifiers, e.g., [1, 2, 3, 4].
- block: bool (default=True)
- If True, treats data as block-based; else session-based (cv-style).
- Returns:
- high: np.ndarray
- Similarity values for pairs in the input sequence.
- low: np.ndarray
- Corresponding values from the random data (used as "low" set).
- """
- pair_indices = [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]
- pair_labels = ['12', '13', '14', '23', '24', '34']
- # Generate forward and reverse sequence-based pairs
- limit = 4 if step == 1 else 4 - step
- sequence_pairs = {f"{sequence[i]}{sequence[(i+step)%4]}" for i in range(limit)}
- sequence_pairs |= {p[::-1] for p in sequence_pairs}
- high, low = [], []
- for (i, j), label in zip(pair_indices, pair_labels):
- if block:
- pat_vals = pattern_data[:, :, i, j]
- rand_vals = random_data[:, :, i, j]
- else:
- pat_vals = np.array([pattern_data[epoch, :, i, j] for epoch in range(pattern_data.shape[0])])
- rand_vals = np.array([random_data[epoch, :, i, j] for epoch in range(random_data.shape[0])])
- if label in sequence_pairs:
- high.append(pat_vals)
- low.append(rand_vals)
- return np.array(high), np.array(low)
- def cv_mahalanobis_parallel(X, y, n_jobs=-1, n_splits=10, verbose=True, shuffle=True):
- """
- Parallelized Cross-validated Mahalanobis distances with tqdm_joblib and NaN safety.
- Parameters:
- X: ndarray (n_trials, n_channels, n_times)
- y: array-like (n_trials,) condition labels
- n_splits: int, number of cross-validation folds
- n_jobs: int, number of parallel jobs (default: -1 = all CPUs)
- verbose: bool, show progress bars
- Returns:
- distances: ndarray (n_times, n_conditions, n_conditions)
- """
- import numpy as np
- from sklearn.model_selection import StratifiedKFold, KFold
- from sklearn.covariance import LedoitWolf
- from scipy.linalg import solve
- from joblib import Parallel, delayed
- from tqdm.auto import tqdm
- from tqdm_joblib import tqdm_joblib
- n_trials, n_channels, n_times = X.shape
- conditions = np.unique(y)
- n_conditions = len(conditions)
- if shuffle:
- skf = StratifiedKFold(n_splits=n_splits, shuffle=True, random_state=42)
- else:
- skf = KFold(n_splits=n_splits, shuffle=False)
- def compute_timepoint(t):
- X_t = X[:, :, t]
- dist_folds = np.zeros((n_conditions, n_conditions, n_splits))
- fold_iterator = skf.split(X_t, y)
- if verbose:
- fold_iterator = tqdm(fold_iterator, total=n_splits, desc=f"Time {t:03}", leave=False, position=t % 8)
- for fold, (train_idx, test_idx) in enumerate(fold_iterator):
- X_train, X_test = X_t[train_idx], X_t[test_idx]
- y_train, y_test = y[train_idx], y[test_idx]
- try:
- lw = LedoitWolf()
- lw.fit(X_train)
- cov = lw.covariance_
- train_means = {c: X_train[y_train == c].mean(axis=0) for c in conditions}
- test_means = {c: X_test[y_test == c].mean(axis=0) for c in conditions}
- for i, ci in enumerate(conditions):
- for j, cj in enumerate(conditions):
- if j <= i:
- continue
- diff_train = train_means[ci] - train_means[cj]
- diff_test = test_means[ci] - test_means[cj]
- dist = diff_train.T @ solve(cov, diff_test, assume_a='pos')
- dist_folds[i, j, fold] = dist
- dist_folds[j, i, fold] = dist
- except (ValueError, np.linalg.LinAlgError, KeyError, ZeroDivisionError):
- # Catch common issues: singular covariances, missing classes, empty slices
- dist_folds[:, :, fold] = np.nan
- return np.nanmean(dist_folds, axis=2) # <- safely ignore folds with NaNs
- time_iterator = range(n_times)
- if verbose:
- with tqdm_joblib(tqdm(desc="Overall Timepoints", total=n_times)) as progress_bar:
- distances = Parallel(n_jobs=n_jobs)(
- delayed(compute_timepoint)(t) for t in time_iterator
- )
- else:
- distances = Parallel(n_jobs=n_jobs)(
- delayed(compute_timepoint)(t) for t in time_iterator
- )
- return np.stack(distances, axis=0)
- def train_test_mahalanobis_fast(X_train, X_test, y_train, y_test, n_jobs=-1, verbose=True):
- """
- Computes Mahalanobis distances between class means in training and testing sets.
- Handles missing conditions by filling with NaNs.
- Returns:
- distances: ndarray (n_times, n_conditions, n_conditions)
- """
- import numpy as np
- from sklearn.covariance import LedoitWolf
- from scipy.linalg import solve
- from joblib import Parallel, delayed
- from tqdm.auto import tqdm
- from tqdm_joblib import tqdm_joblib
- n_trials_train, n_channels, n_times = X_train.shape
- n_trials_test, _, _ = X_test.shape
- conditions = [1, 2, 3, 4]
- n_conditions = len(conditions)
- cond_idx = {c: i for i, c in enumerate(conditions)} # for indexing
- def compute_timepoint(t):
- try:
- Xtr = X_train[:, :, t]
- Xte = X_test[:, :, t]
- # Skip timepoints with too few trials
- if Xtr.shape[0] < 2 or Xte.shape[0] < 2:
- return np.full((n_conditions, n_conditions), np.nan)
- lw = LedoitWolf().fit(Xtr)
- cov = lw.covariance_
- # Only keep conditions present in both train and test
- present_train = set(y_train)
- present_test = set(y_test)
- valid_conditions = list(present_train & present_test)
- train_means = {}
- test_means = {}
- for c in valid_conditions:
- train_means[c] = Xtr[y_train == c].mean(0)
- test_means[c] = Xte[y_test == c].mean(0)
- dist = np.full((n_conditions, n_conditions), np.nan)
- for i, ci in enumerate(conditions):
- for j, cj in enumerate(conditions):
- if j <= i:
- continue
- if ci in train_means and cj in train_means and ci in test_means and cj in test_means:
- diff_train = train_means[ci] - train_means[cj]
- diff_test = test_means[ci] - test_means[cj]
- try:
- mahal = diff_train.T @ solve(cov, diff_test, assume_a='pos')
- except np.linalg.LinAlgError:
- mahal = np.nan
- dist[cond_idx[ci], cond_idx[cj]] = mahal
- dist[cond_idx[cj], cond_idx[ci]] = mahal
- return dist
- except Exception as e:
- print(f"Error at time {t}: {e}")
- return np.full((n_conditions, n_conditions), np.nan)
- time_iterator = range(n_times)
- if verbose:
- with tqdm_joblib(tqdm(desc="Computing Mahalanobis", total=n_times)):
- distances = Parallel(n_jobs=n_jobs)(
- delayed(compute_timepoint)(t) for t in time_iterator
- )
- else:
- distances = Parallel(n_jobs=n_jobs)(
- delayed(compute_timepoint)(t) for t in time_iterator
- )
- return np.stack(distances, axis=0) # shape: (n_times, n_conditions, n_conditions)
- def loocv_mahalanobis_parallel(X, y, n_jobs=-1, verbose=True):
- """
- Parallel cross-validated Mahalanobis distances using LOOCV,
- with per-fold progress bars for each timepoint.
- Parameters:
- X: ndarray (n_trials, n_channels, n_times)
- y: array-like (n_trials,) condition labels
- n_jobs: int, number of parallel jobs (default: -1 = all cores)
- verbose: bool, whether to show progress bars
- Returns:
- distances: ndarray (n_times, n_conditions, n_conditions)
- """
- import numpy as np
- from sklearn.model_selection import LeaveOneOut
- from sklearn.covariance import LedoitWolf
- from scipy.linalg import solve
- from joblib import Parallel, delayed
- from tqdm.auto import tqdm
- n_trials, n_channels, n_times = X.shape
- conditions = np.unique(y)
- n_conditions = len(conditions)
- loo = LeaveOneOut()
- def compute_timepoint(t):
- X_t = X[:, :, t]
- dist_folds = np.full((n_conditions, n_conditions, n_trials), np.nan)
- iterator = loo.split(X_t)
- if verbose:
- iterator = tqdm(iterator, total=n_trials, desc=f"Time {t:03}", leave=False, position=t % 8)
- for fold, (train_idx, test_idx) in enumerate(iterator):
- X_train, X_test = X_t[train_idx], X_t[test_idx]
- y_train, y_test = y[train_idx], y[test_idx]
- if len(np.unique(y_train)) < len(conditions):
- continue
- lw = LedoitWolf()
- lw.fit(X_train)
- cov = lw.covariance_
- train_means = {c: X_train[y_train == c].mean(axis=0) for c in conditions}
- test_means = {c: X_test[y_test == c].mean(axis=0) for c in conditions if np.any(y_test == c)}
- for i, ci in enumerate(conditions):
- for j, cj in enumerate(conditions):
- if j <= i or ci not in test_means or cj not in test_means:
- continue
- diff_train = train_means[ci] - train_means[cj]
- diff_test = test_means[ci] - test_means[cj]
- if np.isnan(diff_test).any() or np.isnan(diff_train).any():
- continue
- dist = diff_train.T @ solve(cov, diff_test, assume_a='pos')
- dist_folds[i, j, fold] = dist
- dist_folds[j, i, fold] = dist
- return np.nanmean(dist_folds, axis=2)
- if verbose:
- time_iterator = tqdm(range(n_times), desc="Timepoints")
- else:
- time_iterator = range(n_times)
- distances = Parallel(n_jobs=n_jobs)(
- delayed(compute_timepoint)(t) for t in time_iterator
- )
- return np.stack(distances, axis=0)
- def interpolate_rdm_nan(rdm):
- """Interpolate nan values in a RDM matrix by computing the mean of previous and subsequent block.
- Args:
- rdm: ndarray of shape (n_blocks, n_times, n_conditions, n_conditions)
- LOOCV Mahalanobis distances between conditions at each time point.
- Returns:
- rdm: ndarray of shape (n_blocks, n_times, n_conditions, n_conditions)
- LOOCV Mahalanobis distances between conditions at each time point.
- present_nan: bool
- True if nan values were present in the RDM, False otherwise
- """
- present_nan = False
- n_blocks, n_times, n_conditions, _ = rdm.shape
- for i in range(n_blocks):
- for j in range(n_times):
- if np.isnan(np.sum(rdm[i, j])):
- present_nan = True
- if i == 0:
- rdm[i, j] = rdm[i + 1, j]
- elif i == n_blocks - 1:
- rdm[i, j] = rdm[i - 1, j]
- else:
- rdm[i, j] = (rdm[i - 1, j] + rdm[i + 1, j]) / 2
- return rdm, present_nan
- def contiguous_regions(condition):
- """Find contiguous True regions in a boolean array."""
- import numpy as np
- d = np.diff(condition.astype(int))
- starts = np.where(d == 1)[0] + 1
- ends = np.where(d == -1)[0] + 1
- if condition[0]:
- starts = np.r_[0, starts]
- if condition[-1]:
- ends = np.r_[ends, condition.size]
- return zip(starts, ends)
- def svd(vector_data):
- """Reduce 3 orientations to dominant orientation using Singular Value Decomposition."""
- # Initialize an array for storing the dominant orientation time series
- dominant_data = np.zeros((vector_data.shape[0], vector_data.shape[1], vector_data.shape[-1])) # (294, 8196, 82)
- for trial in range(vector_data.shape[0]): # Loop over trials
- for source in range(vector_data.shape[1]): # Loop over sources
- u, s, vh = np.linalg.svd(vector_data[trial, source, :, :], full_matrices=False) # SVD over orientation axis (3)
- dominant_time_series = vh[0, :] * s[0] # First right singular vector weighted by singular value
- dominant_data[trial, source, :] = dominant_time_series # Store in new array
- return dominant_data
- def svd_fast(vector_data):
- """Vectorized equivalent of `svd` using NumPy batched SVD."""
- _, s, vh = np.linalg.svd(vector_data, full_matrices=False)
- return vh[..., 0, :] * s[..., [0]]
- def get_train_test_blocks_net(data, fwd, behav, pick_ori, trial_type, block, blocks, rsa=False, verbose=False):
- """Helper function to get source data for training and testing."""
- from mne import compute_covariance, compute_rank
- from mne.beamformer import make_lcmv, apply_lcmv_epochs
- import numpy as np
- weight_norm = "unit-noise-gain-invariant" if pick_ori == 'vector' else "unit-noise-gain"
- this_block = behav.blocks == block
- if not rsa:
- if block in blocks[:3]:
- rand_blocks = np.random.choice(blocks[3:], size=19, replace=False)
- out_blocks = behav.blocks.isin(rand_blocks)
- else:
- out_blocks = (behav.blocks != block) & (behav.sessions != 0)
- else:
- out_blocks = behav.blocks != block
- tt = behav.trialtypes == 2 if trial_type == 'random' else behav.trialtypes == 1
- # compute training data
- tt_out_blocks = tt & out_blocks
- train_blocks = behav[tt_out_blocks]
- train_epochs = data[train_blocks.trials.values]
- assert len(train_blocks) == len(train_epochs), "Length mismatch in training epochs"
- ytrain = train_blocks.positions
- # compute testing data
- tt_this_block = tt & this_block
- test_blocks = behav[tt_this_block]
- test_epochs = data[test_blocks.trials.values]
- ytest = test_blocks.positions
- # compute noise covariance
- random = behav.trialtypes == 2
- noise_epochs = data[random & out_blocks]
- noise_cov = compute_covariance(noise_epochs, tmin=-0.2, tmax=0, method="empirical", rank="info", verbose=verbose)
- data_cov = compute_covariance(train_epochs, method="empirical", rank="info", verbose=verbose)
- rank = compute_rank(data_cov, info=train_epochs.info, rank=None, tol_kind='relative', verbose=verbose)
- filters = make_lcmv(train_epochs.info, fwd, data_cov, reg=0.05, noise_cov=noise_cov,
- pick_ori=pick_ori, weight_norm=weight_norm,
- rank=rank, reduce_rank=True, verbose=verbose)
- # apply LCMV beamformer to epochs
- stcs_train = apply_lcmv_epochs(train_epochs, filters=filters, verbose=verbose)
- stcs_test = apply_lcmv_epochs(test_epochs, filters=filters, verbose=verbose)
- return stcs_train, stcs_test, ytrain, ytest
- def get_train_test_blocks_htc(data, fwd, behav, pick_ori, trial_type, block, blocks, rsa=False, verbose=False):
- """Helper function to get source data for training and testing."""
- from mne import compute_covariance, compute_rank
- from mne.beamformer import make_lcmv, apply_lcmv_epochs
- weight_norm = "unit-noise-gain-invariant" if pick_ori == 'vector' else "unit-noise-gain"
- this_block = behav.blocks == block
- if not rsa:
- if block in blocks[:3]:
- rand_blocks = np.random.choice(blocks[3:], size=19, replace=False)
- out_blocks = behav.blocks.isin(rand_blocks)
- else:
- out_blocks = (behav.blocks != block) & (behav.sessions != 0)
- else:
- out_blocks = behav.blocks != block
- tt = behav.trialtypes == 2 if trial_type == 'random' else behav.trialtypes == 1
- tt_out_blocks = tt & out_blocks
- tt_this_block = tt & this_block
- # compute training data
- train_blocks = behav[tt_out_blocks]
- train_epochs = data[train_blocks.trials.values]
- random = behav.trialtypes == 2
- noise_epochs = data[random & out_blocks]
- noise_cov = compute_covariance(noise_epochs, tmin=-0.2, tmax=0, method="empirical", rank="info", verbose=verbose)
- data_cov = compute_covariance(train_epochs, method="empirical", rank="info", verbose=verbose)
- rank = compute_rank(data_cov, info=train_epochs.info, rank=None, tol_kind='relative', verbose=verbose)
- filters = make_lcmv(train_epochs.info, fwd, data_cov, reg=0.05, noise_cov=noise_cov,
- pick_ori=pick_ori, weight_norm=weight_norm,
- rank=rank, reduce_rank=True, verbose=verbose)
- stcs_train = apply_lcmv_epochs(train_epochs, filters=filters, verbose=verbose)
- ytrain = train_blocks.positions
- assert len(stcs_train) == len(ytrain), "Length mismatch in training data"
- # compute testing data
- test_blocks = behav[tt_this_block]
- test_epochs = data[test_blocks.trials.values]
- stcs_test = apply_lcmv_epochs(test_epochs, filters=filters, verbose=verbose)
- ytest = test_blocks.positions
- assert len(stcs_test) == len(ytest), "Length mismatch in testing data"
- return stcs_train, ytrain, stcs_test, ytest
- def fisher_z_and_ttest(rho_matrix):
- import numpy as np
- from scipy.stats import ttest_1samp
- """
- Applies Fisher z-transform and performs a one-sample t-test against 0 at each timepoint.
- Parameters:
- rho_matrix (np.ndarray): Array of shape (n_subjects, n_timepoints) with rho values.
- Returns:
- z_matrix (np.ndarray): Fisher z-transformed correlations, same shape as input.
- t_stats (np.ndarray): T-statistics at each timepoint, shape (n_timepoints,).
- p_vals (np.ndarray): P-values at each timepoint, shape (n_timepoints,).
- """
- # Clip rho values to avoid infinite z
- rho_matrix = np.clip(rho_matrix, -0.999999, 0.999999)
- # Fisher z-transform
- z_matrix = 0.5 * np.log((1 + rho_matrix) / (1 - rho_matrix))
- # One-sample t-test at each timepoint
- t_stats, p_vals = ttest_1samp(z_matrix, popmean=0, axis=0)
- return z_matrix, t_stats, p_vals
- def fisher_z_transform_3d(rho_3d):
- """
- Apply Fisher's z-transformation to a 3D array of correlation coefficients.
- Parameters:
- rho_3d (np.ndarray): Array of shape (n_subjects, n_timepoints, n_timepoints)
- Returns:
- z_3d (np.ndarray): Fisher z-transformed array, same shape as input.
- """
- import numpy as np
- rho_3d = np.clip(rho_3d, -0.999999, 0.999999)
- z_3d = 0.5 * np.log((1 + rho_3d) / (1 - rho_3d))
- return z_3d
- def reorder(random_events, events, raw_rd, dur=200, nsamples=33):
- import mne
- """
- Reorder random events to match the given sequence.
- Parameters:
- random_events -- array of random events
- events -- target sequence of events
- raw_rd -- raw data corresponding to random events
- dur -- duration (in samples) of pre-task and post-task data
- nsamples -- window duration in samples
- Returns:
- Reordered raw data, original random event indices, and reordered events.
- """
- events = list(events)
- orig_nums = []
- events_reord = []
- raw_Xrd = raw_rd.get_data()
- raw_reord = []
- new_sample = 0
- first_samp = raw_rd.first_samp
- # Start reordered random with the first seconds of random raw
- raw_reord.append(raw_Xrd[:, :dur])
- new_sample += dur
- random_events_numbers = np.arange(len(random_events))
- for event in events:
- if event[2] in random_events[:, 2]:
- index = random_events[:, 2].tolist().index(event[2])
- orig_nums.append(random_events_numbers[index])
- samp = random_events[index, 0] - first_samp
- raw_reord.append(raw_Xrd[:, samp:samp + nsamples])
- random_events = np.delete(random_events, index, axis=0)
- random_events_numbers = np.delete(random_events_numbers, index, axis=0)
- events_reord.append([new_sample, 0, event[2]])
- new_sample += nsamples
- else:
- break
- # End reordered random with the last seconds of random raw
- raw_reord.append(raw_Xrd[:, -dur:])
- orig_nums_reord = np.array(orig_nums)
- events_reord = np.array(events_reord)
- raw_reord = np.concatenate(raw_reord, axis=1)
- raw_reord = mne.io.RawArray(raw_reord, raw_rd.info)
- return raw_reord, orig_nums_reord, events_reord
base.py at commit fbfceb6, no license · at the source
Overview
- Université Claude Bernard Lyon 1, INSERM U1028, CNRS UMR5292, Lyon Neuroscience Research Center (CRNL), EDUWELL team, Lyon, France
- Gran Canaria Cognitive Research Center, Atlántico Medio University, Las Palmas de Gran Canaria, Spain
- School of Biological Sciences, Monash University, Melbourne, VIC 3800 Australia
- Normandie University, UNICAEN, CNRS, EthoS, UMR 6552, Caen, 14000 France
- Institut de Neurosciences de la Timone UMR 7289, Aix Marseille Université, CNRS, Marseille, 13005 France
- Université Claude Bernard Lyon 1, INSERM U1028, CNRS UMR5292, Lyon Neuroscience Research Center (CRNL), IMPACT team, Lyon, France
- BML-NAP Research Group, ELTE Eötvös Loránd University & HUN-REN Research Centre for Natural Sciences, Budapest, Hungary
Abstract
The abstract is not reproduced here: the paper's license (CC BY-NC-ND) does not allow it. Read it in the paper, at the publisher or on Europe PMC.
Repository
Its files are read in the Code ↔ Paper reader above, with 10 matches between paragraphs and lines of code.
MEL-Eduwell-lab/asrt_analyses
fbfceb6a5b64fdab69a14aa2fefeb11ff1a251e3, 8 June 2026Availability: 1 check, the latest on 27 September 2026: the link answers
- 27 September 2026: the link answers
77 files
- 01_behavior/
anova_RTs.py , Python, 100 lines - 01_behavior/
explicit_score.py , Python, 102 lines - 01_behavior/
get_seq.py , Python, 23 lines - 01_behavior/
online_offline.py , Python, 538 lines - 01_behavior/
plot_RTs.py , Python, 225 lines - 02_preprocessing/
convert_to_bids.py , Python, 201 lines - 02_preprocessing/
save_epochs.py , Python, 220 lines, 1 match - 02_preprocessing/
save_reordered_epochs.py , Python, 270 lines - 02_preprocessing/
save_reordered_epochs_pr , Python, 271 lines, 1 matchactice.py - 03_sensors/
rsa/ , Python, 311 linesrsa_figure.py - 03_sensors/
rsa/ , Python, 276 linesrsa_figure_supp.py - 03_sensors/
rsa/ , Python, 98 linesrsa_rdms.py - 03_sensors/
time_gen/ , Python, 137 linestimeg_lobo.py - 03_sensors/
time_gen/ , Python, 120 linestimeg_lobo_prac.py - 03_sensors/
time_gen/ , Python, 127 linestimeg_lobo_reordered.py - 03_sensors/
time_gen/ , Python, 146 linestimeg_lobo_supp.py - 03_sensors/
time_gen/ , Python, 195 linestimeg_plot.py - 03_sensors/
time_gen/ , Python, 154 linestimeg_plot_diag.py - 03_sensors/
time_gen/ , Python, 179 linestimeg_plot_opt_supp.py - 03_sensors/
time_gen/ , Python, 156 linestimeg_plot_reordered.py - 03_sensors/
time_gen/ , Python, 118 linestimeg_plot_supp.py - 04_source/
generate_labels.py , Python, 73 lines - 04_source/
morph_no_mri.py , Python, 37 lines - 04_source/
plot_stc.py , Python, 121 lines - 04_source/
rsa/ , Python, 110 linesrsa_blocks_htc.py - 04_source/
rsa/ , Python, 97 linesrsa_blocks_net.py - 04_source/
rsa/ , Python, 137 linesrsa_blocks_net_supp.py - 04_source/
rsa/ , Python, 409 linesrsa_figure_gam.py - 04_source/
rsa/ , Python, 203 linesrsa_figure_subreg _merged.py - 04_source/
rsa/ , Python, 180 linesrsa_figure_subreg.py - 04_source/
rsa/ , Python, 191 linesrsa_figure_supp.py - 04_source/
rsa/ , Python, 144 linesrsa_net_subreg.py - 04_source/
rsa/ , Python, 166 linesrsa_net_subreg_merged.py - 04_source/
schaefer2018_parc.sh , Shell, 71 lines - 04_source/
schaefer_to_destrieux.py , Python, 107 lines, 1 match - 04_source/
source_recon.py , Python, 200 lines - 04_source/
time_gen/ , Python, 184 lines, 1 matchdecode_htc.py - 04_source/
time_gen/ , Python, 175 linesdecode_net.py - 04_source/
time_gen/ , Python, 371 linesplot_timeg_diag.py - 04_source/
time_gen/ , Python, 348 linesplot_timeg_diag_preact.p y - 04_source/
time_gen/ , Python, 350 linesplot_timeg_diag_supp.py - 04_source/
time_gen/ , Python, 236 linesplot_timeg_matrix.py - 04_source/
time_gen/ , Python, 185 linestimeg_htc.py - 04_source/
time_gen/ , Python, 175 linestimeg_htc_wblock.py - 04_source/
time_gen/ , Python, 174 linestimeg_net.py - 04_source/
time_gen/ , Python, 211 linestimeg_net_subreg_merged. py - 04_source/
time_gen/ , Python, 195 linestimeg_net_subreg_merged_ plot.py - 04_source/
time_gen/ , Python, 180 linestimeg_net_supp.py - 04_source/
time_gen/ , Python, 180 linestimeg_net_supp_plot.py - 04_source/
time_gen/ , Python, 150 linestimeg_net_wblock.py - 04_source/
time_gen/ , Python, 99 linestimeg_pval.py - 05_gam/
get_tables_blocks.py , Python, 938 lines - 05_gam/
get_tables_blocks_supp.p , Python, 939 linesy - 05_gam/
rsa_tables.qmd , Quarto, 600 lines, 1 match - 05_gam/
timeg_tables.qmd , Quarto, 315 lines - 06_bash_files/
decode_htc.sh , Shell, 18 lines - 06_bash_files/
decode_net.sh , Shell, 18 lines - 06_bash_files/
rsa_blocks_htc.sh , Shell, 19 lines - 06_bash_files/
rsa_blocks_net.sh , Shell, 19 lines - 06_bash_files/
rsa_lobo_htc.sh , Shell, 19 lines - 06_bash_files/
rsa_lobo_net.sh , Shell, 19 lines - 06_bash_files/
rsa_net_subreg.sh , Shell, 19 lines - 06_bash_files/
rsa_sess_htc.sh , Shell, 20 lines - 06_bash_files/
rsa_sess_net.sh , Shell, 20 lines - 06_bash_files/
save_epochs.sh , Shell, 20 lines - 06_bash_files/
source_recon.sh , Shell, 18 lines - 06_bash_files/
timeg_htc.sh , Shell, 19 lines - 06_bash_files/
timeg_htc_wblock.sh , Shell, 18 lines - 06_bash_files/
timeg_lobo_reordered.sh , Shell, 19 lines - 06_bash_files/
timeg_net.sh , Shell, 18 lines - 06_bash_files/
timeg_net_subreg.sh , Shell, 19 lines - 06_bash_files/
timeg_net_supp.sh , Shell, 18 lines - 06_bash_files/
timeg_net_wblock.sh , Shell, 19 lines - __init__.py, Python, 1 line
- base.py, Python, 924 lines, 3 matches
- config.py, Python, 59 lines, 2 matches
- README.md, Text, 34 lines
Code availability statement
The paper has a code availability statement. Its license (CC BY-NC-ND) does not allow reproducing it here; in short, from what the harvester recognized in it:
- it points to the authors' code: MEL-Eduwell-lab/
asrt_analyses
Read it in the paper: doi.org/10.1038/s41467-026-74824-0.
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;
- 76 scripts, each with its path and the digest of its content;
- 10 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
- github.com/
nemardatasets/ , at github.com; found in DataCiteon008017 - openneuro:ds008017, at OpenNeuro; found in “Data availability”
Data availability statement
The paper has a data availability statement. Its license (CC BY-NC-ND) does not allow reproducing it here; in short, from what the harvester recognized in it:
- it points to a dataset: OpenNeuro ds008017
Read it in the paper: doi.org/10.1038/s41467-026-74824-0.
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 2, 28 September 2026
- Funding: added Aix-Marseille Université: ANR-11-INBS-0006, MICIU/AEI/10.13039/501100011033, ANR-16-CONV-0002; Ministerio de Ciencia, Innovación y Universidades: 10.13039.501100011033, 10.13039/501100011033/FEDER, AEI/10.13039/501100011033/ FEDER, MICIU /AEI /10.13039/501100011033 / FEDER, / AEI10.13039/501100011033, 13039, MICIU/AEI/ 10.13039/501,100,011,033 /, PID2024, MICIU/AEI/10.13039, 10.13039, 13039/501100011033, 501100011033, MICIU/AEI/10; Agence Nationale de la Recherche: 2019-ANR-LABX-02, MICIU/AEI/10.13039/501100011033, ANR-24-CE37-5807, ANR-16-CONV-0002, ANR-22-CPJ1-0042-01, ANR-16, ANR-11-INBS, ANR-11-INBS-0006, ANR-11-LABX-0042; Hungarian Scientific Research Fund: NKFIH ADVANCED153150; Centre National de la Recherche Scientifique: ANR-11-LABX-0042, CNRS-IN2P3, MICIU/AEI/10.13039/501100011033, ANR-11-INBS-0006; Université Claude Bernard Lyon 1: ED476, ANR-11-LABX-0042, 2019-ANR-LABX-02; Nemzeti Kutatási Fejlesztési és Innovációs Hivatal; LabEx BRAIN; European Regional Development Fund: 10.13039/501100011033, 13039/501100011033, MICIU/ AEI /10.13039/501100011033, AEI/10.13039/501100011033, MICIU/AEI/ 10.13039/501100011033 FEDER, 501100011033, MICIU/AEI/10; Agencia Estatal de Investigación: AEI//10.13039/501100011033/, 501100011033, MICIU/AEI /10.13039/501100011033, 10.13039/501100011033, 13039, 13039/501100011033, 10.13039, 501100011033/FEDER, AEI/10; Institut National de Physique Nucléaire et de Physique des Particules: CNRS/IN2P3
Version 1, 27 September 2026: the first record
Recorded: type, language, journal, volume, issue, pages, dates, 8 authors, 2 keywords, 11 MeSH terms, 68 references.
Cite
This paper
Tirou, C., Abdoun, O., Vékony, T., Tosatto, L., Brovelli, A., Vernet, M., Németh, D., & Quentin, R. (2026). Learning regularities in noise engages both neural predictive activity and representational changes. Nature communications, 17(1), 8372. https://
BibTeX
@article{tirou2026learni
author = {Tirou, Coumarane and Abdoun, Oussama and Vékony, Teodóra and Tosatto, Laure and Brovelli, Andrea and Vernet, Marine and Németh, Dezső and Quentin, Romain},
title = {{Learning regularities in noise engages both neural predictive activity and representational changes}},
journal = {Nature communications},
year = {2026},
month = jul,
volume = {17},
number = {1},
pages = {8372},
publisher = {Nature Publishing Group},
issn = {2041-1723},
doi = {10.1038/
url = {https://
pmid = {42409831},
pmcid = {PMC13473593}
}
RIS
TY - JOUR
AU - Tirou, Coumarane
AU - Abdoun, Oussama
AU - Vékony, Teodóra
AU - Tosatto, Laure
AU - Brovelli, Andrea
AU - Vernet, Marine
AU - Németh, Dezső
AU - Quentin, Romain
TI - Learning regularities in noise engages both neural predictive activity and representational changes
T2 - Nature communications
J2 - Nat Commun
PY - 2026
DA - 2026/
VL - 17
IS - 1
SP - 8372
SN - 2041-1723
PB - Nature Publishing Group
DO - 10.1038/
UR - https://
LA - en
ER -
CSL-JSON
{
"id": "10.1038/
"type": "article-journal",
"title": "Learning regularities in noise engages both neural predictive activity and representational changes",
"container-title": "Nature communications",
"author": [
{
"family": "Tirou",
"given": "Coumarane"
},
{
"family": "Abdoun",
"given": "Oussama"
},
{
"family": "Vékony",
"given": "Teodóra"
},
{
"family": "Tosatto",
"given": "Laure"
},
{
"family": "Brovelli",
"given": "Andrea"
},
{
"family": "Vernet",
"given": "Marine"
},
{
"family": "Németh",
"given": "Dezső"
},
{
"family": "Quentin",
"given": "Romain"
}
],
"container-title-short":
"volume": "17",
"issue": "1",
"page": "8372",
"DOI": "10.1038/
"PMID": "42409831",
"PMCID": "PMC13473593",
"ISSN": "2041-1723",
"publisher": "Nature Publishing Group",
"URL": "https://
"language": "en",
"issued": {
"date-parts": [
[
2026,
7,
7
]
]
}
}
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.1038/s41467-026-73568-1 [code]
- No evidence of neural feature-specific pre-activation during the prediction of an upcoming stimulus.Journal: Nature communicationsIn common: reticulate, easystats, MNE-Python, 4 other tools, cognitive, 4 references, 3 authors
- [2] 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, MNE-BIDS, Pingouin, 12 other tools, MEG, cognitive, 3 references
- [3] 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, MNE-BIDS, Pingouin, 12 other tools, MEG, 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, MNE-BIDS, Pingouin, 12 other tools, 1 reference
- [5] 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, Pingouin, easystats, 10 other tools, cognitive, 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, Pingouin, FreeSurfer, 9 other tools, MEG, 2 references
- [7] doi:10.7554/elife.103097 [code]
- Canonical neurodevelopmental trajectories of structural and functional manifolds.Journal: eLifeIn common: mgcv, reticulate, Pingouin, 8 other tools, 3 references
- [8] 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: MNE-BIDS, Pingouin, easystats, 10 other tools - [9] doi:10.1162/imag.a.1245 [code]
- Towards precision EEG connectomics: Evaluating the benefits of dense sampling.Journal: Imaging neuroscience (Cambridge, Mass.)In common: autoreject, Pingouin, FreeSurfer, 8 other tools, 3 references
- [10] doi:10.1038/s41467-026-75662-w [code]
- Distinct Roles of Deep and Superficial Cortical Layers in Tone Prediction, Comparison, and Adaptation in Human Auditory Cortices.Journal: Nature communicationsIn common: imageio, Pingouin, MNE-Python, 8 other tools, cognitive, 2 references
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, 76 scripts, and 10 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:bc04aa530cb55cf5…
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.
