Data Structures, Dimensions, and Indexing

In MRpro, we use torch.Tensors and “Dataclasses” to store and manage data. Dataclasses serve as containers for different tensors, metadata, and other dataclasses, ensuring a consistent and structured approach to data handling.

For example mrpro.data.KData contains complex raw data, trajectory information, and headers.

Dimensions and Broadcasting Rules

MRpro follows a consistent convention of using at least 5-dimensional tensors for data representation:

  • K-space data: (*other, coils, k2, k1, k0)

  • Real space (image) data: (*other, coils, z, y, x)

Here, *other represents additional dimensions, such slice positions, repetitions or cardiac phases. Singleton dimensions are enforced for quantities that do not vary along a specific axis, ensuring they are broadcastable.

For example, a 2D Cartesian trajectory might have: - kx field: (other, coils=1, k2=1, k1=1, k0) - ky field: (other, coils=1, k2=1, k1, k0=1)

Calling shape on the KTrajectory dataclass returns (other, 1, 1, k1, k0). Neither kx nor ky have this shape, but both can be broadcasted to this shape.

Indexing Conventions

Dataclasses support advanced indexing. It follows numpy-like rules with two key exceptions: 1. Indexing never removes dimensions. 2. New dimensions are added at the beginning.

The index can contain slices, integers, boolean masks, sequences of integers, None, and Ellipsis:

  • Slices: Behave like in numpy, always return a view. Negative step sizes are not supported.

  • Integers: Behave like slicing with index:index+1, always return a view.

  • Boolean masks: Singleton dimensions in the mask are interpreted as full slices. If the mask has more than one non-singleton dimension, a new dimension is added at the beginning.

  • Sequences of integers: Result in concatenation along the indexed dimension. If more than one sequence is used, a new dimension is added at the beginning.

  • Integer tensors: If a single indexing tensor is used, the indexed dimension will be replaced by the last dimension of the indexing tensor. Other dimensions of the indexing tensor are added at the beginning of the result. If multiple indexing tensors are used, the indexed dimension will be replaced by a singleton dimension and all new dimensions are added at the beginning of the result.

  • None: Can only be used to add new axes at the beginning.

  • Ellipsis: Expanded to slice(None) for all axes not indexed.

The indexing is applied recursively to all tensors in the dataclass, ensuring that the dimensions are consistent across all tensors. Indexing is applied as if all tensors where broadcasted to the shape of the dataclass the indexing is applied to. So, for example, indexing a KData object in the coils dimension results in the same trajecktory as before, as trajectories always have coils=1. The behavior is as if the trajectory is broadcasted along the coil dimensions, then indexing is applied, then the boadcasted dimensions is reduced back to singleton.

SpatialDimension

The mrpro.data.SpatialDimension class represents spatial positions as a named tuple of z, y, and x values. It supports basic math operations and indexing when the values are tensors. This class is used, for example, to store position information in headers.

Rotation

The mrpro.data.Rotation class handles orientations and rotations. It supports conversion between different representations (e.g., Euler angles, quaternions, rotation matrices) and can be applied to tensors and SpatialDimension objects.

Units

All values in MRpro are in SI units. For example, spatial dimensions are in meters, time is in seconds, and angles are in radians. This ensures consistency but might require conversion when interfacing with other software or hardware that uses different units, such as milliseconds for echo times or degrees for flip angles.

Operators

The operators in mrpro.operators can be applied to one or multiple torch.Tensor and return tuples of torch.Tensor (even if only a single tensor is returned). The convention regarding dimensions is the same as for dataclasses: if a new dimension is added, it is added in front (example: time points in signal models) and the dimensions are in the order (*other, coils, k2, k1, k0) for k-space data and (*other, coil, z, y, x) for real space data. The operators are always batched, so they can be applied to multiple data points at once.

Examples

Below are practical examples demonstrating the use of dataclasses, indexing, and rotations in MRpro:

import mrpro

# Example: Indexing a broadcasted tensor
kdata = mrpro.data.KData.from_file('example.mrd')
print(kdata.shape)  # Example shape: (4, 8, 64, 64, 128)
print(kdata.header.acq_info.idx.k1.shape)  # Example shape: (4, 1, 64, 64, 1)

kdata_center = kdata[... 16:-16, 16:-16,16:-16]
print(kdata_center.shape)  # Example shape: (4, 8, 32, 32, 96)
print(kdata.header.acq_info.idx.k1.shape)  # Example shape: (4, 1, 32, 32, 1)

# Example: Creating and using SpatialDimension and Rotation
position = mrpro.data.SpatialDimension(z=3, y=2, x=1)
rotation = mrpro.data.Rotation.from_euler("xyz", (0,0,torch.pi))
rotated = rotation(position)