msl 1.3.0
Loading...
Searching...
No Matches
Chapter IV — In which our sequence finally does something, but not enough

The loop sequence defined in the previous parts being complete, the only thing remaining is to play RF pulses and gradient pulses. We will also need to provide RF power information, and, in order to interface with the reconstruction pipeline, we will also need to fill what is know as the measurement data header, or MDH.

All this will be performed by a user-defined graph node, which we will call Kernel (and which will of course replace our previous kernel node which only printed progress).

Custom Nodes

Each node class in the sequence graph derives, directly or indirectly, from the msl::graph::AbstractNode class, and node classes must respect the interface of msl::graph::AbstractNode. At minimum, this means:

  • defining a set of type aliases and construction functions
  • defining a prepare function and a run function, mentioned in the previous parts
  • defining a duration function and a rfInfo function

The duration function returns the duration of real-time objets (for example RF pulses or gradient pulses), if any, contained in the node: the msl::graph::If node has a duration depending on whether the condition is true, the msl::graph::Loop node has a duration equal to the number of repetitions times the duration of all children, etc. The duration of the kernel node for the FLASH sequence is fixed: it is equal to the repetition time since it encompasses everything happening during one repetition of the sequence.

The rfInfo function similarly returns the RF power of real-time objects contained in the node. The FLASH sequence has only one RF pulse per repetition and, since gradient pulses carry no RF power, its rfInfo function will return the RF power of the excitation pulse.

The declaration of the Kernel will be stored in the Kernel.h file, as follows:

#include <msl/graph.h>
class Kernel: public msl::graph::AbstractNode
{
public:
static Pointer New();
~Kernel() override = default;
NLSStatus prepare(
MrProt & protocol, SeqLim & limits,
MrProtocolData::SeqExpo & exports) override;
NLSStatus run(
MrProt & protocol, SeqLim & limits,
MrProtocolData::SeqExpo & exports) override;
uint64_t duration() const override;
MrProtocolData::SeqExpoRFInfo rfInfo() const override;
private:
uint64_t _duration;
Kernel();
};
Base class for all graph nodes.
Definition AbstractNode.h:28
virtual NLSStatus run(MrProt &protocol, SeqLim &limits, SeqExpo &exports)=0
Run the node.
virtual uint64_t duration() const =0
Return the duration in microseconds, requires preparation.
virtual NLSStatus prepare(MrProt &protocol, SeqLim &limits, SeqExpo &exports)=0
Prepare the node.
virtual MrProtocolData::SeqExpoRFInfo rfInfo() const =0
Return the RF information for SAR computation, requires preparation.
#define DECLARE_POINTERS(name)
Declare pointer type aliases.
Definition helpers.h:83

In this declaration, the DECLARE_POINTERS macro simply defines to type aliases (Pointer and ConstPointer) to follow the common API of all nodes. The creation of a node instance is handled by the New static function, as for previous node examples. This function will call the private constructor, which will handle all the necessary initializations. Using these two functions will guarantee that only pointers to nodes can be created while respecting the usual C++ coding guidelines regarding constructors.

In this specific class, the destructor does not do anything, and is thus defaulted. As usual, if objects are allocated in the constructor, they must be de-allocated in the destructor.

The definitions of the class functions will be stored in the Kernel.cpp file, as follows

#include "Kernel.h"
#include <MrMeasSrv/SeqIF/Sequence/sequmsg.h>
Kernel::Pointer
Kernel
::New()
{
return Pointer(new Kernel());
}
NLSStatus
Kernel
::prepare(MrProt & protocol, SeqLim & limits, MrProtocolData::SeqExpo & exports)
{
// The slices are not interleaved, so the duration of the kernel is always
// the full TR
this->_duration = protocol.tr()[0];
return MRI_SEQ_SEQU_NORMAL;
}
NLSStatus
Kernel
::run(MrProt & protocol, SeqLim & limits, MrProtocolData::SeqExpo & exports)
{
return MRI_SEQ_SEQU_NORMAL;
}
uint64_t
Kernel
::duration() const
{
return this->_duration;
}
MrProtocolData::SeqExpoRFInfo
Kernel
::rfInfo() const
{
return {};
}
Kernel
::Kernel()
: AbstractNode()
{
// Nothing else
}

Note that the duration functions cannot directly return the TR since it would need access to the protocol object: this is the reason that the class contains a _duration variable, in which the TR is stored in the prepare function.

The sequence can now be modified to use the Kernel class in place of the previous progress-printing function. In the prepare function, we will also update the duration and SAR information of the sequence based on the graph information.

