RF Pulse Simulator

rf_tools has an RF pulse simulator that computes the rotations produced by your rf pulses.

It does this using spinors, and the Cayley-Klein parameters $\alpha$ and $\beta$. These two complex numbers completely define a rotation.

Once the rotation has been computed, any RF profile can be computed.

First, we need to open the RF toolbox code directory and load the Octave signal toolbox

%use octave

try
    cd rf_tools_octave
catch
    try
        cd ../rf_tools_octave
    catch
        try
            cd ../rf_tools_octave
        catch
            cd ../../rf_tools_octave
        end
    end
end

pkg load signal

Now let’s create an RF pulse

%use octave

rf = msinc(256,2);

This is just a Hamming windowed sinc.

Normalize to $\pi/2$, and plot it. cplot() takes a complex signal and plots both the real and imaginary parts.

%use octave

rf = rf*(pi/2)/sum(rf);

re_rf = real(rf);
im_rf = imag(rf);
%use sos
%get re_rf --from Octave
%get im_rf --from Octave

import matplotlib.pyplot as plt
import plotly.plotly as py
import plotly.graph_objs as go
import numpy as np
from plotly import __version__
from plotly.offline import download_plotlyjs, init_notebook_mode, plot, iplot
config={'showLink': False, 'displayModeBar': False}

init_notebook_mode(connected=True)

from IPython.core.display import display, HTML

re = go.Scatter(
    x = [i for i in range(len(re_rf))],
    y = re_rf,
    name = 'Real',
    text = 'Real',
    hoverinfo = 'x+y+text', 
    line = dict(
        color = ('rgb(22, 96, 167)'),
        ),
)

im = go.Scatter(
    x = [i for i in range(len(im_rf))],
    y = im_rf,
    name = 'Imaginary',
    text = 'Imaginary',
    hoverinfo = 'x+y+text',
    line = dict(
        color = ('rgb(205, 12, 24)'),
        ),
)

data = [re, im]

layout = go.Layout(
    width=600,
    height=350,
    margin=go.layout.Margin(
        l=100,
        r=50,
        b=60,
        t=0,
    ),
    xaxis=dict(
        showgrid=False,
        linecolor='black',
        linewidth=2
    ),
    yaxis=dict(
        showgrid=False,
        linecolor='black',
        linewidth=2
    ),
    legend=dict(
        x=0.65,
        y=0.55,
        traceorder='normal',
        font=dict(
            family='Times New Roman',
            size=12,
            color='#000'
        ),
        bordercolor='#000000',
        borderwidth=2
    )
)

fig = dict(data=data, layout=layout)
plot(fig, filename = 'rf_fig_1.html', config = config)

display(HTML('rf_fig_1.html'))

(View plot code)

Next we need a vector of spatial locations. By default the simulator assumes that the gradient area integrates to 2 pi over the pulse, so the spatial locations are multiples of 2 pi.

%use octave

x = [-64:64]/4;

Then we simulate the result with abr(rf,x). This stands for “alpha-beta rotator”,

%use octave

[a b] = abr(rf,x);

re_a = real(a);
im_a = imag(a);

re_b = real(b);
im_b = imag(b);

The $\alpha$ slice profile mostly characterizes the phase of the profile.

%use sos
%get re_a --from Octave
%get im_a --from Octave

wm = go.Scatter(
    x = [i for i in range(len(re_a))],
    y = re_a,
    name = 'Real',
    text = 'Real',
    hoverinfo = 'x+y+text',
    line = dict(
        color = ('rgb(22, 96, 167)'),
        ),
)

gm = go.Scatter(
    x = [i for i in range(len(im_a))],
    y = im_a,
    name = 'Imaginary',
    text = 'Imaginary',
    hoverinfo = 'x+y+text',
    line = dict(
        color = ('rgb(205, 12, 24)'),
        ),
)



data = [wm, gm]

