OSCR

A hierarchical framework for cortical and subcortical gray-matter parcellation across rodents, primates, and humans.

Code ↔ Paper

22 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 22 matches
  1. [1] § Results › Per-region homology confidence ↔ projects/common_cross_species_atlas/CHA_Validation_Homology_Confidence.ipynb, lines 22–53 · score 0.96 · TEM_Amygdala, FRO_Prefrontal, CIN_Posterior, homology confidence, TEM_Inferior, INS_Posterior
  2. [2] § Methods › Validation of the common atlas across scales and species › Containment validation using finer-scale species-specific atlases ↔ projects/common_cross_species_atlas/CHA_quants.ipynb, lines 582–717 · score 0.88 · best matching target, finer atlas region, raw expected, target parcels, fractional overlap, volume fraction
  3. [3] § Results › Per-region homology confidence ↔ projects/common_cross_species_atlas/CHA_Validation_Homology_Confidence.ipynb, lines 56–89 · score 0.88 · functional analog, nomenclature directness, CIN_Posterior, homology confidence, INS_Posterior, OLF_Piriform
  4. [4] § Results › Per-region homology confidence ↔ projects/common_cross_species_atlas/CHA_quants.ipynb, lines 465–479 · score 0.87 · THL_Thalamus, FRO_Prefrontal, CIN_Posterior, INS_Posterior, TEM_Superior, dorsal
  5. [5] § Methods › Validation of the common atlas across scales and species › Cross-species connectivity validation ↔ projects/common_cross_species_atlas/CHA_Validation_Homology_Confidence.ipynb, lines 22–53 · score 0.85 · adequate marmoset tracer, INS_Posterior, OLF_Piriform, INS_Anterior, OLF_Anterior, cortical regions
  6. [6] § Methods › Validation of the common atlas across scales and species › Cross-species connectivity validation ↔ projects/common_cross_species_atlas/CHA_Validation_Tracer.ipynb, lines 202–208 · score 0.80 · systematic injection coverage, INS_Posterior, OLF_Piriform, INS_Anterior, OLF_Anterior, tracer
  7. [7] § Methods › Validation of the common atlas across scales and species › Per-region homology confidence assessment › Geometric consistency ↔ projects/common_cross_species_atlas/CHA_Validation_Homology_Confidence.ipynb, lines 92–159 · score 0.80 · direction vector, resultant length, mass, centroid, species atlas, CHA region
  8. [8] § Methods › Validation of the common atlas across scales and species › Validation of human parcellation using neuroparc atlases ↔ projects/common_cross_species_atlas/CHA_quants.ipynb, lines 345–452 · score 0.77 · Wilcoxon signed rank, region Median Dice, hypothesis, margins, shifting, framing
  9. [9] § Methods › Validation of the common atlas across scales and species › Per-region homology confidence assessment › Cross-species connectivity correspondence ↔ projects/common_cross_species_atlas/CHA_Validation_Tracer.ipynb, lines 202–208 · score 0.77 · INS_Posterior, OLF_Piriform, injection coverage, INS_Anterior, OLF_Anterior, tracer
  10. [10] § Methods › Validation of the common atlas across scales and species › Per-region homology confidence assessment › Nomenclature directness ↔ projects/common_cross_species_atlas/CHA_Validation_Homology_Confidence.ipynb, lines 56–89 · score 0.74 · functional analog, FRO_Precentral, TEM_Inferior, nomenclature, hippocampus, directness
  11. [11] § Results › Cross-species connectivity validation ↔ projects/common_cross_species_atlas/CHA_Validation_Tracer.ipynb, lines 369–492 · score 0.73 · marmoset coverage quartile, Rank percentile, projection densities, log10, Q1, Q4
  12. [12] § Methods › Validation of the common atlas across scales and species › Cross-species connectivity validation ↔ projects/common_cross_species_atlas/CHA_Validation_Tracer.ipynb, lines 99–160 · score 0.72 · Paxinos area, FLNe matrix, Paxinos space, CHA region, matching, mbm
  13. [13] § Methods › Validation of the common atlas across scales and species › Per-region homology confidence assessment › Cross-species connectivity correspondence ↔ projects/cross_species_connectomics/fig5a.py, lines 1–40 · score 0.71 · INS_Posterior, OLF_Piriform, INS_Anterior, OLF_Anterior, cross species, connectivity
  14. [14] § Results › Validation of the common human atlas using cross-atlas dice similarity ↔ projects/common_cross_species_atlas/CHA_quants.ipynb, lines 345–452 · score 0.71 · Wilcoxon signed rank, inter atlas agreement, Median Dice, sum, CHA
  15. [15] § Methods › Construction of brain parcellations ↔ projects/common_cross_species_atlas/CHA_quants.ipynb, lines 465–479 · score 0.70 · perirhinal, auditory, entorhinal, postcentral, claustrum, subthalamus
  16. [16] § Results › Cross-species parcellation hierarchy ↔ projects/common_cross_species_atlas/CHA_quants.ipynb, lines 25–76 · score 0.67 · basal ganglia, BG, occipital, cingulate, THL, frontal
  17. [17] § Methods › Validation of the common atlas across scales and species › Containment validation using finer-scale species-specific atlases ↔ projects/common_cross_species_atlas/CHA_Integrate.py, lines 177–189 · score 0.66 · raw expected, best matching, volume fraction, parcels, atlas
  18. [18] § Methods › Validation of the common atlas across scales and species › Containment validation using finer-scale species-specific atlases ↔ projects/common_cross_species_atlas/CHA_quants.ipynb, lines 538–578 · score 0.63 · Duke CIVM, CHA rat, WHS, baseline, MBM, ABA
  19. [19] § Methods › Validation of the common atlas across scales and species › Cross-species connectivity validation ↔ projects/common_cross_species_atlas/CHA_Validation_Tracer.ipynb, lines 369–492 · score 0.61 · marmoset signal, rank percentiles, quartiles, transformation, validation, agreement
  20. [20] § Results › Validation of the common human atlas using cross-atlas dice similarity ↔ projects/common_cross_species_atlas/CHA_quants.ipynb, lines 183–273 · score 0.61 · Dice matrix, median Dice, Neuroparc atlases, angle, scatterplot, inter
  21. [21] § Results › Cross-species parcellation hierarchy ↔ projects/cross_species_connectomics/cross_species_nets.py, lines 184–216 · score 0.53 · subcortical gray matter, segmentation limitations, root, species
  22. [22] § Results › Overview of the common atlas framework ↔ projects/common_cross_species_atlas/CHA_Integrate.py, lines 1–56 · score 0.52 · marmoset retrograde tracer, common hierarchical atlas, assignment, template, validated, maps

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

