---
### *The 11th Computational Neuroscience Winter School*
# Tutorial II: Neuronal Dynamics - Point Neuron Model
---
__Date:__ Jan. 21, 2022

__Content Creator:__ Songting Li, Kai Chen, Ziling Wang

## Using tutorial notebook

Please download the latest version of [Jupyter Notebook](https://jupyter.org) if you want to try run all tutorials on your own laptop.

**Gray cells are executable code cells. To execute, click on a cell and press `Shift + Enter`.**

**To modify text in a cell, click on the cell and type.**

Note that most function stubs are suggestions. Feel free to code up whatever solves the task.

If something behaves weird, it always is worth a try to restart the kernel.

---
# Tutorial Objectives
In this notebook, we will learn how to use python to solve differential equations that describe the dynamics of neurons. We will cover single-compartment neurons and simplest spatial neuron models (ball and stick) in next notebook.

In the course of this notebook, we will learn how to simulate the single point neuron model, including LIF and reduced HH neuron (Fitzhugh-Nagumo model):
1. Initialize model and variable state;
1. Record output, such as membrane potentials and spikes
1. Drive the neurons by external input

In doing so we will practice

- To make use of modular programming
- To write testing functions to check and optimize code

## Setup

```python
# @title Import most modules and functions needed
import time
import numpy as np
import matplotlib as mpl
mpl.rcParams['lines.linewidth'] = 2
mpl.rcParams['legend.fontsize'] = 14
mpl.rcParams['axes.labelsize'] = 16
mpl.rcParams['axes.titlesize'] = 18
import matplotlib.pyplot as plt
%matplotlib inline
%config InlineBackend.figure_format = 'retina'
```

# General coding remarks

In your very own interest (but also the interest of everyone who ever has to work with your code) it really helps, if you always:

- structure your code clearly, such that you can quickly grasp where loops begin and end, etc.

- choose sensible names for your functions, e.g. call a routine that stores spikes `store_spikes`, one that plots membrane potentials `plot_pots`, or something similar.

- choose sensible names for your variables, e.g. call a variable that encodes the membrane potential `V` or `Vm`, a space variable `x`, a time variable `t`, and so on. You get the gist :-)

- add comments about what individual lines/sections of code do, for (stupid) example:

```python
>>> v=v+c # this adds the constant c to v
```

