[](https://kaggle.com/kernels/welcome?src=https://raw.githubusercontent.com/NeuromatchAcademy/course-content/master/tutorials/W0D4_Calculus/student/W0D4_Tutorial3.ipynb)

# Tutorial 3: Numerical Methods
**Week 0, Day 3: Calculus**

__Content creators:__ John S Butler, Arvind Kumar with help from Harvey McCone

__Content reviewers:__ Swapnil Kumar, Matthew McCann

__Production editors:__ Matthew McCann, Ella Batty

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

---
# Tutorial Objectives

*Estimated timing of tutorial: 70 minutes*

While a great deal of neuroscience can be described by mathematics, a great deal of the mathematics used cannot be solved exactly. This might seem very odd that you can writing something in mathematics that cannot be immediately solved but that is the beauty and mystery of mathematics. To side step this issue we use Numerical Methods to estimate the solution.

In this tutorial, we will look at the Euler method to estimate the solution of a few different differential equations: the population equation, the Leaky Integrate and Fire model and a simplified version of the Wilson-Cowan model which is a system of differential equations.

**Steps:**
- Code the Euler estimate of the Population Equation;
- Investigate the impact of time step on the error of the numerical solution;
- Code the Euler estimate of the Leaky Integrate and Fire model for a constant input;
- Visualize and listen to the response of the integrate for different inputs;
- Apply the Euler method to estimate the solution of a system of differential equations.

---
# Setup

```python
# Imports

import numpy as np
import matplotlib.pyplot as plt
```

```python
# @title Figure Settings
import IPython.display as ipd
from matplotlib import gridspec

import ipywidgets as widgets  # interactive display
from ipywidgets import Label
%config InlineBackend.figure_format = 'retina'
# use NMA plot style
plt.style.use("https://raw.githubusercontent.com/NeuromatchAcademy/course-content/master/nma.mplstyle")
my_layout = widgets.Layout()
```

```python
# @title Plotting Functions

time = np.arange(0, 1, 0.01)

def plot_slope(dt):
  """
    Args:
      dt  : time-step
    Returns:
      A figure of an exponential, the slope of the exponential and the derivative exponential
  """

  t = np.arange(0, 5+0.1/2, 0.1)

  with plt.xkcd():

    fig = plt.figure(figsize=(6, 4))
    # Exponential
    p = np.exp(0.3*t)
    plt.plot(t, p, label='y')
    # slope
    plt.plot([1, 1+dt], [np.exp(0.3*1), np.exp(0.3*(1+dt))],':og',label=r'$\frac{y(1+\Delta t)-y(1)}{\Delta t}$')
    # derivative
    plt.plot([1, 1+dt], [np.exp(0.3*1), np.exp(0.3*(1))+dt*0.3*np.exp(0.3*(1))],'-k',label=r'$\frac{dy}{dt}$')
    plt.legend()
    plt.plot(1+dt, np.exp(0.3*(1+dt)), 'og')
    plt.ylabel('y')
    plt.xlabel('t')
    plt.show()

def plot_StepEuler(dt):
  """
    Args:
      dt  : time-step
    Returns:
      A figure of one step of the Euler method for an exponential growth function
  """

  t=np.arange(0, 1 + dt + 0.1 / 2, 0.1)

  with plt.xkcd():
    fig = plt.figure(figsize=(6,4))
    p=np.exp(0.3*t)
    plt.plot(t,p)
    plt.plot([1,],[np.exp(0.3*1)],'og',label='Known')
    plt.plot([1,1+dt],[np.exp(0.3*1),np.exp(0.3*(1))+dt*0.3*np.exp(0.3*1)],':g',label=r'Euler')
    plt.plot(1+dt,np.exp(0.3*(1))+dt*0.3*np.exp(0.3*1),'or',label=r'Estimate $p_1$')
    plt.plot(1+dt,p[-1],'bo',label=r'Exact $p(t_1)$')
    plt.vlines(1+dt,np.exp(0.3*(1))+dt*0.3*np.exp(0.3*1),p[-1],colors='r', linestyles='dashed',label=r'Error $e_1$')
    plt.text(1+dt+0.1,(np.exp(0.3*(1))+dt*0.3*np.exp(0.3*1)+p[-1])/2,r'$e_1$')
    plt.legend()
    plt.ylabel('Population (millions)')
    plt.xlabel('time(years)')
    plt.show()

def visualize_population_approx(t, p):
    fig = plt.figure(figsize=(6, 4))
    plt.plot(t, np.exp(0.3*t), 'k', label='Exact Solution')

    plt.plot(t, p,':o', label='Euler Estimate')
    plt.vlines(t, p, np.exp(0.3*t),
              colors='r', linestyles='dashed', label=r'Error $e_k$')

    plt.ylabel('Population (millions)')
    plt.legend()
    plt.xlabel('Time (years)')
    plt.show()

## LIF PLOT
def plot_IF(t, V, I, Spike_time):
  """
    Args:
      t  : time
      V  : membrane Voltage
      I  : Input
      Spike_time : Spike_times
    Returns:
      figure with three panels
      top panel: Input as a function of time
      middle panel: membrane potential as a function of time
      bottom panel: Raster plot
  """

  with plt.xkcd():
    fig = plt.figure(figsize=(12,4))
    gs = gridspec.GridSpec(3, 1,  height_ratios=[1, 4, 1])
    # PLOT OF INPUT
    plt.subplot(gs[0])
    plt.ylabel(r'$I_e(nA)$')
    plt.yticks(rotation=45)
    plt.plot(t,I,'g')
    #plt.ylim((2,4))
    plt.xlim((-50,1000))
    # PLOT OF ACTIVITY
    plt.subplot(gs[1])
    plt.plot(t,V,':')
    plt.xlim((-50,1000))
    plt.ylabel(r'$V(t)$(mV)')
    # PLOT OF SPIKES
    plt.subplot(gs[2])
    plt.ylabel(r'Spike')
    plt.yticks([])
    plt.scatter(Spike_time,1*np.ones(len(Spike_time)), color="grey", marker=".")
    plt.xlim((-50,1000))
    plt.xlabel('time(ms)')
    plt.show()

def plot_rErI(t, r_E, r_I):
  """
    Args:
      t   : time
      r_E : excitation rate
      r_I : inhibition rate

    Returns:
      figure of r_I and r_E as a function of time

  """
  with plt.xkcd():
    fig = plt.figure(figsize=(6,4))
    plt.plot(t,r_E,':',color='b',label=r'$r_E$')
    plt.plot(t,r_I,':',color='r',label=r'$r_I$')
    plt.xlabel('time(ms)')
    plt.legend()
    plt.ylabel('Firing Rate (Hz)')
    plt.show()

def plot_rErI_Simple(t, r_E, r_I):
  """
    Args:
      t   : time
      r_E : excitation rate
      r_I : inhibition rate

    Returns:
      figure with two panels
      left panel: r_I and r_E as a function of time
      right panel: r_I as a function of r_E with Nullclines

  """
  with plt.xkcd():
    fig = plt.figure(figsize=(12,4))
    gs = gridspec.GridSpec(1, 2)
    # LEFT PANEL
    plt.subplot(gs[0])
    plt.plot(t,r_E,':',color='b',label=r'$r_E$')
    plt.plot(t,r_I,':',color='r',label=r'$r_I$')
    plt.xlabel('time(ms)')
    plt.legend()
    plt.ylabel('Firing Rate (Hz)')
    # RIGHT PANEL
    plt.subplot(gs[1])
    plt.plot(r_E,r_I,'k:')
    plt.plot(r_E[0],r_I[0],'go')

    plt.hlines(0,np.min(r_E),np.max(r_E),linestyles="dashed",color='b',label=r'$\frac{d}{dt}r_E=0$')
    plt.vlines(0,np.min(r_I),np.max(r_I),linestyles="dashed",color='r',label=r'$\frac{d}{dt}r_I=0$')

    plt.legend(loc='upper left')

    plt.xlabel(r'$r_E$')
    plt.ylabel(r'$r_I$')
    plt.show()

def plot_rErI_Matrix(t, r_E, r_I, Null_rE, Null_rI):
  """
    Args:
      t   : time
      r_E : excitation firing rate
      r_I : inhibition firing rate
      Null_rE: Nullclines excitation firing rate
      Null_rI: Nullclines inhibition firing rate
    Returns:
      figure with two panels
      left panel: r_I and r_E as a function of time
      right panel: r_I as a function of r_E with Nullclines

  """

  with plt.xkcd():
    fig = plt.figure(figsize=(12,4))
    gs = gridspec.GridSpec(1, 2)
    plt.subplot(gs[0])
    plt.plot(t,r_E,':',color='b',label=r'$r_E$')
    plt.plot(t,r_I,':',color='r',label=r'$r_I$')
    plt.xlabel('time(ms)')
    plt.ylabel('Firing Rate (Hz)')
    plt.legend()
    plt.subplot(gs[1])
    plt.plot(r_E,r_I,'k:')
    plt.plot(r_E[0],r_I[0],'go')

    plt.plot(r_E,Null_rE,':',color='b',label=r'$\frac{d}{dt}r_E=0$')
    plt.plot(r_E,Null_rI,':',color='r',label=r'$\frac{d}{dt}r_I=0$')
    plt.legend(loc='best')
    plt.xlabel(r'$r_E$')
    plt.ylabel(r'$r_I$')
    plt.show()
```

---
# Section 1: Intro to the Euler method using the population differential equation

```python
# @title Video 1: Intro to numerical methods for differential equations
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="BV1gh411Y7gV", 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="NI8c80TA7IQ", 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)
```

## Section 1.1: Slope of line as approximation of derivative

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

 Click here for text recap of relevant part of video

The Euler method is one of the straight forward and elegant methods to approximate a differential. It was designed by [Leonhard Euler](https://en.wikipedia.org/wiki/Leonhard_Euler) (1707-1783).
Simply put we just replace the derivative in the differential equation by the formula for a line and re-arrange.

The slope is the rate of change between two points. The formula for the slope of a line between the points $(t_0,y(t_0))$ and $(t_1,y(t_1))$ is given by:
$$ m=\frac{y(t_1)-y(t_0)}{t_1-t_0}, $$
which can be written as
$$ m=\frac{y_1-y_0}{t_1-t_0}, $$
or as
$$ m=\frac{\Delta y_0}{\Delta t_0}, $$
where $\Delta y_0=y_1-y_0$ and $\Delta t_0=t_1-t_0$ or in words as
$$ m=\frac{\text{ Change in y} }{\text{Change in t}}. $$
The slope can be used as an approximation of the derivative such that
$$ \frac{d}{dt}y(t)\approx \frac{y(t_0+\Delta t)-y(t_0)}{t_0+\Delta t-t_0}=\frac{y(t_0+dt)-y(t_0)}{\Delta t}$$
where $\Delta t$ is a time-step.

### Interactive Demo 1.1: Slope of a Line
The plot below shows a function $y(t)$ in blue, its exact derivative $ \frac{d}{dt}y(t)$ at $t_0=1$ in black, and the approximate derivative calculated using the slope formula $=\frac{y(1+\Delta t)-y(1)}{\Delta t}$ for different time-steps sizes $\Delta t$ in green.

Interact with the widget to see how time-step impacts the accuracy of the slope which is the estimate of the derivative.

- How does the size of $\Delta t$ affect the approximation?

```python
# @markdown Make sure you execute this cell to enable the widget!
my_layout.width = '450px'
@widgets.interact(

    dt=widgets.FloatSlider(1, min=0., max=4., step=.1,
                                layout=my_layout)

)

def Pop_widget(dt):
  plot_slope(dt)
  plt.show()
```

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

## Section 1.2: Euler error for a single step

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

 Click here for text recap of relevant part of video

Linking with the previous tutorial, we will use the population differential equation to get an intuitive feel of using the Euler method to approximate the solution of a differential equation, as it has an exact solution with no discontinuities.

The population differential equation is given by
\begin{align*}
\\
\frac{d}{dt}\,p(t) &= \alpha p(t)\\\\
p(0)&=p_0 \quad \text{Initial Condition}
\end{align*}

where $p(t)$ is the population at time $t$ and $\alpha$ is a parameter representing birth rate. The exact solution of the differential equation is
$$ p(t)=p_0e^{\alpha t}.$$

To numerically estimate the population differential equation we replace the derivate with the slope of the line to get the discrete (not continuous) equation:

\begin{align*}
\\
\frac{p_1-p_0}{t_1-t_0} &= \alpha p_0\\\\
p(0)&=p_0 \quad \text{Initial Condition}
\end{align*}

where $p_1$ is the estimate of $p(t_1)$. Let $\Delta t=t_1-t_0$ be the time-step and re-arrange the equation gives

\begin{align*}
\color{red}{p_1}&=\color{green}{p_0}+\color{blue}{\Delta t} (\color{blue}{\alpha} \color{green}{p_0})
\end{align*}

where $\color{red}{p_1}$ is the unknown future, $\color{green}{p_0}$ is the known current population, $\color{blue}{\Delta t}$ is the chosen time-step parameter and $\color{blue}{\alpha}$ is the given birth rate parameter.
Another way to read the re-arranged discrete equation is:

\begin{align*}
\color{red}{\text{"Future Population"}}&=\color{green}{\text{ "Current Population"}}+\color{blue}{\text{ time-step}} \times (\color{blue}{\text{ "Birth rate}}\times  \color{green}{\text{ Current Population"}}).
\end{align*}

So pretty much, we can use the current population and add a bit of the dynamics of the differential equation to predict the future. We will make millions... But wait there is always an error.

The solution of the Euler method $p_1$ is an estimate of the exact solution $p(t_1)$ at $t_1$ which means there is a bit of error $e_1$ which gives the equation
\begin{align*}
p(t_1)&=p_1+e_1\\\\
\text{Rearranging}\\\\
e_1&=p(t_1)-p_1,\\
\text{Error}&=\text{Exact-Estimate}.
\end{align*}

Most of the time we do not know the exact answer $p(t_1)$ and hence the size of the error $e_1$ but for the population equation we have the exact solution $ p(t)=p_0e^{\alpha}.$

This means we can explore what the error looks like.

### Interactive Demo 1.2: Euler error for a single step
Interact with the widget to see how the time-step $\Delta t$, dt in code, impacts the estimate $p_1$ and the error $e_1$ of the Euler method.

1. What happens to the estimate $p_1$ as the time-step $\Delta t$ increases?

2. Is there a relationship between the size of $\Delta t$ and $e_1$?

3. How would you improve the error $e_1$?

```python
# @markdown Make sure you execute this cell to enable the widget!
my_layout.width = '450px'
@widgets.interact(
    dt=widgets.FloatSlider(1.5, min=0., max=4., step=.1,
                                layout=my_layout)
)

def Pop_widget(dt):
  plot_StepEuler(dt)
  plt.show()
```

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

The error $e_1$ from one time-step is known as the __local error__. For the Euler method the local error is linear, which means that the error decreases linearly as a function of timestep and is written as $O(\Delta t)$.

## Section 1.3: Taking more steps

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

```python
# @title Video 2: Taking more steps
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="BV135411T7QF", 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="cGsXHllGMVo", 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)
```

 Click here for text recap of relevant part of video

In the above exercise we saw that by increasing the time-step $\Delta t$ the error between the estimate $p_1$ and the exact $p(t_1)$ increased.  The largest error in the example above was for $\Delta t=4$, meaning the first time point was at 1 year and the second was at 5 years (as 5 - 1 = 4).

To decrease the error, we can divide the interval $[1, 5]$ into more steps using a smaller $\Delta t$.

In the plot below we use $\Delta t=1$, which divides the interval into four segments
$$n=\frac{5-1}{1}=4,$$
giving
$$ t_0=t[0]=1, \ t_1=t[1]=2, \ t_2=t[2]=3, \ t_3=t[3]=4 \ \text{ and } t_4=t[4]=5. $$
This can be written as
$$ t[k]=1+k\times1, \text{ for } k=0,\cdots 4, $$
and more generally as
$$ t[k]=t[k]+k\times \Delta t, \text{ for } k=0,\cdots n, $$
where $n$ is the number of steps.

Using the Euler method, the continuous population differential equation is written as a series of discrete difference equations of the form:
\begin{align*}
\color{red}{p[k+1]}&=\color{green}{p[k]}+\color{blue}{\Delta t} (\color{blue}{\alpha} \color{green}{p[k]})\\
&\text{for } k=0,1,\cdots n-1
\end{align*}
where $\color{red}{p[k+1]}$ is the unknown estimate of the future population at $t[k+1]$, $\color{green}{p[k]}$ is the known current population estimate at $t_k$, $\color{blue}{\Delta t}$ is the chosen time-step parameter and $\color{blue}{\alpha}$ is the given birth rate parameter.

The Euler method can be applied to all first order differential equations of the form
\begin{align*}
\frac{d}{dt}y(t)&=f(t,y(t)),\\
y(t_{0})&=y_0,\\
\end{align*}
on an interval $[a,b]$.

Using the Euler method all differential equation can be written as discrete difference equations:
\begin{align*}
\frac{\color{red}{y[k+1]}-\color{green}{y[k]}}{\color{blue}{\Delta t}}&=f(\color{blue}{t[k]},\color{green}{y[k]}),\\
\text{Re-arranging}\\
\color{red}{y[k+1]}&=\color{green}{y[k]}+\color{blue}{\Delta t}f(\color{blue}{t[k]},\color{green}{y[k]}),\\
&\text{ for } k=0, \cdots n-1,\\
y[0]&=\color{green}{y_0},\\
\end{align*}
where  $\color{red}{y[k+1]}$ is the unknown estimate at $t[k+1]$, $\color{green}{y[k]}$ is the known at $t_k$, $\color{blue}{\Delta t}$ is the chosen time-step parameter, $\color{blue}{t[k]}$ is the time point and $f$ is the right hand side of the differential equation.
The discrete time steps are:
\begin{align*}
\color{blue}{t[k]}&=\color{blue}{t[0]}+\color{blue}{k}\color{blue}{\Delta t},\\
n&=\frac{b-a}{\Delta t}\\
&\text{ for } k=0, \cdots n.\\
\end{align*}
Once again this can be simply put into words:
\begin{align*}
\color{red}{\text{ "Future" }}&=\color{green}{\text{ "Current Info" }}+\color{blue}{\text{ time-step } }\times\text{ "Dynamics of the system which is a function of } \color{blue}{ \text{ time }} \text{ and }\color{green}{ \text{ Current Info." }}
\end{align*}

```python
# @markdown *Execute this cell to visualize time steps*
dt  =1
t  =np.arange(1, 5+dt/2, dt) # Time from 1 to 5 years in 0.1 steps
with plt.xkcd():
    fig = plt.figure(figsize=(8, 2))
    plt.plot(t, 0*t, ':o')
    plt.plot(t[0] ,0*t[0],':go',label='Initial Condition')
    plt.text(t[0]-0.1, 0*t[0]-0.03, r'$t_0$')
    plt.text(t[1]-0.1, 0*t[0]-0.03, r'$t_1$')
    plt.text(t[2]-0.1, 0*t[0]-0.03, r'$t_2$')
    plt.text(t[3]-0.1, 0*t[0]-0.03, r'$t_3$')
    plt.text(t[4]-0.1, 0*t[0]-0.03,r'$t_4$')
    plt.text(t[0]+0.5, 0*t[0]+0.02, r'$\Delta t$')
    plt.text(t[1]+0.5, 0*t[0]+0.02, r'$\Delta t$')
    plt.text(t[2]+0.5, 0*t[0]+0.02, r'$\Delta t$')
    plt.text(t[3]+0.5, 0*t[0]+0.02, r'$\Delta t$')
    plt.yticks([])#plt.ylabel('Population (millions)')
    plt.xlabel('time(years)')
    plt.show()
```

### Coding Exercise 1.3: Step, step, step
Given the population differential equation:
\begin{align*}
\\
\frac{d}{dt}\,p(t) &= 0.3 p(t),\\
\end{align*}
and the initial condition:
\begin{align*}
\\
p(t_0=1)&=e^{0.3},
\end{align*}

code the difference equation:

\begin{align*}
\color{red}{p[k+1]}&=\color{green}{p[k]}+\color{blue}{\Delta t} (\color{blue}{0.3} \color{green}{p[k]}),\\
\color{green}{p[0]}&=e^{0.3},\quad \text{Initial Condition,}\\
&\text{for } k=0,1,\cdots 4,\\
\end{align*}

to estimate the population on the interval $[1,5]$ with a time-step $\Delta t=1$, denoted by `dt` in code.

```python
# Time step
dt = 1

# Make time range from 1 to 5 years with step size dt
t = np.arange(1, 5+dt/2, dt)

# Get number of steps
n = len(t)

# Initialize p array
p = np.zeros(n)
p[0] = np.exp(0.3*t[0]) # initial condition

# Loop over steps
for k in range(n-1):

  ########################################################################
  ## TODO for students
  ## Complete line of code and remove
  raise NotImplementedError("Student exercise: calculate the population step for each time point")
  ########################################################################

  # Calculate the population step
  p[k+1] = ...

# Visualize
visualize_population_approx(t, p)
```

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

*Example output:*

The error is smaller for 4 time-steps than taking one large time step from 1 to 5 but do note that the error is increasing for each step. This is known as __global error__ so the futher in time you want to predict, the larger the error.
You can read the theorems [here.](https://colab.research.google.com/github/john-s-butler-dit/Numerical-Analysis-Python/blob/master/Chapter%2001%20-%20Euler%20Methods/102_Euler_method_with_Theorems_nonlinear_Growth_function.ipynb)

---
# Section 2: Euler method for the leaky integrate and fire

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

```python
# @title Video 3: Leaky integrate and fire
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="BV1k5411T7by", 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="T85OcgY7xjo", 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)
```

 Click here for text recap of video

The Leaky Integrate and Fire (LIF) differential equation is:

\begin{align}
\frac{dV}{dt} = \frac{-(V-E_L) + R_mI(t)}{\tau_m}\,
\end{align}

where $\tau_m$ is the time constant, $V$ is the membrane potential, $E_L$ is the resting potential, $R_m$ is leak resistance and $I(t)$ is the external input current.

The solution of the LIF can be estimated by applying the Euler method to give the difference equation:

\begin{align}
\frac{\color{red}{V[k+1]}-\color{green}{V[k]}}{\color{blue}{\Delta t}}=\big(\frac{-(\color{green}{V[k]}-\color{blue}{E_L}) + \color{blue}{R_m}I[k]}{\color{blue}{\tau_m}}\big),\\
\end{align}
where $V[k]$ is the estimate of the membrane potential at time point $t[k]$.
Re-arranging the equation such that all the known terms are on the right gives:

\begin{align}
\color{red}{V[k+1]}=\color{green}{V[k]}+\color{blue}{\Delta t}\big(\frac{-(\color{green}{V[k]}-\color{blue}{E_L}) + \color{blue}{R_m}I[k]}{\color{blue}{\tau_m}}\big),\\
\text{for } k=0\cdots n-1,
\end{align}

where $\color{red}{V[k+1]}$ is the unknown membrane potential at $t[k+1]$, $\color{green}{V[k]} $ is known membrane potential, $\color{blue}{E_L}$, $\color{blue}{R_m}$ and $\color{blue}{\tau_m}$ are known parameters, $\color{blue}{\Delta t}$ is a chosen time-step and  $I(t_k)$ is a function for an external input current.

## Coding Exercise 2: LIF and Euler
Code the difference equation for the LIF:

\begin{align}
\color{red}{V[k+1]}=\color{green}{V[k]}+\color{blue}{\Delta t}\big(\frac{-(\color{green}{V[k]}-\color{blue}{E_L}) + \color{blue}{R_m}I[k]}{\color{blue}{\tau_m}}\big),\\
\text{for } k=0\cdots n-1,
\end{align}

with the given parameters set as:
* `V_reset = -75,`
* `E_L = -75,`
* `tau_m = 10,`
* `R_m = 10.`

We will then visualize the result.
The figure has three panels:
* the top panel is the sinusoidal input, $I$,
* the middle panel is the estimate membrane potential $V_k$. To illustrate a spike, $V_k$ is set to $0$ and then reset,
* the bottom panel is the raster plot with each dot indicating a spike.

```python
def Euler_Integrate_and_Fire(I, time, dt):
  """
  Args:
    I: Input
    time: time
    dt: time-step
  Returns:
    Spike: Spike count
    Spike_time: Spike times
    V: membrane potential esitmated by the Euler method
  """

  Spike = 0
  tau_m = 10
  R_m = 10
  t_isi = 0
  V_reset = E_L = -75
  n = len(time)
  V = V_reset * np.ones(n)
  V_th = -50
  Spike_time = []

  for k in range(n-1):
    #######################################################################
    ## TODO for students: calculate the estimate solution of V at t[i+1]
    ## Complete line of codes for dV and remove
    ## Run the code in Section 5.1 or 5.2 to see the output!
    raise NotImplementedError("Student exercise: calculate the estimate solution of V at t[i+1]")
    ########################################################################

    dV = ...
    V[k+1] = V[k] + dt*dV

    # Discontinuity for Spike
    if V[k] > V_th:
      V[k] = 0
      V[k+1] = V_reset
      t_isi = time[k]
      Spike = Spike + 1
      Spike_time = np.append(Spike_time, time[k])

  return Spike, Spike_time, V

# Set up time step and current
dt = 1
t = np.arange(0, 1000, dt)
I = np.sin(4 * 2 * np.pi * t/1000) + 2

# Model integrate and fire neuron
Spike, Spike_time, V = Euler_Integrate_and_Fire(I, t, dt)

# Visualize
plot_IF(t, V,I,Spike_time)
```

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

*Example output:*

As electrophysiologists normally listen to spikes when conducting experiments,  listen to the music of the LIF neuron below. Note: this does not work on all browsers so just move on if you can't hear the audio.

```python
# @markdown *Execute this cell to visualize the LIF for sinusoidal input*

dt = 1
t = np.arange(0, 1000, dt)
I = np.sin(4 * 2 * np.pi * t/1000) + 2
Spike, Spike_time, V = Euler_Integrate_and_Fire(I, t, dt)

plot_IF(t, V,I,Spike_time)
plt.show()
ipd.Audio(V, rate=1000/dt)
```

---
# Section 3: Systems of differential equations

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

```python
# @title Video 4: Systems of differential equations
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="BV1XV411s76a", 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="A3Puozl9nEs", 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)
```

## Section 3.1: Using Euler to approximate a simple system

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

 Click here for text recap of relevant part of video

To get to grips with solving a system of differential equations using the Euler method, we will simplify the Wilson Cowan model, a set of equations that will be explored in more detail in the Dynamical Systems day.
Looking at systems of differential equations will also allow us to introduce the concept of a phase-plane plot which is a method of investigating how different processes interact.

In the previous section we looked at the LIF model for a single neuron. We now model a collection of neurons using a differential equation which describes the firing rate of a population of neurons.
We will model the firing rate $r$ of two types of populations of neurons which interact, the excitation population firing rate $r_E$ and inhibition population firing rate $r_I$. These firing rates of neurons regulate each other by weighted connections $w$. The directed graph below illustrates this.

Our system of differential equations is a linear version of the Wilson Cowan model. Consider the equations,

\begin{align}
\tau_E \frac{dr_E}{dt} &= w_{EE}r_E+w_{EI}r_I, \\
\tau_I \frac{dr_I}{dt} &= w_{IE}r_E+w_{II}r_I
\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.

To start we will further simplify the linear system of equations by setting $w_{EE}$ and $w_{II}$ to zero, we now have the equations

\begin{align}
\tau_E \frac{dr_E}{dt} &= w_{EI}r_I, \\
\tau_I \frac{dr_I}{dt} &= w_{IE}r_E, \qquad (1)
\end{align}

where $\tau_E=100$ and $\tau_I=120$, no internal connection $w_{EE}=w_{II}=0$, and $w_{EI}=-1$ and $w_{IE}=1$,
with the initial conditions

\begin{align}
r_E(0)=30, \\
r_I(0)=20.
\end{align}

The solution can be approximated using the Euler method such that we have the difference equations:

\begin{align*}
\frac{\color{red}{r_E[k+1]}-\color{green}{r_E[k]}}{\color{blue}{\Delta t}}&=\big(\frac{\color{blue}{w_{EI}}\color{green}{r_I[k]}}{\color{blue}{\tau_E}}\big),\\
\frac{\color{red}{r_I[k+1]}-\color{green}{r_I[k]}}{\color{blue}{\Delta t}}&=\big(\frac{\color{blue}{w_{IE}}\color{green}{r_E[k]}}{\color{blue}{\tau_I}}\big),\\
\end{align*}
where $r_E[k]$ and $r_I[k]$ are the numerical estimates of the firing rate of the excitation population $r_E(t[k])$ and inhibition population $r_I(t[k])$ and $\Delta t$ is the time-step.

Re-arranging the equation such that all the known terms are on the right gives:

\begin{align*}
\color{red}{r_E[k+1]}&=\color{green}{r_E[k]}+\color{blue}{\Delta t}\big(\frac{\color{blue}{w_{EI}}\color{green}{r_I[k]}}{\color{blue}{\tau_E}}\big),\\
\color{red}{r_I[k+1]}&=\color{green}{r_I[k]}+\color{blue}{\Delta t}\big(\frac{\color{blue}{w_{IE}}\color{green}{r_E[k]}}{\color{blue}{\tau_I}}\big),\\
&\text{ for  } k=0, \cdots , n-1,\\\\
r_E[0]&=30,\\
r_I[0]&=20.\\
\end{align*}

where $\color{red}{r_E[k+1]}$ and  $\color{red}{r_I[k+1]}$ are unknown, $\color{green}{r_E[k]} $ and  $\color{green}{r_I[k]} $ are known, $\color{blue}{w_{EI}}=-1$, $\color{blue}{w_{IE}}=1$ and $\color{blue}{\tau_E}=100ms$ and $\color{blue}{\tau_I}=120ms$  are known parameters and $\color{blue}{\Delta t}=0.01ms$ is a chosen time-step.

### Coding Exercise 3.1: Euler on a Simple System

Our difference equations are:

\begin{align*}
\color{red}{r_E[k+1]}&=\color{green}{r_E[k]}+\color{blue}{\Delta t}\big(\frac{\color{blue}{w_{EI}}\color{green}{r_I[k]}}{\color{blue}{\tau_E}}\big),\\
\color{red}{r_I[k+1]}&=\color{green}{r_I[k]}+\color{blue}{\Delta t}\big(\frac{\color{blue}{w_{IE}}\color{green}{r_E[k]}}{\color{blue}{\tau_I}}\big),\\
&\text{ for  } k=0, \cdots , n-1,\\\\
r_E[0]&=30,\\
r_I[0]&=20.\\
\end{align*}

where $\color{red}{r_E[k+1]}$ and  $\color{red}{r_I[k+1]}$ are unknown, $\color{green}{r_E[k]} $ and  $\color{green}{r_I[k]} $ are known, $\color{blue}{w_{EI}}=-1$, $\color{blue}{w_{IE}}=1$ and $\color{blue}{\tau_E}=100ms$ and $\color{blue}{\tau_I}=120ms$  are known parameters and $\color{blue}{\Delta t}=0.01ms$ is a chosen time-step.
__Code the difference equations to estimate $r_{E}$ and $r_{I}$.__

Note that the equations have to estimated in the same `for` loop as they depend on each other.

```python
def Euler_Simple_Linear_System(t, dt):
  """
  Args:
    time: time
  dt  : time-step
  Returns:
    r_E : Excitation Firing Rate
    r_I : Inhibition Firing Rate

  """

  # Set up parameters
  tau_E = 100
  tau_I = 120
  n = len(t)
  r_I = np.zeros(n)
  r_I[0] = 20
  r_E = np.zeros(n)
  r_E[0] = 30

  #######################################################################
  ## TODO for students: calculate the estimate solutions of r_E and r_I at t[i+1]
  ## Complete line of codes for dr_E and dr_I and remove
  raise NotImplementedError("Student exercise: calculate the estimate solutions of r_E and r_I at t[i+1]")
  ########################################################################

  # Loop over time steps
  for k in range(n-1):

    # Estimate r_e
    dr_E = ...
    r_E[k+1] = r_E[k] + dt*dr_E

    # Estimate r_i
    dr_I = ...
    r_I[k+1] = r_I[k] + dt*dr_I

  return r_E, r_I

# Set up dt, t
dt = 0.1 # time-step
t = np.arange(0, 1000, dt)

# Run Euler method
r_E, r_I = Euler_Simple_Linear_System(t, dt)

# Visualize
plot_rErI(t, r_E, r_I)
```

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

*Example output:*

### Think! 3.1: Simple Euler solution to the Wilson Cowan model

1. Is the simulation biologically plausible?
2. What is the effect of combined excitation and inhibition?

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

## Section 3.2: Phase Plane Plot and Nullcline

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

 Click here for text recap of relevant part of video

When there are two differential equations describing the interaction of two processes like excitation $r_{E}$ and inhibition $r_{I}$ that are  dependent on each other they can be plotted as a function of each other, which is known as a phase plane plot. The phase plane plot can give insight give insight into the state of the system but more about that later in Neuromatch Academy.

In the animated figure below, the panel of the left shows the excitation firing rate $r_E$ and the inhibition firing rate $r_I$ as a function of time. The panel on the right hand side is the phase plane plot which shows the inhibition firing rate $r_I$ as a function of excitation firing rate $r_E$.

An addition to the phase plane plot are the "nullcline". These lines plot when the rate of change $\frac{d}{dt}$ of the variables is equal to $0$. We saw a variation of this for a single differential equation in the differential equation tutorial.

As we have two differential equations we set $\frac{dr_E}{dt}=0$ and $\frac{dr_I}{dt}=0$ which gives the equations,

\begin{align}
0&= w_{EI}r_I, \\
0&= w_{IE}r_E,\\
\end{align}

these are a unique example as they are a vertical and horizontal line. Where the lines cross is the stable point which the $r_E(t)$ excitatory population and $r_I(t)$ the inhibitory population orbit around.

### Think! 6.1: Discuss the Plots

1. Which representation is more intuitive (and useful), the time plot or the phase plane plot?
2. Why do we only see one circle?
3. What do the quadrants represent?

```python
# @markdown Execute the code to plot the solution to the system
plot_rErI_Simple(t, r_E, r_I)
```

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

## Section 3.3: Adding internal connections

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

Building up the equations in the previous section we re-introduce internal connections $w_{EE}$, $w_{II}$. The two coupled differential equations, each representing the dynamics of the excitatory or inhibitory population are now:

\begin{align}
\tau_E \frac{dr_E}{dt} &=w_{EE}r_E +w_{EI}r_I, \\
\tau_I \frac{dr_I}{dt} &=w_{IE}r_E +w_{II}r_I ,
\end{align}

where $\tau_E=100$ and $\tau_I=120$, $w_{EE}=1$ and $w_{II}=-1$, and $w_{EI}=-5$ and $w_{IE}=0.6$,
with the initial conditions

\begin{align}
r_E(0)=30, \\
r_I(0)=20.
\end{align}

The solutions can be approximated using the Euler method such that the equations become:

\begin{align*}
\frac{\color{red}{rE_{k+1}}-\color{green}{rE_k}}{\color{blue}{\Delta t}}&=\big(\frac{\color{blue}{w_{EE}}\color{green}{rE_k}+\color{blue}{w_{EI}}\color{green}{rI_k}}{\color{blue}{\tau_E}}\big),\\
\frac{\color{red}{rI_{k+1}}-\color{green}{rI_k}}{\color{blue}{\Delta t}}&=\big(\frac{\color{blue}{w_{II}}\color{green}{rI_k}+\color{blue}{w_{IE}}\color{green}{rE_k}}{\color{blue}{\tau_I}}\big),\\
\end{align*}
where $r_E[k]$ and $r_I[k]$ are the numerical estimates of the firing rate of the excitation population $r_E(t_k)$ and inhibition population $r_I(t_K)$ and $\Delta t$ is the time-step.

Re-arranging the equation such that all the known terms are on the right gives:

\begin{align*}
\color{red}{rE_{k+1}}&=\color{green}{rE_k}+\color{blue}{\Delta t}\big(\frac{\color{blue}{w_{EE}}\color{green}{rE_k}+\color{blue}{w_{EI}}\color{green}{rI_k}}{\color{blue}{\tau_E}}\big),\\
\color{red}{rI_{k+1}}&=\color{green}{rI_k}+\color{blue}{\Delta t}\big(\frac{\color{blue}{w_{II}}\color{green}{rI_k}+\color{blue}{w_{IE}}\color{green}{rE_k}}{\color{blue}{\tau_I}}\big),\\
&\text{ for } k=0, \cdots n-1,\\\\
rE_0&=30,\\
rI_0&=20.\\
\end{align*}
where $\color{red}{rE_{k+1}}$ and  $\color{red}{rI_{k+1}}$ are unknown, $\color{green}{rE_{k}} $ and  $\color{green}{rI_{k}} $ are known, $\color{blue}{w_{EI}}=-1$, $\color{blue}{w_{IE}}=1$, $\color{blue}{w_{EE}}=1$,  $\color{blue}{w_{II}}=-1$ and $\color{blue}{\tau_E}=100$ and $\color{blue}{\tau_I}=120$  are known parameters and $\color{blue}{\Delta t}=0.1$ is a chosen time-step.

### Think! 3.3: Oscillations

The code below implements and visualizes the linear Wilson-Cowan model.

1. What will happen to the oscillations if the time period is extended?
2. How would you control or predict the oscillations?

```python
# @markdown *Execute this cell to visualize the Linear Willson-Cowan*

def Euler_Linear_System_Matrix(t, dt, w_EE=1):
  """
  Args:
    time: time
    dt: time-step
    w_EE: Excitation to excitation weight
  Returns:
    r_E: Excitation Firing Rate
    r_I: Inhibition Firing Rate
    N_Er: Nullclines for drE/dt=0
    N_Ir: Nullclines for drI/dt=0
  """

  tau_E = 100
  tau_I = 120
  n = len(t)
  r_I = 20*np.ones(n)
  r_E = 30*np.ones(n)
  w_EI = -5
  w_IE = 0.6
  w_II = -1

  for k in range(n-1):

    # Calculate the derivatives of the E and I populations
    drE = (w_EI*r_I[k] + w_EE*r_E[k]) / tau_E
    r_E[k+1] = r_E[k] + dt*drE

    drI = (w_II*r_I[k] + w_IE*r_E[k]) / tau_I
    r_I[k+1] = r_I[k] + dt*drI

  N_Er = -w_EE / w_EI*r_E
  N_Ir = -w_IE / w_II*r_E

  return r_E, r_I, N_Er, N_Ir

dt = 0.1 # time-step
t = np.arange(0, 1000, dt)
r_E, r_I, _, _ = Euler_Linear_System_Matrix(t, dt)
plot_rErI(t, r_E, r_I)
```

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

## Section 3.4 Phase Plane and Nullclines Part 2

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

Like before, we have two differential equations so we can plot the results on a phase plane. We can also calculate the Nullclines when we set $\frac{dr_E}{dt}=0$ and $\frac{dr_I}{dt}=0$ which gives,
\begin{align}
0&= w_{EE}r_E+w_{EI}r_I, \\
0&= w_{IE}r_E+w_{II}r_I,
\end{align}
re-arranging as two lines
\begin{align}
r_I&= -\frac{w_{EE}}{w_{EI}}r_E, \\
r_I&= -\frac{w_{IE}}{w_{II}}r_E,
\end{align}
which crosses at the stable point.

The panel on the left shows excitation firing rate $r_E$ and inhibition firing rate  $r_I$ as a function of time. On the right side the phase plane plot shows inhibition firing rate $r_I$ as a function of excitation firing rate $r_E$ with the Nullclines for $\frac{dr_E}{dt}=0$ and $\frac{dr_I}{dt}=0.$

```python
# @markdown *Run this cell to visualize the phase plane*
dt = 0.1 # time-step
t = np.arange(0, 1000, dt)
r_E, r_I, N_Er, N_Ir = Euler_Linear_System_Matrix(t, dt)
plot_rErI_Matrix(t, r_E, r_I, N_Er, N_Ir)
```

### Interactive Demo 3.4: A small change changes everything
We will illustrate that even changing one parameter in a system of differential equations can have a large impact on the solutions of the excitation firing rate $r_E$ and inhibition firing rate $r_I$.
Interact with the widget below to change the size of $w_{EE}$.

Take note of:
1. How the solution changes for positive and negative of $w_{EE}$. Pay close attention to the axis.

2. How would you maintain a stable oscillation.

```python
# @markdown Make sure you execute this cell to enable the widget!
my_layout.width = '450px'
@widgets.interact(
    w_EE=widgets.FloatSlider(1, min=-1., max=2., step=.1,
                                layout=my_layout)
)

def Pop_widget(w_EE):
  dt = 0.1 # time-step
  t = np.arange(0,1000,dt)
  r_E, r_I, N_Er, N_Ir = Euler_Linear_System_Matrix(t, dt, w_EE)
  plot_rErI_Matrix(t, r_E, r_I, N_Er, N_Ir)
  plt.show()
```

---
# Summary

*Estimated timing of tutorial: 70 minutes*

```python
# @title Video 5: Summary
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="BV1wM4y1g78M", 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="5UmGgboSc40", 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)
```

Using pretty much the formula for the slope of a line, the solution of differential equation can be estimated with reasonable accuracy.
This will be much more relevant when dealing with more complicated (non-linear) differential equations where there is no known exact solution.

---
# Links to Neuromatch Days

The Euler method is used to estimate differential equations in a number of different Neuromatch days:
* The LIF model is discussed in more details in Model Types (Week 1 Day 1) and  Biological Neuron Models (Week 2 Day 3).
* Drift Diffusion model which is a differential equation for decision making is discussed in Linear Systems (Week 2 Day 2).
* Phase-plane plots are discussed in Linear Systems (Week 2 Day 2) and Dynamic Networks (Week 2 Day 4).
* The Wilson-Cowan model is discussed in Dynamic Networks (Week 2 Day 4).

---
# References
1. Lotka, A. L, (1920)   Analytical note on certain rhythmic  relations inorganic  systems.Proceedings of the National Academy of Sciences,6(7):410–415,1920.

2. Brunel N, van Rossum MC. Lapicque's 1907 paper: from frogs to integrate-and-fire. Biol Cybern. 2007 Dec;97(5-6):337-9. doi: 10.1007/s00422-007-0190-0. Epub 2007 Oct 30. PMID: 17968583.

# Bibliography
1. Dayan, P., & Abbott, L. F. (2001). Theoretical neuroscience: computational and mathematical modeling of neural systems. Computational Neuroscience Series.
2. Strogatz, S. Nonlinear dynamics and chaos: with applications to physics, biology, chemistry, and engineering (studies in nonlinearity), Westview Press; 2 edition (29 July 2014)

## Supplemental Popular Reading List
1. Lindsay, G. (2021). Models of the Mind: How Physics, Engineering and Mathematics Have Shaped Our Understanding of the Brain. Bloomsbury Publishing.
2. Strogatz, S. (2004). Sync: The emerging science of spontaneous order. Penguin UK.

## Popular Podcast
1. Strogatz, S. (Host). (2020-), Joy of X  https://www.quantamagazine.org/tag/the-joy-of-x/ Quanta Magazine

---
# Bonus

---
## Bonus Section 1: The 4th Order Runge Kutta
Another popular numerical method to estimate the solution of differential equations of the general form:
$$ \frac{d}{dt}y=f(t,y)$$
is the 4th Order Runge Kutta:
$$k_1=f(t_k,y_k),$$
$$k_2=f(t_k+\frac{\Delta t}{2},y_k+\frac{\Delta t}{2}k_1),$$
$$k_3=f(t_k+\frac{\Delta t}{2},y_k+\frac{\Delta t}{2}k_2),$$
$$k_4=f(t_k+\Delta t,y_k+\Delta tk_3),$$
$$y_{k+1}=y_{k}+\frac{\Delta t}{6}(k_1+2k_2+2k_3+k_4),$$
for $k=0,1,\cdots,n-1$,
which is more accurate than the Euler method.

Given the population differential equation
\begin{align*}
\\
\frac{d}{dt}\,p(t) &= 0.3 p(t),\\
p(t_0=1)&=e^{0.3}\quad \text{Initial Condition},
\end{align*}
the 4th Runge Kutta difference equation is
\begin{align*}
k_1&=0.3\color{green}{p[k]},\\
k_2&=0.3(\color{green}{p[k]}+\frac{\Delta t}{2}k_1),\\
k_3&=0.3(\color{green}{p[k]}+\frac{\Delta t}{2}k_2),\\
k_4&=0.3(\color{green}{p[k]}+k_3),\\
\color{red}{p_{k[k]}}&=\color{green}{p[k]}+\frac{\color{blue}{\Delta t}}{6}( \color{green}{k_1+2k_2+2k_3+k_4})\\
&\text{for } k=0,1,\cdots 4
\end{align*}
to estimate the population for $\Delta t=0.5$.

The code is implemented below. Note how much more accurate the 4th Order Runge Kutta (yellow) is compared to the Euler Method (blue). The 4th order Runge Kutta is of order 4 which means that if you half the time-step $\Delta t$ the error decreases by a factor of $\Delta t^4$.

```python
# @markdown *Execute this cell to show the difference between the Runge Kutta Method and the Euler Method*

def RK4(dt=0.5):
  t=np.arange(1, 5+dt/2, dt)
  t_fine=np.arange(1, 5+0.1/2, 0.1)
  n = len(t)
  p = np.ones(n)
  pRK4 = np.ones(n)
  p[0] = np.exp(0.3*t[0])
  pRK4[0] = np.exp(0.3*t[0])# Initial Condition

  with plt.xkcd():

    fig = plt.figure(figsize=(6, 4))
    plt.plot(t, np.exp(0.3*t), 'k', label='Exact Solution')

    for i in range(n-1):
      dp = dt*0.3*p[i]
      p[i+1] = p[i] + dp # Euler
      k1 = 0.3*(pRK4[i])
      k2 = 0.3*(pRK4[i] + dt/2*k1)
      k3 = 0.3*(pRK4[i] + dt/2*k2)
      k4 = 0.3*(pRK4[i] + dt*k3)
      pRK4[i+1] = pRK4[i] + dt/6 *(k1 + 2*k2 + 2*k3 + k4)

    plt.plot(t_fine, np.exp(0.3*t_fine), label='Exact')
    plt.plot(t, p,':ro', label='Euler Estimate')
    plt.plot(t, pRK4,':co',  label='4th Order Runge Kutta Estimate')

    plt.ylabel('Population (millions)')
    plt.legend()
    plt.xlabel('Time (years)')
    plt.show()

# @markdown Make sure you execute this cell to enable the widget!
my_layout.width = '450px'
@widgets.interact(
    dt=widgets.FloatSlider(0.5, min=.1, max=4., step=.1,
                                layout=my_layout)
)
def Pop_widget(dt):
    RK4(dt)
```

**Bonus Reference 1: A full course on numerical methods in Python**

For a full course on Numerical Methods for differential Equations you can look here https://github.com/john-s-butler-dit/Numerical-Analysis-Python

---
## Bonus  Section 2: Neural oscillations are a start toward understanding brain activity rather than the end

The differential equations we have discussed above are all to simulate neuronal processes, another way differential equations can be used is to motivate experimental findings.

A great deal of experimental and neurophysiological studies have investigated whether the brain oscillates and/or entrain to a stimulus.
An issue with these studies is that there is not a consistent  definition of what constitutes an oscillation. Right now it is a bit of I know one when I see one problem.

In an essay from May 2021 in PLOS Biology by Keith Dowling (a past Neuromatch TA) and M. Florencia Assaneo discussed a mathematical way of thinking about what should be expected experimentally  when studying  oscillations. In the essay they propose that instead of thinking about the brain we should look at this question from the mathematical side, to motivate what can be defined as an oscillation.

To do this they used Stuart–Landau equations, which is a system of differential equations
\begin{align}
 \frac{dx}{dt} &=\lambda x-\omega y -\gamma (x^2+y^2)x+s\\
 \frac{dy}{dt} &=\lambda y+\omega x -\gamma (x^2+y^2)y
\end{align}

where $s$ is input to the system, and $\lambda$, $\omega$ and $\gamma$ are parameters.

The Stuart–Landau equations are a well described system of non-linear differential equations that generate oscillations. For their purpose,  Dowling and  Assaneo used the equations to motivate what experimenters should expect when conducting experiments looking for oscillations.
In their paper, using the Stuart–Landau equations, they outline
* "What is an oscillator?"
* "What an oscillator is not"
* "Not all that oscillates is an oscillator"
* "Not all oscillators are alike."

The Euler form of the Stuart–Landau system of equations is:
\begin{align*}
\color{red}{x_{k+1}}&=\color{green}{x_k}+\color{blue}{\Delta t}\big(\lambda \color{green}{x_k}-\omega \color{green}{y_k} -\gamma (\color{green}{x_k}^2+\color{green}{y_k}^2)\color{green}{x_k}+s\big),\\
\color{red}{y_{k+1}}&=\color{green}{y_k}+\color{blue}{\Delta t}\big(\lambda \color{green}{y_k}+\omega \color{green}{x_k} -\gamma (\color{green}{x_k}^2+\color{green}{y_k}^2)\color{green}{y_k} \big),\\
&\text{ for } k=0, \cdots n-1,\\
x_0&=1,\\
y_0&=1,\\
\end{align*}
with $ \Delta t=0.1/1000$ ms.

```python
# @title Helper functions
def plot_Stuart_Landa(t, x, y, s):
  """
    Args:
      t  : time
      x  : x
      y  : y
      s : input
    Returns:
      figure with two panels
      top panel: Input as a function of time
      bottom panel: x
  """

  with plt.xkcd():
    fig = plt.figure(figsize=(14, 4))
    gs = gridspec.GridSpec(2, 2,  height_ratios=[1, 4], width_ratios=[4, 1])

    # PLOT OF INPUT
    plt.subplot(gs[0])
    plt.ylabel(r'$s$')
    plt.plot(t, s, 'g')
    #plt.ylim((2,4))

    # PLOT OF ACTIVITY
    plt.subplot(gs[2])
    plt.plot(t ,x)
    plt.ylabel(r'x')
    plt.xlabel(r't')
    plt.subplot(gs[3])
    plt.plot(x,y)
    plt.plot(x[0], y[0],'go')
    plt.xlabel(r'x')
    plt.ylabel(r'y')
    plt.show()

def Euler_Stuart_Landau(s,time,dt,lamba=0.1,gamma=1,k=25):
  """
  Args:
    I: Input
    time: time
    dt: time-step
  """

  n = len(time)
  omega = 4 * 2*np.pi
  x = np.zeros(n)
  y = np.zeros(n)
  x[0] = 1
  y[0] = 1

  for i in range(n-1):
    dx = lamba*x[i] - omega*y[i] - gamma*(x[i]*x[i] + y[i]*y[i])*x[i] + k*s[i]
    x[i+1] = x[i] + dt*dx
    dy = lamba*y[i] + omega*x[i] - gamma*(x[i]*x[i] + y[i]*y[i])*y[i]
    y[i+1] = y[i] + dt*dy

  return x, y
```

### Bonus 2.1: What is an Oscillator?
Doelling & Assaneo (2021), using the Stuart–Landau system, show different possible states of an oscillator by manipulating the $\lambda$ term in the equation.
From the paper: "this qualitative change in behavior takes place at λ = 0: For λ < 0, the system decays to a stable equilibrium, while for λ > 0, it keeps oscillating."

This illustrates an oscillations does not have to maintain all the time, so experimentally we should not expect perfectly maintained oscillations. We see this all the time in $\alpha$ band oscillations in EEG the oscillations come and go.

#### Interactive Demo Bonus 2.1
The plot below shows the estimated solution of the Stuart–Landau system with a base frequency $\omega$ set to $4\times 2\pi$, $\gamma=1$ and $k=25$ over 3 seconds. The input to the system $s(t)$ is plotted in the top panel, $x$ as a function of time in the the bottom panel and on the right the phase plane plot of $x$ and $y$. You can manipulate $\lambda$ to see how the oscillations change.

```python
# @markdown Make sure you execute this cell to enable the widget!
dt=0.1/1000
t=np.arange(0, 3, dt)

my_layout.width = '450px'
@widgets.interact(
    lamda=widgets.FloatSlider(1, min=-1., max=5., step=0.5,
                                layout=my_layout)
)

def Pop_widget(lamda):
    s=np.zeros(len(t))
    x,y=Euler_Stuart_Landau(s,t,dt,lamda)
    plot_Stuart_Landa(t, x, y, s)
    plt.show()
```

### Bonus 2.2 : Not all oscillators are alike

#### Interactive Demo Bonus 2: Stuart-Landau System
The plot below shows estimated solution of the Stuart–Landau system with a base frequency of 4Hz by stetting $\omega$ to $4\times 2\pi$, $\lambda=1$, $\gamma=1$ and $k=50$ over 3 seconds the input to the system $s(t)=\sin(freq 2\pi t) $ in the top panel, $x$ as a function of time in the the bottom panel and on the right the phase plane plot of $x$ and $y$.

You can manipulate the frequency $freq$ of the input to see how the oscillations change and for frequencies $freq$ further and further from the base frequency of 4Hz the oscillations breaks down.

This shoes that if you have an oscillating input into an oscillator with it does not have to respond by oscillating about could even breakdown. Hence the frequency of the input oscillation is important to the system.
So if you flash something at 50Hz, for example, the visual system might not follow the signal but that does not mean the visual system is not an oscillator it might just be the wrong frequency.

```python
# @markdown Make sure you execute this cell to enable the widget!
dt=0.1/1000
t=np.arange(0, 3, dt)

my_layout.width = '450px'
@widgets.interact(
    freq=widgets.FloatSlider(4, min=0.5, max=10., step=0.5,
                                layout=my_layout)
)

def Pop_widget(freq):
    s = np.sin(freq * 2*np.pi * t)

    x, y = Euler_Stuart_Landau(s, t, dt, lamba=1, gamma=.1, k=50)
    plot_Stuart_Landa(t, x, y, s)
```

**Bonus Reference 2**

[Doelling, K. B., & Assaneo, M. F. (2021). Neural oscillations are a start toward understanding brain activity rather than the end. PLoS biology, 19(5), e3001234.](https://journals.plos.org/plosbiology/article?id=10.1371/journal.pbio.3001234)


## PyFolding SI Notebook 4
---

[Authors] ERGM, ARL

---

## Fitting tANK chevron to Three State with fast phase chevron - sharing parameters between models

In this notebook, let's try to fit a complicated model to some example data. Here the idea is to fit two models to two datasets, but share some of the parameters between the two models. In real terms this is difficult to do in most bought fitting software, but we have the full power of a programming language at our disposal.

We can use the GlobalFit object to do exactly what we want. Let's start by getting some nice data digitised from this paper:

```
Structural insights into an equilibrium folding intermediate of an archaeal ankyrin repeat
protein. Low C, Weininger U, Neumann P, Klepsch M, Lilie H, Stubbs MT, and Balbach J
PNAS (2008) 105: 3779–3784
```
[http://www.pnas.org/cgi/doi/10.1073/pnas.0710657105]

---

Remember, if you are less script/computer orientated, you can simply change the data paths/variables, etc for your proteins and re-run the jupyter notebook ( "Kernal/Restart & Run all" from the menu above).

---

### Data Format

Please see PyFolding SI Notebooks 1 and 2 for the format your data has to be in to enable this type of analysis.

---

```python
# First off let's load pyfolding & pyplot into this ipython notebook
# (pyplot allows us to plot more complex figures of our results):

%matplotlib inline
import pyfolding
from pyfolding import models

# let's use some other libraries also
import matplotlib.pyplot as plt
import numpy as np
```

    <IPython.core.display.Javascript object>

    PyFolding: Jupyter autoscrolling has been disabled

```python
# loading the data - The kinetics of each protein is in one .csv as per
#PyFolding SI Notebooks 1 and 2

pth = "../examples/tANK/"
tANK_all = pyfolding.read_kinetic_data(pth,"ChevronAll.csv")
```

```python
# let's give this dataset a good name
tANK_all.ID = 'Thermophilic AR protein (tANK)'
```

---
#### Let's plot the chevron phases:

---

```python
# easy plotting of the entire dataset ...
tANK_all.plot()
```

```python
# ...or, custom plotting

k1_x, k1_y = tANK_all.chevron('k1') # defines the slower chevron rates
k2_x, k2_y = tANK_all.chevron('k2') # defines the faster chevron rates

plt.figure(figsize=(8,5))           # makes the figure 8cm by 5cm
plt.plot(k1_x, k1_y, 'ro', markersize=8) # red filled circles, sized '8'
plt.plot(k2_x, k2_y, 'ko', markersize=8) # black filled circles, sized '8'
plt.rc('xtick', labelsize=14)            # fontsize of the x tick labels
plt.rc('ytick', labelsize=14)            # fontsize of the y tick labels
plt.ylim([0, 8])                         # y axis from 0 to 8
plt.xlim([0, 5])                         # x axis from 0 to 5
plt.grid(False)                          # no grid on the graph
plt.xlabel('[GdmHCl] (M)', fontsize=14)  # x axis title with fontsize
plt.ylabel(r'$\ln k_{obs}$ $(s^{-1})$', fontsize=14)  # y axis title with fontsize
plt.title('Thermophilic AR protein (tANK) folding kinetics') # Plot title
plt.show()

```

---

#### Note that the measurements were performed at $15^\text{o}$C, so we need to adjust the temperature:

---

```python
pyfolding.set_temperature(15.0)
```

    Set temperature to 15.00°C
    (NOTE: Careful, this sets the temperature for all subsequent calculations)

---

#### In the paper they decided that their data showed that their protein was folding via a three state mechanism.  So lets try fitting the digitized data to this model - three state chevron with fast phase. To do this, we need to share the parameters like this:

1. Fit a "two-state chevron" model to the fast phase, and share those params with:
2. A "three-state with fast phase" model for the slower phase

First of all lets print the information on each model:

---

```python
models.TwoStateChevron().info()
```

$$\begin{equation} \
				\begin{aligned} \
				& k_{obs} = k_f + k_u \\ \
				\\ \
				\text{where:} \\ \
				& k_f = k_f^{H_2O}\exp(-m_{kf}x)\\ \
				& k_u = k_u^{H_2O}\exp(m_{ku}x) \\ \
				\text{thus:} \\ \
				& k_{obs} = k_f^{H_2O}\exp(-m_{kf}x) + k_u^{H_2O}\exp(m_{ku}x)\\ \
				\end{aligned} \
				\end{equation}$$

     Two state chevron plot.

    	Folding Scheme:
    		N <-> D

    	Params:
    		k obs = rate constant of unfolding or refolding at a particular denaturant concentration
    		kf = rate constant of refolding at a particular denaturant concentration
    		mf = the gradient of refolding arm of the chevron
    		ku = rate constant of unfolding at a a particular denaturant concentration
    		mu = the gradient of unfolding arm of the chevron
    		x = denaturant concentration (M)

    	Notes:
    		Jackson SE and Fersht AR.  Folding of chymotrypsin inhibitor 2.
    		1. Evidence for a two-state transition.
    		Biochemistry (1991) 30(43):10428-10435.

```python
models.ThreeStateFastPhaseChevron().info()
```

$$\begin{equation} \
				\begin{aligned} \
				& k_{obs} = \frac{k_{fi} + k_{if}} {(1+1/K_{iu})} \\ \
				\\ \
				\text{where:} \\ \
				& k_{fi} = k_{fi}^{H_2O}\exp(m_{fi}x)\\ \
				& k_{if} = k_{if}^{H_2O}\exp(-m_{if}x)\\ \
				& k_{iu} = k_{iu}^{H_2O}\exp(m_{iu}x)\\ \
				& k_{ui} = k_{ui}^{H_2O}\exp(-m_{ui}x)\\ \
				& K_{iu} = \frac{k_{iu}} {k_{iu} + k_{ui}}\\ \
				\end{aligned} \
				\end{equation}$$

     Three state chevron with single intermediate.

    	Folding Scheme: N <-> I <-> D

    	Params:
    		k obs = rate constant of unfolding or refolding at a particular denaturant concentration
    		kfi = microscopic rate constant for the conversion of folded to intermediate
    		kif = microscopic rate constant for the conversion of intermediate to folded
    		kiu = microscopic rate constant for the conversion of intermediate to unfolded
    		kui = microscopic rate constant for the conversion of unfolded to intermediate
    		Kiu = equilibrium constant for the rapid equilibration between intermediate & unfolded
    		mfi = m-value associated with the kinetic transition between folded & intermediate
    		mif = m-value associated with the kinetic transition between intermediate & folded
    		miu = m-value associated with the kinetic transition between intermediate & unfolded
    		mui = m-value associated with the kinetic transition between unfolded & intermediate
    		x = denaturant concentration (M)

    	Reference:
    		Parker et al. An integrated kinetic analysis of
    		intermediates and transition states in protein folding reactions.
    		Journal of molecular biology (1995) vol. 253 (5) pp. 771-86

---

**To fit the data to two equations at once where certain parameters are shared, we need to create a fast phase model, which is essentially a two state chevron, but where the parameters represent the folding and unfolding of the intermediate. We can do this *on-the-fly* by using the template model to create a new **FastPhase** model:**

---

```python
class FastPhase(pyfolding.FitModel):
    """ Fast phase model
    """
    def __init__(self):
        pyfolding.FitModel.__init__(self)
        fit_args = self.fit_func_args
        self.params = tuple( [(fit_args[i],i) for i in xrange(len(fit_args))] )
        self.default_params = np.array([100., 1.3480, 5e-4, 1.])
        self.verified = True

    def fit_func(self, x, kui, mui, kiu, miu):
        k_obs = kui*np.exp(-mui*x) + kiu*np.exp(miu*x)
        return k_obs

    def error_func(self, y):
        return np.log(y)
```

---

### Setting up a custom Global fit

---

```python
def plot_fig():
    # now that we've fitted the data, plot a figure
    plt.figure(figsize=(8,5))
    plt.plot(k1_x, k1_y, 'ro', markersize=8)
    plt.plot(k2_x, k2_y, 'ko', markersize=8)

    # plot each of the chevrons...
    for r in results:
        plt.plot(r.x_fit, r.y_fit, 'k-', linewidth=2)

    plt.xlim([min(k1_x), max(k1_x)])
    plt.rc('xtick', labelsize=18)    # fontsize of the x tick labels
    plt.rc('ytick', labelsize=18)    # fontsize of the y tick labels
    plt.grid(False)
    plt.xlabel('[GdmHCl] (M)', fontsize=18)
    plt.ylabel(r'$\ln k_{obs}$ $(s^{-1})$', fontsize=18)
    plt.ylim([0, 8])
    plt.show()
```

```python
# We first need to differentiate the data into the two phases we will fit
k1_x, k1_y = tANK_all.chevron('k1') # defines the slower chevron rates
k2_x, k2_y = tANK_all.chevron('k2') # defines the faster chevron rates

# start by setting up a GlobalFit object
global_fit = pyfolding.GlobalFit()
global_fit.fit_funcs = (models.ThreeStateFastPhaseChevron, FastPhase)
global_fit.x = (k1_x, k2_x) # remember, k1 defines the slower chevron
global_fit.y = (k1_y, k2_y) # & k2 defines are faster chevron
global_fit.ID = ['slow_phase','fast_phase']
global_fit.shared = ['kui', 'mui', 'kiu', 'miu']
global_fit.initialise()

```

```python
# find out the order of params for p0
print global_fit.params.keys()
```

    ['mui', 'miu', 'kui', 'kiu', 'kif_{slow_phase}', 'mif_{slow_phase}', 'kfi_{slow_phase}', 'mfi_{slow_phase}']

```python
# set some reasonable starting parameters for the fit
p0 = np.array([1.7, 0.01, 1860., 2.7, 2652.0, 2.8, 14., 1.])
bounds = ((-5.,-5.,0.,0.,0.,-5.,0.,-5), (5.,5.,1e4,1e4,1e4,5.,1e4,5.))

# run the fit and get the results
_,_ = global_fit.fit(p0=p0, bounds=bounds)
results = global_fit.results

```

---

**Display the results from the fitting. Note that in the results table, (f) means a free-variable, (s) is shared and (c) is constant.**

---

```python
models.ThreeStateFastPhaseChevron().info()
```

$$\begin{equation} \
				\begin{aligned} \
				& k_{obs} = \frac{k_{fi} + k_{if}} {(1+1/K_{iu})} \\ \
				\\ \
				\text{where:} \\ \
				& k_{fi} = k_{fi}^{H_2O}\exp(m_{fi}x)\\ \
				& k_{if} = k_{if}^{H_2O}\exp(-m_{if}x)\\ \
				& k_{iu} = k_{iu}^{H_2O}\exp(m_{iu}x)\\ \
				& k_{ui} = k_{ui}^{H_2O}\exp(-m_{ui}x)\\ \
				& K_{iu} = \frac{k_{iu}} {k_{iu} + k_{ui}}\\ \
				\end{aligned} \
				\end{equation}$$

     Three state chevron with single intermediate.

    	Folding Scheme: N <-> I <-> D

    	Params:
    		k obs = rate constant of unfolding or refolding at a particular denaturant concentration
    		kfi = microscopic rate constant for the conversion of folded to intermediate
    		kif = microscopic rate constant for the conversion of intermediate to folded
    		kiu = microscopic rate constant for the conversion of intermediate to unfolded
    		kui = microscopic rate constant for the conversion of unfolded to intermediate
    		Kiu = equilibrium constant for the rapid equilibration between intermediate & unfolded
    		mfi = m-value associated with the kinetic transition between folded & intermediate
    		mif = m-value associated with the kinetic transition between intermediate & folded
    		miu = m-value associated with the kinetic transition between intermediate & unfolded
    		mui = m-value associated with the kinetic transition between unfolded & intermediate
    		x = denaturant concentration (M)

    	Reference:
    		Parker et al. An integrated kinetic analysis of
    		intermediates and transition states in protein folding reactions.
    		Journal of molecular biology (1995) vol. 253 (5) pp. 771-86

```python
for r in results:
    r.display()
```

    ================================================================================
    Fitting results
    ================================================================================
    ID: slow_phase
    Model: ThreeStateFastPhaseChevron
    Optimiser: pyfolding.GlobalFit and scipy.optimize.curve_fit
    Temperature: 15.00°C

    (s) kui 2652.18225 ± 54.43026   	 95% CI[2638.45737, 2665.90713]
    (s) mui    2.82889 ± 0.02696    	 95% CI[   2.82210,    2.83569]
    (s) kiu   14.30673 ± 0.37100    	 95% CI[  14.21318,   14.40028]
    (s) miu    0.99906 ± 0.00828    	 95% CI[   0.99698,    1.00115]
    (f) kif 1860.56406 ± 63.30515   	 95% CI[1844.60133, 1876.52680]
    (f) mif    2.73831 ± 0.01748    	 95% CI[   2.73391,    2.74272]
    (f) kfi    0.01057 ± 0.00038    	 95% CI[   0.01047,    0.01066]
    (f) mfi    1.76794 ± 0.00936    	 95% CI[   1.76558,    1.77030]
    --------------------------------------------------------------------------------
    R^2: 0.97224
    ================================================================================

    ================================================================================
    Fitting results
    ================================================================================
    ID: fast_phase
    Model: FastPhase
    Optimiser: pyfolding.GlobalFit and scipy.optimize.curve_fit
    Temperature: 15.00°C

    (s) kui 2652.18225 ± 52.85223   	 95% CI[2638.84956, 2665.51494]
    (s) mui    2.82889 ± 0.02618    	 95% CI[   2.82229,    2.83550]
    (s) kiu   14.30673 ± 0.36024    	 95% CI[  14.21585,   14.39760]
    (s) miu    0.99906 ± 0.00804    	 95% CI[   0.99704,    1.00109]
    --------------------------------------------------------------------------------
    R^2: 0.98521
    ================================================================================

```python
plot_fig()
```

---

### We can also output this with the  "prettier" graphics showing chevron & equilibrium curves together
(as in PyFolding SI Notebook 1)

---

### 1st  - we need to add the kinetic results to the protein object

---

```python
tANK_all.results = results[0]
tANK_all.components = {'fast': np.exp(results[1].y_fit)}
tANK_all.fit_func = models.ThreeStateFastPhaseChevron
```

    Warning: overwriting fit result for <pyfolding.core.Chevron object at 0x11174a490>

---

### 2nd - lets get the equilibrium data & fit to a three state model

---

```python
#1st load the data by adding the equilibrium
tank_Equilm = pyfolding.read_equilibrium_data(pth, "tANK_equilmFluorscence.csv")

#then select the fit function and associate it with the data
tank_Equilm.fit_func = models.ThreeStateEquilibrium

# Fit it with parameters (as the defaults ones are not close enough)
tank_Equilm.fit(p0=[350,2,25,18.5,11.6,34,12.6])

# Plot the figure to check
tank_Equilm.plot()

```

---

### 3rd - lets plot both together & print out to pdf

---

```python
pyfolding.plot_figure(tank_Equilm, tANK_all, display=True)

```

---

#### End of this Notebook.

---


### Python Functions to Calculate and Plot the Lorenz Curve and Coefficient to Quantify Spatial Heterogeneity

#### The University of Texas at Austin, PGE 2020 SURI, Undergraduation Research

#### Alan Scherman, Undergraduate Student, Rice University

#### Supervised by:

#### Michael Pyrcz, Associate Professor, University of Texas at Austin

##### [Twitter](https://twitter.com/geostatsguy) | [GitHub](https://github.com/GeostatsGuy) | [Website](http://michaelpyrcz.com) | [GoogleScholar](https://scholar.google.com/citations?user=QVZ20eQAAAAJ&hl=en&oi=ao) | [Book](https://www.amazon.com/Geostatistical-Reservoir-Modeling-Michael-Pyrcz/dp/0199731446) | [YouTube](https://www.youtube.com/channel/UCLqEr-xV-ceHdXXXrTId5ig)  | [LinkedIn](https://www.linkedin.com/in/michael-pyrcz-61a648a1) | [GeostatsPy](https://github.com/GeostatsGuy/GeostatsPy)

#### Introduction

This is a demonstration of how to calculate the Lorenz Coefficient of a subsurface sample from well log porosity and permeability measurements. The Lorenz Coefficient is an important heuristic imported form the sphere of macroeconomics used to quantify the spatial heterogeneity of a subsurface sample. The Lorenz Coefficient is obtained by doubling the area between the Lorenz Curve and the Homogeneous Line. It ranges from 0 to 1 where, by convention, a coefficient of less than 0.3 suggests low heterogeneity and a coefficient of more than 0.6 indicates high heterogeneity. In practical terms, a low spatial heterogeneity allows for simple displacement of subsurface fluids and a high recovery factor.
***
#### Objective

To understand and apply the methodology to calculate the Lorenz Curve and Coefficient from porosity and permeability measurements through Python functionalities.

***
#### Calculation procedure

The following list contains the necessary steps to determine the Lorenz Coefficient of a subsurface sample:

**(1)** Sort porosity ($\phi$) and permeability (**_k_**) in descending order of ratio **_k_**/$\phi$;

**(2)** Calculate storage (**_S.C._**) and flow capacity (**_F.C._**) of each layer (region between depth measurements) with:

\begin{equation}
S.C. = \phi*h
\end{equation}
\begin{equation}
F.C. = k*h
\end{equation}

where **_h_** is the layer thickness;

**(3)** Calculate the _cumulative_ storage (**_C.S.C._**) and flow capacities (**_C.F.C._**) of each layer with:

\begin{equation}
C.S.C. = \sum^{current layer}_{i = 1} \phi*h
\end{equation}
\begin{equation}
C.F.C. = \sum^{current layer}_{i = 1} k*h
\end{equation}

**(4)** Normalize the cumulative storage and flow capacities by dividing them by the largest cumulative storage and flow capacities (i.e. the last cumulative capacities calculated), respectively;

**(5)** _(Optional)_ Plot the normalized cumulative flow capacities against the normalized cumulative storage capacities (i.e. the Lorenz Curve);

**(6)** Find a curve fit for the normalized capacities in the Cartesian plane (usually a 3rd degree polynomial is sufficient);

**(7)** Integrate to find the area between the Lorenz Curve and the Homogenous Line;

**(8)** Divide the result found in **(7)** by 0.5 to obtain the Lorenz Coefficient:

\begin{equation}
LorenzCoefficient = \frac{\int^{1}_{0} (LorenzCurve - HomogeneousLine)}{0.5}
\end{equation}
***
#### Load the required libraries

The program below utilizes some standard Python packages. These should be previously installed if you have Anaconda or other similar software.

#### Getting Started

Here's the steps to get setup in Python with the GeostatsPy package:

1. Install Anaconda 3 on your machine (https://www.anaconda.com/download/).
2. From Anaconda Navigator (within Anaconda3 group), go to the environment tab, click on base (root) green arrow and open a terminal.
3. In the terminal type: pip install geostatspy.
4. Open Jupyter and in the top block get started by copy and pasting the code block below from this Jupyter Notebook to start using the geostatspy functionality.

There are examples below with these functions. You can go here to see a list of the available functions, https://git.io/fh4eX, other example workflows and source code.

#### Import Required Packages

Let's import the GeostatsPy package.

```python
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt
```

***
#### Declare function

These are the functions to:

1. calculate the Lorenz coefficent and the Lorenz curve

2. plot the Lorenz curve and display the Lorenz coeficient

```python
def lorenz_coefficient(poros, perme, layer_thick = 0.25, depth = None, curve_plot = False):

    """Calculates Lorenz coefficient and curve of subsurface sample given porosity and permeability data.
    :param poros: porosities in %
    :type: 1D numpy.ndarray
    :param perme: permeabilities in mD
    :type: 1D numpy.ndarray
    :param layer_thick: uniform layer thickness; default 0.25 m;
                        if depth is not None, layer_thick is recalculated
    :type: int or float
    :param depth: default None for constant thickness;
                  for variable layer thickness, enter 1D numpy.ndarray of depths in m
    :param curve_plot: default False; set to True to display the Lorenz Curve
    :type: boolean
    :return:lorenz_coeff: Lorenz Coefficient rounded to 3 decimal places
           :type: float
           :lorenz_plot: (optional) figure with plot of Lorenz Curve
    """

    # Converting to row-oriented numpy.ndarrays (if necessary)

    if np.ndim(depth) == 2:
        depth = np.transpose(depth)[0,:]
    if np.ndim(poros) == 2:
        poros = np.transpose(poros)[0,:]
    if np.ndim(perme) == 2:
        perme = np.transpose(perme)[0,:]

    # Determining layer thickness array

    if depth is not None:
        layer_thick = depth - np.concatenate(([0],depth[:-1]))
    else:
        layer_thick = np.ones(len(poros))*layer_thick

    # Exception handling

    if (all(isinstance(x, (int, float)) for x in layer_thick) or all(isinstance(x, (int, float)) for x in poros) or all(isinstance(x, (int, float)) for x in perme)) == False:
        raise TypeError('Not all array entries are integers or floats')

    elif len(layer_thick) != len(poros) or len(layer_thick) != len(perme):
        raise TypeError('There are not sufficient porosity/permeability measurements for all layers')

    elif (np.ndim(layer_thick) or np.ndim(poros) or np.ndim(perme)) != 1:
        raise TypeError('Arguments should be 1D numpy.ndarrays')

    # Sorting data in descending order of perme/poros

    ratio = perme/poros
    layer_thick = np.flip(layer_thick[np.argsort(ratio)], axis = 0)
    poros = np.flip(poros[np.argsort(ratio)], axis = 0)
    perme = np.flip(perme[np.argsort(ratio)], axis = 0)

    # Flow and storage capacities

    storage_cap = poros*layer_thick
    flow_cap = perme*layer_thick

    # Cumulative flow and storage capacities

    cumul_storage_cap = np.zeros(len(flow_cap)+1)
    cumul_flow_cap = np.zeros(len(flow_cap)+1)

    cumul_storage_cap[1] = storage_cap[0]
    cumul_flow_cap[1] = flow_cap[0]

    for i in range(2,len(flow_cap)+1):
        cumul_storage_cap[i] = cumul_storage_cap[i-1] + storage_cap[i-1]
        cumul_flow_cap[i] = cumul_flow_cap[i-1] + flow_cap[i-1]

    # Fractional flow and storage capacities

    frac_storage_cap = cumul_storage_cap/cumul_storage_cap[-1]
    frac_flow_cap = cumul_flow_cap/cumul_flow_cap[-1]

    # Curve fitting

    weights = np.ones(len(frac_storage_cap)) # To ensure the fit converges to (0,0) and (1,1)
    weights[0] = 1000; weights[-1] = 1000

    poly_fit = np.polyfit(frac_storage_cap, frac_flow_cap, deg=3, w=weights)

    poly_fit = np.poly1d(poly_fit)
    integral = np.polyint(poly_fit)
    lorenz_coeff = (integral(1) - 0.5)/0.5

    if curve_plot == True:                                                    # To output Lorenz Curve plot

        # Plotting instructions

        lorenz_plot = plt.figure(figsize = (16,12))

        plt.style.use('fivethirtyeight')

        plt.plot([0,1],[0,1], label='Homogeneous Curve', color = 'red', linewidth = '3')
        plt.plot(np.linspace(0,1,100), poly_fit(np.linspace(0,1,100)), linestyle = 'dashed', \
                 linewidth = '3', label = 'Polynomial Fit', color = 'black')
        plt.fill_between(np.linspace(0,1,100), poly_fit(np.linspace(0,1,100)), np.linspace(0,1,100), alpha = 0.75)
        plt.fill_between(np.linspace(0,1,100), poly_fit(np.linspace(0,1,100)), 1, alpha = 0.75, color = 'orange')

        plt.title('Lorenz Curve and Coefficient', size = 50)
        plt.xlabel('Fraction of Total Storage Capacity (m)', size = 36)
        plt.ylabel('Fraction of Total Flow Capacity (mD*m)', size = 36)
        plt.legend(loc = 4, facecolor = '#C0C0C0', fontsize = 25)
        plt.text(0.3, 0.7, 'lorenz_coeff = \n %.3f' %lorenz_coeff, size = 40, horizontalalignment = 'center', \
                 bbox=dict(facecolor='none', edgecolor='black'))
        plt.subplots_adjust(left=0.0, bottom=0.0, right=1.5, top=1.7, wspace=0.3, hspace=0.3)
        plt.show()

    else:                                                                   # To output only Lorenz Coefficient
        return 'lorenz_coeff = %.3f' %lorenz_coeff
```

***
#### Docstrings

```python
print(lorenz_coefficient.__doc__)
```

    Calculates Lorenz coefficient and curve of subsurface sample given porosity and permeability data.
        :param poros: porosities in %
        :type: 1D numpy.ndarray
        :param perme: permeabilities in mD
        :type: 1D numpy.ndarray
        :param layer_thick: uniform layer thickness; default 0.25 m;
                            if depth is not None, layer_thick is recalculated
        :type: int or float
        :param depth: default None for constant thickness;
                      for variable layer thickness, enter 1D numpy.ndarray of depths in m
        :param curve_plot: default False; set to True to display the Lorenz Curve
        :type: boolean
        :return:lorenz_coeff: Lorenz Coefficient rounded to 3 decimal places
               :type: float
               :lorenz_plot: (optional) figure with plot of Lorenz Curve

***
#### Examples

#### E.g. 1: Lorenz coefficient with default parameters (Data set 1)

```python
df = pd.read_excel('WellPorPermSample_1.xlsx')
poros1 = pd.DataFrame(df, columns = ['Por (%)']); poros1 = poros1.to_numpy()
perme1 = pd.DataFrame(df, columns = ['Perm (mD)']); perme1 = perme1.to_numpy()

lorenz_coefficient(poros1, perme1)
```

    'lorenz_coeff = 0.171'

#### E.g. 2: Lorenz curve and coefficient with irregular thicknesses (Data set 2)

```python
df = pd.read_excel('WellPorPermSample_2.xlsx')
depth2 = pd.DataFrame(df, columns = ['Depth (m)']); depth2 = depth2.to_numpy()
poros2 = pd.DataFrame(df, columns = ['Por (%)']); poros2 = poros2.to_numpy()
perme2 = pd.DataFrame(df, columns = ['Perm (mD)']); perme2 = perme2.to_numpy()

lorenz_coefficient(poros2, perme2, depth = depth2, curve_plot = True)
```

#### E.g. 3: Lorenz coefficient with customized uniform thickness (Data set 3)

```python
df = pd.read_excel('WellPorPermSample_3.xlsx')
depth3 = pd.DataFrame(df, columns = ['Depth (m)']); depth3 = depth3.to_numpy()
poros3 = pd.DataFrame(df, columns = ['Por (%)']); poros3 = poros3.to_numpy()
perme3 = pd.DataFrame(df, columns = ['Perm (mD)']); perme3 = perme3.to_numpy()

lorenz_coefficient(poros3, perme3, layer_thick = 1.5)
```

    'lorenz_coeff = 0.192'

#### Comments

This is a set of functions to calculate and plot the Lorenz coefficient. Appreciation to **Alan Scherman** for writing this code and providing this documentation as part of the SURI program at The University of Texas at Austin.

If you want to learn more about data analytics and machine learning in Python, The Texas Center for Geostatistics has many other demonstrations on the basics of working with DataFrames, ndarrays, univariate statistics, plotting data, declustering, data transformations, trend modeling and many other workflows available [here](https://github.com/GeostatsGuy/PythonNumericalDemos), along with a package for geostatistics in Python called [GeostatsPy](https://github.com/GeostatsGuy/GeostatsPy).

We hope this was helpful,

*Michael*

***

#### More on Michael Pyrcz and the Texas Center for Geostatistics:

### Michael Pyrcz, Associate Professor, University of Texas at Austin
*Novel Data Analytics, Geostatistics and Machine Learning Subsurface Solutions*

With over 17 years of experience in subsurface consulting, research and development, Michael has returned to academia driven by his passion for teaching and enthusiasm for enhancing engineers' and geoscientists' impact in subsurface resource development.

For more about Michael check out these links:

#### [Twitter](https://twitter.com/geostatsguy) | [GitHub](https://github.com/GeostatsGuy) | [Website](http://michaelpyrcz.com) | [GoogleScholar](https://scholar.google.com/citations?user=QVZ20eQAAAAJ&hl=en&oi=ao) | [Book](https://www.amazon.com/Geostatistical-Reservoir-Modeling-Michael-Pyrcz/dp/0199731446) | [YouTube](https://www.youtube.com/channel/UCLqEr-xV-ceHdXXXrTId5ig)  | [LinkedIn](https://www.linkedin.com/in/michael-pyrcz-61a648a1)

#### Want to Work Together?

I hope this content is helpful to those that want to learn more about subsurface modeling, data analytics and machine learning. Students and working professionals are welcome to participate.

* Want to invite me to visit your company for training, mentoring, project review, workflow design and / or consulting? I'd be happy to drop by and work with you!

* Interested in partnering, supporting my graduate student research or my Subsurface Data Analytics and Machine Learning consortium (co-PIs including Profs. Foster, Torres-Verdin and van Oort)? My research combines data analytics, stochastic modeling and machine learning theory with practice to develop novel methods and workflows to add value. We are solving challenging subsurface problems!

* I can be reached at mpyrcz@austin.utexas.edu.

I'm always happy to discuss,

*Michael*

Michael Pyrcz, Ph.D., P.Eng. Associate Professor The Hildebrand Department of Petroleum and Geosystems Engineering, Bureau of Economic Geology, The Jackson School of Geosciences, The University of Texas at Austin

#### More Resources Available at: [Twitter](https://twitter.com/geostatsguy) | [GitHub](https://github.com/GeostatsGuy) | [Website](http://michaelpyrcz.com) | [GoogleScholar](https://scholar.google.com/citations?user=QVZ20eQAAAAJ&hl=en&oi=ao) | [Book](https://www.amazon.com/Geostatistical-Reservoir-Modeling-Michael-Pyrcz/dp/0199731446) | [YouTube](https://www.youtube.com/channel/UCLqEr-xV-ceHdXXXrTId5ig)  | [LinkedIn](https://www.linkedin.com/in/michael-pyrcz-61a648a1)

```python

```