Jupyter notebook · 1,056 lines · 36 KB · MIT · 8 matches

  1. # %%
  2. import os
  3. import nibabel as nb
  4. import numpy as np
  5. import pandas as pd
  6. import matplotlib.pyplot as plt
  7. from itertools import combinations
  8. import re
  9. import seaborn as sns
  10. from adjustText import adjust_text
  11. import matplotlib.colors as mcolors
  12. os.chdir('projects\\common_cross_species_atlas') #<-- set working directory here
  13. print("Current working directory:", os.getcwd())
  14. # %%
  15. # %%
  16. # %% [markdown]
  17. # # Figure 4
  18. # %%
  19. _dir="data\\roistats\\"
  20. species=['mouse', 'marmoset', 'rhesus', 'human']
  21. metric='volume (mm^3)'
  22. exclude_rois=['OLF_Claustrum', 'BG_Substantia_Nigra', 'THL_Subthalamus', 'CBL', 'BST']
  23. dfs=[]
  24. for sp in species:
  25. df1=pd.read_table(_dir+'L2_332_moused_'+sp+'_roistats.txt', index_col=0).T[[metric]]
  26. df1.drop(exclude_rois, axis=0, inplace=True)
  27. #df2=pd.read_table(_dir+sp+'_cran_stats.txt', index_col=0).T[[metric]]
  28. df1[sp]=df1[metric]
  29. print(df1[metric].sum())
  30. df1.drop(metric, axis=1, inplace=True)
  31. dfs.append(df1)
  32. df_cons=dfs[0].join(dfs[1]).join(dfs[2]).join(dfs[3])
  33. df=df_cons
  34. volume_data=df
  35. # Group definitions
  36. region_groups = {
  37. 'Frontal': ["FRO_Precentral", "FRO_Premotor", "FRO_Prefrontal"],
  38. 'Parietal': ["PAR"],
  39. 'Temporal': ["TEM_Superior", "TEM_Inferior", "TEM_Medial", "TEM_Hippocampus", "TEM_Amygdala"],
  40. 'Occipital': ["OCC_Lateral", "OCC_Medial"],
  41. 'Insula': ["INS_Anterior", "INS_Posterior"],
  42. 'Olfactory': ["OLF_Anterior", "OLF_Piriform"],
  43. 'Cingulate': ["CIN_Anterior", "CIN_Posterior"],
  44. 'Basal Ganglia': ["BG_CaudoPutamen", "BG_Accumbens", "BG_Pallidum"],
  45. 'Thalamus': ["THL_Thalamus", "THL_Hypothalamus"]
  46. }
  47. # OPTIONAL split
  48. # Surface/Deep sets (for ordering within each group)
  49. surface_regions = {
  50. "FRO_Precentral","FRO_Premotor","FRO_Prefrontal","PAR",
  51. "TEM_Superior","TEM_Inferior","TEM_Medial",
  52. "OCC_Lateral","OCC_Medial","INS_Anterior","INS_Posterior",
  53. "CIN_Anterior","CIN_Posterior"
  54. }
  55. deep_regions = {
  56. "TEM_Hippocampus","TEM_Amygdala","OLF_Anterior","OLF_Piriform",
  57. "BG_CaudoPutamen","BG_Accumbens","BG_Pallidum",
  58. "THL_Thalamus","THL_Hypothalamus"
  59. }
  60. # %%
  61. # =========================
  62. # CONFIG
  63. # =========================
  64. species_order = ['mouse', 'marmoset', 'rhesus', 'human']
  65. colors = {'mouse':'#1f77b4','marmoset':'#ff7f0e','rhesus':'#2ca02c','human':'#d62728'}
  66. markers = {'mouse':'o','marmoset':'s','rhesus':'D','human':'^'}
  67. normalize = True
  68. ref_species = 'human'
  69. # =========================
  70. # PREP
  71. # =========================
  72. all_defined = [r for gl in region_groups.values() for r in gl]
  73. regions_available = [r for r in df.index if r in set(all_defined)]
  74. df_use = df.loc[regions_available].copy().astype(float)
  75. if normalize:
  76. df_use = df_use.div(df_use.sum(axis=0), axis=1)
  77. df_use = df_use[species_order]
  78. # Order regions: by group; inside group surface→deep; then sort by ref species desc
  79. ordered_regions, group_slices = [], []
  80. cursor = 0
  81. for gname, glist in region_groups.items():
  82. avail = [r for r in glist if r in df_use.index]
  83. if not avail:
  84. continue
  85. g_surface = [r for r in avail if r in surface_regions]
  86. g_deep = [r for r in avail if r in deep_regions]
  87. g_order = g_surface + g_deep if (g_surface or g_deep) else avail
  88. ref_vals = df_use.loc[g_order, ref_species]
  89. g_sorted = ref_vals.sort_values(ascending=False).index.tolist()
  90. start, end = cursor, cursor + len(g_sorted) - 1
  91. ordered_regions.extend(g_sorted)
  92. group_slices.append((gname, start, end))
  93. cursor = end + 1
  94. df_plot = df_use.loc[ordered_regions, :]
  95. # =========================
  96. # PLOT
  97. # =========================
  98. N = len(ordered_regions)
  99. fig_w = 3.0
  100. fig_h = min(6, 0.25 * N) # <= requested sizing
  101. fig, ax = plt.subplots(figsize=(fig_w, fig_h))
  102. y = np.arange(N) # 0..N-1 (top row is 0; we invert later so it appears at top)
  103. # Higher-contrast alternating shading by group (no labels)
  104. for i, (_, start, end) in enumerate(group_slices):
  105. if i % 2 == 0:
  106. ax.axhspan(start - 0.5, end + 0.5, color='k', alpha=0.14)
  107. # Dumbbell stems (min→max per region)
  108. mins = df_plot.min(axis=1).values
  109. maxs = df_plot.max(axis=1).values
  110. ax.hlines(y, mins, maxs, color='0.70', linewidth=2, zorder=1)
  111. # Species markers
  112. handles = []
  113. for sp in species_order:
  114. h = ax.plot(df_plot[sp].values, y, linestyle='None',
  115. marker=markers.get(sp,'o'), markersize=4.5,
  116. color=colors.get(sp), label=sp, zorder=2)[0]
  117. handles.append(h)
  118. # Y axis
  119. ax.set_yticks(y)
  120. ax.set_yticklabels(ordered_regions, fontsize=8)
  121. ax.invert_yaxis()
  122. # X axis, grid, labels
  123. ax.grid(axis='x', alpha=0.3, linewidth=0.6)
  124. ax.tick_params(axis='x', labelsize=8)
  125. ax.set_xlabel('Volume fraction \n(per-species total)' if normalize else 'Volume (mm³)')
  126. #ax.set_title(title, fontsize=10.5, pad=6)
  127. # Figure-level legend above the axes (kept clear of first row)
  128. fig.legend(handles, species_order, loc='upper center', ncol=len(species_order),
  129. frameon=False, bbox_to_anchor=(0.5, 0.995), borderaxespad=0.0)
  130. # Tight layout: leave top room for legend
  131. plt.tight_layout(rect=(0, 0, 1, 0.90))
  132. # %%
  133. df_plot
  134. # %%
  135. # %%
  136. # %%
  137. # %% [markdown]
  138. # # Figure 5a-b
  139. # %% [markdown]
  140. # #### Flexible string search to identify regions across Neuroparc atlases
  141. # atlas source: https://github.com/neurodata/neuroparc
  142. # %%
  143. # Set input path to your directory of Dice matrices\
  144. extract_dir = "data\\dice_validation\\"
  145. dice_files = [f for f in os.listdir(extract_dir) if f.endswith(".csv")]
  146. final_annotated_df = []
  147. boxplot_data = []
  148. for file in dice_files:
  149. region_name = file.replace(".csv", "").replace("DICE_ROI_", "")
  150. df = pd.read_csv(os.path.join(extract_dir, file), index_col=0)
  151. # Exclude low-performing atlases
  152. low_avg_atlases=df.columns[df.median()<.1]
  153. df = df.drop(index=low_avg_atlases, columns=low_avg_atlases, errors='ignore')
  154. if df.shape[0] < 3:
  155. continue
  156. common_labels = [idx for idx in df.index if 'Common' in idx]
  157. if not common_labels:
  158. continue
  159. common_label = common_labels[0]
  160. try:
  161. common_vs_others = df.loc[common_label].drop(labels=[common_label]).dropna()
  162. non_common_df = df.drop(index=common_label, columns=common_label)
  163. inter_comparisons = non_common_df.where(~np.eye(non_common_df.shape[0], dtype=bool)).stack().values
  164. if len(common_vs_others) < 1 or len(inter_comparisons) < 1:
  165. continue
  166. for val in common_vs_others:
  167. boxplot_data.append({'Region': region_name, 'Type': 'Common vs. Others', 'Dice': val})
  168. for val in inter_comparisons:
  169. boxplot_data.append({'Region': region_name, 'Type': 'Others vs. Others', 'Dice': val})
  170. final_annotated_df.append({
  171. "Region": f"{region_name} (n={len(inter_comparisons)})",
  172. "Median Dice (Common vs. Others)": common_vs_others.median(),
  173. "Delta Median (Common vs. Others - Others vs. Others)": common_vs_others.median() - np.median(inter_comparisons)
  174. })
  175. except Exception:
  176. continue
  177. labeled_df = pd.DataFrame(final_annotated_df)
  178. plt.figure(figsize=(5, 4))
  179. ax = sns.scatterplot(
  180. data=labeled_df,
  181. x="Delta Median (Common vs. Others - Others vs. Others)",
  182. y="Median Dice (Common vs. Others)",
  183. color="darkred",
  184. s=30,
  185. edgecolor="black"
  186. )
  187. plt.axvline(0, color='gray', linestyle='--')
  188. plt.xlabel("ΔMedian Dice\n(Common vs. Others - Others vs. Others)", fontsize=12)
  189. plt.ylabel("Median Dice\n(Common vs. Others)", fontsize=12)
  190. texts = []
  191. # Annotate region names with (n)
  192. for _, row in labeled_df.iterrows():
  193. texts.append(ax.text(
  194. row["Delta Median (Common vs. Others - Others vs. Others)"],
  195. row["Median Dice (Common vs. Others)"] + 0.0,
  196. row["Region"],
  197. fontsize=10,
  198. ha='left'
  199. ))
  200. adjust_text(texts, arrowprops=dict(
  201. arrowstyle='->',
  202. color='red',
  203. linewidth=1,
  204. linestyle='dotted',
  205. connectionstyle='angle3,angleA=90,angleB=0',
  206. alpha=0
  207. ))
  208. plt.ylim([.3, .92])
  209. plt.xlim([-.05, .32])
  210. plt.grid(True, axis='y', linestyle='--', linewidth=1, color='gray')
  211. plt.tight_layout()
  212. # %%
  213. # %%
  214. # %%
  215. boxplot_df=pd.DataFrame(boxplot_data)
  216. region_order = (
  217. boxplot_df[boxplot_df['Type'] == 'Common vs. Others']
  218. .groupby('Region')['Dice']
  219. .median()
  220. .sort_values(ascending=False)
  221. .index.tolist()
  222. )
  223. plt.figure(figsize=(5.75, 4))
  224. ax = sns.violinplot(
  225. data=boxplot_df,
  226. x='Region', y='Dice', hue='Type',
  227. order=region_order,
  228. palette='Set2', split=True, cut=0,
  229. inner=None
  230. )
  231. # Get the original colors from palette
  232. palette = sns.color_palette('Set2')
  233. color_map = {
  234. 'Common vs. Others': palette[0],
  235. 'Others vs. Others': palette[1]
  236. }
  237. # Darken each color for marker
  238. def darken_color(color, factor=0.5):
  239. return tuple([max(0, c * factor) for c in color])
  240. # Add bold median markers with darker hues
  241. for i, region in enumerate(region_order):
  242. for typ, offset in zip(['Common vs. Others', 'Others vs. Others'], [-0.05, 0.05]):
  243. subset = boxplot_df[(boxplot_df['Region'] == region) & (boxplot_df['Type'] == typ)]['Dice'].dropna()
  244. median_val = subset.median()
  245. marker_color = darken_color(color_map[typ])
  246. ax.plot(
  247. i + offset,
  248. median_val,
  249. marker='o',
  250. markersize=5,
  251. color=marker_color,
  252. zorder=5
  253. )
  254. plt.xticks(rotation=45, ha='right')
  255. plt.xlabel("",fontsize=12)
  256. plt.ylabel("Dice Coefficient",fontsize=12)
  257. plt.grid(axis='y', linestyle='--', linewidth=1, color='gray')
  258. plt.legend(loc='best', fontsize=10)
  259. plt.tight_layout()
  260. # %%
  261. # %%
  262. boxplot_df
  263. # %%
  264. # %% [markdown]
  265. # #### dice stat tests for revision
  266. # %%
  267. """
  268. Statistical tests for Fig 5a Dice comparisons.
  269. Hypothesis: CHA performs at least as well as inter-atlas agreement
  270. (non-inferiority), not necessarily better.
  271. """
  272. import pandas as pd
  273. import numpy as np
  274. from scipy import stats
  275. # Columns: Region, Type ("Common vs. Others" or "Others vs. Others"), Dice
  276. print(f"Total entries: {len(boxplot_df)}")
  277. print(f"Regions: {boxplot_df['Region'].nunique()}")
  278. print(f"Types: {boxplot_df['Type'].unique()}")
  279. # --- Per-region summary ---
  280. regions = sorted(boxplot_df["Region"].unique())
  281. summary = []
  282. for r in regions:
  283. common = boxplot_df[(boxplot_df["Region"] == r) &
  284. (boxplot_df["Type"] == "Common vs. Others")]["Dice"]
  285. others = boxplot_df[(boxplot_df["Region"] == r) &
  286. (boxplot_df["Type"] == "Others vs. Others")]["Dice"]
  287. if len(common) == 0 or len(others) == 0:
  288. continue
  289. delta_median = common.median() - others.median()
  290. delta_mean = common.mean() - others.mean()
  291. summary.append({
  292. "Region": r,
  293. "n_common": len(common),
  294. "n_others": len(others),
  295. "median_common": common.median(),
  296. "median_others": others.median(),
  297. "delta_median": delta_median,
  298. "mean_common": common.mean(),
  299. "mean_others": others.mean(),
  300. "delta_mean": delta_mean,
  301. })
  302. df_sum = pd.DataFrame(summary)
  303. print(f"\n{'='*70}")
  304. print("PER-REGION SUMMARY")
  305. print(f"{'='*70}")
  306. print(df_sum.to_string(index=False))
  307. # How many regions have delta >= 0?
  308. n_positive = (df_sum["delta_median"] >= 0).sum()
  309. n_total = len(df_sum)
  310. print(f"\nRegions where Common >= Others (median): {n_positive}/{n_total}")
  311. # ============================================================
  312. # TEST 1: Wilcoxon signed-rank on per-region median Dice
  313. # H0: median(Common) < median(Others) (CHA is worse)
  314. # H1: median(Common) >= median(Others) (CHA is non-inferior)
  315. # One-sided test: reject H0 if p < 0.05
  316. # ============================================================
  317. print(f"\n{'='*70}")
  318. print("TEST 1: Wilcoxon signed-rank on per-region median Dice")
  319. print(f"{'='*70}")
  320. deltas = df_sum["delta_median"].values
  321. # One-sided: is delta significantly >= 0?
  322. stat_wilcox, p_two_wilcox = stats.wilcoxon(deltas, alternative="two-sided")
  323. _, p_greater_wilcox = stats.wilcoxon(deltas, alternative="greater")
  324. print(f" n regions: {len(deltas)}")
  325. print(f" Median of deltas: {np.median(deltas):.4f}")
  326. print(f" Mean of deltas: {np.mean(deltas):.4f}")
  327. print(f" Two-sided p: {p_two_wilcox:.4e}")
  328. print(f" One-sided (greater) p: {p_greater_wilcox:.4e}")
  329. if p_greater_wilcox < 0.05:
  330. print(f" → CHA performs significantly at least as well as inter-atlas agreement")
  331. else:
  332. print(f" → Cannot reject that CHA performs worse (but see effect direction)")
  333. # ============================================================
  334. # TEST 2: Non-inferiority with margin
  335. # H0: median(Common) - median(Others) < -margin (CHA is meaningfully worse)
  336. # H1: median(Common) - median(Others) >= -margin (non-inferior)
  337. # Test: Wilcoxon on (deltas + margin), one-sided greater
  338. # ============================================================
  339. print(f"\n{'='*70}")
  340. print("TEST 2: Non-inferiority test")
  341. print(f"{'='*70}")
  342. MARGINS = [0.05, 0.10]
  343. for margin in MARGINS:
  344. shifted = deltas + margin # shift by margin
  345. _, p_ni = stats.wilcoxon(shifted, alternative="greater")
  346. print(f"\n Margin = {margin}:")
  347. print(f" H0: Common - Others < -{margin} (CHA is worse by > {margin})")
  348. print(f" One-sided p: {p_ni:.4e}")
  349. if p_ni < 0.05:
  350. print(f" → Non-inferiority established at margin {margin}")
  351. else:
  352. print(f" → Cannot establish non-inferiority at margin {margin}")
  353. # %%
  354. # %%
  355. # %%
  356. # %% [markdown]
  357. # # Figure 5C
  358. # %%
  359. chf_rois_primate=['FRO_Precentral', 'FRO_Premotor', 'FRO_Prefrontal', 'PAR_Postcentral', 'PAR_Superior', 'PAR_Inferior', 'PAR_Precuneus',
  360. 'TEM_Superior', 'TEM_Inferior', 'TEM_Medial', 'TEM_Hippocampus', 'TEM_Amygdala', 'OCC_Lateral', 'OCC_Medial', 'INS_Anterior',
  361. 'INS_Posterior', 'OLF_Anterior', 'OLF_Piriform', 'CIN_Anterior', 'CIN_Posterior', 'BG_Caudate', 'BG_Putamen',
  362. 'BG_Accumbens', 'BG_Pallidum', 'THL_Thalamus', 'THL_Hypothalamus']
  363. chf_rois_mouse=['FRO_Precentral', 'FRO_Premotor', 'FRO_Prefrontal', 'PAR',
  364. 'TEM_Superior', 'TEM_Inferior', 'TEM_Medial', 'TEM_Hippocampus', 'TEM_Amygdala', 'OCC_Lateral', 'OCC_Medial', 'INS_Anterior',
  365. 'INS_Posterior', 'OLF_Anterior', 'OLF_Piriform', 'CIN_Anterior', 'CIN_Posterior', 'BG_CaudoPutamen',
  366. 'BG_Accumbens', 'BG_Pallidum', 'THL_Thalamus', 'THL_Hypothalamus']
  367. duke_rois_rat=['ACA__Anterior_Cingulate_Area_left','AI__Agranular_Insular_Area_left','AUD__Auditory_Areas_left','ECT__Ectorhinal_Area_left','GU__Gustatory_Areas_left','ILA__Infralimbic_Area_left','MO__Somatomotor_Areas_left','ORB__Orbital_Area_left','PERI__Perirhinal_Area_left','PL__Prelimbic_Area_left','PTLp__Posterior_Parietal_Association_Areas_left','RSP__Retrosplenial_Area_left','SS__Somatosensory_Areas_left','TEa__Temporal_Association_Areas_left','VIS__Visual_Areas_left','VISC__Visceral_Area_left','Isocortex__Isocortex_Uncharted_left','AA__Amygdalar_Area_left','COA__Cortical_Amygdalar_Area_left','PIR__Piriform_Area_left','TT__Tenia_Tecta_left','OLF__Olfactory_Areas_Uncharted_left','BLA__Basolateral_Amygdala_left','CLA__Claustrum_left','ENT__Entorhinal_Area_left','PAR__Parasubiculum_left','POST__Postsubiculum_left','PRE__Presubiculum_left','HPF__Hippocampal_Formation_Uncharted_left','ZI__Zona_Incerta_Uncharted_left','FF__Fields_of_Forel_left','PH__Posterior_Hypothalamic_Nucleus_left','POA__Preoptic_Areas_Uncharted_left','STN__Subthalamic_Nucleus_left','HY__Hypothalamus_Uncharted_left','LSX__Lateral_Septal_Complex_left','PALd__Pallidum_Dorsal_Region_left','STR__Striatum_left','ACB__Nucleus_Accumbens_left','BST__Bed_Nuclei_of_Stria_Terminalis_left','PALv__Pallidum_Ventral_Region_left','VPM__Ventral_Posteromedial_Nucleus_of_the_Thalamus_left','VPL__Ventral_Posterolateral_Nucleus_of_the_Thalamus_left','LD__Lateral_Dorsal_Nucleus_of_the_Thalamus_Uncharted_left','LDVL__Lateral_Dorsal_Nucleus_of_the_Thalamus_Ventrolateral_Part_left','LP__Lateral_Posterior_Nucleus_of_the_Thalamus_left','ATN__Anterior_Group_of_the_Dorsal_Thalamus_left','MG__Medial_Geniculate_Complex_left','LGd__Dorsal_Part_of_the_Lateral_Geniculate_Complex_left','RT__Reticular_Nucleus_of_the_Thalamus_left','PrG__Pregeniculate_Nucleus_left','MD__Mediodorsal_Thalamic_Nucleus_left','TH__Thalamus_Uncharted_left','FRP__Frontal_Pole_Cerebral_Cortex_left']
  368. # %%
  369. # --- Load atlas and CHF images ---
  370. atlas_img = nb.load('data/atlas/mouse/ABA_major_regions.nii.gz').get_fdata()
  371. chf_img = nb.load('data/atlas/mouse/cha_mouse.nii.gz').get_fdata()
  372. # --- Load label maps ---
  373. atlas_label_df = pd.read_table('data/atlas/mouse/ABA_major_regions.txt', index_col=0, header=None, delim_whitespace=True)
  374. chf_label_df = pd.read_table('data/atlas/mouse/cha_mouse.txt', index_col=0, header=None, delim_whitespace=True)
  375. # --- Get valid region labels (exclude 0/background) ---
  376. atlas_labels = np.unique(atlas_img)
  377. atlas_labels = atlas_labels[atlas_labels > 0]
  378. chf_labels = np.unique(chf_img)
  379. chf_labels = chf_labels[chf_labels > 0]
  380. # --- Dice similarity function ---
  381. def dice(mask1, mask2):
  382. intersection = np.logical_and(mask1, mask2).sum()
  383. total = mask1.sum() + mask2.sum()
  384. return 2 * intersection / total if total > 0 else np.nan
  385. # --- Compute Dice matrix ---
  386. dice_matrix = np.zeros((len(atlas_labels), len(chf_labels)))
  387. for i, atlas_val in enumerate(atlas_labels):
  388. mask1 = atlas_img == atlas_val
  389. for j, chf_val in enumerate(chf_labels):
  390. mask2 = chf_img == chf_val
  391. dice_matrix[i, j] = dice(mask1, mask2)
  392. # --- Use region names for row/column labels ---
  393. atlas_names = [atlas_label_df.loc[int(val)][1] for val in atlas_labels]
  394. chf_names = [chf_label_df.loc[int(val)][1] for val in chf_labels]
  395. dice_df = pd.DataFrame(dice_matrix, index=[a[:-1] for a in atlas_names], columns=chf_names)
  396. # %%
  397. fig=plt.figure(figsize=(6, 6))
  398. sns.heatmap(dice_df[chf_rois_mouse],
  399. annot=False, cmap="Reds", fmt=".2f", linewidths=0.5,
  400. cbar_kws={'label': 'Dice Coefficient',
  401. 'shrink': 0.3,
  402. 'aspect': 25,
  403. 'ticks': [0.0, 0.25, 0.5, 0.75, 1.0],
  404. 'orientation': 'vertical'},
  405. square=True)
  406. plt.grid(alpha=.5)
  407. plt.tight_layout()
  408. plt.show()
  409. # %% [markdown]
  410. # # Figure 5d
  411. # ## Containment validation
  412. # %%
  413. import numpy as np
  414. import pandas as pd
  415. import nibabel as nb
  416. import matplotlib.pyplot as plt
  417. from scipy.ndimage import affine_transform
  418. # =============================================================================
  419. # Configuration per species
  420. # =============================================================================
  421. SPECIES_CONFIG = {
  422. 'mouse': {
  423. 'atlas_file': 'ABA.nii.gz',
  424. 'cha_file': 'cha_mouse.nii.gz',
  425. 'atlas_name': 'ABA',
  426. 'baseline_coarser_file': None,
  427. },
  428. 'rat': {
  429. 'atlas_file': 'whs_rat_fit.nii.gz',
  430. 'cha_file': 'cha_rat.nii.gz',
  431. 'atlas_name': 'WHS',
  432. 'baseline_coarser_file': 'duke_civm_fit_combined.nii.gz', # for WHS-in-Duke baseline, an independent evaluation
  433. },
  434. 'marmoset': {
  435. 'atlas_file': 'MBM_vH_subcortical.nii.gz',
  436. 'cha_file': 'cha_marmoset.nii.gz',
  437. 'atlas_name': 'MBM',
  438. 'baseline_coarser_file': None,
  439. },
  440. 'rhesus': {
  441. 'atlas_file': 'civm_rhesus_label.nii.gz',
  442. 'cha_file': 'cha_rhesus.nii.gz',
  443. 'atlas_name': 'CIVM',
  444. 'baseline_coarser_file': None,
  445. },
  446. }
  447. # %%
  448. # =============================================================================
  449. # Helper functions
  450. # =============================================================================
  451. def read_labels(txt_path):
  452. """Read label file: '1 LabelName' per line. Returns DataFrame with index=ID, col 1=name."""
  453. return pd.read_table(txt_path, index_col=0, header=None, sep=r'\s+')
  454. def get_roi_names(txt_path):
  455. """Return list of ROI names from a label txt file."""
  456. labels = read_labels(txt_path)
  457. return list(labels[1].values)
  458. def get_percent_overlap(atlas, target, roi_indices):
  459. """Compute fractional overlap of each atlas ROI with each target parcel.
  460. For each atlas ROI i and target parcel j:
  461. overlap[i,j] = (# voxels where atlas==roi_i AND target==j) / (# voxels where atlas==roi_i)
  462. """
  463. target_ids = np.unique(target)
  464. n_atlas = len(roi_indices)
  465. n_target = len(target_ids)
  466. percent_overlap = np.zeros((n_atlas, n_target))
  467. for i, roi_id in enumerate(roi_indices):
  468. if i % 100 == 0:
  469. print(f' Processing ROI {i}/{n_atlas}')
  470. atlas_mask = (atlas == roi_id)
  471. atlas_count = np.sum(atlas_mask)
  472. if atlas_count == 0:
  473. continue
  474. for j, tid in enumerate(target_ids):
  475. percent_overlap[i, j] = np.sum(atlas_mask & (target == tid)) / atlas_count
  476. return percent_overlap, target_ids
  477. def build_overlap_df(atlas_labels, target_labels_txt, percent_overlap, target_ids):
  478. """Build a DataFrame from overlap matrix with proper column names."""
  479. target_labels = read_labels(target_labels_txt)
  480. # Map target IDs to names; ID=0 maps to '0' (background)
  481. col_names = []
  482. for tid in target_ids:
  483. tid_int = int(tid)
  484. if tid_int == 0:
  485. col_names.append('0')
  486. elif tid_int in target_labels.index:
  487. col_names.append(target_labels.loc[tid_int, 1])
  488. else:
  489. col_names.append(f'unknown_{tid_int}')
  490. popd = pd.DataFrame(percent_overlap, index=atlas_labels[1].values, columns=col_names)
  491. popd['SUM'] = popd.sum(axis=1)
  492. return popd
  493. def compute_containment(popd, target_rois):
  494. """Extract max containment per finer-atlas region across target ROIs."""
  495. valid = popd[target_rois]
  496. valid = valid[valid.max(axis=1) > 0]
  497. max_containment = valid.max(axis=1)
  498. best_match_idx = valid.values.argmax(axis=1)
  499. best_match = [target_rois[i] for i in best_match_idx]
  500. return max_containment, best_match
  501. def compute_target_volume_fractions(target_nii_data, target_labels_txt, target_rois):
  502. """Compute volume fraction of each target ROI relative to total labeled volume."""
  503. target_labels = read_labels(target_labels_txt)
  504. # Build name -> list of IDs mapping
  505. name_to_ids = {}
  506. for idx, row in target_labels.iterrows():
  507. name = row[1]
  508. if name in target_rois:
  509. if name not in name_to_ids:
  510. name_to_ids[name] = []
  511. name_to_ids[name].append(idx)
  512. # Count voxels per target ROI
  513. total_labeled = 0
  514. roi_volumes = {}
  515. for name in target_rois:
  516. if name in name_to_ids:
  517. count = sum(np.sum(target_nii_data == rid) for rid in name_to_ids[name])
  518. else:
  519. count = 0
  520. roi_volumes[name] = count
  521. total_labeled += count
  522. # Convert to fractions
  523. vol_fracs = {name: count / total_labeled if total_labeled > 0 else 0
  524. for name, count in roi_volumes.items()}
  525. return vol_fracs
  526. def adjust_containment(popd, target_rois, target_vol_fracs):
  527. """Apply specificity correction: adjusted = (raw - expected) / (1 - expected).
  528. expected = volume fraction of best-matching target parcel.
  529. Corrects for inflation due to large target parcels.
  530. """
  531. valid = popd[target_rois]
  532. valid = valid[valid.max(axis=1) > 0]
  533. raw_max = valid.max(axis=1)
  534. best_idx = valid.values.argmax(axis=1)
  535. expected = np.array([target_vol_fracs[target_rois[i]] for i in best_idx])
  536. adjusted = (raw_max.values - expected) / (1 - expected)
  537. adjusted = np.clip(adjusted, 0, 1) # floor at 0
  538. return pd.Series(adjusted, index=raw_max.index)
  539. def spatial_perturbation(atlas_data, target_data, atlas_labels, target_labels_txt, rot=0):
  540. """Apply rotation perturbation to target and recompute containment."""
  541. theta = np.deg2rad(rot)
  542. affine_matrix = np.array([
  543. [np.cos(theta), -np.sin(theta), 0],
  544. [np.sin(theta), np.cos(theta), 0],
  545. [0, 0, 1]
  546. ])
  547. transformed = affine_transform(target_data, affine_matrix, offset=[0, 0, 0], order=0, mode='nearest')
  548. percent_overlap, target_ids = get_percent_overlap(atlas_data, transformed, atlas_labels.index.values)
  549. popd = build_overlap_df(atlas_labels, target_labels_txt, percent_overlap, target_ids)
  550. return popd
  551. # %%
  552. def run_containment_analysis(species, use_adjusted=False, plot=True):
  553. """Run full containment analysis for a species.
  554. Parameters
  555. ----------
  556. species : str
  557. One of 'mouse', 'rat', 'marmoset', 'rhesus'
  558. use_adjusted : bool
  559. If True, apply specificity correction for parcel size
  560. plot : bool
  561. If True, generate ECDF plot
  562. """
  563. cfg = SPECIES_CONFIG[species]
  564. atlas_dir = f'data/atlas/{species}/'
  565. # --- Read atlas and CHA files ---
  566. atlas_file = cfg['atlas_file']
  567. cha_file = cfg['cha_file']
  568. atlas_name = cfg['atlas_name']
  569. atlas_labels_txt = atlas_dir + atlas_file.replace('.nii.gz', '.txt')
  570. cha_labels_txt = atlas_dir + cha_file.replace('.nii.gz', '.txt')
  571. atlas_labels = read_labels(atlas_labels_txt)
  572. cha_rois = get_roi_names(cha_labels_txt)
  573. print(f'\n{"="*60}')
  574. print(f'Species: {species}')
  575. print(f'Atlas: {atlas_name} ({len(atlas_labels)} regions)')
  576. print(f'CHA: {len(cha_rois)} regions')
  577. print(f'{"="*60}')
  578. # --- Load volumes (full, unfiltered — matches old code) ---
  579. vols_file = atlas_dir + atlas_file.replace('.nii.gz', '_roistats.txt')
  580. vols_df = pd.read_table(vols_file, index_col=0).loc['volume (mm^3)']
  581. vols = vols_df.values
  582. # --- Load or compute CHA containment ---
  583. cha_overlap_file = (atlas_dir + atlas_file.replace('.nii.gz', '') +
  584. '_overlaps_' + cha_file.replace('.nii.gz', '') + '.csv')
  585. try:
  586. popd_cha = pd.read_csv(cha_overlap_file, index_col=0)
  587. print(f'Loaded CHA containment from {cha_overlap_file}')
  588. except FileNotFoundError:
  589. print(f'Computing CHA containment...')
  590. at = nb.load(atlas_dir + atlas_file)
  591. chf = nb.load(atlas_dir + cha_file)
  592. atlas_data = at.get_fdata()
  593. cha_data = chf.get_fdata()
  594. overlap, target_ids = get_percent_overlap(atlas_data, cha_data, atlas_labels.index.values)
  595. popd_cha = build_overlap_df(atlas_labels, cha_labels_txt, overlap, target_ids)
  596. popd_cha.to_csv(cha_overlap_file)
  597. # --- Compute containment (replicating old code logic) ---
  598. # Filter to regions with any nonzero CHA overlap, get max per region
  599. containment_values_cha = popd_cha[cha_rois][popd_cha[cha_rois].max(axis=1) > 0].max(axis=1)
  600. containment_scores_cha = np.array(containment_values_cha)
  601. # Apply specificity correction if requested
  602. if use_adjusted:
  603. chf = nb.load(atlas_dir + cha_file)
  604. cha_data = chf.get_fdata()
  605. cha_vol_fracs = compute_target_volume_fractions(cha_data, cha_labels_txt, cha_rois)
  606. # Get best-matching CHA region for each atlas region
  607. valid_rows = popd_cha[cha_rois][popd_cha[cha_rois].max(axis=1) > 0]
  608. best_idx = valid_rows.values.argmax(axis=1)
  609. expected = np.array([cha_vol_fracs[cha_rois[i]] for i in best_idx])
  610. containment_scores_cha = np.clip(
  611. (containment_scores_cha - expected) / (1 - expected), 0, 1)
  612. # Volumes: use full array, index with sorted_idx (old code logic)
  613. volumes_cha = np.array(vols)
  614. sorted_idx_cha = np.argsort(containment_scores_cha)
  615. sorted_scores_cha = containment_scores_cha[sorted_idx_cha]
  616. sorted_volumes_cha = volumes_cha[sorted_idx_cha]
  617. total_volume_cha = np.sum(sorted_volumes_cha)
  618. ecdf_cha = np.cumsum(sorted_volumes_cha) / total_volume_cha
  619. # --- Print summary ---
  620. def print_summary(scores, volumes_sorted, total_vol, label):
  621. n = len(scores)
  622. s = np.array(scores)
  623. print(f'\n {label} ({n} regions):')
  624. print(f' >= 0.95: {np.sum(s >= 0.95):3d} ({100*np.sum(s >= 0.95)/n:.0f}%)')
  625. print(f' >= 0.80: {np.sum(s >= 0.80):3d} ({100*np.sum(s >= 0.80)/n:.0f}%)')
  626. print(f' >= 0.50: {np.sum(s >= 0.50):3d} ({100*np.sum(s >= 0.50)/n:.0f}%)')
  627. print(f' Mean: {np.mean(s):.3f} Median: {np.median(s):.3f}')
  628. for threshold in [0.50, 0.80, 0.95]:
  629. vol_frac = np.sum(volumes_sorted[s >= threshold]) / total_vol
  630. print(f' {100*vol_frac:.0f}% vol >= {threshold}')
  631. print_summary(sorted_scores_cha, sorted_volumes_cha, total_volume_cha,
  632. f'{atlas_name} in CHA')
  633. # --- Baseline comparison (if available) ---
  634. sorted_scores_bl = None
  635. sorted_volumes_bl = None
  636. ecdf_bl = None
  637. baseline_name = None
  638. if cfg['baseline_coarser_file'] is not None:
  639. baseline_file = cfg['baseline_coarser_file']
  640. baseline_labels_txt = atlas_dir + baseline_file.replace('.nii.gz', '.txt')
  641. baseline_rois = get_roi_names(baseline_labels_txt)
  642. baseline_name = baseline_file.replace('.nii.gz', '').replace('_combined', '')
  643. baseline_overlap_file = (atlas_dir + atlas_file.replace('.nii.gz', '') +
  644. '_overlaps_' + baseline_file.replace('.nii.gz', '') + '.csv')
  645. try:
  646. popd_bl = pd.read_csv(baseline_overlap_file, index_col=0)
  647. print(f'Loaded baseline containment from {baseline_overlap_file}')
  648. except FileNotFoundError:
  649. print(f'Computing baseline containment...')
  650. at = nb.load(atlas_dir + atlas_file)
  651. bl = nb.load(atlas_dir + baseline_file)
  652. atlas_data = at.get_fdata()
  653. bl_data = bl.get_fdata()
  654. overlap, target_ids = get_percent_overlap(atlas_data, bl_data, atlas_labels.index.values)
  655. popd_bl = build_overlap_df(atlas_labels, baseline_labels_txt, overlap, target_ids)
  656. popd_bl.to_csv(baseline_overlap_file)
  657. # Containment scores for baseline
  658. containment_values_bl = popd_bl[baseline_rois][popd_bl[baseline_rois].max(axis=1) > 0].max(axis=1)
  659. containment_scores_bl = np.array(containment_values_bl)
  660. if use_adjusted:
  661. bl = nb.load(atlas_dir + baseline_file)
  662. bl_data = bl.get_fdata()
  663. bl_vol_fracs = compute_target_volume_fractions(bl_data, baseline_labels_txt, baseline_rois)
  664. valid_rows_bl = popd_bl[baseline_rois][popd_bl[baseline_rois].max(axis=1) > 0]
  665. best_idx_bl = valid_rows_bl.values.argmax(axis=1)
  666. expected_bl = np.array([bl_vol_fracs[baseline_rois[i]] for i in best_idx_bl])
  667. containment_scores_bl = np.clip(
  668. (containment_scores_bl - expected_bl) / (1 - expected_bl), 0, 1)
  669. volumes_bl = np.array(vols)
  670. sorted_idx_bl = np.argsort(containment_scores_bl)
  671. sorted_scores_bl = containment_scores_bl[sorted_idx_bl]
  672. sorted_volumes_bl = volumes_bl[sorted_idx_bl]
  673. total_volume_bl = np.sum(sorted_volumes_bl)
  674. ecdf_bl = np.cumsum(sorted_volumes_bl) / total_volume_bl
  675. print_summary(sorted_scores_bl, sorted_volumes_bl, total_volume_bl,
  676. f'{atlas_name} in {baseline_name}')
  677. # --- Plot ECDF ---
  678. if plot:
  679. fig, ax = plt.subplots(figsize=(2.5, 2.5))
  680. # CHA containment ECDF
  681. ax.plot(sorted_scores_cha, ecdf_cha, marker='.', markersize=2,
  682. linewidth=1, color='seagreen', label=f'{atlas_name} in CHA')
  683. # Baseline ECDF (if available)
  684. if sorted_scores_bl is not None:
  685. ax.plot(sorted_scores_bl, ecdf_bl, marker='.', markersize=2,
  686. linewidth=1, color='gray', alpha=0.6,
  687. label=f'{atlas_name} in {baseline_name}')
  688. ax.legend(fontsize=6)
  689. # Threshold annotations
  690. for threshold in [0.8, 0.95]:
  691. prop = np.sum(sorted_volumes_cha[sorted_scores_cha >= threshold]) / total_volume_cha
  692. ax.axvline(threshold, color='gray', linestyle='--', alpha=0.5)
  693. ax.text(threshold - 0.075, 0.4, f"{prop:.0%} vol ≥ {threshold}",
  694. rotation=90, va='bottom', fontsize=7)
  695. adj_label = ' (adjusted)' if use_adjusted else ''
  696. ax.set_xlabel(f"Max containment per {atlas_name} region", fontsize=8)
  697. ax.set_ylabel(f"Cumulative fraction of\ntotal {atlas_name} volume", fontsize=8)
  698. ax.set_title(f"ECDF: Containment of {atlas_name} regions\n{species}{adj_label}", fontsize=8)
  699. ax.set_xlim([-0.025, 1.05])
  700. ax.tick_params(labelsize=7)
  701. ax.grid(True, linestyle='--', axis='y', alpha=1)
  702. plt.tight_layout()
  703. plt.savefig(f'{atlas_dir}containment_ecdf_{species}.svg')
  704. plt.show()
  705. return sorted_scores_cha, sorted_scores_bl
  706. # %%
  707. for species in ['mouse', 'rat', 'marmoset', 'rhesus']:
  708. run_containment_analysis(species, use_adjusted=True, plot=True)
  709. # %%
  710. # %%
  711. # %%
  712. # %%
  713. from scipy.ndimage import affine_transform
  714. def get_percent_overlap(atlas, tract, roi_indices):
  715. CHF_ROIS=chf_labels_num
  716. percent_overlap=np.zeros((len(roi_indices), len(CHF_ROIS)))
  717. print(len(roi_indices))
  718. for roi_id in range(len(percent_overlap)):
  719. if roi_id%100==0:
  720. print(roi_id)
  721. for id_ in range(len(percent_overlap[roi_id])):
  722. percent_overlap[roi_id][id_] = np.sum(atlas[tract==id_]==roi_indices[roi_id]) /np.sum(atlas==roi_indices[roi_id])
  723. return percent_overlap
  724. #spatial perturbation
  725. def transform_small(atlas_,
  726. chf_nii_,
  727. atlas_labels_,
  728. chf_labels_,
  729. rot=0):
  730. theta = np.deg2rad(rot)
  731. affine_matrix = np.array([
  732. [np.cos(theta), -np.sin(theta), 0],
  733. [np.sin(theta), np.cos(theta), 0],
  734. [0, 0, 1]
  735. ])
  736. offset = [0, 0, 0] # voxel shift
  737. transformed = affine_transform(chf_nii_, affine_matrix, offset=offset, order=0, mode='nearest')
  738. percent_overlap=get_percent_overlap(atlas_, transformed, atlas_labels_.index.values)
  739. ls_=list(chf_labels_[1].values)
  740. ls_.insert(0, '0')
  741. popd_=pd.DataFrame(percent_overlap, index=atlas_labels_[1].values, columns=ls_)
  742. popd_['SUM']=popd_.sum(axis=1)
  743. return popd_
  744. # %%
  745. # %%
  746. # %% [markdown]
  747. # #### Rat preprocessing
  748. # %% [markdown]
  749. # #### apply spatial perturbation
  750. # %% [markdown]
  751. # ### Set species
  752. # %%
  753. species='mouse'
  754. # %%
  755. atlas_dir = f'data/atlas/{species}/'
  756. atlas_file=SPECIES_CONFIG[species]['atlas_file']
  757. atlas_name=SPECIES_CONFIG[species]['atlas_name']
  758. cha_file=SPECIES_CONFIG[species]['cha_file']
  759. baseline_overlap_file = (atlas_dir + atlas_file.replace('.nii.gz', '') +
  760. '_overlaps_' + cha_file.replace('.nii.gz', ''))
  761. # %% [markdown]
  762. # #### read spatial perturbation outputs
  763. # %%
  764. angles=[0,1,2,3,4,5]
  765. containment_values=[]
  766. if species == 'mouse':
  767. chf_rois=chf_rois_mouse
  768. if species == 'rhesus' or species == 'marmoset':
  769. chf_rois=chf_rois_primate
  770. if species == 'rat':
  771. print('containment rotational perturbation only performed for mouse, marmoset and rhesus which do not have an independent baseline containment criteria similar to WHS in DUke for Rat atlas!')
  772. for ang in angles:
  773. popd_=pd.read_csv(baseline_overlap_file+'_rot'+str(ang)+'.csv', index_col=0)
  774. containment_values.append(popd_[chf_rois][popd_[chf_rois].max(axis=1)>0].max(axis=1))
  775. # %%
  776. # Flatten into long-form DataFrame
  777. data = pd.DataFrame({
  778. 'Containment': [val for s in containment_values for val in s],
  779. 'Rotation': [angle for angle, s in zip(angles, containment_values) for _ in s]
  780. })
  781. # Compute summary statistics
  782. mean_values = [s.mean() for s in containment_values]
  783. median_values = [s.median() for s in containment_values]
  784. #plt.figure(figsize=(1.35, 1.55))
  785. plt.figure(figsize=(5,5))
  786. ax = sns.violinplot(
  787. data=data,
  788. x='Rotation',
  789. y='Containment',
  790. palette='Set2',
  791. cut=0,
  792. inner='box', linewidth=0.1
  793. )
  794. # Overlay the curve
  795. plt.plot(angles, median_values, '--', color='black', linewidth=1, label='Median Containment', alpha=.5)
  796. region_counts = [len(s) for s in containment_values]
  797. for i, count in enumerate(region_counts):
  798. ax.text(i-.3, 0.05, f'n={count}', ha='center', va='bottom', fontsize=11, rotation=90)
  799. ax.set_title("Containment Distribution \nAcross Rotations: "+atlas_name, fontsize=11)
  800. ax.set_xlabel("Rotation Angle (°)", fontsize=11)
  801. ax.set_ylabel("")
  802. plt.xticks(fontsize=11)
  803. plt.yticks(fontsize=11)
  804. ax.spines['top'].set_visible(False)
  805. ax.spines['right'].set_visible(False)
  806. plt.tight_layout()
  807. #plt.show()
  808. # %%
  809. # %%
  810. # %%

