Working with Results

Accessing simulation output, observations, and exporting to BIDS

Overview

After running a simulation experiment, TVBO returns an ExperimentResult that provides structured access to all output data — raw state variables, derived observations (BOLD, FC, PSD), algorithm history, and optimization traces.

The result object mirrors the YAML experiment specification: each section of the experiment (integration, algorithms, optimizations, explorations) maps to a corresponding field on the result.

Result Architecture

ExperimentResult
├── name                    # Experiment label
├── source                  # Back-reference to SimulationExperiment
├── integration             # SimulationResult — main simulation
│   ├── data                #   xr.DataArray (time × variable × node [× mode])
│   ├── observations        #   dict of observation outputs (BOLD, FC, …)
│   └── transient           #   SimulationResult — warm-up (if any)
├── algorithms              # dict[str, AlgorithmResult]
│   └── fic                 #   e.g. FIC tuning result
│       ├── state           #     Final tuned state
│       ├── history         #     Per-iteration tracking
│       ├── pre_tuning      #     SimulationResult before algorithm
│       └── post_tuning     #     SimulationResult after algorithm
├── optimizations           # dict[str, OptimizationResult]
│   └── loss_fc             #   e.g. FC-based optimization
│       ├── state           #     Fitted parameters
│       ├── history         #     Loss trajectory
│       └── simulation      #     SimulationResult with fitted params
├── explorations            # dict[str, ExplorationResult]
│   └── grid                #   Parameter sweep results
└── continuations           # dict — bifurcation analysis results

Basic Usage

from tvbo import Dynamics, SimulationExperiment

model = Dynamics.from_db("ReducedWongWangExcInh")
exp = SimulationExperiment(dynamics=model)
result = exp.run("jax")

The ExperimentResult displays a tree summary:

result
Experiment
└── integration
        data: (81920, 2, 1, 1)

Accessing Data

Integration (Main Simulation)

The primary simulation result is an xr.DataArray with named dimensions and coordinates:

result.integration.data
<xarray.DataArray (time: 81920, variable: 2, node: 1, mode: 1)> Size: 1MB
array([[[[0.09998933]],

        [[0.09988083]]],


       [[[0.09997866]],

        [[0.09976182]]],


       [[[0.099968  ]],

        [[0.09964298]]],


       ...,


       [[[0.16456527]],

        [[0.03920144]]],


       [[[0.16456528]],

        [[0.03920144]]],


       [[[0.16456529]],

        [[0.03920144]]]], shape=(81920, 2, 1, 1))
Coordinates:
  * time      (time) float64 655kB 0.01221 0.02441 0.03662 ... 1e+03 1e+03 1e+03
  * variable  (variable) <U3 24B 'S_e' 'S_i'
  * mode      (mode) int64 8B 0
Dimensions without coordinates: node

Use xarray’s label-based selection:

# Select a single state variable across all nodes
result.integration.sel(variable='S_e')
SimulationResult(81920, 1, 1)
# Integer indexing — first 1000 time steps
result.integration.isel(time=slice(0, 1000))
SimulationResult(1000, 2, 1, 1)

Standard properties remain available:

print("state_names:", result.integration.state_names)
print("dims:       ", result.integration.data.dims)
print("shape:      ", result.integration.data.shape)
state_names: [np.str_('S_e'), np.str_('S_i')]
dims:        ('time', 'variable', 'node', 'mode')
shape:       (81920, 2, 1, 1)

Backward Compatibility

ExperimentResult delegates to integration for backward compatibility. This means result.data and result.time work as before:

# These are equivalent:
assert result.data is result.integration.data
print("result.data.shape:", result.data.shape)
result.data.shape: (81920, 2, 1, 1)

Observations

Observations (BOLD, functional connectivity, PSD, etc.) are attached to the simulation that produced them:

result.integration.observations                # dict of observation outputs
result.integration.observations['bold']        # BOLD SimulationResult
result.integration.observations['bold'].data   # xr.DataArray

