msl 1.3.0
Loading...
Searching...
No Matches
Chapter II — In which we scan the k-space, doing nothing at each point

This part introduces two main concepts of msl: the registry, a dictionary which can shared across objects of the sequence, and the sampling, describing how the k-space will be scanned.

Registry

The msl::Sequence class holds a msl::Dictionary object: this dictionary maps keys, in this case strings, to values, which may have any type. As will be shown in later parts, this registry is shared across different objects, so that common data is not duplicated nor needs to be maintained up-to-date at several locations. By default, this registry is empty, but the msl::Sequence class expects a couple of information: these are currently missing, which explains why running the sequence fails.

The registry can be accessed from within the sequence using this->_root->registry(); you can check that it is empty using the following code snippet in the prepare function:

std::cout << this->_root->registry()->size() << " items in registry\n";
for(auto const & x: *this->_root->registry())
{
std::cout << x.first << "\n";
}

Re-compiling and running mp will then show display 0 items in registry in the terminal.

Storing and retrieving a value is handled by the msl::Dictionary::get and msl::Dictionary::set functions. In a msl::Sequence, it can also be accessed by msl::Sequence::get and msl::Sequence::set. You can for example store the double value 3.14 in the key "pi" in the initialize function, and retrieve it in the prepare function:

NLSStatus
FLASH
::initialize(SeqLim & limits)
{
// ...
this->set<double>("pi", 3.14);
return MRI_SEQ_SEQU_NORMAL;
}
NLSStatus
FLASH
::prepare(MrProt & protocol, SeqLim & limits, SeqExpo & exports)
{
std::cout << "Value of pi in the registry: " << this->get<double>("pi") << "\n";
return MRI_SEQ_SEQU_NORMAL;
}

Note that both the get and the set are typed explicitely: you must use the correct when retrieving a value from the registry, otherwise an exception will be thrown. To simplify this, instances of msl::DictionaryItem can be stored in objects which make frequent calls to dictionary objects. Continuing with the previous example, we can add a msl::DictionaryItem as a private member of the FLASH class:

class FLASH: public msl::Sequence
{
// ...
private:
};
Typed accessor for a dictionary item.
Definition DictionaryItem.h:22
Graph-based sequence class.
Definition Sequence.h:37

The code in the initialize functions is untouched, but the prepare function becomes:

NLSStatus
FLASH
::prepare(MrProt & protocol, SeqLim & limits, SeqExpo & exports)
{
std::cout << "Value of pi in the registry: " << this->_pi() << "\n";
// Let's be a bit more precise
this->_pi() = 3.1416;
std::cout << "New value of pi in the registry: " << this->_pi() << "\n";
return MRI_SEQ_SEQU_NORMAL;
}

When "calling" the dictionary item, a reference is returned, which allows retrieving and modifying the stored value, the type of the value being stored once and for all in the private member declaration.

Sampling

The k-space may be sampled different ways: while the Cartesian sampling is the most well-known one, sampling can be radial (as a 2D stack-of-stars or a 3D kooshball), or even non-linear with spiral or rosette trajectories. This behavior is encapsulated in the msl::Sampling class and its children, stored in the msl::samplings namespace (which of course contains msl::samplings::Cartesian).

The sampling object will generate a set of trajectories defined by gradients, coupled with an ADC object to sample the signal. These are stored in children of the msl::Readout class, defined in the msl::readouts. For example, msl::samplings::Cartesian generates msl::readouts::Linear readouts. The current readout is defined by the index property of the sampling class, through the msl::Sampling::index and msl::Sampling::setIndex functions.

Internally, the index is stored as a msl::Counter, an object which enumerates values between 0 (included) and a maximum (excluded) when incremented. Since the value of the counter needs to be shared between the sampling and other objects, it must be stored in the registry, and the msl::Sequence class holds a msl::DictionaryItem referencing the counter, called _trajectories. It is the responsability of the sequence developer to store this object, while incrementation and bound control is handled by the msl::Sequence object. In this tutorial, we store this counter under the "trajectories" key. Add the following line at the end of the initialize function (the comment implies that we will set other similar keys in the following sections).

NLSStatus
FLASH
::initialize(SeqLim & limits)
{
// ...
// Sequence graph traversal information
this->_root->registry()->clear();
this->_root->registry()->insert({
{"trajectories", msl::Counter(0)}
});
// Set-up the keys requested by msl::Sequence
this->_trajectories.setKey("trajectories");
return MRI_SEQ_SEQU_NORMAL;
}
Counter from 0 (included) to end (excluded).
Definition Counter.h:14

Run and Check Modes

The msl::Sequence class actually contains not one but two msl::Sampling objects: one used at run-time (_runSampling), and one used at check time (_checkSampling). The msl::Sequence class also contains two other members to indicate whether the sequence is at check time or at run time:

  1. _activeSamping, a msl::DictionaryItem holding a pointer to either _checkSampling or _runSampling
  2. _kernelMode, a msl::DictionaryItem with a value of KERNEL_CHECK in check mode or KERNEL_IMAGE in image mode.

