BIDS Integration

Reading and writing Brain Imaging Data Structure datasets

Overview

TVBO reads and writes BIDS [1] datasets, so a simulation can consume an empirical connectome and publish its results where neuroimaging tooling can find them. A study is a BIDS dataset in its own right — DatasetType: "study", the type BIDS has carried since 1.10.1 — and three extension proposals reach into it: BEP034 for computational modelling [2], BEP017 for connectivity matrices, and BEP028 for provenance.

All three are still drafts. Everything below is generated from the layout record and from the code that builds the filenames, so where TVBO leaves the standard this page says which entry, and which proposal would settle it, rather than claiming a conformance it does not have.

Tutorials

Tutorial Description
Computational Modelling What BEP034 specifies, and where TVBO differs

A study is a BIDS study dataset

tvbo study init <Name> creates the dataset. Its shape comes from one record, schema/study_layout.yaml, which is also what the path resolvers, the ignore-file writers, tvbo validate study and every block on this page read — so no copy of the tree can fall out of step with the tooling.

<Study>/
  dataset_description.json      declares the dataset type, its name, and the BIDS version it was written against
  README.md                     what the study does, and how to run it
  CITATION.cff
  <Study>.yaml                  the entry recipe: the one specification a run is given
  .gitignore
  .bidsignore
  spec/                         recipe fragments the entry recipe includes, each named for what it specifies: `model-<name>_dynamics.yaml`, `atlas-<name>_network.yaml`, `exp-<id>_experiment.yaml`, `ana-<name>_analysis.yaml`, `fig-<id>_figure.yaml`
  code/                         callables the recipe references by bare `module:` name: builders, transforms, observation and analysis functions
  sourcedata/                   inputs the study did not compute
    README.md                   where each input comes from and how to obtain it
  docs/                         the report and everything it reads
    report.qmd                  the report, every number computed from the run
    _quarto.yml
    references.bib
    report.pdf                  rendered from `report.qmd`, so it is a product like any other
    .quarto/                    quarto's own cache for this project, rewritten on every render
    figures/                    render target for the declarative figures, each named `fig-<id>_desc-<descriptor>` -- the identifier the document prints and a camelCase descriptor -- so a figure's record, the script that draws it, its image and its caption all carry one name
      scripts/                  the plotting script each figure generates, kept beside what it renders
    analysis/                   what the study commits to and how it is checked, written down before the run rather than read off it: a preregistration for a study proposing something of its own, and for a reproduction the target values, the figure inventory and the backend comparison whose three files below carry it
    notes/                      the gap register and open threads, kept local so a note can be blunt
  derivatives/                  nested derivative datasets
    tvbo/                       one flat derivative dataset holding every container this study computes: `exp-<id>_model-<name>_result.h5` for a run, `ana-<name>_result.h5` for an analysis, each beside one `.yaml` sidecar: the frozen, re-runnable spec that produced it
      dataset_description.json  declares the derivative type, the generating tool, and the source dataset, which is the study root two levels up
  logs/                         run logs
  .tvbo/                        build root
    kits/                       self-contained runnable kits packaged from the spec, each with its own shards under `<kit>/shards/`, named `split-<index>`
    cache/                      cached intermediate results, keyed on their inputs

That tree is the study every kind starts from. --template selects a kind the record declares, which adds to it and may replace part of it: replication, for work that reproduces a paper, adds sourcedata/original_study/ with its fig_comparisons/ staging area and the A/B report wrapper; collection, for a study that nests other studies, swaps the single report for the document under docs/sections/ and the committed manuscript_results.yml the prose quotes its numbers from. A name the record does not declare is rejected rather than quietly selecting nothing.

Nothing in that tree is typed by hand twice. A resolver asks the record for a role and gets a path:

from tvbo.utils.study_layout import study_path

study_path("results", root=study_root)   # <root>/derivatives/tvbo
study_path("figures", root=study_root)   # <root>/docs/figures

Moving a directory is then a one-line edit to the record, and tvbo run follows it without being told.

What is tracked, and what is not

