OSCR

Interactions across hemispheres in prefrontal cortex reflect global cognitive processing.

Code ↔ Paper

20 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 20 matches · 5 of them tie a paragraph to a whole file, not to given lines: weak matches, whose lines are not tinted
  1. [1] § Methods › Behavioral task ↔ ex/ex_SimpleTaskDemo.m, lines 1–25 · score 0.80 · memory guided saccade, go cue, target flash, saccade initiation, radius, locations
  2. [2] § Methods › Behavioral task ↔ ex/ex_SimpleJoystickTaskDemo.m, lines 112–259 · score 0.74 · target flash, saccade initiation, target location, go, reward, cue
  3. [3] § Methods › Fitting pCCA-FA and pCCA to simulated data ↔ pcca_fa_mdl.py, lines 11–54 · score 0.74 · EM algorithm, cross validated, area dimensionalities, pCCA FA model, latent variables, dshared
  4. [4] § Methods › Fitting pCCA-FA ↔ pcca_fa_mdl.py, lines 519–601 · score 0.74 · fold cross validation, area dimensionalities, pCCA FA model, likelihood, integer, d1
  5. [5] § Methods › Fitting pCCA-FA ↔ pcca_fa_mdl.py, lines 11–54 · score 0.70 · expectation maximization, EM algorithm, pCCA FA model, orthogonal, fit, matrix
  6. [6] § Methods › Preprocessing of neural activity ↔ ex_bci/utils/getChannelsKeepWithDat.m, the whole file · a weak match · score 0.67 · low firing rates, Fano factor, coincident, electrodes, binned, spikes
  7. [7] § Methods › Computing signal correlation ↔ main_analyses/compute_rsc.py, lines 29–78 · score 0.66 · signal correlation, area rsc, permutation, opposite, tuning, raw
  8. [8] § Methods › Separation of slow and fast components ↔ plot_figureS1.ipynb, lines 18–67 · score 0.66 · timescale component, remove slow timescale, slow component, fast, correlation, activity
  9. [9] § Methods › Preprocessing of neural activity ↔ ex_bci/utils/preprocessDatWithNoStimTrial.m, the whole file · a weak match · score 0.66 · low firing rates, Fano factor, coincident, Preprocessing, binned, spikes
  10. [10] § Methods › Simulated data generation ↔ sim_pcca_fa.py, lines 25–167 · score 0.65 · area co fluctuation, model parameters, orthogonalized, simulated, vector, matrix
  11. [11] § Methods › Fitting pCCA-FA and pCCA to simulated data ↔ pcca_fa_mdl.py, lines 519–601 · score 0.64 · EM algorithm, cross validated, area dimensionality, pCCA FA, fit, model
  12. [12] § Methods › Preprocessing of pupil diameter ↔ main_analyses/compile_pupil_data.m, the whole file · a weak match · score 0.62 · delay period, subtracted, smoothed, pupil, noise, window
  13. [13] § Methods › Probabilistic canonical correlation analysis - factor analysis (pCCA-FA) ↔ sim_pcca_fa.py, lines 25–167 · score 0.61 · independent variance, area latent variables, area component, n1, d1, d2
  14. [14] § Methods › Preprocessing of pupil diameter ↔ ex_control/samp.m, the whole file · a weak match · score 0.59 · EyeLink, Eye position, smoothed, tracking, pupil, window
  15. [15] § Results › Across-area latent variables were related to pupil diameter ↔ main_analyses/compile_pupil_data.m, the whole file · a weak match · score 0.56 · event related, target onset, delay period, pupil
  16. [16] § Methods › Pupil diameter regression ↔ main_analyses/compute_pupil_pred_1d.py, lines 47–98 · score 0.54 · linear regression, trial pupil, r2, pCCA FA, latent, model
  17. [17] § Methods › Pupil diameter regression ↔ main_analyses/compute_pupil_pred.py, lines 47–96 · score 0.54 · linear regression, trial pupil, r2, pCCA FA, latent, model
  18. [18] § Results › pCCA-FA partitions across- and within-area shared variance ↔ plot_figure3.ipynb, lines 173–255 · score 0.52 · ground truth sv, model validation, dshared, pCCA FA, dimensionality, variance
  19. [19] § Methods › Probabilistic canonical correlation analysis - factor analysis (pCCA-FA) ↔ ex_bci/oldBci/FA_distanceBCIsystemBlock/calibrateDistanceBCIFA.m, lines 101–207 · score 0.52 · low rank, covariance matrices, correlation, FA, neurons
  20. [20] § Methods › Probabilistic canonical correlation analysis - factor analysis (pCCA-FA) ↔ ex_bci/oldBci/FA_distanceBCIsystemBlock2BCI/calibrateDistanceBCIFA.m, lines 79–195 · score 0.52 · low rank, covariance matrices, correlation, FA, neurons

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 · 984 lines · 45 KB · Apache-2.0 · 4 matches

  1. import numpy as np
  2. import cca.prob_cca as pcca
  3. import fa.factor_analysis as fa_mdl
  4. import scipy.linalg as slin
  5. import sklearn.model_selection as ms
  6. from joblib import Parallel,delayed
  7. from functools import partial
  8. from psutil import cpu_count
  9. from tqdm import tqdm
  10. class pcca_fa:
  11. '''
  12. pCCA-FA is a dimensionality reduction framework that combines probabilistic canonical correlation analysis (pCCA)
  13. and factor analysis (FA) to model across- and within- dataset interactions.
  14. This class implements the pCCA-FA model, stores parameters, and contains methods for fitting the model to data and computing model metrics.
  15. Methods
  16. -------
  17. train()
  18. Fit a pCCA-FA model to data using expectation-maximization (EM) algorithm.
  19. get_loading_matrices()
  20. Get across- and within-area loading matrices of the fit model.
  21. get_canonical_directions()
  22. Get canonical directions from the parameters of the fit model, as in canonical correlation analysis (CCA).
  23. get_correlative_modes()
  24. Transforms across-area loading matrices to their correlative modes.
  25. get_params()
  26. Get parameters of the fit model.
  27. set_params()
  28. Set parameters of the model.
  29. estep()
  30. Compute expectation of the posterior, according to the E-step of the EM algorithm.
  31. orthogonalize()
  32. Orthogonalize across- and within-area loading matrices using singular value decomposition.
  33. orthogonalize_latents()
  34. Orthogonalize latent variables (posterior means) using singular value decomposition.
  35. crossvalidate()
  36. Perform k-fold cross-validation to select hyperparameters (optimal across- and within-area dimensionality),
  37. then fit a pCCA-FA model with the selected hyperparameters.
  38. Model metric methods
  39. -------
  40. compute_load_sim()
  41. Compute loading similarity in each across- and within-area loading matrix.
  42. compute_dshared()
  43. Compute shared dimensionality (d_shared) in each across- and within-area loading matrix.
  44. compute_part_ratio()
  45. Compute part ratio in each across- and within-area loading matrix.
  46. compute_psv()
  47. Compute percentage of shared variance (%sv) in each across- and within-area loading matrix.
  48. compute_metrics()
  49. Wrapper to compute loading similarity, d_shared, part ratio, %sv, and canonical correlations.
  50. '''
  51. def __init__(self,min_var=0.01):
  52. '''
  53. Initialize pCCA-FA model class.
  54. Parameters:
  55. min_var (float): Used to set the variance floor, to prevent numerical underflow.
  56. '''
  57. self.params = []
  58. self.min_var = min_var
  59. def train(self,X_1,X_2,d,d1,d2,tol=1e-6,max_iter=int(1e6),verbose=False,rand_seed=None,warmstart=True,X_1_early_stop=None,X_2_early_stop=None,start_params=None,parallelize=True):
  60. '''
  61. Fit a pCCA-FA model to data using expectation-maximization (EM) algorithm.
  62. Parameters:
  63. X_1 (array): Array of size N (trials) x n1 (neurons), spike counts in area 1
  64. X_2 (array): Array of size N (trials) x n2 (neurons), spike counts in area 2
  65. d (int): Across-area dimensionality
  66. d1 (int): Within-area dimensionality for area 1
  67. d2 (int): Within-area dimensionality for area 2
  68. tol (float): Tolerance for convergence of the EM algorithm
  69. max_iter (int): Maximum number of iterations of the EM algorithm
  70. verbose (bool): Flag to print out updates during training
  71. rand_seed (int): Seed for random number generator, provide to ensure reproducibility
  72. warmstart (bool): Whether to initialize starting parameters of EM algorithm using pCCA and FA
  73. X_1_early_stop (array): Array of size N (trials) x n1 (neurons), test spike counts in area 1
  74. X_2_early_stop (array): Array of size N (trials) x n2 (neurons), test spike counts in area 2
  75. start_params (dict): Dictionary containing pCCA-FA model parameters to initialize EM algorithm
  76. use_process (bool): Whether to run training in a separate process for better performance
  77. Returns:
  78. LL (array): Training data log likelihood at each iteration of EM algorithm
  79. testLL (array): If using early_stop test data, contains test data log likelihood at each iteration of EM algorithm. Empty array otherwise.
  80. '''
  81. if parallelize:
  82. # Define function to run in parallel
  83. def _train_wrapper(X_1, X_2, d, d1, d2, min_var, tol, max_iter, verbose, rand_seed,
  84. warmstart, X_1_early_stop, X_2_early_stop, start_params):
  85. model = pcca_fa(min_var=min_var)
  86. LL, testLL = model.train(X_1, X_2, d, d1, d2,
  87. tol=tol, max_iter=max_iter,
  88. verbose=verbose, rand_seed=rand_seed,
  89. warmstart=warmstart,
  90. X_1_early_stop=X_1_early_stop,
  91. X_2_early_stop=X_2_early_stop,
  92. start_params=start_params,
  93. parallelize=False)
  94. return LL, testLL, model.get_params()
  95. # Run in parallel with all arguments explicitly passed
  96. result = Parallel(n_jobs=cpu_count(logical=False), backend='loky')([
  97. delayed(_train_wrapper)(X_1, X_2, d, d1, d2, self.min_var, tol, max_iter,
  98. verbose, rand_seed, warmstart, X_1_early_stop,
  99. X_2_early_stop, start_params)]
  100. )[0]
  101. LL, testLL, self.params = result
  102. return LL, testLL
  103. # Regular in-process training
  104. # set random seed
  105. if not(rand_seed is None):
  106. np.random.seed(rand_seed)
  107. early_stop = not(X_1_early_stop is None) and not(X_2_early_stop is None)
  108. # set some useful parameters
  109. N,n1 = X_1.shape
  110. _,n2 = X_2.shape
  111. mu_x1,mu_x2 = X_1.mean(axis=0),X_2.mean(axis=0)
  112. X_1c,X_2c = (X_1-mu_x1), (X_2-mu_x2)
  113. X_total = np.concatenate((X_1c,X_2c),axis=1)
  114. covX_1 = 1/N * (X_1c.T).dot(X_1c)
  115. covX_2 = 1/N * (X_2c.T).dot(X_2c)
  116. sampleCov = 1/N * (X_total.T).dot(X_total)
  117. var_floor = self.min_var*np.diag(sampleCov)
  118. Iz = np.identity(d+d1+d2)
  119. const = (n1+n2)*np.log(2*np.pi)
  120. if early_stop:
  121. X_1c_test,X_2c_test = X_1_early_stop - mu_x1, X_2_early_stop - mu_x2
  122. X_total_test = np.concatenate((X_1c_test,X_2c_test),axis=1)
  123. cov_test = (1/N)*(X_total_test.T.dot(X_total_test))
  124. # check that covariance is full rank
  125. if np.linalg.matrix_rank(sampleCov)==(n1+n2):
  126. x1_scale = np.exp(2/n1*np.sum(np.log(np.diag(slin.cholesky(covX_1)))))
  127. x2_scale = np.exp(2/n2*np.sum(np.log(np.diag(slin.cholesky(covX_2)))))
  128. else:
  129. raise np.linalg.LinAlgError(f'Covariance matrix is low rank ({np.linalg.matrix_rank(sampleCov):d}, should be {n1+n2:d})')
  130. # initialize parameters
  131. if warmstart:
  132. # get across-area loading matrices from pCCA and within-area loading matrices from FA
  133. tmp = pcca.prob_cca()
  134. tmp.train_maxLL(X_1,X_2,d)
  135. W_1 = tmp.get_params()['W_x']
  136. W_2 = tmp.get_params()['W_y']
  137. tmp = fa_mdl.factor_analysis()
  138. tmp.train(X_1,d1,rand_seed=rand_seed)
  139. L_1 = tmp.get_params()['L']
  140. tmp = fa_mdl.factor_analysis()
  141. tmp.train(X_2,d2,rand_seed=rand_seed)
  142. L_2 = tmp.get_params()['L']
  143. Psi = np.diag(sampleCov)
  144. elif not(start_params is None):
  145. # allow for specifying parameter initialization
  146. W_1 = start_params['W_1']
  147. W_2 = start_params['W_2']
  148. L_1 = start_params['L_1']
  149. L_2 = start_params['L_2']
  150. Psi = np.abs(np.append(start_params['psi_1'], start_params['psi_2']))
  151. else:
  152. if d > 0:
  153. W_1 = np.random.randn(n1,d) * np.sqrt(x1_scale/d)
  154. W_2 = np.random.randn(n2,d) * np.sqrt(x2_scale/d)
  155. else:
  156. W_1 = np.random.randn(n1,d)
  157. W_2 = np.random.randn(n2,d)
  158. if d1 > 0:
  159. L_1 = np.random.randn(n1,d1) * np.sqrt(x1_scale/d1)
  160. else:
  161. L_1 = np.random.randn(n1,d1)
  162. if d2 > 0:
  163. L_2 = np.random.randn(n2,d2) * np.sqrt(x2_scale/d2)
  164. else:
  165. L_2 = np.random.randn(n2,d2)
  166. Psi = np.diag(sampleCov)
  167. # define L_total - joint loading matrix
  168. L_top = np.concatenate((W_1,L_1,np.zeros((n1,d2))),axis=1)
  169. L_bottom = np.concatenate((W_2,np.zeros((n2,d1)),L_2),axis=1)
  170. L_total = np.concatenate((L_top,L_bottom),axis=0)
  171. L_mask = np.ones(L_total.shape)
  172. L_mask[:n1,(d+d1):] = np.zeros((n1,d2))
  173. L_mask[n1:,d:(d+d1)] = np.zeros((n2,d1))
  174. # EM algorithm
  175. LL = []
  176. testLL = []
  177. for i in range(max_iter):
  178. # E-step: set q(z) = p(z,zx,zy|x,y)
  179. iPsi = np.diag(1/Psi)
  180. iPsiL = iPsi.dot(L_total)
  181. if d==0 and d1==0 and d2==0:
  182. iSig = iPsi
  183. else:
  184. iSig = iPsi - iPsiL.dot(slin.inv(Iz+(L_total.T).dot(iPsiL))).dot(iPsiL.T)
  185. iSigL = iSig.dot(L_total)
  186. cov_iSigL = sampleCov.dot(iSigL)
  187. E_zz = Iz - (L_total.T).dot(iSigL) + (iSigL.T).dot(cov_iSigL)
  188. # compute log likelihood
  189. logDet = 2*np.sum(np.log(np.diag(slin.cholesky(iSig))))
  190. curr_LL = -N/2 * (const - logDet + np.trace(iSig.dot(sampleCov)))
  191. LL.append(curr_LL)
  192. if early_stop:
  193. curr_testLL = -N/2 * (const - logDet + np.trace(iSig.dot(cov_test)))
  194. testLL.append(curr_testLL)
  195. if verbose:
  196. print('EM iteration ',i,', LL={:.2f}'.format(curr_LL))
  197. # check for convergence (training LL increases by less than tol, or testLL decreases)
  198. if i>1:
  199. if (LL[-1]-LL[-2])<tol or (early_stop and testLL[-1]<testLL[-2]):
  200. break
  201. # M-step: compute new L and Psi
  202. if not(d==0 and d1==0 and d2==0):
  203. L_total = cov_iSigL.dot(slin.inv(E_zz))
  204. L_total = L_total * L_mask
  205. Psi = np.diag(sampleCov) - np.diag(cov_iSigL.dot(L_total.T))
  206. Psi = np.maximum(Psi,var_floor)
  207. # get final parameters after convergence or max_iter
  208. W_1, W_2 = L_total[:n1,:d], L_total[n1:,:d]
  209. L_1, L_2 = L_total[:n1,d:(d+d1)], L_total[n1:,(d+d1):]
  210. psi_1, psi_2 = Psi[:n1], Psi[n1:]
  211. # create parameter dict
  212. self.params = {
  213. 'mu_x1':mu_x1,'mu_x2':mu_x2, # estimated mean per neuron
  214. 'L_total':L_total, # maximum likelihood estimated matrix
  215. 'W_1':W_1,'W_2':W_2, # across-area loading matrices
  216. 'L_1':L_1,'L_2':L_2, # within-area loading matrices
  217. 'psi_1':psi_1,'psi_2':psi_2, # private variance per neuron
  218. 'd':d,'d1':d1,'d2':d2, # selected dimensionalities
  219. }
  220. return np.array(LL), np.array(testLL)
  221. def get_loading_matrices(self):
  222. '''
  223. Get across- and within-area loading matrices of the fit model.
  224. Returns:
  225. W_1 (array): Array of size n1 (neurons) x d (latents) containing the loadings for across-area latent variables onto neurons in area 1
  226. W_2 (array): Array of size n2 (neurons) x d (latents) containing the loadings for across-area latent variables onto neurons in area 2
  227. L_1 (array): Array of size n1 (neurons) x d1 (latents) containing the loadings for within-area latent variables onto neurons in area 1
  228. L_2 (array): Array of size n2 (neurons) x d2 (latents) containing the loadings for within-area latent variables onto neurons in area 2
  229. '''
  230. n1 = len(self.params['mu_x1'])
  231. d, d1 = self.params['d'], self.params['d1']
  232. L_total = self.params['L_total']
  233. # get final parameters
  234. W_1, W_2 = L_total[:n1,:d], L_total[n1:,:d]
  235. L_1, L_2 = L_total[:n1,d:(d+d1)], L_total[n1:,(d+d1):]
  236. return W_1, W_2, L_1, L_2
  237. def get_canonical_directions(self):
  238. '''
  239. Get canonical directions from the parameters of the fit model, as in canonical correlation analysis (CCA).
  240. Returns:
  241. canonical_dirs_x (array): Array of size n1 (neurons) x d (latents) whose columns contain the canonical directions for area 1
  242. canonical_dirs_y (array): Array of size n2 (neurons) x d (latents) whose columns contain the canonical directions for area 2
  243. rho (array): Array of size d (latents) x 1 containing the corresponding canonical correlations
  244. '''
  245. W_1, W_2, L_1, L_2 = self.get_loading_matrices()
  246. psi_1, psi_2 = self.params['psi_1'], self.params['psi_2']
  247. d = self.params['d']
  248. # compute canonical correlations
  249. est_covX_1 = W_1.dot(W_1.T) + L_1.dot(L_1.T) + np.diag(psi_1)
  250. est_covX_2 = W_2.dot(W_2.T) + L_2.dot(L_2.T) + np.diag(psi_2)
  251. est_covX_1X_2 = W_1.dot(W_2.T)
  252. inv_sqrt_covX_1 = slin.inv(slin.sqrtm(est_covX_1))
  253. inv_sqrt_covX_2 = slin.inv(slin.sqrtm(est_covX_2))
  254. K = inv_sqrt_covX_1.dot(est_covX_1X_2).dot(inv_sqrt_covX_2)
  255. u,s,vt = slin.svd(K)
  256. rho = s[0:d]
  257. canonical_dirs_x = slin.inv(slin.sqrtm(est_covX_1)) @ u[:,:d]
  258. canonical_dirs_y = slin.inv(slin.sqrtm(est_covX_2)) @ vt[:d,:].T
  259. return (canonical_dirs_x, canonical_dirs_y), rho
  260. def get_correlative_modes(self):
  261. '''
  262. Transforms across-area loading matrices to their correlative modes.
  263. Follows equations in Bach & Jordan, 2005.
  264. Returns:
  265. CorrModes_x (array): Array of size n1 (neurons) x d (latents) whose columns contain the correlative modes for area 1
  266. CorrModes_y (array): Array of size n2 (neurons) x d (latents) whose columns contain the correlative modes for area 2
  267. '''
  268. W_1, W_2, L_1, L_2 = self.get_loading_matrices()
  269. psi_1, psi_2 = self.params['psi_1'], self.params['psi_2']
  270. d = self.params['d']
  271. # compute canonical correlations
  272. est_covX_1 = W_1.dot(W_1.T) + L_1.dot(L_1.T) + np.diag(psi_1)
  273. est_covX_2 = W_2.dot(W_2.T) + L_2.dot(L_2.T) + np.diag(psi_2)
  274. est_covX_1X_2 = W_1.dot(W_2.T)
  275. inv_sqrt_covX_1 = slin.inv(slin.sqrtm(est_covX_1))
  276. inv_sqrt_covX_2 = slin.inv(slin.sqrtm(est_covX_2))
  277. K = inv_sqrt_covX_1.dot(est_covX_1X_2).dot(inv_sqrt_covX_2)
  278. u,s,vt = slin.svd(K)
  279. rho = s[0:d]
  280. # order W_1, W_2 by canon corrs
  281. pd = np.diag(np.sqrt(rho))
  282. CorrModes_x = slin.sqrtm(est_covX_1).dot(u[:,0:d]).dot(pd)
  283. CorrModes_y = slin.sqrtm(est_covX_2).dot(vt[0:d,:].T).dot(pd)
  284. return CorrModes_x, CorrModes_y
  285. def get_params(self):
  286. '''
  287. Get parameters of the fit model.
  288. Returns:
  289. params (dict): Dictionary containing each parameter of the pCCA-FA model
  290. '''
  291. return self.params
  292. def set_params(self,params):
  293. '''
  294. Set parameters of the model.
  295. Parameters:
  296. params (dict): Dictionary containing each parameter of the pCCA-FA model
  297. '''
  298. self.params = params
  299. def estep(self,X_1,X_2):
  300. '''
  301. Compute expectation of the posterior, according to the E-step of the EM algorithm.
  302. Parameters:
  303. X_1 (array): Array of size N (trials) x n1 (neurons), spike counts in area 1
  304. X_2 (array): Array of size N (trials) x n2 (neurons), spike counts in area 2
  305. Returns:
  306. z (dict): Dictionary containing the mean and covariance of the posterior
  307. LL (float): Log likelihood of the provided spike counts X_1 and X_2 under the fit model parameters
  308. '''
  309. N,n1 = X_1.shape
  310. _,n2 = X_2.shape
  311. d,d1,d2 = self.params['d'],self.params['d1'],self.params['d2']
  312. # get model parameters
  313. mu_x1,mu_x2 = self.params['mu_x1'],self.params['mu_x2']
  314. L_total = self.params['L_total']
  315. psi_1 = self.params['psi_1']
  316. psi_2 = self.params['psi_2']
  317. Psi = np.diag(np.concatenate((psi_1,psi_2)))
  318. # center data and compute covariances
  319. X_1c = X_1-mu_x1
  320. X_2c = X_2-mu_x2
  321. X_total = np.concatenate((X_1c,X_2c),axis=1)
  322. sampleCov = 1/N * (X_total.T).dot(X_total)
  323. # compute z
  324. Iz = np.identity(d+d1+d2)
  325. C = L_total.dot(L_total.T) + Psi
  326. invC = slin.inv(C)
  327. z_mu = X_total.dot(invC).dot(L_total)
  328. z_cov = np.diag(np.diag(Iz - (L_total.T).dot(invC).dot(L_total)))
  329. # compute LL
  330. const = (n1+n2)*np.log(2*np.pi)
  331. logDet = 2*np.sum(np.log(np.diag(slin.cholesky(C))))
  332. LL = -N/2 * (const + logDet + np.trace(invC.dot(sampleCov)))
  333. # return posterior and LL
  334. z = {
  335. 'z_mu':z_mu[:,:d],
  336. 'z_cov':z_cov[:d,:d],
  337. 'zx1_mu':z_mu[:,d:(d+d1)],
  338. 'zx1_cov':z_cov[d:(d+d1),d:(d+d1)],
  339. 'zx2_mu':z_mu[:,(d+d1):],
  340. 'zx2_cov':z_cov[(d+d1):,(d+d1):],
  341. }
  342. return z, LL
  343. def orthogonalize(self,across_mode='paired'):
  344. '''
  345. Orthogonalize across- and within-area loading matrices using singular value decomposition.
  346. Note: this also transforms loading matrices to be in covariant modes (as opposed to correlative modes)
  347. Parameters:
  348. across_mode (str): Parameter to indicate whether to orthogonalize the across-area loading matrices jointly ('paired') or individually in each area ('unpaired')
  349. Returns:
  350. W_1_norm (array): Array of size n1 (neurons) x d (latents) containing orthogonal columns with the loadings for across-area latent variables onto neurons in area 1
  351. W_2_norm (array): Array of size n2 (neurons) x d (latents) containing orthogonal columns the loadings for across-area latent variables onto neurons in area 2
  352. L_1_norm (array): Array of size n1 (neurons) x d1 (latents) containing orthogonal columns the loadings for within-area latent variables onto neurons in area 1
  353. L_2_norm (array): Array of size n2 (neurons) x d2 (latents) containing orthogonal columns the loadings for within-area latent variables onto neurons in area 2
  354. '''
  355. n1 = len(self.params['mu_x1'])
  356. W_1, W_2, L_1, L_2 = self.get_loading_matrices() # output from maximum likelihood estimation
  357. # within-area loading matrices
  358. u,s,_ = slin.svd(L_1,full_matrices=False)
  359. L_1_norm = u @ np.diag(s)
  360. u,s,_ = slin.svd(L_2,full_matrices=False)
  361. L_2_norm = u @ np.diag(s)
  362. # across-area loading matrices
  363. if across_mode == 'paired':
  364. W_total = np.concatenate((W_1,W_2),axis=0)
  365. u,s,_ = slin.svd(W_total,full_matrices=False)
  366. W_1_norm = u[:n1,:] @ np.diag(s)
  367. W_2_norm = u[n1:,:] @ np.diag(s)
  368. elif across_mode == 'unpaired':
  369. u,s,_ = slin.svd(W_1,full_matrices=False)
  370. W_1_norm = u @ np.diag(s)
  371. u,s,_ = slin.svd(W_2,full_matrices=False)
  372. W_2_norm = u @ np.diag(s)
  373. else:
  374. raise ValueError('across-mode must be "paired" or "unpaired"')
  375. return W_1_norm, W_2_norm, L_1_norm, L_2_norm
  376. def orthogonalize_latents(self,zx_mu,zy_mu,do_across=False,z_mu=None,across_mode='paired'):
  377. '''
  378. Orthogonalize latent variables (posterior means) using singular value decomposition.
  379. Parameters:
  380. zx_mu (array): Array of size N (trials) x d1 (latents) containing the within-area latent variables or posterior mean in area 1
  381. zy_mu (array): Array of size N (trials) x d2 (latents) containing the within-area latent variables or posterior mean in area 2
  382. do_across (bool): Whether to orthogonalize the across-area latent variables (True) or not (False)
  383. z_mu (array): Array of size N (trials) x d (latents) containing the across-area latent variables or posterior mean. Only used if do_across is True
  384. across_mode (str): Parameter to indicate whether to orthogonalize the across-area latent variables jointly ('paired') or individually in each area ('unpaired'). Only used if do_across is True
  385. Returns:
  386. z_orth (dict): Dictionary containing the orthogonalized latent variables
  387. W_orth (dict): Dictionary containing the orthogonalized loading matrices
  388. '''
  389. W_1, W_2, L_1, L_2 = self.get_loading_matrices() # output from maximum likelihood estimation
  390. n1 = L_1.shape[0]
  391. # orthogonalize zx
  392. u,s,vt = slin.svd(L_1,full_matrices=False)
  393. L_1_orth = u
  394. TT = np.diag(s).dot(vt)
  395. z_x1 = (TT.dot(zx_mu.T)).T
  396. # orthogonalize zy
  397. u,s,vt = slin.svd(L_2,full_matrices=False)
  398. L_2_orth = u
  399. TT = np.diag(s).dot(vt)
  400. z_x2 = (TT.dot(zy_mu.T)).T
  401. # orthogonalize across-area
  402. across_z_orth = {}
  403. if do_across:
  404. if across_mode == 'paired':
  405. # orthogonalize across-area latents using both area's loading matrix
  406. W_total = np.concatenate((W_1,W_2),axis=0)
  407. u,s,vt = slin.svd(W_total,full_matrices=False)
  408. W_1_orth = u[:n1,:]
  409. W_2_orth = u[n1:,:]
  410. TT = np.diag(s).dot(vt)
  411. z = (TT.dot(z_mu.T)).T
  412. across_z_orth['area1'] = z
  413. across_z_orth['area2'] = z
  414. elif across_mode == 'unpaired':
  415. # orthogonalize across-area latents using each area's loading matrix
  416. u,s,vt = slin.svd(W_1,full_matrices=False)
  417. W_1_orth = u
  418. TT = np.diag(s).dot(vt)
  419. z = (TT.dot(z_mu.T)).T
  420. across_z_orth['area1'] = z
  421. u,s,vt = slin.svd(W_2,full_matrices=False)
  422. W_2_orth = u
  423. TT = np.diag(s).dot(vt)
  424. z = (TT.dot(z_mu.T)).T
  425. across_z_orth['area2'] = z
  426. else:
  427. raise ValueError('across-mode must be "paired" or "unpaired"')
  428. # return z_orth, W_orth
  429. z_orth = {
  430. 'z':across_z_orth, # across area latent variables, empty if do_across is False
  431. 'z1':z_x1, # within-area latent variables for area 1
  432. 'z2':z_x2 # within-area latent variables for area 2
  433. }
  434. W_orth = {
  435. 'W_1':W_1_orth, # across-area loading matrix for area 1
  436. 'W_2':W_2_orth, # across-area loading matrix for area 2
  437. 'L_1':L_1_orth, # within-area loading matrix for area 1
  438. 'L_2':L_2_orth # within-area loading matrix for area 2
  439. }
  440. return z_orth, W_orth
  441. def crossvalidate(self,X_1,X_2,d_list=np.linspace(0,8,9),d1_list=np.linspace(0,8,9),d2_list=np.linspace(0,8,9),n_folds=10,verbose=True,max_iter=int(1e6),tol=1e-6,warmstart=True,rand_seed=None,parallelize=False,early_stop=False):
  442. '''
  443. Perform k-fold cross-validation to select hyperparameters (optimal across- and within-area dimensionality), then fit a pCCA-FA model with the selected hyperparameters.
  444. Parameters:
  445. X_1 (array): Array of size N (trials) x n1 (neurons), spike counts in area 1
  446. X_2 (array): Array of size N (trials) x n2 (neurons), spike counts in area 2
  447. d_list (array): 1-dimensional array containing the across-area dimensionalities to test
  448. d1_list (array): 1-dimensional array containing the within-area dimensionalities to test for area 1
  449. d2_list (array): 1-dimensional array containing the within-area dimensionalities to test for area 2
  450. n_folds (int): The number of folds (k) for cross-validation
  451. verbose (bool): Flag to print out updates during training
  452. max_iter (int): Maximum number of iterations of the EM algorithm
  453. tol (float): Tolerance for convergence of the EM algorithm
  454. warmstart (bool): Whether to initialize starting parameters of EM algorithm using pCCA and FA
  455. rand_seed (int): Seed for random number generator, provide to ensure reproducibility
  456. parallelize (bool): Whether to parallelize cross-validation folds (True) or not (False), to reduce run time
  457. early_stop (bool): Whether to use early_stop (True) or not (False) on the testing data of each cross-validation fold
  458. Returns:
  459. results (dict): Dictionary containing the lists of tested dimensionalities and their corresponding cross-validated data log likelihood and prediction errors,
  460. as well as the selected dimensionalities and its corresponding cross-validated log likelihood
  461. '''
  462. # set random seed
  463. if not(rand_seed is None):
  464. np.random.seed(rand_seed)
  465. # make sure z dims are integers
  466. d_list,d1_list,d2_list = np.meshgrid(d_list.astype(int),d1_list.astype(int),d2_list.astype(int))
  467. d_list = np.matrix.flatten(d_list)
  468. d1_list = np.matrix.flatten(d1_list)
  469. d2_list = np.matrix.flatten(d2_list)
  470. results = {'d_list':d_list,'d1_list':d1_list,'d2_list':d2_list}
  471. # create k-fold iterator
  472. if verbose:
  473. print('Crossvalidating pCCA-FA model to choose # of dims...')
  474. cv_kfold = ms.KFold(n_splits=n_folds,shuffle=True,random_state=rand_seed)
  475. # iterate through train/test splits
  476. i = 0
  477. LLs,PEs = np.zeros([n_folds,len(d_list)]),np.zeros([n_folds,len(d_list)])
  478. for train_idx,test_idx in cv_kfold.split(X_1):
  479. if verbose:
  480. print(' Fold ',i+1,' of ',n_folds,'...')
  481. X_1_train,X_1_test = X_1[train_idx], X_1[test_idx]
  482. X_2_train,X_2_test = X_2[train_idx], X_2[test_idx]
  483. # iterate through each d, provide training and testing trials to the helper function
  484. func = partial(self._cv_helper,X_1train=X_1_train,X_2train=X_2_train,X_1test=X_1_test,X_2test=X_2_test,\
  485. rand_seed=rand_seed,max_iter=max_iter,tol=tol,warmstart=warmstart,early_stop=early_stop)
  486. if parallelize:
  487. tmp = Parallel(n_jobs=cpu_count(logical=False),backend='loky')\
  488. (delayed(func)(d_list[j],d1_list[j],d2_list[j]) for j in range(len(d_list)))
  489. LLs[i,:] = [val[0] for val in tmp]
  490. PEs[i,:] = [val[1] for val in tmp]
  491. else:
  492. for j in tqdm(range(len(d_list))):
  493. tmp = func(d_list[j],d1_list[j],d2_list[j])
  494. LLs[i,j],PEs[i,j] = tmp[0],tmp[1]
  495. i = i+1
  496. sum_LLs = LLs.sum(axis=0)
  497. sum_SEs = PEs.sum(axis=0)
  498. results['LLs'] = sum_LLs
  499. results['PEs'] = sum_SEs
  500. # find the best # of z dimensions and train final pCCA-FA model
  501. max_idx = np.argmax(sum_LLs)
  502. d,d1,d2 = d_list[max_idx],d1_list[max_idx],d2_list[max_idx]
  503. results['d']=d
  504. results['d1']=d1
  505. results['d2']=d2
  506. results['final_LL'] = sum_LLs[max_idx]
  507. self.train(X_1,X_2,d,d1,d2) # sets params of the final model
  508. self.compute_cv_canonical_corrs(X_1,X_2,n_folds=n_folds,verbose=verbose,max_iter=max_iter,tol=tol,warmstart=warmstart,rand_seed=rand_seed)
  509. self.cv_results = results
  510. return results
  511. def _cv_helper(self,d,d1,d2,X_1train,X_2train,X_1test,X_2test,rand_seed=None,max_iter=int(1e5),tol=1e-6,warmstart=True,early_stop=False):
  512. '''
  513. Helper function for crossvalidate().
  514. Runs one train-test split and computes the log-likelihood and prediction error on the testing data.
  515. Parameters:
  516. d (int): Across-area dimensionality
  517. d1 (int): Within-area dimensionality for area 1
  518. d2 (int): Across-area dimensionality for area 2
  519. X_1train (array): Array of size Ntrain (trials) x n1 (neurons), training spike counts in area 1
  520. X_2train (array): Array of size Ntrain (trials) x n2 (neurons), training spike counts in area 2
  521. X_1test (array): Array of size Ntest (trials) x n1 (neurons), testing spike counts in area 1
  522. X_2test (array): Array of size Ntest (trials) x n2 (neurons), testing spike counts in area 2
  523. rand_seed (int): Seed for random number generator, provide to ensure reproducibility
  524. max_iter (int): Maximum number of iterations of the EM algorithm
  525. tol (float): Tolerance for convergence of the EM algorithm
  526. warmstart (bool): Whether to initialize starting parameters of EM algorithm using pCCA and FA
  527. early_stop (bool): Whether to use early_stop (True) or not (False) on the testing data
  528. Returns:
  529. LL (float): Cross-validated data log likelihood of the testing data
  530. PE (float): Prediction error of the testing data using leave-one-out prediction
  531. '''
  532. tmp = pcca_fa()
  533. if early_stop:
  534. tmp.train(X_1train,X_2train,d,d1,d2,rand_seed=rand_seed,max_iter=max_iter,tol=tol,warmstart=warmstart,X_1_early_stop=X_1test,X_2_early_stop=X_2test,parallelize=False)
  535. else:
  536. tmp.train(X_1train,X_2train,d,d1,d2,rand_seed=rand_seed,max_iter=max_iter,tol=tol,warmstart=warmstart,parallelize=False)
  537. # log-likelihood
  538. _,LL = tmp.estep(X_1test,X_2test)
  539. # prediction error
  540. X_1test_pred,X_2test_pred = tmp._leaveoneout_pred(X_1test,X_2test)
  541. PE = np.sum(np.square(X_1test_pred - X_1test)) + np.sum(np.square(X_2test_pred - X_2test))
  542. return (LL,PE)
  543. def _leaveoneout_pred(self,X_1,X_2):
  544. '''
  545. Helper function for crossvalidate().
  546. Runs leave-one-out prediction on provided data.
  547. Parameters:
  548. X_1 (array): Array of size N (trials) x n1 (neurons), spike counts in area 1
  549. X_2 (array): Array of size N (trials) x n2 (neurons), spike counts in area 2
  550. Returns:
  551. pred_x (array): Array of size N (trials) x n1 (neurons) containing the prediction errors for area 1
  552. pred_y (array): Array of size N (trials) x n2 (neurons) containing the prediction errors for area 2
  553. '''
  554. N,n1 = X_1.shape # trials x neurons
  555. n2 = X_2.shape[1]
  556. X_total = np.concatenate((X_1,X_2),axis=1)
  557. # extract model parameters
  558. Psi = np.concatenate((self.params['psi_1'],self.params['psi_2']),axis=0)
  559. mu = np.concatenate((self.params['mu_x1'],self.params['mu_x2']),axis=0)
  560. L_total = self.params['L_total']
  561. # compute covariances
  562. mdl_cov = L_total.dot(L_total.T) + np.diag(Psi)
  563. inv_cov = slin.inv(mdl_cov)
  564. # compute conditional expectations (predictions)
  565. n_total = n1+n2
  566. pred_total = np.zeros((N,n_total))
  567. for i in range(n_total):
  568. E = np.delete(np.delete(inv_cov,i,axis=0),i,axis=1)
  569. f = np.delete(inv_cov[:,i],i,axis=0)
  570. h = inv_cov[i,i]
  571. inv_term = E - (1/h)*np.outer(f,f)
  572. proj_term = np.delete(mdl_cov[i,:],i) # 1 x n_total-1
  573. mean_term = np.delete(X_total,i,axis=1) - np.delete(mu,i,axis=0).T # N x n_total-1
  574. pred = mu[i] + proj_term.dot(inv_term.dot(mean_term.T)) # 1 x N
  575. pred_total[:,i] = pred.T
  576. pred_x = pred_total[:,:n1] # predictions for neurons in area 1
  577. pred_y = pred_total[:,n1:] # predictions for neurons in area 2
  578. return pred_x,pred_y
  579. def compute_cv_canonical_corrs(self,X_1,X_2,n_folds=10,verbose=False,max_iter=int(1e5),tol=1e-6,warmstart=True,rand_seed=None):
  580. '''
  581. Get cross-validated canonical correlations from the fit model.
  582. Parameters:
  583. Parameters:
  584. X_1 (array): Array of size N (trials) x n1 (neurons), spike counts in area 1
  585. X_2 (array): Array of size N (trials) x n2 (neurons), spike counts in area 2
  586. n_folds (int): The number of folds (k) for cross-validation
  587. verbose (bool): Flag to print out updates during training
  588. max_iter (int): Maximum number of iterations of the EM algorithm
  589. tol (float): Tolerance for convergence of the EM algorithm
  590. warmstart (bool): Whether to initialize starting parameters of EM algorithm using pCCA and FA
  591. rand_seed (int): Seed for random number generator, provide to ensure reproducibility
  592. Returns:
  593. cv_rho (array): Array of size d (latents) x 1 containing the cross-validated canonical correlations
  594. '''
  595. # set random seed
  596. if not(rand_seed is None):
  597. np.random.seed(rand_seed)
  598. # check if model has been fit
  599. if not self.params:
  600. raise ValueError('Model must be fit before computing cross-validated canonical correlations. Run train() or crossvalidate() first.')
  601. # cross-validate to get cross-validated canonical correlations
  602. if verbose:
  603. print('Crossvalidating pCCA-FA model to compute canon corrs...')
  604. # set up needed parameters
  605. d,d1,d2 = self.params['d'],self.params['d1'],self.params['d2']
  606. N = X_1.shape[0]
  607. cv_kfold = ms.KFold(n_splits=n_folds,shuffle=True,random_state=rand_seed)
  608. zx1,zx2 = np.zeros((2,N,d))
  609. i=0
  610. for train_idx,test_idx in cv_kfold.split(X_1):
  611. if verbose:
  612. print(' Fold ',i+1,' of ',n_folds,'...')
  613. X_1_train,X_1_test = X_1[train_idx], X_1[test_idx]
  614. X_2_train,X_2_test = X_2[train_idx], X_2[test_idx]
  615. tmp = pcca_fa()
  616. tmp.train(X_1_train,X_2_train,d,d1,d2,rand_seed=rand_seed,max_iter=max_iter,tol=tol,warmstart=warmstart)
  617. W_1,W_2,L_1,L_2 = tmp.get_loading_matrices() # take direct EM outputs to compute E-step
  618. tmp_params = tmp.get_params()
  619. # compute pCCA E-step: E[z|x] and E[z|y]
  620. X_1c = X_1_test - tmp_params['mu_x1']
  621. Cx1 = W_1 @ W_1.T + (L_1 @ L_1.T + np.diag(tmp_params['psi_1']))
  622. invCx1 = slin.inv(Cx1)
  623. zx1_mu = X_1c.dot(invCx1).dot(W_1)
  624. X_2c = X_2_test - tmp_params['mu_x2']
  625. Cx2 = W_2 @ W_2.T + (L_2 @ L_2.T + np.diag(tmp_params['psi_2']))
  626. invCx2 = slin.inv(Cx2)
  627. zx2_mu = X_2c.dot(invCx2).dot(W_2)
  628. zx1[test_idx,:] = zx1_mu
  629. zx2[test_idx,:] = zx2_mu
  630. i+=1
  631. cv_rho = np.zeros(d)
  632. for i in range(d):
  633. tmp = np.corrcoef(zx1[:,i],zx2[:,i])
  634. cv_rho[i] = tmp[0,1]
  635. self.params['cv_rho'] = cv_rho
  636. return cv_rho
  637. def compute_load_sim(self):
  638. '''
  639. Compute loading similarity in each across- and within-area loading matrix.
  640. Returns:
  641. ls (dict): Dictionary containing the loading similarity for each across- and within-area loading matrix
  642. '''
  643. n1 = self.params['W_1'].shape[0]
  644. n2 = self.params['W_2'].shape[0]
  645. # first, orthonormalize each loading matrix
  646. W_1,_,_ = slin.svd(self.params['W_1'],full_matrices=False)
  647. W_2,_,_ = slin.svd(self.params['W_2'],full_matrices=False)
  648. L_1,_,_ = slin.svd(self.params['L_1'],full_matrices=False)
  649. L_2,_,_ = slin.svd(self.params['L_2'],full_matrices=False)
  650. # calculate loading similarity - following equation in Umakantha, Morina, Cowley, et al., 2021.
  651. ls_W_1 = 1 - n1*W_1.var(axis=0,ddof=0)
  652. ls_W_2 = 1 - n2*W_2.var(axis=0,ddof=0)
  653. ls_L_1 = 1 - n1*L_1.var(axis=0,ddof=0)
  654. ls_L_2 = 1 - n2*L_2.var(axis=0,ddof=0)
  655. ls = {
  656. 'ls_W_1':ls_W_1, # across-area loading similarity for area 1, involves W_1
  657. 'ls_W_2':ls_W_2, # across-area loading similarity for area 2, involves W_2
  658. 'ls_L_1':ls_L_1, # within-area loading similarity for area 1, involves L_1
  659. 'ls_L_2':ls_L_2 # within-area loading similarity for area 2, involves L_2
  660. }
  661. return ls
  662. def compute_dshared(self,cutoff_thresh=0.95):
  663. '''
  664. Compute shared dimensionality (d_shared) in each across- and within-area loading matrix.
  665. Parameters:
  666. cutoff_thresh (float): Cutoff percentage (0-1) of across- or within-area shared variance to explain for selecting d_shared
  667. Returns:
  668. dshared (dict): Dictionary containing the across- and within-area d_shared for each area
  669. '''
  670. W_1,W_2,L_1,L_2 = self.get_loading_matrices()
  671. # for across-area
  672. if self.params['d'] > 0:
  673. # area 1
  674. shared = W_1.dot(W_1.T)
  675. s = slin.svdvals(shared) # eigenvalues of WWT
  676. var_exp = np.cumsum(s)/np.sum(s)
  677. dims = np.where(var_exp >= (cutoff_thresh - 1e-9))[0]
  678. dshared_W_1 = dims[0]+1
  679. # area 2
  680. shared = W_2.dot(W_2.T)
  681. s = slin.svdvals(shared) # eigenvalues of WWT
  682. var_exp = np.cumsum(s)/np.sum(s)
  683. dims = np.where(var_exp >= (cutoff_thresh - 1e-9))[0]
  684. dshared_W_2 = dims[0]+1
  685. # overall
  686. W_total = np.concatenate((W_1,W_2),axis=0)
  687. shared = W_total.dot(W_total.T)
  688. s = slin.svdvals(shared) # eigenvalues of WWT
  689. var_exp = np.cumsum(s)/np.sum(s)
  690. dims = np.where(var_exp >= (cutoff_thresh - 1e-9))[0]
  691. dshared_W_total = dims[0]+1
  692. else:
  693. dshared_W_1 = 0
  694. dshared_W_2 = 0
  695. dshared_W_total = 0
  696. # for within area 1
  697. if self.params['d1'] > 0:
  698. shared = L_1.dot(L_1.T)
  699. s = slin.svdvals(shared) # eigenvalues of LLT
  700. var_exp = np.cumsum(s)/np.sum(s)
  701. dims = np.where(var_exp >= (cutoff_thresh - 1e-9))[0]
  702. dshared_L_1 = dims[0]+1
  703. else:
  704. dshared_L_1 = 0
  705. # for within area 2
  706. if self.params['d2'] > 0:
  707. shared = L_2.dot(L_2.T)
  708. s = slin.svdvals(shared) # eigenvalues of LLT
  709. var_exp = np.cumsum(s)/np.sum(s)
  710. dims = np.where(var_exp >= (cutoff_thresh - 1e-9))[0]
  711. dshared_L_2 = dims[0]+1
  712. else:
  713. dshared_L_2 = 0
  714. dshared = {
  715. 'dshared_W_1':dshared_W_1, # d_shared for across-area shared variance in area 1, involves W_1
  716. 'dshared_W_2':dshared_W_2, # d_shared for across-area shared variance in area 2, involves W_2
  717. 'dshared_L_1':dshared_L_1, # d_shared for within-area shared variance in area 1, involves L_1
  718. 'dshared_L_2':dshared_L_2, # d_shared for within-area shared variance in area 2, involves L_2
  719. 'dshared_W_total':dshared_W_total # d_shared for across-area shared variance jointly for area 1 and 2, involves W_1 and W_2
  720. }
  721. return dshared
  722. def compute_part_ratio(self):
  723. '''
  724. Compute part ratio in each across- and within-area loading matrix.
  725. Returns:
  726. pr (dict): Dictionary containing the part ratio for each across- and within-area loading matrix
  727. '''
  728. W_1,W_2,L_1,L_2 = self.get_loading_matrices()
  729. # for across-area
  730. shared = W_1.dot(W_1.T)
  731. s = slin.svdvals(shared)
  732. pr_W_1 = np.square(s.sum()) / np.square(s).sum()
  733. shared = W_2.dot(W_2.T)
  734. s = slin.svdvals(shared)
  735. pr_W_2 = np.square(s.sum()) / np.square(s).sum()
  736. # overall
  737. W_total = np.concatenate((W_1,W_2),axis=0)
  738. shared = W_total.dot(W_total.T)
  739. s = slin.svdvals(shared)
  740. pr_W_total = np.square(s.sum()) / np.square(s).sum()
  741. # for within area 1
  742. shared = L_1.dot(L_1.T)
  743. s = slin.svdvals(shared)
  744. pr_L_1 = np.square(s.sum()) / np.square(s).sum()
  745. # for within area 2
  746. shared = L_2.dot(L_2.T)
  747. s = slin.svdvals(shared)
  748. pr_L_2 = np.square(s.sum()) / np.square(s).sum()
  749. pr = {
  750. 'pr_W_1':pr_W_1, # part ratio for across-area loading matrix in area 1, involves W_1
  751. 'pr_W_2':pr_W_2, # part ratio for across-area loading matrix in area 2, involves W_2
  752. 'pr_L_1':pr_L_1, # part ratio for within-area loading matrix in area 1, involves L_1
  753. 'pr_L_2':pr_L_2, # part ratio for within-area loading matrix in area 2, involves L_2
  754. 'pr_W_total':pr_W_total # part ratio for across-area loading matrix jointly for area 1 and 2, involves W_1 and W_2
  755. }
  756. return pr
  757. def compute_psv(self):
  758. '''
  759. Compute percentage of shared variance (%sv) in each across- and within-area loading matrix.
  760. Returns:
  761. psv (dict): Dictionary containing the across- and within-area %sv for neurons in each area
  762. '''
  763. W_1,W_2,L_1,L_2 = self.get_loading_matrices()
  764. psi_1,psi_2 = self.params['psi_1'],self.params['psi_2']
  765. shared_across_x1 = np.diag(W_1.dot(W_1.T))
  766. shared_across_x2 = np.diag(W_2.dot(W_2.T))
  767. shared_within_x1 = np.diag(L_1.dot(L_1.T))
  768. shared_within_x2 = np.diag(L_2.dot(L_2.T))
  769. total_x = shared_across_x1 + shared_within_x1 + psi_1
  770. total_y = shared_across_x2 + shared_within_x2 + psi_2
  771. # for area 1
  772. psv_W_1 = (shared_across_x1 / total_x).flatten() * 100
  773. psv_L_1 = (shared_within_x1 / total_x).flatten() * 100
  774. ind_var_x1 = (psi_1 / total_x).flatten() * 100
  775. avg_psv_W_1 = np.mean(psv_W_1)
  776. avg_psv_L_1 = np.mean(psv_L_1)
  777. # for area 2
  778. psv_W_2 = (shared_across_x2 / total_y).flatten() * 100
  779. psv_L_2 = (shared_within_x2 / total_y).flatten() * 100
  780. ind_var_x2 = (psi_2 / total_y).flatten() * 100
  781. avg_psv_W_2 = np.mean(psv_W_2)
  782. avg_psv_L_2 = np.mean(psv_L_2)
  783. # overall
  784. avg_psv_W_total = np.mean(np.concatenate((psv_W_1,psv_W_2)))
  785. avg_psv_L_total = np.mean(np.concatenate((psv_L_1,psv_L_2)))
  786. psv = {
  787. 'psv_W_1':psv_W_1, # percent of across-area shared variance for each neuron in area 1
  788. 'psv_W_2':psv_W_2, # percent of across-area shared variance for each neuron in area 2
  789. 'psv_L_1':psv_L_1, # percent of within-area shared variance for each neuron in area 1
  790. 'psv_L_2':psv_L_2, # percent of within-area shared variance for each neuron in area 2
  791. 'avg_psv_W_1':avg_psv_W_1, # percent of across-area shared variance, averaged across neurons in area 1
  792. 'avg_psv_W_2':avg_psv_W_2, # percent of across-area shared variance, averaged across neurons in area 1
  793. 'avg_psv_L_1':avg_psv_L_1, # percent of across-area shared variance, averaged across neurons in area 1
  794. 'avg_psv_L_2':avg_psv_L_2, # percent of across-area shared variance, averaged across neurons in area 1
  795. 'ind_var_x1':ind_var_x1, # percent of independent variance for each neuron in area 1
  796. 'ind_var_x2':ind_var_x2, # percent of independent variance for each neuron in area 2
  797. 'avg_psv_W_total':avg_psv_W_total, # percent of across-area shared variance, averaged across all neurons
  798. 'avg_psv_L_total':avg_psv_L_total, # percent of within-area shared variance, averaged across all neurons
  799. }
  800. return psv
  801. def compute_metrics(self,cutoff_thresh=0.95):
  802. '''
  803. Wrapper to compute loading similarity, d_shared, part ratio, %sv, and canonical correlations.
  804. Returns:
  805. metrics (dict): Dictionary containing the computed metrics (loading similarity, d_shared, part ratio, %sv, and canonical correlations)
  806. '''
  807. dshared = self.compute_dshared(cutoff_thresh=cutoff_thresh)
  808. psv = self.compute_psv()
  809. pr = self.compute_part_ratio()
  810. ls = self.compute_load_sim()
  811. _,rho = self.get_canonical_directions()
  812. metrics = {
  813. 'dshared':dshared, # dictionary of d_shared metric
  814. 'psv':psv, # dictionary of %sv metric
  815. 'part_ratio':pr, # dictionary of part ratio metric
  816. 'load_sim':ls, # dictionary of loading similarity metric
  817. 'rho':rho # array of canonical correlations
  818. }
  819. if 'cv_rho' in self.params:
  820. metrics['cv_rho'] = self.params['cv_rho'] # array of cross-validated canonical correlations (if crossvalidate() was called)
  821. return metrics

