KSFoundation  [October2024]
A platform for structured EPIC programming on GE MR systems
HowTo - Generic Looping

Home

KSFoundation now offers a series of modules for scan-time looping that are independent from the specific sequence being run. All sequences, with the notable exception of ksgre_tutorial, have been modified to employ these generic looping modules. Versions of each sequence that use the old looping functions have been preserved in order to assist users in porting their code but are now marked as deprecated and will be removed in a future release.


The dynamic state

All generic looping depends on KS_DYNAMIC_STATE, a struct that contains all parameters that change dynamically during a scan. All loop indices have been moved here. Note that, since this structure is defined in KSFoundation.h, it can not be modified.


The generic sequence module

To employ a generic looping module, a function of the following prototype must be implemented (here named my_coreslice() but any name works).


KS_CORESLICETIME my_coreslice(const SCAN_INFO *slice_pos, KS_DYNAMIC_STATE *dynamic);


This function is expected to contain one or more sequence modules and play them in the desired order using the provied slice position and dynamic state, which comes from the generic looping modules in ksscan.cc (and ksinversion.cc for inversion).

Important: The returned value is a KS_CORESLICETIME structure containing two fields; the total duration of all sequence modules played (.duration) and the reference time of the excitation (.referencetimepoint)

It is critical that the two fields of KS_CORESLICETIME are filled in properly:

  • .duration: The total duration of all sequence modules that are played in my_coreslice()
  • .referencetimepoint: The time since the start of the first module to the reference time point, which is typically the isocenter of the excitation pulse in the main sequence. For a single sequence module, this may be a couple of ms until the center of the excitation pulse is reached, but with additional prep modules played before, .referencetimepoint needs to account for those modules, while any module played after the main sequence should only contribute to the .duration field of KS_CORESLICETIME.


In ksepi_implementation.e, ksfse_implementation.e, and ksgre_implementation.e, the function name ends with group to make it clear that this function may play more than one module:


ksepi_implementation.e:

KS_CORESLICETIME ksepi_scan_coreslicegroup(const SCAN_INFO *slice_pos, KS_DYNAMIC_STATE *dynamic);


ksfse_implementation.e:

KS_CORESLICETIME ksfse_scan_coreslicegroup(const SCAN_INFO *slice_pos, KS_DYNAMIC_STATE *dynamic);


ksgre_implementation.e:

KS_CORESLICETIME ksgre_scan_coreslicegroup(const SCAN_INFO *slice_pos, KS_DYNAMIC_STATE *dynamic);


For instance, the ksgre.e sequence implements this function as shown below, where the main sequence module is played after a saturation module.

Here, the sat module is played with kssat_scan() using the global kssat KSSAT_MODULE variable declared in @ipgexport in ksmodules/kssat.e. If the sat module is turned off in the UI, kssat_scan() will return zero duration and not play the module.

The main GRE module is played after the sat module with ksgre_scan_coreslice(), also defined in ksgre_implementation.e and which contains the global ksgre (KSGRE_SEQUENCE) defined in @ipgexport in ksgre_implementation.e, holding all sequence data.

KS_CORESLICETIME ksgre_scan_coreslicegroup(const SCAN_INFO *slice_pos, KS_DYNAMIC_STATE *dynamic) {
int premainseq_duration = 0;
/* Sat */
premainseq_duration += kssat_scan(slice_pos, dynamic, &kssat); /* returns an integer duration in [us] */
/* Main */
coreslicetime = ksgre_scan_coreslice(slice_pos, dynamic); /* returns a KS_CORESLICETIME */
coreslicetime.duration += premainseq_duration;
coreslicetime.referencetimepoint += premainseq_duration;
return coreslicetime;
} /* ksgre_scan_coreslicegroup() */

Note that there is no restriction on how many times the main sequence module, or any other sequence module for that matter, is played within this function. Indeed, one could implement MP-RAGE by modifying ksgre_scan_coreslicegroup() to loop over a certain number of coordinates and call to ksgre_scan_coreslice() for each one.


The standard looping module

Most sequences follow the same common looping pattern. In particular, they loop over:

  • volumes
  • passes (a.k.a. acquisitions)
  • shots
  • averages (NEX)
  • slices/slabs

