# Tutorial 2: 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, Siddharth Suresh

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

---
# Tutorial Objectives

*Estimated timing of tutorial: 1 hour, 35 minutes*

In the previous tutorial, you became familiar with a neuronal network consisting of only an excitatory population. Here, we extend the approach we used to include both excitatory and inhibitory neuronal populations in our network. A simple, yet powerful model to study the dynamics of two interacting populations of excitatory and inhibitory neurons, is the so-called **Wilson-Cowan** rate model, which will be the subject of this tutorial.

The objectives of this tutorial are to:

- Write the **Wilson-Cowan** equations for the firing rate dynamics of a 2D system composed of an excitatory (E) and an inhibitory (I) population of neurons
- Simulate the dynamics of the system, i.e., Wilson-Cowan model.
- Plot the frequency-current (F-I) curves for both populations (i.e., E and I).
- Visualize and inspect the behavior of the system using **phase plane analysis**, **vector fields**, and **nullclines**.

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.

Bonus steps (applications):
- 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)_

```python
# @title Tutorial slides

# @markdown These are the slides for the videos in all tutorials today
from IPython.display import IFrame
IFrame(src=f"https://mfr.ca-1.osf.io/render?url=https://osf.io/nvuty/?direct%26mode=render%26action=download%26mode=render", width=854, height=480)
```

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

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

---
# Section 1: Wilson-Cowan model of excitatory and inhibitory populations

```python
# @title Video 1: Phase analysis of the Wilson-Cowan E-I model
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="BV1CD4y1m7dK", 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="GCpQmh45crM", 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)
```

This video explains how to model a network with interacting populations of excitatory and inhibitory neurons (the Wilson-Cowan model). It shows how to solve the network activity vs. time and introduces the phase plane in two dimensions.

## Section 1.1: Mathematical description of the WC model

*Estimated timing to here from start of tutorial: 12 min*

 Click here for text recap of relevant part of video

Many of the rich dynamics recorded in the brain are generated by the interaction of excitatory and inhibitory subtype neurons. Here, similar to what we did in the previous tutorial, we will model two coupled populations of E and I neurons (**Wilson-Cowan** model). We can write two coupled differential equations, each 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.

### Coding Exercise 1.1: Plot out the F-I curves for the E and I populations

Let's first plot out the F-I curves for the E and I populations using the helper function `F` with default parameter values.

```python
help(F)
```

```python
pars = default_pars()
x = np.arange(0, 10, .1)

print(pars['a_E'], pars['theta_E'])
print(pars['a_I'], pars['theta_I'])

###################################################################
# TODO for students: compute and plot the F-I curve here          #
# Note: aE, thetaE, aI and theta_I are in the dictionray 'pars'   #
raise NotImplementedError('student exercise: compute F-I curves of excitatory and inhibitory populations')
###################################################################

# Compute the F-I curve of the excitatory population
FI_exc = ...

# Compute the F-I curve of the inhibitory population
FI_inh = ...

# Visualize
plot_FI_EI(x, FI_exc, FI_inh)
```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W2D4_DynamicNetworks/solutions/W2D4_Tutorial2_Solution_043dd600.py)

*Example output:*

## Section 1.2: Simulation scheme for the Wilson-Cowan model

*Estimated timing to here from start of tutorial: 20 min*

Once again, we can integrate our equations numerically. Using the Euler method, the dynamics of E and I populations can be simulated on a time-grid of stepsize $\Delta t$. The updates for the activity of the excitatory and the inhibitory populations can be written as:

\begin{align}
r_E[k+1] &= r_E[k] + \Delta r_E[k]\\
r_I[k+1] &= r_I[k] + \Delta r_I[k]
\end{align}

with the increments

\begin{align}
\Delta r_E[k] &= \frac{\Delta t}{\tau_E}[-r_E[k] + F_E(w_{EE}r_E[k] -w_{EI}r_I[k] + I^{\text{ext}}_E[k];a_E,\theta_E)]\\
\Delta r_I[k] &= \frac{\Delta t}{\tau_I}[-r_I[k] + F_I(w_{IE}r_E[k] -w_{II}r_I[k] + I^{\text{ext}}_I[k];a_I,\theta_I)]
\end{align}

### Coding Exercise 1.2: Numerically integrate the Wilson-Cowan equations

We will implemenent this numerical simulation of our equations and visualize two simulations with similar initial points.

```python
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):

    ########################################################################
    # TODO for students: compute drE and drI and remove the error
    raise NotImplementedError("Student exercise: compute the change in E/I")
    ########################################################################

    # Calculate the derivative of the E population
    drE = ...

    # Calculate the derivative of the I population
    drI = ...

    # Update using Euler's method
    rE[k + 1] = rE[k] + drE
    rI[k + 1] = rI[k] + drI

  return rE, rI

pars = default_pars()

# Simulate first trajectory
rE1, rI1 = simulate_wc(**default_pars(rE_init=.32, rI_init=.15))

# Simulate second trajectory
rE2, rI2 = simulate_wc(**default_pars(rE_init=.33, rI_init=.15))

# Visualize
my_test_plot(pars['range_t'], rE1, rI1, rE2, rI2)
```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W2D4_DynamicNetworks/solutions/W2D4_Tutorial2_Solution_15eff812.py)

*Example output:*

The two plots above show the temporal evolution of excitatory ($r_E$, blue) and inhibitory ($r_I$, red) activity for two different sets of initial conditions.

### Interactive Demo 1.2: population trajectories with different initial values
In this interactive demo, we will simulate the Wilson-Cowan model and plot the trajectories of each population. We change the initial activity of the excitatory population.

What happens to the E and I population trajectories with different initial conditions?

```python
# @title

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

def plot_EI_diffinitial(rE_init=0.0):

  pars = default_pars(rE_init=rE_init, rI_init=.15)
  rE, rI = simulate_wc(**pars)

  plt.figure()
  plt.plot(pars['range_t'], rE, 'b', label='E population')
  plt.plot(pars['range_t'], rI, 'r', label='I population')
  plt.xlabel('t (ms)')
  plt.ylabel('Activity')
  plt.legend(loc='best')
  plt.show()

_ = widgets.interact(plot_EI_diffinitial, rE_init=(0.30, 0.35, .01))
```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W2D4_DynamicNetworks/solutions/W2D4_Tutorial2_Solution_50331264.py)

### Think! 1.2
It is evident that the steady states of the neuronal response can be different when different initial states are chosen. Why is that?

We will discuss this in the next section but try to think about it first.

---
# Section 2: Phase plane analysis

*Estimated timing to here from start of tutorial: 45 min*

Just like we used a graphical method to study the dynamics of a 1-D system in the previous tutorial, here we will learn a  graphical approach called **phase plane analysis** to study the dynamics of a 2-D system like the Wilson-Cowan model. You have seen this before in the [pre-reqs calculus day](https://compneuro.neuromatch.io/tutorials/W0D4_Calculus/student/W0D4_Tutorial3.html#section-3-2-phase-plane-plot-and-nullcline) and on the [Linear Systems day](https://compneuro.neuromatch.io/tutorials/W2D2_LinearSystems/student/W2D2_Tutorial1.html#section-4-stream-plots)

So far, we have plotted the activities of the two populations as a function of time, i.e., in the `Activity-t` plane, either the $(t, r_E(t))$ plane or the $(t, r_I(t))$ one. Instead, we can plot the two activities $r_E(t)$ and $r_I(t)$ against each other at any time point $t$. This characterization in the `rI-rE` plane $(r_I(t), r_E(t))$ is called the **phase plane**. Each line in the phase plane indicates how both $r_E$ and $r_I$ evolve with time.

```python
# @title Video 2: Nullclines and Vector Fields
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="BV15k4y1m7Kt", 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="V2SBAK2Xf8Y", 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)
```

## Interactive Demo 2: From the Activity - time plane to the **$r_I$ - $r_E$** phase plane

In this demo, we will visualize the system dynamics using both the `Activity-time` and the `(rE, rI)` phase plane. The circles indicate the activities at a given time $t$, while the lines represent the evolution of the system for the entire duration of the simulation.

Move the time slider to better understand how the top plot relates to the bottom plot. Does the bottom plot have explicit information about time? What information does it give us?

```python
# @title

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

pars = default_pars(T=10, rE_init=0.6, rI_init=0.8)
rE, rI = simulate_wc(**pars)

def plot_activity_phase(n_t):
  plt.figure(figsize=(8, 5.5))
  plt.subplot(211)
  plt.plot(pars['range_t'], rE, 'b', label=r'$r_E$')
  plt.plot(pars['range_t'], rI, 'r', label=r'$r_I$')
  plt.plot(pars['range_t'][n_t], rE[n_t], 'bo')
  plt.plot(pars['range_t'][n_t], rI[n_t], 'ro')
  plt.axvline(pars['range_t'][n_t], 0, 1, color='k', ls='--')
  plt.xlabel('t (ms)', fontsize=14)
  plt.ylabel('Activity', fontsize=14)
  plt.legend(loc='best', fontsize=14)

  plt.subplot(212)
  plt.plot(rE, rI, 'k')
  plt.plot(rE[n_t], rI[n_t], 'ko')
  plt.xlabel(r'$r_E$', fontsize=18, color='b')
  plt.ylabel(r'$r_I$', fontsize=18, color='r')

  plt.tight_layout()
  plt.show()

_ = widgets.interact(plot_activity_phase, n_t=(0, len(pars['range_t']) - 1, 1))
```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W2D4_DynamicNetworks/solutions/W2D4_Tutorial2_Solution_5d1fcb72.py)

