Magnetic Resonance Imaging (MRI) stands as a cornerstone of modern diagnostic medicine, offering unparalleled insight into the human body. The intricate dance of magnetization within tissues, which forms the basis of MRI, is precisely governed by a set of fundamental principles encapsulated in the Bloch equations.

For researchers and practitioners eager to delve deeper into the underlying physics of MRI, simulating these dynamics becomes an invaluable tool. These computational models, often referred to as Bloch simulations, allow for a deeper understanding and prediction of how tissue magnetization responds to various RF pulses and gradients.

This article provides a comprehensive walkthrough on implementing MRI physics simulations using the Julia programming language. Julia, a high-performance, open-source language, has gained significant traction in scientific computing due to its speed and ease of use. While a foundational understanding of the Bloch equations and MRI principles is beneficial, we will guide you through each step, from basic excitation and free precession to a more intricate, realistic simulation.

Setting Up Your Julia Environment

To begin, you\’ll need Julia installed on your system. You can easily download Julia from its official website. For those who prefer to experiment without a local installation, Julia can also be tested directly in your web browser.

Key Mathematical Notation

Understanding the following notation is crucial for grasping the Bloch equations and their simulation:

  • \( \mathbf{M} = [M_x, M_y, M_z] \): The magnetization vector, representing the net magnetic moment of a tissue sample.
  • \( M_{xy} = M_x + i M_y \): The transverse component of magnetization, often complex, where \( i = \sqrt{-1} \).
  • \( \mathbf{M}_0 = [0, 0, M_0] \): The equilibrium magnetization vector, typically aligned with the static magnetic field (z-axis).
  • \( M_0 \): The magnitude of equilibrium magnetization.
  • \( T_1 \): The longitudinal (spin-lattice) relaxation time constant.
  • \( T_2 \): The transverse (spin-spin) relaxation time constant.
  • \( \Delta\omega \): The off-resonance frequency, which causes dephasing in the transverse plane.
  • \( \omega_{1,x} \): Rotational frequency due to the excitation RF field along the x-axis.
  • \( \omega_{1,y} \): Rotational frequency due to the excitation RF field along the y-axis.

The Bloch Equations: Governing MRI Physics

The Bloch equations describe the evolution of the magnetization vector ( \mathbf{M}(t) ) under the influence of RF pulses, relaxation, and off-resonance effects. They are expressed as a system of ordinary differential equations (ODEs):

[
\frac{d}{dt} \mathbf{M}(t) = \begin{bmatrix}
-\frac{1}{T_2} & \Delta\omega & -\omega_{1,y}(t) \
-\Delta\omega & -\frac{1}{T_2} & \omega_{1,x}(t) \
\omega_{1,y}(t) & -\omega_{1,x}(t) & -\frac{1}{T_1}
\end{bmatrix} \mathbf{M}(t) + \begin{bmatrix} 0 \ 0 \ \frac{M_0}{T_1} \end{bmatrix}
]

In practical MRI simulations, simplifying assumptions often lead to analytical, closed-form solutions for specific scenarios, making computations more efficient.

Simulating Excitation

During an RF excitation pulse, it\’s often assumed that the pulse is applied instantaneously compared to relaxation and off-resonance effects. Under this simplification, the Bloch equations reduce to:

[
\frac{d}{dt} \mathbf{M}(t) = \begin{bmatrix}
0 & 0 & 0 \
0 & 0 & \omega_{1,x}(t) \
0 & -\omega_{1,x}(t) & 0
\end{bmatrix} \mathbf{M}(t).
]

Assuming the RF pulse is applied along the x-axis for simplicity, the solution to this ODE is a rotation of the magnetization vector:

[
\mathbf{M}(t_0^{+}) = \begin{bmatrix}
1 & 0 & 0 \
0 & \cos(\alpha) & \sin(\alpha) \
0 & -\sin(\alpha) & \cos(\alpha)
\end{bmatrix} \mathbf{M}(t_0^{-}),
]

where ( \alpha ) represents the flip angle of the pulse.

Julia Implementation: Excitation

Let\’s simulate a 90° excitation pulse on magnetization starting at equilibrium (along the z-axis) in Julia.

julia> M = [0, 0, 1] # Initial magnetization along z-axis
3-element Vector{Int64}:
 0
 0
 1

julia> alpha = π / 2 # 90-degree flip angle (type \pi then tab for π)
1.5707963267948966

julia> R = [1 0 0; 0 cos(alpha) sin(alpha); 0 -sin(alpha) cos(alpha)] # Rotation matrix around x-axis
3x3 Matrix{Float64}:
 1.0   0.0          0.0
 0.0   6.12323e-17  1.0
 0.0  -1.0          6.12323e-17

julia> M_ex = R * M # Apply excitation
3-element Vector{Float64}:
 0.0
 1.0
 6.123233995736766e-17

As expected, applying a 90° pulse to magnetization along the z-axis rotates it into the transverse (x-y) plane, specifically along the y-axis in this case (the small non-zero z-component is due to floating-point precision).

Simulating Free Precession

