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

Home

Introduction

This feature introduces an efficient method for changing waveform shapes during scan time. In GE terminology, this preallocates sequencer memory for multiple waveform shapes (states) under one instruction. Instead of rewriting the entire waveform in sequencer memory (like with ks_wave2hardware), you can adjust the shape by simply modifying the waveform pointer in the instruction.

In KS terms, this allows you to generate up to KS_WAVE_MAXNSTATES (currently 16) waveform shapes of equal duration for each KS_WAVE object. The primary motivation for this feature is that we often know the different waveform shapes in advance of the pulse sequence execution. This allows us to account for gradient heating and SAR (Specific Absorption Rate) calculations efficiently.

In the past, you would have relied on wave2hardware(), which is not optimal for pre-scan checks and may result in faulty waveforms if your code contains bugs. The introduction of wave states ensures more predictable behaviour and greater efficiency, as waveforms are transferred to sequencer memory only once before scanning begins.

Constraints

When you generate (pulsegen, pg) any KS_WAVE, it automatically adds the waveform to state0. This waveform must have the highest amplitude among all states because all subsequent states for that instruction use the same amplitude scaling factor (ampscale). Thus, to ensure the correct amplitude (i.e., iamp * iwave), the scaling must occur within waveform memory (iwave), which must not exceed a value of 1. Although we plan to automate this process in the future, you will currently receive an error if you attempt to add a state with an amplitude greater than state0.

Example

In this example, we generate a KS_WAVE that is a cosine function modulated by a sin function (a waveform starting and ending with zero amplitude). We will use the maximum number of states (KS_WAVE_MAXNSTATES) to represent different phase shifts of the cosine function. This wave will be placed on all three gradient boards at the same location, and we will add the wave states generated in eval(). Finally, we modify the wave state of the three KS_WAVE instances based on the shot number.

First, declare KS_WAVEFORMS that will represent the states. Here, we group them with the KS_WAVE object in TRIG_WAVE:

@global
#define NTRIG_STATES KS_WAVE_MAXNSTATES // do as many as possible
typedef struct _TRIG_WAVE {
KS_WAVE wave; // the wave we want to modify with wavestates
KS_WAVEFORM[NTRIG_STATES] states; // memory for storing different shapes
} TRIG_WAVE;
#define INIT_TRIG_WAVE {KS_INIT_WAVE, {0}} // good idea to put it into a known state

The KS_WAVEFORMS used for states need to be exported with the KS_WAVE object. The TRIG_WAVE struct we just defined neatly packages the waveform states with the wave.

@ipgexport
TRIG_WAVE trig = INIT_TRIG_WAVE;

Next, generate the waveforms. We advise caution when using mathematical functions on the target, but as we are on the host, using cosf and sinf is acceptable:

@host //typically somewhere reasonable called from the cvcheck() function - where you eval all other objects on the host
int npts = 512; // num of points in the wave
float omega_sin = PI / (float)npts; // for a windowing function that ensures we start and end at 0
float omega_cos = 6 * omega_sin; // three periods of the cosine function
float phase_step = 2 * PI / (float)NTRIG_STATES; // to change the shape for each state
int state = 0;
int pt = 0;
for(state=0; state < NTRIG_STATES; state++) {
for(pt = 0; pt < npts; pt++) {
trig.states[state][pt] = sinf(omega_sin * pt) * cosf(omega_cos * pt + phase_step * state);
}
}
STATUS status;
status = ks_eval_wave(&trig.wave, "STATELY_WAVE_EXAMPLE", npts, npts * GRAD_UPDATE_TIME, trig.states[0]);
KS_RAISE(status); // stop and propagate error if something went wrong

Now, we place the wave on all three gradient boards and add all states:

