Run on a backend

The one spec runs on many engines: pick a backend, invoke it from Python or the CLI, and understand how sweeps are placed.

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 kindparameters, 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/.seednoise_seed; initial_condition…initial_conditions; subject/samplesubjects; else parameters) and can be overridden:

workflow:
  distribute:
    vectorize: [G, K_e]   # force these into the backend
    workflow:  [seed]     # force this to fan out

See 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