CHA_quants.ipynb at commit 056d459, under MIT · at the source

Overview

Authors: Siva Venkadesh1, Yuhe Tian1, Wen-Jieh Linn2, Jessica Barrios-Martinez1, Harrison Mansour3, James Cook3, David J Schaeffer2,4, Diego Szczupak4, Afonso C Silva2,4, G Allan Johnson3, Fang-Cheng Yeh1
  1. Department of Neurological Surgery, University of Pittsburgh, Pittsburgh, PA USA
  2. Department of Bioengineering, University of Pittsburgh, Pittsburgh, PA USA
  3. Duke Center for In Vivo Microscopy, Departments of Radiology and Biomedical Engineering, Duke University, Durham, NC USA
  4. Department of Neurobiology, University of Pittsburgh, Pittsburgh, PA USA
Institutions: University of Pittsburgh (United States); Duke University (United States); Duke Medical Center (United States)
Journal: Nature communications, volume 17, issue 1, article 8964
Dates: received 19 November 2025; accepted 10 July 2026; published online 23 July 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1038/s41467-026-75837-5 · PMID 42637761 · PMCID PMC13503721 · OpenAlex W7170073313
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: human (organism), mouse (organism), rat (organism), non-human primate (organism), methods / tools (subfield)
Methods: Connectivity, Statistics, Machine learning, fMRI & imaging
Keywords: Brain, Neuroscience
MeSH: Brain*, Gray Matter*, Animals, Brain Mapping, Callithrix, Humans, Macaca mulatta, Magnetic Resonance Imaging, Male, Mice, Rats, Species Specificity (* major topic)
Topic: Functional Brain Connectivity Studies (Cognitive Neuroscience, Neuroscience), according to OpenAlex
Funding: U.S. Department of Health &amp; Human Services | National Institutes of Health (R01NS120954, R01MH134004); NINDS NIH HHS (R01 NS120954); NIMH NIH HHS (R01 MH134004); U.S. Department of Health & Human Services | National Institutes of Health (NIH) (R01MH134004, R01NS120954)
Citations: cited by 1 paper (Europe PMC); 65 references in the paper