## Section 2.1: Nullclines of the Wilson-Cowan Equations

*Estimated timing to here from start of tutorial: 1 hour, 3 min*

An important concept in the phase plane analysis is the "nullcline" which is defined as the set of points in the phase plane where the activity of one population (but not necessarily the other) does not change.

In other words, the $E$ and $I$ nullclines of Equation $(1)$ are defined as the points where $\displaystyle{\frac{dr_E}{dt}}=0$, for the excitatory nullcline, or $\displaystyle\frac{dr_I}{dt}=0$ for the inhibitory nullcline. That is:

\begin{align}
-r_E + F_E(w_{EE}r_E -w_{EI}r_I + I^{\text{ext}}_E;a_E,\theta_E) &= 0  \qquad (2)\\[1mm]
-r_I + F_I(w_{IE}r_E -w_{II}r_I + I^{\text{ext}}_I;a_I,\theta_I) &= 0    \qquad (3)
\end{align}

### Coding Exercise 2.1: Compute the nullclines of the Wilson-Cowan model

In the next exercise, we will compute and plot the nullclines of the E and I population.

Along the nullcline of excitatory population Equation $2$, you can calculate the inhibitory activity by rewriting Equation $2$ into

\begin{align}
r_I = \frac{1}{w_{EI}}\big{[}w_{EE}r_E - F_E^{-1}(r_E; a_E,\theta_E) + I^{\text{ext}}_E \big{]}. \qquad(4)
\end{align}

where $F_E^{-1}(r_E; a_E,\theta_E)$ is the inverse of the excitatory transfer function (defined below). Equation $4$ defines the $r_E$ nullcline.

Along the nullcline of inhibitory population Equation $3$, you can calculate the excitatory activity by rewriting Equation $3$ into
\begin{align}
r_E = \frac{1}{w_{IE}} \big{[} w_{II}r_I + F_I^{-1}(r_I;a_I,\theta_I) - I^{\text{ext}}_I \big{]}.  \qquad (5)
\end{align}

shere $F_I^{-1}(r_I; a_I,\theta_I)$ is the inverse of the inhibitory transfer function (defined below). Equation $5$ defines the $I$ nullcline.

Note that, when computing the nullclines with Equations 4-5, we also need to calculate the inverse of the transfer functions. \\

The inverse of the sigmoid shaped **f-I** function that we have been using is:

$$F^{-1}(x; a, \theta) = -\frac{1}{a} \ln \left[ \frac{1}{x + \displaystyle \frac{1}{1+\text{e}^{a\theta}}} - 1 \right] + \theta \qquad (6)$$

The first step is to implement the inverse transfer function:

```python
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
  """

  #########################################################################
  # TODO for students: compute F_inverse
  raise NotImplementedError("Student exercise: compute the inverse of F(x)")
  #########################################################################

  # Calculate Finverse (ln(x) can be calculated as np.log(x))
  F_inverse = ...

  return F_inverse

# Set parameters
pars = default_pars()
x = np.linspace(1e-6, 1, 100)

# Get inverse and visualize
plot_FI_inverse(x, a=1, theta=3)
```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W2D4_DynamicNetworks/solutions/W2D4_Tutorial2_Solution_f3500f59.py)

*Example output:*

Now you can compute the nullclines, using Equations 4-5 (repeated here for ease of access):

\begin{align}
r_I = \frac{1}{w_{EI}}\big{[}w_{EE}r_E - F_E^{-1}(r_E; a_E,\theta_E) + I^{\text{ext}}_E \big{]}. \qquad(4)
\end{align}

\begin{align}
r_E = \frac{1}{w_{IE}} \big{[} w_{II}r_I + F_I^{-1}(r_I;a_I,\theta_I) - I^{\text{ext}}_I \big{]}.  \qquad (5)
\end{align}

```python
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
  """
  #########################################################################
  # TODO for students: compute rI for rE nullcline and disable the error
  raise NotImplementedError("Student exercise: compute the E nullcline")
  #########################################################################

  # calculate rI for E nullclines on rI
  rI = ...

  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
  """
  #########################################################################
  # TODO for students: compute rI for rE nullcline and disable the error
  raise NotImplementedError("Student exercise: compute the I nullcline")
  #########################################################################

  # calculate rE for I nullclines on rI
  rE = ...

  return rE

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

# Visualize
plot_nullclines(Exc_null_rE, Exc_null_rI, Inh_null_rE, Inh_null_rI)
```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W2D4_DynamicNetworks/solutions/W2D4_Tutorial2_Solution_db10856b.py)

*Example output:*

Note that by definition along the blue line in the phase-plane spanned by $r_E, r_I$, $\displaystyle{\frac{dr_E(t)}{dt}} = 0$, therefore, it is called a nullcline.

That is, the blue nullcline divides the phase-plane spanned by $r_E, r_I$ into two regions: on one side of the nullcline $\displaystyle{\frac{dr_E(t)}{dt}} > 0$ and on the other side $\displaystyle{\frac{dr_E(t)}{dt}} < 0$.

The same is true for the red line along which $\displaystyle{\frac{dr_I(t)}{dt}} = 0$. That is, the red nullcline divides the phase-plane spanned by $r_E, r_I$ into two regions: on one side of the nullcline $\displaystyle{\frac{dr_I(t)}{dt}} > 0$ and on the other side $\displaystyle{\frac{dr_I(t)}{dt}} < 0$.

## Section 2.2: Vector field

*Estimated timing to here from start of tutorial: 1 hour, 20 min*

How can the phase plane and the nullcline curves help us understand the behavior of the Wilson-Cowan model?

 Click here for text recap of relevant part of video

The activities of the $E$ and $I$ populations $r_E(t)$ and $r_I(t)$ at each time point $t$ correspond to a single point in the phase plane, with coordinates $(r_E(t),r_I(t))$. Therefore, the time-dependent trajectory of the system can be described as a continuous curve in the phase plane, and the tangent vector to the trajectory, which is defined as the vector $\bigg{(}\displaystyle{\frac{dr_E(t)}{dt},\frac{dr_I(t)}{dt}}\bigg{)}$, indicates the direction towards which the activity is evolving and how fast is the activity changing along each axis. In fact, for each point $(E,I)$ in the phase plane, we can compute the tangent vector $\bigg{(}\displaystyle{\frac{dr_E}{dt},\frac{dr_I}{dt}}\bigg{)}$, which  indicates the behavior of the system when it traverses that point.

The map of tangent vectors in the phase plane is called **vector field**. The behavior of any trajectory in the phase plane is determined by i) the initial conditions $(r_E(0),r_I(0))$, and ii) the vector field $\bigg{(}\displaystyle{\frac{dr_E(t)}{dt},\frac{dr_I(t)}{dt}}\bigg{)}$.

