# The Unscented Kalman Filter

After studying the Unscented transform in the previous notebook, now is time to apply it to the Kalman Filter for localization.

```python
import pandas as pd
import scipy
import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse
from celluloid import Camera
from IPython.display import HTML
import numpy as np
import os
import math
import seaborn as sns
from matplotlib import cm
from mpl_toolkits.mplot3d import Axes3D
plt.rcParams["figure.figsize"] = (20,20)
from scipy.stats import multivariate_normal
from scipy import stats

%matplotlib inline
```

```python
def normalize_angle(phi):
    # Normalize phi to be between -pi and pi
    while(phi>np.pi):
        phi -= 2*np.pi;

    while(phi<-np.pi):
        phi += 2*np.pi

    phiNorm = phi

    return phiNorm

def normalize_vector_angle(angle):
    # Normalize phi to be between -pi and pi

    for phi in angle:
        while(phi>np.pi):
            phi -= 2*np.pi;

        while(phi<-np.pi):
            phi += 2*np.pi

    return angle
```

# The unscented functions
These functions are explained in my previous notebook about the unscented transform, and thus here I will just directly write the code.

```python
def compute_sigma_points( mu, sigma, lmbda, alpha, beta):
    n = mu.shape[0]
    # initialize sigma points
    sigma_points = np.zeros((n,2*n+1))+mu # matrix product this way is element-wise
    # compute square root matrix
    sqrm = np.sqrt((n+lmbda)*np.abs(sigma))
    for i in range (1,n+1):
        sigma_points[:,i]+= sqrm[:,i-1]
    for i in range(n+1,2*n+1):
        sigma_points[:,i]-= sqrm[:,i-n-1]

    # compute the weights
    wc = np.ones((1,2*n+1))*lmbda/(n+lmbda)
    wc[0][1:] *= (1/(2*lmbda))
    wm = wc.copy()
    wc[0][0] += 1-alpha**2+beta

    return sigma_points,wm,wc
```

When recovering the Gaussian, we must be careful with averagin the angles. To average the angles ${\theta_1,...,\theta_N}$ given their weights  ${w_1,...,w_N}$ , we can first compute the weighted sum of the unit vectors of each rotation as

\begin{equation}
\overline{x} = \sum_{i=1}^N w_i cos(\theta_i) \text{,     } \overline{y} = \sum_{i=1}^N w_i sin(\theta_i)
\end{equation}

Then the average angle $\overline{\theta}$ is given by

\begin{equation}
\overline{\theta} = atan2(\overline{x} ,\overline{y})
\end{equation}

```python
def recover_gaussian(sigmas,wm,wc):
    n = sigmas.shape[0]
    # initialize mean and covariance
    #mu = np.zeros((n,1))
    newsigma = np.zeros((n,n))
    # just in case
    wm = wm.flatten()
    wc = wc.flatten()

    # Compute mean and covar of the resulting distribution given the transformed
    # sigma points and corresponding weights
    mu = np.sum(sigmas*wm, axis = 1,keepdims = True)

    # The set of angles need to be averaged according to the given weights
    xavg = 0.0
    yavg = 0.0

    if (n >= 3): # if we have at least this shape it means we have an angle measurement in index 2
        for i in range(2*n+1):
            xavg += wm[i] * math.cos(sigmas[2,i])
            yavg += wm[i] * math.sin(sigmas[2,i])

        mu[2] = normalize_angle(np.arctan2(yavg,xavg))
    else: # otherwise it is in 1 (range, bearing)
        for i in range(2*n+1):
            xavg += wm[i] * math.cos(sigmas[1,i])
            yavg += wm[i] * math.sin(sigmas[1,i])

        mu[1] = normalize_angle(np.arctan2(yavg,xavg))

    # Compute sigma

    for i in range(2*n+1):
        dif = normalize_vector_angle(sigmas[:,i,np.newaxis]-mu)
        newsigma += wc[i]*np.matmul(dif,dif.T)

    if (n >= 3):
        mu[2] = normalize_angle(np.arctan2(yavg,xavg))
    else:
        mu[1] = normalize_angle(np.arctan2(yavg,xavg))

    # Add motion noise

    motionNoise = 0.1
    R3 = np.identity(n)*motionNoise
    newsigma += R3

    return mu,newsigma
```

# Prediction step

|                 |  EKF  |   UKF  |
|:---------------:|:-----:|:------|
| **PREDICTION**  |   $\mu$ = $g(u_t,u_{t-1})$ from motion model |  $Y^{[i]}= g(X^{[i]})$  $\mu_t$ = $\sum_{i=0}^{2n}Y^{[i]}w^{[i]}_m$  |
|   **STEP**      | $\sum_t $=$ G_t\sum_{t-1}G^T_t+R_t$ | $\sum_t$ = $ \sum_{i=0}^{2n}(Y^{[i]}-\mu')(Y^{[i]}-\mu')^T+ Q$ propagation of motion |

The update is performed in the measurement space, so the transformation function $g$ that we're applying to the sigma points is the one that converts them to the measurement space.
Here, we are using again the odometry motion model for that.

Afterwards, we use the unscented transform to compute the mean and covariance of these points.

$R_t$ is added to the covariance in order to model the additional prediction noise uncertainty, which is assumed to be additive.

```python
def prediction_step(mu,sigma,u):
    ''' Updates the belief of the robot pose according to the motion model.
    '''
    n = mu.shape[0]
    # Unscented parameters
    alpha = 0.9
    beta = 2
    kappa = 1
    lmbda = alpha**2*(n+kappa)-n

    # Compute the gaussian sigma points
    sigma_points, wm, wc = compute_sigma_points(mu,sigma,lmbda,alpha,beta)

    #print('sigma_points, wm, wc',sigma_points, wm, wc)
    # Transform sigma points according to odometry motion model
    for i in range(sigma_points.shape[1]):
        sigma_points[0:3,i,np.newaxis] += np.array([[u['t'].iloc[0]*np.cos(u['r1'].iloc[0]+sigma_points[2,i])],
                                                    [u['t'].iloc[0]*np.sin(u['r1'].iloc[0]+sigma_points[2,i])],
                                                    [u['r1'].iloc[0]+u['r2'].iloc[0]]])
        sigma_points[2,i] = normalize_angle(sigma_points[2,i])

    # Recover mu and sigma
    newmu,newsigma = recover_gaussian(sigma_points,wm.flatten(),wc.flatten())
    return newmu,newsigma
```

# Correction step

We will compute a new set of sigma points from the gaussian we predicten in the previous step, so that the sigma point set $\overline{\chi}_t$ captures the uncertainty after the prediction step.

\begin{equation}
\overline{\chi_t} = \left( \overline{\mu}_t \text{       } \overline{\mu}_t + \sqrt{(n+\lambda)\overline{\sum}_t} \text{      } \overline{\mu}_t - \sqrt{(n+\lambda)\overline{\sum}_t}\right)
\end{equation}

**If a landmark is observed from the first time**, we take each landmark measurement (in this case, in form of range and bearing) and incorporate it to the state vector from the prediction. This way, when obtaining the sigma points, its uncertainties are added to the covariance matrix.
    \begin{equation}
    \overline{\chi_t} = \begin{bmatrix}
                           \overline{\mu_t} \\
                           range \\
                           bearing
                         \end{bmatrix} =
                         \begin{bmatrix}
                           \overline{x_t} \\
                           \overline{y_t} \\
                           \overline{\theta_t} \\
                           range \\
                           bearing
                         \end{bmatrix}
\end{equation}

file:///home/olaya/Pictures/ukf1.png

However, it's not the range and bearing measurements what we want, but the X and Y coordinates of the landmark. We take the range and bearing values from the sigma points and initialize the position of the landmark in the map according to the model of the range sensor:
\begin{equation}
    X^{landmark}_i= x^{robot}_i + range_i * \cos{(\theta_i + bearing_i)} \\
    Y^{landmark}_i= y^{robot}_i + range_i * \sin{(\theta_i + bearing_i)},
    \end{equation}
    with i indicating the ith sigma point.

The range and bearing values from the sigma points are then substituted by subsequent the Xs and Ys of the landmarks, turning the sigma points into this:

  \begin{equation}
    \overline{\chi_t} = \begin{bmatrix}
                           \overline{\mu_t} \\
                           x_{l_1} \\
                           y_{l_1}
                         \end{bmatrix} =
                         \begin{bmatrix}
                           \overline{x_t} \\
                           \overline{y_t} \\
                           \overline{\theta_t} \\
                           x_{l_1} \\
                           y_{l_1}
                         \end{bmatrix}
    \end{equation}

**If we have already seen the landmark**, then, a predicted observation, that is, the predicted landmark position, is computed for each sigma point:
\begin{equation}
\overline{Z_t} = h(\overline{\chi}_t)
\end{equation}

In this context, $h$ corresponds with the following function:

\begin{equation}
    range = \sqrt{(x_{l_1}-x_{robot})^2+(y_{l_1}-y_{robot})^2} \\
    bearing = \arctan{\frac{x_{l_1}-x_{robot}}{y_{l_1}-y_{robot}}-\theta}
    \end{equation}

We sample the gaussian distribution of the predicted observation as:
\begin{equation}
   \hat{z}_t= \sum^{2n}_{i=0}w_m^{[i]}\overline{Z}_t^{[i]}
\end{equation}

After obtaining the predicted observation $\hat{z}_t$ we compute its uncertainty $S_t$.
\begin{equation}
   S_t = \sum^{2n}_{i=0}w_c^{[i]}\left(\overline{Z}_t^{[i]}-\hat{z}_t\right)\left(\overline{Z}_t^{[i]}-\hat{z}_t\right)^T + Q_t
\end{equation}

Then we compute the cross-covariance between state and observation
\begin{equation}
\overline{\sum}_t^{x,z} =  \sum^{2n}_{i=0}w_c^{[i]}\left(\overline{\chi}_t^{[i]}-\overline{\mu}_t\right)\left(\overline{Z}_t^{[i]}-\hat{z}_t\right)^T
\end{equation}

which we use to obtain the Kalman gain:
\begin{equation}
K_t = \overline{\sum}_t^{x,z}S_t^{-1}
\end{equation}

Now we're ready to update mu and sigma

\begin{equation}
\mu_t = \overline{\mu}_t + K_t(z_t-\hat{z}_t) \\
\sum_{t} = \overline{\sum}_t - K_tS_tK_t^T
\end{equation}

