OSCR

Focused ultrasound blood-brain barrier opening reveals a paradoxical remote metabolic response in the primate brain.

Code ↔ Paper

3 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 3 matches
  1. [1] § MATERIALS AND METHODS › Statistical analysis ↔ 3-Combined_Monkeys_OEF_Bayesian_template.ipynb, lines 641–699 · score 0.77 · random intercept, PyMC, ROI side, Student, likelihood, hierarchical
  2. [2] § MATERIALS AND METHODS › Quantitative BOLD modeling ↔ 1-Monkey_qBOLD_BBBO_template.ipynb, lines 96–102 · score 0.62 · PyTorch, model parameters, OEF map, fitting
  3. [3] § MATERIALS AND METHODS › LIFU BBBO ↔ 3-Combined_Monkeys_OEF_Bayesian_template.ipynb, lines 18–100 · score 0.55 · right caudate, left caudate, M2, M1, M3, M4

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 · 951 lines · 48 KB · apgl-v3 · 2 matches

  1. # %%
  2. # -----------------------------------------------------------------------------
  3. # 0) IMPORTS
  4. # -----------------------------------------------------------------------------
  5. import os
  6. from pathlib import Path
  7. import numpy as np
  8. import pandas as pd
  9. import nibabel as nib
  10. import matplotlib.pyplot as plt
  11. import seaborn as sns
  12. import matplotlib.gridspec as gridspec
  13. import jax
  14. import pymc as pm
  15. import arviz as az
  16. from IPython.display import display
  17. # -----------------------------------------------------------------------------
  18. # 1) SETTINGS & PATHS for MULTI‐SUBJECT ANALYSIS
  19. # -----------------------------------------------------------------------------
  20. # Set the base directory for all processed data here. Change this to update all paths.
  21. BASE_DATA_DIR = Path("YOUR DIRECTORY")
  22. output_dir = BASE_DATA_DIR / "CombinedMonkeys_OEF_Bayesian"
  23. output_dir.mkdir(parents=True, exist_ok=True) # Ensure the directory exists
  24. subjects = {
  25. 'M4': dict(
  26. SUBFOLDER = "M4",
  27. VOLUMES = {
  28. "Base": ["1_OEF_Base.nii.gz"],
  29. "RCN": ["2_OEF_BBBO_RCN.nii.gz"],
  30. "LCN": ["3_OEF_BBBO_LCN.nii.gz"]
  31. },
  32. MASK_BRAIN = "BrainMask_common.nii.gz",
  33. MASK_ROI_RCN = "2_FUSROI_BBBO_RCN.nii.gz",
  34. MASK_ROI_LCN = "3_FUSROI_BBBO_LCN.nii.gz",
  35. MASK_Caudate_Right = "RightCaudate.nii.gz",
  36. MASK_Caudate_Left = "LeftCaudate.nii.gz",
  37. MASK_Putamen_Right = "RightPutamen.nii.gz",
  38. MASK_Putamen_Left = "LeftPutamen.nii.gz"
  39. ),
  40. 'M3': dict(
  41. SUBFOLDER = "M3",
  42. VOLUMES = {
  43. "Base": ["1_OEF_Base.nii.gz"], # Ctrl1
  44. "RCN": [
  45. "2_OEF_BBBO_RCN.nii.gz", # RCN3
  46. "3_OEF_BBBO_RCN.nii.gz" # RCN4
  47. ],
  48. "LCN": ["4_OEF_BBBO_LCN.nii.gz"] # LCN
  49. },
  50. MASK_BRAIN = "BrainMask_common.nii.gz",
  51. MASK_ROI_RCN = [
  52. "2_FUSROI_BBBO_RCN.nii.gz", # FUSROI for RCN3
  53. "3_FUSROI_BBBO_RCN.nii.gz" # FUSROI for RCN4
  54. ],
  55. MASK_ROI_LCN = "4_FUSROI_BBBO_LCN.nii.gz",
  56. MASK_Caudate_Right = "RightCaudate.nii.gz",
  57. MASK_Caudate_Left = "LeftCaudate.nii.gz",
  58. MASK_Putamen_Right = "RightPutamen.nii.gz",
  59. MASK_Putamen_Left = "LeftPutamen.nii.gz"
  60. ),
  61. 'M1': dict(
  62. SUBFOLDER = "M1",
  63. VOLUMES = {
  64. "Base": [
  65. "1_OEF_Base.nii.gz",
  66. "2_OEF_Base.nii.gz"
  67. ],
  68. "RCN": ["3_OEF_BBBO_RCN.nii.gz"]
  69. },
  70. MASK_BRAIN = "BrainMask_common.nii.gz",
  71. MASK_ROI_RCN = "3_FUSROI_BBBO_RCN.nii.gz",
  72. MASK_Caudate_Right = "RightCaudate.nii.gz",
  73. MASK_Caudate_Left = "LeftCaudate.nii.gz",
  74. MASK_Putamen_Right = "RightPutamen.nii.gz",
  75. MASK_Putamen_Left = "LeftPutamen.nii.gz"
  76. ),
  77. 'M2': dict(
  78. SUBFOLDER = "M2",
  79. VOLUMES = {
  80. "Base": ["1_OEF_Base.nii.gz"],
  81. "RCN": [
  82. "2_OEF_BBBO_RCN.nii.gz", # RCN2
  83. "3_OEF_BBBO_RCN.nii.gz" # RCN3
  84. ]
  85. },
  86. MASK_BRAIN = "BrainMask_common.nii.gz",
  87. MASK_ROI_RCN = [
  88. "2_FUSROI_BBBO_RCN.nii.gz", # FUSROI for RCN3
  89. "3_FUSROI_BBBO_RCN.nii.gz" # FUSROI for RCN4
  90. ],
  91. MASK_Caudate_Right = "RightCaudate.nii.gz",
  92. MASK_Caudate_Left = "LeftCaudate.nii.gz",
  93. MASK_Putamen_Right = "RightPutamen.nii.gz",
  94. MASK_Putamen_Left = "LeftPutamen.nii.gz"
  95. ),
  96. }
  97. TREATMENT_TYPES = ["RCN","LCN"] # Define all possible treatment types
  98. # seaborn style overrides
  99. custom_params = {
  100. 'ytick.left': True, 'xtick.bottom': True,
  101. 'xtick.direction': 'out','xtick.color': 'black',
  102. 'ytick.color': 'black','text.color': 'black',
  103. 'grid.color': 'black'
  104. }
  105. sns.set_theme(
  106. context='notebook', style='white', palette='deep',
  107. font='sans-serif', font_scale=1, color_codes=True,
  108. rc=custom_params
  109. )
  110. # -----------------------------------------------------------------------------
  111. # 2) HELPER FUNCTIONS (bootstrap_diff, cohens_d, load_nifti are unchanged)
  112. # -----------------------------------------------------------------------------
  113. def load_nifti(path):
  114. return nib.load(str(path)).get_fdata()
  115. def bootstrap_diff(T, B, n_boot=20000, ci=95):
  116. T_flat = np.asarray(T).ravel()
  117. B_flat = np.asarray(B).ravel()
  118. T_clean = T_flat[~np.isnan(T_flat)]
  119. B_clean = B_flat[~np.isnan(B_flat)]
  120. if T_clean.size < 2 or B_clean.size < 2 : # Need at least 2 points for meaningful bootstrap
  121. return np.full(n_boot, np.nan), np.nan, np.nan, np.nan
  122. boot_T_means = np.random.choice(T_clean, (n_boot, T_clean.size), replace=True).mean(axis=1)
  123. boot_B_means = np.random.choice(B_clean, (n_boot, B_clean.size), replace=True).mean(axis=1)
  124. boot_diff_means = boot_T_means - boot_B_means
  125. if np.all(np.isnan(boot_diff_means)):
  126. return boot_diff_means, np.nan, np.nan, np.nan
  127. lo, hi = np.nanpercentile(boot_diff_means, [(100-ci)/2, 100-(100-ci)/2])
  128. p_val_numerator = np.nansum(boot_diff_means <= 0) if np.nanmean(boot_diff_means) > 0 else np.nansum(boot_diff_means >= 0)
  129. p_val_denominator = np.sum(~np.isnan(boot_diff_means))
  130. if p_val_denominator == 0: p = np.nan
  131. else: p = 2 * (p_val_numerator / p_val_denominator); p = min(p, 1.0)
  132. return boot_diff_means, lo, hi, p
  133. def cohens_d(x, y):
  134. x_arr = np.asarray(x).ravel(); y_arr = np.asarray(y).ravel()
  135. x_arr = x_arr[~np.isnan(x_arr)]; y_arr = y_arr[~np.isnan(y_arr)]
  136. nx, ny = len(x_arr), len(y_arr)
  137. if nx < 2 or ny < 2: return np.nan
  138. vx, vy = x_arr.var(ddof=1), y_arr.var(ddof=1)
  139. if vx == 0 and vy == 0: return 0.0 if x_arr.mean() == y_arr.mean() else np.inf * np.sign(x_arr.mean() - y_arr.mean())
  140. pooled_sd_numerator = (nx-1)*vx + (ny-1)*vy
  141. pooled_sd_denominator = nx+ny-2
  142. if pooled_sd_denominator == 0 : return np.nan
  143. pooled_sd = np.sqrt(pooled_sd_numerator / pooled_sd_denominator)
  144. if pooled_sd == 0: return np.nan
  145. return (x_arr.mean() - y_arr.mean()) / pooled_sd
  146. # -----------------------------------------------------------------------------
  147. # 3–7) LOOP OVER SUBJECTS → BUILD DATAFRAMES
  148. # -----------------------------------------------------------------------------
  149. all_df_roi = []
  150. all_df_ic = []
  151. all_df_hemi_ic = []
  152. all_df_sum = []
  153. for subj, cfg in subjects.items():
  154. print(f"Processing subject: {subj}")
  155. brain_mask_path = BASE_DATA_DIR / cfg['SUBFOLDER'] / cfg['MASK_BRAIN']
  156. if not brain_mask_path.exists():
  157. print(f"Critical: Brain mask not found for {subj} at {brain_mask_path}. Skipping subject.")
  158. continue
  159. brain_mask = load_nifti(brain_mask_path) > 0
  160. nx_shape = brain_mask.shape[0] # Get shape for flipping ROIs
  161. # Load structural masks
  162. mask_caudate_r = load_nifti(BASE_DATA_DIR / cfg['SUBFOLDER'] / cfg['MASK_Caudate_Right']) > 0 if (BASE_DATA_DIR / cfg['SUBFOLDER'] / cfg['MASK_Caudate_Right']).exists() else np.zeros(brain_mask.shape, dtype=bool)
  163. mask_caudate_l = load_nifti(BASE_DATA_DIR / cfg['SUBFOLDER'] / cfg['MASK_Caudate_Left']) > 0 if (BASE_DATA_DIR / cfg['SUBFOLDER'] / cfg['MASK_Caudate_Left']).exists() else np.zeros(brain_mask.shape, dtype=bool)
  164. mask_putamen_r = load_nifti(BASE_DATA_DIR / cfg['SUBFOLDER'] / cfg['MASK_Putamen_Right']) > 0 if (BASE_DATA_DIR / cfg['SUBFOLDER'] / cfg['MASK_Putamen_Right']).exists() else np.zeros(brain_mask.shape, dtype=bool)
  165. mask_putamen_l = load_nifti(BASE_DATA_DIR / cfg['SUBFOLDER'] / cfg['MASK_Putamen_Left']) > 0 if (BASE_DATA_DIR / cfg['SUBFOLDER'] / cfg['MASK_Putamen_Left']).exists() else np.zeros(brain_mask.shape, dtype=bool)
  166. # Load and z-score all volumes
  167. zvols_all_sessions = {}
  168. raw_vols_all_sessions = {}
  169. for cond_type, file_list in cfg['VOLUMES'].items():
  170. if not isinstance(file_list, list):
  171. print(f"Warning: VOLUMES for {cond_type} in {subj} is not a list. Skipping this condition type.")
  172. continue
  173. for idx, fname in enumerate(file_list):
  174. session_name = f"{cond_type}{idx+1}" if len(file_list) > 1 or cond_type in TREATMENT_TYPES else cond_type
  175. if len(file_list) == 1 and cond_type == "Base": session_name = "Base"
  176. vol_path = BASE_DATA_DIR / cfg['SUBFOLDER'] / fname
  177. if not vol_path.exists():
  178. print(f"Warning: Volume file not found for {subj}, {session_name}: {vol_path}. Skipping this volume.")
  179. continue
  180. vol_data = load_nifti(vol_path)
  181. raw_vols_all_sessions[session_name] = vol_data
  182. vol_brain_voxels = vol_data[brain_mask & ~np.isnan(vol_data)]
  183. if vol_brain_voxels.size > 1:
  184. μ, σ = vol_brain_voxels.mean(), vol_brain_voxels.std()
  185. if σ != 0:
  186. zvols_all_sessions[session_name] = (vol_data - μ) / σ
  187. else:
  188. zvols_all_sessions[session_name] = np.zeros_like(vol_data) if np.all(vol_brain_voxels == μ) else np.full_like(vol_data, np.nan)
  189. else:
  190. zvols_all_sessions[session_name] = np.full_like(vol_data, np.nan)
  191. print(f"Warning: Not enough brain voxels for z-scoring {subj}, {session_name}.")
  192. base_zvol_keys = [k for k in zvols_all_sessions if k.startswith("Base")]
  193. if not base_zvol_keys:
  194. print(f"Critical: No valid Base volumes processed for subject {subj}. Skipping subject.")
  195. continue
  196. mean_base_z_volume = None
  197. valid_base_zvols_list = [zvols_all_sessions[k] for k in base_zvol_keys if k in zvols_all_sessions and not np.all(np.isnan(zvols_all_sessions[k]))]
  198. if not valid_base_zvols_list:
  199. print(f"Critical: All base volumes for {subj} are NaN or missing. Cannot compute mean base. Skipping subject.")
  200. continue
  201. if len(valid_base_zvols_list) > 1:
  202. mean_base_z_volume = np.nanmean(np.stack(valid_base_zvols_list, axis=-1), axis=-1)
  203. else:
  204. mean_base_z_volume = valid_base_zvols_list[0]
  205. if mean_base_z_volume is None or np.all(np.isnan(mean_base_z_volume)):
  206. print(f"Critical: Mean base z-volume is NaN for subject {subj}. Skipping subject.")
  207. continue
  208. mid = nx_shape // 2
  209. mask_hemi_r = brain_mask.copy(); mask_hemi_r[:mid,...] = False; mask_hemi_r &= brain_mask
  210. mask_hemi_l = brain_mask.copy(); mask_hemi_l[mid:,...] = False; mask_hemi_l &= brain_mask
  211. treatment_session_keys = [k for k in zvols_all_sessions if any(k.startswith(tt) for tt in TREATMENT_TYPES) and k in zvols_all_sessions]
  212. subj_roi_rows = []
  213. subj_ic_rows = []
  214. subj_hemi_ic_rows = []
  215. subj_sum_rows = []
  216. for treat_session_key in treatment_session_keys:
  217. treat_type_prefix = next((tt for tt in TREATMENT_TYPES if treat_session_key.startswith(tt)), None)
  218. if treat_type_prefix is None: continue
  219. zvol_treat = zvols_all_sessions[treat_session_key]
  220. if np.all(np.isnan(zvol_treat)):
  221. print(f"Warning: Treatment session {treat_session_key} for {subj} is all NaN. Skipping this session's analysis.")
  222. continue
  223. session_specific_rois = {}
  224. base_roi_data_ipsi = None
  225. mask_fname_to_load_str = None
  226. mask_key_generic = f"MASK_ROI_{treat_type_prefix}"
  227. if mask_key_generic in cfg and cfg[mask_key_generic] is not None:
  228. mask_value = cfg[mask_key_generic]
  229. # Always treat as list
  230. if not isinstance(mask_value, list):
  231. mask_value = [mask_value]
  232. # Determine session index
  233. try:
  234. session_idx = int(treat_session_key.replace(treat_type_prefix, '')) - 1
  235. except ValueError:
  236. session_idx = 0 # Default to first if cannot parse
  237. if 0 <= session_idx < len(mask_value):
  238. mask_fname_to_load_str = mask_value[session_idx]
  239. else:
  240. print(f"Warning: Index {session_idx} out of bounds for {subj}'s {mask_key_generic} (len {len(mask_value)}). Session: {treat_session_key}")
  241. else:
  242. print(f"Notice: No ROI mask key '{mask_key_generic}' defined for {subj}, session {treat_session_key}.")
  243. if mask_fname_to_load_str:
  244. mask_path_to_load = BASE_DATA_DIR / cfg['SUBFOLDER'] / mask_fname_to_load_str
  245. if mask_path_to_load.exists():
  246. loaded_mask = load_nifti(mask_path_to_load) > 0
  247. if np.any(loaded_mask):
  248. base_roi_data_ipsi = loaded_mask & brain_mask
  249. else:
  250. print(f"Warning: Loaded ROI mask is empty for {subj}, {treat_session_key}: {mask_path_to_load}")
  251. else:
  252. print(f"Warning: ROI mask file not found for {subj}, {treat_session_key}: {mask_path_to_load}")
  253. if base_roi_data_ipsi is not None and np.any(base_roi_data_ipsi):
  254. if treat_type_prefix == "LCN":
  255. session_specific_rois[f"{treat_type_prefix} L"] = base_roi_data_ipsi
  256. session_specific_rois[f"{treat_type_prefix} R"] = np.flip(base_roi_data_ipsi, axis=0) & brain_mask
  257. else:
  258. session_specific_rois[f"{treat_type_prefix} R"] = base_roi_data_ipsi
  259. session_specific_rois[f"{treat_type_prefix} L"] = np.flip(base_roi_data_ipsi, axis=0) & brain_mask
  260. else:
  261. if mask_fname_to_load_str :
  262. print(f"Notice: No valid ROI data for {subj}, session {treat_session_key}. ROI-specific analyses will be skipped for this session.")
  263. for hemi_label, hemi_mask in [('Left', mask_hemi_l), ('Right', mask_hemi_r)]:
  264. if not np.any(hemi_mask): continue
  265. dz_vals_hemi = (zvol_treat[hemi_mask] - mean_base_z_volume[hemi_mask]).ravel()
  266. dz_vals_hemi = dz_vals_hemi[~np.isnan(dz_vals_hemi)]
  267. if dz_vals_hemi.size == 0: continue
  268. is_ipsi_hemi = ((treat_type_prefix in ["RCN", "RV"] and hemi_label=='Right') or \
  269. (treat_type_prefix == "LCN" and hemi_label=='Left'))
  270. ipsicontra_label_hemi = 'Ipsilateral' if is_ipsi_hemi else 'Contralateral'
  271. for v_dz_hemi in dz_vals_hemi:
  272. subj_hemi_ic_rows.append({
  273. 'Subject': subj, 'Session': treat_session_key,
  274. 'FUS_Type': treat_type_prefix, 'Hemisphere': hemi_label,
  275. 'IpsiContra': ipsicontra_label_hemi, 'Δz': float(v_dz_hemi)
  276. })
  277. rois_for_session_analysis = {}
  278. for roi_label, roi_mask in session_specific_rois.items():
  279. rois_for_session_analysis[roi_label] = roi_mask
  280. roi_side = roi_label.split()[-1]
  281. caudate_mask_for_side = mask_caudate_r if roi_side == 'R' else mask_caudate_l
  282. putamen_mask_for_side = mask_putamen_r if roi_side == 'R' else mask_putamen_l
  283. rois_for_session_analysis[f"{roi_label} in Caudate"] = roi_mask & caudate_mask_for_side
  284. rois_for_session_analysis[f"{roi_label} in Putamen"] = roi_mask & putamen_mask_for_side
  285. rois_for_session_analysis[f"{roi_label} outside C&P"] = roi_mask & ~caudate_mask_for_side & ~putamen_mask_for_side
  286. for analysis_roi_label, analysis_roi_mask in rois_for_session_analysis.items():
  287. if not np.any(analysis_roi_mask): continue
  288. base_vals_in_roi = mean_base_z_volume[analysis_roi_mask].ravel()
  289. base_vals_in_roi = base_vals_in_roi[~np.isnan(base_vals_in_roi)]
  290. for v_base_roi in base_vals_in_roi:
  291. subj_roi_rows.append({
  292. 'Subject': subj, 'ROI': analysis_roi_label,
  293. 'Session': "MeanBase", 'FUS_Type': "Base", 'z': float(v_base_roi)
  294. })
  295. treat_vals_in_roi = zvol_treat[analysis_roi_mask].ravel()
  296. treat_vals_in_roi = treat_vals_in_roi[~np.isnan(treat_vals_in_roi)]
  297. for v_treat_roi in treat_vals_in_roi:
  298. subj_roi_rows.append({
  299. 'Subject': subj, 'ROI': analysis_roi_label,
  300. 'Session': treat_session_key, 'FUS_Type': treat_type_prefix, 'z': float(v_treat_roi)
  301. })
  302. dz_vals_roi = (zvol_treat[analysis_roi_mask] - mean_base_z_volume[analysis_roi_mask]).ravel()
  303. dz_vals_roi_clean = dz_vals_roi[~np.isnan(dz_vals_roi)]
  304. if dz_vals_roi_clean.size > 0:
  305. roi_side_char = analysis_roi_label.split()[1]
  306. is_ipsi_roi = ((treat_type_prefix in ["RCN", "RV"] and roi_side_char == 'R') or \
  307. (treat_type_prefix == "LCN" and roi_side_char == 'L'))
  308. ipsicontra_label_roi = 'Ipsilateral' if is_ipsi_roi else 'Contralateral'
  309. for v_dz_roi in dz_vals_roi_clean:
  310. subj_ic_rows.append({
  311. 'Subject': subj, 'Session': treat_session_key, 'FUS_Type': treat_type_prefix,
  312. 'ROI_Class': analysis_roi_label, 'Condition': ipsicontra_label_roi, 'Δz': float(v_dz_roi)
  313. })
  314. T_sum, B_sum = treat_vals_in_roi, base_vals_in_roi
  315. if T_sum.size >= 2 and B_sum.size >= 2:
  316. boot_sum, lo_sum, hi_sum, p_sum = bootstrap_diff(T_sum, B_sum)
  317. d_sum = cohens_d(T_sum, B_sum)
  318. subj_sum_rows.append({
  319. 'Subject': subj, 'Session': treat_session_key, 'FUS_Type': treat_type_prefix,
  320. 'ROI': analysis_roi_label, 'Comparison': f"{treat_session_key} vs MeanBase",
  321. 'Δz': T_sum.mean() - B_sum.mean(), 'CI lower': lo_sum, 'CI upper': hi_sum,
  322. 'p-value': p_sum, "Cohen's d": d_sum
  323. })
  324. else:
  325. subj_sum_rows.append({
  326. 'Subject': subj, 'Session': treat_session_key, 'FUS_Type': treat_type_prefix,
  327. 'ROI': analysis_roi_label, 'Comparison': f"{treat_session_key} vs MeanBase",
  328. 'Δz': np.nanmean(T_sum) - np.nanmean(B_sum) if T_sum.size > 0 and B_sum.size > 0 else np.nan,
  329. 'CI lower': np.nan, 'CI upper': np.nan, 'p-value': np.nan, "Cohen's d": np.nan
  330. })
  331. if subj_roi_rows: all_df_roi.append(pd.DataFrame(subj_roi_rows))
  332. if subj_ic_rows: all_df_ic.append(pd.DataFrame(subj_ic_rows))
  333. if subj_hemi_ic_rows: all_df_hemi_ic.append(pd.DataFrame(subj_hemi_ic_rows))
  334. if subj_sum_rows: all_df_sum.append(pd.DataFrame(subj_sum_rows).set_index(['Subject','Session','ROI','Comparison']))
  335. df_roi = pd.concat(all_df_roi, ignore_index=True) if all_df_roi else pd.DataFrame()
  336. if not df_roi.empty:
  337. # Ensure unique entries for MeanBase per ROI for each subject.
  338. mean_base_entries = df_roi[df_roi['Session'] == 'MeanBase']
  339. other_entries = df_roi[df_roi['Session'] != 'MeanBase']
  340. if not mean_base_entries.empty:
  341. mean_base_entries_dedup = mean_base_entries.drop_duplicates(subset=['Subject', 'ROI', 'Session', 'FUS_Type', 'z'])
  342. df_roi = pd.concat([mean_base_entries_dedup, other_entries], ignore_index=True)
  343. df_ic = pd.concat(all_df_ic, ignore_index=True) if all_df_ic else pd.DataFrame()
  344. df_hemi_ic = pd.concat(all_df_hemi_ic, ignore_index=True) if all_df_hemi_ic else pd.DataFrame()
  345. df_sum = pd.concat(all_df_sum) if all_df_sum else pd.DataFrame() # Per-session summary
  346. # -----------------------------------------------------------------------------
  347. # BUILD REGRESSION DESIGN DATAFRAME (SESSION-LEVEL ROI MEAN Δz)
  348. # -----------------------------------------------------------------------------
  349. if not df_sum.empty:
  350. df_sum_reset = df_sum.reset_index() if isinstance(df_sum, pd.DataFrame) else pd.DataFrame()
  351. df_reg = df_sum_reset.dropna(subset=['Δz', 'Subject', 'Session', 'FUS_Type', 'ROI'])
  352. df_reg = df_reg[df_reg['FUS_Type'].isin(TREATMENT_TYPES)].copy()
  353. df_reg['roi_side'] = df_reg['ROI'].str.split().str[1].map({'R': 1, 'L': -1})
  354. df_reg['fus_side'] = df_reg['FUS_Type'].map({'RCN': 1, 'LCN': -1})
  355. df_reg['Contralateral_Effect'] = df_reg['roi_side'] * df_reg['fus_side'] * (-1)
  356. df_reg['subject_cat'] = pd.Categorical(df_reg['Subject']).codes
  357. # -----------------------------------------------------------------------------
  358. # 8) COMBINED PLOT: SESSION-LEVEL ROI & HEMISPHERIC MEAN Δz BY FUS TYPE
  359. # -----------------------------------------------------------------------------
  360. if not df_sum.empty and not df_hemi_ic.empty:
  361. # ROI effects (already session-level in df_reg)
  362. plot_roi_full = df_reg[~df_reg['ROI'].str.contains(" in | outside ")]
  363. plot_roi_full['Laterality'] = (plot_roi_full['Contralateral_Effect']*(-1)).map({1: 'Ipsilateral', -1: 'Contralateral'})
  364. plot_roi_full['PlotGroup'] = 'ROI ' + plot_roi_full['FUS_Type']
  365. # Hemispheric effects: aggregate mean Δz per subject/session/hemisphere/ipsi-contra
  366. hemi_means = (
  367. df_hemi_ic
  368. .groupby(['Subject', 'Session', 'FUS_Type', 'Hemisphere', 'IpsiContra'])
  369. .agg({'Δz': 'mean'})
  370. .reset_index()
  371. )
  372. # Only keep treatment sessions
  373. hemi_means = hemi_means[hemi_means['FUS_Type'].isin(TREATMENT_TYPES)].copy()
  374. hemi_means['PlotGroup'] = 'Hemisphere ' + hemi_means['FUS_Type']
  375. hemi_means = hemi_means.rename(columns={'IpsiContra': 'Laterality'})
  376. # Match columns for concatenation
  377. plot_hemi = hemi_means[['Subject', 'Session', 'FUS_Type', 'Δz', 'Laterality', 'PlotGroup']].copy()
  378. plot_roi_full = plot_roi_full[['Subject', 'Session', 'FUS_Type', 'Δz', 'Laterality', 'PlotGroup']].copy()
  379. # Combine
  380. plot_df = pd.concat([plot_hemi, plot_roi_full], ignore_index=True)
  381. # Set plotting order
  382. plot_group_order = [
  383. "Hemisphere RCN", # leftmost
  384. "ROI RCN",
  385. "ROI LCN",
  386. "Hemisphere LCN" # rightmost
  387. ]
  388. plot_df = plot_df[plot_df['PlotGroup'].isin(plot_group_order)]
  389. plot_df['PlotGroup'] = pd.Categorical(plot_df['PlotGroup'], categories=plot_group_order, ordered=True)
  390. plot_df['Laterality'] = pd.Categorical(plot_df['Laterality'], categories=['Ipsilateral', 'Contralateral'], ordered=True)
  391. # Calculate n for each group/lat for legend
  392. n_dict = {}
  393. for group in plot_group_order:
  394. for lat in ['Ipsilateral', 'Contralateral']:
  395. mask = (plot_df['PlotGroup'] == group) & (plot_df['Laterality'] == lat)
  396. n = mask.sum()
  397. n_dict[(group, lat)] = n
  398. # Build legend labels with n in parentheses
  399. legend_labels = []
  400. for group in plot_group_order:
  401. n_ipsi = n_dict[(group, 'Ipsilateral')]
  402. n_contra = n_dict[(group, 'Contralateral')]
  403. legend_labels.append(f"{group} (n={n_ipsi})")
  404. fig, ax = plt.subplots(figsize=(6, 7))
  405. dodge_offsets = {
  406. "Hemisphere RCN": -0.375,
  407. "ROI RCN": -0.125,
  408. "ROI LCN": 0.125,
  409. "Hemisphere LCN": 0.375
  410. }
  411. # Define marker styles for each group
  412. marker_map = {
  413. "Hemisphere RCN": "X",
  414. "ROI RCN": "o",
  415. "ROI LCN": "s",
  416. "Hemisphere LCN": "^"
  417. }
  418. # Separate out groups with n=2
  419. plot_df_for_pointplot = plot_df.copy()
  420. for group in ["ROI LCN", "Hemisphere LCN"]:
  421. for lat in ['Ipsilateral', 'Contralateral']:
  422. mask = (plot_df['PlotGroup'] == group) & (plot_df['Laterality'] == lat)
  423. sub_df = plot_df[mask]
  424. if len(sub_df) == 2:
  425. x_val = 0 if lat == 'Ipsilateral' else 1
  426. x_val_dodged = x_val + dodge_offsets[group]
  427. # Plot each point individually at the correct x
  428. for y in sub_df['Δz']:
  429. ax.scatter(x_val_dodged, y, marker=marker_map[group], color='black', s=80, zorder=3)
  430. plot_df_for_pointplot = plot_df_for_pointplot[~mask]
  431. # Plot the rest with error bars
  432. if not plot_df_for_pointplot.empty:
  433. sns.pointplot(
  434. data=plot_df_for_pointplot,
  435. x='Laterality',
  436. y='Δz',
  437. hue='PlotGroup',
  438. hue_order=plot_group_order,
  439. markers=['X', 'o', 's', '^'],
  440. palette=['black', 'black', 'black', 'black'],
  441. linestyles=['none'] * 4,
  442. dodge=0.5,
  443. errorbar=('ci', 95),
  444. capsize=0.05,
  445. err_kws={'linewidth': 1.5, 'alpha': 0.7},
  446. ax=ax,
  447. markersize=8
  448. )
  449. ax.axhline(0, ls='--', color='darkgray', zorder=0)
  450. ax.set_title("", fontsize=24)
  451. ax.set_ylabel(r"$\overline{\Delta z}_{\text{OEF}}$ (Treatment - Control)", fontsize=24)
  452. ax.set_xlabel("", fontsize=24)
  453. ax.set_ylim(-0.4, 0.3)
  454. ax.set_yticks([-0.4, 0, 0.3])
  455. ax.set_yticklabels([str(x) for x in [-0.4, 0, 0.3]], fontsize=22)
  456. #ax.tick_params(axis='both', which='major', labelsize=22)
  457. ax.set_xticklabels(["Ipsilateral", "Contralateral"], fontsize=24)
  458. # Set legend with n in parentheses
  459. handles, _ = ax.get_legend_handles_labels()
  460. #ax.legend(handles, legend_labels, title="", loc='upper left', bbox_to_anchor=(1.02, 1), ncol=1, fontsize=10)
  461. ax.legend(handles, legend_labels, title="", loc='lower right', ncol=1, fontsize=20, framealpha=1, edgecolor='black')
  462. plt.tight_layout()
  463. plt.savefig(output_dir / "SESSION_LEVEL_ROI_HEMISPHERIC_MEAN_Delta_z.svg", format='svg')
  464. plt.show()
  465. plt.close(fig)
  466. else:
  467. print("Skipping Combined Plot: No session-level ROI or hemispheric mean Δz data available.")
  468. # -----------------------------------------------------------------------------
  469. # 8a) PLOTS FOR CAUDATE & PUTAMEN SUB-REGIONS
  470. # -----------------------------------------------------------------------------
  471. def plot_subregion(df, region_name):
  472. df_plot = df[df['ROI'].str.contains(f" in {region_name}")].copy()
  473. if not df_plot.empty:
  474. df_plot['Laterality'] = (df_plot['Contralateral_Effect']*(-1)).map({1: 'Ipsilateral', -1: 'Contralateral'})
  475. # Calculate n for each FUS_Type/Laterality for legend
  476. n_dict = {}
  477. for fus_type in ['RCN', 'LCN']:
  478. for lat in ['Ipsilateral', 'Contralateral']:
  479. mask = (df_plot['FUS_Type'] == fus_type) & (df_plot['Laterality'] == lat)
  480. n = mask.sum()
  481. n_dict[(fus_type, lat)] = n
  482. legend_labels = []
  483. for fus_type in ['RCN', 'LCN']:
  484. n_ipsi = n_dict[(fus_type, 'Ipsilateral')]
  485. n_contra = n_dict[(fus_type, 'Contralateral')]
  486. legend_labels.append(f"{fus_type} (n={n_ipsi})")
  487. fig, ax = plt.subplots(figsize=(5, 7))
  488. # --- Custom plotting logic for N=2 ---
  489. marker_map = {'RCN': 'o', 'LCN': 's'}
  490. dodge_offsets = {'RCN': -0.1, 'LCN': 0.1} # Adjust if you change dodge in pointplot
  491. plot_df_for_pointplot = df_plot.copy()
  492. for fus_type in ['RCN', 'LCN']:
  493. for lat in ['Ipsilateral', 'Contralateral']:
  494. mask = (df_plot['FUS_Type'] == fus_type) & (df_plot['Laterality'] == lat)
  495. sub_df = df_plot[mask]
  496. if len(sub_df) == 2:
  497. x_val = 0 if lat == 'Ipsilateral' else 1
  498. x_val_dodged = x_val + dodge_offsets[fus_type]
  499. for y in sub_df['Δz']:
  500. ax.scatter(x_val_dodged, y, marker=marker_map[fus_type], color='black', s=80, zorder=3)
  501. plot_df_for_pointplot = plot_df_for_pointplot[~mask]
  502. # Plot the rest with error bars (N>2) using seaborn pointplot
  503. if not plot_df_for_pointplot.empty:
  504. sns.pointplot(
  505. data=plot_df_for_pointplot,
  506. x='Laterality',
  507. y='Δz',
  508. hue='FUS_Type',
  509. hue_order=['RCN', 'LCN'],
  510. markers=['o', 's'],
  511. palette=['black', 'black'],
  512. linestyles=['none', 'none'],
  513. dodge=0.2,
  514. errorbar=('ci', 95),
  515. err_kws={'linewidth': 1.5, 'alpha': 0.7},
  516. capsize=0.05,
  517. ax=ax,
  518. markersize=8,
  519. )
  520. ax.axhline(0, ls='--', color='darkgray', zorder=0)
  521. ax.set_title(rf"{region_name}", fontsize=24)
  522. ax.set_ylabel(r"$\overline{\Delta z}_{\text{OEF}}$ (Treatment - Control)", fontsize=24)
  523. ax.set_xlabel("", fontsize=24)
  524. ax.set_ylim(-0.4, 0.3)
  525. ax.set_yticks([-0.4, 0, 0.3])
  526. ax.set_yticklabels([str(x) for x in [-0.4, 0, 0.3]], fontsize=22)
  527. ax.set_xticklabels(["Ipsilateral", "Contralateral"], fontsize=24)
  528. handles, _ = ax.get_legend_handles_labels()
  529. ax.legend(handles, legend_labels, title="", loc='lower right', ncol=1, fontsize=20, shadow=False, frameon=False, framealpha=1, edgecolor='black', borderpad=0.4, borderaxespad=0.4)
  530. plt.tight_layout()
  531. if region_name == "Caudate":
  532. plt.savefig(output_dir / "SESSION_LEVEL_Caudate_ROI_HEMISPHERIC_MEAN_Delta_z.svg", format='svg')
  533. elif region_name == "Putamen":
  534. plt.savefig(output_dir / "SESSION_LEVEL_Putamen_ROI_HEMISPHERIC_MEAN_Delta_z.svg", format='svg')
  535. plt.show()
  536. plt.close(fig)
  537. else:
  538. print(f"No data for {region_name} sub-region plot.")
  539. if not df_reg.empty:
  540. plot_subregion(df_reg, "Caudate")
  541. plot_subregion(df_reg, "Putamen")
  542. # -----------------------------------------------------------------------------
  543. # 10) OVERALL SUMMARY TABLE (Δz, CI, p‐value, Cohen’s d) - Aggregated per ROI_Class
  544. # -----------------------------------------------------------------------------
  545. overall_summaries = []
  546. if not df_roi.empty:
  547. df_roi_overall = df_roi.copy()
  548. df_roi_overall['ROI_Class'] = df_roi_overall['ROI']
  549. df_roi_overall['Condition_Type'] = df_roi_overall['FUS_Type']
  550. unique_roi_classes = sorted(df_roi_overall['ROI_Class'].unique())
  551. for roi_cls in unique_roi_classes:
  552. primary_fus_type = roi_cls.split()[0]
  553. if primary_fus_type not in TREATMENT_TYPES : continue
  554. T_all = df_roi_overall.query("ROI_Class == @roi_cls and Condition_Type == @primary_fus_type").z.values
  555. B_all = df_roi_overall.query("ROI_Class == @roi_cls and Condition_Type == 'Base'").z.values
  556. if T_all.size >= 2 and B_all.size >= 2:
  557. # Check for NaNs after selection, as z-scores could be NaN if original data was problematic
  558. T_all_clean = T_all[~np.isnan(T_all)]
  559. B_all_clean = B_all[~np.isnan(B_all)]
  560. if T_all_clean.size >=2 and B_all_clean.size >=2:
  561. boot_ov, lo_ov, hi_ov, p_ov = bootstrap_diff(T_all_clean, B_all_clean)
  562. d_ov = cohens_d(T_all_clean, B_all_clean)
  563. mean_diff_ov = T_all_clean.mean() - B_all_clean.mean()
  564. else:
  565. mean_diff_ov, lo_ov, hi_ov, p_ov, d_ov = np.nan, np.nan, np.nan, np.nan, np.nan
  566. else:
  567. mean_diff_ov, lo_ov, hi_ov, p_ov, d_ov = np.nan, np.nan, np.nan, np.nan, np.nan
  568. overall_summaries.append({
  569. 'ROI_Class': roi_cls,
  570. 'Comparison': f"{primary_fus_type} (pooled) vs Base (pooled)",
  571. 'Δz': mean_diff_ov,
  572. 'CI lower': lo_ov, 'CI upper': hi_ov,
  573. 'p-value': p_ov, "Cohen's d": d_ov
  574. })
  575. # -----------------------------------------------------------------------------
  576. # 12) BAYESIAN REGRESSION (SESSION-LEVEL ROI MEAN Δz, HIERARCHICAL)
  577. # -----------------------------------------------------------------------------
  578. idata = None
  579. df_reg_full_roi = df_reg[~df_reg['ROI'].str.contains(" in | outside ")].copy()
  580. if not df_reg_full_roi.empty and 'Δz' in df_reg_full_roi and not df_reg_full_roi['Δz'].isnull().all() and len(df_reg_full_roi) > 1:
  581. coords = {
  582. "subject": df_reg_full_roi['Subject'].unique(),
  583. "obs_id": np.arange(len(df_reg_full_roi))
  584. }
  585. subject_map = {name: i for i, name in enumerate(df_reg_full_roi['Subject'].unique())}
  586. subject_indices_for_model = df_reg_full_roi['Subject'].map(subject_map).values
  587. with pm.Model(coords=coords) as model_hierarchical:
  588. # Data
  589. delta_z_obs = pm.Data("delta_z_obs", df_reg_full_roi["Δz"].values, dims="obs_id")
  590. fus_side_data = pm.Data("fus_side_data", df_reg_full_roi["fus_side"].values.astype(float), dims="obs_id")
  591. roi_side_data = pm.Data("roi_side_data", df_reg_full_roi["roi_side"].values.astype(float), dims="obs_id")
  592. contralateral_effect_data = pm.Data("contralateral_effect_data", df_reg_full_roi["Contralateral_Effect"].values.astype(float), dims="obs_id")
  593. subject_idx_data = pm.Data("subject_idx_data", subject_indices_for_model, dims="obs_id")
  594. # Hyperpriors for random intercepts
  595. mu_alpha_subj = pm.Normal('mu_alpha_subj', mu=0, sigma=2)
  596. sigma_alpha_subj = pm.HalfNormal('sigma_alpha_subj', sigma=2)
  597. alpha_subj_offset = pm.Normal('alpha_subj_offset', mu=0, sigma=1, dims="subject")
  598. alpha_subj = pm.Deterministic('alpha_subj', mu_alpha_subj + alpha_subj_offset * sigma_alpha_subj, dims="subject")
  599. # Fixed effects
  600. β_fus = pm.Normal('FUS_Target_Side', mu=0, sigma=2)
  601. β_read = pm.Normal('Read_Side', mu=0, sigma=2)
  602. β_int = pm.Normal('Contralateral_Effect', mu=0, sigma=2)
  603. σ_resid = pm.HalfNormal('sigma_resid', sigma=2)
  604. ν_resid = pm.Exponential('nu_resid', 1/30.0)
  605. mu_eq = alpha_subj[subject_idx_data] + \
  606. β_fus * fus_side_data + \
  607. β_read * roi_side_data + \
  608. β_int * contralateral_effect_data
  609. y_obs = pm.StudentT('y', nu=ν_resid, mu=mu_eq, sigma=σ_resid,
  610. observed=delta_z_obs, dims="obs_id")
  611. print("Sampling Bayesian model using CPU (default NUTS sampler)...")
  612. try:
  613. idata = pm.sample(5000, tune=2000, chains=16,
  614. cores=16,
  615. return_inferencedata=True,
  616. idata_kwargs={"log_likelihood":True},
  617. target_accept=0.95,
  618. random_seed=80)
  619. ppc = pm.sample_posterior_predictive(idata, var_names=['y', 'alpha_subj'], random_seed=80, return_inferencedata=True)
  620. idata.extend(ppc)
  621. print("Sampling complete.")
  622. except Exception as e:
  623. print(f"Error during PyMC sampling: {e}")
  624. idata = None
  625. else:
  626. print("Skipping Bayesian Regression: No session-level ROI mean Δz data available.")
  627. # -----------------------------------------------------------------------------
  628. # 12a) BAYESIAN REGRESSION (SUB-REGIONS)
  629. # -----------------------------------------------------------------------------
  630. idata_v2 = None
  631. df_reg_v2 = df_reg[df_reg['ROI'].str.contains(" in | outside ")].copy()
  632. if not df_reg_v2.empty and 'Δz' in df_reg_v2 and not df_reg_v2['Δz'].isnull().all() and len(df_reg_v2) > 1:
  633. def get_subregion_type(roi_name):
  634. if "in Caudate" in roi_name: return "Caudate"
  635. if "in Putamen" in roi_name: return "Putamen"
  636. if "outside C&P" in roi_name: return "Outside"
  637. return "Other"
  638. df_reg_v2['subregion'] = df_reg_v2['ROI'].apply(get_subregion_type)
  639. df_reg_v2['Contralateral_Effect_Caudate'] = df_reg_v2.apply(lambda row: row['Contralateral_Effect'] if row['subregion'] == 'Caudate' else 0, axis=1)
  640. df_reg_v2['Contralateral_Effect_Putamen'] = df_reg_v2.apply(lambda row: row['Contralateral_Effect'] if row['subregion'] == 'Putamen' else 0, axis=1)
  641. df_reg_v2['Contralateral_Effect_Outside'] = df_reg_v2.apply(lambda row: row['Contralateral_Effect'] if row['subregion'] == 'Outside' else 0, axis=1)
  642. coords_v2 = {
  643. "subject": df_reg_v2['Subject'].unique(),
  644. "obs_id": np.arange(len(df_reg_v2))
  645. }
  646. subject_map_v2 = {name: i for i, name in enumerate(df_reg_v2['Subject'].unique())}
  647. subject_indices_for_model_v2 = df_reg_v2['Subject'].map(subject_map_v2).values
  648. with pm.Model(coords=coords_v2) as model_v2:
  649. delta_z_obs = pm.Data("delta_z_obs", df_reg_v2["Δz"].values, dims="obs_id")
  650. fus_side_data = pm.Data("fus_side_data", df_reg_v2["fus_side"].values.astype(float), dims="obs_id")
  651. roi_side_data = pm.Data("roi_side_data", df_reg_v2["roi_side"].values.astype(float), dims="obs_id")
  652. subject_idx_data = pm.Data("subject_idx_data", subject_indices_for_model_v2, dims="obs_id")
  653. int_caudate_data = pm.Data("int_caudate_data", df_reg_v2["Contralateral_Effect_Caudate"].values.astype(float), dims="obs_id")
  654. int_putamen_data = pm.Data("int_putamen_data", df_reg_v2["Contralateral_Effect_Putamen"].values.astype(float), dims="obs_id")
  655. int_outside_data = pm.Data("int_outside_data", df_reg_v2["Contralateral_Effect_Outside"].values.astype(float), dims="obs_id")
  656. mu_alpha_subj = pm.Normal('mu_alpha_subj', mu=0, sigma=2)
  657. sigma_alpha_subj = pm.HalfNormal('sigma_alpha_subj', sigma=2)
  658. alpha_subj_offset = pm.Normal('alpha_subj_offset', mu=0, sigma=1, dims="subject")
  659. alpha_subj = pm.Deterministic('alpha_subj', mu_alpha_subj + alpha_subj_offset * sigma_alpha_subj, dims="subject")
  660. β_fus = pm.Normal('LIFU_Target_Side', mu=0, sigma=2)
  661. β_read = pm.Normal('ROI_Side', mu=0, sigma=2)
  662. β_int_caudate = pm.Normal('Contralateral_Effect_in_Caudate', mu=0, sigma=2)
  663. β_int_putamen = pm.Normal('Contralateral_Effect_in_Putamen', mu=0, sigma=2)
  664. β_int_outside = pm.Normal('Contralateral_Effect_outside_CP', mu=0, sigma=2)
  665. σ_resid = pm.HalfNormal('sigma_resid', sigma=2)
  666. ν_resid = pm.Exponential('nu_resid', 1/30.0)
  667. mu_eq = (alpha_subj[subject_idx_data] +
  668. β_fus * fus_side_data +
  669. β_read * roi_side_data +
  670. β_int_caudate * int_caudate_data +
  671. β_int_putamen * int_putamen_data +
  672. β_int_outside * int_outside_data)
  673. y_obs = pm.StudentT('y', nu=ν_resid, mu=mu_eq, sigma=σ_resid, observed=delta_z_obs, dims="obs_id")
  674. print("Sampling Bayesian sub-region model...")
  675. try:
  676. idata_v2 = pm.sample(5000, tune=2000, chains=16, cores=16, return_inferencedata=True,
  677. idata_kwargs={"log_likelihood": True}, target_accept=0.95, random_seed=81)
  678. ppc_v2 = pm.sample_posterior_predictive(idata_v2, var_names=['y', 'alpha_subj'], random_seed=81, return_inferencedata=True)
  679. idata_v2.extend(ppc_v2)
  680. print("Sub-region model sampling complete.")
  681. except Exception as e:
  682. print(f"Error during PyMC sampling for sub-region model: {e}")
  683. idata_v2 = None
  684. else:
  685. print("Skipping Sub-region Bayesian Regression: No data available.")
  686. # -----------------------------------------------------------------------------
  687. # 13) TRACE & POSTERIOR PAIR PLOTS
  688. # -----------------------------------------------------------------------------
  689. if idata:
  690. var_names_trace = ['mu_alpha_subj', 'sigma_alpha_subj', 'FUS_Target_Side', 'Read_Side', 'Contralateral_Effect', 'sigma_resid', 'nu_resid']
  691. # Check if all variables are in idata before plotting
  692. valid_vars_for_trace = [var for var in var_names_trace if var in idata.posterior]
  693. if valid_vars_for_trace:
  694. # Reverted to original trace plot call
  695. az.plot_trace(idata, var_names=valid_vars_for_trace, compact=True, figsize=(12, len(valid_vars_for_trace)*2))
  696. plt.tight_layout(); plt.show()
  697. else:
  698. print("No valid variables for trace plot found in idata.posterior.")
  699. if 'alpha_subj' in idata.posterior:
  700. az.plot_forest(idata, var_names=['alpha_subj'], combined=True, hdi_prob=0.95, figsize=(8, max(4, len(idata.posterior.subject)*0.3)))
  701. plt.title("Subject-Specific Intercepts (alpha_subj)")
  702. plt.show()
  703. else:
  704. print("'alpha_subj' not found in idata.posterior for forest plot.")
  705. else:
  706. print("Skipping Trace Plots: No idata from Bayesian model.")
  707. #-----------------------------------------------------------------------------
  708. # 15) POSTERIOR KDES of fixed effect coefficients
  709. # -----------------------------------------------------------------------------
  710. if idata:
  711. fixed_effects_kde = ['FUS_Target_Side','Read_Side','Contralateral_Effect']
  712. valid_vars_for_kde_pair = [var for var in fixed_effects_kde if var in idata.posterior]
  713. if valid_vars_for_kde_pair:
  714. df_post_fixed = idata.posterior[valid_vars_for_kde_pair].to_dataframe().reset_index().melt(
  715. id_vars=['chain','draw'], value_vars=valid_vars_for_kde_pair,
  716. var_name='parameter', value_name='value'
  717. )
  718. if not df_post_fixed.empty:
  719. fig, ax = plt.subplots(figsize=(8,7))
  720. # Style maps: different shades of grey/black, line style, and width
  721. style_map = {
  722. 'FUS_Target_Side': {'hatch': None, 'linestyle': ':', 'linewidth': 1, 'label': 'LIFU Target Side', 'text': None},
  723. 'Read_Side': {'hatch': None, 'linestyle': '--', 'linewidth': 1, 'label': 'ROI Side', 'text': None},
  724. 'Contralateral_Effect': {'hatch': '////', 'linestyle': '-', 'linewidth': 2.5, 'label': 'Contralateral Effect', 'text': 'Contralateral'}
  725. }
  726. for param in valid_vars_for_kde_pair:
  727. data = df_post_fixed.query("parameter==@param")['value']
  728. kde = sns.kdeplot(
  729. data=data,
  730. ax=ax,
  731. color='black',
  732. linestyle=style_map[param]['linestyle'],
  733. linewidth=style_map[param]['linewidth'],
  734. fill=False,
  735. label=style_map[param]['label'],
  736. )
  737. if style_map[param]['text']:
  738. x, y = kde.get_lines()[-1].get_data()
  739. # For Caudate, add a light black face shade
  740. if param == 'Contralateral_Effect':
  741. ax.fill_between(x, 0, y, facecolor='black', alpha=0.2, edgecolor='black', hatch=style_map[param]['hatch'])
  742. else:
  743. ax.fill_between(x, 0, y, facecolor='none', edgecolor='black', hatch=style_map[param]['hatch'], alpha=0.2)
  744. ax.axvline(0, ls='--', color='black', lw=1)
  745. ax.set_xlim(-0.1, 0.3)
  746. ax.set_ylim(0, 25)
  747. sns.despine(ax=ax)
  748. ax.set_xlabel('Coefficient value', fontsize=24)
  749. ax.set_ylabel('Density', fontsize=24)
  750. xticks = ax.get_xticks()
  751. ax.set_xticks(xticks[::2])
  752. ax.set_xticklabels([f"{x:.1f}" for x in xticks[::2]], fontsize=22)
  753. ax.set_yticklabels([f"{y:g}" for y in ax.get_yticks()], fontsize=22)
  754. ax.set_title('')
  755. ax.legend(title='', fontsize=22, shadow=False, frameon=True, framealpha=1, edgecolor='black', borderpad=0.2, borderaxespad=0.2)
  756. plt.tight_layout()
  757. plt.savefig(output_dir / "Regression.svg", format='svg')
  758. plt.show()
  759. plt.close(fig)
  760. else:
  761. print("No data for fixed effects KDE plot after melting.")
  762. else:
  763. print("No valid fixed effect variables for KDE plots found in idata.posterior.")
  764. else:
  765. print("Skipping Posterior KDEs: No idata from Bayesian model.")
  766. # -----------------------------------------------------------------------------
  767. # 15a) POSTERIOR KDES of fixed effect coefficients (SUB-REGIONS)
  768. # -----------------------------------------------------------------------------
  769. if idata_v2:
  770. fixed_effects_kde_v2 = ['LIFU_Target_Side', 'ROI_Side', 'Contralateral_Effect_in_Caudate', 'Contralateral_Effect_in_Putamen', 'Contralateral_Effect_outside_CP']
  771. valid_vars_for_kde_v2 = [var for var in fixed_effects_kde_v2 if var in idata_v2.posterior]
  772. if valid_vars_for_kde_v2:
  773. df_post_fixed_v2 = idata_v2.posterior[valid_vars_for_kde_v2].to_dataframe().reset_index().melt(
  774. id_vars=['chain','draw'], value_vars=valid_vars_for_kde_v2,
  775. var_name='parameter', value_name='value'
  776. )
  777. if not df_post_fixed_v2.empty:
  778. # Improved KDE plot for fixed effect coefficients (Sub-region Model) with distinguishable groups (no color)
  779. # Define hatching and line style for each group
  780. group_hatch_map = {
  781. 'Contralateral_Effect_in_Caudate': {'hatch': '////', 'linestyle': ':', 'linewidth': 2.5, 'label': 'Contralateral Effect in Caudate', 'text': 'Caudate'},
  782. 'Contralateral_Effect_in_Putamen': {'hatch': '\\\\\\', 'linestyle': '-', 'linewidth': 2.5, 'label': 'Contralateral Effect in Putamen', 'text': 'Putamen'},
  783. 'Contralateral_Effect_outside_CP': {'hatch': None, 'linestyle': '-.', 'linewidth': 2.5, 'label': 'Contralateral Effect Outside Striatum', 'text': 'Outside'},
  784. 'LIFU_Target_Side': {'hatch': None, 'linestyle': ':', 'linewidth': 1, 'label': 'LIFU Target Side', 'text': None},
  785. 'ROI_Side': {'hatch': None, 'linestyle': '--', 'linewidth': 1, 'label': 'ROI Side', 'text': None},
  786. }
  787. fig, ax = plt.subplots(figsize=(8, 7))
  788. peak_positions = {}
  789. for param in valid_vars_for_kde_v2:
  790. if param in group_hatch_map:
  791. data = df_post_fixed_v2.query("parameter==@param")['value']
  792. # Plot KDE line
  793. kde = sns.kdeplot(
  794. data=data,
  795. ax=ax,
  796. color='black',
  797. linestyle=group_hatch_map[param]['linestyle'],
  798. linewidth=group_hatch_map[param]['linewidth'],
  799. fill=False,
  800. label=group_hatch_map[param]['label'],
  801. )
  802. # Overlay hatched fill for the three main groups
  803. if group_hatch_map[param]['text']:
  804. x, y = kde.get_lines()[-1].get_data()
  805. # For Caudate, add a light black face shade
  806. if param == 'Contralateral_Effect_in_Putamen':
  807. ax.fill_between(x, 0, y, facecolor='black', alpha=0.2, edgecolor='black', hatch=group_hatch_map[param]['hatch'])
  808. else:
  809. ax.fill_between(x, 0, y, facecolor='none', edgecolor='black', hatch=group_hatch_map[param]['hatch'], alpha=0.2)
  810. # Store peak position for annotation
  811. if 'text' in group_hatch_map[param]:
  812. peak_idx = np.argmax(y)
  813. peak_x = x[peak_idx]
  814. peak_y = y[peak_idx]
  815. peak_positions[param] = (peak_x, peak_y)
  816. # Add text annotations for Caudate, Putamen, Outside
  817. for param, (peak_x, peak_y) in peak_positions.items():
  818. text = group_hatch_map[param]['text']
  819. # Offset 'Outside' label to the right
  820. if text == 'Outside':
  821. peak_x = peak_x + 0.015
  822. peak_y = peak_y - 0.3
  823. if text == 'Putamen':
  824. peak_x = peak_x + 0.01
  825. peak_y = peak_y - 0.2
  826. ax.text(peak_x + 0.01, peak_y + 0.02 * ax.get_ylim()[1], text, ha='center', va='bottom', fontsize=18, fontweight='bold', color='black', bbox=dict(facecolor='white', edgecolor='none', alpha=0.7, pad=1.5))
  827. ax.axvline(0, ls='--', color='black', lw=1)
  828. ax.set_xlim(-0.1, 0.3)
  829. ax.set_ylim(0, 25)
  830. sns.despine(ax=ax)
  831. ax.set_xlabel('Coefficient value', fontsize=24)
  832. ax.set_ylabel('Density', fontsize=24)
  833. xticks = ax.get_xticks()
  834. # Use 0.1 precision and skip every other tick
  835. ax.set_xticks(xticks[::2])
  836. ax.set_xticklabels([f"{x:.1f}" for x in xticks[::2]], fontsize=22)
  837. ax.set_yticklabels([f"{y:g}" for y in ax.get_yticks()], fontsize=22)
  838. ax.set_title('')
  839. ax.legend(title='', fontsize=17.5, shadow=False, frameon=True, framealpha=1, edgecolor='black', borderpad=0.2, borderaxespad=0.2)
  840. plt.tight_layout()
  841. plt.savefig(output_dir / "Regression_subregion.svg", format='svg')
  842. plt.show()
  843. plt.close(fig)
  844. else:
  845. print("No data for fixed effects KDE plot (sub-region model) after melting.")
  846. else:
  847. print("No valid fixed effect variables for KDE plots found in idata_v2.posterior.")
  848. else:
  849. print("Skipping Posterior KDEs for Sub-region Model: No idata_v2 from Bayesian model.")

