{ "cells": [ { "cell_type": "markdown", "metadata": { "kernel": "SoS" }, "source": [ "## Benefits and Pitfalls" ] }, { "cell_type": "markdown", "metadata": { "kernel": "SoS" }, "source": [ "

\n", "The conventional inversion recovery experiment is considered the gold standard T1 mapping technique for several reasons. A typical protocol has a long TR value and a sufficient number of inversion times for stable fitting (typically 5 or more) covering the range [0, TR]. It offers a wide dynamic range of signals (up to [-kM0, kM0]), allowing a number of inversion times where high SNR is available to sample the signal recovery curve (Fukushima 1981). T1 maps produced by inversion recovery are largely insensitive to inaccuracies in excitation flip angles and imperfect spoiling (Stikov et al. 2015), as all parameters except TI are constant for each measurement and only a single acquisition is performed (at TI) during each TR. One important pulse sequence design consideration is to avoid acquiring at inversion times where the signal for T1 values of the tissue-of-interest is nulled, as the magnitude images at this TI time will be dominated by Rician noise which can negatively impact the fit under low SNR circumstances (Figure 6). Inversion recovery can also often be acquired using commonly available standard pulse sequences available on most MRI scanners by setting up a customized acquisition protocol, and does not require any additional calibration measurements.\n", "

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

\n", "\n", "Figure 6. Monte Carlo simulations (mean and standard deviation (STD), blue markers) and fitted T1 values (mean and STD, red and green respectively) generated for a T1 value of 900 ms and 5 TI values linearly spaced across the TR (ranging from 1 to 5 s). A bump in T1 STD occurs near TR = 3000 ms, which coincides with the TR where the second TI is located near a null point for this T1 value. \n", "\n", "

" ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "kernel": "Octave", "tags": [ "hidecode" ] }, "outputs": [], "source": [ "%use octave\n", "\n", "% Verbosity level 0 overrides the disp function and supresses warnings.\n", "% Once executed, they cannot be restored in this session\n", "% (kernel needs to be restarted or a new notebook opened.)\n", "VERBOSITY_LEVEL = 0;\n", "\n", "if VERBOSITY_LEVEL==0\n", " % This hack was used to supress outputs from external tools\n", " % in the Jupyter Book.\n", " function disp(x)\n", " end\n", " warning('off','all')\n", "end\n", "\n", "try\n", " cd qMRLab\n", "catch\n", " try\n", " cd ../../../qMRLab\n", " catch\n", " cd ../qMRLab\n", " end\n", "end\n", "\n", "startup\n", "clear all\n", "\n", "%% Setup parameters\n", "% All times are in seconds\n", "% All flip angles are in degrees\n", "\n", "TR_range = 1000:100:5000; % in seconds\n", "\n", "x = struct;\n", "x.T1 = 900; % in seconds\n", "\n", "Opt.SNR = 25;\n", "Opt.M0 = 1;\n", "Opt.FAexcite = 90; % Excitation flip angle\n", "Opt.FAinv = 180; % Inversion flip angle\n", "\n", "%% Monte Carlo data simulation\n", "% Simulate noisy signal data 1,000 time, fit the data, then calculate the means and standard deviations of the data and fitted T1\n", "% Data is calculated by calculating the a and b values of Eq. 4 from the full analytical equations (Eq. 1)\n", "\n", "Model = inversion_recovery; \n", "\n", "for ii = 1:length(TR_range)\n", " Opt.TR = TR_range(ii);\n", " Opt.T1 = x.T1;\n", " TI_lowres(ii,:) = linspace(0.05, Opt.TR, 6)';\n", " Model.Prot.IRData.Mat = [TI_lowres(ii,:)];\n", " [ra,rb] = Model.ComputeRaRb(x,Opt);\n", " x.rb = rb;\n", " x.ra = ra;\n", " for jj = 1:1000\n", " [FitResult{ii,jj}, noisyData{ii,jj}] = Model.Sim_Single_Voxel_Curve(x,Opt,0); \n", " fittedT1(ii,jj) = FitResult{ii,jj}.T1;\n", " noisyData_array(ii,jj,:) = noisyData{ii,jj}.IRData;\n", " end\n", " \n", " for kk=1:length(TI_lowres(ii,:))\n", " data_mean(ii,kk) = mean(noisyData_array(ii,:,kk));\n", " data_std(ii,kk) = std(noisyData_array(ii,:,kk));\n", " end\n", " \n", " T1_mean(ii) = mean(fittedT1(ii,:));\n", " T1_std(ii) = std(fittedT1(ii,:));\n", "end\n", "\n", "%% Calculate the noiseless data at a higher TI resolution to plot the ideal signal curve.\n", "%\n", "\n", "for ii = 1:length(TR_range)\n", " TI_highres(ii,:) = linspace(0.05, TR_range(ii), 500);\n", " Model.Prot.IRData.Mat = [TI_highres(ii,:)];\n", " Opt.TR = TR_range(ii);\n", " Opt.T1 = x.T1;\n", " [ra,rb] = Model.ComputeRaRb(x,Opt);\n", " x.rb = rb;\n", " x.ra = ra;\n", "\n", " data_noiseless(ii,:) = Model.equation(x);\n", "end" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "kernel": "SoS", "tags": [ "hidecode" ] }, "outputs": [ { "data": { "text/html": [ "" ], "text/vnd.plotly.v1+html": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "
" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "%use sos\n", "%get TR_range --from Octave\n", "%get TI_lowres --from Octave\n", "%get TI_highres --from Octave\n", "%get T1_mean --from Octave\n", "%get T1_std --from Octave\n", "%get data_mean --from Octave\n", "%get data_std --from Octave\n", "%get data_noiseless --from Octave\n", "\n", "\n", "import matplotlib.pyplot as plt\n", "import plotly.plotly as py\n", "import plotly.graph_objs as go\n", "import numpy as np\n", "from plotly import __version__\n", "from plotly.offline import download_plotlyjs, init_notebook_mode, plot, iplot\n", "config={'showLink': False, 'displayModeBar': False}\n", "\n", "init_notebook_mode(connected=True)\n", "\n", "from IPython.core.display import display, HTML\n", "\n", "data1 = [dict(\n", " visible = False,\n", " x = np.squeeze(np.asarray(TI_lowres[ii,:])),\n", " y = np.squeeze(np.asarray(data_mean[ii,:])),\n", " error_y=dict(\n", " type='data',\n", " color = ('rgb(22, 96, 167)'),\n", " array=np.squeeze(np.asarray(data_std[ii,:])),\n", " visible=True\n", " ),\n", " line = dict(\n", " color = ('rgb(22, 96, 167)'),\n", " dash = 'dot'),\n", " mode = 'markers',\n", " name = 'Monte Carlo simulated signal',\n", " text = 'Monte Carlo simulated signal',\n", " hoverinfo = 'x+y+text') for ii in range(len(TR_range))]\n", "\n", "data1[28]['visible'] = True\n", "\n", "data2 = [dict(\n", " visible = False,\n", " x = np.squeeze(np.asarray(TI_highres[ii,:])),\n", " y = np.squeeze(np.asarray(data_noiseless[ii,:])),\n", " line = dict(\n", " color = ('rgb(247, 152, 19)'),\n", " ),\n", " name = 'Noiseless signal',\n", " text = 'Noiseless signal',\n", " hoverinfo = 'x+y+text') for ii in range(len(TR_range))]\n", "\n", "data2[28]['visible'] = True\n", "\n", "data_meanT1 = [dict(\n", " visible = False,\n", " x = TR_range,\n", " y = T1_mean,\n", " name = 'Mean T1 (s)',\n", " text = 'Mean T1 (s)',\n", " hoverinfo = 'x+y+text',\n", " xaxis='x2',\n", " yaxis='y2') for ii in range(len(TR_range))]\n", "\n", "data_meanT1[15]['visible'] = True\n", "\n", "data_stdT1 = [dict(\n", " visible = False,\n", " x = TR_range,\n", " y = T1_std,\n", " name = 'STD T1 (s)',\n", " text = 'STD T1 (s)',\n", " hoverinfo = 'x+y+text',\n", " xaxis='x2',\n", " yaxis='y3') for ii in range(len(TR_range))]\n", "\n", "data_stdT1[28]['visible'] = True\n", "\n", "data = data2 + data1 + data_meanT1 + data_stdT1\n", "\n", "steps = []\n", "for i in range(len(TR_range)):\n", " step = dict(\n", " method = 'restyle', \n", " args = ['visible', [False] * len(data1)],\n", " label = str(TR_range[i])\n", " )\n", " step['args'][1][i] = True # Toggle i'th trace to \"visible\"\n", " steps.append(step)\n", "\n", "sliders = [dict(\n", " x = 0,\n", " y = -0.02,\n", " active = 28,\n", " currentvalue = {\"prefix\": \"TR value (ms): \"},\n", " pad = {\"t\": 50, \"b\": 10},\n", " steps = steps\n", ")]\n", "\n", "layout = go.Layout(\n", " width=540,\n", " height=540,\n", " margin = dict(\n", " t=0,\n", " r=25,\n", " b=100,\n", " l=75),\n", " annotations=[\n", " dict(\n", " x=0.5004254919715793,\n", " y=-0.17,\n", " showarrow=False,\n", " text='Inversion Time – TI (ms)',\n", " font=dict(\n", " family='Times New Roman',\n", " size=26\n", " ),\n", " xref='paper',\n", " yref='paper'\n", " ),\n", " dict(\n", " x=-0.15,\n", " y=0.5,\n", " showarrow=False,\n", " text='Signal (magnitude)',\n", " font=dict(\n", " family='Times New Roman',\n", " size=26\n", " ),\n", " textangle=-90,\n", " xref='paper',\n", " yref='paper'\n", " ),\n", " dict(\n", " x=0.76,\n", " y=0.77,\n", " showarrow=False,\n", " text='TR (ms)',\n", " font=dict(\n", " family='Times New Roman',\n", " size=14\n", " ),\n", " xref='paper',\n", " yref='paper'\n", " ),\n", " dict(\n", " x=0.40,\n", " y=0.35,\n", " showarrow=False,\n", " text='Mean T1 (ms)',\n", " font=dict(\n", " family='Times New Roman',\n", " size=14\n", " ),\n", " textangle=-90,\n", " xref='paper',\n", " yref='paper'\n", " ),\n", " dict(\n", " x=1.00,\n", " y=0.35,\n", " showarrow=False,\n", " text='STD T1 (ms)',\n", " font=dict(\n", " family='Times New Roman',\n", " size=14\n", " ),\n", " textangle=-90,\n", " xref='paper',\n", " yref='paper'\n", " )\n", " ],\n", " xaxis=dict(\n", " autorange=False,\n", " range=[0, 5000],\n", " showgrid=False,\n", " linecolor='black',\n", " linewidth=2\n", " ),\n", " yaxis=dict(\n", " autorange=False,\n", " range=[0, 1],\n", " showgrid=False,\n", " linecolor='black',\n", " linewidth=2\n", " ),\n", " xaxis2=dict(\n", " domain=[0.5, 0.90],\n", " anchor='y2',\n", " mirror = True,\n", " side='top',\n", " ticks='inside',\n", " showline=True,\n", " ),\n", " yaxis2=dict(\n", " autorange=False,\n", " range=[500, 1300],\n", " domain=[0.05, 0.65],\n", " anchor='x2',\n", " mirror = True,\n", " ticks='inside',\n", " showline=True,\n", " ),\n", " yaxis3=dict(\n", " autorange=False,\n", " range=[0, 190],\n", " domain=[0.05, 0.65],\n", " anchor='x2',\n", " overlaying='y2',\n", " side='right',\n", " ticks='inside',\n", " ),\n", " legend=dict(\n", " x=0.3,\n", " y=1.35,\n", " traceorder='normal',\n", " font=dict(\n", " family='Times New Roman',\n", " size=12,\n", " color='#000'\n", " ),\n", " bordercolor='#000000',\n", " borderwidth=2\n", " ), \n", " sliders=sliders\n", ")\n", "\n", "fig = dict(data=data, layout=layout)\n", "\n", "plot(fig, filename = 'ir_fig_6.html', config = config)\n", "display(HTML('ir_fig_6.html'))" ] }, { "cell_type": "markdown", "metadata": { "kernel": "Python3" }, "source": [ "

\n", "Despite a widely acknowledged robustness for measuring accurate T1 maps, inversion recovery is not often used in studies. An important drawback of this technique is the need for long TR values, generally on the order of a few T1 for general models (e.g. Eqs. 1 and 4), and up to 5T1 for long TR approximated models (Eq. 3). It takes about to 10-25 minutes to acquire a single-slice T1 map using the inversion recovery technique, as only one TI is acquired per TR (2-5 s) and conventional cartesian gradient readout imaging acquires one phase encode line per excitation (for a total of ~100-200 phase encode lines). The long acquisition time makes it challenging to acquire whole-organ T1 maps in clinically feasible protocol times. Nonetheless, it is useful as a reference measurement for comparisons against other T1 mapping methods, or to acquire a single-slice T1 map of a tissue to get T1 estimates for optimization of other pulse sequences.\n", "

" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "kernel": "Python3" }, "outputs": [], "source": [] } ], "metadata": { "celltoolbar": "Tags", "kernelspec": { "display_name": "SoS", "language": "sos", "name": "sos" }, "language_info": { "codemirror_mode": "sos", "file_extension": ".sos", "mimetype": "text/x-sos", "name": "sos", "nbconvert_exporter": "sos_notebook.converter.SoS_Exporter", "pygments_lexer": "sos" }, "sos": { "kernels": [ [ "Octave", "octave", "Octave", "#dff8fb" ], [ "Python3", "python3", "Python3", "#FFD91A" ], [ "SoS", "sos", "", "" ] ], "panel": { "displayed": true, "height": 0, "style": "side" }, "version": "0.17.2" } }, "nbformat": 4, "nbformat_minor": 2 }