# Implementation of a Devito acoustic Least-square Reverse time migration

## This tutorial is contributed by SENAI CIMATEC (2021)

This tutorial is based on:

**LEAST-SQUARES REVERSE TIME MIGRATION (LSRTM) IN THE SHOT DOMAIN** (2016)

Antonio Edson Lima de Oliveira, Reynam da Cruz Pestana and Adriano Wagner Gomes dos Santos

Brazilian Journal of Geopyisics

http://dx.doi.org/10.22564/rbgf.v34i3.831

**Plane-wave least-squares reverse-time migration** (2013)

Wei Dai and Gerard T. Schuster

GEOPHYSICS Technical Papers

http://dx.doi.org/10.22564/rbgf.v34i3.831

**Two-point step size gradient method** (1988)

Barzilai, J. and Borwein, J.

IMA Journal of Numerical Analysis

https://doi.org/10.1093/imanum/8.1.141

## Introduction
The goal of this tutorial is to implement and validate the Least-squares reverse time migration (LSRTM) using a 2D three-layered velocity model with a square in the middle. The algorithm has been implemented using the Born's appoximation.

The acoustic wave equation for constant density is:

\begin{equation}
 m_{0} \dfrac{\partial^2 p_0 }{\partial t^2} - \nabla^2 p_0 = s (\mathbf{x},t)   \hspace{0.5cm}     (1)
\end{equation}

where $s (\mathbf{x},t) $ is the source, $p_{0}$ is the background wavefield and $m_{0}$ is the smoothed squared slowness.

A perturbation in the squared slowness $m = m_{0} + \delta m$ produces a background wavefield ( $p_{0}$ ) and scattered wavefield ( $\delta p$ ), so the wavefield $p$ is approximated to $ p = p_0 + \delta p$, that obeys the relation:

\begin{equation}
 m \dfrac{\partial^2 p}{\partial t^2} - \nabla^2 p = s (\mathbf{x},t)   \hspace{0.5cm}     (2)
\end{equation}

Using the approximation of $p$ and $m$ into equation (2),

\begin{equation}
 (m_{0} + \delta m) \dfrac{\partial^2 (p_0 + \delta p)}{\partial t^2} - \nabla^2 (p_0 + \delta p) = s (\mathbf{x},t)   \hspace{0.5cm}     (3)
\end{equation}

Expanding equation (3) we have:
\begin{equation}
m_{0} \dfrac{\partial^2 p_0 }{\partial t^2} - \nabla^2 p_0 + m_{0} \dfrac{\partial^2 \delta p }{\partial t^2} - \nabla^2\delta p + \delta m \dfrac{\partial^2 (p_{0} +\delta p) }{\partial t^2}= s (\mathbf{x},t) \hspace{0.5cm}    (4)
\end{equation}

Reordering the equation (4),
\begin{equation}
m_{0} \dfrac{\partial^2 p_0 }{\partial t^2} - \nabla^2 p_0 + m_{0} \dfrac{\partial^2 \delta p }{\partial t^2} - \nabla^2\delta p = s (\mathbf{x},t) - \delta m \dfrac{\partial^2 (p_{0} +\delta p) }{\partial t^2} \hspace{0.5cm}    (5)
\end{equation}

