Skip to content

The data model

MRIData is the one representation every reader produces and every operator, reconstruction, and pipeline stage consumes. This page is the normative spec for its contents. Frameworks most often trip over axis order and k-space units, so those are pinned down here explicitly.

MRIData

field type notes
kspace array (complex) the samples; layout rules below
kspace_axes tuple[str, ...] one name per kspace axis
encoding EncodingSpace geometry and sampling pattern
acquisition AcquisitionInfo sequence / contrast / nucleus
coils CoilInfo \| None channel count, names, noise covariance
trajectory Trajectory \| None None ⟺ Cartesian
metadata ScannerMetadata vendor, model, field strength
provenance Provenance ordered processing log

MRIData.validate() checks internal consistency and raises ValidationError on the first problem. Readers must return a validated object.

k-space layout

kspace is an n-dimensional complex array. kspace_axes names every axis. The spatial / readout axes come last, in exactly one of two groupings:

sampling trailing axes example full layout
Cartesian "coil", "kz", "ky", "kx" (drop "kz" for 2D) ("repetition", "coil", "kz", "ky", "kx")
non-Cartesian "coil", "shot", "readout" ("echo", "coil", "shot", "readout")

"shot" indexes trajectory interleaves / spokes / projections; "readout" indexes samples along one shot.

Leading axes describe everything else and may appear in any order. Recognised names (unimri.data.KNOWN_AXES):

coil  kx ky kz  shot readout
average contrast echo phase repetition set slice segment user

A dataset is Cartesian iff it has no trajectory and encoding.sampling == CARTESIAN.

Trajectory and the unit convention

Trajectory(coords, units=TrajectoryUnits.NORMALIZED, density_compensation=None)

coords has shape (n_dims, *sample_axes) — e.g. (3, n_shots, n_readout) — where n_dims is 2 or 3, matching encoding.n_dims. The trailing axes must multiply to the same count as kspace's shot * readout.

Axis order. Row d of coords corresponds to image axis d in NumPy order, i.e. (ky, kx) for a 2-D image (ny, nx) and (kz, ky, kx) for a 3-D image (nz, ny, nx) — the same slowest-to-fastest order as the Cartesian kspace_axes (… kz, ky, kx). NUFFTOperator and the reference ndft both assume this.

Units. UniMRI's canonical unit is NORMALIZED: one unit of coords is one sample of the encoded matrix, and the fully-sampled Nyquist window is [-N/2, N/2) per axis. This matches BART and mri-nufft's unitless mode.

enum Nyquist window conversion from normalized (matrix N)
NORMALIZED [-N/2, N/2) —
CYCLES_PER_FOV [-0.5, 0.5) k / N
RADIANS_PER_VOXEL [-π, π) k · 2π / N
INVERSE_METERS physical k / FOV_mm · 1000

NUFFT backends that want radians-per-voxel (e.g. torchkbnufft) do the conversion inside the operator; readers should store NORMALIZED.

Density compensation, when known analytically or supplied by the vendor, lives in Trajectory.density_compensation. It is applied as a separate diagonal operator during reconstruction so it never pollutes the $A/A^H$ adjoint relationship.

EncodingSpace

field meaning
recon_matrix target image matrix (nx, ny, nz)
encoded_matrix nominal fully-sampled k-space matrix (nx, ny, nz)
fov FieldOfView(x, y, z) in mm
sampling SamplingPattern enum (CARTESIAN, RADIAL, SPIRAL, STACK_OF_STARS, CONES, …)
acceleration parallel-imaging factor per PE axis, e.g. (2, 1)
partial_fourier fraction per axis (ro, pe1, pe2), 1.0 = full
n_dims 2 or 3
affine optional 4×4 voxel→world (RAS+) matrix

Both matrices are always length-3 tuples of positive ints (nz = 1 for 2D).

AcquisitionInfo — including multinuclear

Times in seconds, angles in degrees.

field notes
sequence_name, protocol_name
tr_s, te_s (list), ti_s (list), flip_angle_deg
n_contrasts, n_averages
nucleus "1H", "23Na", "31P", "13C", "2H", "17O", "19F", "129Xe"
field_strength_t tesla
larmor_hz scanner-reported centre frequency

Derived: gamma_hz_per_t (from a built-in gyromagnetic table), expected_larmor_hz (|γ| · B₀), is_multinuclear(). X-nuclei support is first-class from day one — it is almost free at the data-model level and is a primary motivation (sodium imaging is where Cartesian-only tools fail).

Provenance

data.provenance.record("remove_oversampling", params={"axis": "kx"}, backend="numpy")

Each ProvenanceStep stores the operation name, its parameters, the unimri version, the backend, and a timestamp. A reconstruction result can therefore be traced back to the raw file and every parameter in between — intended to be pasted directly into a methods section.

On-disk HDF5 schema (planned)

HDF5Reader / an MRIData.to_hdf5() writer will use a single layout:

/kspace                 complex dataset
  @axes                 JSON list of axis names
/trajectory/coords      float dataset            (optional)
/trajectory/dcf         float dataset            (optional)
  @units                "normalized" | …
/encoding               group of scalar attrs
/acquisition            group of scalar attrs
/metadata               group of scalar attrs
/provenance             JSON string attr

This is the format used for test fixtures and result caching, and is deliberately not a competitor to ISMRMRD for interchange.