```python
def correction_step(mu,sigma,z,env_map):
    ''' Updates the belief, i. e., mu and sigma after observing landmarks, and augments
    the env_map after observing landmarks.
    The sensor model measures range and bearing of landmark.
    Each observation z has an id of the landmark, range and bearing.
    The env_map contains al the landmarks observed so far y the order of observation.
    '''
    n = mu.shape[0]

    # Unscented parameters
    alpha = 0.9
    beta = 2
    kappa = 1
    lmbda = alpha**2*(n+kappa)-n

    # Number of measurements in this time step
    m = z.shape[0]

    # Construct the sensor noise matrix Q
    Q = 0.01*np.identity(2)

    for i,row in z.iterrows():
        # Get the id of the landmark corresponding to the i-th observation
        landmarkId = int(row['r1']) # r1 == ID here

        ################ If the landmark is obeserved for the first time:#################
        if not landmarkId in env_map: # look for values of id in env_map list
            print('unknown landmark', landmarkId)
            env_map.append(landmarkId)

            # Append the measurement to the state vector (t->range, r2->bearing)
            mu = np.concatenate((mu,np.array([[row['t']],[row['r2']]])), axis = 0)
            print('concatenamu',mu)
            # Initialize its uncertainty and add it to sigma
            sigma = scipy.linalg.block_diag(sigma,Q)

            # Sample signa points for this new state vector
            sigma_points, wm, wc = compute_sigma_points(mu,sigma,lmbda,alpha,beta)

            for i in range(sigma_points.shape[1]):
                sigma_points[2,i] = normalize_angle(sigma_points[2,i]) # normalize angles

            # Compute the xy location of the landmark according to each sigma point
            ldXY = np.copy(sigma_points[:2,:]) # copy x and y values of robot in sigma points
            for i in range(sigma_points.shape[1]):
                ## X value #####  range value   #####           theta       ####   bearing ##########
                ldXY[0,i] +=  sigma_points[-2,i] *  np.cos(sigma_points[2,i] +  sigma_points[-1,i])# X
                ldXY[1,i] +=  sigma_points[-2,i] *  np.sin(sigma_points[2,i] +  sigma_points[-1,i])# Y

            # change range and bearing values by landmark x and y values
            sigma_points[-2:,:] = ldXY

            # recover mu and sigma
            n = mu.shape[0]
            lmbda = alpha**2*(n+kappa)-n # update values of UKF
            mu,sigma = recover_gaussian(sigma_points,wm,wc)
            print('rmu', mu)
            print('rsigma',sigma)
            #continue # jump to next step in for loop

        else:
            print('known landmark', landmarkId)
            print('nmu', mu)
            print('nsigma',sigma)

            ############# there is no landmark observed for the first time #################
            sigma_points, wm, wc = compute_sigma_points(mu,sigma,lmbda,alpha,beta)

            # extract current location of landmarks for each sigma point
            ldindex = env_map.index(landmarkId)
            ldXY = sigma_points[2*ldindex+3:2*ldindex+3+2,:] # take XY values

            #Compute z_points (2x2n+1), which consists of predicted measurements from all sigma points
            z_points = np.copy(ldXY)
            for i in range(ldXY.shape[1]):
                z_points[0,i] = np.sqrt((sigma_points[0,i]-ldXY[0,i])**2+(sigma_points[1,i]-ldXY[1,i])**2) # range
                z_points[1,i] = np.arctan((sigma_points[1,i]-ldXY[1,i])/(sigma_points[0,i]-ldXY[0,i]))# bearing

            # Now recover the gaussian of the expected measurement for the z points
            # altogether with the innovation covariance matrix S
            zm,S = recover_gaussian(z_points,wm,wc)

            # Compute Sigma_x_z which is equivalent to sigma times the Jacobian H transposed in EKF
            sigma_x_z = np.zeros((n,2))

            for i in range(2*n+1):
                difmu = np.absolute(sigma_points[:,i,np.newaxis]-mu)
                difmu[2] = normalize_angle(difmu[2])
                difz = np.absolute(z_points[:,i,np.newaxis]-zm)
                difz[1] = normalize_angle(difz[1])
                sigma_x_z += wc.flatten()[i]*np.matmul(difmu,difz.T)

            # compute the kalman gain
            k = np.matmul(sigma_x_z, np.linalg.inv(S))

            zsensor = np.array([[np.asarray(row)[-2]],[np.asarray(row)[-1]]])

            # update mu and sigma
            mu = mu + np.matmul(k,zsensor-zm)

            sigma = sigma - np.matmul(k,np.matmul(S,k.T))

            mu[2] = normalize_angle(mu[2])

    return mu,sigma,env_map

```

```python
sigma_points = np.array([[ 0.09951619,  0.82700542,  0.1027237,   0.45757818,  0.09951619,  0.09951619,
  -0.62797305,  0.09630867, -0.2585458,   0.09951619,  0.09951619],
 [ 0.01005453,  0.01326204,  0.73757902,  0.38243684,  0.01005453,  0.01005453,
   0.00684701, -0.71746997, -0.36232779,  0.01005453,  0.01005453],
 [ 1.46993254,  1.82799453,  1.84231486,  0.83122351,  1.46993254,  1.46993254,
   1.11187055,  1.09755022,  2.10864157,  1.46993254,  1.46993254],
 [ 1.89645381,  1.89645381,  1.89645381,  1.89645381,  2.12536428,  1.89645381,
   1.89645381,  1.89645381,  1.89645381,  1.66754335,  1.89645381],
 [ 0.37403189,  0.37403189,  0.37403189,  0.37403189,  0.37403189,  0.60294235,
   0.37403189,  0.37403189,  0.37403189,  0.37403189,  0.14512142]])
```

```python
a = np.array([
              [1,2,3,4,5],
              [6,7,8,9,10],
              [6,6,6,6,6]
             ])
b = np.array([[6,7,8,9,10],[1,2,3,4,5]])

a[1:3,:] = b
a
```

    array([[ 1,  2,  3,  4,  5],
           [ 6,  7,  8,  9, 10],
           [ 1,  2,  3,  4,  5]])

```python
a.shape
```

    (3, 5)

```python

```

## Auxiliar functions

Functions for reading data from the attached files

```python
def read_data(filename,path):
    data = pd.read_csv(path + filename,delimiter = ' ',header=None, names = ['sensor','r1','t','r2']) # or id, range and bearing for sensor
    return (data)

def read_world(filename,path):
    landmarks = pd.read_csv(path+filename,delimiter = ' ',header=None, names = ['x','y'])
    return (np.asarray(landmarks))
```

Functions for plotting

```python
def world_plot(landmarks, fig, ax):
    ''' Draw the world grid with the ground truth landmarks'''
    ax.set_xticks([x for x in range(-2,12)],minor=True )
    ax.set_yticks([y for y in range(-2,12)],minor=True)
    # Plot grid on minor axes in gray (width = 1)
    plt.grid(which='minor',ls='-',lw=1, color='white')
    # Plot grid on major axes in larger width
    plt.grid(which='major',ls='-',lw=2, color='white')
    # Draw the ground truth of the landmarks
    for i,l in enumerate(landmarks):
        ax.text(l[0], l[1], 'x', ha='center', va='center', color='black', fontsize=20)
        #if (observedLandmarks[0][i] == 1.0):
            # plot landmark ellipse
            #e = plot_conf_ellipse(mu[2*i+3:2*i+5],sigma[2*i+3:2*i+5,2*i+3:2*i+5], 0.6, 'blue')
            #ax.add_patch(e)

    return fig,ax

def robot_plot(mu,sigma,fig,ax):
    ''' takes fig with a world plot, draws a robot in it'''
    # Draw the robot
    ax.text(mu[0], mu[1], 'o', ha='center', va='center', color='black', fontsize=20)
    e = plot_conf_ellipse(mu[0:3],sigma[0:3,0:3], 0.6, 'red')
    print(e)
    ax.add_patch(e)
    return fig,ax
```

```python
from scipy.stats.distributions import chi2
def plot_conf_ellipse(x,C,alpha,color):
    # Calculate unscaled half axes
    sxx = C[0,0]
    syy = C[1,1]
    sxy = C[0,1]

    # Remove imaginary parts in case of neg. definite C
    a = np.sqrt(0.5*(sxx+syy+np.sqrt((sxx-syy)**2+4*sxy**2))).real   # always greater
    b = np.sqrt(0.5*(sxx+syy-np.sqrt((sxx-syy)**2+4*sxy**2))).real   # always smaller

    # Scaling in order to reflect specified probability
    a = a*np.sqrt(chi2.ppf(alpha, df=2))
    b = b*np.sqrt(chi2.ppf(alpha, df=2))

    # Calculate inclination (numerically stable)
    if math.isclose(sxx, syy, rel_tol=0.1): # this function launches a warning
        angle =  0.5*np.arctan(2*sxy/(sxx-syy)) if (sxx-syy) != 0 else 0
    elif (sxy==0):
        print(2)
        angle = 0
    elif (sxy>0):
        print(3)
        angle = np.pi/4
    elif (sxy<0):
        print(4)
        angle = -np.pi/4

    return Ellipse((x[0],x[1]), a, b, angle, edgecolor = color, facecolor = color, alpha = alpha)
```

```python
#fig,ax =  plt.subplots()
#fig,ax = world_plot(landmarks,fig,ax)
#fig,ax = robot_plot(mu,sigma,fig,ax)
```

Add a column with the index of the timestep for each sensor read

```python
data = read_data('ukf_sensor_data.dat',os.getcwd()+'/data/')
indexodometry = data[ (data['sensor'] == 'ODOMETRY')].index
timestepindex = []
timestep = 0
for i in range (0,data.shape[0]):
    if(timestep+1 < indexodometry.shape[0]):
        if (i < indexodometry[timestep+1]) :
            timestepindex.append(timestep)
        else:
            timestep +=1
            timestepindex.append(timestep)
    else:
        timestepindex.append(timestep)

data.insert(0, "timestep", timestepindex, True)
data
```

      timestep
      sensor
      r1
      t
      r2

      0
      0
      ODOMETRY
      0.100692
      0.100073
      0.000171

      1
      0
      SENSOR
      1.000000
      1.896454
      0.374032

      2
      0
      SENSOR
      2.000000
      3.853678
      1.519510

      3
      1
      ODOMETRY
      0.099366
      0.099968
      -0.000241

      4
      1
      SENSOR
      1.000000
      1.839227
      0.248026

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

      1538
      330
      SENSOR
      5.000000
      4.951031
      -1.512977

      1539
      330
      SENSOR
      6.000000
      4.917189
      -0.862938

      1540
      330
      SENSOR
      7.000000
      -0.035066
      0.975887

      1541
      330
      SENSOR
      8.000000
      1.900675
      3.144946

      1542
      330
      SENSOR
      9.000000
      4.171552
      0.072790

1543 rows × 5 columns

```python
landmarks = read_world('ukf_world.dat',os.getcwd()+'/data/')
landmarks
```

    array([[ 2,  1],
           [ 0,  4],
           [ 2,  7],
           [ 9,  2],
           [10,  5],
           [ 9,  8],
           [ 5,  5],
           [ 5,  3],
           [ 5,  9]])

Initialize the belief

```python
mu = np.zeros((3,1))
sigma = np.identity((3))*0.1
```

Execute the loop

```python
def ukf_loop(mu, sigma,sensor_data, env_map):
    for t in range (0, 2):#timestepindex[-1]):
        print('timestep',t)
        # Perform the prediction step of the UKF
        mu, sigma = prediction_step(mu,sigma,sensor_data.loc[(sensor_data['timestep'] == t) & (sensor_data['sensor'] == 'ODOMETRY')])
        mu,sigma,env_map = correction_step(mu,sigma,data.loc[(data['timestep'] == t) & (data['sensor'] == 'SENSOR')],env_map)
        print('corrected mu',mu)
        print('corrected sigma',sigma)
    return mu,sigma
```