Abstract

Translational neuroscience requires consistent anatomical frameworks to compare brain organization across species despite differences in size and specialization. Existing atlases are species-specific, limiting cross-species analyses. Here we developed a hierarchical common atlas delineating homologous cortical and subcortical gray matter regions across mouse, rat, marmoset, rhesus macaque, and human, built upon population-averaged minimal deformation templates and uniform tissue segmentation. We validated the atlas using four independent approaches: cross-atlas Dice similarity against established human parcellations, cross-scale containment against species-specific atlases, cross-species geometric consistency of regional positioning, and comparison of independent mouse and marmoset tracer connectivity. A per-region homology confidence index quantifies the strength of each regional assignment. Cross-species tracer comparison revealed a structured gradient of correspondence, with sensorimotor connections showing strong conservation and association connections showing progressive divergence. This freely available atlas provides a unified coordinate system for comparative neuroscience, enabling quantitative evaluation of where cross-species correspondence holds and where species-specific divergence emerges.

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

Repositories

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

Zenodo 17653308

License: MIT
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Size: 1 file
Software Heritage: not checked
Found in: “Code availability”
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: NumPy (14 files), pandas (12 files), Matplotlib (10 files), SciPy (7 files), NiBabel (5 files), NetworkX (4 files), seaborn (4 files), statsmodels (2 files)
Availability: 1 check, the latest on 27 September 2026: the link answers (HTTP 200)
  • 27 September 2026: the link answers (HTTP 200)
