Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Leto Operations Integration

Leto is the Atlas-owned N-dimensional strided array library that provides the array storage, layout descriptors, zero-copy views, and storage backends used by every numeric operation in the RITK pipeline. The diffusion models assemble design matrices into Array2<f64>, signal vectors into Array1<f64>, and pass them to leto-ops solvers through the .view() method.

This chapter documents the integration surface — the Leto array types, views, and storage that RITK consumes — rather than the solver operations themselves, which are covered in the Leto Linear Algebra Operations provider chapter. The two chapters are paired: the provider chapter documents what Leto computes; this chapter documents how RITK holds and passes the data.

Architecture

Leto has a single domain crate (leto) that carries arrays, views, and storage, plus a companion operations crate (leto-ops) that carries the linear algebra and statistical kernels. The crates are separated so that consumers that only need array storage (like apollo-sht) can depend on leto without pulling in the decompositions and solvers.

CrateRole
letoN-dimensional Array<T, S, N>, ArrayView, ArrayViewMut, storage backends, layout descriptors
leto-opssolve_least_squares, nnls, cholesky_solve, qr_decompose, eigenvalues, norms, iterative solvers
coeus-cpuDynamic-rank (Coeus) to const-rank (Leto) dispatch over the CPU backend

RITK consumes leto directly for Array1/Array2 construction, and leto-ops for every linear solve in the diffusion pipeline.

Array Type

Array<T, S, N> is the generic N-dimensional strided array:

pub struct Array<T, S, const N: usize> {
    layout: Layout<N>,    // shape, strides, offset
    storage: S,           // heap-allocated or borrowed backing
    _marker: PhantomData<T>,
}

The type is rank-generic (const N: usize) and storage-generic (S), which means the same array API works for 1-D signals, 2-D matrices, and 3-D image volumes, backed by any storage that implements Storage<T>.

Type Aliases

Most RITK code uses rank-specific type aliases that fix S = VecStorage<T>:

AliasRankTypical use in RITK
Array1<T>1Signal vectors, coefficient vectors
Array2<T>2Design matrices, augmented systems
Array3<T>3Image volumes
Array4<T>44-D DWI series
ArrayD<T>Fixed-large rankHigher-rank tensors (still const-rank)
use leto::{Array1, Array2};

// DTI: one design matrix per voxel (N_dwi x 6)
let design: Array2<f64> = Array2::zeros([n_dwi, 6]);

// CSD: non-negative fODF coefficients
let coefficients: Array1<f64> = result.solution;

Construction

MethodDescription
Array1::zeros([n])Zero-filled 1-D array
Array2::zeros([rows, cols])Zero-filled 2-D array
Array1::from_vec(n, data)1-D from owned Vec<T>
Array2::from_shape_vec([r, c], data)2-D from Vec<T>, validates shape
Array::new(layout, storage)From explicit layout + storage
use leto::Array1;

// Signal vector from per-voxel DWI values
let n_dwi = 60;
let signal: Vec<f64> = (0..n_dwi).map(|i| measured_signal[i]).collect();
let rhs = Array1::from_vec(n_dwi, signal);

Accessors

MethodReturns
shape() -> [usize; N]Shape as const array
strides() -> [isize; N]Strides as const array
offset() -> usizePhysical base offset
len() -> usizeLogical element count
is_empty() -> boolTrue when any axis is zero
layout() -> Layout<N>Full layout descriptor
get([i, j]) -> Result<&T>Element at logical index
as_slice() -> Option<&[T]>Contiguous row-major slice
as_slice_mut() -> Option<&mut [T]>Mutable contiguous slice
iter() -> ElementIterLogical row-major iterator
indexed_iter() -> IndexedIter(multi-index, &elem) iterator
to_contiguous() -> Array<T, VecStorage<T>, N>Materialize C-contiguous copy

Views

The .view() method returns an ArrayView<T, N> — a zero-copy borrowed reference that shares the parent array’s storage:

let design: Array2<f64> = Array2::zeros([60, 6]);
let signal: Array1<f64> = Array1::zeros([60]);

// Pass zero-copy views to leto-ops solvers
let solution = leto_ops::solve_least_squares(
    &design.view(),    // ArrayView2<f64>
    &signal.view(),    // ArrayView1<f64>
)?;

This is the universal pattern in the diffusion pipeline: construct arrays, then pass .view() to solvers. The view is read-only, zero-cost, and validates that the layout does not exceed the backing storage.

ArrayView and ArrayViewMut

ArrayView provides the same shape, strides, indexing, slicing, and iteration methods as Array, but borrows its data from a parent:

View methodDescription
view.shape()Const shape
view.get([i, j]) -> Result<&T>Logical indexing
view.slice(&ranges)Zero-copy sub-view
view.transpose(axes)Zero-copy transposed view
view.reshape(shape)Reinterpret shape (same numel)
view.to_contiguous()Materialize owned Array<T, VecStorage<T>, N>
view.as_array()Zero-copy borrowed Array<T, SliceStorage<T>, N>

ArrayViewMut adds get_mut, fill, assign, and mutable slice accessors.

Storage Backends

Leto’s storage trait hierarchy parallels Coeus’s but is independently owned:

