Query and read the dataset
This page contains unreviewed content created with large language model (LLM) tools.
Open the dataset created in the previous chapter, query its persistent BIDS index, and read images, sidecar metadata, and tabular data.
1 Open the dataset
Datasets open in read-only mode unless read_only=False is specified:
from bitsio import BITSZarrDataset
ds = BITSZarrDataset.open("ds000114.bits")
summary = ds.summary()
print(summary.navigator_count, "images")
print(ds.subjects())
print(ds.datatypes())14 images
['01', '02', '03', '04', '05', '06', '07', '08', '09', '10']
['anat', 'func']
subjects() and datatypes() return distinct values from the BIDS index. sessions() has a different purpose: it reads the BIDS sessions.tsv relation, which this dataset does not contain.
bitsio vs pybids
pybids.BIDSLayout builds an index from a BIDS file tree. A BITS dataset stores its index as part of the dataset; bitsio reads that index from Parquet when executing a query.
2 Query the dataset
query() accepts BIDS entity filters and returns references to matching virtual files:
from tensorbids.query import Query
print(len(ds.query(suffix="T1w")))
retest = ds.query(subject=["01", "02"], session="retest", suffix="bold")
print(retest.table.select("subject", "session", "task"))
print(len(ds.query(task=Query.REQUIRED))) # Images with a non-null task10
shape: (2, 3)
┌─────────┬─────────┬────────────────┐
│ subject ┆ session ┆ task │
│ --- ┆ --- ┆ --- │
│ str ┆ str ┆ str │
╞═════════╪═════════╪════════════════╡
│ 01 ┆ retest ┆ fingerfootlips │
│ 02 ┆ retest ┆ fingerfootlips │
└─────────┴─────────┴────────────────┘
4
Query.REQUIRED matches rows for which the named entity is present. In this dataset, all four BOLD runs define task. result.table is a Polars data frame containing the matching index rows.
For compatibility with pybids, modality="anat" filters the BIDS datatype directory. Use the appropriate entity name when filtering the BIDS mod entity.
3 Read an image and its metadata
read() returns one result for each matching file. The single-result queries below therefore unpack a one-element list:
(t1w,) = ds.read(subject="01", suffix="T1w", form="tensor")
print(t1w.shape, t1w.dtype)
(other,) = ds.read(subject="02", suffix="T1w", form="tensor")
print(other.shape)(256, 156, 256) float32
(156, 256, 256)
The resolver supports three common output forms:
"bytes"(default): reconstructed file-container bytes;"tensor": the numerical array only; and"object": a NiBabel-compatible image object.
(image,) = ds.read(subject="01", suffix="T1w", form="object")
print(image)
print(f"\nTensors match? {(t1w == image.dataobj).all()}")
<class 'nibabel.nifti1.Nifti1Image'>
data shape (256, 156, 256)
affine:
[[ 9.99131918e-01 -5.16291820e-02 1.25127016e-02 -1.25263863e+02]
[ 4.07721959e-02 1.29202044e+00 -9.81179178e-02 -7.31330109e+01]
[-8.54506902e-03 1.28044292e-01 9.95096147e-01 -1.77554291e+02]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00]]
metadata:
<class 'nibabel.nifti1.Nifti1Header'> object, endian='<'
sizeof_hdr : 348
data_type : b''
db_name : b''
extents : 0
session_error : 0
regular : b'r'
dim_info : 0
dim : [ 3 256 156 256 1 1 1 1]
intent_p1 : 0.0
intent_p2 : 0.0
intent_p3 : 0.0
intent_code : none
datatype : float32
bitpix : 32
slice_start : 0
pixdim : [1. 1. 1.2993759 1. 0.009668 0. 0.
0. ]
vox_offset : 0.0
scl_slope : nan
scl_inter : nan
slice_end : 0
slice_code : unknown
xyzt_units : 10
cal_max : 0.0
cal_min : 0.0
slice_duration : 0.0
toffset : 0.0
glmax : 0
glmin : 0
descrip : b'FSL5.0'
aux_file : b''
qform_code : scanner
sform_code : scanner
quatern_b : 0.04923562332987785
quatern_c : 0.0052719819359481335
quatern_d : 0.02015533111989498
qoffset_x : -125.26386260986328
qoffset_y : -73.13301086425781
qoffset_z : -177.55429077148438
srow_x : [ 9.9913192e-01 -5.1629182e-02 1.2512702e-02 -1.2526386e+02]
srow_y : [ 4.0772196e-02 1.2920204e+00 -9.8117918e-02 -7.3133011e+01]
srow_z : [-8.5450690e-03 1.2804429e-01 9.9509615e-01 -1.7755429e+02]
intent_name : b''
magic : b'n+1'
Tensors match? True
nibabel users
For NIfTI data, form="object" returns a Nifti1Image whose array proxy reads values on demand.
Use a query result reference to read the fully inherited JSON sidecar:
(ref,) = ds.query(subject="01", session="test", suffix="bold")
sidecar = ds.sidecar(ref)
print(sidecar["RepetitionTime"], sorted(sidecar))2.5 ['EchoTime', 'FlipAngle', 'RepetitionTime', 'SliceTiming', 'TaskName']
The BIDS source adapter resolves sidecar inheritance during ingestion. The RepetitionTime shown here was inherited from the source dataset’s top-level task-fingerfootlips_bold.json.
A BITS navigator is the typed operation graph that tells a resolver how to construct one virtual BIDS file. Inspect one of the stored navigators:
Fidelity contract level: 3
sub-01/ses-retest/func/sub-01_ses-retest_task-fingerfootlips_bold.nii
kind: reconstitution contract: lossless / int16 nodes: 2
terminal: surface
nodes (topological order):
read read_tensor (source)
surface materialize <- read [terminal]
The graph lists each node’s identifier, operator, and inputs. In this example, read loads an array from the tensor store and surface materializes the result as a file container.
The current NIfTI ingestion path does not retain the source gzip stream. Container-byte reads reconstruct uncompressed NIfTI bytes even when the virtual target path ends in .nii.gz.
4 Read a table
Read the imported BIDS participants.tsv relation:
print(ds.participants())shape: (10, 2)
┌─────────┬───────────────┐
│ subject ┆ dominant_hand │
│ --- ┆ --- │
│ str ┆ str │
╞═════════╪═══════════════╡
│ 01 ┆ left │
│ 02 ┆ right │
│ 03 ┆ right │
│ 04 ┆ right │
│ 05 ┆ right │
│ 06 ┆ left │
│ 07 ┆ right │
│ 08 ┆ right │
│ 09 ┆ right │
│ 10 ┆ left │
└─────────┴───────────────┘
This subset contains ten participant rows and one phenotype column, dominant_hand.