In general, the value of the vector field at a particular point in the phase plane is represented by an arrow. The orientation and the size of the arrow reflect the direction and the norm of the vector, respectively.

### Coding Exercise 2.2: Compute and plot the vector field $\displaystyle{\Big{(}\frac{dr_E}{dt}, \frac{dr_I}{dt} \Big{)}}$

Note that

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

```python
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)."""
  ######################################################################
  # TODO for students: compute drEdt and drIdt and disable the error
  raise NotImplementedError("Student exercise: compute the vector field")
  ######################################################################

  # Compute the derivative of rE
  drEdt = ...

  # Compute the derivative of rI
  drIdt = ...

  return drEdt, drIdt

# Create vector field using EIderivs
plot_complete_analysis(default_pars())
```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W2D4_DynamicNetworks/solutions/W2D4_Tutorial2_Solution_92ba9d03.py)

*Example output:*

The last phase plane plot shows us that:
- Trajectories seem to follow the direction of the vector field
- Different trajectories eventually always reach one of two points depending on the initial conditions.
- The two points where the trajectories converge are the intersection of the two nullcline curves.

## Think! 2.2: Analyzing the vector field

There are, in total, three intersection points, meaning that the system has three fixed points.

1. One of the fixed points (the one in the middle) is never the final state of a trajectory. Why is that?
2. Why the arrows tend to get smaller as they approach the fixed points?

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W2D4_DynamicNetworks/solutions/W2D4_Tutorial2_Solution_a5cd9b5e.py)

---
# Summary

*Estimated timing of tutorial: 1 hour, 35 minutes*

Congratulations! You have finished the fourth day of the second week of the neuromatch academy! Here, you learned how to simulate a rate based model consisting of excitatory and inhibitory population of neurons.

In the last tutorial on dynamical neuronal networks you learned to:
- Implement and simulate a 2D system composed of an E and an I population of neurons using the **Wilson-Cowan** model
- Plot the frequency-current (F-I) curves for both populations
- Examine the behavior of the system using phase **plane analysis**, **vector fields**, and **nullclines**.

Do you have more time? Have you finished early? We have more fun material for you!

In the bonus tutorial, there are some, more advanced concepts on dynamical systems:

- You will learn how to find the fixed points on such a system, and to investigate its stability by linearizing its dynamics and examining the **Jacobian matrix**.
- You will see identify conditions under which the Wilson-Cowan model can exhibit oscillations.

If you need even more, there are two applications of the Wilson-Cowan model:

- Visualization of an Inhibition-stabilized network
- Simulation of working memory


```
%matplotlib inline

import pandas as pd
import numpy as np
import os
import cv2
import matplotlib.pyplot as plt
import random
import tensorflow as tf
```

```
from keras.datasets import mnist
(X_train, y_train), (X_test, y_test) = mnist.load_data()
print(X_train.shape, y_train.shape,X_test.shape, y_test.shape)
```

    (60000, 28, 28) (60000,) (10000, 28, 28) (10000,)

```
X_train = X_train.reshape(60000,784)
X_test = X_test.reshape(10000,784)

```

```
from sklearn.model_selection import train_test_split
from sklearn.decomposition import PCA

from sklearn import preprocessing
from sklearn.preprocessing import StandardScaler

from sklearn import svm
from sklearn.model_selection import GridSearchCV
from sklearn.gaussian_process.kernels import DotProduct
from sklearn.svm import SVC
from sklearn.pipeline import Pipeline

X_train_proc = preprocessing.scale(X_train)
X_test_proc = preprocessing.scale(X_test)

from sklearn.preprocessing import MinMaxScaler
scaling = MinMaxScaler(feature_range=(-1,1)).fit(X_train_proc)
X_train_proc = scaling.transform(X_train_proc)
X_test_proc = scaling.transform(X_test_proc)

pca = PCA(n_components=0.85)
X_train_proc= pca.fit_transform(X_train_proc)
X_test_proc = pca.fit_transform(X_test_proc)

#preproc = [('scaler', MinMaxScaler(feature_range=(-1,1)).fit(X_train)),('pca', PCA()),('clf',SVC(kernel='linear'))]

#pipeline = Pipeline(preproc)
#parameters = {'kernel':('linear', 'rbf'), 'C':[1,10]}

#grid = GridSearchCV(pipeline, param_grid=parameters, cv=5)
#grid.fit(X_train, y_train)

#Create a svm Classifier

from sklearn.model_selection import cross_validate
from sklearn.metrics import recall_score

clf = SVC()
# clf=[clf1, clf2]
param_grid = {
      'kernel': ['linear','rbf'],
      'C': [0.1,0.5],
      'gamma': [0.0001, 0.001, 0.01]
    }
grid=GridSearchCV(clf, param_grid = param_grid , cv = 5, scoring = 'accuracy')
grid_clf = grid.fit( X_train_proc, y_train)

#scoring = ['precision_macro', 'accuracy_macro']
#scores = cross_validate(clf, X_train_proc, y_train, scoring=scoring)
#Train the model using the training sets
#Based on a single run using svm's predict , the ccuracy was around 90%. The overall run for 60k training data with hyperparamtee tuning is timeconsuming
# Therfore, this could not be computed.
#While extremely, slow the final accuracy is around 95.92%
```

```
y_pred = cross_val_predict(clf, X_test_proc, y_test, cv=3)
```

```
i=0
count=0
while i<10000:
  if(y_test[i]==y_pred[i]):
    count=count+1
  i=i+1
print("Accuracy: ",(count/len(y_test)*100),'%')
```

    Accuracy:  95.92 %

\begin{align*}
\text{minimize  } \ w^T.w+C \sum_{n=1}^{N} \xi_{i}
\\
\text{subject to } y_i.(w^T.x_i) \geq {1-\xi_{i}} \text{ and } \xi_i \geq 0 \text{ for i }=1,...,N
\end{align*}

\begin{align}
{\mathcal{L}} = \ {\overrightarrow{w}}^T\overrightarrow{w} + C \sum_{i=1}^N\xi_i + \Sigma_i\alpha_i(y_i.w^Tx_i -1+\xi_i)
\end{align}

Lagrange multipliers $\alpha \geq 0$

Inequality constraints $y_i(w^Tx_i) \geq 1- \xi_i$ and $\xi_i \geq 0$ for $i=1,...,N$

Therefore,

$y_i(w^Tx_i) - 1 \leq \xi_i$

\begin{align}
{\mathcal{L}} = \ {\overrightarrow{w}}^T\overrightarrow{w} + C \sum_{i=1}^N\xi_i - \Sigma_i\alpha_i(y_i.w^Tx_i - 1+ \xi_i)
\end{align}

\begin{align}
\\
  \frac{\partial_{\mathcal{L}}}{\partial_w} = w - \sum_{i=1}^N\alpha_i y_i x_i = 0 \implies w = \sum_{i=1}^N\alpha_i y_i \overrightarrow{x_i}
  \\
  \frac{\partial_{\mathcal{L}}}{\partial_\xi} = 0 \implies C - \alpha_i= 0
  \\
  \implies 0 \leq \alpha_i \leq C
  \end{align}

\begin{align}
\text {Substituting } w = \sum_{i=1}^N\alpha_i y_i \overrightarrow{x_i}, C=\alpha_i
\\
\text { into Lagrange function, we get the dual problem of maximizing: }
\end{align}

\begin{align}
{\mathcal{L}} = \ w^T \sum_{i=1}^N\alpha_i y_i \overrightarrow{x_i} + \alpha_i\sum_{i=1}^N\xi_i + \sum_{i=1}^N\alpha_i(y_i.w^Tx_i -1+\xi_i)
\\= \ w^T \sum_{i=1}^N \alpha_i y_i \overrightarrow{x_i} + \alpha_i\sum_{i=1}^N\xi_i  +\sum_{i=1}^N \alpha_i - w^T \sum_{i=1}^N \alpha_i y_i \overrightarrow{x_i} - \sum_{i=1}^N \alpha_i \xi_i
\end{align}

\begin{align}
\\=\alpha_i\sum_{i=1}^N \xi_i -\sum_{i=1}^N \alpha_i  +\sum_{i=1}^N \alpha_i \sum_{i=1}^N \xi_i
\end{align}

