Python Scripting
datavis can derive new channels from existing ones by running Python scripts. Scripts are numba-compiled for speed and run live and during replay, feeding their outputs back into the store as ordinary channels you can drop on panels.
If numba (or the Python toolchain) is unavailable, scripting degrades
gracefully — the engine disables itself with a warning and the rest of the app
runs normally. Scripting is also a build-time feature (scripting, on by
default); build with --no-default-features to drop the Python dependency
entirely.
The scripts directory
Scripts live in a directory next to config.toml (default scripts/). The
engine scans it for *.py files at startup — these are the available scripts.
Availability is discovery; which scripts are enabled is separate and persisted
in config (see Script Bindings).
The script contract
A script is a .py file that self-declares its bindings as module globals and
implements a numba-compiled compute function. Because compute is compiled
with numba it must be pure: tuples of numpy arrays in, (ts, vals) array
pairs out. No dicts of objects, no .push(), no channel handles cross the
@njit boundary — the engine handles all of that.
Element-wise transform
Magnitude of a co-sampled 3-axis vector:
import numpy as np
import numba
INPUTS = ["accel.x", "accel.y", "accel.z"]
OUTPUTS = [{"name": "accel.magnitude", "type": "float", "unit": "m/s2"}]
@numba.njit
def compute(ts, vals):
t = ts[0] # accel.x timestamps
x, y, z = vals[0], vals[1], vals[2]
mag = np.sqrt(x**2 + y**2 + z**2)
return (t, mag) # one (ts, vals) pair per output
Multi-rate combine
The engine never resamples for you — do it yourself:
import numpy as np
import numba
INPUTS = ["accel.x", "gps.speed"]
OUTPUTS = [{"name": "norm.speed", "type": "float", "unit": "m/s"}]
@numba.njit
def compute(ts, vals):
tx, x = ts[0], vals[0]
tg, g = ts[1], vals[1]
g_on_x = np.interp(tx.astype(np.float64), tg.astype(np.float64), g)
return (tx, g_on_x * x)
Window reduction
Reduce a window to a single sample:
import numpy as np
import numba
INPUTS = ["motor.current"]
OUTPUTS = [{"name": "motor.current.rms", "type": "float", "unit": "A"}]
@numba.njit
def compute(ts, vals):
t, v = ts[0], vals[0]
rms = np.sqrt(np.mean(v**2))
return (t[-1:], np.array([rms])) # length-1 ts + vals -> one sample
Declaring the engine in config
[scripts]
dir = "scripts" # scripts directory, relative to config.toml
window_s = 10.0 # sliding window of input handed to compute
dir— scripts directory. Defaults to"scripts"when the section is absent.window_s— seconds of input history passed to eachcomputecall.
The INPUTS / OUTPUTS in a script are its defaults. You override them per
instance — reusing one script on different channels — in
[[scripts.instances]].