// ...
#include "Kernel.h"
// ...
NLSStatus
FLASH
::initialize(SeqLim & limits)
{
// ...
auto trajectories = msl::graph::Loop::New("trajectories", Kernel::New());
// ...
}
NLSStatus
FLASH
::prepare(MrProt & protocol, SeqLim & limits, SeqExpo & exports)
{
// ...
// Update the timing and SAR information
exports.setRFInfo(this->_root->rfInfo());
exports.setTotalMeasureTimeUsec(double(this->_root->duration()));
exports.setMeasureTimeUsec(
exports.getTotalMeasureTimeUsec()/(1+protocol.repetitions()));
return MRI_SEQ_SEQU_NORMAL;
}
static Pointer New(std::string const &counter, Dictionary::Pointer registry={})
Create a loop with no child.

Things will work as before, with one difference: the expected duration of the sequence, as shown in POET, is now correct, and will update according to changes in TR and resolution.

RF Pulses

RF pulses in msl are defined in the msl::rf_pulses namespace. Two main kinds of obects exist there: pulses per se, for example msl::rf_pulses::Sinc or msl::rf_pulses::Rect, and pulse modifiers, for example msl::rf_pulses::Selective. This version of the FLASH sequence will work with a selective excitation pulse, combining those two kinds of objects.

We will start by adding the _excitation object to the kernel, as well as a way to access the current slice: the slice is required since it will affect the slice selection gradient played during the RF pulse. The access to the slice is provided by a msl::DictionaryItem referencing the "slices" counter defined in the registry.

Modify the Kernel.h file to add the two members, along with their headers, and to add a parameter to the constructor:

#include <msl/graph.h>
#include <msl/rf_pulses.h>
class Kernel: public msl::graph::AbstractNode
{
public:
static Pointer New(std::string const & slices);
// ...
private:
uint64_t _duration;
Kernel(std::string const & slices);
};
Typed accessor for a dictionary item.
Definition DictionaryItem.h:22
Selective RF pulse, i.e. played with a gradient pulse.
Definition Selective.h:23

In the Kernel.cpp file, modify the definition of the constructors accordingly:

// ...
Kernel::Pointer
Kernel
::New(std::string const & slices)
{
return Pointer(new Kernel(slices));
}
// ...
Kernel
::Kernel(std::string const & slices)
: AbstractNode(), _slices(this, slices)
{
// Nothing else
}

Finally, modify the sequence class to use the new prototype:

NLSStatus
FLASH
::initialize(SeqLim & limits)
{
// ...
auto trajectories = msl::graph::Loop::New(
"trajectories", Kernel::New("slices"));
// ...
}

The RF pulse can now be configured in the prepare function of the Kernel class. This consists in setting fields common to all pulse objects:

Other fields are specific to the sinc pulse (msl::rf_pulses::Sinc::setTimeBandwidthProduct) or to selective pulses (msl::rf_pulses::Selective::setThickness, msl::rf_pulses::Selective::setRFDuration).

After being set up, the RF pulse must be prepared: its prepare function returns a status: if the preparation succeeded, we can continue; however, if the preparation failed, the status must be propagated to the caller of the function. We will use the ON_ERROR_RETURN_STATUS macro to perform this.

Put together, we get the following:

NLSStatus
Kernel
::prepare(MrProt & protocol, SeqLim & limits, MrProtocolData::SeqExpo & exports)
{
auto const & slice = this->_slices().item();
this->_excitation
.setIdent("exc")
.setType(msl::RFPulse::Excitation)
.setFlipAngle(protocol.flipAngle())
.setThickness(protocol.sliceSeries().aFront().thickness())
.setRFDuration(2560)
.setSamples(128)
.setStartTime(0)
.setSlice(slice)
.rf()
.setTimeBandwidthProduct(2.70);
ON_ERROR_RETURN_STATUS(this->_excitation.prepare(protocol, limits, exports));
// ...
return MRI_SEQ_SEQU_NORMAL;
}
#define ON_ERROR_RETURN_STATUS(S)
Execute statement S, and, if not MRRESULT_SUCCESS, return the status.
Definition helpers.h:26

We can then play the RF pulse in the Kernel::run function. For this, we need to:

  • set the current slice in the RF pulse object, as in the prepare function
  • inside a real-time events block, call the run function of the RF pulse object

The real-time event block is opened by calling fRTEBInit and close by calling fRTEBFinish, both of the returning a status object, so they will be wrapped in the ON_ERROR_RETURN_STATUS macro. In the event block, we will also add a dummy event at the repetition time: without it, the sequence timing would not be correct.

The run function is then:

NLSStatus
Kernel
::run(MrProt & protocol, SeqLim & limits, MrProtocolData::SeqExpo & exports)
{
auto const & slice = this->_slices().item();
this->_excitation.setSlice(slice);
// Start the event block
ON_ERROR_RETURN_STATUS(fRTEBInit(slice->getROT_MATRIX()));
ON_ERROR_RETURN_STATUS(this->_excitation.run(protocol, limits, exports));
ON_ERROR_RETURN_STATUS(fRTEI(protocol.tr()[0]));
// End of event block
ON_ERROR_RETURN_STATUS(fRTEBFinish());
return MRI_SEQ_SEQU_NORMAL;
}