layout = go.Layout(
    width=600,
    height=350,
    margin=go.layout.Margin(
        l=100,
        r=50,
        b=60,
        t=0,
    ),
    xaxis=dict(
        showgrid=False,
        linecolor='black',
        linewidth=2
    ),
    yaxis=dict(
        showgrid=False,
        linecolor='black',
        linewidth=2
    ),
    annotations=[
        dict(
            x=-0.15,
            y=0.5,
            showarrow=False,
            text='Alpha',
            font=dict(
                family='Times New Roman',
                size=22
            ),
            textangle=-90,
            xref='paper',
            yref='paper'
        ),
    ],
    legend=dict(
        x=0.385,
        y=1.10,
        traceorder='normal',
        font=dict(
            family='Times New Roman',
            size=12,
            color='#000'
        ),
        bordercolor='#000000',
        borderwidth=2
    )
)

fig = dict(data=data, layout=layout)

plot(fig, filename = 'rf_fig_2.html', config = config)

display(HTML('rf_fig_2.html'))

(View plot code)

The $\beta$ profile looks like the slice profile we expect, and has an amplitude of $\sin(\phi/2)$ if $\phi$ is the flip angle. This is a $\pi/2$ pulse so the amplitude is $\sin(\pi/4) = \sqrt{2}/{2}$, or about 0.707. It is negative because we are using right hand rotations, while protons rotate in the negative sense,

%use sos
%get re_b --from Octave
%get im_b --from Octave

re = go.Scatter(
    x = [i for i in range(len(re_b))],
    y = re_b,
    name = 'Real',
    text = 'Real',
    hoverinfo = 'x+y+text',
    line = dict(
        color = ('rgb(22, 96, 167)'),
        ),
)

im = go.Scatter(
    x = [i for i in range(len(im_b))],
    y = im_b,
    name = 'Imaginary',
    text = 'Imaginary',
    hoverinfo = 'x+y+text',
    line = dict(
        color = ('rgb(205, 12, 24)'),
        ),
)



data = [re, im]

layout = go.Layout(
    width=600,
    height=350,
    margin=go.layout.Margin(
        l=100,
        r=50,
        b=60,
        t=0,
    ),
    xaxis=dict(
        showgrid=False,
        linecolor='black',
        linewidth=2
    ),
    yaxis=dict(
        showgrid=False,
        linecolor='black',
        linewidth=2
    ),
    annotations=[
        dict(
            x=-0.15,
            y=0.5,
            showarrow=False,
            text='Beta',
            font=dict(
                family='Times New Roman',
                size=22
            ),
            textangle=-90,
            xref='paper',
            yref='paper'
        ),
    ],
    legend=dict(
        x=0.70,
        y=0.55,
        traceorder='normal',
        font=dict(
            family='Times New Roman',
            size=12,
            color='#000'
        ),
        bordercolor='#000000',
        borderwidth=2
    )
)

fig = dict(data=data, layout=layout)

plot(fig, filename = 'rf_fig_3.html', config = config)

display(HTML('rf_fig_3.html'))

(View plot code)

This is the $\beta$ slice profile. $\alpha$ and $\beta$ determine the rotation produced by the RF pulse for any initial magnetization. We can then compute any slice profile we’d like.

The excitation profile is $M_{xy} = 2 \alpha^*\beta$, and this is computed by ab2ex(a,b),

%use octave

mxy_ex = ab2ex(a,b);

mxy_ex_abs = abs(mxy_ex);
%use sos
%get x --from Octave
%get mxy_ex_abs --from Octave

mag = go.Scatter(
    x = [i for i in range(len(mxy_ex_abs))],
    y = mxy_ex_abs,
    name = 'Slice profile',
    text = 'Slice profile',
    hoverinfo = 'x+y+text',
    line = dict(
        color = ('rgb(22, 96, 167)'),
        ),
)

data = [mag]

