Stack subjects with frame validation at the API surface.
Goal
Concatenate per-subject 4-D BOLD runs into one series for a group reduction, with the shared spatial frame validated before the stack happens — not discovered after a wrong-frame subject has polluted the mean.
Code
from pathlib import Pathimport syssys.path.insert(0, str(Path.cwd().parents[1] /"src"))import numpy as npimport neuroim as nirepo = Path.cwd().parents[1] if Path.cwd().name =="cookbook"else Path.cwd()# Two "subjects" in the same frame (the fixture stands in for two runs).subj_a = ni.read_image(str(repo /"golden_tests/fixtures/tiny_bold.nii.gz"))subj_b = 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")merged = subj_a.concat(subj_b) # also: ni.concat(subj_a, subj_b)roi = ni.ROICoords(np.argwhere(np.asarray(mask.data)), space=mask.space)group_mean_ts = merged.series_roi(roi).values.mean(axis=1)merged.shape, group_mean_ts.shape
((8, 8, 4, 20), (20,))
concat is available as the instance method a.concat(*rest) and the module function ni.concat(*vecs).
What’s guaranteed
Every pair’s spatial frame is checked before concatenation. A subject in a different frame never reaches the stack.
series_roi then re-validates the mask frame against the merged series — the contract holds end to end.
What survives the file
merged.series_roi(roi) returns an ROIExtractionResult with a Receipt; the input_space_hash reflects the subjects’ shared frame. If you build a group map and to_nibabel() it, that hash rides the file.
A frame-mismatched subject raises ValueError: spatial contract mismatch: NeuroVecs must have same spatial dimensions and affine; …. The failure is at the API surface, before any array is stacked.