3-Combined_Monkeys_OEF_Bayesian_template.ipynb, under apgl-v3 · at the source

Overview

Authors: Soroosh Sanatkhani1, Dong Liu1, Fabian Munoz1, Jack Grinband2,3, Elisa E Konofagou3,4, Vincent P Ferrera1,2,5
  1. Zuckerman Mind Brain Behavior Institute, Columbia University, New York, NY, USA
  2. Department of Psychiatry, Columbia University, New York, NY, USA
  3. Department of Radiology, Columbia University, New York, NY, USA
  4. Department of Biomedical Engineering, Columbia University, New York, NY, USA
  5. Department of Neuroscience, Columbia University, New York, NY, USA
Institutions: Mortimer B. Zuckerman Mind Brain Behavior Institute (United States); Columbia University (United States)
Journal: Science advances, volume 12, issue 29, article eaed4944
Dates: received 29 October 2025; accepted 2 June 2026; published online 17 July 2026; in print July 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1126/sciadv.aed4944 · PMID 42467769 · PMCID PMC13378551 · OpenAlex W7169524987
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: structural MRI / diffusion (modality), other (modality), non-human primate (organism), clinical / translational (subfield)
Methods: Statistics, fMRI & imaging
MeSH: Blood-Brain Barrier*, Brain*, Ultrasonic Waves*, Animals, Macaca mulatta, Magnetic Resonance Imaging, Male, Oxygen, Ultrasonography (* major topic)
Topic: Ultrasound and Hyperthermia Applications (Biomedical Engineering, Engineering), according to OpenAlex
Funding: NIMH NIH HHS (R01 MH133020)
Citations: not cited yet (Europe PMC); 66 references in the paper