```python
mu = np.zeros((3,1))
sigma = np.identity((3))*0.001
env_map = []

mu,sigma = ukf_loop(mu,sigma,data, env_map)
```

    timestep 0
    unknown landmark 1
    concatenamu [[0.09951619]
     [0.01005453]
     [0.10086379]
     [1.89645381]
     [0.37403189]]
    rmu [[0.09951619]
     [0.01005453]
     [0.10086379]
     [1.69613846]
     [0.83094222]]
    rsigma [[ 0.20101117  0.00066707  0.00202538  0.09932994  0.00393704]
     [ 0.00066707  0.20111041  0.0063439  -0.00459763  0.11135022]
     [ 0.00202538  0.0063439   0.20110957 -0.07812788  0.16224157]
     [ 0.09932994 -0.00459763 -0.07812788  0.32128035 -0.11219014]
     [ 0.00393704  0.11135022  0.16224157 -0.11219014  0.50384975]]
    unknown landmark 2
    concatenamu [[0.09951619]
     [0.01005453]
     [0.10086379]
     [1.69613846]
     [0.83094222]
     [3.85367751]
     [1.51951018]]
    rmu [[ 0.09951619]
     [ 0.01005453]
     [ 0.10086379]
     [ 1.69613846]
     [ 0.83094222]
     [-0.05137987]
     [ 3.05119181]]
    rsigma [[ 0.40697059  0.06905453  0.15578359  0.35529119  0.20498036 -0.04594926
       0.04245201]
     [ 0.06905453  0.42406923  0.22596077  0.21101343  0.44292141 -0.08172006
       0.28724759]
     [ 0.15578359  0.22596077  0.5498483   0.43828027  0.58956907  0.28528921
       0.15503458]
     [ 0.35529119  0.21101343  0.43828027  0.71552593  0.58259544 -0.01139281
       0.13961386]
     [ 0.20498036  0.44292141  0.58956907  0.58259544  0.99356873  0.12780243
       0.34890179]
     [-0.04594926 -0.08172006  0.28528921 -0.01139281  0.12780243  3.42032107
       0.29676928]
     [ 0.04245201  0.28724759  0.15503458  0.13961386  0.34890179  0.29676928
       2.66277146]]
    corrected mu [[ 0.09951619]
     [ 0.01005453]
     [ 0.10086379]
     [ 1.69613846]
     [ 0.83094222]
     [-0.05137987]
     [ 3.05119181]]
    corrected sigma [[ 0.40697059  0.06905453  0.15578359  0.35529119  0.20498036 -0.04594926
       0.04245201]
     [ 0.06905453  0.42406923  0.22596077  0.21101343  0.44292141 -0.08172006
       0.28724759]
     [ 0.15578359  0.22596077  0.5498483   0.43828027  0.58956907  0.28528921
       0.15503458]
     [ 0.35529119  0.21101343  0.43828027  0.71552593  0.58259544 -0.01139281
       0.13961386]
     [ 0.20498036  0.44292141  0.58956907  0.58259544  0.99356873  0.12780243
       0.34890179]
     [-0.04594926 -0.08172006  0.28528921 -0.01139281  0.12780243  3.42032107
       0.29676928]
     [ 0.04245201  0.28724759  0.15503458  0.13961386  0.34890179  0.29676928
       2.66277146]]
    timestep 1
    known landmark 1
    nmu [[ 0.1043097 ]
     [ 0.01102737]
     [ 0.19998848]
     [ 1.69613846]
     [ 0.83094222]
     [-0.05137987]
     [ 3.05119181]]
    nsigma [[ 1.36063746  1.34960621  1.57829605  1.68433922  1.83805317  0.61800415
       0.84823619]
     [ 1.34960621  2.08933961  2.08723494  2.03362304  2.51425298  0.94640568
       0.93162937]
     [ 1.57829605  2.08723494  2.4997658   2.2947328   2.75861414  0.84566729
       1.29058624]
     [ 1.68433922  2.03362304  2.2947328   2.55371292  2.74924386  1.11334458
       1.14219835]
     [ 1.83805317  2.51425298  2.75861414  2.74924386  3.39033924  1.23577719
       1.32694625]
     [ 0.61800415  0.94640568  0.84566729  1.11334458  1.23577719  1.3319074
      -0.13407754]
     [ 0.84823619  0.93162937  1.29058624  1.14219835  1.32694625 -0.13407754
       2.06969821]]
    known landmark 2
    nmu [[-0.91605683]
     [-1.12687935]
     [ 0.62385781]
     [ 0.47237603]
     [-0.48632226]
     [-0.8244085 ]
     [ 2.11937797]]
    nsigma [[-21.25241808 -24.44843499  10.73249164 -25.83359023 -27.85411354
      -17.85000283 -18.79878166]
     [-24.44843499 -27.34313135  12.53042341 -29.36071044 -31.36070216
      -20.12472149 -21.48114937]
     [ 10.73249164  12.53042341  -1.2061714   13.43427509  14.7782675
        8.32106203   9.24467873]
     [-25.83359023 -29.36071044  13.43427509 -30.93337736 -33.38378772
      -21.36179407 -22.76534201]
     [-27.85411354 -31.36070216  14.7782675  -33.38378772 -35.59771203
      -23.01539743 -24.46938982]
     [-17.85000283 -20.12472149   8.32106203 -21.36179407 -23.01539743
      -13.75535153 -16.17635268]
     [-18.79878166 -21.48114937   9.24467873 -22.76534201 -24.46938982
      -16.17635268 -15.00285909]]
    corrected mu [[19.28846788]
     [20.41439086]
     [ 3.1251751 ]
     [22.67648614]
     [22.54702665]
     [17.50429149]
     [20.9387322 ]]
    corrected sigma [[-567.66976879 -607.02070257  -56.74916078 -626.32386043 -650.75963721
      -513.63916169 -527.66083284]
     [-607.02070257 -648.46258619  -59.41629917 -669.58375216 -695.48215216
      -548.71887249 -564.01318585]
     [ -56.74916078  -59.41629917   -9.54019159  -60.72529875  -62.14956331
       -52.90800059  -53.59903903]
     [-626.32386043 -669.58375216  -60.72529875 -690.84756968 -717.93142505
      -566.21375427 -581.98387972]
     [-650.75963721 -695.48215216  -62.14956331 -717.93142505 -745.6983185
      -588.2057242  -604.56258476]
     [-513.63916169 -548.71887249  -52.90800059 -566.21375427 -588.2057242
      -463.60731988 -477.889839  ]
     [-527.66083284 -564.01318585  -53.59903903 -581.98387972 -604.56258476
      -477.889839   -488.89083676]]

```python
mu
```

    array([[19.28846788],
           [20.41439086],
           [ 3.1251751 ],
           [22.67648614],
           [22.54702665],
           [17.50429149],
           [20.9387322 ]])

```python
sigma
```

    array([[-567.66976879, -607.02070257,  -56.74916078, -626.32386043,
            -650.75963721, -513.63916169, -527.66083284],
           [-607.02070257, -648.46258619,  -59.41629917, -669.58375216,
            -695.48215216, -548.71887249, -564.01318585],
           [ -56.74916078,  -59.41629917,   -9.54019159,  -60.72529875,
             -62.14956331,  -52.90800059,  -53.59903903],
           [-626.32386043, -669.58375216,  -60.72529875, -690.84756968,
            -717.93142505, -566.21375427, -581.98387972],
           [-650.75963721, -695.48215216,  -62.14956331, -717.93142505,
            -745.6983185 , -588.2057242 , -604.56258476],
           [-513.63916169, -548.71887249,  -52.90800059, -566.21375427,
            -588.2057242 , -463.60731988, -477.889839  ],
           [-527.66083284, -564.01318585,  -53.59903903, -581.98387972,
            -604.56258476, -477.889839  , -488.89083676]])

```python
env_map
```

    [1, 2]

```python

```


# Selección óptima de portafolios II

Entonces, tenemos que:
- La LAC describe las posibles selecciones de riesgo-rendimiento entre un activo libre de riesgo y un activo riesgoso.
- Su pendiente es igual al radio de Sharpe del activo riesgoso.
- La asignación óptima de capital para cualquier inversionista es el punto tangente de la curva de indiferencia del inversionista con la LAC.

Para todo lo anterior, supusimos que ya teníamos el portafolio óptimo (activo riesgoso).

En la clase pasada aprendimos a hallar este portafolio óptimo si el conjunto de activos riesgosos estaba conformado únicamente por dos activos:

$$w_{1,EMV}=\frac{(E[r_1]-r_f)\sigma_2^2-(E[r_2]-r_f)\sigma_{12}}{(E[r_2]-r_f)\sigma_1^2+(E[r_1]-r_f)\sigma_2^2-((E[r_1]-r_f)+(E[r_2]-r_f))\sigma_{12}}.$$

- Sin embargo, la complejidad del problema crece considerablemente con el número de variables, y la solución analítica deja de ser viable cuando mencionamos que un portafolio bien diversificado consta aproximadamente de 50-60 activos.
- En esos casos, este problema se soluciona con rutinas numéricas que hagan la optimización por nosotros, porque son una solución viable y escalable a más variables.

**Objetivos:**
- ¿Cuál es el portafolio óptimo de activos riesgosos cuando tenemos más de dos activos?
 - ¿Cómo construir la frontera de mínima varianza cuando tenemos más de dos activos?

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

## 1. Maximizando el radio de Sharpe

### ¿Qué pasa si tenemos más de dos activos riesgosos?

En realidad es algo muy similar a lo que teníamos con dos activos.
- Para dos activos, construir la frontera de mínima varianza es trivial: todas las posibles combinaciones.
- Con más de dos activos, recordar la definición: la frontera de mínima varianza es el lugar geométrico de los portafolios que proveen el mínimo riesgo para un nivel de rendimiento dado.

 Ver en el tablero.

Analíticamente:
- $N$ activos,
- caracterizados por $(\sigma_i,E[r_i])$,
- cada uno con peso $w_i$, con $i=1,2,\dots,N$.

Entonces, buscamos los pesos tales que

\begin{align}
\min_{w_1,\dots,w_n} & \quad \sum_{i=1}^{N}w_i^2\sigma_i^2+\sum_{i=1}^{N}\sum_{j=1,j\neq i}^{N}w_iw_j\sigma_{ij}\\
\text{s.a.}          & \quad \sum_{i=1}^{N}w_i=1, w_i\geq0\\
                     & \quad \sum_{i=1}^{N}w_iE[r_i]=\bar{\mu},
\end{align}

donde $\bar{\mu}$ corresponde a un nivel de rendimiento objetivo.

**Obviamente, tendríamos que resolver este problema para muchos niveles de rendimiento objetivo.**

-  Explicar relación con gráfica.

-  Recordar clase 10.

Lo anterior se puede escribir vectorialmente como:

\begin{align}
\min_{\boldsymbol{w}} & \quad \boldsymbol{w}^T\Sigma\boldsymbol{w}\\
\text{s.a.}          & \quad \boldsymbol{1}^T\boldsymbol{w}=1, \boldsymbol{w}\geq0\\
                     & \quad E[\boldsymbol{r}^T]\boldsymbol{w}=\bar{\mu},
\end{align}

donde:
- $\boldsymbol{w}=\left[w_1,\dots,w_N\right]^T$ es el vector de pesos,
- $\boldsymbol{1}=\left[1,\dots,1\right]^T$ es un vector de unos,
- $E[\boldsymbol{r}]=\left[E[r_1],\dots,E[r_N]\right]^T$ es el vector de rendimientos esperados, y
- $\Sigma=\left[\begin{array}{cccc}\sigma_{1}^2 & \sigma_{12}  & \dots  & \sigma_{1N} \\
                                   \sigma_{21}  & \sigma_{2}^2 & \dots  & \sigma_{2N} \\
                                   \vdots       & \vdots       & \ddots & \vdots      \\
                                   \sigma_{N1}  & \sigma_{N2}  & \dots  & \sigma_{N}^2\end{array}\right]$ es la matriz de varianza-covarianza.

**Esta última forma es la que comúnmente usamos al programar, por ser eficiente y escalable a problemas de N variables.**

### Entonces, ¿para cuántos niveles de rendimiento objetivo tendríamos que resolver el anterior problema con el fin de graficar la frontera de mínima varianza?
- Observar que el problema puede volverse muy pesado a medida que incrementamos el número de activos en nuestro portafolio...
- Una tarea bastante compleja.

### Sucede que, en realidad, sólo necesitamos conocer dos portafolios que estén sobre la *frontera de mínima varianza*.
- Si logramos encontrar dos portafolios sobre la frontera, entonces podemos a la vez encontrar todas las posibles combinaciones de estos dos portafolios para trazar la frontera de mínima varianza.
- Ver el caso de dos activos.

### ¿Qué portafolios usar?
Hasta ahora, hemos estudiando profundamente como hallar dos portafolios muy importantes que de hecho yacen sobre la frontera de mínima varianza:
1. Portafolio de EMV: máximo SR.
2. Portafolio de mínima varianza: básicamente, el mismo problema anterior, sin la restricción de rendimiento objetivo.

Luego, tomar todas las posibles combinaciones de dichos portafolios usando las fórmulas para dos activos de medias y varianzas:
- w: peso para el portafolio EMV,
- 1-w: peso para le portafolio de mínima varianza.

## 2. Ejemplo ilustrativo.

Retomamos el ejemplo de mercados de acciones en los países integrantes del $G5$: EU, RU, Francia, Alemania y Japón.

```python
# Importamos pandas y numpy
import pandas as pd
import numpy as np
```

```python
# Resumen en base anual de rendimientos esperados y volatilidades
annual_ret_summ = pd.DataFrame(columns=['EU', 'RU', 'Francia', 'Alemania', 'Japon'], index=['Media', 'Volatilidad'])
annual_ret_summ.loc['Media'] = np.array([0.1355, 0.1589, 0.1519, 0.1435, 0.1497])
annual_ret_summ.loc['Volatilidad'] = np.array([0.1535, 0.2430, 0.2324, 0.2038, 0.2298])

annual_ret_summ.round(4)
```

      EU
      RU
      Francia
      Alemania
      Japon

      Media
      0.1355
      0.1589
      0.1519
      0.1435
      0.1497

      Volatilidad
      0.1535
      0.243
      0.2324
      0.2038
      0.2298

```python
# Matriz de correlación
corr = pd.DataFrame(data= np.array([[1.0000, 0.5003, 0.4398, 0.3681, 0.2663],
                                    [0.5003, 1.0000, 0.5420, 0.4265, 0.3581],
                                    [0.4398, 0.5420, 1.0000, 0.6032, 0.3923],
                                    [0.3681, 0.4265, 0.6032, 1.0000, 0.3663],
                                    [0.2663, 0.3581, 0.3923, 0.3663, 1.0000]]),
                    columns=annual_ret_summ.columns, index=annual_ret_summ.columns)
corr.round(4)
```

      EU
      RU
      Francia
      Alemania
      Japon

      EU
      1.0000
      0.5003
      0.4398
      0.3681
      0.2663

      RU
      0.5003
      1.0000
      0.5420
      0.4265
      0.3581

      Francia
      0.4398
      0.5420
      1.0000
      0.6032
      0.3923

      Alemania
      0.3681
      0.4265
      0.6032
      1.0000
      0.3663

      Japon
      0.2663
      0.3581
      0.3923
      0.3663
      1.0000

