Stimulus ā
A Stimulus is the input signal delivered to neurons during simulation. It represents the time-varying stimulation pattern - whether electrical current, conductance changes, or MEA-mediated channel inputs - that drives neural activity.
The Stimulus class ā
The Stimulus class wraps a 2D array of stimulation values along with timing metadata:
from livn.stimulus import Stimulus
import numpy as np
stim = Stimulus(
array=np.random.randn(100, 10) * 0.1, # [timesteps, n_targets]
dt=1.0, # timestep in ms
gids=np.arange(10), # target neuron IDs (optional)
)
print(stim.duration) # 100.0 ms
print(len(stim)) # 10 targetsProperties ā
| Property | Type | Description |
|---|---|---|
array | float[T, N] | Stimulation values over time |
dt | float | Timestep in milliseconds |
gids | int[N] | Target neuron IDs (optional) |
duration | float | Total duration in ms (T Ć dt) |
meta_data | dict | Additional metadata |
Creating stimuli ā
From arrays ā
Pass a NumPy array directly:
# Current injection: 200ms at 0.05ms resolution, 10 neurons
stim = Stimulus(array=np.zeros((4000, 10)), dt=0.05)From channel inputs ā
When using an MEA, you typically specify inputs per electrode channel rather than per neuron. cell_stimulus applies such an input and returns a Stimulus:
from livn.io import MEA
mea = MEA()
channel_inputs = np.zeros((100, mea.num_channels))
channel_inputs[:, 5] = 0.5 # stimulate channel 5
stim = mea.cell_stimulus(system.neuron_coordinates, channel_inputs, dt=1.0)The result carries a column only for the sections the inputs actually reach (via stim.gids and stim.sections; stim.expand(gids, sections) provides the all-to-all).
Policies ā
A policy produces a [timestep, n_channels] array of per-channel commands (the input-side counterpart to a Decoding).
A policy's amplitude is a commanded amplitude in channel space, and carries no physical unit. What the number means is the IO layer's business so the same policy can be used for different IO layers.
from livn.policy import BiphasicPulsePolicy
pulses = BiphasicPulsePolicy(
n_channels=16,
channels=[5, 6], # channels to stimulate
amplitude=1.5, # commanded amplitude, see below
phase_duration=0.2, # ms per phase
interphase_gap=0.05, # ms between phases
pulse_times=[0.0, 50.0], # onset times
dt=0.05, # timestep resolution
cathodic_first=True, # cathodic phase first (standard)
)
env.run(100.0, stimulus=pulses)A run too short to deliver the whole policy is refused rather than truncated. Use start_ms to offset it within the run.
Let the env allocate
A per-cell stimulus is dense in both time and cells, driving memory usage. For example, a pulse train spanning a long run is almost entirely zeros but on a 2600-cell graph running 117 s run at dt=0.1, the dense stimulus would amount to ~22 GB!
To avoid that, use policies so the env can materialize only the window it is about to simulate, and only for the cells the driven channels reach. Alternatively, where a run has quiet stretches, simulate them as their own run call with no stimulus at all:
env.run(20_000) # free-running, nothing allocated
env.run(1_000, stimulus=...) # only allocated hereTo prevent accidental allocation, livn does not allocate stimuli larger than 5 GiB. Raise LIVN_MAX_STIMULUS_GB if you really mean it.
The units below only describe what reaches a cell, which is downstream of the IO layer and genuinely known.
From conductance values ā
For synaptic conductance-based inputs:
stim = Stimulus.from_conductance(
conductance=np.random.rand(100, 10) * 0.01, # µS
dt=0.1,
)From current injection ā
For direct current injection:
stim = Stimulus.from_current(
current=np.ones((100, 10)) * 0.5, # nA
dt=0.1,
)From current density ā
For current density injection:
stim = Stimulus.from_current_density(
current_density=np.ones((100, 10)) * 0.1, # mA/cm²
dt=0.1,
)From extracellular voltage ā
For explicit extracellular voltage stimulation:
stim = Stimulus.from_extracellular(
voltage=cell_stim_array, # mV, [timesteps, n_gids]
dt=0.1,
)From irradiance (optical stimulation) ā
For optical stimulation via opsin-expressing neurons. The opsin model is part of the neuron model and does not need to be specified on the stimulus:
stim = Stimulus.from_irradiance(
irradiance=light_at_neurons, # mW/mm^2, [timesteps, n_gids]
dt=0.1,
)Stimulus types ā
Each stimulus carries an input_mode that tells the backend what physical quantity the array represents:
| Type | Factory | Units | Description |
|---|---|---|---|
extracellular | Stimulus(), from_extracellular() | mV | Extracellular voltage (MEA default) |
current | from_current() | nA | Direct intracellular current injection |
current_density | from_current_density() | mA/cm² | Current density normalized to membrane area |
conductance | from_conductance() | µS | Synaptic conductance (requires model E_rev) |
irradiance | from_irradiance() | mW/mm² | Light intensity for optogenetic stimulation |
Access the mode via stim.input_mode:
stim = Stimulus.from_current(np.ones((100, 10)), dt=0.1)
print(stim.input_mode) # "current"Using stimuli ā
Pass a stimulus to env.run():
it, t, *_ = env.run(duration=100, stimulus=stim)Or use it with the encoding interface for structured input pipelines:
result = env(
decoding=MeanFiringRate(duration=100),
inputs=features,
encoding=my_encoding,
)Automatic conversion ā
Stimulus.from_arg() automatically converts common types into a single Stimulus:
# From array
stim = Stimulus.from_arg(np.zeros((100, 10)))
# From tuple (array, dt)
stim = Stimulus.from_arg((np.zeros((100, 10)), 0.05))
# From dict
stim = Stimulus.from_arg({"array": np.zeros((100, 10)), "dt": 0.05})
# Pass through
stim = Stimulus.from_arg(existing_stimulus)
# None = no stimulus
stim = Stimulus.from_arg(None)