Querying the Dataset

A dataset is much easier to work with once it is clear what is in it: which subjects exist, which ROIs ship per subject, which train/test splits are bundled, and so on. This example introduces the two discovery APIs the package exposes for that purpose, and then shows how to inspect a single subject’s on-disk data once a target has been picked.

The plan is to start at the dataset-wide level (no downloads needed) and then zoom in on a single subject. Concretely:

  1. Use laion_fmri.discovery to talk to the S3 bucket and list subjects, ROIs, and the bucket layout.

  2. Use laion_fmri.splits to look at the train/test partitions that ship with the package.

  3. Use Subject to read sub-01 / ses-01 from disk: trial info, betas, ROI data, stimulus metadata.

The bottom half reuses the sub-01 / ses-01 data that Quick Start populates, so plot_01 should be run first if plot_03 is being run in isolation. The subject to inspect is picked on the line below:

SUBJECT = "sub-01"

Initialize a data directory

The discovery and split helpers used in the cells below do not read anything from disk (they talk to the bucket or load bundled metadata), so strictly speaking dataset_initialize is not needed for those calls. It is still set up here for two reasons: it makes the script consistent with the other gallery examples (same data directory, same data on disk), and any follow-up download(...) call needs a destination.

import os

from laion_fmri.config import dataset_initialize

# define and initialize the data directory
data_dir = os.environ.get(
    "LAION_FMRI_EXAMPLE_DATA_DIR",
    os.path.join(os.getcwd(), "laion_fmri_quickstart"),
)
os.makedirs(data_dir, exist_ok=True)
dataset_initialize(data_dir)

from laion_fmri.discovery import (
    describe,
    get_rois,
    get_subjects,
    inspect_bucket,
)

Top-level summary

A good starting point at the beginning of any session is describe(). It prints a one-screen overview (bucket name, subject count, the first subject’s ROI list) and is the quickest way to confirm that the bucket is actually reachable from the current network. If it returns without error, every other discovery call in this example will work too.

describe()
LAION-fMRI Dataset
  Bucket:    s3://laion-fmri
  Subjects:  5 (sub-01, sub-03, sub-05, sub-06, sub-07)
  ROIs:      EBA, FBA, FFA1, FFA2, IPCS, IPS0, LO1, LO2, MPA, MST, MT, OFA, OPA, PPA, SPCS, TO1, TO2, V1d, V1v, V2d, V2v, V3A, V3B, V3d, V3v, VO1, VO2, VWFA1, VWFA2, hV4, laionEVC, laiondorsal, laiongeneral, laionlateral, laionventral, lobjects, mfswords, pSTSfaces, pSTSwords, vobjects

Subjects in the bucket

Picking the analysis cohort is usually the next decision a user makes. get_subjects answers that question. It lists every subject the bucket exposes, including those whose data is only partially uploaded. The count therefore matches the dataset’s published size rather than just the subjects with complete data, worth remembering when filtering down to a clean subset.

print(f"All subjects: {get_subjects()}")
print(f"Querying subject: {SUBJECT}")
All subjects: ['sub-01', 'sub-03', 'sub-05', 'sub-06', 'sub-07']
Querying subject: sub-01

ROI queries: specific / category / all

Most downstream analyses scope themselves to a subset of ROIs rather than the whole brain, so the next thing worth knowing is which ROIs ship per subject and how they are grouped.

The dataset organizes ROIs into eight categories on the bucket (body-, face-, place-areas, …). get_rois is the accessor for them. With no filter it returns the full inventory; with category= it returns one functional family at a time, which is the most common pattern when picking a voxel mask for an analysis.

# define the ROI categories on the bucket
ROI_CATEGORIES = (
    "body", "character", "face", "laion",
    "motion", "object", "place", "retinotopy",
)

# list all ROIs and then iterate per category
print(f"All ROIs ({len(get_rois(SUBJECT))}):")
print(get_rois(SUBJECT))
print()
for cat in ROI_CATEGORIES:
    rois = get_rois(SUBJECT, category=cat)
    print(f"{cat}: {rois}")
All ROIs (40):
['EBA', 'FBA', 'FFA1', 'FFA2', 'IPCS', 'IPS0', 'LO1', 'LO2', 'MPA', 'MST', 'MT', 'OFA', 'OPA', 'PPA', 'SPCS', 'TO1', 'TO2', 'V1d', 'V1v', 'V2d', 'V2v', 'V3A', 'V3B', 'V3d', 'V3v', 'VO1', 'VO2', 'VWFA1', 'VWFA2', 'hV4', 'laionEVC', 'laiondorsal', 'laiongeneral', 'laionlateral', 'laionventral', 'lobjects', 'mfswords', 'pSTSfaces', 'pSTSwords', 'vobjects']

