nifreeze.analysis.sensitivity module

Sensitivity of model predictions to motion of their training volumes.

Two questions are asked of a model, on an aligned dataset:

  • fidelity(): how well does it predict each held-out volume?

  • sensitivity(): how far do its predictions drift from those on the aligned dataset when some volumes move by known rigid motions, or are distorted as by eddy currents (a perturbation)?

The perturbed copies are given, each with its ground truth (a Perturbation): a simulator produces realistic ones (nifreeze.analysis.phantoms reads them), and nifreeze.testing.perturbations resamples a dataset for tests. A fresh model is built on each copy. A held-out prediction never uses its own volume, so every volume can be evaluated, the moved ones included: a moved volume’s prediction sees the motion of the others only. Registration, the estimator and its intensity window are not involved.

class nifreeze.analysis.sensitivity.Distortion(self, magnitude: 'float', axis: 'int', seed: 'int | None', degree: 'int', center: 'np.ndarray', radius: 'np.ndarray', frame: 'np.ndarray', coefficients: 'np.ndarray', readout: 'np.ndarray | None' = None, readout_time: 'float | None' = None) → None[source]

Bases: object

Displacements along the phase-encoding axis, polynomial in space, one per volume.

Volume i shows at each point what lies \(d_i\) mm further along the phase-encoding axis. \(d_i\) is a polynomial of the voxel-axis coordinates, centered on center and divided by radius along each axis.

Without readout, a distorted volume is resampled at the displaced points, with its intensity scaled by the Jacobian \(1 + \partial d_i / \partial x_{PE}\) so that no signal is created or lost. With it, the volume is formed as an EPI readout forms it, which is how TRXScan simulates eddy currents: the displacement is that of an off-resonance field \(f_i = d_i / (T \, \delta)\) (Hz), with \(T\) the total readout time and \(\delta\) the voxel size along phase encoding, and each phase-encoding line accrues the field’s phase at the time it is acquired.

Method generated by attrs for class Distortion.

axis: int

Voxel axis of phase encoding.

basis(points: ndarray) → tuple[ndarray, ndarray][source]

Evaluate each term, and its derivative along the phase-encoding axis.

Parameters:

points (ndarray) – World coordinates, of shape (3, n_points).

Returns:

The terms and their derivatives (per mm), each of shape (n_terms, n_points).

Return type:

tuple of ndarray

center: ndarray

World coordinates of the origin of the polynomial.

coefficients: ndarray

Displacement (mm) carried by each term, of shape (n_volumes, n_terms).

degree: int

Degree of the polynomial.

property direction: ndarray

World direction of the phase-encoding axis.

distort(volume: ndarray, affine: ndarray, coefficients: ndarray, order: int = 3) → ndarray[source]

Distort a volume by the displacement that some coefficients give.

Parameters:
  • volume (ndarray) – The undistorted volume.

  • affine (ndarray) – Its grid, whose voxel axis axis is the phase-encoding axis.

  • coefficients (ndarray) – The displacement (mm) carried by each term, as a row of coefficients.

  • order (int, optional) – Interpolation order, when the volume is resampled.

Returns:

The distorted volume, on the same grid.

Return type:

ndarray

frame: ndarray

Direction cosines of the voxel axes, as columns.

classmethod from_sidecar(eddy: Mapping[str, Any], indices: Sequence[int], n_volumes: int) → Distortion[source]

The eddy currents of a perturbed copy, from its sidecar.

Parameters:
  • eddy (Mapping) – The Eddy entry of the sidecar written by tools/phantoms/phantom.py.

  • indices (Sequence) – The dataset’s index of each volume whose coefficients the sidecar lists.

  • n_volumes (int) – Number of volumes of the dataset; the others are not distorted.

Returns:

The distortion, formed as an EPI readout forms it.

Return type:

Distortion

magnitude: float

Largest displacement (mm) of any voxel of the mask, over all volumes.

radius: ndarray

Scale (mm) of the polynomial’s variables along each voxel axis.

readout: ndarray | None

the n-th of N lines has the spatial frequency N // 2 - n. None to resample.

Type:

Acquisition time (s, from the echo) of each phase-encoding line

readout_time: float | None

Total readout time (s), which turns displacements into fields.

seed: int | None

Seed that drew the map from gradients to coefficients.

nifreeze.analysis.sensitivity.NO_PERTURBATION = Perturbation(magnitude=0.0, seed=None, moved=(), motions=(), distortion=None)

The ground truth of the aligned dataset.

class nifreeze.analysis.sensitivity.Perturbation(self, magnitude: 'float', seed: 'int | None', moved: 'tuple[int, ...]' = (), motions: 'tuple[np.ndarray, ...]' = (), distortion: 'Distortion | None' = None) → None[source]