```python
# Tasa libre de riesgo
rf = 0.05
```

Esta vez, supondremos que tenemos disponibles todos los mercados de acciones y el activo libre de riesgo.

#### 1. Construir la frontera de mínima varianza

##### 1.1. Encontrar portafolio de mínima varianza

```python
# Importamos scipy
from scipy.optimize import minimize
```

```python
## Construcción de parámetros
# 1. Sigma: matriz de varianza-covarianza
D=np.diag(annual_ret_summ.loc['Volatilidad'])
Sigma = D.dot(corr).dot(D)
# 2. Eind: rendimientos esperados activos individuales
Eind = np.array(annual_ret_summ.loc['Media'])
```

```python
# Función objetivo
def varianza(w, Sigma):
    return w.dot(Sigma).dot(w)
```

```python
# Dato inicial
w0 = np.ones((5,))/5
# Cotas de las variables
bnds = ((0,1),)*5
# Restricciones
cons = ({'type': 'eq', 'fun': lambda w: np.sum(w)-1},)
```

```python
# Portafolio de mínima varianza
#min_var = opt.minimize(varianza, w0, args=(Sigma,), bounds=bnds, constraints=cons)
min_var = minimize(varianza, w0, args=(Sigma,),bounds=bnds ,constraints=cons)
min_var
```

         fun: 0.01861776391061502
         jac: array([0.03718246, 0.03881475, 0.03859101, 0.03755156, 0.0370423 ])
     message: 'Optimization terminated successfully.'
        nfev: 49
         nit: 7
        njev: 7
      status: 0
     success: True
           x: array([6.17797049e-01, 3.46944695e-18, 0.00000000e+00, 2.09394358e-01,
           1.72808594e-01])

```python
# Pesos, rendimiento y riesgo del portafolio de mínima varianza
w_minvar = min_var.x
E_minvar = Eind.dot(w_minvar)
s_minvar = np.sqrt(varianza(w_minvar,Sigma))
E_minvar, s_minvar
```

    (0.13962903688859557, 0.13644692708381168)

##### 1.2. Encontrar portafolio EMV

```python
# Función objetivo
def sharpe_r(w, Eind, Sigma, rf):
    erp=Eind.dot(w)
    varp=w.dot(Sigma).dot(w)
    return -(erp-rf)/np.sqrt(varp)
```

```python
# Dato inicial
w0 = np.ones((5,))/5
# Cotas de las variables
bnds = ((0,1),)*5
# Restricciones
cons = ({'type': 'eq', 'fun': lambda w: np.sum(w)-1},)
```

```python
# Portafolio EMV
#min_var = opt.minimize(varianza, w0, args=(Sigma,), bounds=bnds, constraints=cons)
EMV = minimize(sharpe_r, w0, args=(Eind, Sigma, rf), bounds=bnds, constraints=cons)
EMV
```

         fun: -0.6644372965632436
         jac: array([-0.36088951, -0.36076408, -0.36036385, -0.36108153, -0.36062376])
     message: 'Optimization terminated successfully.'
        nfev: 35
         nit: 5
        njev: 5
      status: 0
     success: True
           x: array([0.50714174, 0.07470888, 0.02471533, 0.18943972, 0.20399434])

```python
# Pesos, rendimiento y riesgo del portafolio EMV
w_EMV = EMV.x
E_EMV = Eind.dot(w_EMV)
s_EMV = np.sqrt(w_EMV.dot(Sigma).dot(w_EMV))
w_EMV,E_EMV, s_EMV
```

    (array([0.50714174, 0.07470888, 0.02471533, 0.18943972, 0.20399434]),
     0.1420657564717856,
     0.13856199365687238)

##### 1.3. Construir frontera de mínima varianza

También debemos encontrar la covarianza (o correlación) entre estos dos portafolios:

```python
# Covarianza entre los portafolios
cov_port = w_minvar.dot(Sigma).dot(w_EMV)
cov_port
```

    0.018690275386034134

```python
# Correlación entre los portafolios
corr_port = cov_port/(s_EMV*s_minvar)
corr_port
```

    0.9885708894197612

```python
# Vector de w
w = np.linspace(-4, 10, 1401)
```

```python
# DataFrame de portafolios:
# 1. Índice: i
# 2. Columnas 1-2: w, 1-w
# 3. Columnas 3-4: E[r], sigma
# 4. Columna 5: Sharpe ratio
fron_minvar = pd.DataFrame(columns=['wEMV', '1-wEMV', 'Ret', 'Vol', 'SR'])
fron_minvar['wEMV'] = w
fron_minvar['1-wEMV'] = 1-w
fron_minvar['Ret'] = w*E_EMV+(1-w)*E_minvar
fron_minvar['Vol'] = np.sqrt((w*s_EMV)**2+((1-w)*s_minvar)**2+2*w*(1-w)*cov_port)
fron_minvar['SR'] = (fron_minvar['Ret']-rf)/fron_minvar['Vol']
fron_minvar.round(4)
```

      wEMV
      1-wEMV
      Ret
      Vol
      SR

      0
      -4.00
      5.00
      0.1299
      0.1582
      0.5050

      1
      -3.99
      4.99
      0.1299
      0.1581
      0.5055

      2
      -3.98
      4.98
      0.1299
      0.1580
      0.5060

      3
      -3.97
      4.97
      0.1300
      0.1579
      0.5065

      4
      -3.96
      4.96
      0.1300
      0.1578
      0.5069

      5
      -3.95
      4.95
      0.1300
      0.1577
      0.5074

      6
      -3.94
      4.94
      0.1300
      0.1576
      0.5079

      7
      -3.93
      4.93
      0.1301
      0.1575
      0.5084

      8
      -3.92
      4.92
      0.1301
      0.1573
      0.5089

      9
      -3.91
      4.91
      0.1301
      0.1572
      0.5094

      10
      -3.90
      4.90
      0.1301
      0.1571
      0.5099

      11
      -3.89
      4.89
      0.1302
      0.1570
      0.5104

      12
      -3.88
      4.88
      0.1302
      0.1569
      0.5109

      13
      -3.87
      4.87
      0.1302
      0.1568
      0.5114

      14
      -3.86
      4.86
      0.1302
      0.1567
      0.5119

      15
      -3.85
      4.85
      0.1302
      0.1566
      0.5124

      16
      -3.84
      4.84
      0.1303
      0.1565
      0.5128

      17
      -3.83
      4.83
      0.1303
      0.1564
      0.5133

      18
      -3.82
      4.82
      0.1303
      0.1563
      0.5138

      19
      -3.81
      4.81
      0.1303
      0.1562
      0.5143

      20
      -3.80
      4.80
      0.1304
      0.1561
      0.5148

      21
      -3.79
      4.79
      0.1304
      0.1560
      0.5153

      22
      -3.78
      4.78
      0.1304
      0.1559
      0.5158

      23
      -3.77
      4.77
      0.1304
      0.1558
      0.5163

      24
      -3.76
      4.76
      0.1305
      0.1557
      0.5168

      25
      -3.75
      4.75
      0.1305
      0.1556
      0.5173

      26
      -3.74
      4.74
      0.1305
      0.1555
      0.5178

      27
      -3.73
      4.73
      0.1305
      0.1554
      0.5182

      28
      -3.72
      4.72
      0.1306
      0.1553
      0.5187

      29
      -3.71
      4.71
      0.1306
      0.1552
      0.5192

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

      1371
      9.71
      -8.71
      0.1633
      0.2474
      0.4580

      1372
      9.72
      -8.72
      0.1633
      0.2475
      0.4577

      1373
      9.73
      -8.73
      0.1633
      0.2477
      0.4575

      1374
      9.74
      -8.74
      0.1634
      0.2479
      0.4573

      1375
      9.75
      -8.75
      0.1634
      0.2481
      0.4571

      1376
      9.76
      -8.76
      0.1634
      0.2482
      0.4568

      1377
      9.77
      -8.77
      0.1634
      0.2484
      0.4566

      1378
      9.78
      -8.78
      0.1635
      0.2486
      0.4564

      1379
      9.79
      -8.79
      0.1635
      0.2488
      0.4562

      1380
      9.80
      -8.80
      0.1635
      0.2489
      0.4560

      1381
      9.81
      -8.81
      0.1635
      0.2491
      0.4557

      1382
      9.82
      -8.82
      0.1636
      0.2493
      0.4555

      1383
      9.83
      -8.83
      0.1636
      0.2495
      0.4553

      1384
      9.84
      -8.84
      0.1636
      0.2496
      0.4551

      1385
      9.85
      -8.85
      0.1636
      0.2498
      0.4549

      1386
      9.86
      -8.86
      0.1637
      0.2500
      0.4546

      1387
      9.87
      -8.87
      0.1637
      0.2502
      0.4544

      1388
      9.88
      -8.88
      0.1637
      0.2503
      0.4542

      1389
      9.89
      -8.89
      0.1637
      0.2505
      0.4540

      1390
      9.90
      -8.90
      0.1638
      0.2507
      0.4537

      1391
      9.91
      -8.91
      0.1638
      0.2509
      0.4535

      1392
      9.92
      -8.92
      0.1638
      0.2510
      0.4533

      1393
      9.93
      -8.93
      0.1638
      0.2512
      0.4531

      1394
      9.94
      -8.94
      0.1639
      0.2514
      0.4529

      1395
      9.95
      -8.95
      0.1639
      0.2516
      0.4526

      1396
      9.96
      -8.96
      0.1639
      0.2517
      0.4524

      1397
      9.97
      -8.97
      0.1639
      0.2519
      0.4522

      1398
      9.98
      -8.98
      0.1639
      0.2521
      0.4520

      1399
      9.99
      -8.99
      0.1640
      0.2523
      0.4518

      1400
      10.00
      -9.00
      0.1640
      0.2525
      0.4516

1401 rows × 5 columns

```python
# Importar librerías de gráficos
import matplotlib.pyplot as plt
%matplotlib inline
```

```python
# Gráfica de dispersión de puntos coloreando
# de acuerdo a SR
plt.figure(figsize=(10,7))
plt.plot(s_minvar, E_minvar, '*g', ms=20, label = 'Port. Min. Var.')
plt.plot(s_EMV, E_EMV, '*k', ms=20, label = 'Port. EMV')
plt.plot(annual_ret_summ.loc['Volatilidad'], Eind, 'oy', ms=10, label = 'Activos ind.')
plt.scatter(fron_minvar['Vol'], fron_minvar['Ret'], c=fron_minvar['SR'], cmap='RdYlBu', label = 'Front. Min. Var')
plt.xlabel('Volatility $\sigma$')
plt.ylabel('Expected return $E[r]$')
plt.legend(loc='best')

plt.colorbar()
plt.show()
```

**A partir de lo anterior, solo restaría construir la LAC y elegir la distribución de capital de acuerdo a las preferencias (aversión al riesgo).**
___

## 3. Comentarios finales

### 3.1. Restricciones adicionales

Los inversionistas pueden tener restricciones adicionales:
1. Restricciones en posiciones cortas.
2. Pueden requerir un rendimiento mínimo.
3. Inversión socialmente responsable: prescinden de inversiones en negocios o paises considerados éticamente o políticamente indeseables.

Todo lo anterior se puede incluir como restricciones en el problema de optimización, y puede ser llevado a cabo a costa de un cociente de Sharpe menor.

### 3.2. Críticas a la optimización media varianza
1. Solo importan medias y varianzas: recordar que la varianza subestima el riesgo en algunos casos.
2. Preferencias media-varianza tratan las ganancias y pérdidas simétricamente: el sentimiento de insatisfacción de una perdida es mayor al sentimiento de satisfacción de una ganancia (aversión a pérdidas).
3. La aversión al riesgo es constante: la actitud frente al riesgo puede cambiar, por ejemplo con el estado de la economía.
4. Horizonte corto (un periodo).
5. Basura entra - basura sale: la optimización media varianza es supremamente sensible a las entradas: estimaciones de rendimientos esperados y varianzas.
___

