Protocol resolution#
TL;DR
The value the scanner shows after an edit is the value the design achieves, not the value requested: a request is completed with initial values, evaluated under the scanner limits and returned as resolved.
An evaluation may state an RF layout of one TR, from which a scanner estimates the RF of a prescription without designing it.
Times travel as integer microseconds and other floats at six significant digits, so a resolved protocol sent back resolves to itself.
An operator prescribes an acquisition by editing protocol entries in the scanner UI: echo time, repetition time, field of view, matrix size, receiver bandwidth. A requested value is not always achievable exactly. An echo time shorter than the readout permits is infeasible, a receiver bandwidth is realized on the ADC raster, and a request for the shortest echo time has no numeric value until the sequence is designed. The value the scanner shows after an edit is therefore the value the design achieves, not the value requested.
Resolution#
A scanner sequence (SequencePlugin) maps each
interpreter parameter name to a keyword argument of its sequence function, a function that
returns sequences. Resolving a request proceeds in three steps.
Every entry the request omits takes its initial value, the sequence function’s default unless the entry declares another, so a request is always a complete prescription.
The wire values are converted to the sequence function’s arguments, held as a
Protocol, and the plugin evaluates the protocol under the scanner limits, capped by the design limits the scanner derates for the prescription (Running the services). An evaluation that calls the sequence function designs the events and their timing against those limits without playing the scan, and the sequence function raises an error for a prescription it cannot realize. The shipped plugins evaluate this way. The default evaluation of a sequence function accepts the protocol unchanged and builds nothing, and a plugin overridesevaluate()to check or complete it.The evaluation returns the protocol the design achieves, converted back to wire values, with the scan time, a note and, optionally, the RF layout (RF layout). An evaluation that calls the sequence function reads the values from the sequences it designed: the shipped plugins return the values the main sequence states for the prescription, the echo time and the repetition time in its
TEandTRdefinitions, the receiver bandwidth as the inverse of the dwell time of its ADC event and the slice thickness in itsSliceThicknessdefinition, and an argument they do not read keeps its requested value.
An evaluation answers every edit of an entry, so its duration adds to the latency of the UI, and a design of the whole scan grows with the matrix and the slices. A scan repeats one TR, so the shipped plugins design one repetition of it, one line, partition, spoke, interleaf, echo train or inversion shot of one slice without dummy repetitions, and extrapolate: the scan time is the duration of a repetition times the number of repetitions the prescription plays, and slices that share a TR are dealt into the packets the sequence plays them in, from which the TR and the scan time follow.
The prescription is not a design argument. Every listing ends with its
entries, not editable in the UI, which the interpreter fills from the
scanner’s prescription: the field-of-view offset in fov_offset_x,
fov_offset_y and fov_offset_z, in mm, and the rotation from the logical to
the physical axes in the nine fov_rotation_ij, identity by default.
Resolution returns them unchanged, and a rotation that is not orthonormal is
invalid. The host applies the offset to the designed sequence when it builds
the IR, and checks the design in the physical frame of the rotation
(Designs and the design store). A scanner sequence cannot bind them to an argument.
A request the evaluation rejects is invalid, and the reply carries the request
unchanged. A ValueError or an AssertionError, which pypulseqpp and PyPulseq
raise for an event or a timing they cannot realize, is reported by its
message, which the interpreter shows to the operator. Any other exception also
makes the request invalid, and the message names only its type. A request that
names an entry the protocol does not declare is invalid, and the message names
the entry.
A valid reply carries the resolved values, a note, and the scan time in
seconds the evaluation states. An evaluation that states no scan time, 0.0, is
valid, and the reply reports the scan time as unknown.
RF layout#
The RF a protocol plays depends on the protocol. An evaluation may state it as
an RfLayout, from which a scanner estimates the RF of
a prescription without designing it: the RF definitions of a sequence, the RF
instances of one TR in play order, and for each instance the protocol entry its
amplitude follows.
A definition is one RF pulse: a complex waveform at unit peak magnitude, its
timing and its use. Instances of a definition differ in amplitude and in
frequency and phase offset. In the sequence an evaluation builds, the amplitude
of an instance is relative to the first instance of its definition with a
nonzero amplitude, so a refocusing train with a flip angle schedule is one
definition with one instance per pulse, and RF spoiling, which steps the phase
offset, adds no definition (pypulseqpp.Sequence.rf_instances()).
A control of an instance is the flip angle, or a float user entry, of the plugin’s protocol. It states that the amplitude of the instance is proportional to the entry: a scanner that plays a protocol with another value of the entry multiplies the amplitude by the ratio of that value to the value in the evaluated protocol, which is positive. An instance without a control is played at the amplitude it was evaluated at.
A validation states the amplitude of an instance over the peak_hz the listing
states for its definition, not over the first instance of the definition in the
evaluation of the validation. A protocol that changes a flip angle changes the
peak of the definition in its own evaluation, and the amplitude stated includes
that change, so that the product of the amplitude and the listed peak_hz is the
peak RF amplitude the instance plays.
A layout is one TR: the instances one TR plays, and the TR as its period,
over which a scanner averages their RF. Its size does not depend on the matrix.
The shipped plugins state the first TR of the repetition they design, its shot
repeated once per slice of the largest packet, and a balanced steady state the
TR after its half-angle pulse. A definition is named by its number in the
sequence the evaluation builds, in the order of first play. The listing carries
the definitions of the evaluation at the default protocol, and a layout names
the pulses of the listing only where the protocol does not change which pulses
are played first: the length of a train, a flip angle schedule and the matrix do
not, and a protocol that adds a preparation pulse does.
The layout is an estimate for the prescription. An evaluation that states none states no estimate and is valid, and what the scanner checks before the scan is the stored design (Designs and the design store).
Keys and values#
The keys of a protocol are members of the key enums of
pulserver.protocol, of which ProtocolKey is
the union. A key is a str that equals and hashes as its wire name, so a plain
string indexes a mapping of keys, and a scanner sequence stores a plain string
in protocol as the member it names. A Protocol
maps each key to its value in the units of the sequence function’s argument: seconds for a
time, the argument’s unit for a float, a member of the enum for a
ChoiceParam. The prescription entries bind no
argument and keep the units of the wire.
The wire carries the same protocol as integer microseconds, values in the
entry’s UI unit and, for a choice, the index of the chosen option.
from_wire() and
to_wire() are the only conversions between
the two, and the formatting and parsing functions of
pulserver.protocol are the only conversions between keys and values and
the text of a block. A value block that names no option of a choice is refused
when it is parsed, and a request that names none is invalid when it is given
to validate() as a mapping.
StringListParam(), which builds the enum from option
strings, is deprecated: declare the enum and use ChoiceParam.
Presets#
A preset is a negative value of a time entry that the UI shows as a word, such
as Minimum for the echo time (TEPreset,
TRPreset). A scanner sequence maps each preset it
offers to the argument value it requests: None, which asks the sequence function for its
shortest achievable time; a time in seconds; or a function of the
scanner limits. A preset is resolved like any other request: where the
evaluation records the time the design achieved, the reply carries it in place
of the preset. A time showing a preset holds what the preset requests in a
protocol, and preset() returns the preset.
Units and precision#
The interpreter stores protocol values in scanner parameters. Time parameters
hold integer microseconds, so time entries are exchanged in integer
microseconds and converted to seconds for the sequence function, rounding to the nearest
microsecond. Other float entries are held in float32 parameters, whose
round trip preserves six significant decimal digits, so they
are exchanged at that precision. A float entry carries a scale between its UI
unit and the sequence function’s SI argument, such as 1e-3 for a field of view
shown in mm and designed in m.
Resolved values are reported at the precision in which they are stored. Consequently, sending a resolved protocol back unchanged resolves to the same protocol. This property is what allows a design to be identified by its resolved protocol (Designs and the design store): an operator who reopens a protocol and generates it again obtains the same design.
See also#
Scanner sequences — writing a scanner sequence.
Scanner sequences — the scanner-sequence interface and its UI entries.
Protocol — protocol entries and wire blocks.
Protocol resolution for a 2D gradient echo — bandwidth quantization and minimum echo time, executed.