Abstract

Low-intensity focused ultrasound (LIFU) is a promising technique for opening the blood-brain barrier (BBB) for drug delivery, but its physiological consequences in remote brain regions remain a major blind spot for clinical safety and efficacy. To address this gap, we performed the quantitative mapping of brain metabolism following a focal LIFU-induced BBB opening in a nonhuman primate model, using quantitative BOLD MRI to measure the oxygen extraction fraction (OEF). We report a paradoxical response: While the targeted striatum showed no significant metabolic changes, we observed a profound and spatially specific increase in OEF in the homologous contralateral striatum, an effect predominantly driven by the putamen. These findings demonstrate that focal BBB opening is not merely a localized vascular event but a potent neuromodulatory intervention that induces metabolic stress in distant, untreated brain regions, a discovery with critical implications for the safe and effective clinical translation of all focal brain therapies.

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

Repository

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

Zenodo 19188809

License: apgl-v3
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Languages: Jupyter (4)
Size: 4 files, 4 scripts
Software Heritage: not checked
Found in: “Data, code, and materials availability:”
Holds: 4 notebooks
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: Matplotlib (4 files), NumPy (4 files), NiBabel (3 files), pandas (3 files), seaborn (3 files), ArviZ (2 files), PyMC (2 files), SciPy (2 files), statsmodels (2 files), AFNI (1 file), CuPy (1 file), JAX (1 file), Numba (1 file), PyTorch (1 file), scikit-image (1 file), TensorFlow (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers (HTTP 200)
  • 27 September 2026: the link answers (HTTP 200)
4 files

The paper's code and data availability statement is in the Data section.

Tracing map

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

What the map holds:

  • 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 4 scripts, each with its path and the digest of its content;
  • 3 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

Datasets cited

Data, code, and materials availability

All data and code needed to evaluate and reproduce the results in the paper are present in the paper and/or the Supplementary Materials. The raw MRI datasets supporting the qBOLD analysis have been deposited in a permanent, independent repository and can be accessed at https://doi.org/10.5281/zenodo.19186774. The computational pipeline, consisting of the Jupyter notebooks required to generate the OEF maps and reproduce the statistical analyses, is archived and is available at https://doi.org/10.5281/zenodo.19188809.

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, 6 authors, 9 MeSH terms, 1 funder, 63 references.

Cite

This paper

Sanatkhani, S., Liu, D., Munoz, F., Grinband, J., Konofagou, E. E., & Ferrera, V. P. (2026). Focused ultrasound blood-brain barrier opening reveals a paradoxical remote metabolic response in the primate brain. Science advances, 12(29), eaed4944. https://doi.org/10.1126/sciadv.aed4944

BibTeX

@article{sanatkhani2026focused,
author = {Sanatkhani, Soroosh and Liu, Dong and Munoz, Fabian and Grinband, Jack and Konofagou, Elisa E and Ferrera, Vincent P},
title = {{Focused ultrasound blood-brain barrier opening reveals a paradoxical remote metabolic response in the primate brain}},
journal = {Science advances},
year = {2026},
month = jul,
volume = {12},
number = {29},
pages = {eaed4944},
publisher = {American Association for the Advancement of Science},
issn = {2375-2548},
doi = {10.1126/sciadv.aed4944},
url = {https://doi.org/10.1126/sciadv.aed4944},
pmid = {42467769},
pmcid = {PMC13378551}
}

RIS

TY - JOUR
AU - Sanatkhani, Soroosh
AU - Liu, Dong
AU - Munoz, Fabian
AU - Grinband, Jack
AU - Konofagou, Elisa E
AU - Ferrera, Vincent P
TI - Focused ultrasound blood-brain barrier opening reveals a paradoxical remote metabolic response in the primate brain
T2 - Science advances
J2 - Sci Adv
PY - 2026
DA - 2026/07/17
VL - 12
IS - 29
SP - eaed4944
SN - 2375-2548
PB - American Association for the Advancement of Science
DO - 10.1126/sciadv.aed4944
UR - https://doi.org/10.1126/sciadv.aed4944
LA - en
ER -

CSL-JSON

{
"id": "10.1126/sciadv.aed4944",
"type": "article-journal",
"title": "Focused ultrasound blood-brain barrier opening reveals a paradoxical remote metabolic response in the primate brain",
"container-title": "Science advances",
"author": [
{
"family": "Sanatkhani",
"given": "Soroosh"
},
{
"family": "Liu",
"given": "Dong"
},
{
"family": "Munoz",
"given": "Fabian"
},
{
"family": "Grinband",
"given": "Jack"
},
{
"family": "Konofagou",
"given": "Elisa E"
},
{
"family": "Ferrera",
"given": "Vincent P"
}
],
"container-title-short": "Sci Adv",
"volume": "12",
"issue": "29",
"page": "eaed4944",
"DOI": "10.1126/sciadv.aed4944",
"PMID": "42467769",
"PMCID": "PMC13378551",
"ISSN": "2375-2548",
"publisher": "American Association for the Advancement of Science",
"URL": "https://doi.org/10.1126/sciadv.aed4944",
"language": "en",
"issued": {
"date-parts": [
[
2026,
7,
17
]
]
}
}

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/s42003-026-10957-8 [code]
Brain defence by the extracellular matrix protein Cochlin.
Journal: Communications biology
In common: ArviZ, PyMC, CuPy, 11 other tools
[2] doi:10.1038/s41467-026-72940-5 [code]
Cerebellar growth is associated with domain-specific cerebral maturation and socio-linguistic behavior.
Journal: Nature communications
In common: ArviZ, PyMC, TensorFlow, 7 other tools, 1 reference
[3] doi:10.1162/imag.a.1198 [code]
MEPrep: A robust pipeline for multi-echo fMRI denoising and preprocessing.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: AFNI, scikit-image, NiBabel, 5 other tools, author Jack Grinband
[4] doi:10.1371/journal.pcbi.1014340 [code]
pyhgf: A neural network library for predictive coding.
Journal: PLoS computational biology
In common: ArviZ, PyMC, JAX, 5 other tools, 1 reference
[5] 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: JAX, Numba, TensorFlow, 7 other tools, non-human primate, structural MRI / diffusion
[6] doi:10.1038/s41592-026-03057-2 [code]
CREsted: modeling genomic and synthetic cell-type-specific enhancers across tissues and species.
Journal: Nature methods
In common: CuPy, Numba, TensorFlow, 7 other tools
[7] doi:10.1162/imag.a.1269 [code]
From early to contemporary normative modeling: Mapping individual differences in neurophysiological signals.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: ArviZ, PyMC, statsmodels, 6 other tools
[8] doi:10.1016/j.isci.2026.116825 [code]
Social hierarchy shapes behavioral and transcriptional responses to chronic stress and ketamine in male mice.
Journal: iScience
In common: CuPy, Numba, scikit-image, 7 other tools
[9] doi:10.1038/s41591-026-04287-9 [code]
An international mega-analysis of psychedelic drug effects on brain circuit function.
Journal: Nature medicine
In common: ArviZ, PyMC, NiBabel, 5 other tools, 1 reference
[10] doi: [code]
Naturalistic behavior and self-generated neural activity predictive of self-correction
Journal: bioRxiv : the preprint server for biology
In common: CuPy, JAX, scikit-image, 6 other tools

Contribute

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

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

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.