spin_detector#

class brainpy.state.spin_detector(in_size=1, start=Quantity(0., 'ms'), stop=None, origin=Quantity(0., 'ms'), time_in_steps=False, frozen=False, name=None)#

NEST-compatible detector for binary state decoding from spikes.

spin_detector decodes binary activity (state \(\in \{0, 1\}\)) from spike-event multiplicities and stores a chronological event log containing senders, times, and decoded state for every emitted event. The decode logic mirrors NEST models/spin_detector.{h,cpp} with explicit per-event buffering: a single provisional event is held in a one-slot buffer and revised from state 0 to 1 before being written whenever a same-sender, same-stamp event with multiplicity 1 arrives, while multiplicity 2 events bypass the buffer and are written immediately as state 1.

1. Event Decoding on a Sender-Time Lattice

Let incoming normalized events be \(e_j=(i_j, s_j, \delta_j, m_j)\) with sender \(i_j \in \mathbb{N}\), step stamp \(s_j \in \mathbb{Z}\), offset \(\delta_j\) (ms), and multiplicity \(m_j \ge 0\). The detector maintains one buffered tuple \(b=(i_b, s_b, \delta_b, x_b)\) where \(x_b \in \{0,1\}\) is the provisional decoded state.

For each accepted event in order:

  • If \(m_j = 1\) and \((i_j, s_j) = (i_b, s_b)\), revise \(x_b \leftarrow 1\) before writing.

  • If a buffer exists, write \(b\) to output.

  • If \(m_j = 2\), write current event immediately with state 1 and clear the buffer.

  • Otherwise, set buffer to current event with provisional state 0 when the buffer is empty; if the buffer is not empty, clear it instead.

This ordering ensures that a possible 0 -> 1 revision is applied before the buffered-write emission, exactly as in the NEST C++ reference.

2. Time Model and Activity Window

With simulation resolution \(dt > 0\) (ms), current simulation time \(t\), and step index \(n = \mathrm{round}(t/dt)\), the default event stamp for events received at step \(n\) is

\[s = n + 1.\]

The physical event time in milliseconds is reconstructed as

\[t_{\mathrm{event}} = s \cdot dt - \delta.\]

Recording is gated on stamps by the half-open interval \((s_{\min},\, s_{\max}]\) where

\[s_{\min} = \frac{\mathrm{origin} + \mathrm{start}}{dt}, \qquad s_{\max} = \frac{\mathrm{origin} + \mathrm{stop}}{dt} \quad (\text{or } +\infty \text{ when stop is None}),\]

so an event is accepted iff \(s > s_{\min} \land s \le s_{\max}\). The start bound is exclusive and stop is inclusive.

3. Input Normalization and Multiplicity Inference

Runtime update arrays are flattened to one-dimensional vectors of length \(N\). Scalars for senders, offsets, and stamp_steps are broadcast to \((N,)\).

Let \(a_j = \mathrm{spikes}[j]\). Per-item event multiplicity \(c_j\) is determined as follows:

  • If multiplicities is None and all \(a_j\) are integer-like (within 1e-12 tolerance): \(c_j = \max(\mathrm{round}(a_j),\, 0)\).

  • If multiplicities is None and any \(a_j\) is non-integer: \(c_j = \mathbf{1}[a_j > 0]\) (binary threshold).

  • If multiplicities is provided with non-negative integers \(m_j\): \(c_j = m_j \,\mathbf{1}[a_j > 0]\).

Each event item contributes at most one decode step because \(c_j\) is passed as the multiplicity to _handle_event() rather than used for repeated writes.

4. Assumptions, Constraints, and Computational Implications

dt, t, start, stop (when finite), and origin must be scalar-convertible and aligned to the simulation lattice. Alignment is verified by round-trip integer checks with 1e-12 tolerance. Per update() call, normalization is \(O(N)\) and decoding is \(O(N)\), with persistent storage cost linear in the total number of emitted events \(E\).

