Motion

Koma can easily simulate the effects of motion during acquisitions. As introduced in the previous section, the motion-related information of the phantom is stored in the motion field of its structure.

Koma's motion model has been designed to accomodate a variety of real-world scenarios, including:

  • Patient motion inside a scanner, which may involve simultaneous or sequential translations and rotations of body parts during the acquisition.
  • Myocardial motion, including simulataneous contraction, rotation, torsion, and translation motion within the cardiac cycle.
  • Pseudo-periodic heart patterns, caused by variations in heart rate or arrhythmias that prevent the heart's motion from being perfectly periodic.
  • Flow through blood vessels, where the spin trajectories or fluid fields may have been obtained from Computational Fluid Dynamics (CFD) simulations.
  • Diffusion, which can be modeled, among many other ways, as microscopic Brownian spin trajectories.

... And, ultimately, any type of motion you can think of, no matter how complex!

To handle these scenarios, Koma represents motion as a collection of elementary movements that can be independently configured and combined. This approach allows for the definition of any complex motion pattern, with the ability to specify overlapping time intervals and even model bidirectional motions along predefined trajectories.

Understanding the motion field and its possible values

The motion field within the Phantom struct can take different values depending on whether the phantom is static or dynamic. For static phantoms, the field is set to NoMotion. For dynamic phantoms, the field can be either a Motion or a MotionList struct. A Motion represents a single movement, characterized by an action, a time curve, and a range of affected spins. Regarding the MotionList struct, it is simply a collection of Motion instances, which is useful for defining motion compositions.

struct Phantom{T<:Real}
    (...)
    #Motion
    motion::Union{NoMotion, Motion{T}, MotionList{T}} = NoMotion()
end

NoMotion struct

NoMotion is the default type for static phantoms. Since its structure has no fields, making a phantom static is as simple as:

obj.motion = NoMotion();

Motion struct

The Motion struct contains information about a basic motion, understood as the combination of an action, a time curve and a spins span. This three fields will be described in detail later. Here is an example of how to assign a motion to a phantom in this case:

obj.motion = Motion(Translate(0.0, 0.1, 0.2), TimeRange(0.0, 1.0), AllSpins());
Note

There are Motion constructors that simplify its definition:

obj.motion = Translate(0.0, 0.1, 0.2, TimeRange(0.0, 1.0), AllSpins())

MotionList struct

The MotionList struct contains a single field called motions, which is a vector of Motion instances. This design makes it possible to define both sequential and simultaneous concatenations of motions over time. An example of how this would be used is:

obj.motion = MotionList(
    Motion(Translate(0.0, 0.1, 0.2), TimeRange(0.0, 1.0), AllSpins()),
    Motion(Rotate(0.0, 0.0, 45.0), Periodic(1.0, 0.5), SpinRange(1:1000))
);

The Motion structure and its fields

The Motion struct is the basic building block for defining motion in Koma. As we mentioned earlier, it has three main fields: action, time, and spins. Together, these fields define what the motion is, when it happens, and which spins are involved:

struct Motion{T<:Real}
    action::AbstractAction{T}
    time  ::TimeCurve{T}
    spins ::AbstractSpinSpan
end

The action field

Let's start with the action field, which defines the type and magnitude (i.e., the final state) of the motion. Currently, Koma supports five actions: Translate, Rotate, HeartBeat, Path, and FlowPath. The first three fall under the category of SimpleActions, while the last two belong to the ArbitraryActions. SimpleActions are defined by parameters that are easy to understand and use, such as translation distance, rotation angles, or contraction rates. ArbitraryActions, on the other hand, are more complex and can be defined by a set of spin trajectories.

The time field

The time field defines how the motion behaves over time and must be an instance of the TimeCurve struct, which works similarly to animation curves in video editing, 3D design, or video games. Essentially, it allows you to adjust the "timing" of the motion without affecting its magnitude or other characteristics.

Given an initial and final state (see the action field), time curves allow you to define how the transition between those states should occur. The TimeCurve structure lets you define an animation curve by specifying the coordinates of its points, along with two additional parameters that control its periodicity and pseudo-periodicity:

struct TimeCurve{T<:Real}
    t::AbstractVector{T}
    t_unit::AbstractVector{T}
    periodic::Bool
    periods::Union{T,AbstractVector{T}}
end

This enables you to create any type of curve, and thus, any kind of motion pattern over time.

A full description of this structure, including examples and constructors, can be found in the TimeCurve API reference.

The spins field

Finally, the spins field must be an instance of the AbstractSpinSpan type. It defines which spins in the phantom are affected by the motion, and which of them remain static. This allows you to define motions that only affect a subset of spins, while keeping others unaffected.

See it in action

Now that we have a basic understanding of the motion field and its components, let's see some usage examples. In all cases, we start with the same phantom: a hollow cube with 1 mm side length and 20 µm spin spacing, centered at the origin and aligned with the coordinate axes. To make the motion easier to visualize, each face of the cube is given a different T1 value:

Translation motion

In this first example, we've added a translational motion of -0.5, 0.6, and 0.7 mm along the three spatial directions. The motion lasts for 1 second and affects the entire phantom:

obj.motion = Translate(-5e-4, 6e-4, 7e-4, TimeRange(0.0, 1.0), AllSpins());

Let’s plot this phantom and see how it moves. The time_samples argument specifies the number of time samples to be plotted. You can use the bottom slider to scroll through time and check its exact position at each moment:

p1 = plot_phantom_map(obj, :T1; time_samples=11, height=440)

Rotation motion

In this case, we add a rotational motion to the phantom: 90º around the y-axis and 75º around the z-axis. Like before, the motion lasts for 1 second and affects all spins in the phantom:

obj.motion = Rotate(0.0, 90.0, 75.0, TimeRange(0.0, 1.0), AllSpins());

Adding motion to a phantom subset

Sometimes, you may want to assign motion to just a part of the phantom instead of the whole thing. This can be done using the SpinRange structure, where you specify the indices of the spins that should be affected. In this example, we apply a translational motion to the upper half of the phantom:

obj.motion = Translate(-5e-4, 6e-4, 7e-4, TimeRange(0.0, 1.0), SpinRange(7500:15002));

Motion combination

You can freely add multiple motions to a phantom, each with its own type, time span, and affected spin range. These motions can overlap in time (affecting the phantom simultaneously) or happen one after another. Both cases are fully supported, so you're free to combine different effects across various parts of the phantom and time intervals, creating as complex a motion pattern as you need.

This final example shows two brain phantoms undergoing the same translational and rotational motions, but with different time spans. In the top phantom, the translation takes place from 0 to 0.5 seconds, followed by the rotation from 0.5 to 1 second. In the bottom phantom, both motions happen over the same time span, from 0 to 1 second:

obj1.motion = MotionList(
    Translate(40e-2, 0.0, 0.0, TimeRange(0.0, 0.5),AllSpins()),
    Rotate(0.0, 0.0, 90.0, TimeRange(0.5, 1.0),AllSpins()),
)

obj2.motion = MotionList(
    Translate(40e-2, 0.0, 0.0, TimeRange(0.0, 1.0),AllSpins()),
    Rotate(0.0, 0.0, 90.0, TimeRange(0.0, 1.0),AllSpins()),
)

obj = obj1 + obj2

This page was generated using Literate.jl.