The SimulationExperiment object

The central runnable object: bundle dynamics, network, coupling, integration and observations into one declarative spec, then serialize, render, or run it.

Part of the running example, where stage 1 adds the model, the network and the clock.

Overview

A SimulationExperiment is the central runnable object in TVBO. It bundles everything required to simulate a brain network model into one declarative specification:

  • dynamics: the local neural mass or population model, a Dynamics
  • network: the brain network, its connectome, parcellation and node dynamics
  • coupling: the long-range coupling function between regions
  • integration: the numerical integrator, its method, step size, duration and noise
  • monitors and observations: derived signals such as BOLD, FC and PSD
  • (optional) stimulation, algorithms, optimizations, explorations and continuations: additional analysis layers

The same object can be serialized to YAML, rendered as code for several backends, executed, or exported to BIDS or openMINDS, all from a single in-memory specification that mirrors the LinkML schema.

Quick Start

from tvbo import Dynamics, SimulationExperiment

model = Dynamics.from_db("ReducedWongWangExcInh")
exp = SimulationExperiment(dynamics=model)
result = exp.run("jax", duration=10_000)
result
Experiment
└── integration
        data: (819200, 2)

Constructing an Experiment

From a Dynamics object

The simplest experiment is just a model. Defaults are filled in for coupling, network, integrator, and monitors:

model = Dynamics.from_db("Generic2dOscillator")
exp = SimulationExperiment(dynamics=model)

With explicit components

TVBO is declarative about where information comes from. Every component in an experiment is either:

  1. Fully specified inline. A Python instance, or a dict carrying every parameter and equation.
  2. Loaded from YAML. See from_file and from_string.
  3. Pointed at semantically. A dict with an iri CURIE identifying the source. The IRI’s prefix selects the source (tvbo: for the built-in ontology, but also e.g. neuroml:, kg:, …); the rest of the dict is backfilled from that source.
from tvbo import SimulationExperiment
exp = SimulationExperiment(
    dynamics={"name": "ReducedWongWang", "iri": "tvbo:ReducedWongWangExcInh"},
    network={
        "parcellation": {"iri": "tvbo:DesikanKilliany"},
        "tractogram": {"iri": "tvbo:dTOR"},
        "coupling": {"Linear": {"iri": "tvbo:Linear"}},
    },
    integration={"method": "Heun", "duration": 10_000, "noise": None},
)

A coupling function acts along the edges of a network, so it is declared on the network, as a keyed collection, since a network may carry several. The key is the coupling’s name; writing it a second time inside the entry is redundant.

The IRI is the load-bearing piece: a bare name string is not semantically resolvable across multiple sources.

See Defining Networks, Coupling Functions and Integrators for the per-section details.

From the built-in database

TVBO ships a curated set of full experiment specifications. Load any of them by name:

SimulationExperiment.list_db()[:10]
['AdaptiveExponentialIF_Ex8',
 'Delay_Speed_Synchronization',
 'EI_Tuning_FIC_EIB_Optimization',
 'Epileptor2D_LEMS',
 'FitzHughNagumo1969_Ex9',
 'FitzHughNagumo_Ex9',
 'Generic2dOscillator_LEMS',
 'Hopf_Pareto_ParallelOpt',
 'IntegrateAndFire_Ex0',
 'Izhikevich2007_Ex2']
exp = SimulationExperiment.from_db("Schirner2023_MultiscaleBNM_DM")

From a YAML file

Experiments are round-trippable through YAML (see Code Generation & Export):

exp = SimulationExperiment.from_file("my_experiment.yaml")

The file is read by the shared loader, so it supports keyed collections, scalar shortcuts, anchors, merge keys and !include. See Writing TVB-O YAML.

Warning

A key repeated twice in the same mapping is an error, not a last-one-wins override.

Other constructors

