nifreeze.testing.perturbations module

Known perturbations of a dataset, for the controls of nifreeze.analysis.sensitivity.

The sensitivity analysis compares predictions on a reference dataset with predictions on perturbed copies of it, whose ground truth it is given (a Perturbation). Realistic copies come from a simulator; these helpers resample a dataset through rigid motions and eddy-current-like distortions instead, so that tests can check the analysis against answers known in closed form.

nifreeze.testing.perturbations.eddy_distortion(dataset: Any, magnitude: float, axis: int, degree: int = 1, seed: int = 0, gradients: Buffer | _SupportsArray[dtype[Any]] | _NestedSequence[_SupportsArray[dtype[Any]]] | complex | bytes | str | _NestedSequence[complex | bytes | str] | None = None, mask: ndarray | None = None) → Distortion[source]

Draw distortions that follow the diffusion gradients, as eddy currents do.

The fields that a diffusion gradient leaves behind scale with its amplitude and change sign with its direction. Each volume’s coefficients are therefore one fixed random linear map of its gradient vector, scaled by \(\sqrt{b / b_{max}}\) (the gradient amplitude, at fixed timing). b=0 volumes are not distorted, and antipodal gradients get opposite distortions.

Parameters:
  • dataset (BaseDataset) – The aligned dataset.

  • magnitude (float) – Largest displacement (mm) of any voxel of the mask, over all volumes.

  • axis (int) – Voxel axis of phase encoding.

  • degree (int, optional) – Degree of the polynomial (1: translation, scaling and shears).

  • seed (int, optional) – Seed for the linear map.

  • gradients (ArrayLike, optional) – Gradient table, (n_volumes, 4) with the b-value last; the dataset’s if None.

  • mask (ndarray, optional) – Voxels over which the polynomial is normalized and magnitude reached.

Returns:

The distortions.

Return type:

Distortion

nifreeze.testing.perturbations.mask_centroid(dataset: Any, mask: ndarray | None = None) → ndarray[source]

World coordinates of the centroid of a mask (the brain mask by default).

Parameters:
  • dataset (BaseDataset) – The dataset.

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

Returns:

The centroid, of shape (3,).

Return type:

ndarray

nifreeze.testing.perturbations.one_at_a_time(volumes: Iterable[int], magnitude: float, seed: int = 0, center: Buffer | _SupportsArray[dtype[Any]] | _NestedSequence[_SupportsArray[dtype[Any]]] | complex | bytes | str | _NestedSequence[complex | bytes | str] = (0.0, 0.0, 0.0)) → list[Perturbation][source]

Move each volume alone, to rank the volumes by their influence.

Parameters:
  • volumes (Iterable) – The volumes to move, one per perturbation.

  • magnitude (float) – Motion magnitude (degrees and mm, see rigid_motion()).

  • seed (int, optional) – Seed for the rotation axes and translation directions.

  • center (ArrayLike, optional) – World coordinates of a point on every rotation axis.

Returns:

One perturbation per volume.

Return type:

list of Perturbation

nifreeze.testing.perturbations.perturb(dataset: Any, perturbation: Perturbation, order: int = 3) → Any[source]

Copy a dataset with some of its volumes moved or distorted.

Each moved or distorted volume is resampled once from the aligned original, through its motion and then its distortion. A distortion with an EPI readout (readout) instead forms the moved volume as that readout would. Everything else (gradients, b=0 reference, mask) is left as it was, as a model would see it when the motion and the distortion go uncorrected.

Parameters:
  • dataset (BaseDataset) – The aligned dataset.

  • perturbation (Perturbation) – The volumes to move, their motions, and the distortion.

  • order (int, optional) – Interpolation order.

Returns:

A copy of the dataset with float32 volumes.

Return type:

BaseDataset

nifreeze.testing.perturbations.perturbation_plan(n_volumes: int, magnitudes: Sequence[float], counts: Sequence[int], seeds: int | Sequence[int] = 5, center: Buffer | _SupportsArray[dtype[Any]] | _NestedSequence[_SupportsArray[dtype[Any]]] | complex | bytes | str | _NestedSequence[complex | bytes | str] = (0.0, 0.0, 0.0), fixed: Iterable[int] = ()) → list[Perturbation][source]

Draw the perturbations of a sensitivity experiment.

For each seed, the moved volumes are the first count of a random ordering of the volumes not listed in fixed, each with a random rotation axis and translation direction. Volumes moved with a smaller count are thus also moved with a larger one, and they follow the same axis and direction at every magnitude, so the curves over magnitude and count are paired.

Parameters:
  • n_volumes (int) – Number of volumes in the dataset.

  • magnitudes (Sequence) – Motion magnitudes (degrees and mm, see rigid_motion()).

  • counts (Sequence) – Numbers of moved volumes.

  • seeds (int or Sequence, optional) – Number of seeds (0, 1, ...), or the seeds themselves.

  • center (ArrayLike, optional) – World coordinates of a point on every rotation axis (see mask_centroid()).

  • fixed (Iterable, optional) – Volumes that never move.

Returns:

One perturbation per seed, count and magnitude.

Return type:

list of Perturbation

Examples

>>> plan = perturbation_plan(10, magnitudes=[0.5, 1.0], counts=[1, 3], seeds=2, fixed=[0])
>>> len(plan)
8
>>> any(0 in p.moved for p in plan)
False
>>> set(plan[0].moved) <= set(plan[2].moved)
True
nifreeze.testing.perturbations.rigid_motion(magnitude: float, axis: Buffer | _SupportsArray[dtype[Any]] | _NestedSequence[_SupportsArray[dtype[Any]]] | complex | bytes | str | _NestedSequence[complex | bytes | str], direction: Buffer | _SupportsArray[dtype[Any]] | _NestedSequence[_SupportsArray[dtype[Any]]] | complex | bytes | str | _NestedSequence[complex | bytes | str], center: Buffer | _SupportsArray[dtype[Any]] | _NestedSequence[_SupportsArray[dtype[Any]]] | complex | bytes | str | _NestedSequence[complex | bytes | str] = (0.0, 0.0, 0.0)) → ndarray[source]

Rotate by magnitude degrees about axis and shift by magnitude mm.

Parameters:
  • magnitude (float) – Rotation angle (degrees) and translation length (mm).

  • axis (ArrayLike) – Rotation axis (normalized internally).

  • direction (ArrayLike) – Translation direction (normalized internally).

  • center (ArrayLike, optional) – World coordinates of a point on the rotation axis.

Returns:

The (4, 4) motion, mapping head positions before the motion to positions after it.

Return type:

ndarray

Examples

>>> motion = rigid_motion(90.0, (0, 0, 1), (1, 0, 0), center=(10, 0, 0))
>>> np.round(motion @ (10, 0, 5, 1), 6).tolist()
[100.0, 0.0, 5.0, 1.0]
>>> np.round(motion @ (11, 0, 0, 1), 6).tolist()
[100.0, 1.0, 0.0, 1.0]