OSCR

Intrinsic cortical geometry is associated with individual differences in local functional organization

Code ↔ Paper

5 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 5 matches
  1. [1] § Methods › Shared geometry embedding ↔ src/variograd_utils/embed_utils.py, lines 78–156 · score 0.68 · Laplacian eigenmaps, Cauchy kernel, reference matrices, affinity, embedding
  2. [2] § Methods › Functional connectivity gradients ↔ scripts/07a.connectivity_JE.py, lines 1–68 · score 0.67 · connectivity matrices, joint embedding, functional connectivity, thresholded, mapping, cortical
  3. [3] § Methods › MRI data ↔ scripts/06.downsample_timeseries.py, lines 1–62 · score 0.63 · fMRI, preprocessing, split, FWHM, smoothed, downsampled
  4. [4] § Methods › Functional connectivity gradients ↔ src/variograd_utils/embed_utils.py, lines 78–156 · score 0.59 · Pearson correlations, joint embedding, affinity, mapping, matrices
  5. [5] § Methods › MRI data ↔ src/variograd_utils/brain_utils.py, lines 238–281 · score 0.54 · fMRI, cortical surfaces, HCP, mesh, hemisphere, vertex

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 · 765 lines · 26 KB · MIT · 2 matches

  1. import warnings
  2. import numpy as np
  3. from scipy.sparse import diags
  4. from scipy.linalg import orthogonal_procrustes
  5. from scipy.optimize import linear_sum_assignment
  6. from sklearn.metrics.pairwise import cosine_similarity, euclidean_distances
  7. from sklearn.decomposition import TruncatedSVD
  8. from variograd_utils.core_utils import vector_wise_corr
  9. class JointEmbedding:
  10. """
  11. JointEmbedding class for computing the joint embedding of two matrices.
  12. Parameters:
  13. ----------
  14. method : str, optional
  15. The embedding method to use. Options are:
  16. - "dme": diffusion map embedding (default)
  17. - "le": Laplacian eigenmap
  18. n_components : int, optional
  19. Number of components to compute (default=2).
  20. alignment : str, optional
  21. The alignment method to use:
  22. - "procrustes": orthogonal Procrustes rotation with scaling.
  23. - "rotation": orthogonal Procrustes rotation (default)
  24. - "sort_flip": reorder the embedding dimensions maximizing absolute
  25. correlation coefficients with th independent refrence. Then multiply
  26. dimensions with a negative coefficient by -1.
  27. - "dot_product": align the embedding using the dot product of the joint reference
  28. and independent reference embeddings.
  29. random_state : int, optional
  30. Random seed of the SVDs.
  31. copy : bool, optional
  32. Whether to copy the input matrices (default=True).
  33. Attributes:
  34. ----------
  35. method : str
  36. The embedding method to use.
  37. n_components : int
  38. Number of components to compute.
  39. alignment : str
  40. The alignment method to use.
  41. random_state : int
  42. Random seed of the SVDs.
  43. copy : bool
  44. Whether to copy the input matrices.
  45. vectors : np.ndarray
  46. The eigenvectors of the embedding.
  47. lambdas : np.ndarray
  48. The eigenvalues of the embedding.
  49. independent_ref : np.ndarray
  50. The independent reference embedding used for alignment.
  51. Methods:
  52. --------
  53. fit_transform(M, R, C=None, affinity="cosine", scale=None, method_kwargs=None)
  54. Compute the joint embedding of M and R using the specified method.
  55. _joint_affinity_matrix(M, R, C=None, affinity="cosine", scale=None)
  56. Computes the joint affinity matrix.
  57. _align_embeddings(embedding, joint_reference, independent_reference, method="rotation")
  58. Align the joint embedding with the independently computed reference embedding.
  59. _affinity_matrix(M, method="cosine", scale=None)
  60. Compute the joint affinity matrix of the input data.
  61. """
  62. def __init__(self, method="dme", n_components=2, alignment=None,
  63. random_state=None, copy=True):
  64. self.method = method
  65. self.n_components = n_components
  66. self.random_state = random_state
  67. self.copy = copy
  68. self.alignment = alignment
  69. self.vectors = self.lambdas = None
  70. self.independent_ref = None
  71. def fit_transform(self, M, R, C=None, affinity="cosine", scale=None, method_kwargs=None):
  72. """
  73. Compute the joint embedding of M and R using the specified method.
  74. Parameters:
  75. ----------
  76. M : np.ndarray
  77. Target matrix to embed.
  78. R : np.ndarray
  79. Reference matrix.
  80. C : np.ndarray, optional
  81. Correspondence matrix.
  82. If affinity is not "precomputed", C is used as is and the
  83. specified affinity method is applied only to while M and R.
  84. affinity : str, optional
  85. The method to compute the affinity matrices. Options are:
  86. - "cosine": cosine similarity (default)
  87. - "correlation": Pearson correlation coefficient
  88. - "linear": linear kernel
  89. - "cauchy": Cauchy kernel
  90. - "gauss": Gaussian kernel
  91. - "precomputed": precomputed affinity matrix.
  92. In this case, M and R are assumed to be affinity matrices
  93. and a correspondence matrix C must be specified.
  94. scale : float, optional
  95. The scaling parameter for the kernel methods.
  96. method_kwargs : dict, optional
  97. Additional keyword arguments for the embedding method.
  98. Returns:
  99. -------
  100. self : JointEmbedding
  101. Fitted instance of JointEmbedding.
  102. embedding_M : np.ndarray
  103. The joint embedding of M.
  104. embedding_R : np.ndarray
  105. The joint embedding of R.
  106. Raises:
  107. -------
  108. ValueError
  109. If the affinity is "precomputed" and C is not specified.
  110. """
  111. if (affinity == "precomputed") & (C is None):
  112. raise ValueError("Precomputed affinity assumes M and R are already affinity matrices,"
  113. + "so a correspondance affinity matrx C must be specified too.")
  114. n = M.shape[0]
  115. R = np.array(R, copy=self.copy)
  116. M = np.array(M, copy=self.copy)
  117. if C is not None:
  118. C = np.array(C, copy=self.copy)
  119. A = self._joint_affinity_matrix(M, R, C=C, affinity=affinity, scale=scale)
  120. method_kwargs = {} if method_kwargs is None else method_kwargs
  121. embedding_function = diffusion_map_embedding if self.method == "dme" else laplacian_eigenmap
  122. embedding, vectors, lambdas = embedding_function(A, n_components=self.n_components,
  123. random_state=self.random_state,
  124. **method_kwargs)
  125. self.vectors = vectors
  126. self.lambdas = lambdas
  127. embedding_R = embedding[:n]
  128. embedding_M = embedding[n:]
  129. if self.alignment is not None:
  130. A = _affinity_matrix(R,method=affinity, scale=scale) if affinity != "precomputed" else R
  131. embedding_R_ind, _, _ = embedding_function(A, n_components=self.n_components,
  132. random_state=self.random_state,
  133. **method_kwargs)
  134. embedding_M, embedding_R = self._align_embeddings(embedding_M, embedding_R,
  135. embedding_R_ind,method=self.alignment)
  136. self.independent_ref = embedding_R_ind
  137. return embedding_M, embedding_R
  138. def _joint_affinity_matrix(self, M, R, C=None, affinity="cosine", scale=None):
  139. """
  140. Computes the joint affinity matrix.
  141. Parameters:
  142. ----------
  143. M : np.ndarray
  144. Target matrix to embed.
  145. R : np.ndarray
  146. Reference matrix.
  147. C : np.ndarray, optional
  148. Correspondence matrix of shape (n_samples_M, n_samples_R).
  149. If affinity is not "precomputed", C is used as is and the
  150. specified affinity method is applied only to while M and R.
  151. affinity : str, optional
  152. The method to compute the affinity matrices. Options are:
  153. - "cosine": cosine similarity (default)
  154. - "correlation": Pearson correlation coefficient
  155. - "linear": linear kernel
  156. - "cauchy": Cauchy kernel
  157. - "gauss": Gaussian kernel
  158. - "precomputed": precomputed affinity matrix.
  159. scale : float, optional
  160. The scaling parameter for the kernel methods.
  161. Returns:
  162. -------
  163. A : np.ndarray
  164. The joint affinity matrix.
  165. """
  166. if C is None:
  167. if affinity in ["cosine", "correlation"]:
  168. A = np.vstack([R, M])
  169. A = _affinity_matrix(A, method=affinity, scale=scale)
  170. elif affinity in ["linear", "cauchy", "gauss"]:
  171. M_aff = _affinity_matrix(M, method=affinity, scale=scale)
  172. R_aff = _affinity_matrix(R, method=affinity, scale=scale)
  173. C = pseudo_sqrt(np.dot(M_aff, R_aff), n_components=100)
  174. A = np.block([[R_aff, C.T],
  175. [C, M_aff]])
  176. else:
  177. if affinity == "precomputed":
  178. A = np.block([[R, C.T],
  179. [C, M]])
  180. else:
  181. A = np.block([[_affinity_matrix(R, method=affinity, scale=scale), C.T],
  182. [C, _affinity_matrix(M, method=affinity, scale=scale)]])
  183. return A
  184. def _align_embeddings(self, embedding, joint_reference, independent_reference,
  185. method="rotation"):
  186. """
  187. Align the joint embedding with the independently computed reference embedding.
  188. Parameters:
  189. ----------
  190. embedding : np.ndarray
  191. The joint embedding.
  192. reference : np.ndarray
  193. The reference embedding.
  194. method : str, optional
  195. The method used to align the joint embedding to the independently
  196. computed reference embedding:
  197. - "procrustes": orthogonal Procrustes rotation with scaling.
  198. - "rotation": orthogonal Procrustes rotation (default)
  199. - "sort_flip": reorder the embedding dimensions maximizing absolute
  200. correlation coefficients with th independent refrence. Then multiply
  201. dimensions with a negative coefficient by -1.
  202. - "dot_product": align the embedding using the dot product of the joint reference
  203. and independent reference embeddings.
  204. Returns:
  205. -------
  206. embedding : np.ndarray
  207. The aligned joint embedding.
  208. reference : np.ndarray
  209. The aligned reference embedding.
  210. """
  211. s = 1
  212. if method == "sort_flip":
  213. idx = argsort_axes(joint_reference.copy(), independent_reference.copy())
  214. joint_reference = joint_reference[:, idx]
  215. embedding = embedding[:, idx]
  216. to_flip = vector_wise_corr(joint_reference.copy(), independent_reference.copy()) < 0
  217. R = np.diag([-1 if i else 1 for i in to_flip])
  218. elif method == "dot_product":
  219. R = dot_product_rotation(joint_reference.copy(), independent_reference.copy())
  220. elif method in ["rotation", "procrustes"]:
  221. R, s = procrustes_rotation(joint_reference.copy(), independent_reference.copy())
  222. s = 1 if method == "rotation" else s
  223. else:
  224. raise ValueError(f"Unknown alignment method: {self.alignment}")
  225. joint_reference = np.dot(joint_reference, R) * s
  226. embedding = np.dot(embedding, R) * s
  227. return embedding, joint_reference
  228. def _affinity_matrix(M, method="cosine", scale=None):
  229. """
  230. Convert a distance measure to affinity.
  231. Parameters
  232. ----------
  233. M : array-like
  234. The input matrix of shape (samples, features)
  235. method : str
  236. The method to compute the affinity matrix. Options are:
  237. - "cosine": cosine similarity
  238. - "correlation": Pearson correlation coefficient
  239. - "linear": linear kernel
  240. - "cauchy": Cauchy kernel
  241. - "gauss": Gaussian kernel
  242. scale : float
  243. The scaling parameter for the kernel
  244. Returns
  245. -------
  246. A : array-like
  247. The affinity matrix
  248. """
  249. if method == "cosine":
  250. A = cosine_similarity(M)
  251. elif method == "correlation":
  252. A = np.corrcoef(M)
  253. elif method in {"linear", "cauchy", "gauss"}:
  254. # A = M if _is_square(M) else euclidean_distances(M)
  255. A = kernel_affinity(A, kernel=method, scale=scale)
  256. else:
  257. raise ValueError("Unknown affinity method")
  258. return A
  259. def kernel_affinity(A, kernel="linear", scale=None):
  260. """
  261. Apply kernel to a matrix A
  262. Parameters
  263. ----------
  264. A : array-like
  265. The input matrix of shape smaples x features
  266. kernel : str
  267. The kernel to apply. Options are "cauchy", "gauss", "linear"
  268. scale : float
  269. The scaling parameter for the kernel
  270. Returns
  271. -------
  272. A : array-like
  273. The kernelized matrix
  274. """
  275. if scale is None:
  276. scale = 1 / A.shape[1]
  277. if kernel == "cauchy":
  278. A = 1.0 / (1.0 + (A ** 2) / (scale ** 2))
  279. elif kernel == "gauss":
  280. A = np.exp(-0.5 * (A ** 2) / (scale ** 2))
  281. elif kernel == "linear":
  282. A = 1 / (1 + A / scale)
  283. else:
  284. raise ValueError("Unknown kernel type")
  285. return A
  286. def spectral_affinity(M, R, n_components=2, random_state=None):
  287. """
  288. Compute the spectral similarity between two matrices using Laplacian Eigenmaps and Procrustes alignment.
  289. This function calculates the cosine similarity between Laplacian Eigenmaps of two matrices,
  290. `M` and `R` of shape observations x features. The embeddings are aligned using
  291. the Orthogonal Procrustes transformation before computing similarity.
  292. Parameters
  293. ----------
  294. M : numpy.ndarray
  295. The first input matrix.
  296. R : numpy.ndarray
  297. The second input matrix (also target of the procrustes alignment).
  298. n_components : int, optional
  299. Number of dimensions for the Laplacian Eigenmap embeddings. Default is 2.
  300. random_state : int, optional
  301. Determines random number generation for eigenmap embedding. Default is None.
  302. Returns
  303. -------
  304. numpy.ndarray
  305. A cosine similarity matrix representing the similarity between aligned
  306. embeddings of `M` and `R`.
  307. """
  308. embedding_M, _, _ = laplacian_eigenmap(M, n_components=n_components, normalized=True, random_state=random_state)
  309. embedding_R, _, _ = laplacian_eigenmap(R, n_components=n_components, normalized=True, random_state=random_state)
  310. embedding_M -= embedding_M.mean(axis=0)
  311. embedding_M /= np.linalg.norm(embedding_M)
  312. embedding_R -= embedding_R.mean(axis=0)
  313. embedding_R /= np.linalg.norm(embedding_R)
  314. rotation, scaling = orthogonal_procrustes(embedding_M, embedding_R)
  315. embedding_M = np.dot(embedding_M, rotation) * scaling
  316. A = cosine_similarity(embedding_M, embedding_R)
  317. return A
  318. def diffusion_map_embedding(A, n_components=2, alpha=0.5, diffusion_time=1, random_state=None):
  319. """
  320. Computes the joint diffusion map embedding of an affinity matrix.
  321. Parameters:
  322. ----------
  323. A : np.ndarray
  324. Target matrix to embed.
  325. n_components: int, optional
  326. Number of components to keep (default=2).
  327. alpha: float, optional
  328. Controls Laplacian normalization, balancing local and global structures in embeddings.
  329. Alpha <= 0.5 emphasize local structures, alpha >= 1 focus on global organization.
  330. diffusion_time: float, optional
  331. Determines the scale of the random walk process (default=1).
  332. random_state: int, optional
  333. Random seed of the SVD.
  334. Returns
  335. -------
  336. embedding : numpy.ndarray of shape (n_samples, n_components)
  337. The diffusion map embedding computed from the random walk Laplacian.
  338. vectors : numpy.ndarray of shape (n_samples, n_components)
  339. The corresponding singular vectors (or eigenvectors) used for the embedding.
  340. lambdas : numpy.ndarray of shape (n_components,)
  341. The singular values (or eigenvalues) associated with the embedding.
  342. """
  343. if np.any(A < 0):
  344. warnings.warn(f"{np.sum(A < 0)} negative values in the affinity matrix set to 0. "
  345. + f"Mean negative value: {np.mean(A[A < 0])}({np.std(A[A < 0])})",
  346. RuntimeWarning)
  347. A[A<0] = 0
  348. L = _random_walk_laplacian(A, alpha=alpha)
  349. embedding, vectors, lambdas = _diffusion_map(L, n_components=n_components,
  350. diffusion_time=diffusion_time,
  351. random_state=random_state)
  352. return embedding, vectors, lambdas
  353. def _diffusion_map(L, n_components=2, diffusion_time=1, random_state=None):
  354. """
  355. Computes the diffusion map of the random walk Laplacian L.
  356. Parameters:
  357. ----------
  358. L: np.ndarray
  359. Random walk Laplacian.
  360. n_components: int, optional
  361. Number of components to compute (default=2).
  362. diffusion_time: float, optional
  363. Determines the scale of the random walk process (default=1).
  364. random_state: int, optional
  365. Random seed of the SVD.
  366. Returns
  367. -------
  368. embedding : numpy.ndarray of shape (n_samples, n_components)
  369. The diffusion map embedding computed from the random walk Laplacian.
  370. vectors : numpy.ndarray of shape (n_samples, n_components)
  371. The corresponding singular vectors (or eigenvectors) used for the embedding.
  372. lambdas : numpy.ndarray of shape (n_components,)
  373. The singular values (or eigenvalues) associated with the embedding.
  374. """
  375. n_components = n_components + 1
  376. embedding = TruncatedSVD(n_components=n_components,
  377. random_state=random_state
  378. ).fit(L)
  379. vectors = embedding.components_.T
  380. lambdas = embedding.singular_values_
  381. if np.any(vectors[:, 0] == 0):
  382. warnings.warn("0 values found in the first eigenvector; 1e-15 was added to all vectors.",
  383. RuntimeWarning)
  384. vectors += 1e-16
  385. psi = vectors / np.tile(vectors[:, 0], (vectors.shape[1], 1)).T
  386. lambdas[1:] = np.power(lambdas[1:], diffusion_time)
  387. embedding = psi[:, 1:n_components] @ np.diag(lambdas[1:n_components], 0)
  388. lambdas = lambdas[1:]
  389. vectors = vectors[:, 1:]
  390. return embedding, vectors, lambdas
  391. def _random_walk_laplacian(A, alpha=0.5):
  392. """
  393. Computes the random walk Laplacian of an affinity matrix A.
  394. Parameters:
  395. ----------
  396. A: np.ndarray
  397. Affinity matrix.
  398. alpha: float, optional
  399. Controls Laplacian normalization, balancing local and global structures in embeddings.
  400. Alpha <= 0.5 emphasize local structures, alpha >= 1 focus on global organization.
  401. Returns:
  402. -------
  403. L : np.ndarray
  404. The random walk Laplacian of A.
  405. """
  406. if (A.shape[0] != A.shape[1]) or not np.allclose(A, A.T):
  407. raise ValueError("A must be a squared, symmetrical affinity matrix.")
  408. # Compute the normalized Laplacian
  409. degree = np.sum(A, axis=1)
  410. d_alpha = diags(np.power(degree, -alpha))
  411. L_alpha = d_alpha @ A @ d_alpha
  412. # Compute the random walk Laplacian
  413. degree = np.sum(L_alpha, axis=1)
  414. d_alpha = diags(np.power(degree, -1))
  415. L = d_alpha @ L_alpha
  416. return L
  417. def laplacian_eigenmap(A, n_components=2, normalized=True, random_state=None):
  418. """
  419. Computes the spectral embedding of an affinity matrix.
  420. Parameters:
  421. ----------
  422. A : np.ndarray
  423. Target matrix to embed.
  424. n_components: int, optional
  425. Number of components to compute (default=2).
  426. normalized: bool, optional
  427. Whether to normalize the Laplacian matrix (default=True).
  428. random_state: int, optional
  429. Random seed of the SVD.
  430. Returns
  431. -------
  432. embedding : numpy.ndarray of shape (n_samples, n_components)
  433. The laplaician eigenmap embedding of L.
  434. vectors : numpy.ndarray of shape (n_samples, n_components)
  435. The corresponding singular vectors (or eigenvectors) used for the embedding.
  436. lambdas : numpy.ndarray of shape (n_components,)
  437. The singular values (or eigenvalues) associated with the embedding.
  438. """
  439. if np.any(A < 0):
  440. warnings.warn(f"{np.sum(A < 0)} negative values in the affinity matrix set to 0. "
  441. + f"Mean negative value: {np.mean(A[A < 0])}({np.std(A[A < 0])})",
  442. RuntimeWarning)
  443. A[A<0] = 0
  444. L = _laplacian(A, normalized=normalized)
  445. n_components = n_components + 1
  446. svd = TruncatedSVD(n_components=n_components, random_state=random_state)
  447. embedding = svd.fit_transform(L)[:, 1:]
  448. vectors = svd.components_.T[:, 1:]
  449. lambdas = svd.singular_values_[1:]
  450. return embedding, vectors, lambdas
  451. def _laplacian(A, normalized=True):
  452. """
  453. Computes the Laplacian of an affinity matrix A.
  454. Parameters:
  455. ----------
  456. A : np.ndarray
  457. Affinity matrix.
  458. normalized : bool, optional
  459. Compute the normalized Laplacian (default=True).
  460. Returns:
  461. -------
  462. L : np.ndarray
  463. Laplacian matrix of M.
  464. """
  465. if (A.shape[0] != A.shape[1]) or not np.allclose(A, A.T):
  466. raise ValueError("A must be a squared, symmetrical affinity matrix.")
  467. # Calculate Laplacian
  468. D = np.sum(A, axis=1)
  469. L = D - A
  470. # Normalize Laplacian
  471. if normalized:
  472. D_inv_sqrt = 1.0 / np.sqrt(D)
  473. D_inv_sqrt[np.isinf(D_inv_sqrt)] = 0
  474. D_inv_sqrt = np.diag(D_inv_sqrt)
  475. L = D_inv_sqrt @ (L @ D_inv_sqrt)
  476. return L
  477. def pseudo_sqrt(X, n_components=100):
  478. """
  479. Compute the pseudo-square root of a symmetric matrix.
  480. This function computes a low-rank approximation of the pseudo-square root of
  481. a symmetric matrix using Singular Value Decomposition (SVD).
  482. Parameters
  483. ----------
  484. X : numpy.ndarray
  485. The input square matrix to compute the pseudo-square root for.
  486. n_components : int, optional
  487. The number of components to retain in the low-rank approximation.
  488. Default is 100.
  489. Returns
  490. -------
  491. numpy.ndarray
  492. The approximated symmetric pseudo-square root of the input matrix.
  493. Notes
  494. -----
  495. - If `X` is not symmetric, it is symmetrized as `(X + X.T) / 2`.
  496. - The pseudo-square root is computed as `U * sqrt(S) * U.T`, where `U` and `S`
  497. - The method assumes `X` has a valid SVD decomposition.
  498. Raises
  499. ------
  500. ValueError
  501. If the input matrix `X` is not square.
  502. """
  503. if (X.shape[0] != X.shape[1]) or (X.ndim != 2):
  504. raise ValueError("Input matrix X must be square.")
  505. if not np.allclose(X, X.T, atol=1e-10):
  506. X = (X + X.T) / 2
  507. svd = TruncatedSVD(n_components=n_components)
  508. svd.fit(X)
  509. U = svd.components_.T
  510. S = np.diag(svd.singular_values_)
  511. Ssqrt = S ** 0.5
  512. return np.dot(np.dot(U, Ssqrt), U.T)
  513. def dot_product_rotation(M, R):
  514. """
  515. Compute the rotated dot product matrix between two datasets.
  516. This function takes two matrices, `M` and `R`, and computes the roation matrix
  517. necessary to align them using their dot product.
  518. Parameters
  519. ----------
  520. M : numpy.ndarray
  521. A 2D array of shape `(n_samples, n_features)`. The matrix to be rotated.
  522. R : numpy.ndarray
  523. A 2D array of shape `(n_samples, n_features)`. The target reference matrix.
  524. Returns
  525. -------
  526. numpy.ndarray
  527. A 2D array of shape `(n_features, n_features)`. The rotation matrix from M to R.
  528. Notes
  529. -----
  530. - The input matrices `M` and `R` must have the same shape.
  531. - This function normalizes each row (sample) to have unit norm, ensuring that the dot
  532. product represents cosine similarity between the corresponding features.
  533. Raises
  534. ------
  535. ValueError
  536. If the shapes of `M` and `R` do not match, or if any row contains only zeros.
  537. """
  538. if M.shape != R.shape:
  539. raise ValueError("Input matrices M and R must have the same shape.")
  540. if np.any(np.linalg.norm(M, axis=1) == 0) or np.any(np.linalg.norm(R, axis=1) == 0):
  541. raise ValueError("Rows of M and R must not be all zeros.")
  542. R -= R.mean(axis=1, keepdims=True)
  543. M -= M.mean(axis=1, keepdims=True)
  544. R /= np.linalg.norm(R, axis=1, keepdims=True)
  545. M /= np.linalg.norm(M, axis=1, keepdims=True)
  546. rotation_mat = np.dot(M.T, R)
  547. return rotation_mat
  548. def procrustes_rotation(M, R):
  549. """
  550. Compute the rotation matrix and scaling factor to align a matric M to a reference R
  551. solving the orthogonal Procrustes problem. This is essentially a wrapper over
  552. scipy.linalg.orthogonal_procrustes() that includes the normalization step (see
  553. scipy.spatial.procrustes()).
  554. Parameters
  555. ----------
  556. M : numpy.ndarray
  557. A 2D array of shape `(n_samples, n_features)`. The matrix to be rotated.
  558. R : numpy.ndarray
  559. A 2D array of shape `(n_samples, n_features)`. The target reference matrix.
  560. Returns
  561. -------
  562. R : (N, N) ndarray
  563. A 2D array of shape `(n_features, n_features)`. The rotation matrix from M to R.
  564. scale : float
  565. Sum of the singular values of ``A.conj().T @ B``.
  566. Raises
  567. ------
  568. ValueError
  569. If the shapes of `M` and `R` do not match, or if any row contains only zeros.
  570. """
  571. if M.shape != R.shape:
  572. raise ValueError("Input matrices M and R must have the same shape.")
  573. if np.any(np.linalg.norm(M, axis=1) == 0) or np.any(np.linalg.norm(R, axis=1) == 0):
  574. raise ValueError("Rows of M and R must not be all zeros.")
  575. R -= R.mean(axis=0)
  576. R /= np.linalg.norm(R)
  577. M -= M.mean(axis=0)
  578. M /= np.linalg.norm(M)
  579. return orthogonal_procrustes(M, R)
  580. def argsort_axes(M, R):
  581. """
  582. Determine optimal axis reordering of a subject embedding to match a reference embedding.
  583. This function computes the absolute Pearson correlation between each pair of axes
  584. in the subject embedding `M` and reference embedding `R`, and solves the optimal
  585. one-to-one axis assignment using the Hungarian algorithm to maximize total correlation.
  586. Parameters
  587. ----------
  588. M : numpy.ndarray
  589. A 2D array of shape `(n_samples, n_features)`. The embedding
  590. dimensions to reorder.
  591. R : numpy.ndarray
  592. A 2D array of shape `(n_samples, n_features)`. The target
  593. reference embedding.
  594. Returns
  595. -------
  596. reorder_idx : ndarray of shape (n_dims,)
  597. Indices that reorder the axes of `M` to best match the axes of `R`
  598. in correlation magnitude. Use as: `M[:, reorder_idx]`.
  599. Notes
  600. -----
  601. - Sign alignment (i.e., flipping axes) is not handled here.
  602. - Axis correspondence is determined using absolute Pearson correlation.
  603. See Also
  604. --------
  605. vector_wise_corr : Computes vector-wise Pearson correlation.
  606. """
  607. ndims = M.shape[1]
  608. if R.shape[1] != ndims:
  609. raise ValueError("Both embeddings must have the same number of dimensions")
  610. S = np.array([vector_wise_corr(R, m.reshape(-1,1)) for m in M.T])
  611. return linear_sum_assignment(abs(S), maximize=True)[1]

