From 208b1e80ddfd1f4313b649c67c5d07d711a47ae7 Mon Sep 17 00:00:00 2001 From: Mathieu Boudreau Date: Tue, 20 Feb 2024 11:03:00 -0400 Subject: [PATCH] Reintroduce matplotlib plot code that was removed in 0d05160 and 9fb0c94 --- content/index.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/index.ipynb b/content/index.ipynb index 5d08e02..230eeb0 100644 --- a/content/index.ipynb +++ b/content/index.ipynb @@ -1 +1 @@ -{"metadata":{"celltoolbar":"Tags","kernelspec":{"display_name":"Python 3 (ipykernel)","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.8.10","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"build = 'archive' # 'archive' uses the neurolibre archive of the data., 'latest' would download the latest versions of the data.\nnotebook = 'neurolibre-figures' # neurolibre-figures: no processing, use downloaded processed data and do figures & stats,\n # neurolibre-clean: if some processed data exists, cleanup; Reprocess data\n # colab: download and process data from scratch in Google Colab\n\ntry:\n import google.colab\n notebook = 'colab'\nexcept:\n pass","metadata":{"tags":["remove_output","remove_input"],"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"\n\n\n\n\n\n","metadata":{}},{"cell_type":"markdown","source":"

\n

\n\n

\nAnalysis code for the paper \"RF shimming in the cervical spinal cord at 7T\"\n

\n
\n\n

\nDaniel Papp1, Kyle M. Gilbert2,3, Gaspard Cereza1, Alexandre D’Astous1, Nibardo Lopez-Rios1, Mathieu Boudreau1, Marcus Couch4, Pedram Yazdanbakhsh5, Robert L. Barry6,7,8, Eva Alonso Ortiz1, Julien Cohen-Adad1,9,10\n

\n\n\n
\n\n

\n1NeuroPoly Lab, Institute of Biomedical Engineering, Polytechnique Montreal, Montreal, QC, Canada,\n2 Centre for Functional and Metabolic Mapping, The University of Western Ontario, London, ON, Canada,\n3 Department of Medical Biophysics, The University of Western Ontario, London, ON, Canada,\n4 Siemens Healthcare Limited, Montreal, QC, Canada,\n5 McConnell Brain Imaging Centre, Montreal Neurological Institute, McGill University, Montreal, QC, Canada,\n6 Athinoula A. Martinos Center for Biomedical Imaging, Department of Radiology, Massachusetts General Hospital, Charlestown, MA, USA,\n7 Harvard Medical School, Boston, MA, USA,\n8 Harvard-Massachusetts Institute of Technology Health Sciences & Technology, Cambridge, MA, USA,\n9 Mila - Quebec AI Institute, Montreal, QC, Canada,\n10 Functional Neuroimaging Unit, Centre de recherche de l'Institut universitaire de gériatrie de Montréal QC, Canada\n\n

\n

\n

\n","metadata":{}},{"cell_type":"markdown","source":"

Summary

\n\nData was collected from five participants between two 7T sites with a custom 8Tx/20Rx parallel transmission (pTx) coil. We explored two RF shimming approaches from an MRI vendor and four from an open-source toolbox, showcasing their ability to enhance transmit field and signal homogeneity along the cervical spinal cord.\n\nThe results indicate significant improvements in B1+ efficiency and cerebrospinal fluid / spinal cord signal ratio across various RF shimming conditions compared to the vendor based methods.\n\nThe study's findings highlight the potential of RF shimming to advance 7T MRI's clinical utility for central nervous system imaging by enabling more homogenous and efficient spinal cord imaging. Additionally, the research incorporates a reproducible Jupyter Notebook, enhancing the study's transparency and facilitating peer verification. By also making the data openly accessible on OpenNeuro, we ensure that the scientific community can further explore, validate, and build upon our findings, contributing to the collective advancement in high-resolution imaging techniques.\n\n```{figure} ../featured.png\n---\nwidth: 900px\nname: fig1\nalign: center\n---\n```","metadata":{}},{"cell_type":"markdown","source":"

\nFigure 1. Overview of the RF shimming procedure. The top panel shows the RF coil used for the experiments, alongside the Tx coil geometry and the electromagnetic simulation results (on Gustav model) yielding the CP mode used for this coil. The bottom panel shows the RF shimming procedure (with approximate duration). First, GRE and tfl_rfmap scans are acquired (4min30s). Second, these images are transferred via ethernet socket from the MRI console onto a separate laptop running Shimming Toolbox and SCT (əs). Third, the spinal cord is automatically segmented to produce a mask that is resampled into the space of the individual coil magnitude and phase images of the tfl_rfmap scan (~5s). Fourth, the RF shim weights are calculated according to the defined constraints for each shim scenario (1min total).\n

","metadata":{}},{"cell_type":"markdown","source":"

Statement of Need

\n\nAdvancing the development of 7T MRI for spinal cord imaging is crucial for the enhanced diagnosis and monitoring of various neurodegenerative diseases {cite:p}`Kearney2015-py` and traumas {cite:p}`David2019-jy`. However, a significant challenge at this field strength is the transmit field inhomogeneity {cite:p}`Ibrahim2001-xt,Collins2005-za,Roschmann1987-om,Yang2002-ui`. Such inhomogeneity is particularly problematic for imaging the small, deep anatomical structures of the cervical spinal cord , as it can cause uneven signal intensity and elevate the local specific absorption ratio, compromising image quality. This multi-site study explores several radiofrequency (RF) shimming techniques in the cervical spinal cord at 7T.\n","metadata":{}},{"cell_type":"markdown","source":"# 1     |     Data\n\nThe data can be downloaded from: https://openneuro.org/datasets/ds004906\n\nThe structure of the input dataset is as follows (JSON sidecars are not listed for clarity):\n\n```{toggle}\n~~~\nds004906\n├── CHANGES\n├── README\n├── dataset_description.json\n├── participants.json\n├── participants.tsv\n├── sub-01\n│   ├── anat\n│   │   ├── sub-01_acq-CP_T1w.nii.gz\n│   │   ├── sub-01_acq-CP_T2starw.nii.gz\n│   │   ├── sub-01_acq-CoV_T1w.nii.gz\n│   │   ├── sub-01_acq-CoV_T2starw.nii.gz\n│   │   ├── sub-01_acq-SAReff_T2starw.nii.gz\n│   │   ├── sub-01_acq-patient_T2starw.nii.gz\n│   │   ├── sub-01_acq-phase_T2starw.nii.gz\n│   │   ├── sub-01_acq-target_T2starw.nii.gz\n│   │   ├── sub-01_acq-volume_T2starw.nii.gz\n│   └── fmap\n│   ├── sub-01_acq-anatCP_TB1TFL.nii.gz\n│   ├── sub-01_acq-anatCoV_TB1TFL.nii.gz\n│   ├── sub-01_acq-anatSAReff_TB1TFL.nii.gz\n│   ├── sub-01_acq-anatpatient_TB1TFL.nii.gz\n│   ├── sub-01_acq-anatphase_TB1TFL.nii.gz\n│   ├── sub-01_acq-anattarget_TB1TFL.nii.gz\n│   ├── sub-01_acq-anatvolume_TB1TFL.nii.gz\n│   ├── sub-01_acq-fampCP_TB1TFL.nii.gz\n│   ├── sub-01_acq-fampCoV_TB1TFL.nii.gz\n│   ├── sub-01_acq-fampSAReff_TB1TFL.nii.gz\n│   ├── sub-01_acq-famppatient_TB1TFL.nii.gz\n│   ├── sub-01_acq-fampphase_TB1TFL.nii.gz\n│   ├── sub-01_acq-famptarget_TB1TFL.nii.gz\n│   └── sub-01_acq-fampvolume_TB1TFL.nii.gz\n├── sub-02\n├── sub-03\n├── sub-04\n└── sub-05\n~~~\n```","metadata":{}},{"cell_type":"markdown","source":"# 2     |     Overview of processing pipeline\n\nDuring the data acquisition stage, RF shimming was done using the [Shimming Toolbox](https://github.com/shimming-toolbox/shimming-toolbox) {cite:p}`DAstous2023` during the acquisition stage.\n\nThe post-processing pipeline uses the [Spinal Cord Toolbox](https://spinalcordtoolbox.com) {cite:p}`DeLeener201724`.\n\nFor each subject:\n\n- Process anat/T2starw (GRE)\n - Segment the spinal cord (SC)\n - Label vertebral levels using existing manual disc labels\n - Create a mask of the cerebrospinal fluid (CSF)\n - Extract the SC/CSF magnitude signal to assess the stability of the flip angle across shim methods\n- Process fmap/TFL (flip angle maps)\n - Register each B1 map (CP, CoV, etc.) to the GRE scan\n - Apply the computed warping field to bring the segmentation and vertebral levels to the B1 map\n - Convert the B1 map to nT/V units\n - Extract the B1 map value within the SC\n\n>Slow processes are indicated with the emoji ⏳\n\n","metadata":{}},{"cell_type":"markdown","source":"# 3     |     Requirements\n\n* Install [Spinal Cord Toolbox](https://spinalcordtoolbox.com/user_section/installation.html) {cite:p}`DeLeener201724`, eg\n\n```shell\n# Install SCT ⏳\n!git clone --depth 1 https://github.com/spinalcordtoolbox/spinalcordtoolbox.git\n!yes | spinalcordtoolbox/install_sct\nos.environ['PATH'] += f\":/content/spinalcordtoolbox/bin\"\n```\n\n* Download the project repository\n\n```shell\ngit clone https://github.com/shimming-toolbox/rf-shimming-7t.git\ncd rf-shimming-7t\ngit checkout main\n```\n\n* Install requirements\n\n```shell\npip install -r binder/requirements.txt\n```\n\n* Download data\n\n```shell\ncd content\nrepo2data -r ../binder/data_requirement.json\n```\n","metadata":{}},{"cell_type":"markdown","source":"# 4     |     Environment setup\n\nIn a Python shell:","metadata":{}},{"cell_type":"markdown","source":"Import the necessary modules.","metadata":{}},{"cell_type":"code","source":"from datetime import datetime, timedelta\nimport os\nimport re\nimport json\nimport subprocess\nimport glob\nimport itertools\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport nibabel as nib\nimport pandas as pd\nfrom scipy.interpolate import interp1d\nfrom scipy.ndimage import uniform_filter1d\nfrom scipy.stats import f_oneway, ttest_rel\nimport shutil\nfrom shutil import rmtree\nfrom pathlib import Path\nfrom statsmodels.stats.multicomp import pairwise_tukeyhsd\nfrom statsmodels.stats.anova import AnovaRM\nimport statsmodels.api as sm","metadata":{"tags":["remove_output","hide_input"],"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Start timer\nstart_time = datetime.now()\n\n# Check where we are\n!pwd","metadata":{"tags":["remove_input","remove_output"],"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Google Colab-Only\n# ⚠️ No need to run this cell if you run this notebook locally and already have these dependencies installed.\n\nif notebook == 'colab':\n # Install packages on system\n !sudo apt-get update\n !sudo apt-get install git-annex\n\n # Install Python libaries\n !wget -O requirements.txt https://raw.githubusercontent.com/shimming-toolbox/rf-shimming-7t/main/binder/requirements.txt\n !pip install -r requirements.txt\n\n # Install SCT ⏳\n !git clone --depth 1 https://github.com/spinalcordtoolbox/spinalcordtoolbox.git\n !yes | spinalcordtoolbox/install_sct\n os.environ['PATH'] += f\":/content/spinalcordtoolbox/bin\"","metadata":{"tags":["remove_input","remove_output"],"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Download data and define path variables\n\n# Google Colab-Only\nif notebook=='colab':\n !datalad install https://github.com/OpenNeuroDatasets/ds004906.git\n os.chdir(\"ds004906\")\n !datalad get . # uncomment for production\n # !datalad get sub-01/ # debugging\n # Get derivatives containing manual labels\n !datalad get derivatives\n\nif notebook!='colab':\n # Switch working directory to $HOME/data/rf-shimming-7t/ds004906\n os.chdir(os.path.join(os.getenv('HOME'),\"data/rf-shimming-7t/ds004906\"))\n if notebook=='neurolibre-clean':\n flist = open('cleanup.txt', 'r')\n for f in flist:\n fname = f.rstrip('\\n')\n fname = Path(os.getcwd()) / Path(fname)\n\n # or, if you get rid of os.chdir(path) above,\n # fname = os.path.join(path, f.rstrip())\n if os.path.isfile(fname): # this makes the code more robust\n print('Removing file: ' + str(fname))\n os.remove(fname)\n elif os.path.isdir(fname):\n print('Removing directory: ' + str(fname))\n rmtree(fname)\n\n # also, don't forget to close the text file:\n flist.close()\n","metadata":{"tags":["remove_input","remove_output"],"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Define variables.","metadata":{}},{"cell_type":"code","source":"# Assumes data/rf-shimming-7t is found at $HOME\npath_data = os.path.join(os.getenv('HOME'),\"data/rf-shimming-7t/ds004906\")\nprint(f\"path_data: {path_data}\")\npath_labels = os.path.join(path_data, \"derivatives\", \"labels\")\npath_qc = os.path.join(path_data, \"qc\")\nshim_modes = [\"CP\", \"patient\", \"volume\", \"phase\", \"CoV\", \"target\", \"SAReff\"]\nshim_modes_MPRAGE = [\"CP\", \"CoV\"] \n\nprint(f\"shim_modes: {shim_modes}\")\nsubjects = sorted(glob.glob(\"sub-*\"))\nprint(f\"subjects: {subjects}\")\n\n# Create output folder\npath_results = os.path.join(os.getenv('HOME'),'results')\nderivative_results = os.path.join(path_data, 'derivatives', 'results')\nos.makedirs(path_results, exist_ok=True)\n\n","metadata":{"tags":["hide_input","remove_output"],"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# 5     |     Process anat/T2starw (GRE)\n","metadata":{}},{"cell_type":"markdown","source":"Run segmentation on GRE scan\n","metadata":{}},{"cell_type":"code","source":"\nif notebook != 'neurolibre-figures':\n for subject in subjects:\n os.chdir(os.path.join(path_data, subject, \"anat\"))\n # The \"CoV\" RF shimming scenario was chosen as the segmentation baseline due to the more homogeneous signal intensity in the I-->S direction, which results in a better segmentation peformance in the C7-T2 region\n fname_manual_seg = os.path.join(path_labels, subject, \"anat\", f\"{subject}_acq-CoV_T2starw_label-SC_seg.nii.gz\")\n if os.path.exists(fname_manual_seg):\n # Manual segmentation already exists. Copy it to local folder\n print(f\"{subject}: Manual segmentation found\\n\")\n shutil.copyfile(fname_manual_seg, f\"{subject}_acq-CoV_T2starw_seg.nii.gz\")\n # Generate QC report to make sure the manual segmentation is correct\n !sct_qc -i {subject}_acq-CoV_T2starw.nii.gz -s {subject}_acq-CoV_T2starw_seg.nii.gz -p sct_deepseg_sc -qc {path_qc} -qc-subject {subject}\n else:\n # Manual segmentation does not exist. Run automatic segmentation.\n print(f\"{subject}: Manual segmentation not found\")\n !sct_deepseg_sc -i {subject}_acq-CoV_T2starw.nii.gz -c t2s -qc {path_qc}","metadata":{"tags":["hide_input"],"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Copy CSF masks from the derivatives folder.\n\nFor more details about how these masks were created, see: [https://github.com/shimming-toolbox/rf-shimming-7t/issues/67](https://github.com/shimming-toolbox/rf-shimming-7t/issues/67).","metadata":{}},{"cell_type":"code","source":"if notebook != 'neurolibre-figures':\n for subject in subjects:\n os.chdir(os.path.join(path_data, subject, \"anat\"))\n fname_manual_seg = os.path.join(path_labels, subject, \"anat\", f\"{subject}_acq-CoV_T2starw_label-CSF_seg.nii.gz\")\n if os.path.exists(fname_manual_seg):\n # Manual segmentation already exists. Copy it to local folder\n print(f\"{subject}: Manual segmentation found\\n\")\n shutil.copyfile(fname_manual_seg, f\"{subject}_acq-CoV_T2starw_label-CSF_seg.nii.gz\")\n # Generate QC report to make sure the manual segmentation is correct\n !sct_qc -i {subject}_acq-CoV_T2starw.nii.gz -s {subject}_acq-CoV_T2starw_label-CSF_seg.nii.gz -p sct_deepseg_sc -qc {path_qc} -qc-subject {subject}\n else:\n # Manual segmentation does not exist. Panic!\n print(f\"{subject}: Manual segmentation not found 😱\")","metadata":{"tags":["hide_input"],"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Crop GRE scan for faster processing and better registration results\n","metadata":{}},{"cell_type":"code","source":"if notebook != 'neurolibre-figures':\n for subject in subjects:\n os.chdir(os.path.join(path_data, subject, \"anat\"))\n !sct_crop_image -i {subject}_acq-CoV_T2starw.nii.gz -m {subject}_acq-CoV_T2starw_seg.nii.gz -dilate 20x20x0 -o {subject}_acq-CoV_T2starw_crop.nii.gz\n !sct_crop_image -i {subject}_acq-CoV_T2starw_seg.nii.gz -m {subject}_acq-CoV_T2starw_seg.nii.gz -dilate 20x20x0 -o {subject}_acq-CoV_T2starw_crop_seg.nii.gz\n !sct_crop_image -i {subject}_acq-CoV_T2starw_label-CSF_seg.nii.gz -m {subject}_acq-CoV_T2starw_seg.nii.gz -dilate 20x20x0 -o {subject}_acq-CoV_T2starw_crop_label-CSF_seg.nii.gz","metadata":{"tags":["hide_input"],"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Label vertebrae on GRE scan","metadata":{}},{"cell_type":"code","source":"\n# Given the low resolution of the GRE scan, the automatic detection of C2-C3 disc is unreliable. Therefore we need to use the manual disc labels that are part of the dataset.\nif notebook != 'neurolibre-figures':\n for subject in subjects:\n os.chdir(os.path.join(path_data, subject, \"anat\"))\n fname_label_discs = os.path.join(path_data, \"derivatives\", \"labels\", subject, \"anat\", f\"{subject}_acq-CoV_T2starw_label-discs_dseg.nii.gz\")\n !sct_label_utils -i {subject}_acq-CoV_T2starw_crop_seg.nii.gz -disc {fname_label_discs} -o {subject}_acq-CoV_T2starw_crop_seg_labeled.nii.gz\n # Generate QC report to assess labeled segmentation\n !sct_qc -i {subject}_acq-CoV_T2starw_crop.nii.gz -s {subject}_acq-CoV_T2starw_crop_seg_labeled.nii.gz -p sct_label_vertebrae -qc {path_qc} -qc-subject {subject}","metadata":{"tags":["hide_input"],"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Register *_T2starw to CoV_T2starw ⏳","metadata":{}},{"cell_type":"code","source":"if notebook != 'neurolibre-figures':\n for subject in subjects:\n os.chdir(os.path.join(path_data, subject, \"anat\"))\n for shim_mode in shim_modes:\n # Don't do it for CoV_T2starw\n if shim_mode != 'CoV':\n !sct_register_multimodal -i {subject}_acq-{shim_mode}_T2starw.nii.gz -d {subject}_acq-CoV_T2starw_crop.nii.gz -dseg {subject}_acq-CoV_T2starw_crop_seg.nii.gz -param step=1,type=im,algo=dl -qc {path_qc}","metadata":{"tags":["hide_input"],"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Extract the signal intensity on the GRE scan within the spinal cord between levels C3 and T2 (included), which correspond to the region where RF shimming was prescribed.","metadata":{}},{"cell_type":"code","source":"\nif notebook != 'neurolibre-figures':\n for subject in subjects:\n os.chdir(os.path.join(path_data, subject, \"anat\"))\n for shim_mode in shim_modes:\n # Shim methods are registered to the CoV T2starw scan, so we need to use the added suffix to identify them\n if shim_mode == 'CoV':\n file_suffix = 'crop'\n else:\n file_suffix = 'reg'\n fname_result_sc = os.path.join(derivative_results, f\"{subject}_acq-{shim_mode}_T2starw_label-SC.csv\")\n !sct_extract_metric -i {subject}_acq-{shim_mode}_T2starw_{file_suffix}.nii.gz -f {subject}_acq-CoV_T2starw_crop_seg.nii.gz -method wa -vert 3:9 -vertfile {subject}_acq-CoV_T2starw_crop_seg_labeled.nii.gz -perslice 1 -o {fname_result_sc}\n fname_result_csf = os.path.join(derivative_results, f\"{subject}_acq-{shim_mode}_T2starw_label-CSF.csv\")\n !sct_extract_metric -i {subject}_acq-{shim_mode}_T2starw_{file_suffix}.nii.gz -f {subject}_acq-CoV_T2starw_crop_label-CSF_seg.nii.gz -method wa -vert 3:9 -vertfile {subject}_acq-CoV_T2starw_crop_seg_labeled.nii.gz -perslice 1 -o {fname_result_csf}","metadata":{"tags":["hide_input"],"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Prepare data for figure of CSF/SC signal ratio from T2starw scan","metadata":{}},{"cell_type":"code","source":"%matplotlib inline\n\n# Go back to root data folder\nos.chdir(path_data)\n\ndef smooth_data(data, window_size=50):\n \"\"\" Apply a simple moving average to smooth the data. \"\"\"\n return uniform_filter1d(data, size=window_size, mode='nearest')\n\n# Fixed grid for x-axis\nx_grid = np.linspace(0, 1, 100)\n\n# z-slices corresponding to levels C3 to T2 on the PAM50 template. These will be used to scale the x-label of each subject.\noriginal_vector = np.array([907, 870, 833, 800, 769, 735, 692, 646])\n\n# Normalize the PAM50 z-slice numbers to the 1-0 range (to show inferior-superior instead of superior-inferior)\nmin_val = original_vector.min()\nmax_val = original_vector.max()\nnormalized_vector = 1 - ((original_vector - min_val) / (max_val - min_val))\n\n# Use this normalized vector as x-ticks\ncustom_xticks = normalized_vector\n\n# Vertebral level labels\nvertebral_levels = [\"C3\", \"C4\", \"C5\", \"C6\", \"C7\", \"T1\", \"T2\"]\nlabel_positions = normalized_vector[:-1] + np.diff(normalized_vector) / 2\n\n# Number of subjects determines the number of rows in the subplot\nn_rows = len(subjects)\n\n# Data storage for statistics\ndata_stats = []\n\n# Data storage for Plotly\nt2_data_plotly = {}\n\n# Iterate over each subject and create a subplot\nfor i, subject in enumerate(subjects):\n\n t2_data_plotly[subject]={}\n for shim_mode in shim_modes:\n # Initialize list to collect data for this shim method\n method_data = []\n\n # Get signal in SC\n file_csv = os.path.join(derivative_results, f\"{subject}_acq-{shim_mode}_T2starw_label-SC.csv\")\n df = pd.read_csv(file_csv)\n data_sc = df['WA()']\n data_sc_smoothed = smooth_data(data_sc)\n\n # Get signal in CSF\n file_csv = os.path.join(derivative_results, f\"{subject}_acq-{shim_mode}_T2starw_label-CSF.csv\")\n df = pd.read_csv(file_csv)\n data_csf = df['WA()']\n data_csf_smoothed = smooth_data(data_csf)\n \n # Compute ratio\n data_sc_csf_ratio = data_csf_smoothed / data_sc_smoothed\n\n # Normalize the x-axis to a 1-0 scale for each subject (to go from superior-inferior direction)\n x_subject = np.linspace(1, 0, len(data_sc_csf_ratio))\n\n # Interpolate to the fixed grid\n interp_func = interp1d(x_subject, data_sc_csf_ratio, kind='linear', bounds_error=False, fill_value='extrapolate')\n resampled_data = interp_func(x_grid)\n\n method_data.append(resampled_data)\n\n # If there's data for this shim method, plot it and compute stats\n if method_data:\n t2_data_plotly[subject][shim_mode]=method_data\n \n # Compute stats on the non-resampled data (to avoid interpolation errors)\n mean_data = np.mean(data_sc_csf_ratio)\n sd_data = np.std(data_sc_csf_ratio)\n data_stats.append([subject, shim_mode, mean_data, sd_data])\n else:\n t2_data_plotly[subject][shim_mode]=None\n\n\n\n","metadata":{"tags":["hide_input","remove_output"],"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Perform statistics","metadata":{}},{"cell_type":"code","source":"# Perform statistics\n\n# Convert to DataFrame and save to CSV\ndf_stats = pd.DataFrame(data_stats, columns=['Subject', 'Shim_Mode', 'Average', 'Standard_Deviation'])\ndf_stats.to_csv(os.path.join(path_results, 'stats_t2starw.csv'), index=False)\n\n# Compute statistics across subjects\ngrouped_means = df_stats.groupby('Shim_Mode').agg({'Average': ['mean', 'std'], 'Standard_Deviation': ['mean', 'std']})\n\n# Format mean ± standard deviation\ngrouped_means['Average_formatted'] = grouped_means['Average']['mean'].map(\"{:.2f}\".format) + \" ± \" + grouped_means['Average']['std'].map(\"{:.2f}\".format)\ngrouped_means['Standard_Deviation_formatted'] = grouped_means['Standard_Deviation']['mean'].map(\"{:.2f}\".format) + \" ± \" + grouped_means['Standard_Deviation']['std'].map(\"{:.2f}\".format)\n\n# Drop multi-level index and only keep formatted columns\ngrouped_means = grouped_means.drop(columns=['Average', 'Standard_Deviation'])\ngrouped_means.columns = ['Average', 'Standard_Deviation'] # Rename columns for clarity\ngrouped_means.reset_index().to_csv(os.path.join(path_results, 'stats_t2starw_average_across_subjects.csv'), index=False)\n\n# Perform Repeated Measures ANOVA\naovrm = AnovaRM(df_stats, 'Average', 'Subject', within=['Shim_Mode'])\nres = aovrm.fit()\nprint(res.summary())\n\n# Perform Post Hoc paired t-tests\n\n# Filter out subjects that don't have all shim modes\nshim_modes = df_stats['Shim_Mode'].unique()\nvalid_subjects = df_stats.groupby('Subject').filter(lambda x: all(mode in x['Shim_Mode'].values for mode in shim_modes))\n\n# Pairwise comparisons\npairs = list(itertools.combinations(shim_modes, 2))\np_values = []\n\nfor pair in pairs:\n data1 = valid_subjects[valid_subjects['Shim_Mode'] == pair[0]]['Average']\n data2 = valid_subjects[valid_subjects['Shim_Mode'] == pair[1]]['Average']\n _, p_value = ttest_rel(data1, data2)\n p_values.append(p_value)\n\n# Function for Benjamini-Hochberg FDR correction\ndef benjamini_hochberg(p_values):\n n = len(p_values)\n sorted_p_values = np.sort(p_values)\n sorted_index = np.argsort(p_values)\n adjusted_p_values = np.zeros(n)\n\n for i, p in enumerate(sorted_p_values):\n adjusted_p_values[sorted_index[i]] = min(p * n / (i + 1), 1)\n\n return adjusted_p_values\n\n# Applying Benjamini-Hochberg FDR correction\nadjusted_p_values = benjamini_hochberg(p_values)\n\n# Creating a summary DataFrame\ncomparison_results = pd.DataFrame({'Pair': pairs, 'P-Value': p_values, 'Adjusted P-Value': adjusted_p_values})\n\n# Save to CSV\ncomparison_results.to_csv(os.path.join(path_results, 'stats_t2starw_paired_ttest.csv'), index=False)\n\nprint(f\"Paired t-tests:\\n{comparison_results}\")","metadata":{"tags":["hide_input","report_output"],"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# 6     |     Process fmap/TFL (flip angle maps)\n","metadata":{}},{"cell_type":"markdown","source":"Register TFL flip angle maps to the GRE scan ⏳\n","metadata":{}},{"cell_type":"code","source":"if notebook != 'neurolibre-figures':\n for subject in subjects:\n os.chdir(os.path.join(path_data, subject, \"fmap\"))\n for shim_mode in shim_modes:\n !sct_register_multimodal -i {subject}_acq-anat{shim_mode}_TB1TFL.nii.gz -d ../anat/{subject}_acq-CoV_T2starw_crop.nii.gz -dseg ../anat/{subject}_acq-CoV_T2starw_crop_seg.nii.gz -param step=1,type=im,algo=slicereg,metric=CC -qc {path_qc}","metadata":{"tags":["hide_input"],"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Warping spinal cord segmentation and vertebral level to each flip angle map","metadata":{}},{"cell_type":"code","source":"if notebook != 'neurolibre-figures':\n for subject in subjects:\n os.chdir(os.path.join(path_data, subject, \"fmap\"))\n for shim_mode in shim_modes:\n # Use linear interpolation to preserve partial volume information (needed when extracting values along the cord)\n !sct_apply_transfo -i ../anat/{subject}_acq-CoV_T2starw_crop_seg.nii.gz -d {subject}_acq-anat{shim_mode}_TB1TFL.nii.gz -w warp_{subject}_acq-CoV_T2starw_crop2{subject}_acq-anat{shim_mode}_TB1TFL.nii.gz -x linear -o {subject}_acq-anat{shim_mode}_TB1TFL_seg.nii.gz\n # Use nearest neighbour (nn) interpolation because we are dealing with non-binary discrete labels\n !sct_apply_transfo -i ../anat/{subject}_acq-CoV_T2starw_crop_seg_labeled.nii.gz -d {subject}_acq-anat{shim_mode}_TB1TFL.nii.gz -w warp_{subject}_acq-CoV_T2starw_crop2{subject}_acq-anat{shim_mode}_TB1TFL.nii.gz -x nn -o {subject}_acq-anat{shim_mode}_TB1TFL_seg_labeled.nii.gz","metadata":{"tags":["hide_input"],"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Convert the flip angle maps to B1+ efficiency maps [nT/V] (inspired by code from Kyle Gilbert).\n\nThe approach consists in calculating the B1+ efficiency using a 1ms, pi-pulse at the acquisition voltage, then scale the efficiency by the ratio of the measured flip angle to the requested flip angle in the pulse sequence.\n","metadata":{}},{"cell_type":"code","source":"\nGAMMA = 2.675e8; # [rad / (s T)]\nrequested_fa = 90 # saturation flip angle -- hard-coded in sequence\n\nif notebook != 'neurolibre-figures':\n for subject in subjects:\n os.chdir(os.path.join(path_data, subject, \"fmap\"))\n for shim_mode in shim_modes:\n # Fetch the reference voltage from the JSON sidecar to the TFL B1map sequence\n with open(f\"{subject}_acq-famp{shim_mode}_TB1TFL.json\", \"r\") as f:\n metadata = json.load(f)\n ref_voltage = metadata.get(\"TxRefAmp\", \"N/A\")\n print(f\"ref_voltage [V]: {ref_voltage} ({subject}_acq-famp{shim_mode}_TB1TFL)\")\n\n # Open flip angle map with nibabel\n nii = nib.load(f\"{subject}_acq-famp{shim_mode}_TB1TFL.nii.gz\")\n acquired_fa = nii.get_fdata()\n\n # Siemens maps are in units of flip angle * 10 (in degrees)\n acquired_fa = acquired_fa / 10\n\n # Account for the power loss between the coil and the socket. That number was given by Siemens.\n voltage_at_socket = ref_voltage * 10 ** -0.095\n\n # Compute B1 map in [T/V]\n b1_map = (acquired_fa / requested_fa) * (np.pi / (GAMMA * 1e-3 * voltage_at_socket))\n\n # Convert to [nT/V]\n b1_map = b1_map * 1e9\n\n # Save as NIfTI file\n nii_b1 = nib.Nifti1Image(b1_map, nii.affine, nii.header)\n nib.save(nii_b1, f\"{subject}_acq-{shim_mode}_TB1map.nii.gz\")","metadata":{"tags":["hide_input"],"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Extract B1+ value along the spinal cord between levels C3 and T2 (included)\n","metadata":{}},{"cell_type":"code","source":"if notebook != 'neurolibre-figures':\n for subject in subjects:\n os.chdir(os.path.join(path_data, subject, \"fmap\"))\n for shim_mode in shim_modes:\n fname_result_b1plus = os.path.join(derivative_results, f\"{subject}_TB1map_{shim_mode}.csv\")\n !sct_extract_metric -i {subject}_acq-{shim_mode}_TB1map.nii.gz -f {subject}_acq-anat{shim_mode}_TB1TFL_seg.nii.gz -method wa -vert 3:9 -vertfile {subject}_acq-anat{shim_mode}_TB1TFL_seg_labeled.nii.gz -perslice 1 -o {fname_result_b1plus}","metadata":{"tags":["hide_input"],"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Prepare data for figure of B1+ values along the spinal cord across shim methods","metadata":{}},{"cell_type":"code","source":"# Go back to root data folder\nos.chdir(os.path.join(path_data))\n\ndef smooth_data(data, window_size=20):\n \"\"\" Apply a simple moving average to smooth the data. \"\"\"\n return uniform_filter1d(data, size=window_size, mode='nearest')\n\n# Fixed grid for x-axis\nx_grid = np.linspace(0, 1, 100)\n\n# z-slices corresponding to levels C3 to T2 on the PAM50 template. These will be used to scale the x-label of each subject.\noriginal_vector = np.array([907, 870, 833, 800, 769, 735, 692, 646])\n\n# Normalize the PAM50 z-slice numbers to the 1-0 range (to show inferior-superior instead of superior-inferior)\nmin_val = original_vector.min()\nmax_val = original_vector.max()\nnormalized_vector = 1 - ((original_vector - min_val) / (max_val - min_val))\n\n# Use this normalized vector as x-ticks\ncustom_xticks = normalized_vector\n\n# Vertebral level labels\nvertebral_levels = [\"C3\", \"C4\", \"C5\", \"C6\", \"C7\", \"T1\", \"T2\"]\n# Calculate midpoints for label positions\nlabel_positions = normalized_vector[:-1] + np.diff(normalized_vector) / 2\n\n# Number of subjects determines the number of rows in the subplot\nn_rows = len(subjects)\n\n# Data storage for statistics\ndata_stats = []\n\n# Data storage for Plotly\nb1_data_plotly = {}\n\n# Iterate over each subject and create a subplot\nfor i, subject in enumerate(subjects):\n \n os.chdir(os.path.join(path_data, subject, \"fmap\"))\n b1_data_plotly[subject]={}\n \n for shim_mode in shim_modes:\n # Initialize list to collect data for this shim method\n method_data = []\n\n file_csv = os.path.join(derivative_results, f\"{subject}_TB1map_{shim_mode}.csv\")\n df = pd.read_csv(file_csv)\n wa_data = df['WA()']\n\n # Normalize the x-axis to a 1-0 scale for each subject (to go from superior-inferior direction)\n x_subject = np.linspace(1, 0, len(wa_data))\n\n # Interpolate to the fixed grid\n interp_func = interp1d(x_subject, wa_data, kind='linear', bounds_error=False, fill_value='extrapolate')\n resampled_data = interp_func(x_grid)\n\n # Apply smoothing\n smoothed_data = smooth_data(resampled_data)\n\n method_data.append(smoothed_data)\n\n # If there's data for this shim method, plot it\n if method_data:\n # Plotting each file's data separately\n for resampled_data in method_data:\n b1_data_plotly[subject][shim_mode]=resampled_data\n\n # Compute stats on the non-resampled data (to avoid interpolation errors)\n mean_data = np.mean(wa_data)\n sd_data = np.std(wa_data)\n data_stats.append([subject, shim_mode, mean_data, sd_data])\n else:\n b1_data_plotly[subject][shim_mode]=None\n\n\n","metadata":{"tags":["hide_input","remove_output"],"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Perform statistics","metadata":{}},{"cell_type":"code","source":"# Convert to DataFrame and save to CSV\ndf_stats = pd.DataFrame(data_stats, columns=['Subject', 'Shim_Mode', 'Average', 'Standard_Deviation'])\ndf_stats.to_csv(os.path.join(path_results, 'stats_b1plus.csv'), index=False)\n\n# Compute statistics across subjects\ngrouped_means = df_stats.groupby('Shim_Mode').agg({'Average': ['mean', 'std'], 'Standard_Deviation': ['mean', 'std']})\n\n# Format mean ± standard deviation\ngrouped_means['Average_formatted'] = grouped_means['Average']['mean'].map(\"{:.2f}\".format) + \" ± \" + grouped_means['Average']['std'].map(\"{:.2f}\".format)\ngrouped_means['Standard_Deviation_formatted'] = grouped_means['Standard_Deviation']['mean'].map(\"{:.2f}\".format) + \" ± \" + grouped_means['Standard_Deviation']['std'].map(\"{:.2f}\".format)\n\n# Drop multi-level index and only keep formatted columns\ngrouped_means = grouped_means.drop(columns=['Average', 'Standard_Deviation'])\ngrouped_means.columns = ['Average', 'Standard_Deviation'] # Rename columns for clarity\ngrouped_means.reset_index().to_csv(os.path.join(path_results, 'stats_b1plus_average_across_subjects.csv'), index=False)\n\n# Perform Repeated Measures ANOVA\naovrm = AnovaRM(df_stats, 'Average', 'Subject', within=['Shim_Mode'])\nres = aovrm.fit()\nprint(res.summary())\n\n# Perform Post Hoc paired t-tests\n\n# Filter out subjects that don't have all shim modes\nshim_modes = df_stats['Shim_Mode'].unique()\nvalid_subjects = df_stats.groupby('Subject').filter(lambda x: all(mode in x['Shim_Mode'].values for mode in shim_modes))\n\n# Pairwise comparisons\npairs = list(itertools.combinations(shim_modes, 2))\np_values = []\n\nfor pair in pairs:\n data1 = valid_subjects[valid_subjects['Shim_Mode'] == pair[0]]['Average']\n data2 = valid_subjects[valid_subjects['Shim_Mode'] == pair[1]]['Average']\n _, p_value = ttest_rel(data1, data2)\n p_values.append(p_value)\n\n# Function for Benjamini-Hochberg FDR correction\ndef benjamini_hochberg(p_values):\n n = len(p_values)\n sorted_p_values = np.sort(p_values)\n sorted_index = np.argsort(p_values)\n adjusted_p_values = np.zeros(n)\n\n for i, p in enumerate(sorted_p_values):\n adjusted_p_values[sorted_index[i]] = min(p * n / (i + 1), 1)\n\n return adjusted_p_values\n\n# Applying Benjamini-Hochberg FDR correction\nadjusted_p_values = benjamini_hochberg(p_values)\n\n# Creating a summary DataFrame\ncomparison_results = pd.DataFrame({'Pair': pairs, 'P-Value': p_values, 'Adjusted P-Value': adjusted_p_values})\n\n# Save to CSV\ncomparison_results.to_csv(os.path.join(path_results, 'stats_b1plus_paired_ttest.csv'), index=False)\n\nprint(f\"Paired t-tests:\\n{comparison_results}\")","metadata":{"tags":["hide_input","report_output"],"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# 7     |     Paper figures","metadata":{}},{"cell_type":"markdown","source":"Prepare RF shimming mask for figure\n","metadata":{}},{"cell_type":"code","source":"# Select subject to show\nsubject = 'sub-05'\nos.chdir(os.path.join(path_data, subject, \"fmap\"))\n\n# Create RF shimming mask from the segmentation (re-create the live RF shimming procedure)\nfile_mask = f\"{subject}_acq-anatCP_TB1TFL_mask-shimming.nii.gz\"\nif notebook != 'neurolibre-figures':\n\n !sct_maths -i {subject}_acq-anatCP_TB1TFL_seg_labeled.nii.gz -thr 3 -uthr 9 -o {file_mask}\n !sct_maths -i {file_mask} -bin 1 -o {file_mask}\n !sct_create_mask -i {subject}_acq-anatCP_TB1TFL.nii.gz -p centerline,{file_mask} -size 28mm -f cylinder -o {file_mask}\n\n # Dilate mask\n !sct_maths -i {file_mask} -dilate 2 -shape disk -dim 2 -o {subject}_acq-anatCP_TB1TFL_mask-shimming_dil.nii.gz\n # Subtract dilated mask with mask to get the edge\n !sct_maths -i {subject}_acq-anatCP_TB1TFL_mask-shimming_dil.nii.gz -sub {file_mask} -o {file_mask}","metadata":{"tags":["hide_input"],"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Create figure of B1+ maps","metadata":{}},{"cell_type":"code","source":"%matplotlib inline\n# Select subject to show\nsubject = 'sub-05'\nos.chdir(os.path.join(path_data, subject, \"fmap\"))\nfile_mask = f\"{subject}_acq-anatCP_TB1TFL_mask-shimming.nii.gz\"\n\n# Load the statistics from the CSV file\nstats_df = pd.read_csv(os.path.join(path_results, 'stats_b1plus.csv'))\n\n# Filter for the specific subject\nsubject_stats = stats_df[stats_df['Subject'] == subject]\n\n# Defining crop limits for resulting figure\nxmin = 20\nxmax = 110\nymin = 20\nymax = 150\n\n# Defining dynamic range\ndynmin = 5 \ndynmax = 30\n\n# Create a figure with multiple subplots\nfig, axes = plt.subplots(2, 4, figsize=(8, 6))\nfont_size = 14\naxes=axes.flatten()\n\n# First, plot the anatomical image with an overlay of the mask\n\n# Load data\nCP_anat=nib.load(f\"{subject}_acq-anatCP_TB1TFL.nii.gz\")\nCP_SC=nib.load(file_mask)\nCP_nTpV=nib.load(f\"{subject}_acq-CP_TB1map.nii.gz\")\n\n# Defining mask based on the magnitude image intensity threshold\ncslice=CP_anat.shape[2] // 2 -2 #shows the SC seg best\nthreshold=300\nmask=CP_anat.get_fdata()[xmin:xmax,ymin:ymax, cslice]\nmask=np.where(mask > threshold, 1, 0)\n\n# Cropping anat, SC, B1+\nCP_anat=CP_anat.get_fdata()[xmin:xmax,ymin:ymax, cslice]\nCP_anat=CP_anat*mask\nCP_SC=CP_SC.get_fdata()[xmin:xmax,ymin:ymax, cslice]\nCP_nTpV=CP_nTpV.get_fdata()[xmin:xmax,ymin:ymax, cslice]\nCP_nTpV=CP_nTpV*mask\n\n# All opacity overalys look ugly: workaround, set the anat slice to a max value where the segmentation exists\nCP_anat[CP_SC>0.5]=4000;\n\n# Plotting anat overlayed with SC\nsplot=axes[0]\nsplot.imshow((CP_anat.T), cmap='gray', origin='lower',vmin=0,vmax=2000)#, interpolation='spline36')\nsplot.set_title('Anat', size=font_size)\nsplot.axis('off')\n\n# Then, plot each B1+ map, with an overlay of the mean and CV inside the cord\nfor i,shim_mode in enumerate(shim_modes):\n # Load data\n B1map=nib.load(f\"{subject}_acq-{shim_mode}_TB1map.nii.gz\")\n B1map=B1map.get_fdata()[xmin:xmax,ymin:ymax, cslice]\n B1map=B1map*mask\n\n # Plot\n splot=axes[i+1]\n im = splot.imshow((B1map.T), cmap='viridis', origin='lower',vmin=dynmin,vmax=dynmax)#, interpolation='spline36')\n splot.set_title(shim_mode, size=font_size)\n splot.axis('off')\n\n # Find the statistics for the current shim mode\n shim_stats = subject_stats[subject_stats['Shim_Mode'] == shim_mode]\n if not shim_stats.empty:\n mean_val = shim_stats.iloc[0]['Average']\n std_val = shim_stats.iloc[0]['Standard_Deviation']\n cv = std_val / mean_val * 100 # Coefficient of variation in percentage\n annotation_text = f\"{mean_val:.2f} nT/V\\n{cv:.2f}%\"\n splot.annotate(annotation_text, (0.05, 0.95), xycoords='axes fraction', \n fontsize=10, color='white', \n verticalalignment='top', horizontalalignment='left')\n\nplt.tight_layout()\nplt.subplots_adjust(wspace=0.1, hspace=0, right=0.9)\n\n# Colorbar\n# Assume that the colorbar should start at the bottom of the lower row of subplots and\n# extend to the top of the upper row of subplots\ncbar_bottom = 0.06 # This might need adjustment\ncbar_height = 0.83 # This represents the total height of both rows of subplots\ncbar_ax = fig.add_axes([0.93, cbar_bottom, 0.03, cbar_height])\ncbar = plt.colorbar(im, cax=cbar_ax)\n\n# cbar_ax = fig.add_axes([0.95, 0.5, 0.04, 0.4])\n# cbar = plt.colorbar(im, cax=cbar_ax)\ncbar_ax.set_title('nT/V', size=12)\nplt.show()\n","metadata":{"tags":["hide_input","report_output"],"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Figure 2. B1+ efficiency for one participant (sub-05) across all seven RF shimming conditions. The top left panel shows the tfl_b1map magnitude image with an overlay of the mask that was used to perform RF shimming. Text inserts show the mean (in nT/V) and CoV (in %) of B1+ efficiency along the spinal cord between C3 and T2. ","metadata":{}},{"cell_type":"code","source":"%matplotlib inline\n# PYTHON CODE\n# Module imports\n\n# Base python\nimport os\nfrom os import path\nfrom pathlib import Path\n\n# Graphical\n\nimport plotly.graph_objs as go\nfrom IPython.display import display, HTML\nfrom plotly import __version__\nfrom plotly.offline import init_notebook_mode, iplot, plot\nconfig={\n 'showLink': False,\n 'displayModeBar': True,\n 'toImageButtonOptions': {\n 'format': 'png', # one of png, svg, jpeg, webp\n 'filename': 'custom_image',\n 'height': 2500,\n 'width': 1250,\n 'scale': 2 # Multiply title/legend/axis/canvas sizes by this factor\n }\n }\n\ninit_notebook_mode(connected=True)\n\nfrom plotly.subplots import make_subplots\nimport plotly.graph_objects as go\n\nimport seaborn as sns\n\n# Set the color palette\npal=sns.color_palette()\n\n# Imports\nimport warnings\nwarnings.filterwarnings(\"ignore\")\n\n## Setup for plots\nfig = make_subplots(rows=5, cols=2, vertical_spacing = 0.025,\n subplot_titles=(\n 'sub-01',\n 'sub-01',\n 'sub-02',\n 'sub-02',\n 'sub-03',\n 'sub-03',\n 'sub-04',\n 'sub-04',\n 'sub-05',\n 'sub-05',))\n\nt2_datasets={}\nb1_datasets={}\n\nt2_data = []\nb1_data = []\n\nlegend_bool = True\nfor subject in subjects:\n index = 0\n t2_datasets[subject]={}\n b1_datasets[subject]={}\n\n for shim_mode in shim_modes:\n t2_datasets[subject][shim_mode]={}\n b1_datasets[subject][shim_mode]={}\n\n t2_data=go.Line(\n x=x_grid,\n y=t2_data_plotly[subject][shim_mode][0],\n name=shim_mode,\n legendgroup=shim_mode,\n line=dict(color='rgb'+str(pal[index]), width=3),\n showlegend=False\n )\n\n b1_data=go.Line(\n x=x_grid,\n y=b1_data_plotly[subject][shim_mode],\n name=shim_mode,\n legendgroup=shim_mode,\n line=dict(color='rgb'+str(pal[index]), width=3),\n showlegend=legend_bool\n ) \n\n \n t2_datasets[subject][shim_mode]=t2_data\n b1_datasets[subject][shim_mode]=b1_data\n\n index += 1\n legend_bool=False\n \n\nindex = 1\n# For z-ordering \nfor subject in subjects:\n for shim_mode in shim_modes:\n fig.add_trace(\n t2_datasets[subject][shim_mode],\n row=index, col=1\n )\n fig.add_trace(\n b1_datasets[subject][shim_mode],\n row=index, col=2\n )\n index+=1\n\n\nindex = 1\nfor subject in subjects:\n if index == 5:\n x_title = 'Vertebral Levels'\n showticklabels=True\n else:\n x_title = None\n showticklabels=False\n\n fig.update_xaxes(\n type=\"linear\",\n autorange=True,\n title=x_title,\n showgrid=True,\n gridcolor='rgb(169,169,169)',\n tickvals=label_positions,\n ticktext=vertebral_levels,\n showticklabels=showticklabels,\n title_font_family=\"Times New Roman\",\n title_font_size = 20,\n linecolor='black',\n linewidth=2,\n tickfont=dict(\n family='Times New Roman',\n size=16,\n ),\n row=index, col=1\n )\n if index == 1:\n fig.update_yaxes(\n type=\"linear\",\n title={\n 'text':'CSF/Cord T2*w signal ratio',\n 'standoff':0\n },\n range=[1.05, 1.4],\n showgrid=True,\n gridcolor='rgb(169,169,169)',\n title_font_family=\"Times New Roman\",\n title_font_size = 20,\n linecolor='black',\n linewidth=2,\n tickfont=dict(\n family='Times New Roman',\n size=16,\n ),\n row=index, col=1\n )\n else:\n fig.update_yaxes(\n type=\"linear\",\n title={\n 'text':'CSF/Cord T2*w signal ratio',\n 'standoff':0\n },\n showgrid=True,\n gridcolor='rgb(169,169,169)',\n title_font_family=\"Times New Roman\",\n title_font_size = 20,\n linecolor='black',\n linewidth=2,\n tickfont=dict(\n family='Times New Roman',\n size=16,\n ),\n row=index, col=1\n )\n\n fig.update_xaxes(\n type=\"linear\",\n autorange=True,\n title=x_title,\n showgrid=True,\n gridcolor='rgb(169,169,169)',\n tickvals=label_positions,\n ticktext=vertebral_levels,\n showticklabels=showticklabels,\n title_font_family=\"Times New Roman\",\n title_font_size = 20,\n linecolor='black',\n linewidth=2,\n tickfont=dict(\n family='Times New Roman',\n size=16,\n ),\n row=index, col=2\n )\n fig.update_yaxes(\n type=\"linear\",\n title={\n 'text':'B1+ efficiency [nT/V]',\n 'standoff':0\n },\n showgrid=True,\n gridcolor='rgb(169,169,169)',\n title_font_family=\"Times New Roman\",\n title_font_size = 20,\n linecolor='black',\n linewidth=2,\n tickfont=dict(\n family='Times New Roman',\n size=16,\n ),\n row=index, col=2\n )\n\n index+=1\n\nfig.update_layout(height=1800, width=900)\n\nfor i in fig['layout']['annotations']:\n i['font'] = dict(size=22, family='Times New Roman')\n\nfig.update_layout(legend=dict(\n yanchor=\"top\",\n y=0.999,\n xanchor=\"left\",\n x=0.01,\n font=dict(\n family='Times New Roman',\n size=12\n ),\n bordercolor=\"Black\",\n borderwidth=1.5\n ),\n legend_tracegroupgap=0,\n paper_bgcolor='rgb(255, 255, 255)',\n plot_bgcolor='rgb(255, 255, 255)',\n)\n\nfig.add_annotation(\n dict(\n x=-0.06,\n y=-0.03,\n showarrow=False,\n text='A',\n font=dict(\n family='Times New Roman',\n size=48\n ),\n xref='paper',\n yref='paper'\n ))\nfig.add_annotation(\n dict(\n x=0.5,\n y=-0.03,\n showarrow=False,\n text='B',\n font=dict(\n family='Times New Roman',\n size=48\n ),\n xref='paper',\n yref='paper'\n ),\n)\n#iplot(fig, filename = 'figure3', config = config)\nplot(fig, filename = os.path.join(path_results,'figure3.html'), config = config)\n","metadata":{"tags":["hide_input","remove_output"],"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"display(HTML(os.path.join(path_results,'figure3.html')))","metadata":{"tags":["remove_input","report_output"],"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Figure 3. B1+ efficiency (A) and CSF/Cord signal ratio from the GRE scan (B) across subjects and across different RF shimming conditions. Data were measured in the spinal cord from C3 to T2 vertebral levels. To match the x-ticks across subjects, the C2-C3 and the T2-T3 intervertebral discs of each subject were aligned with that of the PAM50 template {cite:p}`DELEENER2018170`, and the curves were linearly scaled.\n","metadata":{}},{"cell_type":"markdown","source":"# References \n\n```{bibliography}\n:filter: docname in docnames\n```","metadata":{}},{"cell_type":"code","source":"# Indicate duration of data processing\n\nend_time = datetime.now()\ntotal_time = (end_time - start_time).total_seconds()\n\n# Convert seconds to a timedelta object\ntotal_time_delta = timedelta(seconds=total_time)\n\n# Format the timedelta object to a string\nformatted_time = str(total_time_delta)\n\n# Pad the string representation if less than an hour\nformatted_time = formatted_time.rjust(8, '0')\n\nprint(f\"Total Runtime [hour:min:sec]: {formatted_time}\")","metadata":{"tags":["remove_input","remove_output"],"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{},"execution_count":null,"outputs":[]}]} \ No newline at end of file +{"cells":[{"cell_type":"code","execution_count":null,"metadata":{"tags":["remove_output","remove_input"],"trusted":true},"outputs":[],"source":["build = 'archive' # 'archive' uses the neurolibre archive of the data., 'latest' would download the latest versions of the data.\n","notebook = 'neurolibre-figures' # neurolibre-figures: no processing, use downloaded processed data and do figures & stats,\n"," # neurolibre-clean: if some processed data exists, cleanup; Reprocess data\n"," # colab: download and process data from scratch in Google Colab\n","\n","try:\n"," import google.colab\n"," notebook = 'colab'\n","except:\n"," pass"]},{"cell_type":"markdown","metadata":{},"source":["\n","\n","\n","\n","\n","\n"]},{"cell_type":"markdown","metadata":{},"source":["

\n","

\n","\n","

\n","Analysis code for the paper \"RF shimming in the cervical spinal cord at 7T\"\n","

\n","
\n","\n","

\n","Daniel Papp1, Kyle M. Gilbert2,3, Gaspard Cereza1, Alexandre D’Astous1, Nibardo Lopez-Rios1, Mathieu Boudreau1, Marcus Couch4, Pedram Yazdanbakhsh5, Robert L. Barry6,7,8, Eva Alonso Ortiz1, Julien Cohen-Adad1,9,10\n","

\n","\n","\n","
\n","\n","

\n","1NeuroPoly Lab, Institute of Biomedical Engineering, Polytechnique Montreal, Montreal, QC, Canada,\n","2 Centre for Functional and Metabolic Mapping, The University of Western Ontario, London, ON, Canada,\n","3 Department of Medical Biophysics, The University of Western Ontario, London, ON, Canada,\n","4 Siemens Healthcare Limited, Montreal, QC, Canada,\n","5 McConnell Brain Imaging Centre, Montreal Neurological Institute, McGill University, Montreal, QC, Canada,\n","6 Athinoula A. Martinos Center for Biomedical Imaging, Department of Radiology, Massachusetts General Hospital, Charlestown, MA, USA,\n","7 Harvard Medical School, Boston, MA, USA,\n","8 Harvard-Massachusetts Institute of Technology Health Sciences & Technology, Cambridge, MA, USA,\n","9 Mila - Quebec AI Institute, Montreal, QC, Canada,\n","10 Functional Neuroimaging Unit, Centre de recherche de l'Institut universitaire de gériatrie de Montréal QC, Canada\n","\n","

\n","

\n","

\n"]},{"cell_type":"markdown","metadata":{},"source":["

Summary

\n","\n","Data was collected from five participants between two 7T sites with a custom 8Tx/20Rx parallel transmission (pTx) coil. We explored two RF shimming approaches from an MRI vendor and four from an open-source toolbox, showcasing their ability to enhance transmit field and signal homogeneity along the cervical spinal cord.\n","\n","The results indicate significant improvements in B1+ efficiency and cerebrospinal fluid / spinal cord signal ratio across various RF shimming conditions compared to the vendor based methods.\n","\n","The study's findings highlight the potential of RF shimming to advance 7T MRI's clinical utility for central nervous system imaging by enabling more homogenous and efficient spinal cord imaging. Additionally, the research incorporates a reproducible Jupyter Notebook, enhancing the study's transparency and facilitating peer verification. By also making the data openly accessible on OpenNeuro, we ensure that the scientific community can further explore, validate, and build upon our findings, contributing to the collective advancement in high-resolution imaging techniques.\n","\n","```{figure} ../featured.png\n","---\n","width: 900px\n","name: fig1\n","align: center\n","---\n","```"]},{"cell_type":"markdown","metadata":{},"source":["

\n","Figure 1. Overview of the RF shimming procedure. The top panel shows the RF coil used for the experiments, alongside the Tx coil geometry and the electromagnetic simulation results (on Gustav model) yielding the CP mode used for this coil. The bottom panel shows the RF shimming procedure (with approximate duration). First, GRE and tfl_rfmap scans are acquired (4min30s). Second, these images are transferred via ethernet socket from the MRI console onto a separate laptop running Shimming Toolbox and SCT (əs). Third, the spinal cord is automatically segmented to produce a mask that is resampled into the space of the individual coil magnitude and phase images of the tfl_rfmap scan (~5s). Fourth, the RF shim weights are calculated according to the defined constraints for each shim scenario (1min total).\n","

"]},{"cell_type":"markdown","metadata":{},"source":["

Statement of Need

\n","\n","Advancing the development of 7T MRI for spinal cord imaging is crucial for the enhanced diagnosis and monitoring of various neurodegenerative diseases {cite:p}`Kearney2015-py` and traumas {cite:p}`David2019-jy`. However, a significant challenge at this field strength is the transmit field inhomogeneity {cite:p}`Ibrahim2001-xt,Collins2005-za,Roschmann1987-om,Yang2002-ui`. Such inhomogeneity is particularly problematic for imaging the small, deep anatomical structures of the cervical spinal cord , as it can cause uneven signal intensity and elevate the local specific absorption ratio, compromising image quality. This multi-site study explores several radiofrequency (RF) shimming techniques in the cervical spinal cord at 7T.\n"]},{"cell_type":"markdown","metadata":{},"source":["# 1     |     Data\n","\n","The data can be downloaded from: https://openneuro.org/datasets/ds004906\n","\n","The structure of the input dataset is as follows (JSON sidecars are not listed for clarity):\n","\n","```{toggle}\n","~~~\n","ds004906\n","├── CHANGES\n","├── README\n","├── dataset_description.json\n","├── participants.json\n","├── participants.tsv\n","├── sub-01\n","│   ├── anat\n","│   │   ├── sub-01_acq-CP_T1w.nii.gz\n","│   │   ├── sub-01_acq-CP_T2starw.nii.gz\n","│   │   ├── sub-01_acq-CoV_T1w.nii.gz\n","│   │   ├── sub-01_acq-CoV_T2starw.nii.gz\n","│   │   ├── sub-01_acq-SAReff_T2starw.nii.gz\n","│   │   ├── sub-01_acq-patient_T2starw.nii.gz\n","│   │   ├── sub-01_acq-phase_T2starw.nii.gz\n","│   │   ├── sub-01_acq-target_T2starw.nii.gz\n","│   │   ├── sub-01_acq-volume_T2starw.nii.gz\n","│   └── fmap\n","│   ├── sub-01_acq-anatCP_TB1TFL.nii.gz\n","│   ├── sub-01_acq-anatCoV_TB1TFL.nii.gz\n","│   ├── sub-01_acq-anatSAReff_TB1TFL.nii.gz\n","│   ├── sub-01_acq-anatpatient_TB1TFL.nii.gz\n","│   ├── sub-01_acq-anatphase_TB1TFL.nii.gz\n","│   ├── sub-01_acq-anattarget_TB1TFL.nii.gz\n","│   ├── sub-01_acq-anatvolume_TB1TFL.nii.gz\n","│   ├── sub-01_acq-fampCP_TB1TFL.nii.gz\n","│   ├── sub-01_acq-fampCoV_TB1TFL.nii.gz\n","│   ├── sub-01_acq-fampSAReff_TB1TFL.nii.gz\n","│   ├── sub-01_acq-famppatient_TB1TFL.nii.gz\n","│   ├── sub-01_acq-fampphase_TB1TFL.nii.gz\n","│   ├── sub-01_acq-famptarget_TB1TFL.nii.gz\n","│   └── sub-01_acq-fampvolume_TB1TFL.nii.gz\n","├── sub-02\n","├── sub-03\n","├── sub-04\n","└── sub-05\n","~~~\n","```"]},{"cell_type":"markdown","metadata":{},"source":["# 2     |     Overview of processing pipeline\n","\n","During the data acquisition stage, RF shimming was done using the [Shimming Toolbox](https://github.com/shimming-toolbox/shimming-toolbox) {cite:p}`DAstous2023` during the acquisition stage.\n","\n","The post-processing pipeline uses the [Spinal Cord Toolbox](https://spinalcordtoolbox.com) {cite:p}`DeLeener201724`.\n","\n","For each subject:\n","\n","- Process anat/T2starw (GRE)\n"," - Segment the spinal cord (SC)\n"," - Label vertebral levels using existing manual disc labels\n"," - Create a mask of the cerebrospinal fluid (CSF)\n"," - Extract the SC/CSF magnitude signal to assess the stability of the flip angle across shim methods\n","- Process fmap/TFL (flip angle maps)\n"," - Register each B1 map (CP, CoV, etc.) to the GRE scan\n"," - Apply the computed warping field to bring the segmentation and vertebral levels to the B1 map\n"," - Convert the B1 map to nT/V units\n"," - Extract the B1 map value within the SC\n","\n",">Slow processes are indicated with the emoji ⏳\n","\n"]},{"cell_type":"markdown","metadata":{},"source":["# 3     |     Requirements\n","\n","* Install [Spinal Cord Toolbox](https://spinalcordtoolbox.com/user_section/installation.html) {cite:p}`DeLeener201724`, eg\n","\n","```shell\n","# Install SCT ⏳\n","!git clone --depth 1 https://github.com/spinalcordtoolbox/spinalcordtoolbox.git\n","!yes | spinalcordtoolbox/install_sct\n","os.environ['PATH'] += f\":/content/spinalcordtoolbox/bin\"\n","```\n","\n","* Download the project repository\n","\n","```shell\n","git clone https://github.com/shimming-toolbox/rf-shimming-7t.git\n","cd rf-shimming-7t\n","git checkout main\n","```\n","\n","* Install requirements\n","\n","```shell\n","pip install -r binder/requirements.txt\n","```\n","\n","* Download data\n","\n","```shell\n","cd content\n","repo2data -r ../binder/data_requirement.json\n","```\n"]},{"cell_type":"markdown","metadata":{},"source":["# 4     |     Environment setup\n","\n","In a Python shell:"]},{"cell_type":"markdown","metadata":{},"source":["Import the necessary modules."]},{"cell_type":"code","execution_count":null,"metadata":{"tags":["remove_output","hide_input"],"trusted":true},"outputs":[],"source":["from datetime import datetime, timedelta\n","import os\n","import re\n","import json\n","import subprocess\n","import glob\n","import itertools\n","import matplotlib.pyplot as plt\n","import numpy as np\n","import nibabel as nib\n","import pandas as pd\n","from scipy.interpolate import interp1d\n","from scipy.ndimage import uniform_filter1d\n","from scipy.stats import f_oneway, ttest_rel\n","import shutil\n","from shutil import rmtree\n","from pathlib import Path\n","from statsmodels.stats.multicomp import pairwise_tukeyhsd\n","from statsmodels.stats.anova import AnovaRM\n","import statsmodels.api as sm"]},{"cell_type":"code","execution_count":null,"metadata":{"tags":["remove_input","remove_output"],"trusted":true},"outputs":[],"source":["# Start timer\n","start_time = datetime.now()\n","\n","# Check where we are\n","!pwd"]},{"cell_type":"code","execution_count":null,"metadata":{"tags":["remove_input","remove_output"],"trusted":true},"outputs":[],"source":["# Google Colab-Only\n","# ⚠️ No need to run this cell if you run this notebook locally and already have these dependencies installed.\n","\n","if notebook == 'colab':\n"," # Install packages on system\n"," !sudo apt-get update\n"," !sudo apt-get install git-annex\n","\n"," # Install Python libaries\n"," !wget -O requirements.txt https://raw.githubusercontent.com/shimming-toolbox/rf-shimming-7t/main/binder/requirements.txt\n"," !pip install -r requirements.txt\n","\n"," # Install SCT ⏳\n"," !git clone --depth 1 https://github.com/spinalcordtoolbox/spinalcordtoolbox.git\n"," !yes | spinalcordtoolbox/install_sct\n"," os.environ['PATH'] += f\":/content/spinalcordtoolbox/bin\""]},{"cell_type":"code","execution_count":null,"metadata":{"tags":["remove_input","remove_output"],"trusted":true},"outputs":[],"source":["# Download data and define path variables\n","\n","# Google Colab-Only\n","if notebook=='colab':\n"," !datalad install https://github.com/OpenNeuroDatasets/ds004906.git\n"," os.chdir(\"ds004906\")\n"," !datalad get . # uncomment for production\n"," # !datalad get sub-01/ # debugging\n"," # Get derivatives containing manual labels\n"," !datalad get derivatives\n","\n","if notebook!='colab':\n"," # Switch working directory to $HOME/data/rf-shimming-7t/ds004906\n"," os.chdir(os.path.join(os.getenv('HOME'),\"data/rf-shimming-7t/ds004906\"))\n"," if notebook=='neurolibre-clean':\n"," flist = open('cleanup.txt', 'r')\n"," for f in flist:\n"," fname = f.rstrip('\\n')\n"," fname = Path(os.getcwd()) / Path(fname)\n","\n"," # or, if you get rid of os.chdir(path) above,\n"," # fname = os.path.join(path, f.rstrip())\n"," if os.path.isfile(fname): # this makes the code more robust\n"," print('Removing file: ' + str(fname))\n"," os.remove(fname)\n"," elif os.path.isdir(fname):\n"," print('Removing directory: ' + str(fname))\n"," rmtree(fname)\n","\n"," # also, don't forget to close the text file:\n"," flist.close()\n"]},{"cell_type":"markdown","metadata":{},"source":["Define variables."]},{"cell_type":"code","execution_count":null,"metadata":{"tags":["hide_input","remove_output"],"trusted":true},"outputs":[],"source":["# Assumes data/rf-shimming-7t is found at $HOME\n","path_data = os.path.join(os.getenv('HOME'),\"data/rf-shimming-7t/ds004906\")\n","print(f\"path_data: {path_data}\")\n","path_labels = os.path.join(path_data, \"derivatives\", \"labels\")\n","path_qc = os.path.join(path_data, \"qc\")\n","shim_modes = [\"CP\", \"patient\", \"volume\", \"phase\", \"CoV\", \"target\", \"SAReff\"]\n","shim_modes_MPRAGE = [\"CP\", \"CoV\"] \n","\n","print(f\"shim_modes: {shim_modes}\")\n","subjects = sorted(glob.glob(\"sub-*\"))\n","print(f\"subjects: {subjects}\")\n","\n","# Create output folder\n","path_results = os.path.join(os.getenv('HOME'),'results')\n","derivative_results = os.path.join(path_data, 'derivatives', 'results')\n","os.makedirs(path_results, exist_ok=True)\n","\n"]},{"cell_type":"markdown","metadata":{},"source":["# 5     |     Process anat/T2starw (GRE)\n"]},{"cell_type":"markdown","metadata":{},"source":["Run segmentation on GRE scan\n"]},{"cell_type":"code","execution_count":null,"metadata":{"tags":["hide_input"],"trusted":true},"outputs":[],"source":["\n","if notebook != 'neurolibre-figures':\n"," for subject in subjects:\n"," os.chdir(os.path.join(path_data, subject, \"anat\"))\n"," # The \"CoV\" RF shimming scenario was chosen as the segmentation baseline due to the more homogeneous signal intensity in the I-->S direction, which results in a better segmentation peformance in the C7-T2 region\n"," fname_manual_seg = os.path.join(path_labels, subject, \"anat\", f\"{subject}_acq-CoV_T2starw_label-SC_seg.nii.gz\")\n"," if os.path.exists(fname_manual_seg):\n"," # Manual segmentation already exists. Copy it to local folder\n"," print(f\"{subject}: Manual segmentation found\\n\")\n"," shutil.copyfile(fname_manual_seg, f\"{subject}_acq-CoV_T2starw_seg.nii.gz\")\n"," # Generate QC report to make sure the manual segmentation is correct\n"," !sct_qc -i {subject}_acq-CoV_T2starw.nii.gz -s {subject}_acq-CoV_T2starw_seg.nii.gz -p sct_deepseg_sc -qc {path_qc} -qc-subject {subject}\n"," else:\n"," # Manual segmentation does not exist. Run automatic segmentation.\n"," print(f\"{subject}: Manual segmentation not found\")\n"," !sct_deepseg_sc -i {subject}_acq-CoV_T2starw.nii.gz -c t2s -qc {path_qc}"]},{"cell_type":"markdown","metadata":{},"source":["Copy CSF masks from the derivatives folder.\n","\n","For more details about how these masks were created, see: [https://github.com/shimming-toolbox/rf-shimming-7t/issues/67](https://github.com/shimming-toolbox/rf-shimming-7t/issues/67)."]},{"cell_type":"code","execution_count":null,"metadata":{"tags":["hide_input"],"trusted":true},"outputs":[],"source":["if notebook != 'neurolibre-figures':\n"," for subject in subjects:\n"," os.chdir(os.path.join(path_data, subject, \"anat\"))\n"," fname_manual_seg = os.path.join(path_labels, subject, \"anat\", f\"{subject}_acq-CoV_T2starw_label-CSF_seg.nii.gz\")\n"," if os.path.exists(fname_manual_seg):\n"," # Manual segmentation already exists. Copy it to local folder\n"," print(f\"{subject}: Manual segmentation found\\n\")\n"," shutil.copyfile(fname_manual_seg, f\"{subject}_acq-CoV_T2starw_label-CSF_seg.nii.gz\")\n"," # Generate QC report to make sure the manual segmentation is correct\n"," !sct_qc -i {subject}_acq-CoV_T2starw.nii.gz -s {subject}_acq-CoV_T2starw_label-CSF_seg.nii.gz -p sct_deepseg_sc -qc {path_qc} -qc-subject {subject}\n"," else:\n"," # Manual segmentation does not exist. Panic!\n"," print(f\"{subject}: Manual segmentation not found 😱\")"]},{"cell_type":"markdown","metadata":{},"source":["Crop GRE scan for faster processing and better registration results\n"]},{"cell_type":"code","execution_count":null,"metadata":{"tags":["hide_input"],"trusted":true},"outputs":[],"source":["if notebook != 'neurolibre-figures':\n"," for subject in subjects:\n"," os.chdir(os.path.join(path_data, subject, \"anat\"))\n"," !sct_crop_image -i {subject}_acq-CoV_T2starw.nii.gz -m {subject}_acq-CoV_T2starw_seg.nii.gz -dilate 20x20x0 -o {subject}_acq-CoV_T2starw_crop.nii.gz\n"," !sct_crop_image -i {subject}_acq-CoV_T2starw_seg.nii.gz -m {subject}_acq-CoV_T2starw_seg.nii.gz -dilate 20x20x0 -o {subject}_acq-CoV_T2starw_crop_seg.nii.gz\n"," !sct_crop_image -i {subject}_acq-CoV_T2starw_label-CSF_seg.nii.gz -m {subject}_acq-CoV_T2starw_seg.nii.gz -dilate 20x20x0 -o {subject}_acq-CoV_T2starw_crop_label-CSF_seg.nii.gz"]},{"cell_type":"markdown","metadata":{},"source":["Label vertebrae on GRE scan"]},{"cell_type":"code","execution_count":null,"metadata":{"tags":["hide_input"],"trusted":true},"outputs":[],"source":["\n","# Given the low resolution of the GRE scan, the automatic detection of C2-C3 disc is unreliable. Therefore we need to use the manual disc labels that are part of the dataset.\n","if notebook != 'neurolibre-figures':\n"," for subject in subjects:\n"," os.chdir(os.path.join(path_data, subject, \"anat\"))\n"," fname_label_discs = os.path.join(path_data, \"derivatives\", \"labels\", subject, \"anat\", f\"{subject}_acq-CoV_T2starw_label-discs_dseg.nii.gz\")\n"," !sct_label_utils -i {subject}_acq-CoV_T2starw_crop_seg.nii.gz -disc {fname_label_discs} -o {subject}_acq-CoV_T2starw_crop_seg_labeled.nii.gz\n"," # Generate QC report to assess labeled segmentation\n"," !sct_qc -i {subject}_acq-CoV_T2starw_crop.nii.gz -s {subject}_acq-CoV_T2starw_crop_seg_labeled.nii.gz -p sct_label_vertebrae -qc {path_qc} -qc-subject {subject}"]},{"cell_type":"markdown","metadata":{},"source":["Register *_T2starw to CoV_T2starw ⏳"]},{"cell_type":"code","execution_count":null,"metadata":{"tags":["hide_input"],"trusted":true},"outputs":[],"source":["if notebook != 'neurolibre-figures':\n"," for subject in subjects:\n"," os.chdir(os.path.join(path_data, subject, \"anat\"))\n"," for shim_mode in shim_modes:\n"," # Don't do it for CoV_T2starw\n"," if shim_mode != 'CoV':\n"," !sct_register_multimodal -i {subject}_acq-{shim_mode}_T2starw.nii.gz -d {subject}_acq-CoV_T2starw_crop.nii.gz -dseg {subject}_acq-CoV_T2starw_crop_seg.nii.gz -param step=1,type=im,algo=dl -qc {path_qc}"]},{"cell_type":"markdown","metadata":{},"source":["Extract the signal intensity on the GRE scan within the spinal cord between levels C3 and T2 (included), which correspond to the region where RF shimming was prescribed."]},{"cell_type":"code","execution_count":null,"metadata":{"tags":["hide_input"],"trusted":true},"outputs":[],"source":["\n","if notebook != 'neurolibre-figures':\n"," for subject in subjects:\n"," os.chdir(os.path.join(path_data, subject, \"anat\"))\n"," for shim_mode in shim_modes:\n"," # Shim methods are registered to the CoV T2starw scan, so we need to use the added suffix to identify them\n"," if shim_mode == 'CoV':\n"," file_suffix = 'crop'\n"," else:\n"," file_suffix = 'reg'\n"," fname_result_sc = os.path.join(derivative_results, f\"{subject}_acq-{shim_mode}_T2starw_label-SC.csv\")\n"," !sct_extract_metric -i {subject}_acq-{shim_mode}_T2starw_{file_suffix}.nii.gz -f {subject}_acq-CoV_T2starw_crop_seg.nii.gz -method wa -vert 3:9 -vertfile {subject}_acq-CoV_T2starw_crop_seg_labeled.nii.gz -perslice 1 -o {fname_result_sc}\n"," fname_result_csf = os.path.join(derivative_results, f\"{subject}_acq-{shim_mode}_T2starw_label-CSF.csv\")\n"," !sct_extract_metric -i {subject}_acq-{shim_mode}_T2starw_{file_suffix}.nii.gz -f {subject}_acq-CoV_T2starw_crop_label-CSF_seg.nii.gz -method wa -vert 3:9 -vertfile {subject}_acq-CoV_T2starw_crop_seg_labeled.nii.gz -perslice 1 -o {fname_result_csf}"]},{"cell_type":"markdown","metadata":{},"source":["Prepare data for figure of CSF/SC signal ratio from T2starw scan"]},{"cell_type":"code","execution_count":null,"metadata":{"tags":["hide_input","remove_output"],"trusted":true},"outputs":[],"source":["%matplotlib inline\n","\n","# Go back to root data folder\n","os.chdir(path_data)\n","\n","def smooth_data(data, window_size=50):\n"," \"\"\" Apply a simple moving average to smooth the data. \"\"\"\n"," return uniform_filter1d(data, size=window_size, mode='nearest')\n","\n","# Fixed grid for x-axis\n","x_grid = np.linspace(0, 1, 100)\n","\n","# z-slices corresponding to levels C3 to T2 on the PAM50 template. These will be used to scale the x-label of each subject.\n","original_vector = np.array([907, 870, 833, 800, 769, 735, 692, 646])\n","\n","# Normalize the PAM50 z-slice numbers to the 1-0 range (to show inferior-superior instead of superior-inferior)\n","min_val = original_vector.min()\n","max_val = original_vector.max()\n","normalized_vector = 1 - ((original_vector - min_val) / (max_val - min_val))\n","\n","# Use this normalized vector as x-ticks\n","custom_xticks = normalized_vector\n","\n","# Vertebral level labels\n","vertebral_levels = [\"C3\", \"C4\", \"C5\", \"C6\", \"C7\", \"T1\", \"T2\"]\n","label_positions = normalized_vector[:-1] + np.diff(normalized_vector) / 2\n","\n","# Number of subjects determines the number of rows in the subplot\n","n_rows = len(subjects)\n","\n","# Create a figure with multiple subplots\n","fig, axes = plt.subplots(n_rows, 1, figsize=(10, 6 * n_rows))\n","font_size = 18\n","line_width = 3\n","\n","# Check if axes is an array or a single object\n","if n_rows == 1:\n"," axes = [axes]\n","\n","# Data storage for statistics\n","data_stats = []\n","\n","# Data storage for Plotly\n","t2_data_plotly = {}\n","\n","# Iterate over each subject and create a subplot\n","for i, subject in enumerate(subjects):\n"," ax = axes[i]\n"," t2_data_plotly[subject]={}\n"," for shim_mode in shim_modes:\n"," # Initialize list to collect data for this shim method\n"," method_data = []\n","\n"," # Get signal in SC\n"," file_csv = os.path.join(derivative_results, f\"{subject}_acq-{shim_mode}_T2starw_label-SC.csv\")\n"," df = pd.read_csv(file_csv)\n"," data_sc = df['WA()']\n"," data_sc_smoothed = smooth_data(data_sc)\n","\n"," # Get signal in CSF\n"," file_csv = os.path.join(derivative_results, f\"{subject}_acq-{shim_mode}_T2starw_label-CSF.csv\")\n"," df = pd.read_csv(file_csv)\n"," data_csf = df['WA()']\n"," data_csf_smoothed = smooth_data(data_csf)\n"," \n"," # Compute ratio\n"," data_sc_csf_ratio = data_csf_smoothed / data_sc_smoothed\n","\n"," # Normalize the x-axis to a 1-0 scale for each subject (to go from superior-inferior direction)\n"," x_subject = np.linspace(1, 0, len(data_sc_csf_ratio))\n","\n"," # Interpolate to the fixed grid\n"," interp_func = interp1d(x_subject, data_sc_csf_ratio, kind='linear', bounds_error=False, fill_value='extrapolate')\n"," resampled_data = interp_func(x_grid)\n","\n"," method_data.append(resampled_data)\n","\n"," # If there's data for this shim method, plot it and compute stats\n"," if method_data:\n"," t2_data_plotly[subject][shim_mode]=method_data\n"," \n"," # Plotting each file's data separately\n"," for resampled_data in method_data:\n"," ax.plot(x_grid, resampled_data, label=f\"{shim_mode}\", linewidth=line_width)\n","\n"," # Compute stats on the non-resampled data (to avoid interpolation errors)\n"," mean_data = np.mean(data_sc_csf_ratio)\n"," sd_data = np.std(data_sc_csf_ratio)\n"," data_stats.append([subject, shim_mode, mean_data, sd_data])\n"," else:\n"," t2_data_plotly[subject][shim_mode]=None\n","\n"," # Set x-ticks and labels for the bottom subplot\n"," if i == n_rows - 1: # Check if it's the last subplot\n"," # Create a secondary axis for vertebral level labels\n"," secax = ax.secondary_xaxis('bottom')\n"," secax.set_xticks(label_positions)\n"," secax.set_xticklabels(vertebral_levels, fontsize=font_size-4)\n"," secax.tick_params(axis='x', which='major', length=0) # Hide tick marks\n","\n"," ax.set_xticks(custom_xticks)\n"," ax.set_xticklabels([''] * len(custom_xticks))\n"," ax.tick_params(axis='x', which='major', length=0) # Hide tick marks for the primary axis\n","\n"," ax.set_title(f'{subject}', fontsize=font_size)\n"," ax.set_ylabel('CSF/Cord T2starw signal ratio', fontsize=font_size)\n"," ax.tick_params(axis='y', which='major', labelsize=font_size-4)\n","\n"," # Add legend only to the first subplot\n"," if i == 0:\n"," ax.legend(fontsize=font_size)\n","\n"," ax.grid(True)\n","\n","# Adjust the layout so labels and titles do not overlap\n","plt.tight_layout()\n","plt.savefig(os.path.join(path_results, 'fig_gre_csf-sc_ratio.png'), dpi=300, format='png')\n","plt.show()\n","\n"]},{"cell_type":"markdown","metadata":{},"source":["Perform statistics"]},{"cell_type":"code","execution_count":null,"metadata":{"tags":["hide_input","report_output"],"trusted":true},"outputs":[],"source":["# Perform statistics\n","\n","# Convert to DataFrame and save to CSV\n","df_stats = pd.DataFrame(data_stats, columns=['Subject', 'Shim_Mode', 'Average', 'Standard_Deviation'])\n","df_stats.to_csv(os.path.join(path_results, 'stats_t2starw.csv'), index=False)\n","\n","# Compute statistics across subjects\n","grouped_means = df_stats.groupby('Shim_Mode').agg({'Average': ['mean', 'std'], 'Standard_Deviation': ['mean', 'std']})\n","\n","# Format mean ± standard deviation\n","grouped_means['Average_formatted'] = grouped_means['Average']['mean'].map(\"{:.2f}\".format) + \" ± \" + grouped_means['Average']['std'].map(\"{:.2f}\".format)\n","grouped_means['Standard_Deviation_formatted'] = grouped_means['Standard_Deviation']['mean'].map(\"{:.2f}\".format) + \" ± \" + grouped_means['Standard_Deviation']['std'].map(\"{:.2f}\".format)\n","\n","# Drop multi-level index and only keep formatted columns\n","grouped_means = grouped_means.drop(columns=['Average', 'Standard_Deviation'])\n","grouped_means.columns = ['Average', 'Standard_Deviation'] # Rename columns for clarity\n","grouped_means.reset_index().to_csv(os.path.join(path_results, 'stats_t2starw_average_across_subjects.csv'), index=False)\n","\n","# Perform Repeated Measures ANOVA\n","aovrm = AnovaRM(df_stats, 'Average', 'Subject', within=['Shim_Mode'])\n","res = aovrm.fit()\n","print(res.summary())\n","\n","# Perform Post Hoc paired t-tests\n","\n","# Filter out subjects that don't have all shim modes\n","shim_modes = df_stats['Shim_Mode'].unique()\n","valid_subjects = df_stats.groupby('Subject').filter(lambda x: all(mode in x['Shim_Mode'].values for mode in shim_modes))\n","\n","# Pairwise comparisons\n","pairs = list(itertools.combinations(shim_modes, 2))\n","p_values = []\n","\n","for pair in pairs:\n"," data1 = valid_subjects[valid_subjects['Shim_Mode'] == pair[0]]['Average']\n"," data2 = valid_subjects[valid_subjects['Shim_Mode'] == pair[1]]['Average']\n"," _, p_value = ttest_rel(data1, data2)\n"," p_values.append(p_value)\n","\n","# Function for Benjamini-Hochberg FDR correction\n","def benjamini_hochberg(p_values):\n"," n = len(p_values)\n"," sorted_p_values = np.sort(p_values)\n"," sorted_index = np.argsort(p_values)\n"," adjusted_p_values = np.zeros(n)\n","\n"," for i, p in enumerate(sorted_p_values):\n"," adjusted_p_values[sorted_index[i]] = min(p * n / (i + 1), 1)\n","\n"," return adjusted_p_values\n","\n","# Applying Benjamini-Hochberg FDR correction\n","adjusted_p_values = benjamini_hochberg(p_values)\n","\n","# Creating a summary DataFrame\n","comparison_results = pd.DataFrame({'Pair': pairs, 'P-Value': p_values, 'Adjusted P-Value': adjusted_p_values})\n","\n","# Save to CSV\n","comparison_results.to_csv(os.path.join(path_results, 'stats_t2starw_paired_ttest.csv'), index=False)\n","\n","print(f\"Paired t-tests:\\n{comparison_results}\")"]},{"cell_type":"markdown","metadata":{},"source":["# 6     |     Process fmap/TFL (flip angle maps)\n"]},{"cell_type":"markdown","metadata":{},"source":["Register TFL flip angle maps to the GRE scan ⏳\n"]},{"cell_type":"code","execution_count":null,"metadata":{"tags":["hide_input"],"trusted":true},"outputs":[],"source":["if notebook != 'neurolibre-figures':\n"," for subject in subjects:\n"," os.chdir(os.path.join(path_data, subject, \"fmap\"))\n"," for shim_mode in shim_modes:\n"," !sct_register_multimodal -i {subject}_acq-anat{shim_mode}_TB1TFL.nii.gz -d ../anat/{subject}_acq-CoV_T2starw_crop.nii.gz -dseg ../anat/{subject}_acq-CoV_T2starw_crop_seg.nii.gz -param step=1,type=im,algo=slicereg,metric=CC -qc {path_qc}"]},{"cell_type":"markdown","metadata":{},"source":["Warping spinal cord segmentation and vertebral level to each flip angle map"]},{"cell_type":"code","execution_count":null,"metadata":{"tags":["hide_input"],"trusted":true},"outputs":[],"source":["if notebook != 'neurolibre-figures':\n"," for subject in subjects:\n"," os.chdir(os.path.join(path_data, subject, \"fmap\"))\n"," for shim_mode in shim_modes:\n"," # Use linear interpolation to preserve partial volume information (needed when extracting values along the cord)\n"," !sct_apply_transfo -i ../anat/{subject}_acq-CoV_T2starw_crop_seg.nii.gz -d {subject}_acq-anat{shim_mode}_TB1TFL.nii.gz -w warp_{subject}_acq-CoV_T2starw_crop2{subject}_acq-anat{shim_mode}_TB1TFL.nii.gz -x linear -o {subject}_acq-anat{shim_mode}_TB1TFL_seg.nii.gz\n"," # Use nearest neighbour (nn) interpolation because we are dealing with non-binary discrete labels\n"," !sct_apply_transfo -i ../anat/{subject}_acq-CoV_T2starw_crop_seg_labeled.nii.gz -d {subject}_acq-anat{shim_mode}_TB1TFL.nii.gz -w warp_{subject}_acq-CoV_T2starw_crop2{subject}_acq-anat{shim_mode}_TB1TFL.nii.gz -x nn -o {subject}_acq-anat{shim_mode}_TB1TFL_seg_labeled.nii.gz"]},{"cell_type":"markdown","metadata":{},"source":["Convert the flip angle maps to B1+ efficiency maps [nT/V] (inspired by code from Kyle Gilbert).\n","\n","The approach consists in calculating the B1+ efficiency using a 1ms, pi-pulse at the acquisition voltage, then scale the efficiency by the ratio of the measured flip angle to the requested flip angle in the pulse sequence.\n"]},{"cell_type":"code","execution_count":null,"metadata":{"tags":["hide_input"],"trusted":true},"outputs":[],"source":["\n","GAMMA = 2.675e8; # [rad / (s T)]\n","requested_fa = 90 # saturation flip angle -- hard-coded in sequence\n","\n","if notebook != 'neurolibre-figures':\n"," for subject in subjects:\n"," os.chdir(os.path.join(path_data, subject, \"fmap\"))\n"," for shim_mode in shim_modes:\n"," # Fetch the reference voltage from the JSON sidecar to the TFL B1map sequence\n"," with open(f\"{subject}_acq-famp{shim_mode}_TB1TFL.json\", \"r\") as f:\n"," metadata = json.load(f)\n"," ref_voltage = metadata.get(\"TxRefAmp\", \"N/A\")\n"," print(f\"ref_voltage [V]: {ref_voltage} ({subject}_acq-famp{shim_mode}_TB1TFL)\")\n","\n"," # Open flip angle map with nibabel\n"," nii = nib.load(f\"{subject}_acq-famp{shim_mode}_TB1TFL.nii.gz\")\n"," acquired_fa = nii.get_fdata()\n","\n"," # Siemens maps are in units of flip angle * 10 (in degrees)\n"," acquired_fa = acquired_fa / 10\n","\n"," # Account for the power loss between the coil and the socket. That number was given by Siemens.\n"," voltage_at_socket = ref_voltage * 10 ** -0.095\n","\n"," # Compute B1 map in [T/V]\n"," b1_map = (acquired_fa / requested_fa) * (np.pi / (GAMMA * 1e-3 * voltage_at_socket))\n","\n"," # Convert to [nT/V]\n"," b1_map = b1_map * 1e9\n","\n"," # Save as NIfTI file\n"," nii_b1 = nib.Nifti1Image(b1_map, nii.affine, nii.header)\n"," nib.save(nii_b1, f\"{subject}_acq-{shim_mode}_TB1map.nii.gz\")"]},{"cell_type":"markdown","metadata":{},"source":["Extract B1+ value along the spinal cord between levels C3 and T2 (included)\n"]},{"cell_type":"code","execution_count":null,"metadata":{"tags":["hide_input"],"trusted":true},"outputs":[],"source":["if notebook != 'neurolibre-figures':\n"," for subject in subjects:\n"," os.chdir(os.path.join(path_data, subject, \"fmap\"))\n"," for shim_mode in shim_modes:\n"," fname_result_b1plus = os.path.join(derivative_results, f\"{subject}_TB1map_{shim_mode}.csv\")\n"," !sct_extract_metric -i {subject}_acq-{shim_mode}_TB1map.nii.gz -f {subject}_acq-anat{shim_mode}_TB1TFL_seg.nii.gz -method wa -vert 3:9 -vertfile {subject}_acq-anat{shim_mode}_TB1TFL_seg_labeled.nii.gz -perslice 1 -o {fname_result_b1plus}"]},{"cell_type":"markdown","metadata":{},"source":["Prepare data for figure of B1+ values along the spinal cord across shim methods"]},{"cell_type":"code","execution_count":null,"metadata":{"tags":["hide_input","remove_output"],"trusted":true},"outputs":[],"source":["# Go back to root data folder\n","os.chdir(os.path.join(path_data))\n","\n","def smooth_data(data, window_size=20):\n"," \"\"\" Apply a simple moving average to smooth the data. \"\"\"\n"," return uniform_filter1d(data, size=window_size, mode='nearest')\n","\n","# Fixed grid for x-axis\n","x_grid = np.linspace(0, 1, 100)\n","\n","# z-slices corresponding to levels C3 to T2 on the PAM50 template. These will be used to scale the x-label of each subject.\n","original_vector = np.array([907, 870, 833, 800, 769, 735, 692, 646])\n","\n","# Normalize the PAM50 z-slice numbers to the 1-0 range (to show inferior-superior instead of superior-inferior)\n","min_val = original_vector.min()\n","max_val = original_vector.max()\n","normalized_vector = 1 - ((original_vector - min_val) / (max_val - min_val))\n","\n","# Use this normalized vector as x-ticks\n","custom_xticks = normalized_vector\n","\n","# Vertebral level labels\n","vertebral_levels = [\"C3\", \"C4\", \"C5\", \"C6\", \"C7\", \"T1\", \"T2\"]\n","# Calculate midpoints for label positions\n","label_positions = normalized_vector[:-1] + np.diff(normalized_vector) / 2\n","\n","# Number of subjects determines the number of rows in the subplot\n","n_rows = len(subjects)\n","\n","# Create a figure with multiple subplots\n","fig, axes = plt.subplots(n_rows, 1, figsize=(10, 6 * n_rows))\n","font_size = 18\n","line_width = 3\n","\n","# Check if axes is an array or a single object\n","if n_rows == 1:\n"," axes = [axes]\n","\n","# Data storage for statistics\n","data_stats = []\n","\n","# Data storage for Plotly\n","b1_data_plotly = {}\n","\n","# Iterate over each subject and create a subplot\n","for i, subject in enumerate(subjects):\n"," \n"," ax = axes[i]\n","\n"," os.chdir(os.path.join(path_data, subject, \"fmap\"))\n"," b1_data_plotly[subject]={}\n"," \n"," for shim_mode in shim_modes:\n"," # Initialize list to collect data for this shim method\n"," method_data = []\n","\n"," file_csv = os.path.join(derivative_results, f\"{subject}_TB1map_{shim_mode}.csv\")\n"," df = pd.read_csv(file_csv)\n"," wa_data = df['WA()']\n","\n"," # Normalize the x-axis to a 1-0 scale for each subject (to go from superior-inferior direction)\n"," x_subject = np.linspace(1, 0, len(wa_data))\n","\n"," # Interpolate to the fixed grid\n"," interp_func = interp1d(x_subject, wa_data, kind='linear', bounds_error=False, fill_value='extrapolate')\n"," resampled_data = interp_func(x_grid)\n","\n"," # Apply smoothing\n"," smoothed_data = smooth_data(resampled_data)\n","\n"," method_data.append(smoothed_data)\n","\n"," # If there's data for this shim method, plot it\n"," if method_data:\n"," # Plotting each file's data separately\n"," for resampled_data in method_data:\n"," b1_data_plotly[subject][shim_mode]=resampled_data\n"," ax.plot(x_grid, resampled_data, label=f\"{shim_mode}\", linewidth=line_width)\n","\n"," # Compute stats on the non-resampled data (to avoid interpolation errors)\n"," mean_data = np.mean(wa_data)\n"," sd_data = np.std(wa_data)\n"," data_stats.append([subject, shim_mode, mean_data, sd_data])\n"," else:\n"," b1_data_plotly[subject][shim_mode]=None\n","\n"," # Set x-ticks and labels for the bottom subplot\n"," if i == n_rows - 1: # Check if it's the last subplot\n"," # Create a secondary axis for vertebral level labels\n"," secax = ax.secondary_xaxis('bottom')\n"," secax.set_xticks(label_positions)\n"," secax.set_xticklabels(vertebral_levels, fontsize=font_size-4)\n"," secax.tick_params(axis='x', which='major', length=0) # Hide tick marks\n","\n"," ax.set_xticks(custom_xticks)\n"," ax.set_xticklabels([''] * len(custom_xticks))\n"," ax.tick_params(axis='x', which='major', length=0) # Hide tick marks for the primary axis\n","\n"," ax.set_title(f'{subject}', fontsize=font_size)\n"," ax.set_ylabel('B1+ efficiency [nT/V]', fontsize=font_size)\n"," ax.tick_params(axis='y', which='major', labelsize=font_size-4)\n","\n"," # Add legend only to the first subplot\n"," if i == 0:\n"," ax.legend(fontsize=font_size)\n","\n"," ax.grid(True)\n","\n","# Adjust the layout so labels and titles do not overlap\n","plt.tight_layout()\n","plt.savefig(os.path.join(path_results, 'fig_b1plus.png'), dpi=300, format='png')\n","\n","plt.show()\n"]},{"cell_type":"markdown","metadata":{},"source":["Perform statistics"]},{"cell_type":"code","execution_count":null,"metadata":{"tags":["hide_input","report_output"],"trusted":true},"outputs":[],"source":["# Convert to DataFrame and save to CSV\n","df_stats = pd.DataFrame(data_stats, columns=['Subject', 'Shim_Mode', 'Average', 'Standard_Deviation'])\n","df_stats.to_csv(os.path.join(path_results, 'stats_b1plus.csv'), index=False)\n","\n","# Compute statistics across subjects\n","grouped_means = df_stats.groupby('Shim_Mode').agg({'Average': ['mean', 'std'], 'Standard_Deviation': ['mean', 'std']})\n","\n","# Format mean ± standard deviation\n","grouped_means['Average_formatted'] = grouped_means['Average']['mean'].map(\"{:.2f}\".format) + \" ± \" + grouped_means['Average']['std'].map(\"{:.2f}\".format)\n","grouped_means['Standard_Deviation_formatted'] = grouped_means['Standard_Deviation']['mean'].map(\"{:.2f}\".format) + \" ± \" + grouped_means['Standard_Deviation']['std'].map(\"{:.2f}\".format)\n","\n","# Drop multi-level index and only keep formatted columns\n","grouped_means = grouped_means.drop(columns=['Average', 'Standard_Deviation'])\n","grouped_means.columns = ['Average', 'Standard_Deviation'] # Rename columns for clarity\n","grouped_means.reset_index().to_csv(os.path.join(path_results, 'stats_b1plus_average_across_subjects.csv'), index=False)\n","\n","# Perform Repeated Measures ANOVA\n","aovrm = AnovaRM(df_stats, 'Average', 'Subject', within=['Shim_Mode'])\n","res = aovrm.fit()\n","print(res.summary())\n","\n","# Perform Post Hoc paired t-tests\n","\n","# Filter out subjects that don't have all shim modes\n","shim_modes = df_stats['Shim_Mode'].unique()\n","valid_subjects = df_stats.groupby('Subject').filter(lambda x: all(mode in x['Shim_Mode'].values for mode in shim_modes))\n","\n","# Pairwise comparisons\n","pairs = list(itertools.combinations(shim_modes, 2))\n","p_values = []\n","\n","for pair in pairs:\n"," data1 = valid_subjects[valid_subjects['Shim_Mode'] == pair[0]]['Average']\n"," data2 = valid_subjects[valid_subjects['Shim_Mode'] == pair[1]]['Average']\n"," _, p_value = ttest_rel(data1, data2)\n"," p_values.append(p_value)\n","\n","# Function for Benjamini-Hochberg FDR correction\n","def benjamini_hochberg(p_values):\n"," n = len(p_values)\n"," sorted_p_values = np.sort(p_values)\n"," sorted_index = np.argsort(p_values)\n"," adjusted_p_values = np.zeros(n)\n","\n"," for i, p in enumerate(sorted_p_values):\n"," adjusted_p_values[sorted_index[i]] = min(p * n / (i + 1), 1)\n","\n"," return adjusted_p_values\n","\n","# Applying Benjamini-Hochberg FDR correction\n","adjusted_p_values = benjamini_hochberg(p_values)\n","\n","# Creating a summary DataFrame\n","comparison_results = pd.DataFrame({'Pair': pairs, 'P-Value': p_values, 'Adjusted P-Value': adjusted_p_values})\n","\n","# Save to CSV\n","comparison_results.to_csv(os.path.join(path_results, 'stats_b1plus_paired_ttest.csv'), index=False)\n","\n","print(f\"Paired t-tests:\\n{comparison_results}\")"]},{"cell_type":"markdown","metadata":{},"source":["# 7     |     Paper figures"]},{"cell_type":"markdown","metadata":{},"source":["Prepare RF shimming mask for figure\n"]},{"cell_type":"code","execution_count":null,"metadata":{"tags":["hide_input"],"trusted":true},"outputs":[],"source":["# Select subject to show\n","subject = 'sub-05'\n","os.chdir(os.path.join(path_data, subject, \"fmap\"))\n","\n","# Create RF shimming mask from the segmentation (re-create the live RF shimming procedure)\n","file_mask = f\"{subject}_acq-anatCP_TB1TFL_mask-shimming.nii.gz\"\n","if notebook != 'neurolibre-figures':\n","\n"," !sct_maths -i {subject}_acq-anatCP_TB1TFL_seg_labeled.nii.gz -thr 3 -uthr 9 -o {file_mask}\n"," !sct_maths -i {file_mask} -bin 1 -o {file_mask}\n"," !sct_create_mask -i {subject}_acq-anatCP_TB1TFL.nii.gz -p centerline,{file_mask} -size 28mm -f cylinder -o {file_mask}\n","\n"," # Dilate mask\n"," !sct_maths -i {file_mask} -dilate 2 -shape disk -dim 2 -o {subject}_acq-anatCP_TB1TFL_mask-shimming_dil.nii.gz\n"," # Subtract dilated mask with mask to get the edge\n"," !sct_maths -i {subject}_acq-anatCP_TB1TFL_mask-shimming_dil.nii.gz -sub {file_mask} -o {file_mask}"]},{"cell_type":"markdown","metadata":{},"source":["Create figure of B1+ maps"]},{"cell_type":"code","execution_count":null,"metadata":{"tags":["hide_input","report_output"],"trusted":true},"outputs":[],"source":["%matplotlib inline\n","# Select subject to show\n","subject = 'sub-05'\n","os.chdir(os.path.join(path_data, subject, \"fmap\"))\n","file_mask = f\"{subject}_acq-anatCP_TB1TFL_mask-shimming.nii.gz\"\n","\n","# Load the statistics from the CSV file\n","stats_df = pd.read_csv(os.path.join(path_results, 'stats_b1plus.csv'))\n","\n","# Filter for the specific subject\n","subject_stats = stats_df[stats_df['Subject'] == subject]\n","\n","# Defining crop limits for resulting figure\n","xmin = 20\n","xmax = 110\n","ymin = 20\n","ymax = 150\n","\n","# Defining dynamic range\n","dynmin = 5 \n","dynmax = 30\n","\n","# Create a figure with multiple subplots\n","fig, axes = plt.subplots(2, 4, figsize=(8, 6))\n","font_size = 14\n","axes=axes.flatten()\n","\n","# First, plot the anatomical image with an overlay of the mask\n","\n","# Load data\n","CP_anat=nib.load(f\"{subject}_acq-anatCP_TB1TFL.nii.gz\")\n","CP_SC=nib.load(file_mask)\n","CP_nTpV=nib.load(f\"{subject}_acq-CP_TB1map.nii.gz\")\n","\n","# Defining mask based on the magnitude image intensity threshold\n","cslice=CP_anat.shape[2] // 2 -2 #shows the SC seg best\n","threshold=300\n","mask=CP_anat.get_fdata()[xmin:xmax,ymin:ymax, cslice]\n","mask=np.where(mask > threshold, 1, 0)\n","\n","# Cropping anat, SC, B1+\n","CP_anat=CP_anat.get_fdata()[xmin:xmax,ymin:ymax, cslice]\n","CP_anat=CP_anat*mask\n","CP_SC=CP_SC.get_fdata()[xmin:xmax,ymin:ymax, cslice]\n","CP_nTpV=CP_nTpV.get_fdata()[xmin:xmax,ymin:ymax, cslice]\n","CP_nTpV=CP_nTpV*mask\n","\n","# All opacity overalys look ugly: workaround, set the anat slice to a max value where the segmentation exists\n","CP_anat[CP_SC>0.5]=4000;\n","\n","# Plotting anat overlayed with SC\n","splot=axes[0]\n","splot.imshow((CP_anat.T), cmap='gray', origin='lower',vmin=0,vmax=2000)#, interpolation='spline36')\n","splot.set_title('Anat', size=font_size)\n","splot.axis('off')\n","\n","# Then, plot each B1+ map, with an overlay of the mean and CV inside the cord\n","for i,shim_mode in enumerate(shim_modes):\n"," # Load data\n"," B1map=nib.load(f\"{subject}_acq-{shim_mode}_TB1map.nii.gz\")\n"," B1map=B1map.get_fdata()[xmin:xmax,ymin:ymax, cslice]\n"," B1map=B1map*mask\n","\n"," # Plot\n"," splot=axes[i+1]\n"," im = splot.imshow((B1map.T), cmap='viridis', origin='lower',vmin=dynmin,vmax=dynmax)#, interpolation='spline36')\n"," splot.set_title(shim_mode, size=font_size)\n"," splot.axis('off')\n","\n"," # Find the statistics for the current shim mode\n"," shim_stats = subject_stats[subject_stats['Shim_Mode'] == shim_mode]\n"," if not shim_stats.empty:\n"," mean_val = shim_stats.iloc[0]['Average']\n"," std_val = shim_stats.iloc[0]['Standard_Deviation']\n"," cv = std_val / mean_val * 100 # Coefficient of variation in percentage\n"," annotation_text = f\"{mean_val:.2f} nT/V\\n{cv:.2f}%\"\n"," splot.annotate(annotation_text, (0.05, 0.95), xycoords='axes fraction', \n"," fontsize=10, color='white', \n"," verticalalignment='top', horizontalalignment='left')\n","\n","plt.tight_layout()\n","plt.subplots_adjust(wspace=0.1, hspace=0, right=0.9)\n","\n","# Colorbar\n","# Assume that the colorbar should start at the bottom of the lower row of subplots and\n","# extend to the top of the upper row of subplots\n","cbar_bottom = 0.06 # This might need adjustment\n","cbar_height = 0.83 # This represents the total height of both rows of subplots\n","cbar_ax = fig.add_axes([0.93, cbar_bottom, 0.03, cbar_height])\n","cbar = plt.colorbar(im, cax=cbar_ax)\n","\n","# cbar_ax = fig.add_axes([0.95, 0.5, 0.04, 0.4])\n","# cbar = plt.colorbar(im, cax=cbar_ax)\n","cbar_ax.set_title('nT/V', size=12)\n","plt.savefig(os.path.join(path_results, 'fig_b1plus_map.png'), dpi=300, format='png')\n","plt.show()\n"]},{"cell_type":"markdown","metadata":{},"source":["Figure 2. B1+ efficiency for one participant (sub-05) across all seven RF shimming conditions. The top left panel shows the tfl_b1map magnitude image with an overlay of the mask that was used to perform RF shimming. Text inserts show the mean (in nT/V) and CoV (in %) of B1+ efficiency along the spinal cord between C3 and T2. "]},{"cell_type":"code","execution_count":null,"metadata":{"tags":["hide_input","remove_output"],"trusted":true},"outputs":[],"source":["%matplotlib inline\n","# PYTHON CODE\n","# Module imports\n","\n","# Base python\n","import os\n","from os import path\n","from pathlib import Path\n","\n","# Graphical\n","\n","import plotly.graph_objs as go\n","from IPython.display import display, HTML\n","from plotly import __version__\n","from plotly.offline import init_notebook_mode, iplot, plot\n","config={\n"," 'showLink': False,\n"," 'displayModeBar': True,\n"," 'toImageButtonOptions': {\n"," 'format': 'png', # one of png, svg, jpeg, webp\n"," 'filename': 'custom_image',\n"," 'height': 2500,\n"," 'width': 1250,\n"," 'scale': 2 # Multiply title/legend/axis/canvas sizes by this factor\n"," }\n"," }\n","\n","init_notebook_mode(connected=True)\n","\n","from plotly.subplots import make_subplots\n","import plotly.graph_objects as go\n","\n","import seaborn as sns\n","\n","# Set the color palette\n","pal=sns.color_palette()\n","\n","# Imports\n","import warnings\n","warnings.filterwarnings(\"ignore\")\n","\n","## Setup for plots\n","fig = make_subplots(rows=5, cols=2, vertical_spacing = 0.025,\n"," subplot_titles=(\n"," 'sub-01',\n"," 'sub-01',\n"," 'sub-02',\n"," 'sub-02',\n"," 'sub-03',\n"," 'sub-03',\n"," 'sub-04',\n"," 'sub-04',\n"," 'sub-05',\n"," 'sub-05',))\n","\n","t2_datasets={}\n","b1_datasets={}\n","\n","t2_data = []\n","b1_data = []\n","\n","legend_bool = True\n","for subject in subjects:\n"," index = 0\n"," t2_datasets[subject]={}\n"," b1_datasets[subject]={}\n","\n"," for shim_mode in shim_modes:\n"," t2_datasets[subject][shim_mode]={}\n"," b1_datasets[subject][shim_mode]={}\n","\n"," t2_data=go.Line(\n"," x=x_grid,\n"," y=t2_data_plotly[subject][shim_mode][0],\n"," name=shim_mode,\n"," legendgroup=shim_mode,\n"," line=dict(color='rgb'+str(pal[index]), width=3),\n"," showlegend=False\n"," )\n","\n"," b1_data=go.Line(\n"," x=x_grid,\n"," y=b1_data_plotly[subject][shim_mode],\n"," name=shim_mode,\n"," legendgroup=shim_mode,\n"," line=dict(color='rgb'+str(pal[index]), width=3),\n"," showlegend=legend_bool\n"," ) \n","\n"," \n"," t2_datasets[subject][shim_mode]=t2_data\n"," b1_datasets[subject][shim_mode]=b1_data\n","\n"," index += 1\n"," legend_bool=False\n"," \n","\n","index = 1\n","# For z-ordering \n","for subject in subjects:\n"," for shim_mode in shim_modes:\n"," fig.add_trace(\n"," t2_datasets[subject][shim_mode],\n"," row=index, col=1\n"," )\n"," fig.add_trace(\n"," b1_datasets[subject][shim_mode],\n"," row=index, col=2\n"," )\n"," index+=1\n","\n","\n","index = 1\n","for subject in subjects:\n"," if index == 5:\n"," x_title = 'Vertebral Levels'\n"," showticklabels=True\n"," else:\n"," x_title = None\n"," showticklabels=False\n","\n"," fig.update_xaxes(\n"," type=\"linear\",\n"," autorange=True,\n"," title=x_title,\n"," showgrid=True,\n"," gridcolor='rgb(169,169,169)',\n"," tickvals=label_positions,\n"," ticktext=vertebral_levels,\n"," showticklabels=showticklabels,\n"," title_font_family=\"Times New Roman\",\n"," title_font_size = 20,\n"," linecolor='black',\n"," linewidth=2,\n"," tickfont=dict(\n"," family='Times New Roman',\n"," size=16,\n"," ),\n"," row=index, col=1\n"," )\n"," if index == 1:\n"," fig.update_yaxes(\n"," type=\"linear\",\n"," title={\n"," 'text':'CSF/Cord T2*w signal ratio',\n"," 'standoff':0\n"," },\n"," range=[1.05, 1.4],\n"," showgrid=True,\n"," gridcolor='rgb(169,169,169)',\n"," title_font_family=\"Times New Roman\",\n"," title_font_size = 20,\n"," linecolor='black',\n"," linewidth=2,\n"," tickfont=dict(\n"," family='Times New Roman',\n"," size=16,\n"," ),\n"," row=index, col=1\n"," )\n"," else:\n"," fig.update_yaxes(\n"," type=\"linear\",\n"," title={\n"," 'text':'CSF/Cord T2*w signal ratio',\n"," 'standoff':0\n"," },\n"," showgrid=True,\n"," gridcolor='rgb(169,169,169)',\n"," title_font_family=\"Times New Roman\",\n"," title_font_size = 20,\n"," linecolor='black',\n"," linewidth=2,\n"," tickfont=dict(\n"," family='Times New Roman',\n"," size=16,\n"," ),\n"," row=index, col=1\n"," )\n","\n"," fig.update_xaxes(\n"," type=\"linear\",\n"," autorange=True,\n"," title=x_title,\n"," showgrid=True,\n"," gridcolor='rgb(169,169,169)',\n"," tickvals=label_positions,\n"," ticktext=vertebral_levels,\n"," showticklabels=showticklabels,\n"," title_font_family=\"Times New Roman\",\n"," title_font_size = 20,\n"," linecolor='black',\n"," linewidth=2,\n"," tickfont=dict(\n"," family='Times New Roman',\n"," size=16,\n"," ),\n"," row=index, col=2\n"," )\n"," fig.update_yaxes(\n"," type=\"linear\",\n"," title={\n"," 'text':'B1+ efficiency [nT/V]',\n"," 'standoff':0\n"," },\n"," showgrid=True,\n"," gridcolor='rgb(169,169,169)',\n"," title_font_family=\"Times New Roman\",\n"," title_font_size = 20,\n"," linecolor='black',\n"," linewidth=2,\n"," tickfont=dict(\n"," family='Times New Roman',\n"," size=16,\n"," ),\n"," row=index, col=2\n"," )\n","\n"," index+=1\n","\n","fig.update_layout(height=1800, width=900)\n","\n","for i in fig['layout']['annotations']:\n"," i['font'] = dict(size=22, family='Times New Roman')\n","\n","fig.update_layout(legend=dict(\n"," yanchor=\"top\",\n"," y=0.999,\n"," xanchor=\"left\",\n"," x=0.01,\n"," font=dict(\n"," family='Times New Roman',\n"," size=12\n"," ),\n"," bordercolor=\"Black\",\n"," borderwidth=1.5\n"," ),\n"," legend_tracegroupgap=0,\n"," paper_bgcolor='rgb(255, 255, 255)',\n"," plot_bgcolor='rgb(255, 255, 255)',\n",")\n","\n","fig.add_annotation(\n"," dict(\n"," x=-0.06,\n"," y=-0.03,\n"," showarrow=False,\n"," text='A',\n"," font=dict(\n"," family='Times New Roman',\n"," size=48\n"," ),\n"," xref='paper',\n"," yref='paper'\n"," ))\n","fig.add_annotation(\n"," dict(\n"," x=0.5,\n"," y=-0.03,\n"," showarrow=False,\n"," text='B',\n"," font=dict(\n"," family='Times New Roman',\n"," size=48\n"," ),\n"," xref='paper',\n"," yref='paper'\n"," ),\n",")\n","#iplot(fig, filename = 'figure3', config = config)\n","plot(fig, filename = os.path.join(path_results,'figure3.html'), config = config)\n"]},{"cell_type":"code","execution_count":null,"metadata":{"tags":["remove_input","report_output"],"trusted":true},"outputs":[],"source":["display(HTML(os.path.join(path_results,'figure3.html')))"]},{"cell_type":"markdown","metadata":{},"source":["Figure 3. B1+ efficiency (A) and CSF/Cord signal ratio from the GRE scan (B) across subjects and across different RF shimming conditions. Data were measured in the spinal cord from C3 to T2 vertebral levels. To match the x-ticks across subjects, the C2-C3 and the T2-T3 intervertebral discs of each subject were aligned with that of the PAM50 template {cite:p}`DELEENER2018170`, and the curves were linearly scaled.\n"]},{"cell_type":"markdown","metadata":{},"source":["# References \n","\n","```{bibliography}\n",":filter: docname in docnames\n","```"]},{"cell_type":"code","execution_count":null,"metadata":{"tags":["remove_input","remove_output"],"trusted":true},"outputs":[],"source":["# Indicate duration of data processing\n","\n","end_time = datetime.now()\n","total_time = (end_time - start_time).total_seconds()\n","\n","# Convert seconds to a timedelta object\n","total_time_delta = timedelta(seconds=total_time)\n","\n","# Format the timedelta object to a string\n","formatted_time = str(total_time_delta)\n","\n","# Pad the string representation if less than an hour\n","formatted_time = formatted_time.rjust(8, '0')\n","\n","print(f\"Total Runtime [hour:min:sec]: {formatted_time}\")"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":[]}],"metadata":{"celltoolbar":"Tags","kernelspec":{"display_name":"Python 3 (ipykernel)","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.8.10"}},"nbformat":4,"nbformat_minor":4}