iaf_psc_exp_multisynapse#

class brainpy.state.iaf_psc_exp_multisynapse(in_size, E_L=Quantity(-70., "mV"), C_m=Quantity(250., "pF"), tau_m=Quantity(10., "ms"), t_ref=Quantity(2., "ms"), V_th=Quantity(-55., "mV"), V_reset=Quantity(-70., "mV"), tau_syn=Quantity([2.], "ms"), I_e=Quantity(0., "pA"), V_initializer=Constant(value=-70. mV), spk_fun=ReluGrad(alpha=0.3, width=1.0), spk_reset='hard', ref_var=False, name=None)#

NEST-compatible iaf_psc_exp_multisynapse neuron model.

Current-based leaky integrate-and-fire neuron with an arbitrary number of receptor-indexed exponential postsynaptic current channels.

Description

iaf_psc_exp_multisynapse mirrors NEST models/iaf_psc_exp_multisynapse.{h,cpp} and generalizes iaf_psc_exp from two fixed excitatory/inhibitory channels to n_receptors independently parameterized current ports, each with its own exponential decay time constant.

Each receptor k (1-based, NEST convention) carries its own decay constant tau_syn[k-1]. Synaptic weights are signed currents in pA; positive values are depolarizing and negative values are hyperpolarizing.

1. Continuous-Time Dynamics and Receptor States

Define \(V_{\mathrm{rel}} = V_m - E_L\). For receptor \(k\), the synaptic current decays exponentially:

\[\frac{dI_k}{dt} = -\frac{I_k}{\tau_{\mathrm{syn},k}}.\]

The membrane equation couples all receptor currents additively:

\[\frac{dV_{\mathrm{rel}}}{dt} = -\frac{V_{\mathrm{rel}}}{\tau_m} + \frac{\sum_k I_k + I_e + I_0}{C_m},\]

where \(I_0\) is the one-step delayed continuous-current buffer (NEST ring-buffer semantics). Assumptions match NEST’s current-based model: additive receptor currents, constant parameters within one simulation step, and fixed dt for exact propagator coefficients.

2. Exact Discrete Propagator, Derivation Constraints, and Stability

For step size \(h = dt\) (ms), receptor currents are integrated exactly:

\[I_{k,n+1} = P_{11,k}\, I_{k,n} + w_{k,n}, \qquad P_{11,k} = e^{-h/\tau_{\mathrm{syn},k}},\]

where \(w_{k,n}\) is the total weight arriving at receptor \(k\) during step \(n\).

The membrane update uses the exact propagator:

\[V_{\mathrm{rel},n+1} = P_{22}\, V_{\mathrm{rel},n} + P_{20}(I_e + I_{0,n}) + \sum_k P_{21,k}\, I_{k,n},\]

with propagator coefficients

\[P_{22} = e^{-h/\tau_m}, \qquad P_{20} = \frac{\tau_m}{C_m}(1 - P_{22}),\]
\[P_{21,k} = \frac{\tau_{\mathrm{syn},k}\,\tau_m} {C_m\,(\tau_m - \tau_{\mathrm{syn},k})} \left(e^{-h/\tau_m} - e^{-h/\tau_{\mathrm{syn},k}}\right).\]

propagator_exp() (from _utils) evaluates \(P_{21,k}\) with a singular-limit fallback \((h / C_m)\,e^{-h/\tau_m}\) when \(\tau_{\mathrm{syn},k} \approx \tau_m\), preventing catastrophic cancellation in the denominator \((\tau_m - \tau_{\mathrm{syn},k})\). Construction additionally rejects np.isclose(tau_syn, tau_m) to preserve robust conditioning and avoid near-degenerate parameterizations.

3. Update Order per Simulation Step (NEST Semantics)

Per-step execution order:

  1. Integrate membrane with exact propagator for neurons not refractory (\(r = 0\)).

  2. Decrement refractory counters for refractory neurons (\(r > 0\)).

  3. Decay all receptor currents \(I_k\) by \(P_{11,k}\).

  4. Inject receptor-specific spike weights \(w_{k,n}\), including default delta input mapped to receptor 1 when n_receptors > 0.

  5. Apply threshold test, hard reset, refractory assignment, record spike time, and store buffered continuous current x for step \(n+1\).

