Spiking threshold as a parameter

Hi @Vaurien96. Thanks for the question and for taking the time to try to figure out these details and get this code working. I think the most important thing to note is that, for the case of the LIF model, the subthreshold dynamics—what happens to the voltage while it’s operating within the open interval (0, threshold)—are completely linear with respect to the current. Within this operating range, the voltage vector is governed by a leaky integrator (a first-order lowpass filter) – a linear dynamical system applied to the input current.

This observation is important because it tells us that scaling the threshold is mathematically equivalent to inversely scaling the input current by the exact same factor. Another way to say this is that: driving the neuron with an input current of J(t) until it reaches V(t)=1 is the same as driving an identical neuron with an input current R \cdot J(t) until it reaches a threshold of V(t)=R, by linearity. In other words, you can effectively modify the threshold to R (i.e., get the same mathematical outcome) while keeping the actual threshold fixed at 1 by scaling the input current by 1 / R.

Since the input current in the NEF is defined by: $$J = \alpha \left \langle {\bf e}\text{,}\, {\bf x}(t) \right \rangle + \beta$$ and we want to scale J by 1 / R, we can do this by scaling \alpha and \beta by 1 / R, as in: $$\frac{J}{R} = \left( \frac{\alpha}{R} \right) \left \langle {\bf e}\text{,}\, {\bf x}(t) \right \rangle + \left( \frac{\beta}{R} \right) \text{.}$$

In Nengo, \alpha and \beta are referred to as the gain and bias, respectively. These parameters are by default derived from the max_rates and intercepts of the desired tuning curves. This makes the important point that modifying the threshold is mathematically equivalent to modifying your max_rates and intercepts, i.e., your desired tuning curves. These are all completely configurable in Nengo (e.g., see tuning_curves and nef_summary).

Here is some example code that uses this trick to obtain an effective threshold of R = 2, and compares the voltage trace between two otherwise-identical neurons:

import numpy as np
import matplotlib.pyplot as plt

import nengo

threshold = 2.0

with nengo.Network(seed=0) as model1:
    stim1 = nengo.Node(1)
    
    x1 = nengo.Ensemble(n_neurons=1, dimensions=1, encoders=[[1]], max_rates=[100])

    nengo.Connection(stim1, x1)
    p1 = nengo.Probe(x1.neurons, 'voltage')
    
with nengo.Simulator(model1, dt=1e-5) as sim1:
    sim1.run(0.05)
    
gain = sim1.data[x1].gain / threshold
bias = sim1.data[x1].bias / threshold

with nengo.Network(seed=0) as model2:
    stim2 = nengo.Node(1)
    
    x2 = nengo.Ensemble(n_neurons=1, dimensions=1, encoders=[[1]],
                        gain=gain, bias=bias)

    nengo.Connection(stim2, x2)
    p2 = nengo.Probe(x2.neurons, 'voltage')
    
with nengo.Simulator(model2, dt=1e-5) as sim2:
    sim2.run(0.05)
    
plt.figure(figsize=(14, 5))
plt.plot(sim1.trange(), sim1.data[p1],
         label="threshold=1")
plt.plot(sim2.trange(), sim2.data[p2]*threshold,
         label="threshold=%s" % threshold)
plt.xlabel("Time (s)")
plt.ylabel("Voltage")
plt.legend()
plt.show()

To answer your question a bit more directly, though:

The reason you were seeing this behaviour is because: you not only need to change step_math, but you also need to modify the rates method inherited from LIFRate:

        j = J - 1
        output[:] = 0  # faster than output[j <= 0] = 0
        output[j > 0] = self.amplitude / (
            self.tau_ref + self.tau_rc * np.log1p(1. / j[j > 0]))

The decoding weights are learned from the tuning curves produces by rates, and so these must be consistent with step_math. You can use the observation from above to make the appropriate change by rescaling the input current (J) according to the desired threshold.

To summarize, you can either effectively change the LIF threshold by rescaling the input current, or by using your code and making the rates method consistent with your change. But, in either case, this is mathematically equivalent to rescaling the gain and bias parameters of your tuning curves, which you could also achieve by playing with max_rates and intercepts. If your desired tuning curves remain the same (i.e., if you also updated the gain_bias and max_rates_intercepts methods to be consistent with your new rates method), then this change will have no apparent effect, apart from rescaling the voltage trace.

Please feel free to ask more details about the code, math, or how to use this to do something!

1 Like