OSCR

A language network in the individualized functional connectomes of 1199 human brains doing arbitrary tasks.

Code ↔ Paper

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

The 10 matches
  1. [1] § Results ↔ langlocfc/plot_template_matching.py, lines 61–106 · score 0.85 · template matching, PM PPr, CG OP, dATN, LanA, PMN
  2. [2] § Results ↔ parcellate/model.py, lines 235–308 · score 0.82 · linear sum assignment, CG OP, template matching, reference atlases, PMN, SAL
  3. [3] § Results › LangFC shows expected topography, distinct from other brain networks ↔ langlocfc/plot_oracle.py, lines 30–107 · score 0.74 · PM PPr, CG OP, dATN, LanA, PMN, SAL
  4. [4] § Results › LangFC shows expected topography, distinct from other brain networks ↔ langlocfc/plot_rest_v_task.py, lines 61–106 · score 0.73 · PM PPr, CG OP, dATN, LanA, PMN, SAL
  5. [5] § Methods › Additional analyses › LangFC periphery (Fig. 2C) ↔ parcellate/model.py, lines 548–606 · score 0.59 · Poisson binomial distribution, space, subnetworks, probabilistic, atlas, parcellation
  6. [6] § Results › LangFC differs in function from nearby networks ↔ langlocfc/plot_performance.py, lines 22–81 · score 0.58 · working memory, MD, auditory, scrambled, intact, listening
  7. [7] § Methods › Parcellation and evaluation procedure › Alignment ↔ parcellate/data.py, lines 242–325 · score 0.56 · linear sum assignment, greedy, alignment, error, networks, parcellations
  8. [8] § Methods › Parcellation and evaluation procedure › Sampling ↔ parcellate/model.py, lines 29–111 · score 0.54 · connectivity matrix, voxelwise, downsampling, detrended, components, binarized
  9. [9] § Methods › fMRI data acquisition and preprocessing ↔ parcellate/data.py, lines 123–181 · score 0.53 · MNI space, FWHM, NIFTI, smoothed, template, resampled
  10. [10] § Methods › Parcellation and evaluation procedure › Alignment ↔ parcellate/data.py, lines 189–222 · score 0.50 · linear sum assignment, greedy, alignment, parcellations

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 · 1,728 lines · 72 KB · no license · 2 matches

  1. import shutil
  2. import time
  3. import datetime
  4. import inspect
  5. import yaml
  6. import numpy as np
  7. import pandas as pd
  8. from scipy import optimize
  9. from sklearn.cluster import MiniBatchKMeans
  10. from sklearn.decomposition import PCA, FastICA
  11. from sklearn.metrics import mutual_info_score, normalized_mutual_info_score, adjusted_rand_score
  12. # from sica.base import StabilizedICA
  13. from nilearn import image, masking, maskers, datasets
  14. import nitransforms as nt
  15. from fast_poibin import PoiBin
  16. from parcellate.cfg import *
  17. from parcellate.data import *
  18. from parcellate.util import *
  19. ######################################
  20. #
  21. # CORE METHODS
  22. #
  23. ######################################
  24. def sample(
  25. output_dir,
  26. functional_paths,
  27. n_networks=100,
  28. fwhm=None,
  29. sample_id=None,
  30. xfm_path=None,
  31. mask_path=None,
  32. detrend=True,
  33. standardize=True,
  34. envelope=False,
  35. independent_runs=False,
  36. data_fraction=1,
  37. tr=2,
  38. low_pass=0.1,
  39. high_pass=0.01,
  40. n_samples=None,
  41. n_components_pca='auto',
  42. n_components_ica='auto',
  43. cluster=True,
  44. target_affine=None,
  45. downsample_to=None,
  46. use_connectivity_profile=False,
  47. use_connectivity_to_regions=True,
  48. binarize_connectivity=True,
  49. transform_connectivity=False,
  50. clustering_kwargs=None,
  51. compress_outputs=True,
  52. dump_kwargs=True,
  53. indent=0,
  54. **kwargs
  55. ):
  56. """
  57. Sample parcellations by clustering the voxel timecourses
  58. :param output_dir: ``str``; Output directory
  59. :param functional_paths: ``list``; Paths to functional images
  60. :param n_networks: ``int``; Number of networks to sample
  61. :param fwhm: ``float`` or ``None``; Full-width at half-maximum for spatial smoothing. If ``None``, no smoothing is
  62. applied.
  63. :param sample_id: ``str``; Sample ID
  64. :param xfm_path: ``str`` or ``None``; if the parcellation is not in MNI space, path to
  65. transformation from MNI to the parcellation space (e.g., native), which will be applied to atlases if
  66. applicable (e.g., Schaeffer et al 18 atlases if `use_connectivity_to_regions` is `True`).
  67. If ``None``, parcellation is assumed to be in MNI space.
  68. :param mask_path: ``str`` or ``None``; Path to mask image. If ``None``, a mask will be computed from the first
  69. functional image.
  70. :param detrend: ``bool``; Whether to detrend the timecourses
  71. :param standardize: ``bool``; Whether to standardize the timecourses
  72. :param envelope: ``bool``; Whether to use the envelope of the timecourses
  73. :param independent_runs: ``bool``; Whether to treat each run as an independent sample
  74. :param data_fraction: ``float``; Fraction of data to use. If < 1, randomly subsample the data.
  75. :param tr: ``float``; Repetition time
  76. :param low_pass: ``float`` or ``None``; Low-pass filter cutoff frequency. If ``None``, no low-pass filtering is
  77. applied.
  78. :param high_pass: ``float`` or ``None``; High-pass filter cutoff frequency. If ``None``, no high-pass filtering is
  79. applied.
  80. :param n_samples: ``int``; Number of samples to draw
  81. :param n_components_pca: ``int`` or ``str``; Number of components to retain after PCA. If ``'auto'``, no PCA is
  82. applied.
  83. :param n_components_ica: ``int`` or ``None``; Number of components to retain after ICA. If ``None``, no ICA is
  84. applied.
  85. :param cluster: ``bool``; Whether to cluster the timecourses
  86. :param target_affine: ``list`` or ``None``; Target affine for spatial resampling. If ``None``, no resampling is
  87. applied.
  88. :param downsample_to: ``int`` or ``None``; If given, downsample the columns of the connectivity matrix to this
  89. voxel size (in mm). If ``None``, no downsampling is applied. Ignored if ``use_connectivity_to_regions`` is
  90. ``True``.
  91. :param use_connectivity_profile: ``bool``; Whether to use the connectivity profile. If ``False``, the raw
  92. timecourses are used.
  93. :param use_connectivity_to_regions: ``bool``; Whether to use the connectivity to regions. If ``False``, the
  94. voxelwise connectivity is used.
  95. :param binarize_connectivity: ``bool``; Whether to binarize the connectivity matrix
  96. :param transform_connectivity: ``bool``; Whether to transform the connectivity matrix
  97. :param clustering_kwargs: ``dict`` or ``None``; Keyword arguments to pass to the clustering algorithm
  98. :param compress_outputs: ``bool``; Whether to compress the output files
  99. :param dump_kwargs: ``bool``; Whether to dump the keyword arguments to a YAML file
  100. :param indent: ``int``; Indentation level for progress reporting
  101. :param kwargs: ``dict``; Unused keyword arguments
  102. :return: ``None``
  103. """
  104. if len(kwargs):
  105. stderr('WARNING: Unused keyword arguments to `sample()`: %s\n' % ', '.join(kwargs.keys()))
  106. assert isinstance(sample_id, str), 'sample_id must be given as a str'
  107. assert 0 <= data_fraction <= 1, 'data_fraction must be a proportion between 0 and 1'
  108. t0 = time.time()
  109. stderr('%sSampling (sample_id=%s, n_networks=%d)\n' % (' ' * (indent * 2), sample_id, n_networks))
  110. indent += 1
  111. assert isinstance(output_dir, str), 'output_dir must be provided'
  112. sample_dir = get_path(output_dir, 'subdir', 'sample', sample_id)
  113. if not os.path.exists(sample_dir):
  114. os.makedirs(sample_dir)
  115. if clustering_kwargs is None:
  116. clustering_kwargs = dict(
  117. n_init=N_INIT,
  118. init_size=INIT_SIZE
  119. )
  120. if n_samples is None:
  121. n_samples = 0
  122. if dump_kwargs:
  123. kwargs = dict(
  124. output_dir=output_dir,
  125. functional_paths=functional_paths,
  126. n_networks=n_networks,
  127. fwhm=fwhm,
  128. sample_id=sample_id,
  129. xfm_path=xfm_path,
  130. mask_path=mask_path,
  131. detrend=detrend,
  132. standardize=standardize,
  133. envelope=envelope,
  134. independent_runs=independent_runs,
  135. data_fraction=data_fraction,
  136. tr=tr,
  137. low_pass=low_pass,
  138. high_pass=high_pass,
  139. n_samples=n_samples,
  140. n_components_pca=n_components_pca,
  141. n_components_ica=n_components_ica,
  142. cluster=cluster,
  143. target_affine=target_affine,
  144. use_connectivity_profile=use_connectivity_profile,
  145. use_connectivity_to_regions=use_connectivity_to_regions,
  146. binarize_connectivity=binarize_connectivity,
  147. transform_connectivity=transform_connectivity,
  148. clustering_kwargs=clustering_kwargs,
  149. compress_outputs=compress_outputs
  150. )
  151. kwargs_path = get_path(output_dir, 'kwargs', 'sample', sample_id)
  152. with open(kwargs_path, 'w') as f:
  153. yaml.safe_dump(kwargs, f, sort_keys=False)
  154. output_path = get_path(output_dir, 'output', 'sample', sample_id, compressed=compress_outputs)
  155. t1 = time.time()
  156. stderr('%sLoading timecourses' % (' ' * (indent * 2)))
  157. input_data = InputData(
  158. functional_paths=functional_paths,
  159. fwhm=fwhm,
  160. mask_path=mask_path,
  161. standardize=standardize,
  162. detrend=detrend,
  163. envelope=envelope,
  164. tr=tr,
  165. low_pass=low_pass,
  166. high_pass=high_pass
  167. )
  168. v = input_data.v
  169. df = pd.DataFrame([dict(n_trs=input_data.n_trs, n_runs=input_data.n_runs)])
  170. metadata_path = get_path(output_dir, 'metadata', 'sample', sample_id)
  171. df.to_csv(metadata_path, index=False)
  172. stderr(' (%0.2fs)\n' % (time.time() - t1))
  173. stderr('%sN voxels: %d\n' % (' ' * ((indent + 1) * 2), v))
  174. n_runs = input_data.n_runs
  175. samples_all = []
  176. scores_all = []
  177. if independent_runs:
  178. timecourses = input_data.functionals
  179. else:
  180. timecourses = [input_data.timecourses]
  181. for i, timecourse in enumerate(timecourses):
  182. # Sample parcellations by clustering the voxel timecourses
  183. if n_networks > 256:
  184. dtype=np.uint16
  185. else:
  186. dtype=np.uint8
  187. scores = np.zeros(n_samples) # Shape: <n_samples>
  188. X = timecourse
  189. t = X.shape[-1]
  190. X_img = input_data.nii_ref
  191. X_mask = input_data.mask
  192. if target_affine is not None:
  193. stderr('%sSpatial resampling' % (' ' * (indent * 2)))
  194. t1 = time.time()
  195. X_img = input_data.unflatten(X * (1 + 1e-6)) # Hack to force conversion to float
  196. X_img = image.resample_img(
  197. X_img, target_affine=np.diag(np.array(target_affine)), copy_header=True, force_resample=True
  198. )
  199. X_mask = image.new_img_like(input_data.nii_ref, input_data.mask * (1 + 1e-6))
  200. X_mask = image.resample_img(
  201. X_mask, target_affine=np.diag(target_affine), copy_header=True, force_resample=True
  202. )
  203. X_mask = image.get_data(X_mask) > 0.5
  204. X = image.get_data(X_img)[X_mask]
  205. v = X.shape[0]
  206. stderr(' (%0.2fs)\n' % (time.time() - t1))
  207. if n_components_pca:
  208. n_components = n_components_pca
  209. if n_components == 'auto':
  210. n_components = n_networks
  211. stderr('%sPCA transforming (n components = %s)' % (' ' * (indent * 2), n_components))
  212. t1 = time.time()
  213. n_components = min(n_components, t)
  214. m = PCA(n_components=n_components, svd_solver='auto', whiten=True)
  215. X = m.fit_transform(X)
  216. stderr(' (%0.2fs)\n' % (time.time() - t1))
  217. if n_components_ica:
  218. n_components = n_components_ica
  219. if n_components == 'auto':
  220. n_components = n_networks
  221. n_components = min(n_components, X.shape[-1])
  222. stderr('%sICA transforming (n components = %s)' % (' ' * (indent * 2), n_components))
  223. t1 = time.time()
  224. m = FastICA(n_components=n_components, whiten='unit-variance')
  225. X = m.fit_transform(X)
  226. stderr(' (%0.2fs)\n' % (time.time() - t1))
  227. if use_connectivity_profile:
  228. A = standardize_array(X)
  229. if use_connectivity_to_regions:
  230. stderr('%sRetrieving connectivity atlas' % (' ' * (indent * 2)))
  231. t1 = time.time()
  232. B_img = input_data.unflatten(X, mask=X_mask, nii_ref=X_img)
  233. X_mask_img = image.new_img_like(X_img, X_mask > 0.5)
  234. anat_atlas = datasets.fetch_atlas_schaefer_2018(n_rois=1000)
  235. atlas_filename = anat_atlas.maps
  236. atlas_nii = image.smooth_img(atlas_filename, None)
  237. atlas_nii = image.new_img_like(atlas_nii, image.get_data(atlas_nii).astype(np.uint16))
  238. if xfm_path is not None:
  239. xfm = nt.manip.load(xfm_path)
  240. labels = np.unique(image.get_data(atlas_nii))
  241. atlas_nii = image.new_img_like(
  242. atlas_nii,
  243. (image.get_data(atlas_nii)[..., None] == labels[None, None, None, ...]).astype(np.uint16)
  244. )
  245. atlas_nii = nt.resampling.apply(
  246. xfm,
  247. atlas_nii,
  248. input_data.nii_ref,
  249. )
  250. atlas_nii = image.new_img_like(
  251. atlas_nii,
  252. np.argmax(image.get_data(atlas_nii), axis=-1).astype(np.uint16)
  253. )
  254. masker = maskers.NiftiLabelsMasker(labels_img=atlas_nii, mask_img=X_mask_img)
  255. B = standardize_array(masker.fit_transform(B_img).T)
  256. stderr(' (%0.2fs)\n' % (time.time() - t1))
  257. elif downsample_to is not None:
  258. stderr('%sDownsampling columns of connectivity matrix to %d mm' % (' ' * (indent * 2), downsample_to))
  259. t1 = time.time()
  260. B = input_data.unflatten(X * (1 + 1e-6)) # Hack to force conversion to float
  261. B = image.resample_img(
  262. B, target_affine=np.diag(np.array([downsample_to] * 3)), copy_header=True, force_resample=True
  263. )
  264. mask_ = image.new_img_like(input_data.nii_ref, input_data.mask * (1 + 1e-6))
  265. mask_ = image.resample_img(
  266. mask_, target_affine=np.diag(np.array([downsample_to] * 3)), copy_header=True, force_resample=True
  267. )
  268. mask_ = image.get_data(mask_) > 0.5
  269. B = standardize_array(image.get_data(B)[mask_])
  270. stderr(' (%0.2fs)\n' % (time.time() - t1))
  271. else:
  272. B = A
  273. stderr('%sComputing connectivity matrix' % (' ' * (indent * 2)))
  274. t1 = time.time()
  275. X = np.dot(
  276. A,
  277. B.T
  278. )
  279. if binarize_connectivity:
  280. X = (X > np.quantile(X, 0.9)).astype(int)
  281. stderr(' (%0.2fs)\n' % (time.time() - t1))
  282. if transform_connectivity:
  283. stderr('%sTransforming connectivity matrix\n' % (' ' * (indent * 2)))
  284. if n_components_pca:
  285. n_components = n_components_pca
  286. if n_components == 'auto':
  287. n_components = n_networks
  288. stderr('%sPCA transforming (n components = %s)' % (' ' * (indent * 2), n_components))
  289. t1 = time.time()
  290. n_components = min(n_components, t)
  291. m = PCA(n_components=n_components, svd_solver='auto', whiten=True)
  292. X = m.fit_transform(X)
  293. stderr(' (%0.2fs)\n' % (time.time() - t1))
  294. if n_components_ica:
  295. n_components = n_components_ica
  296. if n_components == 'auto':
  297. n_components = n_networks
  298. n_components = min(n_components, X.shape[-1])
  299. stderr('%sICA transforming (n components = %s)\n' % (' ' * (indent * 2), n_components))
  300. t1 = time.time()
  301. m = FastICA(n_components=n_components, whiten='unit-variance')
  302. X = m.fit_transform(X)
  303. stderr(' (%0.2fs)\n' % (time.time() - t1))
  304. if n_samples:
  305. stderr('%sDrawing samples\n' % (' ' * (indent * 2)))
  306. indent += 1
  307. samples = np.zeros((v, n_samples), dtype=dtype) # Shape: <n_voxels, n_samples>
  308. for j in range(n_samples):
  309. if len(timecourses) > 1:
  310. suffix = ' for run %d/%d' % (i + 1, n_runs)
  311. else:
  312. suffix = ''
  313. if n_samples > 1:
  314. stderr('\r%sSample %d/%d%s' % (' ' * (indent * 2), j + 1, n_samples, suffix))
  315. if cluster:
  316. m = MiniBatchKMeans(n_clusters=n_networks, **clustering_kwargs)
  317. _sample = m.fit_predict(X)
  318. _score = m.inertia_
  319. samples[:, j] = _sample
  320. else:
  321. X_ = X
  322. n_components = n_networks
  323. m = FastICA(n_components=n_components, whiten='unit-variance')
  324. X = m.fit_transform(X_)
  325. # Minmax normalize
  326. _sample = X[..., :n_networks]
  327. _sample = np.clip(_sample, 0, np.inf)
  328. _sample = _sample / _sample.max(axis=0, keepdims=True)
  329. _score = 0
  330. if j == 0:
  331. samples = _sample
  332. else:
  333. R = np.dot(standardize_array(samples, axis=0).T, standardize_array(_sample, axis=0))
  334. ix_r, ix_c = optimize.linear_sum_assignment(R, maximize=True)
  335. _sample = _sample[:, ix_c]
  336. samples = (samples * j + _sample) / (j + 1)
  337. scores[j] = _score
  338. if n_samples > 1:
  339. stderr('\n')
  340. indent -= 1
  341. else:
  342. assert n_components_pca or n_components_ica, 'Must use PCA or ICA if not sampling'
  343. assert X.shape == (v, n_networks), 'X must have shape (%s, %s) if not sampling. ' \
  344. 'Got shape %s. Check to make sure that there is ' \
  345. 'at least one matrix decomposition (PCA or ICA) and that the final ' \
  346. 'decomposition has n_components equal to n_networks.' % \
  347. (v, n_networks, str(X.shape))
  348. # Assume a network covers < half the mask volume, flip sign accordingly
  349. X = np.where(np.median(X, axis=0, keepdims=True) > 0, -X, X)
  350. # Clip and normalize (scale is arbitrary)
  351. uq = np.quantile(X, 0.99, axis=0, keepdims=True)
  352. X = np.clip(X, 0, uq) / uq
  353. samples = X.astype(np.float32)
  354. scores = np.zeros((1,))
  355. if target_affine is not None:
  356. samples = input_data.unflatten(samples, mask=X_mask, nii_ref=X_img)
  357. samples = image.resample_to_img(
  358. samples, input_data.nii_ref, interpolation='nearest', copy_header=True, force_resample=True
  359. )
  360. samples = input_data.flatten(samples)
  361. samples_all.append(samples)
  362. scores_all.append(scores)
  363. samples = np.concatenate(samples_all, axis=-1)
  364. samples = input_data.unflatten(samples)
  365. samples.to_filename(output_path)
  366. scores = pd.DataFrame({'sample_score': np.concatenate(scores_all, axis=0)})
  367. evaluation_path = get_path(output_dir, 'evaluation', 'sample', sample_id, compressed=compress_outputs)
  368. scores.to_csv(evaluation_path, index=False)
  369. stderr('%sSampling time: %ds\n' % (' ' * (indent * 2), time.time() - t0))
  370. def align(
  371. output_dir,
  372. alignment_id=None,
  373. sample_id=None,
  374. mask_path=None,
  375. n_alignments=None,
  376. top_k=None,
  377. sort_by_mi=False,
  378. weight_samples=False,
  379. prealign=False,
  380. scoring_method='corr',
  381. minmax_normalize=True,
  382. compress_outputs=True,
  383. dump_kwargs=True,
  384. indent=0,
  385. **kwargs
  386. ):
  387. """
  388. Align sampled clusterings into a probabilistic parcellation.
  389. :param output_dir: ``str``; Output directory
  390. :param alignment_id: ``str``; Alignment ID
  391. :param sample_id: ``str``; Sample ID
  392. :param mask_path: ``str`` or ``None``; Path to mask image. If ``None``, a mask will be computed from the first
  393. functional image.
  394. :param n_alignments: ``int`` or ``None``; Number of alignments to perform. If ``None``, use the number of samples.
  395. :param top_k: ``int`` or ``None``; Number of samples to retain. If ``None``, use all samples.
  396. :param sort_by_mi: ``bool``; Whether to sort samples by mutual information
  397. :param weight_samples: ``bool``; Whether to weight samples by their scores
  398. :param prealign: ``bool``; Whether to prealign the samples using an initial alignment pass
  399. :param scoring_method: ``str``; Scoring method for alignment
  400. :param minmax_normalize: ``bool``; Whether to minmax normalize the aligned networks
  401. :param compress_outputs: ``bool``; Whether to compress the output files
  402. :param dump_kwargs: ``bool``; Whether to dump the keyword arguments to a YAML file
  403. :param indent: ``int``; Indentation level for progress reporting
  404. :param kwargs: ``dict``; Unused keyword arguments
  405. :return: ``None``
  406. """
  407. if len(kwargs):
  408. stderr('WARNING: Unused keyword arguments to `align()`: %s\n' % ', '.join(kwargs.keys()))
  409. assert isinstance(alignment_id, str), 'alignment_id must be given as a str'
  410. assert isinstance(sample_id, str), 'sample_id must be given as a str'
  411. t0 = time.time()
  412. stderr('%sAligning (alignment_id=%s)\n' % (' ' * (indent * 2), alignment_id))
  413. indent += 1
  414. scoring_method = scoring_method.lower()
  415. assert isinstance(output_dir, str), 'output_dir must be provided'
  416. alignment_dir = get_path(output_dir, 'subdir', 'align', alignment_id)
  417. if not os.path.exists(alignment_dir):
  418. os.makedirs(alignment_dir)
  419. if dump_kwargs:
  420. kwargs = dict(
  421. alignment_id=alignment_id,
  422. sample_id=sample_id,
  423. mask_path=mask_path,
  424. n_alignments=n_alignments,
  425. top_k=top_k,
  426. sort_by_mi=sort_by_mi,
  427. weight_samples=weight_samples,
  428. prealign=prealign,
  429. scoring_method=scoring_method,
  430. minmax_normalize=minmax_normalize,
  431. compress_outputs=compress_outputs,
  432. output_dir=output_dir,
  433. )
  434. kwargs_path = get_path(output_dir, 'kwargs', 'align', alignment_id)
  435. with open(kwargs_path, 'w') as f:
  436. yaml.safe_dump(kwargs, f, sort_keys=False)
  437. output_path = get_path(output_dir, 'output', 'align', alignment_id, compressed=compress_outputs)
  438. sample_path = get_path(output_dir, 'output', 'sample', sample_id, compressed=compress_outputs)
  439. assert os.path.exists(sample_path), 'Sample file %s not found' % sample_path
  440. data = Data(
  441. nii_ref_path=sample_path,
  442. mask_path=mask_path
  443. )
  444. sample_nii = data.nii_ref
  445. if image.get_data(sample_nii).dtype in (np.uint8, np.uint16):
  446. samples = data.flatten(sample_nii)
  447. samples = samples.T # Shape: <n_samples, n_voxels>, values are integer network indices
  448. # Get sample scores
  449. sample_scores = pd.read_csv(get_path(output_dir, 'evaluation', 'sample', sample_id))['sample_score'].values
  450. sample_scores = minmax_normalize_array(sample_scores) # Lower inertia is better
  451. # Compute sample orders/weights
  452. if sort_by_mi: # By pairwise similarity
  453. # Compute similarity between samples
  454. # MI = np.zeros((samples.shape[0], samples.shape[0]))
  455. # for i in range(samples.shape[0]):
  456. # for j in range(i + 1, samples.shape[0]):
  457. # print(i, j)
  458. # mi = mutual_info_score(samples[i], samples[j])
  459. # # mi = normalized_mutual_info_score(samples[i], samples[j])
  460. # # mi = adjusted_rand_score(samples[i], samples[j])
  461. # MI[i, j] = mi
  462. # MI[j, i] = mi
  463. # MI[np.diag_indices(MI.shape[0])] = 1
  464. # MI_mean = MI.mean(axis=0)
  465. # ix = np.argmax(MI_mean)
  466. # mi = MI[ix]
  467. mi = np.zeros(samples.shape[0])
  468. ix = np.argmin(sample_scores)
  469. for i in range(samples.shape[0]):
  470. mi[i] = mutual_info_score(samples[ix], samples[i])
  471. s_ix = np.argsort(mi)[::-1]
  472. samples = samples[s_ix]
  473. w = mi[s_ix]
  474. else: # By inertia
  475. s_ix = np.argsort(sample_scores)
  476. samples = samples[s_ix]
  477. sample_scores = sample_scores[s_ix]
  478. w = 1 - sample_scores # Flip to upweight lower inertia
  479. w = minmax_normalize_array(w)
  480. # Align to reference
  481. if top_k:
  482. samples = samples[:top_k]
  483. w = w[:top_k]
  484. f_kwargs = dict(
  485. samples=samples,
  486. scoring_method='corr',
  487. n_alignments=n_alignments,
  488. prealign=prealign,
  489. indent=indent + 1
  490. )
  491. if weight_samples:
  492. f_kwargs['w'] = w
  493. parcellation = align_samples(**f_kwargs)
  494. else:
  495. stderr('%sSamples are already continuous, likely due to being a matrix decomposition. '
  496. 'Skipping alignment...\n' % (' ' * (indent * 2)))
  497. parcellation = data.flatten(sample_nii).T
  498. if minmax_normalize:
  499. parcellation = minmax_normalize_array(parcellation)
  500. parcellation = data.unflatten(parcellation.T)
  501. parcellation.to_filename(output_path)
  502. stderr('%sAlignment time: %ds\n' % (' ' * (indent * 2), time.time() - t0))
  503. def label(
  504. output_dir,
  505. reference_atlases='default',
  506. labeling_id=None,
  507. alignment_id=None,
  508. sample_id=None,
  509. xfm_path=None,
  510. mask_path=None,
  511. average_first=True,
  512. scoring_method='corr',
  513. atlas_threshold=None,
  514. max_subnetworks=None,
  515. minmax_normalize=True,
  516. use_poibin=True,
  517. eps=1e-3,
  518. compress_outputs=True,
  519. dump_kwargs=True,
  520. indent=0,
  521. **kwargs
  522. ):
  523. """
  524. Label the parcellation against reference atlases
  525. :param output_dir: ``str``; Output directory
  526. :param reference_atlases: ``str`` or ``list``; Reference atlases
  527. :param labeling_id: ``str``; Labeling ID
  528. :param alignment_id: ``str``; Alignment ID
  529. :param sample_id: ``str``; Sample ID
  530. :param xfm_path: ``str`` or ``None``; if the parcellation is not in MNI space, path to
  531. transformation from MNI to the parcellation space (e.g., native), which will be applied to atlases.
  532. If ``None``, parcellation is assumed to be in MNI space.
  533. :param mask_path: ``str`` or ``None``; path to GM mask image. If ``None``, compute the mask in MNI space.
  534. :param average_first: ``bool``; Whether to average the samples before labeling
  535. :param scoring_method: ``str``; Scoring method for labeling
  536. :param atlas_threshold: ``float`` or ``None``; Threshold for binarizing the atlas. If ``None``, no binarization is
  537. applied.
  538. :param max_subnetworks: ``int`` or ``None``; Maximum number of subnetworks to retain. If ``None``, no bound on
  539. the number of subnetworks.
  540. :param minmax_normalize: ``bool``; Whether to minmax normalize the labeled networks
  541. :param use_poibin: ``bool``; Whether to use the Poisson binomial distribution for computing probabilities in
  542. composite networks
  543. :param eps: ``float``; Epsilon for numerical stability
  544. :param compress_outputs: ``bool``; Whether to compress the output files
  545. :param dump_kwargs: ``bool``; Whether to dump the keyword arguments to a YAML file
  546. :param indent: ``int``; Indentation level for progress reporting
  547. :param kwargs: ``dict``; Unused keyword arguments
  548. :return: ``None``
  549. """
  550. if len(kwargs):
  551. stderr('WARNING: Unused keyword arguments to `label()`: %s\n' % ', '.join(kwargs.keys()))
  552. assert isinstance(labeling_id, str), 'alignment_id must be given as a str'
  553. t0 = time.time()
  554. stderr('%sLabeling (labeling_id=%s)\n' % (' ' * (indent * 2), labeling_id))
  555. indent += 1
  556. scoring_method = scoring_method.lower()
  557. assert isinstance(output_dir, str), 'output_dir must be provided'
  558. labeling_dir = get_path(output_dir, 'subdir', 'label', labeling_id)
  559. if not os.path.exists(labeling_dir):
  560. os.makedirs(labeling_dir)
  561. if dump_kwargs:
  562. kwargs = dict(
  563. reference_atlases=reference_atlases,
  564. labeling_id=labeling_id,
  565. alignment_id=alignment_id,
  566. sample_id=sample_id,
  567. xfm_path=xfm_path,
  568. mask_path=mask_path,
  569. average_first=average_first,
  570. scoring_method=scoring_method,
  571. atlas_threshold=atlas_threshold,
  572. max_subnetworks=max_subnetworks,
  573. minmax_normalize=minmax_normalize,
  574. use_poibin=use_poibin,
  575. eps=eps,
  576. compress_outputs=compress_outputs,
  577. output_dir=output_dir,
  578. )
  579. kwargs_path = get_path(output_dir, 'kwargs', 'label', labeling_id)
  580. with open(kwargs_path, 'w') as f:
  581. yaml.safe_dump(kwargs, f, sort_keys=False)
  582. output_path = get_path(output_dir, 'output', 'label', labeling_id, compressed=compress_outputs)
  583. if average_first:
  584. assert alignment_id is not None, 'alignment_id must be provided if average_first is True'
  585. input_path = get_path(output_dir, 'output', 'align', alignment_id, compressed=compress_outputs)
  586. assert os.path.exists(input_path), 'Alignment file %s not found' % input_path
  587. else:
  588. assert sample_id is not None, 'sample_id must be provided if average_first is False'
  589. input_path = get_path(output_dir, 'output', 'sample', sample_id, compressed=compress_outputs)
  590. assert os.path.exists(input_path), 'Sample file %s not found' % input_path
  591. input_nii = image.smooth_img(input_path, None)
  592. reference_data = AtlasData(
  593. atlases=reference_atlases,
  594. resampling_target_nii=input_nii,
  595. compress_outputs=compress_outputs,
  596. xfm_path=xfm_path,
  597. mask_path=mask_path
  598. )
  599. reference_atlas_names = reference_data.atlas_names
  600. reference_atlases = reference_data.atlases
  601. v = reference_data.v
  602. reference_data.save_atlases(labeling_dir, prefix=REFERENCE_ATLAS_PREFIX)
  603. input_data = reference_data.flatten(input_nii)
  604. if average_first:
  605. n_networks = input_data.shape[-1]
  606. n_samples = None
  607. else:
  608. n_networks = int(input_data.max() + 1)
  609. n_samples = input_data.shape[-1]
  610. input_data = input_data.T # Shape: <(n_networks | n_samples), v>, values are integer network indices
  611. if not max_subnetworks:
  612. max_subnetworks = n_networks
  613. # Find candidate network(s) for each reference
  614. n_reference_atlases = len(reference_atlas_names)
  615. reference_atlas_scores = np.full((n_reference_atlases,), -np.inf)
  616. candidates = {}
  617. results = []
  618. indent += 1
  619. for j, reference_atlas_name in enumerate(reference_atlas_names):
  620. stderr('%sAtlas: %s\n' % (' ' * (indent * 2), reference_atlas_name))
  621. reference_atlas = reference_atlases[reference_atlas_name]
  622. if atlas_threshold is not None:
  623. _reference_atlas = binarize_array(reference_atlas, threshold=atlas_threshold)
  624. else:
  625. _reference_atlas = reference_atlas
  626. if average_first:
  627. scores = np.zeros(n_networks)
  628. for ni in range(n_networks):
  629. if scoring_method == 'corr':
  630. _score = np.corrcoef(input_data[ni], _reference_atlas)[0, 1]
  631. elif scoring_method == 'avg':
  632. _score = np.dot(input_data[ni], _reference_atlas) / input_data[ni].sum()
  633. else:
  634. raise ValueError('Unrecognized scoring method %s.' % scoring_method)
  635. scores[ni] = _score
  636. reference_ix = np.argsort(scores, axis=-1)[::-1]
  637. else:
  638. indent += 1
  639. stderr('%sDirectly aligning samples to reference\n' % (' ' * (indent * 2)))
  640. samples_relabeled = np.zeros_like(input_data)
  641. scores = np.zeros((n_samples, n_networks))
  642. if scoring_method == 'corr':
  643. _reference_atlas = standardize_array(_reference_atlas)
  644. indent += 1
  645. for si in range(n_samples):
  646. stderr('\r%sSample %d/%d' % (' ' * (indent * 2), si + 1, n_samples))
  647. networks = (input_data[si][None, ...] == np.arange(n_networks)[..., None]).astype(float)
  648. if scoring_method == 'corr':
  649. _networks = standardize_array(networks)
  650. _scores = np.dot(
  651. _networks,
  652. _reference_atlas.T
  653. ) / v
  654. else:
  655. num = np.dot(
  656. networks,
  657. _reference_atlas.T
  658. )
  659. denom = networks.sum(axis=-1)
  660. denom[np.where(denom == 0)] = 1
  661. _scores = num / denom
  662. sort_ix = np.argsort(_scores)[::-1]
  663. ranks = np.argsort(sort_ix)
  664. scores[si] = _scores[sort_ix]
  665. samples_relabeled[si] = ranks[input_data[si]]
  666. input_data = samples_relabeled
  667. reference_ix = np.arange(n_networks)
  668. stderr('\n')
  669. indent -= 2
  670. candidate = None
  671. r = -np.inf
  672. r_prev = -np.inf
  673. candidate_list = []
  674. candidate_scores = []
  675. indent += 1
  676. for ni in range(max_subnetworks):
  677. stderr('\r%sSubnetwork %d' % (' ' * (indent * 2), ni + 1))
  678. ix = reference_ix[ni]
  679. if average_first:
  680. _score = scores[ix]
  681. else:
  682. _score = np.tanh(np.arctanh(scores[:, ix] * (1 - 2 * eps) + eps).mean(axis=-1))
  683. candidate_scores.append(_score)
  684. _candidate = candidate
  685. if average_first:
  686. candidate = input_data[ix]
  687. else:
  688. candidate = input_data == ni
  689. weights = scores[:, ni:ni+1]
  690. weights = minmax_normalize_array(weights)
  691. candidate = (candidate * weights).sum(axis=0) / weights.sum()
  692. candidate = np.clip(candidate, 0, 1)
  693. candidate_list.append(candidate)
  694. if use_poibin and ni > 0:
  695. __candidate = np.zeros(v)
  696. for _v in range(v):
  697. p = 1 - PoiBin([c[_v] for c in candidate_list]).cdf[0]
  698. __candidate[_v] = p
  699. candidate = __candidate
  700. else:
  701. candidate = np.stack(candidate_list, axis=-1).sum(axis=-1)
  702. if scoring_method == 'corr':
  703. r = np.corrcoef(candidate, reference_atlas)[0, 1]
  704. elif scoring_method == 'avg':
  705. r = np.dot(candidate, reference_atlas) / candidate.sum()
  706. else:
  707. raise ValueError('Unrecognized scoring method %s.' % scoring_method)
  708. if r <= r_prev:
  709. candidate_list = candidate_list[:-1]
  710. candidate_scores = candidate_scores[:-1]
  711. candidate = _candidate
  712. r = r_prev
  713. break
  714. r_prev = r
  715. stderr('\n')
  716. indent -= 1
  717. if not use_poibin:
  718. candidate = minmax_normalize_array(candidate)
  719. reference_atlas_scores[j] = r
  720. candidate_list.insert(0, candidate)
  721. candidate_scores.insert(0, r)
  722. for s, candidate in enumerate(candidate_list):
  723. row = {
  724. 'parcel': reference_atlas_name if s == 0 else '%s_sub%d' % (reference_atlas_name, s),
  725. '%sname' % REFERENCE_ATLAS_PREFIX: reference_atlas_name,
  726. '%sscore' % REFERENCE_ATLAS_PREFIX: candidate_scores[s]
  727. }
  728. if s == 0:
  729. row['parcel_type'] = 'network'
  730. else:
  731. row['parcel_type'] = 'subnetwork%d' % s
  732. if minmax_normalize:
  733. candidate = minmax_normalize_array(candidate)
  734. candidate_list[s] = candidate
  735. row['n_voxels'] = candidate.sum()
  736. results.append(row)
  737. candidate = reference_data.unflatten(candidate)
  738. if s == 0:
  739. suffix = ''
  740. else:
  741. suffix = '_sub%d' % s
  742. suffix += get_suffix(compress_outputs)
  743. candidate.to_filename(join(labeling_dir, '%s%s' % (reference_atlas_name, suffix)))
  744. candidates[reference_atlas_name] = candidate_list
  745. indent -= 1
  746. results = pd.DataFrame(results)
  747. results.to_csv(output_path, index=False)
  748. stderr('%sLabeling time: %ds\n' % (' ' * (indent * 2), time.time() - t0))
  749. def evaluate(
  750. output_dir,
  751. evaluation_atlases=None,
  752. evaluation_map=None,
  753. evaluation_id=None,
  754. labeling_id=None,
  755. xfm_path=None,
  756. transform_evaluation_atlases=False,
  757. mask_path=None,
  758. network_threshold=None,
  759. compress_outputs=True,
  760. dump_kwargs=True,
  761. indent=0,
  762. **kwargs
  763. ):
  764. """
  765. Evaluate the labeled atlases against task maps (evaluation atlases)
  766. :param output_dir: ``str``; Output directory
  767. :param evaluation_atlases: ``dict`` or ``None``; Map from evaluation atlas names to paths. If ``None``, no
  768. evaluation will be run.
  769. :param evaluation_map: ``dict`` or ``None``; Map from network names to evaluations to perform. If ``None``,
  770. all evaluations will be performed on each network.
  771. :param evaluation_id: ``str``; Evaluation ID
  772. :param labeling_id: ``str``; Labeling ID
  773. :param xfm_path: ``str`` or ``None``; if the parcellation is not in MNI space, path to
  774. transformation from MNI to the parcellation space (e.g., native), which will be applied to atlases.
  775. If ``None``, parcellation is assumed to be in MNI space.
  776. :param transform_evaluation_atlases: ``bool``; Whether to transform the evaluation atlases from MNI space to
  777. the parcellation space. Ignored unless `xfm` is provided. Defaults to ``False``, i.e., assumes that the
  778. evaluation atlases (subject-specific statmaps) are estimated in the same space as the parcellation. Only use
  779. ``True`` if the evaluation atlases are estimated in MNI space.
  780. :param mask_path: ``str`` or ``None``; path to GM mask image. If ``None``, compute the mask in MNI space.
  781. :param network_threshold: ``float`` or ``None``; Threshold for binarizing the networks. If ``None``, no
  782. binarization is applied.
  783. :param compress_outputs: ``bool``; Whether to compress the output files
  784. :param dump_kwargs: ``bool``; Whether to dump the keyword arguments to a YAML file
  785. :param indent: ``int``; Indentation level for progress reporting
  786. :param kwargs: ``dict``; Unused keyword arguments
  787. :return: ``None``
  788. """
  789. if len(kwargs):
  790. stderr('WARNING: Unused keyword arguments to `evaluate()`: %s\n' % ', '.join(kwargs.keys()))
  791. assert isinstance(evaluation_id, str), 'evaluation_id must be given as a str'
  792. assert isinstance(labeling_id, str), 'alignment_id must be given as a str'
  793. t0 = time.time()
  794. stderr('%sEvaluating (evaluation_id=%s)\n' % (' ' * (indent * 2), evaluation_id))
  795. indent += 1
  796. assert isinstance(output_dir, str), 'output_dir must be provided'
  797. evaluation_dir = get_path(output_dir, 'subdir', 'evaluate', evaluation_id)
  798. if not os.path.exists(evaluation_dir):
  799. os.makedirs(evaluation_dir)
  800. if dump_kwargs:
  801. kwargs = dict(
  802. output_dir=output_dir,
  803. evaluation_atlases=evaluation_atlases,
  804. evaluation_map=evaluation_map,
  805. evaluation_id=evaluation_id,
  806. labeling_id=labeling_id,
  807. xfm_path=xfm_path,
  808. transform_evaluation_atlases=transform_evaluation_atlases,
  809. mask_path=mask_path,
  810. network_threshold=network_threshold,
  811. compress_outputs=compress_outputs,
  812. )
  813. kwargs_path = get_path(output_dir, 'kwargs', 'evaluate', evaluation_id)
  814. with open(kwargs_path, 'w') as f:
  815. yaml.safe_dump(kwargs, f, sort_keys=False)
  816. output_path = get_path(output_dir, 'output', 'evaluate', evaluation_id, compressed=compress_outputs)
  817. suffix = get_suffix(compress_outputs)
  818. if evaluation_atlases is None:
  819. evaluation_atlases = {}
  820. # Collect references atlases and alignments
  821. labeling_dir = get_path(output_dir, 'subdir', 'label', labeling_id)
  822. label_kwargs = get_cfg(get_path(output_dir, 'kwargs', 'label', labeling_id))
  823. if evaluation_map is None:
  824. evaluation_map = {}
  825. for path in os.listdir(labeling_dir):
  826. if path.startswith(REFERENCE_ATLAS_PREFIX):
  827. reference_atlas_name = path[len(REFERENCE_ATLAS_PREFIX):-len(suffix)]
  828. evaluation_map[reference_atlas_name] = list(evaluation_atlases.keys())
  829. else:
  830. for path in os.listdir(labeling_dir):
  831. if path.startswith(REFERENCE_ATLAS_PREFIX):
  832. reference_atlas_name = path[len(REFERENCE_ATLAS_PREFIX):-len(suffix)]
  833. if reference_atlas_name not in evaluation_map:
  834. evaluation_map[reference_atlas_name] = []
  835. reference_atlas_names = label_kwargs['reference_atlases']
  836. if reference_atlas_names is None:
  837. reference_atlas_names = []
  838. elif isinstance(reference_atlas_names, str):
  839. reference_atlas_names = [reference_atlas_names]
  840. _reference_atlas_names = []
  841. for reference_atlas in reference_atlas_names:
  842. if isinstance(reference_atlas, str) and reference_atlas.lower() in ('default', 'all', 'all_reference'):
  843. reference_atlas = ALL_REFERENCE
  844. elif isinstance(reference_atlas, dict):
  845. reference_atlas = list(reference_atlas.keys())
  846. else:
  847. reference_atlas = [reference_atlas]
  848. _reference_atlas_names.extend(reference_atlas)
  849. reference_atlas_names = _reference_atlas_names
  850. reference_atlases = []
  851. candidates = {}
  852. resampling_target_nii = None
  853. for reference_atlas in reference_atlas_names:
  854. reference_atlas_path = join(labeling_dir, '%s%s%s' % (REFERENCE_ATLAS_PREFIX, reference_atlas, suffix))
  855. assert os.path.exists(reference_atlas_path), 'Reference atlas %s not found' % reference_atlas_path
  856. reference_atlases.append({reference_atlas: reference_atlas_path})
  857. for path in os.listdir(labeling_dir):
  858. if path.startswith(reference_atlas) and path.endswith(suffix):
  859. atlas_name = path[:-len(suffix)]
  860. atlas_name = re.sub(r'_sub\d+$', '', atlas_name)
  861. if atlas_name == reference_atlas:
  862. trim = len(suffix)
  863. name = path[:-trim]
  864. path = join(labeling_dir, path)
  865. if reference_atlas not in candidates:
  866. candidates[reference_atlas] = {}
  867. candidates[reference_atlas][name] = get_nii(path, add_to_cache=False)
  868. if network_threshold:
  869. data = image.get_data(candidates[reference_atlas][name])
  870. data = binarize_array(data, threshold=network_threshold)
  871. candidates[reference_atlas][name] = image.new_img_like(candidates[reference_atlas][name], data)
  872. if resampling_target_nii is None:
  873. resampling_target_nii = candidates[reference_atlas][name]
  874. # Format data
  875. reference_data = AtlasData(
  876. atlases=reference_atlases,
  877. resampling_target_nii=resampling_target_nii,
  878. compress_outputs=compress_outputs,
  879. xfm_path=xfm_path,
  880. mask_path=mask_path
  881. )
  882. reference_atlases = reference_data.atlases
  883. evaluation_kwargs = dict(
  884. atlases=evaluation_atlases,
  885. resampling_target_nii=resampling_target_nii,
  886. compress_outputs=compress_outputs,
  887. mask_path=mask_path
  888. )
  889. if xfm_path is not None and transform_evaluation_atlases:
  890. evaluation_kwargs['xfm_path'] = xfm_path
  891. evaluation_data = AtlasData(**evaluation_kwargs)
  892. evaluation_atlases = evaluation_data.atlases
  893. evaluation_data.save_atlases(evaluation_dir, prefix=EVALUATION_ATLAS_PREFIX)
  894. for x in candidates:
  895. for y in candidates[x]:
  896. candidates[x][y] = reference_data.flatten(candidates[x][y])
  897. stderr(' ' * (indent * 2) + 'Results:\n')
  898. results = []
  899. for reference_atlas_name in reference_atlas_names:
  900. # Score reference atlas as if it were a candidate parcellation (baseline)
  901. reference_atlas = reference_atlases[reference_atlas_name]
  902. _evaluation_atlases = {x: evaluation_atlases[x] for x in evaluation_atlases
  903. if x in evaluation_map[reference_atlas_name]}
  904. atlas = reference_atlas
  905. atlas_name = '%s%s' % (REFERENCE_ATLAS_PREFIX, reference_atlas_name)
  906. row = _get_evaluation_row(
  907. atlas,
  908. atlas_name,
  909. reference_atlas_name=reference_atlas_name,
  910. reference_atlases=reference_atlases,
  911. evaluation_atlases=_evaluation_atlases
  912. )
  913. row['parcel_type'] = 'baseline'
  914. results.append(row)
  915. stderr(_pretty_print_evaluation_row(row, indent=indent + 1) + '\n')
  916. # Score evaluation atlases as if they were candidate parcellations (baseline)
  917. for evaluation_atlas_name in _evaluation_atlases:
  918. atlas = _evaluation_atlases[evaluation_atlas_name]
  919. atlas_name = '%s%s' % (EVALUATION_ATLAS_PREFIX, evaluation_atlas_name)
  920. row = _get_evaluation_row(
  921. atlas,
  922. atlas_name,
  923. reference_atlas_name=reference_atlas_name,
  924. reference_atlases=reference_atlases,
  925. evaluation_atlases=_evaluation_atlases
  926. )
  927. row['parcel_type'] = 'baseline'
  928. results.append(row)
  929. # Score candidate parcellations
  930. candidate_names = sorted(
  931. list(candidates[reference_atlas_name].keys()),
  932. key=candidate_name_sort_key
  933. )
  934. for c, atlas_name in enumerate(candidate_names):
  935. atlas = candidates[reference_atlas_name][atlas_name]
  936. row = _get_evaluation_row(
  937. atlas,
  938. atlas_name,
  939. reference_atlas_name=reference_atlas_name,
  940. reference_atlases=reference_atlases,
  941. evaluation_atlases=_evaluation_atlases
  942. )
  943. if c == 0:
  944. row['parcel_type'] = 'network'
  945. else:
  946. row['parcel_type'] = 'subnetwork%d' % c
  947. results.append(row)
  948. if c == 0 or (len(candidate_names) > 2):
  949. stderr(_pretty_print_evaluation_row(row, indent=indent + 1) + '\n')
  950. results = pd.DataFrame(results)
  951. results.to_csv(output_path, index=False)
  952. stderr('%sEvaluation time: %ds\n' % (' ' * (indent * 2), time.time() - t0))
  953. def aggregate(
  954. output_dir,
  955. action_sequence,
  956. grid_params,
  957. aggregation_id=None,
  958. evaluation_id=None,
  959. labeling_id=None,
  960. subnetwork_id=1,
  961. exclude='LANA',
  962. kernel_radius=5,
  963. eps=1e-3,
  964. compress_outputs=None,
  965. dump_kwargs=True,
  966. indent=0,
  967. **kwargs
  968. ):
  969. """
  970. Aggregate the results of a grid search and select to top setting.
  971. :param output_dir: ``str``; Output directory
  972. :param action_sequence: ``list`` of ``dict``; Action sequence
  973. :param grid_params: ``dict``; Grid parameters
  974. :param aggregation_id: ``str``; Aggregation ID
  975. :param evaluation_id: ``str`` or ``None``; Evaluation ID
  976. :param labeling_id: ``str`` or ``None``; Labeling ID
  977. :param subnetwork_id: ``int``; Subnetwork ID to use for scoring
  978. :param exclude: ``str`` or ``list`` of ``str``; Network(s) to exclude from scoring
  979. :param kernel_radius: ``int``; Kernel radius for smoothing the grid
  980. :param eps: ``float``; Epsilon for numerical stability
  981. :param compress_outputs: ``bool`` or ``None``; Whether to compress the output files
  982. :param dump_kwargs: ``bool``; Whether to dump the keyword arguments to a YAML file
  983. :param indent: ``int``; Indentation level for progress reporting
  984. :param kwargs: ``dict``; Unused keyword arguments
  985. :return: ``None``
  986. """
  987. if len(kwargs):
  988. stderr('WARNING: Unused keyword arguments to `aggregate()`: %s\n' % ', '.join(kwargs.keys()))
  989. assert isinstance(output_dir, str), 'output_dir must be given as a str'
  990. assert isinstance(grid_params, dict), 'grid_params must be given as a dict'
  991. t0 = time.time()
  992. stderr('%sAggregating grid\n' % (' ' * (indent * 2)))
  993. indent += 1
  994. _labeling_id = get_action_attr('label', action_sequence, 'id')
  995. if labeling_id is None:
  996. labeling_id = _labeling_id
  997. else:
  998. assert labeling_id == _labeling_id, ('Mismatch between provided labeling_id (%s) '
  999. 'and the one contained in action_sequence (%s).' % labeling_id, _labeling_id)
  1000. _evaluation_id = get_action_attr('evaluate', action_sequence, 'id')
  1001. if evaluation_id is None:
  1002. evaluation_id = _evaluation_id
  1003. else:
  1004. assert evaluation_id == _evaluation_id, ('Mismatch between provided evaluation_id (%s) '
  1005. 'and the one contained in action_sequence (%s).' % evaluation_id, _evaluation_id)
  1006. _aggregation_id = get_action_attr('aggregate', action_sequence, 'id')
  1007. if aggregation_id is None:
  1008. aggregation_id = _aggregation_id
  1009. else:
  1010. assert aggregation_id == _aggregation_id, ('Mismatch between provided aggregation_id (%s) '
  1011. 'and the one contained in action_sequence (%s).' % aggregation_id, _aggregation_id)
  1012. aggregation_dir = get_path(output_dir, 'subdir', 'aggregate', aggregation_id)
  1013. if not os.path.exists(aggregation_dir):
  1014. os.makedirs(aggregation_dir)
  1015. if dump_kwargs:
  1016. kwargs = dict(
  1017. output_dir=output_dir,
  1018. action_sequence=action_sequence,
  1019. grid_params=grid_params,
  1020. evaluation_id=evaluation_id,
  1021. aggregation_id=aggregation_id,
  1022. labeling_id=labeling_id,
  1023. subnetwork_id=subnetwork_id,
  1024. exclude=exclude,
  1025. kernel_radius=kernel_radius,
  1026. eps=eps,
  1027. compress_outputs=compress_outputs
  1028. )
  1029. kwargs_path = get_path(output_dir, 'kwargs', 'aggregate', aggregation_id)
  1030. with open(kwargs_path, 'w') as f:
  1031. yaml.safe_dump(kwargs, f, sort_keys=False)
  1032. output_path = get_path(output_dir, 'output', 'aggregate', aggregation_id, compressed=compress_outputs)
  1033. evaluation_path = get_path(output_dir, 'evaluation', 'aggregate', aggregation_id)
  1034. grid_settings = get_iterator_from_grid_params(grid_params)
  1035. grid_array, ix2val = get_grid_array_from_grid_params(grid_params)
  1036. val2ix = {x: {y: i for i, y in enumerate(ix2val[x])} for x in ix2val}
  1037. grid_keys = sorted(list(ix2val.keys()))
  1038. results = []
  1039. grid_ids = []
  1040. scores = []
  1041. for grid_setting in grid_settings:
  1042. grid_id = get_grid_id(grid_setting)
  1043. grid_ids.append(grid_id)
  1044. _output_dir = get_path(output_dir, 'subdir', 'grid', grid_id)
  1045. labeling_dir = get_path(_output_dir, 'subdir', 'label', labeling_id)
  1046. if os.path.exists(labeling_dir):
  1047. results_file_path = get_path(_output_dir, 'output', 'label', labeling_id)
  1048. _results = pd.read_csv(results_file_path)
  1049. _results['grid_id'] = grid_id
  1050. results.append(_results)
  1051. score = _get_atlas_score_from_df(_results, subnetwork_id=subnetwork_id, exclude=exclude, eps=eps)
  1052. else:
  1053. raise ValueError(('No available selection criteria for grid_id %s (no evaluation or labeling '
  1054. 'data found). Aggregation failed.' % grid_id))
  1055. scores.append(score)
  1056. ix = tuple([val2ix[x][grid_setting[x]] for x in grid_keys])
  1057. grid_array[ix] = score
  1058. results = pd.concat(results, axis=0)
  1059. # Weighted average
  1060. if kernel_radius > 1:
  1061. grid_array = smooth(grid_array, kernel_radius=kernel_radius)
  1062. # Select configuration
  1063. best_ix = np.unravel_index(np.argmax(grid_array), grid_array.shape)
  1064. best_setting = {x: ix2val[x][best_ix[i]] for i, x in enumerate(grid_keys)}
  1065. best_id = get_grid_id(best_setting)
  1066. best_score = grid_array[best_ix]
  1067. results['selected'] = (results.grid_id == best_id) & (results.parcel_type != 'baseline')
  1068. results.to_csv(evaluation_path, index=False)
  1069. # Save configuration
  1070. best_grid_dir = get_path(output_dir, 'subdir', 'grid', best_id)
  1071. _action_sequence = []
  1072. for action in action_sequence:
  1073. action_type, action_id = action['type'], action['id']
  1074. if action_type != 'aggregate':
  1075. action = copy.deepcopy(action)
  1076. kwargs_path = get_path(best_grid_dir, 'kwargs', action_type, action_id)
  1077. exists = os.path.exists(kwargs_path)
  1078. if action_type in ('sample', 'label', 'aggregate'):
  1079. assert exists, '%s does not exist' % kwargs_path
  1080. kwargs = get_cfg(kwargs_path)
  1081. kwargs.update(dict(
  1082. output_dir=output_dir
  1083. ))
  1084. action['kwargs'] = kwargs
  1085. _action_sequence.append(action)
  1086. parcellate_kwargs = dict(
  1087. output_dir=output_dir,
  1088. action_sequence=_action_sequence,
  1089. grid_params=None,
  1090. eps=eps,
  1091. )
  1092. with open(output_path, 'w') as f:
  1093. yaml.safe_dump(parcellate_kwargs, f, sort_keys=False)
  1094. stderr('%sBest grid_id: %s | atlas score: %0.3f\n' % (' ' * (indent * 2), best_id, best_score))
  1095. stderr('%sAggregation time: %ds\n' % (' ' * (indent * 2), time.time() - t0))
  1096. def parcellate(
  1097. output_dir,
  1098. action_sequence,
  1099. grid_params=None,
  1100. grid_only=False,
  1101. eps=1e-3,
  1102. compress_outputs=True,
  1103. overwrite=False,
  1104. dump_kwargs=True,
  1105. indent=0,
  1106. **kwargs
  1107. ):
  1108. """
  1109. Parcellate the data (run an entire action sequence and/or grid search)
  1110. :param output_dir: ``str``; Output directory
  1111. :param action_sequence: ``list`` of ``dict``; Action sequence
  1112. :param grid_params: ``dict`` or ``None``; Grid parameters
  1113. :param grid_only: ``bool``; Whether to only run the grid search
  1114. :param eps: ``float``; Epsilon for numerical stability
  1115. :param compress_outputs: ``bool``; Whether to compress the output files
  1116. :param overwrite: ``bool`` or ``dict``; Overwrite parameters
  1117. :param dump_kwargs: ``bool``; Whether to dump the keyword arguments to a YAML file
  1118. :param indent: ``int``; Indentation level for progress reporting
  1119. :param kwargs: ``dict``; Unused keyword arguments
  1120. :return: ``None``
  1121. """
  1122. if len(kwargs):
  1123. stderr('WARNING: Unused keyword arguments to `parcellate()`: %s\n' % ', '.join(kwargs.keys()))
  1124. assert isinstance(output_dir, str), 'output_dir is required, must be given as a str'
  1125. assert isinstance(action_sequence, list), ('action_sequence is required, must be given as a list of dict'
  1126. 'and grid_params must be provided as dicts, or neither can be.')
  1127. assert grid_params is not None or not grid_only, ('grid_params must be provided if grid_only is True.')
  1128. validate_action_sequence(action_sequence)
  1129. t0 = time.time()
  1130. stderr('%sParcellating\n' % (' ' * (indent * 2)))
  1131. indent += 1
  1132. sample_id = get_action_attr('sample', action_sequence, 'id')
  1133. labeling_id = get_action_attr('label', action_sequence, 'id')
  1134. aggregation_id = get_action_attr('aggregate', action_sequence, 'id')
  1135. parcellation_id = get_action_attr('parcellate', action_sequence, 'id')
  1136. assert isinstance(sample_id, str), 'sample_id is required, must be given as a str.'
  1137. assert isinstance(labeling_id, str), 'labeling_id is required, must be given as a str.'
  1138. assert isinstance(parcellation_id, str), 'parcellation_id is required, must be given as a str.'
  1139. overwrite = get_overwrite(overwrite)
  1140. use_grid = aggregation_id is not None
  1141. grid_optimized = get_action('parcellate', action_sequence)['kwargs'].get('grid_optimized', True)
  1142. parcellation_dir = get_path(output_dir, 'subdir', 'parcellate', parcellation_id)
  1143. if not os.path.exists(parcellation_dir):
  1144. os.makedirs(parcellation_dir)
  1145. if dump_kwargs:
  1146. kwargs = dict(
  1147. output_dir=output_dir,
  1148. action_sequence=action_sequence,
  1149. grid_params=grid_params,
  1150. grid_optimized=grid_optimized,
  1151. eps=eps,
  1152. compress_outputs=compress_outputs,
  1153. )
  1154. kwargs_path = get_path(output_dir, 'kwargs', 'parcellate', parcellation_id)
  1155. with open(kwargs_path, 'w') as f:
  1156. yaml.safe_dump(kwargs, f, sort_keys=False)
  1157. output_path = get_path(output_dir, 'output', 'parcellate', parcellation_id, compressed=compress_outputs)
  1158. for action in action_sequence:
  1159. action['kwargs']['output_dir'] = output_dir
  1160. action['kwargs']['compress_outputs'] = compress_outputs
  1161. suffix = get_suffix(compress_outputs)
  1162. # Grid search
  1163. if use_grid and grid_params:
  1164. stderr('%sGrid searching\n' % (' ' * (indent * 2)))
  1165. # Core loop
  1166. indent += 1
  1167. grid_settings = get_iterator_from_grid_params(grid_params)
  1168. for grid_setting in grid_settings:
  1169. grid_id = get_grid_id(grid_setting)
  1170. stderr('%sGrid id: %s\n' % (' ' * (indent * 2), grid_id))
  1171. # Update kwargs
  1172. _output_dir = get_path(output_dir, 'subdir', 'grid', grid_id)
  1173. _action_sequence = []
  1174. for action in action_sequence:
  1175. if action['type'] != 'aggregate':
  1176. action = copy.deepcopy(action)
  1177. _kwargs = action['kwargs']
  1178. if action['type'] == 'parcellate':
  1179. # Don't pass down the top-level parcellation kwargs
  1180. _kwargs = {x: _kwargs[x] for x in _kwargs if x in
  1181. ('output_dir', 'compress_outputs', 'parcellation_id')}
  1182. action['kwargs'] = _kwargs
  1183. _kwarg_keys = set(inspect.signature(ACTIONS[action['type']]).parameters.keys())
  1184. _grid_setting = {x: grid_setting[x] for x in grid_setting if x in _kwarg_keys}
  1185. _kwargs.update(_grid_setting)
  1186. _action_sequence.append(action)
  1187. # Recursion bottoms out since grid_params is None
  1188. parcellate(
  1189. _output_dir,
  1190. _action_sequence,
  1191. eps=eps,
  1192. compress_outputs=compress_outputs,
  1193. overwrite=overwrite,
  1194. indent=indent + 1
  1195. )
  1196. indent -= 1
  1197. if grid_only:
  1198. stderr('%sTotal time elapsed: %ds\n' % (' ' * (indent * 2), time.time() - t0))
  1199. return
  1200. # Aggregate
  1201. action = None
  1202. _action_sequence = []
  1203. for action in action_sequence:
  1204. if action['type'] == 'evaluate':
  1205. # Evaluation is not a dependency of aggregate
  1206. continue
  1207. _action_sequence.append(action)
  1208. if action['type'] == 'aggregate':
  1209. break
  1210. assert action is not None and action['type'] == 'aggregate', ('action type "aggregate" not found in '
  1211. 'action_sequence')
  1212. mtime, exists = check_deps(
  1213. output_dir,
  1214. _action_sequence,
  1215. compressed=True
  1216. )
  1217. stale = mtime == 1
  1218. if overwrite['aggregate'] or stale or not exists:
  1219. # Recursion bottoms out since grid_params is None
  1220. _aggregate_kwargs = copy.deepcopy(action['kwargs'])
  1221. # _action_sequence.append(get_action('aggregate', action_sequence))
  1222. _aggregate_kwargs.update(dict(
  1223. output_dir=output_dir,
  1224. action_sequence=action_sequence,
  1225. grid_params=grid_params,
  1226. eps=eps,
  1227. compress_outputs=compress_outputs,
  1228. indent=indent,
  1229. ))
  1230. aggregate(**_aggregate_kwargs)
  1231. else:
  1232. stderr('%sAggregation exists. Skipping. To re-aggregate, run with overwrite=True.\n' %
  1233. (' ' * (indent * 2)))
  1234. aggregation_output_path = get_path(output_dir, 'output', 'aggregate', aggregation_id)
  1235. # Parcellate
  1236. action = None
  1237. _action_sequence = []
  1238. with open(aggregation_output_path, 'r') as f:
  1239. parcellate_kwargs = yaml.safe_load(f)
  1240. parcellate_override_kwargs = get_action('parcellate', action_sequence)
  1241. if parcellate_override_kwargs is not None and 'kwargs' in parcellate_override_kwargs:
  1242. parcellate_override_kwargs = parcellate_override_kwargs['kwargs']
  1243. for action in action_sequence:
  1244. if action['type'] == 'aggregate':
  1245. continue
  1246. action_ = get_action(action['type'], parcellate_kwargs['action_sequence'])
  1247. if grid_optimized:
  1248. kwargs_update = {x: action['kwargs'][x] for x in action['kwargs'] if x not in grid_params}
  1249. else:
  1250. kwargs_update = action['kwargs']
  1251. # Change any kwargs that differ between the config and the saved kwargs file
  1252. if action['type'] != 'parcellate':
  1253. _kwarg_keys = set(inspect.signature(ACTIONS[action['type']]).parameters.keys())
  1254. kwargs_update = {x: kwargs_update[x] for x in kwargs_update if x in _kwarg_keys}
  1255. action_['kwargs'].update(kwargs_update)
  1256. # Change any kwargs that are over-ridden by the settings in the parcellate action
  1257. if action['type'] in parcellate_override_kwargs:
  1258. action_['kwargs'].update(parcellate_override_kwargs[action['type']])
  1259. assert action is not None and action['type'] == 'parcellate', ('Final action type in '
  1260. 'grid searched "action_sequence" must be "parcellate"')
  1261. action_prefix = []
  1262. if grid_optimized:
  1263. for action in action_sequence[:-1]: # Add dependencies to grid, ignoring last ('parcellate') action
  1264. if action['type'] != 'evaluate': # Changing the evaluation doesn't make the aggregation stale
  1265. action_prefix.append(dict(
  1266. type=action['type'],
  1267. id=action['id'],
  1268. kwargs={}
  1269. ))
  1270. parcellate_kwargs['action_sequence'] = action_prefix + parcellate_kwargs['action_sequence']
  1271. parcellate_kwargs['dump_kwargs'] = False # Don't let recursive call overwrite top-level kwargs file
  1272. parcellate_kwargs['overwrite'] = overwrite
  1273. parcellate(**parcellate_kwargs)
  1274. else:
  1275. action_sequence_full = action_sequence
  1276. _action_sequence = []
  1277. for a in range(len(action_sequence) - 1, -1, -1):
  1278. action = action_sequence[a]
  1279. _action_sequence.insert(0, action)
  1280. if action['type'] == 'sample':
  1281. break
  1282. action_sequence = _action_sequence
  1283. sample_id = get_action_attr('sample', action_sequence, 'id')
  1284. alignment_id = get_action_attr('align', action_sequence, 'id')
  1285. labeling_id = get_action_attr('label', action_sequence, 'id')
  1286. evaluation_id = get_action_attr('evaluate', action_sequence, 'id')
  1287. aggregation_id = get_action_attr('aggregate', action_sequence_full, 'id')
  1288. parcellate_kwargs = get_action_attr('parcellate', action_sequence, 'kwargs')
  1289. n = len(action_sequence)
  1290. N = len(action_sequence_full)
  1291. for a, action in enumerate(action_sequence):
  1292. action_type = action['type']
  1293. action_kwargs = action['kwargs']
  1294. e = N - n + a + 1
  1295. mtime, exists = check_deps(
  1296. output_dir,
  1297. action_sequence_full[:e],
  1298. compressed=True
  1299. )
  1300. stale = mtime == 1
  1301. if overwrite[action_type] or stale or not exists:
  1302. do_action = True
  1303. else:
  1304. do_action = False
  1305. if action_type == 'sample':
  1306. if do_action:
  1307. action_kwargs.update(dict(
  1308. output_dir=output_dir,
  1309. sample_id=sample_id
  1310. ))
  1311. if action_type in parcellate_kwargs:
  1312. for key in parcellate_kwargs[action_type]:
  1313. action_kwargs[key] = parcellate_kwargs[action_type][key]
  1314. sample(**action_kwargs, indent=indent)
  1315. else:
  1316. stderr('%sSample exists. Skipping. To resample, run with overwrite=True.\n' %
  1317. (' ' * (indent * 2)))
  1318. elif action_type == 'align':
  1319. if do_action:
  1320. action_kwargs.update(dict(
  1321. output_dir=output_dir,
  1322. alignment_id=alignment_id,
  1323. sample_id=sample_id
  1324. ))
  1325. if action_type in parcellate_kwargs:
  1326. for key in parcellate_kwargs[action_type]:
  1327. action_kwargs[key] = parcellate_kwargs[action_type][key]
  1328. align(**action_kwargs, indent=indent)
  1329. else:
  1330. stderr('%sAlignment exists. Skipping. To re-align, run with overwrite=True.\n' %
  1331. (' ' * (indent * 2)))
  1332. elif action_type == 'label':
  1333. if do_action:
  1334. action_kwargs.update(dict(
  1335. output_dir=output_dir,
  1336. labeling_id=labeling_id,
  1337. alignment_id=alignment_id,
  1338. sample_id=sample_id
  1339. ))
  1340. if action_type in parcellate_kwargs:
  1341. for key in parcellate_kwargs[action_type]:
  1342. action_kwargs[key] = parcellate_kwargs[action_type][key]
  1343. label(**action_kwargs, indent=indent)
  1344. else:
  1345. stderr('%sLabeling exists. Skipping. To re-label, run with overwrite=True.\n' %
  1346. (' ' * (indent * 2)))
  1347. elif action_type == 'evaluate':
  1348. if do_action:
  1349. action_kwargs.update(dict(
  1350. output_dir=output_dir,
  1351. evaluation_id=evaluation_id,
  1352. labeling_id=labeling_id
  1353. ))
  1354. if action_type in parcellate_kwargs:
  1355. for key in parcellate_kwargs[action_type]:
  1356. action_kwargs[key] = parcellate_kwargs[action_type][key]
  1357. evaluate(**action_kwargs, indent=indent)
  1358. else:
  1359. stderr('%sEvaluation exists. Skipping. To re-evaluate, run with overwrite=True.\n' %
  1360. (' ' * (indent * 2)))
  1361. elif action_type == 'parcellate':
  1362. # Copy final files to destination
  1363. if do_action:
  1364. results_copied = False
  1365. if aggregation_id is not None:
  1366. parcellation_kwargs_path = get_path(output_dir, 'output', 'aggregate', aggregation_id)
  1367. assert os.path.exists(parcellation_kwargs_path), ('Aggregation output %s not found' %
  1368. parcellation_kwargs_path)
  1369. if grid_optimized:
  1370. shutil.copy(
  1371. parcellation_kwargs_path, join(parcellation_dir, 'parcellate_kwargs_optimized.yml')
  1372. )
  1373. if evaluation_id is not None:
  1374. evaluation_dir = get_path(output_dir, 'subdir', 'evaluate', evaluation_id)
  1375. for filename in os.listdir(evaluation_dir):
  1376. if filename.endswith(suffix) or filename == PATHS['evaluate']['output']:
  1377. shutil.copy(join(evaluation_dir, filename), join(parcellation_dir, filename))
  1378. if filename == PATHS['evaluate']['output']:
  1379. results_copied = True
  1380. if alignment_id is not None:
  1381. alignment_dir = get_path(output_dir, 'subdir', 'align', alignment_id)
  1382. for filename in os.listdir(alignment_dir):
  1383. if filename.endswith(suffix):
  1384. shutil.copy(join(alignment_dir, filename), join(parcellation_dir, filename))
  1385. labeling_dir = get_path(output_dir, 'subdir', 'label', labeling_id)
  1386. for filename in os.listdir(labeling_dir):
  1387. if filename.endswith(suffix) or (not results_copied and filename == PATHS['label']['output']):
  1388. shutil.copy(join(labeling_dir, filename), join(parcellation_dir, filename))
  1389. else:
  1390. stderr('%sParcellation exists. Skipping. To re-parcellate, run with overwrite=True.\n' %
  1391. (' ' * (indent * 2)))
  1392. else:
  1393. raise ValueError('Unrecognized action_type %s' % action_type)
  1394. with open(output_path, 'w') as f:
  1395. f.write(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
  1396. assert os.path.exists(output_path)
  1397. stderr('%sTotal time elapsed: %ds\n' % (' ' * (indent * 2), time.time() - t0))
  1398. ######################################
  1399. #
  1400. # PRIVATE HELPER METHODS
  1401. #
  1402. ######################################
  1403. def _get_n_voxels(
  1404. atlas,
  1405. ):
  1406. m, M = atlas.min(), atlas.max()
  1407. if m < 0 or M > 1:
  1408. atlas = minmax_normalize_array(np.clip(atlas, 0, np.inf))
  1409. n_voxels = atlas.sum()
  1410. row = dict(
  1411. n_voxels=n_voxels
  1412. )
  1413. return row
  1414. def _get_atlas_score(
  1415. atlas,
  1416. reference_atlas,
  1417. ):
  1418. with np.errstate(divide='ignore', invalid='ignore'):
  1419. r = np.corrcoef(reference_atlas, atlas)[0, 1]
  1420. row = {
  1421. '%sscore' % REFERENCE_ATLAS_PREFIX: r
  1422. }
  1423. return row
  1424. def _get_evaluation_spcorr(
  1425. atlas,
  1426. evaluation_atlases,
  1427. ):
  1428. row = {}
  1429. evaluation_atlas_names = list(evaluation_atlases.keys())
  1430. for evaluation_atlas_name in evaluation_atlas_names:
  1431. evaluation_atlas = evaluation_atlases[evaluation_atlas_name]
  1432. with np.errstate(divide='ignore', invalid='ignore'):
  1433. r = np.corrcoef(atlas, evaluation_atlas)[0, 1]
  1434. row['%s_score' % evaluation_atlas_name] = r
  1435. return row
  1436. def _get_evaluation_contrasts(
  1437. atlas,
  1438. evaluation_atlases
  1439. ):
  1440. m, M = atlas.min(), atlas.max()
  1441. if m < 0 or M > 1:
  1442. atlas = minmax_normalize_array(np.clip(atlas, 0, np.inf))
  1443. row = {}
  1444. evaluation_atlas_names = list(evaluation_atlases.keys())
  1445. for evaluation_atlas_name in evaluation_atlas_names:
  1446. evaluation_atlas = evaluation_atlases[evaluation_atlas_name]
  1447. denom = atlas.sum()
  1448. if denom:
  1449. contrast = (atlas * evaluation_atlas).sum() / denom
  1450. else:
  1451. contrast = 0
  1452. row['%s_contrast' % evaluation_atlas_name] = contrast
  1453. return row
  1454. def _get_evaluation_row(
  1455. atlas,
  1456. atlas_name,
  1457. reference_atlas_name,
  1458. reference_atlases=None,
  1459. evaluation_atlases=None
  1460. ):
  1461. row = {
  1462. 'parcel': atlas_name,
  1463. '%sname' % REFERENCE_ATLAS_PREFIX: reference_atlas_name
  1464. }
  1465. row.update(_get_n_voxels(atlas))
  1466. if reference_atlases is not None:
  1467. row.update(_get_atlas_score(
  1468. atlas,
  1469. reference_atlases[reference_atlas_name]
  1470. ))
  1471. _reference_atlases = {'%s%s' % (REFERENCE_ATLAS_PREFIX, x): reference_atlases[x] for x in reference_atlases}
  1472. row.update(_get_evaluation_spcorr(
  1473. atlas,
  1474. _reference_atlases
  1475. ))
  1476. if evaluation_atlases is not None:
  1477. _evaluation_atlases = {'%s%s' % (EVALUATION_ATLAS_PREFIX, x): evaluation_atlases[x] for x in evaluation_atlases}
  1478. row.update(_get_evaluation_spcorr(
  1479. atlas,
  1480. _evaluation_atlases
  1481. ))
  1482. row.update(_get_evaluation_contrasts(
  1483. atlas,
  1484. _evaluation_atlases
  1485. ))
  1486. return row
  1487. def _pretty_print_evaluation_row(
  1488. row,
  1489. max_evals=None,
  1490. indent=0
  1491. ):
  1492. to_print = []
  1493. scores = set()
  1494. contrasts = set()
  1495. for col in row:
  1496. if col == 'parcel':
  1497. to_print.append(row[col])
  1498. elif col == 'n_voxels':
  1499. to_print.append('n voxels: %d' % row[col])
  1500. elif (col == ('%sscore' % REFERENCE_ATLAS_PREFIX) or
  1501. (col.startswith(EVALUATION_ATLAS_PREFIX) and col.endswith('_score'))):
  1502. _col = '_'.join(col.split('_')[:-1])
  1503. if max_evals is None or len(scores) < max_evals:
  1504. if col != 'ref_score':
  1505. scores.add(col)
  1506. to_print.append('%s score: %0.3f' % (_col, row[col]))
  1507. elif col.endswith('contrast'):
  1508. _col = '_'.join(col.split('_')[:-1])
  1509. if max_evals is None or len(contrasts) < max_evals:
  1510. contrasts.add(col)
  1511. to_print.append('%s contrast: %0.3f' % (_col, row[col]))
  1512. to_print = ('\n%s' % (' ' * ((indent + 1) * 2))).join(to_print)
  1513. to_print = ' ' * (indent * 2) + to_print
  1514. return to_print
  1515. def _get_atlas_score_from_df(df_scores, subnetwork_id=None, exclude=None, eps=1e-3):
  1516. if exclude is None:
  1517. exclude = []
  1518. if isinstance(exclude, str):
  1519. exclude = [exclude]
  1520. exclude = set(exclude)
  1521. reference_atlas_names = df_scores['%sname' % REFERENCE_ATLAS_PREFIX].unique().tolist()
  1522. if exclude:
  1523. reference_atlas_names = [x for x in reference_atlas_names if not x in exclude]
  1524. parcel_names = df_scores.parcel
  1525. scores = df_scores['%sscore' % REFERENCE_ATLAS_PREFIX]
  1526. target_parcels = reference_atlas_names
  1527. if subnetwork_id:
  1528. target_parcels = [x + '_sub%d' % subnetwork_id for x in target_parcels]
  1529. sel = parcel_names.isin(target_parcels)
  1530. parcel_names = parcel_names[sel].tolist()
  1531. assert set(target_parcels) == set(parcel_names), (f'Parcel names and reference atlas names mismatch. '
  1532. f'Parcel names: {parcel_names}. Reference atlas names: {reference_atlas_names}')
  1533. scores = scores[sel]
  1534. score = np.tanh(np.arctanh(scores * (1 - 2 * eps) + eps).mean())
  1535. return score
  1536. ######################################
  1537. #
  1538. # CONSTANTS
  1539. #
  1540. ######################################
  1541. ACTIONS = dict(
  1542. sample=sample,
  1543. align=align,
  1544. label=label,
  1545. evaluate=evaluate,
  1546. aggregate=aggregate,
  1547. parcellate=parcellate,
  1548. )

model.py at commit fbb979b, no license · at the source

Overview

  1. Department of Linguistics, Stanford University, Stanford, CA USA
  2. Department of Brain & Cognitive Sciences and McGovern Institute for Brain Research, Massachusetts Institute of Technology, Cambridge, MA USA
Institutions: Stanford University (United States); McGovern Institute for Brain Research (United States); Massachusetts Institute of Technology (United States)
Journal: Nature communications, volume 17, issue 1, article 8180
Dates: received 9 February 2026; accepted 7 July 2026; published online 13 August 2026
Type: Research article · Language: English
License: CC BY-NC-ND
Identifiers: DOI 10.1038/s41467-026-75745-8 · PMID 42595752 · PMCID PMC13473096 · OpenAlex W7163357038
Open access: gold, a free copy (OpenAlex)
Preprint: osf.io/nd5g3
Status: code verified
Categories: human (organism), cognitive (subfield)
Methods: Spectral & time-frequency, Connectivity, Smoothing, state filtering, decompositions, Machine learning, Preprocessing, Statistics, fMRI & imaging
Keywords: Language, Network models
MeSH: Brain*, Connectome*, Language*, Nerve Net*, Adult, Brain Mapping, Female, Humans, Magnetic Resonance Imaging, Male, Young Adult (* major topic)
Topic: Functional Brain Connectivity Studies (Cognitive Neuroscience, Neuroscience), according to OpenAlex
Funding: U.S. Department of Health &amp; Human Services | NIH | National Institute of Neurological Disorders and Stroke (NS121471); U.S. Department of Health & Human Services | NIH | National Institute of Neurological Disorders and Stroke (NINDS) (NS121471); NINDS NIH HHS (U01 NS121471)
Citations: cited by 6 papers (Europe PMC); 172 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 10 matches between paragraphs and lines of code.

coryshain/parcellate

License: none: the authors keep all their rights
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: fbb979b21592f7c2d62d243a759af5d006cf5660, 9 February 2026
Languages: Python (13)
Size: 41 files, 13 scripts
Software Heritage: not archived
Found in: “Code availability”
Holds: README, environment (conda.yml, source/requirements.txt), documentation
Not found: license file, CITATION.cff, tests, continuous integration
Tools: NumPy (5 files), Nilearn (3 files), SciPy (3 files), pandas (2 files), Matplotlib (1 file), Pillow (1 file), scikit-learn (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
14 files

coryshain/langlocFC

License: none: the authors keep all their rights
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: 4f1100e91ebd11cf40fb2f8c880d410d23830bbe, 8 June 2026
Languages: Python (36)
Size: 51 files, 36 scripts
Software Heritage: not archived
Found in: “Code availability”
Holds: README, environment (conda.yml)
Not found: license file, CITATION.cff, tests, continuous integration, documentation
Tools: pandas (27 files), NumPy (21 files), Matplotlib (9 files), Nilearn (9 files), seaborn (6 files), Pillow (4 files), SciPy (3 files), h5py (2 files), NiBabel (2 files)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
37 files

Zenodo 20520281

License: CC-BY-4.0
State: the link answers, verified on 27 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 (5 files), Nilearn (3 files), SciPy (3 files), pandas (2 files), Matplotlib (1 file), Pillow (1 file), scikit-learn (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers (HTTP 200)
  • 27 September 2026: the link answers (HTTP 200)
13 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-75745-8.

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:

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

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

Data

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

Code and data availability statement

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

Read it in the paper: doi.org/10.1038/s41467-026-75745-8.

Versions

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

Version 1, 27 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 2 authors, 2 keywords, 11 MeSH terms, 3 funders, 151 references.

Cite

This paper

Shain, C., & Fedorenko, E. (2026). A language network in the individualized functional connectomes of 1199 human brains doing arbitrary tasks. Nature communications, 17(1), 8180. https://doi.org/10.1038/s41467-026-75745-8

BibTeX

@article{shain2026language,
author = {Shain, Cory and Fedorenko, Evelina},
title = {{A language network in the individualized functional connectomes of 1199 human brains doing arbitrary tasks}},
journal = {Nature communications},
year = {2026},
month = aug,
volume = {17},
number = {1},
pages = {8180},
publisher = {Nature Publishing Group},
issn = {2041-1723},
doi = {10.1038/s41467-026-75745-8},
url = {https://doi.org/10.1038/s41467-026-75745-8},
pmid = {42595752},
pmcid = {PMC13473096}
}

RIS

TY - JOUR
AU - Shain, Cory
AU - Fedorenko, Evelina
TI - A language network in the individualized functional connectomes of 1199 human brains doing arbitrary tasks
T2 - Nature communications
J2 - Nat Commun
PY - 2026
DA - 2026/08/13
VL - 17
IS - 1
SP - 8180
SN - 2041-1723
PB - Nature Publishing Group
DO - 10.1038/s41467-026-75745-8
UR - https://doi.org/10.1038/s41467-026-75745-8
LA - en
ER -

CSL-JSON

{
"id": "10.1038/s41467-026-75745-8",
"type": "article-journal",
"title": "A language network in the individualized functional connectomes of 1199 human brains doing arbitrary tasks",
"container-title": "Nature communications",
"author": [
{
"family": "Shain",
"given": "Cory"
},
{
"family": "Fedorenko",
"given": "Evelina"
}
],
"container-title-short": "Nat Commun",
"volume": "17",
"issue": "1",
"page": "8180",
"DOI": "10.1038/s41467-026-75745-8",
"PMID": "42595752",
"PMCID": "PMC13473096",
"ISSN": "2041-1723",
"publisher": "Nature Publishing Group",
"URL": "https://doi.org/10.1038/s41467-026-75745-8",
"language": "en",
"issued": {
"date-parts": [
[
2026,
8,
13
]
]
}
}

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

Similar papers

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

[1] doi:10.1162/imag.a.1246 [code]
A 3.5-minute-long reading-based fMRI localizer for the language network.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: h5py, NiBabel, seaborn, 4 other tools, 34 references, author Evelina Fedorenko
[2] doi:10.1038/s41467-026-76598-x
Preserved topography, lateralization, selectivity, and functional connectivity of the language network in older brains.
Journal: Nature communications
In common: cognitive, 37 references, author Evelina Fedorenko
[3] doi:10.1038/s41467-026-72916-5 [code]
Precision fMRI reveals that the language network exhibits adult-like left-hemispheric lateralization by 4 years of age.
Journal: Nature communications
In common: 24 references, author Evelina Fedorenko
[4] doi:10.1162/imag.a.1283 [code]
The language network responds robustly to sentences across tasks.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: Nilearn, h5py, seaborn, 5 other tools, 22 references
[5] doi:10.1523/jneurosci.0638-25.2026 [code]
The Extended Language Network: Language-Responsive Brain Areas Whose Contributions to Language Remain To Be Discovered.
Journal: The Journal of neuroscience : the official journal of the Society for Neuroscience
In common: cognitive, 22 references
[6] doi:10.1016/j.neuron.2026.04.011 [code]
Precision fMRI reveals densely interdigitated network patches with conserved motifs in the lateral prefrontal cortex.
Journal: Neuron
In common: Nilearn, NiBabel, scikit-learn, 3 other tools, 18 references
[7] 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: Nilearn, h5py, Pillow, 7 other tools, cognitive, 12 references
[8] 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: Nilearn, h5py, Pillow, 7 other tools, cognitive, 12 references
[9] doi:10.1038/s41586-026-10691-5 [code]
Mapping the neuronal building blocks of human language with language models.
Journal: Nature
In common: scikit-learn, pandas, SciPy, 2 other tools, cognitive, 12 references
[10] doi:10.1162/imag.a.1222 [code]
Network-based near-scalp personalized brain stimulation targets.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: Nilearn, NiBabel, scikit-learn, 3 other tools, 10 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.