The primal margin is :
 $\gamma = \frac{1}{\sqrt{w^T.w +C \sum_{i=1}^{N} \xi_i }}$

The dual margin is :
$\gamma = \frac{1}{\sqrt{\alpha_i + \sum_{i=1}^{N} \alpha_i \sum_{i=1}^{N} \xi_i }}$

### Benefits of maximizing the margin

* Maximizing the margin regularizes SVM weights that helps avoid overfitting.
* A large margin would help reduce the functional space.
* Better generalization of small sample spaces

#### Characterize the support vectors

* Margin boundaries:
\begin{align}
w^Tx + C = 1 \text{ and } w^Tx + C = -1
\end{align}

* Margin region (0 < ξ < 1)

#### Benefits of solving the dual problem instead of the primal problem

* Solving the primal problem gives w but there is no insight on αi.
* Dual representation provides lower bound to primal problem.
* Dual representation can be solved using Kernel trick


```python
%matplotlib inline
import sys, platform, os
import matplotlib
from matplotlib import pyplot as plt
import numpy as np
import scipy as sci
import camb as camb
```

```python
from camb import model, initialpower
print('Using CAMB %s installed at %s'%(camb.__version__,os.path.dirname(camb.__file__)))
```

    Using CAMB 1.3.5 installed at /opt/anaconda3/lib/python3.8/site-packages/camb

```python
import classy as classy
from classy import Class
print('Using CLASS %s installed at %s'%(classy.__version__,os.path.dirname(classy.__file__)))
```

    Using CLASS v2.9.4 installed at /opt/anaconda3/lib/python3.8/site-packages

```python
from ipywidgets.widgets import *
import sympy
from sympy import cos, simplify, sin, sinh, tensorcontraction
from einsteinpy.symbolic import EinsteinTensor, MetricTensor, RicciScalar
sympy.init_printing()
```

```python
from IPython.display import Markdown, display
```

```python
def printmd(string, color='black', math=False, fmt='header2'):
    if math==True:
        mstring = string
    elif math==False:
        mstring="\\textrm{"+string+"}"
    #colorstr = "<span style='color:{}'>{}</span>".format(color, string)
    fmtstr = "${\\color{"+color+"}{"+mstring+"}}$"

    if fmt=='header2':
        fmtstr="## "+fmtstr
    if fmt=='header1':
        fmtstr="# "+fmtstr
    display(Markdown(fmtstr))
    return None
```

```python
from astropy.cosmology import WMAP5
from astropy.cosmology import FlatLambdaCDM
import astropy.units as u
WMAP5.H(0)
```

$70.2 \; \mathrm{\frac{km}{Mpc\,s}}$

```python
WMAP_5 = dict()
```

```python
WMAP_5['ombh2'] = 0.02238   ## Omega_b * h**2
WMAP_5['omch2'] = 0.12011  ## Omega_c * h**2
WMAP_5['ln1010As'] = 3.0448  ## ln(10**10 * As), scalar amplitude
WMAP_5['ns'] = 0.96605       ## spectral index
WMAP_5['ommh2'] = 0.14314    ##   Omega_m * h**2   , total matter
WMAP_5['H0'] = 70.2   ##   H0 = 100h
WMAP_5['sigma8'] = 0.8120    ## amplitude of density fluctuations
WMAP_5['tau'] = 0.0543      ## Optical depth
WMAP_5['age_Gyr'] = 13.7971   ## Age of the Universe
```

```python
WMAP_5['h'] = WMAP_5['H0']/100
WMAP_5['Om'] = WMAP_5['ommh2']/WMAP_5['h']**2
WMAP_5['Ob'] = WMAP_5['ombh2']/WMAP_5['h']**2
WMAP_5['Oc'] = WMAP_5['omch2']/WMAP_5['h']**2
WMAP_5['As'] = np.exp(WMAP_5['ln1010As'])/np.power(10,10)    ## As, scalar amplitude
```

```python
WMAP_5['h']
```

```python
WMAP_5['Or'] = 0.0000930479
```

```python
WMAP_5['Ol'] = 1-np.array([WMAP_5[oo] for oo in ['Oc','Ob','Om']]).sum()  ## Ol = Omega_Lambda
WMAP_5['Ol']
```

```python
cosmo = FlatLambdaCDM(H0=70.2 * u.km / u.s / u.Mpc, Om0=0.3)
```

```python
def a_of_z(z):
    a=1/(1+z)
    return a
```

```python
def Omega_L(Omega_c, Omega_b, Omega_r):
    """
    Function for Omega_Lambda, dark energy.

    For a flat Universe:
        Omega_Lambda = 1-Omega_c-Omega_b-Omega_r

    """
    oL = 1 - Omega_c - Omega_b - Omega_r
    return oL
```

```python
def cosmological_parameters(cosmo_pars=dict()):
    H0 = cosmo_pars.get('H0', WMAP_5['H0'])  # WMAP5 cosmological parameters as default
    Oc = cosmo_pars.get('Oc', WMAP_5['Oc'])
    Ob = cosmo_pars.get('Ob', WMAP_5['Ob'])
    Or = cosmo_pars.get('Or', WMAP_5['Or'])
    Om = Ob+Oc
    OL = Omega_L(Oc, Ob, Or)
    return H0, Oc, Ob, Or, Om, OL
```

```python
cosmological_parameters()
```

```python
def Hubble(z, cosmo_pars=dict()):
    H0, Oc, Ob, Or, Om, OL = cosmological_parameters(cosmo_pars)
    H = H0 * np.sqrt(Om*(1+z)**3 + Or*(1+z)**4 + OL)
    return H
```

```python
Hubble(0.)
```

```python
z_arr = np.linspace(0.,10, 100)
fig, ax = plt.subplots(1, 1, sharey='row', sharex='col', figsize=(10,8))  #all plots in the same row, share the y-axis.

# once you specify an axis, it is in this instance where plots are performed
ax.semilogx(z_arr, Hubble(z_arr), '-', label='WMAP5', color='orange', lw=3)
ax.legend(fontsize=26)
ax.set_xlabel('redshift $z$', fontsize=26)
ax.set_ylabel(r'$H(z)$ in km/s/Mpc', fontsize=26);
```

