A composable framework for building, simulating, and formally bounding closed-loop neuromodulation pipelines.
sense → decode → decide → stimulate — as a typed dataflow graph, with compile-time latency budgets and runtime-enforced safety envelopes.
Closed-loop neuromodulation (responsive DBS, closed-loop seizure abortion, adaptive tremor suppression) is held back by software, not silicon. The control loop has to:
- run deterministically within a hard deadline (sense-to-stim latency of tens of milliseconds),
- never emit a stimulation command that exceeds tissue-safety limits (charge density, amplitude, charge-per-phase), and
- be auditable — every decision traceable, every bound provable before the device is ever near a patient.
Today that logic is hand-rolled per device, per lab, with safety checks scattered through firmware and latency budgets verified by hope. neuroloop makes the pipeline a first-class, typed, analyzable object: you compose stages, and the framework statically rejects graphs that can't meet their deadline or could violate a safety envelope — before you ever run them.
Think of it as "LangChain for closed-loop neural control" — composable stages over a messy substrate — except the substrate is a real-time control loop and the guarantees are latency and safety, not prompts.
- ✅ A framework: typed
Signal→Stage→Graphcomposition. - ✅ A static analyzer: end-to-end latency budgeting + safety-envelope feasibility, checked at graph-build time.
- ✅ A deterministic simulator: a time-triggered scheduler that runs the graph against a numerical model of neural tissue, so you can validate a closed loop with zero hardware.
- ✅ Honest about its scope (see below).
Honesty boundary.
neuroloop's runtime is a deterministic simulation of a time-triggered control loop running in CPython — it models and bounds timing, it is not a hard real-time operating system. The WCET (worst-case execution time) numbers a stage declares are contracts you assert and the analyzer reasons about, not measured on certified hardware. The value here is the abstractions, the static analysis, and the closed-loop simulation — the layer where most closed-loop neuro bugs actually live — not a flight-qualified scheduler. Porting the same typed graph onto an RTOS HAL is explicitly future work.
A pipeline is a list of stages, each declaring its I/O signal types and a worst-case execution time:
from neuroloop import Graph, SignalType
from neuroloop.stages import BandPower, ProportionalController, Stimulator
from neuroloop.safety import SafetyEnvelope
lfp = SignalType("lfp", channels=1, fs=1000.0, units="uV")
env = SafetyEnvelope(
max_amplitude_mA=3.0,
electrode_area_cm2=0.06, # typical DBS contact
pulse_width_us=60.0,
max_charge_density_uC_cm2=30.0, # Shannon-safe limit
)
graph = (
Graph(input_type=lfp, deadline_ms=50.0)
.then(BandPower(band_hz=(4, 6), wcet_us=200)) # tremor-band power
.then(ProportionalController(gain=1.2, wcet_us=50))
.then(Stimulator(envelope=env, wcet_us=80))
)
graph.check() # raises if latency budget blown OR envelope infeasiblegraph.check() is the point of the whole thing. It:
- verifies every stage's output type matches the next stage's input type,
- sums declared WCETs and proves the loop fits inside
deadline_ms, - proves the stimulator stage cannot emit a command outside the safety envelope.
If any of those fail, you get an error at build time, with the offending stage named.
examples/dbs_tremor.py runs a full closed loop against a numerical tremor model:
- a neural plant generating a ~5 Hz tremor oscillation in simulated LFP,
- the graph above sensing tremor-band power and driving a proportional stimulator,
- the stimulation feeding back to suppress the tremor in the plant.
It prints the static latency/safety report and plots tremor power before vs. after the loop engages.
pip install -e .
python -m neuroloop.examples.dbs_tremor # writes results/dbs_tremor.pngEarly, actively built. See ROADMAP.md for milestones. Local research artifact — not affiliated with or derived from any employer's work.
MIT — see LICENSE.