OSCR

Distinct regimes of spatial prediction across the visual field during natural vision

Code ↔ Paper

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

The 6 matches · 1 of them tie a paragraph to a whole file without given lines: a weak match, whose lines are not tinted
  1. [1] § Materials and methods › Regression ↔ classes/analysis.py, lines 501 to 555 · score 0.69 · ridge regression, cross validated, Baseline Model, fold, correlation, fitted · Report a mismatch
  2. [2] § Materials and methods › Regression ↔ funcs/analyses.py, lines 1348 to 1438 · score 0.63 · cross validated Pearson, r_baseline, fitted, unpredictability, voxel, model · Report a mismatch
  3. [3] § Materials and methods › Population receptive field mapping, voxel selection ↔ classes/cortex.py, lines 314 to 379 · score 0.54 · Population receptive fields, central patch, selection, pRFs, NSD, voxels · Report a mismatch
  4. [4] § Materials and methods › Population receptive field mapping, voxel selection ↔ funcs/natspatpred backup.py, lines 1764 to 1803 · score 0.54 · Population receptive fields, central patch, selection, pRFs, NSD, voxels · Report a mismatch
  5. [5] § Results › A Hierarchy of Spatial Predictions Across the Cortex ↔ funcs/utility.py, lines 368 to 452 · score 0.50 · layer assignment, CNN layers, V3, V2, sensitive, hierarchy · Report a mismatch
  6. [6] § Materials and methods › Bottom-up feature encoding reference ↔ scripts/encoding_stack_vgg.sh, the whole file · a weak match · score 0.50 · encoded features, dense layers, VGG, model · Report a mismatch

Paper

