CUBANet

Article 1 derived the continuous-time leaky integrate-and-fire (LIF) neuron from Maxwell’s equations and the membrane circuit. This article picks up where that derivation leaves off and asks: how do you turn the continuous LIF into a discrete update rule that runs on a GPU? Two integrator choices (snnTorch-style and zero-order hold) give two rungs — standard-snn and cuba — both backed by a single Python class, CUBANet, in src/pinglab/models.py. Every empirical result in the notebooks on the CUBA / standard-snn family traces back to the math here.

The implementation lives in src/pinglab/models.py. This article keeps the derivations.

Architecture

CUBANet (current-based LIF) is a single class with one switch:

  • discretisation ∈ {snntorch, zoh} — how the continuous LIF is discretised. Spikes drive the membrane directly (no synaptic filter); the conductance-filtered story lives one rung up in COBANet.

The two canonical rungs:

rungdiscretisation
standard-snnsnnTorch
cubaZOH

Tutorial mode (—kaiming-init) is a further switch that overrides Dale’s law and weight scaling to match snn.Leaky bit-for-bit at initialisation, used only by the parity probe.

The LIF primitive

The forward pass and backward pass share a primitive snn_lif_step that performs one timestep of decay + drive + spike + reset:

mem = β * mem + I
s   = σ_surr(mem − θ)
if reset == "subtract":
    mem = mem − θ · s
else:  # "zero"
    mem = mem · (1 − s)

In words: decay the old membrane, add the (already-scaled) drive II, threshold the new membrane to emit a spike, and apply the reset. The caller is responsible for scaling II — that’s where the two discretisations diverge.

snnTorch discretisation

Used by standard-snn. The update is the snnTorch convention — decay times the previous membrane plus the unscaled drive — with the reset applied after the spike is emitted:

Ut+1=βUt+Wst+b,St+1=σsurr(Ut+1θ)(5)U_{t+1} = \beta\, U_t + W\, s_t + b, \qquad S_{t+1} = \sigma_{\text{surr}}(U_{t+1} - \theta) \tag{5} Ut+1{0reset=zero, St+1=1Ut+1θSt+1reset=subtract(6)U_{t+1} \leftarrow \begin{cases} 0 & \text{reset=zero, } S_{t+1} = 1 \\ U_{t+1} - \theta\, S_{t+1} & \text{reset=subtract} \end{cases} \tag{6}

Two things to notice. First, the spike is computed on the post-update membrane Ut+1U_{t+1}, not the previous one — that’s the order of operations in snn_lif_step. Second, WW and bb enter the update without a Δt\Delta t scaling, so the same trained weights produce different per-step contributions at different Δt\Delta t. This is the dt-fragility shared with snn.Leaky.

Bias-balloon derivation

Take the expectation of (5) under a stationary mean input rate rr (Hz). Each step, the expected input is E[st]=rΔt\mathbb{E}[s_t] = r\Delta t, so E[Wst]=WrΔt\mathbb{E}[W s_t] = W r \Delta t. The fixed-point of the membrane is the value UssU_{ss} that satisfies Uss=βUss+WrΔt+bU_{ss} = \beta U_{ss} + W r \Delta t + b, giving

Uss=WrΔt+b1β(7)U_{ss} = \frac{W r \Delta t + b}{1-\beta} \tag{7}

For Δtτ\Delta t \ll \tau, use the first-order expansion 1βΔt/τ1 - \beta \approx \Delta t / \tau:

UssWrτ  +  bτΔt(8)U_{ss} \approx W r \tau \;+\; \frac{b \tau}{\Delta t} \tag{8}

The spike contribution WrτW r \tau is Δt\Delta t-invariant: halve Δt\Delta t and the per-step kick halves, but you take twice as many steps. The bias contribution scales as 1/Δt1/\Delta t: halve Δt\Delta t and the steady-state bias contribution doubles. This is why standard-snn with a non-zero bias drifts toward firing-rate saturation as Δt\Delta t shrinks at eval time.

ZOH discretisation

Used by cuba. Start from the continuous-time LIF in canonical form (EL=0E_L = 0, R=1R = 1):

τdVdt=V+I(t),I(t)=kWδ(ttk)+b(9)\tau\, \frac{dV}{dt} = -V + I(t), \qquad I(t) = \sum_k W\, \delta(t - t_k) + b \tag{9}

where the spike train kδ(ttk)\sum_k \delta(t - t_k) represents incoming presynaptic spikes at times {tk}\{t_k\}.

Step 1 — set up an integrating factor

Rewrite (9) as

dVdt+1τV=I(t)τ\frac{dV}{dt} + \frac{1}{\tau}\, V = \frac{I(t)}{\tau}

This is a first-order linear ODE.

Multiplying both sides of the original ODE by μ(t)\mu(t):

et/τdVdt+1τet/τV=I(t)τet/τe^{t/\tau}\, \frac{dV}{dt} + \frac{1}{\tau}\, e^{t/\tau}\, V = \frac{I(t)}{\tau}\, e^{t/\tau}

The left-hand side is, by the product rule, the derivative of V(t)et/τV(t)\, e^{t/\tau}:

ddt ⁣[V(t)et/τ]=dVdtet/τ+V(t)1τet/τ\frac{d}{dt}\!\left[V(t)\, e^{t/\tau}\right] = \frac{d V}{dt}\, e^{t/\tau} + V(t)\, \frac{1}{\tau}\, e^{t/\tau}

So the ODE becomes

ddt ⁣[V(t)et/τ]=I(t)τet/τ(10a)\frac{d}{dt}\!\left[V(t)\, e^{t/\tau}\right] = \frac{I(t)}{\tau}\, e^{t/\tau} \tag{10a}

Step 2 — integrate from tt to t+Δtt + \Delta t

Apply tt+Δt()ds\int_t^{t+\Delta t} (\cdot)\, ds to both sides of (10a).

Equating to the integrated right-hand side:

V(t+Δt)e(t+Δt)/τV(t)et/τ=tt+ΔtI(s)τes/τds(10b)V(t + \Delta t)\, e^{(t + \Delta t)/\tau} - V(t)\, e^{t/\tau} = \int_t^{t+\Delta t} \frac{I(s)}{\tau}\, e^{s/\tau}\, ds \tag{10b}

Step 3 — isolate V(t+Δt)V(t + \Delta t)

Divide both sides of (10b) by e(t+Δt)/τe^{(t+\Delta t)/\tau}:

V(t+Δt)V(t)et/τe(t+Δt)/τ=1τe(t+Δt)/τtt+ΔtI(s)es/τdsV(t + \Delta t) - V(t)\, e^{t/\tau}\, e^{-(t+\Delta t)/\tau} = \frac{1}{\tau}\, e^{-(t+\Delta t)/\tau} \int_t^{t+\Delta t} I(s)\, e^{s/\tau}\, ds

The decay coefficient on V(t)V(t) simplifies using the exponent rule eaeb=ea+be^{a}\, e^{b} = e^{a+b}:

et/τe(t+Δt)/τ  =  e(ttΔt)/τ  =  eΔt/τ.e^{t/\tau}\, e^{-(t+\Delta t)/\tau} \;=\; e^{(t - t - \Delta t)/\tau} \;=\; e^{-\Delta t/\tau}.

That gives the homogeneous part of the solution: V(t+Δt)=eΔt/τV(t)+(integral term)V(t + \Delta t) = e^{-\Delta t/\tau}\, V(t) + \text{(integral term)}.

For the integral term, the factor e(t+Δt)/τe^{-(t+\Delta t)/\tau} sits outside the integral but is constant with respect to the integration variable ss — neither tt nor Δt\Delta t depend on ss, they are fixed bounds. A standard property of integrals is that any factor independent of the integration variable can be moved freely across the integral sign:

αtt+Δtf(s)ds  =  tt+Δtαf(s)ds,α independent of s.\alpha \int_t^{t+\Delta t} f(s)\, ds \;=\; \int_t^{t+\Delta t} \alpha\, f(s)\, ds, \qquad \alpha \text{ independent of } s.

Applying that here with α=e(t+Δt)/τ\alpha = e^{-(t+\Delta t)/\tau} and f(s)=I(s)es/τf(s) = I(s)\, e^{s/\tau}:

e(t+Δt)/τtt+ΔtI(s)es/τds  =  tt+ΔtI(s)es/τe(t+Δt)/τds.e^{-(t+\Delta t)/\tau} \int_t^{t+\Delta t} I(s)\, e^{s/\tau}\, ds \;=\; \int_t^{t+\Delta t} I(s)\, e^{s/\tau}\, e^{-(t+\Delta t)/\tau}\, ds.

Inside the integrand the two exponentials now multiply each other. Combining via the same exponent rule:

es/τe(t+Δt)/τ  =  e(stΔt)/τ.e^{s/\tau}\, e^{-(t+\Delta t)/\tau} \;=\; e^{(s - t - \Delta t)/\tau}.

So the integral term becomes 1τtt+ΔtI(s)e(stΔt)/τds\frac{1}{\tau} \int_t^{t+\Delta t} I(s)\, e^{(s - t - \Delta t)/\tau}\, ds, and we can write the full update as

V(t+Δt)=eΔt/τV(t)+1τtt+ΔtI(s)e(stΔt)/τdsV(t + \Delta t) = e^{-\Delta t/\tau}\, V(t) + \frac{1}{\tau} \int_t^{t+\Delta t} I(s)\, e^{(s - t - \Delta t)/\tau}\, ds

Step 4 — change of variable ξ=st\xi = s - t

Then ds=dξds = d\xi, s=t+ξs = t + \xi, and stΔt=ξΔts - t - \Delta t = \xi - \Delta t. The limits transform as s=tξ=0s = t \Rightarrow \xi = 0 and s=t+Δtξ=Δts = t + \Delta t \Rightarrow \xi = \Delta t.

Substituting:

V(t+Δt)=eΔt/τV(t)+1τ0ΔtI(t+ξ)e(Δtξ)/τdξ(10)V(t + \Delta t) = e^{-\Delta t/\tau}\, V(t) + \frac{1}{\tau} \int_0^{\Delta t} I(t + \xi)\, e^{-(\Delta t - \xi)/\tau}\, d\xi \tag{10}

This is exact for any forcing I(t)I(t) — no approximation has been made yet.

Step 5 — replace the Dirac by a rectangle

So far we have an exact equation (10) for Vt+1V_{t+1} given VtV_t and the forcing II over the step. Now we hit the problem the rectangle approximation exists to solve. Suppose exactly one spike lands inside the step at some time tk[t,t+Δt]t_k \in [t,\, t + \Delta t]. The Dirac forcing for that spike alone is

I(t+ξ)  =  Wδ ⁣(t+ξtk),I(t + \xi) \;=\; W\, \delta\!\bigl(t + \xi - t_k\bigr),

i.e. the forcing is zero everywhere across the step except at ξ=tkt\xi = t_k - t, where it is an infinitely tall pulse of area WW. Plugging that into the integral in (10),

1τ0Δte(Δtξ)/τWδ ⁣(t+ξtk)dξ,\frac{1}{\tau} \int_0^{\Delta t} e^{-(\Delta t - \xi)/\tau}\, W\, \delta\!\bigl(t + \xi - t_k\bigr)\, d\xi,

and using the sifting property of the Dirac — f(ξ)δ(ξa)dξ=f(a)\int f(\xi)\, \delta(\xi - a)\, d\xi = f(a) whenever aa lies inside the integration interval — the integrand is collapsed to its value at the spike’s location, ξ=tkt\xi = t_k - t. The contribution to Vt+1V_{t+1} from that single spike comes out as

Wτe(Δt(tkt))/τ.\frac{W}{\tau}\, e^{-(\Delta t - (t_k - t))/\tau}.

Read the exponent: Δt(tkt)\Delta t - (t_k - t) is the amount of time between the spike and the end of the step. So the spike contributes more if it lands late in the step (small remaining time means little decay before reaching t+Δtt + \Delta t) and less if it lands early (long remaining time means heavy exponential decay). In other words: the exact answer depends on where inside the step the spike arrived — on the sub-step arrival time tktt_k - t, a continuous number between 00 and Δt\Delta t.

That sub-step time is information the discrete simulator does not carry. Once we have agreed to integrate in steps of size Δt\Delta t, the only data we keep per neuron per step is “did at least one spike arrive in this step?” — a single Boolean st{0,1}s_t \in \{0, 1\}. We have collapsed the continuous spike time onto a binary step indicator, and the moment we do that we forfeit the dependence on tktt_k - t that the exact integral wants.

We replace each Dirac that lands in [t,t+Δt][t, t+\Delta t] by a rectangle of equal area WW and width Δt\Delta t, i.e. constant height W/ΔtW/\Delta t. Writing st{0,1}s_t \in \{0, 1\} for the indicator that a spike occurred in step tt, the effective forcing over the step is

I(t+ξ)    WstΔt+b,ξ[0,Δt]I(t + \xi) \;\to\; \frac{W\, s_t}{\Delta t} + b, \qquad \xi \in [0, \Delta t]

The bias bb is already constant, so it is unmodified. Define βeΔt/τ\beta \equiv e^{-\Delta t / \tau} and VtV(t)V_t \equiv V(t). Substituting into (10):

Vt+1=βVt+1τ0Δte(Δtξ)/τ[WstΔt+b]dξ(11)V_{t+1} = \beta\, V_t + \frac{1}{\tau} \int_0^{\Delta t} e^{-(\Delta t - \xi)/\tau} \left[\frac{W\, s_t}{\Delta t} + b\right] d\xi \tag{11}

Step 6 — pull the constants out of the integral

The bracket is independent of ξ\xi, so

Vt+1=βVt+[WstΔt+b]1τ0Δte(Δtξ)/τdξcall this JV_{t+1} = \beta\, V_t + \left[\frac{W\, s_t}{\Delta t} + b\right] \cdot \underbrace{\frac{1}{\tau} \int_0^{\Delta t} e^{-(\Delta t - \xi)/\tau}\, d\xi}_{\text{call this } J}

Step 7 — evaluate JJ by uu-substitution

Let u=Δtξu = \Delta t - \xi. Then du=dξdu = -d\xi, i.e. dξ=dud\xi = -du. The limits transform as ξ=0u=Δt\xi = 0 \Rightarrow u = \Delta t and ξ=Δtu=0\xi = \Delta t \Rightarrow u = 0. Substituting:

J=1τu=Δtu=0eu/τ(du)J = \frac{1}{\tau} \int_{u=\Delta t}^{u=0} e^{-u/\tau}\, (-du)

The negative sign flips the limits:

J=1τ0Δteu/τduJ = \frac{1}{\tau} \int_0^{\Delta t} e^{-u/\tau}\, du

The antiderivative of eu/τe^{-u/\tau} with respect to uu is τeu/τ-\tau\, e^{-u/\tau} (check: ddu[τeu/τ]=τ(1/τ)eu/τ=eu/τ\frac{d}{du}[-\tau\, e^{-u/\tau}] = -\tau \cdot (-1/\tau)\, e^{-u/\tau} = e^{-u/\tau}). Evaluate:

J=1τ[τeu/τ]0Δt=1τ[τeΔt/τ(τ1)]=1τ[ττeΔt/τ]=1eΔt/τ=1β(12)J = \frac{1}{\tau}\, \Big[ -\tau\, e^{-u/\tau} \Big]_0^{\Delta t} = \frac{1}{\tau}\, \Big[ -\tau\, e^{-\Delta t/\tau} - (-\tau \cdot 1) \Big] = \frac{1}{\tau}\, \big[\, \tau - \tau\, e^{-\Delta t/\tau} \,\big] = 1 - e^{-\Delta t/\tau} = 1 - \beta \tag{12}

Step 8 — substitute JJ back and distribute

From Step 6 with J=1βJ = 1 - \beta:

Vt+1=βVt+(1β)[WstΔt+b]=βVt+1βΔtWst+(1β)bV_{t+1} = \beta\, V_t + (1 - \beta) \left[\frac{W\, s_t}{\Delta t} + b\right] = \beta\, V_t + \frac{1 - \beta}{\Delta t}\, W\, s_t + (1 - \beta)\, b

Renaming VUV \to U to match the code’s symbol for membrane potential:

Ut+1=βUt+1βΔtWst+(1β)b(13)\boxed{U_{t+1} = \beta\, U_t + \frac{1 - \beta}{\Delta t}\, W\, s_t + (1 - \beta)\, b} \tag{13}

In the code, the two prefactors are computed once per step as spike_scale =(1β)/Δt= (1-\beta)/\Delta t and bias_scale =1β= 1 - \beta; see models.py:705-710.

Step 9 — Δt\Delta t-invariance, part 1: bias steady state

In the absence of spikes (st=0s_t = 0), (13) reduces to Ut+1=βUt+(1β)bU_{t+1} = \beta\, U_t + (1-\beta)\, b. Setting Ut+1=Ut=UˉU_{t+1} = U_t = \bar U for the fixed point:

Uˉ=βUˉ+(1β)b        UˉβUˉ=(1β)b        (1β)Uˉ=(1β)b        Uˉ=b\bar U = \beta\, \bar U + (1 - \beta)\, b \;\;\Longrightarrow\;\; \bar U - \beta\, \bar U = (1 - \beta)\, b \;\;\Longrightarrow\;\; (1 - \beta)\, \bar U = (1 - \beta)\, b \;\;\Longrightarrow\;\; \bar U = b

The steady-state membrane equals bb regardless of Δt\Delta t. ✓

Step 10 — Δt\Delta t-invariance, part 2: single-spike time integral

Consider a network at rest, U0=0U_0 = 0, that receives one spike at step 0 (s0=1s_0 = 1, st=0s_t = 0 for t1t \ge 1), with b=0b = 0. Then from (13):

U1=β0+1βΔtW1+0=K,K1βΔtWU_1 = \beta \cdot 0 + \frac{1 - \beta}{\Delta t}\, W \cdot 1 + 0 = K, \quad K \equiv \frac{1 - \beta}{\Delta t}\, W

For t1t \ge 1 there is no further forcing, so Ut+1=βUtU_{t+1} = \beta\, U_t, which means Ut=Kβt1U_t = K\, \beta^{t-1} for t1t \ge 1. To get a Δt\Delta t-invariant quantity, integrate UU against time using a left-Riemann sum with step Δt\Delta t:

0U(t)dt    t=0UtΔt=U0Δt+t=1Kβt1Δt=0+KΔtm=0βm\int_0^\infty U(t)\, dt \;\approx\; \sum_{t=0}^\infty U_t\, \Delta t = U_0\, \Delta t + \sum_{t=1}^\infty K\, \beta^{t-1}\, \Delta t = 0 + K\, \Delta t \sum_{m=0}^\infty \beta^m

The sum is a geometric series with ratio β(0,1)\beta \in (0, 1), so m=0βm=1/(1β)\sum_{m=0}^\infty \beta^m = 1/(1 - \beta). Substituting:

0U(t)dt    KΔt1β=11β1βΔtWΔt=W\int_0^\infty U(t)\, dt \;\approx\; \frac{K\, \Delta t}{1 - \beta} = \frac{1}{1 - \beta} \cdot \frac{1 - \beta}{\Delta t}\, W \cdot \Delta t = W

The result is exactly WW, independent of Δt\Delta t. The corresponding continuous-time impulse response of (9) is V(t)=(W/τ)et/τ1t0V(t) = (W/\tau)\, e^{-t/\tau}\, \mathbf{1}_{t \ge 0}, whose time integral is

0Wτet/τdt=Wττ[10]=W\int_0^\infty \frac{W}{\tau}\, e^{-t/\tau}\, dt = \frac{W}{\tau} \cdot \tau \,\big[\,1 - 0\,\big] = W

The discrete and continuous time integrals agree. ✓

Step 11 — small-Δt\Delta t limit (sanity check)

Taylor-expand β=eΔt/τ\beta = e^{-\Delta t/\tau} around Δt=0\Delta t = 0:

β=1Δtτ+12 ⁣(Δtτ) ⁣2        1β=Δtτ12 ⁣(Δtτ) ⁣2+\beta = 1 - \frac{\Delta t}{\tau} + \frac{1}{2}\!\left(\frac{\Delta t}{\tau}\right)^{\!2} - \cdots \;\;\Longrightarrow\;\; 1 - \beta = \frac{\Delta t}{\tau} - \frac{1}{2}\!\left(\frac{\Delta t}{\tau}\right)^{\!2} + \cdots

To leading order in Δt/τ\Delta t/\tau:

  • (1β)/Δt=1/τΔt/(2τ2)+O(Δt2)1/τ(1 - \beta)/\Delta t = 1/\tau - \Delta t/(2\tau^2) + O(\Delta t^2) \to 1/\tau.
  • (1β)=Δt/τ+O(Δt2)(1 - \beta) = \Delta t/\tau + O(\Delta t^2), so (1β)/Δt=1/τ+O(Δt)(1 - \beta)/\Delta t = 1/\tau + O(\Delta t).

In words: the per-spike kick (1β)/ΔtW(1-\beta)/\Delta t \cdot W approaches W/τW/\tau — i.e. the impulse response of the continuous LIF — and the per-step bias contribution (1β)b(1-\beta)\, b shrinks linearly with Δt\Delta t. The number of steps per ms scales as 1/Δt1/\Delta t, so the per-millisecond bias contribution is (1β)b/Δtb/τ(1-\beta)\, b / \Delta t \to b/\tau, also Δt\Delta t-invariant.

Contrast with snnTorch

In the snnTorch discretisation, WW and bb have prefactor 11 regardless of Δt\Delta t, so the per-spike kick is WW (not W/τW/\tau) and the per-step bias is bb (not bΔt/τb\, \Delta t/\tau). Changing Δt\Delta t at eval time therefore shifts the effective input scale in standard-snn, while ZOH leaves it fixed — verified empirically in 013 — dt sensitivity.

Reset modes

Two choices on spike, controlled by —reset-mode:

  • zero — hard reset Ut+10U_{t+1} \leftarrow 0. Discards overshoot. Standard for biological neurons.
  • subtractUt+1Ut+1θU_{t+1} \leftarrow U_{t+1} - \theta. Preserves overshoot above threshold for the next step. Standard for snnTorch tutorials.

Defaults: standard-snn uses subtract under tutorial-mode; cuba uses zero.

Refractory period

Optional, controlled by —ref-ms. Implemented as a per-neuron can_fire mask in snn_lif_step: during refractory the membrane is clamped to VresetV_{\text{reset}} and the spike output is forced to zero. Default 0 ms (no refractory) for the CUBA-family rungs.

Where this leads

The two CUBA rungs span the design space of a single LIF cell with current-based inputs. The next step up the ladder adds genuine conductance-based synapses and a recurrent E-I loop — the COBA and PING models, documented in COBANet and exercised throughout the notebooks.