Sequence module and function internals#

The contracts of sequence modules and sequence functions. The concepts are in Sequence modules and functions.

Sequence modules#

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

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

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.

Every excitation and preparation module is an RfModule, which adds sim_rf, the off-resonance simulation of the module’s pulse, by default the first RF event in block order.

Construction#

A subclass implements init_module, assigns self.seq (a pypulseqpp.Sequence; an upstream Sequence is refused), adds blocks to it, and sets center when the module is timed against something other than its start. Assigning seq makes it record the event identities and block tuples added to it. A module that assigns no seq, or adds no blocks, raises TypeError. The constructor forwards its arguments to init_module, and inspect.signature and the class docstring report that method’s parameters, with the keywords a subclass forwards to its parent’s init_module spelt out. A family states its parameters once, and a variant narrows them.

Publication#

After init_module returns, the locals of every init_module frame in the chain of subclasses are scanned; a local that names an event a block played is published as an attribute of the module and on events. The innermost frame is read first, so where a subclass and its base bind one name, the subclass’s meaning is published.

  • Repeated references are deduplicated by identity: one distinct object becomes a scalar event, several become a list in first-seen order.

  • register(name=event) publishes events no block played and preserves the supplied structure, including one-element lists; it takes precedence over automatic names.

  • publish() publishes the caller’s locals, for construction helpers whose locals are not captured automatically.

  • A module that publishes nothing warns. A published name that is also a module attribute warns, and the event stays reachable as events.<name>; the module’s own attribute wins.

Module interface#

blocks returns a copy of the block tuples in play order, holding the original event objects. Mutating those objects changes the published templates and what the loop adds afterwards; it does not rewrite the stored sequence, so the analyses forwarded to seq (calculate_kspace, check_timing, paper_plot, test_report, waveforms_and_times) answer for the layout as constructed. duration is summed from the blocks unless the module derived its timing analytically and assigned it. An unknown attribute raises AttributeError naming the module.

Representation boundary#

Module construction separates the fixed waveform and timing design from the per-view encoding. This mirrors the Pulseq distinction between event definitions and playout instances, described in Pulseq file format. 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.

The signature is sequence(system, **protocol). system is the Opts the sequence is designed under. A function designed for lower gradient or slew limits lowers 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. A new example module exposes the function as main, which makes the module callable as it.

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 the constraint estimates.

Protocol#

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, so sampling figures can use the implemented acquisition order rather than reconstructing it from the prescription.

Prescans and chains#

A prescan is a sequence played before the main sequence, such as a calibration acquisition. 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.

Prescription checks#

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 (Safety 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.