Exercise material of the MSc-level course **Numerical Methods in Geotechnical Engineering**.
Held at Technische Universität Bergakademie Freiberg.

Comments to:

*Prof. Dr. Thomas Nagel
Chair of Soil Mechanics and Foundation Engineering
Geotechnical Institute
Technische Universität Bergakademie Freiberg.*

https://tu-freiberg.de/en/soilmechanics

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

#Some plot settings
plt.style.use('seaborn-deep')
plt.rcParams['lines.linewidth']= 2.0
plt.rcParams['lines.color']= 'black'
plt.rcParams['legend.frameon']=True
plt.rcParams['font.family'] = 'serif'
plt.rcParams['legend.fontsize']=14
plt.rcParams['font.size'] = 14
plt.rcParams['axes.spines.right'] = False
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.spines.left'] = True
plt.rcParams['axes.spines.bottom'] = True
plt.rcParams['axes.axisbelow'] = True
plt.rcParams['figure.figsize'] = (12, 6)
```

# Exercise 6 - Linear FEM: soil column under gravity and top load

## Governing differential equation

Consider the equilibrium conditions on an infinitesimal element of the soil column:

$$
    \downarrow: \quad F_z + \frac{\partial F_z}{\partial z}\text{d}z - F_z + \varrho g A \text{d}z = 0
$$

The vertical force is determined by $F_z = \sigma_{zz}A = E_\text{s} A \epsilon_{zz} = -E_\text{s} A u_{z,z}$. Therefore, the equilibrium conditions read:

$$
    0 = \left[ \frac{\partial}{\partial z} \left(E_\text{s} A \frac{\partial u_z}{\partial z}\right) - \varrho g A \right]\text{d}z
$$

Considering, that the equation has to hold for an arbitrary infinitesimal non-zero $\text{d}z$, we find the ordinary differential equation

$$
    0 = \frac{\partial}{\partial z} \left(E_\text{s} A \frac{\partial u_z}{\partial z}\right) - \varrho g A
$$

Nota bene: compare this to the differential equation for a rod ('Zugstab'): $(EAu')' + n = 0$. While the cross section of a rod can vary along its length, the cross-sectional area of a soil column is considered a constant, simplifying the equation further:

$$
    0 = \frac{\partial}{\partial z} \left(E_\text{s} \frac{\partial u_z}{\partial z}\right) - \varrho g
$$

## Weak form

The displacements can have (the essential/Dirichlet) boundary conditions in the form:

$$
    u = \bar{u}\ \forall z \in \partial \Omega_\mathrm{D}% \qquad \text{and} \qquad - E_\text{s}A \frac{\partial u_z}{\partial z} = \bar{F} \ \forall z \in \partial \Omega_\mathrm{N}
$$

We now introduce a test function $\delta u$ (virtual displacement) which vanishes where the displacement is given

$$
    \delta u = 0\ \forall z \in \partial \Omega_\mathrm{D}
$$

and construct the weak form (using integration by parts):

\begin{align}
   0 &= \int \limits_0^H \left[\frac{\partial}{\partial z} \left(E_\text{s} \frac{\partial u_z}{\partial z}\right) - \varrho g\right] \delta u\, \text{d}z
   \\
   &= \int \limits_0^H \left[\frac{\partial}{\partial z} \left(E_\text{s} \frac{\partial u_z}{\partial z} \delta u \right) - \frac{\partial \delta u}{\partial z} E_\text{s} \frac{\partial u_z}{\partial z} - \varrho g \delta u \right] \, \text{d}z
\end{align}

Integrating the first term yields the natural/Neumann boundary conditions, so that
$$
\left[ \underbrace{E_\text{s} \frac{\partial u_z}{\partial z}}_{\displaystyle -\bar{\sigma}_z} \delta u \right]_0^H - \int \limits_0^H \varrho g \delta u \text{d}z = \int \limits_0^H \frac{\partial \delta u}{\partial z} E_\text{s} \frac{\partial u_z}{\partial z} \, \text{d}z
$$
which is recognised as the principal of virtual work: $\delta W_\text{ext} = \delta W_\text{int}$.

## Finite elements in 1D

We have a soil column of $H=20$ m on top of the bed rock at $z=0$.

We first create an element class. An element knows the number of nodes it has, their IDs in the global node vector, and the coordinates of its nodes. Linear elements have 2 nodes and 2 quadrature points, quadratic elements 3 nodes and 3 quadrature points. The natural coordinates of the element run from -1 to 1, and the quadrature points and weights are directly taken from Numpy.

```python
#element class
class line_element():#local coordinates go from -1 to 1
    #takes number of nodes, global nodal coordinates, global node ids
    def __init__(self, nnodes=2, ncoords=[0.,1.], nids=[0,1]):
        self.__nnodes = nnodes
        if (len(ncoords) != self.__nnodes):
            raise Exception("Number of coordinates does not match number \
                            of nodes of element (%i vs of %i)" %(self.__nnodes,len(ncoords)))
        else:
            self.__coords = np.array(ncoords)

        self.__natural_coords = (self.__coords-self.__coords[0])/(self.__coords[-1]-self.__coords[0])*2. - 1.

        if (len(nids) != self.__nnodes):
            raise Exception("Number of node IDs does not match number \
                            of nodes of element (%i vs of %i)" %(self.__nnodes,len(nids)))
        else:
            self.__global_ids = np.array(nids)
        self.__quad_degree = self.__nnodes
        self.__quad_points, self.__quad_weights = np.polynomial.legendre.leggauss(self.__quad_degree)

```

Next, we wish to generate a one-dimensional mesh by specifying the length of a line, the number of elements into which the mesh is to be split, and the number of nodes per element.

```python
def number_of_nodes(nelems,nodes_per_elem):
    return nelems*nodes_per_elem - (nelems - 1)

def generate_mesh(domain_length,nelems,nodes_per_elem):
    nn = number_of_nodes(nelems,nodes_per_elem)
    #coordinate vector of global nodes
    global_nodal_coordinates = np.linspace(0.,domain_length,nn)
    global_solution = np.array([0.]*nn)

    #generate elements
    element_vector = []
    for i in range(nelems):
        node_start = (nodes_per_elem-1)*i
        element_vector.append(
            line_element(nodes_per_elem,
                         global_nodal_coordinates[node_start:node_start+nodes_per_elem],
                         list(range(node_start,node_start+nodes_per_elem))))

    return global_nodal_coordinates, element_vector, global_solution
```

Let's put this to test.

```python
number_of_elements = 30
L = 10.
nodes_per_element = 3

nodes,elements,solution=generate_mesh(L,number_of_elements,nodes_per_element)
```

```python
plt.xlabel('$z$')
plt.ylabel('element ID')
plt.title('Finite element mesh')
for i,elem in enumerate(elements):
    if (i==0):
        plt.plot(elem._line_element__coords,[i]*elem._line_element__nnodes, 'bs-', label='elements')
    else:
        plt.plot(elem._line_element__coords,[i]*elem._line_element__nnodes, 'bs-')
plt.plot(nodes, [0]*len(nodes), 'ro ', label='nodes')
plt.legend();
```

## Shape functions in 1D

Now we have a mesh consisting of a vector of elements, and a global node vector. The next ingredient required are the shape functions for the elements in order to interpolate variables and determine gradients.

### Linear elements

Linear elements have two nodes with $\xi_1 = -1$ and $\xi_2 = 1$. The linear shape functions are

$$
    N_1(\xi) = \frac{1-\xi}{2} \qquad \text{and} \qquad N_2(\xi) = \frac{1+\xi}{2}
$$

Their gradients in natural coordinates follow as

$$
    \frac{\text{d} N_1}{\text{d}\xi} = - \frac{1}{2} \qquad \text{and} \qquad \frac{\text{d} N_2}{\text{d}\xi} = \frac{1}{2}
$$

### Quadratic elements

Quadratic elements have three nodes with $\xi_1 = -1$, $\xi_2 = 0$ and $\xi_3 = 1$. The quadratic shape functions are

$$
    N_1(\xi) = \frac{\xi (\xi - 1)}{2}, \qquad N_2(\xi) = (1-\xi)(1+\xi) \qquad \text{and} \qquad N_3(\xi) = \frac{\xi(1+\xi)}{2}
$$

Their gradients in natural coordinates follow as

$$
    \frac{\text{d} N_1}{\text{d}\xi} = \xi - \frac{1}{2}, \qquad \frac{\text{d} N_2}{\text{d}\xi} = -2\xi \qquad \text{and} \qquad \frac{\text{d} N_3}{\text{d}\xi} = \xi + \frac{1}{2}
$$

### Jacobians and gradients

An unknown is approximated as $u \approx N_i \hat{u}_i$ where $i$ runs over all nodes of a given element and $\hat{u}_i$ are the nodal degrees of freedom. To peform a gradient calculation (here in 1D)

$$
    \text{grad} u = \frac{\text{d} u}{\text{d} z} = \frac{\text{d}N_i}{\text{d} z} \hat{u}_i = \frac{\text{d}N_i}{\text{d} \xi} \frac{\text{d}\xi}{\text{d} z} \hat{u}_i = \frac{\text{d}N_i}{\text{d} \xi} J^{-1} \hat{u}_i
$$

The element Jacobian follows from the isoparametric coordinate approximation $z \approx N_i \hat{z}_i$ where $\hat{z}_i$ are the nodal coordinates:

$$
    J = \frac{\text{d} z}{\text{d}\xi} = \frac{\text{d} N_i}{\text{d}\xi} \hat{z}_i
$$

We use the following shorthand:

$$
    \nabla N_i = \frac{\text{d}N_i}{\text{d} z} = \frac{\text{d}N_i}{\text{d} \xi} J^{-1}
$$

```python
#N
def shape_function(element_order,xi):
    if (element_order == 2): #-1,1
            return np.array([(1.-xi)/2., (1.+xi)/2.])
    elif (element_order == 3): #-1, 0, 1
            return np.array([(xi - 1.)*xi/2., (1-xi)*(1+xi), (1+xi)*xi/2.])
#dN_dxi
def dshape_function_dxi(element_order,xi):
    if (element_order == 2): #-1,1
        return np.array([-0.5*xi/xi, 0.5*xi/xi])  #xi only later for plotting dimensions
    elif (element_order == 3):#-1,0,1
        return np.array([xi - 0.5,-2.*xi,xi + 0.5])

#dz_dxi
def element_jacobian(element,xi):
    element_order = element._line_element__nnodes
    Jacobian = 0.
    Jacobian += dshape_function_dxi(element_order,xi).dot(element._line_element__coords)
    return Jacobian

#dN_dz
def grad_shape_function(element,xi):
    element_order = element._line_element__nnodes
    Jac = element_jacobian(element,xi)
    return dshape_function_dxi(element_order,xi)/Jac

```

Now let's plot the shape functions to see if they were implemented correctly.

```python
def plot_shape_functions(order):
    xi = np.linspace(-1.,1.,100)
    fig, ax = plt.subplots(ncols=2)
    ax[0].set_xlabel('$\\xi$')
    ax[0].set_ylabel('$N(\\xi)$')
    for i in range(order):
        ax[0].plot(xi,shape_function(order,xi)[i],label='$N_{%i}$' %i)
    ax[0].legend()
    ax[1].set_xlabel('$\\xi$')
    ax[1].set_ylabel('$\\mathrm{d}N(\\xi)/\\mathrm{d}\\xi$')
    for i in range(order):
        ax[1].plot(xi,dshape_function_dxi(order,xi)[i],label='$\\mathrm{d}N_{%i}/\\mathrm{d}\\xi$' %i)
    ax[1].legend()
    fig.tight_layout()