where the volume loop is the outermost and the one over slices/slabs is the innermost. This pattern has been implemented in a generic way (for use with any PSD) via the structure KSSCAN_LOOP_CONTROL. This structure contains a phase encoding plan (KS_PHASEENCODING_PLAN), a slice timing plan (KS_SLICETIMING) and all other parameters necessary to perform the above loops – e.g. number of averages or number of dummy shots.

Contrary to other structures in KSFoundation, generic looping structures are not intended to be modified directly. Instead, a separated design should be set up first with the desired configuration.

In the case of KSSCAN_LOOP_CONTROL, the design structure is KSSCAN_LOOP_CONTROL_DESIGN which, in turn, contains design structures for the phase encoding and slice timing plans. Common for all KS***_DESIGN structs are that they are short-lived recipes of what is desired and have typically local scope, here setting up a global KSSCAN_LOOP_CONTROL for use in scan.

For instance, one could set the following simple configuration in cveval():

loop_design.dda = 1;
loop_design.naverages = 1; /* NEX */
loop_design.nvols = 1;
/* Slice timing */
loop_design.slicetiming_design.is3D = 0; /* 2D acquisition */
loop_design.slicetiming_design.nslices = opslquant;
loop_design.slicetiming_design.minaqs = 2;
/* Generate the list of k-space coordinates */
KS_NOTSET, /* 2D acquisition */
0, 0, /* Partial-Fourier off */
1, 1, /* No acceleration*/
0, 0, /* No ACS lines needed */
RECTANGULAR, /* calibration shape */
RECTANGULAR, /* kspace coverage */
0, 0, 0, 0);
/* Phase encoding */
loop_design.phaseenc_design.encodes_per_shot = opetl;
loop_design->phaseenc_design.center_encode = 0;

Once the design struct is initialized with the desired values, one can pass it to the function ksscan_loop_control_eval_design() in order to set up the actual instance of KSSCAN_LOOP_CONTROL like the following:

s = ksscan_loop_control_eval_design(&my_loop_control, /* Output: KSSCAN_LOOP_CONTROL to steer the scan looping and phase encode changes */
&my_seq.seqctrl, /* Input: KS_SEQ_CONTROL struct of the main sequence */
my_coreslice(), /* Input: function pointer returning KS_CORESLICETIME as shown earlier */
&loop_design); /* Input: Scan looping design */

where my_loop_control and my_seq are globals declared in the @ipgexport section of the PSD (ks***_implementation.e)

@ipgexport
MY_SEQUENCE my_seq = INIT_MY_SEQUENCE; /* for example: KSGRE_SEQUENCE mygre = KSGRE_INIT_SEQUENCE */

Now, during the scan entry point, my_loop_control can be used to execute any looping function defined in ksscan.h. For instance, a complete looping can be achieved with:

ksscan_scanloop(&my_loop_control, NULL, my_coreslice());


Allocation of the phase encoding plan

The most astute readers might have noticed that no evaluation is performed on the IPG side whereas, in previous version of KSFoundation, the phase encoding plan had to be generated twice, both on HOST and IPG.

This was needed because the plan was dynamically generated and there is no mechanism to copy dynamically allocated memory to the IPG in EPIC.

With the introduction of the generic looping modules, the phase encoding plan allocation was switched to use a single memory pool, that is a static array declared in @ipgexport

@ipgexport
#define MY_PHASEENCODING_MEMORYPOOL_SIZE 10000
KS_PHASEENCODING_COORD my_phaseencoding_memorypool[MY_PHASEENCODING_MEMORYPOOL_SIZE] = {KS_INIT_PHASEENCODING_COORD};

This memory pool will be used by KSFoundation to allocate all phase encodsing plan's entries needed by the PSD. Therefore its size should be chosen to accomodate all possible usage scenarios. In order for the declared static array to be used as a memory pool, the programmer needs to register its address and size with:

s = ks_phaseencoding_memorypool_init(my_phaseencoding_memorypool,
MY_PHASEENCODING_MEMORYPOOL_SIZE);

both on HOST and IPG, for instance, in cveval() and pulsegen() respectively.

Inversion

Home