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.