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 limits |
|
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 |
|
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.