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
-
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 theseq
folder.
- Start by creating a new Python file for your sequence. In this example, we'll create a
-
Import Required Modules:
- In your sequence file, import the necessary modules. Ensure access to the
experiment
class from themarcos_client
repository orexperiment_gui
fromcontroller
module and themriBlankSequence
class. New sequences should inherit from themriBlankSequence
class. Also, import theconfigs.hw_config
module for hardware properties andconfigs.hw_units
for setting units of input parameters. - Additionally, I add MaRGE and marcos_client directories to the paths to allow standalone execution.
- In your sequence file, import the necessary modules. Ensure access to the
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
- Create the
Noise
Sequence Class:- In your sequence file (
noise.py
), create theNoise
class that will inherit from themriBlankSeq.MRIBLANKSEQ
class. Include the following methods:sequenceInfo
,sequenceTime
,sequenceRun
, andsequenceAnalysis
.
- In your sequence file (
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 MaRGEtoMaRGE
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')
sequenceInfo
Method
Defining the Provide useful information about the sequence.
def sequenceInfo(self):
print("Be open-minded,\nDo open-source")
sequenceTime
Method
Implementing the Return the duration of the sequence in minutes.
def sequenceTime(self):
return 0 # Duration in minutes (modify as needed)
sequenceRun
Method
Implementing the 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:
-
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.
-
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 classmriBlankSequence
. -
Obtain True Sampling Rate: After defining the experiment, obtain the true sampling rate used by the experiment object using the
get_rx_ts()
method. -
Update Acquision Parameters: Calculate the true bandwidth and acquisition time based on the true sampling rate to prevent data misregistration and ensure precise measurements.
-
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.
-
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.
-
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.
-
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.
-
-
Execute the sequence:
-
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.
-
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.
- If the sequence is set to run (i.e.,
-
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.
-
sequenceAnalysis
Method
Implementing the 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:
-
Data Retrieval: The
acqTime
anddata
are retrieved fromself.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 bySWEEP
method to SWEEP sequences.
-
Data Processing:
- Time Vector: A time vector
tVector
is created usingnp.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
toself.bw / 2
, converted to kilohertz. - Data Lists: The time-domain data (
dataTime
) and frequency-domain data (dataSpec
) are organized into lists for easy plotting.
- Time Vector: A time vector
-
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.
- Time-Domain Plot:
-
Define Output: The
self.output
attribute is set to a list containingresult1
andresult2
. MaRGE will use this list to show the plots in the display area. -
Save Raw Data: The
self.saveRawData()
method is called to save the raw data for future analysis. -
Standalone Execution: If the
mode
is set to 'Standalone', theplotResults
method from themriBlankSeq
class is called to generate the plots. -
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:
-
Create Sequence Object: Instantiate the
Noise
class to create a sequence object (seq
). -
Create Attributes: Call
seq.sequenceAtributes()
to create attributes based on the input parameters' keys and values. -
Execute Sequence: Call
seq.sequenceRun(demo=False)
to execute the sequence. Thedemo
parameter is set toFalse
for a real run (set toTrue
for a simulated run). -
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}