Both ignore files are generated from the record’s per-entry tracked and bids fields. The .gitignore is the study’s copyright gate: sourcedata/ holds inputs the study did not compute, so only its README is tracked and third-party or copyrighted material is documented without being redistributed.

.gitignore

# Generated by `tvbo study init` from schema/study_layout.yaml. Edit the record, not this file.
sourcedata/*
!sourcedata/README.md
docs/.quarto/
docs/figures/
docs/notes/
derivatives/*
!derivatives/tvbo/
derivatives/tvbo/*
!derivatives/tvbo/dataset_description.json
logs/
.tvbo/
docs/report.pdf

.bidsignore

# Generated by `tvbo study init` from schema/study_layout.yaml. Edit the record, not this file.
spec/
/<Study>.yaml

Under --template replication the same sourcedata/ rule also covers the A/B composites that embed the original paper’s figures, because they are staged inside the original-study directory they come from, at sourcedata/original_study/fig_comparisons/. Neither file is meant to be edited: tvbo validate study fails a study whose ignore files have drifted from the record.

Where TVBO leaves the standard

path status waits on
<Study>.yaml unsanctioned BEP034+, which would sanction a recipe at the study root. A named entry rather than a *.yaml glob, so a root YAML that later ought to be validated is not silently exempt.
spec/ unsanctioned BEP034+, which proposes spec/ as a study-root directory for the model and experiment definitions a run reads.

Two entries that look like exceptions are not. derivatives/tvbo/ is a nested derivative dataset, which BIDS sanctions and which carries its own dataset_description.json naming the study root as its source. .tvbo/ is dot-prefixed, which validators skip by convention; everything in it is a pure product of the spec and reproduces from it.

Naming a spec fragment

A fragment under spec/ carries its BIDS entities from the start, so adding a second instance renames nothing: model-<name>_dynamics.yaml, atlas-<name>_network.yaml, exp-<id>_experiment.yaml, ana-<name>_analysis.yaml, fig-<id>_figure.yaml.

The suffix is the part that says what kind of thing the file is — which every TVBO file already declares in its tvbo_class envelope. Making the two the same turns the filename into a checkable restatement of the envelope, and gives BEP034+ a suffix vocabulary to propose in place of eq/param:

suffix declares
_dynamics one Dynamics
_network one Network
_experiment one SimulationExperiment
_analysis one Analysis
_figure one Figure
_study one SimulationStudy

_result is not in that vocabulary. A result container is data rather than a specification, named by the grammar below, and its YAML sidecar takes the container’s name per BIDS rather than its own class’s — so the one file whose suffix and envelope legitimately disagree is not held to a rule it cannot keep.

What a run writes

tvbo run <Study>.yaml persists into the study’s own derivatives/tvbo/. -o relocates it and changes nothing else.

derivatives/tvbo/
├── dataset_description.json            DatasetType: derivative, GeneratedBy: tvbo
├── exp-3_model-JansenRit_result.h5     every recorded observation, one container
├── exp-3_model-JansenRit_result.yaml   the frozen, re-runnable recipe
├── exp-3_model-JansenRit_network.h5    the frozen connectome it ran on
└── ana-fcGradient_result.h5            a declared analysis's own container

One flat directory, so a cross-reference from an analysis to an experiment resolves against a single base. exp- marks a run and ana- a derived analysis. The YAML beside each container is the only sidecar: the whole recipe rather than a curated subset of it, so the two cannot drift.

[sub-{subject}_]exp-{experiment}[_model-{model}][_desc-{description}][_split-{split}]_{suffix<result>}{extension<.h5|.yaml>}
ana-{analysis}[_desc-{description}]_{suffix<result>}{extension<.h5|.yaml>}
entity identifies
sub- The subject a per-subject shard ran, when a dataset fans out over a cohort. Absent for a single-network run.
exp- The experiment id the run came from — the name of one entry under the study’s experiments:.
ana- The name of a declared analysis, in place of exp-. A file carries one or the other, never both.
model- The dynamics the run integrated, so the one fact a reader most wants to filter on is queryable rather than buried in desc-.
desc- BIDS’s free-text discriminator, for two results of the same experiment that differ in nothing a named entity captures.
split- The array-task index of one shard of a sweep, zero-padded. Present only until the shards are gathered.

The .h5 is a netCDF-flavoured xarray dataset: each recorded observation is a data variable, and any swept parameters are shared coordinates. A 15,600-cell sweep is one file rather than 15,600, and xarray.open_dataset reads it without TVBO installed.

import xarray as xr

ds = xr.open_dataset("derivatives/tvbo/exp-3_model-JansenRit_result.h5")
ds["bold"].sel(G=2.1, speed=10)

What a run records

The frozen sidecar beside a container is the recipe — what was asked for. Its provenance: block records what happened, which no recipe can say: the command, when it ran, on which machine, with which package versions, and the checksum of what came out. Written from the datamodel’s own PROV classes, so a record cannot drift from the metadata a run already carries.

# derivatives/tvbo/exp-3_model-Kuramoto_result.yaml — the recipe, and beneath it the run
provenance:
  date_created: 2026-09-03T06:10:20+00:00
  outputs: [theta]                 # read off the container, not predicted from the spec
  digest: {sha256: {value: aa61…}} # the artifact's own checksum
  activities:                      # BEP028's activity record: the command, the clock, what it consumed
    - {iri: "tvbo:exp/Demo/exp-3", command: "tvbo run Demo.yaml", started_at:, ended_at:, used: []}
  environment:                     # the machine, and one requirement per package at the version imported
    platform: macOS-26.6.2-arm64
    requirements: [{name: tvbo, version: 0.5.3},]

BEP028 spreads the same content over four files per container — prov-<label>_{act,ent,env,soft} — in a parallel prov/ tree. One Provenance on the sidecar says it once, in the file a reader already has open, and cannot fall out of step with the spec it describes: activities is the act record, environment carries env and soft together, and digest with outputs is what ent said that the spec does not.

Every field is either a pointer or read off the artifact when it is written; nothing restates a value the frozen spec already carries. That is the rule that keeps this from becoming a second, drifting copy of the recipe.

The record is written on every run, with no flag to set: the sidecar is a product of the run and never tracked, so a study cannot end up with results whose origin it did not record. It is untracked for the same reason it is truthful — it describes one machine at one moment, and committing it would assert a fact the next person’s run contradicts.

Checking a study

tvbo validate study .                        # or `-t replication` for the replication layout

The command reports, as build-failing problems rather than warnings:

  • a directory or file the record marks required that is missing
  • a dataset_description.json whose DatasetType is not what the record declares
  • a .gitignore or .bidsignore that has drifted from what the record generates
  • a spec/ fragment whose suffix disagrees with its own tvbo_class envelope
  • a filename entity that is not in the grammar above

tvbo study layout prints any of these references on demand — tree, gitignore, bidsignore — and tvbo study layout --sync <file> rewrites the generated regions of a document in place, which is how this page and the study README stay current without either restating the record.

Reading BIDS data

from tvbo import Network

net = Network.from_bids("sub-01/connectome")

Network.to_bep017 writes a connectome back out as BEP017 per-measure files, one meas-<name>_relmat.dense.tsv plus JSON sidecar per edge measure, so a connectome tvbo read or built round-trips into the form other tooling reads.

There is one export layout. A run writes derivatives/tvbo/ as described above, and that is what the rest of TVBO resolves against. An earlier exporter wrote a parallel tree of net/, ts/, eq/, coord/ and map/ directories taken from a draft no current BEP034 artifact defines; it has been removed rather than left to imply a second answer.

See Also

References

[1]
R. A. Poldrack et al., “The past, present, and future of the brain imaging data structure (BIDS),” Imaging Neuroscience, vol. 2, Feb. 2024, doi: 10.1162/imag_a_00103.
[2]
M. Schirner and P. Ritter, “BIDS extension proposal 034 (BEP034): BIDS computational model specification,” 2021, doi: 10.5281/ZENODO.7962031.