4. Assumptions, Constraints, and Computational Implications

  • C_m > 0, tau_m > 0, all tau_syn > 0, not isclose(tau_syn, tau_m), t_ref >= 0, and V_reset < V_th are enforced at construction.

  • update(x=...) uses one-step delayed current buffering: current provided at step n contributes through i_const at step n+1, matching NEST ring-buffer event semantics.

  • The update path is fully vectorized over self.varshape and scales as \(O(\prod \mathrm{varshape} \times n\_receptors)\) per call.

  • Internal propagator arithmetic is performed in NumPy float64 before writing back to BrainUnit-typed states.

  • When n_receptors == 0, all spike event inputs are silently ignored.

Parameters:
  • in_size (Size) – Population shape specification. Per-neuron parameters and state variables are broadcast/initialized over self.varshape derived from in_size.

  • E_L (ArrayLike, optional) – Resting potential \(E_L\) in mV; scalar or array broadcastable to self.varshape. Default is -70. * u.mV.

  • C_m (ArrayLike, optional) – Membrane capacitance \(C_m\) in pF; broadcastable to self.varshape and strictly positive. Default is 250. * u.pF.

  • tau_m (ArrayLike, optional) – Membrane time constant \(\tau_m\) in ms; broadcastable and strictly positive. Default is 10. * u.ms.

  • t_ref (ArrayLike, optional) – Absolute refractory period \(t_\mathrm{ref}\) in ms; broadcastable and nonnegative. Converted to integer grid steps via ceil(t_ref / dt). Default is 2. * u.ms.

  • V_th (ArrayLike, optional) – Spike threshold \(V_\mathrm{th}\) in mV; broadcastable to self.varshape. Default is -55. * u.mV.

  • V_reset (ArrayLike, optional) – Post-spike reset potential \(V_\mathrm{reset}\) in mV; broadcastable and constrained by V_reset < V_th elementwise. Default is -70. * u.mV.

  • tau_syn (ArrayLike, optional) – Synaptic decay constants in ms for all receptor ports. Converted to a 1-D float64 array of shape (n_receptors,) via np.asarray(...).reshape(-1). Every entry must be strictly positive and must not be numerically equal to tau_m under np.isclose. The number of entries defines n_receptors. Default is (2.0,) * u.ms (one receptor).

  • I_e (ArrayLike, optional) – Constant injected current \(I_e\) in pA; scalar or array broadcastable to self.varshape. Default is 0. * u.pA.

  • V_initializer (Callable, optional) – Initializer for membrane state V used by init_state(). Default is braintools.init.Constant(-70. * u.mV).

  • spk_fun (Callable, optional) – Surrogate spike function used by get_spike() and returned by update(). Default is braintools.surrogate.ReluGrad().

  • spk_reset (str, optional) – Reset policy inherited from Neuron. 'hard' reproduces NEST hard reset behavior. Default is 'hard'.

  • ref_var (bool, optional) – If True, allocates optional boolean state self.refractory for external refractory inspection. Default is False.

  • name (str or None, optional) – Optional node name passed to the parent module. Default is None.

Parameter Mapping

Table 7 Parameter mapping to model symbols#

Parameter

Type / shape / unit

Default

Math symbol

Semantics

in_size

Size; scalar or tuple

required

Defines population/state shape self.varshape.

E_L

ArrayLike, broadcastable to self.varshape (mV)

-70. * u.mV

\(E_L\)

Leak reversal (resting) potential.

C_m

ArrayLike, broadcastable (pF), > 0

250. * u.pF

\(C_m\)

Membrane capacitance in subthreshold integration.

tau_m

ArrayLike, broadcastable (ms), > 0

10. * u.ms

\(\tau_m\)

Membrane leak time constant.

t_ref

ArrayLike, broadcastable (ms), >= 0

2. * u.ms

\(t_\mathrm{ref}\)

Absolute refractory duration in physical time.

V_th and V_reset

ArrayLike, broadcastable (mV), with V_reset < V_th