```

```python
plot_shape_functions(2)
```

```python
plot_shape_functions(3)
```

## Discretization of virtual work (weak form)

Now let's write the discretized version of the principal of virtual work in the following form:
$$
\left[ \bar{\sigma}_z \delta u \right]_0^H - \int \limits_0^H \varrho g \delta u \text{d}z = \int \limits_0^H \frac{\partial \delta u}{\partial z} E_\text{s} \frac{\partial u_z}{\partial z} \, \text{d}z
$$

using

$$
    u \approx N_i \hat{u}_i, \quad \delta u \approx N_i \delta \hat{u}_i, \quad \frac{\partial u_z}{\partial z} \approx \nabla N_i \hat{u}_i, \quad \frac{\partial \delta u_z}{\partial z} \approx \nabla N_i \delta \hat{u}_i
$$

This yields

$$
\begin{aligned}
    \int \limits_0^H \varrho g \delta u\, \text{d}z &= \int \limits_0^H \varrho g \delta u\, \text{d}z = \int \limits_0^H \varrho g N_i\, \text{d}z\ \delta \hat{u}_i= \bigcup \limits_{e=1}^{n_\text{el}} \int \limits_{z_e} \varrho g N_i \det J \, \text{d}\xi\ \delta \hat{u}_i
    \\
    \int \limits_0^H \frac{\partial \delta u}{\partial z} E_\text{s} \frac{\partial u_z}{\partial z} \, \text{d}z &=  \int \limits_0^H \nabla N_i E_\text{s} \nabla N_k  \, \text{d}z \ \hat{u}_k \delta \hat{u}_i = \bigcup \limits_{e=1}^{n_\text{el}} \int \limits_{z_e} \nabla N_i E_\text{s} \nabla N_k \det J  \, \text{d}\xi \ \hat{u}_k \delta \hat{u}_i
    \\
    \left[ \bar{\sigma}_z \delta u \right]_0^H &= \bar{\sigma}_z(z=H) N_{n_\text{n}}(\xi_{n_\text{n}}) \delta \hat{u}_{n_\text{n}} - \bar{\sigma}_z(z=0) N_{0}(\xi_0) \delta \hat{u}_0 = \left[ \bar{\sigma}_z(z=H) N_{n_\text{n}}(\xi_{n_\text{n}}) \delta_{in_\text{n}} - \bar{\sigma}_z(z=0) N_{0}(\xi_0) \delta_{i0} \right] \delta \hat{u}_i
\end{aligned}
$$

where in the last step we assumed that the node at $z=0$ has the node id $0$, while the node at $z=H$ has the node id $n_\text{n}$.

The discretized system

$$
    \left[ \bar{\sigma}_z(z=H) N_{n_\text{n}}(\xi_{n_\text{n}}) \delta_{in_\text{n}} - \bar{\sigma}_z(z=0) N_{0}(\xi_0) \delta_{i0} - \bigcup \limits_{e=1}^{n_\text{el}} \int \limits_{z_e} \varrho g N_i \det J \, \text{d}\xi \right] \delta \hat{u}_i = \bigcup \limits_{e=1}^{n_\text{el}} \int \limits_{z_e} \nabla N_i E_\text{s} \nabla N_k \det J  \, \text{d}\xi \ \hat{u}_k \delta \hat{u}_i
$$

can be simplified by realizing that the nodal virtual displacements are arbitrary and thus

$$
    \bar{\sigma}_z(z=H) N_{n_\text{n}}(\xi_{n_\text{n}}) \delta_{in_\text{n}} - \bar{\sigma}_z(z=0) N_{0}(\xi_0) \delta_{i0} - \bigcup \limits_{e=1}^{n_\text{el}} \int \limits_{z_e} \varrho g N_i \det J \, \text{d}\xi = \bigcup \limits_{e=1}^{n_\text{el}} \int \limits_{z_e} \nabla N_i E_\text{s} \nabla N_k \det J  \, \text{d}\xi \ \hat{u}_k
$$

which leaves us with $n_\text{n}$ equations for the $n_\text{n}$ unknown nodal displacements $\hat{u}_k$. This is the discretized force balance with

$$
    F_i^\text{surf} + F_i^\text{body} = K_{ik} \hat{u}_k
$$

What we require now is the local assembler to calculate the stiffness matrix and the local right-hand side. Local integration is performed by Gauss quadrature:

$$
    \int \limits_{-1}^1 f(\xi)\,\text{d}\xi \approx \sum \limits_{i=1}^{n_\text{gp}} f(\xi_i) w_i
$$

## Local assember

```python
def Stiffness(z):
    E0 = 1.e8 #Pa
    return E0

def Density(z):
    rho0 = 2600.*(1.-0.38) #kg/m³
    return rho0

def BodyForce(z):#N/m
    g = 9.81
    return g*Density(z)
```

```python
def local_assembler(elem):
    element_order = elem._line_element__nnodes
    K_loc = np.zeros((element_order,element_order))
    b_loc = np.zeros(element_order)
    z_nodes = elem._line_element__coords
    for i in range(elem._line_element__quad_degree):
        #local integration point coordinate
        xi = elem._line_element__quad_points[i]
        #shape function
        N = shape_function(element_order,xi)
        #gradient of shape function
        dN_dX = grad_shape_function(elem,xi)
        #determinant of Jacobian
        detJ = np.abs(element_jacobian(elem,xi))
        #integration weight
        w = elem._line_element__quad_weights[i]

        #global integration point coordinate (for spatially varying properties)
        z_glob = np.dot(N,z_nodes)
        #evaluation of local material/structural properties
        E = Stiffness(z_glob)
        #evaluation of local body force
        rho_g = BodyForce(z_glob)

        #assembly of local stiffness matrix
        K_loc += np.outer(dN_dX,dN_dX) * E * w * detJ

        #assembly of local RHS
        b_loc += N * rho_g * w * detJ
    return K_loc,b_loc
```

```python
local_assembler(elements[3])
```

    (array([[ 7.0e+08, -8.0e+08,  1.0e+08],
            [-8.0e+08,  1.6e+09, -8.0e+08],
            [ 1.0e+08, -8.0e+08,  7.0e+08]]),
     array([ 878.54, 3514.16,  878.54]))

## Global assembly

Now we can construct the global matrix system $\mathbf{K}\mathbf{u} = \mathbf{f}$ or $\mathbf{A}\mathbf{x}=\mathbf{b}$ (see lecture script).

```python
def global_assembler(nodes,elements,solution):
    K_glob = np.zeros((len(nodes),len(nodes)))
    b_glob = np.zeros(len(nodes))
    for i,elem in enumerate(elements):
        K_i, b_i = local_assembler(elem)
        start_id = elem._line_element__global_ids[0]
        end_id = elem._line_element__global_ids[-1]
        K_glob[start_id:end_id+1,start_id:end_id+1] += K_i
        b_glob[start_id:end_id+1] += b_i
    return K_glob, b_glob
```

## Application of boundary conditions

Now we apply the natural (Neumann, nodal force) and the essential (Dirichlet, nodal displacement) boundary conditions.

A nodal force $F_\text{bc}$ at node $m$ is simply added by
$$
    F^\text{ext}_i = \left(
        \begin{array}{c}
            F_0\\ F_1\\ \vdots\\ F_m \\ \vdots \\ F_{n_\text{n}}
        \end{array}
    \right)
     = F_i^\text{body} + F_\text{bc} \delta_{im} = F_i^\text{body} + F_\text{bc}
     \left(
        \begin{array}{c}
            0\\ 0\\ \vdots \\ 1 \\ \vdots \\ 0
        \end{array}
    \right)
$$

```python
def apply_Neumann_bc(b_glob,node_id,value):
    b_glob[node_id] += value
    return b_glob
```

A Dirichlet boundary condition $\bar{u}$ at node $m$ is introduced into the system $K_{ik} u_k = f_i$ via

$$
    \left(
    \begin{array}{c}
        K_{11} & K_{12} & \dots & K_{1m} & \dots & K_{1n_\text{n}}
        \\
        K_{21} & K_{22} & \dots & K_{2m} & \dots & K_{2n_\text{n}}
        \\
        \vdots & \vdots & \vdots & \vdots & \vdots & \vdots
        \\
        K_{m1}:=0 & K_{m2}:=0 & \dots & K_{mm}:=1 & \dots & K_{mn_\text{n}}:=0
        \\
        \vdots & \vdots & \vdots & \vdots & \vdots & \vdots
        \\
        K_{n_\text{n}1} & K_{n_\text{n}2} & \dots & K_{n_\text{n}m} & \dots & K_{n_\text{n}n_\text{n}}
    \end{array}
    \right)
    \left(
    \begin{array}{c}
            u_0\\ u_1\\ \vdots\\ u_m \\ \vdots \\ u_{n_\text{n}}
        \end{array}
    \right)
    =
    \left(
    \begin{array}{c}
            F_0\\ F_1\\ \vdots\\ F_m:=\bar{u} \\ \vdots \\ F_{n_\text{n}}
        \end{array}
    \right)
$$

```python
def apply_Dirichlet_bc(K_glob,b_glob,node_id,value):
    K_glob[node_id,:] = 0.# = K_glob[:,node_id] = 0.
    K_glob[node_id,node_id] = 1.
    b_glob[node_id] = value
    return K_glob, b_glob
```

## Problem solution

We now perform the global assembly, apply a vanishing traction on the top and constrain the displacement at the bottom to zero.

```python
number_of_elements = 20
L = 10.
nodes_per_element = 3

nodes,elements,solution=generate_mesh(L,number_of_elements,nodes_per_element)
```

```python
K, f = global_assembler(nodes,elements,solution)
plt.spy(K,marker='.')
plt.title('sparsity pattern')
plt.tight_layout()
```

```python
f = apply_Neumann_bc(f,len(nodes)-1,0)
```

```python
K, f = apply_Dirichlet_bc(K, f, 0, 0.)
```

```python
solution = np.linalg.solve(K,f)
```

```python
plt.xlabel('$x$')
plt.ylabel('$u$ / mm')
plt.title('Finite element solution')
plt.plot(nodes, solution*1e3, 'ro-', label='$u$')
plt.legend();
```

### Convergence study

Let's do a simple convergence study.

```python
plt.xlabel('$x$')
plt.ylabel('$u$ / mm')
plt.title('Mesh convergence')
for i in [3,5,11]:
    number_of_elements = i
    nodes_per_element = 2
    nodes,elements,solution=generate_mesh(L,number_of_elements,nodes_per_element)
    K, f = global_assembler(nodes,elements,solution)
    f = apply_Neumann_bc(f,len(nodes)-1,0)
    K, f = apply_Dirichlet_bc(K, f, 0, 0.)
    solution = np.linalg.solve(K,f)
    plt.plot(nodes, solution*1e3, marker = 'o', label='%i elements' %i)
plt.legend();
```

That's quite boring. Let's make the case a little bit more interesting, by giving the stiffness a variation with depth and by applying a compressive traction at the surface.

```python
Stiffness = lambda x: 1.e8+0.8e8*np.sin(x) #Pa
sig_top = - 300e3#Pa
```

```python
fig, ax = plt.subplots(ncols=2)
ax[0].set_xlabel('$x$')
ax[1].set_xlabel('$x$')
ax[0].set_ylabel('$u$ / mm')
ax[1].set_ylabel('$E$ / GPa')
nodes_per_element = 2
finest = 1000
for i in [finest,5,10,20]:
    number_of_elements = i
    nodes,elements,solution=generate_mesh(L,number_of_elements,nodes_per_element)
    K, f = global_assembler(nodes,elements,solution)
    f = apply_Neumann_bc(f,len(nodes)-1,sig_top)
    K, f = apply_Dirichlet_bc(K, f, 0, 0.)
    solution = np.linalg.solve(K,f)
    if (i == finest):
        ax[1].plot(nodes, Stiffness(nodes)/1e9, label='reference',color='black')
        ax[0].plot(nodes, solution*1e3, label='reference',color='black')
    else:
        ax[1].plot(nodes, Stiffness(nodes)/1e9, marker = 'o', label='%i elements' %i)
        ax[0].plot(nodes, solution*1e3, marker = 'o', label='%i elements' %i)
