ODIN sequence modelling framework (odinseq library)

This page describes the design guidelines of the module Classes for sequence design (odinseq library)

Introduction

This is a framework for NMR sequence design. It follows a hardware independent, object oriented design approach to reach a high degree of flexibilty and portability while keeping the amount of source code to a minimum.

Motivation

An NMR sequence typically consists of different components like pulses, gradient shapes and so on. From the physicists point of view these objects and their relations can be exclusively described by physical parameters (pulse duration, gradient stength, echo time, ...). This description does not depend on the current hardware the sequence is used on. Furthermore, at this level of abstraction these objects can be grouped together in different ways to form a variety of NMR sequences.

Unfortunately most programming enviroments of contemporary spectrometers/scanners do not reflect this modularity. A sequence written in the native enviroment of the machine often contains a vast amount of redundant source code repeated in every method and a difficult low-level interface. Furthermore each manufacturer follows his own approach to write unportable sequences, which may be a disadvantage for scientists working on different spectrometers.

This is the point where this framework could come into play. It offers a hardware independent programming interface to a C++ class library. This library then performs all the low-level operations that are required for playing out the sequence.

Design Principles

The class hierachy (Classes for sequence design (odinseq library)) is designed according to the following guidelines:

Class Hierachy

Each type of component in an NMR sequence (RF-pulse, gradient pulse, ...) is identified with a class in the C++ programming language. The components are ordered in a 'family tree' where each child is a specialisation of its parent. For instance a constant gradient (SeqGradConst) on one of the gradient channels has a parent (SeqGradChan) that represents arbitrarily shaped gradient waveforms. This inheritance graph is modelled by the inheritence mechanisms for classes in C++. This approach minimises the amount of code that perform certain operations on the objects, e.g. a function for rotating the SeqGradChan class in the spatial domain (set_gradrotmatrix()) is automatically available for all derived classes.

Sequence Objects

In this manual the term 'sequence objects' refers to all components of an NMR experiment that control the timing of the sequence. For example RF-pulses, delays and acquisition windows are all specialised sequence objects. An (abstract) base class SeqObjBase exists for all sequence objects. These objects then have a common interface, e.g. they know how to write themselve to the pulse/gradient program.

Gradient Objects

The term 'gradient objects' refers to all components of an NMR sequence that control the timecourse of the gradient fields, e.g. constant gradients, phase encoding gradients, gradient waveforms. An (abstract) base class SeqGradInterface exists for all gradient objects in the sequence. These objects then have a common interface, i.e. they know how to write themselve to the gradient program or how to be combined among themselves.

Grouping of Sequence Objects

Sequence objects can be grouped together by ordering them along the time axis. The result is then a new, more complex sequence object. In analogy to notations commonly used in scientific papers, this is done by using the + operator. For instance two RF-pulses represented by two objects called alpha and beta can build a new sequence object

alpha + beta

which results in a sequence object which will play out the first pulse and then the second pulse.

Grouping of Gradient Objects

Gradient objects can also be serialised by using the + operator. Furthermore the / operator tries to build a new object that plays out the two operands in parallel if the timing is apropriate. For example two gradient pulses gp1 and gp2, one for the read and another for the phase channel, can be played out in parallel by using the combination

gp1 / gp2

Combining Sequence and Gradient objects

Sequence and gradient objects can also be serialised by the + operator. To play out a gradient and a sequence object in parallel, the / operator can be used. For example let alpha be an excitation pulse and constgrad a constant gradient, then the combination

alpha / constgrad

would give a slice selective pulse.

Container for Sequence Objects

The sequence object SeqObjList can be used as a container for other sequence objects. That is, each SeqObjList can have its own sub-sequence that consists of other sequence components. If a certain operation is applied to a SeqObjList object, the operation will also be applied to all elements of the sub-sequence. For example:

SeqPuls alpha; // excitation pulse
SeqGradConstPulse constgrad; // constant gradient
SeqObjList objlist = alpha + constgrad; // alpha is played out after constgrad

Calling the member function

objlist.get_duration()

would then return the sum of the durations of alpha and constgrad.

Container for Gradient Objects