Loaded from Europe PMC by your browser; OSCR does not store it: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Python · 1,177 lines · 45 KB · no license · 1 match

  1. import fnmatch
  2. import os
  3. import pickle
  4. from typing import Dict, List, Optional, Sequence, Tuple, Union
  5. import joblib
  6. import matplotlib.pyplot as plt
  7. import nibabel as nib
  8. import numpy as np
  9. import pandas as pd
  10. import seaborn as sns
  11. import sklearn as sk
  12. from matplotlib.colors import LinearSegmentedColormap, ListedColormap
  13. from matplotlib.ticker import FixedLocator, FuncFormatter, MaxNLocator
  14. from nilearn import plotting
  15. from scipy import stats
  16. from scipy.stats import zscore as zs
  17. from sklearn.base import clone
  18. from sklearn.linear_model import Ridge
  19. from sklearn.model_selection import KFold
  20. from classes.voxelsieve import VoxelSieve
  21. class Analysis:
  22. def __init__(self, NSPobj):
  23. """Initialize Analysis with NSPobj."""
  24. self.nsp = NSPobj
  25. def load_y(
  26. self,
  27. subject: str,
  28. roi: str,
  29. voxelsieve=VoxelSieve,
  30. n_trials: Union[int, str] = 30000,
  31. include_xyz: bool = False,
  32. ) -> np.ndarray:
  33. """
  34. Loads the y values for a given subject and region of interest (ROI).
  35. Args:
  36. subject (str): The subject.
  37. roi (str): The region of interest.
  38. voxelsieve (VoxelSieve): VoxelSieve instance used to select voxels.
  39. n_trials (int or str, optional): Number of trials to load. If 'all', loads all trials (up to 30000). Default is 30000.
  40. include_xyz (bool, optional): Whether to include the x, y, z coordinates in the output. Default is False.
  41. Returns:
  42. np.ndarray: The loaded y-matrix consisting of the HRF signal betas from the NSD.
  43. Raises:
  44. ValueError: If n_trials is greater than 30000.
  45. """
  46. if isinstance(n_trials, int) and n_trials > 30000:
  47. raise ValueError("n_trials cannot be greater than 30000.")
  48. start_column = 0 if include_xyz else 3
  49. n_trials = 30000 if n_trials == "all" else n_trials
  50. return (
  51. np.load(f"{self.nsp.own_datapath}/{subject}/betas/{roi}/all_betas.npy")[
  52. voxelsieve.vox_pick, start_column:
  53. ]
  54. )[:, :n_trials]
  55. def run_ridge_regression(
  56. self, X: np.ndarray, y: np.ndarray, alpha: float = 1.0, fit_icept: bool = False
  57. ) -> Ridge:
  58. """
  59. Run a ridge regression model on the data.
  60. Args:
  61. X (np.ndarray): Independent variables with shape (n_trials, n_features)
  62. y (np.ndarray): Dependent variable with shape (n_trials, n_outputs)
  63. alpha (float, optional): Regularisation parameter. Defaults to 1.0.
  64. fit_icept (bool, optional): Whether to fit intercept. Defaults to False.
  65. Returns:
  66. Ridge: The fitted Ridge model object.
  67. """
  68. model = Ridge(alpha=alpha, fit_intercept=fit_icept)
  69. model.fit(X, y)
  70. return model
  71. def _get_coefs(self, model: Ridge) -> np.ndarray:
  72. """Return model coefficients."""
  73. return model.coef_
  74. def _get_r(self, y_true: np.ndarray, y_pred: np.ndarray) -> np.ndarray:
  75. """
  76. Correlation coefficient between the columns of a matrix as goodness of fit metric.
  77. Args:
  78. y_true (np.ndarray): True target/response vector.
  79. y_pred (np.ndarray): Predicted target/response vector.
  80. Returns:
  81. np.ndarray: Correlation coefficient for every response.
  82. """
  83. zs = lambda v: (v - v.mean(0)) / v.std(0)
  84. return (zs(y_pred) * zs(y_true)).mean(0)
  85. # Duplicate of _get_r, so remove get_r_numpy
  86. def score_model(
  87. self, X: np.ndarray, y: np.ndarray, model: Ridge, cv: int = 5
  88. ) -> Tuple[np.ndarray, np.ndarray]:
  89. """
  90. Evaluate model performance using cross-validation.
  91. Args:
  92. X (np.ndarray): Independent variables.
  93. y (np.ndarray): Dependent variables.
  94. model (Ridge): Ridge model to score.
  95. cv (int, optional): Number of cross-validation folds. Defaults to 5.
  96. Returns:
  97. Tuple[np.ndarray, np.ndarray]: Predicted values and correlation scores for each fold.
  98. """
  99. kf = KFold(n_splits=cv)
  100. y_hat = []
  101. cor_scores = []
  102. for train_index, test_index in kf.split(X):
  103. X_train, X_test = X[train_index], X[test_index]
  104. y_train, y_test = y[train_index], y[test_index]
  105. model_clone = clone(model)
  106. model_clone.fit(X_train, y_train)
  107. y_hat_fold = model_clone.predict(X_test)
  108. r_fold = self._get_r(y_test, y_hat_fold)
  109. y_hat.append(y_hat_fold)
  110. cor_scores.append(r_fold)
  111. y_hat = np.concatenate(y_hat)
  112. return y_hat, cor_scores
  113. def plot_brain(
  114. self,
  115. prf_dict: dict,
  116. roi_masks: dict,
  117. subject: str,
  118. brain_numpy: np.ndarray,
  119. cmap,
  120. glass_brain: bool = False,
  121. save_img: bool = False,
  122. img_path: str = "brain_image.png",
  123. lay_assign_plot: bool = False,
  124. vmin: float | None = None,
  125. vmax: float | None = None,
  126. ):
  127. """Function to plot a 3D np.ndarray with voxel-specific values on an anatomical brain template of that subject.
  128. Args:
  129. - prf_dict (dict): The pRF dictionary
  130. - roi_masks (dict): The dictionary with the 3D np.ndarray boolean brain masks
  131. - subject (str): The subject ID
  132. - brain_numpy (np.ndarray): The 3D np.ndarray with voxel-specific values
  133. - glass_brain (bool, optional): Optional argument to plot a glass brain instead of a static map. Defaults to False.
  134. - save_img (bool, optional): Optional argument to save the image to a file. Defaults to False.
  135. - img_path (str, optional): The path where the image will be saved. Defaults to 'brain_image.png'.
  136. """
  137. brain_nii = nib.Nifti1Image(
  138. brain_numpy, self.nsp.cortex.anat_templates(prf_dict)[subject].affine
  139. )
  140. if glass_brain:
  141. display = plotting.plot_glass_brain(
  142. brain_nii,
  143. display_mode="ortho",
  144. colorbar=True,
  145. cmap=cmap,
  146. symmetric_cbar=False,
  147. vmin=vmin,
  148. vmax=vmax,
  149. )
  150. else:
  151. display = plotting.plot_stat_map(
  152. brain_nii,
  153. bg_img=self.nsp.cortex.anat_templates(prf_dict)[subject],
  154. display_mode="ortho",
  155. colorbar=True,
  156. cmap=cmap,
  157. symmetric_cbar=False,
  158. vmin=vmin,
  159. vmax=vmax,
  160. )
  161. if lay_assign_plot:
  162. # New code to format colorbar ticks
  163. def format_tick(x, pos):
  164. return f"{x:.0f}"
  165. formatter = FuncFormatter(format_tick)
  166. if display._cbar:
  167. display._cbar.update_ticks()
  168. display._cbar.ax.yaxis.set_major_formatter(formatter)
  169. display._cbar.ax.yaxis.set_major_locator(
  170. FixedLocator(np.arange(0, 6))
  171. ) # set ticks manually
  172. plt.show()
  173. if save_img:
  174. display.savefig(img_path) # save figure to file
  175. def stat_on_brain(
  176. self,
  177. prf_dict: dict,
  178. roi_masks: dict,
  179. subject: str,
  180. stat: np.ndarray,
  181. xyzs: np.ndarray,
  182. glass_brain: bool,
  183. cmap,
  184. save_img: bool = False,
  185. img_path: Optional[str] = "/home/rfpred/data/custom_files",
  186. ):
  187. """Function to create a brain plot based on a specific statistic and the corresponding voxel coordinates.
  188. Args:
  189. - prf_dict (dict): The pRF dictionary
  190. - roi_masks (dict): The dictionary with the 3D np.ndarray boolean brain masks
  191. - subject (str): The subject ID
  192. - stat (np.ndarray): The statistic to plot on the brain
  193. - xyzs (np.ndarray): The voxel coordinates
  194. - glass_brain (bool, optional): Optional argument to plot a glass brain instead of a static map. Defaults to False.
  195. """
  196. n_voxels = len(xyzs)
  197. statmap = np.zeros((n_voxels, 4))
  198. for vox in range(n_voxels):
  199. # statmap[vox, :3] = (xyzs[vox][0][0], xyzs[vox][0][1], xyzs[vox][0][2]) # this is for the old xyzs
  200. statmap[vox, :3] = xyzs[vox]
  201. statmap[vox, 3] = stat[vox]
  202. brainp = self.nsp.utils.coords2numpy(
  203. statmap, roi_masks[subject]["V1_mask"].shape, keep_vals=True
  204. )
  205. self.plot_brain(
  206. prf_dict, roi_masks, subject, brainp, cmap, glass_brain, save_img, img_path
  207. )
  208. def plot_learning_curve(self, X, y, model=None, alpha=1.0, cv=5):
  209. if model is None:
  210. # Create and fit the model
  211. model = self.run_ridge_regression(X, y, alpha)
  212. # Initialize the KFold object
  213. kf = KFold(n_splits=cv)
  214. # Initialize a list to store the scores for each fold
  215. scores = []
  216. # For each fold...
  217. for train_index, test_index in kf.split(X):
  218. # Split the data into training and testing sets
  219. X_train, X_test = X[train_index], X[test_index]
  220. y_train, y_test = y[train_index], y[test_index]
  221. # Fit the model on the training data
  222. model.fit(X_train, y_train)
  223. # Predict the values for the testing data
  224. y_hat = model.predict(X_test)
  225. # Calculate the correlation for each column
  226. scores_fold = [
  227. self._get_r(y_test[:, i], y_hat[:, i]) for i in range(y_test.shape[1])
  228. ]
  229. # Append the average correlation for this fold to the list
  230. scores.append(scores_fold)
  231. # Plot the scores
  232. for i, scores_fold in enumerate(scores, start=1):
  233. # Scatter plot of individual scores
  234. plt.scatter([i] * len(scores_fold), scores_fold, color="blue", alpha=0.5)
  235. # Line plot of mean score
  236. plt.plot(i, np.mean(scores_fold), color="red", marker="o")
  237. plt.xlabel("Fold")
  238. plt.ylabel("Correlation Score")
  239. plt.title("Learning Curve")
  240. # Set x-axis to only show integer values
  241. plt.gca().xaxis.set_major_locator(MaxNLocator(integer=True))
  242. plt.show()
  243. def plot_residuals(self, X, y, model=None, alpha=1.0):
  244. """Plot the residuals of the model, which is the difference between the actual y and the predicted y (y_hat)
  245. Args:
  246. - X (_type_): _description_
  247. - y (_type_): _description_
  248. - model (_type_, optional): _description_. Defaults to None.
  249. - alpha (float, optional): _description_. Defaults to 1.0.
  250. """
  251. if model is None:
  252. # Create and fit the model
  253. model = self.run_ridge_regression(X, y, alpha)
  254. # Get the predicted values
  255. y_hat = model.predict(X)
  256. # Calculate the residuals
  257. residuals = y - y_hat
  258. # Create a scatter plot of the predicted values and residuals
  259. plt.scatter(y_hat, residuals, alpha=0.3)
  260. plt.xlabel("Predicted Values")
  261. plt.ylabel("Residuals")
  262. plt.title("Residual Plot")
  263. plt.show()
  264. def analysis_chain_slim(
  265. self,
  266. subject: str,
  267. ydict: Dict[str, np.ndarray],
  268. voxeldict: Dict[str, VoxelSieve],
  269. X: np.ndarray,
  270. alpha: float,
  271. cv: int,
  272. rois: list,
  273. X_alt: np.ndarray,
  274. fit_icept: bool = False,
  275. save_outs: bool = False,
  276. regname: str | None = "", # remove
  277. plot_hist: bool = True,
  278. alt_model_type: str = "alternative model", # remove
  279. save_folder: str | None = None, # remove
  280. X_str: str | None = None, # remove
  281. ) -> pd.DataFrame:
  282. """Function to run a chain of analyses on the input data for each of the four regions of interest (ROIs).
  283. Includes comparisons with an uninformative dependent variable X matrix (such as a shuffled
  284. version of the original X matrix), to assess the quality of the model in a relative way.
  285. Returns an array of which the first 3 columns contain the voxel coordinates (xyz) and the
  286. fourth contains the across cross validation fold mean correlation R scores between the actual
  287. and predicted dependent variables (y vs. y_hat).
  288. Args:
  289. - ydict (np.ndarray): The dictionary containing the dependent variables y-matrices for each ROI.
  290. - X (np.ndarray): The independent variables X-matrix.
  291. - alpha (float): The regularisation parameter of the Ridge regression model.
  292. - cv (int): The number of cross-validation folds.
  293. - rois (list): The list of regions of interest (ROIs) to analyse.
  294. - X_alt (np.ndarray): The alternative model's X-matrix to compare the model against.
  295. - fit_icept (bool, optional): Whether or not to fit an intercept. If both X and y matrices are z-scored
  296. it is highly recommended to set it to False, otherwise detecting effects becomes difficult. Defaults to False.
  297. - save_outs (bool, optional): Whether or not to save the outputs. Defaults to False.
  298. Returns:
  299. - np.ndarray: Object containing the voxel coordinates and the mean R scores for each ROI. This can be
  300. efficiently turned into a numpy array using NSP.utils.coords2numpy, which in turn can be converted
  301. into a nifti file using nib.Nifti1Image(np.array, affine), in which the affine can be extracted
  302. from a readily available nifti file from the specific subject (using your_nifti.affine).
  303. """
  304. r_values = {}
  305. r_uninformative = {}
  306. regcor_dict = {} # Dictionary to store cor_scores
  307. regcor_dict["X"] = {}
  308. if save_outs:
  309. save_path = f"{self.nsp.own_datapath}/{subject}/results/{save_folder}"
  310. os.makedirs(save_path, exist_ok=True)
  311. # Calculate scores for the given X matrix
  312. for roi in rois:
  313. y = ydict[roi]
  314. model_og = self.run_ridge_regression(X, y, alpha=alpha, fit_icept=False)
  315. _, cor_scores = self.score_model(X, y, model_og, cv=cv)
  316. r_values[roi] = np.mean(cor_scores, axis=0)
  317. regcor_dict["X"][roi] = cor_scores # Save cor_scores to dictionary
  318. xyz = voxeldict[roi].xyz
  319. # Get a vector with the roi name repeated for each voxel
  320. roi_vec = np.array([roi] * len(xyz)).reshape(-1, 1)
  321. this_coords = np.hstack(
  322. (xyz, roi_vec, np.array(r_values[roi]).reshape(-1, 1))
  323. )
  324. this_coefs = np.mean(model_og.coef_, axis=1).reshape(
  325. -1, 1
  326. ) # This is where I get the mean betas for each voxel
  327. this_unpred_coef = model_og.coef_[
  328. :, -1
  329. ] # This is supposed to get the beta coefficient for the last regressor (the unpredictability features)
  330. if roi == "V1":
  331. coords = this_coords
  332. beta_coefs = this_coefs
  333. unpred_coefs = this_unpred_coef.reshape(-1, 1)
  334. else:
  335. coords = np.vstack((coords, this_coords))
  336. beta_coefs = np.vstack((beta_coefs, this_coefs))
  337. unpred_coefs = np.vstack(
  338. (unpred_coefs, this_unpred_coef.reshape(-1, 1))
  339. )
  340. regcor_dict["X_shuffled"] = {}
  341. # Calculate scores for the uninformative/baseline X matrix
  342. for roi in rois:
  343. y = ydict[roi]
  344. model_comp = self.run_ridge_regression(
  345. X_alt, y, alpha=alpha, fit_icept=fit_icept
  346. )
  347. _, cor_scores = self.score_model(X_alt, y, model_comp, cv=cv)
  348. r_uninformative[roi] = np.mean(cor_scores, axis=0)
  349. regcor_dict["X_shuffled"][roi] = cor_scores # Save cor_scores to dictionary
  350. this_uninf_coefs = np.mean(model_comp.coef_, axis=1).reshape(-1, 1)
  351. if roi == "V1":
  352. uninf_scores = r_uninformative[roi].reshape(-1, 1)
  353. uninf_coefs = this_uninf_coefs
  354. else:
  355. uninf_scores = np.vstack(
  356. (uninf_scores, r_uninformative[roi].reshape(-1, 1))
  357. )
  358. uninf_coefs = np.vstack((uninf_coefs, this_uninf_coefs))
  359. delta_r_df = pd.DataFrame()
  360. for i, roi in enumerate(rois[:4]):
  361. # Calculate and store the delta-R values
  362. if roi == "V1":
  363. all_vox_delta_r = (r_values[roi] - r_uninformative[roi]).reshape(-1, 1)
  364. else:
  365. all_vox_delta_r = np.vstack(
  366. (
  367. all_vox_delta_r,
  368. (r_values[roi] - r_uninformative[roi]).reshape(-1, 1),
  369. )
  370. )
  371. this_delta_r = round(
  372. np.mean(r_values[roi]) - np.mean(r_uninformative[roi]), 5
  373. ) # TODO: is this the same as first delta_r /vox and then mean?
  374. delta_r_df[roi] = [this_delta_r]
  375. if plot_hist:
  376. if roi == "V1": # Create a figure with 4 subplots
  377. fig, axs = plt.subplots(2, 2, figsize=(8, 8))
  378. # Flatten the axs array for easy iteration
  379. axs = axs.flatten()
  380. # Underlay with the histogram of r_uninformative[roi] values
  381. axs[i].hist(
  382. r_uninformative[roi],
  383. bins=25,
  384. edgecolor=None,
  385. alpha=1,
  386. label=alt_model_type,
  387. color="black",
  388. ) # Was "burlywood"
  389. # Plot the histogram of r_values[roi] values in the i-th subplot
  390. axs[i].hist(
  391. r_values[roi],
  392. bins=25,
  393. edgecolor="black",
  394. alpha=0.75,
  395. label=X_str,
  396. color="orangered",
  397. ) # Was "dodgerblue"
  398. axs[i].set_title(f"{roi} delta-R: {this_delta_r}")
  399. axs[i].legend() if roi == "V1" else None
  400. if roi == "V4": # Add title and display the figure
  401. plt.suptitle(f"{regname}", fontsize=16)
  402. plt.tight_layout()
  403. plt.show()
  404. plt.savefig(f"{save_path}/{regname}_plot.png") if save_outs else None
  405. coords = np.hstack(
  406. (
  407. coords,
  408. uninf_scores,
  409. all_vox_delta_r.reshape(-1, 1),
  410. beta_coefs,
  411. uninf_coefs,
  412. unpred_coefs,
  413. )
  414. ) # also added beta coefficients as last column. very rough but works
  415. coords_df = pd.DataFrame(
  416. coords,
  417. columns=[
  418. "x",
  419. "y",
  420. "z",
  421. "roi",
  422. "R",
  423. "R_alt_model",
  424. "delta_r",
  425. "betas",
  426. "betas_alt_model",
  427. "beta_unpred",
  428. ],
  429. )
  430. (
  431. coords_df.to_csv(f"{save_path}/{regname}_regdf.csv", index=False)
  432. if save_outs
  433. else None
  434. )
  435. return coords_df
  436. def analysis_chain(
  437. self,
  438. subject: str,
  439. ydict: Dict[str, np.ndarray],
  440. voxeldict: Dict[str, VoxelSieve],
  441. X: np.ndarray,
  442. alpha: float,
  443. cv: int,
  444. rois: list,
  445. X_uninformative: np.ndarray,
  446. fit_icept: bool = False,
  447. save_outs: bool = False,
  448. regname: Optional[str] = "",
  449. plot_hist: bool = True,
  450. shuf_or_baseline: str = "s",
  451. save_folder: str | None = None,
  452. X_str: str | None = None,
  453. ) -> (np.ndarray, pd.DataFrame):
  454. """Function to run a chain of analyses on the input data for each of the four regions of interest (ROIs).
  455. Includes comparisons with an uninformative dependent variable X matrix (such as a shuffled
  456. version of the original X matrix), to assess the quality of the model in a relative way.
  457. Returns an array of which the first 3 columns contain the voxel coordinates (xyz) and the
  458. fourth contains the across cross validation fold mean correlation R scores between the actual
  459. and predicted dependent variables (y vs. y_hat).
  460. Args:
  461. - ydict (np.ndarray): The dictionary containing the dependent variables y-matrices for each ROI.
  462. - X (np.ndarray): The independent variables X-matrix.
  463. - alpha (float): The regularisation parameter of the Ridge regression model.
  464. - cv (int): The number of cross-validation folds.
  465. - rois (list): The list of regions of interest (ROIs) to analyse.
  466. - X_uninformative (np.ndarray): The uninformative X-matrix to compare the model against.
  467. - fit_icept (bool, optional): Whether or not to fit an intercept. If both X and y matrices are z-scored
  468. It is highly recommended to set it to False, otherwise detecting effects becomes difficult. Defaults to False.
  469. - save_outs (bool, optional): Whether or not to save the outputs. Defaults to False.
  470. Returns:
  471. - np.ndarray: Object containing the voxel coordinates and the mean R scores for each ROI. This can be
  472. efficiently turned into a numpy array using NSP.utils.coords2numpy, which in turn can be converted
  473. into a nifti file using nib.Nifti1Image(np.array, affine), in which the affine can be extracted
  474. from a readily available nifti file from the specific subject (using your_nifti.affine).
  475. """
  476. comp_X_str = "Shuffled model" if shuf_or_baseline == "s" else "Baseline model"
  477. X_str = "model" if X_str is None else X_str
  478. r_values = {}
  479. r_uninformative = {}
  480. regcor_dict = {} # Dictionary to store cor_scores
  481. regcor_dict["X"] = {}
  482. # Calculate scores for the given X matrix
  483. for roi in rois:
  484. y = ydict[roi]
  485. model_og = self.run_ridge_regression(X, y, alpha=alpha, fit_icept=False)
  486. _, cor_scores = self.score_model(X, y, model_og, cv=cv)
  487. r_values[roi] = np.mean(cor_scores, axis=0)
  488. regcor_dict["X"][roi] = cor_scores # Save cor_scores to dictionary
  489. xyz = voxeldict[roi].xyz
  490. this_coords = np.hstack((xyz, np.array(r_values[roi]).reshape(-1, 1)))
  491. this_coefs = np.mean(model_og.coef_, axis=1).reshape(-1, 1)
  492. if roi == "V1":
  493. coords = this_coords
  494. beta_coefs = this_coefs
  495. else:
  496. coords = np.vstack((coords, this_coords))
  497. beta_coefs = np.vstack((beta_coefs, this_coefs))
  498. regcor_dict["X_shuffled"] = {}
  499. # Calculate scores for the uninformative/baseline X matrix
  500. for roi in rois:
  501. y = ydict[roi]
  502. model_comp = self.run_ridge_regression(
  503. X_uninformative, y, alpha=alpha, fit_icept=fit_icept
  504. )
  505. _, cor_scores = self.score_model(X_uninformative, y, model_comp, cv=cv)
  506. r_uninformative[roi] = np.mean(cor_scores, axis=0)
  507. regcor_dict["X_shuffled"][roi] = cor_scores # Save cor_scores to dictionary
  508. if roi == "V1":
  509. uninf_scores = r_uninformative[roi].reshape(-1, 1)
  510. else:
  511. uninf_scores = np.vstack(
  512. (uninf_scores, r_uninformative[roi].reshape(-1, 1))
  513. )
  514. coords = np.hstack(
  515. (coords, uninf_scores, beta_coefs)
  516. ) # also added beta coefficients as last column. very rough but works
  517. delta_r_df = pd.DataFrame()
  518. for i, roi in enumerate(rois[:4]):
  519. # Calculate and store the delta-R values
  520. this_delta_r = round(
  521. np.mean(r_values[roi]) - np.mean(r_uninformative[roi]), 5
  522. )
  523. delta_r_df[roi] = [this_delta_r]
  524. if plot_hist:
  525. if roi == "V1": # Create a figure with 4 subplots
  526. fig, axs = plt.subplots(2, 2, figsize=(8, 8))
  527. # Flatten the axs array for easy iteration
  528. axs = axs.flatten()
  529. # Underlay with the histogram of r_uninformative[roi] values
  530. axs[i].hist(
  531. r_uninformative[roi],
  532. bins=25,
  533. edgecolor=None,
  534. alpha=1,
  535. label=comp_X_str,
  536. color="black",
  537. ) # Was "burlywood"
  538. # Plot the histogram of r_values[roi] values in the i-th subplot
  539. axs[i].hist(
  540. r_values[roi],
  541. bins=25,
  542. edgecolor="black",
  543. alpha=0.75,
  544. label=X_str,
  545. color="orangered",
  546. ) # Was "dodgerblue"
  547. axs[i].set_title(f"{roi} delta-R: {this_delta_r}")
  548. axs[i].legend() if roi == "V1" else None
  549. if roi == "V4": # Add title and display the figure
  550. plt.suptitle(f"{regname}", fontsize=16)
  551. plt.tight_layout()
  552. plt.show()
  553. if save_outs:
  554. if save_folder is None:
  555. if "unpred" in regname:
  556. save_folder = "unpred"
  557. elif "baseline" in regname:
  558. save_folder = "baseline"
  559. elif "encoding" in regname:
  560. save_folder = "encoding"
  561. else:
  562. save_folder = "various"
  563. save_path = f"{self.nsp.own_datapath}/{subject}/brainstats/{save_folder}"
  564. os.makedirs(save_path, exist_ok=True)
  565. # Save the delta_r_df to a file
  566. delta_r_df.to_pickle(f"{save_path}/{regname}_delta_r.pkl")
  567. plt.savefig(f"{save_path}/{regname}_plot.png") # Save the plot to a file
  568. # Save cor_scores to a file
  569. np.save(
  570. f"{save_path}/{regname}_regcor_scores.npy", coords
  571. ) # Save the coords to a file
  572. print(
  573. f"Succesfully saved the outputs to {regname}_plot.png and {regname}_regcor_scores.npy"
  574. )
  575. # Save the regcor_dict to a file
  576. with open(f"{save_path}/{regname}_regcor_dict.pkl", "wb") as f:
  577. pickle.dump(regcor_dict, f)
  578. return coords, delta_r_df
  579. def load_regresults(
  580. self,
  581. subject: str,
  582. prf_dict: dict,
  583. roi_masks: dict,
  584. feattype: str,
  585. cnn_layer: Optional[int] = None,
  586. plot_on_viscortex: bool = True,
  587. plot_result: Optional[str] = "r",
  588. lowcap: float = 0,
  589. upcap: float = None,
  590. file_tag: str = "",
  591. verbose: bool = True,
  592. reg_folder: str = "",
  593. ):
  594. """Function to load in the results from the regressions.
  595. Args:
  596. - subject (str): The subject for which the regression results are to be loaded
  597. - prf_dict (dict): The pRF dictionary
  598. - roi_masks (dict): The dictionary with the 3D np.ndarray boolean brain masks
  599. - feattype (str): options: 'rms', 'ce', 'sc', 'unpred', 'alexunet', 'alexown'
  600. - cnn_layer (int): Optional, only necessary for the 'unpred, 'alexunet', and 'alexown' feature types
  601. - plot_on_viscortex (bool): Whether to plot the results on the V1 mask
  602. - plot_result (str): The result to plot, options: 'r', 'r_shuf', 'r_rel', 'betas'
  603. - lowcap (float): The lower cap for the colourmap when plotting the brain
  604. - upcap (float): The upper cap, idem.
  605. - file_tag (str): Optional tag to append to the file name
  606. - verbose (bool): Whether to print the coords dataframe
  607. Out:
  608. - cor_scores_dict (Dict): The dictionary that contains for both the actual X matrix and the shuffled X matrix the
  609. r correlation scores for every separate cv fold.
  610. - coords (pd.DataFrame): This dataframe contains the mean r and beta scores over all of the cross-validation folds
  611. """
  612. reg_str = f"{feattype}"
  613. if feattype in ["unpred", "alexunet", "alexown"] or cnn_layer is not None:
  614. if cnn_layer is None:
  615. raise ValueError(
  616. "Please provide a cnn_layer number for the feature type you have chosen"
  617. )
  618. reg_str = f"{feattype}_lay{cnn_layer}{file_tag}"
  619. # This is the dictionary that contains for both the actual X matrix and the shuffled X matrix the
  620. # r correlation scores for every separate cv fold.
  621. with open(
  622. f"{self.nsp.own_datapath}/{subject}/brainstats/{reg_folder}/{reg_str}_regcor_dict.pkl",
  623. "rb",
  624. ) as f:
  625. # Structure: cor_scores_dict['X' or 'X_uninformative'][roi][cross-validation fold]
  626. cor_scores_dict = pickle.load(f)
  627. # This dataframe contains the mean scores over all of the cross-validation folds
  628. coords = pd.DataFrame(
  629. np.load(
  630. f"{self.nsp.own_datapath}/{subject}/brainstats/{reg_folder}/{reg_str}_regcor_scores.npy"
  631. ),
  632. columns=["x", "y", "z", "r", "r_shuf", "beta"],
  633. )
  634. if plot_result == "r":
  635. plot_val = 3
  636. elif plot_result == "r_shuf":
  637. plot_val = 4
  638. elif plot_result == "r_rel":
  639. plot_val = 6
  640. coords["r_rel"] = coords["r"] - coords["r_shuf"]
  641. elif plot_result == "betas":
  642. plot_val = 5
  643. if verbose:
  644. print(coords)
  645. if plot_on_viscortex:
  646. brain_np = self.nsp.utils.coords2numpy(
  647. np.hstack(
  648. (
  649. np.array(coords)[:, :3],
  650. np.array(coords)[:, plot_val].reshape(-1, 1),
  651. )
  652. ),
  653. roi_masks[subject]["V1_mask"].shape,
  654. keep_vals=True,
  655. )
  656. self.plot_brain(
  657. prf_dict,
  658. roi_masks,
  659. subject,
  660. self.nsp.utils.cap_values(np.copy(brain_np), lowcap, upcap),
  661. False,
  662. save_img=False,
  663. img_path="/home/rfpred/imgs/rel_scores_np.png",
  664. )
  665. self.nsp.cortex.viscortex_plot(
  666. prf_dict=prf_dict,
  667. vismask_dict=roi_masks,
  668. plot_param=None,
  669. subject=subject,
  670. upcap=upcap,
  671. lowcap=lowcap,
  672. inv_colour=False,
  673. cmap="RdGy_r",
  674. regresult=brain_np,
  675. )
  676. return cor_scores_dict, coords
  677. def plot_delta_r(
  678. self,
  679. subject: str,
  680. rois: list,
  681. cnn_type: str = "alex",
  682. file_tag: str = "",
  683. save_imgs: bool = False,
  684. basis_param: str = "betas",
  685. which_reg: str = "unpred",
  686. ):
  687. """Function to plot the delta r values that have resulted from regressions across different cnn layers.
  688. Works for the predictability estimates and for the encoding models.
  689. Args:
  690. - subject (str): The subject.
  691. - rois (list): List of ROIs to consider.
  692. - cnn_type (str, optional): The type of CNN model to use. Defaults to 'alex'.
  693. - file_tag (str, optional): Optional tag to append to the file name. Defaults to ''.
  694. - save_imgs (bool, optional): Whether to save the images. Defaults to False.
  695. - basis_param (str, optional): The basis for assigning layers. Defaults to 'betas', alternative is 'r' or 'delta_r'.
  696. - which_reg (str, optional): The type of regression to use. Defaults to 'unpred'.
  697. """
  698. first_lay = 0
  699. last_lay = 5 if cnn_type == "alex" or cnn_type == "alexnet" else 6
  700. if which_reg == "unpred":
  701. feattype = f"{cnn_type}_unpred"
  702. elif which_reg == "encoding":
  703. feattype = "allvox_alexunet"
  704. first_lay = 1
  705. last_lay = 5
  706. n_layers = last_lay - first_lay
  707. if basis_param == "betas":
  708. param_col = 5
  709. elif basis_param == "r":
  710. param_col = 3
  711. elif basis_param == "delta_r":
  712. param_col = 6
  713. for layer in range(first_lay, last_lay): # Loop over the layers of the alexnet
  714. cnn_layer = (
  715. f"er{str(layer)}" if which_reg == "encoding" else f"{str(layer)}"
  716. )
  717. delta_r_layer = (
  718. pd.read_pickle(
  719. f"{self.nsp.own_datapath}/{subject}/brainstats/{feattype}_lay{cnn_layer}{file_tag}_delta_r.pkl"
  720. )
  721. .values[0]
  722. .flatten()
  723. )
  724. if layer == first_lay:
  725. all_delta_r = delta_r_layer
  726. else:
  727. all_delta_r = np.vstack((all_delta_r, delta_r_layer))
  728. df = pd.DataFrame(all_delta_r, columns=rois)
  729. print(df)
  730. df.reset_index(inplace=True)
  731. # Melt the DataFrame to long-form or tidy format
  732. df_melted = df.melt("index", var_name="ROI", value_name="b")
  733. fig, ax = plt.subplots()
  734. # Create the line plot
  735. sns.lineplot(x="index", y="b", hue="ROI", data=df_melted, marker="o", ax=ax)
  736. ax.set_xticks(range(n_layers)) # Set x-axis ticks to be integers from 0 to 4
  737. ax.set_xlabel(f"{cnn_type} Layer")
  738. ax.set_ylabel("Delta R Value")
  739. ax.set_title(f"Delta R Value per {cnn_type} Layer")
  740. if save_imgs:
  741. # Save the plot
  742. fig.savefig(
  743. f"{self.nsp.own_datapath}/{subject}/brainstats/{feattype}_lay{cnn_layer}{file_tag}_delta_r_plot.png"
  744. )
  745. plt.show()
  746. def assign_layers_old(
  747. self,
  748. subject: str,
  749. prf_dict: dict,
  750. roi_masks: dict,
  751. rois: list,
  752. cmap,
  753. cnn_type: str = "alex",
  754. plot_on_brain: bool = True,
  755. file_tag: str = "",
  756. save_imgs: bool = False,
  757. basis_param: str = "betas",
  758. which_reg: str = "unpred",
  759. man_title: str | None = None,
  760. return_nifti: bool = False,
  761. first_lay: int | None = None,
  762. last_lay: int | None = None,
  763. direct_folder: str | None = None,
  764. ):
  765. """
  766. Assigns layers to voxels based on the maximum beta value across layers for each voxel.
  767. Args:
  768. subject (str): The subject.
  769. prf_dict (dict): Dictionary containing pRF model results.
  770. roi_masks (dict): Dictionary containing ROI masks.
  771. rois (list): List of ROIs to consider.
  772. cnn_type (str, optional): The type of CNN model to use. Defaults to 'alex'.
  773. plot_on_brain (bool, optional): Whether to plot the results on the brain. Defaults to True.
  774. file_tag (str, optional): Optional tag to append to the file name. Defaults to ''.
  775. save_imgs (bool, optional): Whether to save the images. Defaults to False.
  776. basis_param (str, optional): The basis for assigning layers. Defaults to 'betas', alternative is 'r' or 'delta_r'.
  777. which_reg (str, optional): The type of regression to use. Defaults to 'unpred'.
  778. """
  779. first_lay = 0
  780. last_lay = 5 if cnn_type == "alex" or cnn_type == "alexnet" else 6
  781. n_layers = last_lay - first_lay
  782. if basis_param == "betas":
  783. param_col = 5
  784. elif basis_param == "r":
  785. param_col = 3
  786. elif basis_param == "delta_r":
  787. param_col = 6
  788. if direct_folder is None:
  789. if which_reg == "unpred":
  790. feattype = f"{cnn_type}_unpred"
  791. elif which_reg == "encoding":
  792. feattype = "allvox_alexunet"
  793. elif which_reg == "encoding_smallpatch":
  794. feattype = "smallpatch_allvox_alexunet"
  795. for layer in range(
  796. first_lay, last_lay
  797. ): # Loop over the layers of the alexnet
  798. cnn_layer = (
  799. f"er{str(layer)}"
  800. if which_reg == "encoding" or which_reg == "encoding_smallpatch"
  801. else f"{str(layer)}"
  802. )
  803. cordict, coords = self.load_regresults(
  804. subject,
  805. prf_dict,
  806. roi_masks,
  807. feattype,
  808. cnn_layer,
  809. plot_on_viscortex=False,
  810. plot_result="r",
  811. file_tag=file_tag,
  812. verbose=False,
  813. reg_folder=which_reg,
  814. )
  815. coords["delta_r"] = (
  816. coords["r"] - coords["r_shuf"]
  817. ) # Compute the delta_r values
  818. if layer == first_lay:
  819. all_betas = np.hstack(
  820. (
  821. np.array(coords)[:, :3],
  822. np.array(coords)[:, param_col].reshape(-1, 1),
  823. )
  824. )
  825. else:
  826. all_betas = np.hstack(
  827. (all_betas, np.array(coords)[:, param_col].reshape(-1, 1))
  828. )
  829. else:
  830. layer = first_lay if first_lay is not None else 0
  831. # Get the list of filenames
  832. filenames = os.listdir(
  833. f"{self.nsp.own_datapath}/{subject}/brainstats/{direct_folder}"
  834. )
  835. # Sort the filenames based on the layer number
  836. sorted_filenames = sorted(filenames, key=self.nsp.utils._extract_layno)
  837. print(sorted_filenames)
  838. for layerfile in sorted_filenames:
  839. print(layerfile)
  840. if fnmatch.fnmatch(layerfile, "*regcor_dict.pkl"):
  841. with open(
  842. f"{self.nsp.own_datapath}/{subject}/brainstats/{direct_folder}/{layerfile}",
  843. "rb",
  844. ) as f:
  845. cordict = pickle.load(f)
  846. if fnmatch.fnmatch(layerfile, "*regcor_scores.npy"):
  847. coords_np = np.load(
  848. f"{self.nsp.own_datapath}/{subject}/brainstats/{direct_folder}/{layerfile}"
  849. )
  850. coords = pd.DataFrame(
  851. coords_np, columns=["x", "y", "z", "r", "r_shuf", "beta"]
  852. )
  853. del coords_np
  854. print(f"Now looking at layer numero {layer}")
  855. coords["delta_r"] = (
  856. coords["r"] - coords["r_shuf"]
  857. ) # Compute the delta_r values
  858. if layer == first_lay:
  859. all_betas = np.hstack(
  860. (
  861. np.array(coords)[:, :3],
  862. np.array(coords)[:, param_col].reshape(-1, 1),
  863. )
  864. )
  865. else:
  866. all_betas = np.hstack(
  867. (all_betas, np.array(coords)[:, param_col].reshape(-1, 1))
  868. )
  869. layer += 1
  870. for n_roi, roi in enumerate(rois):
  871. n_roivoxels = len(cordict["X"][roi][0])
  872. if roi == "V1":
  873. vox_of_roi = np.ones((n_roivoxels, 1))
  874. else:
  875. vox_of_roi = (
  876. np.vstack((vox_of_roi, (np.ones((n_roivoxels, 1)) * (n_roi + 1))))
  877. ).astype(int)
  878. all_betas_voxroi = np.hstack((all_betas, vox_of_roi))[:, 3:]
  879. all_betas_voxroi[:, :n_layers] = stats.zscore(
  880. all_betas_voxroi[:, :n_layers], axis=0
  881. )
  882. print(all_betas_voxroi)
  883. # Get the index of the maximum value in each row, excluding the last column
  884. max_indices = (
  885. np.argmax(all_betas_voxroi[:, :-1], axis=1) + 1
  886. ) # Add 1 to the max_indices to get the layer number
  887. print(max_indices)
  888. barcmap = LinearSegmentedColormap.from_list(
  889. "NavyBlueVeryLightGreyDarkRed",
  890. ["#000080", "#CCCCCC", "#FFA500", "#FF0000"],
  891. N=n_layers,
  892. )
  893. # Create a new colourmap for the glass brain plot, as it has difficulty adapting the colourmap to non-symmetrical/positive values
  894. colors = np.concatenate(
  895. [barcmap(np.linspace(0, 1, 128)), barcmap(np.linspace(0, 1, 128))]
  896. )
  897. glass_cmap = ListedColormap(colors, name="double_lay_assign")
  898. # Create a DataFrame from the array
  899. df = pd.DataFrame(
  900. all_betas_voxroi,
  901. columns=[f"col_{i}" for i in range(all_betas_voxroi.shape[1])],
  902. )
  903. # Rename the last column to 'ROI'
  904. df.rename(columns={df.columns[-1]: "ROI"}, inplace=True)
  905. # Add the max_indices as a new column
  906. df["AlexNet layer"] = max_indices
  907. # Convert the 'ROI' column to int for plotting
  908. df["ROI"] = df["ROI"].astype(int)
  909. # Calculate the proportions of max_indices within each ROI
  910. df_prop = (
  911. df.groupby("ROI")["AlexNet layer"]
  912. .value_counts(normalize=True)
  913. .unstack(fill_value=0)
  914. )
  915. # Create a mapping from old labels to new labels
  916. roi_mapping = {1: "V1", 2: "V2", 3: "V3", 4: "V4"}
  917. # Change the labels on the x-axis
  918. df_prop.rename(index=roi_mapping, inplace=True)
  919. # Plot the proportions using a stacked bar plot
  920. ax = df_prop.plot(kind="bar", stacked=True, colormap=barcmap)
  921. # Add a y-axis label
  922. ax.set_ylabel("Layer assignment (%)")
  923. # Get current handles and labels
  924. handles, labels = plt.gca().get_legend_handles_labels()
  925. # Reverse handles and labels
  926. handles, labels = handles[::-1], labels[::-1]
  927. # Create legend
  928. legend = plt.legend(
  929. handles,
  930. labels,
  931. title="CNN\nLayer",
  932. loc="center right",
  933. bbox_to_anchor=(1.15, 0.5),
  934. ncol=1,
  935. fancybox=False,
  936. shadow=False,
  937. fontsize=10,
  938. )
  939. if man_title is None:
  940. plt.title(f"Layer assignment {which_reg} {cnn_type} {basis_param}-based")
  941. else:
  942. plt.title(man_title)
  943. if save_imgs:
  944. save_path = f"{self.nsp.own_datapath}/{subject}/brainstats/{cnn_type}_unpred_layassign{file_tag}"
  945. # Save the plot
  946. plt.savefig(f"{save_path}.png")
  947. else:
  948. save_path = ""
  949. plt.show()
  950. if plot_on_brain:
  951. self.stat_on_brain(
  952. prf_dict,
  953. roi_masks,
  954. "subj01",
  955. max_indices,
  956. all_betas[:, :3].astype(int),
  957. glass_brain=True,
  958. cmap=glass_cmap,
  959. save_img=save_imgs,
  960. img_path=f"{save_path}_glassbrain.png",
  961. )
  962. if return_nifti:
  963. brain_coords = np.hstack(
  964. (all_betas[:, :3].astype(int), (max_indices.reshape(-1, 1)))
  965. ) # Earlier I had +1 here
  966. brain_np = self.nsp.utils.coords2numpy(
  967. brain_coords, roi_masks["subj01"]["V1_mask"].shape, keep_vals=True
  968. )
  969. brain_nii = nib.Nifti1Image(
  970. brain_np,
  971. affine=self.nsp.cortex.anat_templates(prf_dict)[subject].affine,
  972. )
  973. if save_imgs:
  974. nib.save(brain_nii, f"{save_path}.nii")
  975. return brain_nii
  976. def explained_var_plot(
  977. self,
  978. relu_layer: int,
  979. n_pcs: int,
  980. smallpatch: bool = False,
  981. modelname: str = "VGG",
  982. ):
  983. """Method to plot the explained variance ratio of the PCA instance created.
  984. Args:
  985. - relu_layer (int): Which ReLU layer of the AlexNet used for the encoding analyses
  986. to plot the explained variance ratio of. Options are 1, 2, 3, 4, and 5.
  987. These correspond to overall layers 1, 4, 7, 9, 11 in the CNN.
  988. - n_pcs (int): The number of principle components. Options are 1000 or 600.
  989. """
  990. smallpatch_str = "smallpatch_" if smallpatch else ""
  991. pca_instance = joblib.load(
  992. f"{self.nsp.own_datapath}/visfeats/cnn_featmaps/{modelname}/pca_{smallpatch_str}{relu_layer}_{n_pcs}pcs.joblib"
  993. )
  994. # Create a figure and a set of subplots
  995. fig, ax = plt.subplots()
  996. # Number of components
  997. n_components = np.arange(1, len(pca_instance.explained_variance_ratio_) + 1)
  998. cumulative_explained_variance_ratio = np.cumsum(
  999. pca_instance.explained_variance_ratio_
  1000. )
  1001. # Plot the explained variance ratio
  1002. ax.bar(
  1003. n_components,
  1004. pca_instance.explained_variance_ratio_,
  1005. alpha=0.5,
  1006. align="center",
  1007. label="individual explained variance",
  1008. )
  1009. # Plot the cumulative explained variance ratio
  1010. ax.step(
  1011. n_components,
  1012. cumulative_explained_variance_ratio,
  1013. where="mid",
  1014. label="cumulative explained variance",
  1015. )
  1016. # Add a horizontal line at y=0.95
  1017. ax.axhline(y=0.95, color="r", linestyle="--", label="95% explained variance")
  1018. # Add labels and title
  1019. ax.set_xlabel("Principal components")
  1020. ax.set_ylabel("Explained variance ratio")
  1021. ax.set_title(
  1022. f"Explained variance ratio across all {n_pcs} PCs of layer {relu_layer}"
  1023. )
  1024. # Add a legend
  1025. ax.legend(loc="best")
  1026. # Show the plot
  1027. plt.show()

