spyx.nn
Spiking-neuron layers (IF, LIF, ALIF, CuBaLIF and recurrent variants), the stateful Sequential container, and the time-major run scan helper.
PSU_LIF (documented below) is a reset-free parallel spiking neuron: a pure linear leaky integrator V_t = clip(beta)·V_{t-1} + x_t that exposes both the standard stepwise __call__ and a parallel(x) associative-scan path with O(log T) depth. See the parallel spiking neurons explanation for the sequential-vs-parallel trade-off and the benchmarking how-to to measure it.
ALIF
Bases: Module
Adaptive LIF Neuron based on the model used in LSNNs:
Bellec, G., Salaj, D., Subramoney, A., Legenstein, R. & Maass, Maass, W. Long short- term memory and learning-to-learn in networks of spiking neurons. 32nd Conference on Neural Information Processing Systems (2018).
Source code in spyx/nn.py
__call__(x, VT)
Tensor from previous layer.
:VT: Neuron state vector.
Source code in spyx/nn.py
__init__(hidden_shape, beta=None, gamma=None, threshold=1, activation=None, *, rngs)
:hidden_shape: Hidden layer shape. :beta: Membrane decay/inverse time constant. :gamma: Threshold adaptation constant. :threshold: Neuron firing threshold. :activation: spyx.axn.Axon object determining forward function and surrogate gradient function.
Source code in spyx/nn.py
ActivityRegularization
Bases: Module
Track the cumulative number of spikes emitted per neuron per batch.
The running spike count is threaded through :func:spyx.nn.run (and
:class:Sequential) as part of the scan carry, exactly like a neuron's
membrane state: :meth:initial_state seeds a zero buffer and each
:meth:__call__ returns the incoming spikes unchanged plus the updated
count. The final accumulated count comes back as this layer's entry in the
final_state returned by run, and can be fed to
spyx.fn.silence_reg / spyx.fn.sparsity_reg for activity penalties.
Threading the count through the carry (rather than mutating an
nnx.Variable in place) is what lets it accumulate inside the raw
jax.lax.scan used by :func:spyx.nn.run, where in-place variable
mutation raises TraceContextError.
Source code in spyx/nn.py
__call__(spikes, spike_count)
:spikes: Spikes emitted by the previous layer at this timestep.
:spike_count: Running per-neuron spike count carried through the scan.
:return: (spikes, spike_count + spikes) -- the spikes pass through
unchanged while the count accumulates.
Source code in spyx/nn.py
__init__(hidden_shape, batch_size=1, dtype=jnp.float32)
:hidden_shape: Per-neuron shape of the layer being regularized. :batch_size: Leading batch dimension of the spike-count buffer. :dtype: Storage dtype for the spike-count buffer.
Source code in spyx/nn.py
AssociativeLIF
Bases: PSU_LIF
Reset-free parallel LIF, named for snnTorch cross-referencing.
.. note::
Experimental. Supported entry point:
:class:spyx.experimental.AssociativeLIF. Thin alias of
:class:PSU_LIF — same __init__ signature, same
(x, V) -> (spikes, V) step contract, same :meth:parallel
associative scan. It exists purely for discoverability by users
coming from snnTorch.
Parity target. This is at exact numeric parity (float32, verified to
< 1e-6 max-abs membrane difference; spike trains identical) with
snnTorch v1.0.0 snntorch.StateLeaky — the reset-free scalar parallel
leaky integrator — after a single beta reparameterisation (see below).
.. warning::
Despite the name, this does not replicate snnTorch's
snntorch.AssociativeLeaky. That neuron is not a leaky
integrate-and-fire unit at all: it is a matrix-valued associative-memory
SSM (linear-attention / fast-weight style) that forms key/value outer
products, decays a matrix state in log-space, and reads out
S_t @ Q_t with input-dependent per-column decay. A scalar LIF cannot
express it; that behaviour would be a separate linear-attention module.
Decay reparameterisation. PSU_LIF uses beta directly as the
per-step decay, :math:V_t = \beta V_{t-1} + x_t. StateLeaky instead
reads beta as a continuous time constant :math:\tau = 1/(1-\beta) and
builds a kernel :math:h[t] = e^{-t/\tau}, so its effective per-step
decay is :math:e^{-1/\tau} = e^{-(1-\beta_\text{snn})}. The same symbol
means different things: beta_snn = 0.9 gives an effective decay of
0.9048, not 0.9. Use :meth:beta_from_snntorch to convert a
snnTorch beta into the spyx beta that reproduces StateLeaky exactly,
and :meth:snntorch_beta_from_beta for the inverse.
Source code in spyx/nn.py
beta_from_snntorch(beta_snn)
staticmethod
Convert a snnTorch StateLeaky beta into the spyx beta.
StateLeaky treats beta as a time constant
:math:\tau = 1/(1 - \beta_\text{snn}) and uses an effective per-step
decay :math:e^{-1/\tau} = e^{-(1 - \beta_\text{snn})}. Passing the
returned value as this class's beta makes the membrane trace (and
therefore the spikes) match StateLeaky exactly.
:beta_snn: snnTorch StateLeaky beta in (0, 1).
:return: the equivalent spyx per-step decay beta.
Source code in spyx/nn.py
snntorch_beta_from_beta(beta)
staticmethod
Inverse of :meth:beta_from_snntorch.
Recovers the snnTorch StateLeaky beta from a spyx per-step decay
via :math:\beta_\text{snn} = 1 + \ln \beta.
:beta: spyx per-step decay beta in (0, 1].
:return: the equivalent snnTorch StateLeaky beta.
Source code in spyx/nn.py
Flatten
Bases: Module
Flatten every non-batch dimension of a per-timestep input.
Stateless: maps x of shape (B, ...) to (B, prod(...)). It has no
initial_state, so :class:Sequential runs it in stateless mode. Used by
:mod:spyx.nir to represent NIR Flatten nodes; flax.nnx has no
built-in flatten layer.
Source code in spyx/nn.py
IF
Bases: Module
Integrate and Fire neuron model.
Source code in spyx/nn.py
__call__(x, V)
Vector coming from previous layer.
:V: Neuron state tensor.
__init__(hidden_shape, threshold=1, activation=None, *, rngs=None)
:hidden_shape: Shape of the layer.
:threshold: threshold for reset. Defaults to 1.
:activation: spyx.activation function.
:rngs: Accepted and ignored — IF is parameterless, but taking rngs
keeps it drop-in interchangeable with the parametric neurons
(LIF, CuBaLIF, ...) that require it.
Source code in spyx/nn.py
LI
Bases: Module
Leaky-Integrate (Non-spiking) neuron model.
Source code in spyx/nn.py
__call__(x, Vin)
__init__(layer_shape, beta=None, *, rngs)
:layer_shape: Shape of the layer. :beta: Decay rate on membrane potential (voltage).
Source code in spyx/nn.py
LIF
Bases: Module
Leaky Integrate and Fire neuron model.
Source code in spyx/nn.py
__call__(x, V)
input vector coming from previous layer.
:V: neuron state tensor.
Source code in spyx/nn.py
__init__(hidden_shape, beta=None, threshold=1.0, activation=None, *, rngs)
:hidden_shape: Shape of the layer. :beta: decay rate. :threshold: threshold for reset. Defaults to 1. :activation: spyx.axn.Axon object.
Source code in spyx/nn.py
PSU_LIF
Bases: Module
Parallel Spiking Unit LIF: a reset-free leaky integrate-and-fire neuron.
.. note::
Experimental. Its supported entry point is
:class:spyx.experimental.PSU_LIF; the API may change without a
deprecation cycle. It is defined here for locality with the other neurons.
A standard :class:LIF subtracts a reset spikes * threshold from the
membrane every step, which couples each timestep to the (nonlinear) spike
of the previous step and forces a strictly sequential O(T) scan.
Dropping the reset turns the membrane into a pure linear leaky integrator,
.. math:: V_t = \beta \, V_{t-1} + x_t ,
which is a first-order associative recurrence and can therefore be
evaluated with :func:jax.lax.associative_scan in O(\log T) parallel
depth on an accelerator. Spikes are a pointwise surrogate threshold applied
to the whole membrane trace, :math:s_t = \sigma(V_t - \text{threshold}).
Removing the reset is a deliberate accuracy/parallelism trade-off: the neuron never depresses after firing, so it can fire on consecutive steps while a well-tuned integration window keeps activity bounded. In exchange the sequence can be scored in logarithmic instead of linear depth.
Two execution modes are provided and are numerically identical:
- :meth:
__call__-- one reset-free timestep(x, V) -> (spikes, V)withV = beta * V + x; a drop-in for :func:spyx.nn.run, :class:Sequential, and NIR, exactly like :class:LIF. - :meth:
parallel-- the whole time-major sequence at once via an associative scan over the leak,O(\log T)depth.
Because both modes use the same clipped beta and the same surrogate,
and :meth:__call__ integrates the input before spiking, scanning
:meth:__call__ over x reproduces :meth:parallel exactly.
Source code in spyx/nn.py
237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 | |
__call__(x, V)
One reset-free timestep.
input vector coming from previous layer.
:V: neuron state tensor.
Integrates the input into the membrane (V = beta * V + x, no
reset), then emits a surrogate spike on the updated membrane so that
scanning this method matches :meth:parallel exactly.
Source code in spyx/nn.py
__init__(hidden_shape, beta=None, threshold=1.0, activation=None, *, rngs)
:hidden_shape: Shape of the layer. :beta: decay rate. Scalar if provided, else learnable per-unit init. :threshold: firing threshold. Defaults to 1. :activation: spyx.axn.Axon object determining the surrogate spike.
Source code in spyx/nn.py
parallel(x)
Score a whole time-major sequence with an associative scan.
input with shape
[Time, Batch, ...].
:return: spikes with shape [Time, Batch, ...].
Computes the full membrane trace V_t = beta * V_{t-1} + x_t (with
V_{-1} = 0) via :func:jax.lax.associative_scan over the time axis
in O(\log T) depth, then applies the surrogate spike pointwise.
Source code in spyx/nn.py
RIF
Bases: Module
Recurrent Integrate and Fire neuron model.
Source code in spyx/nn.py
__call__(x, V)
Vector coming from previous layer.
:V: Neuron state tensor.
Source code in spyx/nn.py
RLIF
Bases: Module
Recurrent LIF Neuron.
Source code in spyx/nn.py
__call__(x, V)
The input data/latent vector from another layer.
:V: The state tensor.
Source code in spyx/nn.py
Sequential
Bases: Sequential
A Sequential container that supports passing state through its layers.
Source code in spyx/nn.py
StatefulLayer
Bases: Protocol
The contract every Spyx neuron/stateful layer follows.
This is a documentation aid, not an enforced base class — Spyx neurons
are plain :class:flax.nnx.Module subclasses and do not inherit from
this Protocol. It captures, in one place, the two-method contract that
:func:run, :class:Sequential, and :mod:spyx.nir rely on:
initial_state(batch_size)returns a fresh zero state for a batch of the given size (the leading axis is the batch dimension).__call__(x, state)advances one timestep, returning(out, new_state)wherenew_statehas the same structure asstateso it can be threaded through :func:jax.lax.scan.
Because it is @runtime_checkable, isinstance(layer, StatefulLayer)
checks for the presence of these methods (not their signatures), which is
handy in tests. New neurons should match this shape so they drop straight
into :class:Sequential and :func:run.
Source code in spyx/nn.py
SumPool
Bases: Module
Sum pool.
Source code in spyx/nn.py
run(model, x, state=None, *, batch_major=False)
Execute a model over a sequence of inputs using jax.lax.scan.
:model: A stateful Flax NNX Module, typically :class:Sequential or a
Spyx neuron following the :class:StatefulLayer contract. It must
either take (x_t, state) -> (out, next_state) or expose an
initial_state(batch_size) method (or both). Plain stateless modules
like nnx.Linear don't fit the contract — wrap them in a
:class:Sequential with at least one stateful layer, or use
jax.vmap if you just want to apply the module per timestep.
Input data. By default this is time-major
[Time, Batch, ...]
(jax.lax.scan walks the leading axis). Pass batch_major=True if
your data is [Batch, Time, ...] instead.
:state: Initial state for the model. If None, model.initial_state is
consulted; if the model has no initial_state and no state is
supplied explicitly, a clear error is raised.
:batch_major: When True, x is treated as [Batch, Time, ...]:
it is transposed to time-major internally for the scan and the outputs
are transposed back to [Batch, Time, ...]. Default False
preserves the historical time-major behaviour.
:return: (outputs, final_state). outputs is time-major
[Time, Batch, ...] by default, or [Batch, Time, ...] when
batch_major=True.
.. note::
Mind the time axis when computing losses. run is time-major by
default (time on axis 0), whereas the :mod:spyx.fn losses/metrics
default to time_axis=1 (batch-major, [Batch, Time, Classes]).
Feeding time-major run outputs straight into an fn loss reduces
over the batch axis instead of time — silently wrong, and
undetectable when Time == Batch. Pick one of:
* call ``run(..., batch_major=True)`` so outputs are ``[Batch, Time,
...]`` and line up with the ``fn`` default, or
* keep time-major and pass ``time_axis=0`` to the ``spyx.fn`` factory.
Source code in spyx/nn.py
sum_pool(value, window_shape, strides, padding, channel_axis=-1)
Sum pool.