pcca_fa_mdl.py at commit d13882e, under Apache-2.0 · at the source

Overview

Authors: Megan E McDonnell1,2, Akash Umakantha1,2,3, Ryan C Williamson1,2,3,4, Matthew A Smith1,2,5, Byron M Yu1,2,5,6
  1. Neuroscience Institute, Carnegie Mellon University, Pittsburgh, PA USA
  2. Center for the Neural Basis of Cognition, Carnegie Mellon University & University of Pittsburgh, Pittsburgh, PA USA
  3. Machine Learning Department, Carnegie Mellon University, Pittsburgh, PA USA
  4. School of Medicine, University of Pittsburgh, Pittsburgh, PA USA
  5. Department of Biomedical Engineering, Carnegie Mellon University, Pittsburgh, PA USA
  6. Department of Electrical and Computer Engineering, Carnegie Mellon University, Pittsburgh, PA USA
Institutions: University of Pittsburgh (United States); Center for the Neural Basis of Cognition (United States); Carnegie Mellon University (United States)
Journal: Nature communications, volume 17, issue 1, article 5088
Dates: received 3 July 2025; accepted 25 March 2026; published online 11 April 2026
Type: Research article · Language: English
License: CC BY-NC-ND
Identifiers: DOI 10.1038/s41467-026-71725-0 · PMID 41965893 · PMCID PMC13247044 · OpenAlex W4411283442
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: non-human primate (organism), cognitive (subfield)
Methods: Spectral & time-frequency, Statistics, Machine learning, Preprocessing, Connectivity, Single-unit activity, calcium imaging, Physiology & signal measures
Keywords: Neural decoding, Cognitive neuroscience
MeSH: Cognition*, Prefrontal Cortex*, Animals, Macaca mulatta, Male, Neurons, Spatial Memory (* major topic)
Topic: Neural dynamics and brain function (Cognitive Neuroscience, Neuroscience), according to OpenAlex
Funding: Simons Foundation (NC-GB-CULM-00003241-05, 543065); National Science Foundation (NSF) (NCS BCS 1734916/1954107, NCS DRL 2124066/2123911); U.S. Department of Health &amp; Human Services | NIH | National Institute of Mental Health (R01 MH118929, R01 MH128393); NIBIB NIH HHS (R01 EB026953, T32 EB029365); NEI NIH HHS (R01 EY035896, R01 EY029250); NIMH NIH HHS (R01 MH118929, R01 MH128393); U.S. Department of Health &amp; Human Services | NIH | National Institute of Neurological Disorders and Stroke (RF1 NS127107); U.S. Department of Health &amp; Human Services | NIH | National Institute of Biomedical Imaging and Bioengineering (T32 EB029365, R01 EB026953); NINDS NIH HHS (RF1 NS127107); U.S. Department of Health &amp; Human Services | NIH | National Eye Institute (R01 EY029250, R01 EY035896)
Citations: cited by 4 papers (Europe PMC); 150 references in the paper

