import os
import json
import numpy as np
# Import TVB-O components
from tvbo.knowledge.simulation.network import Coupling
from tvbo.knowledge import Integrator
from tvbo import Network,SimulationExperiment,DynamicsBIDS Export for TVB Simulations
Brain Imaging Data Structure (BEP034 v1.0.0) Integration
Introduction
This notebook demonstrates how to export simulation results from The Virtual Brain (TVB) to the Brain Imaging Data Structure (BIDS) format, specifically following the BEP034 Computational Modeling Extension v1.0.0.
What is BIDS?
The Brain Imaging Data Structure (BIDS) is a standard for organizing and describing neuroimaging data. It provides:
- Standardized file naming conventions
- JSON sidecar files for metadata
- Interoperability across neuroimaging tools
- Reproducibility of analyses
BEP034: Computational Modeling Extension
BEP034 extends BIDS to support computational modeling data with a specific directory structure:
net/: Network connectivity files (weights, distances)ts/: Simulated time series dataeq/: Model equations (tvbo format)coord/: Region coordinatesmap/: Mapping filescode/: Analysis code
Key features: - File naming with id-<hash> suffix based on JSON sidecar hash - Proper entity-value pairs in filenames - Full provenance tracking
For more information, see: - BIDS Specification - BEP034 Computational Modeling
Setup
Create a Simulation Experiment
First, let’s set up a simulation experiment using the Wilson-Cowan model with a sample connectome.
from tvbo.datamodel.tvbo_datamodel import CouplingInput, Parameter
# | label: create-experiment
# Load a sample connectome
connectome = Network(number_of_nodes=3)
# Define the dynamics model
dynamics = Dynamics.from_ontology("Generic2dOscillator")
dynamics.coupling_inputs['c_glob'] = CouplingInput(name='c_glob')
dynamics.coupling_terms['c_glob'] = Parameter(name='c_glob')
# Define coupling function
coupling = Coupling(name="Linear")
# Create the simulation experiment
experiment = SimulationExperiment(
label="BIDS Demo Simulation",
description="Example simulation for BIDS export demonstration",
dynamics=dynamics,
coupling=coupling,
network=connectome,
integration=Integrator(
method="Euler",
step_size=0.1,
duration=100.0,
),
)Run the Simulation
ts = experiment.run(format="pyrates")
print(f"TimeSeries shape: {ts.shape}")
print(f"Time points: {len(ts.time)}")
print(f"State variables: {ts.variables_labels}")
print(f"Sample period: {ts.sample_period} {ts.sample_period_unit}")Compilation Progress
--------------------
(1) Translating the circuit template into a networkx graph representation...
...finished.
(2) Preprocessing edge transmission operations...
...finished.
(3) Parsing the model equations into a compute graph...
...finished.
Model compilation was finished.
Simulation Progress
-------------------
(1) Generating the network run function...
(2) Processing output variables...
...finished.
(3) Running the simulation...
...finished after 0.013792042000204674s.
TimeSeries shape: (1000, 2, 3, 1)
Time points: 1000
State variables: ['V' 'W']
Sample period: 0.1 ms
Visualize Results
import matplotlib.pyplot as plt
# Plot time series for a few regions
fig, ax = plt.subplots(figsize=(12, 4))
ts.get_state_variable('V').plot(ax=ax)
ax.set_title("Simulated Brain Activity (First Variable)")
plt.tight_layout()
plt.show()
Export to BIDS Format (BEP034)
Now we’ll export the simulation results to BIDS format. The to_bids() method creates a complete BIDS-compliant dataset structure following BEP034 v1.0.0.
Output Formats
Three formats are supported for time series data:
| Format | Extension | Splits by State | Use Case |
|---|---|---|---|
cifti (default) |
.ptseries.nii |
Yes | Neuroimaging tools, named parcels |
tsv |
.tsv |
Yes | Simple tabular data |
h5 |
.h5 |
No | Full dimensionality, parameter sweeps |
Method 1: Export via TimeSeries
# Use the derivatives folder for BIDS output
bids_output = os.path.join(os.path.dirname(__file__) if '__file__' in dir() else '.', 'derivatives', 'tvbo')
os.makedirs(bids_output, exist_ok=True)
# Export using the TimeSeries to_bids method
# The ts entity (ts-V, ts-W) comes from state variable names automatically
# suffix='State' means raw neural state (default), use 'BOLD' for fMRI observation
output_path = ts.to_bids(
output_dir=bids_output,
subject="01",
session="simulation",
description="wilsoncowan",
run=1,
suffix="State", # 'State' for raw, 'BOLD' for fMRI, 'EEG' for EEG
experiment=experiment, # Include experiment for full provenance
)
print(f"BIDS dataset created at: {output_path}")BIDS dataset created at: ./derivatives/tvbo
Method 1b: Export as HDF5 (preserving full dimensions)
For data with multiple dimensions (e.g., parameter sweeps, modes), use HDF5:
# HDF5 preserves full dimensionality without splitting by state variable
# Ideal for: (sweep, time, state, region, mode) or other multi-dimensional data
output_path_h5 = ts.to_bids(
output_dir=bids_output,
subject="01",
timeseries_format="h5", # Keep all dimensions intact
experiment=experiment,
)Method 2: Export via SimulationExperiment
Alternatively, you can export directly from the experiment (which will run the simulation if needed):
# Use derivatives folder for experiment export
bids_output2 = os.path.join(os.path.dirname(__file__) if '__file__' in dir() else '.', 'derivatives', 'tvbo_experiment')
os.makedirs(bids_output2, exist_ok=True)
# Export using the experiment's to_bids method
output_path2 = experiment.to_bids(
output_dir=bids_output2,
subject="02",
description="simulation",
timeseries=ts, # Use pre-computed timeseries
)
print(f"BIDS dataset created at: {output_path2}")BIDS dataset created at: ./derivatives/tvbo_experiment
Inspect the BEP034 Structure
Let’s examine the created BIDS dataset structure. BEP034 organizes data into specific directories:
def print_tree(path, prefix=""):
"""Print directory tree structure."""
entries = sorted(os.listdir(path))
for i, entry in enumerate(entries):
is_last = i == len(entries) - 1
current_prefix = "└── " if is_last else "├── "
print(f"{prefix}{current_prefix}{entry}")
entry_path = os.path.join(path, entry)
if os.path.isdir(entry_path):
next_prefix = prefix + (" " if is_last else "│ ")
print_tree(entry_path, next_prefix)
print("BIDS Dataset Structure (BEP034 v1.0.0):")
print("=" * 50)
print_tree(bids_output)BIDS Dataset Structure (BEP034 v1.0.0):
==================================================
├── dataset_description.json
├── participants.tsv
└── sub-01
├── eq
│ ├── eq-dynamics_desc-dynamics_id-4cea7a22.json
│ └── eq-dynamics_desc-dynamics_id-7517c9e5.json
├── ses-simulation
│ ├── eq
│ │ ├── eq-dynamics_desc-wilsoncowan_id-69850551.json
│ │ ├── eq-dynamics_desc-wilsoncowan_id-6c8ff3dc.json
│ │ └── eq-dynamics_desc-wilsoncowan_id-bc5c9355.json
│ └── ts
│ ├── sub-01_ses-simulation_desc-wilsoncowan_run-1_ts-V_State.json
│ ├── sub-01_ses-simulation_desc-wilsoncowan_run-1_ts-V_State.ptseries.nii
│ ├── sub-01_ses-simulation_desc-wilsoncowan_run-1_ts-W_State.json
│ └── sub-01_ses-simulation_desc-wilsoncowan_run-1_ts-W_State.ptseries.nii
└── ts
├── sub-01_desc-dynamics_ts-all_State.h5
└── sub-01_desc-dynamics_ts-all_State.json
The expected structure follows BEP034:
derivatives/tvbo/
├── dataset_description.json
├── participants.tsv
└── sub-01/
└── ses-simulation/
├── net/
│ ├── sub-01_ses-simulation_desc-wilsoncowan_run-01_net-weights_id-<hash>.tsv
│ ├── sub-01_ses-simulation_desc-wilsoncowan_run-01_net-weights_id-<hash>.json
│ ├── sub-01_ses-simulation_desc-wilsoncowan_run-01_net-distances_id-<hash>.tsv
│ └── sub-01_ses-simulation_desc-wilsoncowan_run-01_net-distances_id-<hash>.json
├── ts/
│ ├── sub-01_ses-simulation_desc-wilsoncowan_run-01_ts-V_State.ptseries.nii
│ ├── sub-01_ses-simulation_desc-wilsoncowan_run-01_ts-V_State.json
│ ├── sub-01_ses-simulation_desc-wilsoncowan_run-01_ts-W_State.ptseries.nii
│ └── sub-01_ses-simulation_desc-wilsoncowan_run-01_ts-W_State.json
├── eq/
│ └── eq-generic2doscillator_desc-wilsoncowan_id-<hash>.json
└── coord/ (if coordinates available)
├── sub-01_ses-simulation_desc-wilsoncowan_coord-centres_id-<hash>.tsv
└── sub-01_ses-simulation_desc-wilsoncowan_coord-centres_id-<hash>.json
Naming convention for time series: - ts-V / ts-W: State variable names (from model equations) - ts-Diff: Derived output name (e.g., output: Diff: rhs: V-W) - State: Raw neural output (no observation model) - BOLD: Output convolved with HRF (fMRI observation) - EEG: Output with EEG forward model
Examples: - ts-V_State.ptseries.nii - Raw state variable V - ts-Diff_BOLD.ptseries.nii - Derived output (V-W) convolved with HRF
Dataset Description
# Read and display the dataset_description.json
desc_path = os.path.join(bids_output, "dataset_description.json")
with open(desc_path, "r") as f:
dataset_desc = json.load(f)
print("dataset_description.json:")
print(json.dumps(dataset_desc, indent=2))dataset_description.json:
{
"Name": "TVB Simulation Output",
"BIDSVersion": "1.9.0",
"DatasetType": "derivative",
"GeneratedBy": [
{
"Name": "tvbo",
"Version": "0.1.0",
"Description": "The Virtual Brain Ontology and Simulation Framework",
"CodeURL": "https://github.com/the-virtual-brain/tvb-ontology"
}
],
"BEP034Version": "1.0.0"
}
Time Series Sidecar (ts/ directory)
# Find and display the JSON sidecar from ts/ directory
import glob
sidecar_files = glob.glob(os.path.join(bids_output, "**", "ts", "*.json"), recursive=True)
if sidecar_files:
with open(sidecar_files[0], "r") as f:
sidecar = json.load(f)
print("Time Series Sidecar Metadata:")
print(json.dumps(sidecar, indent=2))Time Series Sidecar Metadata:
{
"Description": "Simulated time series - all state variables",
"Format": "HDF5",
"Shape": [
1000,
2,
3,
1
],
"Dimensions": [
"Time",
"State Variable",
"Space",
"Mode"
],
"DimensionLabels": {
"State Variable": [
"V",
"W"
],
"Region": [
"node_0",
"node_1",
"node_2"
]
},
"SamplingFrequency": 10000.0,
"SamplingPeriod": 0.1,
"SamplingPeriodUnits": "ms",
"StartTime": 0.0,
"Units": "a.u.",
"GeneratedAt": "2026-01-09T12:41:57.866948",
"Provenance": {
"Model": "Generic2dOscillator - 12 parameters and 2 state variables",
"Integrator": "Integrator({\n 'time_scale': 'ms',\n 'duration': 100.0,\n 'method': 'Euler',\n 'step_size': 0.1,\n 'transient_time': 0.0,\n 'scipy_ode_base': False,\n 'number_of_stages': 1,\n 'update_expression': DerivedVariable({\n 'name': 'dX',\n 'equation': Equation({'lhs': 'X_{t+1}', 'rhs': 'dX0 * dt', 'latex': False}),\n 'conditional': False\n }),\n 'delayed': False\n})",
"StepSize": 0.1,
"GeneratedAt": "2026-01-09T12:41:57.866923",
"Software": "tvbo"
},
"StateVariables": [
"V",
"W"
],
"Datasets": {
"/data": "Time series data with shape (1000, 2, 3, 1)",
"/time": "Time array",
"/labels/*": "Labels for each dimension"
}
}
Network Connectivity (net/ directory)
# Find network files
net_files = glob.glob(os.path.join(bids_output, "**", "net", "*.json"), recursive=True)
if net_files:
with open(net_files[0], "r") as f:
net_sidecar = json.load(f)
print("Network Sidecar Metadata:")
print(json.dumps(net_sidecar, indent=2))Participants File
import pandas as pd
participants_path = os.path.join(bids_output, "participants.tsv")
if os.path.exists(participants_path):
participants = pd.read_csv(participants_path, sep="\t")
print("participants.tsv:")
print(participants)participants.tsv:
participant_id
0 sub-01
BIDS Validation
To validate your BIDS dataset, you can use the BIDS Validator:
# Install bids-validator
npm install -g bids-validator
# Run validation
bids-validator /path/to/your/bids/datasetOr use the online validator: https://bids-standard.github.io/bids-validator/
BEP034 Specific Features
The export includes several BEP034-specific features:
1. Model Equations (eq/ directory)
Model equations are exported in tvbo format (JSON):
# Check for equation files
eq_files = glob.glob(os.path.join(bids_output, "**", "eq", "*.json"), recursive=True)
if eq_files:
print(f"Model equation file: {os.path.basename(eq_files[0])}")
with open(eq_files[0], "r") as f:
eq_data = json.load(f)
print("\nEquation metadata:")
print(json.dumps(eq_data, indent=2))
else:
print("Note: Equation export requires experiment to be provided.")Model equation file: eq-dynamics_desc-dynamics_id-7517c9e5.json
Equation metadata:
{
"Description": "Neural mass model equations - Dynamics",
"ModelType": "Dynamics",
"Format": "tvbo",
"GeneratedAt": "2026-01-09T12:41:57.869350",
"Parameters": {
"number_of_modes": 1
}
}
2. Connectivity Data (net/ directory)
Structural connectivity matrices are exported as TSV files with proper BEP034 naming:
*_net-weights_id-<hash>.tsv: Connection weights*_net-distances_id-<hash>.tsv: Tract lengths/distances
# Check for connectivity files in net/ directory
weights_files = glob.glob(os.path.join(bids_output, "**", "net", "*net-weights*.tsv"), recursive=True)
distances_files = glob.glob(os.path.join(bids_output, "**", "net", "*net-distances*.tsv"), recursive=True)
print(f"Weights files: {len(weights_files)}")
for f in weights_files:
print(f" - {os.path.basename(f)}")
print(f"\nDistances files: {len(distances_files)}")
for f in distances_files:
print(f" - {os.path.basename(f)}")Weights files: 0
Distances files: 0
3. ID-based File Naming
BEP034 uses hash-based IDs computed from the JSON sidecar content:
# Show example of ID pattern
import re
ts_files = glob.glob(os.path.join(bids_output, "**", "ts", "*.tsv"), recursive=True)
if ts_files:
filename = os.path.basename(ts_files[0])
print(f"Example filename: {filename}")
# Extract ID
match = re.search(r'id-([a-f0-9]+)', filename)
if match:
print(f"Extracted ID hash: {match.group(1)}")
print("\nThe ID is computed as SHA256 hash of the JSON sidecar content.")4. Provenance Tracking
The JSON sidecar includes simulation provenance:
if sidecar_files:
with open(sidecar_files[0], "r") as f:
sidecar = json.load(f)
if "SimulationProvenance" in sidecar:
print("Simulation Provenance:")
print(json.dumps(sidecar["SimulationProvenance"], indent=2))Complete Example: Multiple Subjects
Here’s how to create a multi-subject BIDS dataset:
# Create a multi-subject dataset in derivatives
multi_bids = os.path.join(os.path.dirname(__file__) if '__file__' in dir() else '.', 'derivatives', 'tvbo_multi')
os.makedirs(multi_bids, exist_ok=True)
# Simulate and export for multiple "subjects" (different parameter settings)
for i, coupling_strength in enumerate([0.01, 0.02, 0.03]):
# Modify experiment parameters
modified_exp = experiment.copy()
modified_exp.coupling.parameters['a'].value = coupling_strength
# Run simulation
ts_i = modified_exp.run(format="jax")
# Export with subject number
# ts entity comes from state variables (V, W), suffix indicates observation type
ts_i.to_bids(
output_dir=multi_bids,
subject=f"{i+1:02d}",
description=f"coupling{int(coupling_strength*100):02d}",
suffix="State", # Raw neural state (or 'BOLD' for fMRI observation)
experiment=modified_exp,
)
print(f"Exported subject {i+1:02d} with coupling={coupling_strength}")
print("\nMulti-subject dataset structure:")
print_tree(multi_bids)Exported subject 01 with coupling=0.01
Exported subject 02 with coupling=0.02
Exported subject 03 with coupling=0.03
Multi-subject dataset structure:
├── dataset_description.json
├── participants.tsv
├── sub-01
│ ├── eq
│ │ ├── eq-dynamics_desc-coupling01_id-2472db5a.json
│ │ ├── eq-dynamics_desc-coupling01_id-50901b1c.json
│ │ └── eq-dynamics_desc-coupling01_id-7802fa83.json
│ └── ts
│ ├── sub-01_desc-coupling01_ts-V_State.json
│ ├── sub-01_desc-coupling01_ts-V_State.ptseries.nii
│ ├── sub-01_desc-coupling01_ts-W_State.json
│ └── sub-01_desc-coupling01_ts-W_State.ptseries.nii
├── sub-02
│ ├── eq
│ │ ├── eq-dynamics_desc-coupling02_id-3c2c2239.json
│ │ ├── eq-dynamics_desc-coupling02_id-e69041e2.json
│ │ └── eq-dynamics_desc-coupling02_id-ec3dc1d9.json
│ └── ts
│ ├── sub-02_desc-coupling02_ts-V_State.json
│ ├── sub-02_desc-coupling02_ts-V_State.ptseries.nii
│ ├── sub-02_desc-coupling02_ts-W_State.json
│ └── sub-02_desc-coupling02_ts-W_State.ptseries.nii
└── sub-03
├── eq
│ ├── eq-dynamics_desc-coupling03_id-555ee0ca.json
│ ├── eq-dynamics_desc-coupling03_id-a67a5946.json
│ └── eq-dynamics_desc-coupling03_id-bf2e4fa2.json
└── ts
├── sub-03_desc-coupling03_ts-V_State.json
├── sub-03_desc-coupling03_ts-V_State.ptseries.nii
├── sub-03_desc-coupling03_ts-W_State.json
└── sub-03_desc-coupling03_ts-W_State.ptseries.nii
Loading from BIDS (Round-Trip)
You can load a previously exported BIDS dataset back into a SimulationExperiment using the from_bids() class method. This enables full round-trip workflows.
Minimal Example
# Export to BIDS (HDF5 for 100% fidelity)
round_trip_dir = os.path.join('.', 'derivatives', 'tvbo_roundtrip')
os.makedirs(round_trip_dir, exist_ok=True)
ts.to_bids(
output_dir=round_trip_dir,
subject="01",
timeseries_format="h5", # Preserves full dimensionality
experiment=experiment,
)
# Load back from BIDS
loaded_exp, loaded_ts = SimulationExperiment.from_bids(
bids_dir=round_trip_dir,
subject="01",
)
print(f"Original shape: {ts.shape}")
print(f"Loaded shape: {loaded_ts.shape}")
print(f"Model: {loaded_exp.local_dynamics.name}")Original shape: (1000, 2, 3, 1)
Loaded shape: (1000, 2, 3, 1)
Model: Dynamics
Verifying Reproducibility with np.isclose
# Compare original and loaded data
original_data = ts.data
loaded_data = loaded_ts.data
# Check if arrays are close (within numerical tolerance)
all_close = np.allclose(original_data, loaded_data, rtol=1e-5, atol=1e-8)
max_diff = np.max(np.abs(original_data - loaded_data))
print(f"Arrays match (np.allclose): {all_close}")
print(f"Max absolute difference: {max_diff:.2e}")
# Verify with assertion
assert all_close, f"Round-trip failed! Max difference: {max_diff}"
print("✓ Round-trip verification passed!")Arrays match (np.allclose): True
Max absolute difference: 0.00e+00
✓ Round-trip verification passed!
Loading Without HDF5 (CIFTI/TSV)
When loading from CIFTI or TSV format, data is reconstructed from per-state files:
# Load from the earlier CIFTI export
loaded_exp2, loaded_ts2 = SimulationExperiment.from_bids(
bids_dir=bids_output,
subject="01",
session="simulation",
)
print(f"Loaded TimeSeries shape: {loaded_ts2.shape}")
print(f"State variables: {loaded_ts2.variables_labels}")
# Verify CIFTI round-trip
cifti_close = np.allclose(ts.data, loaded_ts2.data, rtol=1e-5, atol=1e-8)
print(f"CIFTI round-trip match: {cifti_close}")Loaded TimeSeries shape: (1000, 2, 3, 1)
State variables: ['V' 'W']
CIFTI round-trip match: True
Format Auto-Detection
The from_bids() method automatically detects the time series format:
| Format | Extension | Fidelity |
|---|---|---|
| HDF5 | .h5 |
100% - All dimensions preserved |
| CIFTI | .ptseries.nii |
High - Per-state-variable files |
| TSV | .tsv |
High - Per-state-variable files |
For 100% reproducibility, use timeseries_format='h5' when exporting.
Output Location
The BIDS datasets are saved in the derivatives folder alongside this notebook:
print(f"Main output: {bids_output}")
print(f"Experiment output: {bids_output2}")
print(f"Multi-subject output: {multi_bids}")Main output: ./derivatives/tvbo
Experiment output: ./derivatives/tvbo_experiment
Multi-subject output: ./derivatives/tvbo_multi
You can inspect these folders directly or use the BIDS validator on them.
Summary
This notebook demonstrated:
- Creating a TVB simulation experiment
- Running the simulation to generate time series data
- Exporting results to BIDS format (BEP034 v1.0.0) using:
TimeSeries.to_bids()methodSimulationExperiment.to_bids()method
- Loading from BIDS format using:
SimulationExperiment.from_bids()method (full round-trip support)
- Understanding the BEP034 directory structure:
net/: Network connectivity (weights, distances)ts/: Time series data (CIFTI, TSV, or HDF5)eq/: Model equationscoord/: Region coordinates
- Inspecting ID-based file naming and provenance tracking
- Verifying reproducibility with
run_to_verify=True
For more information on TVBO, visit the documentation.
References
Gorgolewski, K. J., et al. (2016). The brain imaging data structure, a format for organizing and describing outputs of neuroimaging experiments. Scientific Data, 3, 160044.
BIDS Specification: https://bids-specification.readthedocs.io/
BEP034 Computational Modeling v1.0.0
The Virtual Brain: https://www.thevirtualbrain.org/