embed_utils.py at commit c1a6f0b, under MIT · at the source

Overview

Authors: Francesco Alberti1,2, Pierre-Louis Bazin3, R. Austin Benn1,2, Robert Scholz1,2,4,5, Wei Wei1,2, Alexander Holmes1,2, Victoria Shevchenko1,2, Ulysse Klatzmann1,2, Carla Pallavicini1,2,6,7, Robert Leech8, Daniel S. Margulies1,2
  1. Université Paris Cité, INCC UMR 8002, CNRS, Paris, France
  2. Centre for Integrative Neuroimaging (OxCIN), FMRIB, Nuffield Department of Clinical Neurosciences, University of Oxford
  3. Full Brain Picture Analytics, Leiden, Netherlands
  4. Wilhelm Wundt Institute for Psychology, Leipzig University, Leipzig, Germany
  5. Max Planck School of Cognition, Leipzig, Germany
  6. National Scientific and Technical Research Council (CONICET), Argentina
  7. Cognitive Neuroscience Center, University of San Andres, Buenos Aires, Argentina
  8. Department of Neuroimaging, King’s College London, UK
Dates: published online 8 April 2026
Type: Preprint · Language: English
License: CC BY
Identifiers: DOI 10.21203/rs.3.rs-9200088/v1 · OpenAlex W7151357601
Open access: green, a free copy (OpenAlex)
Status: code verified
Methods: Connectivity, Statistics, fMRI & imaging
Keywords: xx
Topic: Functional Brain Connectivity Studies (Cognitive Neuroscience, Neuroscience), according to OpenAlex
Funding: National Institute for Health Research (NIHR) (NIHR203316); Wellcome Trust (203141/Z/16/Z)
Citations: not cited yet (Europe PMC); 39 references in the paper

