Ready, Integrate, Fire! Part 2: Simulating a simple neuronal model
This is a direct Python translation (with a slight modification) of the Matlab code found in this tutorial paper created by the Goldman Lab at UC Davis.
In my last blog post, I presented the Leaky Integrate-and-Fire Model, where a neuron is modelled as a leaky capacitor with membrane resistance
To generate action potentials, this equation is augmented by the golden rule: whenever
We solved Eq. 1 analytically, and if we know the value
Setting
For concreteness of our simulation, we will use parameter values
To model the spiking of the LIF neuron when it reaches threshold, we will assume that when the membrane potential reaches
We will inject various levels of current
Let's start coding!
Part 1: Model the subthreshold voltage dynamics #
We will add the spiking rule in the latter part of this post. For the first part, we will need to model the subthreshold voltage dynamics governed by Eq. 1. We'll follow the tutorial's general overall strategy to break our code into the following sections:
- Define the parameters in the model.
- Define the vectors that will hold our final results such as the time, voltage, and current; and assign their initial values corresponding to
. - Integrate the equation(s) of the model to obtain the values of te above vectors at later times by updating at the previous time step with the update rule.
- Make good plots of our results.
Step 1: Import Relevant Packages #
We will need only two libraries: Numpy and Matplotlib. These two packages are useful for scientific computing and data visualization, respectively.
import numpy as np
import matplotlib.pyplot as plt
Step 2: Define Parameters #
We'll define the model parameters as discussed above. In general, it is helpful to have a section of your code dedicated to assigning values to all model parameters (the variables that have fixed values throughout the entire simulation). It is also a good programming style to make a comment describing each parameter and noting its units. This will be very valuable in spotting errors once they occur.
dt = 0.1 #time step [ms]
t_end = 500 #total time of run [ms]
t_StimStart = 100 #time to start injecting current [ms]
t_StimEnd = 400 #time to end injecting current [ms]
E_L = -70 #resting membrane potential [mV]
V_th = -55 #spike threshold [mV]
V_reset = -75 #value to reset voltage to after a spike [mV]
V_spike = 20 #value to draw a spike to, when cell spikes [mV]
R_m = 10 #membrane resistance [MOhm]
tau = 10 #membrane time constant [ms]
Step 3: Define Initial Values and Vectors to Hold Results #
We need to set up the initial conditions for the run (i.e. specify what the values of the relevant variables will be at time _vect
on the end of the names of all vector variables to help you distinguish them from the scalars.
t_vect = np.arange(0,t_end+dt,dt) #will hold vector of times
V_vect = np.zeros((len(t_vect),)) #initialize the voltage vector
V_vect[0] = E_L #value of V at t = 0
I_e_vect = np.zeros((len(t_vect),)) #injected current [nA]
I_Stim = 1 #magnitude of pulse of injected current [nA]
I_e_vect[np.int(t_StimStart/dt):np.int(t_StimEnd/dt)+1] = I_Stim*np.ones((np.int((t_StimEnd-t_StimStart)/dt)+1,))
Note that we want that at time V
to this value. We also defined the current injected at all times as
Step 4: Write Leaky Integrate-and-Fire Neuron Model #
We are now ready to integrate Eq. 1. To do this, we need to implement the update rule (Eq. 3) using a for loop that iterates over the values of
for i in range(1,np.int(t_end/dt)+1): #loop through values of t in steps of dt ms, start with t=1 since we already defined V at t=0
V_vect[i] = E_L + I_e_vect[i]*R_m + (V_vect[i-1] - E_L - I_e_vect[i]*R_m)*np.exp(-dt/tau)
Step 5: Make Plots #
Now we are ready to plot the subthreshold dynamics of our leaky integrate-and-fire neuron.
plt.plot(t_vect, V_vect)
plt.title('Voltage vs. time')
plt.xlabel('Time in ms')
plt.ylabel('Voltage in mV')
plt.show()
As can be seen in the above graph, the voltage (in mV) should rise exponentially towards -60 mV starting at
Feel free to try out other values of
Part 2: Add the spiking to the model and calculate the firing rate #
I hope you noticed that no matter how large you made
Step 3 (v2): Define Initial Values and Vectors to Hold Results #
We have to assign a new vector
t_vect = np.arange(0,t_end+dt,dt) #will hold vector of times
V_vect = np.zeros((len(t_vect),)) #initialize the voltage vector
V_plot_vect = np.zeros((len(t_vect),)) #pretty version of V_vect to be plotted that displays a spike whenever voltage reaches threshold
V_vect[0] = E_L #value of V at t = 0
V_plot_vect[0] = V_vect[0] #if no spike, then just plot the actual voltage V
I_e_vect = np.zeros((len(t_vect),)) #injected current [nA]
I_Stim = 1.55 #magnitude of pulse of injected current [nA]
I_e_vect[np.int(t_StimStart/dt):np.int(t_StimEnd/dt)+1] = I_Stim*np.ones((np.int((t_StimEnd-t_StimStart)/dt)+1,))
Step 4 (v2): Write Leaky Integrate-and-Fire Neuron Model #
We add the spiking rule to this step and modify the integration loop for reflect this in our plots and results.
for i in range(1,np.int(t_end/dt)+1): #loop through values of t in steps of dt ms, start with t=1 since we already defined V at t=0
V_vect[i] = E_L + I_e_vect[i]*R_m + (V_vect[i-1] - E_L - I_e_vect[i]*R_m)*np.exp(-dt/tau)
if (V_vect[i] > V_th): #neuron spiked
V_vect[i] = V_reset #set voltage back to V_reset
V_plot_vect[i] = V_spike #set vector that will be plotted to show a spike here
else: #voltage didn't cross threshold so neuron does not spike
V_plot_vect[i] = V_vect[i] #plot actual voltage
Step 5 (v2): Make Plots #
We also need to change our plotting code to plot
plt.plot(t_vect, V_plot_vect)
plt.title('Voltage vs. time')
plt.xlabel('Time in ms')
plt.ylabel('Voltage in mV')
plt.show()
Finally, we would like to compute the average firing rate of the cell during the time of stimulation. A neuron's average firing rate over a specified period of time is calculated as the number of spikes produced over the specified time period:
To calculate this, we need to modify Steps 3-4 and add another step 6.
Step 3 (v3): Define Initial Values and Vectors to Hold Results #
We need to add a variable
t_vect = np.arange(0,t_end+dt,dt) #will hold vector of times
V_vect = np.zeros((len(t_vect),)) #initialize the voltage vector
V_plot_vect = np.zeros((len(t_vect),)) #pretty version of V_vect to be plotted that displays a spike whenever voltage reaches threshold
V_vect[0] = E_L #value of V at t = 0
V_plot_vect[0] = V_vect[0] #if no spike, then just plot the actual voltage V
I_e_vect = np.zeros((len(t_vect),)) #injected current [nA]
I_Stim = 1.55 #magnitude of pulse of injected current [nA]
I_e_vect[np.int(t_StimStart/dt):np.int(t_StimEnd/dt)+1] = I_Stim*np.ones((np.int((t_StimEnd-t_StimStart)/dt)+1,))
NumSpikes = 0 #holds number of spikes that have occurred
Step 4 (v3): Write Leaky Integrate-and-Fire Neuron Model #
We also need to modify the integration loop so that NumSpikes will increase by one each time the neuron spikes.
for i in range(1,np.int(t_end/dt)+1): #loop through values of t in steps of dt ms, start with t=1 since we already defined V at t=0
V_vect[i] = E_L + I_e_vect[i]*R_m + (V_vect[i-1] - E_L - I_e_vect[i]*R_m)*np.exp(-dt/tau)
if (V_vect[i] > V_th): #neuron spiked
V_vect[i] = V_reset #set voltage back to V_reset
V_plot_vect[i] = V_spike #set vector that will be plotted to show a spike here
NumSpikes += 1 #add 1 to the total spike count
else: #voltage didn't cross threshold so neuron does not spike
V_plot_vect[i] = V_vect[i] #plot actual voltage
Step 5 (v2): Make Plots #
plt.plot(t_vect, V_plot_vect)
plt.title('Voltage vs. time')
plt.xlabel('Time in ms')
plt.ylabel('Voltage in mV')
plt.show()
Step 6: Calculate average firing rate #
We need to add this step to explicitly calculate the average firing rate from our simulation. Notice below that we multiply by 1000 because we want to convert from #spikes/ms to #spikes/sec. For
r_ave = 1000*NumSpikes/(t_StimEnd-t_StimStart) #gives average firing rate in [#/sec = Hz]
print('average firing rate: ', r_ave, 'Hz')
average firing rate: 26.666666666666668 Hz
Part 3: Compare theoretical firing rate vs. average firing rate #
We want to verify the theoretical firing rate formula we derived in my last blog post:
where
We compare this theoretical value with the average firing rates we generate from our simulations using the LIF model we constructed above. We'll do this for several values of
#Step 3: Define Initial Values and Vectors to Hold Results
t_vect = np.arange(0,t_end+dt,dt) #will hold vector of times
V_vect = np.zeros((len(t_vect),)) #initialize the voltage vector
V_plot_vect = np.zeros((len(t_vect),)) #pretty version of V_vect to be plotted that displays a spike whenever voltage reaches threshold
I_Stim_vect = np.arange(1.43,1.84,0.04) #magnitude of pulse of injected current [nA]
for j in range(len(I_Stim_vect)): #loop over different I_Stim values
I_Stim = I_Stim_vect[j]
V_vect[0] = E_L #value of V at t = 0
V_plot_vect[0] = V_vect[0] #if no spike, then just plot the actual voltage V
I_e_vect = np.zeros((len(t_vect),)) #injected current [nA]
I_e_vect[np.int(t_StimStart/dt):np.int(t_StimEnd/dt)+1] = I_Stim*np.ones((np.int((t_StimEnd-t_StimStart)/dt)+1,))
NumSpikes = 0 #holds number of spikes that have occurred
#Step 4: Write Leaky Integrate-and-Fire Neuron Model
for i in range(1,np.int(t_end/dt)+1): #loop through values of t in steps of dt ms, start with t=1 since we already defined V at t=0
V_vect[i] = E_L + I_e_vect[i]*R_m + (V_vect[i-1] - E_L - I_e_vect[i]*R_m)*np.exp(-dt/tau)
if (V_vect[i] > V_th): #neuron spiked
V_vect[i] = V_reset #set voltage back to V_reset
V_plot_vect[i] = V_spike #set vector that will be plotted to show a spike here
NumSpikes += 1 #add 1 to the total spike count
else: #voltage didn't cross threshold so neuron does not spike
V_plot_vect[i] = V_vect[i] #plot actual voltage
#Step 5: Make Plots
plt.plot(t_vect, V_plot_vect)
plt.title('Voltage vs. time')
plt.xlabel('Time in ms')
plt.ylabel('Voltage in mV')
plt.show()
#Step 6: Calculate average firing rate
r_ave = 1000*NumSpikes/(t_StimEnd-t_StimStart) #gives average firing rate in [#/sec = Hz]
print('Input current: ', I_Stim, 'nA')
print('Average firing rate: ', r_ave, 'Hz')
Input current: 1.43 nA
Average firing rate: 0.0 Hz
Input current: 1.47 nA
Average firing rate: 0.0 Hz
Input current: 1.51 nA
Average firing rate: 16.666666666666668 Hz
Input current: 1.55 nA
Average firing rate: 26.666666666666668 Hz
Input current: 1.59 nA
Average firing rate: 30.0 Hz
Input current: 1.63 nA
Average firing rate: 33.333333333333336 Hz
Input current: 1.67 nA
Average firing rate: 36.666666666666664 Hz
Input current: 1.71 nA
Average firing rate: 40.0 Hz
Input current: 1.75 nA
Average firing rate: 43.333333333333336 Hz
Input current: 1.79 nA
Average firing rate: 46.666666666666664 Hz
Input current: 1.83 nA
Average firing rate: 50.0 Hz
We see that as we increase the current injected into the neuron, the average firing rate also increases. We want to plot this relationship in a firing rate (Hz) vs. injected current (nA) graph. We'll add another figure that plots the theoretical firing rate vs.
We only have one more modification left. We want to add onto this graph the values of
The next block presents the final compiled code for this in silico experiment of simulating a leaky integrate-and-fire model of a neuron under a static injected current.
#Simulate a Leaky Integrate-and-Fire Neuron Model with Static Input Current
#Step 1: Import Relevant Packages
import numpy as np
import matplotlib.pyplot as plt
#Step 2: Define Parameters
dt = 0.1 #time step [ms]
t_end = 500 #total time of run [ms]
t_StimStart = 100 #time to start injecting current [ms]
t_StimEnd = 400 #time to end injecting current [ms]
E_L = -70 #resting membrane potential [mV]
V_th = -55 #spike threshold [mV]
V_reset = -75 #value to reset voltage to after a spike [mV]
V_spike = 20 #value to draw a spike to, when cell spikes [mV]
R_m = 10 #membrane resistance [MOhm]
tau = 10 #membrane time constant [ms]
#Step 3: Define Initial Values and Vectors to Hold Results
t_vect = np.arange(0,t_end+dt,dt) #will hold vector of times
V_vect = np.zeros((len(t_vect),)) #initialize the voltage vector
V_plot_vect = np.zeros((len(t_vect),)) #pretty version of V_vect to be plotted that displays a spike whenever voltage reaches threshold
I_Stim_vect = np.arange(1.43,1.84,0.04) #magnitude of pulse of injected current [nA]
r_ave_vect = np.zeros((len(I_Stim_vect),)) #vector that will hold the average firing rates for different input current values
for j in range(len(I_Stim_vect)): #loop over different I_Stim values
I_Stim = I_Stim_vect[j]
V_vect[0] = E_L #value of V at t = 0
V_plot_vect[0] = V_vect[0] #if no spike, then just plot the actual voltage V
I_e_vect = np.zeros((len(t_vect),)) #injected current [nA]
I_e_vect[np.int(t_StimStart/dt):np.int(t_StimEnd/dt)+1] = I_Stim*np.ones((np.int((t_StimEnd-t_StimStart)/dt)+1,))
NumSpikes = 0 #holds number of spikes that have occurred
#Step 4: Write Leaky Integrate-and-Fire Neuron Model
for i in range(1,np.int(t_end/dt)+1): #loop through values of t in steps of dt ms, start with t=1 since we already defined V at t=0
V_vect[i] = E_L + I_e_vect[i]*R_m + (V_vect[i-1] - E_L - I_e_vect[i]*R_m)*np.exp(-dt/tau)
if (V_vect[i] > V_th): #neuron spiked
V_vect[i] = V_reset #set voltage back to V_reset
V_plot_vect[i] = V_spike #set vector that will be plotted to show a spike here
NumSpikes += 1 #add 1 to the total spike count
else: #voltage didn't cross threshold so neuron does not spike
V_plot_vect[i] = V_vect[i] #plot actual voltage
#Step 5: Make Plots
plt.plot(t_vect, V_plot_vect)
plt.title('Voltage vs. time')
plt.xlabel('Time in ms')
plt.ylabel('Voltage in mV')
plt.show()
#Step 6: Calculate average firing rate
r_ave = 1000*NumSpikes/(t_StimEnd-t_StimStart) #gives average firing rate in [#/sec = Hz]
r_ave_vect[j] = r_ave #store the average firing rate
print('Input current: ', I_Stim, 'nA')
print('Average firing rate: ', r_ave, 'Hz')
#Step 7: Compare theoretical interspike-interval firing rate with simulated average firing rate
I_th = (V_th - E_L)/R_m #neuron will not fire if the input current is below this level
I_vect = np.arange(I_th+0.001,1.8,0.001) #vector of injected current for producing theory plot
r_isi = 1000/(tau*np.log((R_m*I_vect+E_L-V_reset)/(R_m*I_vect+E_L-V_th))) #theoretical interspike-interval firing rate values
plt.plot(I_vect,r_isi)
plt.plot(I_Stim_vect,r_ave_vect,'ro')
plt.legend(['r_isi','r_ave'], loc = 'best')
plt.title('Comparison of r_isi vs. I_e and r_ave vs. I_e')
plt.xlabel('Input current [nA]')
plt.ylabel('Firing rate [Hz]')
plt.show()
Input current: 1.43 nA
Average firing rate: 0.0 Hz
Input current: 1.47 nA
Average firing rate: 0.0 Hz
Input current: 1.51 nA
Average firing rate: 16.666666666666668 Hz
Input current: 1.55 nA
Average firing rate: 26.666666666666668 Hz
Input current: 1.59 nA
Average firing rate: 30.0 Hz
Input current: 1.63 nA
Average firing rate: 33.333333333333336 Hz
Input current: 1.67 nA
Average firing rate: 36.666666666666664 Hz
Input current: 1.71 nA
Average firing rate: 40.0 Hz
Input current: 1.75 nA
Average firing rate: 43.333333333333336 Hz
Input current: 1.79 nA
Average firing rate: 46.666666666666664 Hz
Input current: 1.83 nA
Average firing rate: 50.0 Hz
As can be seen from the graph above, the plot of average firing rates from our simulations match the theoretical firing rate curve whose formula we derived analytically. The small discrepancies are due to the difference between our experiment's time window of counting and the total number of interspike intervals. Theoretically, if we started our time window of counting spikes for computing
Now it's your time to experiment! #
Modify the code above and let's put our thinking caps on!
- Conduct simulations to find the value of the threshold current
. Compare your output with the analytical value. - Can you find a time window that will make the plot of average firing rates perfectly match the theoretical firing rate curve?
- Play around the relationships of the resting potential
, threshold voltage and the reset value . What happens if is closer to ? What happens if is far smaller than ? What if these three values are closer to each other? farther? - What happens if we change the value of the injected current halfway through the stimulation? What if it changes four times all throughout? What if it changes continuously as a function of time?