- add doc strings to your functions: that is the text in between ''' in the beginning of each function definition (see examples below). Doc strings should describe what the function does and what the input variables and parameters are.

```python
>>> def func():
>>>     '''
>>>     This is a function. It does things.
>>>     '''
>>>
>>> # You can print this info by typing 'print(func.__doc__)'
>>> print(func.__doc__)
This is a function. It does things.

```

Also: **SAVE REGULARLY!!!** You don't want to lose the most brilliant few lines of code you ever produced, nor more regular tedious work to [insert random thing that WILL happen and kill your running program]

Though `Jupyter Notebook` helps by regularly creating checkpoints, you'd better mannully save your modifications.

### Modular Programming

It is extremely helpful to structure your code into seperate independent functions or modules.

For example, instead of having one huge simulation function which initializes all dynamical variables, updates the dynamical states, checks for state transitions (e.g. threshold crossings), and writes output to file, you should split it into basic functions that do one thing, and one thing only [UNIX philosophy:)].

This has several advantages:

- it increases readability of complex code;
- each function can be more easily tested and maintained;
- each function can be easily changed or replaced without affecting the behavior of other functions;
- each function can be easily reused, in the best case scenario saving you work at a later point in time;

## Numpy Broadcasting

If you want to multiply each element of `a` by `2.0`, you may implement a loop operation as follows.
```python
>>> import numpy as np
>>> a = np.array([1.0,2.0,3.0])
>>> for i in range(len(a)):
>>>     a[i] = a[i] * 2.0
>>> print(a)
[2., 4., 6.]
```

Numpy's broadcasting rule allows you to implement element-wise operations by directly interact numpy.array with scalars, shown as the following examples.

```python
>>> import numpy as np
>>> a = np.array([1.0,2.0,3.0])
>>> a = a * 2.0
>>> print(a)
[2., 4., 6.]
```

As the sketch and figure above, broadcasting provides a means of vectorizing array operatings so that looping occurs in C instead of Python. We may use `time.time()` to verify it.

```python
import numpy as np
import time
# Python Looping
a = np.arange(1000000, dtype=float)
t0 = time.time()
for i in range(len(a)):
    a[i] *= 2.0
print(f'Python looping takes {time.time()-t0:.3f}s')
print('Last 5 elements in a: ', a[-5:])

# Numpy broadcasting
a = np.arange(1000000, dtype=float)
t0 = time.time()
a = a * 2.0
print(f'Python looping takes {time.time()-t0:.3f}s')
print('Last 5 elements in a: ', a[-5:])
```

    Python looping takes 0.363s
    Last 5 elements in a:  [1999990. 1999992. 1999994. 1999996. 1999998.]
    Python looping takes 0.004s
    Last 5 elements in a:  [1999990. 1999992. 1999994. 1999996. 1999998.]

**More broadcasting, less loops!**

### Testing functions

It is critical to test each function before putting all of them together. Writing a proper test of a function will give you confidence that a function is working properly. The test should be simple enough, yet still contain  typical situation(s) to be met when all functions are put together.

E.g. if you have a function that detects a threshold crossing and resets the dynamical state as a consequence, you want to make sure that it correctly identifies the crossing and correctly resets to the desired value. To do so, you could give an increasing time series that contains the threshold value as an input and measure the output for expected behavior.

# Point neuron models

## Fitzhugh-Nagumo model

Most of you will in one way or another have encountered the famous Hodgkin-Huxley (HH) model for the dynamics of neuronal membranes. It is a powerful biophysical model to explain many aspects of membrane excitability, such as action potential generation and the role of ion channels, in particular sodium and potassium, in it.

The Fitzhugh-Nagumo model is a model that reduces the complexity of the HH model to two dimensions, while still capturing the key aspects of the dynamics. Most of all, the reduction makes it amenable to dynamical system analysis techniques.

The membrane potential equation for the Fitzhugh-Nagumo model is given by

\begin{eqnarray}
\frac{d}{dt} v(t) &=& v - \frac{v^3}{3} - w + I\quad (1.1)\\
\frac{d}{dt}w(t) &=& \epsilon\,(b_0+b_1 v - w)\quad (1.2)
%\tau \frac{d}{dt}w(t) &=& v + a -bw
\end{eqnarray}

$I$ is a driving current, $v(t)$ is a fast variable (membrane potential), $w(t)$ is a slow recovery variable.

## Step 1: Initialization and Output monitor functions

### Example: membrane potential recorder

To write membrane potential recording and plotting functions we want three different functions:

- `init_rec_pot()`: should initialize the object which you will store potentials in
- `store_pot()`: should record all potential values at a given time point
- `plot_pot()`: should plot the membrane potential as a function of time

We can print the time taken by the whole session by using `time.time()`. This is helpful for optimizing code.

```python
# if you want to initialize all traces to already have the full length=number of sample points=n_timesteps
def init_rec_pot(n_neuron,n_timesteps):
    '''
    Initializes dictionary of potential trace arrays.

    Args:
    n_neuron    : number of neurons for which you want to store the potential
    n_timesteps : number of time steps

    Returns:
    rec_pot     : dictionary object rec_pot
    '''
    rec_pot = np.zeros((n_neuron, n_timesteps))     # initialize a 2-D array with all entries=0
                                                    # i-th row represents the trace of each neuron i
                                                    # j-th column represents the potential of all neurons
                                                    #   at j-th time point;
    return rec_pot

def store_pot(rec_pot,V,it):
    '''
    Stores potentials in dictionary rec_pot

    Args:
    rec_pot: dictionary of potential traces
    V      : vector of current potential values
    it     : current time step

    Returns:
    does not return anything, this functions just updates the storing object
    '''
    rec_pot[:, it] = V      # assign potential value V of all neurons at time it
                            # to corresponding entries of rec_pot

def plot_pot(rec_pot, time=None):
    '''
    Plots potentials stored in dictionary rec_pot

    Args:
    rec_pot: dictionary of potential traces
    time   : time ticks of x-axis

    Returns:
    does not return anything, this functions just plots the contents of rec_pot
    '''
    cnt   = 0               # counter, only needed for plotting in different colors;
                            # for keys==range(n_neuron) can use i directly

    for trace in rec_pot:   # iterate through all rows of the rec_pot array
        # plot gray scale solid lines (-) with linewidth (lw) 2
        if time is None:
            plt.plot(trace,'-',color='%s'%((1.+cnt)/(len(rec_pot)+1)),lw=2, label=str(cnt+1))
        else:
            plt.plot(time, trace,'-',color='%s'%((1.+cnt)/(len(rec_pot)+1)),lw=2, label=str(cnt+1))
        cnt+=1

```

We will now use some mock potential data to see if the three functions init_rec_pot(), store_pot() and plot_pot() do what they should.

```python
T, dt     = 10, 0.1                        # total time T [ms], time increment dt [ms]
n_neuron  = 3                              # number of neurons
ts        = np.arange(0,T,dt)              # time vector
rec_pot   = init_rec_pot(n_neuron,len(ts)) # create rec_pot object
starttime = time.time()                    # you can clock how long your code needs, useful during optimization

#print rec_pot                             # how does it look?

### run "pseudo-dynamics"
# first, create an update function for the potential dynamics (here, simply creating Gaussian random numbers)
def update(n_neuron):
    return np.random.randn(n_neuron)

for it in range(len(ts)):
    vs = update(n_neuron)                  # for each time step update potential values
    store_pot(rec_pot,vs,it)               # hand to store_pot function

#print rec_pot                             # how does it look?

# plot potential traces: should give n_neuron random traces in varying gray tones
plot_pot(rec_pot)
endtime = time.time()
print(f'Running time is {endtime-starttime:0.5f} s')
```

## Step 2: Numerical ODE Solver
---

### Euler integration
Consider a differential equation of the type

$$
\tau \frac{d}{dt} x(t) = -x(t) +I\,,\quad(i)
$$

where $I$ is some constant input that drives the dynamics of the variable $x(t)$, and $\tau$ is some time constant that tunes how fast $x$ reacts to the input $I$.

For this type of equation it is straightforward to write down the solution, i.e.,

$$
x(t) = I\,(1-e^{-t/\tau}) + x_0\,e^{-t/\tau}\,,\quad (ii)
$$

where $x_0=x(t_0)$ is the initial condition. Indeed, if we compute the temporal derivative of $(ii)$ we get

\begin{eqnarray}
\frac{d}{dt} x(t) \quad &=& \quad \frac{d}{dt} \big(I\,(1-e^{-t/\tau}) + x_0\,e^{-t/\tau}\big) \quad =\quad  \frac{1}{\tau} \left(I\,e^{-t/\tau} -x_0 \,e^{-t/\tau}\right)  \\
\Leftrightarrow\quad \tau \frac{d}{dt} x(t) \quad &=& \quad I\,e^{-t/\tau} - x_0 \,e^{-t/\tau} + (I-I) \,\, \,\quad =\quad  -x(t) +I\,.
\end{eqnarray}

Not every differential equation is so easy to solve, so it is useful to know how to numerically compute such problems.

The most straight-forward (though not necessarily always best or optimal) way to do that is by *Euler integration*.

### Discretization

We start from $(i)$ by substituting

$$dt \rightarrow  \Delta t= t_{i+1}-t_i, \quad dx \rightarrow \Delta x = x(t_{i+1})-x(t_i)=: x_{i+1}-x_i$$

The time increment $\Delta t$ is usually a fixed number that should be considerably smaller than the smallest other time constant in the system in order to obtain a good approximation (here $\tau$). You also don't want it to be unnecessarily small, because simulation time will increase the smaller $\Delta t$. If you are unsure, whether you chose a good $\Delta t$, try varying it and observe how much the result varies.

### Initialize

Whenever we want to solve differential equations, we have to define an initial condition. So we will have to have an initialization step for $x$:

$$x(0)=x_0$$

### Update

The discretized version of $(i)$ is given by

\begin{aligned}
\tau\, \frac{\Delta x}{\Delta t} \quad &= \quad -x_i + I\\
\Leftrightarrow\quad  \Delta x \quad = \quad x_{i+1} - x_i  \quad &= \quad \Delta t \left[ \frac{1}{\tau}\,\big( -x_i +I \big)\right]\\
\Leftrightarrow\quad\quad \quad\quad\quad\quad\quad x_{i+1}\quad &= \quad x_i + \Delta t \, \left[ \frac{1}{\tau}\,\big( -x_i +I \big)\right]
\end{aligned}

The Euler integration update step always has the same form, i.e.,

$$
x_{i+1} = x_i + \Delta t f(x) \quad (1)
$$

When we write code, we want to save as much work as possible, not only by letting the computer do all the tedious calculations, but also by reducing the amount of typing work in the future. So we should design our code such that it is flexible and reusable. These are benefits that *modularization* gives us.

We can write a function called euler_step($x$,f,$\Delta t$) that just does Eqn.(1), regardless of what f does.

Then, for what is specific to the dynamics at hand, we will have a function called, here e.g., f = f_x($x,
I,\tau$) that computes $\left[ \frac{1}{\tau}\,\big( -x_i +I \big)\right]$.

This way, whenever we want to solve a dynamics with Euler integration again, we can just use euler_step() again and simply exchange $f$.

So let's do that in python code!

```python
## function for Euler integration step
def euler_step(x,f,dt):
    '''
    Executes Euler integration step

    Expects:
    x     : state variable
    f     : increment of x
    dt    : increment of t

    Returns:
    updated state x
    '''
    return x + dt*f

## specific functional increment
def f_x(x,I,tau):
    '''
    Computes increment dx/dt of state variable for Eqn.(i)

    Expects:
    I    : constant drive
    tau  : time constant

    Returns:
    state variable increment
    '''
    return (I-x)/tau
```

We have all necessary ingredients to run the full dynamics Eqn.(i), we only have to put it together into one function.

```python
def run_dynamics(x0,I,tau,T,dt):
    '''
    Runs the low-pass filter dynamics Eqn.(i)

    Expects:
    x0   : initial condition
    I    : drive
    tau  : time constant
    T    : total run time
    dt   : time increment

    Returns:
    rec_x : dicitionary of x trajectory
    '''
    rec_x = init_rec_pot(1,int(T/dt)) # initialize x-recorder
    x     = x0                        # initialize x
    for it in range(int(T/dt)):
        f = f_x(x,I,tau)              # increment of x
        x = euler_step(x,f,dt)        # update of x
        store_pot(rec_x,x,it)         # store
    return rec_x

```

The code below plots the result of our Euler integration in comparison to the analytical solution. Play with dt to see what effect it has.

```python
## define params
x0   = 0.
I    = 1.
tau  = 1.
T,dt = 20.,0.1
## run dynamics
rx   = run_dynamics(x0,I,tau,T,dt)
ts   = np.arange(0,T,dt)
## plot the trace
plot_pot(rx)

## to check if solved correctly, add analytical solution Eqn.(ii)
def x_sol(t,x0,I,tau):
    return I*(1-np.exp(-t/tau)) + x0*np.exp(-t/tau)
## plot analytical trace
plt.plot(x_sol(ts+dt,x0,I,tau),'r--')
```

### Now that we successfully set up devices to record and plot traces, and learned how to solve differential equations numerically, let's cook up our Fitzhugh Nagumo models.

## Euler integration for the Fitzhugh-Nagumo model
---

---

In order to solve these coupled equations numerically, we again interpret the differential dynamics in discrete time:

$$dt \rightarrow \Delta t= t_{i+1}-t_i, \quad dv \rightarrow \Delta v = v_{i+1}-v_i, \quad dw \rightarrow \Delta w =w_{i+1}- w_i$$

### Initialize

We will need an initialization step for $v,w$:

$$v(0)=v_0, \quad w(0)=w_0$$

### Update

The next step is to update the dynamics from $t_i$ to $t_{i+1}$. We will use Euler integration to do this.

Take good care of the fact that Eqns.(1.1) and (1.2) are coupled. If we want to update one first, the second would already receive updated information, so in order to maintain causality, use some auxiliary names in the update

$$
hv = v_{i+1}\\
hw = w_{i+1}
$$

The discretized dynamics thus becomes:

\begin{eqnarray}
\Delta v = v_{i+1}-v_i &=& \Delta t \left (v_i - \frac{v^3_i}{3} - w_i + I(t_i) \right)\\
\\
\Leftrightarrow  v_{i+1}=:hv &=& v_i \,+ \,\Delta t \left (v_i - \frac{v^3_i}{3} - w_i + I(t_i) \right)\quad (2.1)\\
\\ \\
\Delta w = w_{i+1}-w_i &=& \Delta t\, \epsilon \, \big( b_0 + b_1 v_i -w_i\big)\\ \\
\Leftrightarrow w_{i+1}=: hw &=& w_i+\Delta t\, \epsilon \, \big( b_0 + b_1 v_i -w_i\big) \quad \quad (2.2)
\end{eqnarray}

We thus arrive at

\begin{eqnarray}
hv &=& v \,+ \,\Delta t \left (v- \frac{v^3}{3} - w + I(t_i) \right)\quad (3.1)\\
hw &=& w \,+\,\Delta t\, \epsilon \, \big( b_0 + b_1 v -w\big) \quad (3.2)
\end{eqnarray}

Looking at Eqns.(3.1),(3.2), we recognize the identical Euler integration update structure in both cases ($x=\{v,w\}$):

$$
hx = x + \Delta t f(x) \quad (4)
$$

We can thus simply recycle our euler_step function from before and adapt the state variable increments on the RHS in Eqns.(3.1),(3.2): $f=$fitzhugh_nagumo($v,w,p$), where p contains all relevant parameters ($b_0,b_1,\epsilon$).

After the update is complete, we can set the new $v=hv, w=hw$ and reiterate.

### To do:

Write
- init functions
- functions to compute v,w increments
- run-function, where you recycle euler_step for the update (remember to update both with the information of the current time step, see $hv,hw$ above)

Then, store and plot the resulting traces as we have done in the example on top!

Try to come up with good tests to see if all functions have desired behavior!

Play with the individual parameters and see how the dynamics changes.

```python
def init_v(v0):

    '''
    Initializes v
    '''
    return v0

def init_w(w0):

    '''
    Initializes w
    '''
    return w0

def f_vw(v,w,eps,b0,b1,I):

    '''
    Computes v,w increments for Fitzhugh-Nagumo model

    Expects:
    v,w          : current values of v,w
    eps,b0,b1    : coupling parameters
    I            : drive

    Returns:
    fv,fw        : increments of v,w for Euler integration
    '''
    fv = v - v**3/3. - w + I
    fw = eps*(b0 + b1*v - w)
    return fv,fw

def run_fitzhugh_nagumo(v0,w0,eps,b0,b1,I,T,dt):

    '''
    Runs Fitzhugh-Nagumo dynamics

    Expects:

    v0, w0    : initial values
    eps,b0,b1 : coupling parameters
    I         : drive
    T         : total runtime
    dt        : time increment

    Returns:
    rec_v, rec_w : dictionary of v,w traces
    '''
    rec_v = init_rec_pot(1,int(T/dt))
    rec_w = init_rec_pot(1,int(T/dt))
    v     = init_v(v0)
    w     = init_v(w0)
    for it in range(int(T/dt)):
        fv,fw = f_vw(v,w,eps,b0,b1,I)
        v     = euler_step(v,fv,dt)
        w     = euler_step(w,fw,dt)
        store_pot(rec_v,[v],it)
        store_pot(rec_w,[w],it)
    return rec_v,rec_w

```

```python
epsilon = 0.1
b0      = 0.
b1      = 0.2
I       = -0.5
T,dt    = 100,0.1
v0,w0   = 0.5,0.5
rv,rw   = run_fitzhugh_nagumo(v0,w0,epsilon,b0,b1,I,T,dt)
## params to try:
## I  = [-0.5,-0.4,-0.3,0.2,0.5]
## b1 = [0.2,0.5]
## also vary initial conditions

plot_pot(rv, time = np.arange(0, T, dt))
plt.xlabel('Time (ms)')
plt.ylabel(r'$v(t)$')
```

```python
plot_pot(rw, time = np.arange(0, T, dt))
plt.xlabel('Time (ms)')
plt.ylabel(r'$w(t)$')
```

# >>> Extra: Leaky integrate-and-fire (LIF) neuron <<<

### If you managed to go through all the material so far, you can start working on the LIF model. We will work on this in more detail in the Spiking Network Tutorial

The membrane potential equation for the LIF is given by
\begin{eqnarray}
C_m\frac{dV}{dt} = -g_L(V-V_L) + I
\end{eqnarray}
$I$ is a driving current. $C_m$ is the membrane capacitance of the neuron. We also assume the membrane only has a leak conductance $g_L$, trying to bring the voltage to leak potential $V_L$.

Dividing both sides of the equation by $g_L$ we get,
\begin{align}
\tau_m\frac{dV}{dt} = -(V-V_L) + \frac{I}{g_L}\,,\quad (1)
\end{align}
where $\tau_m=C/g_L$.

If $I$ is sufficiently strong such that $V$ reaches some threshold value $V_{th}$, $V$ will be reset to a reset potential $V_{reset}<V_{th}$, i.e.,

\begin{eqnarray}
\mathrm{if}\quad V(t)\geq V_{th}&:& V(t^+)=V_{reset}
\end{eqnarray}

To numerically solve the LIF membrane potential dynamics, we will again use Euler integration to approximate the continuous dynamics by a piecewise integration on a time-grid of stepsize $\Delta t$.

Note once more, that in order to have a good approximation, $\Delta t$ needs to be small compared to all other time constants in the system (here $\tau_m$). The smaller $\Delta t$, the better the approximation, however, also the total computation time will be longer.

On a grid the dynamics translates to

\begin{aligned}
\tau_m \Delta V =  \tau_m (V_{i+1}-V_i) &= \left(-(V_i-V_L) +\frac{I_{syn}}{g_L}\right)dt \\
\Leftrightarrow \qquad\qquad\qquad\quad V_{i+1} &= V_i+ \left(-(V_i-V_L) +\frac{I_{syn}}{g_L}\right)\frac{dt}{\tau_m} \quad (2)
\end{aligned}

### Simulating LIF dynamics

We will now translate the dynamics Eqn.(2) into code. We start by creating a dictionary `pm` that contains all parameters.
Then we will write several small modular functions that perform the individual operations to solve the dynamics.

We will likely need several functions to

- initialize a dictionary that represents membrane potentials

- initialize a dictionary that represents constant input currents **(Amplitude of constant current is generated by Gaussian distribution with mean $\mu$ and variance $\sigma^2$)**

- an update function that updates the membrane potential in each time step corresponding to the Euler integration scheme above

- a threshold handling function that resets the membrane potential in case of a crossing and registers a spike event

- a main function that combines all of the above in a sensible way to solve the LIF dynamics

```python
# Some typical neuron parameters, stored in a dictionary
pm={"V_th"    : -55.,    # spike threshold [mV]
    "V_reset" : -75.,    # reset potential [mV]
    "tau_m"   : 20.,     # membrane time constant [ms]
    "g_L"     : 20.,     # leak conductance [nS]
    "V_init"  : -65,     # initial potential
    "V_L"     : -65}     # leak reversal potential
```

```python
def init_V(n_neuron,pm):
    '''
    Intialize the array of membrane potentials

    Expects:
    n_neuron : number of neurons
    pm       : parameter dictionary

    Returns:
    vs       : array of membrane potential values (scalars) initialized to V_init
    '''

    # ADD CODE HERE
    vs = pm['V_init'] * np.ones(n_neuron)
    return vs
```

```python
# test init_V()
vs_test = init_V(5, pm)
print(vs_test)
```

    [-65. -65. -65. -65. -65.]

```python
def init_I_syn(n_neuron,mu,sig):
    '''
    Intialize the array of direct input currents, each drawn from Gaussian distribution
    of mean mu and standard deviatiton sig

    Expects:
    n_neuron : number of neurons
    mu, sig  : constant driving currents with amplitudes drawn from Gaussian distribution
               with mean mu and standard deviation sig

    Returns:
    isyns    : array of driving currents

    '''

    # ADD CODE HERE
    isyns = np.random.randn(n_neuron) * sig + mu
    return isyns
```

```python
# test init_I_syn()
isyn_test = init_I_syn(5, 10, 5)
print(isyn_test)
```

    [12.13298981  4.77776708 -0.31843491 20.43021377  5.88550236]

```python
def threshold(V,n_neuron,pm):
    '''
    Handle nonlinear threshold:
    if potential >= V_th: reset to V_reset, indicate spike event

    Expects:
    V        : membrane potential [mV]
    n_neuron : number of neurons
    pm       : parameter dictionary

    Returns:
    V        : updated membrane potential value [mV]
    spiking  : boolean array of length n_neuron with entries True at position i if neuron i
               spiked in current time step, False otherwise
    '''

    # ADD CODE HERE
    spiking = (V>=pm['V_th'])
    V[spiking] = pm['V_reset']
    return V, spiking

```

```python
# test threshold()
V_test = np.random.rand(5)*(pm['V_th']-pm['V_reset']) * 1.5 + pm['V_reset']
print('V_test before threshold operation: ', V_test)
V_test, spiking = threshold(V_test, 5, pm)
print('V_test after threshold operation:  ', V_test)
print(spiking)
```

    V_test before threshold operation:  [-63.32999656 -57.15609381 -52.09815899 -56.96145959 -53.37227525]
    V_test after threshold operation:   [-63.32999656 -57.15609381 -75.         -56.96145959 -75.        ]
    [False False  True False  True]

```python
### For the update make use of Euler integration
def update(V,n_neuron,I_syn,dt,pm):
    '''
    Update dynamical state of all n_neuron membrane potentials
    (assuming all neurons have same parameters pm)

    Expects:
    V        : membrane potential [mV]
    n_neuron : number of neurons
    I_syn    : injected current drive [pA]
    dt       : time increment
    pm       : parameter dictionary

    Returns:
    V        : membrane potential
    spiking  : boolean that indicates whether neuron fired or not
    '''

    # ADD CODE HERE
    dVdt = ((pm['V_L']-V) + I_syn/pm['g_L'])/pm['tau_m']
    V = V + dt*dVdt
    V, spiking = threshold(V, n_neuron, pm)
    return V, spiking

```

```python
# test update()
n_test = 5
isyn_test = init_I_syn(n_test, 10, 5)
V_test = np.random.rand(5)*(pm['V_th']-pm['V_reset']) * 1.2 + pm['V_reset']
print('V_test before threshold operation: ', V_test)
V_test, spiking = update(V_test, n_test, isyn_test, 0.1, pm)
print('V_test after threshold operation:  ', V_test)
print(spiking)
```

    V_test before threshold operation:  [-74.77202524 -68.30202617 -55.71537597 -58.43704214 -57.03846676]
    V_test after threshold operation:   [-74.72188726 -68.28542977 -55.76111342 -58.46598901 -57.07515913]
    [False False False False False]

```python
def run_LIF(n_neuron,mu,sig,T,dt,pm):
    '''
    Run LIF dynamics

    Expects:
    n_neuron   : number of neurons
    mu,sig     : mean and standard deviation of initial input current drives [pA]
    T          : total run time [ms]
    dt         : time increment
    pm         : parameter dictionary

    Returns:
    rec_spikes : dictionary of lists of spike times
    '''

    # ADD CODE HERE
    rec_spikes = {i:[] for i in range(n_neuron)}  # Initialize dictionary of spike trains
    t = np.arange(0, T, dt)
    V = init_V(n_neuron, pm)
    I = init_I_syn(n_neuron, mu, sig)
    for i in range(len(t)):
        V, spiking = update(V,n_neuron,I,dt,pm)
        # add spike times to spike train dictionary
        for j in np.where(spiking)[0]:
            rec_spikes[j].append(t[i] + dt)
    return rec_spikes

```

```python
def plot_raster(spks:dict)->None:
    '''
    Plot raster plot

    Expects:
    spks      : dictionary of spike trains

    Returns:
    None
    '''
    for i in spks.keys():
        plt.plot(spks[i], i*np.ones(len(spks[i])), '|k')
```

```python
# test LIF_run()
# if all worked, uncomment codes below to run LIF dynamics for n_neuron uncoupled neurons

n_neuron   = 100
mu, sig    = 240,100
T,dt       = 100,0.1

rec_spikes = run_LIF(n_neuron,mu,sig,T,dt,pm)
plot_raster(rec_spikes)
```

### >>> EXTRA: LIF driven by Gaussian white noise and Ornstein-Uhlenbeck current <<<

So far the neurons were driven by constant currents, with amplitudes drawn randomly from a Gaussian distribution.

1) Set up a version where the input current is (approximately) Gaussian white noise $\xi(t)$ with mean $E[\xi(t)]=\mu=0$ and autocovariance $E[\xi(t)\xi(t+\tau)]=\sigma_\xi^2 \delta(\tau)$.

Hint: In order to have correct statistics of the GWN approximation on a time grid with increment $dt$ the noise amplitide $\sigma_\xi$ needs to be scaled by $1/\sqrt{dt}$.

Make sure to test that the mean and variance of the current input your function produces has the right mean and variance.

2) Set up a version where the input current is an Ornstein-Uhlenbeck process $\eta(t)$, i.e., low-pass filtered GWN with a time constant $\tau_{\eta}$:

$$\tau_\eta \frac{d}{dt}\eta(t) = -\eta + \sigma_\eta\sqrt{2\tau_\eta}\xi(t)$$

Make sure to test that the mean and variance of the current input your function produces has the right mean and variance.

Hint: An OUP as defined above has $E[\eta(t)]=0$ and autocovariance $[\eta(t)\eta(t+\tau)]=\sigma_\eta^2e^{-|t-\tau|/\tau_\eta}$.


# Divvy usage by time-of-day

**authors:** Peter Carbonetto, Gao Wang

Here we use the Divvy trip data to examine biking trends over the course of a typical day in Chicago.

We begin by loading a few packages, as well as some additional R functions implemented for this project. The `repr` package is used to adjust the dimensions of the figures in the Jupyter notebook.

```R
library(repr)
library(data.table)
library(ggplot2)
source("../code/functions.R")
```

## Read the data

Following [our earlier steps](first-glance.html), we use function `read.divvy.data` to read the trip and station data from the CSV files.

```R
divvy <- read.divvy.data()
```

    Reading station data from ../data/Divvy_Stations_2016_Q4.csv.
    Reading trip data from ../data/Divvy_Trips_2016_Q1.csv.
    Reading trip data from ../data/Divvy_Trips_2016_04.csv.
    Reading trip data from ../data/Divvy_Trips_2016_05.csv.
    Reading trip data from ../data/Divvy_Trips_2016_06.csv.
    Reading trip data from ../data/Divvy_Trips_2016_Q3.csv.
    Reading trip data from ../data/Divvy_Trips_2016_Q4.csv.
    Preparing Divvy data for analysis in R.
    Converting dates and times.

To make it easier to compile statistics by time of day, we convert the "start hour" column to a factor.

```R
divvy$trips <- transform(divvy$trips,start.hour = factor(start.hour,0:23))
```

## Count departures by time-of-day

Now that `start.hour` is a factor, it is easy to create a bar chart showing the total number of departures at each hour. Unsurprisingly, we see very little biking activity at night. Further, the two peaks ("modes") in the bar chart nicely recapitulate the morning and afternoon rush hours.

```R
options(repr.plot.width = 4.75, repr.plot.height = 2.25)
ggplot(divvy$trips,aes(start.hour)) +
  geom_bar(fill = "dodgerblue",width = 0.6) +
  theme_minimal() +
  theme(panel.grid.major = element_blank(),
        panel.grid.minor = element_blank())
```

However, this bar chart is a bit muddled because it is counting trips during the week and on the weekends. Consider that the bin count $x[h]$ for hour $h$ in the histogram above is a sum of the counts for each day of the week:

$$
\begin{align}
x[h]
& = \sum_{i\;\in\;\mathsf{DaysOfTheWeek}} x_i[h] \\
& = x_{\mathsf{Mon}}[h] + x_{\mathsf{Tue}}[h] + x_{\mathsf{Wed}}[h] +
x_{\mathsf{Thu}}[h] + x_{\mathsf{Fri}}[h] + x_{\mathsf{Sat}}[h] +
x_{\mathsf{Sun}}[h]
\end{align}
$$

*Note:* The math above is embedded in the webpage using [MathJax](https://github.com/mathjax/MathJax). See [here](https://math.meta.stackexchange.com/questions/5020/mathjax-basic-tutorial-and-quick-reference) for an excellent reference on MathJax.

Once we plot the counts separately for each the day of the week, the
rush-hour trends become more obvious. (Also notice that the rush-hour
weeks disappear on Saturday and Sunday.)

```R
options(repr.plot.width = 5, repr.plot.height = 5)
ggplot(divvy$trips,aes(start.hour)) +
  geom_bar(fill = "dodgerblue",width = 0.75) +
  facet_wrap(~start.day,ncol = 2) +
  scale_x_discrete(breaks = seq(0,24,2)) +
  theme_minimal() +
  theme(panel.grid.major = element_blank(),
        panel.grid.minor = element_blank())
```

## Time-of-day trends at University of Chicago

The commuting trends are different at the University of Chicago Divvy station---there isn't much of a morning rush hour. This may be because students and staff don't regularly use the Divvy bikes for commuting.

```R
options(repr.plot.width = 5, repr.plot.height = 5)
ggplot(subset(divvy$trips,from_station_name == "University Ave & 57th St"),
       aes(start.hour)) +
  geom_bar(fill = "dodgerblue",width = 0.75) +
  facet_wrap(~start.day,ncol = 2) +
  scale_x_discrete(breaks = seq(0,24,2)) +
  theme_minimal() +
  theme(panel.grid.major = element_blank(),
        panel.grid.minor = element_blank())
```

## Session information

This is the version of Jupyter used to generate these results.

```R
system("jupyter --version",intern = TRUE)
```

'4.3.0'

This is the version of R and the packages that were used to generate these results.

```R
sessionInfo()
```

    R version 3.3.2 (2016-10-31)
    Platform: x86_64-apple-darwin13.4.0 (64-bit)
    Running under: macOS Sierra 10.12.5

    locale:
    [1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

    attached base packages:
    [1] stats     graphics  grDevices utils     datasets  methods   base

    other attached packages:
    [1] ggplot2_2.2.1     data.table_1.10.4 repr_0.12.0

    loaded via a namespace (and not attached):
     [1] Rcpp_0.12.11     assertthat_0.2.0 digest_0.6.12    crayon_1.3.2
     [5] IRdisplay_0.4.4  plyr_1.8.4       grid_3.3.2       R6_2.2.0
     [9] jsonlite_1.5     gtable_0.2.0     magrittr_1.5     scales_0.4.1
    [13] evaluate_0.10.1  stringi_1.1.2    lazyeval_0.2.0   uuid_0.1-2
    [17] IRkernel_0.7     labeling_0.3     tools_3.3.2      stringr_1.2.0
    [21] munsell_0.4.3    colorspace_1.3-2 pbdZMQ_0.2-5     tibble_1.2


## Welcome to Telma's Library

### BiometricModel is a class of Telma.

This class make the basic actuarial table for one cohort or two cohorts either.

##### Using G20 dataset.

Note: this database refers to the G20 countries and that was downloaded from **kaggle**.
Click the following link to obtain it. [Dataset lifeTables](https://www.kaggle.com/cthierfelder/life-table-g20)

```python
import pandas as pd
file = r'C:\Users\Josè Valencia\Desktop\Actuarial2\lifeTable.csv'
dataset = pd.read_csv(file, index_col=0)
dataset.head()
```

      Australia_Male
      Austria_Male
      Belgium_Male
      Bulgaria_Male
      Canada_Male
      Czech Republic_Male
      Denmark_Male
      Estonia_Male
      Finland_Male
      France_Male
      ...
      Netherlands_Female
      Poland_Female
      Portugal_Female
      Russia_Female
      Slovakia_Female
      Slovenia_Female
      Spain_Female
      Sweden_Female
      United Kingdom_Female
      United States_Female

      age

      0
      100000
      100000
      100000
      100000
      100000
      100000
      100000
      100000
      100000
      100000
      ...
      100000
      100000
      100000
      100000
      100000
      100000
      100000
      100000
      100000
      100000

      1
      99528
      99600
      99620
      98938
      99488
      99698
      99689
      99582
      99740
      99586
      ...
      99649
      99493
      99693
      99279
      99526
      99747
      99713
      99759
      99583
      99415

      2
      99495
      99572
      99570
      98887
      99453
      99674
      99663
      99521
      99723
      99554
      ...
      99624
      99461
      99657
      99205
      99486
      99727
      99682
      99735
      99558
      99376

      3
      99471
      99545
      99549
      98854
      99433
      99646
      99657
      99521
      99717
      99531
      ...
      99609
      99443
      99639
      99160
      99444
      99717
      99669
      99718
      99538
      99351

      4
      99458
      99528
      99528
      98795
      99421
      99637
      99654
      99521
      99707
      99513
      ...
      99595
      99424
      99626
      99130
      99414
      99706
      99657
      99700
      99522
      99332

5 rows × 56 columns

#### Example with "Russia_Female" and "French_Female".

```python
rusas = dataset['Russia_Female']
francesas = dataset['France_Female']
```

```python
from telma import BiometricModel
```

## Basic actuarial table

| AGE| COHORT FUNCTION | DEATH |PROBABILTY OF DEATH | PROBABILITY OF LIVING | LX |CENTRAL RATE MORTALITY  | TX|LIFE EXPECTANCY|
|------|----------------------|--------------|---------------------------------------------------|----------------------------------------|---------|--------------|-----------|-----------------
| $x$    | $l(x)$                                   |$d(x)$                                   | $q(x)$                                              | $p(x)$                                   | $Lx$ |$m(x)$|$Tx$ | $e(x)$|

```python
usa_male = dataset['United States_Male']
```

```python
# USA_MALE TABLE. By passing the parameter we obtain
BiometricModel().actuarialtable(usa_male) ## e(x) is "curtate"
```

    --------------------------------------------------
    Actuarial Table of United States_Male:
    --------------------------------------------------
    Date and Time:  Sat, 24 Jul 2021 10:29:31
    Region and Country:  Liguria , IT
    Omega ω :  111

      l(x)
      dx
      qx
      px
      Lx
      Tx
      mx
      ex curtate

      0
      100000
      716.0
      0.007160
      0.992840
      99642.0
      7613388.5
      0.007186
      75.633870

      1
      99284
      47.0
      0.000473
      0.999527
      99260.5
      7513746.5
      0.000474
      75.179314

      2
      99237
      31.0
      0.000312
      0.999688
      99221.5
      7414486.0
      0.000312
      74.214920

      3
      99206
      24.0
      0.000242
      0.999758
      99194.0
      7315264.5
      0.000242
      73.238111

      4
      99182
      18.0
      0.000181
      0.999819
      99173.0
      7216070.5
      0.000182
      72.255833

      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...

      106
      51
      24.0
      0.470588
      0.529412
      39.0
      75.0
      0.615385
      0.941176

      107
      27
      13.0
      0.481481
      0.518519
      20.5
      36.0
      0.634146
      0.777778

      108
      14
      7.0
      0.500000
      0.500000
      10.5
      15.5
      0.666667
      0.500000

      109
      7
      4.0
      0.571429
      0.428571
      5.0
      5.0
      0.800000
      0.000000

      110
      3
      3.0
      1.000000
      0.000000
      NaN
      0.0
      NaN
      NaN

111 rows × 8 columns

#### I can pass more cohorts to the function to have the respective table. Russian and North American example

```python
rusas = dataset.Russia_Female
usa_female = dataset['United States_Female']
```

```python
BiometricModel().actuarialtable2(rusas, usa_female) # It follows the order lx = Russian, ly = American
```

    ----------------------------------------------------------------------
    Actuarial Table of Russia_Female and United States_Female:
    ----------------------------------------------------------------------
    Date and Time:  Sat, 24 Jul 2021 10:30:56
    Region and Country: Liguria , IT
    Omega 1 ω :  111
    Omega 2 ω :  111

      l(x)
      l(y)
      dx
      dy
      qx
      qy
      px
      py
      Lx
      Ly
      Tx
      Ty
      mx
      my
      ex
      ey

      0
      100000
      100000
      721.0
      585.0
      0.007210
      0.005850
      0.992790
      0.994150
      99639.5
      99707.5
      7469927.5
      8103792.0
      0.007236
      0.005867
      74.199270
      80.537840

      1
      99279
      99415
      74.0
      39.0
      0.000745
      0.000392
      0.999255
      0.999608
      99242.0
      99395.5
      7370288.0
      8004084.5
      0.000746
      0.000392
      73.738132
      80.011759

      2
      99205
      99376
      45.0
      25.0
      0.000454
      0.000252
      0.999546
      0.999748
      99182.5
      99363.5
      7271046.0
      7904689.0
      0.000454
      0.000252
      72.793135
      79.043159

      3
      99160
      99351
      30.0
      19.0
      0.000303
      0.000191
      0.999697
      0.999809
      99145.0
      99341.5
      7171863.5
      7805325.5
      0.000303
      0.000191
      71.826170
      78.063049

      4
      99130
      99332
      31.0
      15.0
      0.000313
      0.000151
      0.999687
      0.999849
      99114.5
      99324.5
      7072718.5
      7705984.0
      0.000313
      0.000151
      70.847907
      77.077981

      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...

      106
      14
      198
      7.0
      86.0
      0.500000
      0.434343
      0.500000
      0.565657
      10.5
      155.0
      19.5
      312.0
      0.666667
      0.554839
      0.857143
      1.035354

      107
      7
      112
      4.0
      51.0
      0.571429
      0.455357
      0.428571
      0.544643
      5.0
      86.5
      9.0
      157.0
      0.800000
      0.589595
      0.714286
      0.830357

      108
      3
      61
      1.0
      29.0
      0.333333
      0.475410
      0.666667
      0.524590
      2.5
      46.5
      4.0
      70.5
      0.400000
      0.623656
      0.666667
      0.524590

      109
      2
      32
      1.0
      16.0
      0.500000
      0.500000
      0.500000
      0.500000
      1.5
      24.0
      1.5
      24.0
      0.666667
      0.666667
      0.000000
      0.000000

      110
      1
      16
      1.0
      16.0
      1.000000
      1.000000
      0.000000
      0.000000
      NaN
      NaN
      0.0
      0.0
      NaN
      NaN
      NaN
      NaN

111 rows × 16 columns

# Evaluating a different cohort with different length

### The dummy model

###### The model will be specified as:
$$F(X) = 1- e^{-0.0001x^{e^{x/115}}}$$

$$S(X) = 1-F(X)= e^{-0.0001x^{e^{x/115}}}$$

$Clearly:$

  \begin{equation}
   1- S(X) = F(X)=
    \begin{cases}
      0, & \text{if}\ x < 0 \\
      1 - e^{-0.0001x^{e^{x/115}}}, & \text{if}\ 0 \le X < 115\\
      1, & \text{if}\ x \geq\ 115
    \end{cases}
  \end{equation}
$F(115) = 1$ if $1 - e^{(-0.0001 \cdot 115^{e^{(115/115)}})}.$ Consequently: $S(115) = 0$.

```python
import numpy as np
def vettore(): # the fictious model
    x = np.arange(0,115)
    numera = np.exp(-.0001*x**(np.exp(x/115)))
    l = pd.Series(numera*10**5)
    vettore = l[0:-8].round()
    return vettore
modelo = vettore()
modelo # Age and livings of the model
```

    0      100000.0
    1       99990.0
    2       99980.0
    3       99969.0
    4       99958.0
             ...
    102        54.0
    103        20.0
    104         7.0
    105         2.0
    106         0.0
    Length: 107, dtype: float64

##### Notice that omega $\omega$ is when the cohort desappear

```python
BiometricModel().actuarialtable2(francesas,modelo) # French and Dummy model
```

    ----------------------------------------------------------------------
    Actuarial Table of France_Female and None:
    ----------------------------------------------------------------------
    Date and Time:  Sat, 24 Jul 2021 10:35:03
    Region and Country: Liguria , IT
    Omega 1 ω :  111
    Omega 2 ω :  106

      l(x)
      l(y)
      dx
      dy
      qx
      qy
      px
      py
      Lx
      Ly
      Tx
      Ty
      mx
      my
      ex
      ey

      0
      100000
      100000
      336.0
      10.0
      0.003360
      0.00010
      0.996640
      0.99990
      99832.0
      99995.0
      8444605.0
      7789332.0
      0.003366
      0.00010
      83.945990
      77.393320

      1
      99664
      99990
      26.0
      10.0
      0.000261
      0.00010
      0.999739
      0.99990
      99651.0
      99985.0
      8344773.0
      7689337.0
      0.000261
      0.00010
      83.228999
      76.401060

      2
      99638
      99980
      19.0
      11.0
      0.000191
      0.00011
      0.999809
      0.99989
      99628.5
      99974.5
      8245122.0
      7589352.0
      0.000191
      0.00011
      82.250718
      75.408702

      3
      99619
      99969
      12.0
      11.0
      0.000120
      0.00011
      0.999880
      0.99989
      99613.0
      99963.5
      8145493.5
      7489377.5
      0.000120
      0.00011
      81.266405
      74.416999

      4
      99607
      99958
      11.0
      12.0
      0.000110
      0.00012
      0.999890
      0.99988
      99601.5
      99952.0
      8045880.5
      7389414.0
      0.000110
      0.00012
      80.276195
      73.425189

      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...

      106
      194
      0
      91.0
      0.0
      0.469072
      1.00000
      0.530928
      0.00000
      148.5
      0.0
      284.0
      0.0
      0.612795
      NaN
      0.932990
      0.000000

      107
      103
      0
      51.0
      0.0
      0.495146
      1.00000
      0.504854
      0.00000
      77.5
      0.0
      135.5
      0.0
      0.658065
      NaN
      0.757282
      0.000000

      108
      52
      0
      26.0
      0.0
      0.500000
      1.00000
      0.500000
      0.00000
      39.0
      0.0
      58.0
      0.0
      0.666667
      NaN
      0.500000
      0.000000

      109
      26
      0
      14.0
      0.0
      0.538462
      1.00000
      0.461538
      0.00000
      19.0
      0.0
      19.0
      0.0
      0.736842
      NaN
      0.000000
      0.000000

      110
      12
      0
      12.0
      0.0
      1.000000
      1.00000
      0.000000
      0.00000
      NaN
      NaN
      0.0
      0.0
      NaN
      NaN
      NaN
      NaN

111 rows × 16 columns

### Calculating Omega of one or two cohorts

```python
BiometricModel().omega(rusas,modelo)  ## Omega can evaluate two cohorts
```

    (111, 106)

```python
BiometricModel().omega(modelo)  # likewise Omega can evaluate one cohort
```

    106

```python
BiometricModel().omega(usa_male)
```

    111

```python
len(usa_male), len(modelo) #len is length. Is is length in R
```

    (111, 107)

#### Note que no es del todo trivial en cuanto la longitud de lx puede no ser la misma.
De hecho el modelo fake que plantee tiene longitud 107, pero de hecho en 106 que la coorte desaparece. (inf_actuarial)
Por otro lado la longitudad de rusas tambien es 111 pero de hecho a la edad 110 hay aùn una mujer, por lo que el infinito actuarial es 111.

#### Calculation of probabilities such as life expectancy.
##### It can be calculated if it is curtate or complete. By default it is complete.

Example.

```python
BiometricModel().life_expectancy(usa_male, age=0, kind ='complete') # Complete life expectancy at birth for a usa_male
```

    76.13

```python
BiometricModel().life_expectancy(usa_male, age=0, kind ='curtate') # Curtate life expectancy at birth for a usa_male
```

    75.63

[Usa_male](#gringo) click here to return to the American male table and check results with the actuarial table.

```python
BiometricModel().Tx(usa_male, age=1)
```

    7513746.5

```python
BiometricModel().Lx(usa_male, age=4)
```

    99173.0

```python
BiometricModel().central_rate_mortality(usa_female, age=2)
```

    0.0002516

### Calculating Probabilities with OneLife and MultipleLives

```python
from telma import MultipleLives, OneLife
```

### Una cohorte

```python
OneLife().p_x(francesas, age=23)
```

    The probability that 23 will survive within 1 is:

    0.9997483441042057

```python
OneLife().p_x(francesas, age=23, t=10)
```

    The probability that 23 will survive within 10 is:

    0.9968694006563186

```python
OneLife().p_x(francesas, age=23, t=30, capital = 100000, lump_sum=True) ## Is can calculate lump sump
```

    The life insurance to pay is:

    (53561.02, 0.5356102381659709)

```python

```

```python
italia = pd.read_csv('italiani.csv').x  ## Old italian cohort sim92
italia
```

    0      100000.0
    1       99112.0
    2       99061.0
    3       99025.0
    4       98997.0
             ...
    104        28.0
    105        13.0
    106         6.0
    107         2.0
    108         1.0
    Name: x, Length: 109, dtype: float64

```python
OneLife().p_x(italia, age=23, t=30, capital = 100000, lump_sum=True) ## It can calculate lump sump if True
```

    The life insurance to pay is:

    (51575.35, 0.5157534761499296)

### Joint Probabilities

|Function|Notation|The story|
|-----|----------------|-------|
|p_xy|$p_{xy}$ = $p_x \cdot p_y$| Both survive|
|q_xy|$q_{xy}$= $q_x \cdot p_y+ q_y \cdot p_x$ + $q_x \cdot q_y$|At least one die|
|extinction|$q_\bar{{xy}}$=$q_x \cdot q_y$|Nobody survive|
|no extinction|$p_{\bar{xy}}$ = $p_x \cdot q_y + q_x \cdot p_y+ p_x \cdot p_y $|At least one survive|
|dissolution_no_ex|$p_{\frac{[1]}{xy}}$=$p_x \cdot q_y +q_x \cdot p_y $|It survive exactly one|

```python
MultipleLives().q_xy(usa_male, usa_female, age_x=60, age_y=63, t=3) # qxyt status = 'joint' en R --joint_life
```

    The probability that at least one of lives 60 and 63 will be dead within 3 years is:

    0.06256902018899291

```python
MultipleLives().extinction(usa_male, usa_female, age_x=60, age_y=63, t=3) # qxyt status = 'last' en R --last_survivor
```

    The probability that both lives 60 and 63 will be dead within 3 years is:

    0.000994521015274752

```python
MultipleLives().no_extinction(usa_male, usa_female, age_x=60, age_y=63, t=3) # qxyt status = 'last' en R --last_survivor
```

    The probability that at least one of lives 60 and 63 will be alive after 3 years is:

    0.9990054789847252

```python
MultipleLives().p_xy(usa_male, usa_female, age_x=60, age_y=63, t=3) # pxyt status = 'joint' en R
```

    The probability that both lives 60 and 63 will be alive after 3 years is:

    0.9374309798110071

```python
MultipleLives().dissolution_no_ex(usa_male, usa_female, age_x=60, age_y=63, t=3) # sobrevive exactamente 1
```

    0.061574499173718156

```python
0.06256902018899291 - 0.000994521015274752 # That is dissoltion and no extinction
```

    0.061574499173718156

```python
MultipleLives().p_xy(italia, italia, age_x=22, age_y=44, t=6)
```

    The probability that both lives 22 and 44 will be alive after 6 years is:

    0.9728491313971115

```python
MultipleLives().q_xy(italia, italia, age_x=20, age_y=22, t=3) # pxyt status = 'joint' en R
```

    The probability that at least one of lives 20 and 22 will be dead within 3 years is:

    0.007177736364576526

## References

*  Ayuso, Mercedes; Guillen Estany, Montserrat; Rojo García, José Luis; Pérez Marín, Ana Maria. Estadistica actuarial vida