# Anuncios parroquiales

## 1. Revisar archivo Tarea 6.
## 2. Fin de módulo. Abrir Clase0 y revisar objetivos del módulo.
## 3. [Nota interesante](http://yetanothermathprogrammingconsultant.blogspot.com/2016/08/portfolio-optimization-maximize-sharpe.html)

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


# Introduction

This is the central location where all variables should be defined, and any relationships between them should be given.  Having all definitions collected in one file is useful because other files can reference this one, so there is no need for duplication, and less room for mistakes.  In particular, the relationships between variables are defined only here, so we don't need to reimplement those relationships.  And if you do ever find a mistake, you only have to fix it in one place, then just re-run the other notebooks.

There are two main classes of variables:

  1. Fundamental variables
  2. Derived variables

The distinction is only required for code output, to ensure that everything gets calculated correctly.  The PN equations you write down and manipulate can be in terms of any of these variables.

The fundamental variables that go into PN equations are things like the mass, spins $\chi_1$, and $\chi_2$, orbital angular velocity $\hat{\ell}$, and unit separation vector $\hat{n}$.  We also include the tidal-coupling parameters in this list.  Also, note that only $M_1$ is included.  This is because the total mass is always assumed to be 1, so $M_2 = 1-M_1$.

The derived variables are further distinguished by whether they will need to be recalculated at each time step or not.  For example, though we define the spins fundamentally as $\chi_1$ and $\chi_2$, we can also define derived spins $S$ and $\Sigma$, which need to be recalculated if the system is precessing.  On the other hand, the masses are constant and fundamentally defined by $M_1$, so $M_2$ and $\nu$ only need to be calculated from that information once.

# Set up python

```python
# Make sure division of integers does not round to the nearest integer
from __future__ import division

import sys
sys.path.insert(0, '..') # Look for modules in directory above this one

# Make everything in python's symbolic math package available
from sympy import * # Make sure sympy functions are used in preference to numpy
import sympy # Make sympy. constructions available
from sympy import Rational as frac # Rename for similarity to latex
from sympy import log as ln

# Print symbolic expressions nicely
init_printing()

# We'll use the numpy `array` object for vectors
from numpy import array, cross, dot

# We'll use a custom object to keep track of variables
from Utilities.PNObjects import PNCollection
PNVariables = PNCollection()
```

# Fundamental variables

Only the most basic variables should be defined here.

Note that we will be using (quaternion) logarithmic rotors to describe the orientations of the spins, and the orientation and velocity of the binary itself.  This allows us to reduce the number of constraints in the system, and only evolve the minimal number of equations.  For example, the spins are constant, so only two degrees of freedom are needed.  These can be expressed without ambiguities or singularities in the form of logarithmic rotors: $\mathfrak{r}_1 = \mathfrak{r}_{\chi_1 x} \hat{x} + \mathfrak{r}_{\chi_1 y} \hat{y}$, so that $\vec{\chi}_1 = S_{\chi_1}\, e^{\mathfrak{r}_1}\, \hat{z}\, e^{-\mathfrak{r}_1}\, \bar{S}_{\chi_1}$.  Here, $S_{\chi_1}$ is a constant spinor with magnitude $\sqrt{\lvert \chi_1 \rvert}$ encoding the initial direction of the spin.  This may look complicated, but it performs very well numerically.

We will still be able to write and manipulate the PN equations directly in terms of familiar quantities like $\vec{S}_1 \cdot \hat{\ell}$, etc., but the fundamental objects will be the rotors, which means that the substitutions made for code output will automatically be in terms of the rotors.

```python
# Unit basis vectors
PNVariables.AddBasicConstants('xHat, yHat, zHat', datatype='Quaternions::Quaternion', commutative=False)

# Dimensionful quantities, just in case anybody uses them...
PNVariables.AddBasicConstants('G, c')

# Masses of objects 1 and 2.
PNVariables.AddBasicConstants('M1')
PNVariables.AddBasicConstants('M2')

# Angular speed of separation vector
PNVariables.AddBasicVariables('v')

# Initial spins expressed as spinors taking zHat onto those spins (assumed to have constant magnitudes)
PNVariables.AddBasicConstants('S_chi1', datatype='Quaternions::Quaternion', commutative=False)
PNVariables.AddBasicConstants('S_chi2', datatype='Quaternions::Quaternion', commutative=False)

# Dynamic spin directions
PNVariables.AddBasicVariables('rfrak_chi1_x, rfrak_chi1_y')
PNVariables.AddBasicVariables('rfrak_chi2_x, rfrak_chi2_y')

# Tidal deformabilities, in units where the total mass is 1
PNVariables.AddBasicConstants('lambda1, lambda2')

# Frame aligned to  Orbital angular velocity vector and magnitude ("Newtonian" angular momentum)
PNVariables.AddBasicVariables('rfrak_frame_x, rfrak_frame_y, rfrak_frame_z')
```

# Derived variables

Any variable that can be derived from the variables above should be put in this section.

These variables should probably be left in arbitrary form, unless a particular simplification is desired.  The `substitutions` dictionary should map from the general names and their definitions in terms of basic variables.  In numerical codes, their values can be calculated once per time step and then stored, so that the values do not have to be re-calculated every time they appear in an expression.

Various common combinations of the two masses:

```python
PNVariables.AddDerivedConstant('M', M1+M2)
PNVariables.AddDerivedConstant('delta', (M1-M2)/M)
PNVariables.AddDerivedConstant('nu', M1*M2/M**2)
PNVariables.AddDerivedConstant('nu__2', (M1*M2/M**2)**2)
PNVariables.AddDerivedConstant('nu__3', (M1*M2/M**2)**3)
PNVariables.AddDerivedConstant('q', M1/M2)
```

The system's vector basis is given by $(\hat{\ell}, \hat{n}, \hat{\lambda})$, and will be computed by the code in terms of the fundamental logarithmic rotors defined above.  Here, we give all the substitutions that will be needed in the code.

```python
# This rotor encodes all information about the frame
PNVariables.AddDerivedVariable('R', exp(rfrak_frame_x*xHat + rfrak_frame_y*yHat + rfrak_frame_z*zHat),
                               datatype='Quaternions::Quaternion', commutative=False)

# Unit separation vector between the compact objects
PNVariables.AddDerivedVariable('nHat', R*xHat*conjugate(R), datatype='Quaternions::Quaternion')

# Unit vector orthogonal to the other two; in the direction of velocity
PNVariables.AddDerivedVariable('lambdaHat', R*yHat*conjugate(R), datatype='Quaternions::Quaternion')

# Unit vector in direction of angular velocity
PNVariables.AddDerivedVariable('ellHat', R*zHat*conjugate(R), datatype='Quaternions::Quaternion')

# Components of the above
for i,d in zip(['1','2','3'],['x','y','z']):
    PNVariables.AddDerivedVariable('nHat_'+d, substitution_atoms=[nHat], substitution='nHat['+i+']')
for i,d in zip(['1','2','3'],['x','y','z']):
    PNVariables.AddDerivedVariable('lambdaHat_'+d, substitution_atoms=[lambdaHat], substitution='lambdaHat['+i+']')
for i,d in zip(['1','2','3'],['x','y','z']):
    PNVariables.AddDerivedVariable('ellHat_'+d, substitution_atoms=[ellHat], substitution='ellHat['+i+']')
```

Various spin components and combinations:

```python
# These rotors encode all information about the spin directions
PNVariables.AddDerivedVariable('R_S1', exp(rfrak_chi1_x*xHat + rfrak_chi1_y*yHat),
                               datatype='Quaternions::Quaternion', commutative=False)
PNVariables.AddDerivedVariable('R_S2', exp(rfrak_chi2_x*xHat + rfrak_chi2_y*yHat),
                               datatype='Quaternions::Quaternion', commutative=False)

# The spins are derived from rfrak_chi1_x, etc.
PNVariables.AddDerivedVariable('chiVec1', S_chi1*R_S1*zHat*conjugate(R_S1)*conjugate(S_chi1), datatype='Quaternions::Quaternion')
PNVariables.AddDerivedVariable('chiVec2', S_chi2*R_S2*zHat*conjugate(R_S2)*conjugate(S_chi2), datatype='Quaternions::Quaternion')
for i,d in zip(['1','2','3'],['x','y','z']):
    PNVariables.AddDerivedVariable('chi1_'+d, substitution_atoms=[chiVec1], substitution='chiVec1['+i+']')
for i,d in zip(['1','2','3'],['x','y','z']):
    PNVariables.AddDerivedVariable('chi2_'+d, substitution_atoms=[chiVec2], substitution='chiVec2['+i+']')

PNVariables.AddDerivedConstant('chi1chi1', substitution_atoms=[chiVec1], substitution='chiVec1.normsquared()')
PNVariables.AddDerivedConstant('chi1chi2', substitution_atoms=[chiVec1,chiVec2], substitution='chiVec1.dot(chiVec2)')
PNVariables.AddDerivedConstant('chi2chi2', substitution_atoms=[chiVec2], substitution='chiVec2.normsquared()')

PNVariables.AddDerivedVariable('chi1_n', substitution_atoms=[chiVec1,nHat], substitution='chiVec1.dot(nHat)')
PNVariables.AddDerivedVariable('chi1_lambda', substitution_atoms=[chiVec1,lambdaHat], substitution='chiVec1.dot(lambdaHat)')
PNVariables.AddDerivedVariable('chi1_ell', substitution_atoms=[chiVec1,ellHat], substitution='chiVec1.dot(ellHat)')
PNVariables.AddDerivedVariable('chi2_n', substitution_atoms=[chiVec2,nHat], substitution='chiVec2.dot(nHat)')
PNVariables.AddDerivedVariable('chi2_lambda', substitution_atoms=[chiVec2,lambdaHat], substitution='chiVec2.dot(lambdaHat)')
PNVariables.AddDerivedVariable('chi2_ell', substitution_atoms=[chiVec2,ellHat], substitution='chiVec2.dot(ellHat)')

PNVariables.AddDerivedConstant('sqrt1Mchi1chi1', sqrt(1-chi1chi1))
PNVariables.AddDerivedConstant('sqrt1Mchi2chi2', sqrt(1-chi2chi2))

PNVariables.AddDerivedVariable('S', chiVec1*M1**2 + chiVec2*M2**2, datatype=chiVec1.datatype)
PNVariables.AddDerivedVariable('S_ell', chi1_ell*M1**2 + chi2_ell*M2**2)
PNVariables.AddDerivedVariable('S_n', chi1_n*M1**2 + chi2_n*M2**2)
PNVariables.AddDerivedVariable('S_lambda', chi1_lambda*M1**2 + chi2_lambda*M2**2)

PNVariables.AddDerivedVariable('Sigma', M*(chiVec2*M2 - chiVec1*M1), datatype=chiVec1.datatype)
PNVariables.AddDerivedVariable('Sigma_ell', M*(chi2_ell*M2 - chi1_ell*M1))
PNVariables.AddDerivedVariable('Sigma_n', M*(chi2_n*M2 - chi1_n*M1))
PNVariables.AddDerivedVariable('Sigma_lambda', M*(chi2_lambda*M2 - chi1_lambda*M1))

PNVariables.AddDerivedVariable('S1', chiVec1*M1**2, datatype=chiVec1.datatype)
PNVariables.AddDerivedVariable('S1_ell', chi1_ell*M1**2)
PNVariables.AddDerivedVariable('S1_n', chi1_n*M1**2)
PNVariables.AddDerivedVariable('S1_lambda', chi1_lambda*M1**2)

PNVariables.AddDerivedVariable('S2', chiVec2*M2**2, datatype=chiVec1.datatype)
PNVariables.AddDerivedVariable('S2_ell', chi2_ell*M2**2)
PNVariables.AddDerivedVariable('S2_n', chi2_n*M2**2)
PNVariables.AddDerivedVariable('S2_lambda', chi2_lambda*M2**2)

PNVariables.AddDerivedVariable('chi_s', (chiVec1 + chiVec2)/2, datatype=chiVec1.datatype)
PNVariables.AddDerivedVariable('chi_s_ell', (chi1_ell+chi2_ell)/2)
PNVariables.AddDerivedVariable('chi_s_n', (chi1_n+chi2_n)/2)
PNVariables.AddDerivedVariable('chi_s_lambda', (chi1_lambda+chi2_lambda)/2)

PNVariables.AddDerivedVariable('chi_a', (chiVec1 - chiVec2)/2, datatype=chiVec1.datatype)
PNVariables.AddDerivedVariable('chi_a_ell', (chi1_ell-chi2_ell)/2)
PNVariables.AddDerivedVariable('chi_a_n', (chi1_n-chi2_n)/2)
PNVariables.AddDerivedVariable('chi_a_lambda', (chi1_lambda-chi2_lambda)/2)
```

Other functions of the angular velocity that find frequent use:

```python
PNVariables.AddDerivedVariable('x', v**2)
PNVariables.AddDerivedVariable('Omega_orb', v**3/M)
PNVariables.AddDerivedVariable('logv', log(v))
```

Note the simple expressions $M_{1,2} = M \frac{1 \pm \delta}{2}$, which lead to the useful translations
\begin{align}
  \frac{\vec{S}}{M^2} &= (1-2\nu)\vec{\chi}_s + \delta \vec{\chi}_a, \\
  \frac{\vec{\Sigma}}{M^2} &= -\delta\vec{\chi}_s - \vec{\chi}_a,
\end{align}
and equivalently
\begin{align}
  \chi_s &= \frac{\vec{S} + \delta \vec{\Sigma}}{2M^2\nu}, \\
  \chi_a &= - \frac{\delta \vec{S} + (1-2\nu) \vec{\Sigma}} {2M^2\nu}.
\end{align}


# TP 1 Régression logistique

# Régularisation de Tikhonov

```python
import numpy as np
from scipy.optimize import check_grad
from  diabeticRetinopathyUtils import *
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.metrics import classification_report
from numpy import inf
from time import time
X,Y = load_diabetic_retinopathy("diabeticRetinopathy.csv")
```

## Question 1.1

En posant $ \widetilde{X} = (\tilde{x_i}, \tilde{x_2},\dots, \tilde{x_{n-1}},\tilde{x_n} )  \in \mathbb R^{(p+1) \times n }$ ,où $ \tilde{x_i} =\begin{pmatrix} 1 \\ x_i \end{pmatrix}  \in R^{p+1}$  ,  $\tilde{\omega} = \begin{pmatrix} \omega_0 \\ \omega_1 \end{pmatrix}  \in \mathbb R^{p+1}$ et la matrice diagonale
$$
A = diag(0,1,1,\dots, 1) =
\begin{pmatrix}
0& 0& 0& \dots &\dots &0 \\
0& 1& 0& \dots &\dots &0 \\
\vdots &\vdots& \vdots&  \ddots & & \vdots\\
\vdots &\vdots &\vdots& &\ddots & \vdots \\
0& 0& 0& \dots  &\dots &1
\end{pmatrix}
$$

  Alors la fonction peut être ecrits sous format ci-dessous:

$$ f_1: (\tilde{\omega}) \mapsto \frac{1}{n} \sum_{i=1}^{n} \log(1+exp(-y_i \tilde{x}_i^T\tilde{\omega})) + \frac{\rho}{2}\tilde{\omega}^ TA \tilde{\omega} $$

  Ainsi on obtient le gradient:
        $$
        \begin{aligned}
        \nabla{f_1}(\tilde{\omega}) &= \frac{1}{n}\sum_{i=1}^{n}\frac{-e^{-y_i\tilde x_i^T \tilde \omega}y_i\tilde{\mathbf{x}}_i}{1+e^{-y_i\tilde x_i^T \tilde \omega}} + \rho A\tilde{\mathbf{\omega}} \\
        &= \frac{1}{n}\sum_{i=1}^{n}\frac{-y_i\tilde{\mathbf{x}}_i}{1+e^{y_i\tilde x_i^T \tilde \omega}} +
        \rho A\tilde{\mathbf{\omega}}
        \end{aligned}
        $$

        et la matrice hessienne:
        $$
        \begin{aligned}
        \mathbf{H} = \nabla^2f_1(\tilde{\omega}) &= \frac{1}{n}\sum_{i=1}^{n}\frac{e^{y_i\tilde x_i^T \tilde \omega}(y_i\tilde{\mathbf{x}}_i)(y_i\tilde{\mathbf{x}}_i)^T}{(1+e^{y_i\tilde x_i^T \tilde \omega})^2} + \rho A \\
        & = \frac{1}{n}\sum_{i=1}^{n}\frac{(y_i\tilde{\mathbf{x}}_i)(y_i\tilde{\mathbf{x}}_i)^T}{(1+e^{y_i\tilde x_i^T \tilde \omega})(1+e^{-y_i\tilde x_i^T \tilde \omega})} + \rho A
        \end{aligned}
        $$
Soient $\omega \in \mathbb{R}^{p+1}$, on a:
        $$
        \begin{aligned}
        \omega^TH\omega &= \frac{1}{n}\sum_{i=1}^{n}\frac{\omega^T (y_i\tilde{\mathbf{x}}_i)(y_i\tilde{\mathbf{x}}_i)^T \omega}{(1+e^{y_i\tilde x_i^T \tilde \omega})(1+e^{-y_i\tilde x_i^T \tilde \omega})} + \rho \omega^T A \omega \\
        &= \frac{1}{n}\sum_{i=1}^{n}\frac{(\omega^T y_i\tilde{\mathbf{x}}_i)(\omega^T y_i\tilde{\mathbf{x}}_i)^T}{(1+e^{y_i\tilde x_i^T \tilde \omega})(1+e^{-y_i\tilde x_i^T \tilde \omega})} + \rho \omega^T A^2 \omega \\
        &= \frac{1}{n}\sum_{i=1}^{n}\frac{\|\omega^T y_i\tilde{\mathbf{x}}_i\|_2^2}{(1+e^{y_i\tilde x_i^T \tilde \omega})(1+e^{-y_i\tilde x_i^T \tilde \omega})} + \rho \|A\omega\|_2^2 \geq 0
        \end{aligned}
        $$

Donc, la matrice hessienne est semi-définie positive, la fonction $f_1$ est convexe.

## Question 1.2

```python
def objective_fun(X, Y, omega_0, omega, rho=1.0/X.shape[0]):
    """
    X： matrix de taille n*p
    Y: vecteur de taille n
    omega: vecteur de dimension p
    omega0 : un nombre réel
    """

    n,p = X.shape
    omega_tilde = np.append(omega_0, omega)
    X_tilde = np.c_[np.ones(n), X]
    A = np.identity(p+1)
    A[0][0] = 0

    fun_value = 0
    grad_value = np.zeros(p+1)
    hess_value = np.zeros((p+1,p+1))

    for i in range(n):
        item = np.exp(Y[i]*np.dot(X_tilde[i],omega_tilde))
        fun_value = fun_value + np.log(1.0 + 1.0/item)
        grad_value = grad_value +  (-Y[i] * X_tilde[i])/(1.0+item)
        hess_value = hess_value + (Y[i]* Y[i] * np.outer(X_tilde[i],X_tilde[i]))/((1.+item)*(1.+1.0/item))

    fun_value = fun_value/n + rho/2.0 * np.linalg.norm(omega_tilde)**2
    grad_value = grad_value/n + rho *omega_tilde
    hess_value = hess_value/n + rho*A

    return fun_value, grad_value, hess_value

```

```python
omega = np.arange(19)
n,p = X.shape
obj = lambda omega: objective_fun(X, Y, omega[0],omega[1:])[0]
grad = lambda omega: objective_fun(X, Y, omega[0], omega[1:])[1]

print("The difference of gradient is: %0.12f" % check_grad(obj, grad, np.zeros(20)))
```

    The difference of gradient is: 0.000005581540

```python
omega = np.arange(19)
n,p = X.shape
grad_sum = lambda omega: objective_fun(X, Y, omega[0],omega[1:])[1].sum()
hess = lambda omega: objective_fun(X, Y, omega[0], omega[1:])[2].sum(axis=1)
check_grad(obj, grad, np.zeros(20))
print("The difference of hessian is: %0.12f" % check_grad(grad_sum, hess, np.zeros(20)))
```

    The difference of hessian is: 0.000868842017

Alors, le gradient et la matrice hessienne sont bien vérifié

## Question 1.3

```python
def log(func):
    def warpper(*args, **kw):
        t0 = time()
        fun_val, sln, re_list = func(*args, **kw)
        t_nls = time()-t0
        indice = np.arange(len(re_list))
        plt.semilogy(indice, re_list)
        plt.xlabel("Nombre d'iteration")
        plt.ylabel("Norme de gradien")
        plt.grid(True)
        plt.show()
        print("The value minimal of the objective function is:",fun_val)
        print("Done in %0.3fs, number of iterations: %d" % (t_nls, len(re_list)))
        return sln
    return warpper
```

```python
@log
def method_newtown(omega):
    w = omega
    [fun_v, grad_v, hess_v] = objective_fun(X,Y, omega[0], omega[1:])
    norme_list = list()
    nbIt = 0
    norm = np.linalg.norm(grad_v)

    while(  norm > 10.0e-10):
        w = w -  np.linalg.inv(hess_v).dot(grad_v)
        [fun_v, grad_v, hess_v] = objective_fun(X,Y, w[0], w[1:])
        norme_list.append(norm)
        nbIt = nbIt+1
        norm = np.linalg.norm(grad_v)

    return fun_v,w,norme_list
```

```python
method_newtown(np.zeros(p+1))
```

## Question 1.4

```python
method_newtown(np.ones(p+1) *0.3)
```

On a vue que dans cette condition initiale, le matrix de hessian est singulier, c'est à dire son inverse n'existe pas. Donc on ne peut pas utilise cette méthode de trouver une solutions juste.

## Question 1.5

Notons $h_k = -H^{-1}(x_k) \nabla f(x_k)$,
Notons $\omega^+(\gamma_k)=\omega^k - \gamma_k(\nabla^2 f_1(\omega^k))^{-1}\nabla f_1(\omega^k)$, soient $a \in (0,1)$, $b>0$ et $\beta \in (0,1)$, on cherche le premier entier $l$ non-négatif tel que:

    $$f_1(\omega^+(ba^l)) \leq f_1(\omega^k) + \beta\langle\nabla_{f_1}(\omega^k),\,\omega^+(ba^l)-\omega^k\rangle$$

Ici, on prend $\beta = 0.5$, ainsi que la recherche linéaire d'Armijo devient équicalente à la recherche linéaire de Taylor.

On fixe $b_0 = 1$ et $b_k = 2\gamma_{k-1}$, c'est un choix classique.

On fixe $a = 0.5$, c'est pour faire un compromis entre la précision de recherche et la vitesse de convergence.

```python
@log
def method_newtown_armijo(omega,epsilon,a,b,beta):
    w = omega
    [fun_v, grad_v, hess_v] = objective_fun(X,Y, w[0], w[1:])
    norm_list = list()
    nbIt = 0
    norm = np.linalg.norm(grad_v)
    gamma = beta

    while(  norm > epsilon ):
        h_k = np.linalg.inv(hess_v).dot(-grad_v)
        gamma = gamma*2.0
        w_plus = w + gamma * h_k
        val_plus, grad_plus, hess_plus = objective_fun(X,Y, w_plus[0], w_plus[1:])
        while( val_plus > fun_v + beta*gamma * h_k.dot(grad_v)):
            gamma = gamma * a
            w_plus = w + gamma * h_k
            val_plus, grad_plus, hess_plus = objective_fun(X,Y, w_plus[0], w_plus[1:])

        w = w_plus
        [fun_v, grad_v, hess_v] = objective_fun(X,Y, w[0], w[1:])
        norm_list.append(norm)
        nbIt = nbIt+1
        norm = np.linalg.norm(grad_v)

    return fun_v,w,norm_list
```

```python
w = np.ones(p+1)*0.3
w_tik = method_newtown_armijo(w,10e-10,0.5,1.0,0.5)
```

# Régularisation pour la parcimonie

## Question 2.1
Pourquoi ne peut-on pas utiliser la méthode de Newton pour résoudre ce problème?

Comme la fonction
$$ \underset{\omega \in \mathbb R, \omega_0 \in \mathbb R^p}{\operatorname{min}} \frac{1}{n}
\sum_{i=1}^n \log\left(1+e^{-y_i(x_i^T\omega + \omega_0)}\right) + \rho ||\omega||_1$$
Il est évident que la terme $\rho||\omega||_1 $ n'est pas différentiable en point $\omega = 0$. Donc cette fonction objective n'est pas différentiable. Par conséquent, on ne peut pas utilser la méthode de Newton pour résoudre ce problème.

## Question 2.2

La fonction objective peut être écrit sous la forme ci-dessous:
$$\begin{aligned}
F_2(\omega, \omega_0)  &= \frac{1}{n}
\sum_{i=1}^n \log\left(1+e^{-y_i(x_i^T\omega + \omega_0)}\right) + \rho ||\omega||_1 \\
&= f_2 + g_2 \\
\end{aligned}$$
où
$$\begin{aligned}
f_2(\omega, \omega_0)  &= \frac{1}{n}
\sum_{i=1}^n \log\left(1+e^{-y_i(x_i^T\omega + \omega_0)}\right), \\
g_2(\omega) &=  \rho ||\omega||_1
\end{aligned}$$
D'après le resultat du question précédent et ce que l'on voit sur le cours, on peut facilement déduire que $f_2$ est dérivable et l'opérateur proximal de $g_2$ est facile à calcule.

On a l'opérateur de proximal de $g_2$:
$$\begin{aligned}
prox_{\gamma g}(x) &= \underset{y \in \mathbb R^p}{\operatorname{argmin}} g(y) + \frac{1}{2\gamma} || y - x||_2^2 \\
& =\underset{y \in \mathbb R^p}{\operatorname{argmin}} \rho||y||_1 + \frac{1}{2\gamma} || y - x||_2^2 \\
& = \underset{y \in \mathbb R^p}{\operatorname{argmin}} \rho\sum_{i=1}^{p}|y_i| + \frac{1}{2\gamma} \sum_{i=1}^p (y_i - x_i)^2 \\
& = \underset{y \in \mathbb R^p}{\operatorname{argmin}} \sum_{i=1}^{p} \left(\rho|y_i| + \frac{1}{2\gamma} (y_i - x_i)^2 \right) \\
&= y^* \\
&= \begin{pmatrix} y^*_1 \\ y^*_2 \\ y^*_3 \\ \vdots \\ y^*_p \end{pmatrix}
\end{aligned}$$
où
$$ y^*_i = \underset{y_i \in \mathbb R}{\operatorname{argmin}} \rho|y_i| + \frac{1}{2\gamma} (y_i - x_i)^2 , i \in [\![1;p]\!], \gamma >0$$
En posant
$$t(y_i)= \rho|y_i| + \frac{1}{2\gamma} (y_i - x)^2)$$
Alors

