Skip to content

Use Koma in Julia Scripts ​

Run the same brain/EPI example as the UI and notebook guides from a Julia script or REPL.

Basic Workflow ​

(You can also go to analog steps using UI)

As a general overview, remember the following workflow steps when using KomaMRI:

  • Loading Simulation Inputs: Scanner, Phantom, Sequence

  • Running Simulation

  • Reconstructing Image using MRIReco

Let's replicate these previous steps in a Julia script. You will obtain the following code, which you can copy and paste into the Julia REPL: Install MAT with pkg> add MAT if needed for the export step.

julia
# Import the package
using KomaMRI, MAT

# Define scanner, object and sequence
sys = Scanner()
obj = brain_phantom2D()
obj.Δw .= 0 # Match the UI's default phantom
seq = PulseDesigner.EPI_example(; sys)

# Define simulation parameters and perform simulation
sim_params = KomaMRICore.default_sim_params() 
raw = simulate(obj, seq, sys; sim_params)

# Perform reconstruction to get the image
rec_params = Dict{Symbol,Any}(:reco => "direct")
result = reconstruct_with_labels(raw; rec_params)
Nx, Ny = raw.params["reconSize"][1:2]
image = reshape(first(result.images).image, Nx, Ny)

# Export raw data and the image
save(ISMRMRDFile(joinpath(tempdir(), "data_raw.mrd")), raw)
MAT.matwrite(joinpath(tempdir(), "data_image.mat"), Dict("image" => image))

Let's go through this script step by step.

Loading Simulation Inputs ​

(You can also go to analog steps using UI)

The inputs of the simulation are created in the following part of the script:

julia
# Define scanner, object and sequence
sys = Scanner()
obj = brain_phantom2D()
obj.Δw .= 0
seq = PulseDesigner.EPI_example(; sys)

Scanner ​

Scanner groups limits, gradient, receiver, and transmitter. Inspect the hardware limits with sys.limits:

julia
julia> sys.limits
HardwareLimits
  B0: Float64 1.5
  B1: Float64 1.0e-5
  Gmax: Float64 0.06
  Smax: Float64 500.0
  ADC_Δt: Float64 2.0e-6
  DUR_Δt: Float64 1.0e-5
  GR_Δt: Float64 1.0e-5
  RF_Δt: Float64 1.0e-6
  RF_ring_down_time: Float64 2.0e-5
  RF_dead_time: Float64 0.0001
  ADC_dead_time: Float64 1.0e-5

Phantom ​

The Phantom struct created in this example represents a slice of a brain. To create it, we use the function brain_phantom2D, which is part of KomaMRIBase. While KomaMRI provides some phantom examples for experimentation, you may also want to create your custom Phantom struct tailored to your specific requirements.

The Phantom struct contains MRI parameters related to the magnetization properties of an object. These parameters include magnetization positions, proton density, relaxation times, off-resonance, among others. For more information about Koma's Phantom and what it can do, as well as how to store and share it, check out the Phantom section. Inspect the first spin's parameters in the Julia REPL:

julia
julia> obj[1]
Phantom{Float64}
  name: String "brain2D_axial"
  x: Array{Float64}((1,)) [-0.084]
  y: Array{Float64}((1,)) [-0.03]
  z: Array{Float64}((1,)) [-0.0]
  ρ: Array{Float64}((1,)) [1.0]
  T1: Array{Float64}((1,)) [0.569]
  T2: Array{Float64}((1,)) [0.329]
  T2s: Array{Float64}((1,)) [0.058]
  Δw: Array{Float64}((1,)) [0.0]
  Dλ1: Array{Float64}((1,)) [0.0]
  Dλ2: Array{Float64}((1,)) [0.0]
  Dθ: Array{Float64}((1,)) [0.0]
  motion: NoMotion NoMotion()

You can also visualize the Phantom struct using the plot_phantom_map function:

julia
julia> plot_phantom_map(obj, :ρ)

To utilize test phantoms included with KomaMRI, navigate to the "examples" folder and use the read_phantom_jemris function to read a phantom in .h5 format. The following steps outline how to do this in Julia:

julia
julia> path_koma = dirname(dirname(pathof(KomaMRI)))
julia> path_sphere = joinpath(path_koma, "examples", "2.phantoms", "sphere_chemical_shift.h5")
julia> sphere = read_phantom_jemris(path_sphere)
julia> plot_phantom_map(sphere, :T2)

This explores sphere without replacing the brain phantom obj used below.

Sequence ​

The Sequence struct in the example represents one of the most basic MRI sequences. It excites the object with a 90° RF pulse and then uses EPI gradients to fill the k-space in a "square" manner. While you may want to create your sequences for experiments, you can always use some of the examples already available in KomaMRI.

