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 (computational model derivatives):

output_dir/
├── dataset_description.json
└── sub-{subject}/
    ├── sub-{subject}_desc-{desc}_experiment.yaml   # full LinkML experiment spec
    └── ts/
        ├── sub-{subject}_desc-{desc}_ts-sim_State.nc    # raw state variables
        ├── sub-{subject}_desc-{desc}_ts-sim_State.json  # sidecar metadata
        └── sub-{subject}_desc-{desc}_ts-{obs}.nc        # one per observation
import tempfile, os
outdir = os.path.join(tempfile.mkdtemp(), "my_experiment")
result.export(outdir)
PosixPath('/var/folders/ym/9kw1g21j1nd7kwfn8c0z3st40000gn/T/tmp9kiybybe/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 netCDF4 (.nc) — a self-describing scientific data format built on HDF5. Each file contains the full array with named dimensions and coordinates, readable by any netCDF or HDF5 tool.

Why netCDF and not NIfTI or plain HDF5?

Format Suitable? Reason
NIfTI No Designed for volumetric (voxel) imaging. Region-by-region time series (time × variable × node × mode) don’t fit the NIfTI data model and we’d lose labelled axes.
Plain HDF5 Partially HDF5 has no built-in concept of named dimensions or coordinate variables; we’d need to invent our own conventions.
netCDF4 Yes Self-describing dimensions + coordinates and HDF5-compatible storage. Round-trips xarray DataArrays losslessly.
Tip

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

Reading exported data back

import xarray as xr

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
# Clean up
import shutil
shutil.rmtree(os.path.dirname(outdir))

See Also