OSCR

Non-invasive mapping of the temporal processing hierarchy in the human visual cortex.

Code ↔ Paper

7 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 7 matches · 1 of them tie a paragraph to a whole file, not to given lines: a weak match, whose lines are not tinted
  1. [1] § Methods › Forward modeling approach › Step 1: Estimate pRFs and visual field maps and clusters. ↔ pRFtime/figures.py, lines 389–432 · score 0.72 · V3ab, pIPS, aIPS, V2, LO, VO
  2. [2] § Methods › Forward modeling approach › Step 5: Compare measured and predicted responses. ↔ pRFtime/core.py, lines 185–268 · score 0.67 · gamma ratios, ridge regression, cross validated, fracridge, full model, beta
  3. [3] § Methods › Forward modeling approach › Step 1: Estimate pRFs and visual field maps and clusters. ↔ pRFtime/utils.py, the whole file · a weak match · score 0.60 · binarized stimulus, Gaussian models, visual space, pRFs, eccentricity, vertex
  4. [4] § Methods › Experimental procedure and preprocessing ↔ pRFtime/core.py, lines 127–173 · score 0.60 · overlapping spheres, vertices contribution, gain matrix, weights, signal, sensors
  5. [5] § Methods › Forward modeling approach › Step 3: Predict pRFs’ responses to MEG stimuli. ↔ pRFtime/core.py, lines 92–125 · score 0.57 · binarized stimulus, pRF models, predicted response, surface, vertices, matrix
  6. [6] § Methods › Method validation › Sensor type validation. ↔ pRFtime/core.py, lines 185–268 · score 0.54 · cross validated fitting, optimal, split, full model, variance explained, concatenated
  7. [7] § Methods › Forward modeling approach › Step 1: Estimate pRFs and visual field maps and clusters. ↔ run_pipeline.ipynb, lines 1–62 · score 0.53 · population receptive field, cortical location, Gaussian models, visual space, visual field, pRFs

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 · 432 lines · 25 KB · GPL-3.0 · 4 matches

  1. import numpy as np
  2. import random
  3. from fracridge import FracRidgeRegressorCV
  4. from sklearn.metrics import r2_score
  5. # pRFtime tools
  6. from pRFtime.utils import GaussianModel # model_class preset
  7. class pRFs():
  8. """
  9. Class to create pRF-based sensor-level predictions for regions of interest (ROIs).
  10. pRFs.create_sensor_prediction() is the main function to call, which calls upon the
  11. individual subseps (see below).
  12. """
  13. def __init__(self, prf_parameters,
  14. model_class=GaussianModel,
  15. model_kwargs=None,
  16. verbose=True,
  17. # optional
  18. mask = []
  19. ):
  20. """ Initalizing the pRFs class with a pRF parameters and pRF model to use.
  21. Arguments :
  22. prf_parameters (np.ndarray) : 2D array of shape (vertices, parameters) containing
  23. the parameters necessary to reconstruct the pRF models for each vertex (cortical location).
  24. Number parameters depends on the model_class used, and the parameters it
  25. requires to construct the pRF models.
  26. For the default model_class GaussianModel, 3 parameters are required:
  27. 1) x0, 2) y0 for pRF position, and 3) sigma for pRF's position of each vertex.
  28. model_class (cls) : model class in utils.py with a .name, and functions
  29. .generate() and .predict(). Default: utils.GaussianModel.
  30. model_kwargs (dict) : Dictionary with any keyword arguments the model_class needs.
  31. verbose (str) : True (Default) to print info.
  32. Optional:
  33. mask (np.ndarray) : boolean 1D array of shape (vertices,) which is False for
  34. vertices that should be excluded from the analysis (e.g. vertices with bad fits)
  35. No pRFs will be created for those to save computation time, and not weighted in
  36. for the sensor prediction creation.
  37. Intermediate Output-Aattributes :
  38. .prf_dict (dict) : Dictionary containing at least 'prfs' with reconstructed pRF models.
  39. For GaussianModel class the resulting 'prfs' array is of shape (vertices, pixel, pixel)
  40. containing 2D Gaussian models for each cortical location.
  41. .cortex_predictions (np.ndarray) : 2D array of shape (vertices, stimuli) with
  42. cortical predictions for each pRF, containing the predicted repsonses to each stimulus.
  43. Final Output-Attributes :
  44. .sensor_predictions (np.ndarray) : 3D array of shape (rois, sensors, stimuli) with the
  45. converted sensor-level pRF-based predictions for each region of interest (ROI).
  46. """
  47. self.prf_parameters = prf_parameters
  48. self.model = model_class() # Model class - see utils.py
  49. self.model_kwargs = model_kwargs # Arguments for model class
  50. self.verbose = verbose
  51. # Optional step
  52. self.mask = mask
  53. if len(self.mask) == 0 :
  54. self.mask = np.ones((self.prf_parameters.shape[0],)).astype("bool")
  55. assert self.mask.shape[0] == self.prf_parameters.shape[0]
  56. # Initiate output variables
  57. self.prf_dict = None
  58. self.cortex_predictions = None
  59. self.sensor_predictions = None
  60. def create_prf_models(self):
  61. """ Reconstruct the pRF models for each vertex, based on the
  62. prf_parmaters and model_class provided.
  63. Output:
  64. self.prf_dict (dict) : Dictionary containing at least 'prfs' with reconstructed pRF models.
  65. For GaussianModel class the resulting 'prfs' array is of shape (vertices, pixel, pixel)
  66. containing 2D Gaussian models for each cortical location.
  67. """
  68. if self.verbose: print(f"===== `create_prf_models` =====")
  69. # --- Optional steps ---
  70. # Apply mask if needed
  71. params = np.copy(self.prf_parameters)
  72. params = params[self.mask,:]
  73. if self.verbose:
  74. print(f"(Mask) Using {params.shape[0]} of {self.prf_parameters.shape[0]} vertices...")
  75. print(f"Creating {params.shape[0]} pRF models using '{self.model.name}' model...")
  76. # --- Create pRF models for desired model ---
  77. self.prf_dict = self.model.generate(params,
  78. **self.model_kwargs) # adding prf models to current class
  79. if self.verbose:
  80. print(f"Done.")
  81. print(f"|--> .prf_dict['prfs'] = {self.prf_dict['prfs'].shape} = ((masked-)vertices, pix, pix)")
  82. def create_cortex_predictions(self, design_matrix=np.zeros((0,0,0))):
  83. """ Create predicted responses to stimuli in design_matrix based on pRF models constructed.
  84. Arguments:
  85. design_matrix (np.ndarray) : For the GaussianModel, a 3D array of shape (pix, pix, stimuli)
  86. containing binarized stimulus. Pix is same as nr_pix used for pRF creation.
  87. Output:
  88. self.cortex_predictions (np.ndarray) : 2D array of shape (vertices, stimuli)
  89. Units are arbitrary.
  90. """
  91. # Get pRFs
  92. if self.prf_dict == None: self.create_prf_models () # separate function so that pRF models accesible to visualize in self.prf_dict
  93. if self.verbose:
  94. print(f"===== `create_cortex_predictions` =====")
  95. print(f"Creating '{self.model.name}' cortex predictions...")
  96. # Use model class predict function to be specific to the model
  97. self.cortex_predictions = self.model.predict(self.prf_dict, design_matrix)
  98. # Add predictions back to full number vertices
  99. full_preds = np.zeros((self.mask.shape[0], design_matrix.shape[-1])) # = (vertices, stimuli)
  100. full_preds[self.mask] = self.cortex_predictions.copy()
  101. self.cortex_predictions = full_preds
  102. # Check for NaN (can happen for DN model for example)
  103. nan_vertices = np.unique(np.where(np.isnan(self.cortex_predictions))[0])
  104. if len(nan_vertices) > 0:
  105. if self.verbose: print(f"Setting predictions of {len(nan_vertices)} vertices with NaNs in their surface prediction to 0.")
  106. self.cortex_predictions[nan_vertices,:] = 0
  107. assert not np.any(np.isnan(self.cortex_predictions))
  108. if self.verbose:
  109. print("Done.")
  110. print(f"|--> .cortex_predictions = {self.cortex_predictions.shape} = (vertices, stimuli)")
  111. def create_sensor_predictions(self, gain_matrix=np.zeros((0,0)),
  112. design_matrix=np.zeros((0,0,0)),
  113. roi_masks=[]):
  114. """ Convert cortex predictions to sensor-level for each roi in roi_masks, using the
  115. weights provided in the gain_matrix.
  116. Arguments:
  117. gain_matrix (np.ndarray) : 2D array of shape (sensors, vertices)
  118. containing weights of how much each vertex contributes to the signal
  119. measured in a given sensor. For example obtained using the
  120. Overlapping Spheres model provided by Brainstorm (https://neuroimage.usc.edu/brainstorm/Tutorials/HeadModel).
  121. design_matrix (np.ndarray) : shape depends on stimuli used. For 2D Gaussian model
  122. and 2D visual stimulus example, e.g. (pixel, pixel, stimuli), where pixel number
  123. corresponds to pixel number of the grid used to construct the pRFs (nr_pix).
  124. roi_masks (np.ndarray) : boolean 2D array of shape (rois, vertices)
  125. which is True for vertices belonging to a given region of interest (ROI).
  126. Output :
  127. self.sensor_predictions (np.ndarray) : 3D array of shape (rois, sensors, stimulil)
  128. containing the sensor-level pRF-based predictions for each ROI.
  129. Units same as gain_matrix provided.
  130. """
  131. if self.cortex_predictions == None: self.create_cortex_predictions(design_matrix=design_matrix)
  132. assert gain_matrix.shape[-1] == self.cortex_predictions.shape[0], f"Gain matrix's second dimension must align to number of vertices ({self.cortex_predictions.shape[0]})"
  133. if self.verbose: print(f"===== `create_sensor_predictions` =====")
  134. # Convert for each ROI separately
  135. if len(roi_masks)==0:
  136. roi_masks = np.ones((self.cortex_predictions.shape[0]),).astype('bool')[np.newaxis, ...] # = (1, vertices)
  137. # --- Loop over roi masks to create each sensor prediction ---
  138. # Initiate output
  139. nr_rois, nr_vertices = roi_masks.shape
  140. nr_sensors = gain_matrix.shape[0]
  141. nr_stimuli = design_matrix.shape[-1]
  142. self.sensor_predictions = np.zeros((nr_rois, nr_sensors, nr_stimuli))
  143. if self.verbose: print(f"Creating {nr_rois}-roi sensor predictions...")
  144. for r in range(nr_rois):
  145. roi_mask = roi_mask = np.logical_and(roi_masks[r], self.mask) # = (vertices,) True where in mask and in roi
  146. if self.verbose: print(f"(Mask:) roi-{r} is using {roi_mask.sum()} of originally {roi_masks[r].sum()} vertices...")
  147. roi_hm = gain_matrix[:,roi_mask].copy() # = (sensors, masked_roi vertices)
  148. roi_sensor_preds = np.matmul(roi_hm, self.cortex_predictions[roi_mask, :]) # = (sensors, stimuli) using only vertices in (roi-)mask
  149. self.sensor_predictions[r,:,:] = roi_sensor_preds
  150. if self.verbose:
  151. print("Done.")
  152. print(f"|--> .sensor_predictions = {self.sensor_predictions.shape} = (rois, sensors, stimuli)")
  153. class Fitting():
  154. """
  155. Class to fit sensor predictions (created by pRFs class) to sensor data in a cross-validated
  156. ridge regression.
  157. Fitting.run() is the main function to call and execute all necessary substeps.
  158. Substeps could be called individually - e.g. to repeat the fitting procedure x times
  159. with new nr_sets averages to provide a Confidence Interval, using the same set of
  160. predictions.
  161. """
  162. def __init__(self, sensor_data, sensor_predictions,
  163. nr_sets=4, fracs=np.arange(0.0, 1.0, 0.1),
  164. verbose=True, **kwargs):
  165. """ Initializing object with input sensor data and settings for ridge regression.
  166. Arguments:
  167. sensor_data (list with np.ndarray) : list of length [stimuli,] each 3D arrays of
  168. shape (sensors, trials per stimulus, samples) containing the sensors responses to
  169. each stimulus presentation (trials) for each recorded timepoint (sample).
  170. sensor_predictions (np.ndarray) : 3D array of shape (rois, sensors, stimuli)
  171. containing sensor predictions to each stimuli for each ROI.
  172. Generated with pRFs.create_sensor_predictions() function.
  173. nr_sets (int, default 4) : determines in how many sets the sensor data is split for
  174. cross-validated fitting. Ridge regression will determine
  175. optimal parameters on nr_sets-1 sets, and test on the last set.
  176. E.g. to perform k-fold optimization on 3 sets, nr_sets should be 4.
  177. fracs (np.ndarray) : gamma ratios (fractions) for fracridge to test to optimize ridge regression.
  178. See https://nrdg.github.io/fracridge/ for in depth explanation.
  179. Optional kwargs:
  180. For repeatable analysis:
  181. n_set_inds_per_stim double list with (np.ndarray) : List of length [stimuli,], with list
  182. of length [nr_sets,] containing a 1D array with indices of the trials to use for a
  183. given stimulus and set. To perform analysis on same averaged data sets, otherwise, Fitting
  184. class calls upon ._generate_n_set_indices() to create a new random set of trial indices
  185. appropriate for the stimulus trials of the provided dataset.
  186. To use a subset of given data dimension:
  187. stimulus_indices (np.ndarray) : 1D array with indices of stimuli to use
  188. sensor_indices (np.ndarray) : 1D array with indices of sensors to use
  189. sample_indices (np.ndarray) : 1D array with indices of samples to use
  190. roi_indices (np.ndarray) : 1D array with indices of ROIs to use
  191. Output-Attribute:
  192. Full-model performance over time:
  193. .r2s_full_model (np.ndarray) : 1D array of shape (samples,) containing R-squared values of the
  194. variance explained of the full model (all rois/predictors) in the sensor_data.
  195. Regression parameters over time:
  196. .alphas (np.ndarray) : 1D array of shape (samples,) containing the alpha (regularization)
  197. parameter that was found optimal for a given sample.
  198. .gammas (np.ndarray) : 1D array of shape (samples,) containing the frac (gamma ratio) value
  199. that was fond optimal for a given sample.
  200. ROI performance over time:
  201. .r2s_per_roi (np.ndarray) : 2D array of shape (rois, samples) containing the R-squared values of
  202. each ROI in explaining the sensor_data at a given sample.
  203. .betas_per_roi (np.ndarray) : 2D array of shape (rois, samples) containing the regression weights
  204. of each ROI for a given sample.
  205. """
  206. self.sensor_data = sensor_data
  207. self.sensor_predictions = sensor_predictions
  208. # CV settings
  209. self.nr_sets = nr_sets
  210. self.k_folds = self.nr_sets - 1 # one of the sets if left out for independent testing of the ridge parameters found during k-fold cv
  211. self.fracs = fracs
  212. self.verbose = verbose
  213. # Unpack kwargs
  214. for key, value in kwargs.items():
  215. setattr(self, key, value)
  216. # Get info on dimensions (depending on whether users wants slicing)
  217. if not hasattr(self, 'stimulus_indices'):
  218. self.stimulus_indices = np.arange(len(sensor_data)) # all stimuli in dataset
  219. if not hasattr(self, 'sensor_indices'):
  220. self.sensor_indices = np.arange(sensor_data[0].shape[0]) # all sensors in dataset
  221. if not hasattr(self, 'sample_indices'):
  222. self.sample_indices = np.arange(sensor_data[0].shape[-1]) # all samples in dataset
  223. if not hasattr(self, 'roi_indices'):
  224. self.roi_indices = np.arange(sensor_predictions.shape[0])
  225. self.nr_stimuli = len(self.stimulus_indices)
  226. self.nr_sensors = len(self.sensor_indices)
  227. self.nr_samples = len(self.sample_indices)
  228. self.nr_rois = len(self.roi_indices)
  229. self.nr_datapoints = int(self.nr_stimuli * self.nr_sensors)
  230. # Preset intermediate variables
  231. if not hasattr(self, 'n_set_inds_per_stim'):
  232. self.n_set_inds_per_stim = None # for indices for data averaging
  233. self.average_data = None # for averaged sensor data
  234. self.k_fold_indices = None # for k-fold regression
  235. self.predictions = None # for reshaped (concatenated) predictions
  236. # Results
  237. self.gammas = None
  238. self.alphas = None
  239. self.r2s_full_model = None
  240. self.r2s_per_roi = None
  241. self.betas_per_roi = None
  242. # ======================================================
  243. def run(self):
  244. """ Execute the full fitting pipeline in sequence. """
  245. self._reshape_predictions()
  246. self._generate_n_set_indices()
  247. self._average_data()
  248. self._prepare_k_fold_indices()
  249. self._fit_data()
  250. # ======================================================
  251. # ----- Private Methods -----
  252. def _reshape_predictions(self):
  253. """ Step 1: Reshape predictions and concatenate over (subselected) stimuli and sensors.
  254. Output:
  255. .predictions (rois, stimuli*sensors) with concatenated sensor predictions per ROI
  256. """
  257. self.predictions = np.array([np.concatenate(np.array([[self.sensor_predictions[roi,sensor,stim] for stim in self.stimulus_indices]
  258. for sensor in self.sensor_indices]))
  259. for roi in self.roi_indices]) # = (rois, stimuli*sensors)
  260. if self.verbose: print(f"1: Predictions reshaped to: {self.predictions.shape} (rois, stimuli*sensors)")
  261. def _generate_n_set_indices(self):
  262. """ Step 2: Create n sets of random indices for trial averaging of data.
  263. The function accounts for the fact that different stimuli might have
  264. different number of trials; i.e. shuffles trial indices per stimulus and cuts into n_sets needed.
  265. 'n_set_inds_per_stim' can instead be given to object upon initiation.
  266. Output:
  267. .n_set_inds_per_stim (double list with array) [stimuli,][nr_sets,](trialsperstim)
  268. so each stimulus (with potentially different number of trials), has now n sets
  269. of randomly shuffled trialsthat can be used for averaging
  270. """
  271. if self.n_set_inds_per_stim != None:
  272. # Check shape required
  273. assert len(self.n_set_inds_per_stim)==self.nr_stimuli and len(self.n_set_inds_per_stim[0])==self.nr_sets, \
  274. f"'n_set_inds_per_stim' should be double list [nr_stimuli,] each [nr_sets,] with each 1D array of indices for trial averaging."
  275. if self.verbose: print(f"2: Using {self.nr_sets}-sets of indices for data averaging.") # provided - or from previous call of object
  276. else:
  277. # Create indices for averaging data to n_sets
  278. n_set_inds_per_stim = []
  279. for stim in self.stimulus_indices:
  280. nr_trials = self.sensor_data[stim].shape[1]
  281. cur_inds = np.arange(nr_trials)
  282. random.shuffle(cur_inds)
  283. inds_per_n = np.array_split(cur_inds, self.nr_sets)
  284. n_set_inds_per_stim.append(inds_per_n)
  285. self.n_set_inds_per_stim = n_set_inds_per_stim # [nr_stimuli,][n_sets,](trialsperstim/n_sets,)
  286. if self.verbose: print(f"|- Generated {self.nr_sets} sets of indices for random data averaging for each stimulus.")
  287. def _average_data(self):
  288. """ Step 3: Perform data averaging using the generated indices.
  289. Output:
  290. .average_data [nr_sets,][samples,](stimuli*sensors); each average over specific set of
  291. trials for a given set, concatenated for stimuli and sensors to fit all datapoints at
  292. a given sample.
  293. """
  294. if self.average_data == None: # skip if already in object
  295. n_set_avg_per_sample = []
  296. for n in range(self.nr_sets):
  297. # Take average over subset of trials per stim
  298. avg_per_stim = [np.mean(self.sensor_data[stim][:, self.n_set_inds_per_stim[s][n], :], axis=1)
  299. for s, stim in enumerate(self.stimulus_indices)] # = [stimuli,](sensors,samples) -> averaged over trial's n sets' trial indices
  300. # Concatenate averages over (subselected) stimuli and sensors and order by samples
  301. concavg_per_sample = [np.concatenate( np.array([[avg_per_stim[stim][sensor,sample] for stim in self.stimulus_indices]
  302. for sensor in self.sensor_indices]) )
  303. for sample in self.sample_indices]
  304. n_set_avg_per_sample.append(concavg_per_sample)
  305. self.average_data = n_set_avg_per_sample
  306. if self.verbose: print(f"3: Data averaging for {self.nr_sets} sets completed.")
  307. def _prepare_k_fold_indices(self):
  308. """ Step 4: Generate k-fold indices for CV iterator in ridge regression.
  309. Output:
  310. .k_fold_indices : list [k_folds,] with each tuple (ktrain_inds, ktest_inds),
  311. where ktrain_inds (datapoints, k_folds-1)
  312. and ktest_inds (datapoints)
  313. and each k_fold item has a different test set that is used
  314. """
  315. if self.verbose: print(f"4: Using {self.k_folds}-fold CV to define ridge parameters on training set...")
  316. ks = np.arange(self.k_folds)
  317. test_ks = ks[::-1] # we leave out one set for testing parameters within one fold; i.e. every fold has a different test set and we make it loop from last to first index
  318. train_ks = [np.where(np.invert(ks == test_k))[0]
  319. for test_k in test_ks]
  320. inds_per_set = [np.arange((self.nr_datapoints*k), (self.nr_datapoints*(k+1)))
  321. for k in range(self.k_folds)] # = [k,](datapoints,) -> indices total up to nr_datapoints*k
  322. k_fold_indices = []
  323. for fold in range(self.k_folds):
  324. # First x rows are concatenated for training
  325. ktrain_inds = np.concatenate([inds_per_set[cur_k] for cur_k in train_ks[fold]]) # = (datapoints*k_folds-1,)
  326. # Last rows of indices is used for test of current fold
  327. ktest_inds = inds_per_set[test_ks[fold]]
  328. # Save both as tuple
  329. k_fold_indices.append((ktrain_inds, ktest_inds))
  330. self.k_fold_indices = k_fold_indices
  331. if self.verbose: print(f"|- K-fold indices prepared.")
  332. def _fit_data(self):
  333. """ Step 5: Loop over samples to fit predictions on data.
  334. Output:
  335. Full-model performance over time:
  336. .r2s_full_model (np.ndarray) : 1D array of shape (samples,) containing R-squared values of the
  337. variance explained of the full model (all rois/predictors) in the sensor_data.
  338. Regression parameters over time:
  339. .alphas (np.ndarray) : 1D array of shape (samples,) containing the alpha (regularization)
  340. parameter that was found optimal for a given sample.
  341. .gammas (np.ndarray) : 1D array of shape (samples,) containing the frac (gamma ratio) value
  342. that was fond optimal for a given sample.
  343. ROI performance over time:
  344. .r2s_per_roi (np.ndarray) : 2D array of shape (rois, samples) containing the R-squared values of
  345. each ROI in explaining the sensor_data at a given sample.
  346. .betas_per_roi (np.ndarray) : 2D array of shape (rois, samples) containing the regression weights
  347. of each ROI for a given sample.
  348. """
  349. if self.verbose: print(f"5: Fitting {self.nr_rois} rois, {self.nr_samples} samples and {self.nr_datapoints} datapoints ({self.nr_stimuli} stimuli * {self.nr_sensors} sensors) ...")
  350. # --- Reshape predictions --- (we fit the same X for all samples)
  351. X_test = self.predictions.T # = (datapoints, rois)
  352. X_train = np.vstack([X_test for k in range(self.k_folds)]) # (datapoints*k_folds, rois)
  353. # --- Initiate shaped output ---
  354. self.gammas = np.zeros((self.nr_samples,))
  355. self.alphas = np.zeros((self.nr_samples,))
  356. self.r2s_full_model = np.zeros((self.nr_samples,))
  357. self.r2s_per_roi = np.zeros((self.nr_rois, self.nr_samples))
  358. self.betas_per_roi = np.zeros((self.nr_rois, self.nr_samples))
  359. # --- Loop over samples ---
  360. for sample in range(self.nr_samples): # data is already shaped, so we loop through all (subselected) samples
  361. # Select current samples' data averages
  362. y_test = self.average_data[-1][sample] # = (stimuli*sensors,)
  363. y_train = np.concatenate([self.average_data[k][sample] for k in range(self.k_folds)]) # = ((stimuli*sensors)*k_folds,)
  364. # Use training data (y_train) for k-fold validation (our own random average sets we created)
  365. frr = FracRidgeRegressorCV(cv=self.k_fold_indices)
  366. frr.fit(X_train, y_train, frac_grid=self.fracs)
  367. self.frr = frr
  368. # Calculate full model performance with chosen parameters (gamma, alpha, betas) on left out test set
  369. yhat = frr.predict(X_test)
  370. self.r2s_full_model[sample] = r2_score(y_test, yhat) # = float
  371. # Save belonging parameters
  372. self.gammas[sample] = frr.best_frac_ # = float
  373. self.alphas[sample] = frr.alpha_[0][0] # = float
  374. self.betas_per_roi[:,sample] = frr.coef_
  375. # Calculate performance PER ROI (aka regressors / predictor)
  376. r2s = []
  377. for r in range(self.nr_rois): # again loop over all, since already subselected
  378. yhat_other = yhat - (X_test[:,r] * self.betas_per_roi[r][sample])
  379. rss = np.sum( (y_test - yhat)**2 ) # = float // aka y_test - yhat_pther - yhat_roi
  380. tss = np.sum( (y_test - yhat_other)**2 ) # to account for other rois in data when computing roi
  381. r2s.append( 1 - (rss/tss) )
  382. self.r2s_per_roi[:,sample] = np.array(r2s)
  383. if self.verbose: print(f"|- Fitting process completed.")