ax[0].legend();
ax[1].legend();
fig.tight_layout()
```

Something similar can be done to demonstrate the effect of higher-order elements.

```python
def run_sim(npe,ne):
    nodes,elements,solution=generate_mesh(L,ne,npe)
    K, f = global_assembler(nodes,elements,solution)
    f = apply_Neumann_bc(f,len(nodes)-1,sig_top)
    K, f = apply_Dirichlet_bc(K, f, 0, 0.)
    solution = np.linalg.solve(K,f)
    return nodes, solution, K
```

```python
fig, ax = plt.subplots(ncols=2)
ax[0].set_xlabel('$x$')
ax[1].set_xlabel('$x$')
ax[0].set_ylabel('$u$ / mm')
ax[1].set_ylabel('$E$ / GPa')
#reference
nodes, solution, matrix = run_sim(2,1000)
ax[0].plot(nodes, solution*1e3, label='reference',color='black')
ax[1].plot(nodes, Stiffness(nodes)/1e9, label='reference',color='black')
#10 linear elements
nodes, solution, matrix = run_sim(2,6)
ax[0].plot(nodes, solution*1e3, label='6 elements with 2 nodes',marker='o')
ax[1].plot(nodes, Stiffness(nodes)/1e9, label='6 elements with 2 nodes',marker='o')
#20 linear elements
nodes, solution, matrix = run_sim(2,12)
ax[0].plot(nodes, solution*1e3, label='12 elements with 2 nodes',marker='o')
ax[1].plot(nodes, Stiffness(nodes)/1e9, label='12 elements with 2 nodes',marker='o')
#10 quadratic elements
nodes, solution, matrix = run_sim(3,6)
ax[0].plot(nodes, solution*1e3, label='6 elements with 3 nodes',marker='o')
ax[1].plot(nodes, Stiffness(nodes)/1e9, label='6 elements with 3 nodes',marker='o')

ax[0].legend()
fig.tight_layout();
```

We see that with the same number of nodes, the higher-order approximation is better. *Note* that the lines connecting the nodal values are linear here, whereas the actual approximation between the nodes of one element is quadratic and thus smoother than shown here!

## Interactive playground

```python
from ipywidgets import widgets
from ipywidgets import interact
#Compute reference solution with 100 cells
nodes_ref, solution_ref, matrix_ref = run_sim(2,1000)

@interact(num_nodes=widgets.IntSlider(min=3, max=41, value=9, step=2, description='nodes'),
         npes=widgets.RadioButtons(options=[2,3], value=2, description='element order',))
def plot(num_nodes=5,npes=2):
    fig,ax = plt.subplots(ncols=2)
    ax[0].set_xlabel('$x$')
    ax[0].set_ylabel('$u$ / mm')
    num_cells = int((num_nodes - 1)/(npes - 1))
    x, u, matrix = run_sim(npes,num_cells)
    ax[0].plot(nodes_ref, solution_ref*1e3, '--', color='k', label='reference solution');
    ax[0].plot(x, u*1e3, 'o-', label=str(len(x)) + ' nodes');
    ax[0].legend(loc='lower left')
    ax[1].spy(matrix,marker='.')
    plt.show()
```

    interactive(children=(IntSlider(value=9, description='nodes', max=41, min=3, step=2), RadioButtons(description…

```python

```


.. meta::
   :description: A guide which introduces the most important steps to get started with pymoo, an open-source multi-objective optimization framework in Python.

.. meta::
   :keywords: Multi-Criteria Decision Making, Multi-objective Optimization, Python, Evolutionary Computation, Optimization Test Problem

```python
%%capture
%run ./part_2.ipynb
```

# Part III: Multi-Criteria Decision Making

Having now obtained a set of non-dominated solutions, one can ask how a decision-maker can nail down the set to only a few or even a single solution. This decision-making process for multi-objective problems is also known as Multi-Criteria Decision Making (MCDM). You should know that the main focus of *pymoo* lies in the optimization, not the MCDM part. However, the framework offers some rudimentary tools to find an appropriate solution.

The Pareto-optimal solutions obtained from the optimization procedure are given by:

```python
F = res.F
xl, xu = problem.bounds()
plt.figure(figsize=(7, 5))
plt.scatter(F[:, 0], F[:, 1], s=30, facecolors='none', edgecolors='blue')
plt.title("Objective Space")
plt.show()
```

Before we start using any technique, we should note that the objectives have a different scale. What has not been a problem for single-objective optimization because not more than one dimension existed now becomes fundamentally important to consider.

```python
fl = F.min(axis=0)
fu = F.max(axis=0)
print(f"Scale f1: [{fl[0]}, {fu[0]}]")
print(f"Scale f2: [{fl[1]}, {fu[1]}]")
```

    Scale f1: [1.3377795039158837, 74.97223429467643]
    Scale f2: [0.01809179532919018, 0.7831767823138299]

As one can observe, the lower and upper bounds of the objectives $f_1$ and $f_2$ are very different, and such normalization is required.

A common way is normalizing using the so-called ideal and nadir point. However, for the decision-making purpose here and the sake of generalization, we assume the ideal and nadir points (also referred to as boundary points) and the Pareto-front) are not known.
Thus the points can be approximated by:

```python
approx_ideal = F.min(axis=0)
approx_nadir = F.max(axis=0)
```

```python
plt.figure(figsize=(7, 5))
plt.scatter(F[:, 0], F[:, 1], s=30, facecolors='none', edgecolors='blue')
plt.scatter(approx_ideal[0], approx_ideal[1], facecolors='none', edgecolors='red', marker="*", s=100, label="Ideal Point (Approx)")
plt.scatter(approx_nadir[0], approx_nadir[1], facecolors='none', edgecolors='black', marker="p", s=100, label="Nadir Point (Approx)")
plt.title("Objective Space")
plt.legend()
plt.show()
```

Normalizing the obtained objective values regarding the boundary points is relatively simple by:

```python
nF = (F - approx_ideal) / (approx_nadir - approx_ideal)

fl = nF.min(axis=0)
fu = nF.max(axis=0)
print(f"Scale f1: [{fl[0]}, {fu[0]}]")
print(f"Scale f2: [{fl[1]}, {fu[1]}]")

plt.figure(figsize=(7, 5))
plt.scatter(nF[:, 0], nF[:, 1], s=30, facecolors='none', edgecolors='blue')
plt.title("Objective Space")
plt.show()
```

### Compromise Programming

Without going into too much detail in this getting started guide, one way for decision-making is using decomposition functions. They require the definition of weights that reflect the user's wishes. A vector gives the weights with only positive float numbers summing up to one and a length equal to the number of objectives.

Here for a bi-objective problem, let us assume the first objective is less a bit less important than the second objective by setting the weights to

```python
weights = np.array([0.2, 0.8])
```

Next, we choose the decomposition method called Augmented Scalarization Function (ASF), a well-known metric in the multi-objective optimization literature.

```python
from pymoo.decomposition.asf import ASF

decomp = ASF()
```

Now let us obtain the best solution regarding the ASF. Because ASF is supposed to be minimized, we choose the minimum ASF values calculated from all solutions. You might be wondering why the weights are not passed directly, but `1/weights`. For ASF, different formulations exist, one where the values are divided and one where they are multiplied. In *pymoo*, we divide, which does not reflect the idea of the user's criteria. Thus, the inverse needs to be applied. No worries if this is too much detail for now; however, decision-making about decomposition techniques is vital.

```python
i = decomp.do(nF, 1/weights).argmin()
```

After having found a solution ($i$) we can operate on the original scale to represent the results:

```python
print("Best regarding ASF: Point \ni = %s\nF = %s" % (i, F[i]))

plt.figure(figsize=(7, 5))
plt.scatter(F[:, 0], F[:, 1], s=30, facecolors='none', edgecolors='blue')
plt.scatter(F[i, 0], F[i, 1], marker="x", color="red", s=200)
plt.title("Objective Space")
plt.show()
```

### Pseudo-Weights

A simple way to chose a solution out of a solution set in the context of multi-objective optimization is the pseudo-weight vector approach proposed in <cite data-cite="multi_objective_book"></cite>. Respectively, the pseudo weight $w_i$ for the i-ith objective function can be calculated by:

\begin{equation}
w_i = \frac{(f_i^{max} - f_i {(x)}) \, /\,  (f_i^{max} - f_i^{min})}{\sum_{m=1}^M (f_m^{max} - f_m (x)) \, /\,  (f_m^{max} - f_m^{min})}
\end{equation}

This equation calculates the normalized distance to the worst solution regarding each objective $i$. Please note that for non-convex Pareto fronts, the pseudo weight does not correspond to the result of an optimization using the weighted sum. However, for convex Pareto-fronts, the pseudo weights indicate the location in the objective space.

```python
from pymoo.mcdm.pseudo_weights import PseudoWeights

i = PseudoWeights(weights).do(nF)
```

```python
print("Best regarding Pseudo Weights: Point \ni = %s\nF = %s" % (i, F[i]))

plt.figure(figsize=(7, 5))
plt.scatter(F[:, 0], F[:, 1], s=30, facecolors='none', edgecolors='blue')
plt.scatter(F[i, 0], F[i, 1], marker="x", color="red", s=200)
plt.title("Objective Space")
plt.show()
```


# Modeling Stock Movement
Brian Bahmanyar

***

```python
import numpy as np
import pandas as pd
import scipy.optimize as opt
import seaborn as sns

import sys
sys.path.append('./src/')
from plots import *
```

```python
%matplotlib inline
```

```python
tech = pd.read_csv('data/tech_bundle.csv', index_col=0)
tech.index = pd.to_datetime(tech.index)
```

Recall neither Facebook's and Apple's stock had an obvious autocorrelation structure in their adjusted daily close prices. Thus we conclude that these series follow a random walk.

It can be shown that the following holds: $\frac{y_t}{y_{t-1}} \sim LogN(\mu, \sigma^2)$, where $y_t$ represents the adjusted close price of a stock.

Notice below when we plot $\frac{y_t}{y_{t-1}}$ (daily return ratio) for Facebook's historical stock price the distibution we observe can be approximated by a log-normal distribution.
> We prefer a log-normal to a normal distribution because the daily return ratio can never be negative

```python
def get_daily_return_ratio(series):
    """
    Args: series (ndarray)----the time series to get the lagged return ratios
          window_size (int)---number of trading weeks to use
    Return: (ndarray) y(t)/y(t-1) over the weeks specified
    """

    return (series[1:]/series[:-1])
```

```python
assert(tech.FB.count()-1 == len(get_daily_return_ratio(tech.FB.values)))
assert(tech.FB[1]/tech.FB[0] == get_daily_return_ratio(tech.FB.values)[0])
```

```python
plt.figure(figsize=(10,3));
plt.title("Facebook's Daily Return Ratio");
plt.xlabel('Return Ratio');
plt.ylabel('Count');
sns.distplot(get_daily_return_ratio(tech['FB'].values), kde=False);
```

We will use Maximum Likelyhood Estimation to estimate the parameters of the distribution.

```python
def neg_log_llh(theta, data):
    """
    Args theta (list)-----the params [mu, sigma**2]
         data (ndarray)---data points to be fit by log normal distribution
    Return: (double) negative log-likelihood for the log normal.
    """

    mu, sigma = theta[0], np.sqrt(theta[1])
    neg_log_llhs = np.log(data*sigma*np.sqrt(2*np.pi)) + ((np.log(data)-mu)**2/(2*(sigma**2)))

    return neg_log_llhs.sum()
