Run on a backend
A SimulationExperiment is backend-independent: the same spec is compiled to code and executed by whichever engine you name. This page is the single reference for which backends exist, what each is for, how to invoke one, and how the workflow planner places parameter sweeps across a backend and the workflow engine.
Choosing a backend at a glance
| You want to… | Use |
|---|---|
| Fit / optimise, or sweep with gradients | tvboptim (default) |
| Fastest forward integration on CPU/GPU | jax |
| A reference result to check against | tvb |
| A dependency-light quick run | python |
| Symbolic / custom models, delays | pyrates |
| Julia solvers (stiff, SDDE, performance) | networkdynamics |
| Spiking / event-driven micro-scale | brian2 |
| Continuation & bifurcation diagrams | bifurcationkit · auto |
The default is resolved from the experiment’s declared execution.backend; if none is set it falls back to tvboptim. exp.run(...) and tvbo run … resolve the backend identically.
Invoking a backend
From Python
from tvbo import SimulationExperiment
exp = SimulationExperiment.from_db("Deco2014")
result = exp.run("jax", duration=10_000) # run on JAX
result = exp.run() # default (execution.backend, else tvboptim)run(format=…) configures delays, generates the backend’s code, executes it, and returns an ExperimentResult. Use exp.execute(format=…) instead when you want the prepared simulator object (a JAX kernel, a TVB Simulator, a tvboptim namespace) to drive manually, and exp.render_code(format=…) to inspect the generated code without running it. See Code generation & export.
From the CLI
tvbo run Deco2014 --backend jax --duration 10000 -o out/--backend/-b takes the same identifiers as Python’s format=. Omit it to use the experiment’s declared backend. See tvbo run.
Simulation backends
These integrate the model forward. Capabilities for the first block are the authoritative workflow-planner set (tvbo.cli._backends.BACKENDS, mirrored from ontology/tvb-o-axioms.ttl §4.1).
format= (aliases) |
Engine | Best for | Key capabilities |
|---|---|---|---|
tvboptim (default) (tvb-optim) |
tvboptim (JAX) | fitting, gradient sweeps, per-subject batches | Autodiff · JIT · StochasticSolver · CodeGen |
jax (autodiff) |
pure JAX kernel | fastest forward integration, grids, GPU | Autodiff · JIT · GPU · VectorizedRNG · CodeGen |
tvb |
The Virtual Brain (NumPy) | reference results, DDE/SDDE, built-in models | NumPy · BuiltinModelLibrary · DelayBuffer · StochasticSolver |
pyrates |
PyRates | symbolic/custom models, delays (DDE) | CodeGen · NetworkXTopology · DelayBuffer |
networkdynamics (nd) |
NetworkDynamics.jl | Julia performance, stiff, SDDE | JuliaJIT · DiffEq · DelayBuffer · StochasticSolver · StiffSolver |
brian2 (brian) |
Brian2 | spiking / event-driven micro-scale | Spiking · EventDriven · CodeGen |
numpy / python |
built-in NumPy | dependency-light quick runs | NumPy |
Also reachable via exp.run(format=…) as forward/export targets (documented under Interoperability, not part of the planner capability matrix):
format= (aliases) |
Engine | Notes |
|---|---|---|
mtk (modelingtoolkit) |
ModelingToolkit.jl | symbolic Julia; higher-order lowering |
julia (diffeq) |
DifferentialEquations.jl | Julia solvers directly |
cuda |
generated CUDA (PyCUDA) | GPU C kernels; parallel parameter sweeps, see below |
pde (pde-fem) |
FEM PDE solver | surface / field neural PDEs |
neuroml (nml, lems) |
NeuroML / LEMS | standards-compliant export + run |
The cuda backend
render_code("cuda") emits a RateML-style CUDA kernel, one __global__ function per model integrating every node in a time loop with the delay history held in a ring buffer, and exp.run("cuda") compiles it with PyCUDA and launches it across n_work_items parameter sets in parallel. Its reason to exist is that last part: a parameter sweep costs one launch, not one simulation per point.
It is the least-travelled backend here. Prefer jax, which also runs on GPU, is vectorized, differentiable, and is the one the golden corpus covers.
exp = SimulationExperiment.from_file("RateML_ReducedWongWang.yaml")
result = exp.run("cuda", n_work_items=32) # result["tavg"]: [n_work_items, n_states, n_node]What it needs. PyCUDA, an NVIDIA GPU, and a CUDA toolkit whose nvcc still supports that GPU’s architecture. That last clause is not pedantry: PyCUDA compiles the kernel at run time and passes the detected -arch, so a toolkit newer than the card fails outright. CUDA 13 dropped Volta, so a V100 (sm_70) under CUDA 13.3 aborts with nvcc fatal : Unsupported gpu architecture 'sm_70'.
How far this is verified. The emitted kernel compiles and runs on an NVIDIA L40 (sm_89, CUDA 13.3), launched through a driver that mirrors run_cuda’s argument marshalling. exp.run("cuda") itself has no automated test — CI has no GPU — so treat it as the least-exercised path here and check your first result against another backend.
Time units. The kernel multiplies dt straight into the model’s equations, so dt is in the model’s own time unit and defaults to the experiment’s step_size. Its delay ring, however, indexes length / speed / dt — millimetres over metres-per-second, which is milliseconds. The two agree only for a model whose time_unit is ms; declare anything else and the trajectory is right while the delays are not.
Limits. One model per kernel, so a heterogeneous network is not expressible. Only global_speed and global_coupling are swept per work item. There is no golden-corpus coverage and no test that executes a kernel, because CI has no GPU.
Analysis backends
These do not integrate forward. They trace how fixed points and limit cycles move as a parameter varies. See Bifurcation & stability.
format= (aliases) |
Engine | For |
|---|---|---|
bifurcationkit (bifurcation, bifurcationkit.jl) |
BifurcationKit.jl | numerical continuation & bifurcation (Julia) |
auto (auto-07p, numcont) |
AUTO-07p | continuation (Fortran AUTO) |
pyrates-bifurcation (pycobi) |
PyCoBi / PyRates | continuation via PyRates |
How sweeps are placed: vectorize vs. fan-out
The CLI’s workflow planner is backend-aware: the same study.yaml produces a different execution DAG per backend, because each backend can vectorize a different set of sweep axes internally. Every ExplorationAxis has a kind — parameters, initial_conditions, noise_seed, or subjects:
axis.kind ∈ backend.vectorize_axes → vectorized inside one job
else → fanned out as workflow tasks
| Backend | Vectorize axes |
|---|---|
jax |
parameters, initial_conditions, noise_seed |
tvboptim |
parameters, initial_conditions, noise_seed, subjects |
pyrates |
parameters |
networkdynamics |
parameters |
bifurcationkit |
parameters |
tvb |
(none; everything fans out) |
numpy |
(none) |
Worked example. A study with two axes, G (size 11) and noise_seed (size 8):
| Backend | Vectorized | Fanned out | Workflow cells |
|---|---|---|---|
jax / tvboptim |
both | none | 1 |
pyrates / networkdynamics |
G |
noise_seed |
8 |
tvb |
none | both | 88 |
This is the same study.yaml: work moves between the inside of the simulator and the workflow engine purely on ontology-declared backend capability. An axis’s kind is inferred from its dotted path (…noise_seed/.seed → noise_seed; initial_condition… → initial_conditions; subject/sample → subjects; else parameters) and can be overridden:
workflow:
distribute:
vectorize: [G, K_e] # force these into the backend
workflow: [seed] # force this to fan outSee tvbo workflow plan to preview placement, and Reproducible workflows for running the resulting DAG.
Source of truth
The capability matrix lives in code (tvbo.cli._backends.BACKENDS, a fast dict[str, BackendSpec] with no RDFLib import) and in the ontology (ontology/tvb-o-axioms.ttl §4.1, the machine-readable contract for reasoners and the platform UI). A round-trip test (tests/test_cli_backends_match_ontology.py) keeps them in sync. Run tvbo workflow backends for the live table.
See also
- The SimulationExperiment object: what you’re running
- Results & output: what
run()returns - Code generation & export: inspect or save generated code
tvbo run·tvbo workflow