Abstract

The abstract is not reproduced here: the paper's license (CC BY-NC-ND) does not allow it. Read it in the paper, at the publisher or on Europe PMC.

Repositories

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

meganmcd13/figures-dual-pfc

License: none: the authors keep all their rights
State: the link answers, verified on 29 September 2026
Evidence: files inventoried
Commit: 3b59632fb5b84c377d82a16ae7dd491e70eba4b5, 9 March 2026
Languages: Python (19), Jupyter (16), MATLAB (9)
Size: 52 files, 44 scripts
Software Heritage: not archived
Found in: “Code availability”
Holds: README, 16 notebooks
Not found: license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: NumPy (33 files), SciPy (27 files), Matplotlib (16 files), pandas (15 files), scikit-learn (5 files), Parallel Computing Toolbox (1 file), Signal Processing Toolbox (1 file)
Availability: 1 check, the latest on 29 September 2026: the link answers
  • 29 September 2026: the link answers
45 files

SmithLabNeuro/pcca_fa

License: Apache-2.0
State: the link answers, verified on 29 September 2026
Evidence: files inventoried
Commit: d13882eca2eee8cdb5e6ee4b0047b32b01f08754, 8 March 2026
Languages: Python (2), Jupyter (1)
Size: 11 files, 3 scripts
Software Heritage: not archived
Found in: “Code availability”
Holds: README, license file, environment (environment.yml), 1 notebook
Not found: CITATION.cff, tests, continuous integration, documentation
Tools: NumPy (3 files), SciPy (2 files), Matplotlib (1 file), scikit-learn (1 file)
Availability: 1 check, the latest on 29 September 2026: the link answers
  • 29 September 2026: the link answers
