Usage
Running a Pulse Sequence
Load FID.py
from the system directory:
from matipo import SEQUENCE_DIR, GLOBALS_DIR
from matipo.sequence import Sequence
seq = Sequence(SEQUENCE_DIR+'FID.py')
Or load a custom sequence from local directory (same directory as notebook):
seq = Sequence('custom_FID.py')
A file path may be passed into Sequence()
to load a sequence program from anywhere.
Print the currently set parameters:
print(seq.par)
Load system parameter files:
seq.loadpar(GLOBALS_DIR+'frequency.yaml')
seq.loadpar(GLOBALS_DIR+'hardpulse_90.yaml')
Set parameters directly:
seq.setpar(
n_scans=1,
n_samples=10000,
t_dw=10e-6,
t_end=1
)
Run the pulse sequence program:
data = await seq.run()
The data from a sequence object’s last run can also be accessed later with:
data = seq.data
Data is returned as a 1D numpy array of complex values. This can be plotted with e.g. matplotlib:
import matplotlib.pyplot as plt
plt.plot(data.real, label='real')
plt.plot(data.imag, label='imag')
plt.legend()
plt.show()
Parameters may be saved from the sequence object:
seq.savepar('FID_example.yaml')
Data may be saved using numpy:
import numpy as np
np.save('FID_example.npy', data)
Writing Custom Pulse Sequence Programs
Template
There is some boilerplate that all sequence programs must have. The simplest system sequence is noise.py which can be used as a template:
from matipo import sequence as seq
from matipo import ParDef
from matipo import datalayout
from collections import namedtuple
PARDEF = [
ParDef('f', float, 1e6),
ParDef('t_dw', float, 1e-6),
ParDef('n_samples', int, 1000)
]
ParameterSet = namedtuple('ParameterSet', [pd.name for pd in PARDEF])
def get_options(p: ParameterSet):
return seq.Options(
amp_enabled=True,
rx_gain=7)
def get_datalayout(p: ParameterSet):
return datalayout.Acquisition(
n_samples=p.n_samples,
t_dw=p.t_dw)
def main(p: ParameterSet):
t_acq = p.n_samples * p.t_dw
yield seq.acquire(p.f, 0, p.t_dw, p.n_samples)
yield seq.wait(t_acq)
Input Parameter Definition
The input parameters are defined in the PARDEF
list:
PARDEF = [
ParDef('f', float, 1e6),
ParDef('t_dw', float, 1e-6),
ParDef('n_samples', int, 1000)
]
Where each parameter is defined with ParDef(<name>, <data type>, <default value>)
.
Hardware Options
The get_options()
function should return an Options
object:
def get_options(p: ParameterSet):
return seq.Options(
amp_enabled=True,
rx_gain=7)
amp_enabled
may be used to disable the RF amplifier. In this case the unamplified RF outputsignal is connected to the probe via a directional coupler which can be used to measure reflected power for tuning/matching the probe. rx_gain controls the preamplifier gain in 3dB steps. The value must be an integer in the range 0 to 15.
Data Layout
The get_datalayout()
function should define the layout of the data acquired by the sequence for scan averaging purposes. For a single acquisition this is should be:
def get_datalayout(p: ParameterSet):
return datalayout.Acquisition(
n_samples=p.n_samples,
t_dw=p.t_dw)
As a more complex example, for a CPMG sequence which loops over scans and echoes this would be required:
def get_datalayout(p: ParameterSet):
return datalayout.Scans(
p.n_scans,
datalayout.Repetitions(
p.n_echo,
datalayout.Acquisition(
n_samples=p.n_samples,
t_dw=p.t_dw)))
Which means the acquisition is repeated n_echo
times per scan, for n_scans
scans. Scans
and Repetitions
may be nested arbitrarily to match the looping structure of the sequence.
Main Function
The main()
function is the actual pulse sequence program:
def main(p: ParameterSet):
t_acq = p.n_samples * p.t_dw
yield seq.acquire(p.f, 0, p.t_dw, p.n_samples)
yield seq.wait(t_acq)
This function is a python generator which returns the pulse sequence events using Python’s yield
syntax. Pulse sequence events can be created using the pulse sequence functions:
- matipo.sequence.acquire(freq, phase, dw, samples)
- matipo.sequence.gradient(x, y, z)
- matipo.sequence.pulse_end()
- matipo.sequence.pulse_start(freq, phase, amp)
- matipo.sequence.pulse_update(freq, phase, amp)
- matipo.sequence.shim(x, y, z, z2, zx, xy, zy, x2y2)
- matipo.sequence.wait(time)
Time is in Seconds, Frequency in Hz, phase in degrees, and pulse amplitude as a value in the range 0 to 1.0, where 1 is the maximum possible amplitude. Gradient and shim values are also defined in the range -1 to 1 based on hardware limits and need to be calibrated separately.
RF pulse amplitude may also be negative for convenience, which results in a 180 degree phase shift. e.g. seq.pulse_start(10e6, 0, 1.0)
is equivalent to seq.pulse_start(10e6, 180, -1.0)
. This makes defining shaped pulses simpler as the phase can be kept constant and the amplitude varied with a waveform that contains negative values.
Events may be composed together with +
and saved to a variable to be yielded mutliple times for efficiency, e.g.:
pulse_90 = seq.pulse_start(10e6, 0, 1.0) + seq.wait(10e-6) + seq.pulse_end()
yield pulse_90
yield seq.wait(10e-6)
yield pulse_90
yield seq.wait(10e-6)
yield pulse_90
yield seq.wait(10e-6)
Will result in three 10 microsecond full amplitude pulses at 10 MHz with 10 microseconds delay between them
Timing Constraints
There must be at least a 10e-6
(10 microseconds) delay between successive gradient/shim commands and at least a 1e-6
(1 microsecond) delay between successive pulse (pulse_start
, pulse_update
, pulse_end
) commands.
Hardware Triggering and General Purpose Outputs
GPOs are by default controlled by the OS using matipo.system_api.gpo_write
, but can be individually enabled for pulse sequence control using the matipo.util.iomux
functions, e.g. in a python notebook:
TRIGGER_OUT_PIN = 0b1000 # GPO 4 bitmask
from matipo.util import iomux
iomux.enable_seq_gpo(TRIGGER_OUT_PIN)
# run sequence using TRIGGER_OUT_PIN
seq.setpar(trigger_send=TRIGGER_OUT_PIN)
await seq.run()
# disable pulse sequence control of pin so it is OS controlled again
iomux.disable_seq_gpo(TRIGGER_OUT_PIN)
Inside the pulse sequence program main
function the GPO can be controlled as follows:
TRIGGER_OUT_PIN = 0b1000 # GPO 4 bitmask
yield seq.gpo_set(TRIGGER_OUT_PIN) # sets GPO 4 high without changing other pins
yield seq.wait(10e-6) # wait 10 us
yield seq.gpo_clear(TRIGGER_OUT_PIN) # sets GPO 4 low without changing other pins
yield seq.wait(10e-6)
To wait for a trigger signal from another system, use seq.wait_for_trigger()
. Refer to the system manual for the GPIO connector pinout.