Readout Gradient
In this final part, we will add gradient pulses and get a functional FLASH sequence. The first pulse we are going to add is provided by the msl::Sampling object. As for RF pulses, the msl::Sampling contains a set of real-time events, and can be played in the run function of the kernel. As for the slices and the phase cycling, we will start by passing the sampling object, and its accompanying trajectory counter, to the kernel:
{
public:
static Pointer New(
std::string const & sampling, std::string const & trajectories,
std::string const & slices, std::string const & phaseCycling);
private:
Kernel(
std::string const & sampling, std::string const & trajectories,
std::string const & slices, std::string const & phaseCycling);
};
Typed accessor for a dictionary item.
Definition DictionaryItem.h:22
Base class for all graph nodes.
Definition AbstractNode.h:28
#define DECLARE_POINTERS(name)
Declare pointer type aliases.
Definition helpers.h:83
Kernel::Pointer
Kernel
::New(
std::string const & sampling, std::string const & trajectories,
std::string const & slices, std::string const & phaseCycling)
{
return Pointer(new Kernel(sampling, trajectories, slices, phaseCycling));
}
Kernel
::Kernel(
std::string const & sampling, std::string const & trajectories,
std::string const & slices, std::string const & phaseCycling)
: AbstractNode(), _sampling(this, sampling), _trajectories(this, trajectories),
_slices(this, slices), _phaseCycling(this, phaseCycling)
{
}
NLSStatus
FLASH
::initialize(SeqLim & limits)
{
"trajectories", {
Kernel::New("sampling", "trajectories", "slices", "phaseCycling"),
}
);
}
std::shared_ptr< Dictionary > Pointer
Reference-counted pointer to Dictionary.
Definition Dictionary.h:34
Compute phase cycling with linear and quadratic increments.
Definition PhaseCycling.h:12
static Pointer New(Function const &function, Dictionary::Pointer registry={})
Create an action from a function and registry.
static Pointer New(std::string const &counter, Dictionary::Pointer registry={})
Create a loop with no child.
We then need to prepare the sampling: this requires setting the phase equal to that of the RF pulse (to correctly demodulate the signal during the readout), and the slice. In addition, we will specify the maximal amplitude and slew rate of the gradient using a msl::GradientSpecs object, as well as the time at which the excitation pulse occurs. This last field is required to correctly determine the time at which the center of the k-space is reached. As for the RF pulse, once the sampling is configured, we call its prepare function, wrapped in the error-handling ON_ERROR_RETURN_STATUS macro.
NLSStatus
Kernel
::prepare(MrProt & protocol, SeqLim & limits, MrProtocolData::SeqExpo & exports)
{
.setPhase(rfPhase)
}
virtual RealTimeEvents & setSlice(sSLICE_POS const &slice)
Set the slice used in prepare and run.
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.
Cartesian & setTimeOffset(long timeOffset) override
Set the time offset to get the effective echo time, accounting e.g. for the time of the excitation pu...
#define ON_ERROR_RETURN_STATUS(S)
Execute statement S, and, if not MRRESULT_SUCCESS, return the status.
Definition helpers.h:26
static GradientSpecs current(MrProt const &protocol)
Return the gradient specs from SysProperties and the current gradient mode.
Finally, we run the sampling object in the run function of the kernel. As for the RF pulse, it will require the phase of the RF pulse and the slice. In addition, it also requires the index of the current trajectory, and an update of the readout based on the current index.
NLSStatus
Kernel
::run(MrProt & protocol, SeqLim & limits, MrProtocolData::SeqExpo & exports)
{
auto const & slice = this->_slices().item();
auto const index = this->_trajectories().index();
(*this->_sampling()).setPhase(rfPhase).setIndex(index).setSlice(slice);
this->_sampling()->updateReadout(protocol, limits, exports));
}
After simulation, you will see the readout gradient being played, and the synchronized ADC object, also provided by the sampling object. Note that for Cartesian sampling, all readouts are the same: the displacement in the k-space happening during the readout is constant, only the displacement before the readout, to reach the start of the current line will vary. Calling msl::Sampling::setIndex and msl::Sampling::updateReadout does nothing for this specific samping, but it is a good practice to call these functions regardless of the actual sampling type.
Moving through the k-Space
Right after the excitation pulse, the k-space coordinates are 0 for \(k_x\) and \(k_y\), but non-zero for \(k_z\) due to the part of the slice selection gradient which occurs after the peak of the excitation pulse. For our Cartesian FLASH sequence, we must be at a specific point in the k-space before starting the readout-out:
- on the \(k_x\) axis, the center of the readout must be at the center of the k-space: the start of the readout must be at the "left edge" of the k-space (if the readout amplitude is positive, otherwise at the "right edge")
- the \(k_y\) and \(k_z\) coordinates depend on the current line, and, for a 3D sequence, the current partition.
Moving to the start of the trajectory has then a fixed part on the \(k_x\) axis and on the \(k_z\) axis, and a part depending on the current trajectory on the \(k_y\) and on the \(k_z\) axes.
In msl, each sampling object generate msl::Readout objects, which contain an ADC object (using the raw sREADOUT object from IDEA) for signal recording, and a msl::GradientPulse for the accompanying gradient. The gradient area incurred before the echo is returned by the msl::Readout::areaBeforeEcho function: it is a 3D vector, i.e. a msl::Vector3d. If the echo must be at the center of the readout, then the motion on the \(k_x\) axis must be equal to -readout.areaBeforeEcho().
The msl::GradientPulse objects can also report area between specific time points using the msl::GradientPulse::area, msl::GradientPulse::areaFrom, and msl::GradientPulse::areaTo functions. Real-time objects, as RF pulses and gradient pulses provide timing informations throught the msl::RealTimeEvents::startTime and msl::RealTimeEvents::startTime functions. Using these functions, we can compute the motion needed to compensate the slice-selection gradient occuring after the pulse peak:
- this->_excitation.gradient().areaFrom(this->_excitation.peakTime() - this->_excitation.startTime());
The addition of the fixed part of the move-to-begin gradient can then be added to the kernel:
{
private:
};
Vector< 3, double > Vector3d
3D vector of doubles
Definition Vector.h:136
NLSStatus
Kernel
::prepare(MrProt & protocol, SeqLim & limits, MrProtocolData::SeqExpo & exports)
{
auto const & readout = this->_sampling()->readout();
this->_toBeginFixed =
- readout.areaBeforeEcho()
- this->_excitation.gradient().areaFrom(
this->_excitation.peakTime() - this->_excitation.startTime());
}
The varying part of the to-begin motion depends on the current line and partition (for 3D sequences): the gradient area will be proportional to the line index, and, for a given maximal amplitude, will require a longer time for lines and partitions closer to the edge of the k-space. This means that, if we can fit the gradient lobe corresponding to extremal lines in the available time, then any other line will also fit in the same constraints. The current k-space information is available in the msl::KSpace class: we can add the following the the prepare function of the kernel.
NLSStatus
Kernel
::prepare(MrProt & protocol, SeqLim & limits, MrProtocolData::SeqExpo & exports)
{
auto const extremalKYZ = kSpace.k({0, kSpace.nMin()[1], kSpace.nMin()[2]});
}
Description of the discretized k-space geometry.
Definition KSpace.h:22
To actually run the gradient pulse, we will need to add a msl::GradientPulse object to the kernel class:
{
private:
};
Trapezoidal or arbitrary gradient pulse on three axes.
Definition GradientPulse.h:21
We will then prepare this gradient pulse to be the quickest while accomodating the larges k-space displacement possible, and set its start time so that it ends when the readout starts:
NLSStatus
Kernel
::prepare(MrProt & protocol, SeqLim & limits, MrProtocolData::SeqExpo & exports)
{
auto const extremalKYZ = kSpace.k({0, kSpace.nMin()[1], kSpace.nMin()[2]});
this->_toBeginFixed + extremalKYZ, gradientSpecs);
this->_toBegin.setEndTime(readout.startTime());
}
GradientPulse quickest(Vector3d const &k, double maxAmplitude, double minRiseTime)
Compute the quickest gradient pulse to achieve the given k-space vector.
In the run method, we will then get the index of the current point and convert it to a k-space vector:
NLSStatus
Kernel
::run(MrProt & protocol, SeqLim & limits, MrProtocolData::SeqExpo & exports)
{
auto const point =
.enabledPoints()[index];
auto const kPoint = kSpace.k(
- kSpace.nCenter());
}
Vector< 3, long > Vector3l
3D vector of longs
Definition Vector.h:139
The value of kPoint needs to be added to _toBeginFixed, and we can update the _toBegin gradient pulse, while keeping its duration.
NLSStatus
Kernel
::run(MrProt & protocol, SeqLim & limits, MrProtocolData::SeqExpo & exports)
{
this->_toBeginFixed + kPoint, this->_toBegin, gradientSpecs));
}
GradientPulse fixedDuration(Vector3d const &k, long duration, double maxAmplitude, double minRiseTime)
Compute a gradient pulse with the requested duration.
The last step is to run this gradient lobe in the real-time events block:
NLSStatus
Kernel
::run(MrProt & protocol, SeqLim & limits, MrProtocolData::SeqExpo & exports)
{
}
Gradient Spoiling
Similarly to the to-begin gradient, we need another gradient lobe after the echo: it must go back to the center of the k-space on the \(k_y\) and \(k_z\) axes in order to respect the steady-state conditions, and it must apply spoiling on the \(k_x\) axis.
The spoiling part is fixed and the back-to-center part varies according to lines and partitions, so we will use a similar design as before: add a msl::Vector3d for the spoiler and a msl::GradientPulse for both fixed and varying parts.
In the prepare function of the kernel, we define the spoiler based on the readout, and the gradient pulse to be the shortest based on extremal k-space lines.
NLSStatus
Kernel
::prepare(MrProt & protocol, SeqLim & limits, MrProtocolData::SeqExpo & exports)
{
this->_spoiler = readout.gradient().area()/2.;
this->_spoiler - extremalKYZ, gradientSpecs);
this->_toCenterAndSpoil.setStartTime(
readout.endTime() - readout.gradient().rampDuration());
}
In the run function, we update the gradient with respect to the current line, and run it in the real-time events block.
NLSStatus
Kernel
::run(MrProt & protocol, SeqLim & limits, MrProtocolData::SeqExpo & exports)
{
this->_spoiler - kPoint, this->_toCenterAndSpoil, gradientSpecs));
}
Checking the Kernel Timing
The RF and gradient pulses are now complete for the FLASH sequence. The kernel is however not yet robust: if the user a very low echo time or a very high resolution, the kernel will pass the preparation step, but the real-time events may overlap. There are two constraints to check that this will not happen:
- the start of the readout must happen after the end of the excitation
- the end of the readout must happend before the repetition time
Using the msl::RealTimeEvents::startTime and msl::RealTimeEvents::endTime functions, these constraints can be checked and an error status returned if needed:
#include <MrImaging/libSBB/libSBBmsg.h>
NLSStatus
Kernel
::prepare(MrProt & protocol, SeqLim & limits, MrProtocolData::SeqExpo & exports)
{
if(readout.startTime() < this->_excitation.endTime())
{
return MRI_SBB_SBB_NEGATIV_TEFILL;
}
if(readout.endTime() > protocol.tr()[0])
{
return MRI_SBB_SBB_NEGATIV_TRFILL;
}
return MRI_SEQ_SEQU_NORMAL;
}
Linking the Kernel and the Sequence
The final step is to fill the reconstruction-related information of the acquired data in the measurement data header or MDH. The MDH should be independent of the kernel as the kernel does not have the full information regarding the sequence graph and its loop structure: the code which fills the MDH will then be in the sequence. However, the MDH is contained in the sREADOUT object, itself in the msl::Readout object, which is prepared in the kernel. We can solve this by storing the sREADOUT in the registry, and adding a new parameter to the kernel constructor.
{
public:
static Pointer New(
std::string const & sampling, std::string const & trajectories,
std::string const & slices, std::string const & phaseCycling,
std::string const & adc);
private:
Kernel(
std::string const & sampling, std::string const & trajectories,
std::string const & slices, std::string const & phaseCycling,
std::string const & adc);
};
Kernel::Pointer
Kernel
::New(
std::string const & sampling, std::string const & trajectories,
std::string const & slices, std::string const & phaseCycling,
std::string const & adc)
{
return Pointer(new Kernel(sampling, trajectories, slices, phaseCycling, adc));
}
Kernel
::Kernel(
std::string const & sampling, std::string const & trajectories,
std::string const & slices, std::string const & phaseCycling,
std::string const & adc)
: AbstractNode(), _sampling(this, sampling), _trajectories(this, trajectories),
_slices(this, slices), _phaseCycling(this, phaseCycling), _adc(this, adc)
{
}
We then store the correct value in the prepare function of the kernel: as opposed to the preceding uses of the registry, the informations flows out of the kernel.
NLSStatus
Kernel
::prepare(MrProt & protocol, SeqLim & limits, MrProtocolData::SeqExpo & exports)
{
this->_adc() = &readout.adc();
}
We need to adapt the sequence class to reflect this:
NLSStatus
FLASH
::initialize(SeqLim & limits)
{
"trajectories", {
Kernel::New(
"sampling", "trajectories", "slices", "phaseCycling", "adc"),
}
);
this->_root->registry()->insert({
{"adc", (sREADOUT*)(nullptr)}
});
}
Finally, we can update the MDH from the sequence, by adding an msl::graph::Action node in the sequence graph, before calling the kernel.
{
private:
};
Graph-based sequence class.
Definition Sequence.h:37
NLSStatus
FLASH
::initialize(SeqLim & limits)
{
"trajectories", {
Kernel::New(
"sampling", "trajectories", "slices", "phaseCycling", "adc"),
}
);
}
The updateMDH function will
- Set the slice and average counters (the line and partition counters are set in msl::samplings::Cartesian)
- Tell the reconstruction to perform the Fourier transform once all the data has been received, using the setPhaseFT and setPartitionFT functions
- Notify the system when the current line is the first or the last
Its full code is:
void
FLASH
{
auto & adc = *registry->get<sREADOUT*>("adc");
auto & mdh = adc.getMDH();
auto const & trajectories = registry->get<
msl::Counter>(
"trajectories");
auto const & averages = registry->get<
msl::Counter>(
"averages");
mdh.setCslc(uint16_t(slices.
index()));
mdh.setCacq(uint16_t(averages.
index()));
mdh.setPhaseFT(trajectories.last() && averages.
last());
mdh.setPartitionFT(trajectories.last() && averages.
last());
mdh.setLastScanInMeas(trajectories.last() && slices.
last() && averages.
last());
mdh.setLastScanInConcat(mdh.isLastScanInMeas());
mdh.setLastScanInSlice(trajectories.last());
mdh.setFirstScanInSlice(trajectories.first());
mdh.addToEvalInfoMask(MDH_ONLINE);
}
bool last() const
Test if the index is equal to end-1.
Definition Iterator.h:94
std::size_t index() const
Return the current index.
Definition Iterator.h:53
Counter from 0 (included) to end (excluded).
Definition Counter.h:14
bool last() const
Test if the index is equal to end-1.
std::size_t index() const
Return the current index.
ConstIterator< std::vector< sSLICE_POS > > SliceConstIterator
Non-mutable iterator to a vector of slice specifications.
Definition SliceIterator.h:21
Full Code
FLASH.h
FLASH.cpp
Kernel.h
Kernel.cpp
makefile.trs