5 files

SmithLabNeuro/Ex

License: none: the authors keep all their rights
State: the link answers, verified on 29 September 2026
Evidence: files inventoried
Commit: 1a97dc0873abf4a2450f42ed8575305feaff7a16, 28 August 2026
Languages: MATLAB (415), C (9), Java (1), C/C++ (1)
Size: 495 files, 426 scripts
Software Heritage: not archived
Found in: “Code availability”
Holds: README
Not found: license file, CITATION.cff, environment file, tests, continuous integration, documentation
Availability: 1 check, the latest on 29 September 2026: the link answers
  • 29 September 2026: the link answers
427 files

Zenodo 18913386

License: Apache-2.0
State: the link answers, verified on 29 September 2026
Evidence: files inventoried
Size: 1 file
Software Heritage: not checked
Found in: “Code availability”
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: NumPy (3 files), SciPy (2 files), Matplotlib (1 file), scikit-learn (1 file)
Availability: 1 check, the latest on 29 September 2026: the link answers (HTTP 200)
  • 29 September 2026: the link answers (HTTP 200)
5 files
At the source:

Zenodo 18929016

License: Apache-2.0
State: the link answers, verified on 29 September 2026
Evidence: files inventoried
Size: 1 file
Software Heritage: not checked
Found in: “Code availability”
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: NumPy (33 files), SciPy (27 files), Matplotlib (16 files), pandas (15 files), scikit-learn (5 files), Parallel Computing Toolbox (1 file), Signal Processing Toolbox (1 file)
Availability: 1 check, the latest on 29 September 2026: the link answers (HTTP 200)
  • 29 September 2026: the link answers (HTTP 200)