```

```python
def mle_log_norm(data, init_theta=[1,1]):
    """
    Args: theta (list)-----the params [mu, sigma**2]
          data (ndarray)---data points to be fit by log normal distribution
    Return: (list) mu, sigma**2 for fitted log normal params
    """

    fit = opt.minimize(neg_log_llh, init_theta, data, method='Nelder-Mead')

    return fit.x
```

```python
fit = mle_log_norm(get_daily_return_ratio(tech['FB'].values))
fit
```

    array([ 0.00173078,  0.00054006])

This tells us for Facebook: $\frac{y_t}{y_{t-1}} \sim LogN(0.0017, 0.00054)$

Lets vizualize what this theoretical distribution looks like:

```python
plt.figure(figsize=(10,3));
plt.title(r'$LogN(0.0017, 0.00054)$');
plt.xlabel('Return Ratio');
plt.ylabel('Count');
sns.distplot(np.random.lognormal(fit[0], np.sqrt(fit[1]), size=1000000), hist=False);
```

Now we should be convinced that daily return ratios follow a log-normal distribution we can exploit this to forcast future days prices with a log normal random walk.

Quite simply if $\quad \frac{y_t}{y_{t-1}} \sim LogN(0.0017, 0.00054) \quad\Rightarrow\quad y_{t+1} \sim y_{t}*LogN(0.0017, 0.00054) $

### Modeling Facebook's Daily Adjusted Close Prices

```python
plot_stocks(tech.index, [tech['FB']], ['FB'], label_annually=False)
```

Lets simulate this random walk and see what the movement looks like.

```python
def simulate_random_walk(series, window_size=10, ahead=50):
    """
    Args: series (ndarray)----time series to use
          window_size (int)---number of trading weeks to use for log normal estimation
          ahead (int)---------how many days do you want to forcast head for
    Return (ndarray): simulated future price
    """

    forcasts = np.zeros(ahead)
    window = series[-((window_size*5)+1):].values

    for step in range(ahead):
        mu, sigma2 = mle_log_norm( get_daily_return_ratio(window) )
        forcast = window[-1]*np.random.lognormal(mu, np.sqrt(sigma2), 1)
        forcasts[step] = forcast
        window = np.roll(window, -1)
        window[-1] = forcast

    return forcasts
```

```python
def plot_simulated_forcast(series, window, ahead, train_on=None, n=10):
    """
    Plots Simulated Forcast and Original Series

    Args: series (ndarray)---time series for which to forcast
          window (int)-------window size for MLE of log normal
          ahead (int)--------number of days to forcast for
          train_on (int)-----optional, number of days from series to use to forcast
          n (int)------------number of simulations to plot
    Returns: None, plots inline
    """

    if not train_on:
        train_on = series.count()

    train_x_space = np.arange(0,series.count())
    test_x_space = np.arange(train_on, train_on+ahead)

    plt.figure(figsize=(15,6));
    plt.plot(train_x_space, series)
    for _ in range(n):
        walk = simulate_random_walk(series[:train_on], window, ahead)
        plt.plot(test_x_space, walk, linestyle='-', color='salmon', alpha=0.7)

    plt.xlabel('Time Index (Days)');
    plt.ylabel('Adjusted Close Price');
    plt.title('Simulated Log Normal Random Walk Forcast');
```

Here we are using data from the first 650 prices and a daily return window of 100 days to predict Facebook's price 135 days into the future. We can notice a general positive slope in all our predictions (in pink). However when we approximate our forcast we will want to employ some math to calculate an expected value and variance at each step.

```python
plot_simulated_forcast(tech['FB'], window=100, ahead=135, train_on=650, n=10);
```

Simulation is great but what we really want is the expected value and variance of the process and any time $t+k$.

##### Deriving the k-step ahead expected value

$$
\begin{align}
\hat{y_{t+k}} &= y_{t+(k-1)} \cdot \mathrm{E}[LogN(\mu, \sigma^2)]  \\
              &= y_{t} \cdot \mathrm{E}[LogN(\mu, \sigma^2)]^k \\
              &= y_{t} \cdot {(e^{\mu+\sigma^2/2})}^k
\end{align}
$$

The expected value of a log-normal: $\mathrm{E}[LogN(\mu, \sigma^2)] = e^{\mu+\sigma^2/2}$ as a python function below

```python
def get_expected_value(mu, sigma2):
    """
    Returns expected value for log normal with params mu, sigma2

    Args: mu (float)-------parameter mu
          sigma2 (float)---parameter sigma squared
    Returns: expected value (float) of the log normal
    """

    return (np.exp((mu+sigma2)/2))
```

The variance of a log normal: $\mathrm{Var}(LogN(\mu, \sigma^2)) = (e^{\sigma^2}\!\!-1) e^{2\mu+\sigma^2}$ as a python function below

```python
def get_variance(mu, sigma2):
    """
    Returns variance for log normal with params mu, sigma2

    Args: mu (float)-------parameter mu
          sigma2 (float)---parameter sigma squared
    Returns: variance (float) of the log normal
    """

    return (np.exp(sigma2)-1)*np.exp((2*mu)+sigma2)
```

**Deriving the k-step ahead variance**

$$
\begin{aligned}
\mathrm{V}[y_{t+k}|y_t] &= \mathrm{E}[y^2_{t+k}|y_t] - \mathrm{E}[y_{t+k}|y_t]^2 && \textit{by definition of Variance} \\
                        &= y^2_t \cdot \mathrm{E}\big[Z^2_{t+1}\big]^k - y^2_t \cdot \mathrm{E}\big[Z_{t+1}\big]^{2k} && \textit{let } Z \sim \mathrm{E}[LogN(\mu, \sigma^2)]\\
                        &= y^2_t \cdot \Big(\mathrm{E}\big[Z^2_{t+1}\big]^k - \mathrm{E}\big[Z_{t+1}\big]^{2k}\Big) \\
                        &= y^2_t \cdot \Big[\Big(\mathrm{V}\big[Z_{t+1}\big] + \mathrm{E}\big[Z_{t+1}\big]^{2}\Big)^k - \cdot \mathrm{E}\big[Z_{t+1}\big]^{2k}\Big]
\end{aligned}
$$

```python
def get_k_step_variance(mu, sigma2, k, yt):
    """
    Returns a k step ahead variance for log normal with params mu, sigma2

    Args: mu (float)-------parameter mu
          sigma2 (float)---parameter sigma squared
          k (int)----------number of steps ahead
          yt (float)-------price at time t (end of sample)
    Returns: variance (float) of the log normal
    """

    return (yt**2) * ((get_variance(mu, sigma2) + get_expected_value(mu, sigma2)**2)**k - (get_expected_value(mu, sigma2)**2)**k)
```

We can now use these functions to create an expected forcast.

```python
def get_expected_walk(series, window_size=10, ahead=50):
    """
    Args: series (ndarray)----time series to use
          window_size (int)---number of trading weeks to use for log normal estimation
          ahead (int)---------how many days do you want to forcast head for
    Return (ndarray): simulated future price
    """
    E_Xs = np.zeros(ahead)
    V_Xs = np.zeros(ahead)
    yt = series[-1]
    last_price = series[-1]
    window = series[-((window_size*5)+1):].values

    for step in range(ahead):
        mu, sigma2 = mle_log_norm( get_daily_return_ratio(window) )
        E_Xs[step] = last_price * get_expected_value(mu, sigma2)
        V_Xs[step] = get_k_step_variance(mu, sigma2, step+1, yt)
        last_price = last_price * get_expected_value(mu, sigma2)

    return E_Xs, V_Xs
```

```python
def plot_expected_forcast(series, window, ahead, train_on=None, error=2):
    """
    Plots Expected Forcast and Original Series

    Args: series (ndarray)---time series for which to forcast
          window (int)-------window size for MLE of log normal
          ahead (int)--------number of days to forcast for
          train_on (int)-----optional, number of days from series to use to forcast
          error (int)--------number of standard devations to use for confidence bands
    Returns: None, plots inline
    """

    if not train_on:
        train_on = series.count()

    mu, sigma2 = get_expected_walk(series[:train_on], window, ahead)
    low_bound = mu-(error*np.sqrt(sigma2))
    high_bound = mu+(error*np.sqrt(sigma2))

    train_x_space = np.arange(0,series.count())
    test_x_space = np.arange(train_on, train_on+ahead)

    plt.figure(figsize=(15,6));
    plt.plot(train_x_space, series);
    plt.plot(test_x_space, mu, 'maroon');
    plt.plot(test_x_space, high_bound, 'olive');
    plt.plot(test_x_space, low_bound, 'olive');
    plt.fill_between(test_x_space, low_bound, high_bound, alpha=0.2, color='y')

    plt.xlabel('Time Index (Days)');
    plt.ylabel('Adjusted Close Price');
    plt.title('Expected Log Normal Random Walk Forcast');
```

Below we use the equations above to create an expected forcast using the first 635 data points and a return ratio window of 30 days. We can see that the forcast is doing a great job. The yellow are the 95% confidence interval bands which incease as we get farther from our sample.

```python
plot_expected_forcast(tech.FB, 30, 150, 635, error=2)
```

#### Please ignore, this was an ARMA(p,d,q) I chose to remove

```python
# def acf_objective(order, series):
#     try:
#         model = arima_model.ARIMA(series, order).fit()
#         return model.aic
#     except:
#         return np.inf # if the model is not stationary return inf
```

```python
# def get_optimal_arima(series, cost):

#     grid = (slice(0, 4, 1), slice(1,2,1), slice(0, 4, 1))
#     orders = brute(cost, grid, args=(series,), finish=None)
#     orders = [int(order) for order in orders]

#     arima = arima_model.ARIMA(series, order=orders).fit()
#     print({'p':orders[0], 'd':orders[1], 'q':orders[2]})