Considering that $ \delta m \dfrac{\partial^2 (p_{0} +\delta p) }{\partial t^2} = \delta m \dfrac{\partial^2 p }{\partial t^2} \approx \delta m \dfrac{\partial^2 p_{0} }{\partial t^2}  $( Via Born's approximation $p\approx p_{0}$ ):

\begin{equation}
m_{0} \dfrac{\partial^2 p_0 }{\partial t^2} - \nabla^2 p_0 + m_{0} \dfrac{\partial^2 \delta p }{\partial t^2} - \nabla^2\delta p = s (\mathbf{x},t) -\delta m \dfrac{\partial^2 p_{0} }{\partial t^2}\hspace{0.5cm}    (6)
\end{equation}

Now we get an equations system:

\begin{equation}
 \left\{
\begin{array}{lcl}
m_{0} \dfrac{\partial^2 p_0 }{\partial t^2} - \nabla^2 p_0 = s (\mathbf{x},t),\hspace{0.5cm} (7a) \\
m_{0} \dfrac{\partial^2 \delta p }{\partial t^2} - \nabla^2\delta p = - \delta m \dfrac{\partial^2 p_{0} }{\partial t^2} \hspace{0.5cm} (7b) \\
\end{array}
\right.
\end{equation}

Equations (7a) and (7b) are for Born modelling. The adjoint modelling is defined by the equation:

\begin{equation}
m_{0} \dfrac{\partial^2 v}{\partial t^2} - \nabla^2 v =\textbf{d}  \hspace{0.5cm}     (8)
\end{equation}
where $\textbf{d}$ is the shot recorded.

With all these equations, the migrated image can be constructed

\begin{equation}
\mathbf{m}_{mig}= \sum_{t} \dfrac{\partial^2 p_0}{\partial t^2}v \hspace{0.5cm}     (9)
\end{equation}

In this notebook the migration and gradient has been precoditioned by the source illumination, so equation (9) becomes:

\begin{equation}
\mathbf{m}_{mig}=  \frac{\sum_{t} \dfrac{\partial^2 p_0}{\partial t^2}v}{\sum_{t} p_{0}^2} \hspace{0.5cm}     (10)
\end{equation}

LSRTM aims to solve the reflectivity model $\mathbf{m}$ by minimizing the difference between the forward modeled data $L\mathbf{m}$ and the recorded data $d$ in a least-squares sense:

\begin{equation}
    f(\mathbf{m}) = \frac{1}{2}||L\mathbf{m}-\mathbf{d}||^{2} \hspace{0.5cm}     (11)
\end{equation}

To solve equation (11), it has been implemented here the steepest descent method with an appropriate step-length,

\begin{equation}
\textbf{m}_{k+1} = \textbf{m}_k -\alpha_k \textbf{g}_k \hspace{0.5cm}     (12)
\end{equation}
where $\textbf{g}_k$ is the gradient and $\alpha_k$ is the step-length. The gradient computation is simply taking equation (9) and instead of injecting the shot recorded $\textbf{d}$, injects the residue $\textbf{d}_{calc}-\textbf{d}_{obs}$

For now we are going to import the utilities.

```python
%matplotlib inline
import numpy as np

from devito import Operator,Eq,solve,Grid,SparseFunction,norm
from devito import TimeFunction,Function
from devito import gaussian_smooth
from devito import mmax

from devito.logger import info

from examples.seismic import Model
from examples.seismic import plot_velocity,plot_shotrecord
from examples.seismic import Receiver
from examples.seismic import PointSource
from examples.seismic import plot_image,AcquisitionGeometry
from examples.seismic import TimeAxis

from examples.seismic.self_adjoint import (setup_w_over_q)
from examples.seismic.acoustic import AcousticWaveSolver

import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import ImageGrid
from mpl_toolkits.axes_grid1.axes_divider import make_axes_locatable
import matplotlib.ticker as plticker

from devito import configuration
configuration['log-level'] = 'WARNING'
```

# Seismic modelling with Devito

Now let's import all the parameters needed and create the true 2D velocity model and the smoothed model to perform the Born's modelling.

```python
shape = (101, 101)  # Number of grid point (nx, nz)
spacing = (10., 10.)  # Grid spacing in m. The domain size is now 1km by 1km
origin = (0., 0.)  # What is the location of the top left corner. This is necessary to define

fpeak = 0.025# Source peak frequency is 25Hz (0.025 kHz)
t0w = 1.0 / fpeak
omega = 2.0 * np.pi * fpeak
qmin = 0.1
qmax = 100000
npad=50
dtype = np.float32

nshots = 21
nreceivers = 101
t0 = 0.
tn = 1000.  # Simulation last 1 second (1000 ms)
filter_sigma = (5, 5) # Filter's length

v = np.empty(shape, dtype=dtype)

# Define a velocity profile. The velocity is in km/s
vp_top = 1.5

v[:] = vp_top # Top velocity
v[:, 30:65]= vp_top +0.5
v[:, 65:101]= vp_top +1.5
v[40:60, 35:55]= vp_top+1

init_damp = lambda func, nbl: setup_w_over_q(func, omega, qmin, qmax, npad, sigma=0)
model = Model(vp=v, origin=origin, shape=shape, spacing=spacing,
              space_order=8, bcs=init_damp,nbl=npad,dtype=dtype)
model0 = Model(vp=v, origin=origin, shape=shape, spacing=spacing,
              space_order=8, bcs=init_damp,nbl=npad,dtype=dtype)

dt = model.critical_dt
s = model.grid.stepping_dim.spacing
time_range = TimeAxis(start=t0, stop=tn, step=dt)
nt=time_range.num
```

```python
#NBVAL_IGNORE_OUTPUT
# Create initial model and smooth the boundaries
gaussian_smooth(model0.vp, sigma=filter_sigma)
# Plot the true and initial model
plot_velocity(model)
plot_velocity(model0)
```

Now we are going to set the source and receiver position.

```python
# First, position source centrally in all dimensions, then set depth
src_coordinates = np.empty((1, 2))
src_coordinates[0, :] = np.array(model.domain_size) * .5
src_coordinates[0, -1] = 30.

# Define acquisition geometry: receivers

# Initialize receivers for synthetic and imaging data
rec_coordinates = np.empty((nreceivers, 2))
rec_coordinates[:, 0] = np.linspace(0, model.domain_size[0], num=nreceivers)
rec_coordinates[:, 1] = 30.

# Geometry
geometry = AcquisitionGeometry(model, rec_coordinates, src_coordinates, t0, tn, f0=fpeak, src_type='Ricker')

solver = AcousticWaveSolver(model, geometry, space_order=8)
```

```python
source_locations = np.empty((nshots, 2), dtype=dtype)
source_locations[:, 0] = np.linspace(0., 1000, num=nshots)
source_locations[:, 1] = 30. # Depth is 30m
```

We are going to compute the LSRTM gradient.

```python
def lsrtm_gradient(dm):

    residual = Receiver(name='residual', grid=model.grid, time_range=geometry.time_axis,
                        coordinates=geometry.rec_positions)

    d_obs = Receiver(name='d_obs', grid=model.grid,time_range=geometry.time_axis,
                         coordinates=geometry.rec_positions)

    d_syn = Receiver(name='d_syn', grid=model.grid,time_range=geometry.time_axis,
                         coordinates=geometry.rec_positions)

    grad_full = Function(name='grad_full', grid=model.grid)

    grad_illum = Function(name='grad_illum', grid=model.grid)

    src_illum = Function (name ="src_illum", grid = model.grid)

    # Using devito's reference of virtual source
    dm_true =  (solver.model.vp.data**(-2) - model0.vp.data**(-2))

    objective = 0.
    for i in range(nshots):

        #Observed Data using Born's operator
        geometry.src_positions[0, :] = source_locations[i, :]

        _, u0, _ = solver.forward(vp=model0.vp, save=True)

        solver.jacobian(dm_true, vp=model0.vp, rec = d_obs)

        #Calculated Data using Born's operator
        solver.jacobian(dm, vp=model0.vp, rec = d_syn)

        residual.data[:] = d_syn.data[:]- d_obs.data[:]

        grad_shot,_ = solver.gradient(rec=residual, u=u0, vp=model0.vp)

        src_illum_upd =  Eq(src_illum, src_illum + u0**2)
        op_src = Operator([src_illum_upd])
        op_src.apply()

        grad_sum = Eq(grad_full, grad_full  + grad_shot)
        op_grad = Operator([grad_sum])
        op_grad.apply()

        objective += .5*norm(residual)**2

    grad_f = Eq(grad_illum, grad_full/(src_illum+10**-9))
    op_gradf = Operator([grad_f])
    op_gradf.apply()

    return objective,grad_illum,d_obs,d_syn
```

For the first LSRTM iteration, we used a quite simple step-length using the maximum value of the gradient. For the other LSRTM iterations we used the step-length proposed by Barzilai Borwein.
\begin{equation}
\alpha_{k}^{BB1} = \frac{\mathbf{s}_{k-1}^{T}\mathbf{s}_{k-1}}{\mathbf{s}_{k-1}^{T}\mathbf{y}_{k-1}}
\end{equation}
where $\textbf{s}_{k-1} = \textbf{m}_{k}-\textbf{m}_{k-1}$ and $\textbf{y}_{k-1} = \textbf{g}_{k}-\textbf{g}_{k-1}$

A second option is:
\begin{equation}
\alpha_{k}^{BB2} = \frac{\mathbf{s}_{k-1}^{T}\mathbf{y}_{k-1}}{\mathbf{y}_{k-1}^{T}\mathbf{y}_{k-1}}
\end{equation}

\begin{equation}
\alpha_{k} =
\begin{cases}
\alpha_{k}^{BB2},& \text{if}\ 0 < \frac{\alpha_{k}^{BB2}}{\alpha_{k}^{BB1}} < 1 \\
\alpha_{k}^{BB1},& \text{else}
\end{cases}
\end{equation}

```python
def get_alfa(grad_iter,image_iter,niter_lsrtm):

    term1 = np.dot(image_iter.reshape(-1), image_iter.reshape(-1))

    term2 = np.dot(image_iter.reshape(-1), grad_iter.reshape(-1))

    term3 = np.dot(grad_iter.reshape(-1), grad_iter.reshape(-1))

    if niter_lsrtm == 0:

        alfa = .05 / mmax(grad_full)

    else:
        abb1 = term1 / term2

        abb2 = term2 / term3

        abb3 = abb2 / abb1

        if abb3 > 0 and abb3 < 1:
            alfa = abb2
        else:
            alfa = abb1

    return alfa
```

Now is the kernel of the LSRTM. The migration will be updated iteratively, using the step-length and the gradient.

```python
#NBVAL_IGNORE_OUTPUT
image_up_dev = np.zeros((model0.vp.shape[0], model0.vp.shape[1]),dtype)

image = np.zeros((model0.vp.shape[0], model0.vp.shape[1]))

nrec=101
niter=20 # number of iterations of the LSRTM
history = np.zeros((niter, 1)) #objective function

image_prev = np.zeros((model0.vp.shape[0],model0.vp.shape[1]))

grad_prev  = np.zeros((model0.vp.shape[0],model0.vp.shape[1]))

yk  = np.zeros((model0.vp.shape[0],model0.vp.shape[1]))

sk = np.zeros((model0.vp.shape[0],model0.vp.shape[1]))

for k in range(niter) :

    dm =  image_up_dev # Reflectivity for Calculated data via Born

    print('LSRTM Iteration',k+1)

    objective,grad_full,d_obs,d_syn = lsrtm_gradient(dm)

    history[k] = objective

    yk = grad_full.data - grad_prev

    sk = image_up_dev - image_prev

    alfa = get_alfa(yk,sk,k)

    grad_prev = grad_full.data

    image_prev = image_up_dev

    image_up_dev = image_up_dev - alfa*grad_full.data

    if k == 0: # Saving the first migration using Born operator.

        image = image_up_dev
```

    LSRTM Iteration 1
    LSRTM Iteration 2
    LSRTM Iteration 3
    LSRTM Iteration 4
    LSRTM Iteration 5
    LSRTM Iteration 6
    LSRTM Iteration 7
    LSRTM Iteration 8
    LSRTM Iteration 9
    LSRTM Iteration 10
    LSRTM Iteration 11
    LSRTM Iteration 12
    LSRTM Iteration 13
    LSRTM Iteration 14
    LSRTM Iteration 15
    LSRTM Iteration 16
    LSRTM Iteration 17
    LSRTM Iteration 18
    LSRTM Iteration 19
    LSRTM Iteration 20

```python
#NBVAL_SKIP
plt.figure()
plt.plot(history)
plt.xlabel('Iteration number')
plt.ylabel('Misift value Phi')
plt.title('Convergence')
plt.show()
```

```python
def plot_image(data, vmin=None, vmax=None, colorbar=True, cmap="gray"):
    """
    Plot image data, such as RTM images or FWI gradients.

    Parameters
    ----------
    data : ndarray
        Image data to plot.
    cmap : str
        Choice of colormap. Defaults to gray scale for images as a
        seismic convention.
    """
    domain_size = 1.e-3 * np.array(model.domain_size)
    extent = [model.origin[0], model.origin[0] + domain_size[0],
              model.origin[1] + domain_size[1], model.origin[1]]
    plot = plt.imshow(np.transpose(data),
                      vmin=-.05,
                      vmax=.05,
                      cmap=cmap,extent=extent)

    plt.xlabel('X position (km)')
    plt.ylabel('Depth (km)')

    # Create aligned colorbar on the right
    if colorbar:
        ax = plt.gca()
        divider = make_axes_locatable(ax)
        cax = divider.append_axes("right", size="5%", pad=0.05)
        plt.colorbar(plot, cax=cax)
    plt.show()

```

The image below is our first migration. The reflectors are not well focused and backscaterring noise is very strong.

```python
#NBVAL_IGNORE_OUTPUT
slices=tuple(slice(model.nbl,-model.nbl) for _ in range(2))
rtm = image[slices]
plot_image(np.diff(rtm, axis=1))
```

So here we have the LSRTM migration after 20 iterations, it is clear that the reflector is well focused and backscaterring noise has been strongly attenuated.

```python
#NBVAL_SKIP
slices=tuple(slice(model.nbl,-model.nbl) for _ in range(2))
lsrtm = image_up_dev[slices]
plot_image(np.diff(lsrtm, axis=1))
```

Here we have the true reflectivity. The idea in showing it is to demonstrate that the amplitude range of the LSRTM is in the same amplitude range of the true reflectivity.

```python
#NBVAL_IGNORE_OUTPUT
slices=tuple(slice(model.nbl,-model.nbl) for _ in range(2))
dm_true = (solver.model.vp.data**(-2) - model0.vp.data**(-2))[slices]
plot_image(np.diff(dm_true, axis=1))
```

```python
#NBVAL_SKIP
plt.figure(figsize=(8,9))
x = np.linspace(0,1,101)
plt.plot(rtm[50,:],x,color=plt.gray(),linewidth=2)
plt.plot(lsrtm[50,:],x,'r',linewidth=2)
plt.plot(dm_true[50,:],x, 'k--',linewidth=2)

plt.legend(['Initial reflectivity', 'Reflectivity via LSRTM','True Reflectivity'],fontsize=15)
plt.ylabel('Depth (Km)')
plt.xlabel('Amplitude')
plt.gca().invert_yaxis()
plt.show()
```

```python
#NBVAL_SKIP
time = np.linspace(t0, tn, nt)
plt.figure(figsize=(8,9))
plt.ylabel('Time (ms)')
plt.xlabel('Amplitude')
plt.plot(d_syn.data[:, 20],time, 'y', label='Calculated data (Last Iteration)')
plt.plot(d_obs.data[:, 20],time, 'm', label='Observed data')
plt.legend(loc="upper left",fontsize=12)
ax = plt.gca()
ax.invert_yaxis()
plt.show()
```


# Decision Trees and Random Forest

## Decision Trees

### Motivating Decision Trees

Decision trees are extremely intuitive ways of classifying data: you simply ask a series of questions in order to close in on the class label. For example, if you are into value investing, you might think of the following scheme to decide whether you invest into a certain stock or not:

(P/B = Price/Book ratio, P/E = Price/Earnings ratio, PEG = Price/Earnings to growth ratio, E = Equity, D = Debt).

Sources tell me this scheme is not Warren Buffet's key to success - it is obviously an overly simplified illustration. But it serves well to demonstrate the idea behind decision trees in ML. The binary splits narrow down the options. The big question here though is of course what questions we ought to ask to derive the desired answer and in what sequence. This we will discuss in the next sections - first intuitively and then in more rigorous terms.

### Simple Decision Trees Illustrated

To draw up a simple decision tree, we follow these two steps (James et al. (2013)):
* Divide the predictor space (set of possible values for $X_1, X_2, \ldots, X_p$) into $M$ distinct and non-overlapping regions $R_1, R_2, \ldots, R_M$.
* For every observation in $R_m$ we model the response as a constant. This constant is based on a majority vote among the observation in $R_m$.

Let us illustrate this on a two dimensional data set with features $X_1$ and $X_2$. The color of the dots indicate the class label.

Without any split, we have but one region. A decision tree now splits the regions iteratively into predictor spaces $R_m$. This is shown below. The first figure on the left has two spaces, $R_1, R_2$, The second already has four ($R_1, R_2, R_3, R_4$) etc.. The background color indicates the label that the model would assign to a new data point in the respective area. Note that (as in the introductory decision tree figure) each branch can have a sperate number of splits. Some nodes are purer (get split more) than others.

The `depth=n` argument in the figure title refers to the depth of the tree. Nodes that contain only a single class (color) are not further split. All others will be further split until a stopping criterion is reached, e.g.
* all predictor spaces are pure (contain only one class),
* all predictor spaces contain a limited number of points,
* we limit the number of splits through the `depth` argument etc.

### Mathematical Description
#### Maximizing Information Gain

How do we construct the regions $R_1, \ldots, R_M$? Or put in other words: How do we decide on the splitting variables (i.e. $X_1, X_2, \ldots, X_p$), split points and what topology (shape) the tree should have? To explain this we focus on the CART (classification and regression tree) approach of decision trees as implemented in Scikit-learn.

In order to describe the mathematics let us assume we have a data set with $p$ inputs for each of the $N$ observations: $(x_i, y_i)$ for $i = 1, 2, \ldots, N$, with $x_i = (x_{i1}, x_{i2}, \ldots, x_{ip})$.

Most libraries (including Scikit-learn) have implemented binary decision trees, meaning that each node is split into two child nodes. Hence for binary splits at node $m$ we take initial region $R_m$ and select $j$ (of feature $X_j$) and threshold $t_m$ such that the resulting two half-planes

$$\begin{equation}
R_{\text{left}}(R_m; j, t_m) = \{(X, y) \, | \, X_j < t_m \} \qquad \text{and} \qquad R_{\text{right}}(R_m; j, t_m) = \{(X, y) \, | \, X_j \geq t_m \}
\end{equation}$$

maximize the **information gain ($G$)** for any value of $j$ and $t_m$.

Let us define $\theta = (j, t_m)$ to simplify expressions. Given $R_m$ with $N_m$ being the total number of samples in $R_m$ and $n_{\text{left}}$, $n_{\text{right}}$ the number samples in the left ($R_{\text{left}}$) and right ($R_{\text{right}}$) child nodes, respectively, the information gain function $G$ is defined as

$$\begin{equation}
G(R_m; \theta) = H(R_m) - \left( \frac{n_{\text{left}}}{N_m} H(R_{\text{left}}) + \frac{n_{\text{right}}}{N_m} H(R_{\text{right}})\right)
\end{equation}$$

Here, $H()$ is simply a measure of impurity which we will get to shortly. With that, the information gain $G$ is simply the difference between the impurity of the parent node and the sum of the child node impurities. The lower the impurity of the child nodes, the larger information gain we get (Raschka (2015)).

Our optimization task can therefore be formulated as to find parameter $\theta$ that maximizes the following expression at every node $m$:

$$\begin{equation}
\theta^* = \arg \max_{\theta} G(R_m; \theta)
\end{equation}$$

This is done recursively for every node until the stopping criteria is reached (max. depth, max. number of samples in region etc.; see above).

#### Impurity Measures for Classification Trees

There are three common impurity measures (or splitting criteria), of which only the latter two are recommended for growing decision trees: Classification error rate ($H_E$), Gini index ($H_G$), cross-entropy ($H_H$). To discuss them, let us first define the proportion of class $k$ observations in node $m$ (Friedman et al. (2001)):

$$\begin{equation}
\hat{p}_{mk} = \frac{1}{N_m} \sum_{x_i \in R_m} I(y_i = k)
\end{equation}$$

Earlier we mentioned that all observations in region $R_j$ are assigned to the same class following a majority vote. In more formal terms this means that observations in node $m$ are classified to class $k$ for which $k(m) = \arg \max_k \hat{p}_{mk}$.

Now, the impurity measures are defined as follows:

$$\begin{align}
&\text{Classification Error rate: } & H_E(R_m) &= 1 - \max_k (p_{mk}) \\
&\text{Gini Index: } & H_G(R_m) &= \sum_k p_{mk} (1 - p_{mk}) \\
&\text{Cross-Entropy: } & H_E(R_m) &= - \sum_k p_{mk} \, \log(p_{mk})
\end{align}$$

Note that for binary classification tasks (e.g. $y \in \{0, 1\}$), $\log$ in the cross-entropy is usually the [logarithm to the base 2.](https://stackoverflow.com/questions/1859554/what-is-entropy-and-information-gain)

Below figure graphs the three impurity measures with respect to $p$. As mentioned, the classification error rate should not be used as an impurity measure. In practice it is primarily used to prune a tree - a concept we will not discuss here. Cross-entropy is minimal if all samples at a node belong to the same class and maximal if we have a uniform distribution among the classes. Therefore, cross-entropy can be understood as a criterion that attempts to maximize the mutual information in the tree. The Gini-index on the other hand works towards minimizing the probability of misclassification. Similar to entropy it is maximal if classes are evenly distributed and minimal if the vast majority of samples belong to the same class.

In practice both Gini index as well as cross-entropy usually produce similar results and thus it is not advisable to spend much time on evaluating trees using different impurity criteria (Raschka (2015)).

### Advantages and Disadvantages of Decision Trees

Decision trees have several advantages over the other classification approaches discussed so far:

* Intuition is easy to explain
* Trees can be displayed graphically and are easily interpreted even by a layperson
* Trees handle quantitative as well as qualitative features; there's no need to scale the values
* Some people argue that decision trees mirror human thinking very closely, moreso than other approaches.

However, simple decision trees have also disadvantages, some of them are significant:
* Instability of trees/high variance: Small changes in the data often result in very different series of splits
* Decision trees tend to build complex decision boundaries, which often results in overfitting the data
* Low level of predictive accuracy compared other classification approaches discussed in this course.

### Decision Trees in Python

To show how decision trees are applied in Python we once again rely on functions implemented in the Scikit-learn package. This time we will use the `Carseats` data set. It is again a data set that corresponds to James et al. (2013)'s book and contains information on child carseat sales at 400 different stores. `Sales` is the response variable with number of sold units in thousands. All other values are used as features. A detailed description of the data set can be found [here](https://cran.r-project.org/web/packages/ISLR/ISLR.pdf).

```python
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split

df = pd.read_csv('Data/Carseats.csv')
df.head()
```

      Sales
      CompPrice
      Income
      Advertising
      Population
      Price
      ShelveLoc
      Age
      Education
      Urban
      US

      0
      9.50
      138
      73
      11
      276
      120
      Bad
      42
      17
      Yes
      Yes

      1
      11.22
      111
      48
      16
      260
      83
      Good
      65
      10
      Yes
      Yes

      2
      10.06
      113
      35
      10
      269
      80
      Medium
      59
      12
      Yes
      Yes

      3
      7.40
      117
      100
      4
      466
      97
      Medium
      55
      14
      Yes
      Yes

      4
      4.15
      141
      64
      3
      340
      128
      Bad
      38
      13
      Yes
      No

Let us assume you are a financial consultant and work on a market study that discusses appropriate sales channels for child car seats. You know that your client's strategy is to grow the business. Furthermore you learned that to run sales at break even at a sales location the company needs to sell approximately 4'000 units. What feature drives sales and in which stores would you advise your client to offer their product? Here a decision tree helps very much in visualizing the sales driver.

Before we implement the model we prepare the data. Notice that decision trees are invariant to scaling meaning we could, but don't have to scale values. This is true for both quantitative as well as categorical variables. What we need to do though, is transforming categorical values `ShelveLoc, Urban` and `US` into numeric values. We will use pandas `map` method to (a) show an alternative to `pd.factorize` introduced in previous chapters and (b) ensure a mapping that does not confuse (`pd.factorize()` maps a column's first entry to value 0, second to 1 etc. This would mean that `pd.factorize()` would label `Yes` in columns `Urban` or `US` as 0. Yet `Yes` is predominantly represented by a 1. Therefore, with the `map` function we preclude any confusion).

```python
# Create 'BreakEven' column with 1 if Sales >= 4k, else 0
df['BreakEven'] = df.Sales.map(lambda x: 1 if x>=4 else 0)

# Replace category names with numbers
df.ShelveLoc = df.ShelveLoc.map({'Bad':0, 'Medium': 1, 'Good': 2})
df.Urban     = df.Urban.map({'No':0, 'Yes':1})
df.US        = df.US.map({'No':0, 'Yes':1})
print(df.head(3))
```

       Sales  CompPrice  Income  Advertising  Population  Price  ShelveLoc  Age  \
    0   9.50        138      73           11         276    120          0   42
    1  11.22        111      48           16         260     83          2   65
    2  10.06        113      35           10         269     80          1   59

       Education  Urban  US  BreakEven
    0         17      1   1          1
    1         10      1   1          1
    2         12      1   1          1

```python
# Assign features & response to X and y, respectively
X = df.drop(['Sales', 'BreakEven'], axis=1)
y = df.BreakEven

# Train/test split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=0)
```

Now we are in a position to run the `DecisionTreeClassifier`.

```python
from sklearn.tree import DecisionTreeClassifier

tree = DecisionTreeClassifier(max_depth=4)
tree.fit(X_train, y_train)
```

    DecisionTreeClassifier(class_weight=None, criterion='gini', max_depth=4,
                max_features=None, max_leaf_nodes=None,
                min_impurity_decrease=0.0, min_impurity_split=None,
                min_samples_leaf=1, min_samples_split=2,
                min_weight_fraction_leaf=0.0, presort=False, random_state=None,
                splitter='best')

Creating a decision tree is as simple as that. From the above output we see that per default the function will use the Gini index as criterion. The only argument we set is the maximal depth. Alternatively we could define the maximum tree nodes, the minimum number of samples required to split an internal node or the minimum number of samples required to be at a leaf node. There are more options and it is best to check the [documentation page](http://scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeClassifier.html#sklearn.tree.DecisionTreeClassifier) to get a thorough understanding of the available options.

As with all other Scikit-learn functions, we can again call the usual performance metrics. The interpretation is as discussed in chapter 8.

```python
# Load metrics sublibrary
from sklearn import metrics

# Print performance metrics
print('True proportion of sales >= 4k: ', y.sum() / y.shape[0])
print('Train score: ', tree.score(X_train, y_train))
print('Test score: ', tree.score(X_test, y_test))
print(37*'-')

# Confusion matrix
y_pred = tree.predict(X_test)
print('Confusion matrix: \n',
      metrics.confusion_matrix(y_test, y_pred))

# Manual confusion matrix as pandas DataFrame
confm = pd.DataFrame({'Predicted sales >=4k': y_pred,
                      'True sales >=4k': y_test})
confm.replace(to_replace={0:'No', 1:'Yes'}, inplace=True)
print(confm.groupby(['True sales >=4k','Predicted sales >=4k']).size().unstack('Predicted sales >=4k'))
```

    True proportion of sales >= 4k:  0.91
    Train score:  0.94375
    Test score:  0.8875
    -------------------------------------
    Confusion matrix:
     [[ 0  4]
     [ 5 71]]
    Predicted sales >=4k   No   Yes
    True sales >=4k
    No                    NaN   4.0
    Yes                   5.0  71.0

### Visualizing Decision Trees

Unfortunately, neither `matplotlib` nor `sklearn` have a plotting function integrated to visualize decision trees. However, with the help of additional libraries/packages, Python is able to create a decision tree figure detailing each decision step. To plot these decision trees, we have to rely on third party software. Getting things to run as expected has (at least in my case) proven to be a rather cumbersome experience. To spare you this experience, follow these steps (which at the time of this writing in December 2017 have solved the problem):

* Download and install Graphviz from [the web](http://www.graphviz.org/download/). Mac users select the `.pkg` file corresponding to your system version (Snow Leopard, Lion etc.). Windows users select the `.msi` file. Run the execution file and follow the instructions.
* Type in `!pip list` into an input field in a Jupyter notebook. This will list all installed Python packages (and its versions). Check if you have a package called `graphviz`. If yes, proceed with next step. If not, type `pip install graphviz` into a new command line. This should install the needed Python package.

```python
!pip list
```

    alabaster (0.7.10)
    anaconda-client (1.6.5)
    anaconda-navigator (1.6.8)
    anaconda-project (0.8.0)
    asn1crypto (0.22.0)
    astroid (1.5.3)
    astropy (2.0.2)
    babel (2.5.0)
    backports.shutil-get-terminal-size (1.0.0)
    beautifulsoup4 (4.6.0)
    bitarray (0.8.1)
    bkcharts (0.2)
    blaze (0.11.3)
    bleach (2.0.0)
    bokeh (0.12.7)
    boto (2.48.0)
    Bottleneck (1.2.1)
    CacheControl (0.12.3)
    certifi (2017.7.27.1)
    cffi (1.10.0)
    chardet (3.0.4)
    click (6.7)
    cloudpickle (0.4.0)
    clyent (1.2.2)
    colorama (0.3.9)
    comtypes (1.1.2)
    conda (4.4.8)
    conda-build (3.0.22)
    conda-verify (2.0.0)
    contextlib2 (0.5.5)
    cryptography (2.0.3)
    cycler (0.10.0)
    Cython (0.26.1)
    cytoolz (0.8.2)
    dask (0.15.2)
    datashape (0.5.4)
    decorator (4.1.2)
    distlib (0.2.5)
    distributed (1.18.3)
    docutils (0.14)
    entrypoints (0.2.3)
    et-xmlfile (1.0.1)
    fastcache (1.0.2)
    filelock (2.0.12)
    Flask (0.12.2)
    Flask-Cors (3.0.3)
    gevent (1.2.2)
    glob2 (0.5)
    graphviz (0.8.1)
    greenlet (0.4.12)
    h5py (2.7.0)
    heapdict (1.0.0)
    html5lib (0.999999999)
    idna (2.6)
    imageio (2.2.0)
    imagesize (0.7.1)
    ipykernel (4.6.1)
    ipython (6.1.0)
    ipython-genutils (0.2.0)
    ipywidgets (7.0.0)
    isort (4.2.15)
    itsdangerous (0.24)
    jdcal (1.3)
    jedi (0.10.2)
    Jinja2 (2.9.6)
    jsonschema (2.6.0)
    jupyter-client (5.1.0)
    jupyter-console (5.2.0)
    jupyter-core (4.3.0)
    jupyterlab (0.27.0)
    jupyterlab-launcher (0.4.0)
    lazy-object-proxy (1.3.1)
    llvmlite (0.20.0)
    locket (0.2.0)
    lockfile (0.12.2)
    lxml (3.8.0)
    MarkupSafe (1.0)
    matplotlib (2.0.2)
    mccabe (0.6.1)
    menuinst (1.4.8)
    mistune (0.7.4)
    mpmath (0.19)
    msgpack-python (0.4.8)
    multipledispatch (0.4.9)
    navigator-updater (0.1.0)
    nbconvert (5.3.1)
    nbformat (4.4.0)
    networkx (1.11)
    nltk (3.2.4)
    nose (1.3.7)
    notebook (5.0.0)
    numba (0.35.0+10.g143f70e)
    numexpr (2.6.2)
    numpy (1.13.1)
    numpydoc (0.7.0)
    odo (0.5.1)
    olefile (0.44)
    openpyxl (2.4.8)
    packaging (16.8)
    pandas (0.20.3)
    pandas-datareader (0.5.0)
    pandocfilters (1.4.2)
    partd (0.3.8)
    path.py (10.3.1)
    pathlib2 (2.3.0)
    patsy (0.4.1)
    pep8 (1.7.0)
    pickleshare (0.7.4)
    Pillow (4.2.1)
    pip (9.0.1)
    pkginfo (1.4.1)
    ply (3.10)
    progress (1.3)
    prompt-toolkit (1.0.15)
    psutil (5.2.2)
    py (1.4.34)
    pycodestyle (2.3.1)
    pycosat (0.6.3)
    pycparser (2.18)
    pycrypto (2.6.1)
    pycurl (7.43.0)
    pydotplus (2.0.2)
    pyflakes (1.5.0)
    Pygments (2.2.0)
    pylint (1.7.2)
    pyodbc (4.0.17)
    pyOpenSSL (17.2.0)
    pyparsing (2.2.0)
    PySocks (1.6.7)
    pytest (3.2.1)
    python-dateutil (2.6.1)
    pytz (2017.2)
    PyWavelets (0.5.2)
    pywin32 (221)
    PyYAML (3.12)
    pyzmq (16.0.2)
    QtAwesome (0.4.4)
    qtconsole (4.3.1)
    QtPy (1.3.1)
    requests (2.18.4)
    requests-file (1.4.1)
    requests-ftp (0.3.1)
    rope (0.10.5)
    ruamel-yaml (0.11.14)
    scikit-image (0.13.0)
    scikit-learn (0.19.0)
    scipy (0.19.1)
    seaborn (0.8)
    setuptools (36.5.0.post20170921)
    simplegeneric (0.8.1)
    singledispatch (3.4.0.3)
    six (1.10.0)
    snowballstemmer (1.2.1)
    sortedcollections (0.5.3)
    sortedcontainers (1.5.7)
    Sphinx (1.6.3)
    sphinxcontrib-websupport (1.0.1)
    spyder (3.2.3)
    SQLAlchemy (1.1.13)
    statsmodels (0.8.0)
    sympy (1.1.1)
    tables (3.4.2)
    tblib (1.3.2)
    testpath (0.3.1)
    toolz (0.8.2)
    tornado (4.5.2)
    traitlets (4.3.2)
    typing (3.6.2)
    unicodecsv (0.14.1)
    urllib3 (1.22)
    wcwidth (0.1.7)
    webencodings (0.5.1)
    Werkzeug (0.12.2)
    wheel (0.29.0)
    widgetsnbextension (3.0.2)
    win-inet-pton (1.0.1)
    win-unicode-console (0.5)
    wincertstore (0.2)
    wrapt (1.10.11)
    xlrd (1.1.0)
    XlsxWriter (0.9.8)
    xlwings (0.11.4)
    xlwt (1.3.0)
    zict (0.1.2)

    DEPRECATION: The default format will switch to columns in the future. You can use --format=(legacy|columns) (or define a format=(legacy|columns) in your pip.conf under the [list] section) to disable this warning.

* Finally, we need to make sure that Graphviz' `.exe` file was added to your system `PATH`. [This is an environmental variable that tells the computer which directories to search for executable files (i.e., ready-to-run programs) in response to commands issued by a user](http://www.linfo.org/path_env_var.html). How do you know if Graphviz is on your `PATH`? Run below code to find out. (Alternatively you might open a shell window and on Windows call `$env:path.split(";")` or on a Mac `echo $PATH$`).

```python
# On a Windows PC (--> remove # to have output)
%echo %PATH%'

# On a Mac
#%echo $PATH
```

    C:\Program Files\ImageMagick-7.0.5-Q16;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\System32\WindowsPowerShell\v1.0\;C:\Program Files\MiKTeX 2.9\miktex\bin\x64\;C:\Program Files\Git\cmd;C:\Anaconda;C:\Anaconda\Library\mingw-w64\bin;C:\Anaconda\Library\usr\bin;C:\Anaconda\Library\bin;C:\Anaconda\Scripts;C:\Users\Ben Zimmermann\AppData\Local\Microsoft\WindowsApps;C:\Users\Ben Zimmermann\AppData\Local\GitHubDesktop\bin;C:\Program Files (x86)\Graphviz2.38\bin;;%USERPROFILE%\AppData\Local\Microsoft\WindowsApps'

* You see that in my case, Graphviz2.38 is the last entry on the list. In case it is missing on your machine, add graphviz to the `PATH`, by following the steps described [in this discussion on StackOverflow](https://stackoverflow.com/questions/18438997/why-is-pydot-unable-to-find-graphvizs-executables-in-windows-8). But be careful: This might cause some problems in the background with the Spyder IDE ([see discussion here](https://github.com/ContinuumIO/anaconda-issues/issues/1666)).

Having sorted this Graphviz installation out, we are now able to proceed with plotting the decision tree. Of the function's available input arguments (as in below's code snippet), `filled=True, rounded=True, class_names=['Loss', 'BrEven']` and `feature_names=X.columns.values` are optional but help in making the image visually more appealing by adding color, rounded box edges, showing the class labels counts at each node, and displaying the feature name according to the majority vote at the respective node. The plot below helps understand this.

```python
import graphviz
from sklearn.tree import export_graphviz

# Create decision tree object
dot_data = export_graphviz(tree, filled=True, rounded=True,
                           class_names=['Loss', 'BrEven'],
                           feature_names=X.columns.values,
                           out_file=None)

# Visualize/Plot graph
graph = graphviz.Source(dot_data)
graph
```

The above decision tree plot helps us understand how the algorithm decides on a class. The coloring indicates the label. What we learn is that the driving factor for the given training set is the shelve location and after that the product's selling price. If shelve location is 0 (bad) we end up in the left side of the tree. Is the shelve location medium or good we run through the right hand side. `Gini` represents the resulting Gini index at that node, `samples` is the number of observations that end up in the respective node, `values` indicates the true label, e.g. 2nd node from the bottom-left `[4, 40]` means of the 44 samples 4 are of label `Loss` and 40 belong to `BrEven`. Following the majority vote, the decision tree algorithm labels all 44 samples in this node to class 'BrEven'.

If you wish to save the decision tree to a `.pdf` or `.png` file, make sure to have the package `pydotplus` installed. You can use the same code snippet from above:
* With `!pip list` check if the package is already installed
* If not, use `!pip install pydotplus` to install the package

Then you are ready to save the output.

```python
from pydotplus import graph_from_dot_data

# Convert graph to dot-file
graph = graph_from_dot_data(dot_data)

# Here I safe 'graph' to the 'Graphics' folder as png
# If pdf is prefered, replace both '...png' with '...pdf'
graph.write_png('Graphics/0210_DT_Carseats.png');
```

### Decision Trees and Cross Validation

We have mentioned above that one of the major disadvantages of decision trees is the risk of overfitting. Having discussed cross validation in the previous chapter, we ought to ask the question how cross validation can be used to prevent overfitting. The answer to this seemingly trivial question might be more complex than initially thought. If we apply a $k$-fold cross validation and generate $k$ decision tree for every fold in the training set, then these $k$ tree will most certainly look different from fold to fold. Each of the $k$ decision trees will still suffer from overfitting. Furthermore, CV does not tell us anything about which of the $k$ tree we are supposed to select for prediction, and ultimately, this ought to be our goal. If CV produces 10 different trees but doesn't tell us which one of these to choose, we have gained nothing. Choosing the one tree (among the $k$) with (for example) the lowest error rate will not do it as this approach is most probably flawed: such a model is based on even less information than what would be available through the full training set and in general models with more information beat those with less. So CV will not help us on this end.

However, CV is still of use. To recapitulate the idea behind CV: Fundamentally, the purpose of cross validation is not to help select a particular instance of the $k$ decision trees but rather to qualify the model, i.e. to provide metrics such as error rate etc. which in turn can be useful in asserting the level of precision one can expect from the model. Therefore, CV comes into play when we are tuning the model to find the optimal hyperparameter. As an example: usually we do not know the optimal tree form. Does it generalize best when it has depth 2, 5, 10? Or is our stopping criterion to have no more than 5, 10, 20 observation per region $R_j$? Here we can run different parameter in combination with CV and this, thus, will provide an answer on how well the model will generalize to new data - given the set of hyperparameter.

#### Cross Validation
Below we show two setups to do this: with loops or grid search. The first approach follows what we learned in the previous chapter on cross validation.

```python
# Max depth
maxDepth = np.array([1, 2, 5, 10])

# Minimum number of samples required to split any internal node
minSamplesNode = np.array([2, 5, 10, 20])

# The minimum number of samples required to be at a leaf/terminal node
minSamplesLeaf = np.array([2, 5, 10, 20])
```

```python
# Import necessary functions
from sklearn.model_selection import StratifiedKFold, cross_val_score

# Create k-Fold CV object
kFold = StratifiedKFold(n_splits=10, random_state=10)

# Loop through maxDept values, run CV and print results
for i in maxDepth:
    tree = DecisionTreeClassifier(max_depth=i, random_state=0)
    scrs = cross_val_score(tree, X_train, y_train, cv=kFold)
    print('Score (depth ={0: 3.0f}): {1: .3f} +/- {2: .3f}'.format(i, np.mean(scrs), np.std(scrs)))

print(50*'-')

# Loop through minSamplesNode values, run CV and print results
for i in minSamplesNode:
    tree = DecisionTreeClassifier(min_samples_split=i, random_state=0)
    scrs = cross_val_score(tree, X_train, y_train, cv=kFold)
    print('Score (min sample at node ={0: 3.0f}): {1: .3f} +/- {2: .3f}'.format(i, np.mean(scrs), np.std(scrs)))

print(50*'-')

# Loop through minSamplesNode values, run CV and print results
for i in minSamplesLeaf:
    tree = DecisionTreeClassifier(min_samples_leaf=i, random_state=0)
    scrs = cross_val_score(tree, X_train, y_train, cv=kFold)
    print('Score (min sample at leaf ={0: 3.0f}): {1: .3f} +/- {2: .3f}'.format(i, np.mean(scrs), np.std(scrs)))
```

    Score (depth =  1):  0.900 +/-  0.011
    Score (depth =  2):  0.866 +/-  0.050
    Score (depth =  5):  0.857 +/-  0.043
    Score (depth = 10):  0.851 +/-  0.070
    --------------------------------------------------
    Score (min sample at node =  2):  0.851 +/-  0.070
    Score (min sample at node =  5):  0.851 +/-  0.080
    Score (min sample at node = 10):  0.863 +/-  0.065
    Score (min sample at node = 20):  0.850 +/-  0.047
    --------------------------------------------------
    Score (min sample at leaf =  2):  0.854 +/-  0.080
    Score (min sample at leaf =  5):  0.866 +/-  0.050
    Score (min sample at leaf = 10):  0.869 +/-  0.047
    Score (min sample at leaf = 20):  0.869 +/-  0.056

Based on the output we can conclude that we get better scores with fewer nodes. As for the min sample at a node/leaf, the differences are too small to judge. However, what we can also conclude is that this is a fairly cumbersome process. Three separate loops to find the optimal hyperparameter. Furthermore, these three loops just check for one criterion, but what if we were interested in **all possible combinations**? Maybe we get better results if we combine them - we only know if we check. And, as you should be expecting by now, there's a convenient way of doing this: via grid search.

#### Grid Search
The approach of grid search is fairly simple: it's a brute-force search paradigm where we specify a list of values for different hyperparameters. The algorithm evaluates the model performance for each combination of hyperparameter to obtain the optimal combination of values from this set (Raschka (2015)). As usual we use a code example to show how this works.

```python
from sklearn.model_selection import GridSearchCV

# Define the hyperparameter values to be tested
param_grid = {'criterion': ['gini', 'entropy'],
              'max_depth': maxDepth,
              'min_samples_split': minSamplesNode,
              'min_samples_leaf': minSamplesLeaf}

# Run brute-force grid search
gs = GridSearchCV(estimator=DecisionTreeClassifier(random_state=0),
                  param_grid=param_grid,
                  scoring='accuracy',
                  cv=kFold, n_jobs=-1)
gs = gs.fit(X_train, y_train)
print(gs.best_score_)
print(gs.best_params_)
```

    0.9
    {'criterion': 'gini', 'max_depth': 1, 'min_samples_leaf': 2, 'min_samples_split': 2}

Using the preciding code, we train and tune a `DecisionTreeClassifier` on the given parameter grid. For this we define a dictionary called `param_grid` and apply this to the `GridSearchCV`. Using the training data we obtain the score of the best-performing model via the `best_score_` attribute (here based on the accuracy measure) and the corresponding parameter via the `best_params_`.

As it turns out, we are unable to increase the accuracy score by combining stopping criterions. The best result is indeed when we use a max. depth of 1. If a combination of the three criterions (or the entropy criterion) were to yield better results, it would mean that `min_samples_leaf` and/or `min_samples_split` would have values $\geq$ than the minimum value of 2.

It is important to note that this result should not deceive you to believe that overfitting with decision trees is a myth. Here we seem to come across a rare exception where a prune tree of depth 1 yields the best performance. In general, decision trees have much better training results on deeper grown trees. Hence the risk of overfitting.

> Note that grid search might be a convenient and powerful way of tuning hyperparameter but because it is a brute-force approach it is computationally very expensive. Depending on the number of processors you run your script on and the task at hand this might take substantial time. If, for whatever reasons, this is not feasible, the `RandomizedSearchCV` class might be a feasible alternative. This class draws random parameter from sampling distributions with a specified budget. See [the documentation for more details](http://scikit-learn.org/stable/modules/grid_search.html#randomized-parameter-optimization).

Finally, to estimate the performance of these parameter on the independent test dataset, we can run these three lines:

```python
# Extract best parameter
clf = gs.best_estimator_

# Fit model given best parameter
clf.fit(X_train, y_train)

# Print out score on Test dataset
print('Test accuracy: {0: .4f}'.format(clf.score(X_test, y_test)))
```

    Test accuracy:  0.9500

## Random Forest
### Turning Weaknesses into Strengths

CV is so valuable because it provides reliable information on how well a model generalizes. Recall that given a set of $n$ independent observations $Z_1, Z_2, \ldots, Z_n$, each with variance $\sigma^2$, the variance of the mean $\bar{Z}$ of the observations is given by $\sigma^2/n$ (see appendix of the script). This shows that if we average a set of (independent) performance metrics (e.g. classification error), we actually reduce the variance of this error. And in doing so, we increase the validity of said metric.

If we now extend this idea to not only assessing performance metrics but also predicting outcomes, we enter the field of **ensemble models**. These models produce $k$ independent predictions (e.g. trees) and assign the class label based on a majority vote of the $k$ outcomes. In doing so we lower the variance without compromising on the low bias of decision trees. Therefore it is easy to see that ensemble methods have proven to be extremely valuable, especially in the field of decision trees. The most prominent ensemble algorithm with respect to decision trees is called 'Random Forest'.

Let us first define the steps of a random forest model (Raschka (2015, p. 90)):

1. Draw a random bootstrap sample of size $n$ (randomly choose $n$ samples from the training set with replacement)
2. Grow a decision tree from the bootstrap sample. At each node:
    1. Randomly select $m$ features without replacement (with $m < p$)
    2. Split the node using the feature that provides the best split according to the objective function (e.g. by maximizing the information gain)
3. Repeat steps 1. and 2. $B$ times
4. Aggregate the $B$ predictions (of each tree) and assign the class label by majority vote

Step two above might seem odd at first: Why would we restrict the model to only choose from a subset $m$ of features (instead of selecting from the complete set $p$)? This is best explained with James et al. (2013, p. 320):

> "Suppose that there is one very strong predictor in the data set, along with a number of other moderately strong predictors. Then in the collection of bagged trees, most or all of the trees will use this strong predictor in the top split. Consequently, all of the bagged trees will look quite similar to each other. Hence the predictions from the bagged trees will be highly correlated. Unfortunately, averaging many highly correlated quantities does not lead to as large of a reduction in variance as averaging many uncorrelated quantities. [...] Random forests overcome this problem by forcing each split to consider only a subset of the predictors. Therefore, on average $(p - m)/p$ of the splits will not even consider the strong predictor, and so other predictors will have more of a chance. We can think of this process as *decorrelating* the trees, thereby making the average of the resulting trees less variable and hence more reliable."

On a side note: There exists a predecessor algorithm that works similar to random forests except that it sets $m=p$ in step 2.1 per default. Python has it implement as `BaggingClassifier()`. Since random forests improves on the problem of correlated features, it is today clearly the preferred approach. For this reason we will only discuss the random forest implementation.

### Selecting Random Forest's Hyperparameter

Though the interpretability of a random forest does not meet the simplicity of a simple decision tree, a big advantage is that we do not have to worry that much about choosing good hyperparameter values. We have three primary values to set:
* The size $n$ of the bootstrap (step 1)
* The subset $m$ of possible features (step 2)
* The number of iterations $B$ (step 3)

Typically, the larger number of trees $B$, the better the performance of our random forest classifier. But this of course comes at the expense of (potentially significant) increased computational costs. Additionally, the marginal improvement decreases as the number of trees is increased, i.e. at a certain point the cost in computation time will outgrow the benefit in prediction accuracy from more trees. In the Scikit-learn implementation, this hyperparameter is steered through the `n_estimators` argument.

The feature subset size ($m$) to consider at each node is typically set to $m = \sqrt{p}$, that is, the number of predictors considered at each split is approximately equal to the square root of the total number of predictors $p$. Scikit-learn uses the `max_feature` argument to control for it.

Finally, via the size $n$ of the bootstrap we control the bias-variance trade-off. A large value for $n$ will decrease randomness and thus such a model is more likely to overfit. On the other hand, preventing overfitting by selecting smaller values come at the expense of the model predictive performance. And since predictive accuracy is what we are most interested in, the vast majority of random forest implementations, including the `RandomForestClassifier` implementation in Scikit-learn, have set the bootstrap sample size $n$ per default to the number of samples in the original training set. This provides a good bias-variance trade-off (Raschka (2015)).

### Random Forest in Scikit-Learn

There is an easily accessible implementation of a random forest classifier in Scikit-learn that we can use. For [a description of the available hyperparameter please check again the function's documentation](http://scikit-learn.org/stable/modules/generated/sklearn.ensemble.RandomForestClassifier.html). It is also left to the reader to investigate how CV and/or grid-search can improve the performance. For this the same steps as explained for the `DecisionTreeClasifier` function can be applied.

```python
from sklearn.ensemble import RandomForestClassifier

# Create classifier object and fit it to data
forest = RandomForestClassifier(criterion='gini', random_state=0, n_jobs=-1)
forest.fit(X_train, y_train)
```

    RandomForestClassifier(bootstrap=True, class_weight=None, criterion='gini',
                max_depth=None, max_features='auto', max_leaf_nodes=None,
                min_impurity_decrease=0.0, min_impurity_split=None,
                min_samples_leaf=1, min_samples_split=2,
                min_weight_fraction_leaf=0.0, n_estimators=10, n_jobs=-1,
                oob_score=False, random_state=0, verbose=0, warm_start=False)

```python
# Print test score
print('Test accuracy: {0: .4f}'.format(clf.score(X_test, y_test)))
```

    Test accuracy:  0.9500

# Further Ressources

In writing this notebook, many ressources were consulted. For internet ressources the links are provided within the textflow above and will therefore not be listed again. Beyond these links, the following ressources were consulted and are recommended as further reading on the discussed topics:

* Friedman, Jerome, Trevor Hastie, and Robert Tibshirani, 2001, *The Elements of Statistical Learning* (Springer, New York, NY).
* James, Gareth, Daniela Witten, Trevor Hastie, and Robert Tibshirani, 2013, *An Introduction to Statistical Learning: With Applications in R* (Springer Science & Business Media, New York, NY).
* Müller, Andreas C., and Sarah Guido, 2017, *Introduction to Machine Learning with Python* (O’Reilly Media, Sebastopol, CA).
* Raschka, Sebastian, 2015, *Python Machine Learning* (Packt Publishing Ltd., Birmingham, UK)
* Vanderplas, Jake, 2016, *Python Data Science Handbook* (O'Reilly Media, Sebastopol, CA)


# PharmSci 175/275 (UCI)
## What is this??
The material below is Lecture 2 (on Energy Minimization) from Drug Discovery Computing Techniques, PharmSci 175/275 at UC Irvine.
Extensive materials for this course, as well as extensive background and related materials, are available on the course GitHub repository: [github.com/mobleylab/drug-computing](https://github.com/mobleylab/drug-computing)

This material is a set of slides intended for presentation with RISE as detailed [in the course materials on GitHub](https://github.com/MobleyLab/drug-computing/tree/master/uci-pharmsci/lectures/energy_minimization). While it may be useful without RISE, it will also likely appear somewhat less verbose than it would if it were intended for use in written form.

# Energy landscapes and energy minimization

Today: Energy landscapes, energy functions and energy mimimization

### Instructor: David L. Mobley

### Contributors to today's materials:
- David L. Mobley (UCI)
- David Wych (Mobley lab, UCI)
- [Previous contributions](https://engineering.ucsb.edu/~shell/che210d/) from M. Scott Shell (UCSB); some images used here are drawn from his materials.

## Energy landscapes provide a useful conceptual device

### Energy landscapes govern conformations and flexibility

### Chemistry takes place on energy landscapes

- Reactions and barriers
- Conformational change
- Binding/association

### Energy landscapes govern dynamics and thermodynamics

- Reaction rates
- Equilibrium properties

## Often, we are interested in exploring the energy landscape

A potential, $U(\bf{r^N})$ describes the energy as a function of the coordinates of the particles; here ${\bf r}$ is the coordinates, **boldface** denotes it is a vector, and the superscript denotes it is coordinates of all $N$ particles in the system.

### Landscapes have many features, including
- Global minimum, the most stable state (caveat: entropy)
- Local minima: other stable/metastable states

### We can't visualize 3N dimensions, so we often project onto fewer dimensions

### Background: Vector notation

- For a single particle, we have coordinates $x$, $y$, and $z$, or $x_1$, $y_1$, and $z_1$ if it is particle 1
- We might write these as $(-1, 3, 2)$ for example, if $x=-1$, $y=3$, $z=2$.
- For even two particles, we have $x_1$ and $x_2$, $y_1$ and $y_2$, etc.
- Writing out names of coordinates becomes slow, e.g. $f(x_1, y_1, z_1, x_2, y_2, z_2, ... z_N)$
- We simplify by writing $f({\bf r}^N)$ and remember that ${\bf r}^N$ really means:
\begin{equation}
{\bf r}^N=
\begin{bmatrix}
x_1 & y_1 & z_1 \\
x_2 & y_2 & z_2 \\
... & ... & ... \\
x_N & y_N & z_N \\
\end{bmatrix}
\end{equation}

### A concrete example
Imagine we have two particles with particle 1 having coordinates $x_1 = 1$, $y_1 = 3$, and $z_1 = 2$, and particle 2 having coordinates $x_2 = -1$, $y_2 = 3$, $z_2 = -1$. That would give us an array like this:
\begin{equation}
{\bf r}^N=
\begin{bmatrix}
1 & 3 & 2 \\
-1 & 3 & -1 \\
\end{bmatrix}
\end{equation}

### In Python, we'd store that as a numpy array

```python
import numpy as np
r_N = np.array( [[1, 3, 2], [-1, 3, -1]], float)
print('y coordinate of first particle is %.2f' % r_N[0,1])
```

    y coordinate of first particle is 3.00

We could compute the distance between particles as
$d = \sqrt{(x_1-x_2)^2 + (y_1-y_2)^2 + (z_1-z_2)^2}$

You could code that up directly in Python, or you can use array operations:

```python
d = np.sqrt( ((r_N[0,:]-r_N[1,:])**2).sum() )
print(d)
```

    3.60555127546

### Let's let this sink in for a minute

We're using the notation $
{\bf r}^N$ as shorthand to refer to the x, y, and z positions of all of the particles in a system. This is actually an array, or a matrix, of particle coordinates:

\begin{equation}
{\bf r}^N=
\begin{bmatrix}
1 & 3 & 2 \\
-1 & 3 & -1 \\
\end{bmatrix}
\end{equation}

Each row of that matrix has the x, y, and z coordinates of an atom in the system. **We will use this concept heavily in the Energy Minimization assignment*.

## Forces are properties of energy landscapes, too

The force is the slope (technically, gradient):

$f_{x,i} = -\frac{\partial U({\bf r^N})}{\partial x_i}$, $f_{y,i} = -\frac{\partial U({\bf r^N})}{\partial y_i}$, $f_{z,i} = -\frac{\partial U({\bf r^N})}{\partial z_i}$

As shorthand, this may be written ${\bf f}^N = -\frac{\partial U({\bf r^N})}{\partial {\bf r^N}}$ or ${\bf f}^N = -\nabla \cdot U({\bf r^N})$ where the result, ${\bf f}^N$, is an Nx3 array (matrix, if you prefer)

If energy function is pairwise additive, can evaluate via summing individual interactions -- force on atom k is
\begin{equation}
{\bf f}_k = \sum_{j\neq k} \frac{ {\bf r}_{kj}}{r_{kj}}  \frac{\partial}{\partial r_{kj}} U(r_{kj})
\end{equation} where ${\bf r_{kj}} = {\bf r}_j - {\bf r_k}$. Note not all force calculations are necessary: ${\bf f}_{kj} = -{\bf f}_{jk}$

## The matrix of second derivatives is called the Hessian and distinguishes minima from saddle points

\begin{equation}
{\bf H}({\bf r}^N)=
\begin{bmatrix}
\frac{d^2 U({\bf r^N}) }{ d x_1^2} & \frac{d^2 U({\bf r^N}) }{ dx_1 d y_1} & ... & \frac{d^2 U({\bf r^N}) }{ dx_1 d z_1} \\
\frac{d^2 U({\bf r^N}) }{ d y_1 d x_1} & \frac{d^2 U({\bf r^N}) }{ d y_1^2} & ... & \frac{d^2 U({\bf r^N}) }{ dy_1 d z_N}\\
... & ... & ... & ... \\
\frac{d^2 U({\bf r^N}) }{ d z_N d x_1} & \frac{d^2 U({\bf r^N}) }{ d z_N d y_1} & ... & \frac{d^2 U({\bf r^N}) }{ dz_N^2}\\
\end{bmatrix}
\end{equation}

## Types of stationary points can be distiguished from derivatives

- Stationary points have zero force on each particle: $ \nabla \cdot U({\bf r^N}) = {\bf 0}$
- These can be minima or maxima
- Minima have negative curvature in all directions (restoring force is towards the minimum)

## Energy landscapes have *lots* of minima

For an Lennard-Jones 38 cluster:

See also Doye, Miller, and Wales, J. Chem. Phys. 111: 8417 (1999)

- They have a disconnectivity graph shows minima for 13 LJ atoms
- To move between two minima, have to go to point where lines from two minima reach same energy
- 1467 distinct minima for 13 atoms!
- This is a different system, more atoms, far more minima

Note related ["Python energy landscape explorer" (PELE)](https://github.com/pele-python/pele)

(Image source https://pele-python.github.io/pele/disconnectivity_graph.html, CC-BY license, by the Pele authors: https://github.com/pele-python/pele/graphs/contributors)

Here's a bonus disconnectivity graph:

(Image source https://commons.wikimedia.org/wiki/File:Fedg.png#filelinks, by Snd0, CC-BY-SA 4.0)

## We care a lot about finding minima

- As noted, gives a first guess about most stable states
- Minima are stable structures, point of contact with experiment
- Initial structures need to be relaxed so forces are not too large
  - Remove strained bonds, atom overlaps
- Minimization is really optimization:
  - If you can find the minimum of $U$, you can find the minimum of $-U$
  - Same techniques apply to other things, i.e. finding set of parameters that minimizes an error, etc.

### Findining minima often becomes a numerical task, because analytical solutions become impractical very quickly

Consider $U(x,y) = x^2 + (y-1)^2$ for a single particle in a two dimensional potential. Finding $\nabla\cdot U = 0$ yields:

$2x = 0$ and $2(y-1)=0$ or $x=0$, $y=1$

simple enough

But in general, N dimensions means N coupled, potentially nonlinear equations. Consider
\begin{equation}
U = x^2 z^2 +x (y-1)^2 + xyz + 14y + z^3
\end{equation}
Setting the derivatives to zero yields:

$0 = 2xz^2 + (y-1)^2 +yz$

$0 = 2x(y-1) + xz + 14$

$0 = 2x^2z + xy + 3z^2$

**Volunteers??** It can be solved, but not fun.

**And this is just for a single particle in a 3D potential**, so we are typically forced to numerical minimizations, even when potential is analytic

### Energy minimization is a sub-class of the more general problem of finding roots

Common problem: For some $f(x)$, find values of $x$ for which $f(x)=0$

Many equations can be re-cast this way. *i.e.*, if you need to solve $g(x) = 3$, define $f(x) = g(x)-3$ and find $x$ such that $f(x)=0$

If $f(x)$ happens to be the force, this maps to energy minimization

As a consequence: Algorithms used for energy minimization typically have broader application to finding roots

### Let's check out a toy minimization problem to see how this would work
Here we'll set up a simple function to represent an energy landscape in 1D, and play with energy minimizing on that landscape.

```python
#Import pylab library we'll use
import scipy.optimize
#Get pylab ready for plotting in this notebook
%pylab inline

#Define a range of x values to look at in your plot
xlower = -5 #Start search at xlower
xupper = 5 #End search at xupper
#Define a starting guess for the location of the minimum
xstart = 0.01
#Create an array of x values for our plot, starting with xlower
#and running to xupper with step size 0.01
xvals = np.arange( xlower, xupper, 0.01)
```

    Populating the interactive namespace from numpy and matplotlib

```python
#Define and the function f we want to minimize
def f(x):
    return 10*np.cos(x)+x**2-3.
#Store function values at those x values for our plot
fvals = f(xvals)

#Do our minimization ("line search" of sorts), store results to 'res'
res = scipy.optimize.minimize(f, xstart) # Apply canned minimization algorithm from scipy
```

```python
#Make a plot of our function over the specified range
plot(xvals, fvals, 'k-') #Use a black line to show the function
plot(res.x, f(res.x), 'bo') #Add the identified minimum to the plot as a blue circle
plot(xstart, f(xstart), 'ro') #Add starting point as red circle

#Add axis labels
xlabel('x')
ylabel('f(x)')
```

### Sandbox section

Try adjusting the above to explore what happens if you alter the starting conditions or the energy landscape or both. You might try:
- Change the starting point (`xstart`) so it is slightly to the left or slightly to the right
- Change the starting point so it is far up the wall to the left or the right
- Change the energy landscape to alter its shape, perhaps adding a term proportional to `+x`. Can you make it so one of the wells is a local minimum, perhaps by altering the coefficient of this term?
- If you adjust the starting point further, can you make the blue ball get stuck in a local minimum? Can you make it so it still finds the global minimum?

## Steepest descents is a simple minimization algorithm that always steps as far as possible along the direction of the force
Take ${\bf f}^N = -\frac{\partial U({\bf r}^N)}{\partial {\bf r}^N}$, then:
1. Move in direction of last force until the minimum *in that direction* is found
2. Compute new ${\bf f}_i^N $ for iteration $i$, perpendicular to previous force

Repeat until minimum is found

**Limitations**:

Oscillates in narrow valleys; slow near minimum.

### Reminder: Here we're using vector notation for forces and positions

${\bf f}^N$ is the force on all of the atoms, as an array, where each row of the array is the force vector on that atom.

$U({\bf r}^N)$ is the potential energy, as a function of the positions of all of the atoms.

These use the same vector and array notation we introdued above for ${\bf r}^N$.

### Steepest descents oscillates in narrow valleys and is slow near the minimum

(Illustration, P.A. Simonescu, [Wikipedia](https://en.wikipedia.org/wiki/Gradient_descent#/media/File:Banana-SteepDesc.gif), [CC-BY-SA](https://creativecommons.org/licenses/by-sa/3.0/))

### Another illustration further highlights this

(In this case, steepest *ascents*, but it's just the negative...)

(Images public domain: [source](https://upload.wikimedia.org/wikipedia/commons/d/db/Gradient_ascent_%28contour%29.png), [source](https://upload.wikimedia.org/wikipedia/commons/6/68/Gradient_ascent_%28surface%29.png))

## A line search can make many minimization methods more efficient

A line search is an efficient way to find a minimum along a particular direction
- Line search: Bracket minimum
  - Start with initial set of coordinates ${\bf r}$ and search direction ${\bf v}$ that is downhill
  - Generate pairs of points a distance $d$ and $2d$ along the line (${\bf r} + d{\bf v}, {\bf r}+2d{\bf v}$)
     1. If the energy at the further point is higher than the energy at the nearer point, stop
     2. Otherwise, move the pair of points $d$ further along the line and go back to 1.

### To finish a line search, identify the minimum precisely

- Fit a quadratic to our 3 points (initial, and two bracket points)
- Guess that minimum is at the zero of the quadratic, call this point 4.
- Fit a new quadratic using points 2, 3, and 4, and move to the zero.
- Repeat until the energy stops changing within a given tolerance

## To do better than steepest descents, let's consider its pros and cons

- Good to go in steepest direction initially
- It’s a good idea to move downhill
- It is initially very fast (see Leach Table 5.1)
- But steepest descent overcorrects

## SciPy does't implement steepest descents because of these issues
SciPy has lots of functions and tools for [optimization](https://docs.scipy.org/doc/scipy/reference/optimize.html), but it doesn't even implement steepest descents because of poor reliability.
However, the `Nelder-Mead` minimization method applies a downhill simplex method which also is less than ideal, so let's play around with that a bit. First, let's make a suitable landscape:

```python
#Define and the function f we want to minimize
def f(arr):
    return 10*np.cos(arr[0])+arr[0]**2-3.+arr[1]**2

#Define a range of x, y values to look at in your plot
# NOTE IF YOU WANT TO ADJUST THESE YOU NEED TO RE-RUN ALL THREE CELLS (shift-enter)
xlower, ylower = -5, -5 #Start search at xlower and yupper
xupper, yupper = 5, 5 #End search at xupper and yupper
#Define a starting guess for the location of the minimum
xstart, ystart = -1.0, 1.0
#Create an array of coordinates for our plot
xvals = np.arange( xlower, xupper, 0.01)
yvals = np.arange( ylower, yupper, 0.01)

# Make a grid of x and y values
xx, yy = np.meshgrid(xvals, yvals)
```

```python
#Store function values at those x and y values for our plot
fvals = f(np.array([xx,yy]))

colors = np.linspace(0,1,len(xx))
#Create 9''x7'' figure
plt.figure(figsize=(9, 7))
#Plot the Energy Landscape with a colorbar to the side
plt.contourf(xvals, yvals, fvals, 10)
plt.colorbar()
plt.show()
```

```python
res = scipy.optimize.minimize(f, np.array((xstart,ystart)), method='Nelder-Mead', options={'return_all':True})

plt.figure(figsize=(9, 7))
plt.contourf(xvals, yvals, fvals, 10)
plt.colorbar()

# Plot path of minimization
xvals = [ entry[0] for entry in res.allvecs ]
yvals = [ entry[1] for entry in res.allvecs ]

colors = np.linspace(0,1,len(xvals))
plt.scatter(xvals, yvals, c=colors, cmap='spring', s=5, marker = "o")
plt.plot(xvals, yvals, 'm-')
```

## Conjugate gradient (CG) works like steepest descent but chooses a different direction

- Start with an initial direction ${\bf v}$ that is downhill, move in that direction until a minimum is reached.

- Compute a new direction using ${\bf v}_i = {\bf f}^N_i + \gamma_i {\bf v}_{i-1}^N$ where $\gamma_i = \frac{({\bf f}^N_i-{\bf f}^N_{i-1}){\bf f}^N_i}{{\bf f}^N_{i-1} {\bf f}^N_{i-1}}$; note $\gamma_i$ is a scalar.

Note that by ${\bf f}^N {\bf f}^N$ we mean vector multiplication, not a matrix multiplication; that is:
\begin{equation}
{\bf f}^N {\bf f}^N = f^2_{x,1} + f^2_{y,1} + f^2_{z,1} + f^2_{x,2} + ... + f^2_{z, N}
\end{equation}

(In Python, this can be coded by multiplying ${\bf f}\times {\bf f}$, where ${\bf f}$ are arrays, and taking the sum of the result; for normal matrix multiplication of arrays one would use a dot product)

$\gamma_i$ is designed so that the new direction is *conjugate* to the old direction so that the new step does not undo any of the work done by the old step (causing oscillation)

### Conjugate gradient (CG) is more efficient

- Green: Steepest descent (with optimal step size)
- Red: Conjugate gradient

Ideally takes at most $M$ steps, where $M$ is the number of degrees of freedom, often $3N$, though in practice even small precision errors make it take longer than this.

(Image: [Wikipedia](https://en.wikipedia.org/wiki/Conjugate_gradient_method#/media/File:Conjugate_gradient_illustration.svg), public domain. Oleg Alexandrov.)

## Let's look at a CG example

```python
#Define and the function f we want to minimize
def f(arr):
    return 10*np.cos(arr[0])+arr[0]**2-3.+arr[1]**2

#Define a range of x, y values to look at in your plot
# NOTE IF YOU WANT TO ADJUST THESE YOU NEED TO RE-RUN ALL THREE CELLS (shift-enter)
xlower, ylower = -5, -5 #Start search at xlower and yupper
xupper, yupper = 5, 5 #End search at xupper and yupper
#Define a starting guess for the location of the minimum
xstart, ystart = -1.0, 1.0
#Create an array of coordinates for our plot
xvals = np.arange( xlower, xupper, 0.01)
yvals = np.arange( ylower, yupper, 0.01)

# Make a grid of x and y values
xx, yy = np.meshgrid(xvals, yvals)

#Store function values at those x and y values for our plot
fvals = f(np.array([xx,yy]))

colors = np.linspace(0,1,len(xx))
```

```python
res = scipy.optimize.minimize(f, np.array((xstart,ystart)), method='CG', options={'return_all':True})
```

```python
plt.figure(figsize=(9, 7))
plt.contourf(xvals, yvals, fvals, 10)
plt.colorbar()

# Plot path of minimization
xvals = [ entry[0] for entry in res.allvecs ]
yvals = [ entry[1] for entry in res.allvecs ]

colors = np.linspace(0,1,len(xvals))
plt.scatter(xvals, yvals, c=colors, cmap='spring', s=5, marker = "o")
plt.plot(xvals, yvals, 'm-')
```

## More advanced minimization methods can have considerable advantages

- Newton-Raphson: Based on Taylor expansion of potential around zero
  - reaches minimum in one step for quadratic potentials
  - converges to minima and saddle points, so requires initial moves to reach minima
  - slow because requires Hessian at every step
- Quasi-Newton methods approximate the Hessian to go faster
  - L-BFGS is a popular quasi-Newton method

```python
#Define and the function f we want to minimize
def f(arr):
    return 10*np.cos(arr[0])+arr[0]**2-3.+arr[1]**2

#Define a range of x, y values to look at in your plot
xlower, ylower = -5, -5 #Start search at xlower and yupper
xupper, yupper = 5, 5 #End search at xupper and yupper
#Define a starting guess for the location of the minimum
xstart, ystart = -0.1, 1.0
#Create an array of coordinates for our plot
xvals = np.arange( xlower, xupper, 0.01)
yvals = np.arange( ylower, yupper, 0.01)

xx, yy = np.meshgrid(xvals, yvals)

#Store function values at those x and y values for our plot
fvals = f(np.array([xx,yy]))
```

```python
res = scipy.optimize.minimize(f, np.array((xstart,ystart)), method='BFGS', options={'return_all':True})

plt.figure(figsize=(9, 7))
plt.contourf(xvals, yvals, fvals, 10)
plt.colorbar()

xvals = [ entry[0] for entry in res.allvecs ]
yvals = [ entry[1] for entry in res.allvecs ]

colors = np.linspace(0,1,len(xvals))
plt.scatter(xvals, yvals, c=colors, cmap='spring', s=5, marker = "o")
plt.plot(xvals, yvals, 'm-')
```

## Locating global minima is challenging and often involves combination of techniques

- Need some way to cross barriers, in addition to minimization
- Often combine techniques
  - i.e. Molecular Dynamics or Monte Carlo + minimization
- Normal mode analysis can be used to identify collective motions
  - Once identified, system can be moved in those directions

### When the number of dimensions is small, global minima can be found by repeated trials

One can simply do lots of minimizations from random starting points and find the global minimum sometimes, if there are not too many minima in total. See e.g. this [local and global minima](http://people.duke.edu/~ccc14/sta-663-2016/13_Optimization.html#Local-and-global-minima) discussion.

As an exercise, you might try to find the global minimum of this function by picking random starting points and minimizing many times (for solutions, see that link):

```python
def f(x, offset):
    return -np.sinc(x-offset)
x = np.linspace(-20, 20, 100)
plt.plot(x, f(x, 5));
```

```python

```


# Free Body Diagram for Rigid Bodies

Renato Naville Watanabe

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

## Equivalent systems

Figure from [this notebook](FreeBodyDiagram.ipynb)

## Constraints

Examples of constraints

## 1) Horizontal fixed bar

The resultant force being applied to the bar is:

\begin{equation}
    \vec{\bf{F}} = -mg\hat{\bf{j}} + \vec{\bf{F_1}}
\end{equation}

And the total moment in the z direction around the point O is:

\begin{equation}
    \vec{\bf{M_O}} = \vec{\bf{r_{C/O}}}\times-mg\hat{\bf{j}} + \vec{\bf{M}}
\end{equation}

The vector from the point O to the point C is given by $\vec{\bf{r_{C/O}}} =\frac{l}{2}\hat{\bf{i}}$.

As the bar is fixed, all the accelerations are zero. So we can find the forces and the moment at the restraint.

\begin{equation}
    \vec{\bf{F}} = \vec{\bf{0}} \rightarrow -mg\hat{\bf{j}} + \vec{\bf{F_1}} = \vec{\bf{0}} \rightarrow   \vec{\bf{F_1}} = mg\hat{\bf{j}}
\end{equation}

\begin{equation}
    \vec{\bf{M_O}} = \vec{\bf{0}} \rightarrow  \vec{\bf{r_{C/O}}}\times-mg\hat{\bf{j}} + \vec{\bf{M}} = \vec{\bf{0}} \rightarrow \frac{l}{2}\hat{\bf{i}}\times-mg\hat{\bf{j}} + \vec{\bf{M}} = \vec{\bf{0}} \rightarrow -\frac{mgl}{2}\hat{\bf{k}} + \vec{\bf{M}} = \vec{\bf{0}} \rightarrow \vec{\bf{M}} = \frac{mgl}{2}\hat{\bf{k}}
\end{equation}

## 2) Rotating ball with drag force

A basketball has a mass $m = 0.63$ kg and radius equal $R = 11.5$ cm.

The resultant force being applied at the ball is:

\begin{equation}
    \vec{\bf{F}} = -mg\hat{\bf{j}} - b_l\vec{\bf{v}} + b_m\omega\hat{\bf{k}}\times \vec{\bf{v}} = -mg\hat{\bf{j}} - b_l\frac{d\vec{\bf{r}}}{dt} = -mg\hat{\bf{j}} - b_l\frac{d\vec{\bf{r}}}{dt}
\end{equation}

\begin{equation}
    \vec{\bf{M_C}} = - b_r\omega\hat{\bf{k}}=- b_r\frac{d\theta}{dt}\hat{\bf{k}}
\end{equation}

\begin{equation}
    \frac{d\vec{\bf{H_C}}}{dt} = I_{zz}^{C}\frac{d^2\theta}{dt^2}\hat{\bf{k}}
\end{equation}

So, by the Newton-Euler laws:

\begin{equation}
    \frac{d\vec{\bf{H_C}}}{dt}=\vec{\bf{M_C}} \rightarrow I_{zz}^{C}\frac{d^2\theta}{dt^2} = - b_r\frac{d\theta}{dt}
\end{equation}

and

\begin{equation}
    m\frac{d\vec{\bf{r}}}{dt}=\vec{\bf{F}} \rightarrow \frac{d\vec{\bf{r}}}{dt} = -g\hat{\bf{j}} - \frac{b_l}{m}\frac{d\vec{\bf{r}}}{dt}
\end{equation}

So, we can split the differential equations above in three equations:

\begin{equation}
    \frac{d^2\theta}{dt^2} = - \frac{b_r}{I_{zz}^{C}}\frac{d\theta}{dt}
\end{equation}

\begin{equation}
    \frac{d^2x}{dt^2} = - \frac{b_l}{m}\frac{dx}{dt}
\end{equation}

\begin{equation}
    \frac{d^2y}{dt^2} = -g - \frac{b_l}{m}\frac{dy}{dt}
\end{equation}

To solve these equations numerically, we can split each of these equations in first-order equations. The first-order equations we be written in a matrix form:

\begin{equation}
    \left[\begin{array}{c}\frac{d\omega}{dt}\\\frac{dv_x}{dt}\\\frac{dv_y}{dt}\\\frac{d\theta}{dt}\\\frac{dx}{dt}\\\frac{dy}{dt} \end{array}\right] = \left[\begin{array}{c}- \frac{b_r}{I_{zz}^{C}}\omega\\- \frac{b_l}{m}v_x\\-g - \frac{b_l}{m}v_y\\\omega\\v_x\\v_y\end{array}\right]
\end{equation}

```python
m = 0.63
R = 0.12
I = 2.0/3*m*R**2

bl = 0.5
br = 0.001
g = 9.81

x0 = 0
y0 = 2

v0 = 9.5
angle = 51*np.pi/180.0

vx0 = v0*np.cos(angle)
vy0 = v0*np.sin(angle)

dt = 0.001
t = np.arange(0, 2.1, dt)

x = x0
y = y0
vx = vx0
vy = vy0

omega = 100
theta = 0

r = np.array([x,y])
ballAngle = np.array([theta])

state = np.array([omega, vx, vy, theta, x, y])

while state[4]<=4.6:

    dstatedt = np.array([-br/I*state[0],-bl/m*state[1], -g-bl/m*state[2],state[0], state[1], state[2] ])
    state = state + dt * dstatedt

    r = np.vstack((r, [state[4], state[5]]))
    ballAngle = np.vstack((ballAngle, [state[3]]))

plt.figure()
plt.plot(r[0:-1:50,0], r[0:-1:50,1], 'o', color = np.array([1, 0.6,0]), markerSize =10)
plt.plot(np.array([4, 4.45]), np.array([3.05, 3.05]))
for i in range(len(r[0:-1:50,0])):
    plt.plot(r[i*50,0]+np.array([-0.05*(np.cos(ballAngle[i*50])-np.sin(ballAngle[i*50])), 0.05*(np.cos(ballAngle[i*50])-np.sin(ballAngle[i*50]))]),
             r[i*50,1] + np.array([-0.05*(np.sin(ballAngle[i*50])+np.cos(ballAngle[i*50])), 0.05*(np.sin(ballAngle[i*50])+np.cos(ballAngle[i*50]))]),'k')
plt.ylim((0,4.5))
plt.show()
print(state[0])
```

    <IPython.core.display.Javascript object>

    82.15108205121524

## 3) Rotating ball with drag force  and magnus force

A basketball has a mass $m = 0.63$ kg and radius equal $R = 11.5$ cm.

The resultant force being applied at the ball is:

\begin{equation}
    \vec{\bf{F}} = -mg\hat{\bf{j}} - b_l\vec{\bf{v}} + b_m\omega\hat{\bf{k}}\times \vec{\bf{v}} = -mg\hat{\bf{j}} - b_l\frac{d\vec{\bf{r}}}{dt} + b_m\omega\hat{\bf{k}}\times \frac{d\vec{\bf{r}}}{dt} = -mg\hat{\bf{j}} - b_l\frac{d\vec{\bf{r}}}{dt} + b_m\omega\frac{dx}{dt}\hat{\bf{j}}-b_m\omega\frac{dy}{dt}\hat{\bf{i}}
\end{equation}

\begin{equation}
    \vec{\bf{M_C}} = - b_r\omega\hat{\bf{k}}=- b_r\frac{d\theta}{dt}\hat{\bf{k}}
\end{equation}

\begin{equation}
    \frac{d\vec{\bf{H_C}}}{dt} = I_{zz}^{C}\frac{d^2\theta}{dt^2}\hat{\bf{k}}
\end{equation}

So, by the Newton-Euler laws:

\begin{equation}
    \frac{d\vec{\bf{H_C}}}{dt}=\vec{\bf{M_C}} \rightarrow I_{zz}^{C}\frac{d^2\theta}{dt^2} = - b_r\frac{d\theta}{dt}
\end{equation}

and

\begin{equation}
    m\frac{d\vec{\bf{r}}}{dt}=\vec{\bf{F}} \rightarrow \frac{d\vec{\bf{r}}}{dt} = -g\hat{\bf{j}} - \frac{b_l}{m}\frac{d\vec{\bf{r}}}{dt} + \frac{b_m}{m}\omega\frac{dx}{dt}\hat{\bf{j}}-\frac{b_m}{m}\omega\frac{dy}{dt}\hat{\bf{i}}
\end{equation}

So, we can split the differential equations above in three equations:

\begin{equation}
    \frac{d^2\theta}{dt^2} = - \frac{b_r}{I_{zz}^{C}}\frac{d\theta}{dt}
\end{equation}

\begin{equation}
    \frac{d^2x}{dt^2} = - \frac{b_l}{m}\frac{dx}{dt} -\frac{b_m}{m}\omega\frac{dy}{dt}
\end{equation}

\begin{equation}
    \frac{d^2y}{dt^2} = -g - \frac{b_l}{m}\frac{dy}{dt} + \frac{b_m}{m}\omega\frac{dx}{dt}
\end{equation}

To solve these equations numerically, we can split each of these equations in first-order equations. The first-order equations we be written in a matrix form:

\begin{equation}
    \left[\begin{array}{c}\frac{d\omega}{dt}\\\frac{dv_x}{dt}\\\frac{dv_y}{dt}\\\frac{d\theta}{dt}\\\frac{dx}{dt}\\\frac{dy}{dt} \end{array}\right] = \left[\begin{array}{c}- \frac{b_r}{I_{zz}^{C}}\omega\\- \frac{b_l}{m}v_x - \frac{b_m}{m}\omega v_y\\-g - \frac{b_l}{m}v_y + \frac{b_m}{m}\omega v_x\\\omega\\v_x\\v_y\end{array}\right]
\end{equation}

```python
m = 0.63
R = 0.12
I = 2.0/3*m*R**2

bl = 0.5
br = 0.001
bm = 16/3*np.pi**2*R**3
g = 9.81

x0 = 0
y0 = 2

v0 = 9.4
angle = 50*np.pi/180.0

vx0 = v0*np.cos(angle)
vy0 = v0*np.sin(angle)

dt = 0.001
t = np.arange(0, 2.1, dt)

x = x0
y = y0
vx = vx0
vy = vy0

omega = 30
theta = 0

r = np.array([x,y])
ballAngle = np.array([theta])

state = np.array([omega, vx, vy, theta, x, y])

while state[4]<=4.6:

    dstatedt = np.array([-br/I*state[0],-bl/m*state[1]-bm/m*state[2], -g-bl/m*state[2]+bm/m*state[1],state[0], state[1], state[2] ])
    state = state + dt * dstatedt

    r = np.vstack((r, [state[4], state[5]]))
    ballAngle = np.vstack((ballAngle, [state[3]]))

plt.figure()
plt.plot(r[0:-1:50,0], r[0:-1:50,1], 'o', color = np.array([1, 0.6,0]), markerSize =10)
plt.plot(np.array([4, 4.45]), np.array([3.05, 3.05]))
for i in range(len(r[0:-1:50,0])):
    plt.plot(r[i*50,0]+np.array([-0.05*(np.cos(ballAngle[i*50])-np.sin(ballAngle[i*50])), 0.05*(np.cos(ballAngle[i*50])-np.sin(ballAngle[i*50]))]),
             r[i*50,1] + np.array([-0.05*(np.sin(ballAngle[i*50])+np.cos(ballAngle[i*50])), 0.05*(np.sin(ballAngle[i*50])+np.cos(ballAngle[i*50]))]),'k')
plt.ylim((0,4.5))
plt.show()
print(state[0])
```

    <IPython.core.display.Javascript object>

    24.446442150144627

## 3) Force platform

## 4) Pendulum

The moment around the fixed point is:

\begin{equation}
    \vec{\bf{M_O}} = \vec{\bf{r_{cm/O}}} \times (-mg\hat{\bf{j}})
\end{equation}

The resultant force applied in the bar is:

\begin{equation}
        \vec{\bf{F}} = -mg\hat{\bf{j}} + \vec{\bf{F_1}}
\end{equation}

The angular momentum derivative of the bar around point O is:

\begin{equation}
    \frac{d\vec{\bf{H_O}}}{dt} = I_{zz}^{cm} \frac{d^2\theta}{dt^2} \hat{\bf{k}}  + m \vec{\bf{r_{cm/O}}} \times \vec{\bf{a_{cm}}}
\end{equation}

The vector from point O to the center of mass is:

\begin{equation}
    \vec{\bf{r_{cm/O}}} = \frac{l}{2}\sin{\theta}\hat{\bf{i}}-\frac{l}{2}\cos{\theta}\hat{\bf{j}}
\end{equation}

The position of the center of mass is, considering the point O as the origin, equal to $\vec{\bf{r_{cm/O}}}$. So, the center of mass acceleration is obtained by deriving it twice.

\begin{equation}
    \vec{\bf{v_{cm}}} = \frac{\vec{\bf{r_{cm/O}}}}{dt} = \frac{l}{2}(\cos{\theta}\hat{\bf{i}}+\sin{\theta}\hat{\bf{j}})\frac{d\theta}{dt} \rightarrow \vec{\bf{a_{cm}}} = \frac{l}{2}(-\sin{\theta}\hat{\bf{i}}+\cos{\theta}\hat{\bf{j}})\left(\frac{d\theta}{dt}\right)^2 + \frac{l}{2}(\cos{\theta}\hat{\bf{i}}+\sin{\theta}\hat{\bf{j}})\frac{d^2\theta}{dt^2}
\end{equation}

So, the moment around the point O:

\begin{equation}
    \vec{\bf{M_O}} = \left(\frac{l}{2}\sin{\theta}\hat{\bf{i}}-\frac{l}{2}\cos{\theta}\hat{\bf{j}}\right) \times (-mg\hat{\bf{j}}) = \frac{-mgl}{2}\sin{\theta}\hat{\bf{k}}
\end{equation}

And the derivative of the angular momentum is:

\begin{equation}
    \frac{d\vec{\bf{H_O}}}{dt} = I_{zz}^{cm} \frac{d^2\theta}{dt^2}\hat{\bf{k}}  + m \frac{l}{2}(\sin{\theta}\hat{\bf{i}}-\cos{\theta}\hat{\bf{j}}) \times \left[ \frac{l}{2}(-\sin{\theta}\hat{\bf{i}}+\cos{\theta}\hat{\bf{j}})\left(\frac{d\theta}{dt}\right)^2 + \frac{l}{2}(\cos{\theta}\hat{\bf{i}}+\sin{\theta}\hat{\bf{j}})\frac{d^2\theta}{dt^2}    \right] = I_{zz}^{cm} \frac{d^2\theta}{dt^2}\hat{\bf{k}}  + m \frac{l^2}{4}\frac{d^2\theta}{dt^2} \hat{\bf{k}}  =\left(I_{zz}^{cm} + \frac{ml^2}{4}\right)\frac{d^2\theta}{dt^2} \hat{\bf{k}}
\end{equation}

Now, by using the Newton-Euler laws, we can obtain the differential equation that describes the bar angle along time:

\begin{equation}
    \frac{d\vec{\bf{H_O}}}{dt} = \vec{\bf{M_O}} \rightarrow \frac{-mgl}{2}\sin{\theta} = \left(I_{zz}^{cm} + \frac{ml^2}{4}\right)\frac{d^2\theta}{dt^2} \rightarrow \frac{d^2\theta}{dt^2} = \frac{-2mgl}{\left(4I_{zz}^{cm} + ml^2\right)}\sin{\theta}
\end{equation}

## 5) Inverted Pendulum

The moment around the fixed point is:

\begin{equation}
    \vec{\bf{M_O}} = \vec{\bf{r_{cm/O}}} \times (-mg\hat{\bf{j}})
\end{equation}

The resultant force applied in the bar is:

\begin{equation}
        \vec{\bf{F}} = -mg\hat{\bf{j}} + \vec{\bf{F_1}}
\end{equation}

The angular momentum derivative of the bar around point O is:

\begin{equation}
    \frac{d\vec{\bf{H_O}}}{dt} = I_{zz}^{cm} \frac{d^2\theta}{dt^2} \hat{\bf{k}}  + m \vec{\bf{r_{cm/O}}} \times \vec{\bf{a_{cm}}}
\end{equation}

Until this part, the equations are exactly the same of the pendulum example. The difference is in the kinematics of the bar. Now,the vector from point O to the center of mass is:

\begin{equation}
    \vec{\bf{r_{cm/O}}} = -\frac{l}{2}\sin{\theta}\hat{\bf{i}}+\frac{l}{2}\cos{\theta}\hat{\bf{j}}
\end{equation}

The position of the center of mass of the bar is equal to the vector $\vec{\bf{r_{cm/O}}}$, since the point O is with velocity zero relative to the global reference frame. So the center of mass acceleration can be obtained by deriving this vector twice:

\begin{equation}
    \vec{\bf{v_{cm}}} = \frac{\vec{\bf{r_{cm/O}}}}{dt} = -\frac{l}{2}(\cos{\theta}\hat{\bf{i}}+\sin{\theta}\hat{\bf{j}})\frac{d\theta}{dt} \rightarrow \vec{\bf{a_{cm}}} = \frac{l}{2}(\sin{\theta}\hat{\bf{i}}-\cos{\theta}\hat{\bf{j}})\left(\frac{d\theta}{dt}\right)^2 - \frac{l}{2}(\cos{\theta}\hat{\bf{i}}+\sin{\theta}\hat{\bf{j}})\frac{d^2\theta}{dt^2}
\end{equation}

So, the moment around the point O is:

\begin{equation}
    \vec{\bf{M_O}} = \left(-\frac{l}{2}\sin{\theta}\hat{\bf{i}}+\frac{l}{2}\cos{\theta}\hat{\bf{j}}\right) \times (-mg\hat{\bf{j}}) = \frac{mgl}{2}\sin{\theta} \hat{\bf{k}}
\end{equation}

And the derivative of the angular momentum is:

\begin{equation}
    \frac{d\vec{\bf{H_O}}}{dt} = I_{zz}^{cm} \frac{d^2\theta}{dt^2} \hat{\bf{k}}  + m \left(-\frac{l}{2}\sin{\theta}\hat{\bf{i}}+\frac{l}{2}\cos{\theta}\hat{\bf{j}}\right) \times \left[\frac{l}{2}(\sin{\theta}\hat{\bf{i}}-\cos{\theta}\hat{\bf{j}})\left(\frac{d\theta}{dt}\right)^2 - \frac{l}{2}(\cos{\theta}\hat{\bf{i}}+\sin{\theta}\hat{\bf{j}})\frac{d^2\theta}{dt^2}\right] = I_{zz}^{cm} \frac{d^2\theta}{dt^2} \hat{\bf{k}}  + m \frac{l^2}{4}\frac{d^2\theta}{dt^2}\hat{\bf{k}} = \left(I_{zz}^{cm} +  \frac{ml^2}{4}\right)\frac{d^2\theta}{dt^2}\hat{\bf{k}}
\end{equation}

By using the Newton-Euler laws, we can find the equation of motion of the bar:

\begin{equation}
    \frac{d\vec{\bf{H_O}}}{dt} = \vec{\bf{M_O}} \rightarrow \left(I_{zz}^{cm} +  \frac{ml^2}{4}\right)\frac{d^2\theta}{dt^2} = \frac{mgl}{2}\sin{\theta} \rightarrow \frac{d^2\theta}{dt^2} = \frac{2mgl}{\left(4I_{zz}^{cm} + ml^2\right)}\sin(\theta)
\end{equation}

## 6) Quiet standing

Adapted from [[3]](http://dx.doi.org/10.1371/journal.pcbi.1003944)

The model is very similar to the inverted pendulum shown above. The difference is the muscular moment at the ankle joint. It is usual in Biomechanics to represent the net torque generated by all the muscles on a single joint as a single moment applied to the body.

The process to obtain the equation of motion of the angle is very similar. The moment around the ankle being applied to the body is:

\begin{equation}
    \vec{\bf{M_A}} = \vec{\bf{T_A}} + \vec{\bf{r_{cm/A}}} \times (-m_Bg\hat{\bf{j}})
\end{equation}

And the derivative of the angular momentum is:

\begin{equation}
    \frac{d\vec{\bf{H_A}}}{dt} = I_{zz}^{cm}\frac{d\theta_A}{dt} + m\vec{\bf{r_{cm/A}}} \times \vec{\bf{a_{cm}}}
\end{equation}

To find the kinematics of the bar, we could do the same procedure we have used in the pendulum and inverted pendulum examples, but this time we will use polar coordinates (for a revision on polar coordinates, see [Polar coordinates notebook](PolarCoordinates.ipynb)).

\begin{equation}
    \vec{\bf{r_{cm/A}}} = \vec{\bf{r_{cm}}} = h_G\hat{\bf{e_r}} \rightarrow \vec{\bf{v_{cm}}} = h_G\frac{d\theta_A}{dt}\hat{\bf{e_\theta}} \rightarrow \vec{\bf{a_{cm}}} = -h_G\left(\frac{d\theta_A}{dt}\right)^2\hat{\bf{e_r}} + h_G\frac{d^2\theta_A}{dt^2}\hat{\bf{e_\theta}}
\end{equation}

where $\hat{\bf{e_r}} = -\sin(\theta_A)\hat{\bf{i}} + \cos(\theta_A)\hat{\bf{j}}$ and $\hat{\bf{e_\theta}} = -\cos(\theta_A)\hat{\bf{i}} - \sin(\theta_A)\hat{\bf{j}}$

Having the kinematics computed, we can go back to the moment and derivative of the angular momentum:

\begin{equation}
     \vec{\bf{M_A}} =  \vec{\bf{T_A}} + h_G\hat{\bf{e_r}} \times (-m_Bg\hat{\bf{j}}) = T_A\hat{\bf{k}} + h_Gm_Bg\sin(\theta_A)\hat{\bf{k}}
\end{equation}

\begin{equation}
    \frac{d\vec{\bf{H_A}}}{dt} = I_{zz}^{cm}\frac{d^2\theta_A}{dt^2} \hat{\bf{k}} + mh_G\hat{\bf{e_r}} \times \left(-h_G\left(\frac{d^2\theta_A}{dt^2}\right)^2\hat{\bf{e_r}} + h_G\frac{d^2\theta_A}{dt^2}\hat{\bf{e_\theta}}\right) = I_{zz}^{cm}\frac{d^2\theta_A}{dt^2}\hat{\bf{k}} + mh_G^2\frac{d^2\theta_A}{dt^2}\hat{\bf{k}} = \left(I_{zz}^{cm} + mh_G^2\right)\frac{d^2\theta_A}{dt^2}\hat{\bf{k}}
\end{equation}

By using the Newton-Euler equations, we can now find the equation of motion of the body during quiet standing:

\begin{equation}
    \vec{\bf{M_A}}=\frac{d\vec{\bf{H_A}}}{dt} \rightarrow  \left(I_{zz}^{cm} + mh_G^2\right)\frac{d^2\theta_A}{dt^2} =  T_A + h_Gm_B g\sin(\theta_A) \rightarrow \frac{d^2\theta_A}{dt^2} = \frac{h_Gm_B g}{I_{zz}^{cm} + mh_G^2}\sin(\theta_A)+ \frac{T_A}{I_{zz}^{cm} + mh_G^2}
\end{equation}

## 7) Segway

The horizontal acceleration of the point A of the segway is known. There is a motor at point A that generates a moment $\vec{\bf{T}}$

Considering the  bar, the moment around the point A is:

\begin{equation}
    \vec{\bf{M_A}} = \vec{\bf{r_{cm/A}}} \times (-mg\hat{\bf{j}}) + \vec{\bf{T}}
\end{equation}

And the derivative of the angular momentum around the point A is:

\begin{equation}
        \frac{d\vec{H_A}}{dt} = I_{zz}^{cm}\frac{d^2\theta}{dt^2}+m\vec{\bf{r_{cm/A}}}\times \vec{\bf{a_{cm}}}
\end{equation}

The vector from the point A to the center of mass is:

\begin{equation}
    \vec{\bf{r_{cm/A}}} = \frac{l}{2}\hat{\bf{e_R}}
\end{equation}

and the acceleration of the center of mass is obtained by deriving the position of the center of mass twice:

\begin{equation}
    \vec{\bf{r_{cm}}} = \vec{\bf{r_{A}}} + \vec{\bf{r_{cm/A}}} = \vec{\bf{r_{A}}} + \frac{l}{2}\hat{\bf{e_R}} \rightarrow  \vec{\bf{v_{cm}}} = \vec{\bf{v_{A}}} + \frac{l}{2}\frac{d\theta}{dt}\hat{\bf{e_\theta}} \rightarrow  \vec{\bf{a_{cm}}} = \vec{\bf{a_{A}}} - \frac{l}{2}\left(\frac{d\theta}{dt}\right)^2\hat{\bf{e_R}}+\frac{l}{2}\frac{d^2\theta}{dt^2}\hat{\bf{e_\theta}}
\end{equation}

where $\hat{\bf{e_R}} = -\sin(\theta)\hat{\bf{i}}+\cos(\theta)\hat{\bf{j}}$ and $\hat{\bf{e_\theta}} = -\cos(\theta)\hat{\bf{i}}-\sin(\theta)\hat{\bf{j}}$.

Using the Newton-Euler laws, we can find the equation of motion of the upper part of the segway:

\begin{equation}
    \frac{d\vec{H_A}}{dt} = \vec{\bf{M_A}} \rightarrow  I_{zz}^{cm}\frac{d^2\theta}{dt^2}\hat{\bf{k}}+m\vec{\bf{r_{cm/A}}}\times \vec{\bf{a_{cm}}} =  \vec{\bf{r_{cm/A}}} \times (-mg\hat{\bf{j}}) + \vec{\bf{T}} \rightarrow I_{zz}^{cm}\frac{d^2\theta}{dt^2}+m\frac{l}{2}\hat{\bf{e_R}}\times\left(a_{A}\hat{\bf{i}} - \frac{l}{2}\left(\frac{d\theta}{dt}\right)^2\hat{\bf{e_R}}+\frac{l}{2}\frac{d^2\theta}{dt^2}\hat{\bf{e_\theta}}\right) = ( \frac{l}{2}\hat{\bf{e_R}} \times (-mg\hat{\bf{j}}) + \vec{\bf{T}}) \rightarrow I_{zz}^{cm}\frac{d^2\theta}{dt^2}\hat{\bf{k}}-\frac{ml}{2}a_{A}\cos(\theta)\hat{\bf{k}} +\frac{ml^2}{4}\frac{d^2\theta}{dt^2}\hat{\bf{k}} = \frac{mgl}{2}\sin(\theta)\hat{\bf{k}} + T\hat{\bf{k}}
\end{equation}

So, the equation of motion of the upper part of the segway is:

\begin{equation}
    \frac{d^2\theta}{dt^2} = \frac{mgl}{2\left(I_{zz}^{cm}+\frac{ml^2}{4}\right)}\sin(\theta) + \frac{ml}{2\left(I_{zz}^{cm}+\frac{ml^2}{4}\right)}a_{A}\cos(\theta)+ \frac{T}{\left(I_{zz}^{cm}+\frac{ml^2}{4}\right)}
\end{equation}

## 9) Bars linked by spring and damping

## 8) Double pendulum with actuators

This model can represent, for example, the arm and forearm.

First, we can analyse both bars together.

### Problems

- Solve the problems 6 and 8 from [this Notebook](FreeBodyDiagram.ipynb).

- Solve the problems 16.2.9, 18.2.24, 18.3.26, 18.3.29 from Ruina and Pratap book.

### References

- Ruina A., Rudra P. (2015) [Introduction to Statics and Dynamics](http://ruina.tam.cornell.edu/Book/index.html). Oxford University Press.

- Duarte M. (2017) [Free body diagram](FreeBodyDiagram.ipynb)

- [Elias, L A, Watanabe, R N, Kohn, A F.(2014) Spinal Mechanisms May Provide a Combination of Intermittent and Continuous Control of Human Posture: Predictions from a Biologically Based Neuromusculoskeletal Model. PLOS Computational Biology (Online)](http://dx.doi.org/10.1371/journal.pcbi.1003944)

```python

```

notebooks/.ipynb_checkpoints/FreeBodyDiagramForRigidBodies-checkpoint (rnwatanabe-Inspiron-5457's conflicted copy 2018-04-02).ipynb