45 files
At the source:

Zenodo 18913385

License: Apache-2.0
State: the link answers, verified on 29 September 2026
Evidence: files inventoried
Size: 1 file
Software Heritage: not checked
Found in: the references
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: NumPy (3 files), SciPy (2 files), Matplotlib (1 file), scikit-learn (1 file)
Availability: 1 check, the latest on 29 September 2026: the link answers (HTTP 200)
  • 29 September 2026: the link answers (HTTP 200)
5 files
At the source:

Code availability statement

The paper has a code availability statement. Its license (CC BY-NC-ND) does not allow reproducing it here; in short, from what the harvester recognized in it:

Read it in the paper: doi.org/10.1038/s41467-026-71725-0.

Tracing map

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

What the map holds:

  • 6 repositories of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 523 scripts, each with its path and the digest of its content;
  • 20 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 statement

The paper has a data availability statement. Its license (CC BY-NC-ND) does not allow reproducing it here; in short, from what the harvester recognized in it:

  • no repository, dataset or request procedure was recognized in it

Read it in the paper: doi.org/10.1038/s41467-026-71725-0.

Versions

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

Version 1, 29 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 5 authors, 2 keywords, 7 MeSH terms, 10 funders, 137 references.

Cite

This paper

McDonnell, M. E., Umakantha, A., Williamson, R. C., Smith, M. A., & Yu, B. M. (2026). Interactions across hemispheres in prefrontal cortex reflect global cognitive processing. Nature communications, 17(1), 5088. https://doi.org/10.1038/s41467-026-71725-0