-55. * u.mV, -70. * u.mV

\(V_\mathrm{th}\), \(V_\mathrm{reset}\)

Threshold and post-spike reset levels.

tau_syn

ArrayLike, flattened to (n_receptors,) (ms), each > 0 and not isclose to tau_m

(2.0,) * u.ms

\(\tau_{\mathrm{syn},k}\)

Receptor-specific exponential PSC decay constants; number of entries defines n_receptors.

I_e

ArrayLike, broadcastable (pA)

0. * u.pA

\(I_e\)

Constant current added each update step.

V_initializer

Callable

Constant(-70. * u.mV)

Initializer for membrane state V.

spk_fun

Callable

ReluGrad()

Surrogate nonlinearity used for spike output.

spk_reset

str

'hard'

Reset mode from Neuron.

ref_var

bool

False

If True, exposes boolean state self.refractory.

name

str | None

None

Optional node name.

Raises:
  • ValueError – Raised at initialization or update time if any of the following holds: - V_reset >= V_th. - C_m <= 0, tau_m <= 0, any tau_syn <= 0, or t_ref < 0. - Any tau_syn is numerically equal to tau_m under np.isclose. - A spike event receptor index is outside [1, n_receptors].

  • TypeError – If parameters or inputs are not unit-compatible with expected conversions (mV, ms, pF, pA).

  • KeyError – If simulation context entries (for example t or dt) are missing when update() is called.

  • AttributeError – If update() is called before init_state() creates required state holders.

V#

Membrane potential in mV; shape self.varshape.

Type:

brainstate.HiddenState

i_syn#

Per-receptor synaptic currents in pA; shape self.varshape + (n_receptors,).

Type:

brainstate.ShortTermState

i_const#

Buffered continuous current (pA) applied on the next simulation step. Shape self.varshape.

Type:

brainstate.ShortTermState

refractory_step_count#

Integer countdown of remaining refractory steps (jnp.int32). Shape self.varshape.

Type:

brainstate.ShortTermState

last_spike_time#

Simulation time of the most recent spike (ms). Shape self.varshape.

Type:

brainstate.ShortTermState

refractory#

Boolean refractory mask; only present when ref_var=True.

Type:

brainstate.ShortTermState

Notes

  • This implementation uses exact (analytical) integration of the linear subthreshold ODE via pre-computed propagator coefficients, matching NEST’s update precision for fixed-step simulation.

  • Continuous current input x is combined with I_e and any additional current sources registered via sum_current_inputs(); the combined value is buffered one step (NEST ring-buffer semantics).

  • Spike weights from spike_events and sum_delta_inputs are signed currents in pA: positive for depolarizing, negative for hyperpolarizing receptors.

  • Default delta input from sum_delta_inputs is routed to receptor 1 when n_receptors > 0, replicating NEST default port behavior.

  • If n_receptors == 0, all spike event inputs are silently ignored and sum_delta_inputs is discarded.

Examples

>>> import brainstate
>>> import saiunit as u
>>> from brainpy_state._nest.iaf_psc_exp_multisynapse import (
...     iaf_psc_exp_multisynapse,
... )
>>> with brainstate.environ.context(dt=0.1 * u.ms):
...     neu = iaf_psc_exp_multisynapse(
...         in_size=2,
...         tau_syn=(2.0, 8.0) * u.ms,
...         I_e=180.0 * u.pA,
...     )
...     neu.init_state()
...     with brainstate.environ.context(t=0.0 * u.ms):
...         spk = neu.update(
...             spike_events=[{'receptor_type': 2, 'weight': 35.0 * u.pA}]
...         )
...     _ = spk.shape
>>> import brainstate
>>> import saiunit as u
>>> from brainpy_state._nest.iaf_psc_exp_multisynapse import (
...     iaf_psc_exp_multisynapse,
... )
>>> with brainstate.environ.context(dt=0.1 * u.ms):
...     neu = iaf_psc_exp_multisynapse(in_size=1, tau_syn=(2.0,) * u.ms)
...     neu.init_state()
...     with brainstate.environ.context(t=0.0 * u.ms):
...         _ = neu.update(x=250.0 * u.pA)
...     with brainstate.environ.context(t=0.1 * u.ms):
...         spk_next = neu.update()
...     _ = spk_next