19 files
At the source:

sivaven/omniconnectmodel

License: MIT
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: 056d459a3c13d0be365f5f52ac1306c429e7941c, 17 June 2026
Languages: Python (12), Jupyter (3), Shell (2)
Size: 4,197 files, 17 scripts
Software Heritage: not archived
Found in: the Zenodo archive record
Holds: README, license file, environment (projects/common_cross_species_atlas/requirements.txt), 3 notebooks
Not found: CITATION.cff, tests, continuous integration, documentation
Tools: NumPy (14 files), pandas (12 files), Matplotlib (10 files), SciPy (7 files), NiBabel (5 files), NetworkX (4 files), seaborn (4 files), statsmodels (2 files)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
19 files

Code availability

All analysis and figure-generation code supporting the findings of this study, including a standalone module (CHA_integrate.py) implementing atlas and connectome integration, are archived at Zenodo (10.5281/zenodo.17653308) in the directory projects/common_cross_species_atlas/.

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:

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

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

Data

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

Data Availability Statement

The Common Hierarchical Atlas (CHA) generated in this study is provided as Supplementary Data with this paper. The source datasets analyzed in this study are publicly available: the ICBM 2009a human template (ref. 9); the Brain/MINDS Marmoset Brain MRI dataset NA216 (DOI 10.24475/bminds.mri.thj.4624; ref. 50) and the Marmoset Brain Mapping v3 templates (ref. 10); rhesus macaque images from the PRIMatE Data Exchange (PRIME-DE) via the International Neuroimaging Data-sharing Initiative (ref. 51); the Allen Mouse Brain Connectivity Atlas (connectivity.brain-map.org; ref. 26); the Marmoset Brain Connectivity Atlas (marmosetbrain.org; ref. 27); the Waxholm Space rat atlas (WHS v4.01; refs. 24,42); the Duke Wistar rat atlas (ref. 25); the Neuroparc human parcellation repository (ref. 4); and the reference atlases used for homology mapping (Allen Brain Atlas, ref. 40; CIVM rhesus, ref. 57; INIA19-NeuroMaps, ref. 58; Brain/MINDS marmoset, ref. 59; MBM, ref. 60; Paxinos, ref. 61; FreeSurfer, refs. 54,55; Brodmann, ref. 56). The mouse and rat diffusion MRI used to construct the templates were acquired at the Duke Center for In Vivo Microscopy and are described in refs. 43,44; no new animal data were generated for this study. The source data underlying Figs. 4–7 are openly available as .csv and .txt files in the repository deposited at Zenodo (see Code Availability), and a documented Jupyter notebook with figure-labelled cells regenerates all panels from these source files.

