Skip to content

Layers


This page documents the core layer building blocks in NWAVE, split into:

  • Hardware layers (chip-specific abstractions for H1v1 and H1v2 neuronal models)
  • LIF layers (generic software LIF layers, with optional GPU path via comPaSSo)

The goal is to make it clear how to use each layer, what it returns, and how to run simulations that respect hardware non-idealities (mismatch, quantization).


Conventions (inputs/outputs)

NWAVE networks are typically simulated one timestep at a time:

  • Synapses map spikes → charge/current
    spk[t] → cur[t]
  • Neuron layers map charge/current → spikes (+ membrane)
    cur[t] → (spk[t], mem[t])

Shapes

At a single timestep:

  • Spike tensor: [B, N]
  • Charge/current tensor: [B, N]
  • Weight matrices are divided in:
    • Feedforward synapse: [N_in, N_out]
    • Recurrent: [N, N]
  • Time series of the NWaveDataloaders (input of the network) are usually [B, T, N] and you iterate over t.

Note

Layers accept 1D inputs of shape [N] and internally convert them to [1, N]. But it is a general practice to pass [B, N] consistently.


Preparing layers each batch: prepare_net

Many layers build internal quantities (e.g., effective synapse matrix, quantized weights, sampled mismatch noise) that must be refreshed before simulation.

Call prepare_net(model) once per batch, before you loop over timesteps.

from nwavesdk.layers import prepare_net

prepare_net(model)
for t in range(T):
    ...

For every submodule of the network it resets the states, and optionally applies mismatch effects, charge conversions and quantization. Even if you don't want to simulate non-idealities, you generally still want to call prepare_net before running a sequence, to avoid stale buffers/states.

Warning

If you forget to call prepare_net, you will likely see runtime errors such as missing tensors, NaNs, or you may simulate with stale synaptic/mismatch values.


Hardware Layers

API naming update

Hardware modules are now split explicitly by chip generation:

  • H1v1Frontend, H1v1Synapse, H1v1Layer
  • H1v2Frontend, H1v2Synapse, H1v2Layer

Legacy Frontend, HWSynapse, and HWLayer names have been replaced in the public layer API.

Typical migration:

# old
from nwavesdk.layers import Frontend, HWSynapse, HWLayer

# new (H1v1-compatible path)
from nwavesdk.layers import H1v1Frontend, H1v1Synapse, H1v1Layer

# new (H1v2 path)
from nwavesdk.layers import H1v2Frontend, H1v2Synapse, H1v2Layer

For H1v2 APIs, stddev is not accepted on synapses/frontends; variability is configured in H1v2Layer.


H1v1Frontend

Module: H1v1Frontend(nb_inputs, quantization_bit=None, stddev=None, init=..., lif_threshold=..., device="cpu"|"gpu")

Diagonal hardware frontend for H1v1 (16 -> 16 one-to-one mapping on chip).

Import with:

from nwavesdk.layers import H1v1Frontend

Key call notes:

  • stddev is available (synaptic mismatch on the diagonal frontend weights).
  • init for frontends supports uniform_ / normal_ only (1D weights).
  • Effective matrix is refreshed by prepare_net().

H1v1Synapse

Module: H1v1Synapse(nb_inputs, nb_outputs, quantization_bit=None, stddev=None, init=..., lif_threshold=..., device="cpu"|"gpu")

Dense H1v1 synapse with quantization and optional synaptic mismatch (stddev).

Import with:

from nwavesdk.layers import H1v1Synapse

Input / Output:

  • Input: [B, N_in]
  • Output: [B, N_out]

The effective synapse is built in prepare_net(). Calling forward() before preparation raises a runtime error.

Tip

For the fluctuation-driven hardware initializer compatible with H1v1 and H1v2 models, see fluct_init.


H1v1Layer

Module: H1v1Layer(n_neurons, taus, dt, ileak_mismatch=False, spike_grad=..., layer_topology="FF"|"RC", init=..., lif_threshold=..., quantization_bit=None, stddev=None, device="cpu"|"gpu", detach_reset=False)

H1v1 neuron dynamics layer with optional recurrent path (RC).

Import with:

from nwavesdk.layers import H1v1Layer

Key call notes:

  • stddev is accepted only if layer_topology="RC" and applies to recurrent synaptic mismatch.
  • quantization_bit in H1v1Layer also applies only to recurrent weights.
  • ileak_mismatch=True enables layer leak variability.