References

See also

iaf_psc_exp

LIF with two fixed exponential PSC channels (exc/inh)

iaf_psc_alpha_multisynapse

Multisynapse variant with alpha-shaped PSCs

iaf_psc_delta

LIF neuron with delta-function PSCs (voltage-jump synapses)

LIF

Leaky integrate-and-fire (brainpy parameterization)

get_spike(V=None)[source]#

Evaluate surrogate spike activation for a voltage tensor.

Scales the voltage relative to threshold and reset to compute a dimensionless argument passed to the surrogate nonlinearity self.spk_fun:

\[\text{out} = \mathrm{spk\_fun}\!\left( \frac{V - V_\mathrm{th}}{V_\mathrm{th} - V_\mathrm{reset}} \right).\]
Parameters:

V (ArrayLike or None, optional) – Membrane voltage in mV, broadcast-compatible with self.varshape. If None, self.V.value is used.

Returns:

out – Surrogate spike output from self.spk_fun with the same shape as V (or self.V.value when V is None). Positive values indicate a spike; the argument to spk_fun is positive when \(V > V_\mathrm{th}\).

Return type:

dict

Raises:

TypeError – If V cannot participate in arithmetic with membrane parameters due to incompatible dtype or unit.

init_state(**kwargs)[source]#

Initialize membrane potential and all synaptic/refractory states.

Parameters:

**kwargs (Any) – Unused compatibility arguments; accepted for interface consistency with other nodes.

Raises:
  • ValueError – If V_initializer output cannot be broadcast to the target state shape.

  • TypeError – If initializer values are incompatible with required numeric/unit conversions.

property n_receptors#

Number of independent synaptic receptor ports.

Returns:

out – Length of self.tau_syn; equals len(tau_syn) as supplied at construction.

Return type:

int

update(x=Quantity(0., 'pA'), spike_events=None, w_by_rec=None)[source]#

Advance the neuron state by one simulation step.

Executes the full NEST-compatible per-step update: exact membrane propagation for non-refractory neurons, receptor current decay and spike injection, threshold/reset/refractory logic, and buffered current storage.

Parameters:
  • x (ArrayLike, optional) – Continuous current input in pA for this step. x is accumulated through sum_current_inputs() (which also adds any registered projection currents) and stored in i_const for use on the next step, matching NEST ring-buffer semantics. Scalar or array broadcastable to self.varshape. Default is 0. * u.pA.

  • spike_events (iterable or None, optional) –

    Receptor-indexed spike weight events to inject this step. Each entry must be either:

    • A (receptor_type, weight) tuple where receptor_type is a 1-based integer in [1, n_receptors] and weight is a scalar or array in pA broadcastable to self.varshape.

    • A dict with keys 'receptor_type' (or 'receptor') and 'weight'.

    Multiple events for the same receptor are accumulated additively. None injects no receptor spike events. Default is None. Ignored when w_by_rec is provided.

  • w_by_rec (array-like or None, optional) – Pre-computed per-receptor spike weights in pA (dimensionless), shape broadcastable to self.varshape + (n_receptors,). When provided, bypasses spike_events parsing and sum_delta_inputs, making the update JIT-compatible for use inside brainstate.transform.for_loop. Default is None.

Returns:

out – Surrogate spike output from get_spike() with shape self.V.value.shape. For neurons that fire this step, the voltage argument to get_spike() is nudged \(\theta + E_L + 10^{-12}\) mV (above threshold) to ensure a positive surrogate activation is returned even after the hard voltage reset.

Return type:

jax.Array

Raises:
  • ValueError – If any receptor index in spike_events is outside [1, n_receptors].

  • KeyError – If the simulation environment context does not supply t or dt.

  • AttributeError – If state variables are missing because init_state() has not been called before update.

  • TypeError – If x or stored states are not unit-compatible with expected pA / mV arithmetic.

  • ValueError – If provided inputs cannot be broadcast to the internal state shape.