Tutorial: Integrating recording systems¶
WIP
The reason to put stimulus rendering on a dedicated device with real trigger lines is that electrophysiology and imaging rigs already run on TTL logic. They record incoming TTLs alongside neural data, and emit TTLs to drive cameras, lasers, and reward hardware. vstimd's Virtual Trigger Lines plug straight into that world.
This page covers the two directions of integration and how the timing lines up.
The shape of the integration¶
flowchart TB
subgraph DEV["Stimulus device (vstimd)"]
out["VTL output 'stim_onset'<br/>pulsed at vblank on the exact onset frame"]
vin["VTL input 'trial_start'"]
end
subgraph REC["Recording system (ephys / imaging)"]
acq["digital acquisition<br/>records TTLs with neural data<br/>emits TTLs (trial gate, …)"]
end
out -->|"TTL: onset"| acq
acq -->|"TTL: trigger"| vin
clock["the SAME acquisition clock<br/>timestamps both sides"]
clock -.-> acq
clock -.-> out
The key property: because the onset marker is recorded by your acquisition system, in its own clock, you never have to align two software timelines after the fact. The stimulus event and the neural data share one timebase.
The hardware bridge¶
Real TTL lines reach vstimd through a small companion daemon that maps physical
pins onto the VTL shared-memory bank. On a Jetson/Pi that is gpiochip-daqd
(GPIO pins); other backends (NI-DAQ, LabJack, …) follow the same pattern. The
daemon:
- writes input lines — latches a rising/falling edge into the VTL bank when a pin changes, which vstimd drains at the next vblank;
- reads output lines — is woken once per frame by an output strobe, then pulses the corresponding physical pins.
An Arduino or microcontroller works just as well as a lab DAQ for either
direction: pulse a GPIO into the bridge to trigger a stimulus, or read vstimd's
output pin to gate other equipment. From vstimd's side it is all just trigger
lines — see the vtl crate README
for the shared-memory contract a custom bridge would implement.
You don't write DAQ code inside vstimd
vstimd never talks to DAQ hardware directly. It only reads and writes the shared-memory VTL bank; the bridge daemon owns the hardware. That keeps the render loop free of driver code and lets you swap DAQ backends without touching the stimulus server.
Direction 1 — Markers out (timestamp your stimuli)¶
Have the animation that shows a stimulus pulse an output line on its onset frame. Wire that output pin into a spare digital input on your acquisition system.
from vstimd import Connection, VtlHandle, VtlKind, VtlEdge, StartAction, FinalAction
with Connection("tcp://stimulus-pc:5555") as conn:
conn.vtl.set_line_name(bank=0, bit=0, kind=VtlKind.OUTPUT, name="stim_onset")
stim_onset = VtlHandle.named("stim_onset", VtlKind.OUTPUT)
target = conn.stimuli.shapes.create_circle(pos=(0, 0), radius=100)
conn.stimuli.set_enabled(target, False)
flash = conn.animations.create_flash(
target,
duration_frames=30,
start_action_mask=StartAction.ENABLE | StartAction.START_ACTION_TRIGGER_LINE,
start_action_trigger_line=stim_onset, # HIGH for one frame at onset
final_action_mask=FinalAction.DISABLE,
)
conn.animations.arm(flash)
Every acquisition system that records a TTL will now have your stimulus onset in
its own timeline, frame-accurate. Add a photodiode on the panel (toggle it with
StartAction.TOGGLE_PHOTODIODE) if you want an optical ground-truth of the actual
light change to cross-check the electronic marker.
Direction 2 — Triggers in (let the rig drive stimuli)¶
Point the other way: your recording system (or a behavioural controller, lever, or Arduino) emits a TTL that vstimd reacts to. Arm the animation on a VTL input line.
conn.vtl.set_line_name(bank=0, bit=1, kind=VtlKind.INPUT, name="trial_gate")
trial_gate = VtlHandle.named("trial_gate", VtlKind.INPUT)
flash = conn.animations.create_flash(
target,
duration_frames=30,
start_trigger=trial_gate, # wait for the rig's TTL
start_edge=VtlEdge.RISING,
start_action_mask=StartAction.ENABLE | StartAction.START_ACTION_TRIGGER_LINE,
start_action_trigger_line=stim_onset,
final_action_mask=FinalAction.DISABLE,
)
conn.animations.arm(flash)
The stimulus now appears one frame after the external edge, deterministically, and also emits its own onset marker — so the trigger-to-stimulus and stimulus-to-marker delays are both fixed and known.
Closed-loop position from an external source¶
For closed-loop work (e.g. moving a stimulus with an eye/hand tracker or a physics model running elsewhere), a producer writes 2-D positions into a shared-memory float array and vstimd reads it every frame:
The position update is applied on the render loop, so it is as timely as the display allows — no per-sample network round-trip.
Why this beats aligning logs after the fact¶
Software-only toolkits typically record stimulus events in the experiment PC's clock and rely on post-hoc synchronisation (or an extra photodiode channel) to line them up with neural data. With vstimd the timing-critical path — trigger → stimulus change → recorded marker — is realised in hardware, on the render loop, and the marker is captured by the acquisition system itself. There is no second clock to reconcile.
See also¶
- Triggers & animations — the full VTL and animation API.
- Frame timing — how onset frames are pinned to vblank.
vtlcrate README — shared-memory layout for building a custom DAQ bridge.