Imports and plot style
import warnings; warnings.filterwarnings("ignore")
import numpy as np
import matplotlib.pyplot as plt
import bsplot
from tvbo import SimulationExperiment, Dynamics
bsplot.style.use("tvbo")From one equation to a whole-brain simulation
TVBO describes a simulation as data, not code. A model, a network, an integrator and whatever you want to measure are declared in one specification; TVBO generates the code and runs it on the backend you choose. The same file runs on JAX, TVB, PyRates, Julia or NeuroML, and it stays readable and version-controllable.
This page goes from a single equation to a whole-brain run in three steps. If you already know brain network modelling, skip to step 3 — the first two are the mechanics.
Any system of ODEs can be written directly. A damped oscillator,
\[ \dot{x} = v, \qquad \dot{v} = -k\,x - c\,v \]
becomes a specification by naming the state variables, their right-hand sides, and the parameters:
MINIMAL = """
label: "My first model"
dynamics:
name: DampedOscillator
state_variables:
x:
equation: {rhs: "v"}
initial_value: 1.0
v:
equation: {rhs: "-k*x - c*v"}
initial_value: 0.0
parameters:
k: {value: 1.0} # stiffness
c: {value: 0.1} # damping
network:
number_of_nodes: 1
integration:
method: Heun
step_size: 0.01
duration: 40.0
"""
result = SimulationExperiment.from_string(MINIMAL).run()
x = result.integration.data.sel(variable="x").squeeze()
print(f"released at x = {float(x[0]):.2f}, settled to x = {float(x[-1]):.3f}")INFO [tvbo.run] [+0s] STEP 1: Running simulation...
INFO [tvbo.run] [+0s] Simulation period: 40.0 ms, dt: 0.01 ms
INFO [tvbo.run] [+0s] Simulation complete.
INFO [tvbo.run] [+0s] Experiment complete.
released at x = 1.00, settled to x = -0.080
That is the whole loop: declare → run → inspect. Everything after this is the same loop with richer ingredients.
You rarely write neural mass models by hand — TVBO ships a curated library. Reference one by its ontology iri instead of spelling out the equations:
jr = Dynamics.from_db("JansenRit")
print("Jansen-Rit state variables:", list(jr.state_variables))
print("accepted coupling inputs :", list(jr.coupling_inputs))Jansen-Rit state variables: ['y0', 'y1', 'y2', 'y3', 'y4', 'y5']
accepted coupling inputs : ['c_glob', 'local_coupling']
JR = """
label: "Jansen-Rit, one region"
dynamics:
iri: tvbo:JansenRit
network:
number_of_nodes: 1
integration:
method: Heun
step_size: 0.5
duration: 2000.0
transient_time: 500.0
"""
jr_res = SimulationExperiment.from_string(JR).run()
print("dims:", dict(zip(jr_res.integration.data.dims, jr_res.integration.data.shape)))INFO [tvbo.run] [+0s] STEP 1: Running simulation...
INFO [tvbo.run] [+0s] Simulation period: 2000.0 ms, dt: 0.5 ms
INFO [tvbo.run] [+0s] Transient period: 500.0 ms (settled on (-500.0, 0], warm-started via update_history)
INFO [tvbo.run] [+0s] Simulation complete.
INFO [tvbo.run] [+0s] Experiment complete.
dims: {'time': 4000, 'variable': 6}
Browse the full library with Dynamics.list_db(), or the model gallery.
A brain network model is the same specification with two additions: a connectome to say which regions connect, and a coupling function to say how activity travels.
WHOLE_BRAIN = """
label: "Whole-brain simulation"
dynamics:
iri: tvbo:Generic2dOscillator
network:
iri: tvbo:DesikanKilliany # 87-region connectome from the database
transforms:
- name: weight
equation: {rhs: "weight / mean(weight[weight > 0])"} # normalize: mean non-zero weight -> 1
coupling:
c_glob: # key = the model's coupling input
iri: tvbo:Linear
delayed: false
incoming_states: [V]
integration:
method: Heun
step_size: 0.1
duration: 600.0
transient_time: 200.0
"""
brain = SimulationExperiment.from_string(WHOLE_BRAIN).run()
V = brain.integration.data.sel(variable="V")
print("regions:", V.sizes["node"], "| timepoints:", V.sizes["time"])INFO [tvbo.run] [+0s] STEP 1: Running simulation...
INFO [tvbo.run] [+1s] Simulation period: 600.0 ms, dt: 0.1 ms
INFO [tvbo.run] [+1s] Transient period: 200.0 ms (settled on (-200.0, 0], warm-started via update_history)
INFO [tvbo.run] [+1s] Simulation complete.
INFO [tvbo.run] [+1s] Experiment complete.
regions: 87 | timepoints: 6000
fig, axes = plt.subplots(1, 2, figsize=(11, 3.0))
axes[0].plot(V.time, V.isel(node=slice(0, 8)), lw=0.7)
axes[0].set_xlabel("time (ms)"); axes[0].set_ylabel("V")
axes[0].set_title("8 regions", loc="left", fontsize=10)
brain.integration.plot(type="raster", ax=axes[1])
axes[1].set_title("all regions", loc="left", fontsize=10)
plt.tight_layout()
plt.show()
NaN. The transforms block above rescales so the mean non-zero weight is \(1\).c_glob: not a name you invent. Get it from list(exp.dynamics.coupling_inputs).The documentation follows the shape of an experiment — specify it, run it, then analyze the result:
| You want to… | Go to |
|---|---|
| See the whole spec as one object | The SimulationExperiment object |
| Write or pick a model | Models & dynamics |
| Build or load a connectome | Networks & connectomes |
| Measure something (BOLD, FC, EEG) | Observation models |
| Sweep parameters | Parameter exploration |
| Match a model to data | Fitting, inference & optimization |
| Run on JAX, TVB, Julia, … | Run on a backend |
| Find where behaviour changes | Bifurcation & stability |
| Add spiking or multi-scale detail | Beyond the mean field |
| Scale up to a cluster | Reproducible workflows |
New to the science itself? The natural reading order follows the field’s own arc: single-system dynamics and bifurcations → neuron and neural-mass models → whole-brain networks. Start with Models & dynamics, then Bifurcation & stability, then Networks & connectomes.
Coming from another tool? Interoperability maps TVBO onto TVB, PyRates, NeuroML, tvboptim, the Julia backends, BIDS and openMINDS.