Skip to main content
stimpack: two display screens angled around a subject

stimpack

A modular framework for precise multisensory stimulus generation in systems neuroscience.


Documentation License: GPL v3

stimpack presents multisensory stimuli to a subject, in open or closed loop, and records what was presented with timing precise enough to line it up with whatever the rig acquires alongside. It drives perspective-corrected visual displays, sound playback, movement trackers and analog output hardware from one protocol, and keeps everything specific to a particular lab — rig geometry, hardware drivers, protocols — outside the package.

📖 Documentation · 🚀 Your first stimulus · 🤝 Contributing

Installation

Requires Python ≥ 3.10.

python3 -m venv .stimpack
source .stimpack/bin/activate     # Windows: .stimpack\Scripts\activate
pip install stimpack

Both data backends (HDF5 and NWB) are installed; pick one per config, or in the startup dialog. To work on stimpack itself, clone the repository and pip install -e .[test].

Running stimpack opens the experiment GUI. See the installation guide if it doesn't.

The AudiovisualPairing protocol class beside the GUI's Main tab built from it, captured mid-run

From protocol code to a running experiment. Left: the built-in AudiovisualPairing protocol, abridged, and the module calls stimpack makes from its descriptors on each trial — one visual, one audio, one shared start. Right: the Main tab mid-run, its parameter fields built from the class's own declarations, so a new protocol is drivable without writing interface code; the list-valued freq sweeps across trials, and "This trial" shows the current draw.

A stimulus in ten lines

from stimpack.visual_stim.stim_server import launch_stim_server
from stimpack.visual_stim.screen import Screen
from time import sleep

manager = launch_stim_server(Screen(fullscreen=False, vsync=True))
sleep(2)

manager.load_stim(name='Checkerboard')
manager.start_stim()
sleep(2)
manager.stop_stim(print_profile=True)

More, all runnable, in examples/.

How it fits together

An experiment runs as several processes:

ExperimentGUI ── BaseClient ──socket── BaseServer ──┬── visual      ── one subprocess per screen (GL)
                                                    ├── locomotion  ── tracker
                                                    ├── voltage_out ── DAQ
                                                    └── audio       ── sound card

The client runs the protocol, decides what each trial contains, and writes the data file. The server owns the hardware and usually runs on the rig machine while the client runs wherever the experimenter is sitting. Each screen is its own subprocess with its own GL context, so one display stalling cannot stall another.

They talk over a small JSON protocol, addressed to a module:

manager.target('visual').load_stim(name='MovingPatch', width=10, height=30)
manager.target('voltage_out').output_step(output_channels='DAC0', pre_time=0, step_time=1)

A protocol's timed module calls on the left, routed by the stimulus server to its modules on the right

A protocol names the module each call is for, and the server routes it there. Inputs update a subject state that outputs follow, so the closed loop does not pass through the client. The four modules shown all ship with stimpack; a lab adds a further capability as a new module rather than a change to the core.

Calls are one-way. There is no return value to branch on, and attribute access alone never fails — a mistyped name still produces a callable. The failure isn't silent, though: the server pushes messages back over the same link, so a name it doesn't have is reported — an error that aborts the run when the call can only be a mistake, a warning when it's a legitimate difference between rigs. Use has_server_function() to check before calling, and stimpack --check-labpack to catch the rest before a run.

Perspective-corrected rendering

Stimuli are rendered for a subject at a known position relative to a display of known size and placement, so an object subtends the angle it should. There are two paths, chosen by the type of screen, and one rig may use both.

Flat screens are described by the three physical corners of each region, in meters — pa lower-left, pb lower-right, pc upper-left. Each stimulus is drawn once per region through a generalized off-axis perspective (Kooima 2009), which is what corrects for a screen that is neither square to the subject nor equidistant from it.

Display coordinates

Curved screens — a bowl, a cylinder — cannot be described by a flat frustum, so they render through a cube map instead. The scene is drawn into the faces of a cube from the subject's position, then the screen is drawn once in projector coordinates, each fragment sampling the cube along its own direction. The screen is described by a surface and a projector:

# a 7.7 cm bowl in front of the subject, lit by a projector on the bowl's own axis
CurvedScreen(
    surface=SphericalSurface(radius=0.0775, elevation_range=(25, 90), pole=(0, 1, 0)),
    projector=PinholeProjector(position=(0, 0.35, 0), look_at=(0, 0, 0), up=(0, 0, 1),
                               throw_ratio=1.58),
)   # 94% of the bowl lit, ±65° azimuth and ±53° elevation

Three rigs, the frames sent to their displays, and the subject's visual field reconstructed from those frames

One stimulus specification, three display geometries, one visual experience. A single monitor, two monitors meeting ahead of the subject, and a hemisphere lit by a projector (a) each receive their own frames (b) — the same scene, warped per display — yet the subject's visual field, reconstructed from those very frames (c), matches wherever coverages overlap. Red: the photodiode watching each display's synchronization square; its trace (d) catches two dropped frames.

Because the screen is one draw call however finely it is tessellated, the cost scales with the scene and the number of cube faces — not with the screen's complexity. draw_curved_screen() plots the geometry, and the mesh reports its own coverage and pixels-per-degree, so a rig can be checked before anything is projected onto it.

Labpacks

stimpack contains no hardware-specific code. A labpack is a lab's own directory of protocols, rig configs, custom stimuli and device drivers, kept in its own repository and pointed at by a config file. See labpack-template to start one, and --check-labpack to verify it.

Data output

Experiments write HDF5 by default, or NWB with data_format: nwb in the config. One GUI handles both. See the config reference.

Tests

pip install -e .[test]
pytest -m unit          # fast; no GL, GUI or hardware
pytest                  # everything the machine can run

Tiers and what each needs are described in CONTRIBUTING.md.

Citing

If stimpack contributes to work you publish, please cite it — see CITATION.cff.

License

GNU General Public License v3.0 — see LICENSE.

Metadata

Release files for stimpack 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for stimpack 1.0.0
File Size Uploaded
stimpack-1.0.0.tar.gz 426.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for stimpack 1.0.0
File Interpreter ABI Platform
stimpack-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 868.9 kB

Release files / stimpack-1.0.0.tar.gz

Download URL stimpack-1.0.0.tar.gz
Size 426.7 kB
Tags Source
SHA-256 checksum
How to use checksums
a872892cdb4ec64899f229b3ef9b44f949fbd3ca2e64869740139f5d3a3fef1c
BLAKE2b-256 checksum
How to use checksums
2973b59a5fffabd967ce9d29f62844fb16400463ed9e1467edcc530c25e85e17
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 15, 2026.

Transparency log

Release files / stimpack-1.0.0-py3-none-any.whl

Download URL stimpack-1.0.0-py3-none-any.whl
Size 442.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
90765193b23bb36decab4cd8be431679941463be4a2632202387af6d17ebed9c
BLAKE2b-256 checksum
How to use checksums
b00be42c9b48abddb7b437984817a7de1e8ab5ed2c7db4099d44070a74bf2fe5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 15, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files

0.2.0

2 release files

0.1.0

2 release files

0.0.11

2 release files

0.0.10

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page