Sequence design in pypulseqpp#

TL;DR

  • A SequenceModule solves a reusable block layout independently of the acquisition loop: block tuples, named mutable event templates and a timing reference, center, in seconds from its start. Module timing composes through center, independent of block boundaries.

  • A complete sequence is a function sequence(system, **protocol) that returns the designed Sequence, or a list of sequences with the prescans first. Its protocol is read from the signature and the NumPy-style Parameters section, by parameters() and by the command line.

  • Labels writes the label events a block changes; write() writes a list as files linked by NextSequence, so each file keeps one period of repetition.

  • A Cartesian acquisition is specified by its support, which views are acquired, and its temporal ordering, when each is acquired. The sampling routines keep the two separate and create neither events nor labels; ordering indices are row numbers into the array the caller passed, not coordinates.

The package places three abstractions above the flat block list of Pulseq representation: a sequence module, a reusable block layout whose timing and gradient waveforms are solved once; a sequence function, which adds a prescription, a sampling order and a scan loop; and the sampling routines that feed the loop. None of them is part of the file format; a sequence written through them is an ordinary .seq file.

Sequence modules#

A SequenceModule solves a reusable block layout independently of the acquisition loop. The module contains block tuples, named mutable event templates, and a timing reference.

SequenceModule
├── blocks: [(event, ...), ...]
├── events: named mutable event templates
└── center: timing reference (s)

Timing centre#

The center attribute gives the module timing reference in seconds from its start, usually an RF pulse centre or an echo. For an inversion module followed by an excitation module, the recovery delay required for inversion time \(T_I\) is

\[ t_{\mathrm{delay}} = T_I - (t_{\mathrm{inv}} - t_{\mathrm{centre,inv}}) - t_{\mathrm{centre,exc}}. \]

This definition composes pulse-centre timing without depending on block boundaries.

Module contents#

Module family

Blocks and events

Responsibility

Excitation

RF event, selection gradient, rephaser

Excitation or refocusing geometry and pulse timing.

Preparation

RF and gradient preparation blocks

Inversion, T2 preparation, saturation, diffusion, or magnetisation transfer.

Readout

Prephasing, ADC event, readout gradients, rewinding or spoiling

Echo timing, acquisition bandwidth, and sampling trajectory.

Named events are published both as attributes and through events. An acquisition loop may change an event template before add_block, for example by setting an RF phase offset or scaling a phase-encode gradient. Blocks already added to a sequence, and the module’s internal sequence, are not changed by it.

A readout constructed with te=None uses its shortest realizable echo time. Requested bandwidths and times are rasterized, and the achieved values are reported by the module.

Representation boundary#

Module construction separates fixed waveform and timing design from per-view encoding. This mirrors the Pulseq distinction between event definitions and playout instances; see Storage, deduplication and file revisions. Sampling order and repetition structure belong to the sequence function.

Sequence functions#

A complete sequence is designed from the system limits and a prescription: the field of view, matrix, timing, flip angle and sampling order of the acquisition. A sequence function is that design as a callable, from which a script, the command line and a protocol editor obtain the sequence and the prescription.

A sequence function has the signature sequence(system, **protocol). system is the Opts the sequence is designed under. A function designed for lower gradient or slew limits lowers the limits of system to them with cap_system(), which never raises a limit. The keyword parameters after system are the protocol, and each states its unit in its description. The function returns a Sequence, or, for a scan with prescans, a list of sequences in play order.

Concern

Sequence function

Prescription

Keyword parameters after system, documented in the Parameters section

System limits

system, capped to the design’s limits with cap_system

Modules, timing and sampling arrays

The body, before the loop

Sampling order

The loop

One repetition

The body of the loop

Pulseq definitions required by reconstruction

set_definition after the loop

Prescans

The elements of the returned list before the main sequence

The Python module of a shipped sequence defines its design limits as the constants MAX_GRAD (mT/m) and MAX_SLEW (T/m/s). Changing one redesigns the gradient waveforms and may alter echo spacing, acquisition duration, and constraint estimates.

Protocol#

The protocol is read from the function. A parameter’s type and default come from its annotation and default value, and its unit, choices and description from the NumPy-style Parameters section. The unit is the parenthesised group in the first sentence of a description, Echo time (s)., and the choices of a string parameter are the values its documented type lists in braces. An annotation that admits None leaves the value to the design. parameters() returns one ProtocolParameter per keyword parameter, in signature order, which is what a protocol editor presents.

pypulseqpp.cli.run() derives one command-line option from each scalar keyword parameter and takes its help text from the first sentence of the parameter’s description. The limit options --max-grad-mtm (mT/m) and --max-slew-tm-s (T/m/s) build the Opts passed as system, and the result is written with write().

Encoding labels#

A loop records encoding indices with Pulseq label extensions. The principal labels are LIN, PAR, ECO, SEG, REP, and SET. A label keeps its value across blocks until an event changes it, so a block carries the events of the labels it changes and no others. Labels holds that state: calling it with the new value of each label returns the events to add to the block. An unchanged value writes nothing, a change equal to the label’s previous change is an INC, and any other change is a SET. evaluate_labels() recovers the ADC order directly from the sequence. Sampling figures can therefore use the implemented acquisition order rather than reconstructing it from the prescription.

Prescans#