BibTeX

@article{mcdonnell2026interactions,
author = {McDonnell, Megan E and Umakantha, Akash and Williamson, Ryan C and Smith, Matthew A and Yu, Byron M},
title = {{Interactions across hemispheres in prefrontal cortex reflect global cognitive processing}},
journal = {Nature communications},
year = {2026},
month = apr,
volume = {17},
number = {1},
pages = {5088},
publisher = {Nature Publishing Group},
issn = {2041-1723},
doi = {10.1038/s41467-026-71725-0},
url = {https://doi.org/10.1038/s41467-026-71725-0},
pmid = {41965893},
pmcid = {PMC13247044}
}

RIS

TY - JOUR
AU - McDonnell, Megan E
AU - Umakantha, Akash
AU - Williamson, Ryan C
AU - Smith, Matthew A
AU - Yu, Byron M
TI - Interactions across hemispheres in prefrontal cortex reflect global cognitive processing
T2 - Nature communications
J2 - Nat Commun
PY - 2026
DA - 2026/04/11
VL - 17
IS - 1
SP - 5088
SN - 2041-1723
PB - Nature Publishing Group
DO - 10.1038/s41467-026-71725-0
UR - https://doi.org/10.1038/s41467-026-71725-0
LA - en
ER -

CSL-JSON

{
"id": "10.1038/s41467-026-71725-0",
"type": "article-journal",
"title": "Interactions across hemispheres in prefrontal cortex reflect global cognitive processing",
"container-title": "Nature communications",
"author": [
{
"family": "McDonnell",
"given": "Megan E"
},
{
"family": "Umakantha",
"given": "Akash"
},
{
"family": "Williamson",
"given": "Ryan C"
},
{
"family": "Smith",
"given": "Matthew A"
},
{
"family": "Yu",
"given": "Byron M"
}
],
"container-title-short": "Nat Commun",
"volume": "17",
"issue": "1",
"page": "5088",
"DOI": "10.1038/s41467-026-71725-0",
"PMID": "41965893",
"PMCID": "PMC13247044",
"ISSN": "2041-1723",
"publisher": "Nature Publishing Group",
"URL": "https://doi.org/10.1038/s41467-026-71725-0",
"language": "en",
"issued": {
"date-parts": [
[
2026,
4,
11
]
]
}
}

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.neuron.2026.07.016 [code]
Inferring brain-wide interactions using data-constrained recurrent neural network models.
Journal: Neuron
In common: Parallel Computing Toolbox, Image Processing Toolbox, Statistics and Machine Learning Toolbox, 2 other tools, 14 references
[2] doi:10.7554/elife.99278 [code]
Brain-wide arousal signals are segregated from movement planning in the superior colliculus of the macaque.
Journal: eLife
In common: NumPy, non-human primate, 11 references, author Matthew A Smith
[3] doi:10.1038/s41467-026-75347-4 [code]
Sleep reveals dynamics integrating and segregating movement and stimulus representations in V1.
Journal: Nature communications
In common: export_fig, Optimization Toolbox, Parallel Computing Toolbox, 8 other tools, 7 references
[4] doi:10.1371/journal.pbio.3003915 [code]
Noise-invariant representations of sound emerge along the canonical cortical hierarchy.
Journal: PLoS biology
In common: Optimization Toolbox, Statistics and Machine Learning Toolbox, scikit-learn, 4 other tools, 9 references
[5] doi:10.1038/s41467-026-75705-2 [code]
Redundant prefrontal hemispheres adapt storage strategy to working memory demands.
Journal: Nature communications
In common: scikit-learn, pandas, SciPy, 2 other tools, non-human primate, cognitive, 6 references, author Matthew A Smith
[6] doi:10.1126/sciadv.adz6495
Pupil-linked arousal heterogeneously modulates cell-type-specific sensory processing.
Journal: Science advances
In common: 13 references
[7] doi:10.1038/s41598-026-55225-1 [code]
Benchmarking criteria to determine latent linear dimensionality in neural data.
Journal: Scientific reports
In common: Statistics and Machine Learning Toolbox, pandas, 11 references
[8] doi:10.1038/s41586-026-10631-3 [code]
A prognostic human brain network for diffuse midline glioma.
Journal: Nature
In common: export_fig, Psychtoolbox, Optimization Toolbox, 9 other tools
[9] doi:10.1126/sciadv.adv5652 [code]
The anterior cingulate cortex modulates pupil-linked arousal.
Journal: Science advances
In common: pandas, SciPy, Matplotlib, 1 other tool, 9 references
[10] doi:10.1002/hbm.70602 [code]
Neuroimaging Correlates of Post-Stroke Pain After Ischemic Stroke: Secondary Analysis of the INSPiRE-TMS Trial.
Journal: Human brain mapping
In common: export_fig, Psychtoolbox, Optimization Toolbox, 8 other tools

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.