When the RF pulse is off, magnetization undergoes “free precession.” This period is characterized by off-resonance frequency effects, T1 relaxation (recovery to longitudinal equilibrium), and T2 decay (loss of transverse magnetization). The Bloch equations for free precession become:

[
\frac{d}{dt} \mathbf{M}(t) = \begin{bmatrix}
-\frac{1}{T_2} & \Delta\omega & 0 \
-\Delta\omega & -\frac{1}{T_2} & 0 \
0 & 0 & -\frac{1}{T_1}
\end{bmatrix} \mathbf{M}(t) + \begin{bmatrix} 0 \ 0 \ \frac{M_0}{T_1} \end{bmatrix}.
]

The solution to this ODE over a time interval ( \Delta t = t – t_0 ) is given by a matrix multiplication:

[
\begin{align}
\mathbf{M}(t) &= \mathbf{M}_0 + \mathbf{E}(\Delta t) \mathbf{F}(\Delta t) (\mathbf{M}(t_0) – \mathbf{M}_0) \
\mathbf{E}(\Delta t) &= \begin{bmatrix}
e^{-\Delta t/T_2} & 0 & 0 \ 0 & e^{-\Delta t/T_2} & 0 \ 0 & 0 & e^{-\Delta t/T_1}
\end{bmatrix} \
\mathbf{F}(\Delta t) &= \begin{bmatrix}
\cos(\Delta\omega \Delta t) & \sin(\Delta\omega \Delta t) & 0 \
-\sin(\Delta\omega \Delta t) & \cos(\Delta\omega \Delta t) & 0 \
0 & 0 & 1
\end{bmatrix}
\end{align}
]

Here, ( \mathbf{E} ) accounts for T1 and T2 relaxation, while ( \mathbf{F} ) describes the rotation due to off-resonance frequency.

Julia Implementation: Free Precession

Let\’s simulate free precession for a magnetization vector initially along the y-axis, using typical tissue parameters.

julia> M = [0, 1, 0] # Initial magnetization along y-axis
3-element Vector{Int64}:
 0
 1
 0

julia> (M0, T1, T2, dw, dt) = (1, 1, 0.1, 25π, 0.01) # M0=1, T1=1s, T2=0.1s, off-resonance=25π rad/s, time step=0.01s
(1, 1, 0.1, 78.53981633974483, 0.01)

julia> M0_vec = [0, 0, M0] # Equilibrium magnetization vector
3-element Vector{Int64}:
 0
 0
 1

julia> E = [exp(-dt/T2) 0 0; 0 exp(-dt/T2) 0; 0 0 exp(-dt/T1)] # Relaxation matrix
3x3 Matrix{Float64}:
 0.904837  0.0       0.0
 0.0       0.904837  0.0
 0.0       0.0       0.99005

julia> phi = dw * dt; F = [cos(phi) sin(phi) 0; -sin(phi) cos(phi) 0; 0 0 1] # Precession matrix
3x3 Matrix{Float64}:
  0.707107  0.707107  0.0
 -0.707107  0.707107  0.0
  0.0       0.0       1.0

julia> M_fp = M0_vec + E * F * (M - M0_vec) # Apply free precession
3-element Vector{Float64}:
 0.6398166741645539
 0.639816674164554
 0.009950166250831893

The magnetization vector has evolved, showing the effects of dephasing and initial recovery towards equilibrium.

A More Realistic Bloch Simulation

To simulate MRI physics more accurately, especially for excitation pulses that are not truly instantaneous, we can combine these two building blocks. A common approach for extended RF pulses is to break them down into many small time segments ( \Delta t ). For each segment, the simulation proceeds as follows:

  • First, simulate free precession for a duration of \( \Delta t / 2 \).
  • Then, apply an instantaneous RF pulse, where the flip angle is determined by the RF waveform\’s amplitude during that segment.
  • Finally, simulate free precession again for another \( \Delta t / 2 \).

This “slice-by-slice” approach allows us to model continuous RF waveforms more precisely.

Let\’s simulate a 1 ms excitation pulse followed by 9 ms of free precession, with a fine time step of 10 μs.

julia> (t_ex, t_fp, dt) = (0.001, 0.009, 0.00001) # Excitation time, free precession time, time step
(0.001, 0.009, 1.0e-5)

julia> t_total = t_ex + t_fp
0.009999999999999998

julia> nsteps = round(Int, t_total / dt) # Total number of simulation steps
1000

julia> M_history = Vector{Vector{Float64}}(undef, nsteps + 1) # Pre-allocate storage for magnetization history
1001-element Vector{Vector{Float64}}:
 #undef
 
 #undef

Next, we define our RF pulse. We\’ll use a sinc pulse, which is common in MRI, scaled to achieve a 90° flip angle.

julia> nrf = round(Int, t_ex / dt) # Number of RF pulse steps
100

julia> rf_waveform = sinc.(range(-3, 3, nrf)); # Generate sinc waveform

julia> rf_waveform .*= π/2 / sum(rf_waveform); # Scale for 90-degree flip angle

Initialize magnetization at equilibrium and define constants:

