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_detectordecodes binary activity (state\(\in \{0, 1\}\)) from spike-event multiplicities and stores a chronological event log containingsenders,times, and decodedstatefor every emitted event. The decode logic mirrors NESTmodels/spin_detector.{h,cpp}with explicit per-event buffering: a single provisional event is held in a one-slot buffer and revised from state0to1before being written whenever a same-sender, same-stamp event with multiplicity1arrives, while multiplicity2events bypass the buffer and are written immediately as state1.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
1and clear the buffer.Otherwise, set buffer to current event with provisional state
0when the buffer is empty; if the buffer is not empty, clear it instead.
This ordering ensures that a possible
0 -> 1revision 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
startbound is exclusive andstopis inclusive.3. Input Normalization and Multiplicity Inference
Runtime
updatearrays are flattened to one-dimensional vectors of length \(N\). Scalars forsenders,offsets, andstamp_stepsare broadcast to \((N,)\).Let \(a_j = \mathrm{spikes}[j]\). Per-item event multiplicity \(c_j\) is determined as follows:
If
multiplicities is Noneand all \(a_j\) are integer-like (within1e-12tolerance): \(c_j = \max(\mathrm{round}(a_j),\, 0)\).If
multiplicities is Noneand any \(a_j\) is non-integer: \(c_j = \mathbf{1}[a_j > 0]\) (binary threshold).If
multiplicitiesis 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), andoriginmust be scalar-convertible and aligned to the simulation lattice. Alignment is verified by round-trip integer checks with1e-12tolerance. Perupdate()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 bybrainstate.nn.Dynamics. The detector is event-driven and does not return dense tensors, soin_sizeis retained for API compatibility only. Default is1.start (
saiunit.Quantityorfloat, optional) – Scalar relative exclusive lower bound of the recording window, convertible to ms. Must be finite and an integer multiple ofdt. The effective gate isstamp_step > (origin + start) / dt. Default is0.0 * u.ms.stop (
saiunit.Quantity,float, orNone, optional) – Scalar relative inclusive upper bound of the recording window, convertible to ms. Must beNoneor finite and aligned todt. Must satisfystop >= startwhen notNone. The effective gate isstamp_step <= (origin + stop) / dt.Nonemeans no upper bound (\(s_{\max} = +\infty\)). Default isNone.origin (
saiunit.Quantityorfloat, optional) – Scalar global time-origin shift added to bothstartandstopwhen constructing the active window, convertible to ms. Shifting the origin displaces the entire recording window without changing its duration. Must be finite and aligned todt. Default is0.0 * u.ms.time_in_steps (
bool, optional) – Controls the time representation inevents. IfFalse,events['times']storesfloat64milliseconds computed as \(s \cdot dt - \delta_j\). IfTrue,events['times']stores integer step stamps (int64) andevents['offsets']stores the correspondingfloat64offsets in ms. Becomes immutable after the firstupdate()call. Default isFalse.frozen (
bool, optional) – NEST-compatibility flag.Trueis unconditionally rejected because this device cannot be frozen. Default isFalse.name (
strorNone, optional) – Optional node name forwarded tobrainstate.nn.Dynamics. Default isNone.
Parameter Mapping
Table 44 Mapping of constructor parameters to model symbols# Parameter
Default
Math symbol
Semantics
start0.0 * u.ms\(t_{\mathrm{start,rel}}\)
Relative exclusive lower bound of the recording window.
stopNone\(t_{\mathrm{stop,rel}}\)
Relative inclusive upper bound of the recording window.
origin0.0 * u.ms\(t_0\)
Global shift applied to both window boundaries.
time_in_stepsFalse\(\mathrm{repr}_t\)
Output-time representation: ms float or integer
(step, offset)pair.- Raises:
ValueError – If
frozen=True; ifdtis non-positive; if time parameters are non-scalar, non-finite where finite values are required, misaligned to the simulation step, or violatestop >= start; iftis not on the simulation grid; iftime_in_stepsis modified after simulation begins; ifn_eventsis set to any value other than0; if provided arrays have inconsistent sizes; ifspikes/offsetscontain non-finite values; or if explicitmultiplicitiescontain 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'ordt) are unavailable viabrainstate.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_stepsbecomes immutable after the firstupdate()call that accesses simulation context, matching NEST backend constraints.NEST semantics are defined for multiplicities
1and2. This implementation also accepts other non-negative values, which follow them != 2branch in_handle_event().spikes=Noneis a no-op that flushes the buffer and returns the currenteventswithout 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
1before 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 — multiplicity2events are written immediately with state1:>>> 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])
- 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
tand resolutiondtfrombrainstate.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 (
ArrayLikeorNone, 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 currenteventsdict is returned immediately.Integer-like values (all within
1e-12of an integer) withmultiplicities 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 (
ArrayLikeorNone, optional) – Sender node IDs cast toint64, shape(N,)or scalar broadcastable to(N,). Default sender ID is1for all entries whenNone.offsets (
ArrayLikeorNone, optional) – Per-event sub-step timing offsets \(\delta_j\) in ms, shape(N,)or scalar broadcastable to(N,). Values may carry asaiunittime unit and are converted to ms. Must contain only finite values. Default is0.0 * u.msfor all entries.multiplicities (
ArrayLikeorNone, optional) – Explicit non-negative integer event multiplicities cast toint64, shape(N,)or scalar broadcastable to(N,). When provided, the integer-like inference path fromspikesis disabled; the count rule becomes \(c_j = m_j \,\mathbf{1}[a_j > 0]\). Negative values raiseValueError. Default isNone.stamp_steps (
ArrayLikeorNone, optional) – Explicit integer step stamps \(s_j\) for each event, cast toint64, shape(N,)or scalar broadcastable to(N,). WhenNone, 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 isNone.
- 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'—float64ms whentime_in_steps=False;int64step stamps whentime_in_steps=True.'offsets'—float64ms, shape(E,)(only present whentime_in_steps=True).
- Return type:
dict[str,np.ndarray]- Raises:
ValueError – If
tis not grid-aligned todt; ifstart,stop, ororiginare invalid with respect todt; ifdt <= 0; if provided payload array sizes are incompatible with theNinferred fromspikes; ifoffsetscontain non-finite values; or if explicitmultiplicitiescontain negative entries.TypeError – If numeric or unit conversion of any payload or time parameter fails.
KeyError – If required simulation context entries (
't'ordt) are not available viabrainstate.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.