Create your own sequence - josalggui/MaRGE GitHub Wiki

Step-by-Step Tutorial: Adding a New Sequence to the GUI in MaRGE

In this tutorial, we'll guide you through the process of creating and adding a new sequence to MaRGE. By following these steps, you'll be able to run simple sequences within the GUI. We'll use the example of creating a noise measurement sequence as a starting point.

Body of the Sequence

  1. Create a New Sequence File:

    • Start by creating a new Python file for your sequence. In this example, we'll create a noise.py file inside the seq folder.
  2. Import Required Modules:

    • In your sequence file, import the necessary modules. Ensure access to the experiment class from the marcos_client repository or experiment_gui from controller module and the mriBlankSequence class. New sequences should inherit from the mriBlankSequence class. Also, import the configs.hw_config module for hardware properties and configs.hw_units for setting units of input parameters.
    • Additionally, I add MaRGE and marcos_client directories to the paths to allow standalone execution.
import os
import sys
#*****************************************************************************
# Get the directory of the current script
main_directory = os.path.dirname(os.path.realpath(__file__))
parent_directory = os.path.dirname(main_directory)
parent_directory = os.path.dirname(parent_directory)

# Define the subdirectories you want to add to sys.path
subdirs = ['MaRGE', 'marcos_client']

# Add the subdirectories to sys.path
for subdir in subdirs:
    full_path = os.path.join(parent_directory, subdir)
    sys.path.append(full_path)
#******************************************************************************
import controller.experiment_gui as ex
import numpy as np
import seq.mriBlankSeq as blankSeq  # Import the mriBlankSequence for any new sequence.
import configs.hw_config as hw
import configs.units as units
  1. Create the Noise Sequence Class:
    • In your sequence file (noise.py), create the Noise class that will inherit from the mriBlankSeq.MRIBLANKSEQ class. Include the following methods: sequenceInfo, sequenceTime, sequenceRun, and sequenceAnalysis.
class Noise(mriBlankSeq.MRIBLANKSEQ):
    def __init__(self):
        super(Noise, self).__init__()

    def sequenceInfo(self):
        # Provide sequence information here

    def sequenceTime(self):
        # Return the time required by the sequence in minutes

    def sequenceRun(self, plotSeq=0, demo=False, standalone=False):
        self.demo = demo
        self.plotSeq = plotSeq
        self.standalone = standalone
        
        # Create sequence instructions
        
        # Input instructions into the Red Pitaya
        
        # Use the plotSeq argument to control plotting versus running.
        
        # Use the demo argument to control if you want to simulate signals.

    def sequenceAnalysis(self, mode=None):
        self.mode = mode
        ...
        result1 = {}
        result2 = {}
        ...
        self.output = [result1, result2]
        
        return self.output
        
        
        # Implement data analysis logic here

Adding Input Parameters to the Sequence

To configure and customize your sequence within MaRGE, add input parameters using the addParameter method in the constructor of your sequence class. Mandatory parameters are seqName and toMaRGE

  • seqName is a string that will be used later to identify the sequence within MaRGE
  • toMaRGE is a bool that must be True to be included in MaRGE from release v0.9.0
def __init__(self):
    super(Noise, self).__init__()

    # Input parameters
    self.addParameter(key='seqName', string='NoiseInfo', val='Noise')
    self.addParameter(key='toMaRGE', val=True)
    self.addParameter(key='larmorFreq', string='Central frequency (MHz)', val=3.00, field='RF', units=units.MHz)
    self.addParameter(key='nPoints', string='Number of points', val=2500, field='RF')
    self.addParameter(key='bw', string='Acquisition bandwidth (kHz)', val=50.0, field='RF', units=units.kHz)
    self.addParameter(key='rxChannel', string='Rx channel', val=0, field='RF')

Defining the sequenceInfo Method

Provide useful information about the sequence.

def sequenceInfo(self):
    print("Be open-minded,\nDo open-source")

Implementing the sequenceTime Method

Return the duration of the sequence in minutes.

def sequenceTime(self):
    return 0  # Duration in minutes (modify as needed)

Implementing the sequenceRun Method

Manage the input of instructions into the Red Pitaya and control whether to run or plot the sequence.