Algorithm Results

When running experiments with algorithms (e.g. FIC, EIB), results track the full iteration history:

# Load and run an experiment with algorithms
exp = SimulationExperiment.from_db("EI_Tuning_FIC_EIB_Optimization")
result = exp.run("tvboptim")

fic = result.algorithms['fic']
fic.name                  # 'fic'
fic.n_iterations          # 200
fic.state                 # Final tuned state (parameter arrays)
fic.history               # Per-iteration tracking
fic.pre_tuning            # SimulationResult before tuning
fic.post_tuning           # SimulationResult after tuning
fic.convergence           # Computed convergence metrics

Optimization Results

Optimization results track loss trajectory and parameter evolution:

opt = result.optimizations['gradient_eib']
opt.name                 # 'gradient_eib'
opt.n_steps              # Number of gradient steps
opt.final_loss           # Final loss value
opt.loss_trajectory      # Loss at each step (array)
opt.state                # Fitted parameters
opt.simulation           # Post-optimization SimulationResult

Exporting Results

BIDS-Compatible Export

ExperimentResult.export() writes simulation data and experiment metadata to a BIDS-compatible directory following BEP034 conventions:

import tempfile, os
outdir = os.path.join(tempfile.mkdtemp(), "my_experiment")
result.export(outdir)
PosixPath('/var/folders/ym/9kw1g21j1nd7kwfn8c0z3st40000gn/T/tmprnqnjnsa/my_experiment')
# Show what was written
for root, dirs, files in os.walk(outdir):
    level = root.replace(outdir, "").count(os.sep)
    indent = "  " * level
    print(f"{indent}{os.path.basename(root)}/")
    for f in sorted(files):
        print(f"  {indent}{f}")
my_experiment/
  dataset_description.json
  sub-01/
    sub-01_desc-tvbsim_experiment.yaml
    ts/
      sub-01_desc-tvbsim_ts-sim_State.json
      sub-01_desc-tvbsim_ts-sim_State.nc

Export Options

result.export(
    "output/",
    subject="02",              # BIDS subject label (default: "01")
    session="pre",             # Optional BIDS session
    description="fic_tuned",   # desc- entity (default: "tvbsim")
)

What Gets Written

Component File Format
Experiment specification *_experiment.yaml YAML (LinkML)
Simulation data *_ts-sim_State.nc netCDF (self-describing, HDF5-based)
Observations *_ts-{name}.nc netCDF per observation
Algorithm post-tuning *_ts-{algo}_State.nc netCDF
Optimization simulation *_ts-{opt}_State.nc netCDF
Sidecar metadata *_State.json JSON (shape, dims, sample period)
Dataset description dataset_description.json JSON (BIDS required)

Data Format: netCDF

Simulation data is stored as netCDF — a self-describing scientific data format built on HDF5. Each file contains the full array with named dimensions and coordinates, readable by any netCDF/HDF5 tool:

import xarray as xr

# Read back exported data
nc_files = [f for f in os.listdir(os.path.join(outdir, "sub-01", "ts")) if f.endswith(".nc")]
nc_path = os.path.join(outdir, "sub-01", "ts", nc_files[0])
ds = xr.open_dataset(nc_path)
ds['data']
<xarray.DataArray 'data' (time: 81920, variable: 2, node: 1, mode: 1)> Size: 1MB
[163840 values with dtype=float64]
Coordinates:
  * time      (time) float64 655kB 0.01221 0.02441 0.03662 ... 1e+03 1e+03 1e+03
  * variable  (variable) object 16B 'S_e' 'S_i'
  * mode      (mode) int32 4B 0
Dimensions without coordinates: node
Tip

netCDF4 files are HDF5 files — any HDF5 reader (h5py, HDFView, MATLAB’s h5read) can open them directly. The netCDF layer adds self-describing dimension names and coordinates.

# Clean up
import shutil
shutil.rmtree(os.path.dirname(outdir))

See Also