The values of those two members are set automatically by msl. However, the keys associated with them have to be set by the user of the class. As above for the _trajectory counter, set up the keys for the active sampling and kernel mode.

NLSStatus
FLASH
::initialize(SeqLim & limits)
{
// ...
// Sequence graph traversal information
this->_root->registry()->clear();
this->_root->registry()->insert({
{"trajectories", msl::Counter(0)},
// Current mode and active sampling
{"kernelMode", long(KERNEL_IMAGE)},
{"sampling", msl::Sampling::Pointer()}
});
// Set-up the keys requested by msl::Sequence
this->_trajectories.setKey("trajectories");
this->_activeSampling.setKey("sampling");
this->_kernelMode.setKey("kernelMode");
return MRI_SEQ_SEQU_NORMAL;
}
std::shared_ptr< Sampling > Pointer
Reference-counted pointer to Sampling.
Definition Sampling.h:27

Cartesian Sampling Mask

By default, a Cartesian sampling will sample the whole k-space. This behavior can be changed using a msl::Mask, for example to implement GRAPPA or to sample a subset of trajectories while in the check function. The mask is stored as a 2D binary grid in the \(k_y, k_z\) space: any point with value true is sampled, any point with value false is skipped. Pre-defined masks are available (for example msl::partialMask for partial Fourier or msl::ellipticalMask for elliptical scanning), and masks can be combined with the usual boolean operator of '|' for union and & for intersection.

For the run-mode, a high-level helper function merging elliptical scanning, partial Fourier and iPAT is also available in msl::acceleration::mask. For the check mode, it is usual to only sample the extremal k-space lines: in msl, this is provided by msl::cornersMask.

Putting It Together

In addition to the previous code snippets, we only need to include a few headers, initialize the two sampling objects and to configure their masks. The initialization is performed in the initialize function:

#include <msl/Counter.h>
#include <msl/KSpace.h>
#include <msl/masks.h>
#include <msl/Sampling.h>
// ...
NLSStatus
FLASH
::initialize(SeqLim & limits)
{
// ...
// Create the two sampling objects, one for run, one for check
this->_runSampling = std::make_shared<msl::samplings::Cartesian>();
this->_checkSampling = std::make_shared<msl::samplings::Cartesian>();
return MRI_SEQ_SEQU_NORMAL;
}

The configuration of the masks is performed in the prepare function. It involves the notion of msl::GradientSpecs, which describes the maximum gradient amplitude and minimum rise-time. Both values can be set directly, or obtained from the current protocol using msl::GradientSpecs::current.

NLSStatus
FLASH
::prepare(MrProt & protocol, SeqLim & limits, SeqExpo & exports)
{
// Check the acceleration settings, compute the iPAT mask
auto const runMask = msl::acceleration::mask(protocol);
msl::acceleration::updateExports(protocol, runMask, exports);
// Prepare the run sampling with the iPAT mask
auto const gradientSpecs = msl::GradientSpecs::current(protocol);
static_cast<msl::samplings::Cartesian &>(*this->_runSampling)
.setMask(runMask)
.setGradientSpecs(gradientSpecs);
this->_runSampling->prepare(protocol, limits, exports);
// Prepare the check with a corners mask
auto const kShape = msl::KSpace(protocol).shape();
static_cast<msl::samplings::Cartesian &>(*this->_checkSampling)
.setMask(msl::cornersMask({kShape[1], kShape[2]}, {2, 2}))
.setGradientSpecs(gradientSpecs);
this->_checkSampling->prepare(protocol, limits, exports);
return MRI_SEQ_SEQU_NORMAL;
}
Description of the discretized k-space geometry.
Definition KSpace.h:22
Vector3l const & shape() const
Return the shape of the discrete k-space.
Cartesian sampling with an iPAT mask (used in Image-only mode if iPAT is disabled).
Definition Cartesian.h:23
Cartesian & setGradientSpecs(GradientSpecs const &gradientSpecs) override
Set the gradient specifications constraining the read-out duration.
NLSStatus prepare(MrProt &protocol, SeqLim &limits, SeqExpo &exports) override
Prepare the real-time events.
#define ON_ERROR_RETURN_STATUS(S)
Execute statement S, and, if not MRRESULT_SUCCESS, return the status.
Definition helpers.h:26
void updateExports(MrProt const &protocol, iPATMask const &iPATMask, SeqExpo &exports)
Update the iPAT-related fields in SeqExpo.
iPATMask mask(MrProt &protocol)
Merge (as a logical and) the elliptical scanning, partial Fourier and iPAT masks.
NLSStatus check(MrProt &protocol, SeqLim const &limits)
Check the iPAT-related constraints.
Mask cornersMask(Mask::Shape const &shape, Mask::Point size)
Create a mask where every point is disabled except at the corners.
static GradientSpecs current(MrProt const &protocol)
Return the gradient specs from SysProperties and the current gradient mode.

As before, the sequence should compile and load in POET. In addition, it can now even be simulated, albeit with a disappointing result: the sequence still does nothing.

Full Code

FLASH.h

FLASH.cpp

makefile.trs