Reset behavior:

  • H1v1 uses reset-to-zero after spike (membrane is multiplied by (1 - spike)).

H1v2Frontend

Module: H1v2Frontend(nb_inputs, quantization_bit=None, init=..., lif_threshold=..., device="cpu"|"gpu")

Diagonal hardware frontend for H1v2.

Import with:

from nwavesdk.layers import H1v2Frontend

Key call differences vs H1v1:

  • stddev is not part of the H1v2 frontend API since there is no mismatch in H1v2 synapses

H1v2Synapse

Module: H1v2Synapse(nb_inputs, nb_outputs, quantization_bit=None, init=..., lif_threshold=..., device="cpu"|"gpu")

Dense H1v2 synapse.

Import with:

from nwavesdk.layers import H1v2Synapse

Key call differences vs H1v1:

  • stddev is not part of the H1v2 synapse API (no synaptic mismatch parameter).
  • Weight range used by H1v2 quantization is different (see Weight initialization).

H1v2Layer

Module: H1v2Layer(n_neurons, taus, dt, ileak_mismatch=False, mem_learn=False, spike_grad=..., layer_topology="FF"|"RC", init=..., lif_threshold=..., quantization_bit=None, device="cpu"|"gpu", detach_reset=False, threads=256)

H1v2 neuron dynamics layer with optional recurrent path (RC).

Import with:

from nwavesdk.layers import H1v2Layer

Key call differences vs H1v1:

  • No stddev argument in H1v2Layer constructor.
  • Mismatch is layer-side and enabled by ileak_mismatch=True, including:
  • leak-profile variability
  • threshold variability
  • taus are snapped to supported H1v2 leak profiles.

Note

In H1v2, reset is no longer "reset to 0". After a spike, a reset-drop value is subtracted from membrane (reset subtraction behavior). This makes H1v2's layer much more informative about past events.


H1v1 vs H1v2 quick behavior summary

  • Naming: H1v1* / H1v2* modules replace HW* names from older NWAVE versions.
  • Synapse mismatch:
  • H1v2 synaptic modules (H1v2Synapse, H1v2Frontend) do not expose stddev.
  • H1v2 mismatch modeling is concentrated in H1v2Layer via ileak_mismatch (leak + threshold variability).
  • Reset:
  • H1v1 layer: reset-to-zero.
  • H1v2 layer: reset-subtraction.

CPU vs GPU usage (hardware layers)

Hardware layers keep the same execution pattern:

  • CPU: iterate timesteps in Python (x[:, t, :]).
  • GPU: feed full sequence ([B, T, N]) directly.

Example (H1v1 stack):

import torch
import torch.nn as nn
from nwavesdk.layers import H1v1Frontend, H1v1Synapse, H1v1Layer, prepare_net

class H1SNNCPU(nn.Module):
    def __init__(self, num_classes=2, dt=1e-3):
        super().__init__()
        taus = 10e-3

        self.syn1 = H1v1Frontend(nb_inputs=16, device="cpu")
        self.h1_1 = H1v1Layer(n_neurons=16, taus=taus, dt=dt, device="cpu")

        self.syn2 = H1v1Synapse(16, num_classes, device="cpu")
        self.h1_2 = H1v1Layer(n_neurons=num_classes, taus=taus, dt=dt, device="cpu")

    def forward(self, x):
        prepare_net(self)
        spk2_trace = []
        for t in range(x.shape[1]):
            cur1 = self.syn1(x[:, t, :])
            spk1, _ = self.h1_1(cur1)
            cur2 = self.syn2(spk1)
            spk2, _ = self.h1_2(cur2)
            spk2_trace.append(spk2)
        return torch.stack(spk2_trace, dim=1)

Tip

to_gpu() is available on hardware layers/synapses/frontends to move an already-trained CPU layer to the GPU backend while preserving parameters.


Errors and inconsistencies to expect

  • In FF topology, recurrent-only arguments raise KeyError (e.g., quantization_bit, init, lif_threshold; and for H1v1 also recurrent stddev).
  • Calling hardware forward() before prepare_net() can fail because effective synapses are not built yet.

Weight initialization (LIF vs Hardware)

NWAVE exposes a common init interface across layers, built around PyTorch initializers (e.g., torch.nn.init.xavier_uniform_).

How to pass a custom initializer