#     return arima
```

```python
# def fit_arima(series, p, d, q):
#     model = arima_model.ARIMA(series, [p,d,q], method='css').fit()
#     return model.aic
```

```python
# fit = opt.minimize(hat, -2, (a, b))
```

```python
# fig, ax = plt.subplots(figsize=(10,5))
# ax.plot(np.arange(1,787), tech.FB.values)
# fig = arima.plot_predict(649, 785, dynamic=True, ax=ax,
#                          plot_insample=False)
```

___

```python
from IPython.display import HTML
HTML("""<style>@import "http://fonts.googleapis.com/css?family=Lato|Source+Code+Pro|Montserrat:400,700";@font-face{font-family:"Computer Modern";src:url('http://mirrors.ctan.org/fonts/cm-unicode/fonts/otf/cmunss.otf')}.rendered_html h1,h2,h3,h4,h5,h6,p{font-family:'Computer Modern'}p,ul{font-family:'Computer Modern'}div#notebook-container{-webkit-box-shadow:none;box-shadow:none}h1{font-size:70pt}h2{font-size:50pt}h3{font-size:40pt}h4{font-size:35pt}h5{font-size:30pt}h6{font-size:25pt}.rendered_html p{font-size:11pt;line-height:14pt}.CodeMirror pre{font-family:'Source Code Pro', monospace;font-size:09pt}div.input_area{border:none;background:#f5f5f5}ul{font-size:10.5pt;font-family:'Computer Modern'}ol{font-size:11pt;font-family:'Computer Modern'}.output_png img{display:block;margin-left:auto;margin-right:auto}</style>""")
```

<style>@import "http://fonts.googleapis.com/css?family=Lato|Source+Code+Pro|Montserrat:400,700";@font-face{font-family:"Computer Modern";src:url('http://mirrors.ctan.org/fonts/cm-unicode/fonts/otf/cmunss.otf')}.rendered_html h1,h2,h3,h4,h5,h6,p{font-family:'Computer Modern'}p,ul{font-family:'Computer Modern'}div#notebook-container{-webkit-box-shadow:none;box-shadow:none}h1{font-size:70pt}h2{font-size:50pt}h3{font-size:40pt}h4{font-size:35pt}h5{font-size:30pt}h6{font-size:25pt}.rendered_html p{font-size:11pt;line-height:14pt}.CodeMirror pre{font-family:'Source Code Pro', monospace;font-size:09pt}div.input_area{border:none;background:#f5f5f5}ul{font-size:10.5pt;font-family:'Computer Modern'}ol{font-size:11pt;font-family:'Computer Modern'}.output_png img{display:block;margin-left:auto;margin-right:auto}</style>


# Time evolution of the entanglement entroy of a product state

This project is part of the Computational Physics course held at Perimeter, Fall 2018.

```julia
using LinearAlgebra
using Arpack
using Plots
```

The following functions are used to compute the entanglement entropy, given a state separated in sub-states A and B.

```julia
function compute_rdmA(PsiAB)
    @assert ndims(PsiAB) == 2 # did we input a matrix?
    @assert size(PsiAB,1) < 2^13 # make sure the resulting matrix is not too large!
    rdm = PsiAB*PsiAB'
end

function compute_entropy(dm)
    p = eigvals(dm) # we diagonalize and use the eigenvalues. the more Julia way
    # has a problem when the log is very large
    entropy = 0.0
    #entropy = - p'*log2.(p) # problems when p[n] is of order 0 (possibly negative)
    for n in 1:size(p,1)
        if abs(p[n]) > 1e-12
            entropy = entropy - p[n]*log2(p[n])
        end
    end
    entropy
end
```

## Product state

We use the outer product of 1 spin states to construct a separable state:

\begin{equation}
| \Psi_{\text{sep}} > = |\Psi>_1 \otimes |\Psi>_2 \otimes \cdots \otimes |\Psi>_N
\end{equation}

```julia
N=2*5 # > 2

