Sequence-analysis computations#
What the analysis functions of pypulseqpp.Sequence and
TransformFOV compute, in which frame and units, and what a
caller can rely on. The concepts are in
Sequence analysis.
One gradient, two integrals: the unbroken area that shift phases use and the trajectory that echo anchoring uses.#
Waveforms and times#
waveforms_and_times() returns gradient channels as
(2, n) arrays of time (s) and amplitude (Hz/m), and an optional complex RF
channel in Hz. Each block’s rotation is applied, so the channels are the
logical axes. A trapezoid is its corner points. A time range keeps times
measured from the start of the scan; a block range restarts them at its first
block, and the two are not given together.
With compat=True the five values are upstream’s: wave_data,
tfp_excitation, tfp_refocusing, t_adc, fp_adc. With compat=False a
named result carries all seven Pulseq RF uses, per-sample ADC phases and the
1-based block each pulse and sample belongs to; the five values cannot hold
the uses other than excitation and refocusing.
k-space#
calculate_kspace() returns, in order, the k-space
location of each ADC sample (3, n) in 1/m, the full trajectory sampled
through ramps and at event times, the centres of the excitation and refocusing
pulses and the ADC sample times, all in seconds from the start of the selected
range. trajectory_delay is a per-axis timing correction in seconds, positive
advancing the gradient; gradient_offset is a background gradient in Hz/m.
calculate_kspacePP is the same calculation under upstream’s second spelling.
The trajectory restarts at the centre of an excitation pulse, or of a pulse
with no use recorded, and is inverted at the centre of a refocusing pulse.
adc_kspace() returns the first result alone, with
the trajectory between the samples left unbuilt; it integrates identically and
accepts the same options. With readouts=(first, stop), two 0-based indices in
the order adc_echoes() lists them, it returns the
samples of those readouts, each where the whole sequence puts it. The
integration starts at the last block before readout first that plays an
excitation, or a pulse with no use recorded, and does not acquire, so a
trajectory delay that moves an earlier block’s gradient past that pulse’s
centre is not seen. readouts is not combined with block_range.
adc_echoes() returns one entry per block that
acquires: the 1-based block, num_samples, first_sample (the column in
adc_kspace), moving (whether the readout’s k-space spans more than 1e-6 of
its widest span along x, y and z) and echo, the first and last 0-based
sample no further from the centre of k-space, over the moving axes, than the
nearest sample plus 1 % of the larger k step beside it; -1 for a readout that
does not move or has fewer than two samples. start, path, paths,
rotation and rotations give each readout’s k-space as its first sample plus
one of the distinct paths from there, turned by the block’s rotation. They are
None when an RF pulse that excites or refocuses, or has no use recorded, plays
in a block that acquires, or when more than one readout in 16 has a path of its
own.
Two integrals of the gradients are kept distinct. The unbroken integral is
the cumulative gradient area without RF resets and is the reference of the
RF and ADC shift phases. The excitation- and refocusing-aware k-space is the
one that anchors echoes. TransformFOV stores them as
swept_k and block_k_origin, both in 1/m in the frame of the translation,
and preserves both across consecutive ranges.
Repetition#
repetition() returns (size, start): blocks per
repetition, and the 1-based first block, always 1. The size is, in order of
preference:
the shortest period of the block-definition stream from the first block that every later block repeats, the last copy possibly cut short;
the shortest period dividing the table over which blocks match in duration and in the channels they play;
the whole sequence.
A slice acquired with its own preparation and dummy shots is one repetition,
and a block played once makes the whole sequence one. A sequence with no
blocks has size zero. A TRSize definition shorter than the sequence, dividing
it and repeated by the blocks is taken instead, so a longer hyper-TR can be
declared; one the blocks contradict is ignored. Nothing is written into the
sequence, and the answer is recomputed after a native mutation (a block added,
a block retimed, duplicates collapsed).
Because the stream is of definitions, shots that differ only in amplitudes, rotations, shifted gradient waveforms or phase-encode steps repeat, and a phase-encode table is one definition at many amplitudes. Labelling a pulse splits the definition it shared.
RF power#
calc_rf_power() returns one event’s energy (Hz² s), peak
power (Hz²) and RMS amplitude (Hz), as MATLAB Pulseq’s calcRfPower, resampled
at dt; the energy does not depend on dt.
calc_rf_power() returns mean power, peak power, RMS
and energy over all blocks or a 1-based inclusive block_range; with
window_duration the energy, mean power and RMS are the largest over runs of
whole blocks no longer than the window, each divided by it. A dynamic pTx
pulse is read as the root-sum-square of its channels, so channel powers add and
never cancel. The values are relative: divide the RMS by gamma for tesla and
the powers by gamma squared. SAR is check_sar().
Field-of-view transform#
TransformFOV takes at least one of rotation,
translation, scale or a 4-by-4 transform, which excludes the other two;
its translation is stated in the output frame and is converted to the frame of
translation with the transpose of its rotation. apply_to_sequence()
applies scaling, then translation, then rotation, to a copy unless
in_place=True, over block_range (1-based, inclusive) or time_range,
not both.
Operation |
Frame and units |
|---|---|
scale |
multipliers on the channel axes; field of view scales inversely, zero disables encoding on an axis |
translation |
metres, along the channel axes; along the logical axes with |
rotation |
3-by-3 matrix or SciPy rotation, composed after the rotation each block holds; stored as a scalar-first quaternion |
Translation moves RF and ADC by a phase from the unbroken gradient area: a steady gradient under a pulse or readout costs two numbers, a moving one a phase shape, and a readout is referenced to its window centre. Repeated readouts share one reference and register one modulation shape. A rotation given with a translation does not turn it. RF phase shapes store cycles; ADC modulation and phase offsets are in radians.
A matrix of determinant -1 is played as rotation @ diag(1, 1, -1) after
negating the gradient on channel axis z, each block’s own rotation conjugated
by that reflection (reflected_axis is 2). A rotation is always an
annotation; use_rotation_extension=False, which would bake it into new
waveforms, is refused.
NOSCL, NOROT and NOPOS labels exempt blocks from scaling, rotation and
translation, evaluated from the selected range’s start without inheriting
earlier values. A nonzero translation still integrates exempt blocks, because
where k stands depends on everything played before it. A nonzero translation
updates swept_k and block_k_origin; reuse them only for consecutive
ranges, initialised to the state entering the first selected block. A block
range does not integrate the blocks before it.
trajectories() returns (block, (3, n)) ADC
trajectories on the channel axes from block_k_origin, without updating it and
without applying rotation extensions.
Plotting#
plot() needs the optional SeqEyes viewer
(pip install 'pypulseqpp[plot]') and is also Sequence.plot.
paper_plot() draws one repetition over the others, up to
max_underlays, as Sequence.paper_plot.
plot_kspace() draws the ADC locations in 1/m after each
block’s rotation.