```python
#Set up a new set of parameters for CAMB
pars = camb.CAMBparams()
#This function sets up CosmoMC-like settings, with one massive neutrino and helium set using BBN consistency
pars.set_cosmology(H0=WMAP_5['H0'], ombh2=WMAP_5['ombh2'], omch2=WMAP_5['omch2'])
```

    class: <CAMBparams>
     WantCls = True
     WantTransfer = False
     WantScalars = True
     WantTensors = False
     WantVectors = False
     WantDerivedParameters = True
     Want_cl_2D_array = True
     Want_CMB = True
     Want_CMB_lensing = True
     DoLensing = True
     NonLinear = NonLinear_none
     Transfer: <TransferParams>
       high_precision = False
       accurate_massive_neutrinos = False
       kmax = 0.9
       k_per_logint = 0
       PK_num_redshifts = 1
       PK_redshifts = [0.0]
     want_zstar = False
     want_zdrag = False
     min_l = 2
     max_l = 2500
     max_l_tensor = 600
     max_eta_k = 5000.0
     max_eta_k_tensor = 1200.0
     ombh2 = 0.02238
     omch2 = 0.12011
     omk = 0.0
     omnuh2 = 0.0006451383989381787
     H0 = 70.2
     TCMB = 2.7255
     YHe = 0.24540281330622907
     num_nu_massless = 2.030666666666667
     num_nu_massive = 1
     nu_mass_eigenstates = 1
     share_delta_neff = False
     nu_mass_degeneracies = [1.0153333333333332]
     nu_mass_fractions = [1.0]
     nu_mass_numbers = [1]
     InitPower: <InitialPowerLaw>
       tensor_parameterization = tensor_param_rpivot
       ns = 0.96
       nrun = 0.0
       nrunrun = 0.0
       nt = -0.0
       ntrun = -0.0
       r = 0.0
       pivot_scalar = 0.05
       pivot_tensor = 0.05
       As = 2e-09
       At = 1.0
     Recomb: <Recfast>
       min_a_evolve_Tm = 0.0011098779505118728
       RECFAST_fudge = 1.125
       RECFAST_fudge_He = 0.86
       RECFAST_Heswitch = 6
       RECFAST_Hswitch = True
       AGauss1 = -0.14
       AGauss2 = 0.079
       zGauss1 = 7.28
       zGauss2 = 6.73
       wGauss1 = 0.18
       wGauss2 = 0.33
     Reion: <TanhReionization>
       Reionization = True
       use_optical_depth = False
       redshift = 10.0
       optical_depth = 0.0
       delta_redshift = 0.5
       fraction = -1.0
       include_helium_fullreion = True
       helium_redshift = 3.5
       helium_delta_redshift = 0.4
       helium_redshiftstart = 5.5
       tau_solve_accuracy_boost = 1.0
       timestep_boost = 1.0
       max_redshift = 50.0
     DarkEnergy: <DarkEnergyFluid>
       w = -1.0
       wa = 0.0
       cs2 = 1.0
       use_tabulated_w = False
     NonLinearModel: <Halofit>
       Min_kh_nonlinear = 0.005
       halofit_version = mead2020
       HMCode_A_baryon = 3.13
       HMCode_eta_baryon = 0.603
       HMCode_logT_AGN = 7.8
     Accuracy: <AccuracyParams>
       AccuracyBoost = 1.0
       lSampleBoost = 1.0
       lAccuracyBoost = 1.0
       AccuratePolarization = True
       AccurateBB = False
       AccurateReionization = True
       TimeStepBoost = 1.0
       BackgroundTimeStepBoost = 1.0
       IntTolBoost = 1.0
       SourcekAccuracyBoost = 1.0
       IntkAccuracyBoost = 1.0
       TransferkBoost = 1.0
       NonFlatIntAccuracyBoost = 1.0
       BessIntBoost = 1.0
       LensingBoost = 1.0
       NonlinSourceBoost = 1.0
       BesselBoost = 1.0
       LimberBoost = 1.0
       SourceLimberBoost = 1.0
       KmaxBoost = 1.0
       neutrino_q_boost = 1.0
     SourceTerms: <SourceTermParams>
       limber_windows = True
       limber_phi_lmin = 100
       counts_density = True
       counts_redshift = True
       counts_lensing = False
       counts_velocity = True
       counts_radial = False
       counts_timedelay = True
       counts_ISW = True
       counts_potential = True
       counts_evolve = False
       line_phot_dipole = False
       line_phot_quadrupole = False
       line_basic = True
       line_distortions = True
       line_extra = False
       line_reionization = False
       use_21cm_mK = True
     z_outputs = []
     scalar_initial_condition = initial_adiabatic
     InitialConditionVector = []
     OutputNormalization = 1
     Alens = 1.0
     MassiveNuMethod = Nu_best
     DoLateRadTruncation = True
     Evolve_baryon_cs = False
     Evolve_delta_xe = False
     Evolve_delta_Ts = False
     Do21cm = False
     transfer_21cm_cl = False
     Log_lvalues = False
     use_cl_spline_template = True
     SourceWindows = []
     CustomSources: <CustomSources>
       num_custom_sources = 0
       c_source_func = None
       custom_source_ell_scales = []

```python
pars.H0
```

```python
results = camb.get_results(pars)
results.calc_background(pars)
```

```python
results.get_derived_params()
```

    {'age': 13.545508925002487,
     'zstar': 1089.9127705851572,
     'rstar': 144.39813840951794,
     'thetastar': 1.0496437751173935,
     'DAstar': 13.75687083871557,
     'zdrag': 1059.9673043922098,
     'rdrag': 147.0538901702859,
     'kd': 0.14091424370378375,
     'thetad': 0.16205980754637161,
     'zeq': 3405.073594348986,
     'keq': 0.010392623290320202,
     'thetaeq': 0.819467940898796,
     'thetarseq': 0.45281650365615894}

```python
WMAP_5.keys()
```

    dict_keys(['ombh2', 'omch2', 'ln1010As', 'ns', 'ommh2', 'H0', 'sigma8', 'tau', 'age_Gyr', 'h', 'Om', 'Ob', 'Oc', 'As', 'Or', 'Ol'])

```python
z_arr = np.linspace(0.,10, 100)
dA_camb = results.angular_diameter_distance(z_arr);
rz_camb = results.comoving_radial_distance(z_arr);
```

```python
# Define your cosmology (what is not specified will be set to CLASS default parameters)
## CLASS is more flexible in the names of parameters passed, because the names are "interpreted"
params = {
    'H0': WMAP_5['H0'],
    'omega_b': WMAP_5['ombh2'],
    'Omega_cdm': WMAP_5['Oc']}

# Create an instance of the CLASS wrapper
cosmo = Class()
# Set the parameters to the cosmological code
cosmo.set(params)
cosmo.compute()
```

```python
cosmo.angular_distance(0.2)
```

```python
dA_class = np.array([cosmo.angular_distance(zi) for zi in z_arr])
```

```python
cosmo.z_of_r([0.2])
```

    (array([815.69127983]), array([0.00025764]))

```python
rz_class, dz_dr_class  = cosmo.z_of_r(z_arr)
```

```python
fig, ax = plt.subplots(2, 1, sharex='col', figsize=(10,8))  #all plots in the same row, share the y-axis.

# once you specify an axis, it is in this instance where plots are performed
ax[0].plot(z_arr, rz_camb, '-', label='CAMB $r(z)$', color='orange', lw=3)
ax[0].plot(z_arr, rz_class, '-.', label='CLASS $r(z)$', color='purple', lw=3)
ax[0].legend(fontsize=20)
ax[0].set_xlabel('redshift $z$', fontsize=22)
ax[0].set_ylabel(r'$r(z)$ in Mpc', fontsize=22);

ax[1].plot(z_arr, dA_camb, '-', label='CAMB $d_A(z)$', color='teal', lw=3)
ax[1].plot(z_arr, dA_class, '-.', label='CLASS $d_A(z)$', color='firebrick', lw=3)
ax[1].legend(fontsize=20)
ax[1].set_xlabel('redshift $z$', fontsize=22)
ax[1].set_ylabel(r'$d_A(z)$ in Mpc', fontsize=22);
```

Notice that (at least in a flat Universe ) objects of a fixed physical size, appear larger at larger redshifts. At a very high redshift, the angle subtended by an object of constant comoving size, would occupy the entire sky!

```python

```


# LSTM Structure and Hidden State

We know that RNNs are used to maintain a kind of memory by linking the output of one node to the input of the next. In the case of an LSTM, for each piece of data in a sequence (say, for a word in a given sentence),
there is a corresponding *hidden state* $h_t$. This hidden state is a function of the pieces of data that an LSTM has seen over time; it contains some weights and, represents both the short term and long term memory components for the data that the LSTM has already seen.

So, for an LSTM that is looking at words in a sentence, **the hidden state of the LSTM will change based on each new word it sees. And, we can use the hidden state to predict the next word in a sequence** or help identify the type of word in a language model, and lots of other things!

## LSTMs in Pytorch

To create and train an LSTM, you have to know how to structure the inputs, and hidden state of an LSTM. In PyTorch an LSTM can be defined as: `lstm = nn.LSTM(input_size=input_dim, hidden_size=hidden_dim, num_layers=n_layers)`.

In PyTorch, an LSTM expects all of its inputs to be 3D tensors, with dimensions defined as follows:
>* `input_dim` = the number of inputs (a dimension of 20 could represent 20 inputs)
>* `hidden_dim` = the size of the hidden state; this will be the number of outputs that each LSTM cell produces at each time step.
>* `n_layers ` = the number of hidden LSTM layers to use; this is typically  a value between 1 and 3; a value of 1 means that each LSTM cell has one hidden state. This has a default value of 1.

### Hidden State

Once an LSTM has been defined with input and hidden dimensions, we can call it and retrieve the output and hidden state at every time step.
 `out, hidden = lstm(input.view(1, 1, -1), (h0, c0))`