core.py at commit d415a5a, under GPL-3.0 · at the source

Overview

Authors: Katharina Eickhoff1,2,3, Arjan Hillebrand4,5,6, Tomas Knapen1,2,3, Maartje C. de Jong1,2, Serge O. Dumoulin1,2,3,7
  1. Spinoza Centre for Neuroimaging, Amsterdam, the Netherlands
  2. Computational Cognitive Neuroscience and Neuroimaging, Netherlands Institute for Neuroscience, Amsterdam, the Netherlands
  3. Experimental and Applied Psychology, Vrije Universiteit, Amsterdam, the Netherlands
  4. Clinical Neurophysiology and Magnetoencephalography Centre, Amsterdam UMC, Amsterdam, the Netherlands
  5. Amsterdam Neuroscience, Brain Imaging, Amsterdam, the Netherlands
  6. Amsterdam Neuroscience, Systems and Network Neuroscience, Amsterdam, the Netherlands
  7. Experimental Psychology, Utrecht University, Utrecht, the Netherlands
Journal: PLoS computational biology, volume 22, issue 7, article e1014434
Dates: received 26 May 2025; accepted 11 June 2026; published online 10 July 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1371/journal.pcbi.1014434 · PMID 42430453 · PMCID PMC13379088 · OpenAlex W4410325642
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: fMRI (modality), MEG (modality), human (organism), systems (subfield)
Methods: Statistics, Machine learning, Preprocessing, Connectivity, Physiology & signal measures
MeSH: Brain Mapping*, Visual Cortex*, Visual Perception*, Adult, Computational Biology, Female, Humans, Magnetic Resonance Imaging, Magnetoencephalography, Male, Models, Neurological, Visual Fields (* major topic)
Journal subjects: Biology and Life Sciences, Neuroscience, Brain Mapping, Functional Magnetic Resonance Imaging, Medicine and Health Sciences, Diagnostic Medicine, Diagnostic Radiology, Magnetic Resonance Imaging, Research and Analysis Methods, Imaging Techniques, Radiology and Imaging, Neuroimaging, Cognitive Science, Cognitive Psychology, Perception, Sensory Perception, Vision, Psychology, Social Sciences, Computer and Information Sciences, Software Engineering, Preprocessing, Engineering and Technology, Equipment, Measurement Equipment, Magnetometers, Magnetoencephalography, Physiology, Sensory Physiology, Visual System, Sensory Systems, Anatomy, Brain, Visual Cortex
Topic: Neural Networks and Applications (Artificial Intelligence, Computer Science), according to OpenAlex
Citations: not cited yet (Europe PMC); 78 references in the paper

