Skip to content

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:

python
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 targets

Properties ​

PropertyTypeDescription
arrayfloat[T, N]Stimulation values over time
dtfloatTimestep in milliseconds
gidsint[N]Target neuron IDs (optional)
durationfloatTotal duration in ms (T Ɨ dt)
meta_datadictAdditional metadata

Creating stimuli ​

From arrays ​

Pass a NumPy array directly:

python
# 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:

python
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.

python
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:

python
env.run(20_000)                # free-running, nothing allocated
env.run(1_000, stimulus=...)   # only allocated here

To 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:

python
stim = Stimulus.from_conductance(
    conductance=np.random.rand(100, 10) * 0.01,  # µS
    dt=0.1,
)

From current injection ​

For direct current injection:

python
stim = Stimulus.from_current(
    current=np.ones((100, 10)) * 0.5,  # nA
    dt=0.1,
)

From current density ​

For current density injection:

python
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:

python
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:

python
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:

TypeFactoryUnitsDescription
extracellularStimulus(), from_extracellular()mVExtracellular voltage (MEA default)
currentfrom_current()nADirect intracellular current injection
current_densityfrom_current_density()mA/cm²Current density normalized to membrane area
conductancefrom_conductance()µSSynaptic conductance (requires model E_rev)
irradiancefrom_irradiance()mW/mm²Light intensity for optogenetic stimulation

Access the mode via stim.input_mode:

python
stim = Stimulus.from_current(np.ones((100, 10)), dt=0.1)
print(stim.input_mode)  # "current"

Using stimuli ​

Pass a stimulus to env.run():

python
it, t, *_ = env.run(duration=100, stimulus=stim)

Or use it with the encoding interface for structured input pipelines:

python
result = env(
    decoding=MeanFiringRate(duration=100),
    inputs=features,
    encoding=my_encoding,
)

Automatic conversion ​

Stimulus.from_arg() automatically converts common types into a single Stimulus:

python
# 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)

Released under the MIT License.