The inputs to an LSTM are **`(input, (h0, c0))`**.
>* `input` = a Tensor containing the values in an input sequence; this has values: (seq_len, batch, input_size)
>* `h0` = a Tensor containing the initial hidden state for each element in a batch
>* `c0` = a Tensor containing the initial cell memory for each element in the batch

`h0` nd `c0` will default to 0, if they are not specified. Their dimensions are: (n_layers, batch, hidden_dim).

These will become clearer in the example in this notebook. This and the following notebook are modified versions of [this PyTorch LSTM tutorial](https://pytorch.org/tutorials/beginner/nlp/sequence_models_tutorial.html#lstm-s-in-pytorch).

Let's take a simple example and say we want to process a single sentence through an LSTM. If we want to run the sequence model over one sentence "Giraffes in a field", our input should look like this `1x4` row vector of individual words:

\begin{align}\begin{bmatrix}
   \text{Giraffes  }
   \text{in  }
   \text{a  }
   \text{field}
   \end{bmatrix}\end{align}

In this case, we know that we have **4 inputs words** and we decide how many outputs to generate at each time step, say we want each LSTM cell to generate **3 hidden state values**. We'll keep the number of layers in our LSTM at the default size of 1.

The hidden state and cell memory will have dimensions (n_layers, batch, hidden_dim), and in this case that will be (1, 1, 3) for a 1 layer model with one batch/sequence of words to process (this one sentence) and 3 genereated, hidden state values.

### Example Code

Next, let's see an example of one LSTM that is designed to look at a sequence of 4 values (numerical values since those are easiest to create and track) and generate 3 values as output. This is what the sentence processing network from above will look like, and you are encouraged to change these input/hidden-state sizes to see the effect on the structure of the LSTM!

```python
import torch
import torch.nn as nn
import matplotlib.pyplot as plt

%matplotlib inline

torch.manual_seed(2) # so that random variables will be consistent and repeatable for testing
```

    <torch._C.Generator at 0x7f7963ff00f0>

### Define a simple LSTM

**A note on hidden and output dimensions**

The `hidden_dim` and size of the output will be the same unless you define your own LSTM and change the number of outputs by adding a linear layer at the end of the network, ex. fc = nn.Linear(hidden_dim, output_dim).

```python
from torch.autograd import Variable

# define an LSTM with an input dim of 4 and hidden dim of 3
# this expects to see 4 values as input and generates 3 values as output
input_dim = 4
hidden_dim = 3
lstm = nn.LSTM(input_size=input_dim, hidden_size=hidden_dim)

# make 5 input sequences of 4 random values each
inputs_list = [torch.randn(1, input_dim) for _ in range(5)]
print('inputs: \n', inputs_list)
print('\n')

# initialize the hidden state
# (1 layer, 1 batch_size, 3 outputs)
# first tensor is the hidden state, h0
# second tensor initializes the cell memory, c0
h0 = torch.randn(1, 1, hidden_dim)
c0 = torch.randn(1, 1, hidden_dim)

h0 = Variable(h0)
c0 = Variable(c0)
# step through the sequence one element at a time.
for i in inputs_list:
    # wrap in Variable
    i = Variable(i)

    # after each step, hidden contains the hidden state
    out, hidden = lstm(i.view(1, 1, -1), (h0, c0))
    print('out: \n', out)
    print('hidden: \n', hidden)

```

    inputs:
     [tensor([[1.4934, 0.4987, 0.2319, 1.1746]]), tensor([[-1.3967,  0.8998,  1.0956, -0.5231]]), tensor([[-0.8462, -0.9946,  0.6311,  0.5327]]), tensor([[-0.8454,  0.9406, -2.1224,  0.0233]]), tensor([[ 0.4836,  1.2895,  0.8957, -0.2465]])]

    out:
     tensor([[[-0.4372,  0.2583,  0.2947]]], grad_fn=<StackBackward>)
    hidden:
     (tensor([[[-0.4372,  0.2583,  0.2947]]], grad_fn=<StackBackward>), tensor([[[-0.7344,  0.6209,  0.4191]]], grad_fn=<StackBackward>))
    out:
     tensor([[[-0.2836,  0.1314,  0.4133]]], grad_fn=<StackBackward>)
    hidden:
     (tensor([[[-0.2836,  0.1314,  0.4133]]], grad_fn=<StackBackward>), tensor([[[-0.5041,  0.2672,  0.6370]]], grad_fn=<StackBackward>))
    out:
     tensor([[[-0.3404,  0.4880,  0.1949]]], grad_fn=<StackBackward>)
    hidden:
     (tensor([[[-0.3404,  0.4880,  0.1949]]], grad_fn=<StackBackward>), tensor([[[-0.5552,  0.7909,  0.3300]]], grad_fn=<StackBackward>))
    out:
     tensor([[[-0.3544,  0.2405,  0.3150]]], grad_fn=<StackBackward>)
    hidden:
     (tensor([[[-0.3544,  0.2405,  0.3150]]], grad_fn=<StackBackward>), tensor([[[-0.5645,  1.0073,  0.6101]]], grad_fn=<StackBackward>))
    out:
     tensor([[[-0.3328,  0.0437,  0.3817]]], grad_fn=<StackBackward>)
    hidden:
     (tensor([[[-0.3328,  0.0437,  0.3817]]], grad_fn=<StackBackward>), tensor([[[-0.5311,  0.1181,  0.5304]]], grad_fn=<StackBackward>))

```python
input_dim = 4
hidden_dim = 2

lstm = nn.LSTM(input_size=input_dim, hidden_size=2)

input_lists = [torch.rand(1, input_dim) for _ in range(5)]
```

```python
h0 = torch.rand(1, 1, hidden_dim)
c0 = torch.rand(1, 1, hidden_dim)
```

```python
for event in input_lists:
    out, hidden = lstm(event.view(1, 1, -1), (h0, c0))
    print(f"out: {out}")
    print(f"hidden: {hidden}\n")
```

    out: tensor([[[0.3965, 0.0711]]], grad_fn=<StackBackward>)
    hidden: (tensor([[[0.3965, 0.0711]]], grad_fn=<StackBackward>), tensor([[[0.5256, 0.1860]]], grad_fn=<StackBackward>))

    out: tensor([[[0.3637, 0.0920]]], grad_fn=<StackBackward>)
    hidden: (tensor([[[0.3637, 0.0920]]], grad_fn=<StackBackward>), tensor([[[0.4714, 0.2373]]], grad_fn=<StackBackward>))

    out: tensor([[[0.4169, 0.1332]]], grad_fn=<StackBackward>)
    hidden: (tensor([[[0.4169, 0.1332]]], grad_fn=<StackBackward>), tensor([[[0.5882, 0.3409]]], grad_fn=<StackBackward>))

    out: tensor([[[0.4571, 0.1420]]], grad_fn=<StackBackward>)
    hidden: (tensor([[[0.4571, 0.1420]]], grad_fn=<StackBackward>), tensor([[[0.6963, 0.4126]]], grad_fn=<StackBackward>))

    out: tensor([[[0.4182, 0.1429]]], grad_fn=<StackBackward>)
    hidden: (tensor([[[0.4182, 0.1429]]], grad_fn=<StackBackward>), tensor([[[0.6402, 0.4584]]], grad_fn=<StackBackward>))

```python
lstm()
```

```python
hidden_dim = 4
lstm = nn.LSTM(input_dim,  hidden_dim, 2)
input_list = [torch.randn(1, input_dim) for _ in range(5)]

h0 = Variable(torch.randn(2, 1, hidden_dim))
c0 = Variable(torch.randn(2, 1, hidden_dim))

for i in input_list:
  i = Variable(i)
  print(f"in: {i.view(1, 1, -1)}")
  out, hidden = lstm(i.view(1, 1, -1), (h0, c0))
  print(f'out: {out}')
  print(f"hidden: {hidden}\n")
```

    in: tensor([[[ 0.2569, -0.4643,  2.0006, -1.5214]]])
    out: tensor([[[-0.1663,  0.2665, -0.3480, -0.5081]]], grad_fn=<StackBackward>)
    hidden: (tensor([[[-0.4254,  0.0519, -0.2965,  0.1403]],

            [[-0.1663,  0.2665, -0.3480, -0.5081]]], grad_fn=<StackBackward>), tensor([[[-0.6324,  0.0787, -0.6183,  0.6796]],

            [[-0.2729,  0.8146, -0.9502, -0.8631]]], grad_fn=<StackBackward>))

    in: tensor([[[ 0.0369, -1.2487, -1.5460, -0.3628]]])
    out: tensor([[[-0.1785,  0.2389, -0.2996, -0.4956]]], grad_fn=<StackBackward>)
    hidden: (tensor([[[-0.0434,  0.0475, -0.0868,  0.1875]],

            [[-0.1785,  0.2389, -0.2996, -0.4956]]], grad_fn=<StackBackward>), tensor([[[-0.0669,  0.0757, -0.3222,  1.2102]],

            [[-0.2991,  0.7377, -0.9638, -0.8427]]], grad_fn=<StackBackward>))

    in: tensor([[[ 1.3392, -0.6601,  0.9427, -1.1863]]])
    out: tensor([[[-0.1748,  0.2686, -0.3431, -0.4984]]], grad_fn=<StackBackward>)
    hidden: (tensor([[[-0.4195,  0.1033, -0.1714,  0.1887]],

            [[-0.1748,  0.2686, -0.3431, -0.4984]]], grad_fn=<StackBackward>), tensor([[[-0.5879,  0.1792, -0.5478,  0.6953]],

            [[-0.2906,  0.7957, -0.9896, -0.8518]]], grad_fn=<StackBackward>))

    in: tensor([[[ 1.0948,  0.0318, -0.7739,  0.0496]]])
    out: tensor([[[-0.1917,  0.2521, -0.3137, -0.4936]]], grad_fn=<StackBackward>)
    hidden: (tensor([[[-0.1812,  0.1079, -0.0981,  0.3066]],

            [[-0.1917,  0.2521, -0.3137, -0.4936]]], grad_fn=<StackBackward>), tensor([[[-0.3009,  0.1995, -0.2912,  1.1768]],

            [[-0.3252,  0.7569, -1.0025, -0.8376]]], grad_fn=<StackBackward>))

    in: tensor([[[-0.6174,  2.2406,  1.3194, -1.3208]]])
    out: tensor([[[-0.1716,  0.2476, -0.3120, -0.5076]]], grad_fn=<StackBackward>)
    hidden: (tensor([[[-0.2144, -0.1029, -0.2457,  0.1905]],

            [[-0.1716,  0.2476, -0.3120, -0.5076]]], grad_fn=<StackBackward>), tensor([[[-0.5536, -0.2203, -0.3159,  1.1233]],

            [[-0.2768,  0.7429, -0.9149, -0.8576]]], grad_fn=<StackBackward>))

You should see that the output and hidden Tensors are always of length 3, which we specified when we defined the LSTM with `hidden_dim`.

### All at once

A for loop is not very efficient for large sequences of data, so we can also, **process all of these inputs at once.**

1. concatenate all our input sequences into one big tensor, with a defined batch_size
2. define the shape of our hidden state
3. get the outputs and the *most recent* hidden state (created after the last word in the sequence has been seen)

The outputs may look slightly different due to our differently initialized hidden state.

```python
input_dim = 4
hidden_dim = 3
lstm = nn.LSTM(input_size=input_dim, hidden_size=hidden_dim)

# turn inputs into a tensor with 5 rows of data
# add the extra 2nd dimension (1) for batch_size
# inputs = (No. of input, batch_size, Sequence in one row)
inputs = torch.cat(inputs_list).view(len(inputs_list), 1, -1)

# print out our inputs and their shape
# you should see (number of sequences, batch size, input_dim)
print('inputs size: \n', inputs.size())
print('\n')

print('inputs: \n', inputs)
print('\n')

# initialize the hidden state
h0 = torch.randn(1, 1, hidden_dim)
c0 = torch.randn(1, 1, hidden_dim)

# wrap everything in Variable
inputs = Variable(inputs)
h0 = Variable(h0)
c0 = Variable(c0)
# get the outputs and hidden state
out, hidden = lstm(inputs, (h0, c0))

print('out: \n', out)
print('hidden: \n', hidden)
```

    inputs size:
     torch.Size([5, 1, 4])

    inputs:
     tensor([[[ 1.4934,  0.4987,  0.2319,  1.1746]],

            [[-1.3967,  0.8998,  1.0956, -0.5231]],

            [[-0.8462, -0.9946,  0.6311,  0.5327]],

            [[-0.8454,  0.9406, -2.1224,  0.0233]],

            [[ 0.4836,  1.2895,  0.8957, -0.2465]]])

    out:
     tensor([[[-0.1378,  0.1774, -0.0339]],

            [[-0.0184,  0.0613, -0.0383]],

            [[-0.0090, -0.0309,  0.0185]],

            [[-0.1073,  0.0349, -0.0157]],

            [[-0.1489,  0.2706,  0.0120]]], grad_fn=<StackBackward>)
    hidden:
     (tensor([[[-0.1489,  0.2706,  0.0120]]], grad_fn=<StackBackward>), tensor([[[-0.5563,  0.4140,  0.0539]]], grad_fn=<StackBackward>))

### Next: Hidden State and Gates

This notebooks shows you the structure of the input and output of an LSTM in PyTorch. Next, you'll learn more about how exactly an LSTM represents long-term and short-term memory in it's hidden state, and you'll reach the next notebook exercise.

#### Part of Speech

In the notebook that comes later in this lesson, you'll see how to define a model to tag parts of speech (nouns, verbs, determinants), include an LSTM and a Linear layer to define a desired output size, *and* finally train our model to create a distribution of class scores that associates each input word with a part of speech.

```python
hidden_dim = 3
input_dim = 4

input_lists = [torch.rand(1, input_dim) for _ in range(5)]
input_lists = torch.cat(input_lists).view(len(input_lists), 1, -1)

```

```python
print(input_lists.shape)
```

    torch.Size([5, 1, 4])

```python
lstm = nn.LSTM(input_size=input_dim, hidden_size=hidden_dim)

```

```python
h0 = torch.rand(1, 1, hidden_dim)
c0 = torch.rand(1, 1, hidden_dim)
```

```python
out, hidden = lstm(input_lists, (h0, c0))
```

```python
print(f"OUT: {out}")
print(f"HID: {hidden}")
```

    OUT: tensor([[[ 0.0182,  0.2954,  0.1938]],

            [[-0.0810,  0.1936,  0.1729]],

            [[-0.1733,  0.1751,  0.1016]],

            [[-0.0602,  0.0628,  0.0142]],

            [[-0.1710,  0.1106,  0.0680]]], grad_fn=<StackBackward>)
    HID: (tensor([[[-0.1710,  0.1106,  0.0680]]], grad_fn=<StackBackward>), tensor([[[-0.4062,  0.2009,  0.1184]]], grad_fn=<StackBackward>))

```python

```


# Using multimetric experiments in SigOpt to identify multiple good solutions

If you have not yet done so, please make sure you are comfortable with the content in the [intro](multimetric_intro.ipynb) notebook.

Below we create the standard SigOpt [connection](https://sigopt.com/docs/overview/python) tool.  If the `SIGOPT_API_TOKEN` is present in the environment variables, it is imported; otherwise, you need to copy and paste your key from the [API tokens page](https://sigopt.com/tokens).

```python
import os
import numpy
from time import sleep
from matplotlib import pyplot as plt
%matplotlib inline

# Matplotlib stuff for generating plots
efficient_opts = {'linewidth': 0, 'marker': '+', 'color': 'r', 'markersize': 10, 'markeredgewidth': 2}
dominated_opts = {'linewidth': 0, 'marker': '.', 'color': 'k', 'alpha': .4}

from sigopt.interface import Connection

if 'SIGOPT_API_TOKEN' in os.environ:
    SIGOPT_API_TOKEN = os.environ['SIGOPT_API_TOKEN']
else:
    SIGOPT_API_TOKEN = None

assert SIGOPT_API_TOKEN is not None
conn = Connection(client_token=SIGOPT_API_TOKEN)
```

One natural situation for a multimetric experiment is in finding a second answer to an optimization problem.  Given a function $f$ and domain $\Omega$ on which the maximum of $f$ occurs at $x^*$, the search for other points which have high values but are at least some distance away can be phrased as

\begin{align}
    \text{value}:&\quad \max_{x\in\Omega} f(x) \\
    \text{distance}:&\quad \max_{x\in\Omega} \|x - x^*\| \\
\end{align}

Here the norm is presumed to be the 2-norm, but could actually be any measurement of distance; that ambiguity is especially valuable in the situation where categorical parameters are present or a custom definition of distance is preferred.

### Finding $x^*$, the solution to the standard optimization of $f$

We start by defining a multimodal function $f$ which has multiple local optima below.  The associated meta is for a standard, single metric experiment.

```python
def multimodal_function(x1, x2):
    return (
        .5 * numpy.exp(-10 * ((x1 + .8) ** 2 + .3 * (x2 + .6) ** 2)) +
        .5 * numpy.exp(-9 * (.4 * (x1 + .7) ** 2 + .4 * (x2 - .4) ** 2)) +
        .5 * numpy.exp(-11 * (.2 * (x1 - .6) ** 2 + .5 * (x2 + .5) ** 2)) +
        .5 * numpy.exp(-11 * (.6 * (x1) ** 2 + .5 * (x2 + .8) ** 2)) +
        .5 * numpy.exp(-12 * (.4 * (x1 - .1) ** 2 + .7 * (x2 - .8) ** 2)) +
        .5 * numpy.exp(-13 * (.8 * (x1) ** 2 + .7 * (x2) ** 2)) +
        .5 * numpy.exp(-8 * (.3 * (x1 - .8) ** 2 + .6 * (x2 - .3) ** 2))
    )

multimodal_first_solution_meta = {
    'name': 'SigOpt Multimetric Demo - Single Metric Optimization (python)',
    'project': 'sigopt-examples',
    'metrics': [{'name': 'multimodal_function_value', 'objective': 'maximize'}],
    'parameters': [
        {'name': 'x1', 'bounds': {'min': -1.0, 'max': 1.0}, 'type': 'double'},
        {'name': 'x2', 'bounds': {'min': -1.0, 'max': 1.0}, 'type': 'double'},
    ],
    'type': 'offline',
    'observation_budget': 40,
}
```

We can run the initial optimization to find this $x^*$ value which is the maximum of the function.  The `sleep(2)` command helps simulate an actual experiment, where the cost of creating an observation is (significant) greater than the function we are studying here.

```python
experiment = conn.experiments().create(**multimodal_first_solution_meta)
```

```python
while experiment.progress.observation_count < experiment.observation_budget:
    suggestion = conn.experiments(experiment.id).suggestions().create()
    sleep(2)
    value = multimodal_function(**suggestion.assignments)
    conn.experiments(experiment.id).observations().create(suggestion=suggestion.id, value=value)
    experiment = conn.experiments(experiment.id).fetch()
```

The solution to this problem $x^*$ must be extracted for identifying a second solution.

```python
initial_optimization_best_assignments = conn.experiments(experiment.id).best_assignments().fetch()
x_star = initial_optimization_best_assignments.data[0].assignments
```

This contour plot shows that there is an amount of complexity in this function, and that choosing a diverse portfolio of solutions may be difficult.  The initial solution $x^*$, as determined from the optimization above, is also plotted.

```python
xplt = numpy.linspace(-1, 1, 40)
X1, X2 = numpy.meshgrid(xplt, xplt)
Y = multimodal_function(x1=X1, x2=X2)
plt.contour(X1, X2, Y)
plt.plot(x_star['x1'], x_star['x2'], '*k', markersize=20)
plt.xlabel('$x_1$', fontsize=15)
plt.ylabel('$x_2$', fontsize=15);
```

### Using the initial solution $x^*$ to search for a second solution

Now that we have this initial solution, we can define our multimetric experiment.  Note that the distance we are using is the standard 2-norm.  The multimetric function returns a list of dictionaries, one each per metric to be optimized.

```python
def distance_function(assignments, x_star):
    return numpy.sqrt((assignments['x1'] - x_star['x1']) ** 2 + (assignments['x2'] - x_star['x2']) ** 2)

def multimetric_value_distance_function(assignments, x_star):
    function_value = multimodal_function(**assignments)
    distance_from_x_star = distance_function(assignments, x_star)
    return [
        {'name': 'function value', 'value': function_value},
        {'name': 'distance from x_star', 'value': distance_from_x_star},
    ]

multimetric_second_solution_meta = {
    'name': 'SigOpt Multimetric Demo - Search for Second Solution (python)',
    'metrics': [
        {'name': 'function value', 'objective': 'maximize'},
        {'name': 'distance from x_star', 'objective': 'maximize'},
    ],
    'parameters': [
        {'name': 'x1', 'bounds': {'min': -1.0, 'max': 1.0}, 'type': 'double'},
        {'name': 'x2', 'bounds': {'min': -1.0, 'max': 1.0}, 'type': 'double'},
    ],
    'type': 'offline',
    'observation_budget': 100,
}
```

```python
experiment = conn.experiments().create(**multimetric_second_solution_meta)
```

```python
while experiment.progress.observation_count < experiment.observation_budget:
    suggestion = conn.experiments(experiment.id).suggestions().create()
    sleep(2)
    values = multimetric_value_distance_function(suggestion.assignments, x_star)
    conn.experiments(experiment.id).observations().create(suggestion=suggestion.id, values=values)
    experiment = conn.experiments(experiment.id).fetch()
```

Extracting the solution to this multimetric optimization problem yields an array of points, each of which is [Pareto efficient](https://www.sigopt.com/docs/overview/pareto_efficiency).  Plotting the metric values associated with these efficient points can help provide some guidance as to what possible solutions are available, and which might be preferred in an actual application.

Recall that metric evaluations are organized in **alphabetical order** when returned from SigOpt, which may differ from the order in which they were originally defined in the experiment.  The loop below recovers the values in the same value in which they were defined initially.

We also, again, produce some random data to explore the full feasible domain.

```python
pareto_efficient_results = conn.experiments(experiment.id).best_assignments().fetch()
efficient_points = numpy.empty((pareto_efficient_results.count, 2))
efficient_values = numpy.empty((pareto_efficient_results.count, 2))
for k, data in enumerate(pareto_efficient_results.data):
    efficient_points[k, :] = [data.assignments['x1'], data.assignments['x2']]
    dv = {d.name: d.value for d in data.values}
    efficient_values[k, :] = [dv[m['name']] for m in multimetric_second_solution_meta['metrics']]

rand_pts = numpy.random.uniform(
    [p['bounds']['min'] for p in multimetric_second_solution_meta['parameters']],
    [p['bounds']['max'] for p in multimetric_second_solution_meta['parameters']],
    (1000, 2)
)
random_values = numpy.empty((len(rand_pts), 2))
for k, pt in enumerate(rand_pts):
    random_values[k, :] = [v['value'] for v in multimetric_value_distance_function({'x1': pt[0], 'x2': pt[1]}, x_star)]
```

The graph on the left again shows the SigOpt generated Pareto frontier with the red + signs.

As we can see for the contour plot associated with this multimetric experiment on the right, multiple regions of solutions emerge which have high values but are at least some distance away.  This contour plot is only feasible for low dimensional problems -- for higher dimensional problems a different analysis of the efficient points will be necessary.

```python
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))

ax1.plot(random_values[:, 0], random_values[:, 1], **dominated_opts)
ax1.plot(efficient_values[:, 0], efficient_values[:, 1], **efficient_opts)
ax1.set_xlabel('function value', fontsize=15)
ax1.set_ylabel('distance to $x^*$', fontsize=15)

xplt = numpy.linspace(-1, 1, 40)
X1, X2 = numpy.meshgrid(xplt, xplt)
Y = multimodal_function(x1=X1, x2=X2)
ax2.contour(X1, X2, Y)
ax2.plot(x_star['x1'], x_star['x2'], '*k', markersize=20)
ax2.plot(efficient_points[:, 0], efficient_points[:, 1], **efficient_opts)
ax2.set_xlabel('$x_1$', fontsize=15)
ax2.set_ylabel('$x_2$', fontsize=15);
```

```python

```