Designs and the design store#
TL;DR
A design is identified by the SHA-256 of the plugin, the code it designs with, the limits and the resolved protocol, and a request that resolves to a stored design returns it.
The design store holds one directory per design, written whole and never modified, and pruned least recently used first.
A design is checked under the scanner limits in the physical frame of the prescription before it is stored, and the cache records each subsequence’s SAR against a reference pulse.
The reconstruction side finds a design by the identifier the raw-data header carries.
A scanner sequence is validated on every protocol edit and generated when the scan is prepared, often many times for one protocol, and a generated design has to remain available, unchanged, for as long as data acquired with it may be reconstructed. Pulserver stores each design by its content: a design is identified by what it depends on, and a request that resolves to a stored design returns it instead of designing again. No design call keeps state between calls; the design store is all that persists.
Design identity#
A generated design depends on the scanner-sequence plugin, the code that
designs with it, the scanner limits and the resolved protocol. Its identity is
the SHA-256 hash of these (design_identity()):
the plugin name;
a digest of the plugin file, the source file of the sequence function it binds, and the installed versions of pypulseqpp and pulserver;
the scanner limits, design limits, conversion options and check limits, with the contents of the VOP file they name;
the resolved protocol, prescription included.
A changed plugin, sequence function or package gives a new design for an unchanged
protocol, and so does a VOP file rewritten at the same path. Because the hash
is computed over the resolved protocol, two requests that differ only in a
value the design replaces, such as a preset and the time it resolves to,
identify the same design (Protocol resolution). A design is made from its resolved
protocol: a request that resolves to itself, such as the value block of a
validate reply sent back, is designed as it stands, and one that resolves to
other values is designed from those, so the files of a design are a function of
its identity.
An imported chain is identified by the names and contents of its files, the limits and the prescription of the import, offset and rotation.
Design identifier#
The identifier of a design is the first 18 hexadecimal digits of its identity,
72 bits (design_id()). The interpreter records the
numbers it passes to the reconstruction client in float32 fields of the
raw-data header, and a float32 represents every integer below \(2^{24}\)
exactly, so the identifier takes three of them, of six digits each. The store
refuses a design whose identifier already holds another identity.
The store#
The store is a directory with one subdirectory per design:
designs/
3f9c0a1b2c3d4e5f60/
sequence.seq first file of the NextSequence chain, binary Pulseq
sequence_main.seq the remaining files, when there are prescans
sequence.pseg IR cache
resolved.protocol
manifest.json identity and identifier, limits, package
versions, creation time, plugin and source,
scan time, field-of-view offset (and rotation,
for an imported chain), SHA-256 of every file
A design is written into a staging directory in the store and renamed into place once complete, so a reader sees it whole or not at all, and two processes that write one design leave one directory. A design is not modified afterwards. Designs are removed only by pruning (Running the services), least recently used first; a design is used when it is written or found again.
Checks before a design is stored#
The gradient coils are limited per physical axis, and the peak a gradient
reaches on one axis depends on the orientation it is played in: two logical
axes at 0.8 of the amplitude limit each put \(0.8\sqrt{2}\) of it on one physical
axis at 45°. So the checks are made in the physical frame. The prescription’s
rotation \(R\), from logical to physical axes, reaches the host in the nine
fov_rotation_ij entries of the protocol, element \((i, j)\) of \(R\), and each
file is rotated by it as the scanner plays it: composed after each block’s own
rotation, with blocks labelled NOROT left unrotated. A prescription with a
reflection in it is checked as it plays, reflection included. A design is held
under limits per logical axis that the scanner derates for the rotation, the
design_max_grad and design_max_slew of the call’s limits, and its physical
axes are checked against the gradient coils’ own, max_grad and max_slew
(Running the services).
Before a chain is converted, check() runs pypulseqpp’s
timing check, gradient continuity included, and its gradient amplitude and
slew-rate checks on every file, against the gradient limits, dead times and
ringdown time of the scanner. Where the call’s limits carry them
(CheckLimits), it also runs pypulseqpp’s PNS check
under the scanner’s nerve model and its mechanical-resonance check against the
scanner’s forbidden gradient bands. The waveforms are timed by the rasters the
file declares. The interpreter passes these limits with every design call
(Running the services); it computes the SAR and the gradient heating.
No design is stored for a generated design or an imported chain that fails a
check: generate and import reply with the problems, as they do with a
design error. The checks compute estimates; passing them does not
establish scanner or patient safety.
SAR against a reference pulse#
The interpreter computes SAR under its own calibration of the transmit chain. Where
local SAR is computed from virtual observation points (VOPs), the energy a
pulse deposits at VOP \(v\) is \(\int \mathbf{b}(t)^H Q_v\, \mathbf{b}(t)\,dt\),
with \(\mathbf{b}\) the drive of each transmit channel and \(Q_v\) the VOP’s
matrix, and it depends on the shape of the pulse and its channel weights. The
host evaluates it against a reference: the hard pulse of 180° and 1 ms, played
in the default channel weights. For each repetition \(w\) of a subsequence, the
blocks before the first repetition and after the last included, as
pypulseqpp’s SAR check averages over them, sar_ratios()
computes
with \(E_{v,w}\) the energy of the repetition at VOP \(v\), \(N_w\) the number of pulses it plays and \(E_v^{\mathrm{ref}}\) the energy of the reference pulse there: the energy of the repetition over that of the same repetition with each of its pulses replaced by the reference. A 1 ms hard pulse of 90° counts a quarter of the reference, and the ratio is 1 for a repetition of reference pulses. The global SAR matrix of the VOP file gives the same ratio for global SAR. A scale common to every channel’s drive and to the VOPs cancels in both.
The cache carries the two ratios of each subsequence in its
pulseg_subseq_info, zero without VOPs or without RF, and the interpreter
computes the SAR of the subsequence as the ratio times its SAR for the reference
repetition.
Access from the reconstruction side#
The raw-data header of a series names its design in the pulserver_design
user parameter (Reconstruction clients). A design is
immutable, so the reconstruction proxy reads and tabulates it once and reuses
the result for later series acquired with it, keeping the designs it read most
recently (DesignCache).
When the design calls and the proxy run on different computers, the proxy
reads either a store both can reach or a store of its own, which its design
intake fills with the designs the calls push (DesignIntake).
A pushed design is carried as a bundle of the files of its directory, and the
intake stores it only when the manifest’s identity is a SHA-256, its identifier
is that of the identity and the one the push names, and every file has the
SHA-256 the manifest records, so the two stores hold the same files under the
same identifier. A design is pushed before its identifier is replied, and a
design that cannot be pushed fails the call, so the interpreter plays no design
the proxy’s store lacks.
See also#
Running the services — the design calls, the warm server, pushing and pruning.
Design calls — the design calls and the store.
Protocol resolution — the resolution of a request into the protocol a design plays.