Abstract

Vision, and brain processing more broadly, is inherently dynamic across space and time, so understanding brain function requires consideration of both spatial and temporal dimensions. However, simultaneously capturing the fine spatial details and the rapid temporal dynamics of visual processing remains a major challenge, resulting in a gap in our understanding of spatiotemporal dynamics. Here, we introduce a forward modeling technique that bridges high-spatial resolution fMRI with high-temporal resolution MEG, enabling us to non-invasively estimate different levels of the visual hierarchy in humans and their involvement in visual processing with millisecond precision. Using fMRI, levels of the visual hierarchy were identified by measuring individuals’ population receptive fields and determining visual field maps. We predicted how much the activity patterns in each visual field map would contribute to brain responses measured with MEG. By comparing these predicted responses with the measured MEG responses, we assessed how much a given visual field map contributed to the measured MEG response, and, most importantly, when. We combined information from all MEG sensors and revealed a cortical processing hierarchy across visual field maps. We validated the method using cross-validations and demonstrated that the model generalized across MEG sensor types, stimulus shapes, and was robust to the number of visual field maps included in the model. The primary visual cortex captured most of the variance in the MEG sensors and did so earlier in time than extrastriate regions. We effectively combined the advantages of two very different neuroimaging techniques, opening avenues for answering research questions that require recordings with high spatiotemporal detail. By bridging traditionally separate areas of research, our approach helps close longstanding gaps in our understanding of brain function.

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 7 matches between paragraphs and lines of code.