body: ['EBA', 'FBA']
character: ['VWFA1', 'VWFA2', 'mfswords', 'pSTSwords']
face: ['FFA1', 'FFA2', 'OFA', 'pSTSfaces']
laion: ['laionEVC', 'laiondorsal', 'laiongeneral', 'laionlateral', 'laionventral']
motion: ['MST', 'MT']
object: ['lobjects', 'vobjects']
place: ['MPA', 'OPA', 'PPA']
retinotopy: ['IPCS', 'IPS0', 'LO1', 'LO2', 'SPCS', 'TO1', 'TO2', 'V1d', 'V1v', 'V2d', 'V2v', 'V3A', 'V3B', 'V3d', 'V3v', 'VO1', 'VO2', 'hV4']

Bucket diagnostic listing

When the discovery results look unexpected (a subject that should be there is missing, an ROI count is off), it helps to look at the bucket layout itself. inspect_bucket is the low-level helper for that. It prints the immediate top-level prefixes plus a count of subject directories under each derivative tree, so structural quirks (missing derivatives, an extra prefix) show up at a glance.

inspect_bucket()
Bucket: s3://laion-fmri
Top-level prefixes (2):
  derivatives/
  stimuli/
derivatives/glmsingle-tedana/: 5 entries, 5 sub-* entries
derivatives/rois/: 5 entries, 5 sub-* entries
derivatives/freesurfer/: 5 entries, 5 sub-* entries
derivatives/anatomical/: 5 entries, 5 sub-* entries

Bundled train/test splits (no download required)

A lot of modeling work hinges on which trials count as train and which as test. To make that choice reproducible, and to make published baselines directly comparable, the package ships a set of pre-computed train/test partitions over the stimulus set under laion_fmri.splits.

The data backing these splits is bundled with the package, so the helpers below work offline; no bucket round-trip and no subject data needs to be on disk.

from laion_fmri.splits import (
    get_train_test_ids,
    list_ood_types,
    list_pools,
    list_splits,
    load_split,
)

# list the available pools, splits, and OOD types
print(f"Pools:     {list_pools()}")
print(f"Splits:    {list_splits()}")
print(f"OOD types: {list_ood_types()}")
Pools:     ['shared', 'sub-01', 'sub-03', 'sub-05', 'sub-06', 'sub-07']
Splits:    ['random_0', 'random_1', 'random_2', 'random_3', 'random_4', 'cluster_k5_0', 'cluster_k5_1', 'cluster_k5_2', 'cluster_k5_3', 'cluster_k5_4', 'tau', 'ood']
OOD types: ['cropped', 'gabor', 'gaudy', 'illusion-classic', 'illusion-natural', 'relations', 'selfmade', 'shape', 'unusual']

Inspect one split

Once a split has been picked, two helpers cover the common uses. load_split(name, pool=...) returns a Split object describing the split’s sizes and family, useful when the analysis needs to log what was used. get_train_test_ids is the matching shortcut. It returns the actual train and test ID lists in one call, ready to be used to filter the metadata table or beta arrays downstream.

# load and inspect one split
split = load_split("random_0", pool="shared")
print(f"Split:    {split.name}")
print(f"Pool:     {split.pool}")
print(f"Family:   {split.split_family}")
print(f"n_train:  {split.n_train}")
print(f"n_test:   {split.n_test}")

# fetch the matching train / test ID lists
train_ids, test_ids = get_train_test_ids("random_0", pool="shared")
print(f"Loaded:   {len(train_ids)} train / {len(test_ids)} test ids")
Split:    random_0
Pool:     shared
Family:   random
n_train:  897
n_test:   224
Loaded:   897 train / 224 test ids

OOD splits with a type filter

A particularly useful split family is ood, since it partitions the test set by stimulus category, so models can be evaluated on the categories the training data did not contain. For focused analyses on one category at a time, the ood_types= argument restricts which category labels are kept on the test side; everything else is dropped.

# restrict the OOD test set to a single category
_, test_shape = get_train_test_ids(
    "ood", pool="shared", ood_types=["shape"],
)
print(f"OOD shape only:  test ids = {len(test_shape)}")
OOD shape only:  test ids = 82

Per-subject queries that need local data

The cells above answer “what is available?” without touching the disk. Once the analysis commits to a specific subject, the questions shift to “what does this subject’s data actually look like?”, such as which sessions, which runs, which voxels, which stimuli. That second class of question is answered by Subject, and the methods read on-disk files rather than the bucket.

The cells below reuse the sub-01 / ses-01 data that Quick Start already downloaded into the shared data directory; if plot_03 is being run in isolation, run plot_01 first (or call download(subject="sub-01", ses="ses-01") directly).