Parameters:
  • in_size (Size, optional) – Shape/size metadata consumed by brainstate.nn.Dynamics. The detector is event-driven and does not return dense tensors, so in_size is retained for API compatibility only. Default is 1.

  • start (saiunit.Quantity or float, optional) – Scalar relative exclusive lower bound of the recording window, convertible to ms. Must be finite and an integer multiple of dt. The effective gate is stamp_step > (origin + start) / dt. Default is 0.0 * u.ms.

  • stop (saiunit.Quantity, float, or None, optional) – Scalar relative inclusive upper bound of the recording window, convertible to ms. Must be None or finite and aligned to dt. Must satisfy stop >= start when not None. The effective gate is stamp_step <= (origin + stop) / dt. None means no upper bound (\(s_{\max} = +\infty\)). Default is None.

  • origin (saiunit.Quantity or float, optional) – Scalar global time-origin shift added to both start and stop when constructing the active window, convertible to ms. Shifting the origin displaces the entire recording window without changing its duration. Must be finite and aligned to dt. Default is 0.0 * u.ms.

  • time_in_steps (bool, optional) – Controls the time representation in events. If False, events['times'] stores float64 milliseconds computed as \(s \cdot dt - \delta_j\). If True, events['times'] stores integer step stamps (int64) and events['offsets'] stores the corresponding float64 offsets in ms. Becomes immutable after the first update() call. Default is False.

  • frozen (bool, optional) – NEST-compatibility flag. True is unconditionally rejected because this device cannot be frozen. Default is False.

  • name (str or None, optional) – Optional node name forwarded to brainstate.nn.Dynamics. Default is None.

Parameter Mapping

Table 44 Mapping of constructor parameters to model symbols#

Parameter

Default

Math symbol

Semantics

start

0.0 * u.ms

\(t_{\mathrm{start,rel}}\)

Relative exclusive lower bound of the recording window.

stop

None

\(t_{\mathrm{stop,rel}}\)

Relative inclusive upper bound of the recording window.

origin

0.0 * u.ms

\(t_0\)

Global shift applied to both window boundaries.

time_in_steps

False

\(\mathrm{repr}_t\)

Output-time representation: ms float or integer (step, offset) pair.

Raises:
  • ValueError – If frozen=True; if dt is non-positive; if time parameters are non-scalar, non-finite where finite values are required, misaligned to the simulation step, or violate stop >= start; if t is not on the simulation grid; if time_in_steps is modified after simulation begins; if n_events is set to any value other than 0; if provided arrays have inconsistent sizes; if spikes/offsets contain non-finite values; or if explicit multiplicities contain negative entries.

  • TypeError – If unit conversion or numeric coercion of scalar/array inputs fails.

  • KeyError – If get() is called with an unsupported key, or if required simulation context values ('t' or dt) are unavailable via brainstate.environ.

Notes

  • Input events are processed strictly in the order supplied, and one buffered event is finalized at the end of every update() call.

  • Connection weight and delay do not participate in decode logic.

  • time_in_steps becomes immutable after the first update() call that accesses simulation context, matching NEST backend constraints.

  • NEST semantics are defined for multiplicities 1 and 2. This implementation also accepts other non-negative values, which follow the m != 2 branch in _handle_event().

  • spikes=None is a no-op that flushes the buffer and returns the current events without writing any new events.

  • init_state() clears all accumulated events and the one-slot buffer; it can be used to reset the detector between simulation segments without reconstructing the object.

References

Examples

Detect binary state for two same-sender, same-stamp events — the second event (multiplicity 1, matching sender and stamp) upgrades the state to 1 before the buffered event is written:

>>> import brainpy
>>> import brainstate
>>> import saiunit as u
>>> import numpy as np
>>> with brainstate.environ.context(dt=0.1 * u.ms):
...     det = brainpy.state.spin_detector(start=0.0 * u.ms, stop=1.0 * u.ms)
...     with brainstate.environ.context(t=0.0 * u.ms):
dftype = brainstate.environ.dftype()
ditype = brainstate.environ.ditype()
...         _ = det.update(
...             spikes=np.array([1.0, 1.0], dtype=dftype),
...             senders=np.array([7, 7], dtype=ditype),
...             stamp_steps=np.array([1, 1], dtype=ditype),
...         )
...     ev = det.flush()
...     _ = (ev['senders'][0], ev['state'][0])

Record a multiplicity-2 event with a sub-step offset using time_in_steps=True, which splits the timestamp into an integer step index and a float offset — multiplicity 2 events are written immediately with state 1:

>>> import brainpy
>>> import brainstate
>>> import saiunit as u
>>> import numpy as np
>>> with brainstate.environ.context(dt=0.1 * u.ms):
...     det = brainpy.state.spin_detector(time_in_steps=True)
...     with brainstate.environ.context(t=0.0 * u.ms):
...         _ = det.update(
...             spikes=np.array([2.0], dtype=dftype),
...             senders=np.array([3], dtype=ditype),
...             offsets=np.array([0.02], dtype=dftype) * u.ms,
...         )
...     ev = det.events
...     _ = (ev['times'][0], ev['offsets'][0], ev['state'][0])
init_state(batch_size=None, **kwargs)[source]#

State initialization function.

update(spikes=None, senders=None, offsets=None, multiplicities=None, stamp_steps=None)[source]#

Decode binary states from spike events for the current simulation step.

Reads the current simulation time t and resolution dt from brainstate.environ, derives the default stamp step \(s = \mathrm{round}(t/dt) + 1\), normalizes the input arrays, applies the activity-window gate, and passes each accepted event through the one-slot decode buffer via _handle_event(). After all items are processed, _flush_last_event() finalizes any remaining buffered event.

Parameters:
  • spikes (ArrayLike or None, optional) –

    Input spike payload, flattened to shape (N,). Accepted dtypes include boolean, integer, and floating-point values.

    • None: no new events are processed; the buffer is flushed and the current events dict is returned immediately.

    • Integer-like values (all within 1e-12 of an integer) with multiplicities is None: each element \(j\) contributes multiplicity \(c_j = \max(\mathrm{round}(a_j),\, 0)\).

    • Non-integer floating values with multiplicities is None: each element contributes \(c_j = \mathbf{1}[a_j > 0]\) (binary threshold).

  • senders (ArrayLike or None, optional) – Sender node IDs cast to int64, shape (N,) or scalar broadcastable to (N,). Default sender ID is 1 for all entries when None.

  • offsets (ArrayLike or None, optional) – Per-event sub-step timing offsets \(\delta_j\) in ms, shape (N,) or scalar broadcastable to (N,). Values may carry a saiunit time unit and are converted to ms. Must contain only finite values. Default is 0.0 * u.ms for all entries.

  • multiplicities (ArrayLike or None, optional) – Explicit non-negative integer event multiplicities cast to int64, shape (N,) or scalar broadcastable to (N,). When provided, the integer-like inference path from spikes is disabled; the count rule becomes \(c_j = m_j \,\mathbf{1}[a_j > 0]\). Negative values raise ValueError. Default is None.

  • stamp_steps (ArrayLike or None, optional) – Explicit integer step stamps \(s_j\) for each event, cast to int64, shape (N,) or scalar broadcastable to (N,). When None, all events are stamped at \(s = n + 1\) where \(n = \mathrm{round}(t/dt)\). Providing custom stamps allows events generated at different simulation steps to be injected in a single call. Default is None.

Returns:

events – Current accumulated events dictionary after processing this step. All arrays are one-dimensional with length \(E\) equal to the total number of stored events:

  • 'senders'int64, shape (E,).

  • 'state'int64, shape (E,): decoded binary state (\(0\) or \(1\)).

  • 'times'float64 ms when time_in_steps=False; int64 step stamps when time_in_steps=True.

  • 'offsets'float64 ms, shape (E,) (only present when time_in_steps=True).

Return type:

dict[str, np.ndarray]

Raises:
  • ValueError – If t is not grid-aligned to dt; if start, stop, or origin are invalid with respect to dt; if dt <= 0; if provided payload array sizes are incompatible with the N inferred from spikes; if offsets contain non-finite values; or if explicit multiplicities contain negative entries.

  • TypeError – If numeric or unit conversion of any payload or time parameter fails.

  • KeyError – If required simulation context entries ('t' or dt) are not available via brainstate.environ.

Notes

Events are stamped at \(s = \mathrm{round}(t/dt) + 1\) by default and then gated by the active window \((s_{\min},\, s_{\max}]\) in step space. Events outside the window are discarded before reaching _handle_event(). The one-slot buffer is always flushed at the end of each call regardless of how many new events were processed.