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.
initializeBatch
method according to your sequence
Step 6: Define your Here, you will create dummy pulses that will be initialized for each new batch.
createBatches
method
Step 7: Define your 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 cmdfov
: fied of view displacement in mmaxesOrientation
: orientation of your reference systemangle
: angle of rotation in degreesrotationAxis
: 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.