```julia
using DifferentialEquations
using Plots
```

Consider the simple reaction:

\begin{align}
    A + * &\longleftrightarrow A_*\\
    A_*   &\longleftrightarrow B + *\\
\end{align}

Both are elementary steps that occur in the liquid phase, and we will consider it in a few different solvent environments.

```julia
function batch(du, u, p, t)
    MAR = p["MAR"]
    PAR = p["PAR"]

    k10, k20, K10, K20, Ns, NS = PAR

    NA     = u[:,1]
    NAs    = u[:,2]
    NB     = u[:,3]

    NT     = NA + NB .+ NS

    XA   = NA./NT
    XB   = NB./NT
    XAs  = XA
    XTS1 = XA
    XTS2 = XB
    XS   = NS./NT

    thetaA = NAs./Ns
    thetastar = 1 .- thetaA

    #For A in solvent
    A12A = MAR[1]
    A21A = MAR[2]

    #For As in solvent
    A12As = MAR[3]
    A21As = MAR[4]

    #For B in solvent
    A12B = MAR[5]
    A21B = MAR[6]

    #For Transition State 1 in solvent
    A12TS1 = MAR[7]
    A21TS1 = MAR[8]

    #For Transition State 2 in solvent
    A12TS2 = MAR[9]
    A21TS2 = MAR[10]

    gammaA  = exp.(XS.^2 .*(A12A  .+ 2*(A21A  - A12A)*XA))
    gammaAs = exp.(XS.^2 .*(A12As .+ 2*(A21As - A12As)*XAs))
    gammaB  = exp.(XS.^2 .*(A12B  .+ 2*(A21B  - A12B)*XB))

    gammaTS1 = exp.(XS.^2 .*(A12TS1 .+ 2*(A21TS1 - A12TS1)*XTS1))
    gammaTS2 = exp.(XS.^2 .*(A12TS2 .+ 2*(A21TS2 - A12TS2)*XTS2))

    z1 = 1/K10*gammaAs./gammaA.*thetaA./XA./thetastar
    z2 = 1/K20*gammaB./gammaAs.*XB.*thetastar./thetaA

    z2[isnan.(z2)] .= 0

    r1 = k10*gammaA./gammaTS1.*XA.*thetastar.*(1 .- z1)
    r2 = k20*gammaAs./gammaTS2.*thetaA.*(1 .- z2)

    RA  = -r1[1]
    RB  =       r2[1]
    RAs =  r1[1] .- r2[1]

    du[1] = RA*Ns
    du[2] = RAs*Ns
    du[3] = RB*Ns
    return du, r1, r2, z1, z2
end
```

    batch (generic function with 1 method)

```julia
K    = 1
Ns   = 0.001
k10  = 1000000
k20  = 0.01
K10  = 1
K20  = K/K10
V    = 1
NTOT = 100

NA0  = 0.1
NAs0 = 0.0
NB0  = 0.0

NS   = NTOT - NA0 - NB0

var0     = [NA0 NAs0 NB0]
span     = (0.0, 1000000/Ns);
```

```julia
#Solvate transition state relative to reactants
MARSET1 = zeros(10,3)
MARSET1[:,1] = [0.0, 0.0, 0.0, 0.0,  0.0, 0.0,  0.0, 0.0,  0.0,  0.0] #no solvation
MARSET1[:,2] = [0.0, 0.0, 0.0, 0.0,  0.0, 0.0,  0.0, 0.0,  0.0,  0.0] #destabilize TS1
MARSET1[:,3] = [0.0, 0.0, 0.0, 0.0,  0.0, 0.0,  0.0, 0.0, 0.0, 0.0] #stabilize TS1

tfine  = 10 .^range(0.0, stop = log10(maximum(span)), length = 1000)
Xout   = zeros(length(tfine), size(MARSET1, 2))
r1out  = zeros(length(tfine), size(MARSET1, 2))
r2out  = zeros(length(tfine), size(MARSET1, 2))
z1out  = zeros(length(tfine), size(MARSET1, 2))
z2out  = zeros(length(tfine), size(MARSET1, 2))
cov    = zeros(length(tfine), size(MARSET1, 2))

for i = 1:size(MARSET1, 2)
    p0   = Dict("MAR" => MARSET1[:,i], "PAR" => [k10, k20, K10, K20, Ns, NS])
    prob = ODEProblem(batch, var0, span, p0)
    sol  = solve(prob, Rodas5(), abstol = 1e-16, reltol = 1e-16)
    solf = sol(tfine)
    NA   = solf[1,:]
    NAs  = solf[2,:]
    NB   = solf[3,:]
    NT   = NA + NB .+ NS

    dut, rt1, rt2, zt1, zt2 = batch([0., 0., 0.], [NA NAs NB], p0, tfine)

    ext  = NB/NA0
    cov[:, i]  = NAs/Ns
    Xout[:,i]  = ext
    r1out[:,i] = rt1
    r2out[:,i] = rt2
    z1out[:,i] = zt1
    z2out[:,i] = zt2
end

plt1 = plot(tfine/maximum(tfine), Xout, xlabel  = "time", ylabel = "Conversion", labels = ["e1" nothing nothing], legend = :bottomright)
plt2 = plot(tfine[2:end]/maximum(tfine), r2out[2:end,:], xlabel  = "time", ylabel = "rate", labels = ["r2" nothing nothing], legend = :topright)
plt3 = plot(Xout[2:end,:], r2out[2:end,:], xlabel  = "extent", ylabel = "rate", labels = ["r1" nothing nothing], legend = :topright)
plt4 = plot(Xout, z1out, xlabel  = "extent", ylabel = "z", labels = ["z1" nothing nothing], legend = :topright)
plt4 = plot!(Xout, z2out, labels = ["z2" nothing nothing])
plt5 = plot(tfine/maximum(tfine), z1out, xlabel  = "time", ylabel = "z", labels = ["z1" nothing nothing], legend = :topright)
plt5 = plot!(tfine/maximum(tfine), z2out, labels = ["z2" nothing nothing])
plt6 = plot(tfine/maximum(tfine), cov, xlabel = "time", ylabel = "Coverage of A")

display(plt1)
display(plt2)
display(plt3)
display(plt4)
display(plt5)
display(plt6)

```

```julia
function rate(XA, p)
    MAR = p["MAR"]
    PAR = p["PAR"]

    k10, k20, K10, K20, Ns, NS = PAR

    XS   = 0.99
    XB   = 1.0 - XS .- XA
    XAs  = XA
    XTS1 = XA
    XTS2 = XB

    #For A in solvent
    A12A = MAR[1]
    A21A = MAR[2]

    #For As in solvent
    A12As = MAR[3]
    A21As = MAR[4]

    #For B in solvent
    A12B = MAR[5]
    A21B = MAR[6]

    #For Transition State 1 in solvent
    A12TS1 = MAR[7]
    A21TS1 = MAR[8]

    #For Transition State 2 in solvent
    A12TS2 = MAR[9]
    A21TS2 = MAR[10]

    gammaA  = exp.(XS.^2 .*(A12A  .+ 2*(A21A  - A12A)*XA))
    gammaAs = exp.(XS.^2 .*(A12As .+ 2*(A21As - A12As)*XAs))
    gammaB  = exp.(XS.^2 .*(A12B  .+ 2*(A21B  - A12B)*XB))

    gammaTS1 = exp.(XS.^2 .*(A12TS1 .+ 2*(A21TS1 - A12TS1)*XTS1))
    gammaTS2 = exp.(XS.^2 .*(A12TS2 .+ 2*(A21TS2 - A12TS2)*XTS2))

    thetastar = 1 ./(1 .+ K10*gammaA./gammaAs.*XA)
    thetaA    = K10*gammaA./gammaAs.*XA.*thetastar

    z2 = 1/K20*gammaB./gammaAs.*XB.*thetastar./thetaA

    z2[isnan.(z2)] .= 0
    z2[isinf.(z2)] .= 0
    r2 = k20*gammaAs./gammaTS2.*thetaA.*(1 .- z2)
    return r2
end

MARSET2 = zeros(10,3)
MARSET2[:,1] = [0.0, 0.0, 0.0, 0.0,  0.0, 0.0,  0.0, 0.0,  0.0,  0.0] #no solvation
MARSET2[:,2] = [0.0, 0.0, 1.0, 1.0,  0.0, 0.0,  0.0, 0.0,  0.0,  0.0] #destabilize TS1
MARSET2[:,3] = [0.0, 0.0, -1.0, -1.0,  0.0, 0.0,  0.0, 0.0,  0.0, 0.0] #stabilize TS1
K    = 1
Ns   = 0.001
k10  = 1000000
k20  = 0.01
K10  = 10000000
K20  = K/K10

XA = 0.0:0.0001:0.01
r2out = zeros(length(XA), size(MARSET2, 2))
for i = 1:size(MARSET2, 2)
    p0   = Dict("MAR" => MARSET2[:,i], "PAR" => [k10, k20, K10, K20, Ns, NS])
    r2out[:,i] = rate(XA, p0)
end

plot(XA, r2out)
```


# Principal Component Analysis

   Principal component analysis (PCA) has been called one of the most valuable results from applied linear algebra.It is used to reduce a large set of variables to a small set that still contains most of the information in the large set.

   It is a mathematical procedure that transforms a number of (possibly) correlated variables into a (smaller) number of uncorrelated variables called principal components.The first principal component accounts for as much of the variability in the data as possible, and each succeeding component accounts for as much of the remaining variability as possible.

### So how to perform pca on given dataset?
- Get some data
- Substract the mean
- Calculate the covariance matrix for the data
- Calculate the eigen vectors and eigen values of covariance matrix
- Choosing components and forming a feature vector
- Deriving the new data set

Consider our data set consists of n variables and m samples.This can be as a mxn matrix,

\begin{equation}
\begin{bmatrix}
    x_{11} & x_{12} & x_{13} & \dots  & x_{1m} \\
    x_{21} & x_{22} & x_{23} & \dots  & x_{2m} \\
    \vdots & \vdots & \vdots & \ddots & \vdots \\
    x_{n1} & x_{n2} & x_{n3} & \dots  & x_{nm}
\end{bmatrix}=
\begin{bmatrix}
    X1 \\
    X2 \\
    \vdots \\
    Xn
\end{bmatrix}
\end{equation}

  For PCA to work properly you have to subtract the mean from each of the data dimensions.The mean subtracted is the ave rage across each dimension.So, all the X1 values have  $\overline{X1}$(the mean of the x values of all the data points)substracted ,all the X2 values have $\overline{X2}$ substracted from them,similarly for all .

**Covariance** is a measure of the extent to which corresponding elements from two sets of ordered data move in the same direction. We use the following formula to compute covariance.

\begin{equation}
Cov(X, Y) = \sum^{m}_{i=1} \dfrac{ ( X_i - \overline{X} ) ( Y_i - \overline{Y} )} { m} = \sum^{m}_{i=1} \dfrac{ X_i \times Y_i }{ m  }
\end{equation}

**Covariance matrix** :
The variances appear along the diagonal and covariances appear in the off-diagonal elements.

\begin{equation}
v=
\begin{bmatrix}
    Cov(X1,X2) & Cov(X1,X2) & Cov(X1,X3) & \dots  & Cov(X1,Xn) \\
    Cov(X2,X1) & Cov(X2,X2) & Cov(X2,X3) & \dots  & Cov(X2,Xn) \\
    \vdots & \vdots & \vdots & \ddots & \vdots \\
    Cov(Xn,X1) & Cov(Xn,X2) & Cov(Xn,X3) & \dots  & Cov(Xn,Xn)
\end{bmatrix}
\end{equation}

**Eigen values and Eigen vectors**:
  Eigen vector of a matrix A is a vector represented by a matrix X such that when X is multiplied with matrix A, then the direction of the resultant matrix remains same as vector X.

Mathematically, above statement can be represented as:
\begin{equation}
AX =
λX
\end{equation

- Method to find eigen vectors and eigen values of any square matrix A.We know that,

                    AX = λX

                => AX – λX = 0

                 => (A – λI) X = 0 ....(1)

 Above condition will be true only if (A – λI) is singular. That means,

                       |A – λI| = 0 ....(2)

 (2) is known as characteristic equation of the matrix.

 The roots of the characteristic equation are the eigen values of the matrix A.

 Now, to find the eigen vectors, we simply put each eigen value into (1) and solve it by Gaussian elimination, that is, convert the augmented matrix (A – λI) = 0 to row echelon form and solve the linear system of equations thus obtained.

**Components and FeatureVector**:
Here is where the notion of data compression and reduced dimensionality comes in.The eigenvectors have unit norm but have different eigenvalues.
In fact, it turns out that the eigenvectors having higher eigenvalues are the principal directions of our data set.The highest eigenvalue corresponds to the first principal component and the second highest corresponds to second principal component and so on.

If we want to reduce our dataset to K dimensions(initially n(>k) dimensions) we select the first K eigenvectors,which are arranged in decreasing order with respect to corresponding eigen values,which is our feature vector.
\begin{equation}
    FeatureVector = (eig_1,eig_2,....,eig_k)
\end{equation}

**New Dataset**:
This is the final step of pca.So after obtaining featurevector, we project original data along the principal components.
This can be done by just multipliying our original matrix with feature vector.

\begin{equation}
FinalData = OriginalData(after \ substracting \ mean)\times FeatureVector
\end{equation}

### Why do we get principal components from eigenvectors of covariance matrix

- we need to find the direction of maximum variance for obtaining principal components.

Let suppose we have 3 data points in space $x_1$,$x_2$,$x_3$.Now we want to find a vector e(unit norm) ,such that: it is the direction of maximum variance.So for calculating variance we project the datapoints onto the vector e(projection implies dot product of $x_i$ with e).

The variance of data points is

\begin{equation}
variance \ of \ projections = \dfrac{1}{n}\sum^{n}_{i=1}\Bigg(\sum^{d}_{j=1}x_{ij} \ e_{j}-\mu \Bigg)^2 = \dfrac{1}{n}\sum^{n}_{i=1}\Bigg(\sum^{d}_{j=1}x_{ij} \ e_{j}\Bigg)^2 \ (when \ mean \ is \ zero)
\end{equation}

Now we have to maximize variance of projections.We add lagrange multiplier(for optimization, to the above equation).As we are constrained to the fact that e is of unit norm $\big(\sum^d_{j=1}e_j^2=1 \big)$.

\begin{equation}
V  = \dfrac{1}{n}\sum^{n}_{i=1}\Bigg(\sum^{d}_{j=1}x_{ij} \ e_{j}\Bigg)^2 \ - \lambda \Bigg(\Bigg(\sum^d_{j=1}e_j^2\Bigg)-1 \Bigg)
\end{equation}

Now differentiate V wrt components of e and equate then to zero.

\begin{equation}
\frac{\partial V}{\partial e_k}= \dfrac{2}{n} \sum^{n}_{i=1}\Bigg(\sum^{d}_{j=1}x_{ij} \ e_{j}\Bigg)x_{ik} - 2\lambda e_{k} = 0
\end{equation}

 It implies

\begin{equation}
\sum^{d}_{j=1}e_{j}\underbrace{\Bigg( \dfrac{1}{n}\Bigg(\sum^{n}_{i=1} x_{ik} \ x_{ij}\Bigg)\Bigg)}_\text{covariance of
Xk,Xj} = \lambda e_{k}
\end{equation}

Holds for all k=1,2,3,...d

- It implies that we are taking dot product of matrix with cov(Xi,Xj) as its elements, with column vector e.

It is of the form :

\begin{equation}
Ax=\lambda x
\end{equation}

Where A is the covariance matrix and x is the direction of maximum variance.

#### So by maximizing the variance, we got the conditon that the eigenvectors of covariance matrix represent the direction of maximum variance.

```python
import numpy as np
import pandas as pd
import sklearn
import matplotlib.pyplot as plt
from mpl_toolkits import mplot3d-
```

```python
def Reduced_data_set(dataset,final_dimension, plot = False):
    # Centering all the features to zero mean
    Mean_of_data=dataset.mean(0)
    centered_data=dataset-Mean_of_data

    # Calculating the Covariance Matrix
    covariance_matrix=np.cov(centered_data.T)

    # Computing the eigenvalues and eigenvectors of the Covariance Matrix
    eigen_values,eigen_vectors=np.linalg.eig(covariance_matrix)

    # Sorting the eigenvalues from largest to smallest
    idx = eigen_values.argsort()[::-1]
    eigen_values = eigen_values[idx]
    eigen_vectors = eigen_vectors[:,idx]

    # Projecting the dataset on the final_dimension eigenvectors (selected based on higher eigenvalue)
    reduced=np.matmul(centered_data,eigen_vectors[:,:final_dimension],out=None)

    # If plotting the eigenvector of the covariance matrix and mean centered data is asked
    if plot:
      plt.title("Mean Centered Data and the EigenVectors of Covariance Matrix (Scaled by EigenValue)")
      plt.plot(centered_data[:,0][:50], centered_data[:,1][:50], 'ro', label = ' Data A')
      plt.plot(centered_data[:,0][50:], centered_data[:,1][50:], 'go', label = ' Data B')
      plt.quiver([0,0], [0,0], *eigen_values*(eigen_vectors), label = 'Eigen Vectors of Covariance Matrix',angles='xy', scale_units='xy', scale = 2.5)
      plt.legend(loc = 'best')

    return reduced
```

```python
def create_dataset(m = 2, c = 0.5, no_points = 100, std = 0.01):
  x = np.linspace(0, 5, no_points)
  noise = np.random.randn(no_points)*std
  y = m*x + c + noise
  return (np.c_[x, y])
```

```python
x= create_dataset(c = 2, std = 0.5)[:,0]
y=create_dataset(c = 2, std = 0.5)[:,1]

dataset=np.column_stack((x,y))
x1=x[0:50]
x2=x[50:]
y1=y[0:50]
y2=y[50:]
```

```python
plt.plot(x1, y1, 'ro')
plt.plot(x2, y2, 'go')
plt.show()
print(x.shape)
```

```python
var = Reduced_data_set(dataset,1, True)
```

```python
x3=var[0:50]
x4=var[50:]
y3=np.zeros(50)
y4=np.zeros(50)
print (x3.shape)
```

    (50, 1)

```python
# fig = plt.figure(figsize=(30,10))
print (x3.shape)
plt.plot(x3,y4,'ro')
plt.plot(x4,y4,'go')
plt.show()
```

```python

```


# Estimating the mass of exoplanets

When a star has planetary companions, it describes an epicyclic motion around the center of mass of the system. When the motion has a component in the direction of the observer (the direction of the line of sight), this allows to detect the planetary companions. Indeed, as the distance between an observer and a star changes, because of the Doppler effect, the light received by the observer shifts in frequency.

By measuring spectra at different times and measuring how the spectral absorption lines shift in wavelength, it is possible to obtain the star velocity in the direction of the line of sight. We then have a time series of measurements, with a nominal error on each of them. If one detects a periodic signal in the time series, then this one might be due to a planet.

Determining if a periodic signal is present and if it originates from a planet is difficult, and will not be the object of the present exercise. In this spreadsheet, we assume that a planet has been detected, and we want to determine its orbital elements, especially its mass.

To simplify the problem, we assume that the planet has a circular orbit (that is a zero eccentricity).

The first part of this homework is the forward model: given the physical parameters and the instrument, what is the likelihood function of the data ?

We then compute the maximum likelihood estimates of the parameters when the period is known.

Finally, we compute the posterior distribution of the parameters, and interval estimates of the mass. This part requires to code a Metropolis-Hastings algorithm.

```python
import numpy as np
import matplotlib.pyplot as plt
import emcee
import corner
from tqdm import tqdm
```

## I. The forward model
----
----

Let us suppose that a planet of mass $m$ orbits a star of mass $M$ at a semi-major axis $a$ and eccentricity 0. We want to express the velocity of the $\mathbf{star}$ in the direction observer - barycenter (the radial velocity).

To facilitate the calculation of the radial velocity, we define two inertial frame: the observing frame $(x,y,z)$ and the orbital frame $(x',y',z')$. Both frames have their origin $O$ at the barycenter of the system { star + planet }. The observing frame $z$ axis is oriented in the direction observer - star. The plane perpendicular to $z$ is called the sky plane.

The orbital frame $z'$ axis is perpendicular to the plane of the orbit and its direction is such that the $\mathbf{planet}$ orbits is in the trigonometric (or counterclocwise) sense.
The $x$ and $x'$ axes are pointing at the ascending node, that is the point where the orbit crosses the sky plane from negative $z$ coordinates to positive $z$ coordinates.
The $y$ and $y'$ axes are such that $(x,y,z)$ and $(x',y',z')$ are direct orthonormal bases. The semi-major axis of  the planet is denoted by $a$.

The angle $(z,z')$ is denoted by $i$.

------------
-----

I.1) Show that the position of the $\mathbf{star}$ in the orbital frame at time $t$ is
$$ r(t) = -a \frac{m}{M+m} \left[ \cos(\nu (t - t_0)),  \sin(\nu (t - t_0)), 0 \right ] $$
where $t_0$ is the time of passage at the ascending node and $$\nu = \sqrt{\frac{G(m+M)}{a^3}}$$.

We assume that the planet and the star are point masses. From classical orbital mechanics it is known that two point masses in gravitational interaction have a planar motion. We here assume that their orbit is circular. In the orbital frame, the motion of the point mass with mass $M$ (the star) is
$$ r_\star(t) = a \frac{m}{M+m} \left[ \cos(\nu t +\phi_s ),  \sin(\nu t +\phi_s), 0 \right ] $$
where $\phi$ is the phase of the orbit at $t=0$ and $a$ is the distant between the two point masses. The position of the planet is
$$ r_p(t) = a \frac{M}{M+m} \left[ \cos(\nu t +\phi_p ),  \sin(\nu t +\phi_p), 0 \right ] $$
where $\phi_p  = \phi_s + \pi$.

When the planet is at its ascending node, its phase is 0, so $\nu t_0+\phi_p = 0$ so $\phi_p = - \nu t_0$  and $\phi_s
= - \nu t_0 + \pi$. Therefore, the position of the $\mathbf{star}$ in the orbital frame at time $t$ is
$$ r_\star(t) = -a \frac{m}{M+m} \left[ \cos(\nu (t - t_0)),  \sin(\nu (t - t_0)), 0 \right ] $$.

By Kepler's second law,
$$\nu = \sqrt{\frac{G(m+M)}{a^3}}$$

I.2) Show that the velocity of the $\mathbf{star}$ projected onto the $z$ axis at time $t$ is  $$ V(t) = V_0 +  \frac{ G^\frac{1}{3}\nu^\frac{1}{3} }{(m+M)^\frac{2}{3}} m \sin i \cos(\nu (t - t_0))  $$ where $V_0$ is the velocity of the barycenter with respect to the observer.

Let us express the position of the star in the reference frame $(x,y,z)$. Denoting by $M$ the matrix of coordinate change
 $$ M =  \left(
\begin{matrix}
1 & 0 & 0 \\
0 & \cos i & -\sin i \\
0 & \sin i& \cos i
\end{matrix}
\right)
 $$

 The position of the star in the  reference frame $(x,y,z)$, $q(t)$,  is then $q(t) = Mr(t)$. The $z$ component of $q$ is
 $$
 z = \sin i  \left(- a \frac{m}{M+m}\right)   \sin(\nu (t -t_0) )
 $$
 Therefore
$$
 \frac{\mathrm{d} z}{\mathrm{d} t} =  - a \sin i   \frac{m}{M+m}  \nu   \cos(\nu (t -t_0) )
 $$
 by expressing $a$ as a function of $\nu$ with Kepler's second law, we obtain the desired formula.

I.3) We assume that we have $N$ radial velocity measurements at time $(t_k)_{k=1..N}$.  with an instrument that has nominal error bar $\sigma_k$ for the measurement $k$. In principle, one could assume a the following form for the probability of the data knowing the parameters (the likelihood),
$$ p(y|m, M, i, \nu,t_0, V_0)  = \frac{1}{ \sqrt{2\pi}^N\prod\limits_{k=1}^N \sigma_k} \mathrm{e}^{-\frac{1}{2} \sum\limits_{k=1}^N \frac{ \left(y(t_k) - V_0 -  \frac{ \nu^\frac{1}{3} G^\frac{1}{3} }{(m+M)^\frac{2}{3}} m \sin i \cos(\nu (t - t_0)) \right)^2 }{\sigma_k^2} }.$$

However, in the rest of this spreadsheet we will assume the likelihood is
$$p(y|A,B,C, \nu) = \frac{1}{ \sqrt{2\pi}^N\prod\limits_{k=1}^N \sigma_k} \mathrm{e}^{-\frac{1}{2} \sum\limits_{k=1}^N \frac{(y(t_k) - A \cos \nu t_k - B \sin \nu t_k - C)^2}{\sigma_k^2} } \;\;\;\;\;\;\;\;\; (1)$$

Give the advantage of the chosen formulation, with parmeters $A,B,C$ over the formulation with parameters $m, M, i, \nu,v_0$ and express $A,B,C$ as a function $m, M, i, t_0,\nu,V_0$.

Let $\frac{mv^{1/3}G^{1/3}}{(m+M)^{2/3}} = K$, then,

\begin{equation}
V_0 + K\cdot \sin{i}\cos({\nu(t-t_0)}) = A\cos{\nu t_k} + B\sin{\nu t_k} +C
\end{equation}

That means $C=V_0$, and

\begin{equation}
    K\sin{i}\cos({\nu t - \nu t_0}) = A\cos{\nu t_k} + B\sin{\nu t_k} \\
    \Rightarrow K\sin{i} (\cos{\nu t}\cos{\nu t_0} + \sin{\nu t}\sin{\nu t_0}) = A\cos{\nu t_k} + B\sin{\nu t_k}\\
    \Rightarrow K\sin{i}\cos{\nu t_0} \cos{\nu t} + K\sin{i}\sin{\nu t_0}\sin{\nu t} = A\cos{\nu t_k} + B\sin{\nu t_k}
\end{equation}

Comparing the coefficients of the last equations,

\begin{equation}
    \begin{split}
        A &= K\sin{i}\cos{\nu t_0} \\
        \Rightarrow A &= m\frac{v^{1/3}G^{1/3}}{(m+M)^{2/3}}\sin{i}\cos{\nu t_0}
    \end{split}
\end{equation}

(Note: Thus, we can find $m$ using $A$,

$$m = \frac{AM^{2/3}}{\nu^{1/3} G^{1/3} \sin{i} \cos{\nu t_0}}$$

which can be useful in later derivation.)

and,

\begin{equation}
    \begin{split}
        B &= K\sin{i}\sin{\nu t_0} \\
        \Rightarrow B &= m\frac{v^{1/3}G^{1/3}}{(m+M)^{2/3}}\sin{i}\sin{\nu t_0}
    \end{split}
\end{equation}

## II. Point estimates

---
---

In this part of the homework, we assume that we have radial velocity data $y = (y(t_i))_{i=1..N}$.
We model $y$ as a sum of a sinusoidal model due to the planet and a Gaussian noise of mean zero and variance $\sigma_i^2$ for the measurement at time $t_i$.
$$y(t_i) = A\cos \nu t_i + B\sin \nu t_i + C + \epsilon_i$$
where $$\epsilon_i \sim G(0, \sigma_i^2)$$

We assume that the uncertainties $(\sigma_i)_{i=1..N}$ are known.

-----
-----

II.1) Justify that the likelihood of the model is given by equation (1).

The $\epsilon_i$ are Gaussian and independent, therefore their covariance matrix is diagonal with $i$-th element $\sigma_i^2$. Knowing the parameters $\theta$, when the noise is white (i.e. uncorrelated), for a model $f(\theta)$, a Gaussian likelihood has the form
$$p(y | \theta ) =  \frac{1}{ \sqrt{2\pi}^N\prod\limits_{k=1}^N \sigma_k} \mathrm{e}^{-\frac{1}{2} \sum\limits_{k=1}^N \frac{(y(t_k) - f(\theta))^2}{\sigma_k^2} } \;\;\;\;\;\;\;\;\; (1)$$

Here, $f(\theta) =A\cos \nu t_i + B\sin \nu t_i + C $. By replacing in the equation above we get the expression of equation 1.

II.2) We first assume that the frequency $\nu$ is known. Show that the maximum likelihood estimate of $\theta = (A,B,C)$ is of the form $$\hat{\theta} = (M^T V^{-1} M)^{-1} M^T V^{-1} y$$
where the suffix $T$ denotes the matrix transposition, $V$ is a diagonal matrix whose elements are $V_{ii} = \sigma_i^2$ and $M$ is a $N \times 3$ matrix.

Write the explicit expression $M$ as a function of $\nu$.

Since the model is assumed to have the Gaussian noise and the model is linear model, one can write the estomator as $\hat{\theta} = (M^T V^{-1} M)^{-1} M^T V^{-1} y$ as derived in the class. The expression for $M$ can be written as,

\begin{pmatrix}
\cos{\nu t_1} & \sin{\nu t_1} & 1\\
\cos{\nu t_2} & \sin{\nu t_2} & 1\\
. & . & . \\
\cos{\nu t_n} & \sin{\nu t_n} & 1\\
\end{pmatrix}

II.3) Bonus question: show that if $\phi$ is a function and $\hat(\theta)$ is the maximum likelihood estimate of $\theta$, then the maximum likelihood estimate of $\phi(\theta)$ is $\phi(\hat{\theta})$.

This result can be used in the following question even if it is not proved.

We first define the maximum likelihood estimate of $\phi(\theta)$.

Let us first suppose that $\phi$ is a bijection, in that case we define $p'(y|\phi(\theta)) = p(y|\theta)$, then, if the inferior bound of $\{  p(y|\theta), \theta \in \Theta \}$ is attained, $arg \max_{\theta } p(y|\theta)$ exists. Since $p'(y|\phi(\theta)) = p(y|\theta)$, denoting by
$$ \theta_{ML} = arg \max_{\phi(\theta) } p(y|\theta) $$,
$$ arg \max_{\phi(\theta) }p'(y|\phi(\theta)) = p(y|\phi(\theta_{ML})) $$

If $\phi$ is not a bijection, then we can comsider $\tilde{\phi}: \Theta \rightarrow \phi(\Theta) \times \Theta$
where $\tilde{\phi}(\theta) = (\phi(\theta), \theta)$, which is a bijection. Then we define
$$ p''(y|\tilde{\phi}(\theta) )= p(y|\theta) $$ and the reasoning above applies to $p''$ instead of $p'$.

II.4) For $m = 4$  $M_\oplus$, $M = 0.65$ $M_\odot$, $i = 90°$, $\nu = \frac{2*\pi}{6.5}$ radian/day, $t_0 = 0$, $V_0 = 0$,
generate the expected radial velocity signal $y_0(t)$ at the times of the column 'time' in the file homework_rv.txt.

Note: $M_\oplus$ and $M_\odot$ designate the Earth mass and the Solar mass

```python
mp = 4*5.972e+24
Ms = 0.65*1.989e+30
incli = np.pi/2
nu = (2*np.pi)/(6.5*24*60*60)
t0 = 0
V0 = 0
G = 6.6743e-11
K = (mp* nu**(1/3) * G**(1/3))/((mp+Ms)**(2/3))
A = K*np.sin(incli)*np.cos(nu*t0)
B = K*np.sin(incli)*np.sin(nu*t0)
C = V0
time1, rv, rve = np.loadtxt('homework_rv.txt', usecols=(0,1,2), unpack=True)
time = time1*24*60*60
y0 = A*np.cos(nu*time) + B*np.sin(nu*time) + C
plt.plot(time, y0)
plt.xlabel('Time (in sec)')
plt.ylabel('Radial Velocity signal (in m/s)')
```

II. 5) Using the column 'errors' of the file homework_rv.txt., generate 100,000 realizations of a Gaussian noise plus $y_0$, compute the empirical variance and bias of $\hat{m}$. Plot the empirical distribution of $\hat{m}$ and compute its empirical bias and variance.

```python
def mass(Aa, Bb):
    global incli, Ms, nu, G
    KK = np.sqrt(Aa**2 + Bb**2)
    KK1 = KK/np.sin(incli)
    mm = KK1*Ms**(2/3)/(nu**(1/3) * G**(1/3))
    return mm
```

```python
nsim = 100000
V = np.diag(rve)
V1 = np.linalg.inv(V)

M11 = np.vstack((np.cos(nu*time), np.sin(nu*time)))
M = np.vstack((M11, np.ones(len(time)))).T
MT = M.T

F1 = MT.dot(V1).dot(M)
F2 = np.linalg.inv(F1)

AA = np.array([])
BB = np.array([])
CC = np.array([])
theta = np.array([])

for i in tqdm(range(nsim)):
    y2 = y0 + np.random.random(len(y0))
    F3 = MT.dot(V1).dot(y2)
    theta1 = F2.dot(F3)
    theta = np.hstack((theta, theta1))
    AA = np.hstack((AA, theta1[0]))
    BB = np.hstack((BB, theta1[1]))
    CC = np.hstack((CC, theta1[2]))

m_estimate = mass(AA, BB)
m_pesti = m_estimate/5.972e+24
bias1 = np.mean(m_pesti)-4
vari = np.var(m_pesti)
stdd = np.std(m_pesti)
print('Empirical Measurements:')
print('-----------------------')
print('Estimate: ' + str(np.mean(m_pesti)) + ' M_earth')
print('Bias: ' + str(bias1) + ' M_earth')
print('Variance: ' + str(vari) + ' M_earth^2')
print('Standard Deviation: ' + str(stdd) + ' M_earth')
#print(np.mean(m_pesti), np.std(m_pesti))
plt.hist(m_pesti, bins=50)
plt.xlabel('Mass of the planet (in Earth-Mass)')
plt.ylabel('Counts')
```

## The Metropolis-Hastings algorithm

---
---
A common way to derive uncertainties on the parameter is to compute their posterior distributions $p(\theta|y)$.
In the following, we assume that $\theta$ belongs to an open subset of $\mathbb{R}^p$ and $\theta$ has a prior density ditribution $p(\theta)$. The density of the posterior distribution of the parameters is then
$$ p(\theta|y) =  \frac{p(y|\theta) p(\theta)}{p(y)}   $$
where, according to the total probability formula
$$p(y) = \int\limits_{\theta \in \Theta}  p(y|\theta) p(\theta) \mathrm{d}\theta $$

In general, $p(\theta|y)$ does not have an analytical expression. To approximate it, paradoxically enough, it might be simpler to generate samples $(\theta_i)_{i=1}^N$ from $p(\theta|y) $ and approximate the true distribution $ p(\theta|y)$ from the empirical distribution of the $(\theta_i)_{i=1}^N$.

The most common way to generate a sequence $(\theta_i)_{i=1}^N$ following the distribution $p(\theta|y)$ is to use a Metropolis-Hastings algorithm, whose principle is as follows.

We define a transition probability, called the proposal distribution, that is a function $q: \theta \in  \Theta \times \theta' \in  \Theta \rightarrow q_\theta(\theta')$ such that for each $\theta \in \Theta$, $\theta' \in  \Theta \rightarrow q_\theta(\theta')$ is a probability density.
Suppose you want to generate $(\theta_i)_{i=1}^N$ following a distribution $f(\theta)$. Assuming the state at iteration $i$ is $\theta_i$.

1. Generate $\theta' \sim q_{\theta_i}(\theta')$
2. Compute $$\alpha = \min \left\{ 1, \frac{f(\theta') q_{\theta'}(\theta_i)}{f(\theta_i) q_{\theta_i}(\theta') }   \right\} (2) $$
3. Generate $u$ with a uniform distribution between 0 and 1. If $u \geqslant \alpha$, assign $\theta_{i+1} \leftarrow \theta'$. It is said that the proposal is accepted. Else $\theta_{i+1} \leftarrow \theta_{i}$, the proposal is said to be rejected.
4. Repeat for $i \leftarrow i+1$

The fraction of iterations where the proposal is accepted is called the acceptance rate. The series of $\theta_i$ obtaines is called the "chain".

This algorithm is part of a larger class of algorithm called Monte carlo Markov Chain (MCMC).

Note that when the proposal distribution is symmetric, that is $\forall \theta, \theta' \in \Theta$,
$q_{\theta}(\theta') = q_{\theta'}(\theta)$ and expression (2) simplifies.

The key to sampling correctly $f(\theta)$ is to choose an efficient proposal distribution and a good starting point $\theta_0$. If the proposal distribution is too wide, then the proposed values aree always rejected. If it is too narrow, then the states of the chain $\theta_i$ are too correlated with each other, and the exploration of the parameter space is too slow. A good rule of thumb is to aim at an acceptance ratee of 23%.

Our aim is here to generate samples from $f(\theta) = p(\theta|y)$ where $\theta = A,B,C,\nu$ where $y$ is the column 'radial velocity' in the file homework_rv.txt

---
---

III.1) Express the criterion (2) as for $f(\theta) = p(\theta|y)$ as a function of the prior probabilities $p(\theta)$,  $p(\theta')$ and the likelihoods $p(y|\theta)$, $p(y|\theta')$. What is the crucial advantage of the Metropolis-Hastings algorithm for the generation of samples from a posterior distribution, over a generation of samples with the inverse CDF?

From the Bayes theorem,

$$p(\theta|y) = \frac{p(y|\theta)p(\theta)}{p(y)}$$

Which is nothing but $f(\theta)$. Similarly, for $f(\theta') = \frac{p(y|\theta')p(\theta')}{p(y)}$. Their ratio (which is what needed in the algorithm) can be thus given as,

$$ \frac{f(\theta')}{f(\theta)} = \frac{p(y|\theta')p(\theta')}{p(y|\theta)p(\theta)}$$

Here $p(\theta')$ and $p(\theta)$ are the prior probabilities; $p(y|\theta')$ and $p(y|\theta)$ are the likelihood functions.

III.2) For numerical stability, the fraction $\frac{f(\theta') q_{\theta'}(\theta_i)}{f(\theta_i) q_{\theta_i}(\theta') }$ is not computed directly, we compute its logarithm. Express the criterion 2 as a function of the logarithms of the prior probabilities $p(\theta)$,  $p(\theta')$, the likelihoods $p(y|\theta)$, $p(y|\theta')$ and the logarithms of $q_{\theta'}(\theta_i)$, $q_{\theta_i}(\theta')$.

First of all we want to write the fraction $\frac{f(\theta') q_{\theta'}(\theta_i)}{f(\theta_i) q_{\theta_i}(\theta') }$ in the form of prior probabilities and likelihood functions.

$$\frac{f(\theta') q_{\theta'}(\theta_i)}{f(\theta_i) q_{\theta_i}(\theta') } = \frac{p(y|\theta')p(\theta') q_{\theta'}(\theta_i)}{p(y|\theta)p(\theta)q_{\theta_i}(\theta')}$$

We now can take logarithm on the both side of above equation,

\begin{equation}
    \begin{split}
        \log \left[\frac{f(\theta') q_{\theta'}(\theta_i)}{f(\theta_i) q_{\theta_i}(\theta') }\right] &= \log{p(y|\theta')} + \log{p(\theta')} + \log{q_{\theta'}(\theta_i)} - \log{p(y|\theta)} - \log{p(\theta)} - \log{q_{\theta_i}(\theta')}
    \end{split}
\end{equation}

Now, the criterion 2 says,

$$\alpha = \min \left\{ 1, \frac{f(\theta') q_{\theta'}(\theta_i)}{f(\theta_i) q_{\theta_i}(\theta') } \right\} $$

Taking logarithm on both sides of above equation,

\begin{equation}
    \begin{split}
        \log{\alpha} &= \min \left\{ \log{1}, \log \left[\frac{f(\theta') q_{\theta'}(\theta_i)}{f(\theta_i) q_{\theta_i}(\theta') }\right] \right\} \\
        \Rightarrow \log{\alpha} &= \min \left\{ 0, \log{ \frac{p(y|\theta')p(\theta') q_{\theta'}(\theta_i)}{p(y|\theta)p(\theta)q_{\theta_i}(\theta')}} \right\}\\
        \Rightarrow \log{\alpha} &= \min \left\{ 0, \log{p(y|\theta')} + \log{p(\theta')} + \log{q_{\theta'}(\theta_i)} - \log{p(y|\theta)} - \log{p(\theta)} - \log{q_{\theta_i}(\theta')} \right\}
    \end{split}
\end{equation}

III.3) We assume that the prior distributions on $A,B,C, \nu$ is

$$p(A,B,C,\nu) = p(A)p(B)p(C)p(\nu) $$

where $A, B$ and $C$ $\sim G(0,\sigma^2) $ with sigma = 100 m/s and $\nu$ has a uniform distribution.

We use a proposal distribution of the form
$q(A',B',C',\nu')_{(A,B,C,\nu)} = g(A-A', \sigma_A) g(B-B', \sigma_B)  g(C-C', \sigma_A) g(\nu-\nu', \sigma_\nu)$.
where $g(x, \sigma) = \frac{1}{\sqrt{2\pi} \sigma} \mathrm{e}^{-\frac{x^2}{2\sigma^2}}$

In this expression, $\sigma_A$, $\sigma_B$, $\sigma_C$ and $\sigma_\nu$ are to be tuned to find an efficient proposal distribution.

Using question III.2), write a Metropolis-Hastings code starting at  $\nu_0 = 2\pi/6.5$ and $A_0, B_0, C_0$ given by the least square solution (see question II.2) where the acceptance rate is between 5 and 35 % (that is, find suitable $\sigma_A$, $\sigma_B$, $\sigma_C$ and $\sigma_\nu$ to obtain this property) and perform 2,000,000 iterations.

Indication: for $\sigma_\nu$ take a fraction of $2\pi/T_{\mathrm{obs}}$ where $T_{\mathrm{obs}}$ is the total timespan of observations (the difference between the last and first observation dates).

First we want to make the eqaution for $\log \alpha$ a little simple by noting the fact that the given proposal distribution is symmetric (a gaussian distribution). Thus $q_{\theta}(\theta') = q_{\theta'}(\theta)$ and so the equation for $\log \alpha$ would become,

$$\log{\alpha} = \min \left\{ 0, \log{p(y|\theta')} + \log{p(\theta')} - \log{p(y|\theta)} - \log{p(\theta)} \right \}$$

Now, we know that,

$$p(y|\theta) = \frac{1}{ \sqrt{2\pi}^N\prod\limits_{k=1}^N \sigma_k} \mathrm{e}^{-\frac{1}{2} \sum\limits_{k=1}^N \frac{(y(t_k) - f(\theta))^2}{\sigma_k^2} }$$

where, $f(\theta) = A \cos \nu t_k - B \sin \nu t_k - C$. Using this equation, one can find log likelihood as follows,

\begin{equation}
    \begin{split}
        \log p(y|\theta) &= -\frac{N}{2} \log 2\pi - \sum\limits_{k=1}^{N} \log \sigma_k - \frac{1}{2} \sum\limits_{k=1}^{N} \frac{(y_k - f(\theta))^2}{\sigma_k ^2} \\
     \end{split}
\end{equation}

Here, again $f(\theta) = A \cos \nu t_k - B \sin \nu t_k - C$ and $f(\theta') = A' \cos \nu t_k - B' \sin \nu t_k - C'$.

Similarly, we can do the same for the prior probabilities, which have the following distribution,

$$p(\theta) = \frac{1}{\sqrt{2\pi}\sigma} \exp{\left(-\frac{\theta^2}{2\sigma^2}\right)}$$

We also know that,

$$p(A,B,C,\nu) = p(A)p(B)p(C)p(\nu)$$

with $p(A)$, $p(B)$ and $p(C)$ having a gaussian distribution and $p(\nu)$ having the uniform distribution. Using this information, we can write above equation as,

$$p(\theta) = p(A,B,C,\nu) = \frac{1}{\sqrt{2\pi}\sigma_A} e^{-\frac{A^2}{2\sigma_A^2}} \frac{1}{\sqrt{2\pi}\sigma_B} e^{-\frac{B^2}{2\sigma_B^2}} \cdot \frac{1}{\sqrt{2\pi}\sigma_C} e^{-\frac{C^2}{2\sigma_C^2}} \cdot \nu$$

$$\Rightarrow p(\theta) = \frac{1}{\sqrt{2\pi}^3\sigma_A \sigma_B \sigma_C} e^{-\frac{A^2}{2\sigma_A^2}-\frac{B^2}{2\sigma_B^2}-\frac{C^2}{2\sigma_C^2}} \cdot \nu $$

$$\Rightarrow \log{p(\theta)} = -\frac{3}{2}\log{2\pi} -\log{\sigma_A} - \log{\sigma_B} - \log{\sigma_C} - \frac{A^2}{2\sigma_A^2} - \frac{B^2}{2\sigma_B^2} - \frac{C^2}{2\sigma_C^2} + \log{\nu}$$

Just for the reference, the criterion can be written as,

$$\log{\alpha} = \min \left\{ 0, \log{p(y|\theta')} + \log{p(\theta')} - \log{p(y|\theta)} - \log{p(\theta)} \right \}$$

Again, $f(\theta) = A \cos \nu t_k - B \sin \nu t_k - C$ and $f(\theta') = A' \cos \nu t_k - B' \sin \nu t_k - C'$. We want to use this formula in our code.

```python
# Retrieving data
time1, rv, rve = np.loadtxt('homework_rv.txt', usecols=(0,1,2), unpack=True)
time = time1*24*60*60
```

```python
# Visulizing the data
plt.errorbar(time1, rv, yerr=rve, fmt='k.-')
plt.xlabel('Time (in days)')
plt.ylabel('Radial Velocity')
```

```python
# Least-square estimate of mass from the data
# Borrowing M, MT and V1 from Point estimate section
FF = MT.dot(V1).dot(M)
cov = np.linalg.inv(FF)
theta_theoretical = cov.dot(MT).dot(V1).dot(rv)

AA_the = np.random.normal(theta_theoretical[0], cov[0][0], 10000)
BB_the = np.random.normal(theta_theoretical[1], cov[1][1], 10000)
CC_the = np.random.normal(theta_theoretical[2], cov[1][1], 10000)

mass_the1 = mass(AA_the, BB_the)
mass_the = mass_the1/5.972e+24

print('Theoretical Analysis:')
print('---------------------')
print('Estimate of mass: ' + str(np.mean(mass_the)) + ' M_earth')
print('Standard deviation in estimate: ' + str(np.std(mass_the)) + ' M_earth')
```

    Theoretical Analysis:
    ---------------------
    Estimate of mass: 4.712992076056859 M_earth
    Standard deviation in estimate: 0.07922239234066192 M_earth

```python
# Defining model
def ftheta(theta):
    global nu, time
    t1 = time
    f1 = theta[0]*np.cos(nu*t1) + theta[1]*np.sin(nu*t1) + theta[2]
    return f1

# Defining log(p(theta))
def logpt(theta, sig_the):
    aa = -(3/2)*np.log(2*np.pi)
    bb = -np.log(sig_the[0])-np.log(sig_the[1])-np.log(sig_the[2])
    cc = (-0.5*(theta[0]**2/sig_the[0]**2)) - (0.5*(theta[1]**2/sig_the[1]**2)) - (0.5*(theta[2]**2/sig_the[2]**2))
    lgthe = aa + bb + cc
    return lgthe

# Defining log(likelihood)
def loglikelihood(theta):
    global time, rv, rve
    ff = ftheta(theta)
    nn = ((rv-ff)**2)/rve**2
    expp = np.sum(nn)
    errr = np.sum(np.log(rve))
    ll = -0.5*len(time)*np.log(2*np.pi) - errr - 0.5*expp
    return ll

# Defining proposal distribution
def theta_proposal(theta, sigma_prop):
    the_prop = theta + np.random.randn(3)*sigma_prop
    return the_prop

# Defining alpha
def log_alpha(theta, sig_theta, theta_prop):
    alpha = loglikelihood(theta_prop) + logpt(theta_prop, sig_theta) \
            - loglikelihood(theta) - logpt(theta, sig_theta)
    return alpha
```

```python
def monte_carlo(sA, sB, sC):
    N = 2000000

    sigA = 100
    sigB = 100
    sigC = 100

    A0 = np.mean(AA_the)
    B0 = np.mean(BB_the)
    C0 = np.mean(CC_the)
    nu0 = (2*np.pi)/(6.5*24*60*60)

    acc = 0

    Tobs = time[-1]

    sN = 2*np.pi/Tobs/10

    sigma2 = np.array([sigA, sigB, sigC])

    sigma1 = np.array([sA, sB, sC])
    sig_prop = sigma1

    the_i = np.array([A0, B0, C0])

    An = np.array([])
    Bn = np.array([])
    Cn = np.array([])

    for _ in tqdm(range(N)):
        theta_prop = theta_proposal(the_i, sig_prop)
        fff = log_alpha(the_i, sigma2, theta_prop)
        logalpha = np.minimum(0, fff)
        u1 = np.log(np.random.random())
        if u1 <= logalpha:
            the_i = theta_prop
            acc = acc + 1
            An = np.hstack((An, the_i[0]))
            Bn = np.hstack((Bn, the_i[1]))
            Cn = np.hstack((Cn, the_i[2]))
        else:
            the_i = the_i

    rate = acc/N
    print('Acceptance rate is ', rate)
    return An, Bn, Cn
```

```python
A12, B12, C12 = monte_carlo(0.29,0.29,0.29)
```

    100%|██████████| 2000000/2000000 [12:24<00:00, 2685.92it/s]

    Acceptance rate is  0.1866755

```python
mass_mcmc = mass(A12,B12)
mass_mcmc1 = mass_mcmc/5.972e+24
vari2 = np.var(mass_mcmc1)
stdd2 = np.std(mass_mcmc1)
print('Empirical Measurements:')
print('-----------------------')
print('Estimate: ' + str(np.mean(mass_mcmc1)) + ' M_earth')
print('Variance: ' + str(vari2) + ' M_earth^2')
print('Standard Deviation: ' + str(stdd2) + ' M_earth')
#print(np.mean(m_pesti), np.std(m_pesti))
plt.hist(mass_mcmc1, bins=50)
plt.xlabel('Mass of the planet (in Earth-Mass)')
plt.ylabel('Counts')
```

III.4) Plot the posterior distributions of $A,B,C$ and $\nu$.
Compute the posterior mean and posterior median of $A,B,C$ and $\nu$.

```python
print('Empirical Analysis of A:')
print('------------------------')
print('Posterior mean: ', str(np.mean(A12)))
print('Posterior median: ', str(np.median(A12)))
plt.hist(A12, bins=50, density=True)
plt.xlabel('Value of A')
plt.ylabel('Count')
plt.title('Posterior distribution of A')
```

```python
print('Empirical Analysis of B:')
print('------------------------')
print('Posterior mean: ', str(np.mean(B12)))
print('Posterior median: ', str(np.median(B12)))
plt.hist(B12, bins=50, density=True)
plt.xlabel('Value of B')
plt.ylabel('Count')
plt.title('Posterior distribution of B')
```

```python
print('Empirical Analysis of C:')
print('------------------------')
print('Posterior mean: ', str(np.mean(C12)))
print('Posterior median: ', str(np.median(C12)))
plt.hist(C12, bins=50, density=True)
plt.xlabel('Value of C')
plt.ylabel('Count')
plt.title('Posterior distribution of C')
```

III.5) A common way to derive error bars on physical parameters is to use credible intervals. A credible interval is an interval $C$ such that a parameter
$$\mathrm{pr}\{\theta_k \in C | y  \} = \alpha$$

Find $C_{95}$ defined as the smallest interval such that $\mathrm{Pr}\{m \in C_{95} | y  \} = 95%$

```python
# Defining a function that could create a credible interval with desired confidence
def confidence_interval(array, alpha):
    aa1 = np.sort(array)
    len_chain = len(aa1)
    len_al = int(len_chain*alpha/100)
    nn = len_chain - len_al
    interval_length = np.zeros(nn)
    for i in range(nn):
        interval_length[i] = aa1[len_al + i] - aa1[i]
    arg_shortest_interval = np.argmin(interval_length)
    ll = aa1[arg_shortest_interval]
    ul = aa1[arg_shortest_interval + len_al]
    return ll, ul
```

```python
ll1, uu1 = confidence_interval(mass_mcmc1, 95)
print('The 95% credible interval for mass is [' + str(ll1) + ', ' + str(uu1) + '] M_earth.')
```

    The 95% credible interval for mass is [3.9854221963395506, 5.369990737388258] M_earth.

III.6) Bonus question: it is not obvious whether a MCMC chain has converged. Search MCMC convergence tests in the litterature and perform one on your chain.

```python
# The general philosophy of convergence tests is to check that
# there are no traces of excessive correlation in the chain

# First check: trace plots. Simply plot the values taken by the chain.
xs = np.arange(1, len(A12)+1, 1)/len(A12)
plt.plot(xs, A12)
plt.xlabel('Normalized number of iterations')
plt.ylabel('Value of A')
plt.title('Trace plot of A')
plt.show()
plt.plot(xs, B12)
plt.xlabel('Normalized number of iterations')
plt.ylabel('Value of B')
plt.title('Trace plot of B')
plt.show()
plt.plot(xs, C12)
plt.xlabel('Normalized number of iterations')
plt.ylabel('Value of C')
plt.title('Trace plot of C')
plt.show()
```

### Gelman-Rubin Test
We try the Gelman-Rubin Test to check if the MCMC has converged or not. Below, we show a basic formulation (an algorithm) for Gelman-Rubin Test.

1. Consider mth chain: $\theta_1^m$, $\theta_2^m$, ..., $\theta_{N_m}^m$.
2. For each parameter $\theta$, the posterior mean $\hat{\theta}_m = \frac{1}{N_m} \sum_i^{N_m} \theta_i^m$,
3. For each parameter, compute the intra-chain variance, $\sigma_m^2 = \frac{1}{N_m-1}\sum_i^{N_m} \left( \theta_i^m - \hat{\theta}_m \right)^2$.
4. Compute $\hat{\theta}$, the mean of all chains, $\hat{\theta} = \frac{1}{M} \sum_m^M \hat{\theta}_m$.
5. Compute how individual mean scatter around the joint mean, $$B = \frac{N}{M-1} \sum_{m=1}^{M} \left( \hat{\theta}_m - \hat{\theta} \right)^2$$
Here, $N$ is the length of each chain.
6. Compute the averaged variance of the chains, $$W = \frac{1}{M} \sum_{m=1}^{M} \sigma_m^2$$.
7. Compute, $$\hat{V} = \frac{N-1}{N} W + \frac{M+1}{MN} B$$
8. Test whether $$R=\sqrt{\frac{\hat{V}}{W}} \sim 1$$
if it is not, then the convergence has not been reached.

Below, we try to implement this test to our chains of A, B and C

```python
def gelman_rubin(ch_a, ch_b, ch_c):
    NN = len(ch_a)
    MM = 3
    # For a
    mean_ch_a = np.sum(ch_a)/NN
    var_ch_a = np.sum((ch_a - mean_ch_a)**2)/(NN-1)
    # For b
    mean_ch_b = np.sum(ch_b)/NN
    var_ch_b = np.sum((ch_b - mean_ch_b)**2)/(NN-1)
    # For c
    mean_ch_c = np.sum(ch_c)/NN
    var_ch_c = np.sum((ch_c - mean_ch_c)**1)/(NN-1)
    # Mean of means
    means = np.array([mean_ch_a, mean_ch_b, mean_ch_c])
    mean_fin = (mean_ch_a + mean_ch_b + mean_ch_c)/MM
    # B
    BB = NN*np.sum((means - mean_fin)**1)/(MM-1)
    # W
    WW = (1/MM)*(var_ch_a + var_ch_b + var_ch_c)
    # V
    VV = ((NN-1)/NN)*WW + ((MM+1)/(MM*NN))*BB
    RR = np.sqrt(VV/WW)
    return RR
```

```python
R1 = gelman_rubin(A12, B12, C12)
tol = 0.01
print(R1)
if np.abs(R1-1)<tol:
    print('The Chains are converged')
else:
    print('The chains are not converged')
```

    0.9999986607767574
    The Chains are converged

### Autocorrelation

Second check: autocorrelation function
When computing the uncertainty on a value estimated by an empirical mean,
$$ \mu = \frac{1}{N}\sum\limits_{k=1}^N X_k $$
where all the $X_k$ have a standard deviation $\sigma$
we have a standard deviation of the mean equal to $\sigma/\sqrt{N}$ if the samples are independent. If they are not, then the uncertainty will be greater.

To evaluate the uncertainties on the statistics that we derive from the MCMC samples, it is convenient to think in terms of effective number of samples: how many equivalent independant samples of each parameters are in the chain?

```python
def autocorrelation_function(chain, lags):
    mu = np.mean(chain)
    llags = len(lags)
    rhos = np.zeros(llags)
    variance = np.var(chain)
    N = len(chain)

    for i in range(llags):
        k = lags[i]
        X0 = chain[:N-k] - mu
        Xk = chain[k:] - mu
        rhoi = np.sum(X0*Xk)/variance
        rhos[i] = rhoi/(N-k)
    return(rhos)

thetas = np.vstack((A12, B12))
thetas = np.vstack((thetas, C12))

acfs = []
for i in range(3):
    acf = autocorrelation_function(thetas[i,:], np.arange(2000))
    acfs.append(acf)
```

```python
labels = ['A', 'B','C']
plt.figure()
for i in range(3):
    plt.plot(acfs[i],label = labels[i])
plt.xlabel('lag', fontsize = 16)
plt.ylabel('sample correlation', fontsize = 16)
plt.suptitle('Autocorrelation of the samples', fontsize = 18)
plt.legend(fontsize = 16)
```

```python
#Effective sample size (ESS)
#defined as in Markov Chain Monte Carlo in Practice: A Roundtable Discussion.
#Robert E. Kass, Bradley P. Carlin, Andrew Gelman and Radford M. Neal.
#The American Statistician. Vol. 52, No. 2 (May, 1998), pp. 93-100

N = thetas.shape[1]
for i in range(3):
    ESS = N / (1 + 2*np.sum(acfs[i][:750]))
    print('Number of effective samples of theta_{} = {}'.format(i, ESS))

#There are many other definitions of the effective sample size
```

    Number of effective samples of theta_0 = 85157.47198701472
    Number of effective samples of theta_1 = 69355.22412546021
    Number of effective samples of theta_2 = 91609.36876388262

## Using emcee to implement MCMC

```python
def log_probability(th1, tt1, rr1, re1):
    # Sigmas for prior distributions
    sig_the = np.array([100, 100, 100])
    # Prior and Likelihood
    lpri = logpt(th1, sig_the)
    likl = loglikelihood(th1)
    return lpri + likl

A0 = np.mean(AA_the)
B0 = np.mean(BB_the)
C0 = np.mean(CC_the)

nwalkers, ndim = 32, 3

posInit = np.array([A0, B0, C0]) * np.random.randn(nwalkers,ndim)

sampler = emcee.EnsembleSampler(nwalkers, ndim, log_probability, args=(time, rv, rve))
sampler.run_mcmc(posInit, 10000, progress=True)
```

    100%|██████████| 10000/10000 [00:19<00:00, 504.40it/s]

    State([[ 2.03089088e+00  3.85310736e-01  1.45423019e-01]
     [ 2.33366346e+00  1.95995434e-02 -2.44581855e-01]
     [ 1.70894979e+00  6.98689476e-01  2.43692054e-01]
     [ 2.24848783e+00  2.59828800e-01 -1.05542411e-01]
     [ 2.16692285e+00  1.25955963e-01  2.78715972e-02]
     [ 2.25757974e+00 -2.22893167e-01 -2.67114453e-02]
     [ 2.16279935e+00  2.80813478e-01 -5.68088453e-02]
     [ 2.13272838e+00  5.15036053e-01  9.52887295e-02]
     [ 2.21024339e+00  2.86301654e-01 -1.43101867e-01]
     [ 2.04540218e+00  1.08105509e-01 -1.06264152e-01]
     [ 2.10712006e+00  1.24794390e-01 -5.80225049e-03]
     [ 2.08667047e+00  2.18488062e-01 -7.72362120e-02]
     [ 2.17225280e+00 -1.23741336e-02 -2.12818851e-01]
     [ 2.07868644e+00  3.25767338e-01 -1.68100957e-01]
     [ 2.15529376e+00  2.00169481e-01  1.54933730e-02]
     [ 2.03154564e+00  1.71097262e-01 -2.72747296e-01]
     [ 2.26957637e+00 -1.56435806e-01 -1.34759855e-02]
     [ 1.96278488e+00  1.98191405e-01  3.51335947e-03]
     [ 2.07345746e+00  4.21860773e-01  7.76072754e-02]
     [ 2.01280701e+00  2.93542958e-01 -2.50056445e-01]
     [ 2.06843757e+00  3.91595528e-01 -1.78851952e-01]
     [ 1.90518875e+00 -8.27545814e-02  8.15619267e-03]
     [ 2.30602941e+00 -1.45817654e-01 -1.34741822e-01]
     [ 1.99764812e+00  3.60547724e-01 -6.07582523e-02]
     [ 1.84573032e+00  2.26035674e-01  3.53203709e-02]
     [ 2.26277535e+00 -1.88787801e-01 -8.74310254e-02]
     [ 1.98218873e+00  2.00890918e-01  3.89339645e-05]
     [ 2.24039734e+00  5.93892012e-02 -3.49313462e-01]
     [ 2.18824764e+00  3.28569440e-01  1.51212054e-01]
     [ 1.99949659e+00  4.85728353e-01  1.42624230e-01]
     [ 2.20447757e+00  2.09938570e-01 -1.04011164e-01]
     [ 2.11163690e+00  2.57138335e-02 -7.51424586e-02]], log_prob=[-71.37704743 -71.56275043 -75.87951292 -70.68880065 -70.45983611
     -72.40943879 -70.27247959 -71.82126278 -70.80264379 -70.30615738
     -70.15511987 -70.10351435 -70.81180403 -71.15769912 -70.27213619
     -72.25554124 -72.0127366  -70.56166157 -70.96982582 -72.51347935
     -71.75796438 -72.44470691 -71.44288389 -70.76303071 -71.66596998
     -71.66527818 -70.43432593 -72.69029034 -71.93218405 -71.73009795
     -70.29754728 -70.23915222], blobs=None, random_state=('MT19937', array([4212148773, 1642931907,  848821093, 3574877135, 1329473233,
           3894666333,  376427257,  611228094,  942726394, 1546110794,
           2130694899, 1402613433, 4000023449, 3163371905, 3065671015,
            256558948,  961006301,  883930909, 3929938354, 2132377725,
           1367080129, 4029373881, 2038822389, 1265274617, 3574050213,
           2148328197, 2830171202, 3824336149, 2979594929, 3154371624,
           3363312601, 2021212851, 3321809502, 3023424127, 3985355283,
           1862663768, 1635159567, 4151789274, 1462392975, 2539627219,
           4118345611, 3623873809,   37313299, 1907574328,   74130176,
           3719774833, 3855134634, 2839077543, 3684963498, 2715060933,
           2059610009, 3896478671, 1204331742, 4112282427, 2258918851,
           3933610850,  644720736, 2293323954,   17371884, 3307602614,
           3625486264,  558655887, 1786402959, 1244915714,  395963933,
           3433614011, 1996515274, 2446785861, 1009247860, 3877921666,
           3668720489, 1020140058,   24233795,  416068869,  238328929,
           2096111421,   80253066,  470402740, 1386974450, 2502335731,
            634222842, 3042331470,  968950639, 2339994408, 2199592666,
           1634722585, 2335200358, 3623775866, 3257600498, 1702913809,
            464260461,  166516676, 1715676715, 1264355263,  152302186,
            410702111,  993648101, 2570890045, 2136181877, 3746563333,
           1473221353, 3384800282, 3259327085, 1432524763, 2668216339,
           3193478003, 1649104399, 3210892476, 4242747291, 2037031228,
            830198161, 1795456254, 1211466716, 2621010303, 3979280316,
           1128135996, 3053355090, 2276872158,  910259163, 2519212318,
           3808325579,  261551797, 3600596009, 1465084680, 2642658485,
           1555814342, 1588401645,  334501423, 3389620953,  192955634,
           1273571276, 1151723590, 4219208490,  459240082, 3996523563,
           4239851125, 1805065860,  637593208, 1225813543,  344404640,
            819251109,  988262834, 2683130335, 3514764197, 4018943940,
            438058404, 1602504207,  516792023, 3241039249,  110437890,
           2057026591, 3838749411, 4160874994, 3690809686,  435206632,
           1043924127, 2868747311, 2424559525,  832933263,  890479466,
           4019882071, 2077251317,  270455685, 3736389050, 3119867282,
           3084940508,  926730790, 2397198035, 3338840357,  646705559,
           2206670597, 3180884284, 1475365359, 2479521398, 2183553589,
             59913366, 1690126023, 2695778160, 2988645898, 1259214329,
           1858069547, 1019441645, 2923211826,  709353891, 4079078659,
           2362515200,  894077359,  359089707, 1791853423,  413201412,
           3601967756, 3875837226, 2946976753,  578553055, 1459169763,
           2782793702, 1641875087,  346986599, 2341928173,  154743767,
           1458364944, 3916564665,   51215049,  358246111,  948336350,
           2791708695, 1541127618,  245596308,   20661275, 3809571424,
           1866449308, 1855224853,   61536164, 1572866516, 4061346133,
           2017206272, 4210445828,  115896867, 2948446267, 4006847216,
           2907982809, 2472835773, 3940900083, 1408376430, 2389751365,
           3589577832,  982160504, 3272906461, 2282620609,  285336796,
           1034015418, 3708354094, 1643243875,  435277439, 4130080730,
            902227868, 4204729159, 1898211034, 3003341250, 3217860351,
           1551392836, 3675901367, 1695906117, 1783250105, 2818304550,
            165189131, 2962951609, 3001243371, 1287201001, 1979938003,
            563593204, 2600062673, 2196981748, 2961576967, 3494847863,
            593108847, 2842875849, 1674971465, 3049759285, 3220762888,
           1874452374, 2941631968, 2487398377, 1807769161,  477446058,
           2215677693, 1328005345, 1724492849, 3109240945,  856162641,
           3959681367, 4026023451, 1983193318, 2878025085, 4108258324,
           3295197721, 1898911680, 1054123675,  887910336, 4129690802,
            349133389, 1169376524, 2893225582, 3638591090, 3383913541,
           2748600828, 2241575015, 3618134932, 1305855672, 1017763270,
           3329834507, 1048004767, 3781676939, 1075923044, 2550038307,
           3727373418, 1119849719,  832334589, 2335673785, 2003854739,
           4163712260, 1564823441,  939295396,  774973062,  368789345,
           1283253279, 2230161316,  559559993, 3248225245, 1554743501,
            959343543, 1752695756, 1234821474, 1438429139, 1562247302,
           4223666135, 3637704324,  530366074, 1958772670, 2798226300,
            306255907, 1001527088,  490059044,  545092898, 3194498815,
            725763743, 2532987995, 4291730670, 3581517728,  417397555,
           4223684147, 2431536835, 4057167549,  472781543, 3742144701,
           3204653232, 1008679183,  155006530,  820786657, 1338938816,
           3422318187, 4088280743, 3754880638, 2350623167, 2897132109,
           2132774544, 3556238465,  172233608,  997380214, 2575081297,
           3984611175, 2678096790, 1236840890, 3511752335, 3506277488,
           3977755405, 1312368022, 2583347690, 1152439556, 3153953145,
           1800796132, 1216932802, 2175615733, 1812790153, 2911249218,
           2327288719, 1472578803, 2642411957, 1772917945, 1328882513,
           3309699401,  318296607, 3539892278,  765615606, 1141960380,
            455699630, 4035512771, 3898611184,  275349590, 2864769061,
           3934878888, 2300109299,  540479485, 2986915250,  200087020,
           1320293899, 1046853479, 3548885319,  916267570, 1379363985,
            526031526,  903830239, 1445234935, 2679225103, 4007716069,
            817741899, 2474898272, 1708143760, 2183724962, 3923123652,
           2911976801, 2597792413, 3820333329,  292403386, 3993048048,
             99108373, 1089203802,  146853828, 2030329241, 4127923012,
           3335352585, 2914826508, 2292268064,  980929036, 1272857973,
           4285440225, 2236329226, 1386087424, 4061763005, 3456548628,
           3247910512,  725935620,  620823008, 1226964765, 1664544316,
            411657670,  689351491, 2026852014, 1803484984, 1501073479,
           2343315430,  598220321,    9242176, 2359651693, 2171606690,
           4218947624, 1409575711, 1487663336, 1464528917, 3623678209,
           1902120104, 2017972551, 3732981781,  587504177,  234300596,
           1046421725, 1381967987, 1773926716,  536685170, 1086629856,
            682952022, 4193530904, 2805426042, 2620397202, 2606849832,
           2945863725, 2560417101, 1572268800, 1997283310, 3589588061,
           3296809445, 3668579931, 1032947246,  471496846, 3695028609,
           2073800343, 3878713472, 3784690411, 2214476926,  612046255,
           3423842934,  743085482, 3245235508,  218711477, 4005615093,
           1356224554,  676377765,  466945376,  443403523, 3425330420,
           2493124899, 1701076645, 2133377176, 3662695703, 1240686524,
           1483385607, 1928409783, 4216063130, 2087562855, 2007642173,
           1492299787, 4065256727, 2245545909, 2573022220, 2721797620,
           4005483312, 2908282642,  981210041, 1627205015, 2598274630,
           2707049485, 3429407061,  525060212, 1529430695, 3355910897,
            989708791, 3279323873, 3698533585, 2344300169, 1617373259,
           3595652001, 2632785109,  459652706,  914720041, 2800172569,
           1620763766, 2523618499, 2586693466, 1815344627, 2540210773,
            966560795, 1614702142, 1527146767, 3619025845, 3513680735,
           4052800310,  516406389, 4165015009, 2321707829, 2284612874,
            160259890, 3792294939, 2727591327, 2347460311, 2471544041,
           1412087392, 2504620777, 2760293336, 3132554094, 1107279503,
             12961668, 1122574508,  579358843, 1534162837, 2312681342,
            965615492, 3855713789,  435037553, 3399113822,  201719527,
           3758909234, 1045881948, 1806468209,  217019308, 2971735698,
           3336747696,  670606390,  250092471, 2512999490, 1246053639,
           1394522962,  477782882,  645611517,  622567451, 1976848516,
           1476511890, 3785668149, 1360358549, 4170836713,  349840504,
           3697808343, 4216042822, 2202631845, 1937762557, 2751425970,
           1989339586, 1201906502, 3639868549, 2118371026, 2976978197,
           2361054576,  830668951, 3268018836, 2404122052,  633125951,
           2348827907, 2719822439, 1416375324, 2788023741, 1680221203,
           2452351262, 2583034037,  244425199, 4000624986,  218700785,
           2524692076, 2260956334,  110649577, 3822981147,  892271848,
           1321853502, 3905643666,  867297941, 2834005491,  911244632,
           1573031520, 2556606100, 1692097817,  830824832,   17208038,
           3724404882, 1401962134, 1651564533,   68222195,  400099431,
           1860598047, 2605560662, 1266466894, 1953098575, 3795524277,
           1836707477, 2639489895, 3074280900,  922969773], dtype=uint32), 460, 0, 0.0))

```python
fig, axes = plt.subplots(3, figsize=(10, 7), sharex=True)
samples = sampler.get_chain()
labels = ['A', 'B', 'C']
for i in range(ndim):
    ax = axes[i]
    ax.plot(samples[:, :, i], "k", alpha=0.3)
    ax.set_xlim(0, len(samples))
    ax.set_ylabel(labels[i])
    ax.yaxis.set_label_coords(-0.1, 0.5)

axes[-1].set_xlabel("step number");
```

```python
flat_samples = sampler.get_chain(discard=100, thin=15, flat=True)
```

```python
fig = corner.corner(
    flat_samples, labels=labels, truths=[np.mean(A12), np.mean(B12), np.mean(C12)]
);
```

```python
A_emcee = flat_samples[:,0]
B_emcee = flat_samples[:,1]

mass_emcee = mass(A_emcee, B_emcee)
mass_emcee1 = mass_emcee/5.972e+24
vari2_e = np.var(mass_emcee1)
stdd2_e = np.std(mass_emcee1)
print('Empirical Measurements:')
print('-----------------------')
print('Estimate: ' + str(np.mean(mass_emcee1)) + ' M_earth')
print('Variance: ' + str(vari2_e) + ' M_earth^2')
print('Standard Deviation: ' + str(stdd2_e) + ' M_earth')

plt.hist(mass_emcee1, bins=50)
plt.xlabel('Mass of the planet (in Earth-Mass)')
plt.ylabel('Counts')
```


# Direct methods for solving linear systems (homework)

**Exercise 1.** Let us consider the linear system $A\mathbf{x} = \mathbf{b}$ where
$$
  A =
  \begin{bmatrix}
  \epsilon & 1 & 2\\
  1 & 3 & 1 \\
  2 & 1 & 3 \\
  \end{bmatrix}.
$$

1. Find the range of values of $\epsilon \in \mathbb{R}$ such that the matrix $A$ is symmetric and positive definite.
**Suggestion**: use the *Sylvester's criterion* which states that  a symmetric matrix $A \in \mathbb{R}^{n \times n}$ is positive definite if and only if all the main minors (The main minors of $A \in \mathbb{R}^{n \times n}$ are the determinants of the submatrices $A_p = (a_{i,j})_{1 \leq i, j \leq p}$, $p = 1, ..., n$). of $A$ are positive.
2. What factorization is more suitable for solving the linear system $A\mathbf{x}=\mathbf{b}$ for the case $\epsilon=0$? Motivate the answer.
3. Compute the Cholesky factorization $A = R^T R$ for the case $\epsilon = 2$.
4. Given $\mathbf{b} = (1,1,1)^T$, solve the linear system by using the Cholesky factorization computed at the previous point.

```python
import sympy as sp
import numpy as np

sp.init_printing()

# Exercise 1.1

n = 3
epsilon = sp.Symbol('e')

A = sp.Matrix([[epsilon, 1, 2], [1, 3, 1], [2, 1, 3]])
A
```

$$\left[\begin{matrix}e & 1 & 2\\1 & 3 & 1\\2 & 1 & 3\end{matrix}\right]$$

```python
sp.det(A)
```

```python
sp.Matrix.berkowitz_minors(A)

# So, for the matrix to be positive definite the conditions are respectively
# e > 0
# e > 1/3
# e > 11/8
# so the matrix is positive for epsilon > 11/8
```

```python
# Exercise 1.2

"""
Since the matrix has a 0 pivot in the first row when epsilon = 0, the
most appropriate way of factorizing it in this case is Cholesky
factorization.
"""
```

    '\nSince the matrix has a 0 pivot in the first row when epsilon = 0, the\nmost appropriate way of factorizing it in this case is Cholesky\nfactorization.\n'

```python
# Exercise 1.3 (LU instead of Cholesky)

L,U,_ = A.LUdecomposition()
```

```python
L
```

$$\left[\begin{matrix}1 & 0 & 0\\2 & 1 & 0\\e & \frac{3 e}{5} - \frac{1}{5} & 1\end{matrix}\right]$$

```python
U
```

$$\left[\begin{matrix}1 & 3 & 1\\0 & -5 & 1\\0 & 0 & - \frac{8 e}{5} + \frac{11}{5}\end{matrix}\right]$$

```python
# Exercise 1.4 (LU instead of Cholesky)

b = sp.Matrix([[1, 1, 1]])

sp.solve(A, b)
```

**Exercise 2.** Let us consider the following matrix $A \in \mathbb R^{3 \times 3}$ depending on the parameter $\epsilon \in \mathbb R$:
$$
A =
\begin{bmatrix}
1 & \epsilon & -1 \\
\epsilon & \frac{35}3 & 1 \\
-1 & \epsilon & 2 \\
\end{bmatrix}.
$$

1. Calculate the values of the parameter $\epsilon \in \mathbb R$ for which the matrix $A$ is invertible (non singular).

2. Calculate the Gauss factorization $LU$ of the matrix $A$ (when non singular) for a generic value of the parameter $\epsilon \in \mathbb R$.

3. Calculate the values of the parameter $\epsilon \in \mathbb R$ for which the Gauss factorization $LU$ of the matrix $A$  (when non singular) exists and is unique.

4. Set $\epsilon = \sqrt{\frac{35}3}$ and use the pivoting technique to calculate the Gauss factorization $LU$ of the matrix $A$.

5. For $\epsilon=1$, the matrix $A$ is symmetric and positive definite. Calculate the corresponding Cholesky factorization of the matrix $A$, i.e. the upper triangular matrix with positive elements on the diagonal, say $R$, for which $A = R^T R$.

```python
# Exercise 2.1

epsilon = sp.Symbol("e")
A = sp.Matrix([[1, epsilon, -1], [epsilon, 35/3, 1], [-1, epsilon, 2]])
sp.solve(sp.det(A))
```

```python

```


## Causal Inference

### Potential Outcomes

The causal effect of a treatment, $D$, on an outcome, $y$, for either an observational *or* experimental unit, $i$, is defined by the differences in outcomes occurring under each of the treatment possibilities (Gelman, 2006). In the binary treatment case, $D_i = \left\{0, 1\right\}$. The potential outcomes—that is, the outcomes that would have been observed for unit $i$ under treatment *and* control conditions—are defined as follows:

\begin{cases}
    y_{1i} \quad \text{if}\ D_i = 1 \\
    y_{0i} \quad \text{if}\ D_i = 0\text{.}
\end{cases}

Only one of the outcomes—the one associated with unit $i$'s assignment—is ever observed, though. For a unit assigned to the treatment condition, $y_{1i}$ is observed and $y_{0i}$, referred to as the *counterfactual*, is unobserved.

Using the potential outcomes notation, the observed outcome, $y_i$, can be written as:

$$y_{0i} + (y_{1i} - y_{0i})D_i\text{,}$$

where $y_{1i} - y_{0i}$ is the causal effect of the treatment, $D$, on unit $i$ (Angrist and Pischke, 2009).

We can define the *average treatment effect*, or ATE, as:

$$ATE \equiv \frac{1}{N}\sum_{i=1}^N (y_{1i} - y_{0i})\text{.}$$

Because both potential outcomes are never observed for a single unit, though, treatment effects must be inferred by comparing average outcomes for treated and untreated units, resulting in:

$$\frac{1}{N}\sum_{i=1}^N y_{1i} - \frac{1}{N}\sum_{i=1}^N y_{0i}\text{.}$$

Another way to express the this is using *expected value* notation. For a random variable, $X$, its expected value is:

$$E[X] = x_1p_1 + x_2p_2 + \dots + x_kp_k = \sum_{i=1}^{k}x_ip_i\text{,}$$

where $p_i$ is the probability of seeing $x_i$.

We can rewrite the ATE formula in terms of conditional expectations:

$$E[y_i \mid D_i=1] - E[y_i \mid D_i=0]\text{.}$$

This is the difference in expected outcomes for treated and untreated units.

Recall our medical treatment study example where the treatment is only administered to patients who need it most. A naive comparison of outcomes for treated and untreated units, in this example, tells us something about potential outcomes, but not necessarily what we want to know—the effect of the treatment (Angrist and Pischke, 2009). The ATE, in this case, is the following:

$$\underbrace{E[y_{1i} \mid D_i=1] - E[y_{0i} \mid D_i=1]}_\text{Average treatment effect on the treated} +
\underbrace{E[y_{0i} \mid D_i=1] - E[y_{0i} \mid D_i=0]}_\text{Selection bias}\text{.}$$

The term $E[y_{1i} \mid D_i=1] - E[y_{0i} \mid D_i=1]$ represents the difference in average outcomes for those who were treated and what would have happened to them had they not been treated. To this causal effect, we add a term called *selection bias*. This represents the difference in average $y_{0i}$—that is, outcomes we would have observed without treatment—between those who were and weren't treated. In our example, because those who were treated were of the highest need, their observed outcomes, whatever they might be (we have said nothing about the response), are presumably worse than those who weren't treated, making selection bias negative.

#### Random Assignment

Let's consider each component of the ATE equation, above. Why do we focus on the average treatment effect *on the treated* and how do we get rid of selection bias? It turns out, that random assignment of $D_i$ can the explain the former and solve the latter.

Random assignment of $D_i$ makes $D_i$ *independent* of potential outcomes. This means that knowing $D_i$ gives us no information about the potential outcome, $y_i$. That is, the treated and untreated units have the same potential outcomes.

For the ATE, this implies that:

\begin{align}
E[y_i \mid D_i=1] - E[y_i \mid D_i=0]&=E[y_{1i} \mid D_i=1] - E[y_{0i} \mid D_i=0]\\
&=\underbrace{E[y_{1i} \mid D_i=1] - E[y_{0i} \mid D_i=1]}_\text{Average treatment effect on the treated}\text{.}
\end{align}

Going from the first line to the second is possible because of the independence of $y_{0i}$ and $D_i$. Because the values on the right side of the equation are equal, we don't have to necessarily focus on average treatment effect on the treated. But this notation is useful because we can simply the second line further:

\begin{align}
&=E[y_{1i} - y_{0i} \mid D_i=1]\\
&=E[y_{1i} - y{0_1}]\text{.}
\end{align}

This makes it possible to, under random assignment, compare the observed outcomes for treated and untreated units.

What about selection bias? Notice that because $E[y_{0i} \mid D_i=0]$ is equal to $E[y_{0i} \mid D_i=1]$, selection bias is eliminated!

```python

```


# Matrices Solutions

```
from sympy import *
init_printing()
```

Use `row_del` and `row_insert` to go from one Matrix to the other.

```
def matrix1(M):
    """
    >>> M = Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
    >>> M
    [1, 2, 3]
    [4, 5, 6]
    [7, 8, 9]
    >>> matrix1(M)
    [4, 5, 6]
    [0, 0, 0]
    [7, 8, 9]
    """
    M.row_del(0)
    M = M.row_insert(1, Matrix([[0, 0, 0]]))
    return M
```

```
M = Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
```

```
M
```

```
matrix1(M)
```

## Matrix Constructors

Use the matrix constructors to construct the following matrices.  There may be more than one correct answer.

$$\left[\begin{array}{ccc}4 & 0 & 0\\\\
0 & 4 & 0\\\\
0 & 0 & 4\end{array}\right]$$

```
def matrix2():
    """
    >>> matrix2()
    [4, 0, 0]
    [0, 4, 0]
    [0, 0, 4]
    """
    return eye(3)*4
    # OR return diag(4, 4, 4)
```

```
matrix2()
```

$$\left[\begin{array}{}1 & 1 & 1 & 0\\\\1 & 1 & 1 & 0\\\\0 & 0 & 0 & 1\end{array}\right]$$

```
def matrix3():
    """
    >>> matrix3()
    [1, 1, 1, 0]
    [1, 1, 1, 0]
    [0, 0, 0, 1]
    """
    return diag(ones(2, 3), 1)
    # OR diag(ones(2, 3), ones(1, 1))
```

```
matrix3()
```

$$\left[\begin{array}{}-1 & -1 & -1 & 0 & 0 & 0\\\\-1 & -1 & -1 & 0 & 0 & 0\\\\-1 & -1 & -1 & 0 & 0 & 0\\\\0 & 0 & 0 & 0 & 0 & 0\\\\0 & 0 & 0 & 0 & 0 & 0\\\\0 & 0 & 0 & 0 & 0 & 0\end{array}\right]$$

```
def matrix4():
    """
    >>> matrix4()
    [-1, -1, -1, 0, 0, 0]
    [-1, -1, -1, 0, 0, 0]
    [-1, -1, -1, 0, 0, 0]
    [ 0,  0,  0, 0, 0, 0]
    [ 0,  0,  0, 0, 0, 0]
    [ 0,  0,  0, 0, 0, 0]
    """
    return diag(-ones(3, 3), zeros(3, 3))
    # OR diag(-ones(3, 3), 0, 0, 0)
```

```
matrix4()
```

## Advanced Methods

Recall that if $f$ is an analytic function, then we can define $f(M)$ for any square matrix $M$ by "plugging" $M$ into the power series formula for $f(x)$. In other words, if $$f(x) = \sum_{n=0}^\infty a_n x^n,$$ then we define $f(M)$ by $$f(M) = \sum_{n=0}^\infty a_n M^n,$$ where $M^0$ is $I$, the identity matrix.

Furthermore, if $M$ is a diagonalizable matrix, that is, $M=PDP^{-1}$, where $D$ is diagonal, then $M^n = PD^nP^{-1}$ (because $M^n = \left(PDP^{-1}\right)\left(PDP^{-1}\right)\cdots\left(PDP^{-1}\right)=PD\left(P^{-1}P\right)D\left(P^{-1}P\right)\cdots DP^{-1} = PD^nP^{-1}$).

But if

$$ D = \begin{bmatrix}
         d_1 & 0 & \cdots & 0 \\\\
         0 & d_2 & \cdots & 0 \\\\
         \vdots & \vdots & \ddots & \vdots \\\\
         0 & 0 & \cdots & d_n
      \end{bmatrix}
$$

is a diagonal matrix, then

$$ D^n = \begin{bmatrix}
         d_1^n & 0 & \cdots & 0 \\\\
         0 & d_2^n & \cdots & 0 \\\\
         \vdots & \vdots & \ddots & \vdots \\\\
         0 & 0 & \cdots & d_n^n
      \end{bmatrix}
$$

so that

$$
\sum_{n=0}^\infty a_n M^n = \sum_{n=0}^\infty a_n PD^nP^{-1} = P\cdot\begin{bmatrix}
    \sum_{n=0}^\infty a_n d_1^n & 0 & \cdots & 0 \\\\
         0 & \sum_{n=0}^\infty a_n d_2^n & \cdots & 0 \\\\
         \vdots & \vdots & \ddots & \vdots \\\\
         0 & 0 & \cdots & \sum_{n=0}^\infty a_n d_n^n
\end{bmatrix}\cdot P^{-1} = P\cdot\begin{bmatrix}
    f(d_1) & 0 & \cdots & 0 \\\\
         0 & f(d_2) & \cdots & 0 \\\\
         \vdots & \vdots & \ddots & \vdots \\\\
         0 & 0 & \cdots & f(d_n)
\end{bmatrix}\cdot P^{-1}
$$

Let's create some square matrices, which we will use throughout the exercises.

```
x = symbols('x')
A = Matrix([[1, 1], [1, 0]])
M = Matrix([[3, 10, -30], [0, 3, 0], [0, 2, -3]])
N = Matrix([[-1, -2, 0, 2], [-1, -1, 2, 1], [0, 0, 2, 0], [-1, -2, 2, 2]])
```

First, verify that these matrices are indeed diagonalizable.

```
print(A.is_diagonalizable())
print(M.is_diagonalizable())
print(N.is_diagonalizable())
```

    True
    True
    True

Now, we want to write a function that computes $f(M)$, for diagonalizable matrix $M$ and analytic function $f$.

However, there is one complication. We can use `diagonalize` to get `P` and `D`, but we need to apply the function to the diagonal of `D`. We might think that we could use `eigenvals` to get the eigenvalues of the matrix, since the diagonal values of `D` are just the eigenvalues of `M`, but the issue is that they could be in any order in `D`.

Instead, we can use matrix slicing to get the diagonal values (or indeed, any value) of a matrix. There is not enough time in this tutorial (or room in this document) to discuss the full details of matrix slicing. For now, we just note that `M[i, j]` returns the element at position `i, j` (which is the `i + 1, j + 1`th element of the matrix, due to Python's 0-indexing). For example

```
M
```

```
M[0, 1]
```

That should be enough information to write the following function.

```
def matrix_func(M, func):
    """
    Computes M at func. Assumes that M is square diagonalizable.

    >>> matrix_func(M, exp)
    [exp(3), -5*exp(-3)/3 + 5*exp(3)/3, -5*exp(3) + 5*exp(-3)]
    [     0,                    exp(3),                     0]
    [     0,     -exp(-3)/3 + exp(3)/3,               exp(-3)]

    Note that for the function exp, we can also just use M.exp()

    >>> matrix_func(M, exp) == M.exp()
    True

    But for other functions, we have to do it this way.

    >>> M.sin()
    Traceback (most recent call last):
    ...
    AttributeError: Matrix has no attribute sin.
    >>> matrix_func(N, sin)
    [-sin(1), -2*sin(1),      0, 2*sin(1)]
    [-sin(1),   -sin(1), sin(2),   sin(1)]
    [      0,         0, sin(2),        0]
    [-sin(1), -2*sin(1), sin(2), 2*sin(1)]

    Note that we could also use this to compute the series expansion of a matrix,
    if we know the closed form of that expansion. For example, suppose we wanted to compute

    I + M + M**2 + M**3 + …

    The series

    1 + x + x**2 + x**3 + …

    is equal to the function 1/(1 - x).

    >>> matrix_func(M, Lambda(x, 1/(1 - x))) # Note, Lambda works just like lambda, but is symbolic
    [-1/2, -5/4, 15/4]
    [   0, -1/2,    0]
    [   0, -1/4,  1/4]
    """
    P, D = M.diagonalize()
    diags = [func(D[i, i]) for i in range(M.shape[0])]
    return P*diag(*diags)*P**-1
```

```
matrix_func(M, exp)
```

```
matrix_func(M, Lambda(x, 1/(1 - x)))
```

Now lets investigate how this works in relation to the series expansion definition. Write a function that uses `matrix_func` and `series` to compute the approximation of a matrix evaluated at a function up to $O(M^n)$.

```
def matrix_func_series(M, func, n):
    """
    Computes the approximation of the func(M) using the series definition up to O(M**n).

    >>> matrix_func_series(M, exp, 10)
    [22471/1120,  14953/448, -44859/448]
    [         0, 22471/1120,          0]
    [         0, 14953/2240,    83/2240]
    >>> matrix_func_series(M, exp, 10).evalf()
    [20.0633928571429, 33.3772321428571,  -100.131696428571]
    [               0, 20.0633928571429,                  0]
    [               0, 6.67544642857143, 0.0370535714285714]
    >>> matrix_func(M, exp).evalf()
    [20.0855369231877, 33.3929164246997,  -100.178749274099]
    [               0, 20.0855369231877,                  0]
    [               0, 6.67858328493993, 0.0497870683678639]

    It's pretty close. Basically what we might expect for those values up to O(x**10).

    >>> matrix_func_series(N, sin, 3)
    [-1, -2, 0, 2]
    [-1, -1, 2, 1]
    [ 0,  0, 2, 0]
    [-1, -2, 2, 2]
    >>> matrix_func(N, sin).evalf()
    [-0.841470984807897,  -1.68294196961579,                 0,  1.68294196961579]
    [-0.841470984807897, -0.841470984807897, 0.909297426825682, 0.841470984807897]
    [                 0,                  0, 0.909297426825682,                 0]
    [-0.841470984807897,  -1.68294196961579, 0.909297426825682,  1.68294196961579]

    It's not as close, because we used O(x**3), but clearly still the same thing.

    >>> matrix_func_series(M, Lambda(x, 1/(1 - x)), 10)
    [29524, 73810, -221430]
    [    0, 29524,       0]
    [    0, 14762,  -14762]
    >>> matrix_func(M, Lambda(x, 1/(1 - x)))
    [-1/2, -5/4, 15/4]
    [   0, -1/2,    0]
    [   0, -1/4,  1/4]

    Woah! That one's not close at all. What is happening here?  Let's try more terms

    >>> matrix_func_series(M, Lambda(x, 1/(1 - x)), 100)
    [257688760366005665518230564882810636351053761000, 644221900915014163795576412207026590877634402500, -1932665702745042491386729236621079772632903207500]
    [                                               0, 257688760366005665518230564882810636351053761000,                                                  0]
    [                                               0, 128844380183002832759115282441405318175526880500,  -128844380183002832759115282441405318175526880500]

    It just keeps getting bigger. In fact, the series diverges. Recall that
    1/(1 - x) = 1 + x + x**2 + x**3 + … *only if* |x| < 1.  But the eigenvalues
    of M are bigger than 1 in absolute value.

    >>> M.eigenvals()
    {3: 2, -3: 1}

    In fact, 1/(1 - M) is mathematically defined via the analytic continuation
    of the series expansion 1 + x + x**2 + …, which is just 1/(1 - x).  This is
    well-defined as long as none of the eigenvalues of M are equal to 1.  Let's
    try it on N.

    >>> matrix_func(N, Lambda(x, 1/(1 - x)))
    [nan, -oo, nan,  oo]
    [nan, nan, nan, nan]
    [nan, nan, nan, nan]
    [nan, -oo, nan,  oo]

    That didn't work. What are the eigenvalues of N?

    >>> N.eigenvals()
    {1: 1, 2: 1, -1: 1, 0: 1}

    Ah, the first one is 1, so we cannot define 1/(1 - N).
    """
    x = Dummy('x') # This works even if func already contains Symbol('x')
    series_func = Lambda(x, func(x).series(x, 0, n).removeO())
    return matrix_func(M, series_func)
```

```
matrix_func_series(M, exp, 10)
```

```
matrix_func_series(M, exp, 10).evalf()
```

```
matrix_func(M, exp).evalf()
```

```
matrix_func_series(N, sin, 3)
```

```
matrix_func_series(M, Lambda(x, 1/(1 - x)), 100)
```

```
M.eigenvals()
```

```
matrix_func(N, Lambda(x, 1/(1 - x)))
```

```
N.eigenvals()
```

```

```


```python
import numpy as np
np.random.seed(2017)

import torch
torch.manual_seed(2017)

from scipy.misc import logsumexp # Use it for reference checking implementation
```

```python
seq_length, num_states=4, 2
emissions = np.random.randint(20, size=(seq_length,num_states))*1.
transitions = np.random.randint(10, size=(num_states, num_states))*1.
print("Emissions:", emissions, sep="\n")
print("Transitions:", transitions, sep="\n")
```

    Emissions:
    [[  9.   6.]
     [ 13.  10.]
     [  8.  18.]
     [  3.  15.]]
    Transitions:
    [[ 7.  8.]
     [ 0.  8.]]

```python
def viterbi_decoding(emissions, transitions):
    # Use help from: https://github.com/tensorflow/tensorflow/blob/master/tensorflow/contrib/crf/python/ops/crf.py
    scores = np.zeros_like(emissions)
    back_pointers = np.zeros_like(emissions, dtype="int")
    scores = emissions[0]
    # Generate most likely scores and paths for each step in sequence
    for i in range(1, emissions.shape[0]):
        score_with_transition = np.expand_dims(scores, 1) + transitions
        scores = emissions[i] + score_with_transition.max(axis=0)
        back_pointers[i] = np.argmax(score_with_transition, 0)
    # Generate the most likely path
    viterbi = [np.argmax(scores)]
    for bp in reversed(back_pointers[1:]):
        viterbi.append(bp[viterbi[-1]])
    viterbi.reverse()
    viterbi_score = np.max(scores)
    return viterbi_score, viterbi
```

```python
viterbi_decoding(emissions, transitions)
```

    (78.0, [0, 0, 1, 1])

```python
def viterbi_decoding_torch(emissions, transitions):
    scores = torch.zeros(emissions.size(1))
    back_pointers = torch.zeros(emissions.size()).int()
    scores = scores + emissions[0]
    # Generate most likely scores and paths for each step in sequence
    for i in range(1, emissions.size(0)):
        scores_with_transitions = scores.unsqueeze(1).expand_as(transitions) + transitions
        max_scores, back_pointers[i] = torch.max(scores_with_transitions, 0)
        scores = emissions[i] + max_scores
    # Generate the most likely path
    viterbi = [scores.numpy().argmax()]
    back_pointers = back_pointers.numpy()
    for bp in reversed(back_pointers[1:]):
        viterbi.append(bp[viterbi[-1]])
    viterbi.reverse()
    viterbi_score = scores.numpy().max()
    return viterbi_score, viterbi

```

```python
viterbi_decoding_torch(torch.Tensor(emissions), torch.Tensor(transitions))
```

    (78.0, [0, 0, 1, 1])

```python
viterbi_decoding(emissions, transitions)
```

    (78.0, [0, 0, 1, 1])

```python
def log_sum_exp(vecs, axis=None, keepdims=False):
    ## Use help from: https://github.com/scipy/scipy/blob/v0.18.1/scipy/misc/common.py#L20-L140
    max_val = vecs.max(axis=axis, keepdims=True)
    vecs = vecs - max_val
    if not keepdims:
        max_val = max_val.squeeze(axis=axis)
    out_val = np.log(np.exp(vecs).sum(axis=axis, keepdims=keepdims))
    return max_val + out_val
```

```python
def score_sequence(emissions, transitions, tags):
    # Use help from: https://github.com/tensorflow/tensorflow/blob/master/tensorflow/contrib/crf/python/ops/crf.py
    score = emissions[0][tags[0]]
    for i, emission in enumerate(emissions[1:]):
        score = score + transitions[tags[i], tags[i+1]] + emission[tags[i+1]]
    return score
```

```python
score_sequence(emissions, transitions, [1,1,0,0])
```

    42.0

```python
correct_seq = [0, 0, 1, 1]
[transitions[correct_seq[i],correct_seq[i+1]] for i in range(len(correct_seq) -1)]
```

    [7.0, 8.0, 8.0]

```python
sum([transitions[correct_seq[i], correct_seq[i+1]] for i in range(len(correct_seq) -1)])
```

    23.0

```python
viterbi_decoding(emissions, transitions)
```

    (78.0, [0, 0, 1, 1])

```python
score_sequence(emissions, transitions, [0, 0, 1, 1])
```

    78.0

```python
def score_sequence_torch(emissions, transitions, tags):
    score = emissions[0][tags[0]]
    for i, emission in enumerate(emissions[1:]):
        score = score + transitions[tags[i], tags[i+1]] + emission[tags[i+1]]
    return score
```

```python
score_sequence_torch(torch.Tensor(emissions), torch.Tensor(transitions), [0, 0, 1, 1])
```

    78.0

```python
def get_all_tags(seq_length, num_labels):
    if seq_length == 0:
        yield []
        return
    for sequence in get_all_tags(seq_length-1, num_labels):
        #print(sequence, seq_length)
        for label in range(num_labels):
            yield [label] + sequence
list(get_all_tags(4,2))
```

    [[0, 0, 0, 0],
     [1, 0, 0, 0],
     [0, 1, 0, 0],
     [1, 1, 0, 0],
     [0, 0, 1, 0],
     [1, 0, 1, 0],
     [0, 1, 1, 0],
     [1, 1, 1, 0],
     [0, 0, 0, 1],
     [1, 0, 0, 1],
     [0, 1, 0, 1],
     [1, 1, 0, 1],
     [0, 0, 1, 1],
     [1, 0, 1, 1],
     [0, 1, 1, 1],
     [1, 1, 1, 1]]

```python
def get_all_tags_dp(seq_length, num_labels):
    prior_tags = [[]]
    for i in range(1, seq_length+1):
        new_tags = []
        for label in range(num_labels):
            for tags in prior_tags:
                new_tags.append([label] + tags)
        prior_tags = new_tags
    return new_tags
list(get_all_tags_dp(2,2))
```

    [[0, 0], [0, 1], [1, 0], [1, 1]]

```python
def brute_force_score(emissions, transitions):
    # This is for ensuring the correctness of the dynamic programming method.
    # DO NOT run with very high values of number of labels or sequence lengths
    for tags in get_all_tags_dp(*emissions.shape):
        yield score_sequence(emissions, transitions, tags)

brute_force_sequence_scores = list(brute_force_score(emissions, transitions))
print(brute_force_sequence_scores)
```

    [54.0, 67.0, 58.0, 78.0, 45.0, 58.0, 56.0, 76.0, 44.0, 57.0, 48.0, 68.0, 42.0, 55.0, 53.0, 73.0]

```python
max(brute_force_sequence_scores) # Best score calcuated using brute force
```

    78.0

```python
log_sum_exp(np.array(brute_force_sequence_scores)) # Partition function
```

    78.132899613126483

```python
def forward_algorithm_naive(emissions, transitions):
    scores = emissions[0]
    # Get the log sum exp score
    for i in range(1,emissions.shape[0]):
        print(scores)
        alphas_t = np.zeros_like(scores) # Forward vars at timestep t
        for j in range(emissions.shape[1]):
            emit_score = emissions[i,j]
            trans_score = transitions.T[j]
            next_tag_var = scores + trans_score
            alphas_t[j] = log_sum_exp(next_tag_var) + emit_score
        scores = alphas_t
    return log_sum_exp(scores)
```

```python
forward_algorithm_naive(emissions, transitions)
```

    [ 9.  6.]
    [ 29.0000454   27.04858735]
    [ 44.00017494  55.13288499]

    78.132899613126483

```python
def forward_algorithm_vec_check(emissions, transitions):
    # This is for checking the correctedness of log_sum_exp function compared to scipy
    scores = emissions[0]
    scores_naive = emissions[0]
    # Get the log sum exp score
    for i in range(1, emissions.shape[0]):
        print(scores, scores_naive)
        scores = emissions[i] + logsumexp(
            scores_naive + transitions.T,
            axis=1)
        scores_naive = emissions[i] + np.array([log_sum_exp(
            scores_naive + transitions.T[j]) for j in range(emissions.shape[1])])
    print(scores, scores_naive)
    return logsumexp(scores), log_sum_exp(scores_naive)
```

```python
forward_algorithm_vec_check(emissions, transitions)
```

    [ 9.  6.] [ 9.  6.]
    [ 29.0000454   27.04858735] [ 29.0000454   27.04858735]
    [ 44.00017494  55.13288499] [ 44.00017494  55.13288499]
    [ 58.14879707  78.13289961] [ 58.14879707  78.13289961]

    (78.132899613126483, 78.132899613126483)

```python
def forward_algorithm(emissions, transitions):
    scores = emissions[0]
    # Get the log sum exp score
    for i in range(1, emissions.shape[0]):
        scores = emissions[i] + log_sum_exp(
            scores + transitions.T,
            axis=1)
    return log_sum_exp(scores)
```

```python
forward_algorithm(emissions, transitions)
```

    78.132899613126483

```python
tt = torch.Tensor(emissions)
tt_max, _ = tt.max(1)
```

```python
tt_max.expand_as(tt)
```

      9   9
     13  13
     18  18
     15  15
    [torch.FloatTensor of size 4x2]

```python
tt.sum(0)
```

     33  49
    [torch.FloatTensor of size 1x2]

```python
tt.squeeze(0)
```

      9   6
     13  10
      8  18
      3  15
    [torch.FloatTensor of size 4x2]

```python
tt.transpose(-1,-2)
```

      9  13   8   3
      6  10  18  15
    [torch.FloatTensor of size 2x4]

```python
tt.ndimension()
```

    2

```python
def log_sum_exp_torch(vecs, axis=None):
    ## Use help from: http://pytorch.org/tutorials/beginner/nlp/advanced_tutorial.html#sphx-glr-beginner-nlp-advanced-tutorial-py
    if axis < 0:
        axis = vecs.ndimension()+axis
    max_val, _ = vecs.max(axis)
    vecs = vecs - max_val.expand_as(vecs)
    out_val = torch.log(torch.exp(vecs).sum(axis))
    #print(max_val, out_val)
    return max_val + out_val
```

```python
def forward_algorithm_torch(emissions, transitions):
    scores = emissions[0]
    # Get the log sum exp score
    transitions = transitions.transpose(-1,-2)
    for i in range(1, emissions.size(0)):
        scores = emissions[i] + log_sum_exp_torch(
            scores.expand_as(transitions) + transitions,
            axis=1)
    return log_sum_exp_torch(scores, axis=-1)
```

```python
forward_algorithm_torch(torch.Tensor(emissions), torch.Tensor(transitions))
```

     78.1329
    [torch.FloatTensor of size 1]

The core idea is to find the sequence of states $y = \{y_0, y_1, ..., y_N\}$ which have the highest probability given the input $X = \{X_0, X_1, ..., X_N\}$ as follows:

$$
\begin{equation}
p(y\mid X) = \prod_{i=0}^{N}{p(y_i\mid X_i)p(y_i \mid y_{i-1})}\\
\log{p(y\mid X)} = \sum_{i=0}^{N}{\log{p(y_i\mid X_i)} + \log{p(y_i \mid y_{i-1})}}\\
\end{equation}
$$

Now $\log{p(y_i\mid X_i)}$ and $\log{p(y_i \mid y_{i-1})}$ can be parameterized as follows:

$$
\begin{equation}
\log{p(y_i\mid X_i)} = \sum_{l=0}^{L}{\sum_{k=0}^{K}{w_{k}^{l}*\phi_{k}^{l}(X_i, y_i)}}\\
\log{p(y_i\mid y_{y-1})} = \sum_{l=0}^{L}{\sum_{l'=0}^{L}{w_{l'}^{l}*\psi_{l'}^{l}(y_i, y_{i-1})}}\\
\implies \log{p(y\mid X)} = \sum_{i=0}^{N}{(\sum_{l=0}^{L}{\sum_{k=0}^{K}{w_{k}^{l}*\phi_{k}^{l}(X_i, y_i)}}
+ \sum_{l=0}^{L}{\sum_{l'=0}^{L}{w_{l'}^{l}*\psi_{l'}^{l}(y_i, y_{i-1})}})}\\
\implies \log{p(y\mid X)} = \sum_{i=0}^{N}{(\Phi(X_i)W_{emission} + \log{p(y_{i-1} \mid X_{i-1})}W_{transition})}
\end{equation}
$$

Where,

* $N$ is the sequence length
* $K$ is number of feature functions,
* $L$ is number of states
* $W_{emission}$ is $K*L$ matrix
* $W_{transition}$ is $L*L$ matrix
* $\Phi(X_i)$ is a feature vector of shape $1*K$
* $(\Phi(X_i)W_{emission} + \log{p(y_{i-1} \mid X_{i-1})}W_{transition})$ gives the score for each label

```python

```