julia> (M0, T1, T2, dw) = (1, 1, 0.1, 25π)
(1, 1, 0.1, 78.53981633974483)

julia> M_history[1] = [0, 0, M0] # Starting at equilibrium
3-element Vector{Int64}:
 0
 0
 1

To streamline the simulation, we\’ll encapsulate our excitation and free precession logic into functions:

julia> function apply_excitation_segment(current_M, alpha_segment, M0, T1, T2, dw, segment_dt)

           # Free precession for dt/2
           M_pre_rf = apply_freeprecession(current_M, M0, T1, T2, dw, segment_dt/2)
           
           # Instantaneous RF rotation (assuming x-axis excitation)
           R_rf = [1 0 0; 0 cos(alpha_segment) sin(alpha_segment); 0 -sin(alpha_segment) cos(alpha_segment)]
           M_post_rf = R_rf * M_pre_rf
           
           # Free precession for another dt/2
           M_next = apply_freeprecession(M_post_rf, M0, T1, T2, dw, segment_dt/2)

           return M_next

       end
apply_excitation_segment (generic function with 1 method)

julia> function apply_freeprecession(current_M, M0, T1, T2, dw, delta_t)

           M0_vec = [0, 0, M0]
           E_matrix = [exp(-delta_t/T2) 0 0; 0 exp(-delta_t/T2) 0; 0 0 exp(-delta_t/T1)]
           phi_rotation = dw * delta_t
           F_matrix = [cos(phi_rotation) sin(phi_rotation) 0; -sin(phi_rotation) cos(phi_rotation) 0; 0 0 1]
           M_next = M0_vec + E_matrix * F_matrix * (current_M - M0_vec)

           return M_next

       end
apply_freeprecession (generic function with 1 method)

Now, we execute the full simulation loop:

julia> for i = 1:nsteps
           if i <= nrf # During the RF pulse duration
               M_history[i+1] = apply_excitation_segment(M_history[i], rf_waveform[i], M0, T1, T2, dw, dt)
           else # After the RF pulse, pure free precession
               M_history[i+1] = apply_freeprecession(M_history[i], M0, T1, T2, dw, dt)
           end
       end

This simulation provides a detailed trajectory of the magnetization vector under a combined excitation and free precession sequence.

Visualizing the Simulation Results

To interpret our simulation, plotting the magnetization components over time is essential. We\’ll use the Plots.jl package in Julia.

julia> using Plots

julia> time_ms = (0:nsteps) .* (dt * 1000) # Time in milliseconds
0.0:0.01:10.0

julia> full_rf_waveform = [0; rf_waveform; zeros(nsteps - nrf)]; # Pad RF waveform for plotting

julia> p_rf = plot(time_ms, full_rf_waveform; label = "RF Pulse Amplitude", xlabel = "Time (ms)", ylabel = "Flip Angle (rad)");

julia> Mx_vals = getindex.(M_history, 1); My_vals = getindex.(M_history, 2); Mz_vals = getindex.(M_history, 3);

julia> p_Mxyz = plot(time_ms, Mx_vals; label = "Mx", xlabel = "Time (ms)", ylabel = "Magnetization");

julia> plot!(p_Mxyz, time_ms, My_vals; label = "My");

julia> plot!(p_Mxyz, time_ms, Mz_vals; label = "Mz");

julia> plot(p_rf, p_Mxyz; layout = (2, 1), size = (800, 600))

Magnetization over time

We can also analyze the transverse magnetization ( M_{xy} = M_x + i M_y ), specifically its magnitude and phase, which are crucial for understanding the MRI signal.

julia> Mxy_vals = complex.(Mx_vals, My_vals);

julia> p_mag = plot(time_ms, abs.(Mxy_vals); label = "|Mxy|", xlabel = "Time (ms)", ylabel = "Magnitude", color = 1);

# To type ∠, type \angle<tab>
julia> p_phase = plot(time_ms, angle.(Mxy_vals); label = "∠Mxy", xlabel = "Time (ms)", ylabel = "Phase (rad)", color = 2);

julia> plot(p_mag, p_phase; layout = (2, 1), size = (800, 600))

Magnitude and phase

These plots vividly demonstrate the characteristic T2 decay (loss of transverse magnitude), off-resonance precession (linear change in phase), and T1 recovery (return of Mz to M0) of the magnetization vector, confirming a successful Bloch simulation.

To observe the magnetization returning significantly closer to equilibrium, you can extend the free precession duration (e.g., to 3 * T1). For longer simulations, it\’s practical to use larger time steps during the free precession phase than during the RF pulse, as the dynamics are slower (the example image below uses a 1 ms time step for free precession for this longer duration).

Magnetization returning to equilibrium

Conclusion

This article has provided a comprehensive journey into the world of MRI Bloch simulations using the Julia programming language. We\’ve dissected the fundamental Bloch equations, implemented the core concepts of excitation and free precession, and built a more realistic simulation pipeline. The ability to simulate and visualize these complex physical phenomena empowers a deeper understanding of MRI pulse sequences and tissue behavior.

Further Resources

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.
You need to agree with the terms to proceed