Bases: object

The ground truth of a perturbed dataset.

Some volumes moved by rigid motions, and optionally every volume distorted.

Method generated by attrs for class Perturbation.

property count: int

Number of moved volumes.

distortion: Distortion | None

Distortion of every volume, applied after its motion, in the scanner’s frame.

magnitude: float

Rotation (degrees) and translation (mm) of every moved volume.

motions: tuple[ndarray, ...]

Head motion of each moved volume, as a (4, 4) matrix in world coordinates.

moved: tuple[int, ...]

Indices of the moved volumes.

seed: int | None

Seed that drew the moved volumes and their motions.

nifreeze.analysis.sensitivity.fidelity(model: str | ~collections.abc.Callable[[...], ~typing.Any], dataset: ~typing.Any, evaluate: ~collections.abc.Iterable[int] | None = None, mask: ~numpy.ndarray | None = None, metrics: ~collections.abc.Mapping[str, ~collections.abc.Callable[[...], float]] = {'cc': <function masked_cc>, 'mi': <function mutual_information>, 'nrmse': <function nrmse>}, **model_kwargs: ~typing.Any) → list[dict[str, Any]][source]

Compare each held-out prediction with its volume, on an aligned dataset.

Parameters:
  • model (str or Callable) – The model (see held_out_predictions()).

  • dataset (BaseDataset) – The aligned dataset.

  • evaluate (Iterable, optional) – Volumes to predict (all if None).

  • mask (ndarray, optional) – Voxels compared; the dataset’s brain mask, or the whole volume, if None.

  • metrics (Mapping, optional) – Similarity metrics by name, with the signature of masked_cc().

  • **model_kwargs – Arguments for the model.

Returns:

One row per evaluated volume, with each metric.

Return type:

list of dict

nifreeze.analysis.sensitivity.held_out_predictions(model: str | Callable[[...], Any], dataset: Any, evaluate: Iterable[int] | None = None, **model_kwargs: Any) → dict[int, ndarray][source]

Predict each evaluated volume from all the others, with a fresh model.

Parameters:
  • model (str or Callable) – A model name for ModelFactory, or a callable building a model from a dataset (such as a model class).

  • dataset (BaseDataset) – The dataset the model is built on.

  • evaluate (Iterable, optional) – Volumes to predict (all if None).

  • **model_kwargs – Arguments for the model.

Returns:

The prediction of each evaluated volume.

Return type:

dict

nifreeze.analysis.sensitivity.influence(model: Any) → ndarray[source]

Analytic influence of each volume on the predictions of a linear smoother.

The closed-form models predict volume \(i\) as \(\mathbf{S}_{i} \mathbf{Y}\), so moving volume \(j\) by \(\Delta\mathbf{y}_j\) changes every prediction by \(\mathbf{S}_{:, j} \Delta\mathbf{y}_j\).

Parameters:

model (ClosedFormLOVOMixin) – A closed-form model.

Returns:

The norm of each column of the smoother, \(\|\mathbf{S}_{:, j}\|_2\).

Return type:

ndarray

nifreeze.analysis.sensitivity.sensitivity(model: str | Callable[[...], Any], reference: Any, cells: Iterable[tuple[Perturbation, Any]], evaluate: Iterable[int] | None = None, mask: ndarray | None = None, metrics: Mapping[str, Callable[[...], float]] | None = None, **model_kwargs: Any) → list[dict[str, Any]][source]

Measure how far held-out predictions drift from those on a reference dataset.

Each cell pairs a perturbed copy of reference (same grid, same volumes, some moved or distorted) with the Perturbation that made it. Each prediction on a copy is compared with the prediction of the same volume on the reference. The change is reported as an NRMSE divided by the standard deviation of the actual volume, not by that of the reference prediction: a smooth prediction would otherwise inflate its own error, and models could not be compared.

Parameters:
  • model (str or Callable) – The model (see held_out_predictions()).

  • reference (BaseDataset) – The aligned dataset.

  • cells (Iterable) – Pairs of a Perturbation and the perturbed copy of reference, or a function without arguments that loads it (each copy is then held in memory only while it is analyzed).

  • evaluate (Iterable, optional) – Volumes to predict (all if None), moved or not.

  • mask (ndarray, optional) – Voxels compared; the reference’s brain mask, or the whole volume, if None.

  • metrics (Mapping, optional) – Further metrics by name, with the signature of masked_cc(), called as metric(perturbed, aligned, mask).

  • **model_kwargs – Arguments for the model.

Returns:

One row per cell and evaluated volume, with the volume’s shell (its b-value to the nearest 100 s/mm², NaN without gradients), the nrmse, each further metric, whether that volume was itself moved, and the magnitude of the distortion (0 without one).

Return type:

list of dict