Focused ultrasound blood-brain barrier opening reveals a paradoxical remote metabolic response in the primate brain.
The 3 matches
- [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] § 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] § 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
- # %%
- # -----------------------------------------------------------------------------
- # 0) IMPORTS
- # -----------------------------------------------------------------------------
- import os
- from pathlib import Path
- import numpy as np
- import pandas as pd
- import nibabel as nib
- import matplotlib.pyplot as plt
- import seaborn as sns
- import matplotlib.gridspec as gridspec
- import jax
- import pymc as pm
- import arviz as az
- from IPython.display import display
- # -----------------------------------------------------------------------------
- # 1) SETTINGS & PATHS for MULTI‐SUBJECT ANALYSIS
- # -----------------------------------------------------------------------------
- # Set the base directory for all processed data here. Change this to update all paths.
- BASE_DATA_DIR = Path("YOUR DIRECTORY")
- output_dir = BASE_DATA_DIR / "CombinedMonkeys_OEF_Bayesian"
- output_dir.mkdir(parents=True, exist_ok=True) # Ensure the directory exists
- subjects = {
- 'M4': dict(
- SUBFOLDER = "M4",
- VOLUMES = {
- "Base": ["1_OEF_Base.nii.gz"],
- "RCN": ["2_OEF_BBBO_RCN.nii.gz"],
- "LCN": ["3_OEF_BBBO_LCN.nii.gz"]
- },
- MASK_BRAIN = "BrainMask_common.nii.gz",
- MASK_ROI_RCN = "2_FUSROI_BBBO_RCN.nii.gz",
- MASK_ROI_LCN = "3_FUSROI_BBBO_LCN.nii.gz",
- MASK_Caudate_Right = "RightCaudate.nii.gz",
- MASK_Caudate_Left = "LeftCaudate.nii.gz",
- MASK_Putamen_Right = "RightPutamen.nii.gz",
- MASK_Putamen_Left = "LeftPutamen.nii.gz"
- ),
- 'M3': dict(
- SUBFOLDER = "M3",
- VOLUMES = {
- "Base": ["1_OEF_Base.nii.gz"], # Ctrl1
- "RCN": [
- "2_OEF_BBBO_RCN.nii.gz", # RCN3
- "3_OEF_BBBO_RCN.nii.gz" # RCN4
- ],
- "LCN": ["4_OEF_BBBO_LCN.nii.gz"] # LCN
- },
- MASK_BRAIN = "BrainMask_common.nii.gz",
- MASK_ROI_RCN = [
- "2_FUSROI_BBBO_RCN.nii.gz", # FUSROI for RCN3
- "3_FUSROI_BBBO_RCN.nii.gz" # FUSROI for RCN4
- ],
- MASK_ROI_LCN = "4_FUSROI_BBBO_LCN.nii.gz",
- MASK_Caudate_Right = "RightCaudate.nii.gz",
- MASK_Caudate_Left = "LeftCaudate.nii.gz",
- MASK_Putamen_Right = "RightPutamen.nii.gz",
- MASK_Putamen_Left = "LeftPutamen.nii.gz"
- ),
- 'M1': dict(
- SUBFOLDER = "M1",
- VOLUMES = {
- "Base": [
- "1_OEF_Base.nii.gz",
- "2_OEF_Base.nii.gz"
- ],
- "RCN": ["3_OEF_BBBO_RCN.nii.gz"]
- },
- MASK_BRAIN = "BrainMask_common.nii.gz",
- MASK_ROI_RCN = "3_FUSROI_BBBO_RCN.nii.gz",
- MASK_Caudate_Right = "RightCaudate.nii.gz",
- MASK_Caudate_Left = "LeftCaudate.nii.gz",
- MASK_Putamen_Right = "RightPutamen.nii.gz",
- MASK_Putamen_Left = "LeftPutamen.nii.gz"
- ),
- 'M2': dict(
- SUBFOLDER = "M2",
- VOLUMES = {
- "Base": ["1_OEF_Base.nii.gz"],
- "RCN": [
- "2_OEF_BBBO_RCN.nii.gz", # RCN2
- "3_OEF_BBBO_RCN.nii.gz" # RCN3
- ]
- },
- MASK_BRAIN = "BrainMask_common.nii.gz",
- MASK_ROI_RCN = [
- "2_FUSROI_BBBO_RCN.nii.gz", # FUSROI for RCN3
- "3_FUSROI_BBBO_RCN.nii.gz" # FUSROI for RCN4
- ],
- MASK_Caudate_Right = "RightCaudate.nii.gz",
- MASK_Caudate_Left = "LeftCaudate.nii.gz",
- MASK_Putamen_Right = "RightPutamen.nii.gz",
- MASK_Putamen_Left = "LeftPutamen.nii.gz"
- ),
- }
- TREATMENT_TYPES = ["RCN","LCN"] # Define all possible treatment types
- # seaborn style overrides
- custom_params = {
- 'ytick.left': True, 'xtick.bottom': True,
- 'xtick.direction': 'out','xtick.color': 'black',
- 'ytick.color': 'black','text.color': 'black',
- 'grid.color': 'black'
- }
- sns.set_theme(
- context='notebook', style='white', palette='deep',
- font='sans-serif', font_scale=1, color_codes=True,
- rc=custom_params
- )
- # -----------------------------------------------------------------------------
- # 2) HELPER FUNCTIONS (bootstrap_diff, cohens_d, load_nifti are unchanged)
- # -----------------------------------------------------------------------------
- def load_nifti(path):
- return nib.load(str(path)).get_fdata()
- def bootstrap_diff(T, B, n_boot=20000, ci=95):
- T_flat = np.asarray(T).ravel()
- B_flat = np.asarray(B).ravel()
- T_clean = T_flat[~np.isnan(T_flat)]
- B_clean = B_flat[~np.isnan(B_flat)]
- if T_clean.size < 2 or B_clean.size < 2 : # Need at least 2 points for meaningful bootstrap
- return np.full(n_boot, np.nan), np.nan, np.nan, np.nan
- boot_T_means = np.random.choice(T_clean, (n_boot, T_clean.size), replace=True).mean(axis=1)
- boot_B_means = np.random.choice(B_clean, (n_boot, B_clean.size), replace=True).mean(axis=1)
- boot_diff_means = boot_T_means - boot_B_means
- if np.all(np.isnan(boot_diff_means)):
- return boot_diff_means, np.nan, np.nan, np.nan
- lo, hi = np.nanpercentile(boot_diff_means, [(100-ci)/2, 100-(100-ci)/2])
- p_val_numerator = np.nansum(boot_diff_means <= 0) if np.nanmean(boot_diff_means) > 0 else np.nansum(boot_diff_means >= 0)
- p_val_denominator = np.sum(~np.isnan(boot_diff_means))
- if p_val_denominator == 0: p = np.nan
- else: p = 2 * (p_val_numerator / p_val_denominator); p = min(p, 1.0)
- return boot_diff_means, lo, hi, p
- def cohens_d(x, y):
- x_arr = np.asarray(x).ravel(); y_arr = np.asarray(y).ravel()
- x_arr = x_arr[~np.isnan(x_arr)]; y_arr = y_arr[~np.isnan(y_arr)]
- nx, ny = len(x_arr), len(y_arr)
- if nx < 2 or ny < 2: return np.nan
- vx, vy = x_arr.var(ddof=1), y_arr.var(ddof=1)
- 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())
- pooled_sd_numerator = (nx-1)*vx + (ny-1)*vy
- pooled_sd_denominator = nx+ny-2
- if pooled_sd_denominator == 0 : return np.nan
- pooled_sd = np.sqrt(pooled_sd_numerator / pooled_sd_denominator)
- if pooled_sd == 0: return np.nan
- return (x_arr.mean() - y_arr.mean()) / pooled_sd
- # -----------------------------------------------------------------------------
- # 3–7) LOOP OVER SUBJECTS → BUILD DATAFRAMES
- # -----------------------------------------------------------------------------
- all_df_roi = []
- all_df_ic = []
- all_df_hemi_ic = []
- all_df_sum = []
- for subj, cfg in subjects.items():
- print(f"Processing subject: {subj}")
- brain_mask_path = BASE_DATA_DIR / cfg['SUBFOLDER'] / cfg['MASK_BRAIN']
- if not brain_mask_path.exists():
- print(f"Critical: Brain mask not found for {subj} at {brain_mask_path}. Skipping subject.")
- continue
- brain_mask = load_nifti(brain_mask_path) > 0
- nx_shape = brain_mask.shape[0] # Get shape for flipping ROIs
- # Load structural masks
- 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)
- 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)
- 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)
- 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)
- # Load and z-score all volumes
- zvols_all_sessions = {}
- raw_vols_all_sessions = {}
- for cond_type, file_list in cfg['VOLUMES'].items():
- if not isinstance(file_list, list):
- print(f"Warning: VOLUMES for {cond_type} in {subj} is not a list. Skipping this condition type.")
- continue
- for idx, fname in enumerate(file_list):
- session_name = f"{cond_type}{idx+1}" if len(file_list) > 1 or cond_type in TREATMENT_TYPES else cond_type
- if len(file_list) == 1 and cond_type == "Base": session_name = "Base"
- vol_path = BASE_DATA_DIR / cfg['SUBFOLDER'] / fname
- if not vol_path.exists():
- print(f"Warning: Volume file not found for {subj}, {session_name}: {vol_path}. Skipping this volume.")
- continue
- vol_data = load_nifti(vol_path)
- raw_vols_all_sessions[session_name] = vol_data
- vol_brain_voxels = vol_data[brain_mask & ~np.isnan(vol_data)]
- if vol_brain_voxels.size > 1:
- μ, σ = vol_brain_voxels.mean(), vol_brain_voxels.std()
- if σ != 0:
- zvols_all_sessions[session_name] = (vol_data - μ) / σ
- else:
- zvols_all_sessions[session_name] = np.zeros_like(vol_data) if np.all(vol_brain_voxels == μ) else np.full_like(vol_data, np.nan)
- else:
- zvols_all_sessions[session_name] = np.full_like(vol_data, np.nan)
- print(f"Warning: Not enough brain voxels for z-scoring {subj}, {session_name}.")
- base_zvol_keys = [k for k in zvols_all_sessions if k.startswith("Base")]
- if not base_zvol_keys:
- print(f"Critical: No valid Base volumes processed for subject {subj}. Skipping subject.")
- continue
- mean_base_z_volume = None
- 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]))]
- if not valid_base_zvols_list:
- print(f"Critical: All base volumes for {subj} are NaN or missing. Cannot compute mean base. Skipping subject.")
- continue
- if len(valid_base_zvols_list) > 1:
- mean_base_z_volume = np.nanmean(np.stack(valid_base_zvols_list, axis=-1), axis=-1)
- else:
- mean_base_z_volume = valid_base_zvols_list[0]
- if mean_base_z_volume is None or np.all(np.isnan(mean_base_z_volume)):
- print(f"Critical: Mean base z-volume is NaN for subject {subj}. Skipping subject.")
- continue
- mid = nx_shape // 2
- mask_hemi_r = brain_mask.copy(); mask_hemi_r[:mid,...] = False; mask_hemi_r &= brain_mask
- mask_hemi_l = brain_mask.copy(); mask_hemi_l[mid:,...] = False; mask_hemi_l &= brain_mask
- 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]
- subj_roi_rows = []
- subj_ic_rows = []
- subj_hemi_ic_rows = []
- subj_sum_rows = []
- for treat_session_key in treatment_session_keys:
- treat_type_prefix = next((tt for tt in TREATMENT_TYPES if treat_session_key.startswith(tt)), None)
- if treat_type_prefix is None: continue
- zvol_treat = zvols_all_sessions[treat_session_key]
- if np.all(np.isnan(zvol_treat)):
- print(f"Warning: Treatment session {treat_session_key} for {subj} is all NaN. Skipping this session's analysis.")
- continue
- session_specific_rois = {}
- base_roi_data_ipsi = None
- mask_fname_to_load_str = None
- mask_key_generic = f"MASK_ROI_{treat_type_prefix}"
- if mask_key_generic in cfg and cfg[mask_key_generic] is not None:
- mask_value = cfg[mask_key_generic]
- # Always treat as list
- if not isinstance(mask_value, list):
- mask_value = [mask_value]
- # Determine session index
- try:
- session_idx = int(treat_session_key.replace(treat_type_prefix, '')) - 1
- except ValueError:
- session_idx = 0 # Default to first if cannot parse
- if 0 <= session_idx < len(mask_value):
- mask_fname_to_load_str = mask_value[session_idx]
- else:
- print(f"Warning: Index {session_idx} out of bounds for {subj}'s {mask_key_generic} (len {len(mask_value)}). Session: {treat_session_key}")
- else:
- print(f"Notice: No ROI mask key '{mask_key_generic}' defined for {subj}, session {treat_session_key}.")
- if mask_fname_to_load_str:
- mask_path_to_load = BASE_DATA_DIR / cfg['SUBFOLDER'] / mask_fname_to_load_str
- if mask_path_to_load.exists():
- loaded_mask = load_nifti(mask_path_to_load) > 0
- if np.any(loaded_mask):
- base_roi_data_ipsi = loaded_mask & brain_mask
- else:
- print(f"Warning: Loaded ROI mask is empty for {subj}, {treat_session_key}: {mask_path_to_load}")
- else:
- print(f"Warning: ROI mask file not found for {subj}, {treat_session_key}: {mask_path_to_load}")
- if base_roi_data_ipsi is not None and np.any(base_roi_data_ipsi):
- if treat_type_prefix == "LCN":
- session_specific_rois[f"{treat_type_prefix} L"] = base_roi_data_ipsi
- session_specific_rois[f"{treat_type_prefix} R"] = np.flip(base_roi_data_ipsi, axis=0) & brain_mask
- else:
- session_specific_rois[f"{treat_type_prefix} R"] = base_roi_data_ipsi
- session_specific_rois[f"{treat_type_prefix} L"] = np.flip(base_roi_data_ipsi, axis=0) & brain_mask
- else:
- if mask_fname_to_load_str :
- print(f"Notice: No valid ROI data for {subj}, session {treat_session_key}. ROI-specific analyses will be skipped for this session.")
- for hemi_label, hemi_mask in [('Left', mask_hemi_l), ('Right', mask_hemi_r)]:
- if not np.any(hemi_mask): continue
- dz_vals_hemi = (zvol_treat[hemi_mask] - mean_base_z_volume[hemi_mask]).ravel()
- dz_vals_hemi = dz_vals_hemi[~np.isnan(dz_vals_hemi)]
- if dz_vals_hemi.size == 0: continue
- is_ipsi_hemi = ((treat_type_prefix in ["RCN", "RV"] and hemi_label=='Right') or \
- (treat_type_prefix == "LCN" and hemi_label=='Left'))
- ipsicontra_label_hemi = 'Ipsilateral' if is_ipsi_hemi else 'Contralateral'
- for v_dz_hemi in dz_vals_hemi:
- subj_hemi_ic_rows.append({
- 'Subject': subj, 'Session': treat_session_key,
- 'FUS_Type': treat_type_prefix, 'Hemisphere': hemi_label,
- 'IpsiContra': ipsicontra_label_hemi, 'Δz': float(v_dz_hemi)
- })
- rois_for_session_analysis = {}
- for roi_label, roi_mask in session_specific_rois.items():
- rois_for_session_analysis[roi_label] = roi_mask
- roi_side = roi_label.split()[-1]
- caudate_mask_for_side = mask_caudate_r if roi_side == 'R' else mask_caudate_l
- putamen_mask_for_side = mask_putamen_r if roi_side == 'R' else mask_putamen_l
- rois_for_session_analysis[f"{roi_label} in Caudate"] = roi_mask & caudate_mask_for_side
- rois_for_session_analysis[f"{roi_label} in Putamen"] = roi_mask & putamen_mask_for_side
- rois_for_session_analysis[f"{roi_label} outside C&P"] = roi_mask & ~caudate_mask_for_side & ~putamen_mask_for_side
- for analysis_roi_label, analysis_roi_mask in rois_for_session_analysis.items():
- if not np.any(analysis_roi_mask): continue
- base_vals_in_roi = mean_base_z_volume[analysis_roi_mask].ravel()
- base_vals_in_roi = base_vals_in_roi[~np.isnan(base_vals_in_roi)]
- for v_base_roi in base_vals_in_roi:
- subj_roi_rows.append({
- 'Subject': subj, 'ROI': analysis_roi_label,
- 'Session': "MeanBase", 'FUS_Type': "Base", 'z': float(v_base_roi)
- })
- treat_vals_in_roi = zvol_treat[analysis_roi_mask].ravel()
- treat_vals_in_roi = treat_vals_in_roi[~np.isnan(treat_vals_in_roi)]
- for v_treat_roi in treat_vals_in_roi:
- subj_roi_rows.append({
- 'Subject': subj, 'ROI': analysis_roi_label,
- 'Session': treat_session_key, 'FUS_Type': treat_type_prefix, 'z': float(v_treat_roi)
- })
- dz_vals_roi = (zvol_treat[analysis_roi_mask] - mean_base_z_volume[analysis_roi_mask]).ravel()
- dz_vals_roi_clean = dz_vals_roi[~np.isnan(dz_vals_roi)]
- if dz_vals_roi_clean.size > 0:
- roi_side_char = analysis_roi_label.split()[1]
- is_ipsi_roi = ((treat_type_prefix in ["RCN", "RV"] and roi_side_char == 'R') or \
- (treat_type_prefix == "LCN" and roi_side_char == 'L'))
- ipsicontra_label_roi = 'Ipsilateral' if is_ipsi_roi else 'Contralateral'
- for v_dz_roi in dz_vals_roi_clean:
- subj_ic_rows.append({
- 'Subject': subj, 'Session': treat_session_key, 'FUS_Type': treat_type_prefix,
- 'ROI_Class': analysis_roi_label, 'Condition': ipsicontra_label_roi, 'Δz': float(v_dz_roi)
- })
- T_sum, B_sum = treat_vals_in_roi, base_vals_in_roi
- if T_sum.size >= 2 and B_sum.size >= 2:
- boot_sum, lo_sum, hi_sum, p_sum = bootstrap_diff(T_sum, B_sum)
- d_sum = cohens_d(T_sum, B_sum)
- subj_sum_rows.append({
- 'Subject': subj, 'Session': treat_session_key, 'FUS_Type': treat_type_prefix,
- 'ROI': analysis_roi_label, 'Comparison': f"{treat_session_key} vs MeanBase",
- 'Δz': T_sum.mean() - B_sum.mean(), 'CI lower': lo_sum, 'CI upper': hi_sum,
- 'p-value': p_sum, "Cohen's d": d_sum
- })
- else:
- subj_sum_rows.append({
- 'Subject': subj, 'Session': treat_session_key, 'FUS_Type': treat_type_prefix,
- 'ROI': analysis_roi_label, 'Comparison': f"{treat_session_key} vs MeanBase",
- 'Δz': np.nanmean(T_sum) - np.nanmean(B_sum) if T_sum.size > 0 and B_sum.size > 0 else np.nan,
- 'CI lower': np.nan, 'CI upper': np.nan, 'p-value': np.nan, "Cohen's d": np.nan
- })
- if subj_roi_rows: all_df_roi.append(pd.DataFrame(subj_roi_rows))
- if subj_ic_rows: all_df_ic.append(pd.DataFrame(subj_ic_rows))
- if subj_hemi_ic_rows: all_df_hemi_ic.append(pd.DataFrame(subj_hemi_ic_rows))
- if subj_sum_rows: all_df_sum.append(pd.DataFrame(subj_sum_rows).set_index(['Subject','Session','ROI','Comparison']))
- df_roi = pd.concat(all_df_roi, ignore_index=True) if all_df_roi else pd.DataFrame()
- if not df_roi.empty:
- # Ensure unique entries for MeanBase per ROI for each subject.
- mean_base_entries = df_roi[df_roi['Session'] == 'MeanBase']
- other_entries = df_roi[df_roi['Session'] != 'MeanBase']
- if not mean_base_entries.empty:
- mean_base_entries_dedup = mean_base_entries.drop_duplicates(subset=['Subject', 'ROI', 'Session', 'FUS_Type', 'z'])
- df_roi = pd.concat([mean_base_entries_dedup, other_entries], ignore_index=True)
- df_ic = pd.concat(all_df_ic, ignore_index=True) if all_df_ic else pd.DataFrame()
- df_hemi_ic = pd.concat(all_df_hemi_ic, ignore_index=True) if all_df_hemi_ic else pd.DataFrame()
- df_sum = pd.concat(all_df_sum) if all_df_sum else pd.DataFrame() # Per-session summary
- # -----------------------------------------------------------------------------
- # BUILD REGRESSION DESIGN DATAFRAME (SESSION-LEVEL ROI MEAN Δz)
- # -----------------------------------------------------------------------------
- if not df_sum.empty:
- df_sum_reset = df_sum.reset_index() if isinstance(df_sum, pd.DataFrame) else pd.DataFrame()
- df_reg = df_sum_reset.dropna(subset=['Δz', 'Subject', 'Session', 'FUS_Type', 'ROI'])
- df_reg = df_reg[df_reg['FUS_Type'].isin(TREATMENT_TYPES)].copy()
- df_reg['roi_side'] = df_reg['ROI'].str.split().str[1].map({'R': 1, 'L': -1})
- df_reg['fus_side'] = df_reg['FUS_Type'].map({'RCN': 1, 'LCN': -1})
- df_reg['Contralateral_Effect'] = df_reg['roi_side'] * df_reg['fus_side'] * (-1)
- df_reg['subject_cat'] = pd.Categorical(df_reg['Subject']).codes
- # -----------------------------------------------------------------------------
- # 8) COMBINED PLOT: SESSION-LEVEL ROI & HEMISPHERIC MEAN Δz BY FUS TYPE
- # -----------------------------------------------------------------------------
- if not df_sum.empty and not df_hemi_ic.empty:
- # ROI effects (already session-level in df_reg)
- plot_roi_full = df_reg[~df_reg['ROI'].str.contains(" in | outside ")]
- plot_roi_full['Laterality'] = (plot_roi_full['Contralateral_Effect']*(-1)).map({1: 'Ipsilateral', -1: 'Contralateral'})
- plot_roi_full['PlotGroup'] = 'ROI ' + plot_roi_full['FUS_Type']
- # Hemispheric effects: aggregate mean Δz per subject/session/hemisphere/ipsi-contra
- hemi_means = (
- df_hemi_ic
- .groupby(['Subject', 'Session', 'FUS_Type', 'Hemisphere', 'IpsiContra'])
- .agg({'Δz': 'mean'})
- .reset_index()
- )
- # Only keep treatment sessions
- hemi_means = hemi_means[hemi_means['FUS_Type'].isin(TREATMENT_TYPES)].copy()
- hemi_means['PlotGroup'] = 'Hemisphere ' + hemi_means['FUS_Type']
- hemi_means = hemi_means.rename(columns={'IpsiContra': 'Laterality'})
- # Match columns for concatenation
- plot_hemi = hemi_means[['Subject', 'Session', 'FUS_Type', 'Δz', 'Laterality', 'PlotGroup']].copy()
- plot_roi_full = plot_roi_full[['Subject', 'Session', 'FUS_Type', 'Δz', 'Laterality', 'PlotGroup']].copy()
- # Combine
- plot_df = pd.concat([plot_hemi, plot_roi_full], ignore_index=True)
- # Set plotting order
- plot_group_order = [
- "Hemisphere RCN", # leftmost
- "ROI RCN",
- "ROI LCN",
- "Hemisphere LCN" # rightmost
- ]
- plot_df = plot_df[plot_df['PlotGroup'].isin(plot_group_order)]
- plot_df['PlotGroup'] = pd.Categorical(plot_df['PlotGroup'], categories=plot_group_order, ordered=True)
- plot_df['Laterality'] = pd.Categorical(plot_df['Laterality'], categories=['Ipsilateral', 'Contralateral'], ordered=True)
- # Calculate n for each group/lat for legend
- n_dict = {}
- for group in plot_group_order:
- for lat in ['Ipsilateral', 'Contralateral']:
- mask = (plot_df['PlotGroup'] == group) & (plot_df['Laterality'] == lat)
- n = mask.sum()
- n_dict[(group, lat)] = n
- # Build legend labels with n in parentheses
- legend_labels = []
- for group in plot_group_order:
- n_ipsi = n_dict[(group, 'Ipsilateral')]
- n_contra = n_dict[(group, 'Contralateral')]
- legend_labels.append(f"{group} (n={n_ipsi})")
- fig, ax = plt.subplots(figsize=(6, 7))
- dodge_offsets = {
- "Hemisphere RCN": -0.375,
- "ROI RCN": -0.125,
- "ROI LCN": 0.125,
- "Hemisphere LCN": 0.375
- }
- # Define marker styles for each group
- marker_map = {
- "Hemisphere RCN": "X",
- "ROI RCN": "o",
- "ROI LCN": "s",
- "Hemisphere LCN": "^"
- }
- # Separate out groups with n=2
- plot_df_for_pointplot = plot_df.copy()
- for group in ["ROI LCN", "Hemisphere LCN"]:
- for lat in ['Ipsilateral', 'Contralateral']:
- mask = (plot_df['PlotGroup'] == group) & (plot_df['Laterality'] == lat)
- sub_df = plot_df[mask]
- if len(sub_df) == 2:
- x_val = 0 if lat == 'Ipsilateral' else 1
- x_val_dodged = x_val + dodge_offsets[group]
- # Plot each point individually at the correct x
- for y in sub_df['Δz']:
- ax.scatter(x_val_dodged, y, marker=marker_map[group], color='black', s=80, zorder=3)
- plot_df_for_pointplot = plot_df_for_pointplot[~mask]
- # Plot the rest with error bars
- if not plot_df_for_pointplot.empty:
- sns.pointplot(
- data=plot_df_for_pointplot,
- x='Laterality',
- y='Δz',
- hue='PlotGroup',
- hue_order=plot_group_order,
- markers=['X', 'o', 's', '^'],
- palette=['black', 'black', 'black', 'black'],
- linestyles=['none'] * 4,
- dodge=0.5,
- errorbar=('ci', 95),
- capsize=0.05,
- err_kws={'linewidth': 1.5, 'alpha': 0.7},
- ax=ax,
- markersize=8
- )
- ax.axhline(0, ls='--', color='darkgray', zorder=0)
- ax.set_title("", fontsize=24)
- ax.set_ylabel(r"$\overline{\Delta z}_{\text{OEF}}$ (Treatment - Control)", fontsize=24)
- ax.set_xlabel("", fontsize=24)
- ax.set_ylim(-0.4, 0.3)
- ax.set_yticks([-0.4, 0, 0.3])
- ax.set_yticklabels([str(x) for x in [-0.4, 0, 0.3]], fontsize=22)
- #ax.tick_params(axis='both', which='major', labelsize=22)
- ax.set_xticklabels(["Ipsilateral", "Contralateral"], fontsize=24)
- # Set legend with n in parentheses
- handles, _ = ax.get_legend_handles_labels()
- #ax.legend(handles, legend_labels, title="", loc='upper left', bbox_to_anchor=(1.02, 1), ncol=1, fontsize=10)
- ax.legend(handles, legend_labels, title="", loc='lower right', ncol=1, fontsize=20, framealpha=1, edgecolor='black')
- plt.tight_layout()
- plt.savefig(output_dir / "SESSION_LEVEL_ROI_HEMISPHERIC_MEAN_Delta_z.svg", format='svg')
- plt.show()
- plt.close(fig)
- else:
- print("Skipping Combined Plot: No session-level ROI or hemispheric mean Δz data available.")
- # -----------------------------------------------------------------------------
- # 8a) PLOTS FOR CAUDATE & PUTAMEN SUB-REGIONS
- # -----------------------------------------------------------------------------
- def plot_subregion(df, region_name):
- df_plot = df[df['ROI'].str.contains(f" in {region_name}")].copy()
- if not df_plot.empty:
- df_plot['Laterality'] = (df_plot['Contralateral_Effect']*(-1)).map({1: 'Ipsilateral', -1: 'Contralateral'})
- # Calculate n for each FUS_Type/Laterality for legend
- n_dict = {}
- for fus_type in ['RCN', 'LCN']:
- for lat in ['Ipsilateral', 'Contralateral']:
- mask = (df_plot['FUS_Type'] == fus_type) & (df_plot['Laterality'] == lat)
- n = mask.sum()
- n_dict[(fus_type, lat)] = n
- legend_labels = []
- for fus_type in ['RCN', 'LCN']:
- n_ipsi = n_dict[(fus_type, 'Ipsilateral')]
- n_contra = n_dict[(fus_type, 'Contralateral')]
- legend_labels.append(f"{fus_type} (n={n_ipsi})")
- fig, ax = plt.subplots(figsize=(5, 7))
- # --- Custom plotting logic for N=2 ---
- marker_map = {'RCN': 'o', 'LCN': 's'}
- dodge_offsets = {'RCN': -0.1, 'LCN': 0.1} # Adjust if you change dodge in pointplot
- plot_df_for_pointplot = df_plot.copy()
- for fus_type in ['RCN', 'LCN']:
- for lat in ['Ipsilateral', 'Contralateral']:
- mask = (df_plot['FUS_Type'] == fus_type) & (df_plot['Laterality'] == lat)
- sub_df = df_plot[mask]
- if len(sub_df) == 2:
- x_val = 0 if lat == 'Ipsilateral' else 1
- x_val_dodged = x_val + dodge_offsets[fus_type]
- for y in sub_df['Δz']:
- ax.scatter(x_val_dodged, y, marker=marker_map[fus_type], color='black', s=80, zorder=3)
- plot_df_for_pointplot = plot_df_for_pointplot[~mask]
- # Plot the rest with error bars (N>2) using seaborn pointplot
- if not plot_df_for_pointplot.empty:
- sns.pointplot(
- data=plot_df_for_pointplot,
- x='Laterality',
- y='Δz',
- hue='FUS_Type',
- hue_order=['RCN', 'LCN'],
- markers=['o', 's'],
- palette=['black', 'black'],
- linestyles=['none', 'none'],
- dodge=0.2,
- errorbar=('ci', 95),
- err_kws={'linewidth': 1.5, 'alpha': 0.7},
- capsize=0.05,
- ax=ax,
- markersize=8,
- )
- ax.axhline(0, ls='--', color='darkgray', zorder=0)
- ax.set_title(rf"{region_name}", fontsize=24)
- ax.set_ylabel(r"$\overline{\Delta z}_{\text{OEF}}$ (Treatment - Control)", fontsize=24)
- ax.set_xlabel("", fontsize=24)
- ax.set_ylim(-0.4, 0.3)
- ax.set_yticks([-0.4, 0, 0.3])
- ax.set_yticklabels([str(x) for x in [-0.4, 0, 0.3]], fontsize=22)
- ax.set_xticklabels(["Ipsilateral", "Contralateral"], fontsize=24)
- handles, _ = ax.get_legend_handles_labels()
- 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)
- plt.tight_layout()
- if region_name == "Caudate":
- plt.savefig(output_dir / "SESSION_LEVEL_Caudate_ROI_HEMISPHERIC_MEAN_Delta_z.svg", format='svg')
- elif region_name == "Putamen":
- plt.savefig(output_dir / "SESSION_LEVEL_Putamen_ROI_HEMISPHERIC_MEAN_Delta_z.svg", format='svg')
- plt.show()
- plt.close(fig)
- else:
- print(f"No data for {region_name} sub-region plot.")
- if not df_reg.empty:
- plot_subregion(df_reg, "Caudate")
- plot_subregion(df_reg, "Putamen")
- # -----------------------------------------------------------------------------
- # 10) OVERALL SUMMARY TABLE (Δz, CI, p‐value, Cohen’s d) - Aggregated per ROI_Class
- # -----------------------------------------------------------------------------
- overall_summaries = []
- if not df_roi.empty:
- df_roi_overall = df_roi.copy()
- df_roi_overall['ROI_Class'] = df_roi_overall['ROI']
- df_roi_overall['Condition_Type'] = df_roi_overall['FUS_Type']
- unique_roi_classes = sorted(df_roi_overall['ROI_Class'].unique())
- for roi_cls in unique_roi_classes:
- primary_fus_type = roi_cls.split()[0]
- if primary_fus_type not in TREATMENT_TYPES : continue
- T_all = df_roi_overall.query("ROI_Class == @roi_cls and Condition_Type == @primary_fus_type").z.values
- B_all = df_roi_overall.query("ROI_Class == @roi_cls and Condition_Type == 'Base'").z.values
- if T_all.size >= 2 and B_all.size >= 2:
- # Check for NaNs after selection, as z-scores could be NaN if original data was problematic
- T_all_clean = T_all[~np.isnan(T_all)]
- B_all_clean = B_all[~np.isnan(B_all)]
- if T_all_clean.size >=2 and B_all_clean.size >=2:
- boot_ov, lo_ov, hi_ov, p_ov = bootstrap_diff(T_all_clean, B_all_clean)
- d_ov = cohens_d(T_all_clean, B_all_clean)
- mean_diff_ov = T_all_clean.mean() - B_all_clean.mean()
- else:
- mean_diff_ov, lo_ov, hi_ov, p_ov, d_ov = np.nan, np.nan, np.nan, np.nan, np.nan
- else:
- mean_diff_ov, lo_ov, hi_ov, p_ov, d_ov = np.nan, np.nan, np.nan, np.nan, np.nan
- overall_summaries.append({
- 'ROI_Class': roi_cls,
- 'Comparison': f"{primary_fus_type} (pooled) vs Base (pooled)",
- 'Δz': mean_diff_ov,
- 'CI lower': lo_ov, 'CI upper': hi_ov,
- 'p-value': p_ov, "Cohen's d": d_ov
- })
- # -----------------------------------------------------------------------------
- # 12) BAYESIAN REGRESSION (SESSION-LEVEL ROI MEAN Δz, HIERARCHICAL)
- # -----------------------------------------------------------------------------
- idata = None
- df_reg_full_roi = df_reg[~df_reg['ROI'].str.contains(" in | outside ")].copy()
- 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:
- coords = {
- "subject": df_reg_full_roi['Subject'].unique(),
- "obs_id": np.arange(len(df_reg_full_roi))
- }
- subject_map = {name: i for i, name in enumerate(df_reg_full_roi['Subject'].unique())}
- subject_indices_for_model = df_reg_full_roi['Subject'].map(subject_map).values
- with pm.Model(coords=coords) as model_hierarchical:
- # Data
- delta_z_obs = pm.Data("delta_z_obs", df_reg_full_roi["Δz"].values, dims="obs_id")
- fus_side_data = pm.Data("fus_side_data", df_reg_full_roi["fus_side"].values.astype(float), dims="obs_id")
- roi_side_data = pm.Data("roi_side_data", df_reg_full_roi["roi_side"].values.astype(float), dims="obs_id")
- contralateral_effect_data = pm.Data("contralateral_effect_data", df_reg_full_roi["Contralateral_Effect"].values.astype(float), dims="obs_id")
- subject_idx_data = pm.Data("subject_idx_data", subject_indices_for_model, dims="obs_id")
- # Hyperpriors for random intercepts
- mu_alpha_subj = pm.Normal('mu_alpha_subj', mu=0, sigma=2)
- sigma_alpha_subj = pm.HalfNormal('sigma_alpha_subj', sigma=2)
- alpha_subj_offset = pm.Normal('alpha_subj_offset', mu=0, sigma=1, dims="subject")
- alpha_subj = pm.Deterministic('alpha_subj', mu_alpha_subj + alpha_subj_offset * sigma_alpha_subj, dims="subject")
- # Fixed effects
- β_fus = pm.Normal('FUS_Target_Side', mu=0, sigma=2)
- β_read = pm.Normal('Read_Side', mu=0, sigma=2)
- β_int = pm.Normal('Contralateral_Effect', mu=0, sigma=2)
- σ_resid = pm.HalfNormal('sigma_resid', sigma=2)
- ν_resid = pm.Exponential('nu_resid', 1/30.0)
- mu_eq = alpha_subj[subject_idx_data] + \
- β_fus * fus_side_data + \
- β_read * roi_side_data + \
- β_int * contralateral_effect_data
- y_obs = pm.StudentT('y', nu=ν_resid, mu=mu_eq, sigma=σ_resid,
- observed=delta_z_obs, dims="obs_id")
- print("Sampling Bayesian model using CPU (default NUTS sampler)...")
- try:
- idata = pm.sample(5000, tune=2000, chains=16,
- cores=16,
- return_inferencedata=True,
- idata_kwargs={"log_likelihood":True},
- target_accept=0.95,
- random_seed=80)
- ppc = pm.sample_posterior_predictive(idata, var_names=['y', 'alpha_subj'], random_seed=80, return_inferencedata=True)
- idata.extend(ppc)
- print("Sampling complete.")
- except Exception as e:
- print(f"Error during PyMC sampling: {e}")
- idata = None
- else:
- print("Skipping Bayesian Regression: No session-level ROI mean Δz data available.")
- # -----------------------------------------------------------------------------
- # 12a) BAYESIAN REGRESSION (SUB-REGIONS)
- # -----------------------------------------------------------------------------
- idata_v2 = None
- df_reg_v2 = df_reg[df_reg['ROI'].str.contains(" in | outside ")].copy()
- 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:
- def get_subregion_type(roi_name):
- if "in Caudate" in roi_name: return "Caudate"
- if "in Putamen" in roi_name: return "Putamen"
- if "outside C&P" in roi_name: return "Outside"
- return "Other"
- df_reg_v2['subregion'] = df_reg_v2['ROI'].apply(get_subregion_type)
- df_reg_v2['Contralateral_Effect_Caudate'] = df_reg_v2.apply(lambda row: row['Contralateral_Effect'] if row['subregion'] == 'Caudate' else 0, axis=1)
- df_reg_v2['Contralateral_Effect_Putamen'] = df_reg_v2.apply(lambda row: row['Contralateral_Effect'] if row['subregion'] == 'Putamen' else 0, axis=1)
- df_reg_v2['Contralateral_Effect_Outside'] = df_reg_v2.apply(lambda row: row['Contralateral_Effect'] if row['subregion'] == 'Outside' else 0, axis=1)
- coords_v2 = {
- "subject": df_reg_v2['Subject'].unique(),
- "obs_id": np.arange(len(df_reg_v2))
- }
- subject_map_v2 = {name: i for i, name in enumerate(df_reg_v2['Subject'].unique())}
- subject_indices_for_model_v2 = df_reg_v2['Subject'].map(subject_map_v2).values
- with pm.Model(coords=coords_v2) as model_v2:
- delta_z_obs = pm.Data("delta_z_obs", df_reg_v2["Δz"].values, dims="obs_id")
- fus_side_data = pm.Data("fus_side_data", df_reg_v2["fus_side"].values.astype(float), dims="obs_id")
- roi_side_data = pm.Data("roi_side_data", df_reg_v2["roi_side"].values.astype(float), dims="obs_id")
- subject_idx_data = pm.Data("subject_idx_data", subject_indices_for_model_v2, dims="obs_id")
- int_caudate_data = pm.Data("int_caudate_data", df_reg_v2["Contralateral_Effect_Caudate"].values.astype(float), dims="obs_id")
- int_putamen_data = pm.Data("int_putamen_data", df_reg_v2["Contralateral_Effect_Putamen"].values.astype(float), dims="obs_id")
- int_outside_data = pm.Data("int_outside_data", df_reg_v2["Contralateral_Effect_Outside"].values.astype(float), dims="obs_id")
- mu_alpha_subj = pm.Normal('mu_alpha_subj', mu=0, sigma=2)
- sigma_alpha_subj = pm.HalfNormal('sigma_alpha_subj', sigma=2)
- alpha_subj_offset = pm.Normal('alpha_subj_offset', mu=0, sigma=1, dims="subject")
- alpha_subj = pm.Deterministic('alpha_subj', mu_alpha_subj + alpha_subj_offset * sigma_alpha_subj, dims="subject")
- β_fus = pm.Normal('LIFU_Target_Side', mu=0, sigma=2)
- β_read = pm.Normal('ROI_Side', mu=0, sigma=2)
- β_int_caudate = pm.Normal('Contralateral_Effect_in_Caudate', mu=0, sigma=2)
- β_int_putamen = pm.Normal('Contralateral_Effect_in_Putamen', mu=0, sigma=2)
- β_int_outside = pm.Normal('Contralateral_Effect_outside_CP', mu=0, sigma=2)
- σ_resid = pm.HalfNormal('sigma_resid', sigma=2)
- ν_resid = pm.Exponential('nu_resid', 1/30.0)
- mu_eq = (alpha_subj[subject_idx_data] +
- β_fus * fus_side_data +
- β_read * roi_side_data +
- β_int_caudate * int_caudate_data +
- β_int_putamen * int_putamen_data +
- β_int_outside * int_outside_data)
- y_obs = pm.StudentT('y', nu=ν_resid, mu=mu_eq, sigma=σ_resid, observed=delta_z_obs, dims="obs_id")
- print("Sampling Bayesian sub-region model...")
- try:
- idata_v2 = pm.sample(5000, tune=2000, chains=16, cores=16, return_inferencedata=True,
- idata_kwargs={"log_likelihood": True}, target_accept=0.95, random_seed=81)
- ppc_v2 = pm.sample_posterior_predictive(idata_v2, var_names=['y', 'alpha_subj'], random_seed=81, return_inferencedata=True)
- idata_v2.extend(ppc_v2)
- print("Sub-region model sampling complete.")
- except Exception as e:
- print(f"Error during PyMC sampling for sub-region model: {e}")
- idata_v2 = None
- else:
- print("Skipping Sub-region Bayesian Regression: No data available.")
- # -----------------------------------------------------------------------------
- # 13) TRACE & POSTERIOR PAIR PLOTS
- # -----------------------------------------------------------------------------
- if idata:
- var_names_trace = ['mu_alpha_subj', 'sigma_alpha_subj', 'FUS_Target_Side', 'Read_Side', 'Contralateral_Effect', 'sigma_resid', 'nu_resid']
- # Check if all variables are in idata before plotting
- valid_vars_for_trace = [var for var in var_names_trace if var in idata.posterior]
- if valid_vars_for_trace:
- # Reverted to original trace plot call
- az.plot_trace(idata, var_names=valid_vars_for_trace, compact=True, figsize=(12, len(valid_vars_for_trace)*2))
- plt.tight_layout(); plt.show()
- else:
- print("No valid variables for trace plot found in idata.posterior.")
- if 'alpha_subj' in idata.posterior:
- az.plot_forest(idata, var_names=['alpha_subj'], combined=True, hdi_prob=0.95, figsize=(8, max(4, len(idata.posterior.subject)*0.3)))
- plt.title("Subject-Specific Intercepts (alpha_subj)")
- plt.show()
- else:
- print("'alpha_subj' not found in idata.posterior for forest plot.")
- else:
- print("Skipping Trace Plots: No idata from Bayesian model.")
- #-----------------------------------------------------------------------------
- # 15) POSTERIOR KDES of fixed effect coefficients
- # -----------------------------------------------------------------------------
- if idata:
- fixed_effects_kde = ['FUS_Target_Side','Read_Side','Contralateral_Effect']
- valid_vars_for_kde_pair = [var for var in fixed_effects_kde if var in idata.posterior]
- if valid_vars_for_kde_pair:
- df_post_fixed = idata.posterior[valid_vars_for_kde_pair].to_dataframe().reset_index().melt(
- id_vars=['chain','draw'], value_vars=valid_vars_for_kde_pair,
- var_name='parameter', value_name='value'
- )
- if not df_post_fixed.empty:
- fig, ax = plt.subplots(figsize=(8,7))
- # Style maps: different shades of grey/black, line style, and width
- style_map = {
- 'FUS_Target_Side': {'hatch': None, 'linestyle': ':', 'linewidth': 1, 'label': 'LIFU Target Side', 'text': None},
- 'Read_Side': {'hatch': None, 'linestyle': '--', 'linewidth': 1, 'label': 'ROI Side', 'text': None},
- 'Contralateral_Effect': {'hatch': '////', 'linestyle': '-', 'linewidth': 2.5, 'label': 'Contralateral Effect', 'text': 'Contralateral'}
- }
- for param in valid_vars_for_kde_pair:
- data = df_post_fixed.query("parameter==@param")['value']
- kde = sns.kdeplot(
- data=data,
- ax=ax,
- color='black',
- linestyle=style_map[param]['linestyle'],
- linewidth=style_map[param]['linewidth'],
- fill=False,
- label=style_map[param]['label'],
- )
- if style_map[param]['text']:
- x, y = kde.get_lines()[-1].get_data()
- # For Caudate, add a light black face shade
- if param == 'Contralateral_Effect':
- ax.fill_between(x, 0, y, facecolor='black', alpha=0.2, edgecolor='black', hatch=style_map[param]['hatch'])
- else:
- ax.fill_between(x, 0, y, facecolor='none', edgecolor='black', hatch=style_map[param]['hatch'], alpha=0.2)
- ax.axvline(0, ls='--', color='black', lw=1)
- ax.set_xlim(-0.1, 0.3)
- ax.set_ylim(0, 25)
- sns.despine(ax=ax)
- ax.set_xlabel('Coefficient value', fontsize=24)
- ax.set_ylabel('Density', fontsize=24)
- xticks = ax.get_xticks()
- ax.set_xticks(xticks[::2])
- ax.set_xticklabels([f"{x:.1f}" for x in xticks[::2]], fontsize=22)
- ax.set_yticklabels([f"{y:g}" for y in ax.get_yticks()], fontsize=22)
- ax.set_title('')
- ax.legend(title='', fontsize=22, shadow=False, frameon=True, framealpha=1, edgecolor='black', borderpad=0.2, borderaxespad=0.2)
- plt.tight_layout()
- plt.savefig(output_dir / "Regression.svg", format='svg')
- plt.show()
- plt.close(fig)
- else:
- print("No data for fixed effects KDE plot after melting.")
- else:
- print("No valid fixed effect variables for KDE plots found in idata.posterior.")
- else:
- print("Skipping Posterior KDEs: No idata from Bayesian model.")
- # -----------------------------------------------------------------------------
- # 15a) POSTERIOR KDES of fixed effect coefficients (SUB-REGIONS)
- # -----------------------------------------------------------------------------
- if idata_v2:
- fixed_effects_kde_v2 = ['LIFU_Target_Side', 'ROI_Side', 'Contralateral_Effect_in_Caudate', 'Contralateral_Effect_in_Putamen', 'Contralateral_Effect_outside_CP']
- valid_vars_for_kde_v2 = [var for var in fixed_effects_kde_v2 if var in idata_v2.posterior]
- if valid_vars_for_kde_v2:
- df_post_fixed_v2 = idata_v2.posterior[valid_vars_for_kde_v2].to_dataframe().reset_index().melt(
- id_vars=['chain','draw'], value_vars=valid_vars_for_kde_v2,
- var_name='parameter', value_name='value'
- )
- if not df_post_fixed_v2.empty:
- # Improved KDE plot for fixed effect coefficients (Sub-region Model) with distinguishable groups (no color)
- # Define hatching and line style for each group
- group_hatch_map = {
- 'Contralateral_Effect_in_Caudate': {'hatch': '////', 'linestyle': ':', 'linewidth': 2.5, 'label': 'Contralateral Effect in Caudate', 'text': 'Caudate'},
- 'Contralateral_Effect_in_Putamen': {'hatch': '\\\\\\', 'linestyle': '-', 'linewidth': 2.5, 'label': 'Contralateral Effect in Putamen', 'text': 'Putamen'},
- 'Contralateral_Effect_outside_CP': {'hatch': None, 'linestyle': '-.', 'linewidth': 2.5, 'label': 'Contralateral Effect Outside Striatum', 'text': 'Outside'},
- 'LIFU_Target_Side': {'hatch': None, 'linestyle': ':', 'linewidth': 1, 'label': 'LIFU Target Side', 'text': None},
- 'ROI_Side': {'hatch': None, 'linestyle': '--', 'linewidth': 1, 'label': 'ROI Side', 'text': None},
- }
- fig, ax = plt.subplots(figsize=(8, 7))
- peak_positions = {}
- for param in valid_vars_for_kde_v2:
- if param in group_hatch_map:
- data = df_post_fixed_v2.query("parameter==@param")['value']
- # Plot KDE line
- kde = sns.kdeplot(
- data=data,
- ax=ax,
- color='black',
- linestyle=group_hatch_map[param]['linestyle'],
- linewidth=group_hatch_map[param]['linewidth'],
- fill=False,
- label=group_hatch_map[param]['label'],
- )
- # Overlay hatched fill for the three main groups
- if group_hatch_map[param]['text']:
- x, y = kde.get_lines()[-1].get_data()
- # For Caudate, add a light black face shade
- if param == 'Contralateral_Effect_in_Putamen':
- ax.fill_between(x, 0, y, facecolor='black', alpha=0.2, edgecolor='black', hatch=group_hatch_map[param]['hatch'])
- else:
- ax.fill_between(x, 0, y, facecolor='none', edgecolor='black', hatch=group_hatch_map[param]['hatch'], alpha=0.2)
- # Store peak position for annotation
- if 'text' in group_hatch_map[param]:
- peak_idx = np.argmax(y)
- peak_x = x[peak_idx]
- peak_y = y[peak_idx]
- peak_positions[param] = (peak_x, peak_y)
- # Add text annotations for Caudate, Putamen, Outside
- for param, (peak_x, peak_y) in peak_positions.items():
- text = group_hatch_map[param]['text']
- # Offset 'Outside' label to the right
- if text == 'Outside':
- peak_x = peak_x + 0.015
- peak_y = peak_y - 0.3
- if text == 'Putamen':
- peak_x = peak_x + 0.01
- peak_y = peak_y - 0.2
- 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))
- ax.axvline(0, ls='--', color='black', lw=1)
- ax.set_xlim(-0.1, 0.3)
- ax.set_ylim(0, 25)
- sns.despine(ax=ax)
- ax.set_xlabel('Coefficient value', fontsize=24)
- ax.set_ylabel('Density', fontsize=24)
- xticks = ax.get_xticks()
- # Use 0.1 precision and skip every other tick
- ax.set_xticks(xticks[::2])
- ax.set_xticklabels([f"{x:.1f}" for x in xticks[::2]], fontsize=22)
- ax.set_yticklabels([f"{y:g}" for y in ax.get_yticks()], fontsize=22)
- ax.set_title('')
- ax.legend(title='', fontsize=17.5, shadow=False, frameon=True, framealpha=1, edgecolor='black', borderpad=0.2, borderaxespad=0.2)
- plt.tight_layout()
- plt.savefig(output_dir / "Regression_subregion.svg", format='svg')
- plt.show()
- plt.close(fig)
- else:
- print("No data for fixed effects KDE plot (sub-region model) after melting.")
- else:
- print("No valid fixed effect variables for KDE plots found in idata_v2.posterior.")
- else:
- 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
- Zuckerman Mind Brain Behavior Institute, Columbia University, New York, NY, USA
- Department of Psychiatry, Columbia University, New York, NY, USA
- Department of Radiology, Columbia University, New York, NY, USA
- Department of Biomedical Engineering, Columbia University, New York, NY, USA
- Department of Neuroscience, Columbia University, New York, NY, USA
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
Availability: 1 check, the latest on 27 September 2026: the link answers (HTTP 200)
- 27 September 2026: the link answers (HTTP 200)
4 files
- 1-Monkey_qBOLD_BBBO_temp
late.ipynb , Jupyter, 597 lines, 1 match - 2-Combined_Monkeys_OEF_A
nalysis_template.ipynb , Jupyter, 231 lines - 3-Combined_Monkeys_OEF_B
ayesian_template.ipynb , Jupyter, 951 lines, 2 matches - 4-Monkey_OEF_Visualizati
on_template.ipynb , Jupyter, 1,616 lines
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
- zenodo:19186774, at Zenodo; found in “Data, code, and materials availability:”
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/
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://
BibTeX
@article{sanatkhani2026f
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/
url = {https://
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/
VL - 12
IS - 29
SP - eaed4944
SN - 2375-2548
PB - American Association for the Advancement of Science
DO - 10.1126/
UR - https://
LA - en
ER -
CSL-JSON
{
"id": "10.1126/
"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":
"volume": "12",
"issue": "29",
"page": "eaed4944",
"DOI": "10.1126/
"PMID": "42467769",
"PMCID": "PMC13378551",
"ISSN": "2375-2548",
"publisher": "American Association for the Advancement of Science",
"URL": "https://
"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 biologyIn 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 communicationsIn 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 biologyIn 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 oneIn 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 methodsIn 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: iScienceIn 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 medicineIn common: ArviZ, PyMC, NiBabel, 5 other tools, 1 reference
- [10] doi: [code]
- Naturalistic behavior and self-generated neural activity predictive of self-correctionJournal: bioRxiv : the preprint server for biologyIn 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.
Claim this paper
Correct its record
Say what each link of this record is, remove the ones that are not the paper's, add the ones that are missing. The correction becomes a new version of the record, in its Versions section.
Validate its tracing map
You validate the map as this page shows it: 1 repository of the authors' code, each at its verified commit and with its license, 4 scripts, and 3 matches between paragraphs and code (see the Code and Map sections). It then receives a DOI on Zenodo, with you (your ORCID iD) and OSCR as its creators; the code itself is not deposited.
The map's fingerprint: sha256:85c17c29b48de40e…
Add the badge to its README
The badge links the code to this page. Copy one of these into the README of the paper's code: only you decide where it goes, and nothing is changed for you.
Markdown
[.
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.
