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());
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.