@pg // typically wrapped up in a pg function. Remember this needs to be called on the host and target from pulsegen()
STATUS status;
// To illustrate instance based control we now place the wave on each gradient board creating 3 instances
int board = 0; // 0=XGRAD, 1=YGRAD 2=ZGRAD
for(board=XGRAD; board<ZGRAD; board++) {
loc.board = board;
status = ks_pg_wave(&trig.wave, loc, seq_ctrl);
KS_RAISE(status);
}
// Note how states and instances are orthogonal, during the scan each instance can be set to any state.
int state,
for(state=1; state < NTRIG_STATES; state++) { // start at 1 because pg took care of special state0
status = ks_pg_addwaveformstate(&trig.wave, trig.states[state], state);
KS_RAISE(status);
}

Now the fun part, switching waveforms during scan time:

@pg // somewhere in the scan_loop, perhaps in the coreslice function where we have access to KS_DYNAMIC_STATE (dynamic)
int board = 0; // 0=XGRAD, 1=YGRAD 2=ZGRAD
for(board=XGRAD; board<ZGRAD; board++) {
int arbitrary_state;
if (dynamic->shot > 0) { //ensure we compute a valid state
arbitrary_state = (dynamic->shot + board * 4) % NTRIG_STATES; // set the state based on the shot and board
} else {
arbitrary_state = (board * 4) % NTRIG_STATES; // set the state based on the shot and board
}
ks_scan_setwavestate(&trig.wave, arbitrary_state, board /*instance*/); // in this example board is mapped to instance
}

As these modifications are known on the host, they can be recorded and viewed in the html plotting. Checkout the configurations slider.

ks_scan_wave2hardware - when there aren't enough states

In cases where you require hundreds or even thousands of unique waveforms, you might run out of sequencer memory or hit the KS_WAVE_MAXNSTATES limit (currently a conservative 16). In such situations, you will need to use ks_scan_wave2hardware.

Here’s the function signature:

void ks_scan_wave2hardware(KS_WAVE *wave, const KS_WAVEFORM newwaveform, int state);

Unlike ks_pg_addwaveformstate (used above), this function is a scan-time version and doesn’t account for gradient and SAR heating. Be cautious when using it and ensure your sequence is in a representative state for the grad_heat_play function. It is also not possible to use ks_scan_wave2hardware on a KS_WAVE with only one state. This is because the pulse sequence code and sequencer are asynchronous, you could therefore modify the waveform shape of the previous TR while it is actually playing. Fortunately, each KS_WAVE object has a .current_state member which is an array of length ninstances (how many times the wave was placed) that keeps track of which states are currently active on the sequencer. If you try call ks_scan_wave2hardware on an active state the scan will continue without doing anything, so you will need to check the logfile ks_error.txt to see if you have actually been successful (empty is good).

Example continued - with wave2hardware

Let’s modify the trig wave to decay over many shots using wave2hardware:

@pg // somewhere in the scan_loop, perhaps in the coreslice function where we have access to KS_DYNAMIC_STATE * (dynamic)
int board = 0; // 0=XGRAD, 1=YGRAD 2=ZGRAD
for(board=XGRAD; board<ZGRAD; board++) {
int arbitrary_state;
if (dynamic->shot > 0) { //ensure we compute a valid state
arbitrary_state = (dynamic->shot + board * 4) % NTRIG_STATES; // set the state based on the shot and board
} else {
arbitrary_state = (board * 4) % NTRIG_STATES; // set the state based on the shot and board
}
int pt;
for(pt = 0; pt < trig.wave.res; pt++) {
trig.states[arbitrary_state][pt] *= 0.9; // (can be any KS_WAVEFORM or float array) here we overwrite the global variable to make the wavesforms 90% smaller each time this code is called
}
ks_scan_wave2hardware(&trig.wave, trig.states[arbitrary_state], arbitrary_state); // write in the new waveform to sequencer memory
ks_scan_setwavestate(&trig.wave, arbitrary_state, board /*instance*/); // play the new waveform in the next TR
}

As these changes are not recorded for html plots. To debug you will need to use the GE tool called 'plotter'.

Home