In MRI, the sequence must be carefully designed with precise timing to obtain an image. It includes subcomponents such as gradients, radio-frequency excitation signals, and sample acquisition. For more information on constructing a Sequence struct, refer to the Sequence section.

You can view general information about a Sequence struct by displaying it in the Julia REPL:

julia
julia> seq
Sequence[ τ = 66.847 ms | blocks: 204 | ADC: 101 | GR: 205 | RF: 1 | EXT: 0 | DEF: 17 ]

For more precise timing checks, you can use the plot_seq function:

julia
julia> plot_seq(seq; range=[0 30])

It is important to consider how the sequence traverses through k-space. The plot_kspace function does precisely that:

julia
julia> plot_kspace(seq)

For new sequences, use PulseDesigner constructors such as PulseDesigner.make_block_pulse, PulseDesigner.make_sinc_pulse, PulseDesigner.make_trapezoid, and PulseDesigner.make_adc. See Build Sequences with @addblock.

KomaMRI is also compatible with Pulseq. The package installation includes some .seq files in Pulseq format, which can be read and used as a Sequence struct. Here's how to read a spiral Pulseq file stored in the "examples" folder of KomaMRI:

julia
julia> path_koma = dirname(dirname(pathof(KomaMRI)))
julia> path_spiral = joinpath(path_koma, "examples", "1.sequences", "spiral.seq")
julia> spiral = read_seq(path_spiral)
julia> plot_seq(spiral)
julia> plot_kspace(spiral)

This explores spiral without replacing the EPI sequence seq used below.

Running Simulation ​

(You can also go to analog steps using UI)

The following lines in the example script configure and perform the simulation:

julia
# Define simulation parameters and perform simulation
sim_params = KomaMRICore.default_sim_params() 
raw = simulate(obj, seq, sys; sim_params)

Simulation Parameters ​

To perform simulations, KomaMRI requires certain parameters. You can use the default parameters for testing, but you also have the option to customize specific simulation aspects. In the example, we use the KomaMRICore.default_sim_params function to create a dictionary with default simulation parameters. You can view the keys that can be modified by displaying the sim_params variable:

julia
julia> sim_params
Dict{String, Any} with 14 entries:
  "gpu_groupsize_excitation" => 256
  "sim_method"               => Bloch()
  "return_type"              => "raw"
  "gpu_device"               => nothing
  "max_rf_block_length"      => Inf
  "Nthreads"                 => 4
  "gpu_groupsize_precession" => 256
  "Δt"                       => 0.001
  "precision"                => "f32"
  "freq_in_phase"            => false
  "gpu"                      => true
  "preserve_samples"         => (:gradients, :rf_center)
  "Δt_rf"                    => 5.0e-5
  "max_block_length"         => 512

"sampling_rule" controls sequence sampling. The compatibility keys "Δt" and "Δt_rf" specify maximum time steps, not scanner raster times. "return_type" defaults to MRD raw data; "mat" returns a signal array. "gpu" selects GPU use, and "precision" sets floating-point precision. See Simulation Parameters.

Raw Signal ​

The simulation is performed using the simulate function, which requires three arguments: a Scanner struct, a Phantom struct, and a Sequence struct. Optionally, you can include the keyword argument sim_params if you wish to use custom simulation parameters.

In the example, we can see that the output of the simulation is a special struct:

julia
julia> typeof(raw)
RawAcquisitionData

julia> raw
RawAcquisitionData[SeqName: epi | 101 Profile(s) of 101×1]

You can plot the simulation result with the plot_signal function like so:

julia
julia> plot_signal(raw)

Reconstructing Image using MRIReco ​

(You can also go to analog steps using UI)

Use reconstruct_with_labels, the same MRIReco-based reconstruction used by the UI. This example contains one 2D image:

julia
rec_params = Dict{Symbol,Any}(:reco => "direct")
result = reconstruct_with_labels(raw; rec_params)
Nx, Ny = raw.params["reconSize"][1:2]
image = reshape(first(result.images).image, Nx, Ny)

See the reconstruction reference for acquisition labels, coil combination, and reconstruction policies.

Display the image with the same magnitude scaling as the UI:

julia
julia> plot_image(image * (Nx * Ny))

Exporting Results ​

Same outputs: UI.

Save the existing raw data as MRD and the reconstructed image as MAT:

julia
using MAT
save(ISMRMRDFile(joinpath(tempdir(), "data_raw.mrd")), raw)
MAT.matwrite(joinpath(tempdir(), "data_image.mat"), Dict("image" => image))

The files are in tempdir(); replace that path with your output directory.