analysis.py at commit 7f0a706, no license · at the source

Overview

  1. Donders Institute for Brain, Cognition and Behaviour, Radboud University, Nijmegen, The Netherlands
  2. Amsterdam Brain and Cognition, University of Amsterdam, Amsterdam, The Netherlands
  3. Max Planck Institute for Psycholinguistics, Nijmegen, The Netherlands
Dates: published online 28 March 2026
Type: Preprint
License: CC BY
Identifiers: DOI 10.64898/2026.03.27.714859 · OpenAlex W7141816908
Open access: green, a free copy (OpenAlex)
Status: code verified (how the code is found and verified)
Categories: fMRI (modality)
Methods: Smoothing, state filtering, decompositions, Machine learning, Statistics, Connectivity, fMRI & imaging
Keywords: Neuroscience, Psychological and Cognitive Sciences, visual cortex, spatial prediction, natural vision, fMRI, generative models, predictive processing
Topic: Face Recognition and Perception (Cognitive Neuroscience, Neuroscience), according to OpenAlex
Funding: Dutch Research Council (NWO) (VI.Veni.231G.043)
Citations: not cited yet (Europe PMC); 67 references in the paper

Abstract

Prediction is foundational to theories of perceptual processing and learning, in both neuroscience and AI. However, it remains unclear whether prediction occurs routinely during naturalistic perception, and what level of abstraction the brain predicts. Here, we address both questions by analysing 7T fMRI recordings of humans viewing 73,000 natural images. We use deep generative models to quantify spatial predictability at multiple levels of abstraction, and relate these to retinotopically precise responses across V1-V4, while rigorously controlling for local image features. This reveals that, even during natural scene viewing, responses throughout visual cortex are modulated by spatial predictability, with more predictable inputs evoking weaker responses. In central vision, we observe a hierarchy of predictions that parallels the feature-encoding gradient: V1 is most sensitive to low-level unpredictability, with later areas progressively sensitive to higher-level unpredictability – diverging from recent proposals, in both neuroscience and AI, that prediction operates primarily at higher levels of abstraction. At higher eccentricities, prediction effects are amplified but even V1 is tuned to high-level predictability, consistent with these prior accounts. Together, these results suggest that the visual system implements distinct prediction regimes across the visual field, thereby reconciling conflicting accounts of what visual cortex predicts.

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