Abstract

It is widely accepted that the geometry of the cerebral cortex constrains its functional topography. However, how geometric properties contribute to individual differences in cortical organization has not been fully characterized. Here, we investigate whether cortical function varies with cortical geometry across individuals at the local or global scale. We characterize functional organization using the first three gradients of functional connectivity, and project individual surfaces into a shared embedding that captures their intrinsic geometry. Fitting localized spatial models within this embedding via lattice Kriging, we test whether interindividual variation of the gradients is linked to differences in spatial location. These models capture a common spatial structure underlying local gradient transitions across individuals, but do not capture differences in global gradient layout. This suggests that universal geometric properties shape the functional transitions between otherwise stable functional systems, meaningfully contributing to subject-specific functional topography.

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

alberti-f/VarioGrad

License: MIT
State: the link answers, verified on 29 September 2026
Evidence: files inventoried
Commit: c1a6f0b1ba34caeafac778cb97a6d671fbf9c7c9, 23 June 2026
Languages: Python (22), R (11), Jupyter (3)
Size: 43 files, 36 scripts
Software Heritage: not archived
Found in: “Code Availability”
Holds: README, license file, environment (pyproject.toml, requirements.txt), 3 notebooks
Not found: CITATION.cff, tests, continuous integration, documentation
Tools: NumPy (21 files), NiBabel (8 files), scikit-learn (8 files), pandas (7 files), SciPy (7 files), Matplotlib (4 files), Connectome Workbench (4 files), reticulate (2 files), seaborn (2 files), statsmodels (2 files), caret (1 file), h5py (1 file)
Availability: 1 check, the latest on 29 September 2026: the link answers
  • 29 September 2026: the link answers