$$ \frac{dt}{dy_i} = +\rho\frac{|y_i|}{y_i} + \frac{1}{\gamma}(y_i-x)$$
En posant $\frac{dt}{dy_i} = 0$, on peut facile d'obtenir le $y^*_i$
$$ y_i^* = \left\{
\begin{align}
\,x_i-\gamma\rho  &,\, si \,& x_i > \gamma\rho \\
\,x_i+\gamma\rho\,  &,\, si \, &x_i < -\gamma\rho \\
\,0 &,\, sinon \\
\end{align} \right.$$

Pour le gradient de $f_2$, on a
$$
        \begin{aligned}
        \nabla{f_2}(\omega_0, \omega) &= \frac{1}{n}\sum_{i=1}^{n}\frac{-e^{-y_i\tilde x_i^T \tilde \omega}y_i\tilde{\mathbf{x}}_i}{1+e^{-y_i\tilde x_i^T \tilde \omega}} \\
        &= \frac{1}{n}\sum_{i=1}^{n}\frac{-y_i\tilde{\mathbf{x}}_i}{1+e^{y_i\tilde x_i^T \tilde \omega}}
        \end{aligned}
$$
Pour lla matrix hessian, on a
 $$
        \begin{aligned}
        \mathbf{H_2} = \nabla^2f_2(\omega_0, \omega) &= \frac{1}{n}\sum_{i=1}^{n}\frac{e^{y_i\tilde x_i^T \tilde \omega}(y_i\tilde{\mathbf{x}}_i)(y_i\tilde{\mathbf{x}}_i)^T}{(1+e^{y_i\tilde x_i^T \tilde \omega})^2} \\
        & = \frac{1}{n}\sum_{i=1}^{n}\frac{(y_i\tilde{\mathbf{x}}_i)(y_i\tilde{\mathbf{x}}_i)^T}{(1+e^{y_i\tilde x_i^T \tilde \omega})(1+e^{-y_i\tilde x_i^T \tilde \omega})}
        \end{aligned}
$$

Soient $\omega \in \mathbb{R}^{p+1}$, on a:
$$
\begin{aligned}
\omega^TH_2\omega &= \frac{1}{n}\sum_{i=1}^{n}\frac{\omega^T (y_i\tilde{\mathbf{x}}_i)(y_i\tilde{\mathbf{x}}_i)^T \omega}{(1+e^{y_i\tilde x_i^T \tilde \omega})(1+e^{-y_i\tilde x_i^T \tilde \omega})} \\
&= \frac{1}{n}\sum_{i=1}^{n}\frac{(\omega^T y_i\tilde{\mathbf{x}}_i)(\omega^T y_i\tilde{\mathbf{x}}_i)^T}{(1+e^{y_i\tilde x_i^T \tilde \omega})(1+e^{-y_i\tilde x_i^T \tilde \omega})} \\
 &= \frac{1}{n}\sum_{i=1}^{n}\frac{\|\omega^T y_i\tilde{\mathbf{x}}_i\|_2^2}{(1+e^{y_i\tilde x_i^T \tilde \omega})(1+e^{-y_i\tilde x_i^T \tilde \omega})} \geq 0
\end{aligned}
$$

Donc, la matrice hessienne de $f_2$ est semi-définie positive, la fonction $f_2$ est convexe.
  $$
        \begin{aligned}
        g_2(\omega_0, \omega) &= \rho\|\omega\|_1 \\
        &= \rho \sum_{i=1}^{n}|\omega_i|
        \end{aligned}
    $$

La fonction de valeur absolue est convexe pour chaque élément de $\omega$, pour $\rho \geq 0$, $g_2$ est aussi convexe.

Donc $F_2 = f_2 + g_2$ est convexe pour $\rho \geq 0$.

## Question 2.3

```python
def prox_g(x, rho, gamma):
    if(rho<0 or gamma <0):
        raise ValueError("Value rho or gamma have to be positive: rho=%2.2f, gamma=%2.2f"% (rho, gamma))
    def prox_real(x):
        if(x> rho*gamma):
            return x-rho*gamma
        elif(x <-rho*gamma):
            return x+rho*gamma
        else:
            return 0.0
    return np.array([*map(prox_real, x)])

def reg_obj_fun(X, Y, omega_0, omega, rho):
    """
    X： matrix de taille n*p
    Y: vecteur de taille n
    omega: vecteur de dimension p
    omega0 : un nombre réel
    """
    n,p = X.shape
    omega_tilde = np.append(omega_0, omega)
    X_tilde = np.c_[np.ones(n), X]

    fun_value = 0.0

    for i in range(n):
        item = np.exp(-Y[i]*np.dot(X_tilde[i],omega_tilde))
        fun_value = fun_value + np.log(1.0 + item)

    fun_value = fun_value/n + rho * np.absolute(omega_tilde).sum()

    return fun_value

def f(X, Y, omega_0, omega, rho, value_only = False):
    """
    X： matrix de taille n*p
    Y: vecteur de taille n
    omega: vecteur de dimension p
    omega0 : un nombre réel
    """

    n,p = X.shape
    omega_tilde = np.append(omega_0, omega)
    X_tilde = np.c_[np.ones(n), X]

    fun_value = 0
    grad_value = np.zeros(p+1)
    hess_value = np.zeros((p+1,p+1))

    for i in range(n):
        item = np.exp(Y[i]*np.dot(X_tilde[i],omega_tilde))
        fun_value = fun_value + np.log(1.0 + 1.0/item)
        if(not value_only):
            grad_value = grad_value +  (-Y[i] * X_tilde[i])/(1.0+item)
            hess_value = hess_value + (Y[i]* Y[i] * np.outer(X_tilde[i],X_tilde[i]))/((1.+item)*(1.+1.0/item))

    fun_value = fun_value/n
    if(not value_only):
        grad_value = grad_value/n
        hess_value = hess_value/n

    if(not value_only):
        return fun_value, grad_value, hess_value
    else:
        return fun_value
```

```python
@log
def method_proximal(omega,epsilon,a,b, rho):
    w = omega
    f_v, f_grad, f_hess = f(X,Y, omega[0], omega[1:], rho)

    nbIt = 0
    old_val = inf
    changement = np.fabs(f_v - old_val)
    deltaList = list()

    while(changement > epsilon):
        gamma = b # initialiser la valeur de gamma
        w_plus = prox_g(w - gamma * f_grad, rho, gamma)
        val_plus = f(X,Y, w_plus[0], w_plus[1:],rho, value_only=True)

        while( val_plus > f_v + f_grad.dot(w_plus-w)  +  1.0/gamma * np.linalg.norm(w_plus-w)**2):
            gamma = gamma * a
            w_plus = prox_g(w - gamma * f_grad, rho, gamma)
            val_plus = f(X,Y,w_plus[0], w_plus[1:], rho, value_only = True )

        w = w_plus
        old_val = f_v
        f_v, f_grad, f_hess = f(X,Y, w[0], w[1:],rho)
        nbIt = nbIt+1
        changement =np.fabs(f_v - old_val)
        deltaList.append(changement)

    return reg_obj_fun(X, Y, w[0], w[1:], rho),w, deltaList
```

```python
w = np.ones(p+1) * 0.3
w_prox = method_proximal(w,10e-10,0.5,2, 0.1)
```

# Comparaison

## Question 3.1

```python

```

## Question 3.2

```python
y_pred_nls = np.sign(np.c_[np.ones(n), X].dot(w_tik))
y_pred_pgls = np.sign(np.c_[np.ones(n), X].dot(w_prox))
print("The chance level is: %f" % max(np.mean(Y == 1), 1-np.mean(Y == 1)))
print("The score by Newton method with line search is: %f" % np.mean(Y == y_pred_nls))
print("The score by proximal gradient method with line search is: %f" % np.mean(Y == y_pred_pgls))

#print('-'*60)
print("Classification report for Newton method")
print(classification_report(Y, y_pred_nls))
#print('-'*60)
print("Classification report for proximal gradient method")
print(classification_report(Y, y_pred_pgls))
```

    The chance level is: 0.530843
    The score by Newton method with line search is: 0.741095
    The score by proximal gradient method with line search is: 0.592528
    Classification report for Newton method
                 precision    recall  f1-score   support

           -1.0       0.69      0.82      0.75       540
            1.0       0.81      0.67      0.73       611

    avg / total       0.75      0.74      0.74      1151

    Classification report for proximal gradient method
                 precision    recall  f1-score   support

           -1.0       0.56      0.63      0.59       540
            1.0       0.63      0.56      0.59       611

    avg / total       0.60      0.59      0.59      1151

```python

```

```python

```

```python

```

```python

```


# Lid driven cavity

$$
\renewcommand{\DdQq}[2]{{\mathrm D}_{#1}{\mathrm Q}_{#2}}
\renewcommand{\drondt}{\partial_t}
\renewcommand{\drondx}{\partial_x}
\renewcommand{\drondy}{\partial_y}
\renewcommand{\drondtt}{\partial_{tt}}
\renewcommand{\drondxx}{\partial_{xx}}
\renewcommand{\drondyy}{\partial_{yy}}
\renewcommand{\dx}{\Delta x}
\renewcommand{\dt}{\Delta t}
\renewcommand{\grandO}{{\mathcal O}}
\renewcommand{\density}[2]{\,f_{#1}^{#2}}
\renewcommand{\fk}[1]{\density{#1}{\vphantom{\star}}}
\renewcommand{\fks}[1]{\density{#1}{\star}}
\renewcommand{\moment}[2]{\,m_{#1}^{#2}}
\renewcommand{\mk}[1]{\moment{#1}{\vphantom{\star}}}
\renewcommand{\mke}[1]{\moment{#1}{e}}
\renewcommand{\mks}[1]{\moment{#1}{\star}}
$$

In this tutorial, we consider the classical $\DdQq{2}{9}$ and $\DdQq{3}{15}$ to simulate a lid driven acvity modeling by the Navier-Stokes equations. The $\DdQq{2}{9}$ is used in dimension $2$ and the $\DdQq{3}{15}$ in dimension $3$.

```python

from six.moves import range
%matplotlib inline
```

## The $\DdQq{2}{9}$ **for Navier-Stokes**

The $\DdQq{2}{9}$ is defined by:

* a space step $\dx$ and a time step $\dt$ related to the scheme velocity $\lambda$ by the relation $\lambda=\dx/\dt$,
* nine velocities $\{(0,0), (\pm1,0), (0,\pm1), (\pm1, \pm1)\}$, identified in pylbm by the numbers $0$ to $8$,
* nine polynomials used to build the moments

$$
\{1, \lambda X, \lambda Y, 3E-4, (9E^2-21E+8)/2, 3XE-5X, 3YE-5Y,X^2-Y^2, XY\},
$$

where $E = X^2+Y^2$.

* three conserved moments $\rho$, $q_x$, and $q_y$,
* nine relaxation parameters (three are $0$ corresponding to conserved moments): $\{0,0,0,s_\mu,s_\mu,s_\eta,s_\eta,s_\eta,s_\eta\}$, where $s_\mu$ and $s_\eta$ are in $(0,2)$,
* equilibrium value of the non conserved moments

$$
\begin{aligned}\mke{3} &= -2\rho + 3(q_x^2+q_y^2)/(\rho_0\lambda^2), \\ \mke{4} &= \rho-3(q_x^2+q_y^2)/(\rho_0\lambda^2), \\ \mke{5} &= -q_x/\lambda, \\ \mke{6} &= -q_y/\lambda, \\ \mke{7} &= (q_x^2-q_y^2)/(\rho_0\lambda^2), \\ \mke{8} &= q_xq_y/(\rho_0\lambda^2),\end{aligned}
$$

where $\rho_0$ is a given scalar.

This scheme is consistant at second order with the following equations (taken $\rho_0=1$)

$$
\begin{gathered} \drondt\rho + \drondx q_x + \drondy q_y = 0,\\ \drondt q_x + \drondx (q_x^2+p) + \drondy (q_xq_y) = \mu \drondx (\drondx q_x + \drondy q_y ) + \eta (\drondxx+\drondyy)q_x, \\ \drondt q_y + \drondx (q_xq_y) + \drondy (q_y^2+p) = \mu \drondy (\drondx q_x + \drondy q_y ) + \eta (\drondxx+\drondyy)q_y,\end{gathered}
$$

with $p=\rho\lambda^2/3$.

We write the dictionary for a simulation of the Navier-Stokes equations on $(0,1)^2$.

In order to impose the boundary conditions, we use the bounce-back conditions to fix $q_x=q_y=0$ at south, east, and west and $q_x=\rho u$, $q_y=0$ at north. The driven velocity $u$ could be $u=\lambda/10$.

The solution is governed by the Reynolds number $Re = \rho_0u / \eta$. We fix the relaxation parameters to have $Re=1000$. The relaxation parameters related to the bulk viscosity $\mu$ should be large enough to ensure the stability (for instance $\mu=10^{-3}$).

We compute the stationary solution of the problem obtained for large enough final time. We plot the solution with the function quiver of matplotlib.

```python
import numpy as np
import sympy as sp
import matplotlib.pyplot as plt
import pylbm

X, Y, LA = sp.symbols('X, Y, LA')
rho, qx, qy = sp.symbols('rho, qx, qy')

def bc(f, m, x, y):
    m[qx] = rhoo * vup

def plot(sol):
    pas = 2
    y, x = np.meshgrid(sol.domain.y[::pas], sol.domain.x[::pas])
    u = sol.m[qx][::pas,::pas] / sol.m[rho][::pas,::pas]
    v = sol.m[qy][::pas,::pas] / sol.m[rho][::pas,::pas]
    nv = np.sqrt(u**2+v**2)
    normu = nv.max()
    u = u / (nv+1e-5)
    v = v / (nv+1e-5)
    plt.quiver(x, y, u, v, nv, pivot='mid')
    plt.title('Solution at t={0:8.2f}'.format(sol.t))
    plt.show()

# parameters
Re = 1000
dx = 1./128  # spatial step
la = 1.      # velocity of the scheme
Tf = 10      # final time of the simulation
vup = la/5   # maximal velocity obtained in the middle of the channel
rhoo = 1.    # mean value of the density
mu = 1.e-4   # bulk viscosity
eta = rhoo*vup/Re  # shear viscosity
# initialization
xmin, xmax, ymin, ymax = 0., 1., 0., 1.
dummy = 3.0/(la*rhoo*dx)
s_mu = 1.0/(0.5+mu*dummy)
s_eta = 1.0/(0.5+eta*dummy)
s_q = s_eta
s_es = s_mu
s  = [0.,0.,0.,s_mu,s_es,s_q,s_q,s_eta,s_eta]
dummy = 1./(LA**2*rhoo)
qx2 = dummy*qx**2
qy2 = dummy*qy**2
q2  = qx2+qy2
qxy = dummy*qx*qy

print("Reynolds number: {0:10.3e}".format(Re))
print("Bulk viscosity : {0:10.3e}".format(mu))
print("Shear viscosity: {0:10.3e}".format(eta))
print("relaxation parameters: {0}".format(s))

dico = {
    'box':{'x':[xmin, xmax], 'y':[ymin, ymax], 'label':[0,0,0,1]},
    'space_step':dx,
    'scheme_velocity':la,
    'parameters':{LA:la},
    'schemes':[
        {
            'velocities':list(range(9)),
            'conserved_moments':[rho, qx, qy],
            'polynomials':[
                1, LA*X, LA*Y,
                3*(X**2+Y**2)-4,
                0.5*(9*(X**2+Y**2)**2-21*(X**2+Y**2)+8),
                3*X*(X**2+Y**2)-5*X, 3*Y*(X**2+Y**2)-5*Y,
                X**2-Y**2, X*Y
            ],
            'relaxation_parameters':s,
            'equilibrium':[
                rho, qx, qy,
                -2*rho + 3*q2,
                rho-3*q2,
                -qx/LA, -qy/LA,
                qx2-qy2, qxy
            ],
            'init':{rho:rhoo, qx:0., qy:0.},
        },
    ],
    'boundary_conditions':{
        0:{'method':{0: pylbm.bc.BouzidiBounceBack}, 'value':None},
        1:{'method':{0: pylbm.bc.BouzidiBounceBack}, 'value':bc}
    },
    'generator': 'cython',
}

sol = pylbm.Simulation(dico)
while (sol.t<Tf):
    sol.one_time_step()
plot(sol)
```

## The $\DdQq{3}{15}$ **for Navier-Stokes**

The $\DdQq{3}{15}$ is defined by:

* a space step $\dx$ and a time step $\dt$ related to the scheme velocity $\lambda$ by the relation $\lambda=\dx/\dt$,
* fifteen velocities $\{(0,0,0), (\pm1,0,0), (0,\pm1,0), (0,0,\pm1), (\pm1, \pm1,\pm1)\}$, identified in pylbm by the numbers $\{0,\ldots,6,19,\ldots,26\}$,
* fifteen polynomials used to build the moments

$$
\{1, E-2, (15E^2-55E+32)/2, X, X(5E-13)/2, Y, Y(5E-13)/2, Z, Z(5E-13)/2, 3X^2-E, Y^2-Z^2, XY, YZ, ZX, XYZ \},
$$

where $E = X^2+Y^2+Z^2$.

* four conserved moments $\rho$, $q_x$, $q_y$, and $q_z$,
* fifteen relaxation parameters (four are $0$ corresponding to conserved moments): $\{0, s_1, s_2, 0, s_4, 0, s_4, 0, s_4, s_9, s_9, s_{11}, s_{11}, s_{11}, s_{14}\}$,
* equilibrium value of the non conserved moments

$$
\begin{aligned} \mke{1} &= -\rho + q_x^2 + q_y^2 + q_z^2,\\ \mke{2} &= -\rho,\\ \mke{4} &= -7q_x/3, \\ \mke{6} &= -7q_y/3, \\ \mke{8} &= -7q_z/3, \\ \mke{9} &= (2q_x^2-(q_y^2+q_z^2))/3, \\ \mke{10} &= q_y^2-q_z^2, \\ \mke{11} &= q_xq_y, \\ \mke{12} &= q_yq_z, \\ \mke{13} &= q_zq_x, \\ \mke{14} &= 0. \end{aligned}
$$

This scheme is consistant at second order with the Navier-Stokes equations with the shear viscosity $\eta$ and the relaxation parameter $s_9$ linked by the relation

$$
s_9 = \frac{2}{1 + 6\eta /\dx}.
$$

We write a dictionary for a simulation of the Navier-Stokes equations on $(0,1)^3$.

In order to impose the boundary conditions, we use the bounce-back conditions to fix $q_x=q_y=q_z=0$ at south, north, east, west, and bottom and $q_x=\rho u$, $q_y=q_z=0$ at top. The driven velocity $u$ could be $u=\lambda/10$.

We compute the stationary solution of the problem obtained for large enough final time. We plot the solution with the function quiver of matplotlib.

```python
X, Y, Z, LA = sp.symbols('X, Y, Z, LA')
rho, qx, qy, qz = sp.symbols('rho, qx, qy, qz')

def bc(f, m, x, y, z):
    m[qx] = rhoo * vup

def plot(sol):
    plt.clf()
    pas = 4
    nz = int(sol.domain.shape_in[1] / 2) + 1
    y, x = np.meshgrid(sol.domain.y[::pas], sol.domain.x[::pas])
    u = sol.m[qx][::pas,nz,::pas] / sol.m[rho][::pas,nz,::pas]
    v = sol.m[qz][::pas,nz,::pas] / sol.m[rho][::pas,nz,::pas]
    nv = np.sqrt(u**2+v**2)
    normu = nv.max()
    u = u / (nv+1e-5)
    v = v / (nv+1e-5)
    plt.quiver(x, y, u, v, nv, pivot='mid')
    plt.title('Solution at t={0:9.3f}'.format(sol.t))
    plt.show()

# parameters
Re = 2000
dx = 1./64   # spatial step
la = 1.      # velocity of the scheme
Tf = 3       # final time of the simulation
vup = la/10  # maximal velocity obtained in the middle of the channel
rhoo = 1.    # mean value of the density
eta = rhoo*vup/Re  # shear viscosity
# initialization
xmin, xmax, ymin, ymax, zmin, zmax = 0., 1., 0., 1., 0., 1.
dummy = 3.0/(la*rhoo*dx)

s1 = 1.6
s2 = 1.2
s4 = 1.6
s9 = 1./(.5+dummy*eta)
s11 = s9
s14 = 1.2
s  = [0, s1, s2, 0, s4, 0, s4, 0, s4, s9, s9, s11, s11, s11, s14]

r = X**2+Y**2+Z**2

print("Reynolds number: {0:10.3e}".format(Re))
print("Shear viscosity: {0:10.3e}".format(eta))

dico = {
    'box':{
        'x':[xmin, xmax],
        'y':[ymin, ymax],
        'z':[zmin, zmax],
        'label':[0,0,0,0,0,1]
    },
    'space_step':dx,
    'scheme_velocity':la,
    'parameters':{LA:la},
    'schemes':[
        {
            'velocities':list(range(7)) + list(range(19,27)),
            'conserved_moments':[rho, qx, qy, qz],
            'polynomials':[
                1,
                r - 2, .5*(15*r**2-55*r+32),
                X, .5*(5*r-13)*X,
                Y, .5*(5*r-13)*Y,
                Z, .5*(5*r-13)*Z,
                3*X**2-r, Y**2-Z**2,
                X*Y, Y*Z, Z*X,
                X*Y*Z
            ],
            'relaxation_parameters':s,
            'equilibrium':[
                rho,
                -rho + qx**2 + qy**2 + qz**2,
                -rho,
                qx,
                -7./3*qx,
                qy,
                -7./3*qy,
                qz,
                -7./3*qz,
                1./3*(2*qx**2-(qy**2+qz**2)),
                qy**2-qz**2,
                qx*qy,
                qy*qz,
                qz*qx,
                0
            ],
            'init':{rho:rhoo, qx:0., qy:0., qz:0.},
        },
    ],
    'boundary_conditions':{
        0:{'method':{0: pylbm.bc.BouzidiBounceBack}, 'value':None},
        1:{'method':{0: pylbm.bc.BouzidiBounceBack}, 'value':bc}
    },
    'generator': 'cython',
}

sol = pylbm.Simulation(dico)
while (sol.t<Tf):
    sol.one_time_step()
plot(sol)
```

```python

```