Save a derived map with provenance

Write a tSNR map whose Receipt rides the file.

Goal

Compute a masked temporal-SNR map and write it so the Receipt is embedded in the .nii.gz itself — no sidecar JSON, no manifest.

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")

tsnr = bold.temporal_snr(mask=mask)

out = Path(tempfile.mkdtemp()) / "tsnr.nii.gz"
nib.save(tsnr.to_nibabel(), str(out))        # Receipt embeds as NIfTI ecode-6 'comment'

out.stat().st_size > 0, tsnr.provenance.method_name
(True, 'temporal_snr')
Note

to_nibabel() + nib.save(...) is the explicit write path that embeds the Receipt. ni.write_vol(tsnr, path) routes through to_nibabel() and embeds the same Receipt, so either path lets the file itself explain which operation, input image, and mask produced the map.

What’s guaranteed

  • The mask/BOLD frame is validated inside temporal_snr before the reduction runs.
  • to_nibabel() writes exactly one neuroim/receipt/v1: extension, replacing any prior one; foreign comment extensions are untouched.

What survives the file

The eight Receipt fields, byte-identical, in a NIfTI comment extension (ecode 6), recoverable by ten lines of nibabel alone:

import json

img = nib.load(str(out))
ext = next(e for e in img.header.extensions if int(e.get_code()) == 6)
text = bytes(ext.get_content()).rstrip(b"\x00").decode("utf-8")
receipt = json.loads(text[len("neuroim/receipt/v1:"):])
sorted(receipt)
['input_space_hash',
 'mask_hash',
 'method_name',
 'n_voxels',
 'neuroim_version',
 'radius',
 'seed',
 'source_affine_hash']

Trapdoors

from neuroim.neuro_space import NeuroSpace

wrong = NeuroSpace(tuple(mask.space.dim), spacing=(9, 9, 9))
bold.temporal_snr(mask=ni.DenseNeuroVol(mask.data, wrong))
---------------------------------------------------------------------------
SpaceMismatchError                        Traceback (most recent call last)
Cell In[3], line 4
      1 from neuroim.neuro_space import NeuroSpace
      2 
      3 wrong = NeuroSpace(tuple(mask.space.dim), spacing=(9, 9, 9))
----> 4 bold.temporal_snr(mask=ni.DenseNeuroVol(mask.data, wrong))

File ~/work/neuroim-python/neuroim-python/src/neuroim/neuro_vec.py:575, in NeuroVec.temporal_snr(self, mask)
    573 mask_data = None
    574 if mask is not None:
--> 575     _verify.assert_same_space(self, mask)
    576     mask_data = np.asarray(mask.data, dtype=bool)
    578 data = np.asarray(self.to_dense().data, dtype=np.float64)

File ~/work/neuroim-python/neuroim-python/src/neuroim/verify.py:114, in assert_same_space(a, b)
    112         sa.compatible_with(sb)
    113     except ValueError as exc:
--> 114         raise SpaceMismatchError(
    115             f"assert_same_space: spatial contract mismatch — {exc}"
    116         ) from None
    117     return
    119 ha = _space_hash_of(a)

SpaceMismatchError: assert_same_space: spatial contract mismatch — NeuroSpace mismatch in affine: left=[[2.0, 0.0, 0.0, -8.0], [0.0, 2.0, 0.0, -8.0], [0.0, 0.0, 2.5, -5.0], [0.0, 0.0, 0.0, 1.0]], right=[[9.0, 0.0, 0.0, 0.0], [0.0, 9.0, 0.0, 0.0], [0.0, 0.0, 9.0, 0.0], [0.0, 0.0, 0.0, 1.0]]

A wrong-frame mask raises ValueError (spatial contract mismatch) before any numeric work — the failed call leaves the BOLD untouched.

See also

Back to top