All analysis and figure-generation code supporting the findings of this study, including a standalone module (CHA_integrate.py) implementing atlas and connectome integration, are archived at Zenodo (10.5281/zenodo.17653308) in the directory projects/common_cross_species_atlas/.

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

Versions

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

Version 1, 27 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 11 authors, 2 keywords, 12 MeSH terms, 4 funders, 58 references.

Cite

This paper

Venkadesh, S., Tian, Y., Linn, W.-J., Barrios-Martinez, J., Mansour, H., Cook, J., Schaeffer, D. J., Szczupak, D., Silva, A. C., Johnson, G. A., & Yeh, F.-C. (2026). A hierarchical framework for cortical and subcortical gray-matter parcellation across rodents, primates, and humans. Nature communications, 17(1), 8964. https://doi.org/10.1038/s41467-026-75837-5

BibTeX

@article{venkadesh2026hierarchical,
author = {Venkadesh, Siva and Tian, Yuhe and Linn, Wen-Jieh and Barrios-Martinez, Jessica and Mansour, Harrison and Cook, James and Schaeffer, David J and Szczupak, Diego and Silva, Afonso C and Johnson, G Allan and Yeh, Fang-Cheng},
title = {{A hierarchical framework for cortical and subcortical gray-matter parcellation across rodents, primates, and humans}},
journal = {Nature communications},
year = {2026},
month = jul,
volume = {17},
number = {1},
pages = {8964},
publisher = {Nature Publishing Group},
issn = {2041-1723},
doi = {10.1038/s41467-026-75837-5},
url = {https://doi.org/10.1038/s41467-026-75837-5},
pmid = {42637761},
pmcid = {PMC13503721}
}

RIS

TY - JOUR
AU - Venkadesh, Siva
AU - Tian, Yuhe
AU - Linn, Wen-Jieh
AU - Barrios-Martinez, Jessica
AU - Mansour, Harrison
AU - Cook, James
AU - Schaeffer, David J
AU - Szczupak, Diego
AU - Silva, Afonso C
AU - Johnson, G Allan
AU - Yeh, Fang-Cheng
TI - A hierarchical framework for cortical and subcortical gray-matter parcellation across rodents, primates, and humans
T2 - Nature communications
J2 - Nat Commun
PY - 2026
DA - 2026/07/23
VL - 17
IS - 1
SP - 8964
SN - 2041-1723
PB - Nature Publishing Group
DO - 10.1038/s41467-026-75837-5
UR - https://doi.org/10.1038/s41467-026-75837-5
LA - en
ER -

CSL-JSON

{
"id": "10.1038/s41467-026-75837-5",
"type": "article-journal",
"title": "A hierarchical framework for cortical and subcortical gray-matter parcellation across rodents, primates, and humans",
"container-title": "Nature communications",
"author": [
{
"family": "Venkadesh",
"given": "Siva"
},
{
"family": "Tian",
"given": "Yuhe"
},
{
"family": "Linn",
"given": "Wen-Jieh"
},
{
"family": "Barrios-Martinez",
"given": "Jessica"
},
{
"family": "Mansour",
"given": "Harrison"
},
{
"family": "Cook",
"given": "James"
},
{
"family": "Schaeffer",
"given": "David J"
},
{
"family": "Szczupak",
"given": "Diego"
},
{
"family": "Silva",
"given": "Afonso C"
},
{
"family": "Johnson",
"given": "G Allan"
},
{
"family": "Yeh",
"given": "Fang-Cheng"
}
],
"container-title-short": "Nat Commun",
"volume": "17",
"issue": "1",
"page": "8964",
"DOI": "10.1038/s41467-026-75837-5",
"PMID": "42637761",
"PMCID": "PMC13503721",
"ISSN": "2041-1723",
"publisher": "Nature Publishing Group",
"URL": "https://doi.org/10.1038/s41467-026-75837-5",
"language": "en",
"issued": {
"date-parts": [
[
2026,
7,
23
]
]
}
}

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/s41592-026-03159-x [code]
Siibra: a software tool suite for realizing a Multilevel Human Brain Atlas from complex data resources.
Journal: Nature methods
In common: NiBabel, seaborn, pandas, 3 other tools, methods / tools, 4 references
[2] doi:10.1038/s42003-026-10276-y [code]
The cellular correlates and adolescent reorganisation of cortical myelination networks in the common marmoset.
Journal: Communications biology
In common: NiBabel, statsmodels, seaborn, 4 other tools, non-human primate, 3 references
[3] doi:10.1038/s41467-026-73072-6 [code]
Mapping the spatiotemporal continuum of structural connectivity development across the human connectome in youth.
Journal: Nature communications
In common: NiBabel, statsmodels, pandas, 1 other tool, 2 references, author Fang-Cheng Yeh
[4] doi:10.1016/j.ebiom.2026.106259 [code]
Translating brain anatomy and disease from mouse to human in latent gene expression space.
Journal: EBioMedicine
In common: NiBabel, seaborn, pandas, 3 other tools, mouse, 3 references
[5] doi:10.1038/s41593-026-02205-3 [code]
Competitive interactions shape mammalian brain network dynamics and computation.
Journal: Nature neuroscience
In common: NetworkX, NiBabel, seaborn, 4 other tools, non-human primate, mouse, 1 reference
[6] 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: NiBabel, seaborn, pandas, 3 other tools, 3 references
[7] 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: NiBabel, seaborn, pandas, 3 other tools, 3 references
[8] doi:10.1002/hbm.70483 [code]
Untamed: Unconstrained Tensor Decomposition and Graph Node Embedding for Cortical Parcellation.
Journal: Human brain mapping
In common: NiBabel, statsmodels, SciPy, 2 other tools, methods / tools, 3 references
[9] doi:10.1371/journal.pone.0346575 [code]
Statistically valid explainable black-box machine learning: applications in sex classification across species using brain imaging.
Journal: PloS one
In common: NiBabel, seaborn, pandas, 3 other tools, non-human primate, methods / tools, 2 references
[10] doi:10.1038/s41467-026-75959-w [code]
Charting higher-order models of brain function beyond pairwise interactions.
Journal: Nature communications
In common: NetworkX, NiBabel, statsmodels, 5 other tools, 1 reference

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

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.