Every layer that accepts init=... supports one of these forms:

  • Callable: init(tensor)
    Example: init=torch.nn.init.kaiming_uniform_
  • (callable, kwargs): (init_fn, {"a": ..., "mode": ...})
  • (callable, args, kwargs): (init_fn, (arg1, arg2, ...), {"kw": ...})

This mirrors how most torch.nn.init.* functions are called.

import torch.nn as nn

# simple callable
init = nn.init.xavier_uniform_

# callable + kwargs
init = (nn.init.kaiming_uniform_, {"a": 0.1, "mode": "fan_in", "nonlinearity": "relu"})

Beyond direct init=... usage, NWAVE provides a fluctuation-driven initializer for H1v1 and H1v2 hardware networks: fluct_init. It is documented separately because it is derived from Rossbroich et al. (2022) and adds NWAVE-specific hardware PSP measurement and dead-neuron handling.

LIF weights: standard meaning

For LIF layers/synapses, weights have the usual “software” meaning (they scale currents/charges in the update).
So your init behaves as you expect from PyTorch: it directly sets the scale of the synaptic effect and can easily make a neuron cross threshold in one step depending on your input data, dt, taus, and thresholds.

Hardware weights: membrane-jump mapping

For hardware synapses/layers, the weight parameter does not represent an arbitrary “current gain” but a membrane jump step.

To make initialization user-friendly, hardware synapses use a LIF-equivalent target during init: the initializer produces a “software-like” weight sample, then NWAVE rescales it so that (roughly) a spike would cause a membrane jump proportional to the lif threshold.

Quantization ranges by chip

  • H1v1: [-0.9, 0.9]
  • H1v2: [-1.66, 1.66]

lif_threshold (hardware init only)

lif_threshold is only used for hardware weight initialization: it is the reference LIF threshold you want the initializer to assume when setting the initial synaptic strength.

  • Larger lif_threshold → smaller intended membrane jump (more conservative init)
  • Smaller lif_threshold → larger intended membrane jump (more aggressive init)

This does not change the hardware neuron threshold (that is a chip parameter); it only changes how the trainable weights are initialized.

from nwavesdk.layers import H1v1Synapse
import torch.nn as nn

syn = H1v1Synapse(
    64, 64,
    init=nn.init.xavier_uniform_,
    lif_threshold=1.0,   # reference LIF threshold used ONLY for init scaling
)

Tip

If you are porting a working LIF model to HW simulation, set lif_threshold close to your LIF layer threshold(s) to start from a comparable effective synaptic strength (then fine-tune with HW-aware losses / mismatch / quantization).


LIF Layers

LIFSynapse

Module: LIFSynapse(nb_inputs, nb_outputs, device="cpu"|"gpu", use_bias=False, bias_learn=False, quantization_bit=None, init=...)

A generic (non-hardware) synapse used with LIFLayer.

Import with:

from nwavesdk.layers import LIFSynapse

Parameters

  • nb_inputs, nb_outputs (int)
  • device (str):
  • "cpu" uses a standard synapse
  • "gpu" uses batched matmul
  • use_bias (bool): if True, adds a bias term to the output. Default False.
  • bias_learn (bool): if True, the bias is a trainable nn.Parameter; if False, it is fixed at its initial value (0.1). Only used when use_bias=True. Default False.
  • quantization_bit (int | None): quantization bit applied
  • init (callable / InitSpec): torch-style initializer applied directly to the synaptic weight matrix (see Weight initialization).

Input / Output

CPU (serial):

  • Input: [B, nb_inputs]
  • Output: [B, nb_outputs]

GPU path:

  • Designed to support batched matmul (@). Typically used with time-series tensors:
  • Input: [B, T, nb_inputs] (or [B, nb_inputs])
  • Output: [B, T, nb_outputs] (or [B, nb_outputs])

Warning

Like other synapses, LIFSynapse expects you to run prepare_net() first if you enabled quantization.


LIFLayer

Module: LIFLayer(n_neurons, taus, thresholds, reset_mechanism, dt, device="cpu"|"gpu", detach_reset=False, learn_taus=False, learn_thresholds=False, ...)

A standard LIF neuron layer supporting:

  • configurable thresholds (optionally learnable)
  • configurable taus (optionally learnable)
  • optional recurrent connections on CPU (layer_topology="RC")
  • an experimental GPU parallel path (comPaSSo) optimized for long sequences

