Create your own sequence PyPulseq version - josalggui/MaRGE GitHub Wiki

Adding a New Sequence to the GUI in MaRGE

Starting from version v0.6.0, MaRGE allows users to write their own sequences using PyPulseq. The process of coding sequences with PyPulseq that are compatible with MaRGE follows a similar structure to the mriBlankSeq framework. In fact, sequences still inherit from mriBlankSeq, which now includes methods to convert PyPulseq files into mriBlankSeq arrays.

You can find a sequence template in seq/sequence_template.py, which includes instructions on how to fill out the script to make it compatible with MaRGE, allowing you to run the sequence from the graphical user interface (GUI).

The fundamental structure remains the same as when using the standard mriBlankSeq framework. The sequence must include the following methods: sequenceInfo, sequenceTime, sequenceAtributes, sequenceRun, and sequenceAnalysis. The only major difference is in the sequenceRun method, where the sequence is coded using PyPulseq.

Step 1: Define the interpreter for FloSeq/PSInterpreter

The interpreter is responsible for converting the high-level pulse sequence description into low-level instructions for the scanner hardware. Typically, this interpreter is updated during scanner calibration.

self.flo_interpreter = PSInterpreter(
    tx_warmup=hw.blkTime,  # Transmit chain warm-up time (us)
    rf_center=hw.larmorFreq * 1e6,  # Larmor frequency (Hz)
    rf_amp_max=hw.b1Efficiency / (2 * np.pi) * 1e6,  # Maximum RF amplitude (Hz)
    gx_max=hw.gFactor[0] * hw.gammaB,  # Maximum gradient amplitude in X (Hz/m)
    gy_max=hw.gFactor[1] * hw.gammaB,  # Maximum gradient amplitude in Y (Hz/m)
    gz_max=hw.gFactor[2] * hw.gammaB,  # Maximum gradient amplitude in Z (Hz/m)
    grad_max=np.max(hw.gFactor) * hw.gammaB,  # Maximum gradient amplitude (Hz/m)
    grad_t=hw.grad_raster_time * 1e6  # Gradient raster time (us)
)

Step 2: Define system properties using PyPulseq (pp.Opts)

These properties define the capabilities of the MRI scanner hardware, such as maximum gradient strengths, slew rates, and dead times. They are typically set based on the hardware configuration file (hw_config).

self.system = pp.Opts(
    rf_dead_time=hw.blkTime * 1e-6,  # Dead time between RF pulses (s)
    max_grad=np.max(hw.gFactor) * 1e3,  # Maximum gradient strength (mT/m)
    grad_unit='mT/m',  # Units of gradient strength
    max_slew=hw.max_slew_rate,  # Maximum gradient slew rate (mT/m/ms)
    slew_unit='mT/m/ms',  # Units of gradient slew rate
    grad_raster_time=hw.grad_raster_time,  # Gradient raster time (s)
    rise_time=hw.grad_rise_time,  # Gradient rise time (s)
    rf_raster_time=1e-6,
    block_duration_raster=1e-6
)

Step 3: Perform any necessary calculations for the sequence

At this step, you should perform calculations such as timing, RF amplitudes, and gradient strengths before defining the sequence blocks.

Step 4: Define the experiment to obtain the true bandwidth

You need to obtain the actual bandwidth used in the experiment. To do this, define an experiment and retrieve the sampling period using get_rx_ts()[0].

if not self.demo:
    expt = ex.Experiment(
        lo_freq=hw.larmorFreq,  # Larmor frequency in MHz
        rx_t=sampling_period,  # Sampling time in us
        init_gpa=False,  # Initialize GPA board (False for True)
        gpa_fhdo_offset_time=(1 / 0.2 / 3.1),  # GPA offset time calculation
        auto_leds=True  # Automatic control of LEDs (False or True)
    )
    sampling_period = expt.get_rx_ts()[0]  # us
    bw = 1 / sampling_period / hw.oversamplingFactor  # MHz
    print("Acquisition bandwidth set to: %0.3f kHz" % (bw * 1e3))
    expt.__del__()
self.mapVals['bw_MHz'] = bw
self.mapVals['sampling_period_us'] = sampling_period

Step 5: Define the sequence blocks

At this step, define the building blocks of the MRI sequence, including RF pulses and gradient pulses.

Note: The method that generates the mriBlankSeq waveform from the PyPulseq sequence automatically incorporates the gradient delay specified in the hw_config.py file. This means you do NOT have to adjust your sequence blocks for gradient delays. However, ensure that the first gradient does not start earlier than the gradient delay; otherwise, it will result in a negative time for the first gradient instruction, leading to a timing error.

Step 6: Define your initializeBatch method according to your sequence

Here, you will create dummy pulses that will be initialized for each new batch.

Step 7: Define your createBatches method

In this step, you populate the batches by adding the blocks defined in step 5, and ensure that you account for the number of acquired points to determine if a new batch is required.

Step 8: Run the batches

This step handles the execution of different batches and retrieves the resulting data. This step should not be modified. The oversampled data will be available in self.mapVals['data_over'].

waveforms, n_readouts = createBatches()
return self.runBatches(waveforms,
                       n_readouts,
                       frequency=hw.larmorFreq,  # MHz
                       bandwidth=bw_ov  # MHz
                       )

Make your sequence compatible with angulation

The method runBatches is able to account for angulation if the sequence is properly configured. To make your image sequence compatible with angulation, you should add a few code to your sequence. First, make sure you have these input parameters:

  • fov: field of view in cm
  • dfov: fied of view displacement in mm
  • axesOrientation: orientation of your reference system
  • angle: angle of rotation in degrees
  • rotationAxis: rotation axis
self.addParameter(key='fov', string='Field of View (cm)', val=[15.0, 15.0, 15.0], units=units.cm, field='IM',
                  tip='Field of View (cm).')
self.addParameter(key='dfov', string='dFOV[x,y,z] (mm)', val=[0.0, 0.0, 0.0], units=units.mm, field='IM',
                  tip="Position of the gradient isocenter")
self.addParameter(key='axesOrientation', string='Axes[rd,ph,sl]', val=[2, 1, 0], field='IM',
                  tip="0=x, 1=y, 2=z")
self.addParameter(key='angle', string='Angle (ยบ)', val=0.0, field='IM',
                  tip='Angle in degrees to rotate the fov')
self.addParameter(key='rotationAxis', string='Rotation axis', val=[0, 0, 1], field='IM',
                  tip='Axis of rotation')

Then, copy your sequenceAtributes() method as follows:

def sequenceAtributes(self):
    super().sequenceAtributes()

    # Conversion of variables to non-multiplied units
    self.angle = self.angle * np.pi / 180  # rads

    # Add rotation, dfov and fov to the history
    self.rotation = self.rotationAxis.tolist()
    self.rotation.append(self.angle)
    self.rotations.append(self.rotation)
    self.dfovs.append(self.dfov.tolist())
    self.fovs.append(self.fov.tolist())

and make sure to incude this in step 3:

# Set the fov
self.dfov = self.getFovDisplacement()
self.dfov = self.dfov[self.axesOrientation]
self.fov = self.fov[self.axesOrientation]

The last step will give you the field of view displacement in the new reference system, so you can use it for image reconstruction.