from laion_fmri.subject import load_subject

# load the subject
sub = load_subject(SUBJECT)

# list the sessions present on disk
print(f"Sessions on disk: {sub.get_sessions()}")

# fetch the trial info (runs, repetitions, stimulus labels);
# columns include: session, run, beta_index, label
trials = sub.get_trial_info(session="ses-01")
print(f"Trial-info columns: {trials.columns.tolist()}")
print(f"Runs in ses-01:     {trials['run'].unique()}")
print(f"Trials in ses-01:   {len(trials)}")

# load single-trial betas with the multi-level ROI grammar.
# Shapes are (n_trials, n_voxels), voxel count depends on
# the ROI (or ROI union for category / 'all' queries).
betas_one = sub.get_betas(session="ses-01", roi="FFA1")
betas_face = sub.get_betas(session="ses-01", roi="face")
betas_all = sub.get_betas(session="ses-01", roi="all")
print(f"FFA1 betas:      {betas_one.shape}")
print(f"face union:      {betas_face.shape}")
print(f"all-ROI union:   {betas_all.shape}")

# load multi-format ROI data. ``roi["FFA1"]`` is a nested dict:
# {
#   "volume": <1-D bool>,
#   "gii": {"hemi-L": {"func.gii": ..., "label": ...},
#           "hemi-R": {...}},
# }
roi = sub.get_roi_data("FFA1", format="all", hemi="all")
print(f"ROI keys:        {list(roi.keys())}")
print(f"FFA1 formats:    {list(roi['FFA1'].keys())}")
print(f"FFA1 gii hemis:  {list(roi['FFA1']['gii'].keys())}")
Sessions on disk: ['ses-01']
Trial-info columns: ['session', 'run', 'beta_index', 'label']
Runs in ses-01:     [ 1  2  3  4  5  6  7  8  9 10 11 12]
Trials in ses-01:   1044
FFA1 betas:      (1044, 222)
face union:      (1044, 1100)
all-ROI union:   (1044, 16569)
ROI keys:        ['FFA1']
FFA1 formats:    ['volume', 'gii']
FFA1 gii hemis:  ['hemi-L', 'hemi-R']

Cross-subject discovery

The same discovery helpers can be applied at scale by looping over the full subject list. This is the right pattern for sanity checks across the cohort, for instance confirming that an ROI the analysis depends on actually exists for every subject. The cell below uses it to print per-subject ROI totals and face-area counts; ROI counts can differ across subjects, so a quick scan like this catches the difference before it surfaces in a downstream model.

# count total / face ROIs per subject
for sub_id in get_subjects():
    n_face = len(get_rois(sub_id, category="face"))
    n_total = len(get_rois(sub_id))
    print(f"  {sub_id}: {n_total:>3} ROIs total, {n_face} face")
sub-01:  40 ROIs total, 4 face
sub-03:  43 ROIs total, 6 face
sub-05:  41 ROIs total, 6 face
sub-06:  41 ROIs total, 5 face
sub-07:  41 ROIs total, 6 face

Stimulus metadata

The final question is usually about the stimuli themselves: which image was shown on which trial, which trials are shared across subjects, which session a given trial belongs to. The Subject.metadata property is the answer, a pandas.DataFrame with one row per single-trial beta, indexed by global trial index (0 .. n_total_trials-1).

The columns combine the per-session events TSV with derived fields like image_name, session, session_trial, stim_idx, and unique_or_shared, which makes it the natural pivot table for aligning betas, stimuli, and splits. The same table is used in Loading Data to pair betas with images.

This reads sub-01 from the shared data directory that Quick Start populates; if plot_03 is being run in isolation, run plot_01 first (or call download(...) directly).

from laion_fmri.subject import load_subject

# load the subject and inspect the metadata table
sub = load_subject(SUBJECT)
df = sub.metadata
print(df.head())
print(f"Total trials: {len(df)}")
shared = (df["unique_or_shared"] == "shared").sum()
print(f"Shared:       {shared}")
print(f"Per session:  {df['session'].value_counts().to_dict()}")
  session  run  beta_index  ... stim_idx  unique_or_shared dataset
0  ses-01    1           0  ...    22187            unique   LAION
1  ses-01    1           1  ...    19600            unique   LAION
2  ses-01    1           2  ...      259            shared   LAION
3  ses-01    1           3  ...    17805            unique   LAION
4  ses-01    1           4  ...    13346            unique   LAION

[5 rows x 9 columns]
Total trials: 1044
Shared:       469
Per session:  {'ses-01': 1044}

Total running time of the script: (6 minutes 56.043 seconds)

Gallery generated by Sphinx-Gallery