Repository

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

WiegerScheurer/prfpred-nsd

License: none: the authors keep all their rights
State: the link answers, verified on 30 September 2026
Evidence: files inventoried
Commit: 7f0a706f29d424c137bc354296b4620d989dc690, 22 May 2026
Languages: Python (61), Shell (32)
Size: 113 files, 93 scripts
Software Heritage: not archived
Found in: “Data and code availability”
Holds: README
Not found: license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: NumPy (52 files), Matplotlib (36 files), SciPy (36 files), pandas (31 files), NiBabel (23 files), scikit-learn (22 files), Pillow (21 files), h5py (20 files), PyTorch (19 files), seaborn (17 files), scikit-image (9 files), OpenCV (8 files), Nilearn (7 files), pycortex (4 files), FreeSurfer (1 file), FSL (1 file), Nipype (1 file)
Availability: 1 check, the latest on 30 September 2026: the link answers
  • 30 September 2026: the link answers
94 files

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

Tracing map

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

What the map holds:

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

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

Data

No dataset and no data link were found in the paper.

Data and code availability

The Natural Scenes Dataset (NSD) is openly available and can be accessed via https://naturalscenesdataset.org. The codebase of the current study is found at https://github.com/WiegerScheurer/prfpred-nsd.

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

Versions

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