A prescan is a sequence played before the main sequence, such as a calibration acquisition. A sequence function returns it ahead of the main sequence in a list. write() writes the list as separate files: the first at the given path, and each later one beside it as <stem>_<Name>.seq, where Name is the sequence’s Name definition, or its position in the list, counted from 0 at the first, when it has none. A later file whose name an earlier file has taken is written as <stem>_<Name>_<position>.seq, so every sequence has a file of its own. Each file but the last names the next with NextSequence. The chain represents one acquisition while each file keeps one period for repetition() and the analyses that use it. duration() is the sum of the durations of the sequences in the chain.

Checking a prescription#

Calling a sequence function designs every block, and the number of blocks grows with the matrix. A prescription the design cannot meet, such as an echo time shorter than the readout admits, raises ValueError. The call does not evaluate the waveforms against gradient, PNS or SAR limits, which take the designed sequence (Constraint checks).

The function records the prescription as designed in the definitions it sets after the loop. A value the design chooses, such as the shortest echo time for te=None, is recorded as TE, and prescribing the recorded value as te designs a sequence that records the same TE. The receiver bandwidth as designed is the reciprocal of the dwell time of the ADC events, which is rounded to the ADC raster.

Sampling support and ordering#

A Cartesian acquisition is specified by two independent choices: its support, the set of phase- and partition-encoding views that are acquired, and its temporal ordering, the repetition, shot and echo at which each acquired view is played. The sampling routines keep the two separate, and neither creates events or labels. A sequence function combines them in its scan loop.

acquisition prescription     matrix, acceleration, ACS, partial Fourier, CAIPI
        │
        ▼
acquired support             encoded view indices (y, z), split into
        │                    calibration and imaging views
        ▼
temporal ordering            loop order, or [shot][echo] indices into the views
        │
        ▼
scan loop                    one iteration per repetition
        │
        ├─▶ gradient scaling      phase and partition encodes from (y, z)
        └─▶ Pulseq labels         LIN, PAR, ECO, SEG, IMA, ...

Kinds of value#

The routines exchange values of distinct kinds, and the distinction is part of their interface.

Kind

Example

Produced by

Meaning

A. Encoded view indices

(y, z) with 0 ≤ y < n_y

make_cartesian_axis_sampling(), make_cartesian_plane_sampling()

Zero-based line and partition numbers on the encoding grid, as the LIN and PAR labels count them. The k-space centre is (n_y // 2, n_z // 2).

B. Centred coordinates

(y - n_y // 2, z - n_z // 2)

the caller

Offsets from the k-space centre in encoding steps. The echo-train orderings measure distance and angle in this frame.

C. Boolean support mask

mask[y, z]

make_*_mask

True where the view (y, z) is acquired. No order and no role.

D. Ordering indices

trains[s][e]

make_*_order

Row numbers into the array the caller passed, grouped by shot s and echo e. Not coordinates.

E. EPI relative offsets

(Δky, Δkz) per echo

make_epi_shot_offsets()

Offsets from echo 0 of one shot. The scan loop chooses each shot’s origin.

F. Orientation schedules

angle per shot, or 3D directions

calc_*_angles, calc_projection_shell()

Rotations applied to a non-Cartesian readout, in radians.

G. RF schedules

phase or flip angle per repetition

make_*_schedule

Values applied to the RF (and ADC) events of each repetition.

Centred coordinates are the input of the geometric echo-train orderings because the k-space centre is a property of the encoding grid, not of the acquired set. With an even matrix, partial Fourier or an asymmetric undersampled support, the centroid of the acquired views is not the centre, and an ordering that measured distance from it would acquire the wrong view at the target echo. The conversion is explicit:

calibration, imaging = pp.make_cartesian_plane_sampling(
    (n_y, n_z), (2, 2), (24, 24), partial_fourier=(0.75, 1.0)
)
views = np.array(calibration + imaging)       # A: encoded indices
centred = views - (n_y // 2, n_z // 2)        # B: centred coordinates
trains = pp.make_radial_adaptive_order(       # D: [shot][echo] indices
    centred, etl, center_echo=te_echo
)
view_of = [[tuple(views[i]) for i in train] for train in trains]

The same indices i select the encoded view views[i], from which the scan loop scales the phase-encoding gradient and writes the LIN and PAR labels. A boolean mask enters the same way, through np.argwhere(mask).

Poisson-disc support and T2 Shuffling#

Both are associated with variable-density, echo-resolved acquisitions, and they answer different questions.

Poisson-disc sampling

T2 Shuffling

Determines

which views are acquired

when already selected views are acquired along the echo train

Routine

make_cartesian_plane_sampling(..., sampling='poisson'), make_poisson_disc_mask()

make_shuffling_order()

Output

encoded views, or a boolean mask

trains[shot][echo] indices

The shipped fast-spin-echo sequence combines them under ordering='shuffling'. The MPRAGE sequence’s ordering='shuffling' pairs the same Poisson-disc support with a random line order within each partition. Either choice can be used without the other.

The sampling routines return plain Python and NumPy values and create no label events. Which calibration views are played first, whether the support is reordered before acquisition, and which labels each repetition writes are decided by the sequence function: LIN and PAR from the encoded view, ECO from the echo index, IMA from membership of the calibration list, and SEG, SLC, SET or REP from its position in the loop (Encoding labels).

See also#