{ "cells": [ { "cell_type": "markdown", "metadata": { "kernel": "SoS" }, "source": [ "## Signal Modelling\n", "\n", "

\n", "The steady-state longitudinal magnetization of an ideal variable flip angle experiment can be analytically solved from the Bloch equations for the spoiled gradient echo pulse sequence {θn–TR}:\n", "

\n", "\n", "

\n", "

\n", "

\n", "\n", "

\n", "where Mz is the longitudinal magnetization, M0 is the magnetization at thermal equilibrium, TR is the pulse sequence repetition time (Figure 1), and θn is the excitation flip angle. The Mz curves of different T1 values for a range of θn and TR values are shown in Figure 2.\n", "

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

\n", "\n", "Figure 2. Variable flip angle technique signal curves (Eq. 1) for three different T1 values, approximating the main types of tissue in the brain at 3T.\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 milliseconds\n", "% All flip angles are in degrees\n", "\n", "TR_range = 5:5:200;\n", "\n", "params.EXC_FA = 1:90;\n", "\n", "%% Calculate signals\n", "%\n", "% To see all the options available, run `help vfa_t1.analytical_solution`\n", "\n", "for ii = 1:length(TR_range)\n", " params.TR = TR_range(ii);\n", " \n", " % White matter\n", " params.T1 = 900; % in milliseconds\n", "\n", " signal_WM(ii,:) = vfa_t1.analytical_solution(params);\n", "\n", " % Grey matter\n", " params.T1 = 1500; % in milliseconds\n", " signal_GM(ii,:) = vfa_t1.analytical_solution(params);\n", "\n", " % CSF\n", " params.T1 = 4000; % in milliseconds\n", " signal_CSF(ii,:) = vfa_t1.analytical_solution(params);\n", "end\n", "\n" ] }, { "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 params --from Octave\n", "%get TR_range --from Octave\n", "%get signal_WM --from Octave\n", "%get signal_GM --from Octave\n", "%get signal_CSF --from Octave\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", " mode = 'lines',\n", " x = params[\"EXC_FA\"],\n", " y = abs(np.squeeze(np.asarray(signal_WM[ii]))),\n", " name = 'T1 = 0.9 s (White Matter)',\n", " text = 'T1 = 0.9 s (White Matter)',\n", " hoverinfo = 'x+y+text') for ii in range(len(TR_range))]\n", "\n", "data1[4]['visible'] = True\n", "\n", "data2 = [dict(\n", " visible = False,\n", " mode = 'lines',\n", " x = params[\"EXC_FA\"],\n", " y = abs(np.squeeze(np.asarray(signal_GM[ii]))),\n", " name = 'T1 = 1.5 s (Grey Matter)',\n", " text = 'T1 = 1.5 s (Grey Matter)',\n", " hoverinfo = 'x+y+text') for ii in range(len(TR_range))]\n", "\n", "data2[4]['visible'] = True\n", "\n", "data3 = [dict(\n", " visible = False,\n", " mode = 'lines',\n", " x = params[\"EXC_FA\"],\n", " y = abs(np.squeeze(np.asarray(signal_CSF[ii]))),\n", " name = 'T1 = 4.0 s (Cerebrospinal Fluid)',\n", " text = 'T1 = 4.0 s (Cerebrospinal Fluid)',\n", " hoverinfo = 'x+y+text') for ii in range(len(TR_range))]\n", "\n", "data3[4]['visible'] = True\n", "\n", "data = data1 + data2 + data3\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 = 2,\n", " currentvalue = {\"prefix\": \"TR value (ms): \"},\n", " pad = {\"t\": 50, \"b\": 10},\n", " steps = steps\n", ")]\n", "\n", "layout = go.Layout(\n", " width=580,\n", " height=450,\n", " margin=go.layout.Margin(\n", " l=80,\n", " r=40,\n", " b=60,\n", " t=10,\n", " ),\n", " annotations=[\n", " dict(\n", " x=0.5004254919715793,\n", " y=-0.18,\n", " showarrow=False,\n", " text='Excitation Flip Angle (°)',\n", " font=dict(\n", " family='Times New Roman',\n", " size=22\n", " ),\n", " xref='paper',\n", " yref='paper'\n", " ),\n", " dict(\n", " x=-0.15,\n", " y=0.5,\n", " showarrow=False,\n", " text='Long. Magnetization (Mz)',\n", " font=dict(\n", " family='Times New Roman',\n", " size=22\n", " ),\n", " textangle=-90,\n", " xref='paper',\n", " yref='paper'\n", " ),\n", " ],\n", " xaxis=dict(\n", " autorange=False,\n", " range=[0, params['EXC_FA'][-1]],\n", " showgrid=False,\n", " linecolor='black',\n", " linewidth=2\n", " ),\n", " yaxis=dict(\n", " autorange=True,\n", " showgrid=False,\n", " linecolor='black',\n", " linewidth=2\n", " ),\n", " legend=dict(\n", " x=0.5,\n", " y=0.9,\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 = 'vfa_fig_2.html', config = config)\n", "display(HTML('vfa_fig_2.html'))\n" ] }, { "cell_type": "markdown", "metadata": { "kernel": "Python3" }, "source": [ "

\n", "From Figure 2, it is clearly seen that the flip angle at which the steady-state signal is maximized is dependent on the T1 and TR values. This flip angle is a well known quantity, called the Ernst angle (Ernst & Anderson 1966), which can be solved analytically from Equation 1 using properties of calculus:\n", "

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

\n", "

\n", "

\n", "\n", "

\n", "The closed-form solution (Equation 1) makes several assumptions which in practice may not always hold true if care is not taken. Mainly, it is assumed that the longitudinal magnetization has reached a steady state after a large number of TRs, and that the transverse magnetization is perfectly spoiled at the end of each TR. Bloch simulations – a numerical approach at solving the Bloch equations for a set of spins at each time point – provide a more realistic estimate of the signal if the number of repetition times is small (i.e. a steady-state is not achieved). As can be seen from Figure 3, the number of repetitions required to reach a steady state not only depends on T1, but also on the flip angle; flip angles near the Ernst angle need more TRs to reach a steady state. Preparation pulses or an outward-in k-space acquisition pattern are typically sufficient to reach a steady state by the time that the center of k-space is acquired, which is where most of the image contrast resides.\n", "

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

\n", "\n", "Figure 3. Signal curves simulated using Bloch simulations (orange) for a number of repetitions ranging from 1 to 150, plotted against the ideal case (Equation 1 – blue). Simulation details: TR = 25 ms, T1 = 900 ms, 100 spins. Ideal spoiling was used for this set of Bloch simulations (transverse magnetization was set to 0 at the end of each TR).\n", "\n", "

" ] }, { "cell_type": "code", "execution_count": 3, "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 milliseconds\n", "% All flip angles are in degrees\n", "\n", "% White matter\n", "params.T1 = 900; % in milliseconds\n", "params.T2 = 10000;\n", "params.TR = 25;\n", "params.TE = 5;\n", "params.EXC_FA = 1:90;\n", "Nex_range = 1:1:150;\n", "\n", "%% Calculate signals\n", "%\n", "% To see all the options available, run `help vfa_t1.analytical_solution`\n", "\n", "for ii = 1:length(Nex_range)\n", " params.Nex = Nex_range(ii);\n", " \n", " signal_analytical(ii,:) = vfa_t1.analytical_solution(params);\n", "\n", " [~, complex_signal] = vfa_t1.bloch_sim(params);\n", " signal_blochsim(ii,:) = abs(complex(complex_signal));\n", "end\n", "\n" ] }, { "cell_type": "code", "execution_count": 4, "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 params --from Octave\n", "%get Nex_range --from Octave\n", "%get signal_analytical --from Octave\n", "%get signal_blochsim --from Octave\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", " mode = 'lines',\n", " x = params[\"EXC_FA\"],\n", " y = abs(np.squeeze(np.asarray(signal_analytical[ii]))),\n", " name = 'Analytical Solution',\n", " text = 'Analytical Solution',\n", " hoverinfo = 'x+y+text') for ii in range(len(Nex_range))]\n", "\n", "data1[49]['visible'] = True\n", "\n", "data2 = [dict(\n", " visible = False,\n", " mode = 'lines',\n", " x = params[\"EXC_FA\"],\n", " y = abs(np.squeeze(np.asarray(signal_blochsim[ii]))),\n", " name = 'Bloch Simulation',\n", " text = 'Bloch Simulation',\n", " hoverinfo = 'x+y+text') for ii in range(len(Nex_range))]\n", "\n", "data2[49]['visible'] = True\n", "\n", "data = data1 + data2\n", "\n", "steps = []\n", "for i in range(len(Nex_range)):\n", " step = dict(\n", " method = 'restyle', \n", " args = ['visible', [False] * len(data1)],\n", " label = str(Nex_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 = 49,\n", " currentvalue = {\"prefix\": \"nth TR: \"},\n", " pad = {\"t\": 50, \"b\": 10},\n", " steps = steps\n", ")]\n", "\n", "layout = go.Layout(\n", " width=580,\n", " height=450,\n", " margin=go.layout.Margin(\n", " l=80,\n", " r=40,\n", " b=60,\n", " t=10,\n", " ),\n", " annotations=[\n", " dict(\n", " x=0.5004254919715793,\n", " y=-0.18,\n", " showarrow=False,\n", " text='Excitation Flip Angle (°)',\n", " font=dict(\n", " family='Times New Roman',\n", " size=22\n", " ),\n", " xref='paper',\n", " yref='paper'\n", " ),\n", " dict(\n", " x=-0.15,\n", " y=0.5,\n", " showarrow=False,\n", " text='Signal',\n", " font=dict(\n", " family='Times New Roman',\n", " size=22\n", " ),\n", " textangle=-90,\n", " xref='paper',\n", " yref='paper'\n", " ),\n", " ],\n", " xaxis=dict(\n", " autorange=False,\n", " range=[0, params['EXC_FA'][-1]],\n", " showgrid=False,\n", " linecolor='black',\n", " linewidth=2\n", " ),\n", " yaxis=dict(\n", " autorange=True,\n", " showgrid=False,\n", " linecolor='black',\n", " linewidth=2\n", " ),\n", " legend=dict(\n", " x=0.5,\n", " y=0.9,\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 = 'vfa_fig_3.html', config = config)\n", "display(HTML('vfa_fig_3.html'))\n" ] }, { "cell_type": "markdown", "metadata": { "kernel": "Python3" }, "source": [ "

\n", "Sufficient spoiling is likely the most challenging parameter to control for in a VFA experiment. A combination of both gradient spoiling and RF phase spoiling (Zur et al. 1991; Bernstein et al. 2004) are typically recommended (Figure 4). It has also been shown that the use of very strong gradients, introduces diffusion effects (not considered in Figure 4), further improving the spoiling efficacy in the VFA pulse sequence (Yarnykh 2010).\n", "

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

\n", "\n", "Figure 4. Signal curves estimated using Bloch simulations for three categories of signal spoiling: (1) ideal spoiling (blue), gradient & RF Spoiling (orange), and no spoiling (green). Simulations details: TR = 25 ms, T1 = 900 ms, Te = 100 ms, TE = 5 ms, 100 spins. For the ideal spoiling case, the transverse magnetization is set to zero at the end of each TR. For the gradient & RF spoiling case, each spin is rotated by different increments of phase (2𝜋 / # of spins) to simulate complete decoherence from gradient spoiling, and the RF phase of the excitation pulse is ɸn = ɸn-1 + nɸ0 = ½ ɸ0(n2 + n + 2) (Bernstein et al. 2004) with ɸ0 = 117° (Zur et al. 1991) after each TR.\n", "\n", "

" ] }, { "cell_type": "code", "execution_count": 5, "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 milliseconds\n", "% All flip angles are in degrees\n", "\n", "% White matter\n", "params.T1 = 900; % in milliseconds\n", "params.T2 = 100;\n", "params.TR = 25;\n", "params.TE = 5;\n", "params.EXC_FA = 1:90;\n", "Nex_range = [1:9, 10:10:100];\n", "\n", "%% Calculate signals\n", "%\n", "% To see all the options available, run `help vfa_t1.analytical_solution`\n", "\n", "for ii = 1:length(Nex_range)\n", " params.Nex = Nex_range(ii);\n", " \n", " params.crushFlag = 1;\n", " \n", " [~, complex_signal] = vfa_t1.bloch_sim(params);\n", " signal_ideal_spoil(ii,:) = abs(complex_signal);\n", " \n", " \n", " params.inc = 117;\n", " params.partialDephasing = 1;\n", " params.partialDephasingFlag = 1;\n", " params.crushFlag = 0;\n", " \n", " [~, complex_signal] = vfa_t1.bloch_sim(params);\n", " signal_optimal_crush_and_rf_spoil(ii,:) = abs(complex_signal);\n", " \n", " params.inc = 0;\n", " params.partialDephasing = 0;\n", "\n", " [~, complex_signal] = vfa_t1.bloch_sim(params);\n", " signal_no_gradient_and_rf_spoil(ii,:) = abs(complex_signal);\n", "end\n", "\n" ] }, { "cell_type": "code", "execution_count": 6, "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 params --from Octave\n", "%get Nex_range --from Octave\n", "%get signal_ideal_spoil --from Octave\n", "%get signal_optimal_crush_and_rf_spoil --from Octave\n", "%get signal_no_gradient_and_rf_spoil --from Octave\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", " mode = 'lines',\n", " x = params[\"EXC_FA\"],\n", " y = abs(np.squeeze(np.asarray(signal_ideal_spoil[ii]))),\n", " name = 'Ideal Spoiling',\n", " text = 'Ideal Spoiling',\n", " hoverinfo = 'x+y+text') for ii in range(len(Nex_range))]\n", "\n", "data1[10]['visible'] = True\n", "\n", "data2 = [dict(\n", " visible = False,\n", " mode = 'lines',\n", " x = params[\"EXC_FA\"],\n", " y = abs(np.squeeze(np.asarray(signal_optimal_crush_and_rf_spoil[ii]))),\n", " name = 'Gradient & RF Spoiling',\n", " text = 'Gradient & RF Spoiling',\n", " hoverinfo = 'x+y+text') for ii in range(len(Nex_range))]\n", "\n", "data2[10]['visible'] = True\n", "\n", "data3 = [dict(\n", " visible = False,\n", " mode = 'lines',\n", " x = params[\"EXC_FA\"],\n", " y = abs(np.squeeze(np.asarray(signal_no_gradient_and_rf_spoil[ii]))),\n", " name = 'No Spoiling',\n", " text = 'No Spoiling',\n", " hoverinfo = 'x+y+text') for ii in range(len(Nex_range))]\n", "\n", "data3[10]['visible'] = True\n", "\n", "data = data1 + data2+ data3\n", "\n", "steps = []\n", "for i in range(len(Nex_range)):\n", " step = dict(\n", " method = 'restyle', \n", " args = ['visible', [False] * len(data1)],\n", " label = str(Nex_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 = 10,\n", " currentvalue = {\"prefix\": \"nth TR: \"},\n", " pad = {\"t\": 50, \"b\": 10},\n", " steps = steps\n", ")]\n", "\n", "layout = go.Layout(\n", " width=580,\n", " height=450,\n", " margin=go.layout.Margin(\n", " l=80,\n", " r=40,\n", " b=60,\n", " t=10,\n", " ),\n", " annotations=[\n", " dict(\n", " x=0.5004254919715793,\n", " y=-0.18,\n", " showarrow=False,\n", " text='Excitation Flip Angle (°)',\n", " font=dict(\n", " family='Times New Roman',\n", " size=22\n", " ),\n", " xref='paper',\n", " yref='paper'\n", " ),\n", " dict(\n", " x=-0.15,\n", " y=0.5,\n", " showarrow=False,\n", " text='Signal',\n", " font=dict(\n", " family='Times New Roman',\n", " size=22\n", " ),\n", " textangle=-90,\n", " xref='paper',\n", " yref='paper'\n", " ),\n", " ],\n", " xaxis=dict(\n", " autorange=False,\n", " range=[0, params['EXC_FA'][-1]],\n", " showgrid=False,\n", " linecolor='black',\n", " linewidth=2\n", " ),\n", " yaxis=dict(\n", " autorange=True,\n", " showgrid=False,\n", " linecolor='black',\n", " linewidth=2\n", " ),\n", " legend=dict(\n", " x=0.5,\n", " y=0.9,\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 = 'vfa_fig_4.html', config = config)\n", "display(HTML('vfa_fig_4.html'))\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 }