38 files

Code Availability

The HCP preprocessing pipelines can be found at https://github.com/Washington-University/HCPpipelines, while the code used to run all subsequent analyses is made available as an installable package on F.A.'s GitHub repository https://github.com/alberti-f/VarioGrad.

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

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;
  • 36 scripts, each with its path and the digest of its content;
  • 5 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

Data were provided by the Human Connectome Project, WU-Minn Consortium (Principal Investigators: David Van Essen and Kamil Ugurbil; 1U54MH091657) funded by the 16 NIH Institutes and Centers that support the NIH Blueprint for Neuroscience Research; and by the McDonnell Center for Systems Neuroscience at Washington University. All data are obtainable from the HCP website (https://db.humanconnectome.org/).

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

Recorded: type, language, journal, dates, 11 authors, 1 keyword, 2 funders, 37 references.

Cite

This paper

Alberti, F., Bazin, P.-L., Benn, R. A., Scholz, R., Wei, W., Holmes, A., Shevchenko, V., Klatzmann, U., Pallavicini, C., Leech, R., & Margulies, D. S. (2026). Intrinsic cortical geometry is associated with individual differences in local functional organization. Research Square (preprint). https://doi.org/10.21203/rs.3.rs-9200088/v1

BibTeX

@article{alberti2026intrinsic,
author = {Alberti, Francesco and Bazin, Pierre-Louis and Benn, R. Austin and Scholz, Robert and Wei, Wei and Holmes, Alexander and Shevchenko, Victoria and Klatzmann, Ulysse and Pallavicini, Carla and Leech, Robert and Margulies, Daniel S.},
title = {{Intrinsic cortical geometry is associated with individual differences in local functional organization}},
journal = {Research Square (preprint)},
year = {2026},
month = apr,
publisher = {Research Square},
issn = {2693-5015},
doi = {10.21203/rs.3.rs-9200088/v1},
url = {https://doi.org/10.21203/rs.3.rs-9200088/v1}
}

RIS

TY - JOUR
AU - Alberti, Francesco
AU - Bazin, Pierre-Louis
AU - Benn, R. Austin
AU - Scholz, Robert
AU - Wei, Wei
AU - Holmes, Alexander
AU - Shevchenko, Victoria
AU - Klatzmann, Ulysse
AU - Pallavicini, Carla
AU - Leech, Robert
AU - Margulies, Daniel S.
TI - Intrinsic cortical geometry is associated with individual differences in local functional organization
T2 - Research Square (preprint)
J2 - Res Sq
PY - 2026
DA - 2026/04/08
SN - 2693-5015
PB - Research Square
DO - 10.21203/rs.3.rs-9200088/v1
UR - https://doi.org/10.21203/rs.3.rs-9200088/v1
LA - en
ER -

CSL-JSON

{
"id": "10.21203/rs.3.rs-9200088/v1",
"type": "article",
"title": "Intrinsic cortical geometry is associated with individual differences in local functional organization",
"container-title": "Research Square (preprint)",
"author": [
{
"family": "Alberti",
"given": "Francesco"
},
{
"family": "Bazin",
"given": "Pierre-Louis"
},
{
"family": "Benn",
"given": "R. Austin"
},
{
"family": "Scholz",
"given": "Robert"
},
{
"family": "Wei",
"given": "Wei"
},
{
"family": "Holmes",
"given": "Alexander"
},
{
"family": "Shevchenko",
"given": "Victoria"
},
{
"family": "Klatzmann",
"given": "Ulysse"
},
{
"family": "Pallavicini",
"given": "Carla"
},
{
"family": "Leech",
"given": "Robert"
},
{
"family": "Margulies",
"given": "Daniel S."
}
],
"container-title-short": "Res Sq",
"DOI": "10.21203/rs.3.rs-9200088/v1",
"ISSN": "2693-5015",
"publisher": "Research Square",
"URL": "https://doi.org/10.21203/rs.3.rs-9200088/v1",
"language": "en",
"issued": {
"date-parts": [
[
2026,
4,
8
]
]
}
}

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

Similar papers

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

[1] doi:10.1038/s41467-026-71270-w [code]
Spatiotemporal dynamics of the human cortical functional hierarchy across the lifespan.
Journal: Nature communications
In common: Connectome Workbench, h5py, NiBabel, 6 other tools, 5 references
[2] doi:10.1038/s41467-026-76011-7 [code]
Human cortex organizes dynamic co-fluctuations along the sensorimotor-association axis.
Journal: Nature communications
In common: Connectome Workbench, NiBabel, SciPy, 2 other tools, 7 references
[3] doi:10.1038/s41467-026-72931-6 [code]
Three parsimonious spatiotemporal patterns in cerebellum reveal individual traits in function and behavior.
Journal: Nature communications
In common: Connectome Workbench, NiBabel, statsmodels, 6 other tools, 4 references
[4] doi:10.7554/elife.103097 [code]
Canonical neurodevelopmental trajectories of structural and functional manifolds.
Journal: eLife
In common: reticulate, h5py, NiBabel, 5 other tools, 4 references
[5] doi:10.21203/rs.3.rs-9326213/v1 [code]
Multi-task fMRI outperforms resting-state fMRI for revealing task-invariant organization of the human brain
Journal: Research Square (preprint)
In common: Connectome Workbench, h5py, NiBabel, 6 other tools, 4 references
[6] doi:10.64898/2026.03.09.710558 [code]
Multi-task fMRI outperforms resting-state fMRI for revealing task-invariant organization of the human brain
Journal: bioRxiv (preprint)
In common: Connectome Workbench, h5py, NiBabel, 6 other tools, 4 references
[7] doi:10.1038/s41467-026-75959-w [code]
Charting higher-order models of brain function beyond pairwise interactions.
Journal: Nature communications
In common: h5py, NiBabel, statsmodels, 6 other tools, 4 references
[8] doi:10.1038/s41467-026-76452-0 [code]
Music evokes shared neural representations of imagined narratives across sensory modalities.
Journal: Nature communications
In common: Connectome Workbench, h5py, NiBabel, 7 other tools, 1 reference
[9] doi:10.1162/imag.a.1222 [code]
Network-based near-scalp personalized brain stimulation targets.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: Connectome Workbench, NiBabel, scikit-learn, 3 other tools, 4 references
[10] doi:10.1038/s41467-026-73668-y [code]
Convergent and divergent brain-cognition development in early adolescence.
Journal: Nature communications
In common: Connectome Workbench, NiBabel, seaborn, 5 other tools, 3 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.