kateic/pRFtime

License: GPL-3.0
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: d415a5afb9b87860f688762ea0c9b1443a1f6d47, 13 May 2025
Languages: Python (5), Jupyter (1)
Size: 8 files, 6 scripts
Software Heritage: not archived
Found in: “Data Availability”
Holds: README, license file, environment (setup.py), 1 notebook
Not found: CITATION.cff, tests, continuous integration, documentation
Tools: NumPy (4 files), Matplotlib (2 files), NetworkX (2 files), seaborn (2 files), scikit-learn (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
8 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;
  • 6 scripts, each with its path and the digest of its content;
  • 7 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 Availability

The code used for the analysis is publicly available at https://github.com/kateic/pRFtime. The minimal anonymized data necessary to replicate these study findings have been deposited in DANS Data Station Life Sciences (https://doi.org/10.17026/LS/AKGXRM; K. Eickhoff; A. Hillebrand; T. Knapen; M.C. de Jong; S.O. Dumoulin, 2026, “Replication Data for: Non-invasive mapping of the temporal processing hierarchy in the human visual cortex”). The raw MRI and MEG data is considered personal data pursuant to the General Data Protection Regulation (GDPR) and can only be shared as a de-identified data set based on and subject to the Netherlands Institute for Neurosciences of the Royal Netherlands Academy of Sciences policies.

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

Recorded: type, language, journal, volume, issue, pages, dates, 5 authors, 12 MeSH terms, 1 funder, 78 references.

Cite

This paper

Eickhoff, K., Hillebrand, A., Knapen, T., de Jong, M. C., & Dumoulin, S. O. (2026). Non-invasive mapping of the temporal processing hierarchy in the human visual cortex. PLoS computational biology, 22(7), e1014434. https://doi.org/10.1371/journal.pcbi.1014434

BibTeX

@article{eickhoff2026non,
author = {Eickhoff, Katharina and Hillebrand, Arjan and Knapen, Tomas and de Jong, Maartje C. and Dumoulin, Serge O.},
title = {{Non-invasive mapping of the temporal processing hierarchy in the human visual cortex}},
journal = {PLoS computational biology},
year = {2026},
month = jul,
volume = {22},
number = {7},
pages = {e1014434},
publisher = {PLOS},
issn = {1553-734X},
doi = {10.1371/journal.pcbi.1014434},
url = {https://doi.org/10.1371/journal.pcbi.1014434},
pmid = {42430453},
pmcid = {PMC13379088}
}

RIS

TY - JOUR
AU - Eickhoff, Katharina
AU - Hillebrand, Arjan
AU - Knapen, Tomas
AU - de Jong, Maartje C.
AU - Dumoulin, Serge O.
TI - Non-invasive mapping of the temporal processing hierarchy in the human visual cortex
T2 - PLoS computational biology
J2 - PLoS Comput Biol
PY - 2026
DA - 2026/07/10
VL - 22
IS - 7
SP - e1014434
SN - 1553-734X
PB - PLOS
DO - 10.1371/journal.pcbi.1014434
UR - https://doi.org/10.1371/journal.pcbi.1014434
LA - en
ER -

CSL-JSON

{
"id": "10.1371/journal.pcbi.1014434",
"type": "article-journal",
"title": "Non-invasive mapping of the temporal processing hierarchy in the human visual cortex",
"container-title": "PLoS computational biology",
"author": [
{
"family": "Eickhoff",
"given": "Katharina"
},
{
"family": "Hillebrand",
"given": "Arjan"
},
{
"family": "Knapen",
"given": "Tomas"
},
{
"family": "de Jong",
"given": "Maartje C."
},
{
"family": "Dumoulin",
"given": "Serge O."
}
],
"container-title-short": "PLoS Comput Biol",
"volume": "22",
"issue": "7",
"page": "e1014434",
"DOI": "10.1371/journal.pcbi.1014434",
"PMID": "42430453",
"PMCID": "PMC13379088",
"ISSN": "1553-734X",
"publisher": "PLOS",
"URL": "https://doi.org/10.1371/journal.pcbi.1014434",
"language": "en",
"issued": {
"date-parts": [
[
2026,
7,
10
]
]
}
}

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.1162/imag.a.1210 [code]
Modeling 2D spatio-tactile population receptive fields of the fingertip in human primary somatosensory cortex.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: fMRI, systems, 7 references
[2] doi:10.1371/journal.pcbi.1013007 [code]
Traveling waves in the human visual cortex: An MEG-EEG model-based approach
Journal: n/a
In common: Matplotlib, NumPy, MEG, fMRI, systems, 5 references
[3] doi:10.21203/rs.3.rs-9940836/v1 [code]
Representational Tuning Models: Parametric Encoding Within Arbitrary Stimulus Domains
Journal: Research Square (preprint)
In common: seaborn, scikit-learn, Matplotlib, 1 other tool, fMRI, systems, 4 references
[4] doi:10.7554/elife.108408 [code]
Frequency and laminar profile of feature-specific visual activity revealed by interleaved EEG-fMRI.
Journal: eLife
In common: seaborn, Matplotlib, NumPy, fMRI, systems, 4 references
[5] 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: seaborn, scikit-learn, Matplotlib, 1 other tool, 4 references
[6] doi:10.1093/cercor/bhag075 [code]
Cortical dynamics of icon perception: effects of concreteness and attractiveness.
Journal: Cerebral cortex (New York, N.Y. : 1991)
In common: scikit-learn, Matplotlib, NumPy, MEG, 4 references
[7] doi:10.1016/j.neuroimage.2026.122051 [code]
Determining hemispheric language dominance from MEG beta-power modulations: Concordance with fMRI.
Journal: NeuroImage
In common: scikit-learn, Matplotlib, NumPy, MEG, fMRI, 3 references
[8] doi:10.1167/jov.26.8.4 [code]
The neural processes of illusory occlusion in object recognition.
Journal: Journal of vision
In common: seaborn, scikit-learn, Matplotlib, 1 other tool, 3 references
[9] doi:10.7554/elife.109640
Concurrent category-selective neural activity across the ventral occipito-temporal cortex supports a non-hierarchical view of human visual recognition.
Journal: eLife
In common: 5 references
[10] doi:10.1371/journal.pbio.3003831 [code]
Disinhibitory signaling enables flexible coding of top-down information in cortical networks.
Journal: PLoS biology
In common: NetworkX, scikit-learn, Matplotlib, 1 other tool, systems, 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.