The gradient object SeqGradChanList can be used as a container for other gradient objects which share the same gradient channel. That is, each SeqGradChanList can have its own sub-sequence that consists of other gradient components. These sub-objects are then played out subsequently. This analogous to the above described sequence container.

Appending to Container Objects

New objects can be added to the container class SeqObjList by using the += operator.

Sequence Loops ans Vectors

Loops are special sequence containers. They are used to repeat a certain part of the sequence. Within ODIN, the class SeqObjLoop accomplishes this task. Two kind of loops are possible when using this class: Pure repitition loops and vector loops. Repitition loops simply repeat the specified part of the sequence without altering it. Let loop be of type SeqObjLoop, then heir syntax is:

loop ( kernel ) [ n ];

This statement will return a sequence object that repeats the sequence object kernel n times, where n is an integer number.

Vector loops also repeat a specified part of the sequence. Furthermore they increment the value of a sequence vector each time it is played out. Sequence vectors are for example phase encoding gradients or pulses with a frequency list. All sequence vectors share the same base class SeqVector. Let loop be of type SeqObjLoop, then the syntax for vector loops is:

loop ( kernel ) [ vector1 ] [ vector2 ] ...;

This statement will return a sequence object that repeats the sequence object kernel while incrementing the values of the attached sequence vectors vector1, vector2, ... The vectors must contain the same number of values. The loop is then repeated this number of times.

Specialialized Sequence Vectors

Physical Units

ODIN is consistent corncerning the physical units. That is, everywhere in the library the following units and their combinations are used:

This system has shown to be of practical value for NMR because the numbers are then in a reasonable range. For example the gradient strength is then given in mT/mm and the frequency is given in 1/ms=kHz. The only exception is the angular unit which is treated internally as rad but can be specified at the interface functions in degree for phaselists and flipangles.

Coding Standards

Building new classes

If you want to write your own class that can be plugged into the ODIN framework, please use the following form for your class 'SeqMyClass' that is derived from 'SeqBaseClass':

class SeqMyClass : public SeqBaseClass {
public:
SeqMyClass(const STD_string& object_label, float parameter1, ... );
SeqMyClass(const STD_string& object_label = "unnamedSeqMyClass" );
SeqMyClass(const SeqMyClass& sct);
SeqMyClass& operator = (const SeqMyClass& sct);
~SeqMyClass();
private:
float parameter1;
};
SeqMyClass::SeqMyClass(const STD_string& object_label, float parameter1_value, ... ) : SeqBaseClass(object_label) {
parameter1=parameter1_value;
...
}
SeqMyClass::SeqMyClass(const STD_string& object_label ) : SeqBaseClass(object_label) {
parameter1=0.0;
...
}
SeqMyClass::SeqMyClass(const SeqMyClass& sct) {
SeqMyClass::operator = (sct);
}
SeqMyClass& SeqMyClass::operator = (const SeqMyClass& sct) {
SeqBaseClass::operator = (sct);
parameter1=sct.parameter1;
...
return *this;
}
SeqMyClass::~SeqMyClass() {
}

Following this standard prevents the base classes from getting confused by not correctly initialising or copying them.

Using the C++ standard library

Unfortunately, on some systems the stanard C++ library seems to be either broken or absent. For these platforms, ODIN contains its own implementation that is a subset of the original library. To use the same source code on different platforms, classes of the standard library are used together with the prefix STD_, e.g. STD_list instead of std::list. These macros will be replaced by the appropriate classes on each platform.

Debugging/Tracing

To generate debugging and tracing output, please use the Log template class instead of using streams. Instances of that class offer a stream to log trace messages via the ODINLOG macro:

int MyClass::myfunction() {
Log<Seq> odinlog("MyClass","myfunction");
ODINLOG(odinlog,significantDebug) << "Hello World" << STD_endl;
}

Which will result in the following output:

Seq | MyClass.myfunction : START
Seq | MyClass.myfunction : Hello World
Seq | MyClass.myfunction : END

It can generate debugging output at different levels of verbosity and the output is redirected to the native logging channel (console, log file) of the current platform. Furthermore, it can be completely removed from the executable for release compilations in a safe way without changing the source code.