In the previous tutorial, you learned about the Wilson-Cowan rate model. Here we will dive into some deeper analyses of this model.
Bonus steps:
Applications of Wilson-Cowan model:
Reference paper:
Wilson, H., and Cowan, J. (1972). Excitatory and inhibitory interactions in localized populations of model neurons. Biophysical Journal 12. doi: 10.1016/S0006-3495(72)86068-5
# Imports
import matplotlib.pyplot as plt
import numpy as np
import scipy.optimize as opt # root-finding algorithm
# @title Figure Settings
import logging
logging.getLogger('matplotlib.font_manager').disabled = True
import ipywidgets as widgets # interactive display
%config InlineBackend.figure_format = 'retina'
plt.style.use("https://raw.githubusercontent.com/NeuromatchAcademy/course-content/main/nma.mplstyle")
# @title Plotting Functions
def plot_FI_inverse(x, a, theta):
f, ax = plt.subplots()
ax.plot(x, F_inv(x, a=a, theta=theta))
ax.set(xlabel="$x$", ylabel="$F^{-1}(x)$")
def plot_FI_EI(x, FI_exc, FI_inh):
plt.figure()
plt.plot(x, FI_exc, 'b', label='E population')
plt.plot(x, FI_inh, 'r', label='I population')
plt.legend(loc='lower right')
plt.xlabel('x (a.u.)')
plt.ylabel('F(x)')
plt.show()
def my_test_plot(t, rE1, rI1, rE2, rI2):
plt.figure()
ax1 = plt.subplot(211)
ax1.plot(pars['range_t'], rE1, 'b', label='E population')
ax1.plot(pars['range_t'], rI1, 'r', label='I population')
ax1.set_ylabel('Activity')
ax1.legend(loc='best')
ax2 = plt.subplot(212, sharex=ax1, sharey=ax1)
ax2.plot(pars['range_t'], rE2, 'b', label='E population')
ax2.plot(pars['range_t'], rI2, 'r', label='I population')
ax2.set_xlabel('t (ms)')
ax2.set_ylabel('Activity')
ax2.legend(loc='best')
plt.tight_layout()
plt.show()
def plot_nullclines(Exc_null_rE, Exc_null_rI, Inh_null_rE, Inh_null_rI):
plt.figure()
plt.plot(Exc_null_rE, Exc_null_rI, 'b', label='E nullcline')
plt.plot(Inh_null_rE, Inh_null_rI, 'r', label='I nullcline')
plt.xlabel(r'$r_E$')
plt.ylabel(r'$r_I$')
plt.legend(loc='best')
plt.show()
def my_plot_nullcline(pars):
Exc_null_rE = np.linspace(-0.01, 0.96, 100)
Exc_null_rI = get_E_nullcline(Exc_null_rE, **pars)
Inh_null_rI = np.linspace(-.01, 0.8, 100)
Inh_null_rE = get_I_nullcline(Inh_null_rI, **pars)
plt.plot(Exc_null_rE, Exc_null_rI, 'b', label='E nullcline')
plt.plot(Inh_null_rE, Inh_null_rI, 'r', label='I nullcline')
plt.xlabel(r'$r_E$')
plt.ylabel(r'$r_I$')
plt.legend(loc='best')
def my_plot_vector(pars, my_n_skip=2, myscale=5):
EI_grid = np.linspace(0., 1., 20)
rE, rI = np.meshgrid(EI_grid, EI_grid)
drEdt, drIdt = EIderivs(rE, rI, **pars)
n_skip = my_n_skip
plt.quiver(rE[::n_skip, ::n_skip], rI[::n_skip, ::n_skip],
drEdt[::n_skip, ::n_skip], drIdt[::n_skip, ::n_skip],
angles='xy', scale_units='xy', scale=myscale, facecolor='c')
plt.xlabel(r'$r_E$')
plt.ylabel(r'$r_I$')
def my_plot_trajectory(pars, mycolor, x_init, mylabel):
pars = pars.copy()
pars['rE_init'], pars['rI_init'] = x_init[0], x_init[1]
rE_tj, rI_tj = simulate_wc(**pars)
plt.plot(rE_tj, rI_tj, color=mycolor, label=mylabel)
plt.plot(x_init[0], x_init[1], 'o', color=mycolor, ms=8)
plt.xlabel(r'$r_E$')
plt.ylabel(r'$r_I$')
def my_plot_trajectories(pars, dx, n, mylabel):
"""
Solve for I along the E_grid from dE/dt = 0.
Expects:
pars : Parameter dictionary
dx : increment of initial values
n : n*n trjectories
mylabel : label for legend
Returns:
figure of trajectory
"""
pars = pars.copy()
for ie in range(n):
for ii in range(n):
pars['rE_init'], pars['rI_init'] = dx * ie, dx * ii
rE_tj, rI_tj = simulate_wc(**pars)
if (ie == n-1) & (ii == n-1):
plt.plot(rE_tj, rI_tj, 'gray', alpha=0.8, label=mylabel)
else:
plt.plot(rE_tj, rI_tj, 'gray', alpha=0.8)
plt.xlabel(r'$r_E$')
plt.ylabel(r'$r_I$')
def plot_complete_analysis(pars):
plt.figure(figsize=(7.7, 6.))
# plot example trajectories
my_plot_trajectories(pars, 0.2, 6,
'Sample trajectories \nfor different init. conditions')
my_plot_trajectory(pars, 'orange', [0.6, 0.8],
'Sample trajectory for \nlow activity')
my_plot_trajectory(pars, 'm', [0.6, 0.6],
'Sample trajectory for \nhigh activity')
# plot nullclines
my_plot_nullcline(pars)
# plot vector field
EI_grid = np.linspace(0., 1., 20)
rE, rI = np.meshgrid(EI_grid, EI_grid)
drEdt, drIdt = EIderivs(rE, rI, **pars)
n_skip = 2
plt.quiver(rE[::n_skip, ::n_skip], rI[::n_skip, ::n_skip],
drEdt[::n_skip, ::n_skip], drIdt[::n_skip, ::n_skip],
angles='xy', scale_units='xy', scale=5., facecolor='c')
plt.legend(loc=[1.02, 0.57], handlelength=1)
plt.show()
def plot_fp(x_fp, position=(0.02, 0.1), rotation=0):
plt.plot(x_fp[0], x_fp[1], 'ko', ms=8)
plt.text(x_fp[0] + position[0], x_fp[1] + position[1],
f'Fixed Point1=\n({x_fp[0]:.3f}, {x_fp[1]:.3f})',
horizontalalignment='center', verticalalignment='bottom',
rotation=rotation)
# @title Helper functions
def default_pars(**kwargs):
pars = {}
# Excitatory parameters
pars['tau_E'] = 1. # Timescale of the E population [ms]
pars['a_E'] = 1.2 # Gain of the E population
pars['theta_E'] = 2.8 # Threshold of the E population
# Inhibitory parameters
pars['tau_I'] = 2.0 # Timescale of the I population [ms]
pars['a_I'] = 1.0 # Gain of the I population
pars['theta_I'] = 4.0 # Threshold of the I population
# Connection strength
pars['wEE'] = 9. # E to E
pars['wEI'] = 4. # I to E
pars['wIE'] = 13. # E to I
pars['wII'] = 11. # I to I
# External input
pars['I_ext_E'] = 0.
pars['I_ext_I'] = 0.
# simulation parameters
pars['T'] = 50. # Total duration of simulation [ms]
pars['dt'] = .1 # Simulation time step [ms]
pars['rE_init'] = 0.2 # Initial value of E
pars['rI_init'] = 0.2 # Initial value of I
# External parameters if any
for k in kwargs:
pars[k] = kwargs[k]
# Vector of discretized time points [ms]
pars['range_t'] = np.arange(0, pars['T'], pars['dt'])
return pars
def F(x, a, theta):
"""
Population activation function, F-I curve
Args:
x : the population input
a : the gain of the function
theta : the threshold of the function
Returns:
f : the population activation response f(x) for input x
"""
# add the expression of f = F(x)
f = (1 + np.exp(-a * (x - theta)))**-1 - (1 + np.exp(a * theta))**-1
return f
def dF(x, a, theta):
"""
Derivative of the population activation function.
Args:
x : the population input
a : the gain of the function
theta : the threshold of the function
Returns:
dFdx : Derivative of the population activation function.
"""
dFdx = a * np.exp(-a * (x - theta)) * (1 + np.exp(-a * (x - theta)))**-2
return dFdx
def F_inv(x, a, theta):
"""
Args:
x : the population input
a : the gain of the function
theta : the threshold of the function
Returns:
F_inverse : value of the inverse function
"""
# Calculate Finverse (ln(x) can be calculated as np.log(x))
F_inverse = -1/a * np.log((x + (1 + np.exp(a * theta))**-1)**-1 - 1) + theta
return F_inverse
def get_E_nullcline(rE, a_E, theta_E, wEE, wEI, I_ext_E, **other_pars):
"""
Solve for rI along the rE from drE/dt = 0.
Args:
rE : response of excitatory population
a_E, theta_E, wEE, wEI, I_ext_E : Wilson-Cowan excitatory parameters
Other parameters are ignored
Returns:
rI : values of inhibitory population along the nullcline on the rE
"""
# calculate rI for E nullclines on rI
rI = 1 / wEI * (wEE * rE - F_inv(rE, a_E, theta_E) + I_ext_E)
return rI
def get_I_nullcline(rI, a_I, theta_I, wIE, wII, I_ext_I, **other_pars):
"""
Solve for E along the rI from dI/dt = 0.
Args:
rI : response of inhibitory population
a_I, theta_I, wIE, wII, I_ext_I : Wilson-Cowan inhibitory parameters
Other parameters are ignored
Returns:
rE : values of the excitatory population along the nullcline on the rI
"""
# calculate rE for I nullclines on rI
rE = 1 / wIE * (wII * rI + F_inv(rI, a_I, theta_I) - I_ext_I)
return rE
def EIderivs(rE, rI,
tau_E, a_E, theta_E, wEE, wEI, I_ext_E,
tau_I, a_I, theta_I, wIE, wII, I_ext_I,
**other_pars):
"""Time derivatives for E/I variables (dE/dt, dI/dt)."""
# Compute the derivative of rE
drEdt = (-rE + F(wEE * rE - wEI * rI + I_ext_E, a_E, theta_E)) / tau_E
# Compute the derivative of rI
drIdt = (-rI + F(wIE * rE - wII * rI + I_ext_I, a_I, theta_I)) / tau_I
return drEdt, drIdt
def simulate_wc(tau_E, a_E, theta_E, tau_I, a_I, theta_I,
wEE, wEI, wIE, wII, I_ext_E, I_ext_I,
rE_init, rI_init, dt, range_t, **other_pars):
"""
Simulate the Wilson-Cowan equations
Args:
Parameters of the Wilson-Cowan model
Returns:
rE, rI (arrays) : Activity of excitatory and inhibitory populations
"""
# Initialize activity arrays
Lt = range_t.size
rE = np.append(rE_init, np.zeros(Lt - 1))
rI = np.append(rI_init, np.zeros(Lt - 1))
I_ext_E = I_ext_E * np.ones(Lt)
I_ext_I = I_ext_I * np.ones(Lt)
# Simulate the Wilson-Cowan equations
for k in range(Lt - 1):
# Calculate the derivative of the E population
drE = dt / tau_E * (-rE[k] + F(wEE * rE[k] - wEI * rI[k] + I_ext_E[k],
a_E, theta_E))
# Calculate the derivative of the I population
drI = dt / tau_I * (-rI[k] + F(wIE * rE[k] - wII * rI[k] + I_ext_I[k],
a_I, theta_I))
# Update using Euler's method
rE[k + 1] = rE[k] + drE
rI[k + 1] = rI[k] + drI
return rE, rI
The helper functions included:
default_pars(**kwargs)
. You can use:pars = default_pars()
to get all the parameters, and then you can execute print(pars)
to check these parameters.pars = default_pars(T=T_sim, dt=time_step)
to set a different simulation time and time steppars = default_pars()
, use par['New_para'] = value
to add a new parameter with its valuefunc(**pars)
F(x, a, theta)
dF(x, a, theta)
F_inv
get_E_nullcline
, get_I_nullcline
EIderivs
simulate_wc
As in Tutorial 2, we will be looking at the Wilson-Cowan model, with coupled equations representing the dynamics of the excitatory or inhibitory population:
τEdrEdt=−rE+FE(wEErE−wEIrI+IextE;aE,θE)τIdrIdt=−rI+FI(wIErE−wIIrI+IextI;aI,θI)(1)rE(t) represents the average activation (or firing rate) of the excitatory population at time t, and rI(t) the activation (or firing rate) of the inhibitory population. The parameters τE and τI control the timescales of the dynamics of each population. Connection strengths are given by: wEE (E → E), wEI (I → E), wIE (E → I), and wII (I → I). The terms wEI and wIE represent connections from inhibitory to excitatory population and vice versa, respectively. The transfer functions (or F-I curves) FE(x;aE,θE) and FI(x;aI,θI) can be different for the excitatory and the inhibitory populations.
The intersection points of the two nullcline curves are the fixed points of the Wilson-Cowan model in Equation (1).
In the next exercise, we will find the coordinate of all fixed points for a given set of parameters.
We'll make use of two functions, similar to ones we saw in Tutorial 1, which use a root-finding algorithm to find the fixed points of the system with Excitatory and Inhibitory populations.
# @markdown Execute to visualize nullclines
# Set parameters
pars = default_pars()
Exc_null_rE = np.linspace(-0.01, 0.96, 100)
Inh_null_rI = np.linspace(-.01, 0.8, 100)
# Compute nullclines
Exc_null_rI = get_E_nullcline(Exc_null_rE, **pars)
Inh_null_rE = get_I_nullcline(Inh_null_rI, **pars)
plot_nullclines(Exc_null_rE, Exc_null_rI, Inh_null_rE, Inh_null_rI)
# @markdown *Execute the cell to define `my_fp` and `check_fp`*
def my_fp(pars, rE_init, rI_init):
"""
Use opt.root function to solve Equations (2)-(3) from initial values
"""
tau_E, a_E, theta_E = pars['tau_E'], pars['a_E'], pars['theta_E']
tau_I, a_I, theta_I = pars['tau_I'], pars['a_I'], pars['theta_I']
wEE, wEI = pars['wEE'], pars['wEI']
wIE, wII = pars['wIE'], pars['wII']
I_ext_E, I_ext_I = pars['I_ext_E'], pars['I_ext_I']
# define the right hand of wilson-cowan equations
def my_WCr(x):
rE, rI = x
drEdt = (-rE + F(wEE * rE - wEI * rI + I_ext_E, a_E, theta_E)) / tau_E
drIdt = (-rI + F(wIE * rE - wII * rI + I_ext_I, a_I, theta_I)) / tau_I
y = np.array([drEdt, drIdt])
return y
x0 = np.array([rE_init, rI_init])
x_fp = opt.root(my_WCr, x0).x
return x_fp
def check_fp(pars, x_fp, mytol=1e-6):
"""
Verify (drE/dt)^2 + (drI/dt)^2< mytol
Args:
pars : Parameter dictionary
fp : value of fixed point
mytol : tolerance, default as 10^{-6}
Returns :
Whether it is a correct fixed point: True/False
"""
drEdt, drIdt = EIderivs(x_fp[0], x_fp[1], **pars)
return drEdt**2 + drIdt**2 < mytol
help(my_fp)
Help on function my_fp in module __main__: my_fp(pars, rE_init, rI_init) Use opt.root function to solve Equations (2)-(3) from initial values
From the above nullclines, we notice that the system features three fixed points with the parameters we used. To find their coordinates, we need to choose a proper initial value to give to the opt.root
function inside of the function my_fp
we just defined, since the algorithm can only find fixed points in the vicinity of the initial value.
In this exercise, you will use the function my_fp
to find each of the fixed points by varying the initial values. Note that you can choose the values near the intersections of the nullclines as the initial values to calculate the fixed points.
pars = default_pars()
######################################################################
# TODO: Provide initial values to calculate the fixed points
# Check if x_fp's are the correct with the function check_fp(x_fp)
# Hint: vary different initial values to find the correct fixed points
raise NotImplementedError('student exercise: find fixed points')
######################################################################
my_plot_nullcline(pars)
# Find the first fixed point
x_fp_1 = my_fp(pars, ..., ...)
if check_fp(pars, x_fp_1):
plot_fp(x_fp_1)
# Find the second fixed point
x_fp_2 = my_fp(pars, ..., ...)
if check_fp(pars, x_fp_2):
plot_fp(x_fp_2)
# Find the third fixed point
x_fp_3 = my_fp(pars, ..., ...)
if check_fp(pars, x_fp_3):
plot_fp(x_fp_3)
--------------------------------------------------------------------------- NotImplementedError Traceback (most recent call last) <ipython-input-7-06b854545a9e> in <cell line: 7>() 5 # Check if x_fp's are the correct with the function check_fp(x_fp) 6 # Hint: vary different initial values to find the correct fixed points ----> 7 raise NotImplementedError('student exercise: find fixed points') 8 ###################################################################### 9 NotImplementedError: student exercise: find fixed points
First, let's first rewrite the system 1 as:
drEdt=GE(rE,rI)drIdt=GI(rE,rI)where
GE(rE,rI)=1τE[−rE+FE(wEErE−wEIrI+IextE;a,θ)]GI(rE,rI)=1τI[−rI+FI(wIErE−wIIrI+IextI;a,θ)]By definition, drEdt=0 and drIdt=0 at each fixed point. Therefore, if the initial state is exactly at the fixed point, the state of the system will not change as time evolves.
However, if the initial state deviates slightly from the fixed point, there are two possibilities the trajectory will be attracted back to the
These two possibilities define the type of fixed point, i.e., stable or unstable. Similar to the 1D system studied in the previous tutorial, the stability of a fixed point (r∗E,r∗I) can be determined by linearizing the dynamics of the system (can you figure out how?). The linearization will yield a matrix of first-order derivatives called the Jacobian matrix:
J=[∂∂rEGE(r∗E,r∗I)∂∂rIGE(r∗E,r∗I)∂∂rEGI(r∗E,r∗I)∂∂rIGI(r∗E,r∗I)](7)The eigenvalues of the Jacobian matrix calculated at the fixed point will determine whether it is a stable or unstable fixed point.
We can now compute the derivatives needed to build the Jacobian matrix. Using the chain and product rules the derivatives for the excitatory population are given by:
The same applies to the inhibitory population.
Here, you can use dF(x,a,theta)
defined in the Helper functions
to calculate the derivative of the F-I curve.
def get_eig_Jacobian(fp,
tau_E, a_E, theta_E, wEE, wEI, I_ext_E,
tau_I, a_I, theta_I, wIE, wII, I_ext_I, **other_pars):
"""Compute eigenvalues of the Wilson-Cowan Jacobian matrix at fixed point."""
# Initialization
rE, rI = fp
J = np.zeros((2, 2))
###########################################################################
# TODO for students: compute J and disable the error
raise NotImplementedError("Student exercise: compute the Jacobian matrix")
###########################################################################
# Compute the four elements of the Jacobian matrix
J[0, 0] = ...
J[0, 1] = ...
J[1, 0] = ...
J[1, 1] = ...
# Compute and return the eigenvalues
evals = np.linalg.eig(J)[0]
return evals
# Compute eigenvalues of Jacobian
eig_1 = get_eig_Jacobian(x_fp_1, **pars)
eig_2 = get_eig_Jacobian(x_fp_2, **pars)
eig_3 = get_eig_Jacobian(x_fp_3, **pars)
print(eig_1, 'Stable point')
print(eig_2, 'Unstable point')
print(eig_3, 'Stable point')
As is evident, the stable fixed points correspond to the negative eigenvalues, while unstable points correspond to at least one positive eigenvalue.
The sign of the eigenvalues is determined by the connectivity (interaction) between excitatory and inhibitory populations.
Below we investigate the effect of wEE on the nullclines and the eigenvalues of the dynamical system.
* Critical change is referred to as pitchfork bifurcation.
wEE
on the nullclines and the eigenvalues¶How do the nullclines move for different values of the parameter wEE? What does this mean for fixed points and system activity?
# @markdown Make sure you execute this cell to enable the widget!
@widgets.interact(
wEE=widgets.FloatSlider(6., min=6., max=10., step=0.01)
)
def plot_nullcline_diffwEE(wEE):
"""
plot nullclines for different values of wEE
"""
pars = default_pars(wEE=wEE)
# plot the E, I nullclines
Exc_null_rE = np.linspace(-0.01, .96, 100)
Exc_null_rI = get_E_nullcline(Exc_null_rE, **pars)
Inh_null_rI = np.linspace(-.01, .8, 100)
Inh_null_rE = get_I_nullcline(Inh_null_rI, **pars)
plt.figure(figsize=(12, 5.5))
plt.subplot(121)
plt.plot(Exc_null_rE, Exc_null_rI, 'b', label='E nullcline')
plt.plot(Inh_null_rE, Inh_null_rI, 'r', label='I nullcline')
plt.xlabel(r'$r_E$')
plt.ylabel(r'$r_I$')
plt.legend(loc='best')
plt.subplot(222)
pars['rE_init'], pars['rI_init'] = 0.2, 0.2
rE, rI = simulate_wc(**pars)
plt.plot(pars['range_t'], rE, 'b', label='E population', clip_on=False)
plt.plot(pars['range_t'], rI, 'r', label='I population', clip_on=False)
plt.ylabel('Activity')
plt.legend(loc='best')
plt.ylim(-0.05, 1.05)
plt.title('E/I activity\nfor different initial conditions',
fontweight='bold')
plt.subplot(224)
pars['rE_init'], pars['rI_init'] = 0.4, 0.1
rE, rI = simulate_wc(**pars)
plt.plot(pars['range_t'], rE, 'b', label='E population', clip_on=False)
plt.plot(pars['range_t'], rI, 'r', label='I population', clip_on=False)
plt.xlabel('t (ms)')
plt.ylabel('Activity')
plt.legend(loc='best')
plt.ylim(-0.05, 1.05)
plt.tight_layout()
plt.show()
We can also investigate the effect of different wEI, wIE, wII, τE, τI, and IextE on the stability of fixed points. In addition, we can also consider the perturbation of the parameters of the gain curve F(⋅).
For some values of interaction terms (wEE,wIE,wEI,wII), the eigenvalues can become complex. When at least one pair of eigenvalues is complex, oscillations arise. The stability of oscillations is determined by the real part of the eigenvalues (+ve real part oscillations will grow, -ve real part oscillations will die out). The size of the complex part determines the frequency of oscillations.
For instance, if we use a different set of parameters, wEE=6.4, wEI=4.8, wIE=6., wII=1.2, and IextE=0.8, then we shall observe that the E and I population activity start to oscillate! Please execute the cell below to check the oscillatory behavior.
# @markdown Make sure you execute this cell to see the oscillations!
pars = default_pars(T=100.)
pars['wEE'], pars['wEI'] = 6.4, 4.8
pars['wIE'], pars['wII'] = 6.0, 1.2
pars['I_ext_E'] = 0.8
pars['rE_init'], pars['rI_init'] = 0.25, 0.25
rE, rI = simulate_wc(**pars)
plt.figure(figsize=(8, 5.5))
plt.plot(pars['range_t'], rE, 'b', label=r'$r_E$')
plt.plot(pars['range_t'], rI, 'r', label=r'$r_I$')
plt.xlabel('t (ms)')
plt.ylabel('Activity')
plt.legend(loc='best')
plt.show()
We can also understand the oscillations of the population behavior using the phase plane. By plotting a set of trajectories with different initial states, we can see that these trajectories will move in a circle instead of converging to a fixed point. This circle is called "limit cycle" and shows the periodic oscillations of the E and I population behavior under some conditions.
Let's plot the phase plane using the previously defined functions.
# @markdown Execute to visualize phase plane
pars = default_pars(T=100.)
pars['wEE'], pars['wEI'] = 6.4, 4.8
pars['wIE'], pars['wII'] = 6.0, 1.2
pars['I_ext_E'] = 0.8
plt.figure(figsize=(7, 5.5))
my_plot_nullcline(pars)
# Find the correct fixed point
x_fp_1 = my_fp(pars, 0.8, 0.8)
if check_fp(pars, x_fp_1):
plot_fp(x_fp_1, position=(0, 0), rotation=40)
my_plot_trajectories(pars, 0.2, 3,
'Sample trajectories \nwith different initial values')
my_plot_vector(pars)
plt.legend(loc=[1.01, 0.7])
plt.xlim(-0.05, 1.01)
plt.ylim(-0.05, 0.65)
plt.show()
From the above examples, the change of model parameters changes the shape of the nullclines and, accordingly, the behavior of the E and I populations from steady fixed points to oscillations. However, the shape of the nullclines is unable to fully determine the behavior of the network. The vector field also matters. To demonstrate this, here, we will investigate the effect of time constants on the population behavior. By changing the inhibitory time constant τI, the nullclines do not change, but the network behavior changes substantially from steady state to oscillations with different frequencies.
Such a dramatic change in the system behavior is referred to as a bifurcation.
# @markdown Make sure you execute this cell to enable the widget!
@widgets.interact(
tau_i=widgets.FloatSlider(1.5, min=0.2, max=3., step=.1)
)
def time_constant_effect(tau_i=0.5):
pars = default_pars(T=100.)
pars['wEE'], pars['wEI'] = 6.4, 4.8
pars['wIE'], pars['wII'] = 6.0, 1.2
pars['I_ext_E'] = 0.8
pars['tau_I'] = tau_i
Exc_null_rE = np.linspace(0.0, .9, 100)
Inh_null_rI = np.linspace(0.0, .6, 100)
Exc_null_rI = get_E_nullcline(Exc_null_rE, **pars)
Inh_null_rE = get_I_nullcline(Inh_null_rI, **pars)
plt.figure(figsize=(12.5, 5.5))
plt.subplot(121) # nullclines
plt.plot(Exc_null_rE, Exc_null_rI, 'b', label='E nullcline', zorder=2)
plt.plot(Inh_null_rE, Inh_null_rI, 'r', label='I nullcline', zorder=2)
plt.xlabel(r'$r_E$')
plt.ylabel(r'$r_I$')
# fixed point
x_fp_1 = my_fp(pars, 0.5, 0.5)
plt.plot(x_fp_1[0], x_fp_1[1], 'ko', zorder=2)
eig_1 = get_eig_Jacobian(x_fp_1, **pars)
# trajectories
for ie in range(5):
for ii in range(5):
pars['rE_init'], pars['rI_init'] = 0.1 * ie, 0.1 * ii
rE_tj, rI_tj = simulate_wc(**pars)
plt.plot(rE_tj, rI_tj, 'k', alpha=0.3, zorder=1)
# vector field
EI_grid_E = np.linspace(0., 1.0, 20)
EI_grid_I = np.linspace(0., 0.6, 20)
rE, rI = np.meshgrid(EI_grid_E, EI_grid_I)
drEdt, drIdt = EIderivs(rE, rI, **pars)
n_skip = 2
plt.quiver(rE[::n_skip, ::n_skip], rI[::n_skip, ::n_skip],
drEdt[::n_skip, ::n_skip], drIdt[::n_skip, ::n_skip],
angles='xy', scale_units='xy', scale=10, facecolor='c')
plt.title(r'$\tau_I=$'+'%.1f ms' % tau_i)
plt.subplot(122) # sample E/I trajectories
pars['rE_init'], pars['rI_init'] = 0.25, 0.25
rE, rI = simulate_wc(**pars)
plt.plot(pars['range_t'], rE, 'b', label=r'$r_E$')
plt.plot(pars['range_t'], rI, 'r', label=r'$r_I$')
plt.xlabel('t (ms)')
plt.ylabel('Activity')
plt.title(r'$\tau_I=$'+'%.1f ms' % tau_i)
plt.legend(loc='best')
plt.tight_layout()
plt.show()
Both τE and τI feature in the Jacobian of the two population network (eq 7). So here is seems that the by increasing τI the eigenvalues corresponding to the stable fixed point are becoming complex.
Intuitively, when τI is smaller, inhibitory activity changes faster than excitatory activity. As inhibition exceeds above a certain value, high inhibition inhibits excitatory population but that in turns means that inhibitory population gets smaller input (from the exc. connection). So inhibition decreases rapidly. But this means that excitation recovers -- and so on ...
As described above, one can obtain the linear approximation around the fixed point as
ddr→R=[∂GE∂rE∂GE∂rI∂GI∂rE∂GI∂rI]→R,where →R=[rE,rI]T is the vector of the E/I activity.
Let's direct our attention to the excitatory subpopulation which follows:
Recall that, around fixed point (r∗E,r∗I):
From Equation. (8), it is clear that ∂GE∂rI is negative since the dFdx is always positive. It can be understood by that the recurrent inhibition from the inhibitory activity (I) can reduce the excitatory (E) activity. However, as described above, ∂GE∂rE has negative terms related to the "leak" effect, and positive terms related to the recurrent excitation. Therefore, it leads to two different regimes:
network (non-ISN) regime**
network (ISN) regime**
Implement the function to calculate the ∂GE∂rE for the default parameters, and the parameters of the limit cycle case.
def get_dGdE(fp, tau_E, a_E, theta_E, wEE, wEI, I_ext_E, **other_pars):
"""
Compute dGdE
Args:
fp : fixed point (E, I), array
Other arguments are parameters of the Wilson-Cowan model
Returns:
J : the 2x2 Jacobian matrix
"""
rE, rI = fp
##########################################################################
# TODO for students: compute dGdrE and disable the error
raise NotImplementedError("Student exercise: compute the dG/dE, Eq. (13)")
##########################################################################
# Calculate the J[0,0]
dGdrE = ...
return dGdrE
# Get fixed points
pars = default_pars()
x_fp_1 = my_fp(pars, 0.1, 0.1)
x_fp_2 = my_fp(pars, 0.3, 0.3)
x_fp_3 = my_fp(pars, 0.8, 0.6)
# Compute dGdE
dGdrE1 = get_dGdE(x_fp_1, **pars)
dGdrE2 = get_dGdE(x_fp_2, **pars)
dGdrE3 = get_dGdE(x_fp_3, **pars)
print(f'For the default case:')
print(f'dG/drE(fp1) = {dGdrE1:.3f}')
print(f'dG/drE(fp2) = {dGdrE2:.3f}')
print(f'dG/drE(fp3) = {dGdrE3:.3f}')
print('\n')
pars = default_pars(wEE=6.4, wEI=4.8, wIE=6.0, wII=1.2, I_ext_E=0.8)
x_fp_lc = my_fp(pars, 0.8, 0.8)
dGdrE_lc = get_dGdE(x_fp_lc, **pars)
print('For the limit cycle case:')
print(f'dG/drE(fp_lc) = {dGdrE_lc:.3f}')
SAMPLE OUTPUT
For the default case:
dG/drE(fp1) = -0.650
dG/drE(fp2) = 1.519
dG/drE(fp3) = -0.706
For the limit cycle case:
dG/drE(fp_lc) = 0.837
Recall that the E nullcline follows
That is, the firing rate rE can be a function of rI. Let's take the derivative of rE over rI, and obtain
That is, in the phase plane rI-rE
-plane, we can obtain the slope along the E nullcline as
Similarly, we can obtain the slope along the I nullcline as
drIdrE=F′IwIEF′IwII+1(13)Then, we can find that (drIdrE)I−nullcline>0 in Equation (13).
However, in Equation (12), the sign of (drIdrE)E−nullcline depends on the sign of (F′EwEE−1). Note that, (F′EwEE−1) is the same as what we show above (Equation (8)). Therefore, we can have the following results:
network (non-ISN) regime**
network (ISN) regime**
In addition, it is important to point out the following two conclusions:
Conclusion 1: The stability of a fixed point can determine the relationship between the slopes Equations (12) and (13). As discussed above, the fixed point is stable when the Jacobian matrix (J in Equation (7)) has two eigenvalues with a negative real part, which indicates a positive determinant of J, i.e., det(J)>0.
From the Jacobian matrix definition and from Equations (8-11), we can obtain:
J=[1τE(wEEF′E−1)−1τEwEIF′E1τIwIEF′I1τI(−wIIF′I−1)]Note that, if we let
then, using matrix notation, J=T−1(FW−I) where I is the identity matrix, i.e., I=[1001].
Therefore, det(J)=det(T−1(FW−I))=(det(T−1))(det(FW−I)).
Since det(T−1)>0, as time constants are positive by definition, the sign of det(J) is the same as the sign of det(FW−I), and so
det(FW−I)=(F′EwEI)(F′IwIE)−(F′IwII+1)(F′EwEE−1)>0.Then, combining this with Equations (12) and (13), we can obtain
(drIdrE)I−nullcline(drIdrE)E−nullcline>1.Therefore, at the stable fixed point, I nullcline has a steeper slope than the E nullcline.
Conclusion 2: Effect of adding input to the inhibitory population.
While adding the input δIextI into the inhibitory population, we can find that the E nullcline (Equation (5)) stays the same, while the I nullcline has a pure left shift: the original I nullcline equation,
remains true if we take IextI→IextI+δIextI and rE→r′E=rE−δIextIwIE to obtain
Putting these points together, we obtain the phase plane pictures shown below. After adding input to the inhibitory population, it can be seen in the trajectories above and the phase plane below that, in an ISN, rI will increase first but then decay to the new fixed point in which both rI and rE are decreased compared to the original fixed point. However, by adding δIextI into a non-ISN, rI will increase while rE will decrease.
In this interactive widget, we inject excitatory (IextI>0) or inhibitory (IextI<0) drive into the inhibitory population when the system is at its equilibrium (with parameters wEE=6.4, wEI=4.8, wIE=6., wII=1.2, IextE=0.8, τI=0.8, and IextI=0). How does the firing rate of the I population changes with excitatory vs inhibitory drive into the inhibitory population?
# @title
# @markdown Make sure you execute this cell to enable the widget!
pars = default_pars(T=50., dt=0.1)
pars['wEE'], pars['wEI'] = 6.4, 4.8
pars['wIE'], pars['wII'] = 6.0, 1.2
pars['I_ext_E'] = 0.8
pars['tau_I'] = 0.8
@widgets.interact(
dI=widgets.FloatSlider(0., min=-0.2, max=0.2, step=.05)
)
def ISN_I_perturb(dI=0.1):
Lt = len(pars['range_t'])
pars['I_ext_I'] = np.zeros(Lt)
pars['I_ext_I'][int(Lt / 2):] = dI
pars['rE_init'], pars['rI_init'] = 0.6, 0.26
rE, rI = simulate_wc(**pars)
plt.figure(figsize=(8, 1.5))
plt.plot(pars['range_t'], pars['I_ext_I'], 'k')
plt.xlabel('t (ms)')
plt.ylabel(r'$I_I^{\mathrm{ext}}$')
plt.ylim(pars['I_ext_I'].min() - 0.01, pars['I_ext_I'].max() + 0.01)
plt.show()
plt.figure(figsize=(8, 4.5))
plt.plot(pars['range_t'], rE, 'b', label=r'$r_E$')
plt.plot(pars['range_t'], rE[int(Lt / 2) - 1] * np.ones(Lt), 'b--')
plt.plot(pars['range_t'], rI, 'r', label=r'$r_I$')
plt.plot(pars['range_t'], rI[int(Lt / 2) - 1] * np.ones(Lt), 'r--')
plt.ylim(0, 0.8)
plt.xlabel('t (ms)')
plt.ylabel('Activity')
plt.legend(loc='best')
plt.show()
The input into the neurons measured in the experiment is often very noisy (links). Here, the noisy synaptic input current is modeled as an Ornstein-Uhlenbeck (OU)process, which has been discussed several times in the previous tutorials.
# @markdown Make sure you execute this cell to enable the function my_OU and plot the input current!
def my_OU(pars, sig, myseed=False):
"""
Expects:
pars : parameter dictionary
sig : noise amplitute
myseed : random seed. int or boolean
Returns:
I : Ornstein-Uhlenbeck input current
"""
# Retrieve simulation parameters
dt, range_t = pars['dt'], pars['range_t']
Lt = range_t.size
tau_ou = pars['tau_ou'] # [ms]
# set random seed
if myseed:
np.random.seed(seed=myseed)
else:
np.random.seed()
# Initialize
noise = np.random.randn(Lt)
I_ou = np.zeros(Lt)
I_ou[0] = noise[0] * sig
# generate OU
for it in range(Lt-1):
I_ou[it+1] = (I_ou[it]
+ dt / tau_ou * (0. - I_ou[it])
+ np.sqrt(2 * dt / tau_ou) * sig * noise[it + 1])
return I_ou
pars = default_pars(T=50)
pars['tau_ou'] = 1. # [ms]
sig_ou = 0.1
I_ou = my_OU(pars, sig=sig_ou, myseed=2020)
plt.figure(figsize=(8, 5.5))
plt.plot(pars['range_t'], I_ou, 'b')
plt.xlabel('Time (ms)')
plt.ylabel(r'$I_{\mathrm{OU}}$')
plt.show()
With the default parameters, the system fluctuates around a resting state with the noisy input.
# @markdown Execute this cell to plot activity with noisy input current
pars = default_pars(T=100)
pars['tau_ou'] = 1. # [ms]
sig_ou = 0.1
pars['I_ext_E'] = my_OU(pars, sig=sig_ou, myseed=20201)
pars['I_ext_I'] = my_OU(pars, sig=sig_ou, myseed=20202)
pars['rE_init'], pars['rI_init'] = 0.1, 0.1
rE, rI = simulate_wc(**pars)
plt.figure(figsize=(8, 5.5))
ax = plt.subplot(111)
ax.plot(pars['range_t'], rE, 'b', label='E population')
ax.plot(pars['range_t'], rI, 'r', label='I population')
ax.set_xlabel('t (ms)')
ax.set_ylabel('Activity')
ax.legend(loc='best')
plt.show()
Then, let's use a brief 10-ms positive current to the E population when the system is at its equilibrium. When this amplitude (SE below) is sufficiently large, a persistent activity is produced that outlasts the transient input. What is the firing rate of the persistent activity, and what is the critical input strength? Try to understand the phenomena from the above phase-plane analysis.
# @markdown Make sure you execute this cell to enable the widget!
def my_inject(pars, t_start, t_lag=10.):
"""
Expects:
pars : parameter dictionary
t_start : pulse starts [ms]
t_lag : pulse lasts [ms]
Returns:
I : extra pulse time
"""
# Retrieve simulation parameters
dt, range_t = pars['dt'], pars['range_t']
Lt = range_t.size
# Initialize
I = np.zeros(Lt)
# pulse timing
N_start = int(t_start / dt)
N_lag = int(t_lag / dt)
I[N_start:N_start + N_lag] = 1.
return I
pars = default_pars(T=100)
pars['tau_ou'] = 1. # [ms]
sig_ou = 0.1
pars['I_ext_I'] = my_OU(pars, sig=sig_ou, myseed=2021)
pars['rE_init'], pars['rI_init'] = 0.1, 0.1
# pulse
I_pulse = my_inject(pars, t_start=20., t_lag=10.)
L_pulse = sum(I_pulse > 0.)
@widgets.interact(
SE=widgets.FloatSlider(0., min=0., max=1., step=.05)
)
def WC_with_pulse(SE=0.):
pars['I_ext_E'] = my_OU(pars, sig=sig_ou, myseed=2022)
pars['I_ext_E'] += SE * I_pulse
rE, rI = simulate_wc(**pars)
plt.figure(figsize=(8, 5.5))
ax = plt.subplot(111)
ax.plot(pars['range_t'], rE, 'b', label='E population')
ax.plot(pars['range_t'], rI, 'r', label='I population')
ax.plot(pars['range_t'][I_pulse > 0.], 1.0*np.ones(L_pulse), 'r', lw=3.)
ax.text(25, 1.05, 'stimulus on', horizontalalignment='center',
verticalalignment='bottom')
ax.set_ylim(-0.03, 1.2)
ax.set_xlabel('t (ms)')
ax.set_ylabel('Activity')
ax.legend(loc='best')
plt.show()
Explore what happens when a second, brief current is applied to the inhibitory population.