layout = go.Layout(
    width=600,
    height=350,
    margin=go.layout.Margin(
        l=100,
        r=50,
        b=60,
        t=40,
    ),
    xaxis=dict(
        showgrid=False,
        linecolor='black',
        linewidth=2
    ),
    yaxis=dict(
        showgrid=False,
        linecolor='black',
        linewidth=2
    ),
    annotations=[
        dict(
            x=0.5004254919715793,
            y=1.15,
            showarrow=False,
            text='Slice profile',
            font=dict(
                family='Times New Roman',
                size=22
            ),
            xref='paper',
            yref='paper'
        ),
    ],
    legend=dict(
        x=0.70,
        y=0.55,
        traceorder='normal',
        font=dict(
            family='Times New Roman',
            size=12,
            color='#000'
        ),
        bordercolor='#000000',
        borderwidth=2
    )
)

fig = dict(data=data, layout=layout)

plot(fig, filename = 'rf_fig_4.html', config = config)

display(HTML('rf_fig_4.html'))

(View plot code)

The inversion profile is $M_z = 1-2\beta\beta^*$, and is computed by ab2inv(a,b),

%use octave

mz_inv = ab2inv(a,b);

re_mz_inv = real(mz_inv);
%use sos
%get x --from Octave
%get re_mz_inv --from Octave

mag = go.Scatter(
    x = [i for i in range(len(re_mz_inv))],
    y = re_mz_inv,
    name = 'Slice profile',
    text = 'Slice profile',
    hoverinfo = 'x+y+text',
    line = dict(
        color = ('rgb(22, 96, 167)'),
        ),
)

data = [mag]

layout = go.Layout(
    width=600,
    height=350,
    margin=go.layout.Margin(
        l=100,
        r=50,
        b=60,
        t=40,
    ),
    xaxis=dict(
        showgrid=False,
        linecolor='black',
        linewidth=2
    ),
    yaxis=dict(
        showgrid=False,
        linecolor='black',
        linewidth=2
    ),
    annotations=[
        dict(
            x=0.5004254919715793,
            y=1.15,
            showarrow=False,
            text='Inversion or Saturation Profile',
            font=dict(
                family='Times New Roman',
                size=22
            ),
            xref='paper',
            yref='paper'
        ),
    ],
    legend=dict(
        x=0.70,
        y=0.55,
        traceorder='normal',
        font=dict(
            family='Times New Roman',
            size=12,
            color='#000'
        ),
        bordercolor='#000000',
        borderwidth=2
    )
)

fig = dict(data=data, layout=layout)

plot(fig, filename = 'rf_fig_5.html', config = config)

display(HTML('rf_fig_5.html'))

(View plot code)

The spin echo profile is $M_{xy} = -i \beta^2$, and is computed by ab2se(a,b),

%use octave

mxy_se = ab2se(a,b);

re_mxy_se = real(mxy_se);
%use sos

%get x --from Octave
%get re_mxy_se --from Octave

mag = go.Scatter(
    x = [i for i in range(len(re_mxy_se))],
    y = re_mxy_se,
    name = 'Slice profile',
    text = 'Slice profile',
    hoverinfo = 'x+y+text',
    line = dict(
        color = ('rgb(22, 96, 167)'),
        ),
)

data = [mag]

layout = go.Layout(
    width=600,
    height=350,
    margin=go.layout.Margin(
        l=100,
        r=50,
        b=60,
        t=40,
    ),
    xaxis=dict(
        showgrid=False,
        linecolor='black',
        linewidth=2
    ),
    yaxis=dict(
        showgrid=False,
        linecolor='black',
        linewidth=2
    ),
    annotations=[
        dict(
            x=0.5004254919715793,
            y=1.15,
            showarrow=False,
            text='Spin-Echo Profile',
            font=dict(
                family='Times New Roman',
                size=22
            ),
            xref='paper',
            yref='paper'
        ),
    ],
    legend=dict(
        x=0.70,
        y=0.55,
        traceorder='normal',
        font=dict(
            family='Times New Roman',
            size=12,
            color='#000'
        ),
        bordercolor='#000000',
        borderwidth=2
    )
)

fig = dict(data=data, layout=layout)

plot(fig, filename = 'rf_fig_6.html', config = config)

display(HTML('rf_fig_6.html'))

(View plot code)

Not too surprising, a $\pi/2$ pulse is not a great spin echo pulse.

Now we just need better RF pulses to simulate! That is next.