Reconstruction proxy#
How the reconstruction runtime places the readouts of a reconstruction unit in its buffers, and how the proxy runs, schedules and isolates the reconstructions of each series (Raw-data enrichment and routing).
Placement#
The readouts of a Cartesian encoding space are placed as Gadgetron’s acquisition bucket places them.
Along the readout axis, a readout is placed by its echo. The samples from
discard_pre to number_of_samples - discard_post are copied so that the
sample at center_sample lies at sample N // 2 of the buffer, N being the
number of samples of the buffer along the readout. N is the number of samples
of the unit’s first readout when that is a full echo centred in its samples
(center_sample == number_of_samples // 2), and otherwise the matrix size of
the encoded space along the readout. Readouts that sample different parts of
the echo, such as a partial echo and a full one, are aligned by it, and the
samples a gadget added to complete an echo, which the gadget declares as
discarded, are not placed. ReconBuffer.readout is the first and last sample
placed. A readout whose placed samples fall outside the buffer is an error.
Along the phase-encoding and partition axes, a counter is placed at
counter - center + extent // 2, with extent the matrix size of the encoded
space along the axis and center the centre of the counter’s encoding limit.
The k-space centre lies at extent // 2 whichever counter the sequence gives
it, so undersampled, partial-Fourier and offset-numbered acquisitions are
placed on one grid. The partition axis is shifted only when the encoded space
has more than one partition. A counter that falls outside the grid once placed
is an error. The calibration buffer is placed by the same rule and holds the
lines its readouts cover; its origin is the position of its first line on the
grid of the imaging buffer.
A space with a trajectory has no echo or centre line to place by. Its readouts are aligned to the end of the readout axis, its counters are the positions of its views, and the k-space location of each sample is the trajectory the buffer holds.
Two gadgets bring a Cartesian readout to the form the buffer places.
AsymmetricEcho zero-fills a partial echo to the full
echo that is symmetric about its center_sample and declares the zeros as
discarded. RemoveReadoutOversampling crops a full
echo to the readout field of view of the reconstruction, the ratio of the
encoded to the reconstructed field of view being the oversampling. A gadget
states the echo of the readout it returns by assigning center_sample,
discard_pre and discard_post of the acquisition it is given. That is a copy
of the acquisition: neither the stream’s acquisition nor
ReconData.acquisitions changes.
Workers#
Each series is reconstructed in its own worker process, with the reconstruction plugin the client’s configuration names. The design names none, so the choice is independent of the scanner sequence the series was played from, and one design can be reconstructed by different plugins. A worker reconstructs one series and exits, which releases the host and GPU memory the reconstruction allocated. Importing a reconstruction engine takes seconds, so the proxy keeps spare worker processes that have already imported it.
The series of one exam share a directory on the reconstruction computer. What a series stores in its exam cache is written there, and a later series of the exam reads it back, such as a coil calibration it need not compute again. The exam is the one the header names, and the directory is the host’s rather than one proxy’s: a reconstruction computer runs one proxy per acquisition, so the series of an exam are reconstructed by different processes. It is deleted once no series of that exam is being reconstructed anywhere on the host.
Three maps have names the series agree on, and a hook reaches them as
attributes of its context: b0_map is off-resonance in Hz, b1_map the
transmit field as a fraction of what was asked for, and coil_sensitivities
the receive sensitivity of each coil, a
CoilSensitivities. A calibration hook assigns what it
measured and a later hook reads it, with None meaning no series of the exam
has measured it yet.
def recon(self, context, branch, data):
maps = context.coil_sensitivities # measured by an earlier series
image, transmit = parallel_imaging_and_b1_fit(data, maps)
context.b1_map = transmit # read by a later one
return recon.ReconResult(image)
A map is stored as measured, in the frame and on the grid of its series.
Nothing resamples, regrids or reslices it for a series acquired at another
position, orientation or matrix; coil_maps() compares the
coil sensitivities with the unit that needs them, as Calibration, noise and coil compression states.
Those three are the whole vocabulary of the exam’s maps, so a misspelt name
raises rather than storing a map where nothing looks for it. An artifact a
plugin carries for itself goes in context.exam under a key of its own, as
NOISE_COVARIANCE does for the whitening of a noise
series.
The number of series reconstructed concurrently is bounded by a number of
slots, derived from the available memory unless it is specified. On a host with
GPUs, which the proxy finds from CUDA_VISIBLE_DEVICES or nvidia-smi without
importing a GPU library, each slot holds one of them, one series per GPU unless
more are allowed, and the reconstruction finds its GPU in context.device. A
worker is an ordinary process, so a reconstruction may start processes of its
own. A series that
arrives when every slot is occupied is written to disk, enriched, as it
arrives, and replayed to a worker once a slot is released. The client remains
connected meanwhile, and the images are returned through its connection.
Images, DICOM datasets and text produced by the worker are relayed to the client as they are produced. Closing either connection closes the other. Once the client’s stream ends, the proxy waits for the worker to close however long the reconstruction takes, unless it was started with a reconstruction timeout, past which the worker is terminated and the client told so.