```python
from IPython.display import Image
Image('../../../python_for_probability_statistics_and_machine_learning.jpg')
```

[Python for Probability, Statistics, and Machine Learning](https://www.springer.com/fr/book/9783319307152)

```python
%pylab inline
from __future__ import division
```

    Populating the interactive namespace from numpy and matplotlib

We saw with maximum likelihood estimation how we could use the
principle of maximum likelihood to derive a formula of the data that
would estimate the underlying parameters (say, $\theta$). Under that
method, the parameter was fixed, but unknown. If we change our
perspective slightly and consider the underlying parameter as a random
variable in its own right, this leads to additional flexibility in
estimation.  This method is the simplest of the family of Bayesian
statistical methods and is most closely related to maximum likelihood
estimation. It is very popular in communications and signal processing
and is the backbone of many important algorithms in those areas.

Given that the parameter $\theta$ is also a random variable, it has a
joint distribution with the other random variables, say,
$f(x,\theta)$.  Bayes' theorem gives the following:

$$
\mathbb{P}(\theta|x) = \frac{\mathbb{P}(x|\theta)\mathbb{P}(\theta)}{\mathbb{P}(x)}
$$

 The $\mathbb{P}(x|\theta)$ term is the usual likelihood term we have
seen before. The term in the denominator is *prior* probability of the data $x$
and it explicitly makes a very powerful claim: even before collecting or
processing any data,  we know what the probability of that data is.  The $\mathbb{P}(\theta)$ is the prior probability of the
parameter. In other words, regardless of the data that is collected, this is
the probability of the parameter itself.

In a particular application, whether or not you feel justified making these
claims is something that you have to reconcile for yourself and the problem at
hand.  There are many persuasive  philosophical arguments one way or the other,
but the main thing to keep in mind when applying any method is whether or not
the assumptions are reasonable for the problem at hand.

However, for now, let's just assume that we somehow have $\mathbb{P}(\theta)$
and the next step is the maximizing of this expression over the $\theta$.
Whatever results from that maximization is the maximum a-posteriori (MAP)
estimator for $\theta$. Because the maximization takes place with respect to
$\theta$ and not $x$, we can ignore the $\mathbb{P}(x)$ part. To make things
concrete, let us return to our original coin flipping problem.  From our
earlier analysis, we know that the likelihood function for this problem is
the following:

$$
\ell(\theta) := \theta^k (1-\theta)^{ (n-k) }
$$

 where the probability of the coin coming up heads is $\theta$. The
next step is the prior probability, $\mathbb{P}(\theta)$. For this example, we
will choose the $\beta(6,6)$ distribution (shown in the top left panel of
[Figure](#fig:MAP_001)). The $\beta$ family of distributions is a gold mine
because it allows for a wide variety of distributions using few input
parameters. Now that we have all the ingredients, we turn to maximizing the
posterior function, $\mathbb{P}(\theta|x)$. Because the logarithm is convex, we
can use it to make the maximization process easier by converting the product to
a sum without changing the extrema that we are looking for. Thus, we prefer the
to work with the logarithm of $\mathbb{P}(\theta|x)$ as in the following.

$$
\mathcal{L} := \log \mathbb{P}(\theta|x) =  \log \ell(\theta) + \log\mathbb{P}(\theta) - \log\mathbb{P}(x)
$$

 This is tedious to do by hand and therefore an excellent job
for Sympy.

```python
>>> import sympy
>>> from sympy import stats as st
>>> from sympy.abc import p,k,n
# setup objective function using sympy.log
>>> obj=sympy.expand_log(sympy.log(p**k*(1-p)**(n-k)*
                         st.density(st.Beta('p',6,6))(p)))
# use calculus to maximize objective
>>> sol=sympy.solve(sympy.simplify(sympy.diff(obj,p)),p)[0]
>>> sol
(k + 5)/(n + 10)
```

    (k + 5)/(n + 10)

 which means that our MAP estimator of $\theta$ is the following:

$$
\hat{\theta}_{MAP} = \frac{k+5}{n+10}
$$

 where $k$ is the number of heads in the sample. This is obviously a
biased estimator of $\theta$,

$$
\mathbb{E}(\hat{\theta}_{MAP}) = \frac{5+n \theta}{10 +n} \neq \theta
$$

 But is this bias *bad*? Why would anyone want a biased estimator?
Remember that we constructed this entire estimator using the idea of the prior
probability of $\mathbb{P}(\theta)$ which *favors* (biases!) the estimate
according to the prior.  For example, if $\theta=1/2$, the MAP estimator
evaluates to $\hat{\theta}_{MAP}=1/2$. No bias there! This is because the peak
of the prior probability is at $\theta=1/2$.

To compute the corresponding variance for this estimator, we need this
intermediate result,

$$
\mathbb{E}(\hat{\theta}_{MAP}^2) =\frac{25 +10 n \theta + n \theta((n-1) p+1)}{(10+n)^2}
$$

 which gives the following variance,

$$
\mathbb{V}(\hat{\theta}_{MAP}) = \frac{n (1-\theta) \theta}{(n+10)^2}
$$

Let's pause and compare this to our previous maximum likelihood (ML) estimator
shown below:

$$
\hat{\theta}_{ML} = \frac{1}{n} \sum_{i=1}^n X_i =  \frac{k}{n}
$$

 As we discussed before, the ML-estimator is unbiased with the
following variance.

$$
\mathbb{V}(\hat{\theta}_{ML}) = \frac{\theta(1-\theta)}{n}
$$

 How does this variance compare to that of the MAP? The ratio of the
two is the following:

$$
\frac{\mathbb{V}(\hat{\theta}_{MAP})}{\mathbb{V}(\hat{\theta}_{ML})}=\frac{n^2}{(n+10)^2}
$$

  This ratio shows that the variance for the MAP-estimator is smaller
than that of the the ML-estimator. This is payoff for having a biased
MAP-estimator --- it requires fewer samples to estimate if the underlying
parameter is consistent with the prior probability. If not, then it will take
more samples to pull the estimator away from the bias. In the limit as $n
\rightarrow \infty$, the ratio goes to one.  This means that the benefit of the
reduced variance vanishes with enough samples.

The above discussion admits a level of arbitrariness via the prior
distribution. We don't have to choose just one prior, however. The
following shows how we can use the previous posterior distribution as the
prior for the next posterior distribution,

$$
\mathbb{P}(\theta|x_{k+1}) = \frac{\mathbb{P}(x_{k+1}|\theta)\mathbb{P}(\theta|x_k)}{\mathbb{P}(x_{k+1})}
$$

 This is a very different strategy because we are using every data
sample $x_k$ as a parameter for the posterior distribution instead of lumping
all the samples together in a summation (this is where we got the $k$
term in the prior case). This case is much harder to analyze because now
every incremental posterior distribution is itself a random function because of
the injection of the $x$ random variable. On the other hand, this is more in
line with more general Bayesian methods because it is clear that the output of
this estimation process is a posterior distribution function, not just a single
parameter estimate.

[Figure](#fig:MAP_001) illustrates this method. The graph
in the top row, far left shows the prior probability ($\beta(6,6)$) and the dot
on the top shows the most recent MAP-estimate for $\theta$. Thus, before we
obtain any data, the peak of the prior probability  is the estimate. The next
graph to right shows the effect of $x_0=0$ on the incremental prior
probability.  Note that the estimate has barely moved to the left. This is
because the influence of the data has not caused the prior probability to drift
away from the original $\beta(6,6)$-distribution.  The first two rows of the
figure all have $x_k=0$ just to illustrate how far left the original prior
probability can be moved by those data. The dots on the tops of the sub-graphs
show how the MAP estimate changes frame-by-frame as more data is incorporated.
The remaining graphs, proceeding top-down, left-to-right show the incremental
change in the prior probability for $x_k=1$.  Again, this shows how far to the right
the estimate can be pulled from where it started. For this example, there
are an equal number of $x_k=0$ and $x_k=1$ data, which corresponds
to $\theta=1/2$.

The prior probability is the $\beta(6,6)$ distribution shown in the top left panel. The dots near the peaks of each of the subgraphs indicate the MAP estimate at that frame

**Programming Tip.**

Although the IPython Notebook accompanying this section has the full source
code, the following is a quick paraphrase of how [Figure](#fig:MAP_001) was
constructed. The first step is to recursively create the
posteriors from the data. Note the example data is sorted
to make the progression easy to see as a sequence.

```python
from sympy.abc import p,x
from sympy.stats import density, Beta, Bernoulli
prior = density(Beta('p',6,6))(p)
likelihood=density(Bernoulli('x',p))(x)
data = (0,0,0,0,0,0,0,1,1,1,1,1,1,1,1)
posteriors = [prior]
for i in data:
    posteriors.append(posteriors[-1]*likelihood.subs(x,i))
```

 With the posteriors in hand, the next step
is to compute the peak values at each frame using the
`fminbound` function from Scipy's  `optimize` module.

```python
from scipy.optimize import fminbound

fig,ax = subplots(4,4,sharex=True,
                  subplot_kw={'yticks':[''],
                              'yticklabels':[''],
                              'xticks':[0,0.5,1],
                              'xticklabels':[0,0.5,1]})
fig.set_size_inches((8,8))
pvals = linspace(0,1,100)
mxvals = []
for i,j in zip(ax.flat,posteriors):
    i.plot(pvals, sympy.lambdify(p,j)(pvals),color='k')
    mxval = fminbound(sympy.lambdify(p,-j),0,1)
    mxvals.append(mxval)
    h = i.axis()[-1]
    i.axis(ymax=h*1.3)
    i.plot(mxvals[-1],h*1.2,'ok')
    i.plot(mxvals[:-1],[h*1.2]*len(mxvals[:-1]),'o',color='gray',alpha=.3)

```

The [Figure](#fig:MAP_002) is the same as [Figure](#fig:MAP_001) except that
the initial prior probability is the $\beta(1.3,1.3)$-distribution, which has a
wider lobe that the $\beta(6,6)$-distribution. As shown in the figure, this
prior has the ability to be swayed more violently one way or the other based on
the $x_k$ data that is incorporated. This means that it can more quickly adapt
to data that is not so consistent with the initial prior and thus does not
require a large amount of data in order to *unlearn* the prior probability.
Depending on the application, the ability to unlearn the prior probability or
stick with it is a design problem for the analyst. In this example, because the
data are representative of a $\theta=1/2$ parameter, both priors eventually
settle on an estimated posterior that is about the same. However, if this had
not been the case ($\theta \neq 1/2$), then the second prior
would have produced a better estimate for the same amount of data [^IPyNote].

[^IPyNote]: The IPython Notebook corresponding to this
chapter contains the source code sot hat you can try different
combinations of priors and data values.

For this example, the prior probability is the $\beta(1.3,1.3)$ distribution, which has a wider main lobe than the $\beta(6,6)$ distribution. The dots near the peaks of each of the subgraphs indicate the MAP estimate at that frame.

Because we have the entire posterior density available, we can compute
something that is closely related to the confidence interval we discussed
earlier, except in this situation, given the Bayesian interpretation, it is
called a *credible interval* or *credible set*.  The idea is that we want to
find a symmetric interval around the peak that accounts for 95% (say) of the
posterior density. This means that we can then say the probability that the
estimated parameter is within the credible interval is 95%. The computation
requires significant numerical processing because even though we have the
posterior density in hand, it is hard to integrate analytically and requires
numerical quadrature (see Scipy's `integrate` module).  [Figure](#fig:MAP_003)
shows extent of the interval and the shaded region under the posterior density
that accounts for 95%.

The credible interval in Bayesian maximum a-posteriori is the interval corresponding to the shaded region in the posterior density.

```python
from sympy.abc import p,x
prior = density(Beta('p',1.3,1.3))(p)
# prior = density(Beta('p',6,6))(p)
likelihood=density(Bernoulli('x',p))(x)
data = (0,0,0,0,0,0,0,1,1,1,1,1,1,1,1)
posteriors = [prior]
for i in data:
    posteriors.append(posteriors[-1]*likelihood.subs(x,i))
```

```python
fig,ax = subplots(4,4,sharex=True,
                  subplot_kw={'yticks':[''],
                              'yticklabels':[''],
                              'xticks':[0,0.5,1],
                              'xticklabels':[0,0.5,1]})
fig.set_size_inches((8,8))
pvals = linspace(0,1,100)
mxvals = []
for i,j in zip(ax.flat,posteriors):
    i.plot(pvals, sympy.lambdify(p,j)(pvals),color='k')
    mxval = fminbound( sympy.lambdify(p,-j),0,1)
    mxvals.append(mxval)
    h = i.axis()[-1]
    i.axis(ymax=h*1.3)
    i.plot(mxvals[-1],h*1.2,'ok')
    i.plot(mxvals[:-1],[h*1.2]*len(mxvals[:-1]),'o',color='gray',alpha=.3)
```


# Bonus Tutorial: Extending the Wilson-Cowan Model
**Week 2, Day 4: Dynamic Networks**

**By Neuromatch Academy**

__Content creators:__ Qinglong Gu, Songtin Li, Arvind Kumar, John Murray, Julijana Gjorgjieva

__Content reviewers:__ Maryam Vaziri-Pashkam, Ella Batty, Lorenzo Fontolan, Richard Gao, Spiros Chavlis, Michael Waskom

__Content editors:__

__Production editors:__ Siddharth Suresh

**Our 2021 Sponsors, including Presenting Sponsor Facebook Reality Labs**

---
# Tutorial Objectives
In the previous tutorial, you became familiar the **Wilson-Cowan** rate model. Here we will dive into some deeper analyses of this model.

Bonus steps:

- Find and plot the **fixed points** of the Wilson-Cowan model.
- Investigate the stability of the Wilson-Cowan model by linearizing its dynamics and examining the **Jacobian matrix**.
- Learn how the Wilson-Cowan model can reach an oscillatory state.

Applications of Wilson-Cowan model:
- Visualize the behavior of an Inhibition-stabilized network.
- Simulate working memory using the Wilson-Cowan model.

\\
Reference paper:

_[Wilson H and Cowan J (1972) Excitatory and inhibitory interactions in localized populations of model neurons. Biophysical Journal 12](https://doi.org/10.1016/S0006-3495(72)86068-5)_

---
# Setup

```python
# Imports

import matplotlib.pyplot as plt
import numpy as np
import scipy.optimize as opt       # root-finding algorithm
```

```python
# @title Figure Settings
import ipywidgets as widgets       # interactive display
%config InlineBackend.figure_format = 'retina'
plt.style.use("https://raw.githubusercontent.com/NeuromatchAcademy/course-content/master/nma.mplstyle")
```

```python
# @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)
```

```python
# @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:

- Parameter dictionary: `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 step
  - After `pars = default_pars()`, use `par['New_para'] = value` to add a new parameter with its value
  - Pass to functions that accept individual parameters with `func(**pars)`
- F-I curve: `F(x, a, theta)`
- Derivative of the F-I curve: `dF(x, a, theta)`
- Inverse of F-I curve: `F_inv`
- Nullcline calculations: `get_E_nullcline`, `get_I_nullcline`
- Derivatives of E/I variables: `EIderivs`
- Simulate the Wilson-Cowan model: `simulate_wc`

---
# Section 1: Fixed points, stability analysis, and limit cycles in the Wilson-Cowan model

*Correction to video: this is now the first part of the second bonus tutorial, not the last part of the second tutorial*

```python
# @title Video 1: Fixed points and their stability
from ipywidgets import widgets

out2 = widgets.Output()
with out2:
  from IPython.display import IFrame
  class BiliVideo(IFrame):
      def __init__(self, id, page=1, width=400, height=300, **kwargs):
          self.id=id
          src = 'https://player.bilibili.com/player.html?bvid={0}&page={1}'.format(id, page)
          super(BiliVideo, self).__init__(src, width, height, **kwargs)

  video = BiliVideo(id="BV1Pf4y1d7dx", width=854, height=480, fs=1)
  print('Video available at https://www.bilibili.com/video/{0}'.format(video.id))
  display(video)

out1 = widgets.Output()
with out1:
  from IPython.display import YouTubeVideo
  video = YouTubeVideo(id="jIx26iQ69ps", width=854, height=480, fs=1, rel=0)
  print('Video available at https://youtube.com/watch?v=' + video.id)
  display(video)

out = widgets.Tab([out1, out2])
out.set_title(0, 'Youtube')
out.set_title(1, 'Bilibili')

display(out)
```

    Tab(children=(Output(), Output()), _titles={'0': 'Youtube', '1': 'Bilibili'})

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:

\begin{align}
\tau_E \frac{dr_E}{dt} &= -r_E + F_E(w_{EE}r_E -w_{EI}r_I + I^{\text{ext}}_E;a_E,\theta_E)\\
\tau_I \frac{dr_I}{dt} &= -r_I + F_I(w_{IE}r_E -w_{II}r_I + I^{\text{ext}}_I;a_I,\theta_I)    \qquad (1)
\end{align}

$r_E(t)$ represents the average activation (or firing rate) of the excitatory population at time $t$, and $r_I(t)$ the activation (or firing rate) of the inhibitory population. The parameters $\tau_E$ and $\tau_I$ control the timescales of the dynamics of each population. Connection strengths are given by: $w_{EE}$ (E $\rightarrow$ E), $w_{EI}$ (I $\rightarrow$ E), $w_{IE}$ (E $\rightarrow$ I), and $w_{II}$ (I $\rightarrow$ I). The terms $w_{EI}$ and $w_{IE}$ represent connections from inhibitory to excitatory population and vice versa, respectively. The transfer functions (or F-I curves) $F_E(x;a_E,\theta_E)$ and $F_I(x;a_I,\theta_I)$ can be different for the excitatory and the inhibitory populations.

## Section 1.1: Fixed Points of the E/I system

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.

```python
# @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)
```

```python
# @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

### Coding Exercise 1.1: Find the fixed points of the Wilson-Cowan model

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 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.

```python
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, .1, .1)
if check_fp(pars, x_fp_1):
  plot_fp(x_fp_1)

# Find the second fixed point
x_fp_2 = my_fp(pars, .3, .2)
if check_fp(pars, x_fp_2):
  plot_fp(x_fp_2)

# Find the third fixed point
x_fp_3 = my_fp(pars, .8, .6)
if check_fp(pars, x_fp_3):
  plot_fp(x_fp_3)
```

```python
# to_remove solution
pars = default_pars()

with plt.xkcd():
  my_plot_nullcline(pars)

  # Find the first fixed point
  x_fp_1 = my_fp(pars, 0.1, 0.1)
  if check_fp(pars, x_fp_1):
    plot_fp(x_fp_1)

  # Find the second fixed point
  x_fp_2 = my_fp(pars, 0.3, 0.3)
  if check_fp(pars, x_fp_2):
    plot_fp(x_fp_2)

  # Find the third fixed point
  x_fp_3 = my_fp(pars, 0.8, 0.6)
  if check_fp(pars, x_fp_3):
    plot_fp(x_fp_3)
```

## Section 1.2: Stability of a fixed point and eigenvalues of the Jacobian Matrix

First, let's first rewrite the system $1$ as:

\begin{align}
&\frac{dr_E}{dt} = G_E(r_E,r_I)\\[0.5mm]
&\frac{dr_I}{dt} = G_I(r_E,r_I)
\end{align}
where

\begin{align}
&G_E(r_E,r_I) = \frac{1}{\tau_E} [-r_E + F_E(w_{EE}r_E -w_{EI}r_I + I^{\text{ext}}_E;a,\theta)]\\[1mm]
&G_I(r_E,r_I) = \frac{1}{\tau_I} [-r_I + F_I(w_{IE}r_E -w_{II}r_I + I^{\text{ext}}_I;a,\theta)]
\end{align}

By definition, $\displaystyle\frac{dr_E}{dt}=0$ and $\displaystyle\frac{dr_I}{dt}=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

1.   The trajectory will be attracted back to the fixed point
2.   The trajectory will diverge from the fixed point.

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:

 \begin{equation}
   J=
  \left[ {\begin{array}{cc}
   \displaystyle{\frac{\partial}{\partial r_E}}G_E(r_E^*, r_I^*) & \displaystyle{\frac{\partial}{\partial r_I}}G_E(r_E^*, r_I^*)\\[1mm]
   \displaystyle\frac{\partial}{\partial r_E} G_I(r_E^*, r_I^*) & \displaystyle\frac{\partial}{\partial r_I}G_I(r_E^*, r_I^*) \\
  \end{array} } \right] \quad (7)
\end{equation}

\\

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:

\\

\begin{align}
&\frac{\partial}{\partial r_E} G_E(r_E^*, r_I^*) = \frac{1}{\tau_E} [-1 + w_{EE} F_E'(w_{EE}r_E^* -w_{EI}r_I^* +  I^{\text{ext}}_E;\alpha_E, \theta_E)] \\[1mm]
&\frac{\partial}{\partial r_I} G_E(r_E^*, r_I^*)= \frac{1}{\tau_E} [-w_{EI} F_E'(w_{EE}r_E^* -w_{EI}r_I^* +  I^{\text{ext}}_E;\alpha_E, \theta_E)]
\end{align}

\\

The same applies to the inhibitory population.

### Coding Exercise 1.2: Compute the Jacobian Matrix for the Wilson-Cowan model

Here, you can use `dF(x,a,theta)` defined in the `Helper functions` to calculate the derivative of the F-I curve.

```python
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] = ( 1 / tau_E ) * ( -1 + wEE*dF(wEE*fp[0] - wEI*fp[1] + I_ext_E, a_E, theta_E) )
  J[0, 1] = ( 1 / tau_E ) * ( -wEI*dF(wEE*fp[0] - wEI*fp[1] + I_ext_E, a_E, theta_E) )
  J[1, 0] = ( 1 / tau_I ) * ( wIE*dF(wIE*fp[0] - wII*fp[1] + I_ext_I, a_I, theta_I) )
  J[1, 1] = ( 1 / tau_I ) * ( -1 - wII*dF(wIE*fp[0] - wII*fp[1] + I_ext_I, a_I, theta_I) )

  # 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')
```

    [-0.62338386+0.13110957j -0.62338386-0.13110957j] Stable point
    [ 1.05720798 -0.87266898] Unstable point
    [-0.95956219 -1.42197413] Stable point

```python
# to_remove solution
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))

  # Compute the four elements of the Jacobian matrix
  J[0, 0] = (-1 + wEE * dF(wEE * rE - wEI * rI + I_ext_E,
                           a_E, theta_E)) / tau_E

  J[0, 1] = (-wEI * dF(wEE * rE - wEI * rI + I_ext_E,
                       a_E, theta_E)) / tau_E

  J[1, 0] = (wIE * dF(wIE * rE - wII * rI + I_ext_I,
                      a_I, theta_I)) / tau_I

  J[1, 1] = (-1 - wII * dF(wIE * rE - wII * rI + I_ext_I,
                           a_I, theta_I)) / tau_I

  # 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')
```

    [-0.62338386+0.13110957j -0.62338386-0.13110957j] Stable point
    [ 1.05720798 -0.87266898] Unstable point
    [-0.95956219 -1.42197413] Stable point

As is evident, the stable fixed points correspond to the negative eigenvalues, while unstable point corresponds 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 $w_{EE}$ on the nullclines and the eigenvalues of the dynamical system.

\* _Critical change is referred to as **pitchfork bifurcation**_.

## Section 1.3: Effect of `wEE` on the nullclines and the eigenvalues

### Interactive Demo 1.3: Nullclines position in the phase plane changes with parameter values

How do the nullclines move for different values of the parameter $w_{EE}$? What does this mean for fixed points and system activity?

```python
# @title

# @markdown Make sure you execute this cell to enable the widget!

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()

_ = widgets.interact(plot_nullcline_diffwEE, wEE=(6., 10., .01))
```

    interactive(children=(FloatSlider(value=8.0, description='wEE', max=10.0, min=6.0, step=0.01), Output()), _dom…

```python
# to_remove explanation

"""

- For low values of wEE there is only one fixed point and it is stable so initial
conditions do not matter and the system always converge to the only fixed point

- For high values of wEE we have three fixed points of which two are stable and
one is unstable (or saddle). Now it matters where the initial conditions are. If
the initial conditions are in the attractor region of the high activity fixed
point then the system will converge to that (the bottom example).
""";
```

We can also investigate the effect of different $w_{EI}$, $w_{IE}$, $w_{II}$, $\tau_{E}$, $\tau_{I}$, and $I_{E}^{\text{ext}}$ on the stability of fixed points. In addition, we can also consider the perturbation of the parameters of the gain curve $F(\cdot)$.

## Section 1.4: Limit cycle - Oscillations

For some values of interaction terms ($w_{EE}, w_{IE}, w_{EI}, w_{II}$), 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, $w_{EE}=6.4$, $w_{EI}=4.8$, $w_{IE}=6.$, $w_{II}=1.2$, and $I_{E}^{\text{ext}}=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.

```python
# @title

# @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.

```python
# @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()
```

### Interactive Demo 1.4: Limit cycle and oscillations.

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 $\tau_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**.

\\
Please execute the code below to check this out.

```python
# @title

# @markdown Make sure you execute this cell to enable the widget!

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()

_ = widgets.interact(time_constant_effect, tau_i=(0.2, 3, .1))
```

    interactive(children=(FloatSlider(value=0.5, description='tau_i', max=3.0, min=0.2), Output()), _dom_classes=(…

Both $\tau_E$ and $\tau_I$ feature in the Jacobian of the two population network (eq 7). So here is seems that the by increasing $\tau_I$ the eigenvalues corresponding to the stable fixed point are becoming complex.

Intuitively, when $\tau_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 ...

---
# Section 2: Inhibition-stabilized network (ISN)

## Section 2.1: Inhibition-stabilized network

As described above, one can obtain the linear approximation around the fixed point as

 \begin{equation}
   \frac{d}{dr}  \vec{R}=
  \left[ {\begin{array}{cc}
   \displaystyle{\frac{\partial G_E}{\partial r_E}} & \displaystyle{\frac{\partial G_E}{\partial r_I}}\\[1mm]
   \displaystyle\frac{\partial G_I}{\partial r_E} & \displaystyle\frac{\partial G_I}{\partial r_I} \\
  \end{array} } \right] \vec{R},
\end{equation}

\\

where $\vec{R} = [r_E, r_I]^{\rm T}$ is the vector of the E/I activity.

Let's direct our attention to the excitatory subpopulation which follows:

\\

\begin{equation}
\frac{dr_E}{dt} = \frac{\partial G_E}{\partial r_E}\cdot r_E + \frac{\partial G_E}{\partial r_I} \cdot r_I
\end{equation}

\\

Recall that, around fixed point $(r_E^*, r_I^*)$:

\\

\begin{align}
&\frac{\partial}{\partial r_E}G_E(r_E^*, r_I^*) = \frac{1}{\tau_E} [-1 + w_{EE} F'_{E}(w_{EE}r_E^* -w_{EI}r_I^* + I^{\text{ext}}_E; \alpha_E, \theta_E)] \qquad (8)\\[1mm]
&\frac{\partial}{\partial r_I}G_E(r_E^*, r_I^*) = \frac{1}{\tau_E} [-w_{EI} F'_{E}(w_{EE}r_E^* -w_{EI}r_I^* + I^{\text{ext}}_E; \alpha_E, \theta_E)] \qquad (9)\\[1mm]
&\frac{\partial}{\partial r_E}G_I(r_E^*, r_I^*) = \frac{1}{\tau_I} [w_{IE} F'_{I}(w_{IE}r_E^* -w_{II}r_I^* + I^{\text{ext}}_I; \alpha_I, \theta_I)] \qquad (10)\\[1mm]
&\frac{\partial}{\partial r_I}G_I(r_E^*, r_I^*) = \frac{1}{\tau_I} [-1-w_{II} F'_{I}(w_{IE}r_E^* -w_{II}r_I^* + I^{\text{ext}}_I; \alpha_I, \theta_I)] \qquad (11)
\end{align} \\

From Equation. (8), it is clear that $\displaystyle{\frac{\partial G_E}{\partial r_I}}$ is negative since the $\displaystyle{\frac{dF}{dx}}$ 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, $\displaystyle{\frac{\partial G_E}{\partial r_E}}$ has negative terms related to the "leak" effect, and positive term related to the recurrent excitation. Therefore, it leads to two different regimes:

- $\displaystyle{\frac{\partial}{\partial r_E}G_E(r_E^*, r_I^*)}<0$, **noninhibition-stabilized
network (non-ISN) regime**

- $\displaystyle{\frac{\partial}{\partial r_E}G_E(r_E^*, r_I^*)}>0$, **inhibition-stabilized
network (ISN) regime**

### Coding Exercise 2.1: Compute $\displaystyle{\frac{\partial G_E}{\partial r_E}}$
Implemet the function to calculate the $\displaystyle{\frac{\partial G_E}{\partial r_E}}$ for the default parameters, and the parameters of the limit cycle case.

```python
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 excercise: compute the dG/dE, Eq. (13)")
  ##########################################################################
  # Calculate the J[0,0]
  dGdrE = ( 1 / tau_E ) * ( -1 + wEE*dF(wEE*rE - wEI*rI + I_ext_E, a_E, theta_E) )

  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}')
```

    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

```python
# to_remove solution
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

  # Calculate the J[0,0]
  dGdrE = (-1 + wEE * dF(wEE * rE - wEI * rI + I_ext_E, a_E, theta_E)) / tau_E

  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}')
```

    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

**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
```

## Section 2.2: Nullcline analysis of the ISN

Recall that the E nullcline follows

\\

\begin{align}
r_E = F_E(w_{EE}r_E -w_{EI}r_I + I^{\text{ext}}_E;a_E,\theta_E).
\end{align}

\\

That is, the firing rate $r_E$ can be a function of $r_I$. Let's take the derivative of $r_E$ over $r_I$, and obtain

\\

\begin{align}
&\frac{dr_E}{dr_I} = F_E' \cdot (w_{EE}\frac{dr_E}{dr_I} -w_{EI}) \iff \\
&(1-F_E'w_{EE})\frac{dr_E}{dr_I} = -F_E' w_{EI} \iff \\
&\frac{dr_E}{dr_I} = \frac{F_E' w_{EI}}{F_E'w_{EE}-1}.
\end{align}

\\

That is, in the phase plane `rI-rE`-plane, we can obtain the slope along the E nullcline as

\\

$$\frac{dr_I}{dr_E} = \frac{F_E'w_{EE}-1}{F_E' w_{EI}} \qquad (12)$$

Similarly, we can obtain the slope along the I nullcline as

\\

$$\frac{dr_I}{dr_E} = \frac{F_I'w_{IE}}{F_I' w_{II}+1} \qquad (13)$$

\\

Then, we can find that $\Big{(} \displaystyle{\frac{dr_I}{dr_E}} \Big{)}_{\rm I-nullcline} >0$ in Equation (13).

\\

However, in Equation (12), the sign of $\Big{(} \displaystyle{\frac{dr_I}{dr_E}} \Big{)}_{\rm E-nullcline}$ depends on the sign of $(F_E'w_{EE}-1)$. Note that, $(F_E'w_{EE}-1)$ is the same as what we show above (Equation (8)). Therefore, we can have the following results:

- $\Big{(} \displaystyle{\frac{dr_I}{dr_E}} \Big{)}_{\rm E-nullcline}<0$, **noninhibition-stabilized
network (non-ISN) regime**

- $\Big{(} \displaystyle{\frac{dr_I}{dr_E}} \Big{)}_{\rm E-nullcline}>0$, **inhibition-stabilized
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., $\text{det}(J)>0$.

From the Jacobian matrix definition and from Equations (8-11), we can obtain:

$   J=
  \left[ {\begin{array}{cc}
   \displaystyle{\frac{1}{\tau_E}(w_{EE}F_E'-1)} & \displaystyle{-\frac{1}{\tau_E}w_{EI}F_E'}\\[1mm]
   \displaystyle {\frac{1}{\tau_I}w_{IE}F_I'}& \displaystyle {\frac{1}{\tau_I}(-w_{II}F_I'-1)} \\
  \end{array} } \right] $

\\

Note that, if we let

\\

$   T=
  \left[ {\begin{array}{cc}
   \displaystyle{\tau_E} & \displaystyle{0}\\[1mm]
   \displaystyle 0& \displaystyle \tau_I \\
  \end{array} } \right] $,
$   F=
  \left[ {\begin{array}{cc}
   \displaystyle{F_E'} & \displaystyle{0}\\[1mm]
   \displaystyle 0& \displaystyle F_I' \\
  \end{array} } \right] $, and
  $   W=
  \left[ {\begin{array}{cc}
   \displaystyle{w_{EE}} & \displaystyle{-w_{EI}}\\[1mm]
   \displaystyle w_{IE}& \displaystyle -w_{II} \\
  \end{array} } \right] $

\\

then, using matrix notation, $J=T^{-1}(F W - I)$ where $I$ is the identity matrix, i.e., $I = \begin{bmatrix}
1 & 0 \\
0 & 1
\end{bmatrix}.$

\\

Therefore, $\det{(J)}=\det{(T^{-1}(F W - I))}=(\det{(T^{-1})})(\det{(F W - 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{(F W - I)}$, and so

$$\det{(FW - I)} = (F_E' w_{EI})(F_I'w_{IE}) - (F_I' w_{II} + 1)(F_E'w_{EE} - 1) > 0.$$

\\

Then, combining this with Equations (12) and (13), we can obtain
$$\frac{\Big{(} \displaystyle{\frac{dr_I}{dr_E}} \Big{)}_{\rm I-nullcline}}{\Big{(} \displaystyle{\frac{dr_I}{dr_E}} \Big{)}_{\rm 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 $\delta I^{\rm ext}_I$ 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,

\\

\begin{equation}
r_I = F_I(w_{IE}r_E-w_{II}r_I + I^{\text{ext}}_I ; \alpha_I, \theta_I)
\end{equation}

\\

remains true if we take $I^{\text{ext}}_I \rightarrow I^{\text{ext}}_I +\delta I^{\rm ext}_I$ and $r_E\rightarrow r_E'=r_E-\frac{\delta I^{\rm ext}_I}{w_{IE}}$ to obtain

\\

\begin{equation}
r_I = F_I(w_{IE}r_E'-w_{II}r_I + I^{\text{ext}}_I +\delta I^{\rm ext}_I; \alpha_I, \theta_I)
\end{equation}

\\

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**, $r_I$ will increase first but then decay to the new fixed point in which both $r_I$ and $r_E$ are decreased compared to the original fixed point. However, by adding $\delta I^{\rm ext}_I$ into a **non-ISN**, $r_I$ will increase while $r_E$ will decrease.

### Interactive Demo 2.2: Nullclines of Example **ISN** and **non-ISN**

In this interactive widget, we inject excitatory ($I^{\text{ext}}_I>0$) or inhibitory ($I^{\text{ext}}_I<0$) drive into the inhibitory population when the system is at its equilibrium (with parameters $w_{EE}=6.4$, $w_{EI}=4.8$, $w_{IE}=6.$, $w_{II}=1.2$, $I_{E}^{\text{ext}}=0.8$, $\tau_I = 0.8$, and $I^{\text{ext}}_I=0$). How does the firing rate of the $I$ population changes with excitatory vs inhibitory drive into the inhibitory population?

```python
# @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

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()

_ = widgets.interact(ISN_I_perturb, dI=(-0.2, 0.21, .05))
```

    interactive(children=(FloatSlider(value=0.1, description='dI', max=0.21, min=-0.2, step=0.05), Output()), _dom…

```python
# to_remove explanation

"""
Discussion:

Here we observe a paradoxical effect; if we inject excitatory current to the I
population, the r_I goes down, whereas when we inject inhibitory current, the r_I
increases. Recall that we inject a constant excitatory current to the E population,
which also drives, indirectly, the I population. When Iext>0, the r_I increases
but this drives E to a low state, which in turn leads to rI decrease. Whereas,
when Iext<0, the effect is negative on I population for a short amount of time,
which is sufficient to drive the E population to a high steady state, and then due
to E to I connections, the I population activity is increased.
""";
```

---
# Section 3: Fixed point and working memory

The input into the neurons measured in the experiment is often very noisy ([links](http://www.scholarpedia.org/article/Stochastic_dynamical_systems)). Here, the noisy synaptic input current is modeled as an Ornstein-Uhlenbeck (OU)process, which has been discussed several times in the previous tutorials.

```python
# @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.

```python
# @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()
```

## Interactive Demo 3: Short pulse induced persistent activity
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.

```python
# @title

# @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.)

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()

_ = widgets.interact(WC_with_pulse, SE=(0.0, 1.0, .05))
```

    interactive(children=(FloatSlider(value=0.0, description='SE', max=1.0, step=0.05), Output()), _dom_classes=('…

```python
# to_remove explanation

"""
Discussion:

When a system has more than one fixed points, depending on the input strength,
the network will settle in one of the fixed points. In this case, we have two
fixed points, one of the fixed points corresponds to high activity. So when input
drives the network to the high activity fixed points, the network activity will
remain there -- it is a stable fixed point. Because the network retains its
activity (persistent activity) even after the input has been removed, we can
take the persistent activity as working memory.
""";
```

Explore what happened when a second, brief current is applied to the inhibitory population.


# WBAI Aphasia (Pure Alexia) Handson 01

    17/Sep/2018

    浅川 伸一 <asakawa@ieee.org>

# SRN

SRN の基本は以下の式である。
\begin{equation}
\mathbf{y} = \sigma\left(\mathbf{Xw}_{in}+\mathbf{Hw}_{rec}+\mathbf{b}\right)
\end{equation}

ここで $\mathbf{X}$ はデータ行列で $n$ 行，$d$ 列 のデータ行列である。一時刻あたり一行のデータである。
時間発展を考えると $\mathbf{X}$ は逐次実行してまず $\mathbf{H}$ を作らなければならない。

---
- リカレントニューラルネットワークの本質として，一時刻前の情報を保持しておく機構がネットワーク内に存在していることが挙げられる。
- 時間展開した図

```python
# -*- coding: utf-8 -*-
#from __future__ import print_function
#from six.moves import range

import os
import sys
import time
import numpy as np
import codecs
import argparse
import matplotlib.pyplot as plt
%matplotlib inline

import wbai_aphasia as handson
from wbai_aphasia import xavier_initializer
from wbai_aphasia import sigmoid, tanh, relu, elu, identity, softsign, softplus, sat, clu
from wbai_aphasia import fc_layer, softmax_layer, srn_layer
from wbai_aphasia import fetch_mnist, view_mnist
from wbai_aphasia import one_hot_vector, txt2data
#from wbai_aphasia import cost
```

```python
!date; printf '\n'
!uname -a; printf '\n'
!type python; printf '\n'
!python --version; printf '\n'
!gcc --version; printf '\n'
!conda --version; printf '\n'
```

    Mon Sep 17 03:42:54 JST 2018

    Darwin Himalia.local 17.7.0 Darwin Kernel Version 17.7.0: Thu Jun 21 22:53:14 PDT 2018; root:xnu-4570.71.2~1/RELEASE_X86_64 x86_64

    python is /Users/asakawa/anaconda3/bin/python

    Python 3.6.6

    Configured with: --prefix=/Library/Developer/CommandLineTools/usr --with-gxx-include-dir=/usr/include/c++/4.2.1
    Apple LLVM version 9.1.0 (clang-902.0.39.2)
    Target: x86_64-apple-darwin17.7.0
    Thread model: posix
    InstalledDir: /Library/Developer/CommandLineTools/usr/bin

    conda 4.5.11

```python
### preparation to draw graphs
plt.rcParams['figure.figsize'] = (12, 8) # set default size of plots
plt.rcParams['image.interpolation'] = 'nearest'
plt.rcParams['image.cmap'] = 'gray'

np.set_printoptions(precision=3, suppress=True)
np.random.seed(seed=None)
```

```python
np.set_printoptions(formatter={'float': '{: 0.3f}'.format})
```

```python
class elman(object):
    """A simple elman network model demo.

    Parameters:
        hidden_layer_sizes : tuple, length = n_layers - 2, default (100,)
            The ith element represents the number of neurons in the ith
            hidden layer.

        activation : {identity, sigmoid, tanh, relu, elu, clu}, default tanh
            Activation function for the hidden layer.

        solver : {'sgd', 'adagrad', 'adadelta, 'rmsprop, 'adam', 'nadam'},
            default 'adam'.  The solver for weight optimization.
            **NOT IMPLEMENTED**

        lambda_ : float, optional, default 1e-4
            L2 penalty (regularization term) parameter.

        lr : the learning ratio: float
        lr_init: float, default 1e-2

        random_state : int, RandomState instance or None, optional, default None
            If int, random_state is the seed used by the random number generator;
            If RandomState instance, random_state is the random number generator;
            If None, the random number generator is the RandomState instance used
            by `np.random`.

        tol : float, optional, default 1e-4
            Tolerance for the optimization. When the loss or score is not improving
            by at least tol for two consecutive iterations, unless `learning_rate`
            is set to 'adaptive', convergence is considered to be reached and
            training stops.

        verbose : bool, optional, default False
            Whether to print progress messages to stdout.

    """

    def __init__(self, X, y, \
                 n_hid=128, \
                 max_iter=1e+3, \
                 lr=1e-2, \
                 activation=tanh, \
                 verbose=False):
        self.max_iter = max_iter if max_iter is not None else 200
        self.n_hid = n_hid if n_hid is not None else 128
        self.X, self.y = np.copy(X), np.copy(y)
        self.activation = activation if activation is not None else tanh
        self.verbose = verbose if verbose is not None else False

        self.n_inp = self.X.shape[1]
        self.n_out = self.n_inp
        self.H = srn_layer(self.n_inp, self.n_hid, activation=activation)
        self.O = softmax_layer(self.n_hid, self.n_out)

    def fit(self, X, y, lr=1e-2, max_iter=1000, verbose=False):
        """Fit the model to data matrix X and target y.

        Parameters:
            X : array-like or sparse matrix, shape (n_samples, n_features)
                The input data.

            y : array-like, shape (n_samples,) or (n_samples, n_outputs)
                The target values (class labels in classification).

        Returns:
            self : returns a trained MLP model.
        """
        return self.train(X, y, max_iter=max_iter, verbose=verbose)

    def train(self, X, y, lr=1e-2, max_iter=1000, verbose=False):
        """Train the model to data matrix X and target y.

        Parameters:
            X : array-like or sparse matrix, shape (n_samples, n_features)
                The input data.

            y : array-like, shape (n_samples,) or (n_samples, n_outputs)
                The target values (class labels in classification).

        Returns:
            self : returns a trained the Elman network model.
        """
        X = self.X if X is None else X
        y = self.y if y is None else y
        verbose = verbose if verbose is not None else self.verbose
        max_iter = max_iter if max_iter is not None else self.max_iter
        hInit = np.zeros((1, self.n_hid),dtype=np.float32)
        state = np.zeros((X.shape[0],self.n_hid),dtype=np.float32)
        lossList = list()
        for t in range(max_iter):
            inp = X
            Hout, state = self.H.forward(X, state, hInit)
            y_ = self.O.forward(Hout)

            Err = y_ - y
            #loss = np.mean(Err**2)
            loss = handson.binary_log_loss(y,y_)
            lossList.append(loss)
            if (t % (max_iter>>3) == 0) and verbose:
                #print('Iter={0:05d} loss={1:0.3f}'.format(iter, loss))
                p = np.argmax(y,axis=0)
                p_ = np.argmax(y_,axis=0)
                accuracy = np.average(p == p_)
                print('Iter={0:05d} loss={1:0.4f}, cost={2:0.4f}, accuracy={3:0.4f}'.format(
                    t,
                    loss,
                    handson.cost(y_),
                    accuracy))
            delta = Err
            gradW2 = self.O.backward(delta, Hout)
            delta = np.dot(delta, self.O.W.T)
            gradW1, gradWr, _ = self.H.backward(X, state, delta)

            self.O.W -= lr * gradW2
            self.O.bias -= lr * np.mean(gradW2)
            self.H.W -= lr * gradW1
            self.H.bias -= lr * np.mean(gradW1)
            self.H.Wr -= lr * gradWr

        if verbose:
            plt.plot(lossList)
            #plt.show()
            print('#Target: ', end='')
            for x in y:
                print(idx2wrd[np.argmax(x)],end='')
            print('\n#Output: ', end='')
            for x in y_:
                print(idx2wrd[np.argmax(x)],end='')
            print()

        return y_, lossList

    def predict(self, X):
        """Predict using this elman neural network.

        Parameters:
            X : {array-like, matrix}, shape (n_samples, n_features)
            The input data.

        Returns:
            y : array-like, shape (n_samples,) or (n_samples, n_classes)
            The predicted classes.
        """

        hInit = np.zeros((1, self.n_hid),dtype=np.float32)
        state = np.zeros((X.shape[0],self.n_hid),dtype=np.float32)
        Hout, state = self.H.forward(X, state, hInit)
        y_ = self.O.forward(Hout)
        return y_

    def predict_log_prob(self, X):
        """Return the log of probability estimates.

        Parameters:
            X : array-like, shape (n_samples, n_features)
                The input data.

        Returns:
            log_y_prob : array-like, shape (n_samples, n_classes)
                The predicted log-probability of the sample for each class
                in the model, where classes are ordered as they are in
                `self.classes_`. Equivalent to log(predict_proba(X))
        """
        yprob_ = self.predict_prob(X)
        return np.log(yprob_, out=yprob_)

    def predict_prob(self, X):
        """Probability estimates.

        Parameters:
            X : {array-like, sparse matrix}, shape (n_samples, n_features)
                The input data.

        Returns:
            y_prob : array-like, shape (n_samples, n_classes)
                The predicted probability of the sample for each class in the
                model, where classes are ordered as they are in `self.classes_`.
        """
        y_ = self._predict(X)

        if self.n_outputs_ == 1:
            y_ = y_.ravel()

        if y_.ndim == 1:
            return np.vstack([1 - y_, y_]).T
        else:
            return y_

```

```python
filename = 'j_constitution9.txt'
filename = 'basyo.txt'
nHid = 128
activation = tanh
max_iter = 1000
lr = 1e-2
X, y, idx2wrd, wrd2idx = txt2data(filename)
model = elman(X, y, n_hid=nHid, lr=lr, max_iter=max_iter, activation=activation)
start_time = time.time()
y_, lossList = model.train(X, y, max_iter=max_iter,verbose=True)
#y_, _ = model.fit(X, y, max_iter=max_iter, verbose=True)
end_time = time.time()
print('time elasped {}'.format(end_time - start_time))
```

```python
def main(filename=None, max_iter=1000, nHid=16, activation=tanh, lr=1e-2, verbose=False):
    X, y, idx2wrd, wrd2idx = txt2data(filename)
    model = elman(X, y, n_hid=nHid, lr=lr, max_iter=max_iter, activation=tanh, verbose=verbose)
    y_, lossList = model.train(X, y, max_iter=max_iter, verbose=verbose)

    return model, X, y_, lossList, idx2wrd, wrd2idx
```

```python
if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Simple demo for both MLP and SRN')
    parser.add_argument('--seed', type=int, default=1, help='# Seed number to gerenrate random number sequence.')
    parser.add_argument('--max_iter', type=int, default=1000, help='# maximum number of iteration.')
    parser.add_argument('--nHid', type=int, default=32, help='# The number of the hidden unints.')
    parser.add_argument('--lr', type=float, default=1e-2, help='# Learning ratio.')
    parser.add_argument('--activation', default='tanh', help='# activation function')
    #parser.add_argument('--filename', default=__file__, help='# The input text file name')
    parser.add_argument('--filename', default='j_constitution9.txt', help='# The input text file name')
    parser.add_argument('--verbose', action='store_false', help='# Verbose flag for debug')

    args = parser.parse_args()
    if args.seed:
        seed = args.seed
    if args.max_iter:
        max_iter = args.max_iter
    if args.nHid:
        nHid = args.nHid
    if args.lr:
        lr = args.lr
    if args.activation:
        activation=args.activation
    if args.filename:
        filename=args.filename
    if args.verbose:
        verbose=True

    main(filename=filename, max_iter=max_iter, nHid=nHid, activation=activation, lr=lr, verbose=verbose)
```

```python

```


<link rel="stylesheet" href="../../styles/theme_style.css">

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">

        ECG Sensor - Unit Conversion

        Difficulty Level:

                    Tags
                    pre-process☁ecg☁conversion

The OpenSignals outputted file formats contain raw data, so each sample has a digital unit.

In scientific terms it is recommended the use of specific units, like electric tension (V) or electric current (A).
Each sensor that *Plux* commercialise has a datasheet where a transfer function is mentioned for unit conversion be done.

The next lines are intended to explain how this conversion can be programmatically done.

In spite of the unit conversion procedure has some common steps applicable to all sensors, the current Jupyter Notebook is dedicated to the unit conversion procedure of signals acquired with ECG sensor.

1 - Importation of the needed packages

```python
# biosignalsnotebooks Python package with useful functions that support and complement the available Notebooks
import biosignalsnotebooks as bsnb

# Function used for creating a numpy array, where a mathematical operation can be applied to each entry in an easy and automatic way. On the other side, linspace, here will be used for generation of a time-axis.
from numpy import array, linspace
```

2 - Download of the sensor datasheet (from http://www.biosignalsplux.com/en/learn/documentation).

In this case we are working with ECG, being our file located at http://www.biosignalsplux.com/datasheets/ECG_Sensor_Datasheet.pdf

```python
# Embedding of .pdf file
from IPython.display import IFrame
IFrame(src="https://www.biosignalsplux.com/datasheets/ECG_Sensor_Datasheet.pdf#page=2", width="100%", height="350")
```

3 - Extraction of the transfer function from the beginning of the second page

\begin{equation}
ECG_{mV} = \frac{(\frac{ADC}{2^n} - \frac{1}{2}).VCC}{G_{ECG}}
\end{equation}

        $ECG_{mV}$ - ECG value in mV
        $ADC$ - Digital value sampled from the channel (initialism of "Analog to Digital Converter")
        $n$ - Number of bits of the channel (dependent on the chosen resolution specified on OpenSignals previously to the acquisition stage [8, 12 or 16 bits])

        $VCC$ - 3000 mV
        $G_{ECG}$ - 1000

4 - Loading of data stored in biosignalsnotebooks own signal library

```python
# Data loading
data, header = bsnb.load_signal("ecg_sample", get_header=True)
```

In the following cell, some relevant information is stored inside variables. This relevant information includes the mac-address of the device, channel number and signal acquisition parameters such as resolution and sampling rate.

For a detailed explanation of how to access this info, the "Signal Loading - Working with File Header" Notebook should be consulted.

```python
ch = "CH1" # Channel
sr = 200 # Sampling rate
resolution = 16 # Resolution (number of available bits)
```

Access to acquired signal samples and storage inside a new variable.

```python
signal = data[ch]
```

5 - Final unit conversion (to mV) by applying the transfer function sample by sample

Definition of $VCC$ and $G_{ECG}$ constants

```python
vcc = 3000 # mV
gain = 1000
```

```python
signal_mv = (((array(signal) / 2**resolution) - 0.5) * vcc) / gain
```

6 - Time axis generation

```python
time = bsnb.generate_time(signal_mv, sr)
```

Comparison between RAW and mV signal.

```python
bsnb.plot([time, time], [signal, signal_mv], y_axis_label=["Raw Data", "Electric Voltage (mV)"], grid_lines=1, grid_columns=2, grid_plot=True)
```

Similar Notebooks, dedicated to the unit conversion of other sensors, are available in the following "conversion" section of Notebooks Grouped by Tag page

A list of Plux's available sensors (and the respective datasheets) can be accessed at Sensors page .

We hope that you have enjoyed this guide. biosignalsnotebooks is an environment in continuous expansion, so don't stop your journey and learn more with the remaining Notebooks  !

            ☌ Project Presentation

            ☌ GitHub Repository

            ☌ How to install biosignalsnotebooks Python package ?

            ☌ Signal Library

            ☌ Notebook Categories

            ☌ Notebooks by Difficulty

            ☌ Notebooks by Signal Type

            ☌ Notebooks by Tag

```python
from biosignalsnotebooks.__notebook_support__ import css_style_apply
css_style_apply()
```

    .................... CSS Style Applied to Jupyter Notebook .........................