Version 1, 30 September 2026: the first record

Recorded: type, journal, dates, 2 authors, 8 keywords, 1 funder, 49 references.

Cite

This paper

Scheurer, W. H., & Heilbron, M. (2026). Distinct regimes of spatial prediction across the visual field during natural vision. bioRxiv (preprint). https://doi.org/10.64898/2026.03.27.714859

BibTeX

@article{scheurer2026distinct,
author = {Scheurer, Wieger H. and Heilbron, Micha},
title = {{Distinct regimes of spatial prediction across the visual field during natural vision}},
journal = {bioRxiv (preprint)},
year = {2026},
month = mar,
publisher = {bioRxiv},
issn = {2692-8205},
doi = {10.64898/2026.03.27.714859},
url = {https://doi.org/10.64898/2026.03.27.714859}
}

RIS

TY - JOUR
AU - Scheurer, Wieger H.
AU - Heilbron, Micha
TI - Distinct regimes of spatial prediction across the visual field during natural vision
T2 - bioRxiv (preprint)
J2 - bioRxiv
PY - 2026
DA - 2026/03/28
SN - 2692-8205
PB - bioRxiv
DO - 10.64898/2026.03.27.714859
UR - https://doi.org/10.64898/2026.03.27.714859
ER -

CSL-JSON

{
"id": "10.64898/2026.03.27.714859",
"type": "article",
"title": "Distinct regimes of spatial prediction across the visual field during natural vision",
"container-title": "bioRxiv (preprint)",
"author": [
{
"family": "Scheurer",
"given": "Wieger H."
},
{
"family": "Heilbron",
"given": "Micha"
}
],
"container-title-short": "bioRxiv",
"DOI": "10.64898/2026.03.27.714859",
"ISSN": "2692-8205",
"publisher": "bioRxiv",
"URL": "https://doi.org/10.64898/2026.03.27.714859",
"issued": {
"date-parts": [
[
2026,
3,
28
]
]
}
}

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

Similar papers

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

[1] doi:10.1016/j.isci.2025.112967 [code]
Contextual responses drive a unique laminar signature in human V1.
Journal: iScience
In common: pycortex, Nipype, Nilearn, 13 other tools, 3 references
[2] doi:10.1038/s41562-025-02252-z [code]
In silico discovery of representational relationships across visual cortex.
Journal: Nature human behaviour
In common: pycortex, Nilearn, FreeSurfer, 11 other tools, fMRI, 4 references
[3] doi:10.1038/s41597-024-04088-0 [code]
An fMRI dataset in response to large-scale short natural dynamic facial expression videos.
Journal: Scientific data
In common: Nilearn, FreeSurfer, OpenCV, 11 other tools, fMRI, 4 references
[4] doi:10.1038/s41597-026-07248-6 [code]
A large-scale fMRI dataset for vision-language semantic association.
Journal: Scientific data
In common: Nilearn, FreeSurfer, FSL, 11 other tools, fMRI, 3 references
[5] doi:10.1162/imag_a_00459 [code]
Hierarchical surprise signals in naturalistic violation of expectations.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: Nipype, Nilearn, FreeSurfer, 10 other tools, fMRI, 3 references
[6] doi:10.1038/s41467-026-68711-x [code]
Neural and computational mechanisms underlying one-shot perceptual learning in humans.
Journal: Nature communications
In common: pycortex, Nilearn, FreeSurfer, 12 other tools, 1 reference
[7] doi:10.1371/journal.pbio.3002365 [code]
The regional variation of laminar thickness in the human isocortex is related to cortical hierarchy and interregional connectivity.
Journal: PLoS biology
In common: pycortex, Nilearn, FreeSurfer, 12 other tools, 1 reference
[8] doi:10.1038/s41597-026-06591-y [code]
CNeuroMod-THINGS, a densely-sampled fMRI dataset for visual neuroscience.
Journal: Scientific data
In common: pycortex, Nipype, Nilearn, 9 other tools, fMRI, 2 references
[9] doi:10.1038/s42003-024-06877-0 [code]
Abnormal structural‒functional coupling patterning in progressive supranuclear palsy is associated with diverse gradients and histological features.
Journal: Communications biology
In common: pycortex, Nilearn, FreeSurfer, 12 other tools
[10] doi:10.1016/j.dcn.2025.101598 [code]
High-field fMRI at 7 Tesla reveals topographic responses tuned to number in the developing human brain.
Journal: Developmental cognitive neuroscience
In common: pycortex, Nipype, Nilearn, 10 other tools, fMRI, 1 reference

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.

Add the badge to the code's README

OSCR: the badge of this paper

If you are an author or a maintainer of this paper's code, you can add this badge. It says what OSCR knows of the paper (its code, its validated tracing map) and links the code to this page. Paste one of these snippets under the README's title. Keep the two marks around it, so that the badge can be replaced in place and is never added twice. OSCR changes nothing in your repository.

Markdown
<!-- oscr-badge:start -->
[![OSCR](https://openscicode.org/badge/doi/10.64898/2026.03.27.714859.svg)](https://openscicode.org/badge/doi/10.64898/2026.03.27.714859)
<!-- oscr-badge:end -->
reStructuredText
.. oscr-badge:start

.. image:: https://openscicode.org/badge/doi/10.64898/2026.03.27.714859.svg
   :target: https://openscicode.org/badge/doi/10.64898/2026.03.27.714859
   :alt: OSCR

.. oscr-badge:end
HTML
<!-- oscr-badge:start -->
<a href="https://openscicode.org/badge/doi/10.64898/2026.03.27.714859"><img src="https://openscicode.org/badge/doi/10.64898/2026.03.27.714859.svg" alt="OSCR"></a>
<!-- oscr-badge:end -->

On GitHub, open the README, choose Edit (the pencil), paste the snippet under the title, then "Commit changes".

If you choose to, you can have the badge kept up to date for you, with oscr badge --write from the command line or with the GitHub Action in your repository's workflows. Every other form and option (a card, AsciiDoc, LaTeX, shields.io) is on the badge 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: code errors and mismatches between the code and the paper about this paper and its code, from readers and its authors, beside the paper and its tracing map.

Report a mismatch between the code and the paper · Report a code error (signed in)

Reproductions: reports from readers who ran the authors' code and did not get the paper's result, what they ran, with which environment, commit and data.

Report a reproduction failure (signed in)

Versions of its code: the releases of its code tied to a version of this paper (the preprint, the accepted manuscript, the version of record…), each with the tracing map versioned with it.

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