msl 1.1.0
 
Loading...
Searching...
No Matches
Sequence Graph

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.

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

  • the Node class simply groups children together
  • the Loop class runs its children multiple times, with the loop counter stored in the registry
  • the If class runs its children if a condition (stored in the registry or from a function call) is met
  • the Block class encapsulates an object which behaves like a SeqBuildBlock in the graph

Refer 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):

#include <MrImagingFW/libSBBFW/StdSeqIF.h>
#include <msl/graph/Node.h>
// ...
class Sequence: public StdSeqIF
{
// ...
private:
msl::graph::Node::Pointer _root;
};

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

#include <msl/graph/Loop.h>
#include "Kernel.h"
// ...
Sequence
::Sequence()
: _root(msl::graph::Node::New())
{
// Nothing else.
}
NLSStatus
Sequence
::initialize(SeqLim & limits)
{
// ...
// Make sure the graph is empty
this->_root->clearChildren();
// Create a scan loop under the root node, and add the kernel under the loop node
this->_root->appendChild(
"scan",
msl::graph::Block<Kernel>::New("slice", this->_root->registry())
)
);
// ...
}
static Pointer New(std::string const &slice, Args &&... args)
Definition Block.h:56
static Pointer New(std::string const &counter, Dictionary::Pointer registry={})
Create a loop with no child.
Definition acceleration.h:17

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.

NLSStatus
Sequence
::initialize(SeqLim & limits)
{
// ...
// Make sure the graph is empty
this->_root->clearChildren();
// Add the introduction node, enabled only if selected
this->_root->appendChild(
"tokTokTok",
)
);
// Add the loops, same as above with an extra slice loop
this->_root->appendChild(
"slice",
"scan",
msl::graph::Block<Kernel>::New("slice", this->_root->registry())
)
)
);
// ...
}
static Pointer New(std::string const &key, Dictionary::Pointer registry={})
Create an If node without children.

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.

NLSStatus
Sequence
::prepare(MrProt & protocol, SeqLim & limits, SeqExpo & exports)
{
// ...
// Prepare the sequence graph, update the timing and SAR information
try
{
ON_ERROR_RETURN_STATUS(this->_root->prepare(protocol, limits, exports));
}
catch(std::exception const & e)
{
SEQ_TRACE_ERROR << __PRETTY_FUNCTION__ << " " << e.what();
return MRI_SEQ_SEQU_ERROR;
}
exports.setRFInfo(this->_root->rfInfo());
exports.setMeasureTimeUsec(this->_root->duration());
// ...
}
#define ON_ERROR_RETURN_STATUS(S)
Execute statement S, and, if not MRRESULT_SUCCESS, return the status.
Definition helpers.h:21

Similarly, running the root node will run its children

NLSStatus
Sequence
::run(MrProt & protocol, SeqLim & limits, SeqExpo & exports)
{
// ...
try
{
ON_ERROR_RETURN_STATUS(this->_root->run(protocol, limits, exports));
}
catch(std::exception const & e)
{
SEQ_TRACE_ERROR << __PRETTY_FUNCTION__ << " " << e.what();
return MRI_SEQ_SEQU_ERROR;
}
return MRI_SEQ_SEQU_NORMAL;
}

Registry

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

NLSStatus
Sequence
::initialize(SeqLim & limits)
{
// ...
// Set default values, they will be updated in the prepare function
this->_root->registry()->set("tokTokTok", false);
this->_root->registry()->set("slice", msl::Counter(0));
this->_root->registry()->set("scan", msl::Counter(0));
// ...
}
NLSStatus
Sequence
::prepare(MrProt & protocol, SeqLim & limits, SeqExpo & exports)
{
// ...
// Set graph traversal information from protocol
// NOTE: the "tokTokTok" is not required if a function is used in the If node
this->_root->registry()->get<bool>("tokTokTok") = protocol.intro();
this->_root->registry()->get<msl::Counter>("slice").setEnd(
protocol.sliceSeries().size());
// ...
}
Counter from 0 (included) to end (excluded).
Definition Counter.h:14

Using a function for the If Node, the example could then adapted as follows:

NLSStatus
Sequence
::initialize(SeqLim & limits)
{
// ...
this->_root->registry()->set(
[](MrProt & p, SeqLim &, SeqExpo &){ return p.intro(); }));
// ...
}
NLSStatus
Sequence
::prepare(MrProt & protocol, SeqLim & limits, SeqExpo & exports)
{
// ...
// No need to update the "tokTokTok" key of the registry: the value will be
// loaded from the current protocol.
// ...
}
boost::variant< std::function< bool()>, std::function< bool(MrProt &)>, std::function< bool(MrProt &, SeqLim &, SeqExpo &)> > Function
Definition If.h:35

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.

Masks

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:

NLSStatus
Sequence
::prepare(MrProt & protocol, SeqLim & limits, SeqExpo & exports)
{
// ...
auto const & kSpace = protocol.kSpace();
msl::Mask::Shape const kShape{
msl::Mask::Shape::value_type(kSpace.phaseEncodingLines()),
msl::Mask::Shape::value_type(kSpace.partitions())};
msl::Mask mask(kShape, true);
}
A two dimensional mask.
Definition Mask.h:18
Vector2l Shape
Shape of the mask, as (column, row)
Definition Mask.h:23
typename Container::value_type value_type
Definition Vector.h:20

Depending on the protocol, the final mask will be the intersection of a full mask, and subsampling masks:

// ...
msl::Mask mask(kShape, true);
if(kSpace.ellipticalScanning())
{
mask &= msl::ellipticalMask(kShape);
}
if(kSpace.phasePartialFourierFactor() != SEQ::PF_OFF)
{
kShape,
{kSpace.dPartialFourierFactor(kSpace.phasePartialFourierFactor()), 1});
}
Mask ellipticalMask(Mask::Shape const &shape)
Create an ellipse-shaped mask, where the width and height of the ellipse equal the shape of the mask.
Mask partialMask(Mask::Shape const &shape, Vector2d const &ratio, Vector2b const &useLowerQuadrant)
Create a mask where an interval of each quadrant is enabled.

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:

this->_root->registry()->set("scan", msl::Counter(mask.count()));
this->_root->registry()->set("indices", mask.enabledPoints());

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:

class Sequence: public StdSeqIF
{
// ...
private:
msl::Mask _runMask, _checkMask;
};
Container node, prepare and run its children sequentially.
Definition Node.h:28

In the initialize function, the root node is created, and default values are set in its registry

NLSStatus
Sequence
::initialize(SeqLim & limits)
{
// ...
// Sequence graph: optional introduction (TokTokTok), followed by the
// acquisition loops
this->_root->clearChildren();
this->_root->appendChild(
"tokTokTok",
this->_root->appendChild(
"slice",
"scan",
msl::graph::Block<Kernel>::New("slice", this->_root->registry()))));
// Sequence graph traversal information
this->_root->registry()->set(
[](MrProt & p, SeqLim &, SeqExpo &){ return p.intro(); }));
this->_root->registry()->set("slice", msl::Counter(0));
this->_root->registry()->set("scan", msl::Counter(0));
this->_root->registry()->set("indices", Points());
this->_root->registry()->set("kernelMode", long(KERNEL_IMAGE));
// ...
}

In the prepare function, the registry is updated to reflect the current protocol, masks are computed, and duration and SAR information are updated.

NLSStatus
Sequence
::prepare(MrProt & protocol, SeqLim & limits, SeqExpo & exports)
{
// Set graph traversal information from protocol
this->_root->registry()->get<msl::Counter>("slice").setEnd(
protocol.sliceSeries().size());
// NOTE: scan counter must be set after mask has been computed
// Set the run and check masks
auto const & kSpace = protocol.kSpace();
msl::Mask::Shape const kShape{
msl::Mask::Shape::value_type(kSpace.phaseEncodingLines()),
msl::Mask::Shape::value_type(kSpace.partitions())};
this->_runMask = msl::Mask(kShape, true);
if(kSpace.ellipticalScanning())
{
this->_runMask &= msl::ellipticalMask(kShape);
}
if(
kSpace.phasePartialFourierFactor() != SEQ::PF_OFF
|| kSpace.slicePartialFourierFactor() != SEQ::PF_OFF)
{
this->_runMask &= msl::partialMask(
kShape, {
kSpace.dPartialFourierFactor(kSpace.phasePartialFourierFactor()),
kSpace.dPartialFourierFactor(kSpace.slicePartialFourierFactor())});
}
this->_checkMask = msl::cornersMask(kShape, {2, 2});
// Prepare the sequence graph, update the timing and SAR information
this->_root->registry()->get<msl::Counter>("scan").setEnd(
this->_runMask.count());
try
{
ON_ERROR_RETURN_STATUS(this->_root->prepare(protocol, limits, exports));
}
catch(std::exception const & e)
{
SEQ_TRACE_ERROR << __PRETTY_FUNCTION__ << " " << e.what();
return MRI_SEQ_SEQU_ERROR;
}
exports.setRFInfo(this->_root->rfInfo());
exports.setMeasureTimeUsec(this->_root->duration());
exports.setTotalMeasureTimeUsec(exports.getMeasureTimeUsec());
exports.setMeasuredPELines(int32_t(this->_runMask.count()));
// ...
}
Mask cornersMask(Mask::Shape const &shape, Mask::Point size)
Create a mask where every point is disabled except at the corners.

The check function instructs the kernel to use the _checkMask and runs the kernel with the correct mode.

NLSStatus
Sequence
::check(MrProt & protocol, SeqLim & limits, SeqExpo & exports, SEQCheckMode *)
{
this->_root->registry()->get<msl::Counter>("scan").setEnd(
this->_checkMask.count());
this->_root->registry()->get<Points>("indices") =
this->_checkMask.enabledPoints();
this->_root->registry()->get<long>("kernelMode") = KERNEL_CHECK;
try
{
ON_ERROR_RETURN_STATUS(this->_root->run(protocol, limits, exports));
}
catch(std::exception const & e)
{
SEQ_TRACE_ERROR << __PRETTY_FUNCTION__ << " " << e.what();
return MRI_SEQ_SEQU_ERROR;
}
return MRI_SEQ_SEQU_NORMAL;
}

Finally, the run function is mostly similar, using the _runMask.

NLSStatus
Sequence
::run(MrProt & protocol, SeqLim & limits, SeqExpo & exports)
{
this->_root->registry()->get<msl::Counter>("scan").setEnd(
this->_runMask.count());
this->_root->registry()->get<Points>("indices") =
this->_runMask.enabledPoints();
this->_root->registry()->get<long>("kernelMode") = KERNEL_IMAGE;
mSEQTest(protocol, limits, exports, RTEB_ORIGIN_fSEQRunStart, 0, 0, 0, 0, 0);
try
{
ON_ERROR_RETURN_STATUS(this->_root->run(protocol, limits, exports));
}
catch(std::exception const & e)
{
SEQ_TRACE_ERROR << __PRETTY_FUNCTION__ << " " << e.what();
return MRI_SEQ_SEQU_ERROR;
}
mSEQTest(protocol, limits, exports, RTEB_ORIGIN_fSEQRunFinish, 0, 0, 0, 0, 0);
return MRI_SEQ_SEQU_NORMAL;
}