def sequenceRun(self, plotSeq=0, demo=False, standalone=False):
     self.demo = demo
     self.plotSeq = plotSeq
     self.standalone = standalone
    
    # Update bandwidth by oversampling factor
    self.bw = self.bw * hw.oversamplingFactor  # Hz
    samplingPeriod = 1 / self.bw  # s

    # Create the experiment object
    self.expt = ex.Experiment(
        lo_freq=self.larmorFreq * 1e-6,  # MHz
        rx_t=samplingPeriod * 1e6,  # us
        init_gpa=False,
        gpa_fhdo_offset_time=(1 / 0.2 / 3.1),
        print_infos=False
    )
    
    # Get true sampling period from experiment object
    samplingPeriod = self.expt.get_rx_ts()[0]  # us
    
    # Update bandwidth and acquisition time
    self.bw = 1 / samplingPeriod / hw.oversamplingFactor  # MHz
    acqTime = self.nPoints / self.bw  # us
    self.mapVals['acqTime'] = acqTime * 1e-6  # s

    # Create sequence instructions
    self.iniSequence(20, np.array((0, 0, 0)))
    self.rxGateSync(20, acqTime, channel=self.rxChannel)
    self.endSequence(acqTime + 40)
    
    # Send instructions to Red Pitaya
    if self.floDict2Exp():
       print("\nSequence waveforms loaded successfully")
    else:
       print("\nERROR: sequence waveforms out of hardware bounds")
       return False
    
    # Execute the sequence
    if not plotSeq:
        rxd, msgs = self.expt.run()
        data = rxd['rx%i' % self.rxChannel]
        data = self.decimate(data, nRdLines=1, option='Normal')
        self.mapVals['data'] = data
    self.expt.__del__()

    # Return true
    return True

The sequenceRun method plays a pivotal role in your sequence as it manages the input of instructions into the Red Pitaya and controls whether to run or plot the sequence. The value of the plotSeq parameter determines the behavior: plotSeq = 0 is used to run the sequence, while plotSeq = 1 is used to plot the sequence.

You may assume that values associated to keys created in the constructor are available as attributes.

Here's a step-by-step breakdown of how to implement the sequenceRun method:

  1. Get the True Bandwidth and Sampling Period: To address an issue related to the CIC filter in the Red Pitaya, an oversampling factor is applied to the acquired data. This factor can be encapsulated in the hardware module.

  2. Initialize the Experiment Object: Next, initialize the experiment object (self.expt) using the parameters you've defined. The experiment object must be defined within the self object so that it can be accessed by methods of the parent class mriBlankSequence.

  3. Obtain True Sampling Rate: After defining the experiment, obtain the true sampling rate used by the experiment object using the get_rx_ts() method.

  4. Update Acquision Parameters: Calculate the true bandwidth and acquisition time based on the true sampling rate to prevent data misregistration and ensure precise measurements.

  5. Create sequence instructions: Now that we have the true values of the sampling period and sampling time, we create the instructions of the pulse sequence.

    1. Initialization:

      • To begin the sequence, we initialize the necessary arrays and parameters.
      • In this step, we ensure that all relevant variables are set to zero and that the Red Pitaya is ready for data acquisition.
    2. Rx Gate Configuration:

      • The next step involves configuring the Rx gate for data measurement.
      • We specify the duration of the Rx gate, which is determined by the acquisition time and the selected Rx channel.
    3. Completing the Sequence:

      • To finish the experiment, we perform cleanup tasks.
      • All arrays and variables are reset to their initial values, ensuring a clean slate for the next sequence or experiment.
      • The total duration of the sequence is adjusted to account for the Rx gate duration and additional time for safety.
  6. Execute the sequence:

    1. Conditional Execution:

      • Before running the sequence, we determine whether it should be executed or just plotted.
      • The decision is made based on the value of the plotSeq keyword argument, where:
        • plotSeq = 0: The sequence will be executed to collect data.
        • plotSeq = 1: The sequence will be plotted without data acquisition.
      • This flexibility allows users to visualize the sequence before running it, which can be useful for verification and debugging.
    2. Data Acquisition (if applicable):

      • If the sequence is set to run (i.e., plotSeq = 0), the Red Pitaya performs data acquisition as instructed.
      • Data collected during the Rx gate period is processed to yield meaningful experimental results.
      • The acquired data is decimated, filtered, and stored for subsequent analysis.
    3. Cleanup:

      • Once data acquisition is complete, the sequence is finalized.
      • Cleanup tasks ensure that the Red Pitaya and related resources are reset to their initial state.
      • This step is crucial for maintaining the integrity of subsequent experiments.

Implementing the sequenceAnalysis Method

The sequenceAnalysis method is essential for analyzing the data acquired during the experiment. It processes the raw data, generates plots, and prepares the results for display. Here’s a detailed implementation:

def sequenceAnalysis(self, mode=None):
    # Set the mode attribute
    self.mode = mode

    # 1) Data retrieval
    acqTime = self.mapVals['acqTime']  # s
    data = self.mapVals['data']  # mV
    self.mapVals['dataPoint'] = np.std(data)

    # 2) Data processing
    tVector = np.linspace(0, acqTime, num=self.nPoints)  # Time vector in milliseconds (ms)
    spectrum = np.fft.ifftshift(np.fft.ifftn(np.fft.ifftshift(data)))  # Signal spectrum
    fVector = np.linspace(-self.bw / 2, self.bw / 2, num=self.nPoints) * 1e3  # Frequency vector in kilohertz (kHz)
    dataTime = [tVector, data]  # Time-domain data as a list [time vector, data]
    dataSpec = [fVector, spectrum]  # Frequency-domain data as a list [frequency vector, spectrum]
    
    # 3) Create result dictionaries
    # Plot signal versus time
    result1 = {'widget': 'curve',
               'xData': dataTime[0],
               'yData': [np.abs(dataTime[1]), np.real(dataTime[1]), np.imag(dataTime[1])],
               'xLabel': 'Time (ms)',
               'yLabel': 'Signal amplitude (mV)',
               'title': 'Noise vs time',
               'legend': ['abs', 'real', 'imag'],
               'row': 0,
               'col': 0}

    # Plot spectrum
    result2 = {'widget': 'curve',
               'xData': dataSpec[0],
               'yData': [np.abs(dataSpec[1])],
               'xLabel': 'Frequency (kHz)',
               'yLabel': 'Mag FFT (a.u.)',
               'title': 'Noise spectrum',
               'legend': [''],
               'row': 1,
               'col': 0}
    
    # 4) Define output
    self.output = [result1, result2]
    
    # 5) Save the rawData
    self.saveRawData()
    
    # In case of 'Standalone' execution, use the plotResults method from mriBlankSeq
    if self.mode == 'Standalone':
        self.plotResults()
    
    # 6) Return the self.output
    return self.output

Explanation:

  1. Data Retrieval: The acqTime and data are retrieved from self.mapVals, which stores the acquisition time and the raw data. Here is recommended to save:

    • dataPoint for 1d sequences with one relevant output parameter, e.g. rms noise, larmor frequency...
    • sampledCartesian for image sequences with four columns array that contains kx, ky, kz and s(kx, ky, kz) with k-space. This will be used by SWEEP method to SWEEP sequences.
  2. Data Processing:

    • Time Vector: A time vector tVector is created using np.linspace, spanning from 0 to the acquisition time (acqTime).
    • Signal Spectrum: The spectrum of the signal is calculated using the Inverse Fast Fourier Transform (np.fft.ifftn).
    • Frequency Vector: A frequency vector fVector is created, spanning from -self.bw / 2 to self.bw / 2, converted to kilohertz.
    • Data Lists: The time-domain data (dataTime) and frequency-domain data (dataSpec) are organized into lists for easy plotting.
  3. Create Result Dictionaries:

    • Time-Domain Plot: result1 is a dictionary containing the time-domain plot data. It specifies the widget type ('curve'), x and y data, labels, title, legend, and plot position.
    • Frequency-Domain Plot: result2 is a dictionary containing the frequency-domain plot data, with similar specifications.
  4. Define Output: The self.output attribute is set to a list containing result1 and result2. MaRGE will use this list to show the plots in the display area.

  5. Save Raw Data: The self.saveRawData() method is called to save the raw data for future analysis.

  6. Standalone Execution: If the mode is set to 'Standalone', the plotResults method from the mriBlankSeq class is called to generate the plots.

  7. Return Output: The method returns self.output, which contains the processed results.

Execute the Sequence in Standalone

To test and execute the sequence in standalone mode, the following script can be used:

if __name__ == '__main__':
    seq = Noise()   # Create the sequence object
    seq.sequenceAtributes()     # Create attributes according to input parameters keys and values
    seq.sequenceRun(plotSeq=False, demo=False, standalone=True) # Execute the sequence
    seq.sequenceAnalysis(mode='Standalone') # Show results

Explanation:

  1. Create Sequence Object: Instantiate the Noise class to create a sequence object (seq).

  2. Create Attributes: Call seq.sequenceAtributes() to create attributes based on the input parameters' keys and values.

  3. Execute Sequence: Call seq.sequenceRun(demo=False) to execute the sequence. The demo parameter is set to False for a real run (set to True for a simulated run).

  4. Show Results: Call seq.sequenceAnalysis(mode='Standalone') to analyze the data and display the results.

By following these steps, you can create and add a new sequence to MaRGE, allowing you to run simple sequences within the GUI. The example provided demonstrates how to create a noise measurement sequence, configure input parameters, implement the required methods, and execute the sequence in standalone mode.

Output dictionaries

The self.output variable of the sequenceAnalysis method contains a list of dictionaries that will be used by the GUI to plot the results in the display area. There are two types of dictionaries accepted by the GUI depending on what you want to show. There are curves and image types:

# Dictionary for curve:
# result1 = {'widget': 'curve',
#            'xData':  # float numpy array of n elements,
#            'yData':  # list of float numpy arrays with n elements,
#            'xLabel':  # String,
#            'yLabel':  # String,
#            'title':  # String,
#            'legend':  # List of strings,
#            'row':  # int,
#            'col':  # int}

# Dictionary for image:
# result1 = {'widget': 'image',
#            'data':  # float 3d numpy array,
#            'xLabel':  # String,
#            'yLabel':  # String,
#            'title':  # String,
#            'row':  # int,
#            'col':  # int}