|
msl 1.3.0
|
The objects from the previous section provide the building blocks of the sequence kernel, i.e. the part which is run at each repetition. An usual MRI sequence is however made of several nested loops, including e.g the sampling of the k-space and the repetitions, and of options, which are chosen by the user at run-time, e.g. whether a gradient noise is played at the start to warn the subject that the sequence is about to start.
Historically, nested for-loops and if statements were used, which forced a static structure on the execution flow. As has been done in computer graphics with scene graphs (e.g. Open Inventor), Siemens has introduced the SeqLoop to provide a dynamic way to nest the loops and the other control structures. The documentation of the SeqLoop and its successor, the CompositeSeqLoop, is however lacking, regarding both the default structure and ways to modify the execution. msl takes a different approach in which the user defines a sequence graph from scratch: the execution flow is then transparent, and can be easily fine-tuned to the specific sequence.
The sequence graph (more precisely a directed acyclic graph) is a collection of nodes, some of them having children, and some of them being terminals, or leaves.
A simple example, shown below, would be a sequence where an optional gradient noise is played at the beginning if the user selected it in the interface, followed by the averages of the sequence. Inside each average, all lines are sampled, by running the kernel.
msl has various classes for the different kind of nodes, e.g. msl::graph::Node for the root node, msl::graph::If for checking whether the gradient noise should be run, msl::graph::Loop for the two nested loops, etc.
The recursive nature of the graph (a node contains other nodes) implies that nodes are stored as pointers, specifically shared pointers so that the user needs not perform explicit allocations or deallocations. Nodes are not created using their constructor, but using the New function:
The nodes have two main functions, as with the sequence and real-time objects present in IDEA: prepare, in which the user-defined parameters are checked and the static information is updated, and run, in which the effective action of the node is performed.
The nodes of a sequence graph require shared data: this data can come from an external source, e.g. the choices of the user in the interface, or can be an information required by multiple nodes, e.g. the k-space coordinate of the current line. The msl::Dictionary class assigns a generic value to a name, similar to Python dictionaries, and an instance of this is stored in each node, and by default shared across all children of a node.
The following code sample shows how to create an empty dictionary, a boolean value, and retrieve it.
Note that since C++ is a statically typed language, it is necessary to specify the correct type when retrieving an object from a dictionary.
As said above, each node contains a dictionary, known as the registry of this node. Since this registry may be shared across nodes, it is stored as a pointer. It can be accessed through the msl::graph::AbstractNode::registry function, or directly through the msl::graph::AbstractNode::get function:
In addition to the SeqLoop, Siemens also provides Sequence Building Blocks, or SBB: these objects, inheriting from SeqBuildBlock, encapsulate some code, e.g. gradient noise, noise scan, or an excitation RF pulse. msl allows using those objects in a graph, using the msl::graph::Block node, which is templated by the SBB it contains. The constructor of msl::graph::Block simply passes parameters to the underlying SBB, and the SBB can be accessed by the msl::graph::Block::block variable. For example, adding a TokTokTok SBB to the root node is done as follows:
The previous examples plays the TokTokTok SBB unconditionally: since there is a checkbox in the interface to enable or disable this feature, there must be a way to conditionally run a part of the graph. While this can be achieved in the prepare function by fully re-creating the sequence graph, it is easier to use an msl::graph::If node.
The constuctor of the msl::graph::If node takes two parameters: the name of registry key which, and a child or set of children. The registry value associated with the key decides whether or not the child must be run: it can be a boolean, or a function, which is called each time the graph is run.
For the boolean form, there will be a part in the initialize function:
This form will also require a part in the prepare function, in order to update the registry:
Using a boolean requires splitting the condition from its node: this can be solved using the function form of the msl::graph::If node, in which all the code will be in the initialize function:
For more complex functions, the lambda expression can be replaced by a function object or function pointer:
The function may have any of the following prototypes:
bool()bool(msl::Dictionary::Pointer)bool(MrProt &)bool(MrProt &, msl::Dictionary::Pointer)bool(MrProt &, SeqLim &, SeqExpo &)bool(MrProt &, SeqLim &, SeqExpo &, msl::Dictionary::Pointer)This variety of prototypes makes it easier to write condition-function in most circumstances, but creates a rare case in which the type of the object stored in the registry must be explicitely specified (msl::graph::If::Function(runIntro)).
For cases more complex than a simple true/false decision, the msl::graph::Case node may be used in a similar way: instead of a single child, it will have as many children as the case-function may return. Refer to the tests of msl for usage samples.
The main part of an MRI sequence is the nested loops (repetitions, averages, partitions, lines, etc.). msl provides two types of nodes to encapsulate loops
As for the decision nodes, the looping information is stored in a key of the registry. For the msl::graph::Loop node, a msl::Counter object is used: this object contains information about its current value (msl::Counter::index), and about whether the iteration at the first element, at the last element or finished, i.e. past the last element (msl::Counter::first, msl::Counter::last, msl::Counter::done).
These nodes will also hold children, as for the decision nodes: the child nodes will be prepared once, but executed as many times as there are steps in the counter: in the following sample, the kernel node will be prepared once, but run 256 times.
Loops can be nested within other loops, as follows:
A run-time function can be added to the sequence graph using the msl::graph::Action node: its parameters must match one of the list item described above, and its return type must be void. Other than these two constraints, it may modify the registry and perform any kind of action; one common example is to update the MDH (data header) of the readout object at each repetition to correctly fill-in the line and partition indices.
Assuming a read-out object is stored in the registry, the start of the updateMDH function could read:
The action node can then be added to the graph:
The main structure of our tutorial FLASH sequence consists of a loop on all lines (either 2D or 3D) at each iteration of which:
The associated graph is then
The code to create this subset of the graph first creates the loop node, then adds each of the three children.
By nesting calls to the node constructor, it can also be written as a single statement:
Both forms are equivalent, and the choice of one of them only depends on the developer.