Spiral Motion to Velocity-Modulated LFP#
This guide walks through the spiral_to_dynamic_pink_outlet.py example,
which generates a simulated cursor moving in a spiral pattern and encodes its
velocity into LFP-like colored noise streamed over Lab Streaming Layer (LSL).
Overview#
The example demonstrates velocity-to-LFP encoding with a predictable, synthetic input:
Clock -> SpiralGenerator -> Diff -> CART2POL -> Velocity2LFP -> LSLOutlet
The spiral motion produces smoothly varying velocity vectors that sweep through all directions while also varying in magnitude, providing known ground truth for testing decoders.
Prerequisites#
Install the required packages:
uv add ezmsg-simbiophys ezmsg-lsl
Running the Example#
Basic usage:
cd examples
uv run python spiral_to_dynamic_pink_outlet.py
With custom parameters:
uv run python spiral_to_dynamic_pink_outlet.py \
--cursor-fs 100 \
--output-fs 30000 \
--output-ch 256
Command-Line Arguments#
--graph-addrAddress for the ezmsg graph server (ip:port). Set empty to disable. Default:
127.0.0.1:25978--cursor-fsSimulated cursor update rate in Hz. Default:
100.0--output-fsOutput sampling rate in Hz. Default:
30000.0--output-chNumber of output channels. Default:
256--seedRandom seed for reproducibility. Default:
6767
Pipeline Components#
Clock#
Generates timing signals at the specified rate (default 100 Hz).
SpiralGenerator#
Generates spiral 2-dimensional motion where both radius and angle vary over time:
SpiralGenerator(SpiralGeneratorSettings(
r_mean=150.0, # Mean radius
r_amp=50.0, # Amplitude of radial oscillation
radial_freq=0.1, # Radial oscillation frequency (Hz)
angular_freq=0.25, # Angular rotation frequency (Hz)
))
The parametric equations are:
r(t) = r_mean + r_amp * sin(2*pi*radial_freq*t)theta(t) = 2*pi*angular_freq*tx(t) = r(t) * cos(theta(t))y(t) = r(t) * sin(theta(t))
This creates a “breathing” spiral where the cursor rotates while moving in and out from the center.
Diff#
Differentiates position to get velocity. With scale_by_fs=True, the
output is in pixels per second.
CART2POL (CoordinateSpaces)#
Converts Cartesian velocity (vx, vy) to polar coordinates (magnitude, angle). This transformation is done once upstream and shared if both spike and LFP encoding are used (via VelocityEncoder).
Velocity2LFP#
Encodes polar velocity into LFP-like colored noise using a cosine tuning model:
Cosine encoder: Each of
n_lfp_sources(default 8) has a random preferred direction. The spectral exponent beta is computed as:beta = baseline + modulation * magnitude * cos(angle - pd)Clip: Ensures beta values stay within valid range [0, 2]
Colored noise: Generate 1/f^β noise with β dynamically modulated per source
Spatial mixing: Project n_lfp_sources onto output_ch channels using sinusoidal mixing patterns
The result is multi-channel colored noise where spectral properties vary with cursor velocity direction and magnitude.
LSLOutlet#
Streams the output over LSL with name SpiralModulatedPinkNoise and
type EEG.
Understanding the Encoding#
Cosine Tuning Model#
Each of the n_lfp_sources (default 8) has a randomly assigned preferred
direction. The spectral exponent beta for each source is computed using
a cosine tuning model:
beta = baseline + modulation * magnitude * cos(angle - pd)
With default settings:
baseline = 1.0(pink noise at rest)modulation = 1.0 / max_velocity(scales with velocity)max_velocity = 315.0(pixels/second)
When moving at maximum velocity in a source’s preferred direction, beta reaches 2.0 (brown noise). When moving opposite to the preferred direction, beta reaches 0.0 (white noise). The output is clipped to [0, 2].
Spectral Exponent Effects#
The spectral exponent β controls the noise color:
β = 0: White noise (flat spectrum)
β = 1: Pink noise (1/f, equal power per octave)
β = 2: Brown noise (1/f², random walk)
Each source responds differently to velocity direction based on its preferred direction, creating a rich mixture of spectral characteristics.
Spatial Mixing#
The n_lfp_sources noise sources are projected onto output_ch channels
using a mixing matrix based on sinusoidal weights at different spatial
frequencies, plus random perturbations:
weights = np.zeros((n_sources, output_ch))
for i in range(n_sources):
freq = (i + 1) / n_sources
phase = 2 * np.pi * i / n_sources
weights[i, :] = np.sin(2 * np.pi * freq * ch_idx / output_ch + phase)
weights += 0.3 * rng.standard_normal((n_sources, output_ch))
This creates spatially-varying patterns where different channels have different mixtures of the velocity-modulated sources, mimicking the spatial spread of LFP signals across electrode arrays.
Verifying the Output#
The spiral motion provides predictable ground truth:
Varying velocity magnitude: Oscillates due to radial breathing
Linearly varying angle: Rotates at
angular_freqHzPeriodic behavior: Angular period = 1/angular_freq seconds (4s default), radial period = 1/radial_freq seconds (10s default)
You can verify the encoding by:
Recording the LSL stream
Computing spectral features from the output
Checking that spectral properties correlate with the known velocity pattern
Example Analysis#
import numpy as np
from pylsl import StreamInlet, resolve_stream
# Capture one angular period (4 seconds with default settings)
streams = resolve_stream('name', 'SpiralModulatedPinkNoise')
inlet = StreamInlet(streams[0])
samples = []
for _ in range(int(4 * 30000)): # 4 seconds at 30 kHz
sample, _ = inlet.pull_sample()
samples.append(sample)
data = np.array(samples)
# Compute spectrum for each second
from scipy import signal
for i in range(4):
segment = data[i*30000:(i+1)*30000, 0] # First channel
f, psd = signal.welch(segment, fs=30000)
# Compare spectral slope across segments
See Also#
Mouse Velocity to Simulated Ecephys - Real mouse input with full ecephys output
ezmsg.simbiophys.system.velocity2lfp- API documentationezmsg.simbiophys.cosine_encoder- Cosine tuning encoderezmsg.simbiophys.dynamic_colored_noise- Colored noise generatorezmsg.simbiophys.oscillator- SpiralGenerator and SinGenerator