Note
Go to the end to download the full example code.
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:
Use
laion_fmri.discoveryto talk to the S3 bucket and list subjects, ROIs, and the bucket layout.Use
laion_fmri.splitsto look at the train/test partitions that ship with the package.Use
Subjectto readsub-01 / ses-01from 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.
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)