---
### *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.319s
    Last 5 elements in a:  [1999990. 1999992. 1999994. 1999996. 1999998.]
    Python looping takes 0.005s
    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.

---
## Exercise: 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
    '''
    # Fill up code below:
    fv = ...
    fw = ...
    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)):
        # Fill up code here

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

```python
# test init_V()

# ADD CODE HERE
```

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

```

```python
# test init_I_syn()

# ADD CODE HERE
```

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

```

```python
# test threshold()

# ADD CODE HERE
```

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

```

```python
# test update()

# ADD CODE HERE
```

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

```

```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,p)
# 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}$.


```python
import grtoolkit.Circuits as c
import grtoolkit.Circuits.Capacitors as cc

```

```python
from sympy.abc import *
from sympy import *
```

```python
from math import *
from cmath import *
```

```python
class signal:
    """
    Types: "rect" "polar_rad" "polar_deg"
    https://docs.python.org/2/reference/datamodel.html#emulating-numeric-types
    """
    def __init__(self, val=complex(0,0), format="rect"):
        """
        Types: "rect" "polar_rad" "polar_deg" "wave_deg" "wave_rad"

        Input:
            rect: val = complex(0,0)
            polar_rad = (A,phi_rad)
            polar_deg = (A, phi_deg)
            wave = (A, "cos", w, phi) or (A, "sin", w, phi)

        """

        if format == "rect":
            self.rect = val
        elif format == "polar_rad":
            self.polar_rad = val
        elif format == "polar_deg":
            self.polar_deg = val
        elif "wave" in format:
            self.wave_original = val
            self.wave_original_type = format
            phasor = ()
            val = list(val)
            if "deg" in format:
                val[3] = radians(val[3])
            if val[1] == "cos":
                phasor = self.__cos2Phasor(val[0], val[3])
            elif val[1] == "sin":
                phasor = self.__sin2Phasor(val[0], val[3])
            else:
                raise 'Not a valid sinusoid. Format must be (A, "cos", w, phi) or (A, "cos", w, phi)'
            self.polar_rad = (phasor[0], phasor[1])
        else:
            raise 'type must be: "rect" "polar_rad" "polar_deg"'

    @property
    def rect(self):
        return self._rect
    @rect.setter
    def rect(self, val):
        self._rect = val
        self._polar_rad = self.__rect2polar(self._rect)
        self._polar_deg = self.__polar_deg_view()

    @property
    def polar_rad(self):
        return self._polar_rad
    @polar_rad.setter
    def polar_rad(self, val):
        self._polar_rad = val
        self._polar_deg = self.__polar_deg_view()
        self._rect = self.__polar2rect(self._polar_rad)

    def __polar_deg_view(self):
        """
        Does not return actual polar as actual polar needs to be in radians.
        For viewing ONLY.
        """
        polar = self._polar_rad
        polar_fix = list()
        polar_fix.append(polar[0])
        polar_fix.append(degrees(polar[1]))
        return polar_fix

    @property
    def polar_deg(self):
        return self._polar_deg
    @polar_deg.setter
    def polar_deg(self, val):
        self._polar_deg = val
        self._polar_rad = self.__polar_rad_view()
        self._rect = self.__polar2rect(self._polar_rad)

    @property
    def sinusoid(self):
        return self._sinusoid
    @sinusoid.setter
    def sinusoid(self, val):
        self._sinusoid = val

    def __polar2rect(self, r,phi=0):
        """
        Output: class <complex>
        """
        if isinstance(r,tuple) or isinstance(r,list):
            return rect(r[0],r[1])
        return rect(r,phi)

    def __rect2polar(self,z):
        """
        Polar cannot do math.
        Output: class <tuple>
        """
        return polar(z)

    def __polar_rad_view(self):
        """
        Does not return actual polar as actual polar needs to be in radians.
        For viewing ONLY.
        """
        polar = self._polar_deg
        polar_fix = list()
        polar_fix.append(polar[0])
        polar_fix.append(radians(polar[1]))
        return polar_fix

    def __cos2Phasor(self, A,phi):
        """
        Format: A*cos(wt+phi)
        Output: [A, phi] which represents polar form A angle(phi)
        """
        if A < 0:
            return self.__cos2Phasor(-A, phi+radians(180))
        else:
            return A, phi
    def __sin2Phasor(self, A, phi):
        if A < 0:
            return self.__sin2Phasor(-A, phi+radians(180))
        else:
            return A, phi-radians(90)

    def sin_wave(self, format="rad"):
        if format == "deg":
            return f"{self._polar_rad[0]} sin (wt * {degrees(self.polar_rad[1]-radians(90))})"
        return f"{self._polar_rad[0]} sin (wt * {self.polar_rad[1]-radians(90)})"

    def cos_wave(self, format="rad"):
        if format == "deg":
            return f"{self._polar_rad[0]} cos (wt * {degrees(self.polar_rad[1])})"
        return f"{self._polar_rad[0]} cos (wt * {self.polar_rad[1]})"

    def __add__(self,other):
        return signal(self._rect + other._rect)
    def __radd__(self, other):
        return self.__add__(other)

    def __sub__(self,other):
        return signal(self._rect - other._rect)
    def __rsub__(self, other):
        #Doesn't work?
        return other - self._rect

    def __mul__(self,other):
        return signal(self._rect * other._rect)
    def __rmul__(self, other):
        return self.__mul__(other)

    def __truediv__(self,other):
        return signal(self._rect / other._rect)
    def __rtruediv__(self,other):
        # return signal(other / self._rect)
        return other / self._rect

    def __pow__(self,other):
        # return signal(other / self._rect)
        return (self.rect**(other))

    def sqrt(self):
        return signal(self.rect**(1/2))

## NOTE:
# DERIVATIVE
# dv/dt      ==      jwV
# time domain    frquency domain

# INTEGRAL
# integrate(v,t)    ==    V/jw
# time domain          frquency domain

```

```python
z1 = signal(complex(5,2))
z2 = signal(complex(-1,4))
z3 = signal((-5,60), format="polar_deg")

```

```python
z4 = z1*z2+z3
z5 = z4.rect.conjugate()
z5
```

    (-15.5-13.669872981077807j)

```python

```


[Sascha Spors](https://orcid.org/0000-0001-7225-9992),
Professorship Signal Theory and Digital Signal Processing,
[Institute of Communications Engineering (INT)](https://www.int.uni-rostock.de/),
Faculty of Computer Science and Electrical Engineering (IEF),
[University of Rostock, Germany](https://www.uni-rostock.de/en/)

# Tutorial Signals and Systems (Signal- und Systemtheorie)

Summer Semester 2021 (Bachelor Course #24015)

- lecture: https://github.com/spatialaudio/signals-and-systems-lecture
- tutorial: https://github.com/spatialaudio/signals-and-systems-exercises

WIP...
The project is currently under heavy development while adding new material for the summer semester 2021

Feel free to contact lecturer [frank.schultz@uni-rostock.de](https://orcid.org/0000-0002-3010-0294)

## Übung / Exercise 7 Ideal Dirac Comb Sampling and Ideal Lowpass Reconstruction for Time Domain Signals

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

def my_sinc(x):  # we rather use definition sinc(x) = sin(x)/x, thus:
    return np.sinc(x/np.pi)
```

The signal

\begin{align}
x(t)= \sin(3 t)
\end{align}

is to be ideally sampled (multiplied) with a Dirac comb

\begin{align}
h_s(t) = \frac{2}{\pi} {\bot \!\! \bot \!\! \bot}(\frac{2 t}{\pi})
\end{align}

and then to be reconstructed with an ideal lowpass filter that exhibits the Fourier transform spectrum

\begin{align}
H(\mathrm{j}\omega) = H_r(\mathrm{j}\omega) = \frac{\pi}{2}\mathrm{rect}(\frac{\omega}{4})
\end{align}

The sampling model is given below as block diagram. Following that

* 1a.) Calculate and sketch the Fourier transform spectra of $x(t)$, $h_s(t)$, $x_a(t)=x_s(t)$ and $x_r(t)$.
* 1b.) Sketch $H_r(\mathrm{j}\omega)$ and $h_r(t)$.
* 2.) Calculate the signal $x_r(t)$.
* 3.) Can we successfully reconstruct the signal $x(t)$? Why?

# Solution Preliminaries

### Dirac Impulse Comb

The Fourier transform pair of the Dirac comb reads

\begin{equation}
\frac{1}{T} {\bot \!\! \bot \!\! \bot}(\frac{t}{T}) \circ\!\!-\!\!\bullet {\bot \!\! \bot \!\! \bot}(\frac{\omega T}{2 \pi}) = {\bot \!\! \bot \!\! \bot}(\frac{\omega}{\omega_s})
\end{equation}

with sampling interval $T = \frac{1}{f_s}>0$ and angular sampling frequency $\omega_s = \frac{2 \pi}{T}$.

The Dirac comb is defined as
\begin{equation}
{\bot \!\! \bot \!\! \bot}(t) := \sum_{m=-\infty}^{+\infty} \delta(t-m) \quad\mathrm{for}\quad m\in\mathbb{Z}.
\end{equation}

With the scaling property of the Dirac
\begin{equation}
\delta(a t) = \frac{1}{|a|} \delta(t) \longrightarrow \delta(t) = |a| \delta(a t)
\end{equation}
we get for a shifted Dirac by sampling interval $T$
\begin{equation}
\delta(t - m T) = \frac{1}{T}\cdot \delta(\frac{t - m T}{T}) = \frac{1}{T} \cdot \delta(\frac{t}{T}-m).
\end{equation}
Thus, scaling of the Dirac comb definition is
\begin{equation}
\sum_{m=-\infty}^{+\infty} \delta(t-m T) = \frac{1}{T}\sum_{m=-\infty}^{+\infty} \delta(\frac{t}{T}-m) = \frac{1}{T} {\bot \!\! \bot \!\! \bot}(\frac{t}{T}).
\end{equation}

When using another summing variable, the Dirac comb definition simply becomes
\begin{equation}
{\bot \!\! \bot \!\! \bot}(\omega) := \sum_{\mu=-\infty}^{+\infty} \delta(\omega-\mu) \quad\mathrm{for}\quad \mu\in\mathbb{Z}.
\end{equation}

We can use this to motivate Dirac shifting in frequency domain by intervalls of the sampling frequency.
Scaling property again yields

\begin{equation}
\delta(\omega - \mu \omega_s) = \frac{1}{\omega_s}\delta(\frac{\omega - \mu \omega_s}{\omega_s}) = \frac{1}{\omega_s} \cdot \delta(\frac{\omega}{\omega_s}-\mu),
\end{equation}

and therefore the scaling of the Dirac comb for frequency domain is

\begin{equation}
\omega_s \sum_{\mu=-\infty}^{+\infty} \delta(\omega-\mu \omega_s) = \sum_{\mu=-\infty}^{+\infty} \delta(\frac{\omega}{\omega_s}-\mu) = {\bot \!\! \bot \!\! \bot}(\frac{\omega}{\omega_s}).
\end{equation}

# Solution

* 1.)

## Dirac Comb

Let us start with the Dirac comb that models ideal sampling.

For $T = \frac{\pi}{2}$ we have $\omega_s = \frac{2 \pi}{\frac{\pi}{2}} = 4$ and thus

\begin{equation}
h_s(t) = \frac{2}{\pi} {\bot \!\! \bot \!\! \bot}(\frac{2 t}{\pi}) \circ-\bullet H_s(\mathrm{j}\omega) = {\bot \!\! \bot \!\! \bot}(\frac{\omega}{4}) = 4 \sum_{\mu=-\infty}^{+\infty} \delta(\omega-4 \mu)
\end{equation}

 for which the Fourier transform is depicted below in the range $|\omega| < 12$ rad/s.

```python
plt.figure(figsize=(6, 4))
ax = plt.axes()
for k in range(-12, 16, 4):
    ax.arrow(k, 0, 0, 1, head_width=0.5, head_length=0.05,
             fc='C3', ec='C3', width=0.2)
plt.text(0, 1.05, '(4)')
plt.yticks((0, 2))
plt.xticks(np.arange(-12, 16, 2))
plt.xlim(-13, 13)
plt.ylim(0, 1.2)
plt.yticks(np.arange(-1.5, 2, 0.5), ['', '', '', '0', '', '', ''])
plt.grid(True)
plt.xlabel(r'$\omega$ / (rad/s)')
plt.ylabel(r'$S(\mathrm{j} \omega)$')
plt.savefig('Sampling_01_DiracComb_EF235EE3D8.pdf')
```

## Sine Signal

The harmonic signal with $\omega_0=3$ rad/s

\begin{equation}
x(t) = \sin(\omega_0 t) = \sin(3 t)
\end{equation}

is to be sampled with the above introduced Dirac comb.

The Fourier transform

\begin{equation}
X(\mathrm{j} \omega) = \mathrm{j} \pi [ \delta(\omega+\omega_0) - \delta(\omega-\omega_0) ] = \mathrm{j} \pi [\delta(\omega+3) - \delta(\omega-3)]
\end{equation}

of $x(t)$ is depicted below.
Note that the spectrum exhibits an imaginary part only.

For convenience a red line is drawn between the two Dirac entries of the Fourier transform. This will help clarify the aliasing contributions in the spectrum of the sampled signal.

```python
plt.figure(figsize=(6, 4))
ax = plt.axes()
ax.arrow(+3, 0, 0, -1, head_width=0.5,
         head_length=0.05, fc='C0', ec='C0', lw=3)
ax.arrow(-3, 0, 0, +1, head_width=0.5,
         head_length=0.05, fc='C0', ec='C0', lw=3)
plt.plot((-3, 3), (+1, -1), 'C0', lw=0.5)
plt.text(-2.6, 1.05, r'($+\pi$)')
plt.text(+3.4, -1.05, r'($-\pi$)')
plt.yticks((0, 2))
plt.xticks(np.arange(-12, 16, 2))
plt.xlim(-13, 13)
plt.ylim(-1.2, 1.2)
plt.yticks(np.arange(-1.5, 2, 0.5), ['', '', '', '0', '', '', ''])
plt.grid(True)
plt.xlabel(r'$\omega$ / (rad/s)')
plt.ylabel(r'$\Im\{(X(\mathrm{j} \omega)\}$')
plt.savefig('Sampling_02_Sine_EF235EE3D8.pdf')
```

## Sampling

The sampling process is modeled as

\begin{equation}
x_s(t) = x(t) \cdot h_s(t) \circ-\bullet X_s(\mathrm{j}\omega) = \frac{1}{2\pi} X(\mathrm{j}\omega) * H_s(\mathrm{j}\omega),
\end{equation}

where the convolution with the Dirac comb in frequency domain is conveniently used to obtain the spectrum of $x_s(t)$.

This Fourier transform is depicted below.

Note that the convolution is with respect of a pure real signal (Dirac comb) and a pure imaginary signal (sine) leading to a pure imaginary result.

```python
plt.figure(figsize=(6, 4))
ax = plt.axes()

for k in range(-5, 5):
    if k == 0:
        ax.arrow(+3+4*k, 0, 0, -1, head_width=0.5,
                 head_length=0.05, fc='C0', ec='C0', lw=3)
        ax.arrow(-3+4*k, 0, 0, +1, head_width=0.5,
                 head_length=0.05, fc='C0', ec='C0', lw=3)

        ax.arrow(+3+4*k, 0, 0, -1, head_width=0.5,
                 head_length=0.05, fc='k', ec='k', lw=1)
        ax.arrow(-3+4*k, 0, 0, +1, head_width=0.5,
                 head_length=0.05, fc='k', ec='k', lw=1)

        plt.plot((-3+4*k, 3+4*k), (+1, -1), 'C0', lw=0.5)
        plt.text(+3.4+4*k, -1.05, '(-2)')
        plt.text(-2.6+4*k, +1.05, '(+2)')
    else:
        ax.arrow(+3+4*k, 0, 0, -1, head_width=0.5,
                 head_length=0.05, fc='C3', ec='C3', lw=2)
        ax.arrow(-3+4*k, 0, 0, +1, head_width=0.5,
                 head_length=0.05, fc='C3', ec='C3', lw=2)
        ax.arrow(+3+4*k, 0, 0, -1, head_width=0.5,
                 head_length=0.05, fc='k', ec='k', lw=1)
        ax.arrow(-3+4*k, 0, 0, +1, head_width=0.5,
                 head_length=0.05, fc='k', ec='k', lw=1)
        plt.plot((-3+4*k, 3+4*k), (+1, -1), 'C3', lw=0.5)

plt.xticks(np.arange(-12, 16, 2))
plt.xlim(-13, 13)
plt.ylim(-1.2, 1.2)
plt.yticks(np.arange(-1.5, 2, 0.5), ['', '', '', '0', '', '', ''])
plt.grid(True)
plt.xlabel(r'$\omega$ / (rad/s)')
plt.ylabel(r'$\Im\{X_\mathrm{s}(\mathrm{j} \omega)\}$')
plt.savefig('Sampling_03_DiscreteSine_EF235EE3D8.pdf')
```

## Reconstruction

Reconstruction is modeled as

\begin{equation}
x_r(t) = x_s(t) * h_r(t) \circ-\bullet X_r(\mathrm{j}\omega) = X_s(\mathrm{j}\omega) \cdot H_r(\mathrm{j}\omega)
\end{equation}

with the spectrum of the ideal lowpass filter

\begin{equation}
H_r(\mathrm{j}\omega) = \frac{\pi}{2}\mathrm{rect}(\frac{\omega}{4}) \bullet - \circ h_r(t) = \mathrm{sinc}(2 t)
\end{equation}

is depicted below.

The cut frequency is $|\omega_c| = 2$ rad/s.

```python
plt.figure(figsize=(6, 4))
ax = plt.axes()
plt.plot((-13, -2, -2, +2, +2, +13), (0, 0, 1, 1, 0, 0), 'C2', lw=3)
plt.xticks(np.arange(-12, 16, 2))
plt.xlim(-13, 13)
plt.ylim(-1.2, 1.2)
plt.yticks(np.arange(-1.5, 2, 0.5),
           ['', '', '', '0', '', r'$\frac{\pi}{2}$', ''])
plt.text(2.5, 1, r'$\frac{\pi}{2}$', color='C2')
plt.grid(True)
plt.xlabel(r'$\omega$ / (rad/s)')
plt.ylabel(r'$H_\mathrm{r}(\mathrm{j} \omega)$')
plt.savefig('Sampling_04_LowpassSpectrum_EF235EE3D8.pdf')
```

The sinc-shaped, infinite (and thus not feasible) impulse response of the ideal lowpass filter is depicted below.

```python
plt.figure(figsize=(6, 4.5))
t = np.arange(-50, +50, 0.1)
hr = my_sinc(2*t)
plt.plot(t, hr, 'C2', lw=3, label=r'$h_\mathrm{r}(t)$')
plt.xlabel(r'$t$ / s')
plt.xlim(-50, +50)
plt.ylabel(r'$h_\mathrm{r}(t)$')
plt.legend()
plt.grid(True)
plt.savefig('Sampling_05_LowpassIR_EF235EE3D8.pdf')
```

We can include the spectrum $H_r(\mathrm{j} \omega)$ with proportionally correct amplitude into the graphics of $X_s(\mathrm{j} \omega)$.

This is shown in the next plot.

```python
plt.figure(figsize=(6, 4))
ax = plt.axes()
for k in range(-5, 5):
    if k == 0:
        ax.arrow(+3+4*k, 0, 0, -1, head_width=0.5,
                 head_length=0.05, fc='C0', ec='C0', lw=3)
        ax.arrow(-3+4*k, 0, 0, +1, head_width=0.5,
                 head_length=0.05, fc='C0', ec='C0', lw=3)
        ax.arrow(+3+4*k, 0, 0, -1, head_width=0.5,
                 head_length=0.05, fc='k', ec='k', lw=1)
        ax.arrow(-3+4*k, 0, 0, +1, head_width=0.5,
                 head_length=0.05, fc='k', ec='k', lw=1)
        plt.plot((-3+4*k, 3+4*k), (+1, -1), 'C0', lw=0.5)
        plt.text(+3.4+4*k, -1.05, '(-2)')
        plt.text(-2.6+4*k, +1.05, '(+2)')
    else:
        ax.arrow(+3+4*k, 0, 0, -1, head_width=0.5,
                 head_length=0.05, fc='C3', ec='C3', lw=2)
        ax.arrow(-3+4*k, 0, 0, +1, head_width=0.5,
                 head_length=0.05, fc='C3', ec='C3', lw=2)
        ax.arrow(+3+4*k, 0, 0, -1, head_width=0.5,
                 head_length=0.05, fc='k', ec='k', lw=1)
        ax.arrow(-3+4*k, 0, 0, +1, head_width=0.5,
                 head_length=0.05, fc='k', ec='k', lw=1)
        plt.plot((-3+4*k, 3+4*k), (+1, -1), 'C3', lw=0.5)

plt.text(2.5, 1/2*np.pi/2, r'$\frac{\pi}{2}$', color='C2')
plt.plot((-13, -2, -2, +2, +2, +13), (0, 0, 1/2 *
         np.pi/2, 1/2*np.pi/2, 0, 0), 'C2', lw=3)
plt.xticks(np.arange(-12, 16, 2))
plt.xlim(-13, 13)
plt.ylim(-1.2, 1.2)
plt.yticks(np.arange(-1.5, 2, 0.5), ['', '', '', '0', '', '', ''])
plt.grid(True)
plt.xlabel(r'$\omega$ / (rad/s)')
plt.ylabel(
    r'$\Im\{X_\mathrm{s}(\mathrm{j} \omega)\}$, $H_\mathrm{r}(\mathrm{j} \omega)$')
plt.savefig('Sampling_06_DiscreteSineLowpassSpectrum_EF235EE3D8.pdf')
```

The filter cancels all contributions for $|\omega|>2$ rad/s and leaves exactly two Dirac impulses at $\omega = \pm 1$ rad/s.
These contributions are aliasing contributions mirrored into the passband of the reconstruction filter.
The resulting spectrum $X_r(\mathrm{j} \omega)$ is depicted below.

Note that the result remains purely imaginary, since the lowpass spectrum is purely real.

```python
plt.figure(figsize=(6, 4))
ax = plt.axes()
ax.arrow(-1, 0, 0, -1, head_width=0.5,
         head_length=0.05, fc='C1', ec='C1', lw=2)
ax.arrow(+1, 0, 0, +1, head_width=0.5,
         head_length=0.05, fc='C1', ec='C1', lw=2)
plt.text(1.3, 1.05, r'($+\pi$)')
plt.text(-0.7, -1.05, r'($-\pi$)')
plt.yticks((0, 2))
plt.xticks(np.arange(-12, 16, 2))
plt.xlim(-13, 13)
plt.ylim(-1.2, 1.2)
plt.yticks(np.arange(-1.5, 2, 0.5), ['', '', '', '0', '', '', ''])
plt.grid(True)
plt.xlabel(r'$\omega$ / (rad/s)')
plt.ylabel(r'$\Im\{X_\mathrm{r}(\mathrm{j} \omega)\}$')
plt.savefig('Sampling_07_ReconstructedSine_EF235EE3D8.pdf')
```

* 2.)

The signal that is been reconstructed can be directly evaluated from the simple spectrum $X_r(\mathrm{j} \omega)$.
One might recognize, that these two diracs correspond to a sine function

\begin{equation}
x_r(t) = - \sin(1\cdot t) \circ-\bullet X_r(\mathrm{j} \omega) = -\mathrm{j} \pi [\delta(\omega+1) - \delta(\omega-1)]
\end{equation}

i.e. a sine with $\omega_{0,s} = 1$ rad/s (instead original $\omega_{0} = 3$ rad/s) and inverted polarity (instead of non-inverted original).

* 3.)

As seen in 2.) reconstruction was not successful, due to undersampling of the signal $x(t)$.

For correct reconstruction two conditions must be achieved:

* smaller sampling intervall, $T\leq\frac{\pi}{3}$, i.e. larger sampling frequency $\omega_s\geq 6$ rad/s. So called critical/Nyquist sampling is realized if $T=\frac{\pi}{3}$ and thus $\omega_s=\frac{2 \pi}{\frac{\pi}{3}}= 6$ rad/s.

* larger bandwidth of the lowpass filter, such that the passband includes $\omega_0$, but no aliasing contributions.For critical sampling with $\omega_s = 6$ rad/s the required cut frequency $\omega_c = 3$ rad/s for the ideal lowpass filter leads to
\begin{equation}
H_r(\mathrm{j}\omega) = \frac{\pi}{3}\mathrm{rect}(\frac{\omega}{6}) \bullet - \circ h_r(t) = \mathrm{sinc}(3 t).
\end{equation}

```python
plt.figure(figsize=(6, 4.5))
t = np.arange(-np.pi, 4*np.pi, (2*np.pi/3)/2**6)

x = np.sin(3*t)
plt.plot(t, x, 'C0', label=r'$x(t)$', lw=3)
xr = -np.sin(1*t)
plt.plot(t, xr, 'C1', label=r'$x_\mathrm{r}(t)$', lw=2)

td = np.arange(-np.pi, 4*np.pi+np.pi/2, np.pi/2)

s = td*0 + 1
plt.stem(td, s,
         use_line_collection=True,
         linefmt='C3',
         markerfmt='C3d',
         basefmt='C3', label=r'$s(t)$')
xs = np.sin(3*td)
plt.stem(td, xs,
         use_line_collection=True,
         linefmt='k:',
         markerfmt='ko',
         basefmt='k:', label=r'$x_\mathrm{s}(t)$')

plt.xticks(np.arange(0, 4*np.pi, np.pi/2),
           ['0', r'$\frac{\pi}{2}$', r'$\pi$', r'$\frac{3\pi}{2}$', r'$2\pi$', r'$\frac{5\pi}{2}$', r'$3 \pi$', r'$\frac{7\pi}{2}$'])

plt.text(-0.6, 1, '(1)', color='C3')
plt.text(-0.6+3/2*np.pi, 1, '(1)', color='C0')

plt.xlim(-np.pi/4, 3*np.pi+np.pi/4)
plt.xlabel('t / s')
plt.legend(loc='lower center')
plt.grid(True)
plt.savefig('TimeSignals_EF235EE3D8.pdf')
```

```python
def sine_sampling_sinc_interpolation():
    t = np.arange(-np.pi, 4*np.pi, (2*np.pi/3)/2**6)

    x = np.sin(w0*t)
    xr = np.zeros_like(x)

    plt.figure(figsize=(6, 4.5))

    M = 200
    for m in range(-M, +M+1):
        td = (m*Ts)
        xm = np.sin(w0*td) * my_sinc(wc*(t-td))
        plt.plot(t, xm, 'C7', lw=0.5)
        xr += xm
    plt.plot(t, xr, 'C1', lw=3, label=r'$\approx x_\mathrm{r}(t)$')

    for m in range(1, 6):
        td = (m*Ts)
        xm = np.sin(w0*td) * my_sinc(wc*(t-td))
        plt.plot(t, xm, color='C'+str(m+1), lw=2, label=r'$m=%d$' % m)

    plt.xticks(np.arange(0, 4*np.pi, np.pi/2),
               ['0', r'$\frac{\pi}{2}$', r'$\pi$', r'$\frac{3\pi}{2}$', r'$2\pi$', r'$\frac{5\pi}{2}$', r'$3 \pi$', r'$\frac{7\pi}{2}$'])
    plt.xlim(-np.pi/4, 3*np.pi+np.pi/4)
    plt.xlabel('t / s')

    plt.title(r'$x(t)=sin(\omega_0 t)$, $\omega_0=$%2.1f rad/s, $T_s=\pi/$%2.1f s, $\omega_s=$%2.1f rad/s, $\omega_c$=%2.1f rad/s' %
              (w0, np.pi/Ts, ws, wc))

    plt.plot(t, x, 'gold', lw=1, label=r'$x(t)$')

    plt.legend()
    plt.grid(True)

    print(w0, Ts, ws, wc)
```

```python
Ts = np.pi/2
ws = 2*np.pi/Ts
wc = 2
w0 = 3
sine_sampling_sinc_interpolation()
plt.savefig('SincInterpolation_Aliasing_EF235EE3D8.pdf')
```

```python
w0 = 3
ws = 8
wc = ws/2
Ts = 2*np.pi/ws
sine_sampling_sinc_interpolation()
plt.savefig('SincInterpolation_NoAliasing_EF235EE3D8.pdf')
```

## Copyright

This tutorial is provided as Open Educational Resource (OER), to be found at
https://github.com/spatialaudio/signals-and-systems-exercises
accompanying the OER lecture
https://github.com/spatialaudio/signals-and-systems-lecture.
Both are licensed under a) the Creative Commons Attribution 4.0 International
License for text and graphics and b) the MIT License for source code.
Please attribute material from the tutorial as *Frank Schultz,
Continuous- and Discrete-Time Signals and Systems - A Tutorial Featuring
Computational Examples, University of Rostock* with
``main file, github URL, commit number and/or version tag, year``.


# ¿Cómo medir rendimiento y riesgo en un portafolio? II

> La clase pasada y la presente, están dedicadas a obtener medidas de rendimiento y riesgo en un portafolio.

> Vimos que podemos obtener los rendimientos de un portafolio mediante la relación $r_p=\sum_{i=1}^{n}w_ir_i$, y una vez teniendo los rendimientos del portafolio, lo podemos tratar como un activo individual.

> Por otra parte, vimos que si conocemos los rendimientos esperados de cada activo que conforma el portafolio $E[r_i]$, podemos calcular el rendimiento esperado del portafolio como el promedio ponderado de los rendimientos esperados de los activos $E[r_p]=\sum_{i=1}^{n}w_iE[r_i]$.

> Sin embargo, vimos que esto no es válido para la medida de riesgo (desviación estándar). Es decir, la varianza (o volatilidad, o desviación estándar) no es el promedio ponderado de las varianzas individuales. Anticipamos que esto es clave en el concepto de **diversificación**.

**Objetivos:**
- Medir el riesgo en un portafolio a partir del riesgo de cada uno de los activos que lo conforman.

*Referencias*
- Notas del curso "Portfolio Selection and Risk Management", Rice University, disponible en Coursera.
___

## 1. Midiendo el riesgo en un portafolio

### 1.1. Volatilidad de un portafolio

Retomamos el ejemplo qur veníamos trabajando la clase pasada...

**Ejemplo.** Supongamos que tenemos inversión en activos de Toyota, Walmart y Pfizer. Tenemos cuatro posibles estados económicos:

```python
import numpy as np
import pandas as pd
```

```python
# Creamos tabla
tabla = pd.DataFrame(columns=['Prob', 'Toyota', 'Walmart', 'Pfizer'],
                     index=['Expansion', 'Normal', 'Recesion', 'Depresion'])
tabla.index.name = 'Estado'
tabla['Prob']=np.array([0.1, 0.4, 0.3, 0.2])
tabla['Toyota']=np.array([0.06, 0.075, 0.02, -0.03])
tabla['Walmart']=np.array([0.045, 0.055, 0.04, -0.01])
tabla['Pfizer']=np.array([0.025, -0.005, 0.01, 0.13])

tabla.round(4)
```

      Prob
      Toyota
      Walmart
      Pfizer

      Estado

      Expansion
      0.1
      0.060
      0.045
      0.025

      Normal
      0.4
      0.075
      0.055
      -0.005

      Recesion
      0.3
      0.020
      0.040
      0.010

      Depresion
      0.2
      -0.030
      -0.010
      0.130

```python
## Rendimientos esperados
# Toyota
erT = (tabla['Prob'] * tabla['Toyota']).sum()
# Walmart
erW = (tabla['Prob'] * tabla['Walmart']).sum()
# Pfizer
erP = (tabla['Prob'] * tabla['Pfizer']).sum()
# Mostrar
erT, erW, erP
```

    (0.036, 0.036500000000000005, 0.029500000000000002)

```python
## Volatilidad
# Toyota
sT = (tabla['Prob'] * (tabla['Toyota'] - erT)**2).sum()**0.5
# Walmart
sW = (tabla['Prob'] * (tabla['Walmart'] - erW)**2).sum()**0.5
# Pfizer
sP = (tabla['Prob'] * (tabla['Pfizer'] - erP)**2).sum()**0.5
# Mostrar
sT, sW, sP
```

    (0.04017461885320133, 0.02408837894089181, 0.05111017511220246)

```python
# Portafolio 0.5Toyota+0.5Pfizer
tabla['PortTP'] = 0.5 * tabla['Toyota'] + 0.5 * tabla['Pfizer']
tabla
```

      Prob
      Toyota
      Walmart
      Pfizer
      PortTP

      Estado

      Expansion
      0.1
      0.060
      0.045
      0.025
      0.0425

      Normal
      0.4
      0.075
      0.055
      -0.005
      0.0350

      Recesion
      0.3
      0.020
      0.040
      0.010
      0.0150

      Depresion
      0.2
      -0.030
      -0.010
      0.130
      0.0500

```python
# Rendimiento portafolio (como activo individual)
erTP = (tabla['Prob'] * tabla['PortTP']).sum()
# Rendimiento portafolio (como suma ponderada de rendimientos individuales)
erTP_ = 0.5 * erT + 0.5 * erP
erTP, erTP_
```

    (0.03275, 0.03275)

```python
# Volatilidad del portafolio
sTP = (tabla['Prob'] * (tabla['PortTP'] - erTP)**2).sum()**0.5
sTP
```

    0.012867109232457772

```python
# Notar que sTP < 0.5 * sT + 0.5 * sP
# la volatilidad del portafolio siempre es menor
# a la suma ponderada de las volatilidades individuales
0.5 * sT + 0.5 * sP
```

    0.045642396982701894

**Actividad.** Encontrar la volatilidad del portafolio formado $0.5$ Toyota y $0.5$ Walmart.

```python
# Encontrar los rendimientos del portafolio en cada estado de la economía
tabla['PortTW'] = 0.5 * tabla['Toyota'] + 0.5 * tabla['Walmart']
tabla
```

      Prob
      Toyota
      Walmart
      Pfizer
      PortTP
      PortTW

      Estado

      Expansion
      0.1
      0.060
      0.045
      0.025
      0.0425
      0.0525

      Normal
      0.4
      0.075
      0.055
      -0.005
      0.0350
      0.0650

      Recesion
      0.3
      0.020
      0.040
      0.010
      0.0150
      0.0300

      Depresion
      0.2
      -0.030
      -0.010
      0.130
      0.0500
      -0.0200

```python
# Encontrar el rendimiento esperado del portafolio
erTW = 0.5 * erT + 0.5 * erW
erTW
```

    0.036250000000000004

```python
# Encontrar la volatilidad de Toyota, Walmart y el portafolio
sTW = (tabla['Prob'] * (tabla['PortTW'] - erTW)**2).sum()**0.5
sTW
```

    0.03164747225293041

```python
# Notar que sTW < 0.5 * sT + 0.5 * sW
# la volatilidad del portafolio siempre es menor
# a la suma ponderada de las volatilidades individuales
0.5 * sT + 0.5 * sW
```

    0.03213149889704657

```python
tabla[['Toyota', 'Pfizer', 'PortTP']].plot()
```

```python
tabla[['Toyota', 'Walmart', 'PortTW']].plot()
```

### 1.2. Midiendo el co-movimiento entre instrumentos

- Una vez más, concluimos que la volatilidad (varianza) **NO** es el promedio ponderado de las varianzas individales.

- Por el contrario, la varianza de los rendimientos de un portafolio está afectada por el movimiento relativo de un activo individual respecto a otro.

- Por tanto, necesitamos definir las medidas de **covarianza** y **correlación**, que nos permiten evaluar las fluctuaciones relativas entre los activos.

#### Covarianza:

Es una medida el movimiento relativo entre dos instrumentos.

Matemáticamente, si tenemos dos activos $A_1$ y $A_2$ cuyos rendimientos son $r_1$ y $r_2$, respectivamente, entonces la covarianza de los rendimientos de los activos es

$$\text{cov}(r_1,r_2)=\sigma_{12}=\sum_{j=1}^{m}p_j(r_{1j}-E[r_1])(r_{2j}-E[r_2]).$$

$$\text{cov}(r_2,r_1)=\sigma_{21}=\sum_{j=1}^{m}p_j(r_{2j}-E[r_2])(r_{1j}-E[r_1]) = \sigma_{12}.$$

Podemos notar fácilmente que la covarianza de los rendimientos de un activo con los rendimientos del mismo activo corresponde a la varianza

$$\text{cov}(r_1,r_1)=\sigma_{11}=\sum_{j=1}^{m} p_j(r_{1j}-E[r_1])(r_{1j}-E[r_1])=\sigma_1^2=\text{var}(r_1).$$

**Ejemplo.** Calcular la covarianza entre los rendimientos de Toyota y Pfizer.

```python
# Mostrar tabla
tabla
```

      Prob
      Toyota
      Walmart
      Pfizer
      PortTP
      PortTW

      Estado

      Expansion
      0.1
      0.060
      0.045
      0.025
      0.0425
      0.0525

      Normal
      0.4
      0.075
      0.055
      -0.005
      0.0350
      0.0650

      Recesion
      0.3
      0.020
      0.040
      0.010
      0.0150
      0.0300

      Depresion
      0.2
      -0.030
      -0.010
      0.130
      0.0500
      -0.0200

```python
# Calcular la covarianza
covTP = (tabla['Prob'] * (tabla['Toyota'] - erT) * (tabla['Pfizer'] - erP)).sum()
covTP
```

    -0.0017820000000000004

El signo negativo en la covarianza nos dice que los rendimientos se mueven de manera contraria:

```python
tabla[['Toyota', 'Pfizer']].plot()
```

**Actividad.** Calcular la covarianza entre los rendimientos de Toyota y Walmart.

```python
# Calcular la covarianza
covTW = (tabla['Prob'] * (tabla['Toyota'] - erT) * (tabla['Walmart'] - erW)).sum()
covTW
```

    0.0009060000000000001

El signo positivo en la covarianza nos dice que los rendimientos se mueven en sentidos similares:

```python
tabla[['Toyota', 'Walmart']].plot()
```

¿Qué nos dice este número?
- El signo nos dice las direcciones relativas entre los rendimientos de cada activo. Por ejemplo, la covarianza entre los rendimientos de Toyota y Pfizer es negativa... ver los rendimientos.
- La magnitud de la covarianza no nos dice mucho acerca de la fuerza con que se relacionan o no estos rendimientos.

**Correlación:**

Un posible problema de la covarianza es que la magnitud de esta medida no nos dice mucho acerca de la fuerza de los movimientos relativos. La *correlación* es una medida normalizada del movimiento relativo entre los rendimientos de dos activos.

Matemáticamente,

$$\text{corr}(r_1,r_2)=\rho_{12}=\rho_{21}=\frac{\sigma_{12}}{\sigma_1\sigma_{2}}.$$

$$\sigma_{12} = \rho_{12} \sigma_{1} \sigma_{2}$$

Propiedades:

- Podemos notar fácilmente que la correlación de los rendimientos de un activo con los rendimientos del mismo activo es $1$: $$\text{corr}(r_1,r_1)=\rho_{11}=\frac{\sigma_{11}}{\sigma_1\sigma_1}=\frac{\sigma_{1}^2}{\sigma_1\sigma_1}=1.$$
- El signo de la correlación y la covarianza es el mismo.
- La correlación satisface: $$-1\leq\rho_{12}\leq 1.$$

**Ejemplo.** Calcular la correlación entre los rendimientos de Toyota y Pfizer.

```python
corrTP = covTP / (sT * sP)
corrTP
```

    -0.8678577889660902

**Actividad.** Calcular la correlación entre los rendimientos de Toyota y Walmart.

```python
corrTW = covTW / (sT * sW)
corrTW
```

    0.9362004766821975

**Conclusión.**
- Es una medida normalizada de la fluctuación relativa de los rendimientos de dos activos.
- En los ejemplos que vimos, sería conveniente invertir en el portafolio de Toyota y Pfizer puesto que su correlación es negativa, y esto impactaría positivamente en la diversificación del riesgo.

___

## 2. Uniendo todo...

- Entonces, vimos mediante ejemplos que el riesgo en un portafolio se ve afectado significativamente por como los rendimientos de los activos se mueven relativamente.
- Este movimiento relativo lo medimos mediante la covarianza o la correlación.
- Si se mueven de una manera que no están perfectamente correlacionados ($\rho<1$), entonces el riesgo del portafolio siempre será menor que el promedio ponderado de los riesgos individuales.

## Ésta es la razón por la que combinar activos en un portafolio permite diversificar el riesgo...

Entonces, ¿cómo podemos incorporar esta medida en el cálculo de la varianza del portafolio?
-  Ver en el tablero...

$$
\sigma_p^2 = \sum_{i=1}^{n} \sum_{k=1}^{n} w_i w_k \sigma_{ik} = w^T \Sigma w
$$

- ¿Cómo sería para dos activos? ($\sigma_{12} = \rho_{12} \sigma_{1} \sigma_{2}$)

\begin{align}
\sigma_p^2 & = w_1^2 \sigma_1^2 + w_2^2 \sigma_2^2 + 2 w_1 w_2 \sigma_{12} \\
           & = w_1^2 \sigma_1^2 + w_2^2 \sigma_2^2 + 2 w_1 w_2 \rho_{12} \sigma_1 \sigma_2
\end{align}

**Ejemplo.** Calcular por fórmula para el portafolio de Toyota y Pfizer. Comparar.

```python
sTP
```

    0.012867109232457772

```python
w1, w2 = 0.5, 0.5
sTP_ = (w1**2 * sT**2 + 2 * w1 * w2 * covTP + w2**2 * sP**2)**0.5
sTP_
```

    0.012867109232457761

**Actividad.** Calcular por fórmula para el portafolio de Toyota y Walmart. Comparar.

```python
sTW
```

    0.03164747225293041

```python

```

## 2.1.  Ver en el tablero...
### Matriz de varianza covarianza.
### Matriz de correlación.

# Anuncios parroquiales

## 1. Recordar quiz la siguiente clase. Temas: Clases 6 y 7.
## 2. Tarea: revisar archivo "Tarea4_MidiendoRendimientoRiesgo" en clase.

Created with Jupyter by Esteban Jiménez Rodríguez.


# HF Radars & Coastal Altimetry

This notebook is a demonstration of how to read, subset and visualize HF radar data. Specifically, the IOOS HFRADAR US East and Gulf Coast 6km Resolution Hourly RTV dataset. The dataset's THREDDS catalog can be found in the link below:

https://hfrnet-tds.ucsd.edu/thredds/HFRADAR_USEGC.html

The urls have to be modified accordingly to use a different dataset.

Another goal of this notebook is to compare with the geostrophic current velocities estimated from satellite altimeter sea level anomaly data and the CMEMS global analysis. Please note that the global analysis system is not assimilating the US East Coast HF radar observations.

The satellite altimeter dataset used is XTRACK which is dedicated for coastal applications.

https://www.aviso.altimetry.fr/index.php?id=3047

Information about the CMEMS global analysis model can be found here:

https://resources.marine.copernicus.eu/?option=com_csw&view=details&product_id=GLOBAL_ANALYSIS_FORECAST_PHY_001_024

```python
# import libraries

import os
import sys
module_path = os.path.abspath(os.path.join('..'))
if module_path not in sys.path:
    sys.path.append(module_path)

import xarray as xr
import numpy as np
from datetime import datetime, timedelta
from libr import geost1D, bounds, quiver_comp, loess
#plotting libraries
import cartopy.crs as ccrs
import cartopy.feature as cfeature
from cartopy.io import shapereader
from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER
from cartopy.mpl.ticker import LongitudeFormatter, LatitudeFormatter
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
import matplotlib as mpl
from matplotlib.lines import Line2D
```

## Select area of interest and cycle

This notebook focuses on the Mid Atlantic bight. The along-track 228 from the Jason 3 (a successor of Topex Poseidon, Jason 1 and 2) cycle 157 was selected but the same process can be followed for any altimeter track from the XTRACK (or other along track datasets).

more details about the Jason 3 mission:

https://www.aviso.altimetry.fr/en/missions/current-missions/jason-3.html

```python
min_lon, max_lon = -76, -73
min_lat, max_lat = 36.5, 39.5
min_cycle, max_cycle = 1019, 1019
```

## Load the dataset

The XTRACK dataset is publickly available on the AVISO server. The area and cycle of interest can be selected using xarray.

```python
#load file of selected track
loc = "./data/SLAFILT/ctoh.sla.filt.ref.TP+J1+J2+J3.gulfstream.228.nc"
ds = xr.open_dataset(loc)
#assign coordinates
ds = ds.rename({"missions_cycles": "cycles", "cycle":"xtrack_cycle"}).set_coords(["lat", "lon", "cycles"])
#subset by set bounds
point_idx, cycle_idx = bounds(ds.lat.data,
                              ds.lon.data,
                              ds.xtrack_cycle.data,
                              min_lat,
                              max_lat,
                              min_lon,
                              max_lon,
                              min_cycle,
                              max_cycle)
ds = ds.isel(nbpoints = point_idx, nbcycles = cycle_idx)
cycle_array = ds.xtrack_cycle.data
```

## Calculate across-track geostrophic current velocity

Only the across-track velocities can be calculated using the altimeter SLA values. The XTRACK SLA along-track values are already filtered with 40-km loess filter. The geost1D function is used for the estimation of the geostrophic current velocities.

Satellite is measuring sea surface height. A picture shows the commonly used conventions and names of variables used in satellite altimetry of ocean surface. The difference from climatology (also called mean sea surface (MSS)) is often called sea level anomaly (SLA). The difference between the geoid and MSS is called mean dynamic topography (MDT). Together with SLA, one can define absolute dynamic topography (ADT), which can be used to determine the total contribution of geostrophic current across-track.

We use a simplification of Navier-Stokes equations to obtain the geostrophic velocity associated with sea surface height differences along the track at distance, d, between measurements. Therefore, it gives a component of geostrophic velocity across the track. This is described by equation:

\begin{equation}
u_{total} = - \frac{g}{f} \left(\frac{\partial \mathit{SLA}}{\partial d} + \frac{\partial \mathit{MDT}}{\partial d}\right) = - \frac{g}{f} \left(\frac{\partial \mathit{ADT}}{\partial d} \right)
\end{equation}

Credits: Fabien Leger

```python
#initalise array
u_geo = np.nan * ds.sla.data
#run for each cycle
for i in enumerate(cycle_array):
    #select respective data slice
    sel_cycle = ds.isel(nbcycles = i[0])
    sel_point = ds.isel(nbcycles = 0)
    #calculate geostrophic speeds for the cycle (dynamic comp)
    u_geo[:, i[0]] = geost1D(sel_cycle.lon.data, sel_cycle.lat.data, sel_cycle.sla.data)
    #create DataArray for time and sea level anomaly
    time_da = xr.DataArray(data = ds.time.data,
                           dims = ["loc", "cycle"],
                           coords = dict(lat = ("loc", ds.lat.data),
                                         lon = ("loc", ds.lon.data),
                                         cycle = ("cycle", ds.cycles.data.astype('int'))))
    sla_da = xr.DataArray(data = ds.sla.data,
                          dims = ["loc", "cycle"],
                          coords = dict(lat = ("loc", ds.lat.data),
                                        lon = ("loc", ds.lon.data),
                                        cycle = ("cycle", ds.cycles.data.astype('int'))))
#convert to DataArray
u_geo = xr.DataArray(data = u_geo, dims = ["loc", "cycle"],
                     coords = dict(lat = ("loc", ds.lat.data),
                                   lon = ("loc", ds.lon.data),
                                   cycle = ("cycle", ds.cycles.data.astype('int'))))
#calculate mean geostrophic speeds (mean comp) and convert to DataArray
u_mean = geost1D(sel_point.lon.data, sel_point.lat.data, sel_point.mdt_cnes_cls_18.data)
u_mean = xr.DataArray(data = u_mean, dims = ["loc"], coords = dict(lat = ("loc", ds.lat.data),
                                                                  lon = ("loc", ds.lon.data)))
#calculate total geostrophic speed
u_total = u_geo + u_mean

#calculate zonal and meridional component
x_comp = np.nan * ds.sla.data
y_comp = np.nan * ds.sla.data
v_angle = np.nan * ds.sla.data
for i in enumerate(cycle_array):
    u_total_sel = u_total.isel(cycle = i[0])
    x_comp[:, i[0]], y_comp[:, i[0]], v_angle[:, i[0]], _ = quiver_comp(u_total_sel.data,
                                                                        u_total_sel.lat.data,
                                                                        u_total_sel.lon.data)
x_comp = xr.DataArray(data = x_comp,
                      dims = ["loc", "cycle"],
                      coords = dict(lat = ("loc", ds.lat.data),
                                    lon = ("loc", ds.lon.data),
                                    cycle = ("cycle", ds.cycles.data.astype('int'))))
y_comp = xr.DataArray(data = x_comp,
                      dims = ["loc", "cycle"],
                      coords = dict(lat = ("loc", ds.lat.data),
                                    lon = ("loc", ds.lon.data),
                                    cycle = ("cycle", ds.cycles.data.astype('int'))))
v_angle = xr.DataArray(data = v_angle,
                       dims = ["loc", "cycle"],
                       coords = dict(lat = ("loc", ds.lat.data),
                                     lon = ("loc", ds.lon.data),
                                     cycle = ("cycle", ds.cycles.data.astype('int'))))
```

```python
#create a dataset
geostrophic = xr.Dataset(data_vars = {"u_total": u_total, "u_geo": u_geo, "time": time_da,
                                      "u_mean": u_mean, "x_comp": x_comp, "y_comp": y_comp,
                                      "current_angle": v_angle, "sla": sla_da})
```

## Save the file

```python
geostrophic.to_netcdf("./data/alt_ugeo.nc")
```

## Download HFR data to compare with alongtrack altimetry

The HF Radar dataset can be accessed using the THREDDS catalog. Since its size is about 100 GB, it is more efficient to read and select the parameters of interest rather than downloading it. However, it may take a few minutes for subsetting the dataset. Therefore, a small sample of the dataset is saved as netcdf and used for this notebook.

```python
# Access the whole dataset (optional)

aggr_url = "https://hfrnet-tds.ucsd.edu/thredds/dodsC/HFR/USEGC/6km/hourly/RTV/HFRADAR_US_East_and_Gulf_Coast_6km_Resolution_Hourly_RTV_best.ncd"
data = xr.open_dataset(aggr_url)
```

```python
#open dataset
hfr_ds = xr.open_dataset("./data/hf_radar_05_2020.nc")
#select nearest time option
hfr_ds = hfr_ds.sel(time = "2020-05-02T01:00:00")
```

## Download CMEMS reanalysis

```python
#open dataset
cmems_ds = xr.open_dataset("./data/CMEMS-global-analysis-forecast-phy-001-024-hourly-u-v.nc")
cmems_ds = cmems_ds.rename({"latitude": "lat", "longitude": "lon"})
#select nearest time option
cmems_ds = cmems_ds.sel(time = "2020-05-22T03:30:00")
```

## Visualize

```python
##only suitable for a single (!) cycle

fig = plt.figure(figsize=(8,12))
proj = ccrs.PlateCarree()
ax=fig.add_subplot(1,1,1,projection=proj)
ax.set_extent([min_lon-1, max_lon+1, min_lat-1, max_lat+1])

ax.xaxis.set_major_formatter(LongitudeFormatter())
ax.yaxis.set_major_formatter(LatitudeFormatter())

lon_ticks = np.arange(min_lon, max_lon+1, 1)
lat_ticks = np.arange(min_lat-1, max_lat+1, 1)

ax.set_xticks(lon_ticks, crs=ccrs.PlateCarree())
ax.set_yticks(lat_ticks, crs=ccrs.PlateCarree())

ax.xaxis.set_tick_params(which='major', size=2., width=1, direction='in', top='on', pad=7)
ax.yaxis.set_tick_params(which='major', size=2., width=1, direction='in', right='on',pad=7)

ax.tick_params(labelleft=True,labelbottom=True,labeltop=True,labelright=True)

ax.add_feature(cfeature.LAND.with_scale('10m'), color='grey', alpha=0.4)

ax.coastlines(resolution='10m',linewidth=0.3)

bounds = np.array([0.,0.25,0.5,0.75,1.,1.25,1.5])

xi, yi = np.meshgrid(hfr_ds.lon.data, hfr_ds.lat.data)
m = np.sqrt(hfr_ds.u.data*hfr_ds.u.data + hfr_ds.v.data*hfr_ds.v.data)

# hf radar
cp = ax.pcolor(xi, yi, m, cmap = plt.cm.Reds, label='HF radar',vmin=0., vmax=1.5)

# add the vectors (hf radar, altimetry) and the scale for the geostrophic currents magnitude

cf = ax.quiver(xi,yi,hfr_ds.u,hfr_ds.v,color='black',scale=15,width=.0015)
ax.quiver(geostrophic.lon.data, geostrophic.lat.data, x_comp.data, y_comp.data,scale=15,width=.0015, color='blue',
              label='Jason 3 track 228')

qk = ax.quiverkey(cf, 0.5, 0.78, 1., label='$1 \ m/s$', labelpos='E',
                   coordinates='figure')

cax,kw = mpl.colorbar.make_axes(ax,location='bottom',pad=0.05,aspect=50)
out=fig.colorbar(cp, cax=cax, ticks=bounds,extend='max',**kw)
out.set_label('Surface current velocity (m/s)',size=10)
out.ax.tick_params(direction='in',size=2., width=1)

cmap = plt.cm.Reds
hfradar = Line2D([0], [0], marker='s', color='white', linestyle="", label='HF radar',
                          markerfacecolor=cmap(0.5),markeredgecolor=cmap(0.5), markersize=5)
altimeter = Line2D([0], [0], marker='', markersize=5, color='blue', linestyle="-", label='Jason 3 track 228', markeredgecolor='blue',
              markeredgewidth=0.5)

ax.legend(handles=[altimeter,hfradar], loc='upper left',fontsize=10, facecolor='white')

plt.savefig('./img/alt_hfr.png', dpi=300, transparent=False, bbox_inches='tight')
```

```python
interp_method = 'nearest'
plot_xaxis_altimeter = geostrophic.lat.data
plot_yaxis_altimeter = geostrophic.u_total.data
plot_xaxis_hfr = hfr_ds.lat.interp(lat = geostrophic.lat)
U_hfr = np.sqrt(hfr_ds.u.interp(lat = geostrophic.lat, lon = geostrophic.lon, method = interp_method)*
                         hfr_ds.u.interp(lat = geostrophic.lat, lon = geostrophic.lon, method = interp_method) +
                         hfr_ds.v.interp(lat = geostrophic.lat, lon = geostrophic.lon, method = interp_method)*
                         hfr_ds.v.interp(lat = geostrophic.lat, lon = geostrophic.lon, method = interp_method))
A_hfr = np.arctan2(hfr_ds.v.interp(lat = geostrophic.lat, lon = geostrophic.lon, method = interp_method),
                  hfr_ds.u.interp(lat = geostrophic.lat, lon = geostrophic.lon, method = interp_method))
plot_yaxis_hfr = U_hfr * np.cos(A_hfr - geostrophic.current_angle)
fig = plt.figure()
ax = fig.add_subplot(1,1,1)
ax.plot(plot_xaxis_altimeter, plot_yaxis_altimeter, 'r--', label = 'X-Track')
ax.plot(plot_xaxis_hfr, plot_yaxis_hfr, 'b--', label = 'HF radar')
ax.set_xlabel('Latitude ($^\circ$N)')
ax.set_ylabel(r'Current magnitude (ms$^{-1}$)')
ax.legend(loc = 'best')
plt.savefig("./img/alt_hfr_lineplot.png")
```

```python
fig = plt.figure(figsize=(8,12))
proj = ccrs.PlateCarree()
ax=fig.add_subplot(1,1,1,projection=proj)
ax.set_extent([min_lon-1, max_lon+1, min_lat-1, max_lat+1])

ax.xaxis.set_major_formatter(LongitudeFormatter())
ax.yaxis.set_major_formatter(LatitudeFormatter())

lon_ticks = np.arange(min_lon, max_lon+1, 1)
lat_ticks = np.arange(min_lat-1, max_lat+1, 1)

ax.set_xticks(lon_ticks, crs=ccrs.PlateCarree())
ax.set_yticks(lat_ticks, crs=ccrs.PlateCarree())

ax.xaxis.set_tick_params(which='major', size=2., width=1, direction='in', top='on', pad=7)
ax.yaxis.set_tick_params(which='major', size=2., width=1, direction='in', right='on',pad=7)

ax.tick_params(labelleft=True,labelbottom=True,labeltop=True,labelright=True)

ax.add_feature(cfeature.LAND.with_scale('10m'), color='grey', alpha=0.4)

ax.coastlines(resolution='10m',linewidth=0.3)
bounds = np.array([0.,0.25,0.5,0.75,1.,1.25,1.5])
norm = mpl.colors.Normalize(vmin=0., vmax=1.5)

xi, yi = np.meshgrid(cmems_ds.lon.data, cmems_ds.lat.data)
m = np.sqrt(cmems_ds.uo.data*cmems_ds.uo.data + cmems_ds.vo.data*cmems_ds.vo.data)[0,:,:]
cp = ax.pcolor(xi, yi, m, cmap = plt.cm.Reds,vmin=0., vmax=1.5)

cf = ax.quiver(xi,yi,cmems_ds.uo[0,:,:],cmems_ds.vo[0,:,:],color='black',scale=15,width=.0015)
ax.quiver(geostrophic.lon.data, geostrophic.lat.data, x_comp.data, y_comp.data,scale=15,width=.0015, color='blue',
              label='Jason 3 track 228')

qk = ax.quiverkey(cf, 0.5, 0.78, 1., label='$1 \ m/s$', labelpos='E',
                   coordinates='figure')

cax,kw = mpl.colorbar.make_axes(ax,location='bottom',pad=0.05,aspect=50)

out=fig.colorbar(cp,cax=cax,ticks=bounds,extend='max',**kw)
out.set_label('Surface current velocity (m/s)',size=10)
out.ax.tick_params(direction='in',size=2., width=1)

cmap = plt.cm.Reds
analysis = Line2D([0], [0], marker='s', color='white', linestyle="", label='CMEMS Analysis',
                          markerfacecolor=cmap(0.5),markeredgecolor=cmap(0.5), markersize=5)
altimeter = Line2D([0], [0], marker='', markersize=5, color='blue', linestyle="-", label='Jason 3 track 228', markeredgecolor='blue',
              markeredgewidth=0.5)

ax.legend(handles=[altimeter,analysis], loc='upper left',fontsize=10, facecolor='white')

plt.savefig('./img/alt_cmems.png', dpi=300, transparent=False, bbox_inches='tight')
```

```python

```