You can simulate the sequence, with something finally happening: on the RF channel, you can see sinc pulses, and their accompanying slice selection gradient on the Z gradient channel.

RF-spoling

Since the FLASH is a sequence with a short TR, some spoiling will be required. The next part will add gradient spoiling, and we will add RF spoiling in this part. RF spoiling consists in incrementing the phase of the RF pulse in a quadratic pattern. This can of course be handled manually, but the msl::PhaseCycling object will take care of this for us.

Start by adding a object of type msl::PhaseCycling to the registry:

// ...
// ...
NLSStatus
FLASH
::initialize(SeqLim & limits)
{
// ...
this->_root->registry()->insert({
{"slices", msl::SliceConstIterator()}, {"trajectories", msl::Counter(0)},
{"averages", msl::Counter(0)},
// Current mode and active sampling
{"kernelMode", long(KERNEL_IMAGE)},
{"sampling", msl::Sampling::Pointer()},
// RF spoiling: quadratic increment of the phase of the RF pulse [deg]
{"phaseCycling", msl::PhaseCycling(0, 50)}
});
// ...
}
Counter from 0 (included) to end (excluded).
Definition Counter.h:14
Compute phase cycling with linear and quadratic increments.
Definition PhaseCycling.h:12
std::shared_ptr< Sampling > Pointer
Reference-counted pointer to Sampling.
Definition Sampling.h:27
ConstIterator< std::vector< sSLICE_POS > > SliceConstIterator
Non-mutable iterator to a vector of slice specifications.
Definition SliceIterator.h:21

The two parameters of the constructor are respectively the linear phase increment and the quadratic phase increment. The phase can be increment as for any integer (++phaseCycling), and the current phase is retrieved through phaseCycling.phase(). We will then need to increment the phase after each kernel call using an msl::graph::Action node:

NLSStatus
FLASH
::initialize(SeqLim & limits)
{
// ...
auto trajectories = msl::graph::Loop::New(
"trajectories", {
Kernel::New("slices", "phaseCycling"),
++d->get<msl::PhaseCycling>("phaseCycling"); })
}
);
// ...
}
std::shared_ptr< Dictionary > Pointer
Reference-counted pointer to Dictionary.
Definition Dictionary.h:34
static Pointer New(Function const &function, Dictionary::Pointer registry={})
Create an action from a function and registry.

We then add a new parameter to the kernel constructor, and a matching msl::DictionaryItem, and update the construction of the kernel in the sequence:

// ...
// ...
class Kernel: public msl::graph::AbstractNode
{
public:
static Pointer New(
std::string const & slices, std::string const & phaseCycling);
// ...
private:
// ...
// ...
Kernel(std::string const & slices, std::string const & phaseCycling);
};
// ...
Kernel::Pointer
Kernel
::New(std::string const & slices, std::string const & phaseCycling)
{
return Pointer(new Kernel(slices, phaseCycling));
}
// ...
Kernel
::Kernel(std::string const & slices, std::string const & phaseCycling)
: AbstractNode(), _slices(this, slices), _phaseCycling(this, phaseCycling)
{
// Nothing else
}
NLSStatus
FLASH
::initialize(SeqLim & limits)
{
// ...
auto trajectories = msl::graph::Loop::New(
"trajectories", {
Kernel::New("slices", "phaseCycling"),
++d->get<msl::PhaseCycling>("phaseCycling"); })
}
);
// ...
}

The last modification to implement RF spoiling requires setting the phase of the RF pulse, both in the prepare and in the run functions of the kernel:

NLSStatus
Kernel
::prepare(MrProt & protocol, SeqLim & limits, MrProtocolData::SeqExpo & exports)
{
auto const & slice = this->_slices().item();
auto const rfPhase = this->_phaseCycling().phase();
this->_excitation
// ...
.setFlipAngle(protocol.flipAngle())
.setAdditionalPhase(rfPhase)
// ...
.rf()
.setTimeBandwidthProduct(2.70);
ON_ERROR_RETURN_STATUS(this->_excitation.prepare(protocol, limits, exports));
// ...
}
NLSStatus
Kernel
::run(MrProt & protocol, SeqLim & limits, MrProtocolData::SeqExpo & exports)
{
auto const & slice = this->_slices().item();
auto const rfPhase = this->_phaseCycling().phase();
this->_excitation.setAdditionalPhase(rfPhase).setSlice(slice);
// ...
}

The phase of the RF pulse can be seen on the NC1 channel in the simulation.

Full Code

FLASH.h

FLASH.cpp

Kernel.h

Kernel.cpp

makefile.trs