The sequence graph features of msl aim at replacing the libCSL from IDEA with documented, simpler tools. The idea behind the two is the same: the structure of the loops of the sequence (repetitions, averages, lines, etc.) and the blocks of events (e.g. kernel of the sequence, introduction noise) are represented by a tree (or more precisely a directed acyclic graph, hence the name).
Each node will run its children in some way, e.g. a Loop node will run its children several times, while an If node will run its children depending on an external condition. The information regarding the traversal of the graph (e.g. how many times a loop is executed, whether or not a node is executed, which k-space lines are included) is stored in a registry shared by all the nodes of the graph.
The structure of the sequence is described by a graph, described by its root node, the children of the root nodes, and further descendants. All nodes are derived from the AbstractNode class, and every derived class performs a specific action when executed, for example
SeqBuildBlock in the graphRefer to the documentation of the msl::graph namespace for a complete list of nodes.
The root node will be used at least in the prepare and run functions of the sequence class, and so should be a member of the sequence class. If the structure of the sequence is constant, it can be defined in the initialize function; on the other hand, if the structure depends on user-selected parameters, the graph must be updated in the prepare function. A sequence class could for example define the root node as such (it needs not be public):
Note that the root node, as other nodes will be, is a pointer: this is due to the fact that nodes need to contain other nodes, a feature which requires using pointers in C++. The memory management is however automatic, as theses pointers are so-called smart pointers which handle allocation and de-allocation of memory.
Assuming the sequence kernel is defined in a class named Kernel in a file named Kernel.h, inheriting from SeqBuildBlock, a simple structure with a single slice group, single averaging, single repetion, can be defined as
In the previous example, "scan" and "slice" reference counters in the registry. The first one will be increment by the Loop node while the second one is, in this example, only used to provide a sSLICE_POS object to the block run function to respect the Siemens API.
When prepared, the root note will prepare its Loop child, thus resetting the counter and preparing the kernel, as it is the child of the Loop node. When run, the root node will run its Loop child, thus resetting the counter and running the kernel a number of times equal to the size of the "scan" counter in the registry.
This can be easily extended in a more complex and more realistic example with an introduction node (gradient noise), being run depending on the user choice in the Sequence card and a slice loop. The introduction block is provided by Siemens in the SeqBuildBlockTokTokTok class.
This example introduces the If node which run its children according to a boolean value in the registry, here named "tokTokTok". The If node can also use a function which takes, as the prepare and run functions, an MrProt &, a SeqLim &, and a SeqExpo &. The syntax is the same, and the node will handle both situations.
Once the structure of the sequence has beed defined, the root node can be prepared (thus preparing its children). The root node can then report its total duration and its total RF power, so that both informations can be added to the exported data.
Similarly, running the root node will run its children
While the graph describes the structure of the sequence, the registry describes how the sequence will be executed: it will contain the counters for the Loop nodes and the condition variables for the If nodes. The registry is common to all the children of a node: changing the registry of the root node will propagate the changes throughout the tree, regardless of whether the graph is amended at a later point. Since it is shared between nodes, it is stored in a pointer.
The registry is a generic key-value container of type [Dictionary](msl::Dictionary). For the previous example, the registry can be initialized early, and modified in the prepare function according to the user choices
Using a function for the If Node, the example could then adapted as follows:
Note that the "scan" counter is not yet configured: this will be done in the last step, as it depends on the mask of the k-space.
For accelerated acquisitions (e.g. elliptical mask, partial Fourier or GRAPPA), the k-space defined by the geometry of the image is not scanned in full. This concept is stored within a Mask, a binary array describing whether or not a k-space point is sampled.
At its simplest, all points of the k-space are sampled:
Depending on the protocol, the final mask will be the intersection of a full mask, and subsampling masks:
Refer to the documentation of masks.h for a complete list of mask-generating functions.
Once a mask is defined, the scan counter can be updated, and, if needed, the coordinates of the enabled k-space points stored in the registry:
Masks are also used to define the points scanned in the check function. A full example is provided below. The Sequence class contains its root node, and two masks: one used in the run function, the other used in the check function:
In the initialize function, the root node is created, and default values are set in its registry
In the prepare function, the registry is updated to reflect the current protocol, masks are computed, and duration and SAR information are updated.
The check function instructs the kernel to use the _checkMask and runs the kernel with the correct mode.
Finally, the run function is mostly similar, using the _runMask.