1+ # -*- coding: utf-8 -*-
2+ """
3+ The following Python script runs simulations of the MQIF Neuron model,
4+ with the aims of characterising its bursting patterns.
5+ """
6+
7+ import numpy as np
8+ import matplotlib .pyplot as plt
9+ from mpl_toolkits import mplot3d
10+ plt .style .use ('seaborn' )
11+ plt .rcParams ['figure.edgecolor' ] = 'blue'
12+ plt .rcParams ['scatter.marker' ] = 'x'
13+ import time as clock
14+
15+ #ODEs governing MQIF model
16+ def Vs_dot (V , Vs , tau_s ):
17+ return 250 * (V - Vs ) / tau_s
18+
19+ def Vus_dot (V , Vus , tau_us ):
20+ return 250 * (V - Vus ) / tau_us
21+
22+ def V_dot (V , Vs , Vus , I_ext , # Current values
23+ V0 , Vs0 , Vus0 , #Intial values
24+ g_f , g_s , g_us , # Conductances
25+ C ): #Capacitance
26+
27+ return 250 * (I_ext + g_f * ((V - V0 )** 2 ) - g_s * ((Vs - Vs0 )** 2 ) - g_us * ((Vus - Vus0 )** 2 )) / C
28+
29+
30+ def simulate_MQIF (num_steps , dt , V0 , Vs0 , Vus0 , I_ext ,
31+ V_threshold = 20 , V_reset = - 45 , Vs_reset = 7.5 , delta_Vus = 1.7 ,
32+ g_f = 1.0 , g_s = 0.5 , g_us = 0.015 ,
33+ tau_s = 4.3 , tau_us = 278 ,
34+ C = 0.82 ):
35+ """
36+ Function that generate arrays holding the values of each variable as a time-series.
37+ Integrates the ODEs and fire spikes.
38+
39+ Parameters
40+ ----------
41+ num_steps, dt : INTEGER
42+ Governs the timebase of the simulation. num_steps denotes the total
43+ number of updates steps for the whole simulation. dt is the time spacing
44+ between these steps.
45+
46+ V0, Vs0, Vus0 : INTEGER/FLOAT
47+ The initial/equilibrium values in the ODEs. This is passed into the
48+ 'v_dot' ODE function. V governs membrane potential. Vs governs the slow
49+ currents within the ODEs. Vus governs the ultraslow currents.
50+
51+ I_ext : ARRAY
52+ Input current waveform.
53+
54+ V_threshold, V_reset, VS_reset, delta_Vus : INTEGER/FLOAT
55+ Paramters governing the reset characteristics. When membrane potential
56+ reaches V_threshold, a spike occurs, and the state variables are reset
57+ using these paramters.
58+
59+ g_f, g_s, g_us : FLOAT
60+ Paramters governing the associated conductances of each of the potential
61+ differences that arise within the 'v_dot' ODE function.
62+
63+ tau_s, tau_us : INTEGER/FLOAT
64+ Time constants governing the slow and ultraslow charactersitics, as set
65+ in the Vs and Vus ODEs.
66+
67+ C : FLOAT
68+ Capactiance used in the V ODE.
69+
70+ Returns
71+ -------
72+ V_values, Vs_values, Vus_values : ARRAY
73+ Timeseries containing the values of each of the state variables at each
74+ time-step. Each element is separated by 'dt' milliseconds of time.
75+
76+ spikes : ARRAY
77+ Array of integers used to relay the number of spikes within a burst.
78+ When a burst begins, the first spike is logged as '1', in an array element
79+ whose index corresponds to the time step at which it occured. The next
80+ spike is logged as '2', then '3', and so on until the burst ends and
81+ the counter is reset.
82+
83+ time : ARRAY
84+ Array to keep track of the time in seconds. Useful for plotting.
85+
86+ """
87+ time = np .arange (0 , num_steps * dt , dt )
88+
89+ # Initialise array to store spike log
90+ spikes = np .zeros (num_steps )
91+ timer = 0
92+ num_in_burst = 0
93+
94+ # Initialise arrays to store results
95+ V_values = np .zeros (num_steps )
96+ V_values [0 ] = V0
97+
98+ Vs_values = np .zeros (num_steps )
99+ Vs_values [0 ] = Vs0
100+
101+ Vus_values = np .zeros (num_steps )
102+ Vus_values [0 ] = Vus0
103+
104+ # Perform forward Euler integration
105+ for i in range (0 , num_steps - 1 ):
106+ # Set current value
107+ V_i , Vs_i , Vus_i = V_values [i ], Vs_values [i ], Vus_values [i ]
108+ I_i = I_ext [i ]
109+
110+ # Update step
111+ Vs_new = Vs_i + dt * Vs_dot (V_i , Vs_i , tau_s )
112+ Vus_new = Vus_i + dt * Vus_dot (V_i , Vus_i , tau_us )
113+
114+ V_new = V_i + dt * V_dot (V_i , Vs_i , Vus_i , I_i ,
115+ V0 , Vs0 , Vus0 ,
116+ g_f , g_s , g_us ,
117+ C )
118+
119+ # Update array
120+ if V_new > V_threshold :
121+ # Reset after spike
122+ V_values [i + 1 ] = V_reset
123+ Vs_values [i + 1 ] = Vs_reset
124+ Vus_values [i + 1 ] = Vus_new + delta_Vus
125+
126+ # Log spike according to its number in the burst.
127+ num_in_burst += 1
128+ spikes [i ] = num_in_burst
129+ timer = 0
130+
131+ else :
132+ V_values [i + 1 ] = V_new
133+ Vs_values [i + 1 ] = Vs_new
134+ Vus_values [i + 1 ] = Vus_new
135+ timer += 1
136+
137+ # If the timer exceeds 4% of the total runtime, then the burst has ended.
138+ if timer > 0.04 * num_steps :
139+ num_in_burst = 0
140+
141+ return V_values , Vs_values , Vus_values , spikes , time
142+
143+ def plot_MQIF (time , I_ext , V , Vs , Vus , state_variables = False , phase_portrait = False ):
144+ """
145+ Function for plotting a single simulation. Feed this function the generated
146+ arrays from simulate_MQIF to view the dynamics.
147+
148+ Parameters
149+ ----------
150+ time : ARRAY
151+ Array to keep track of the time in milliseconds.
152+
153+ I_ext : ARRAY
154+ Input current waveform.
155+
156+ V, Vs, Vus : ARRAY
157+ Contains the values of each state variable at each time-step. V is the
158+ most important here - membrane potential. The others are state variables
159+ used in the ODEs.
160+
161+ state_variables : BOOLEAN
162+ Set to true if you also want to plot the state variables, Vs and Vus,
163+ with respect to time.
164+
165+ phase_portrait : BOOLEAN
166+ Set to true if you also want to plot phase portraits of Vs against V and
167+ Vus against V.
168+
169+ """
170+ fig1 , axes = plt .subplots (1 , 2 , figsize = (12 ,4 ))
171+
172+ axes [0 ].plot (time , I_ext , color = 'blue' )
173+ axes [0 ].set_title ('Current Input' )
174+ axes [0 ].set_xlabel ('time (s)' )
175+ axes [0 ].set_ylabel ('I_ext (mA/nF)' )
176+
177+ axes [1 ].plot (time , V , color = 'crimson' )
178+ axes [1 ].set_title (f'MQIF Membrane Potential, dt={ time [1 ]} ' )
179+ axes [1 ].set_xlabel ('time (s)' )
180+ axes [1 ].set_ylabel ('V (mV)' )
181+
182+ if state_variables :
183+ fig2 , axes = plt .subplots (1 , 2 , figsize = (8 ,3 ))
184+
185+ axes [0 ].plot (time , Vs , color = 'mediumorchid' )
186+ axes [0 ].set_xlabel ('time (ms)' )
187+ axes [0 ].set_ylabel ('Vs (mV)' )
188+
189+ axes [1 ].plot (time , Vus , color = 'mediumorchid' )
190+ axes [1 ].set_xlabel ('time (ms)' )
191+ axes [1 ].set_ylabel ('Vus (mV)' )
192+
193+ axes [0 ].set_title ('STATE VARIABLES MQIF' , loc = 'left' )
194+
195+ plt .show ()
196+
197+ if phase_portrait :
198+ fig3 , axes = plt .subplots (1 , 2 , figsize = (8 ,3 ))
199+
200+ axes [0 ].plot (V , Vs , color = 'mediumorchid' )
201+ axes [0 ].set_xlabel ('V (mV)' )
202+ axes [0 ].set_ylabel ('Vs (mV)' )
203+
204+ axes [1 ].plot (V , Vus , color = 'mediumorchid' )
205+ axes [1 ].set_xlabel ('V (mV)' )
206+ axes [1 ].set_ylabel ('Vus (mV)' )
207+
208+ axes [0 ].set_title ('PHASE PORTRAITS MQIF' , loc = 'left' )
209+
210+ plt .show ()
211+
212+ def characterise_spiketrain (dt , spike_array ):
213+ """
214+ Function for characterising the bursting behaviour of a generated spike train.
215+
216+ Parameters
217+ ----------
218+ dt : FLOAT
219+ Time_step used in simulation
220+
221+ spike_array : ARRAY
222+ Array containing logged spikes, as described in the simulate_MQIF docstring.
223+
224+ Returns
225+ -------
226+ frequency : FLOAT
227+ Describes the burst frequency. i.e. the reciprocal of the
228+ time period of the bursts.
229+
230+ spikes_per_burst : INTEGER
231+ Reflects the number of spikes contatined within a single
232+ burst.
233+
234+ burst_duration : FLOAT
235+ The amount of time each burst lasts for.
236+
237+ duty_cycle : FLOAT
238+ The ratio between the burst duration and the time period of the membrane
239+ potential. i.e. how long for the time period is the neuron bursting for?
240+ """
241+ # Indexes of when each burst begins
242+ burst_times = np .where (spike_array == 1 )[0 ]
243+
244+ # Initialise
245+ spikes_per_burst = 0
246+ burst_duration = 0
247+ frequency = 0
248+ time_period = 1
249+
250+ if len (burst_times ) > 2 :
251+ """
252+ The following script runs if there are more than two "initial" spikes,
253+ i.e. if there are multiple spikes with logged values > 1, then we have
254+ bursting behaviour,not just continous spiking.
255+
256+ We need at least 3 bursts to be able to characterise the pattern, since
257+ the initial burst is generally unrepresentative of the others, which
258+ are uniform copies of one another. Hence, 'len(burst_times) > 2'
259+
260+ 'burst_times[0]' is avoided for the same reason - the initial burst is
261+ usually not representative of the other bursts.
262+ """
263+
264+ # Time between initial spikes = time period of bursts
265+ time_period = (burst_times [2 ]- burst_times [1 ])* dt
266+ frequency = 1 / time_period
267+
268+ # Extract a short array looking at a singular burst for analysis
269+ single_burst = spike_array [burst_times [1 ]:burst_times [2 ]]
270+
271+ # The spikes were counted within spike_array, so the final value
272+ # corresponds to the number of spikes per burst.
273+ spikes_per_burst = max (single_burst )
274+
275+ # We extracted single_burst out of the overall spike train
276+ # to use as a close-up of one burst. Therefore, the index of the
277+ # final spike within single_burst corresponds to the burst duration.
278+ burst_duration = np .argmax (single_burst )* dt
279+
280+ # We have spiking, but not bursting.
281+ elif len (burst_times ) == 1 :
282+ spikes_per_burst = 1
283+ frequency = 0 # Set frequency to zero to indicate there are no bursts.
284+
285+ duty_cycle = burst_duration * 100 / time_period
286+
287+ return frequency , spikes_per_burst , burst_duration , duty_cycle
288+
289+
290+ # Please note: dt = 0.1 is too big unfortunately. The simulation becomes inaccurate.
291+ dt = 0.0001
292+ runtime = 10 # Desired simulation time in s
293+ num_steps = int (runtime / dt )
294+
295+
296+ # Generate input current waveform. Currently set up as a step input of 5 mA
297+ amplitude = 5
298+ I_ext = np .zeros (num_steps )
299+ start_index = num_steps // 6
300+ I_ext [start_index :num_steps ] = amplitude
301+
302+
303+ # Parameters to hold constant
304+ V_t , V_r , Vs_r , d_Vus = 20 , - 45 , 7.5 , 1.3 #V_threshold, V_reset, Vs_reset, delta_Vus
305+ # g_f, g_s, g_us = 1.0, 0.5, 0.015 # Conductances
306+ g_f , g_s , g_us = 1.0 , 0.5 , 0.015 # Conductance
307+ C = 0.82 # Capacitance
308+
309+ # Simulate and charactersise
310+ t1 = clock .time ()
311+ V_MQIF , Vs , Vus , spikes , time = simulate_MQIF (num_steps , dt , - 52 , - 50 , - 52 , I_ext , V_t , V_r , Vs_r , d_Vus , g_f , g_s , g_us , 4.3 , 278 , C )
312+ f , s , t , d = characterise_spiketrain (dt , spikes )
313+ elapsed2 = clock .time () - t1
314+
315+ #print(f"A single simulation takes {elapsed1} seconds. A simulation plus characterisation takes {elapsed2} seconds.")
316+ print (f"Frequency: { f } Hz, Spikes per Burst: { s } , Duration: { t } s, Duty Cycle: { d } %" )
317+ plot_MQIF (time , I_ext , V_MQIF , Vs , Vus )
318+ f , s , t , d = characterise_spiketrain (dt , spikes )
0 commit comments