Parameters

  • n_neurons (int): number of neurons.
  • taus (float | Tensor): membrane time constant(s). Scalar or per-neuron.
  • thresholds (float | Tensor): spike threshold(s). Scalar or per-neuron.
  • reset_mechanism (str): "zero", "subtraction", or "none" (see below).
  • dt (float): timestep in seconds.
  • spike_grad (surrogate module): surrogate gradient function. Default fast_sigmoid().
  • layer_topology (str): "FF" (feedforward) or "RC" (recurrent, CPU only).
  • device (str): "cpu" or "gpu" (comPaSSo path).
  • detach_reset (bool): if True, the reset contribution is excluded from the gradient. Default False.
  • learn_taus (bool): if True, time constants become trainable parameters. Default False.
  • learn_thresholds (bool): if True, thresholds become trainable parameters. Default False.
  • init (callable / InitSpec): initializer for recurrent weights (RC mode only).
  • quantization_bit (int | None): quantization for recurrent weights (RC mode only).

Import with:

from nwavesdk.layers import LIFLayer

Input / Output

CPU (serial timestep simulation):

  • Input (per timestep): x of shape [B, n_neurons]
  • Output: (spk, mem) each [B, n_neurons]

GPU:

  • Input (whole sequence): x of shape [B, T, n_neurons]
  • Output: (spk_seq, mem_seq) each [B, T, n_neurons]

Reset mechanisms

  • "zero": reset membrane to zero on spike
  • "subtraction": subtract threshold on spike
  • "none": no reset

Example: hardware network definition (minimal)

import torch
import torch.nn as nn
from nwavesdk.layers import H1v1Frontend, H1v1Synapse, H1v1Layer, prepare_net

class H1SNN(nn.Module):
    def __init__(self, num_classes=2, dt=1e-3):
        super().__init__()
        taus = 10e-3

        self.syn1 = H1v1Frontend(nb_inputs=16)        # chip-faithful frontend
        self.h1_1 = H1v1Layer(n_neurons=16, taus=taus, dt=dt)

        self.syn2 = H1v1Synapse(16, num_classes)      # dense synapse
        self.h1_2 = H1v1Layer(n_neurons=num_classes, taus=taus, dt=dt)

    def forward(self, x):
        # x: [B, T, 16]
        B, T, _ = x.shape
        prepare_net(self)  # IMPORTANT: call once per batch/sequence

        spk2_trace = []
        for t in range(T):
            cur1 = self.syn1(x[:, t, :])
            spk1, _ = self.h1_1(cur1)

            cur2 = self.syn2(spk1)
            spk2, _ = self.h1_2(cur2)

            spk2_trace.append(spk2)

        return torch.stack(spk2_trace, dim=1)  # [B, T, num_classes]

Practical training knobs (mismatch & quantization)

You can enable or disable non-idealities per layer, without changing the rest of the model:

Quantization-aware training (QAT)

  • H1v1Frontend(quantization_bit=...)
  • H1v2Frontend(quantization_bit=...)
  • H1v1Synapse(quantization_bit=...)
  • H1v2Synapse(quantization_bit=...)
  • H1v1Layer(..., layer_topology="RC", quantization_bit=...) (recurrent only)
  • H1v2Layer(..., layer_topology="RC", quantization_bit=...) (recurrent only)
  • LIFSynapse(quantization_bit=...)

For deployment, you can optionally snap weights:

  • H1v1Frontend.get_quant_weights()
  • H1v1Frontend.get_quant_weights()
  • H1v1Synapse.get_quant_weights()
  • H1v2Synapse.get_quant_weights()
  • LIFSynapse.get_quant_weights()

Mismatch-aware training

  • H1v1 path (backward-compatible mismatch controls):
  • H1v1Frontend(stddev=...): synaptic mismatch
  • H1v1Synapse(stddev=...): synaptic mismatch
  • H1v1Layer(ileak_mismatch=True): leak mismatch
  • H1v1Layer(..., layer_topology="RC", stddev=...): recurrent synaptic mismatch
  • H1v2 path:
  • H1v2Synapse / H1v2Frontend: no stddev argument
  • H1v2Layer(ileak_mismatch=True): mismatch in layer dynamics (leak + threshold variability)

Mismatch is resampled by prepare_net(), which makes training robust to variability by training the net to be independent from empirical noises.

Tip

If you want deterministic evaluation, disable mismatch (stddev=None for H1v1, ileak_mismatch=False for H1v2). If you want stochastic robustness, keep prepare_net() in the training loop.