Calling your own code
The escape hatch, and its contract
module: resolves, and what stays traceable on an array backend.
Part of the running example, where stage 6 adds several experiments, their analyses and their figures.
YAML expresses equations, networks, sweeps and figures well. It does not express a custom spectral parameterisation, a bespoke connectome builder, or the table a specific paper reports. Every real study reaches Python at some point, and TVB-O gives that reach one shape: the recipe names a callable, and declares separately where the code lives.
That separation is the whole design. The specification says what to call; code_source says where to find it. Move the code, publish it or pin it to a commit, and the recipe does not change.
Declaring where the code lives
code_source: {path: code}One line, and every module: in the recipe resolves against that directory. path is relative to the recipe YAML, which is why the study directory puts callables in code/ beside the spec.
| Field | Meaning |
|---|---|
path |
A local directory, relative to the recipe or absolute. |
git |
A repository URL instead, shallow-cloned and cached under ~/.cache/tvbo/code_sources. |
ref |
The branch, tag or commit to check out. Pin it if the result must be reproducible. |
subdir |
A subdirectory within the source to put on the import path. |
path and git are mutually exclusive. The resolved directory goes onto the import path at load time, which is what makes a bare module name enough.
Naming a callable
Anywhere the schema takes a callable, it takes a name and the module it lives in:
callable: {name: power_table, module: kadak2025_analysis}That is code/kadak2025_analysis.py, function power_table. No package prefix, no path, no import statement in the recipe.
The same shape appears wherever the recipe hands work to Python:
| Where | What it does |
|---|---|
analyses[].callable |
Reduce results to reported numbers. |
network.graph_generator.builder |
Build a connectome the database does not hold. |
observations[].pipeline[].callable |
A processing step the equation grammar cannot express. |
figures[].panels[].callable |
A bespoke panel the figure grammar cannot draw. |
What the callable receives
Arguments are declared, keyed by parameter name, and each is either a literal value or a used: reference into another container:
arguments:
psd_post: {used: {experiment: 4, output: psd_post}}
iaf_nominal: {value: 10.5}So the function signature is ordinary Python, def power_table(psd_post, iaf_nominal): ..., and nothing in it knows about TVB-O. It receives labelled arrays and returns labelled arrays.
The invocation yields a mapping of name to labelled array, and each key becomes an observation__<key> data-variable that keeps its dims and coordinates. Returning a bare positional array throws that away, and every consumer downstream — a figure encoding, a later analysis sel — then has to guess which axis is which.
traceable, and when it matters
A callable used inside the integration runs under the backend’s tracer, not as ordinary Python. On an array backend that means it must be written in the backend’s array API, jnp rather than numpy, and must avoid Python control flow over traced values.
callable: {name: my_step, module: mystudy_obs, traceable: true}traceable: true is the recipe asserting that the callable satisfies this. It is not a check and it does not convert anything; it tells the code generator it may inline the call into a jitted, vmapped solve. A callable that is not traceable belongs in an analysis over saved data, where it runs as plain Python on the host and can use whatever it likes.
Requirements the code brings with it
If the callable needs a package tvbo does not install, say so, with the version it was reproduced against, because matching a published method often means matching the tool that method used.
requires:
fooof:
version: "1.1.1"
doi: "10.1038/s41593-020-00744-x"
description: >-
The spectral parameterisation the paper's own code uses. A different peak-finder would estimate something else.