Design Principles

Overview

NiFreeze’s architecture is modular, facilitating extensibility and adaptability. NiFreeze is designed around the following core concepts:

  • Data ingestion: Handles input data, providing generalized 4D data management across modalities.

  • Preprocessing pipelines: Implements modality-specific preprocessing steps prior to artifact estimation. Examples of preprocessing operations are value clipping and smoothing.

  • Modelling: Implements statistical models to estimate and correct artifacts. Models follow a fit & predict design, where the fit step fits the model to the data, and the predict step estimates the volume that has been held out.

  • Estimation: Orchestrates the registration target prediction. Estimates the motion and distortion artifacts of the data employing a particular model and iterating over the data using a leave-one-volume-out approach. It computes the registration transforms on the estimated, artifact-free volumes. Estimator instances can be stacked so that the output of an estimator instance becomes the input to the subsequent instance.

Main Concepts

Data Ingestion

NiFreeze implements a set of modality-specific 4D data readers that expose a uniform API, enabling transparent access to individual volumes by index.

dMRI

dRMI modality gradients are handled internally arrays arranged according to the [gx gy gz b] layout, where [gx gy gz] are the components of a given gradient vector, and b is the b-value in units of \(s/mm^2\).

# ToDo # Or use [ R A S+ b ] instead of [gx gy gz b] as in usage.rst

The dMRI data class holds a single b0 volume. The rationale behind this is that this volume serves as a canonical reference for transforming non-zero gradient volumes. Users can compute such reference b0 volume using their preferred method and provide it when instantiating a api/nifreeze.data.dmri.DWI class (e.g. through the from_nii()); otherwise, NiFreeze computes a reference b0 as the median volume across non-diffusion-weighted volumes. Note that following this design choice, the gradient data hosted by the api/nifreeze.data.dmri.DWI instance only contains (nonzero) diffusion-weighted gradient values.

Preprocessing

NiFreeze provides a set of minimal preprocessing utilities under the form of api/nifreeze.estimator.Filters. api/nifreeze.estimator.Filters can be specified as inputs to estimators so that the data used by the estimator is processed through the given filter instance.

Modeling

These modules encapsulate the algorithms used for artifact estimation and correction. By abstracting the modeling logic, NiFreeze enables the integration of diverse methodologies tailored to specific research requirements.

Models do not preprocess the data, and users are expected to have their data preprocessed minimally (e.g., computing a unique, meaningful b0 volume for DWI data, smoothing and thresholding the PET data, clipping or detrending the data).

Iterators

Iterators in NiFreeze manage the traversal of data volumes, allowing access operations to particular data volumes. They are designed to be extensible, allowing developers to implement custom traversal logic as needed.

Estimation

NiFreeze estimators rely on predictive models to reconstruct artifact-free volumes from artifact-affected 4D datasets. Each model maintains access to the full dataset, while the estimator traverses the data according to a specified iteration strategy (e.g., forward, reverse, or random ordering). At each step, the estimator selects a target (held-out) volume as defined by the iterator, fits the model on the remaining volumes, and predicts the held-out volume. A registration process estimates the affine transformation between the held-out and predicted volumes, storing the result at the corresponding index in the dataset’s motion parameter attribute.

Important

The Update & iterate procedure follows a strict design principle: resampling is intentionally excluded from the update step; including it would alter the data distribution, making it impossible to attribute observed improvements solely to the predictive model.

Corrected datasets and associated metadata are produced in standardized formats for downstream analysis.

Extending NiFreeze

To extend NiFreeze’s functionality,

Creating New Iterators

To extend NiFreeze’s functionality, such as implementing a custom iterator, follow these general steps:

  1. Create a new function: Define a new iterator function implementing the desired logic.

    from typing import Iterator
    
    def my_iterator(**kwargs) -> Iterator[int]:
        # Size is the number of volumes in the dataset
        size = get_size(**kwargs)
    
        # Implement custom iteration logic
        index_order = establish_order(size)
    
        return (x for x in index_order)
    
  1. Integrate the function: Incorporate the newly created iterator into the NiFreeze workflow by updating the configuration or pipeline definitions to utilize your new implementation.

    list(my_iterator(10))
    
  1. Test the Implementation: Ensure that your custom class functions as intended by running unit tests and validating the outputs against expected results.

Creating New Models

To implement a new model:

1. Subclass the api/nifreeze.model.base.BaseModel class. 1. Implement the fit(data) method, which returns a 3D volume. 1. Register the model in your pipeline or CLI wrapper.

Example: Extrapolation model

from nifreeze.data.base import BaseDataset
from nifreeze.model.base import BaseModel

class ExtrapolationModel(BaseModel):
    """Model a volume extrapolating across training volumes."""

    __slots__ = {
        "_param1": "Parameter 1",
        "_param2": "Parameter 2",
    }

    def __init__(self, dataset: BaseDataset, param1: float, param2: float, **kwargs):

        super().__init__(dataset, **kwargs)

        self._param1 = param1
        self._param2 = param2

    def _fit(self, index, n_jobs=None, **kwargs):
        # Fit the data
        pass

    def fit_predict(self, index: int, **kwargs):
        # Compute prediction
        return pred

so fit_predict is suitable for case where the model is fit only once per iteration, and predict is for cases where the model is fit across multiple volumes in each iteration.

Usage

model = ExtrapolationModel()
result = model.fit(data, index=3)

Fitting and Prediction

Models have a _locked_fit property that allows to fit all available data if no index is provided. This is achieved by calling:

.. code-block:: python

model.fit_predict(None)

In this case, the predicted data will always be the same regardless of the index. The single_ prefix in the fitting strategy provided to the api/nifreeze.estimator.Estimator instances tells the estimator to proceed this way.

Usage

The NiFreeze command-line interface supports specifying multiple models, which are applied in a cascade fashion. Users can indicate which model should be used to address a specific problem, such as head motion or eddy current correction.

The transformations computed at each level are stored in an HDF5 file and are used to initialize the immediately next level of the registration process.

Refer to the Usage document for a self-contained usage example.

Contributing to NiFreeze

Set up for Development

Install the appropriate modules:

.. code-block:: bash
pip install .[test, doc, style, contributing]

Download data from OSF, OpenNeuro, etc.:

.. code-block:: bash
download

Typechecking, spellchecking, style, doc

Developer Resources

Refer to the Developer’s Corner document for detailed development guidelines.

For guidance on contributing to NiFreeze, refer to the NiPreps Contributing Guidelines:

https://www.nipreps.org/community/CONTRIBUTING/

This resource provides detailed instructions on community principles and contributor acknowledgements.