OSCR

Learning regularities in noise engages both neural predictive activity and representational changes.

Code ↔ Paper

10 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 10 matches · 2 of them tie a paragraph to a whole file, not to given lines: weak matches, whose lines are not tinted
  1. [1] § Methods › Representational similarity analysis ↔ base.py, lines 114–170 · score 0.81 · Ledoit Wolf, covariance matrix, Mahalanobis distance, OLS, shrinkage, residuals
  2. [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. [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. [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. [5] § Methods › Decoding ↔ base.py, lines 193–208 · score 0.64 · standard scaler, logistic regression, stratified, folding, pipeline, classifier
  6. [6] § Methods › Representational similarity analysis ↔ base.py, lines 446–526 · score 0.62 · Ledoit Wolf, Cross validation, class, covariance, distance, training
  7. [7] § Methods › Source reconstruction ↔ config.py, the whole file · a weak match · score 0.58 · central executive, dorsal attention, limbic, salience, Freesurfer, thalamus
  8. [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. [9] § Methods › Source reconstruction ↔ 04_source/schaefer_to_destrieux.py, lines 1–22 · score 0.57 · FreeSurfer, Schaefer, parcellate, atlas, brain, networks
  10. [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

  1. import os
  2. import os.path as op
  3. import numpy as np
  4. from mne.stats import permutation_cluster_1samp_test
  5. from sklearn.pipeline import make_pipeline
  6. from mne.decoding import SlidingEstimator
  7. from sklearn.model_selection import StratifiedKFold
  8. from sklearn.preprocessing import StandardScaler
  9. from sklearn.linear_model import LogisticRegressionCV
  10. def ensure_dir(path):
  11. if not os.path.exists(path):
  12. os.makedirs(path, exist_ok=True)
  13. def ensured(path):
  14. if not os.path.exists(path):
  15. os.makedirs(path, exist_ok=True)
  16. return path
  17. def decod_stats(X, jobs):
  18. """Statistical test applied across subjects for Decoding"""
  19. # check input
  20. if not isinstance(X, np.ndarray):
  21. X = np.array(X)
  22. X = X.astype(np.float64)
  23. # stats function report p_value for each cluster
  24. T_obs_, clusters, p_values, _ = permutation_cluster_1samp_test(
  25. X, out_type='mask', n_permutations=2**12, n_jobs=jobs,
  26. verbose=False)
  27. # format p_values to get same dimensionality as X
  28. p_values_ = np.ones_like(X[0]).T
  29. for cluster, pval in zip(clusters, p_values):
  30. p_values_[cluster] = pval
  31. return np.squeeze(p_values_)
  32. def gat_stats(X, jobs):
  33. """Statistical test applied across subjects for Temporal Generalization"""
  34. from mne.stats import spatio_temporal_cluster_1samp_test
  35. # check input
  36. X = np.array(X)
  37. X = X[:, :, None] if X.ndim == 2 else X
  38. # stats function report p_value for each cluster
  39. T_obs_, clusters, p_values, _ = spatio_temporal_cluster_1samp_test(
  40. X, out_type='mask',
  41. n_permutations=2**10, n_jobs=jobs, verbose=True)
  42. # format p_values to get same dimensionality as X
  43. p_values_ = np.ones_like(X[0]).T
  44. for cluster, pval in zip(clusters, p_values):
  45. p_values_[cluster.T] = pval
  46. return np.squeeze(p_values_).T
  47. def gat_t1samp(X):
  48. from scipy.stats import ttest_1samp
  49. X = np.array(X)
  50. X = X[:, :, None] if X.ndim == 2 else X
  51. t_values = np.zeros_like(X[0])
  52. for itime in range(X.shape[1]):
  53. t_values[itime] = ttest_1samp(X[:, itime], 0)[0]
  54. return t_values
  55. def do_pca(epochs):
  56. import mne
  57. from mne.decoding import UnsupervisedSpatialFilter
  58. from sklearn.decomposition import PCA
  59. n_component = 30
  60. pca = UnsupervisedSpatialFilter(PCA(n_component), average=False)
  61. pca_data = pca.fit_transform(epochs.get_data())
  62. sampling_freq = epochs.info['sfreq']
  63. info = mne.create_info(n_component, ch_types='mag', sfreq=sampling_freq)
  64. all_epochs = mne.EpochsArray(pca_data, info = info, events=epochs.events, event_id=epochs.event_id)
  65. return all_epochs
  66. def get_sequence(behav_dir):
  67. """Get the sequence of a subject from the behavioral file."""
  68. behav_files = [f for f in os.listdir(behav_dir) if (not f.startswith('.') and ('_eASRT_Epoch_' in f))]
  69. behav = open(op.join(behav_dir, behav_files[0]), 'r')
  70. lines = behav.readlines()
  71. column_names = lines[0].split()
  72. sequence = list()
  73. for line in lines[1:]:
  74. trialtype = int(line.split()[column_names.index('trialtype')])
  75. if trialtype == 1:
  76. sequence.append(int(line.split()[column_names.index('position')]))
  77. if len(sequence) == 4:
  78. break
  79. return sequence
  80. def get_random_low(behav_dir):
  81. behav_files = [f for f in os.listdir(behav_dir) if (not f.startswith('.') and ('_eASRT_Epoch_' in f))]
  82. behav = open(op.join(behav_dir, behav_files[0]), 'r')
  83. lines = behav.readlines()
  84. column_names = lines[0].split()
  85. rdm_low = list()
  86. for iline, line in enumerate(lines[1:]):
  87. triplet = int(line.split()[column_names.index('triplet')])
  88. if triplet == 34:
  89. first = line.split()[column_names.index('position')]
  90. second = lines[iline-2].split()[column_names.index('position')]
  91. pair = first + second
  92. if pair not in rdm_low:
  93. # print(pair)
  94. rdm_low.append(pair)
  95. # if len(rdm_low) == 3:
  96. # break
  97. return rdm_low
  98. def get_rdm(epoch, behav):
  99. from scipy.spatial.distance import pdist, squareform
  100. import scipy.stats
  101. import statsmodels.api as sm
  102. # from tqdm.auto import tqdm
  103. from sklearn.covariance import LedoitWolf
  104. import pandas as pd
  105. # Prepare the design matrix
  106. ntrials = len(epoch)
  107. nconditions = 4
  108. design_matrix = np.zeros((ntrials, nconditions))
  109. if type(behav) == pd.core.frame.DataFrame:
  110. y = behav["positions"]
  111. else:
  112. y = behav
  113. for icondi, condi in enumerate(y):
  114. # assert isinstance(condi, np.int64)
  115. design_matrix[icondi, condi-1] = 1
  116. assert np.sum(design_matrix.sum(axis=1) == 1) == len(epoch)
  117. meg_data_V = epoch
  118. _, nchs, ntimes = meg_data_V.shape
  119. meg_data_V = scipy.stats.zscore(meg_data_V, axis=0)
  120. coefs = np.zeros((nconditions, nchs, ntimes))
  121. resids = np.zeros_like(meg_data_V)
  122. # for ich in tqdm(range(nchs)):
  123. for ich in range(nchs):
  124. for itime in range(ntimes):
  125. y = meg_data_V[:, ich, itime]
  126. model = sm.OLS(endog=y, exog=design_matrix, missing="raise")
  127. results = model.fit()
  128. coefs[:, ich, itime] = results.params # (4, 248, 163)
  129. resids[:, ich, itime] = results.resid # (ntrials, 248, 163)
  130. # Calculate pairwise mahalanobis distance between regression coefficients
  131. rdm_times = np.zeros((nconditions, nconditions, ntimes))
  132. for itime in range(ntimes):
  133. response = coefs[:, :, itime] # (4, 248)
  134. residuals = resids[:, :, itime] # (51, 248)
  135. # Estimate covariance from residuals
  136. lw_shrinkage = LedoitWolf(assume_centered=True)
  137. cov = lw_shrinkage.fit(residuals)
  138. # Compute pairwise mahalanobis distances
  139. VI = np.linalg.inv(cov.covariance_) # inverse of covariance matrix needed for mahalonobis
  140. rdm = squareform(pdist(response, metric="mahalanobis", VI=VI))
  141. # rdm = squareform(pdist(response, metric="cosine"))
  142. assert ~np.isnan(rdm).any()
  143. rdm_times[:, :, itime] = rdm # rdm_times (4, 4, 163), rdm (4, 4)
  144. return rdm_times
  145. def get_inseq(sequence):
  146. """Get the pairs in the sequence."""
  147. # create list of possible pairs
  148. pairs_in_sequence = list()
  149. pairs_in_sequence.append(str(sequence[0]) + str(sequence[1]))
  150. pairs_in_sequence.append(str(sequence[1]) + str(sequence[2]))
  151. pairs_in_sequence.append(str(sequence[2]) + str(sequence[3]))
  152. pairs_in_sequence.append(str(sequence[3]) + str(sequence[0]))
  153. return pairs_in_sequence
  154. def print_proportions(subject, all_beh):
  155. #### get stimuli proportions
  156. print(f"############### {subject}")
  157. for i, sess in zip(range(5), ['prac', 'b1', 'b2', 'b3', 'b4']):
  158. print(f"{sess} ----------------------")
  159. unique, values = np.unique(all_beh[i].positions, return_counts=True)
  160. for un, val in zip(unique, values):
  161. print(un, round((val/np.sum(values)*100), 2))
  162. def make_predictions(X, y, folds, jobs, scoring, verbose):
  163. # set-up the classifier and cv structure
  164. clf = make_pipeline(StandardScaler(), LogisticRegressionCV(multi_class="ovr", max_iter=100000, solver='saga', random_state=42)) # use JAX maybe
  165. # clf = make_pipeline(StandardScaler(), SGDRegressor(loss="squared_error", max_iter=100000, random_state=42)) # use JAX maybe
  166. clf = SlidingEstimator(clf, scoring=scoring, n_jobs=jobs, verbose=verbose) # get time of one sample (slide), try with less jobs maybe ?
  167. cv = StratifiedKFold(folds, shuffle=True)
  168. pred = np.zeros((len(y), X.shape[-1]))
  169. pred_rock = np.zeros((len(y), X.shape[-1], len(set(y))))
  170. # there is only randoms in practice sessions
  171. for train, test in cv.split(X, y):
  172. clf.fit(X[train], y[train])
  173. pred[test] = np.array(clf.predict(X[test]))
  174. pred_rock[test] = np.array(clf.predict_proba(X[test]))
  175. return test, pred, pred_rock
  176. def get_volume_estimate_time_course(stcs, fwd, subject, subjects_dir):
  177. """Extracts time courses for each label from volume source estimates.
  178. Args:
  179. stcs (list of mne.VolSourceEstimate): List of volume source estimates.
  180. fwd (dict): Forward solution.
  181. subject (str): Subject name.
  182. subjects_dir (str): Path to SUBJECTS_DIR.
  183. Returns:
  184. dict: A dictionary with label names as keys and arrays of shape
  185. (n_epochs, n_vertices_in_label, n_times) as values.
  186. """
  187. import numpy as np
  188. from mne import get_volume_labels_from_src
  189. from tqdm.auto import tqdm
  190. labels = get_volume_labels_from_src(fwd['src'], subject, subjects_dir)
  191. vertices_info = dict()
  192. for label in labels:
  193. vertices_info[label.name] = len(label.vertices)
  194. # Initialize a dictionary to hold time courses for each label
  195. label_time_courses = {}
  196. # Loop through each STC (source time course) for each epoch
  197. for stc in tqdm(stcs):
  198. # Extract data from the STC
  199. stc_data = stc.data # shape: (n_vertices, n_times)
  200. # Loop through each label to extract the time course
  201. for ilabel, label in enumerate(labels):
  202. if ilabel >= len(stc.vertices):
  203. # If ilabel exceeds the number of vertex arrays, break the loop
  204. break
  205. # Get the vertices in the label
  206. label_vertices = np.intersect1d(stc.vertices[ilabel+2], label.vertices)
  207. if label_vertices.size == 0:
  208. continue
  209. # Get indices of these vertices in the STC data
  210. indices = np.searchsorted(stc.vertices[ilabel+2], label_vertices)
  211. # Extract the time courses for these vertices
  212. vertices_time_courses = stc_data[indices, :]
  213. # Store the time courses in the dictionary
  214. if label.name not in label_time_courses:
  215. label_time_courses[label.name] = []
  216. label_time_courses[label.name].append(vertices_time_courses)
  217. # Convert to numpy arrays
  218. for label in label_time_courses:
  219. label_time_courses[label] = np.array(label_time_courses[label]) # shape: (n_trials, n_vertices_in_label, n_times)
  220. return label_time_courses, vertices_info
  221. def get_labels_from_vol_src(src, subject, subjects_dir):
  222. from mne import Label
  223. from mne import get_volume_labels_from_aseg
  224. """Return a list of Label of segmented volumes included in the src space.
  225. Parameters
  226. ----------
  227. src : instance of SourceSpaces
  228. The source space containing the volume regions.
  229. %(subject)s
  230. subjects_dir : str
  231. Freesurfer folder of the subjects.
  232. Returns
  233. -------
  234. labels_aseg : list of Label
  235. List of Label of segmented volumes included in src space.
  236. """
  237. # from ..label import Label
  238. # Read the aseg file
  239. aseg_fname = op.join(subjects_dir, subject, "mri", "aseg.mgz")
  240. all_labels_aseg = get_volume_labels_from_aseg(aseg_fname, return_colors=True)
  241. if any(np.any(s["type"] != "vol") for s in src):
  242. raise ValueError("source spaces have to be of vol type")
  243. labels_aseg = list()
  244. for nr in range(len(src)):
  245. vertices = src[nr]["vertno"]
  246. pos = src[nr]["rr"][src[nr]["vertno"], :]
  247. roi_str = src[nr]["seg_name"]
  248. try:
  249. ind = all_labels_aseg[0].index(roi_str)
  250. color = np.array(all_labels_aseg[1][ind]) / 255
  251. except ValueError:
  252. pass
  253. if "left" in roi_str.lower():
  254. hemi = "lh"
  255. roi_str = roi_str.replace("Left-", "") + "-lh"
  256. elif "right" in roi_str.lower():
  257. hemi = "rh"
  258. roi_str = roi_str.replace("Right-", "") + "-rh"
  259. else:
  260. hemi = "both"
  261. label = Label(
  262. vertices=vertices,
  263. pos=pos,
  264. hemi=hemi,
  265. name=roi_str,
  266. color=color,
  267. subject=subject,
  268. )
  269. labels_aseg.append(label)
  270. return labels_aseg
  271. def get_volume_estimate_tc(stcs, fwd, offsets, subject, subjects_dir):
  272. """Extracts time courses for each label from volume source estimates."""
  273. import numpy as np
  274. from mne import get_volume_labels_from_src
  275. labels = get_volume_labels_from_src(fwd['src'], subject, subjects_dir)
  276. vertices_info = dict()
  277. for label in labels:
  278. vertices_info[label.name] = len(label.vertices)
  279. # Initialize a dictionary to hold time courses for each label
  280. label_time_courses = {}
  281. for stc in stcs:
  282. stc_data = stc.data # shape: (n_vertices, n_times)
  283. for ilabel, label in enumerate(labels):
  284. tc = stc_data[offsets[ilabel]:offsets[ilabel+1]]
  285. if label.name not in label_time_courses:
  286. label_time_courses[label.name] = []
  287. label_time_courses[label.name].append(tc)
  288. # Convert to numpy arrays
  289. for label in label_time_courses:
  290. label_time_courses[label] = np.array(label_time_courses[label]) # shape: (n_trials, n_vertices_in_label, n_times)
  291. return label_time_courses, vertices_info
  292. def rsync_files(source, destination, options=""):
  293. """Rsync files from source to destination with given options."""
  294. import subprocess
  295. try:
  296. # Construct the rsync command
  297. command = f"rsync {options} --progress --ignore-existing {source} {destination}"
  298. # Execute the command
  299. result = subprocess.run(command, shell=True, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  300. # Decode and print the output and errors (if any)
  301. stdout = result.stdout.decode()
  302. stderr = result.stderr.decode()
  303. print(stdout)
  304. if stderr:
  305. print(f"Errors during rsync: {stderr}")
  306. print("Rsync operation completed successfully.")
  307. return stdout
  308. except subprocess.CalledProcessError as e:
  309. print(f"An error occurred: {e.stderr.decode()}")
  310. return None
  311. def get_in_out_seq(sequence, similarities, random_lows, analysis):
  312. """Get in-sequence and out-sequence similarity values based on the analysis type."""
  313. import numpy as np
  314. # create list of possible pairs
  315. pairs_in_sequence = list()
  316. pairs_in_sequence.append(str(sequence[0]) + str(sequence[1]))
  317. pairs_in_sequence.append(str(sequence[1]) + str(sequence[2]))
  318. pairs_in_sequence.append(str(sequence[2]) + str(sequence[3]))
  319. pairs_in_sequence.append(str(sequence[3]) + str(sequence[0]))
  320. in_seq, out_seq = [], []
  321. pairs = ['12', '13', '14', '23', '24', '34']
  322. rev_pairs = ['21', '31', '41', '32', '42', '43']
  323. if analysis == 'pat_high_rdm_high':
  324. for pair, rev_pair, pat_sim, rand_sim in zip(pairs, rev_pairs, similarities, random_lows):
  325. if ((pair in pairs_in_sequence) or (rev_pair in pairs_in_sequence)):
  326. in_seq.append(pat_sim)
  327. out_seq.append(rand_sim)
  328. elif analysis == 'pat_high_rdm_low':
  329. for pair, rev_pair, pat_sim, rand_sim in zip(pairs, rev_pairs, similarities, random_lows):
  330. if ((pair in pairs_in_sequence) or (rev_pair in pairs_in_sequence)):
  331. in_seq.append(pat_sim)
  332. else:
  333. out_seq.append(rand_sim)
  334. else:
  335. for pair, rev_pair, pat_sim, rand_sim in zip(pairs, rev_pairs, similarities, random_lows):
  336. if ((pair in pairs_in_sequence) or (rev_pair in pairs_in_sequence)):
  337. in_seq.append(pat_sim)
  338. else:
  339. out_seq.append(pat_sim)
  340. return np.array(in_seq), np.array(out_seq)
  341. def get_all_high_low(pattern_data, random_data, sequence, block=True, step=1):
  342. """
  343. Extracts high and low similarity sets from RDMs based on a stimulus sequence.
  344. Parameters:
  345. pattern_data: np.ndarray
  346. RDM for patterned stimuli:
  347. - block=True: (n_blocks, n_timepoints, n_items, n_items)
  348. - block=False: (n_epochs, n_timepoints, n_items, n_items)
  349. random_data: np.ndarray
  350. RDM for random stimuli (same shape as pattern_data).
  351. sequence: list[int]
  352. A list of 4 stimulus identifiers, e.g., [1, 2, 3, 4].
  353. block: bool (default=True)
  354. If True, treats data as block-based; else session-based (cv-style).
  355. Returns:
  356. high: np.ndarray
  357. Similarity values for pairs in the input sequence.
  358. low: np.ndarray
  359. Corresponding values from the random data (used as "low" set).
  360. """
  361. pair_indices = [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]
  362. pair_labels = ['12', '13', '14', '23', '24', '34']
  363. # Generate forward and reverse sequence-based pairs
  364. limit = 4 if step == 1 else 4 - step
  365. sequence_pairs = {f"{sequence[i]}{sequence[(i+step)%4]}" for i in range(limit)}
  366. sequence_pairs |= {p[::-1] for p in sequence_pairs}
  367. high, low = [], []
  368. for (i, j), label in zip(pair_indices, pair_labels):
  369. if block:
  370. pat_vals = pattern_data[:, :, i, j]
  371. rand_vals = random_data[:, :, i, j]
  372. else:
  373. pat_vals = np.array([pattern_data[epoch, :, i, j] for epoch in range(pattern_data.shape[0])])
  374. rand_vals = np.array([random_data[epoch, :, i, j] for epoch in range(random_data.shape[0])])
  375. if label in sequence_pairs:
  376. high.append(pat_vals)
  377. low.append(rand_vals)
  378. return np.array(high), np.array(low)
  379. def cv_mahalanobis_parallel(X, y, n_jobs=-1, n_splits=10, verbose=True, shuffle=True):
  380. """
  381. Parallelized Cross-validated Mahalanobis distances with tqdm_joblib and NaN safety.
  382. Parameters:
  383. X: ndarray (n_trials, n_channels, n_times)
  384. y: array-like (n_trials,) condition labels
  385. n_splits: int, number of cross-validation folds
  386. n_jobs: int, number of parallel jobs (default: -1 = all CPUs)
  387. verbose: bool, show progress bars
  388. Returns:
  389. distances: ndarray (n_times, n_conditions, n_conditions)
  390. """
  391. import numpy as np
  392. from sklearn.model_selection import StratifiedKFold, KFold
  393. from sklearn.covariance import LedoitWolf
  394. from scipy.linalg import solve
  395. from joblib import Parallel, delayed
  396. from tqdm.auto import tqdm
  397. from tqdm_joblib import tqdm_joblib
  398. n_trials, n_channels, n_times = X.shape
  399. conditions = np.unique(y)
  400. n_conditions = len(conditions)
  401. if shuffle:
  402. skf = StratifiedKFold(n_splits=n_splits, shuffle=True, random_state=42)
  403. else:
  404. skf = KFold(n_splits=n_splits, shuffle=False)
  405. def compute_timepoint(t):
  406. X_t = X[:, :, t]
  407. dist_folds = np.zeros((n_conditions, n_conditions, n_splits))
  408. fold_iterator = skf.split(X_t, y)
  409. if verbose:
  410. fold_iterator = tqdm(fold_iterator, total=n_splits, desc=f"Time {t:03}", leave=False, position=t % 8)
  411. for fold, (train_idx, test_idx) in enumerate(fold_iterator):
  412. X_train, X_test = X_t[train_idx], X_t[test_idx]
  413. y_train, y_test = y[train_idx], y[test_idx]
  414. try:
  415. lw = LedoitWolf()
  416. lw.fit(X_train)
  417. cov = lw.covariance_
  418. train_means = {c: X_train[y_train == c].mean(axis=0) for c in conditions}
  419. test_means = {c: X_test[y_test == c].mean(axis=0) for c in conditions}
  420. for i, ci in enumerate(conditions):
  421. for j, cj in enumerate(conditions):
  422. if j <= i:
  423. continue
  424. diff_train = train_means[ci] - train_means[cj]
  425. diff_test = test_means[ci] - test_means[cj]
  426. dist = diff_train.T @ solve(cov, diff_test, assume_a='pos')
  427. dist_folds[i, j, fold] = dist
  428. dist_folds[j, i, fold] = dist
  429. except (ValueError, np.linalg.LinAlgError, KeyError, ZeroDivisionError):
  430. # Catch common issues: singular covariances, missing classes, empty slices
  431. dist_folds[:, :, fold] = np.nan
  432. return np.nanmean(dist_folds, axis=2) # <- safely ignore folds with NaNs
  433. time_iterator = range(n_times)
  434. if verbose:
  435. with tqdm_joblib(tqdm(desc="Overall Timepoints", total=n_times)) as progress_bar:
  436. distances = Parallel(n_jobs=n_jobs)(
  437. delayed(compute_timepoint)(t) for t in time_iterator
  438. )
  439. else:
  440. distances = Parallel(n_jobs=n_jobs)(
  441. delayed(compute_timepoint)(t) for t in time_iterator
  442. )
  443. return np.stack(distances, axis=0)
  444. def train_test_mahalanobis_fast(X_train, X_test, y_train, y_test, n_jobs=-1, verbose=True):
  445. """
  446. Computes Mahalanobis distances between class means in training and testing sets.
  447. Handles missing conditions by filling with NaNs.
  448. Returns:
  449. distances: ndarray (n_times, n_conditions, n_conditions)
  450. """
  451. import numpy as np
  452. from sklearn.covariance import LedoitWolf
  453. from scipy.linalg import solve
  454. from joblib import Parallel, delayed
  455. from tqdm.auto import tqdm
  456. from tqdm_joblib import tqdm_joblib
  457. n_trials_train, n_channels, n_times = X_train.shape
  458. n_trials_test, _, _ = X_test.shape
  459. conditions = [1, 2, 3, 4]
  460. n_conditions = len(conditions)
  461. cond_idx = {c: i for i, c in enumerate(conditions)} # for indexing
  462. def compute_timepoint(t):
  463. try:
  464. Xtr = X_train[:, :, t]
  465. Xte = X_test[:, :, t]
  466. # Skip timepoints with too few trials
  467. if Xtr.shape[0] < 2 or Xte.shape[0] < 2:
  468. return np.full((n_conditions, n_conditions), np.nan)
  469. lw = LedoitWolf().fit(Xtr)
  470. cov = lw.covariance_
  471. # Only keep conditions present in both train and test
  472. present_train = set(y_train)
  473. present_test = set(y_test)
  474. valid_conditions = list(present_train & present_test)
  475. train_means = {}
  476. test_means = {}
  477. for c in valid_conditions:
  478. train_means[c] = Xtr[y_train == c].mean(0)
  479. test_means[c] = Xte[y_test == c].mean(0)
  480. dist = np.full((n_conditions, n_conditions), np.nan)
  481. for i, ci in enumerate(conditions):
  482. for j, cj in enumerate(conditions):
  483. if j <= i:
  484. continue
  485. if ci in train_means and cj in train_means and ci in test_means and cj in test_means:
  486. diff_train = train_means[ci] - train_means[cj]
  487. diff_test = test_means[ci] - test_means[cj]
  488. try:
  489. mahal = diff_train.T @ solve(cov, diff_test, assume_a='pos')
  490. except np.linalg.LinAlgError:
  491. mahal = np.nan
  492. dist[cond_idx[ci], cond_idx[cj]] = mahal
  493. dist[cond_idx[cj], cond_idx[ci]] = mahal
  494. return dist
  495. except Exception as e:
  496. print(f"Error at time {t}: {e}")
  497. return np.full((n_conditions, n_conditions), np.nan)
  498. time_iterator = range(n_times)
  499. if verbose:
  500. with tqdm_joblib(tqdm(desc="Computing Mahalanobis", total=n_times)):
  501. distances = Parallel(n_jobs=n_jobs)(
  502. delayed(compute_timepoint)(t) for t in time_iterator
  503. )
  504. else:
  505. distances = Parallel(n_jobs=n_jobs)(
  506. delayed(compute_timepoint)(t) for t in time_iterator
  507. )
  508. return np.stack(distances, axis=0) # shape: (n_times, n_conditions, n_conditions)
  509. def loocv_mahalanobis_parallel(X, y, n_jobs=-1, verbose=True):
  510. """
  511. Parallel cross-validated Mahalanobis distances using LOOCV,
  512. with per-fold progress bars for each timepoint.
  513. Parameters:
  514. X: ndarray (n_trials, n_channels, n_times)
  515. y: array-like (n_trials,) condition labels
  516. n_jobs: int, number of parallel jobs (default: -1 = all cores)
  517. verbose: bool, whether to show progress bars
  518. Returns:
  519. distances: ndarray (n_times, n_conditions, n_conditions)
  520. """
  521. import numpy as np
  522. from sklearn.model_selection import LeaveOneOut
  523. from sklearn.covariance import LedoitWolf
  524. from scipy.linalg import solve
  525. from joblib import Parallel, delayed
  526. from tqdm.auto import tqdm
  527. n_trials, n_channels, n_times = X.shape
  528. conditions = np.unique(y)
  529. n_conditions = len(conditions)
  530. loo = LeaveOneOut()
  531. def compute_timepoint(t):
  532. X_t = X[:, :, t]
  533. dist_folds = np.full((n_conditions, n_conditions, n_trials), np.nan)
  534. iterator = loo.split(X_t)
  535. if verbose:
  536. iterator = tqdm(iterator, total=n_trials, desc=f"Time {t:03}", leave=False, position=t % 8)
  537. for fold, (train_idx, test_idx) in enumerate(iterator):
  538. X_train, X_test = X_t[train_idx], X_t[test_idx]
  539. y_train, y_test = y[train_idx], y[test_idx]
  540. if len(np.unique(y_train)) < len(conditions):
  541. continue
  542. lw = LedoitWolf()
  543. lw.fit(X_train)
  544. cov = lw.covariance_
  545. train_means = {c: X_train[y_train == c].mean(axis=0) for c in conditions}
  546. test_means = {c: X_test[y_test == c].mean(axis=0) for c in conditions if np.any(y_test == c)}
  547. for i, ci in enumerate(conditions):
  548. for j, cj in enumerate(conditions):
  549. if j <= i or ci not in test_means or cj not in test_means:
  550. continue
  551. diff_train = train_means[ci] - train_means[cj]
  552. diff_test = test_means[ci] - test_means[cj]
  553. if np.isnan(diff_test).any() or np.isnan(diff_train).any():
  554. continue
  555. dist = diff_train.T @ solve(cov, diff_test, assume_a='pos')
  556. dist_folds[i, j, fold] = dist
  557. dist_folds[j, i, fold] = dist
  558. return np.nanmean(dist_folds, axis=2)
  559. if verbose:
  560. time_iterator = tqdm(range(n_times), desc="Timepoints")
  561. else:
  562. time_iterator = range(n_times)
  563. distances = Parallel(n_jobs=n_jobs)(
  564. delayed(compute_timepoint)(t) for t in time_iterator
  565. )
  566. return np.stack(distances, axis=0)
  567. def interpolate_rdm_nan(rdm):
  568. """Interpolate nan values in a RDM matrix by computing the mean of previous and subsequent block.
  569. Args:
  570. rdm: ndarray of shape (n_blocks, n_times, n_conditions, n_conditions)
  571. LOOCV Mahalanobis distances between conditions at each time point.
  572. Returns:
  573. rdm: ndarray of shape (n_blocks, n_times, n_conditions, n_conditions)
  574. LOOCV Mahalanobis distances between conditions at each time point.
  575. present_nan: bool
  576. True if nan values were present in the RDM, False otherwise
  577. """
  578. present_nan = False
  579. n_blocks, n_times, n_conditions, _ = rdm.shape
  580. for i in range(n_blocks):
  581. for j in range(n_times):
  582. if np.isnan(np.sum(rdm[i, j])):
  583. present_nan = True
  584. if i == 0:
  585. rdm[i, j] = rdm[i + 1, j]
  586. elif i == n_blocks - 1:
  587. rdm[i, j] = rdm[i - 1, j]
  588. else:
  589. rdm[i, j] = (rdm[i - 1, j] + rdm[i + 1, j]) / 2
  590. return rdm, present_nan
  591. def contiguous_regions(condition):
  592. """Find contiguous True regions in a boolean array."""
  593. import numpy as np
  594. d = np.diff(condition.astype(int))
  595. starts = np.where(d == 1)[0] + 1
  596. ends = np.where(d == -1)[0] + 1
  597. if condition[0]:
  598. starts = np.r_[0, starts]
  599. if condition[-1]:
  600. ends = np.r_[ends, condition.size]
  601. return zip(starts, ends)
  602. def svd(vector_data):
  603. """Reduce 3 orientations to dominant orientation using Singular Value Decomposition."""
  604. # Initialize an array for storing the dominant orientation time series
  605. dominant_data = np.zeros((vector_data.shape[0], vector_data.shape[1], vector_data.shape[-1])) # (294, 8196, 82)
  606. for trial in range(vector_data.shape[0]): # Loop over trials
  607. for source in range(vector_data.shape[1]): # Loop over sources
  608. u, s, vh = np.linalg.svd(vector_data[trial, source, :, :], full_matrices=False) # SVD over orientation axis (3)
  609. dominant_time_series = vh[0, :] * s[0] # First right singular vector weighted by singular value
  610. dominant_data[trial, source, :] = dominant_time_series # Store in new array
  611. return dominant_data
  612. def svd_fast(vector_data):
  613. """Vectorized equivalent of `svd` using NumPy batched SVD."""
  614. _, s, vh = np.linalg.svd(vector_data, full_matrices=False)
  615. return vh[..., 0, :] * s[..., [0]]
  616. def get_train_test_blocks_net(data, fwd, behav, pick_ori, trial_type, block, blocks, rsa=False, verbose=False):
  617. """Helper function to get source data for training and testing."""
  618. from mne import compute_covariance, compute_rank
  619. from mne.beamformer import make_lcmv, apply_lcmv_epochs
  620. import numpy as np
  621. weight_norm = "unit-noise-gain-invariant" if pick_ori == 'vector' else "unit-noise-gain"
  622. this_block = behav.blocks == block
  623. if not rsa:
  624. if block in blocks[:3]:
  625. rand_blocks = np.random.choice(blocks[3:], size=19, replace=False)
  626. out_blocks = behav.blocks.isin(rand_blocks)
  627. else:
  628. out_blocks = (behav.blocks != block) & (behav.sessions != 0)
  629. else:
  630. out_blocks = behav.blocks != block
  631. tt = behav.trialtypes == 2 if trial_type == 'random' else behav.trialtypes == 1
  632. # compute training data
  633. tt_out_blocks = tt & out_blocks
  634. train_blocks = behav[tt_out_blocks]
  635. train_epochs = data[train_blocks.trials.values]
  636. assert len(train_blocks) == len(train_epochs), "Length mismatch in training epochs"
  637. ytrain = train_blocks.positions
  638. # compute testing data
  639. tt_this_block = tt & this_block
  640. test_blocks = behav[tt_this_block]
  641. test_epochs = data[test_blocks.trials.values]
  642. ytest = test_blocks.positions
  643. # compute noise covariance
  644. random = behav.trialtypes == 2
  645. noise_epochs = data[random & out_blocks]
  646. noise_cov = compute_covariance(noise_epochs, tmin=-0.2, tmax=0, method="empirical", rank="info", verbose=verbose)
  647. data_cov = compute_covariance(train_epochs, method="empirical", rank="info", verbose=verbose)
  648. rank = compute_rank(data_cov, info=train_epochs.info, rank=None, tol_kind='relative', verbose=verbose)
  649. filters = make_lcmv(train_epochs.info, fwd, data_cov, reg=0.05, noise_cov=noise_cov,
  650. pick_ori=pick_ori, weight_norm=weight_norm,
  651. rank=rank, reduce_rank=True, verbose=verbose)
  652. # apply LCMV beamformer to epochs
  653. stcs_train = apply_lcmv_epochs(train_epochs, filters=filters, verbose=verbose)
  654. stcs_test = apply_lcmv_epochs(test_epochs, filters=filters, verbose=verbose)
  655. return stcs_train, stcs_test, ytrain, ytest
  656. def get_train_test_blocks_htc(data, fwd, behav, pick_ori, trial_type, block, blocks, rsa=False, verbose=False):
  657. """Helper function to get source data for training and testing."""
  658. from mne import compute_covariance, compute_rank
  659. from mne.beamformer import make_lcmv, apply_lcmv_epochs
  660. weight_norm = "unit-noise-gain-invariant" if pick_ori == 'vector' else "unit-noise-gain"
  661. this_block = behav.blocks == block
  662. if not rsa:
  663. if block in blocks[:3]:
  664. rand_blocks = np.random.choice(blocks[3:], size=19, replace=False)
  665. out_blocks = behav.blocks.isin(rand_blocks)
  666. else:
  667. out_blocks = (behav.blocks != block) & (behav.sessions != 0)
  668. else:
  669. out_blocks = behav.blocks != block
  670. tt = behav.trialtypes == 2 if trial_type == 'random' else behav.trialtypes == 1
  671. tt_out_blocks = tt & out_blocks
  672. tt_this_block = tt & this_block
  673. # compute training data
  674. train_blocks = behav[tt_out_blocks]
  675. train_epochs = data[train_blocks.trials.values]
  676. random = behav.trialtypes == 2
  677. noise_epochs = data[random & out_blocks]
  678. noise_cov = compute_covariance(noise_epochs, tmin=-0.2, tmax=0, method="empirical", rank="info", verbose=verbose)
  679. data_cov = compute_covariance(train_epochs, method="empirical", rank="info", verbose=verbose)
  680. rank = compute_rank(data_cov, info=train_epochs.info, rank=None, tol_kind='relative', verbose=verbose)
  681. filters = make_lcmv(train_epochs.info, fwd, data_cov, reg=0.05, noise_cov=noise_cov,
  682. pick_ori=pick_ori, weight_norm=weight_norm,
  683. rank=rank, reduce_rank=True, verbose=verbose)
  684. stcs_train = apply_lcmv_epochs(train_epochs, filters=filters, verbose=verbose)
  685. ytrain = train_blocks.positions
  686. assert len(stcs_train) == len(ytrain), "Length mismatch in training data"
  687. # compute testing data
  688. test_blocks = behav[tt_this_block]
  689. test_epochs = data[test_blocks.trials.values]
  690. stcs_test = apply_lcmv_epochs(test_epochs, filters=filters, verbose=verbose)
  691. ytest = test_blocks.positions
  692. assert len(stcs_test) == len(ytest), "Length mismatch in testing data"
  693. return stcs_train, ytrain, stcs_test, ytest
  694. def fisher_z_and_ttest(rho_matrix):
  695. import numpy as np
  696. from scipy.stats import ttest_1samp
  697. """
  698. Applies Fisher z-transform and performs a one-sample t-test against 0 at each timepoint.
  699. Parameters:
  700. rho_matrix (np.ndarray): Array of shape (n_subjects, n_timepoints) with rho values.
  701. Returns:
  702. z_matrix (np.ndarray): Fisher z-transformed correlations, same shape as input.
  703. t_stats (np.ndarray): T-statistics at each timepoint, shape (n_timepoints,).
  704. p_vals (np.ndarray): P-values at each timepoint, shape (n_timepoints,).
  705. """
  706. # Clip rho values to avoid infinite z
  707. rho_matrix = np.clip(rho_matrix, -0.999999, 0.999999)
  708. # Fisher z-transform
  709. z_matrix = 0.5 * np.log((1 + rho_matrix) / (1 - rho_matrix))
  710. # One-sample t-test at each timepoint
  711. t_stats, p_vals = ttest_1samp(z_matrix, popmean=0, axis=0)
  712. return z_matrix, t_stats, p_vals
  713. def fisher_z_transform_3d(rho_3d):
  714. """
  715. Apply Fisher's z-transformation to a 3D array of correlation coefficients.
  716. Parameters:
  717. rho_3d (np.ndarray): Array of shape (n_subjects, n_timepoints, n_timepoints)
  718. Returns:
  719. z_3d (np.ndarray): Fisher z-transformed array, same shape as input.
  720. """
  721. import numpy as np
  722. rho_3d = np.clip(rho_3d, -0.999999, 0.999999)
  723. z_3d = 0.5 * np.log((1 + rho_3d) / (1 - rho_3d))
  724. return z_3d
  725. def reorder(random_events, events, raw_rd, dur=200, nsamples=33):
  726. import mne
  727. """
  728. Reorder random events to match the given sequence.
  729. Parameters:
  730. random_events -- array of random events
  731. events -- target sequence of events
  732. raw_rd -- raw data corresponding to random events
  733. dur -- duration (in samples) of pre-task and post-task data
  734. nsamples -- window duration in samples
  735. Returns:
  736. Reordered raw data, original random event indices, and reordered events.
  737. """
  738. events = list(events)
  739. orig_nums = []
  740. events_reord = []
  741. raw_Xrd = raw_rd.get_data()
  742. raw_reord = []
  743. new_sample = 0
  744. first_samp = raw_rd.first_samp
  745. # Start reordered random with the first seconds of random raw
  746. raw_reord.append(raw_Xrd[:, :dur])
  747. new_sample += dur
  748. random_events_numbers = np.arange(len(random_events))
  749. for event in events:
  750. if event[2] in random_events[:, 2]:
  751. index = random_events[:, 2].tolist().index(event[2])
  752. orig_nums.append(random_events_numbers[index])
  753. samp = random_events[index, 0] - first_samp
  754. raw_reord.append(raw_Xrd[:, samp:samp + nsamples])
  755. random_events = np.delete(random_events, index, axis=0)
  756. random_events_numbers = np.delete(random_events_numbers, index, axis=0)
  757. events_reord.append([new_sample, 0, event[2]])
  758. new_sample += nsamples
  759. else:
  760. break
  761. # End reordered random with the last seconds of random raw
  762. raw_reord.append(raw_Xrd[:, -dur:])
  763. orig_nums_reord = np.array(orig_nums)
  764. events_reord = np.array(events_reord)
  765. raw_reord = np.concatenate(raw_reord, axis=1)
  766. raw_reord = mne.io.RawArray(raw_reord, raw_rd.info)
  767. return raw_reord, orig_nums_reord, events_reord

base.py at commit fbfceb6, no license · at the source

Overview

Authors: Coumarane Tirou1, Oussama Abdoun1, Teodóra Vékony2, Laure Tosatto3,4, Andrea Brovelli5, Marine Vernet6, Dezső Németh1,2,7, Romain Quentin1
  1. Université Claude Bernard Lyon 1, INSERM U1028, CNRS UMR5292, Lyon Neuroscience Research Center (CRNL), EDUWELL team, Lyon, France
  2. Gran Canaria Cognitive Research Center, Atlántico Medio University, Las Palmas de Gran Canaria, Spain
  3. School of Biological Sciences, Monash University, Melbourne, VIC 3800 Australia
  4. Normandie University, UNICAEN, CNRS, EthoS, UMR 6552, Caen, 14000 France
  5. Institut de Neurosciences de la Timone UMR 7289, Aix Marseille Université, CNRS, Marseille, 13005 France
  6. Université Claude Bernard Lyon 1, INSERM U1028, CNRS UMR5292, Lyon Neuroscience Research Center (CRNL), IMPACT team, Lyon, France
  7. BML-NAP Research Group, ELTE Eötvös Loránd University & HUN-REN Research Centre for Natural Sciences, Budapest, Hungary
Journal: Nature communications, volume 17, issue 1, article 8372
Dates: received 27 October 2025; accepted 12 June 2026; published online 7 July 2026
Type: Research article · Language: English
License: CC BY-NC-ND
Identifiers: DOI 10.1038/s41467-026-74824-0 · PMID 42409831 · PMCID PMC13473593 · OpenAlex W7167509062
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: MEG (modality), human (organism), cognitive (subfield)
Methods: Spectral & time-frequency, Statistics, Smoothing, state filtering, decompositions, Machine learning, Connectivity, fMRI & imaging, Physiology & signal measures
Keywords: Cognitive neuroscience, Learning and memory
MeSH: Brain*, Learning*, Noise*, Attention, Female, Humans, Magnetoencephalography, Male, Nerve Net, Photic Stimulation, Psychomotor Performance (* major topic)
Topic: Neural dynamics and brain function (Cognitive Neuroscience, Neuroscience), according to OpenAlex
Funding: 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)
Citations: not cited yet (Europe PMC); 74 references in the paper

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

License: none: the authors keep all their rights
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: fbfceb6a5b64fdab69a14aa2fefeb11ff1a251e3, 8 June 2026
Languages: Python (55), Shell (19), Quarto (2)
Size: 105 files, 76 scripts
Software Heritage: not archived
Found in: “Code availability”
Holds: README, environment (environment.yml), 2 notebooks
Not found: license file, CITATION.cff, tests, continuous integration, documentation
Tools: NumPy (50 files), pandas (49 files), MNE-Python (35 files), Matplotlib (24 files), SciPy (18 files), scikit-learn (13 files), autoreject (3 files), easystats (2 files), emmeans (2 files), mgcv (2 files), Plotly (2 files), tidyverse (2 files), FreeSurfer (1 file), imageio (1 file), MNE-BIDS (1 file), NiBabel (1 file), Pingouin (1 file), reticulate (1 file), statsmodels (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
77 files

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:

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

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:

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://doi.org/10.1038/s41467-026-74824-0

BibTeX

@article{tirou2026learning,
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/s41467-026-74824-0},
url = {https://doi.org/10.1038/s41467-026-74824-0},
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/07/07
VL - 17
IS - 1
SP - 8372
SN - 2041-1723
PB - Nature Publishing Group
DO - 10.1038/s41467-026-74824-0
UR - https://doi.org/10.1038/s41467-026-74824-0
LA - en
ER -

CSL-JSON

{
"id": "10.1038/s41467-026-74824-0",
"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": "Nat Commun",
"volume": "17",
"issue": "1",
"page": "8372",
"DOI": "10.1038/s41467-026-74824-0",
"PMID": "42409831",
"PMCID": "PMC13473593",
"ISSN": "2041-1723",
"publisher": "Nature Publishing Group",
"URL": "https://doi.org/10.1038/s41467-026-74824-0",
"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 communications
In 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 consciousness
In 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 data
In 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 data
In 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: eLife
In 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 journal
In 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 communications
In 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.

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.