|
msl 1.3.0
|
At the heart of any MRI sequence is the repetition of operations, for example RF pulses and gradient pulses at each k-space trajectory, or averaging to increase the SNR. These sets of repetitions will also be coupled with decisions of whether to execute an optional part of the sequence, for example, the introductory sound or to perform a noise measurement for GRAPPA. Although this can be written out explicitely using nested for loops and if conditions, it is easier to provide the user with a set of basic features which said user can arrange to their wishes in a sequence graph. In this graph, each node represents one action (for example looping, conditional execution, or playing a gradient pulse). 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, which contains the RF and gradient pulses.
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. Some nodes will require data to perform their action: the averages loop requires a counter, and the lines loop requires a set of trajectories. This data is stored in msl::Dictionary, as presented in the previous parts, shared by all nodes of a graph. The user then only needs to store data in a single space, and each node can pick the data it needs.
To translate the previous graph in msl code, we start with the msl::Sequence class, which already contains the root node of the sequence graph, in the _root member variable. This variable, as well as all other nodes, is stored as a pointer, and not as a value: this is mandated by the way the C++ language handles recursive structures. These pointers are however smart pointers, so that the user does not need to explicitely manage the memory.
The msl::graph::Node class, the type of the _root node is simply designed to group its children together. Besides using is as the root of the graph, it is also useful to group together and re-use parts of the graph, as will be shown later.
All nodes have two main entry points, following the usual IDEA API:
In the case of msl::graph::Node, both functions simply call the respective functions of all its children. Since we have added no node to the graph, both prepare and run return immediately.
The left branch of the previous graph contains an action, labeled gradient noise: if the user check the Introduction checkbox in the user interface, the sequence will play short gradient pulses which sounding like a knock on a door to warn the subject that a sequence is about to begin; in IDEA, this is known as the TokTokTok.
This TokTokTok is provided by IDEA in what is known as a SeqBuildBlock, or SBB: a set of RF and gradient pulses designed for a specific goal. In msl, these SBBs can be added to the graph using a msl::graph::Block node. As for all SBBs, and as for many other objects, a slice definition is required to, among other things, define the transformation from the image coordinates to the gradient coordinates.
These slices are stored in the registry, using an object of type msl::SliceConstIterator, which stores a sequence of slices, and provides a mean to iterate on them. Add a new item to the registry in the initialize function:
In the prepare function, you then need to initialize this object from the data contained in the protocol:
With the slices configured, the TokTokTok can be added to the graph in the initialize function, for now as the single child of the _root node.
The sequence graph then needs to be prepared: it will be performed twice, once for each sampling. Add the following to the prepare function:
If you compile your sequence and simulated it (sim command), it will show the three short gradients lobe generating the TokTokTok sound. If you simulate it with the Introduction checkbox unchecked in POET, those gradient lobes will not be visible.
The TokTokTok block provided in IDEA checks whether the Introduction option is checked in the user interface. We are now going to add a second SBB, linked with GRAPPA, namely a noise scan. Several noise scan strategies can be used, here we will integrate it with each run of the sequence: to do so, we will use a SeqBuildBlockNoiseMeas object, wrapped in msl::graph::Block as previously. However, the SeqBuildBlockNoiseMeas class runs unconditionnally, so we will need a way to decide, based on the sequence settings, whether the noise scan needs to run; this will be done using a msl::graph::If node, which prepares and runs its children if a condition is met. This condition can be driven by a boolean stored in the registry or by a function.
The noiseAdjust() function of the protocol provides the necessary information, and our condition function simply takes a MrProt as parameter, for example named p and returns p.noiseAdjust(). It can be defined as a free-standing function, but, since it is used only here and only has one statement, using it as a lambda function ([](MrProt & p){ return p.noiseAdjust(); }) yields a simpler code. Modify your initialize function as follows, compile, and simulate your sequence in POET after enabling GRAPPA. When GRAPPA is disabled, the simulation does not change, but when enabled you will notice an ADC event after the TokTokTok: this is the noise scan taking place.
Besides calling the two SBBs, our sequence will continue to do nothing in this part, as RF and gradient pulses will be added in the next part. We will however now add loops, and, in order to better understand what is going on, we will print progress at every trajectory of every slice. This is achieved using the msl::graph::Action node, which does nothing when prepared, and which calls a function when run. Update the initialize function with the following code:
Note that this node is not currently part of the sequence graph, as it does not appear in any call to addChild, and so will not yet change the behavior of the sequence. As for the condition function in the msl::graph::If node, it could also have been defined as a free-standing function. When a node requires a function, as is the case with msl::graph::If, msl::graph::Action, and other nodes presented later, the return type is node-dependent, and the prototype of the function must be restricted to one of the following six forms:
In msl, repetition nodes come in two forms: a msl::graph::Loop based on a msl::Counter, and an msl::graph::Iterator, based on a user-provided container. The trajectory repetition will take the former form, while the slice repetition will take the latter. Since the state of the repetion, i.e. at which step the repetition currently is, will be shared with other objects, the counter or the iterator underlying the repetition nodes must be stored in the registry: this is already the case, with the keys "trajectories" and "slices" defined previously.
Adding the two repetitions can be done by creating the nodes as separate variables:
The graph structure is however more apparent with nested call. Modify the initialize function as such:
When simulating the sequence, you will see, in the command window, the nodes being run for all slices (outer repetition) and for all trajectories (inner repetition), with increasing indices.
It is also possible to add multiple children to a msl::graph::Node at the same time, using msl::graph::Node::appendChildren :
In the user interface, we defined the averaging mode (limits.setAveragingMode): this parameters translates to two visible options when more than one average is selected, Long term and Short term. The usual implementation, which we will follow here is to nest the average loop within the slice loop for the short-term average, and to nest the slice loop withing the average loop for the long-term average. In both cases, the trajectory loop is inserted at the deepest level.
This could be solved with another msl::graph::If node, but it is more explicit and less error-prone to handle the real values of the averaging mode. For this, we are going to use the msl::graph::Case node: it is a generalization of the msl::graph::If node which compares a reference value, stored in the graph or returned by a function.
Start by adding an averages counter, and refactoring the kernel to print this new counter:
Since the trajectory loop is shared between two branches, we will define it as a node not yet inserted in the graph, as for the kernel node:
We can then rewrite the sequence graph with the msl::graph::Case node: if the averaging mode is equal to INNER_LOOP, we run the average loop nested in the slice loop, and, if the averaging mode is equal to OUTER_LOOP, we run the slice loop nesed in the average loop: