Audit a derived map

Recover a chained pipeline from the .nii.gz alone.

Goal

You receive a .nii.gz with no producer, no sidecar, no Slack thread. Recover what produced it, from what input, with what mask, through what chain of operations from the file’s bytes alone.

Code

from pathlib import Path
import sys
import tempfile

sys.path.insert(0, str(Path.cwd().parents[1] / "src"))

import nibabel as nib
import neuroim as ni

repo = Path.cwd().parents[1] if Path.cwd().name == "cookbook" else Path.cwd()
bold = ni.read_image(str(repo / "golden_tests/fixtures/tiny_bold.nii.gz"))
mask = ni.read_image(str(repo / "golden_tests/fixtures/tiny_mask.nii.gz"), type="vol")

# Producer: a two-stage pipeline — resample, then temporal SNR.
template4 = mask.space.add_dim(size=bold.shape[-1])
resampled = ni.resample_vec(bold, template4, interpolation=1)
tsnr = resampled.temporal_snr(mask=mask)

out = Path(tempfile.mkdtemp()) / "tsnr_template.nii.gz"
nib.save(tsnr.to_nibabel(), str(out))

# Auditor: a fresh process with only the path.
audited = ni.read_image(str(out))
audited.provenance.method_name
'resample_vec(order=1,source=d0f6bff35b9c7ce5,target=d0f6bff35b9c7ce5)+temporal_snr'

The method_name is chained — it names both pipeline stages, in order, with the resample’s parameters folded in: resample_vec(order=1,source=…,target=…)+temporal_snr.

What’s guaranteed

  • resample_vec records source/target space hashes and the interpolation order in its Receipt.
  • temporal_snr merges the upstream Receipt rather than discarding it — the chain prefix is preserved across the reduction.
  • The chained Receipt embeds on to_nibabel() and rehydrates on read_image().

What survives the file

p = audited.provenance
(p.method_name, p.input_space_hash[:10], p.mask_hash[:10])
('resample_vec(order=1,source=d0f6bff35b9c7ce5,target=d0f6bff35b9c7ce5)+temporal_snr',
 'd0f6bff35b',
 '649ca2f312')

The terminal input_space_hash is the template frame; the mask_hash is the mask used at the reduction; the method_name carries the lineage. An auditor can confirm the source and mask by recomputing their hashes if those files are still around.

Trapdoors

  • A pipeline step that returns a bare ndarray breaks the chain — the next step has no upstream Receipt to merge. Keep derived values in their typed carriers until the final write.
  • Use to_nibabel() with nib.save(...) when you want the Receipt embedded in the .nii.gz itself.

See also

Back to top