TypeDescription
VecStorage<T>Heap-allocated Vec<T> — the default for Array1/Array2
SliceStorage<'a, T>Borrowed &'a [T] — created by view.as_array()
CowStorage<T>Copy-on-write wrapper over any StorageMut<T>
MnemosyneStorage<T>Custom-allocated via Mnemosyne (feature-gated)
StackStorage<T, N>Fixed-size stack allocation

In RITK, VecStorage<T> is the default for construction; SliceStorage is used by zero-copy view-to-array conversion; MnemosyneStorage is used by the Coeus tensor stack for custom allocator integration.

Layout System

Layout<N> carries shape, strides, and offset — the metadata that describes how logical indices map to physical positions in a flat slice:

pub struct Layout<const N: usize> {
    pub shape: [usize; N],
    pub strides: [isize; N],
    pub offset: usize,
}

Layout validation ensures that no logical index accesses memory before the offset or beyond the storage length. Contiguity predicates determine whether as_slice() is safe:

MethodCondition
is_c_contiguous()Row-major strides, offset 0
is_f_contiguous()Column-major strides, offset 0
is_c_dense()Row-major strides, any offset
is_contiguous()Dense in either C or F order, any offset

The diffusion pipeline’s arrays are always C-contiguous at offset 0, so .as_slice() and .view() always succeed without materialization.

coeus-cpu Interop

The coeus-cpu crate bridges the dynamic-rank Coeus Layout to Leto’s const-rank Layout<N>:

use coeus_cpu::{to_leto_view, contiguous_values};

// Dynamic-rank Coeus tensor to const-rank Leto view
let coeus_layout = tensor.layout(); // coeus_core::Layout (rank at runtime)
let leto_view = to_leto_view::<f64, 2>(coeus_layout, tensor.as_slice())?;

// Materialize logical values from any layout
let values = contiguous_values(coeus_layout, tensor.as_slice())?;

The dispatch shim resolves Coeus’s runtime rank to a Leto const N through a bounded match (up to MAX_DISPATCH_RANK = 6), then calls the monomorphized Leto kernel. This keeps Leto purely const-rank (the source of its compile-time shape safety) and consolidates all CPU array operations through one authoritative kernel set.

In the diffusion pipeline, contiguous_values is called by the DTI/DKI estimators when building Leto arrays from Coeus tensor slices — it extracts logical row-major values from potentially strided or offset Coeus storage.

Diffusion Pipeline Usage

Every linear solve in the diffusion pipeline follows the same pattern:

use leto::{Array1, Array2};
use leto_ops::solve_least_squares;

// 1. Assemble design matrix and RHS in Leto arrays
let mut design = Array2::zeros([n_dwi, 6]);
let mut log_signals = Array1::zeros([n_dwi]);

for i in 0..n_dwi {
    let (x, y, z) = scheme.directions[i].unit_direction();
    design[[i, 0]] = x * x;
    design[[i, 1]] = y * y;
    // ... fill Voigt design matrix ...
    log_signals[i] = log(signal[i]);
}

// 2. Solve via leto-ops
let solution = solve_least_squares(
    &design.view(),
    &log_signals.view(),
)?;

The mapping between model and Leto constructs is uniform:

ModelDesign matrixRHS vectorSolverResult type
DTIArray2<f64> (N×6 Voigt)Array1<f64> (log signals)solve_least_squaresArray1<f64> (6 tensor elements)
ODFArray2<f64> (augmented, N+K×K)Array1<f64> (augmented RHS)solve_least_squaresArray1<f64> (K SH coefficients)
CSDArray2<f64> (N×K deconvolution)Array1<f64> (signals)nnlsArray1<f64> (K non-negative coeffs)

Iterators

Leto provides several zero-allocation iterator types over arrays and views:

IteratorYieldsNotes
iter()&T in logical row-major orderWorks on any strides
indexed_iter()([usize; N], &T) pairsMulti-index + value
exact_chunks([cs; N])ArrayView<T, N> sub-viewsNon-overlapping tiles
axis_chunks_iter(axis, len)ArrayView<T, N> sub-viewsChunks along one axis
windows([ws; N])ArrayView<T, N> sub-viewsSliding windows
lanes::<M>(axis)1-D lane viewsStrided axis-as-lane views
axis_iter::<M>(axis)Sub-views of rank N-1Dimension-reducing iteration

These iterators are used by the diffusion pipeline’s volumetric fitting loops — for example, axis_chunks_iter(0, 256) parcels voxels into 256-element chunks for batched dispatch through Moirai.

Sparse Storage

Leto provides three sparse matrix formats in leto::sparse:

FormatTypeDescription
CSRCsrArray<T>Compressed Sparse Row — efficient SpMV
CSCCscArray<T>Compressed Sparse Column — efficient SpMV transpose
COOCooArray<T>Coordinate list — construction format

These are consumed by coeus-cpu’s spmm_into / spmv_into dispatch kernels for sparse matrix-matrix and matrix-vector multiplication. Non-diffusion pipelines (graph algorithms, registration) use these formats.

Boundary

Leto owns the Array<T, S, N> type family, ArrayView/ArrayViewMut, the Layout<N> descriptor, storage backends, and all iterator types. leto-ops owns the linear algebra kernels and decompositions.

RITK constructs Array1/Array2 from diffusion data, assembles design matrices, and calls leto-ops solvers through .view(). A RITK-local array type, local linear solve, or local decomposition is a boundary violation — the Leto array is the single source of truth for numeric data in the RITK pipeline.

References