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]
- Feedforward synapse:
- Time series of the NWaveDataloaders (input of the network) are usually
[B, T, N]and you iterate overt.
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,H1v1LayerH1v2Frontend,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:
stddevis available (synaptic mismatch on the diagonal frontend weights).initfor frontends supportsuniform_/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:
stddevis accepted only iflayer_topology="RC"and applies to recurrent synaptic mismatch.quantization_bitinH1v1Layeralso applies only to recurrent weights.ileak_mismatch=Trueenables 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:
stddevis 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:
stddevis 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
stddevargument inH1v2Layerconstructor. - Mismatch is layer-side and enabled by
ileak_mismatch=True, including: - leak-profile variability
- threshold variability
tausare 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 replaceHW*names from older NWAVE versions. - Synapse mismatch:
- H1v2 synaptic modules (
H1v2Synapse,H1v2Frontend) do not exposestddev. - H1v2 mismatch modeling is concentrated in
H1v2Layerviaileak_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
FFtopology, recurrent-only arguments raiseKeyError(e.g.,quantization_bit,init,lif_threshold; and for H1v1 also recurrentstddev). - Calling hardware
forward()beforeprepare_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 matmuluse_bias(bool): ifTrue, adds a bias term to the output. DefaultFalse.bias_learn(bool): ifTrue, the bias is a trainablenn.Parameter; ifFalse, it is fixed at its initial value (0.1). Only used whenuse_bias=True. DefaultFalse.quantization_bit(int | None): quantization bit appliedinit(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. Defaultfast_sigmoid().layer_topology(str):"FF"(feedforward) or"RC"(recurrent, CPU only).device(str):"cpu"or"gpu"(comPaSSo path).detach_reset(bool): ifTrue, the reset contribution is excluded from the gradient. DefaultFalse.learn_taus(bool): ifTrue, time constants become trainable parameters. DefaultFalse.learn_thresholds(bool): ifTrue, thresholds become trainable parameters. DefaultFalse.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):
xof shape[B, n_neurons] - Output:
(spk, mem)each[B, n_neurons]
GPU:
- Input (whole sequence):
xof 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 mismatchH1v1Synapse(stddev=...): synaptic mismatchH1v1Layer(ileak_mismatch=True): leak mismatchH1v1Layer(..., layer_topology="RC", stddev=...): recurrent synaptic mismatch- H1v2 path:
H1v2Synapse/H1v2Frontend: nostddevargumentH1v2Layer(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.