function normalizeVec(Vec)
    Vec = Vec/sqrt(abs(Vec'*Vec))
end

# here we implement the outer product
function separable(N)
    PsiInitial= reshape( (randn(2) + im*randn(2))*(randn(2) + im*randn(2))', 2^2 )
    for i in 3:N
        PsiInitial = reshape( PsiInitial*(randn(2) + im*randn(2))', 2^i )
    end
    return normalizeVec(PsiInitial)
end

PsiInitial = separable(N);
```

```julia
# checking separability
nA = Int(N/2) # number spins/qubits in A
nB = Int(N/2) # number of spins/qubits in B

PsiAB = reshape(PsiInitial, (2^(nA),2^(nB)))

rdmA = compute_rdmA(PsiAB)

isapprox(compute_entropy(rdmA), 0, atol=10^-14)  # checking that entropy is zero to 10^-14 accuracy
```

    true

## Time evolution

To obtain $|\Psi(t)\rangle \equiv \exp (-i t H) |\Psi(0)\rangle$, we can use
 $|\Psi(t+\delta t)\rangle = |\Psi(t)\rangle - i \delta t H |\Psi(t)\rangle$. We want to keep the accuracy within $10^{-5}$ at each time step.

We want the code to be able to deal with large spin chains, so we do not want to record the full Hamiltonian matrix.

```julia
# Ising model
function buildIsing(theta=pi/4) # Ising model with transverse magnetic field h (critical theta=pi/4 by default)
    X = [0. 1; 1 0]
    Z = [1. 0; 0 -1]
    E = diagm(0=>ones(2))
    XX = kron(X,X)
    HXX = XX
    HZ = kron(Z,E) + kron(E,Z)
    H2 = -(cos(theta)*XX + sin(theta)/2*HZ)
    return H2
end

# Energy shift
theta = pi/4 # critical magnetic field
H2 = buildIsing(theta) # Hamiltonian
D,U = eigen(H2)
shiftE = D[end]
H2 = H2 - shiftE*diagm(0=>ones(4))

# Sparse multiplication
function multiplyHPsi(Psi, H2)
    L = length(Psi)  # Dimension of the vector space
    N = convert(Int64,log2(L))  # Number of spins
    HPsi = zeros(L)
    for n=1:N                              # This multiplies by the Hamiltonian H
        Psi = reshape(Psi,(4,2^(N-2)))
        HPsi = reshape(HPsi,(4,2^(N-2)))
        HPsi += H2*Psi
        Psi = reshape(Psi,(2,2^(N-1)))
        HPsi = reshape(HPsi,(2,2^(N-1)))
        Psi = permutedims(Psi,(2,1))
        HPsi = permutedims(HPsi,(2,1))
    end
    Psi = reshape(Psi,L)
    HPsi = reshape(HPsi, L)
    return HPsi
end
```

    multiplyHPsi (generic function with 1 method)

```julia
# Initialization
init_steps = 1
entropy = zeros(1)
normdiff = zeros(1)
initial_step=1
Nsteps = 0;

N = 2 * 6
Psi = separable(N);
deltat = 0.0001
```

    0.0001

The for loop implements $|\Psi(t+\delta t)\rangle = |\Psi(t)\rangle - i \delta t H |\Psi(t)\rangle$.

```julia
# Loop over the time steps
initial_step += Nsteps
Nsteps = 80000  # change this number if the entanglement entropy has not reached its plateau
final_step = initial_step + Nsteps-1

for n=initial_step:final_step
    Psi = Psi .- im * deltat .* multiplyHPsi(Psi,H2)
    if n%400==0
    normdiff = [normdiff; abs(1 - sqrt(abs(Psi'*Psi)))]
    #Psi = normalizeVec(Psinew)
    PsiAB = reshape(Psi, (2^(Int(N/2)),2^(Int(N/2))))
    rdmA = compute_rdmA(PsiAB)
    entropynew = compute_entropy(rdmA)
    entropy = [entropy; entropynew]
    end
    if n%4000==0 print(n-initial_step+1, ":", Nsteps, " ") end
end
```

    4000:80000 8000:80000 12000:80000 16000:80000 20000:80000 24000:80000 28000:80000 32000:80000 36000:80000 40000:80000 44000:80000 48000:80000 52000:80000 56000:80000 60000:80000 64000:80000 68000:80000 72000:80000 76000:80000 80000:80000

We plot the difference from 1 of the normalization, to check the precision of our time integration.

```julia
tarray = collect(range(0, stop = deltat * Nsteps, length = length(normdiff)))
standard = [10^-5 for i in 1:length(tarray)]
plot(tarray, standard, label = "goal", xlabel = "t", ylabel = "normalization loss")
plot!(tarray, normdiff, label = "numerical", markershape = :hexagon, markerstrokealpha = 0,
    markersize = 2)
```

```julia
savefig("12spins_Ising_norm.png")
```

## Evolution of the entanglement entropy

We can finally plot the evolution of the entanglement entropy of the half chain with time.

```julia
tarray = collect(range(0, stop = deltat * Nsteps, length = length(normdiff)))
asympt = [N/2 for i in 1:length(tarray)]
plot(tarray, asympt, xlabel = "t", ylabel = "entanglement entropy", label = "N/2")
plot!(tarray, entropy, label = "numerical", markershape = :hexagon, markerstrokealpha = 0,
    markersize = 2)
#(length(tarray),length(entropy))
```

For the Ising model, with similar parameters as the ones used for the modified Hamiltonian, we do not saturate at the same point.

```julia
savefig("12spins_modIsing_ee.png")
```


# Spectral Estimation of Random Signals

*This jupyter notebook is part of a [collection of notebooks](../index.ipynb) on various topics of Digital Signal Processing. Please direct questions and suggestions to [Sascha.Spors@uni-rostock.de](mailto:Sascha.Spors@uni-rostock.de).*

## The Welch Method

In the previous section it has been shown that the [periodogram](periodogram.ipynb) as a non-parametric estimator of the power spectral density (PSD) $\Phi_{xx}(\mathrm{e}^{\,\mathrm{j}\,\Omega})$ of a random signal $x[k]$ is not consistent. This is due to the fact that its variance does not converge towards zero even when the length of the random signal is increased towards infinity. In order to overcome this problem, the [Bartlett method](https://en.wikipedia.org/wiki/Bartlett's_method) and [Welch method](https://en.wikipedia.org/wiki/Welch's_method)

1. split the random signal into segments,
2. estimate the PSD for each segment, and
3. average over these local estimates.

The averaging reduces the variance of the estimated PSD. While Barlett's method uses non-overlapping segments, Welch's is a generalization using windowed overlapping segments. For the discussion of Welch's method we assume a wide-sense ergodic real-valued random process.

### Derivation

The random signal $x[k]$ is split into into $L$ overlapping segments of length $N$, starting at multiples of the step size $M \in {1,2, \dots, N}$. These segments are windowed by the window $w[k]$ of length $N$, resulting in a windowed $l$-th segment $x_l[k]$ with $0\leq l\leq L-1$. The discrete-time Fourier transformation (DTFT) $X_l(\mathrm{e}^{\,\mathrm{j}\,\Omega})$ of the windowed $l$-th segment is then given as

\begin{equation}
X_l(\mathrm{e}^{\,\mathrm{j}\,\Omega}) = \sum_{k = 0}^{N-1} x[k + l \cdot M] \, w[k] \; \mathrm{e}^{\,-\mathrm{j}\,\Omega\,k}
\end{equation}

where the window $w[k]$ defined within $0\leq k\leq N-1$ should be normalized as $\frac{1}{N} \sum\limits_{k=0}^{N-1} | w[k] |^2 = 1$. The latter condition ensures that the power of the signal is maintained in the estimate. The stepsize $M$ determines the overlap between the segments. In general, $N-M$ number of samples overlap between adjacent segments. For $M = N$ no overlap occurs. The overlap is sometimes given as ratio $\frac{N-M}{N}\cdot 100\%$.

Introducing $X_l(\mathrm{e}^{\,\mathrm{j}\,\Omega})$ into the definition of the periodogram yields the periodogram of the $l$-th segment

\begin{equation}
\hat{\Phi}_{xx,l}(\mathrm{e}^{\,\mathrm{j}\,\Omega}) = \frac{1}{N} \,| X_l(\mathrm{e}^{\,\mathrm{j}\,\Omega}) |^2
\end{equation}

The estimated PSD is then given by averaging over the segment's periodograms $\hat{\Phi}_{xx,l}(\mathrm{e}^{\,\mathrm{j}\,\Omega})$

\begin{equation}
\hat{\Phi}_{xx}(\mathrm{e}^{\,\mathrm{j}\,\Omega}) = \frac{1}{L} \sum_{l = 0}^{L-1} \hat{\Phi}_{xx,l}(\mathrm{e}^{\,\mathrm{j}\,\Omega})
\end{equation}

Note, that the total number $L$ of segments has to be chosen such that the last required sample $(L-1)\cdot M + N - 1$ does not exceed the total length of the random signal. Otherwise the last segment $x_{L-1}[k]$ may be zeropadded towards length $N$.

The Bartlett method uses a rectangular window $w[k] = \text{rect}_N[k]$ and non-overlapping segments $M=N$. The Welch method uses overlapping segments and a window that must be chosen according to the intended spectral analysis task.

### Example

The following example is equivalent to the previous [periodogram example](periodogram.ipynb#Example---Periodogram). We aim at estimating the PSD of a random process which draws samples from normally distributed white noise with zero-mean and unit variance. The true PSD is consequently given as $\Phi_{xx}(\mathrm{e}^{\,\mathrm{j}\,\Omega}) = 1$.

```python
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
import scipy.signal as sig

N = 128  # length of segment
M = 64  # stepsize
L = 100  # total number of segments

# generate random signal
np.random.seed(5)
x = np.random.normal(size=L*M)

# compute periodogram by Welch's method
nf, Pxx = sig.welch(x, window='hamming', nperseg=N, noverlap=(N-M))
Pxx = .5*Pxx  # due to normalization in scipy.signal
Om = 2*np.pi*nf

# plot results
plt.figure(figsize=(10,4))
plt.stem(Om, Pxx, 'b', label=r'$\hat{\Phi}_{xx}(e^{j \Omega})$', basefmt=' ')
plt.plot(Om, np.ones_like(Pxx), 'r', label=r'$\Phi_{xx}(e^{j \Omega})$')
plt.title('Estimated and true PSD')
plt.xlabel(r'$\Omega$')
plt.axis([0, np.pi, 0, 2])
plt.legend()

# compute mean value of the periodogram
print('Mean value of the periodogram: %f' %np.mean(np.abs(Pxx)))
```

**Exercise**

* Compare the results to the periodogram example. Is the variance of the estimator lower?
* Change the number of segments `L`. What changes?
* Change the segment length `N` and stepsize `M`. What changes?

Solution: When comparing both the estimates of the PSD in the previous periodogram and above example, it is obvious that the variance of the Welch estimator is lower. Increasing the number of segments `L` lowers the variance further. Increasing the segment length `N` increases the total number of discrete frequencies in the estimated PSD. Since in above example the total number of segments is kept constant, the variance increases. Lowering the stepsize `M` has the same result, since the total number of samples is reduced for a fixed number of segments.

### Evaluation

It is shown in [[Stoica et al.](../index.ipynb#Literature)] that Welch's method is asymptotically unbiased. Under the assumption of a wide-sense stationary (WSS) random process, the periodograms $\hat{\Phi}_{xx,l}(e^{j \Omega})$ of the segments can be assumed to be approximately uncorrelated. Hence, averaging over these reduces the overall variance of the estimator. It can be shown formally that in the limiting case of an infinitely number of segments (infintely long signal) the variance tends towards zero. As a result Welch's method is an asymptotically consistent estimator of the PSD.

Note, that for a finite segment length $N$ the properties of the estimated PSD $\hat{\Phi}_{xx}(e^{j \Omega})$ depend on the length $N$ of the segments and the window function $w[k]$ due to the [leakage effect](../spectral_analysis_deterministic_signals/leakage_effect.ipynb).

**Copyright**

This notebook is provided as [Open Educational Resource](https://en.wikipedia.org/wiki/Open_educational_resources). Feel free to use the notebook for your own purposes. The text is licensed under [Creative Commons Attribution 4.0](https://creativecommons.org/licenses/by/4.0/), the code of the IPython examples under the [MIT license](https://opensource.org/licenses/MIT). Please attribute the work as follows: *Sascha Spors, Digital Signal Processing - Lecture notes featuring computational examples, 2016-2018*.


# Normalizing flows

Here is very good read on normalizing flows : [blogpost](https://lilianweng.github.io/lil-log/2018/10/13/flow-based-deep-generative-models.html)

# Density estimation using Real NVP

Normalizing flows is the class of probabilistic models that provides flexible parametrical probabilistic models, where the probability density function can be computed exactly. In the assignment, we will consider a real-valued non-volume preserving normalizing flows (Real NVP) -- a special case of normalizing flow.

## Problem setting

Our goal is to train a generative network $g_\theta:  Z \rightarrow X, g = f^{-1}$ that maps latent variable $z \sim p(z)$ to a sample $x \sim p(x)$. Where $p(z)$ is a prior distibiution and $p(x)$ is a data distibution. An illustrative example is provided below.

## Change of variable formula

Given an observed data variable $x \in X$,
a simple prior probability distribution $p_{Z}$ on a latent variable $z \in Z$,
and a bijection $f: X \rightarrow Z$ (with $g = f^{-1}$),
the change of variable formula defines a model distribution of $X$ by

where $\frac{\partial f(x)}{\partial x^T}$ is the Jacobian of $f$ at $x$.

Exact samples from the resulting distribution can be generated by using the inverse transform sampling rule. A sample $z \sim p_{Z}$ is drawn in the latent space, and its inverse image $x = f^{-1}(z) = g(z)$ generates a sample in the original space. Computing the density at a point $x$ is accomplished by computing the density of its image $f(x)$ and multiplying by the associated Jacobian determinant $\det\left(\frac{\partial f(x)}{\partial x^T}\right)$.

## Real NVP

Real NVP presents a class of functions where $\log\left(\left|\det\left(\frac{\partial f(x)}{\partial x^T}\right)\right|\right)$ can be computed efficiently (see, 3.3 Properties, https://arxiv.org/abs/1605.08803). Every layer of Real NVP is a coupling layer followed by permutation layer. Combination of coupling and permutation layers can be implemented as a masked version of the coupling layer:
## $$y = b \odot x + (1 - b) \odot \Big(x \odot \exp\big(s(b \odot x)\big) + t(b \odot x)\Big)$$

where $s$ and $t$ stand for scale and translation, and are functions from $R^{D} \mapsto R^{D}$, and $\odot$ is the Hadamard product or element-wise product, $b$ is a binary mask. For more details on the model see the paper Density estimation using Real NVP https://arxiv.org/abs/1605.08803.

# Implementation of Real NVP

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

from pylab import rcParams
rcParams['figure.figsize'] = 5, 4
rcParams['figure.dpi'] = 150

import torch
from torch import nn
from torch import distributions
from torch.nn.parameter import Parameter

from sklearn import datasets
```

```python
nets = lambda: nn.Sequential(nn.Linear(2, 256), nn.LeakyReLU(), nn.Linear(256, 256), nn.LeakyReLU(), nn.Linear(256, 2), nn.Tanh())
nett = lambda: nn.Sequential(nn.Linear(2, 256), nn.LeakyReLU(), nn.Linear(256, 256), nn.LeakyReLU(), nn.Linear(256, 2))
# functions that take no arguments and return a pytorch model, dim(X) -> dim(X)

masks = torch.from_numpy(np.array([[0, 1], [1, 0], [0, 1], [1, 0], [0, 1], [1, 0]]).astype(np.float32))
# torch.Tensor of size #number_of_coupling_layers x #dim(X)
```

```python
from torch import distributions
prior = distributions.MultivariateNormal(torch.zeros(2), torch.eye(2))
print(prior.log_prob(torch.Tensor([0,0])))
print(prior.sample((3,)))
```

    tensor(-1.8379)
    tensor([[ 0.8131, -0.4593],
            [-1.0915,  2.1433],
            [-0.3912, -0.0338]])

```python
class RealNVP(nn.Module):
    def __init__(self, nets, nett, mask, prior):
        super(RealNVP, self).__init__()

        # Create a flow
        # nets:  a function that returns a PyTorch neural network, e.g., nn.Sequential, s = nets(), s: dim(X) -> dim(X)
        # nett:  a function that returns a PyTorch neural network, e.g., nn.Sequential, t = nett(), t: dim(X) -> dim(X)
        # mask:  a torch.Tensor of size #number_of_coupling_layers x #dim(X)
        # prior: an object from torch.distributions e.g., torch.distributions.MultivariateNormal

        self.prior = prior
        self.mask = nn.Parameter(mask, requires_grad=False)
        self.t = torch.nn.ModuleList([nett() for _ in range(len(masks))])
        self.s = torch.nn.ModuleList([nets() for _ in range(len(masks))])

    def g(self, z):
        # Compute and return g(z) = x,
        #    where self.mask[i], self.t[i], self.s[i] define a i-th masked coupling layer
        # z: a torch.Tensor of shape batchSize x 1 x dim(X)
        # return x: a torch.Tensor of shape batchSize x 1 x dim(X)
        return x

    def f(self, x):
        # Compute f(x) = z and log_det_Jakobian of f,
        #    where self.mask[i], self.t[i], self.s[i] define a i-th masked coupling layer
        # x: a torch.Tensor, of shape batchSize x dim(X), is a datapoint
        # return z: a torch.Tensor of shape batchSize x dim(X), a hidden representations
        # return log_det_J: a torch.Tensor of len batchSize

        return z, log_det_J

    def log_prob(self, x):
        # Compute and return log p(x)
        # using the change of variable formula and log_det_J computed by f
        # return logp: torch.Tensor of len batchSize
        return logp

    def sample(self, batchSize):
        # Draw and return batchSize samples from flow using implementation of g
        # return x: torch.Tensor of shape batchSize x 1 x dim(X)
        return x
```

```python
flow = RealNVP(nets, nett, masks, prior)
# Check that a flow is reversible g(f(x)) = x
```

```python
from sklearn import datasets

trainable_parametrs = # list of all trainable parameters in a flow
optimizer = # choose an optimizer, use module torch.optim

for t in range(5001):
    noisy_moons = datasets.make_moons(n_samples=100, noise=.05)[0].astype(np.float32)
    loss = # compute the maximum-likelihood loss

    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    if t % 500 == 0:
        print('iter %s:' % t, 'loss = %.3f' % loss)

# Check that the loss decreases
# Is the visualization below good?
```

# Visualization

```python
from sklearn import datasets
import matplotlib.pyplot as plt
%matplotlib inline
from pylab import rcParams
rcParams['figure.figsize'] = 10, 8
rcParams['figure.dpi'] = 300
```

Draw several plots:
- samples from flow
- samples from prior
- data samples
- mapping form data to prior

The goal is to obtain figure similar to https://arxiv.org/abs/1605.08803

```python
noisy_moons = datasets.make_moons(n_samples=1000, noise=.05)[0].astype(np.float32)
z = flow.f(torch.from_numpy(noisy_moons))[0].detach().numpy()
plt.subplot(221)
plt.scatter(z[:, 0], z[:, 1])
plt.title(r'$z = f(X)$')

z = np.random.multivariate_normal(np.zeros(2), np.eye(2), 1000)
plt.subplot(222)
plt.scatter(z[:, 0], z[:, 1])
plt.title(r'$z \sim p(z)$')

plt.subplot(223)
x = datasets.make_moons(n_samples=1000, noise=.05)[0].astype(np.float32)
plt.scatter(x[:, 0], x[:, 1], c='r')
plt.title(r'$X \sim p(X)$')

plt.subplot(224)
x = flow.sample(1000).detach().numpy()
plt.scatter(x[:, 0, 0], x[:, 0, 1], c='r')
plt.title(r'$X = g(z)$')
```


# SMC2017: Exercise sheet IV

## Setup

```python
import numpy as np
from scipy import stats

from tqdm import tqdm_notebook

%matplotlib inline
import matplotlib.pyplot as plt
import seaborn as sns

sns.set_style()
```

## IV.1 Particle Metropolis-Hastings

Consider the state-space model
$$
\begin{array}{rcll}
x_t & = & \cos\left(\theta x_{t - 1}\right) + v_t, &\qquad v_t \sim \mathcal{N}(0, 1)\\
y_t & = & x_t + e_t, &\qquad e_t \sim \mathcal{N}(0, 1) \\
x_0 & \sim & \mathcal{N}(0, 1) &
\end{array}
$$
which admits the probabilistic model
$$
\begin{array}{lcl}
p(x_0) & = & \mathcal{N}\left(x_0;\,0,\,1\right) \\
p(x_t\,\big|\,x_{t - 1}) & = & \mathcal{N}\left(x_t;\,\cos\left(\theta x_{t - 1}\right),\,1\right) \\
p(y_t\,\big|\,x_t) & = & \mathcal{N}\left(y_t;\,x_t,\,1\right)
\end{array}
$$

For now, I will use the bootstrap particle filter (for simplicity).

### Simulate data

During the simulation $\theta = 1$ will be assumed. During the inference it will be assumed that $\theta \sim \mathcal{N}(0, 1)$.

```python
T = 50

xs_sim = np.zeros((T + 1,))
ys_sim = np.zeros((T,))

# Initial state
xs_sim[0] = 0.
for t in range(T):
    xs_sim[t + 1] = np.cos(xs_sim[t]) + stats.norm.rvs()

ys_sim = xs_sim[1:] + stats.norm.rvs(0, 1, T)

fig, axs = plt.subplots(2, 1, figsize=(10, 10))
axs[0].plot(xs_sim, 'o-')
axs[1].plot(range(1, T + 1), ys_sim, 'o-r')
```

Bootstrap particle filter giving an estimate $\widehat{z}_\theta$ of the joint likelihood $p(y_{1:T}\,\big|\,\theta)$.

```python
def log_likelihood_bootstrap_pf(y, N=20, theta=1):
    # Cumulatively build up log-likelihood
    ll = 0.0
    # Initialisation
    samples = stats.norm.rvs(0, 1, N)
    weights = 1 / N * np.ones((N,))

    # Determine the number of time steps
    T = len(y)
    # Loop through all time steps
    for t in range(T):
        # Resample
        ancestors = np.random.choice(samples, size=N,
                                     replace=True, p=weights)

        # Propagate
        samples = stats.norm.rvs(0, 1, N) + np.cos(theta * ancestors)

        # Weight
        weights = stats.norm.logpdf(y[t], loc=samples, scale=1)
        # Calculate the max of the weights
        max_weights = np.max(weights)
        # Subtract the max
        weights = weights - max_weights
        # Update log-likelihood
        ll += max_weights + np.log(np.sum(np.exp(weights))) - np.log(N)

        # Normalize weights to be probabilities
        weights = np.exp(weights) / np.sum(np.exp(weights))

    return ll
```

```python
log_likelihood_bootstrap_pf(ys_sim, N=50, theta=3)
```

    -102.29933166823245

As a proposal we can use $q(\theta'\,\big|\,\theta[k - 1]) = \mathcal{N}\left(\theta';\,\theta[k - 1], \tau\right)$ with an appropriately chosen $\tau$.

Implement a Metropolis-Hastings sampler with the above.

```python
def particle_metropolis_hastings(y, M=10000, N=20, tau=1):
    theta = np.zeros((M + 1,))
    alpha = np.zeros((M,))
    z = np.zeros((M + 1,))

    # Initial state
    theta[0] = 0
    z[0] = log_likelihood_bootstrap_pf(y, N=N, theta=theta[0])

    # Iterate the chain
    t = tqdm_notebook(range(M))
    for i in t:
        # Sample a new value
        theta_prop = stats.norm.rvs(theta[i], tau, 1)
        # Sample to be compared to the acceptance probability
        u = stats.uniform.rvs()

        # Terms in the second part of the acceptance probability -
        # Proposal is symmetric, so terms containing the proposal will
        # cancel each other out
        z_prop = log_likelihood_bootstrap_pf(y, N=N, theta=theta_prop)
        num = z_prop + stats.norm.logpdf(theta_prop)
        denom = z[i] + stats.norm.logpdf(theta[i])

        # Acceptance probability
        alpha[i] = min(1, np.exp(num - denom))
        t.set_postfix({'a_mean': np.mean(alpha[:(i + 1)])})
        # Set next state depending on acceptance probability
        if u <= alpha[i]:
            z[i + 1] = z_prop
            theta[i + 1] = theta_prop
        else:
            z[i + 1] = z[i]
            theta[i + 1] = theta[i]

    return theta, alpha
```

```python
theta, alpha = particle_metropolis_hastings(ys_sim, M=10000, N=50, tau=0.7)
```

Failed to display Jupyter Widget of type HBox.

  If you're reading this message in Jupyter Notebook or JupyterLab, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another notebook frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

    9993/|/100%|| 9993/10000 [01:51<00:00, 89.90it/s, a_mean=0.231]

```python
np.mean(alpha)
```

    0.23624512348377344

```python
fig, ax = plt.subplots()
ax.plot(theta, '.-')
```

```python
fig, ax = plt.subplots()
ax.hist(theta[2000:], normed=True, bins=60);
```

## IV.2 Conditional Particle Filter

I will turn the fully adapted particle filter from exercise II.2 into a conditional particle filter by including a reference state trajectory and in each propagation step the refernence state trajectory delivers one of the particles. States and their ancestors will be saved and the algorithm returns a new state trajectory conditional on the old one.

The state-space model under consideration is (normal distribution parametrized with $\sigma$)
$$
\begin{array}{rll}
x_{t + 1} &= \cos(x_t)^2 + v_t, & v_t \sim N(0, 1) \\
y_t &= 2 x_t + e_t, & e_t \sim N(0, 0.1)
\end{array}
$$
which leads to the probabilistic model
$$
\begin{align}
p(x_t\,|\,x_{t - 1}) &= N\left(x_t;\,\cos(x_t)^2,\,1\right) \\
p(y_t\,|\,x_t) &= N\left(y_t;\,2 x_t,\,0.1\right)
\end{align}
$$
This admits the necessary pdfs
$$
\begin{align}
p(y_t\,|\,x_{t - 1}) &= N(y_t;\,2 \cos(x_{t - 1})^2,\,\sqrt{4.01}) \\
p(x_t\,|\,x_{t - 1},\,y_t) &= N\left(x_t;\,\frac{2 y_t + 0.01 \cos(x_{t - 1})^2}{4.01}, \frac{0.1}{\sqrt{4.01}}\right)
\end{align}
$$

```python
def conditional_FAPF(x_ref, y, N=200):
    # Determine length of data
    T = len(y)
    # Save the paths of all final particles
    xs = np.zeros((N, T + 1))
    # Initialisation
    xs[:, 0] = stats.norm.rvs(0, 1, N)
    # Replace last state with state from reference trajectory
    xs[N - 1, 0] = x_ref[0]

    for t in range(T):
        # Calculate resampling weights in case of FAPF
        ws = stats.norm.logpdf(y[t], loc=2*np.power(np.cos(xs[:, t]), 2),
                               scale=np.sqrt(4.01))
        # Subtract maximum weight
        ws -= np.max(ws)
        # Normalize the resampling weights
        ws = np.exp(ws) / np.sum(np.exp(ws))

        # Resample
        ancestors = np.random.choice(range(N), size=N, replace=True, p=ws)

        # Propagate
        xs[:, t + 1] = stats.norm.rvs(0, 1, N) * 0.1 / np.sqrt(4.01) + \
            (2 / 4.01) * y[t] + (0.01 / 4.01) * \
            np.power(np.cos(xs[ancestors, t]), 2)
        # Replace last sample with reference trajectory
        ancestors[N - 1] = N - 1
        xs[N - 1, t + 1] = x_ref[t + 1]
        # Update the ancestor lines
        xs[:, 0:t] = xs[ancestors, 0:t]

    # Randomly choose trajectory which will be returned
    # All normalized weights are 1 / N, so that no draw from
    # a categorical distribution is necessary. A uniform draw
    # is satisfactory.
    b = np.random.randint(N)

    return xs[b, :]
```

Simulate from the model given above.

```python
T = 100

# Allocate arrays for results
ys_sim = np.zeros((T,))
xs_sim = np.zeros((T + 1,))
# Initial value for state
xs_sim[0] = 0.1
# Walk through all time steps
for t in range(T):
    xs_sim[t + 1] = np.power(np.cos(xs_sim[t]), 2) + stats.norm.rvs(0, 1, 1)
    ys_sim[t] = 2 * xs_sim[t + 1] + stats.norm.rvs(0, 0.1, 1)

fig, axs = plt.subplots(2, 1, figsize=(10, 10))
axs[0].plot(range(T + 1), xs_sim, 'o-');
axs[1].plot(range(1, T + 1), ys_sim, 'o-r');
```

This is a Markov kernel which can be used in Gibbs sampling where the parameters and the hidden state are sampled repeatedly consecutively.

```python
xs = conditional_FAPF(xs_sim, ys_sim, N=1000)

fig, ax = plt.subplots()
ax.plot(xs_sim, 'o-')
ax.plot(xs, 'x-');
```

## IV.3 Conditional importance sampling

### a) Conditional importance sampling with few particles

Sample from $\pi(x) = \mathcal{N}\left(x\,\big|\,1,\,1\right)$ by using conditional importance sampling with the proposal $q(x) = \mathcal{N}\left(x\,\big|\,0,\,1\right)$.

```python
def cond_imp_sampling_kernel(x, N=2):
    # Sample new proposals
    xs = stats.norm.rvs(0, 1, N)
    # Set the last sample to the reference
    xs[N - 1] = x

    # Calculate weights
    ws = stats.norm.logpdf(xs, loc=1, scale=1) - \
        stats.norm.logpdf(xs, loc=0, scale=1)
    ws -= np.max(ws)
    ws = np.exp(ws) / np.sum(np.exp(ws))

    return xs[np.random.choice(range(N), size=1, p=ws)[0]]
```

Use that kernel to sample from the target distribution.

```python
def cond_imp_sampling_mcmc(M=1000, N=2):
    # Initialisation
    xs = np.zeros((M + 1,))
    for m in tqdm_notebook(range(M)):
        xs[m + 1] = cond_imp_sampling_kernel(xs[m], N=N)

    return xs
```

Run the sampler

```python
xs = cond_imp_sampling_mcmc(M=70000)
```

Failed to display Jupyter Widget of type HBox.

  If you're reading this message in Jupyter Notebook or JupyterLab, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another notebook frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

Plot the result

```python
fig, ax = plt.subplots()
ax.hist(xs, normed=True, bins=40);
```

### b) Lower bound for probability that draw from cond. imp. sampling kernel falls in a set $A$

Theoretical exercise. Solution will be in `exercises_on_paper`.

## IV.4 An SMC sampler for localization

A point $x_0$ is supposed to be localized in the plane $[-12,\,12]^2$.

There are some measurements $y_{1:M}$ which are corrupted by heavy-tailed noise from an exponential distribution.

We want to find the distribution $p\left(x_0\,\big|\,y_{1:M}\right)$.

### a) Simulate data

$M$ simulated measurements from the model
$$
\begin{align}
    y_t^1 &= x_0^1 + n_m^1 b_m^1 \\
    y_t^2 &= x_0^2 + n_m^2 b_m^2
\end{align}
$$
where
$$
\begin{align}
    m &= 1, 2, \dots, M \\
    x_0 &= \left(x_0^1, x_0^2\right) \\
    n_m^1, n_m^2 &\sim \mathrm{Exp}\left(2\right) \\
    \mathbb{P}\left(b_m^1 = 1\right) &= \mathbb{P}\left(b_m^1 = -1\right) = \frac{1}{2}
\end{align}
$$
and analogously for $b_m^2$.

```python
M = 50
x0 = np.array([6.0, -5.5])
ns = np.reshape(stats.expon.rvs(scale=2, size=2 * M), (2, M))
bs = np.reshape(np.random.choice([-1, 1], size=2 * M,
                                 replace=True, p=[0.5, 0.5]),
                (2, M))

ys = np.reshape(np.repeat(x0, M), (2, M)) + ns * bs
ys = ys.T
```

Visualize simulated observations and true $x_0$

```python
fig, ax = plt.subplots(figsize=(8, 8))
ax.scatter(ys[:, 0], ys[:, 1])
ax.set_xlim([-12, 12])
ax.set_ylim([-12, 12])
ax.scatter(x0[0], x0[1], facecolors='none', edgecolors='r', s=100)
```

### b) Likelihood

As derived on paper, it holds that
$$
p\left(y_m^j\,\big|\,x_0^j\right) =
\begin{cases}
    \frac{1}{4} \exp\left(-\frac{y_m^j - x_0^j}{2}\right) & y_m^j > x_0 \\
    \frac{1}{4} \exp\left(\frac{y_m^j - x_0^j}{2}\right) & y_m^j < x_0
\end{cases}
$$
and since the components of $y_m$ are independent we get
$$
p\left(y_m\,\big|\,x_0\right) = p\left(y_m^1\,\big|\,x_0^1\right) \cdot p\left(y_m^2\,\big|\,x_0^2\right)
$$

```python
def log_likelihood(x, ys):
    return np.sum(np.log(0.25) + 0.5 *
                  np.power(-1, ((ys - x) > 0).astype('int')) * (ys - x))
```

### c) Metropolis-Hastings kernel for $\pi_k$

This function evaluates $\log\left(\pi_k\right)$

```python
def tempered_logpdf(x, ys, k, K=10):
    # k / K comes from likelihood tempering
    return k / K * log_likelihood(x, ys) + \
        stats.multivariate_normal.logpdf(x, mean=[0, 0],
                                         cov=7 * np.eye(2))
```

The Metropolis-Hastings kernel produces one new sample of the Markov chain, conditional on the last sample.

```python
def mh_kernel(x, ys, k, K=10, tau=0.5):
    # Propose a new value
    x_prop = stats.multivariate_normal.rvs(mean=x,
                                           cov=tau**2 * np.eye(2),
                                           size=1)

    # Terms in the second part of the acceptance probability
    # Proposal is symmetric, so terms containing the proposal will
    # cancel each other out

    # Acceptance probability
    alpha = min(0, tempered_logpdf(x_prop, ys, k, K=K) -
                   tempered_logpdf(x, ys, k, K=K))

    # Sample to be compared to the acceptance probability
    u = stats.uniform.rvs()

    # Set next state depending on acceptance probability
    if np.log(u) <= alpha:
        return x_prop, np.exp(alpha)
    else:
        return x, np.exp(alpha)
```

```python
mh_kernel(x0, ys, 2)
```

    (array([ 5.51042449, -4.8984264 ]), 0.83749898804356571)

### e) Putting together the actual SMC sampler

```python
def smc_sampler(ys, K=10, N=100, ess_min=50, tau=0.5, progressbar=True):
    # Vectors for saving
    xs = np.zeros((K + 1, N, 2))
    ancs = np.zeros((K, N), dtype='int64')
    ws = np.zeros((K + 1, N))

    # Initialisation
    xs[0, :, :] = stats.multivariate_normal.rvs(mean=[0, 0],
                                                cov=7 * np.eye(2),
                                                size=N)
    ws[0, :] = 1 / N * np.ones((N,))

    if progressbar:
        t = tqdm_notebook(range(K))
    else:
        t = range(K)

    for k in t:
        # Update weights
        for i in range(N):
            ws[k + 1, i] = np.log(ws[k, i]) + \
                tempered_logpdf(xs[k, i, :], ys, k=k + 1, K=K) - \
                tempered_logpdf(xs[k, i, :], ys, k=k, K=K)
        # and normalize them
        ws[k + 1, :] -= np.max(ws[k + 1, :])
        ws[k + 1, :] = np.exp(ws[k + 1, :]) / np.sum(np.exp(ws[k + 1, :]))

        # Resample depending on ESS
        if 1 / np.sum(np.power(ws[k + 1, :], 2)) < ess_min:
            ancs[k, :] = np.random.choice(range(N), size=N,
                                          replace=True, p=ws[k + 1, :])
            ws[k + 1, :] = 1 / N * np.ones((N,))
        else:
            ancs[k, :] = range(N)

        # Propagate / Sample from next element in the sequence
        # Here, via a Metropolis-Hastings kernel
        for i in range(N):
            xs[k + 1, i, :] = mh_kernel(xs[k, ancs[k, i], :], ys,
                                        k=k + 1, K=K, tau=tau)[0]

    return xs, ancs, ws
```

```python
xs, ancs, ws = smc_sampler(ys, N=1000, ess_min=750)
```

Failed to display Jupyter Widget of type HBox.

  If you're reading this message in Jupyter Notebook or JupyterLab, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another notebook frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

```python
np.sum(xs[10, :, 0] * ws[10])
```

    6.255091866535226

```python
np.sum(xs[10, :, 1] * ws[10])
```

    -5.4955400504095433

### f) Visualisation and testing of the SMC sampling

Sample the probability distributions of interest to be able to draw contour lines.

```python
x = np.arange(-12, 12, 0.25)
y = np.arange(-12, 12, 0.25)

X, Y = np.meshgrid(x, y)

Z = np.zeros((len(x), len(y), 10))

for k in tqdm_notebook(range(10)):
    for i in range(len(x)):
        for j in range(len(y)):
            Z[i, j, k] = tempered_logpdf(np.array([X[i, j], Y[i, j]]),
                                       ys, k, K=10)

    Z[:, :, k] -= np.max(Z[:, :, k])
    Z[:, :, k] = np.exp(Z[:, :, k])
```

Failed to display Jupyter Widget of type HBox.

  If you're reading this message in Jupyter Notebook or JupyterLab, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another notebook frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

```python
fig, axs = plt.subplots(5, 2, figsize=(8.5, 20))

for k in range(10):
    levels=np.linspace(np.min(Z[:, :, k]),
                       np.max(Z[:, :, k]), 8)
    axs[k // 2, k % 2].contour(X, Y, Z[:, :, k])
    axs[k // 2, k % 2].scatter(x0[0], x0[1],
                               facecolors='none', edgecolors='r', s=100)
    axs[k // 2, k % 2].scatter(xs[k, :, 0], xs[k, :, 1], color='k')

fig.tight_layout()
```

### g) Comparison to standard Metropolis Hastings sampler

This is the Metropolis Hastings sampler for the distribution $\pi_k$

```python
def mh_sampler(ys, k=10, K=10, M=1000, tau=0.5, progressbar=True):
    # Prepare vectors for saving
    xs = np.zeros((M + 1, 2))
    alpha = np.zeros((M,))

    # Initial state
    # Choose zero as the initial state

    # Iterate the chain
    if progressbar:
        t = tqdm_notebook(range(M))
    else:
        t = range(M)

    for i in t:
        xs[i + 1], alpha[i] = mh_kernel(xs[i], ys, k, K=K, tau=tau)
        if progressbar:
            t.set_postfix({'mean acc': np.mean(alpha[:(i + 1)])})

    return xs, alpha
```

```python
xs, _ = mh_sampler(ys, M=30000, tau=0.7, progressbar=True)
```

Failed to display Jupyter Widget of type HBox.

  If you're reading this message in Jupyter Notebook or JupyterLab, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another notebook frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

Some visualisations of the marginal distributions for the two coordinates determined by the Metropolis-Hastings run.

```python
fig, axs = plt.subplots(2, 1, figsize=(8, 6))

burnin = 500

axs[0].hist(xs[burnin:, 0], normed=True, bins=50);
axs[0].axvline(np.mean(xs[burnin:, 0]), color='r', linestyle='--')
axs[0].axvline(np.median(xs[burnin:, 0]), color='k', linestyle='--')

axs[1].hist(xs[burnin:, 1], normed=True, bins=50);
axs[1].axvline(np.mean(xs[burnin:, 1]), color='r', linestyle='--')
axs[1].axvline(np.median(xs[burnin:, 1]), color='k', linestyle='--')
```

```python
means_mh = np.zeros((10, 2))
means_smc = np.zeros((10, 2))

for m in tqdm_notebook(range(10)):
    xs, _ = mh_sampler(ys, M=25000, tau=0.7, progressbar=True)
    means_mh[m, :] = np.mean(xs[500:], axis=0)

    xs, _, ws = smc_sampler(ys, N=2000, ess_min=1500, progressbar=True)
    means_smc[m, :] = [np.sum(xs[10, :, 0] * ws[10]),
                       np.sum(xs[10, :, 1] * ws[10])]
```

Failed to display Jupyter Widget of type HBox.

  If you're reading this message in Jupyter Notebook or JupyterLab, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another notebook frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

Failed to display Jupyter Widget of type HBox.

  If you're reading this message in Jupyter Notebook or JupyterLab, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another notebook frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

Failed to display Jupyter Widget of type HBox.

  If you're reading this message in Jupyter Notebook or JupyterLab, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another notebook frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

Failed to display Jupyter Widget of type HBox.

  If you're reading this message in Jupyter Notebook or JupyterLab, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another notebook frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

Failed to display Jupyter Widget of type HBox.

  If you're reading this message in Jupyter Notebook or JupyterLab, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another notebook frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

Failed to display Jupyter Widget of type HBox.

  If you're reading this message in Jupyter Notebook or JupyterLab, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another notebook frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

Failed to display Jupyter Widget of type HBox.

  If you're reading this message in Jupyter Notebook or JupyterLab, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another notebook frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

Failed to display Jupyter Widget of type HBox.

  If you're reading this message in Jupyter Notebook or JupyterLab, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another notebook frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

Failed to display Jupyter Widget of type HBox.

  If you're reading this message in Jupyter Notebook or JupyterLab, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another notebook frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

Failed to display Jupyter Widget of type HBox.

  If you're reading this message in Jupyter Notebook or JupyterLab, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another notebook frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

Failed to display Jupyter Widget of type HBox.

  If you're reading this message in Jupyter Notebook or JupyterLab, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another notebook frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

Failed to display Jupyter Widget of type HBox.

  If you're reading this message in Jupyter Notebook or JupyterLab, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another notebook frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

Failed to display Jupyter Widget of type HBox.

  If you're reading this message in Jupyter Notebook or JupyterLab, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another notebook frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

Failed to display Jupyter Widget of type HBox.

  If you're reading this message in Jupyter Notebook or JupyterLab, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another notebook frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

Failed to display Jupyter Widget of type HBox.

  If you're reading this message in Jupyter Notebook or JupyterLab, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another notebook frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

Failed to display Jupyter Widget of type HBox.

  If you're reading this message in Jupyter Notebook or JupyterLab, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another notebook frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

Failed to display Jupyter Widget of type HBox.

  If you're reading this message in Jupyter Notebook or JupyterLab, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another notebook frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

Failed to display Jupyter Widget of type HBox.

  If you're reading this message in Jupyter Notebook or JupyterLab, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another notebook frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

Failed to display Jupyter Widget of type HBox.

  If you're reading this message in Jupyter Notebook or JupyterLab, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another notebook frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

Failed to display Jupyter Widget of type HBox.

  If you're reading this message in Jupyter Notebook or JupyterLab, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another notebook frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

Failed to display Jupyter Widget of type HBox.

  If you're reading this message in Jupyter Notebook or JupyterLab, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another notebook frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

```python
np.mean(np.linalg.norm(means_smc - x0, axis=1, ord=1))
```

    0.27328780563178762

```python
np.mean(np.linalg.norm(means_mh - x0, axis=1, ord=1))
```

    0.26820684064366568