Method Source
SimulationExperiment.from_db(name) built-in tvbo/database/studies/*.yaml
SimulationExperiment.from_file(path) local YAML file
SimulationExperiment.from_string(yaml) inline YAML string
SimulationExperiment.from_pyrates(path) PyRates YAML
SimulationExperiment.from_pydantic(obj) Pydantic schema instance
SimulationExperiment.from_tvb_simulator(sim) live tvb.simulator.Simulator
SimulationExperiment.from_platform(...) TVB-O platform API
SimulationExperiment.from_openminds(src) openMINDS JSON-LD

Inspecting an Experiment

print(exp)
SimulationExperiment3
# Full parameter collection (model + coupling + integrator + network)
list(exp.parameters)[:8]
['dynamics', 'network', 'coupling']
# Symbolic representation of the right-hand side
exp.symbolic()
{'state': [Eq(Derivative(S_e(t), t), gamma_e*(1 - S_e(t))*H_e(t) - S_e(t)/tau_e),
  Eq(Derivative(S_i(t), t), gamma_i*H_i(t) - S_i(t)/tau_i)],
 'functions': [],
 'derived_parameters': [],
 'derived': [Eq(J_N_S_e(t), J_N*S_e(t)),
  Eq(coupling(t), G*J_N*(c_glob + local_coupling*S_e(t))),
  Eq(x_e(t), a_e*(I_ext + I_o*W_e - J_i*S_i(t) + w_p*J_N_S_e(t) + coupling(t)) - b_e),
  Eq(x_i(t), a_i*(I_o*W_i + lamda*coupling(t) + J_N_S_e(t) - S_i(t)) - b_i),
  Eq(H_e(t), x_e(t)/(1 - exp(-d_e*x_e(t)))),
  Eq(H_i(t), x_i(t)/(1 - exp(-d_i*x_i(t))))],
 'parameters': {G: 2.0,
  I_ext: 0.0,
  I_o: 0.382,
  J_N: 0.15,
  J_i: 1.0,
  W_e: 1.0,
  W_i: 0.7,
  a_e: 310.0,
  a_i: 615.0,
  b_e: 125.0,
  b_i: 177.0,
  d_e: 0.16,
  d_i: 0.087,
  gamma_e: 0.000641,
  gamma_i: 0.001,
  lamda: 0.0,
  tau_e: 100.0,
  tau_i: 10.0,
  w_p: 1.4},
 'units': {G: None,
  I_ext: None,
  I_o: 'nA',
  J_N: None,
  J_i: None,
  W_e: None,
  W_i: None,
  a_e: 'per_nC',
  a_i: 'per_nC',
  b_e: 'Hz',
  b_i: 'Hz',
  d_e: 's',
  d_i: 's',
  gamma_e: None,
  gamma_i: None,
  lamda: None,
  tau_e: 'ms',
  tau_i: 'ms',
  w_p: None,
  S_e(t): None,
  S_e: None,
  S_i(t): None,
  S_i: None,
  J_N_S_e(t): None,
  J_N_S_e: None,
  coupling(t): None,
  coupling: None,
  x_e(t): None,
  x_e: None,
  x_i(t): None,
  x_i: None,
  H_e(t): None,
  H_e: None,
  H_i(t): None,
  H_i: None,
  c_glob: None,
  local_coupling: None},
 'coupling': {}}
# Round-trip through YAML
print(exp.to_yaml()[:400])
id: 3
model: ReducedWongWang
part: main
dynamics:
  name: ReducedWongWang
  iri: tvbo:ReducedWongWangExcInh
  parameters:
    G:
      name: G
      definition: Global coupling scaling
      value: 2.0
      domain:
        enforce: none
        lo: 0.0
        hi: 10.0
        step: 0.01
        log_scale: false
      description: Global coupling scaling
    I_ext:
      name: I_ext
      definit

Running

exp.run(format=..., **kwargs) configures delays, generates code for the requested backend, executes it, and returns an ExperimentResult:

result = exp.run("jax", duration=5_000)
print(type(result).__name__, "→", result.integration.data.shape)
ExperimentResult → (409600, 2, 87)

The format= (default tvboptim) selects the engine: jax for the fastest forward integration, tvb for a reference result, plus pyrates, networkdynamics, brian2, and the bifurcationkit / auto analysis backends. The full catalogue — capabilities, CLI invocation, and how sweeps are placed — is on Run on a backend.

exp.run() accepts these common kwargs:

  • duration: overrides integration.duration
  • initial_conditions: a TimeSeries or array; defaults from collect_initial_conditions()
  • backend-specific kwargs (e.g. jit=False for JAX)

execute() vs run()

  • exp.execute(format=...) returns the prepared simulator object (a TVB Simulator, a JIT-compiled JAX kernel, a tvboptim namespace, …). Use it when you want to drive the simulation manually.
  • exp.run(format=...) calls execute() and runs it, then wraps the output in an ExperimentResult.

Configuration & Lifecycle

Most users never need to call configure() directly, because run() does it. It performs auto-fixes such as disabling delay logic when the connectome has no path lengths or conduction speed is infinite.

exp.configure()         # idempotent, called by run()
exp.collect_initial_conditions()   # build a TimeSeries of initial states
exp.collect_state()                 # snapshot of dynamics + parameters
exp.add_stimulus(stim)              # attach a Stimulus or ontology term

Inspecting Generated Code

You can render the simulator code without executing it (useful for debugging or for using the kernel outside TVBO):

print(exp.render_code("jax")[:600])
import logging

import jax
from tvbo.data.types import TimeSeries
import jax.numpy as jnp

logger = logging.getLogger("tvbo.run")


def cfun(weights, history, current_state, p, delay_indices, t):
    n_node = weights.shape[0]
    b, a = p.b, p.a

    x_j = jnp.array(
        [
            history[0, delay_indices[0].T, delay_indices[1]],
        ]
    )

    pre = x_j
    pre = pre.reshape(-1, n_node, n_node)

    def op(x):
        return jnp.sum(weights * x, axis=-1)

    gx = jax.vmap(op, in_axes=0)(pre)
    return b + a * gx


import jax.numpy as jnp


def dfun(current_state, t, cX, _p):
 

See Code Generation & Export for the full list of supported targets.

Saving & Exporting

Goal Call
Round-trippable YAML exp.to_yaml("path.yaml")
openMINDS JSON-LD exp.to_openminds("experiment.jsonld")
LEMS / NeuroML NeuroMLAdapter(exp).render_code()
Standalone code file exp.save_code("out/", file_name="kernel.py")
Markdown / HTML report exp.report(format="markdown")

For exporting simulation results (time series, observations, metadata) in BIDS layout, see Working with Results → BIDS Export.

See Also