 ILI286 - Computación Científica II

 Valores y Vectores Propios

 [[S]cientific [C]omputing [T]eam](#acknowledgements)

 Version: 1.11

# Tabla de Contenidos
* [Introducción](#intro)
* [Marco Teórico](#teo)
* [Algoritmos e Implementaciones](#alg)
    * [Power Iteration](#pi)
    * [Inverse Power Iteration](#invpi)
    * [Rayleigh Quotient Iteration](#rq)
    * [SciPy Eigenvalue](#sp)
    * [Problema de Aplicación](#problema)
* [Acknowledgements](#acknowledgements)

# Introducción

Determinar los valores y vectores propios de una matriz, aporta gran información acerca de las características y propiedades de esta, como también posee gran cantidad de aplicaciones prácticas como: Análisis de convergencia de sistemas dinámicos, PCA (Principal Component Analysis), análisis espectral, Eigenfaces, etc.

Sin embargo la determinación de los valores y vectores propios no es un problema simple. Como ya debe haber estudiado en cursos anteriores, existe un método directo basado en cálculo de las raíces del polinomio característico $p(x)$. Pero este problema resulta ser _mal condicionado_, esto es, a pequeñas variaciones en la matriz $A$ original, existe una gran variación en los resultados de los valores y vectores propios obtenidos (Ver polinomio de Wilkinson, texto guia).

En este notebook estudiaremos un método iterativo conocido como _Power Iteration_ (y sus extensiones), que de modo similar a una iteración de punto fijo, permite obtener numéricamente los eigen(valores/vectores).

# Marco Teórico

La motivación tras PI (Power Iteration) es que la multiplicación por matrices, tiende a "dirigir" a los vectores hacia el vector propio dominante (aquel con valor propio de mayor magnitud).

El algoritmo en cuestión es como sigue:

```python
x = 'Initial guess'
for i in range n_iter:
    u = x / ||x||             #normalization step
    x = dot(A,u)              #power iteration step
    lamb = dot(u, dot(A, u))  #Rayleigh quotient
return x / ||x||
```

en donde se agrega una paso de _normalización_, para evitar que la magnitud del vector aumente sin límite, y el valor propio asociado se obtiene por medio del cociente de Rayleigh:

$$ \lambda = \frac{x^T A x}{x^T x} $$

Para entender porque se de esta convergencia, considere una matriz $A \in \mathbb{R}^{m \times m}$ con valores propios reales $\lambda_1, \lambda_2, \ldots, \lambda_m$ tales que $|\lambda_1| > |\lambda_2| \geq |\lambda_3| \geq \ldots \geq |\lambda_m|$, tales que los vectores propios $\{v_1, v_2, \ldots, v_m \}$ conforman una base de $\mathbb{R}^m$. Sea entonces $x_0$ el _initial guess_, este puede ser expresado como una combinación lineal de los vectores propios $v_k$:

\begin{align}
A x_0 &= c_1 A v_1 + \cdots + c_m A v_m = c_1 \lambda_1 v_1 + \cdots + c_m \lambda_m v_m \\
A^2 x_0 & = c_1 \lambda_1 A v_1 + \cdots + c_m \lambda_m A v_m = c_1 \lambda_1^2 v_1 + \cdots + c_m \lambda_m^2 v_m \\
\vdots &= \vdots \\
A^k x_0 &= c_1 \lambda_1^k v_1 + \cdots + c_m \lambda_m^k v_m
\end{align}

Factorizando $\lambda_1^k$ del último resultado se obtiene:

$$ \frac{A^k x_0}{\lambda_1^k} = c_1 v_1 + c_2 \left(\frac{\lambda_2}{\lambda_1}\right)^k v_2 + \cdots + c_m \left(\frac{\lambda_m}{\lambda_1}\right)^k v_m$$

Dado que $|\lambda_1|>|\lambda_i| \ \ \forall i \neq 1$, a medida que $k \rightarrow \infty$ todos los términos excepto el primero tienden a cero, con razón de convergencia $S \leq |\lambda_2/\lambda_1|$. Obteniendo como resultado un vector que es múltiplo del vector propio dominante.

**Nota**: Para más detalles revisar: _Numerical Analysis, Tymothy Sauer, Chapter 12: Eigenvalues and Singular Values_

# Algoritmos e Implementaciones

### Librerías utilizadas durante la clase

```python
import numpy as np
from scipy import linalg
from matplotlib import pyplot as plt
%matplotlib inline
```

### Matriz y vector de prueba

```python
A = np.array([[1, 0.5],[0.5, 1]])
x = np.array([1.,0.])
A = np.array([[1., 0.5,-0.1],[0.5, 1.,10.0],[2.,3.,5.]])
x = np.array([1.,0.,0.])
print("A =\n",A)
print("x =",x)
```

    A =
     [[ 1.   0.5 -0.1]
     [ 0.5  1.  10. ]
     [ 2.   3.   5. ]]
    x = [1. 0. 0.]

## Power Iteration
A continuación se entrega el código del algoritmo clásico de Power Iteration. Pruebe cambiando las matrices y los parámetros del algoritmo.

```python
def power_iteration(A, x, k, verbose=False):
    """
    Program 12.1 Power iteration
    Computes dominant eigenvector of square matrix
    Input: matrix A, initial (nonzero) vector x, number of steps k
    Output: dominant eigenvalue lam, eigenvector u
    """
    if verbose: print("Power Iteration Method\n%s"%('='*80))
    for j in range(k):
        u = x/np.linalg.norm(x)
        x = np.dot(A, u)
        lam = np.dot(u, x) #not really necessary to compute it at each iteration
        if verbose: print("k=%d, lambda=%+.3f, u=%s"%(j,lam,str(u.T)))
    u = x/np.linalg.norm(x)
    if verbose: print("k=%d, lambda=%+.3f, u=%s\n"%(j+1,lam,str(u.T)))
    return (lam, u)
```

```python
# Testing algorithm
lam, u = power_iteration(A, x, 20, verbose=True)
print("lambda = {0}".format(lam))
print("u (dominant eigenvector) = {0}".format(u))
```

    Power Iteration Method
    ================================================================================
    k=0, lambda=+0.663, u=[1. 0. 0.]
    k=1, lambda=+2.215, u=[0.40573094 0.10328364 0.90813815]
    k=2, lambda=+2.614, u=[0.64137324 0.45291844 0.61927801]
    k=3, lambda=+2.687, u=[0.53087639 0.35121787 0.77124333]
    k=4, lambda=+2.699, u=[0.57670422 0.39828204 0.71329072]
    k=5, lambda=+2.701, u=[0.55828001 0.37971504 0.73765841]
    k=6, lambda=+2.701, u=[0.56585764 0.38737082 0.72783857]
    k=7, lambda=+2.701, u=[0.56278091 0.38426193 0.73186093]
    k=8, lambda=+2.701, u=[0.56403742 0.38553117 0.73022429]
    k=9, lambda=+2.701, u=[0.56352557 0.38501404 0.73089201]
    k=10, lambda=+2.701, u=[0.56373429 0.3852249  0.73061989]
    k=11, lambda=+2.701, u=[0.56364922 0.38513895 0.73073084]
    k=12, lambda=+2.701, u=[0.5636839  0.38517399 0.73068561]
    k=13, lambda=+2.701, u=[0.56366976 0.3851597  0.73070405]
    k=14, lambda=+2.701, u=[0.56367553 0.38516553 0.73069653]
    k=15, lambda=+2.701, u=[0.56367318 0.38516315 0.73069959]
    k=16, lambda=+2.701, u=[0.56367413 0.38516412 0.73069835]
    k=17, lambda=+2.701, u=[0.56367374 0.38516373 0.73069885]
    k=18, lambda=+2.701, u=[0.5636739  0.38516389 0.73069865]
    k=19, lambda=+2.701, u=[0.56367384 0.38516382 0.73069873]
    k=20, lambda=+2.701, u=[0.56367386 0.38516385 0.7306987 ]

    lambda = 2.7014626840392237
    u (dominant eigenvector) = [0.56367386 0.38516385 0.7306987 ]

## Inverse Power Iteration

Una de las complicaciones que tiene el algoritmo anterior, es que sólo permite encontrar el valor y vectores propios dominantes. Luego ¿Cómo encontramos el resto?. Para responder esta pregunta, es necesario examinar dos propiedades importantes:

1. Los valores propios de la matriz inversa $A^{-1}$ son los recíprocos de los valores propios de $A$, es decir: $\lambda_1^{-1}, \lambda_2^{-1}, \ldots , \lambda_m^{-1}$. Los vectores propios de se mantienen inalterados.
2. Los valores propios de la matriz con _shift_ $A - sI$ son: $\lambda_1-s, \lambda_2-s, \ldots, \lambda_m-s$. Del mismo modo, los vectores propios se mantienen inalterados.

**Tarea**: Pruebe estas propiedades!

La idea es entonces realizar un shift $\widetilde{s}$ cercano a algún valor propio $s_k$, y computar PI sobre $(A - \widetilde{s}I)^{-1}$. Luego:

$$ |\lambda_k - \widetilde{s}| < |\lambda_i - \widetilde{s}| \leftrightarrow  \bigg| \frac{1}{\lambda_k - \widetilde{s}} \bigg| > \bigg| \frac{1}{\lambda_i - \widetilde{s}} \bigg| \ \ \forall i \neq k \ $$

entonces $\frac{1}{\lambda_k - \widetilde{s}}$ corresponderá con el vector propio dominante de $(A - \widetilde{s}I)^{-1}$. Notar que por lo enunciado en las propiedades, los vectores propios se mantienen sin alteraciones.

La idea anterior se ve reflejada en el algoritmo implementado a continuación:

```python
def inverse_power_iteration(A, x, s, k, verbose=False):
    """
    Program 12.2 Inverse Power iteration
    Computes eigenvector of square matrix nearest to input s
    Input: matrix A, initial (nonzero) vector x, shift s, number of steps k
    Output: dominant eigenvalue lam, eigenvector of inv(A-sI)
    """
    if verbose: print("Inverse Power Iteration Method\n%s"%('='*80))
    As = A - s*np.eye(*A.shape)
    for j in range(k):
        u = x/np.linalg.norm(x)
        x = np.linalg.solve(As, u)
        lam = np.dot(u.T, x)
        if verbose: print("k=%d, lambda=%+.3f, u=%s"%(j,1./lam+s,str(u.T)))
    u = x/np.linalg.norm(x)
    if verbose: print("k=%d, lambda=%+.3f, u=%s\n"%(j+1,1./lam+s,str(u.T)))
    return (1./lam+s, u)
```

```python
# Testing algoritm
lam, u = inverse_power_iteration(A, x, s=1./4, k=10, verbose=True)
print("lambda = {0}".format(lam))
print("v = {0}".format(u))
```

    Inverse Power Iteration Method
    ================================================================================
    k=0, lambda=+0.667, u=[1. 0. 0.]
    k=1, lambda=+0.708, u=[ 0.83205029 -0.5547002   0.        ]
    k=2, lambda=+0.689, u=[ 0.85215434 -0.5224972  -0.02880359]
    k=3, lambda=+0.692, u=[ 0.84821173 -0.52903451 -0.02567759]
    k=4, lambda=+0.692, u=[ 0.84877374 -0.52810527 -0.02622915]
    k=5, lambda=+0.692, u=[ 0.84868498 -0.52825191 -0.02614794]
    k=6, lambda=+0.692, u=[ 0.84869852 -0.52822953 -0.02616062]
    k=7, lambda=+0.692, u=[ 0.84869643 -0.52823299 -0.02615868]
    k=8, lambda=+0.692, u=[ 0.84869675 -0.52823246 -0.02615898]
    k=9, lambda=+0.692, u=[ 0.84869671 -0.52823254 -0.02615893]
    k=10, lambda=+0.692, u=[ 0.84869671 -0.52823252 -0.02615894]

    lambda = 0.6918800781738075
    v = [ 0.84869671 -0.52823252 -0.02615894]

## Rayleigh Quotient Iteration

Como se analizó anteriormente, PI e _Inverse_ PI tienen convergencia lineal con razón de convergencia $S \approx \frac{\lambda_2}{\lambda_1}$. Además sabemos que _Inverse_ PI converge hacia el valor propio más cercano al shift, y que mientras más cercano sea el shift a tal valor, más rápido se logra la convergencia.

Entonces la idea de RQI es la siguiente: Si en cada iteración se tiene una aproximación del valor propio que andamos buscando, podemos ocupar esta aproximación como shift $s$, y dado que el shift será más cercano al valor propio, se acelerará la convergencia.

Tal valor aproximado es obtenido con el cociente de Rayleigh, y entonces el shift es actualizado con este cociente en cada iteración. Como resultado se produce el siguiente _trade-off_:

1. La convergencia pasa a ser cuadrática (de modo general) y cúbica para matrices simétricas.
2. Sin embargo, se paga el costo de tener que resolver un sistema de ecuaciones diferentes en cada iteración.

A continuación se presenta una implementación del RQI:

```python
def rqi(A, x, k, verbose=False):
    """
    Program 12.3 Rayleigh Quotient Iteration
    Input: matrix A, initial (nonzero) vector x, number of steps k
    Output: eigenvalue lam, eigenvector of inv(A-sI)
    """
    if verbose: print("Rayleigh Quotient Iteration\n%s"%('='*80))
    for j in range(k):
        u = x/np.linalg.norm(x)
        lam = np.dot(u.T, np.dot(A, u))
        try:
            x = np.linalg.solve(A -lam*np.eye(*A.shape), u)
        except numpy.linalg.LinAlgError:
            break
        if verbose: print("k=%d, lambda=%+.3f, u=%s"%(j,lam,str(u.T)))
    u = x/np.linalg.norm(x)
    lam = float(np.dot(u.T, np.dot(A, u)))
    if verbose: print("k=%d, lambda=%+.3f, u=%s\n"%(j+1,lam,str(u.T)))
    return (lam, u)
```

**Preguntas:**
1. ¿Porque es necesario el `try` y `except` en las líneas 11 y 13? ¿Que significa que el sistema no pueda ser resuelto?
2. Como puede observar RQI no recibe shift como parámetro. ¿A cuál valor/vector propio convergerá? ¿Como forzar/guiar a que tienda hacia un valor/vector propio distinto?

```python
# Testing algorithm
lam, v = rqi(A, x, k=2)
print("lambda = {0}".format(lam))
print("v = {0}".format(v))
```

    lambda = 0.24303460773645452
    v = [ 0.64632243 -0.75691367 -0.09669029]

## $\texttt{SciPy}$ Eigenvalue
La librería scipy tiene implementados algoritmos que permite calcular los valores y vectores propios. Las opciones posibles son:

  - En la librería scipy.linalg: eigvals/eigvalsh/eigvals_banded, eig/eigh/eig_banded,

  - En la librería scipy.sparse.linalg: eigen, eigs, eigsh.

En general siempre conviene utilizar las funciones desde scipy y no de numpy. La librería numpy hace un excelente trabajo al permitir el uso de vectores de tipo numérico, pero contiene solo algunos algoritmos numéricos y no necesariamente los más rápidos.

A continuación se muestra como utilizar algunas de estas funciones.

```python
# Full matrices
from scipy import linalg as LA
N = 3
Aux = np.random.rand(N,N)
A = Aux + Aux.T # symmetric, so we'll deal with real eigs.
print(LA.eigvals(A)) # Only the eigenvalues, A not necessarily symmetric
print("*"*80)
print(LA.eigvalsh(A)) # Only the eigenvalues, A symmetric
print("*"*80)
print(LA.eig(A))     # All the eigenvalues and eigenvectors, A not necessarily symmetric
print("*"*80)
print(LA.eigh(A))    # All the eigenvalues and eigenvectors, A symmetric (faster)
print("*"*80)
lambdas, V = LA.eigh(A)    # All the eigenvalues and eigenvectors, A symmetric (faster)
l1 = lambdas[0]
v1 = V[:,0]
print(l1)
print(v1)
print(np.dot(A, v1))
print(l1*v1)
```

    [ 2.70146268+0.j  0.24304501+0.j -1.10128726+0.j]
    ********************************************************************************
    [-1.10128726  0.24304501  2.70146268]
    ********************************************************************************
    (array([ 2.70146268+0.j,  0.24304501+0.j, -1.10128726+0.j]), array([[-0.56367386, -0.64305493, -0.5184131 ],
           [-0.38516384,  0.75983704, -0.52373323],
           [-0.73069871,  0.09554075,  0.67598178]]))
    ********************************************************************************
    (array([-1.10128726,  0.24304501,  2.70146268]), array([[-0.5184131 ,  0.64305493, -0.56367386],
           [-0.52373323, -0.75983704, -0.38516384],
           [ 0.67598178, -0.09554075, -0.73069871]]))
    ********************************************************************************
    -1.101287258356309
    [-0.5184131  -0.52373323  0.67598178]
    [ 0.57092174  0.57678073 -0.74445012]
    [ 0.57092174  0.57678073 -0.74445012]

## Problema de Aplicación

Las matrices simétricas tiene una propiedad muy interesante:

* Los vectores propios de las matrices simétricas son ortogonales entre sí.

En base a lo anterior se propone el siguiente algoritmo para encontrar los primeros $k$ valores/vectores propios:

```python
def kEigenFinder(A, k, p):
    m = A.shape[0]
    lamb = 0.
    v = np.zeros((m,1))
    Lamb = []
    V = []
    for i in range(k):
        A -= lamb*np.dot(v,v.T)
        lamb,v = power_iteration(A, p)
        Lamb.append(lamb)
        V.append(v)
    return Lamb,V
```

1. Justifique la validez de tal procedimiento.
2. Construya una matriz simétrica de $100 \times 100$ y ejecute el `kEigenFinder` sobre tal matriz. Una forma fácil de construir una matriz simétrica es la [matriz de covarianza](https://en.wikipedia.org/wiki/Covariance_matrix):
$$\Sigma_X = \frac{1}{n-1}X^T X$$
donde $X \in \mathbb{R}^{m \times n}$, con $m$ _samples_ y $n$ _features_.

3. Concluya acerca de la utilidad del procedimiento propuesto.

# Acknowledgements
* _Material creado por profesor Claudio Torres_ (`ctorres@inf.utfsm.cl`) _y ayudantes: Alvaro Salinas y Martín Villanueva. DI UTFSM. Abril 2016._

***
### DISCLAIMER ###

El presente notebook ha sido creado para el curso **ILI286 - Computación Científica 2**, del [Departamento de Informática](http://www.inf.utfsm.cl/), [Universidad Técnica Federico Santa María](http://www.utfsm.cl/).

El material ha sido creado por Claudio Torres <ctorres@inf.utfsm.cl> y Sebastian Flores <sebastian.flores@usm.cl>, y es distribuido sin restricciones. En caso de encontrar un error, por favor no dude en contactarnos.

[Update 2015] Se ha actualizado los notebooks a Python3 e includio el "magic" "%matplotlib inline" antes de cargar matplotlib para que los gráficos se generen en el notebook.

[Update 2016] (Martín) Modificaciones mayores al formato original. Agregado contexto: Introducción, marco teórico, explicaciones y tareas. Modificaciones menores en los algoritmos. Agregada la sección de Problema de Aplicación.

[Update 2018] (C.Torres) Using np.linalg.
***


# Assignment 6: Harmonic model

This exercise on the Harmonic model will help you better understand the issue of fundamental frequency ($f_0$) estimation by analyzing several sound examples with harmonic content. There are four parts to this exercise: 1) Estimate fundamental frequency, $f_0$, in an polyphonic audio file, 2) Segmentation of stable note regions in an audio signal using $f_0$, 3) Compute amount of inharmonicity present in a sound, and 4) Improving the implementation of the two way mismatch $f_0$ estimation algorithm.

### Relevant Concepts

__Harmonic model parameters:__ The Harmonic model is used for the analysis of harmonic sounds. The file `harmonicModel.py` provides the code for Harmonic model analysis and synthesis. The key component of the harmonic model is the estimation of the fundamental frequency ($f_0$) and its harmonics. Apart from parameters that have been covered in previous exercises, such as the window, FFT size or the peak picking threshold, we have a few additional parameters used by the harmonic model.

1. `nH`: maximum number of harmonics to be used in the analysis.
2. `maxf0`: maximum $f_0$ frequency in Hz to be considered.
3. `minf0`: minimum $f_0$ frequency in Hz to be considered. Setting the `maxf0` and `minf0` accurately help to narrow down the $f_0$ candidates used by TWM algorithm and lead to better $f_0$ estimation.
4. `f0et`: error threshold in the $f_0$ detection. This is the maximum error allowed in the TWM algorithm. If the TWM mismatch error is larger than `f0et`, no $f_0$ is detected and the TWM algorithm returns $f_0 = 0$ for the frame.
5. `harmDevSlope`: slope of harmonic deviation allowed in the estimated harmonic frequency, compared to a perfect harmonic frequency. This is used to compute the threshold to generate the harmonics.

__Melody representation:__ For computational analysis, melody is represented typically by the pitch (fundamental frequency). The fundamental frequency ($f_0$) is usually estimated in $Hz$ but for a musically meaningful representation, we convert $f_0$ from Hz to Cent. Cent is a logarithmic scale computed as

\begin{equation}
\label{eq:HztoCent}
f_{0,\mathrm{Cents}}=1200\log_{2}\left(\frac{f_{0,\mathrm{Hz}}}{55.0}\right)
\end{equation}

Assuming a tuning frequency of A4 = 440 Hz, the reference frequency used in the Cent scale is the frequency of the note A1 = 55Hz, i.e. 55Hz = 0 Cent.

__Segmentation and transcription:__
Audio segmentation and transcription are two important music information retrieval tasks. Audio segmentation aims to segment the audio into musically meaningful entities. Music Transcription aims to automatically obtain a score-like representation from a music audio piece. Segmentation is often a preprocessing step in transcription. Both these tasks have several different approaches that have been explored.

In this exercise, we will consider a simple approach to note level segmentation of melodies. Given the audio file, we first estimate the pitch (fundamental frequency $f_0$) for the whole file. We then segment the pitch contour into stable regions. The stable regions most likely correspond to notes of the melody. We then have the start and end time stamps of each note played in the melody. A limitation of this approach to segmentation is that it might not work for notes with a vibrato.

You will only implement the segmentation as described above. However, additionally for each segment, given a tuning frequency (say A = 440 Hz), you can obtain the notes played in the melody by quantizing the pitch in each segment to a note - a note level transcription of the melody.

__Inharmonicity:__ In music, inharmonicity is the degree to which the frequencies of the partials depart from integer multiples of the fundamental frequency (harmonic series). An ideal, homogeneous, infinitesimally thin or infinitely flexible string or column of air has exactly harmonic modes of vibration. However, in any real musical instrument, the resonant body that produces the music tone - typically a string, wire, or column of air—deviates from this ideal and has some small or large amount of inharmonicity. You can read more about inharmonicity at http://en.wikipedia.org/wiki/Inharmonicity.

A typical example of an instrument that exhibits inharmonicity is the piano. For the piano, several models have been proposed to obtain the partials of the piano, which can be used to estimate the inharmonicity. One of the models proposed by Fletcher (Harvey Fletcher, "Normal Vibration Frequencies of a Stiff Piano String", J. Acoust. Soc. Am. 36, 203 (1964); http://dx.doi.org/10.1121/1.1918933) is shown in the following equation, where $f_r$ is the frequency of the $r^{\mathrm{th}}$ partial, $f_0$ is the fundamental frequency and $B$ is the inharmonicity coefficient.

\begin{equation}
\label{eq:fletcher}
 f_r = rf_{0}\sqrt{(1+Br^{2})}
\end{equation}

In this exercise, you will measure the inharmonicity in a piano note using the harmonic model. With the estimates of the fundamental frequency $f_0$ and of the harmonics $\mathbf{f}_{est}$ for a frame $l$, we can obtain a measure of inharmonicity as,

\begin{equation}
\label{eq:inharm}
I[l]=\frac{1}{R}\overset{R}{\underset{r=1}{\sum}}\left(\frac{\left|f_{\mathrm{est}}^{r}[l]-r\, f_{0}[l]\right|}{r}\right)
\end{equation}

where $R$ is the number of harmonics (the number of harmonics being used to compute inharmonicity), $f_0[l]$ is the fundamental frequency estimated at the frame $l$ and $f_{\mathrm{est}}^{r}[l]$ is the estimated frequency of the $r^{\mathrm{th}}$ harmonic at the frame. Note that the first harmonic is the fundamental.

We can then compute the mean inharmonicity in a specific time region between the frame indexes $l_1$ and $l_2$ as,

\begin{equation}
I_{\mathrm{mean}} = \frac{1}{l_2-l_1+1}\overset{l_2}{\underset{l=l_1}{\sum}}I[l]
\end{equation}

__TWM algorithm candidate selection:__ The two way mismatch algorithm implemented in sms-tools needs a set of $f_0$ candidates to start with. An easy choice of candidates are the peaks of the magnitude spectrum within a specific range of frequencies. However, this way of choosing $f_0$ candidates fails when there is no peak corresponding to the true $f_0$ value. The generation of $f_0$ candidates can be done better by also including the sub-harmonics of the peak frequencies as $f_0$ candidates.

__Searching numpy arrays:__ Numpy provides an efficient way to search for a specific element(s) of an array that satisfy a given condition. You can use `np.where()` in such cases. e.g. Given a numpy array `a = array([ 0.9193727 ,  0.6359579 ,  0.8335968 ,  0.20568055,  0.13874869])` and you want to extract the indexes of elements less than 0.5, you can use `np.where(a<0.5)[0]`. The function returns `array([3, 4])` corresponding the indexes of the elements in `a` less than 0.5.

## Part 1 - Estimate one fundamental frequency in polyphonic audio signal

Perform a good fundamental frequency estimation of one sound source within a simple polyphonic sound using the Two-way mismatch algorithm.

The sound is a cello recording `cello-double-2.wav`, in which two strings are played simultaneously. One string
plays a constant drone while the other string plays a simple melody. You have to choose the analysis parameter values such that the `f0` frequency of the simple melody is tracked.

The core function used is `f0Detection()`, part of the `harmonicModel.py` module, which in turn uses the Two-way mismatch algorithm. Of all possible analysis parameters we will focus on the following ones:

1. `window` (string): analysis window
2. `M` (integer): window size used for computing the spectrum
3. `N` (integer): FFT size used for computing the spectrum
4. `f0et` (float): error threshold used for the f0 computation
5. `t` (float): magnitude threshold in dB used in spectral peak picking
6. `minf0` (float): minimum fundamental frequency in Hz
7. `maxf0` (float): maximum fundamental frequency in Hz

Be cautious while choosing the window size. Window size should be large enough to resolve the spectral peaks and small enough to preserve the note transitions. Very large window sizes may smear the `f0` contour at note transitions.

NOTE: Do not do just trial and error. Understand the problem and choose the parameters that should work best. Then test it and refine the parameters by trying different parameter values and visualizing/hearing the output `f0`. The output will not be perfect, so try to understand the problems encountered.

```python
import os
import sys
import numpy as np
import math
from scipy.signal import get_window
import matplotlib.pyplot as plt
import IPython.display as ipd

sys.path.append('../software/models/')
import utilFunctions as UF
import harmonicModel as HM
import sineModel as SM
import stft
import dftModel as DFT

eps = np.finfo(float).eps
```

```python
input_file = '../sounds/cello-double-2.wav'

### Change these analysis parameter values marked as XX

window = XX
M = XX
N = XX
f0et = XX
t = XX
minf0 = XX
maxf0 = XX

# No need to modify the code below, just understand it
H = 256

fs, x = UF.wavread(input_file)
w  = get_window(window, M)
f0 = HM.f0Detection(x, fs, w, N, H, t, minf0, maxf0, f0et)
y = UF.sinewaveSynth(f0, 0.8, H, fs)

ipd.display(ipd.Audio(data=x, rate=fs))
ipd.display(ipd.Audio(data=y, rate=fs))

# Code for plotting the f0 contour on top of the spectrogram
maxplotfreq = 500.0
fig = plt.figure(figsize=(15, 9))

mX, pX = stft.stftAnal(x, w, N, H)
mX = np.transpose(mX[:,:int(N*(maxplotfreq/fs))+1])

timeStamps = np.arange(mX.shape[1])*H/float(fs)
binFreqs = np.arange(mX.shape[0])*fs/float(N)

plt.pcolormesh(timeStamps, binFreqs, mX)
plt.plot(timeStamps, f0, color = 'k', linewidth=1.5)

plt.ylabel('Frequency (Hz)')
plt.xlabel('Time (s)')
plt.legend(('f0',))

```

## Part 2 - Segmentation of stable note regions in an audio signal

Complete the function `segment_stable_notes_regions()` to identify the stable regions of notes in a specific
monophonic audio signal. The function should return an array of segments where each segment contains the
start and end frame index of a stable note.

The input argument to the function are the fundamental frequency of a monophonic audio signal (`f0`), threshold to
be used for deciding stable notes (`stdThsld`) in cents, minimum allowed duration of a stable note (`minNoteDur`),
number of samples to be considered for computing standard deviation (`winStable`). The function returns a numpy array of `shape (k,2)`, where `k` is the total number of detected segments. The two columns in each row contains the starting and the ending frame indexes of a stable note segment. The segments must be returned in the increasing order of their start times.

In order to facilitate the exercise we have configured the input parameters to work with a particular sound, `'../sounds/sax-phrase-short.wav'`. Except the code for the `segment_stable_notes_regions()` function, we give the code to perform the whole analysis, thus, to read the sound, perform the detection of the fundamental frequency, using `f0Detection()` and ploting the output.

To implement the segmentation in `segment_stable_notes_regions()` you should consider the following issues:

1. In order to make the processing musically relevant, the `f0` values should be converted first from Hertz to Cents, which is a logarithmic scale.
2. At each time frame (for each `f0` value) you should compute the standard deviation of the past `winStable` number of `f0` samples (including the `f0` sample at the current audio frame).
3. You should then apply a deviation threshold, `stdThsld`, to determine if the current frame belongs to a stable note region or not. Since we are interested in the stable note regions, the standard deviation of the previous `winStable` number of `f0` samples (including the current sample) should be less than `stdThsld` i.e. use the current sample and `winStable-1` previous samples. Ignore the first `winStable-1` samples in this computation.
4. All the consecutive frames belonging to the stable note regions should be grouped together into segments. For example, if the indexes of the frames corresponding to the stable note regions are 3,4,5,6,12,13,14, we get two segments, first 3-6 and second 12-14.
5. After grouping frame indexes into segments filter/remove the segments which are smaller in duration than `minNoteDur`. Return the segment indexes in the increasing order of their start frame index.

```python
def segment_stable_notes_regions(f0, stdThsld, minNoteDur, winStable):
    """
    Function to segment the stable note regions in an audio signal from its f0 values
    Input:
        f0 (array): array of f0 values of a sound
        stdThsld (float): threshold for detecting stable regions in the f0 contour (in cents)
        minNoteDur (float): minimum allowed segment length (note duration)
        winStable (integer): number of samples used for computing standard deviation
    Output:
        segments (np.ndarray): numpy array containing starting and ending frame indexes of every segment.
    """

    ### your code here

    # 1. convert f0 values from Hz to Cents (as described in pdf document)
    f0Cents = 1200*np.log2((f0+eps)/55.0)                       # cents conversion

    #2. create an array containing standard deviation of last winStable samples
    stdArr = 10000000000*np.ones(f0.shape)
    for ii in range(winStable-1, len(f0)):
        stdArr[ii] = np.std(f0Cents[ii-winStable+1:ii+1])

    #3. apply threshold on standard deviation values to find indexes of the stable points in melody
    indFlat = np.where(stdArr<=stdThsld)[0]
    flatArr = np.zeros(f0.shape)
    flatArr[indFlat] = 1

    #4. create segments of continuous stable points such that consecutive stable points belong to same segment
    onset = np.where((flatArr[1:]-flatArr[:-1])==1)[0]+1
    offset = np.where((flatArr[1:]-flatArr[:-1])==-1)[0]

    indRem = np.where(offset<onset[0])[0]              #this step removes any offset before onset (to sync them)
    offset = np.delete(offset, indRem)

    minN = min(onset.size, offset.size)
    segments = np.transpose(np.vstack((onset[:minN], offset[:minN])))

    #5. apply segment filtering, i.e. remove segments with are < minNoteDur in length
    minNoteSamples = int(np.ceil(minNoteDur*fs/H))
    diff = segments[:,1] - segments[:,0]
    indDel = np.where(diff<minNoteSamples)
    segments = np.delete(segments,indDel, axis=0)

    return segments
```

Now run and test `segment_stable_notes_regions()`, using the test cases to validate your code.

__Test case 1:__ Using `input_file='../sounds/cello-phrase.wav', stdThsld=10, minNoteDur=0.1,
winStable = 3, window='hamming', M=1025, N=2048, H=256, f0et=5.0, t=-100, minf0=310, maxf0=650`,
the function `segmentStableNotesRegions()` should return 9 segments.

__Test case 2:__ Using `input_file='../sounds/cello-phrase.wav', stdThsld=20, minNoteDur=0.5,
winStable = 3, window='hamming', M=1025, N=2048, H=256, f0et=5.0, t=-100, minf0=310, maxf0=650`,
the function `segmentStableNotesRegions()` should return 6 segments.

__Test case 3:__ Using `input_file='../sounds/sax-phrase-short.wav', stdThsld=5, minNoteDur=0.6,
winStable = 3, window='hamming', M=1025, N=2048, H=256, f0et=5.0, t=-100, minf0=310, maxf0=650`,
the function `segmentStableNotesRegions()` should return just one segment.

We include all the needed code to compute f0. Call the function `segment_stable_notes_regions()`, and plot the `f0` contour and the detected segments on the top of the spectrogram of the audio signal.

Analyse the outcome of your function and compare different cases. Try different sounds.

```python
### parameters to change

input_file = 'XX'
stdThsld = XX
minNoteDur = XX
winStable = XX
window = 'XX'
M = XX
N = XX
H = XX
f0et = XX
t = XX
minf0 = XX
maxf0 = XX

# no need to change any code after here

# compute f0 and segments
fs, x = UF.wavread(input_file)
w  = get_window(window, M)
f0 = HM.f0Detection(x, fs, w, N, H, t, minf0, maxf0, f0et)
segments = segment_stable_notes_regions(f0,stdThsld, minNoteDur,winStable)

# plot spectrogram, f0, and segments
maxplotfreq = 1000.0

plt.figure(figsize=(15, 9))

mX, pX = stft.stftAnal(x, w, N, H)
mX = np.transpose(mX[:,:int(N*(maxplotfreq/fs))+1])

timeStamps = np.arange(mX.shape[1])*H/float(fs)
binFreqs = np.arange(mX.shape[0])*fs/float(N)

plt.pcolormesh(timeStamps, binFreqs, mX)
plt.plot(timeStamps, f0, color = 'k', linewidth=5)

for i in range(segments.shape[0]):
    plt.plot(timeStamps[segments[i,0]:segments[i,1]], f0[segments[i,0]:segments[i,1]], color = '#A9E2F3', linewidth=1.5)
plt.ylabel('Frequency (Hz)', fontsize = fontSize)
plt.xlabel('Time (s)', fontsize = fontSize)
plt.legend(('f0','segments'))
```

## Part 3 - Compute amount of inharmonicity present in a sound

Complete the function `estimate_inharmonicity()` to measure the amount of inharmonicity present in a pitched/harmonic sound. The function should measure the mean inharmonicity from the harmonics obtained by a harmonic analysis.

The input argument is the 2D array containing the harmonic frequencies and it should return a single numpy float, which is the mean inharmonicity.

Use the formula given in the Relevant Concepts section above to compute the inharmonicity measure for the given interval. Note that for some frames some of the harmonics might not be detected due to their low energy. For handling such cases use only the detected harmonics (and set the value of `R` in the equation to the number of detected hamonics) to compute the inharmonicity measure. All the detected harmonics have a non-zero frequency.

In this question we will work with a piano sound (`'../sounds/piano.wav'`), a typical example of an
instrument that exhibits inharmonicity (http://en.wikipedia.org/wiki/Piano_acoustics#Inharmonicity_and_piano_size).

```python
def estimate_inharmonicity(xhfreq):
    """
    Input:
        harmonic frequencies of a sound (2D array)
    Output:
        mean inharmonicity over all the frames (float)
    """
    ### Your code here
    devArray = []
    nFrames, nH = xhfreq.shape
    harms = (np.arange(nH)).astype(np.float)+1
    for i in range(0,nFrames):
        dev = abs(xhfreq[i,:]-(xhfreq[i,0]*harms))/harms
        indZero = np.where(xhfreq[i,:]==0)[0]
        dev[indZero] = 0
        if not len(indZero) == nH:
            devArray.append(np.sum(dev)/float(nH-len(indZero)))
    return np.sum(devArray)/float(len(devArray))
```

Test and run `estimate_inharmonicity()` using the code bellow, which performs the harmonic analysis using the function `harmonicModelAnal()`, calls `estimate_inharmonicity()`, and then plots the harmonics that have been used in the computation. Use the following test cases to validate your code.

__Test case 1:__ If you run your code with `inputFile = '../sounds/piano.wav', t1=0.2, t2=0.4, window='hamming', M=2047, N=2048, H=128, f0et=5.0, t=-90, minf0=130, maxf0=180, nH = 25`, the returned output should be `1.4607`.

__Test case 2:__ If you run your code with `inputFile = '../sounds/piano.wav', t1=2.3, t2=2.55, window='hamming', M=2047, N=2048, H=128, f0et=5.0, t=-90, minf0=230, maxf0=290, nH = 15`, the returned output should be `1.4852`.

__Test case 3:__ If you run your code with `inputFile = '../sounds/piano.wav', t1=2.55, t2=2.8, window='hamming', M=2047, N=2048, H=128, f0et=5.0, t=-90, minf0=230, maxf0=290, nH = 5`, the returned output should be `0.1743`.

You should compare the inharmonicities present in the sounds of different instruments.

```python
# test and run estimate_inharmonicity()
### Your code here

```

## Part 4 - Improving the implementation of the two way mismatch f0 estimation algorithm

Try to improve the implementation of the two way mismatch algorithm used for fundamental frequency estimation. There is no definite answer for this question. The main purpose of this part is to understand the limitations of the current implementation of the TWM algorithm.

You should directly modify the core functions that implement the TWM algorithm and that are copied here. Hence, you just need to modify the functions in this file.

Estimating fundamental frequency from an audio signal is still a challenging and unsolved problem. By now you might have realized that many times the performance of the TWM `f0` estimation algorithm falls short of the expectations. There can be a systematic explanation for the scenarios where TWM fails for specific categories or characteristics of the sounds. Some of the known scenarios where the current implementation of the TWM algorithm fails to estimate a correct fundamental frequency are:

1) Missing fundamental frequency: For many sounds the fundamental frequency component is very low and therefore during the spectral peak picking step we do not obtain any peak corresponding to the `f0`. Since the TWM algorithm implemented in sms-tools considers only the detected spectral peaks as the `f0` candidates, we do not get any candidate corresponding to the `f0`. This causes `f0` estimation to fail. For example, such a scenario is encountered in low pitched vocal sounds.
2) Pseudo-harmonicity in the sound: Many instruments, such as piano, exhibit some deviation from perfect harmonicity wherein their harmonic partials are not perfectly located at integral multiples of the fundamental frequency. Since the TWM algorithm computes error function assuming that the harmonic locations are at integral multiples, its performance is poorer when such deviations exist.

In this question we propose to work on these two scenarios. Go to freesound and download sound examples of low pitched vocal sounds and of piano. Run current implementation of TMW to identify the limitations and propose improvements to the code in order to obtain better `f0` estimation for those two particular scenarios.

The core TWM algorithm is implemented in the function `TWM_p()`, which takes in an array of `f0` candidates and detect the candidate that has the lowest error. `TWM_p()` is called by `f0Twm()`, which generates `f0` candidates `(f0c = np.argwhere((pfreq>minf0) & (pfreq<maxf0))[:,0])`. This function also implements a memory based prunning of the `f0` candidates. If the `f0` contour is found to be stable (no drastic transitions across frames) then only the `f0` candidates close to the stable `f0` value are retained. `f0Twm()` is called for every audio frame by `f0Detection()`.

You should use the code suplied, which calls `f0Detection()` for estimating `f0` and plots the `f0` contour on top of the spectrogram of the sound.

TIP: An identified limitation of the current implementation for the case of low vocal sounds is that it can only find `f0` if there is a peak present in the magnitude spectrum. A possible improvement is to generate additional `f0` candidates from the identified peaks. Another identified limitation for the case of piano sounds is the assumption of perfect harmonicity. For these sounds you can think of modifying the generation of the ideal harmonic series that is computed in the code, incorporating the typical deviation from harmonicity encountered in piano sounds.

NOTE: Before you start making changes in the TWM implementation make sure you have reached the best possible performance that can be achieved by tuning the analysis parameters. If the analysis parameters are inappropriately set, it is not completely meaningful to just improve the TWM implementation.

To maintain the integrity of the sms-tools package for future assignments, please make changes only to the functions in this file and not the other files in sms-tools.

In the cell below explain what you tried to do and what results did you obtained.

```python
### modify anything
def f0Detection(x, fs, w, N, H, t, minf0, maxf0, f0et):
    """
    Fundamental frequency detection of a sound using twm algorithm
    x: input sound; fs: sampling rate; w: analysis window;
    N: FFT size; t: threshold in negative dB,
    minf0: minimum f0 frequency in Hz, maxf0: maximim f0 frequency in Hz,
    f0et: error threshold in the f0 detection (ex: 5),
    returns f0: fundamental frequency
    """
    if (minf0 < 0):                                            # raise exception if minf0 is smaller than 0
        raise ValueError("Minumum fundamental frequency (minf0) smaller than 0")

    if (maxf0 >= 10000):                                       # raise exception if maxf0 is bigger than fs/2
        raise ValueError("Maximum fundamental frequency (maxf0) bigger than 10000Hz")

    if (H <= 0):                                               # raise error if hop size 0 or negative
        raise ValueError("Hop size (H) smaller or equal to 0")

    hN = N/2                                                   # size of positive spectrum
    hM1 = int(math.floor((w.size+1)/2))                        # half analysis window size by rounding
    hM2 = int(math.floor(w.size/2))                            # half analysis window size by floor
    x = np.append(np.zeros(hM2),x)                             # add zeros at beginning to center first window at sample 0
    x = np.append(x,np.zeros(hM1))                             # add zeros at the end to analyze last sample
    pin = hM1                                                  # init sound pointer in middle of anal window
    pend = x.size - hM1                                        # last sample to start a frame
    fftbuffer = np.zeros(N)                                    # initialize buffer for FFT
    w = w / sum(w)                                             # normalize analysis window
    f0 = []                                                    # initialize f0 output
    f0t = 0                                                    # initialize f0 track
    f0stable = 0                                               # initialize f0 stable
    while pin<pend:
        x1 = x[pin-hM1:pin+hM2]                                  # select frame
        mX, pX = DFT.dftAnal(x1, w, N)                           # compute dft
        ploc = UF.peakDetection(mX, t)                           # detect peak locations
        iploc, ipmag, ipphase = UF.peakInterp(mX, pX, ploc)      # refine peak values
        ipfreq = fs * iploc/N                                    # convert locations to Hez
        f0t = f0Twm(ipfreq, ipmag, f0et, minf0, maxf0, f0stable)  # find f0
        if ((f0stable==0)&(f0t>0)) \
                or ((f0stable>0)&(np.abs(f0stable-f0t)<f0stable/5.0)):
            f0stable = f0t                                         # consider a stable f0 if it is close to the previous one
        else:
            f0stable = 0
        f0 = np.append(f0, f0t)                                  # add f0 to output array
        pin += H                                                 # advance sound pointer
    return f0

```

```python
### modify anything
def f0Twm(pfreq, pmag, ef0max, minf0, maxf0, f0t=0):
    """
    Function that wraps the f0 detection function TWM, selecting the possible f0 candidates
    and calling the function TWM with them
    pfreq, pmag: peak frequencies and magnitudes,
    ef0max: maximum error allowed, minf0, maxf0: minimum  and maximum f0
    f0t: f0 of previous frame if stable
    returns f0: fundamental frequency in Hz
    """
    if (minf0 < 0):                                  # raise exception if minf0 is smaller than 0
        raise ValueError("Minumum fundamental frequency (minf0) smaller than 0")

    if (maxf0 >= 10000):                             # raise exception if maxf0 is bigger than 10000Hz
        raise ValueError("Maximum fundamental frequency (maxf0) bigger than 10000Hz")

    if (pfreq.size < 3) & (f0t == 0):                # return 0 if less than 3 peaks and not previous f0
        return 0

    f0c = np.argwhere((pfreq>minf0) & (pfreq<maxf0))[:,0] # use only peaks within given range
    if (f0c.size == 0):                              # return 0 if no peaks within range
        return 0
    f0cf = pfreq[f0c]                                # frequencies of peak candidates
    f0cm = pmag[f0c]                                 # magnitude of peak candidates

    if f0t>0:                                        # if stable f0 in previous frame
        shortlist = np.argwhere(np.abs(f0cf-f0t)<f0t/2.0)[:,0]   # use only peaks close to it
        maxc = np.argmax(f0cm)
        maxcfd = f0cf[maxc]%f0t
        if maxcfd > f0t/2:
            maxcfd = f0t - maxcfd
        if (maxc not in shortlist) and (maxcfd>(f0t/4)): # or the maximum magnitude peak is not a harmonic
            shortlist = np.append(maxc, shortlist)
        f0cf = f0cf[shortlist]                         # frequencies of candidates

    if (f0cf.size == 0):                             # return 0 if no peak candidates
        return 0

    f0, f0error = TWM_p(pfreq, pmag, f0cf)        # call the TWM function with peak candidates

    if (f0>0) and (f0error<ef0max):                  # accept and return f0 if below max error allowed
        return f0
    else:
        return 0

```

```python
### modify anything
def TWM_p(pfreq, pmag, f0c):
    """
    Two-way mismatch algorithm for f0 detection (by Beauchamp&Maher)
    [better to use the C version of this function: UF_C.twm]
    pfreq, pmag: peak frequencies in Hz and magnitudes,
    f0c: frequencies of f0 candidates
    returns f0, f0Error: fundamental frequency detected and its error
    """

    p = 0.5                                          # weighting by frequency value
    q = 1.4                                          # weighting related to magnitude of peaks
    r = 0.5                                          # scaling related to magnitude of peaks
    rho = 0.33                                       # weighting of MP error
    Amax = max(pmag)                                 # maximum peak magnitude
    maxnpeaks = 10                                   # maximum number of peaks used
    harmonic = np.matrix(f0c)
    ErrorPM = np.zeros(harmonic.size)                # initialize PM errors
    MaxNPM = min(maxnpeaks, pfreq.size)
    for i in range(0, MaxNPM) :                      # predicted to measured mismatch error
        difmatrixPM = harmonic.T * np.ones(pfreq.size)
        difmatrixPM = abs(difmatrixPM - np.ones((harmonic.size, 1))*pfreq)
        FreqDistance = np.amin(difmatrixPM, axis=1)    # minimum along rows
        peakloc = np.argmin(difmatrixPM, axis=1)
        Ponddif = np.array(FreqDistance) * (np.array(harmonic.T)**(-p))
        PeakMag = pmag[peakloc]
        MagFactor = 10**((PeakMag-Amax)/20)
        ErrorPM = ErrorPM + (Ponddif + MagFactor*(q*Ponddif-r)).T
        harmonic = harmonic+f0c

    ErrorMP = np.zeros(harmonic.size)                # initialize MP errors
    MaxNMP = min(maxnpeaks, pfreq.size)
    for i in range(0, f0c.size) :                    # measured to predicted mismatch error
        nharm = np.round(pfreq[:MaxNMP]/f0c[i])
        nharm = (nharm>=1)*nharm + (nharm<1)
        FreqDistance = abs(pfreq[:MaxNMP] - nharm*f0c[i])
        Ponddif = FreqDistance * (pfreq[:MaxNMP]**(-p))
        PeakMag = pmag[:MaxNMP]
        MagFactor = 10**((PeakMag-Amax)/20)
        ErrorMP[i] = sum(MagFactor * (Ponddif + MagFactor*(q*Ponddif-r)))

    Error = (ErrorPM[0]/MaxNPM) + (rho*ErrorMP/MaxNMP)  # total error
    f0index = np.argmin(Error)                       # get the smallest error
    f0 = f0c[f0index]                                # f0 with the smallest error

    return f0, Error[f0index]
```

```python
# test the f0Detection()
### modify anything

input_file = '../sounds/piano.wav'
window = 'hamming'
M = 2048
N = 2048
H = 256
f0et = 5.0
t = -80
minf0 = 100
maxf0 = 300

ipd.display(ipd.Audio('../sounds/piano.wav'))
fs, x = UF.wavread(input_file)
w  = get_window(window, M)
f0 = f0Detection(x, fs, w, N, H, t, minf0, maxf0, f0et)

## Code for plotting the f0 contour on top of the spectrogram
maxplotfreq = 500.0
plt.figure(figsize=(15, 5))

mX, pX = stft.stftAnal(x, w, N, H)
mX = np.transpose(mX[:,:int(N*(maxplotfreq/fs))+1])

timeStamps = np.arange(mX.shape[1])*H/float(fs)
binFreqs = np.arange(mX.shape[0])*fs/float(N)

plt.pcolormesh(timeStamps, binFreqs, mX)
plt.plot(timeStamps, f0, color = 'k', linewidth=1.5)

plt.ylabel('Frequency (Hz)')
plt.xlabel('Time (s)')

```

## Your explanation of Part 4 here


# Two Degree-of-Freedom Saddle-Node Hamiltonian

## Introduction

Molecular reaction dynamics is concerned with the breaking and forming of bonds between the atoms that make up a molecule. The description of such mechanisms requires analyzing how the bonds change in time, that is, it is a *dynamical* mechanism requiring a phase space description. The phase space will have coordinates that correspond to the bond configurations with a corresponding canonical momentum for each bond configuration coordinate. Thus, the dynamics of chemical bonds can be formulated in the language of Hamiltonian dynamics and its resulting phase space geometry. In recent years there has been significant developments in the mathematical description and analysis of chemical reaction dynamics in phase space \cite{Komatsuzaki00,uzer2002geometry,Komatsuzaki02a}.

Our objective is to describe the change in a bond that signals the occurrence of a chemical reaction in terms of a unique characteristic of a trajectory of Hamilton's equations. This characteristic requires an understanding of the geometry of the phase space of the molecule in a way that enables us to divide an appropriate volume of phase space into a region corresponding to ''reactants'' and a region corresponding to ''products''. The passage from reactants to products, that is ''reaction'', occurs when a trajectory crosses the ''dividing surface'' (DS) between the reactants and products. In this framework for understanding chemical reactions, the flux through such a dividing surface would be related to the reaction rate, and therefore the construction of a DS between such regions is of interest in reaction dynamics. The description of the regions of reactants and products can often be inferred from the nature of the development of the coordinates used in the mathematical model of the chemical reaction. Once the model is developed, the DS must be constructed in the context of this model. In relating the flux through the DS to a reaction rate it is desirable that trajectories crossing the DS from the reactant side proceed to the product side before possibly recrossing the DS back to the reactant region. Thus, we require the DS to have the ''no-recrossing'' property \cite{MacKay90, waalkens2004direct}. Since Hamiltonian dynamics conserves energy, the trajectories evolve on the energy surface, which is of codimension one in phase space. The DS is required to be codimension one in the energy surface. Hence, we require a DS between reactants and products to be codimension one in the energy surface and to have the no-recrossing property. We note that Wigner had already described these properties for a DS in phase space much earlier \cite{Wigner38, Wigner39}, and a review of classical and quantum versions of DS constructed in phase space can be found in \cite{WaalkensSchubertWiggins08}.

The construction of a DS separating the phase space into two parts, reactants and products, has been a focus from the dynamical systems point of view in recent years. However, the lack of a firm theoretical basis for the construction of such surfaces for molecular systems with three and more degree-of-freedom (DoF) has until recently been a major obstacle in the development of the theory. In phase space, that is for nonzero momentum, the role of the *saddle point* is played by an *invariant manifold* of saddle stability type, the normally hyperbolic invariant manifold (NHIM) (see sec:HSN_2DOF) \cite{Wiggins88,wiggins90,wiggins2013normally}. In order to fully appreciate the NHIM and its role in reaction rate theory, it is useful to begin with a precursor concept -- the *periodic orbit dividing surface* or PODS. For systems with two DoF described by a natural Hamiltonian, kinetic plus potential energy, the problem of constructing the DS in phase space was solved during the 1970s by McLafferty, Pechukas and Pollak \cite{Pechukas73,Pechukas77,Pollak78,Pechukas79}. They demonstrated that the DS at a specific energy is related to an invariant phase space structure, an unstable periodic orbit (UPO). The UPO defines (it is the boundary of) the bottleneck in phase space through which the reaction occurs and the DS which intersects trajectories evolving from reactants to products can be shown to have the geometry of a hemisphere in phase space whose boundary is the unstable PO \cite{wiggins2001impenetrable,waalkens2004direct}. The same construction can be carried out for a DS intersecting trajectories crossing from products to reactants and these two hemispheres form a sphere for which the UPO is the equator. Generalisation of this construction of DS to high dimensional systems has been a central question in reaction dynamics and has only received a satisfactory answer in recent years \cite{wiggins2001impenetrable,uzer2002geometry}. The key difficulty concerns the high dimensional analogue of the unstable PO used in the two DoF system for the construction of the DS. This difficulty is resolved by considering the NHIM, which has the appropriate dimensionality for anchoring the dividing surface in phase space.

Results from dynamical systems theory show that transport in phase space is controlled by high dimensional manifolds, NHIMs, which are the natural generalisation of the UPO of the two DoF case \cite{wiggins90}. Normal hyperbolicity of these invariant manifolds means that their stability, in a precise sense, is of saddle type in the transverse direction, which implies that they possess stable and unstable invariant manifolds that are impenetrable barriers and mediate transport in phase space. These invariant manifolds of the NHIM are structurally stable, that is, stable under perturbation \cite{wiggins2013normally}.

In this chapter we describe the influence of Hamiltonian saddle-node bifurcations on the high-dimensional phase space structures that mediate transport and characterize reaction dynamics \cite{GG2019}. We do so by identifying the relevant invariant manifolds, NHIMs and their stable and unstable manifolds, using the method of Lagrangian descriptors (LDs). In a general context, a saddle-node bifurcation takes place when a saddle and an elliptic equilibrium point of a dynamical system 'collide' as the value of a physical parameter is varied. This phenomenon occurs naturally in a Chemistry setup as a consequence of lowering the energy barrier height associated to a saddle point of a given potential energy surface (PES), which results in the connection of the potential well region on one side of the saddle to a escape channel. Therefore, this mechanism facilitates that trajectories cross the phase space bottleneck in the neighborhood of the index-1 saddle, and hence has a great influence on reaction dynamics. The drastic changes produced in the geometry of the phase space structures can then be used to account for corrections to Kramers' reaction rate as the barrier height decreases \cite{hathcock2019renormalization}, providing a significant step for the experimental study of single bond dynamics of molecules and the control of micro- and nano-electromechanical devices \cite{hathcock2019renormalization,husson2009force,miller2012escape,herbert2017predictability}. A nice example regarding the influence of Hamiltonian saddle-node bifurcations on isomerization dynamics can be found in that of the LiCN/LiNC molecule \cite{borondo1995,borondo1996,revuelta2019unveiling}. Other applications include the study of ionization and dissociation rates in atomic systems \cite{de_leon_intramolecular_1981,mitchell_geometry_2003_I,mitchell_geometry_2003_II,mitchell_chaos-induced_2004,mitchell_analysis_2004,mitchell_structure_2007,mitchell_nonlinear_2009,wang_photoionization_2010}.

In the framework of chemical reaction dynamics, saddle-node bifurcations occur in problems that address the escape from a potential well by crossing an energy barrier and the influence that the barrier height has on the reactive flux. These situations can be identified for instance with dissociation or fragmentation reactions, where a chemical transformation takes place if the bond of a molecule A breaks up, giving rise to two products B and C. In this situation, the equilibrium conformation of the given molecule A is represented by a potential well in a PES, and dissociation into B and C can take place if the system has sufficient energy to cross the potential barrier that separates bounded (vibration) from unbounded (bond breakup) motion. This phenomenon can be modeled for instance by means of a cubic potential energy function as illustrated in Fig. \ref{fig:pes_snbif}, which will be our building block to construct the normal form Hamiltonian that describes saddle-node bifurcations in phase space. Our goal is to determine the geometrical phase space structures that ''divide'' reactants from products, and identify dynamically distinct phase space regions corresponding to reactive and non-reactive initial conditions for the dynamical system.

\label{fig:pes_snbif}
\caption{Cubic potential energy function as a basic model for dissociation chemical reactions. The shape of this potential is the building block (normal form) for Hamiltonian saddle-node bifurcations in phase space.}

## Problem Development

In this section we introduce the normal form Hamiltonian that determines saddle-node bifurcations for systems with two DoF. The natural extension of our previous one DoF model is to add another DoF called $x$, in the form of a harmonic oscillator with frequency $\omega > 0$ and mass $m = 1$. This extra DoF plays the role of a bath mode in the terminology of chemical reaction dynamics, and we suppose a quadratic coupling with the reactive DoF $q$. Under these assumptions, the Hamiltonian becomes:

\begin{equation}
H(q,x,p,p_x) = \dfrac{1}{2} \left(p^2 + p_x^2 \right) - \sqrt{\mu} \, q^2 + \frac{\alpha}{3} \,q^3 + \dfrac{\omega^2}{2} x^2 + \dfrac{\varepsilon}{2} \left(x-q\right)^2 \; ,
\label{eq:ham_2dof}
\end{equation}

where $\varepsilon \geqslant 0$ is the strength of the coupling between the reaction and the bath mode, and the kinetic $(T)$ and potential $(V)$ energies are given by:

\begin{equation}
T(p,p_x) = \frac{1}{2}\left(p^2 + p_x^2\right) \quad,\quad V(q,x) = - \sqrt{\mu} \, q^2 + \frac{\alpha}{3} \,q^3 + \dfrac{\omega^2}{2} x^2 + \dfrac{\varepsilon}{2} \left(x-q\right)^2
\label{eq:pes_2dof}
\end{equation}

For this Hamiltonian, the equations of motion have the form:

\begin{equation}
\begin{cases}
\dot{q} = \dfrac{\partial H}{\partial p} =  p \\[.4cm]
\dot{x} = \dfrac{\partial H}{\partial p_x} = p_x \\[.4cm]
\dot{p} = -\dfrac{\partial H}{\partial q} =  2\sqrt{\mu} \, q - \alpha \, q^2 + \varepsilon (x - q) \\[.4cm]
\dot{p}_x = -\dfrac{\partial H}{\partial x} = -\omega^2 x + \varepsilon (q-x)
\end{cases}
\label{eq:hameq_2dof}
\end{equation}

The phase space for this dynamical system is four dimensional, and due to energy coservation, trajectories evolve on a three dimensional energy hypersurface.

For the Hamiltonian system that we are analyzing, we would like to point out that there exists a simple and interesting mathematical connection between the coupling parameter, the eigenfrequencies at the barrier which determine the local dynamics in the phase space bottleneck region and the geometry of the PES, described by means of the Gaussian curvature. For a given surface $V(x,y)$, the Gaussian curvature can be calculated from the following formula \cite{spivak} :

\begin{equation}
\mathcal{K} = \dfrac{\det \left(\mathcal{H}_V\right)}{\left(1 + \| \nabla V \|^2\right)^2}
\end{equation}

where $\mathcal{H}_V$ denotes the Hessian matrix associated to the PES. It is straightforward to show then that the coupling strength linearly determines the Gaussian curvature at the minimum of the well and the barrier of the PES:

\begin{equation}
\mathcal{K}_{\text{well}} = \left(\omega^2 - 2\sqrt{\mu}\right) \left(\varepsilon_c - \varepsilon\right) \quad , \quad \mathcal{K}_{\text{barrier}} = -\mathcal{K}_{\text{well}}
\end{equation}

and that the proportionality constant depends only on the difference of the squares of the eigenfrequencies of the uncoupled system at the barrier region. This means that the local geometry of the bottleneck can be controlled by these parameters.

We illustrate in Fig. \ref{fig:figpes_2dof} how the geometry of the PES changes as the coupling strength $\varepsilon$ is varied. For this visualization, we have used the following values of the model parameters $\mu = 0.25$, $\alpha = 2$, and $\omega = 1.25$. The reason for doing so is that they satisfy the condition $\omega^2 > 2\sqrt{\mu}$ and consequently a critical value of the perturbation parameter exists, given by $\varepsilon_c = 25/9$, at which the two equilibrium points 'collide' at the origin resulting in a *saddle-node bifurcation*. The dynamics after this collision is beyond the scope of this study and therefore we will only focus on the description of the system dynamics for different values of the coupling strength $\varepsilon < \varepsilon_c$. Observe in Fig. \ref{fig:figpes_2dof} A) and B) that the location of the well  lies on the $q$-axis when the DoF are uncoupled $(\varepsilon = 0)$. As $\varepsilon$ is increased, Fig. \ref{fig:figpes_2dof} C) and D) show that its effect is to *tilt* the PES with respect to the configuration space plane, and the position of the potential well moves off the $q$-axis towards the origin. Finally, the situation for which the coupling strength reaches the critical value $\varepsilon_c$ is shown in Fig. \ref{fig:figpes_2dof} E) and F) for which the collision has happened and there is only one critical point of the potential at the origin.

\label{fig:figpes_2dof}
\caption{Geometry of the PES (left-column) and equipotentials in configuration space  (right column) for the model parameters $\mu = 0.25$, $\alpha = 2$ and $\omega = 1.25$. A) and B) correspond to $\varepsilon = 0$; C) and D) represent the case $\varepsilon = 0.25$; E) and F) are for the critical coupling strength $\varepsilon = 25/9$.}

## Revealing the Phase Space Structure

It is straightforward to check that the equilibrium points for this system are located at:

\begin{equation}
\mathbf{x}_1^e = (0,0,0,0) \quad,\quad \mathbf{x}_2^e =
\frac{2\sqrt{\mu} \, \omega^2 + \left(2\sqrt{\mu}-\omega^2\right)\varepsilon}{\alpha \, (\omega^2 + \varepsilon)}
\left(1,\frac{\varepsilon}{\omega^2 + \varepsilon},0,0\right)
\label{eq:cfSp_eqCoords}
\end{equation}

Notice that there exists a critical value for the coupling strength:

\begin{equation}
\varepsilon_c = \dfrac{2\sqrt{\mu} \, \omega^2}{\omega^2 - 2\sqrt{\mu}} \;,
\label{eq:crit_epsi}
\end{equation}

for which the Hamiltonian in Eq. \eqref{eq:hameq_2dof} has only one equilibrium point at the origin, and interestingly, a saddle-node bifurcation takes place in phase space when $\varepsilon = \varepsilon_c$. We will analyze later the influence of the perturbation strength $\varepsilon$ on the geometry of the phase space structures. Observe that this critical value is independent of the nonlinearity strength parameter $\alpha$ and requires $\omega^2 > 2\sqrt{\mu}$ to be satisfied, since the coupling strength $\varepsilon$ is a non-negative parameter. Moreover, the critical coupling is characterized by a functional relationship between the squares of the eigenfrequencies of the reactive and bath DoF of the uncoupled system ($\varepsilon = 0$). Physically, the interpretation of this condition is that the bath mode has more influence on the dynamics of the system than that of the reactive DoF, which describes the behavior in the vicinity of the barrier. In the limiting case where $\omega^2 = 2\sqrt{\mu}$, the configuration space coordinates of the equilibrium point $\mathbf{x}_2^e$ corresponding to the potential well are:

\begin{equation}
q_e = \frac{4\mu}{\alpha(2\sqrt{\mu} + \varepsilon)} \quad,\quad x_e =  \frac{4\mu \, \varepsilon}{\alpha(2\sqrt{\mu} + \varepsilon)^2}
\end{equation}

showing that this point approaches the origin as $\varepsilon \to \infty$, and therefore no bifurcation occurs in this situation. Finally, for the case $\omega^2 < 2\sqrt{\mu}$ we have seen that there is no bifurcation as $\varepsilon$ is varied. In fact, if  $\varepsilon \to \infty$ the potential well will approach the limit point in configuration space:

\begin{equation}
q_e = x_e = \frac{2\sqrt{\mu} - \omega^2}{\alpha} > 0
\end{equation}

At this point, we carry out a linear stability analysis to show that the equilibrium point at the origin is an index-1 saddle of the PES. This analysis will be useful for computing the unstable periodic orbit (UPO), or normally hyperbolic invariant manifold (NHIM), associated to the index-1 saddle. Also, it will allow us to determine the NHIM's invariant stable and unstable manifolds that act as codimension one impenetrable barriers on the constant energy hypersurface. The global geometry of these invariant manifolds is paramount for quantifying the reaction rate across the phase space bottleneck in the neighborhood of the index-1 saddle. The Jacobian matrix for the Hamiltonian system in Eq. \eqref{eq:hameq_2dof} is given by:

\begin{equation}
J(q,x,p,p_x) =
\begin{pmatrix}
0 & 0 & 1 & 0 \\[.1cm]
0 & 0 & 0 & 1 \\[.1cm]
2\sqrt{\mu} - 2\alpha q - \varepsilon & \varepsilon & 0 & 0 \\[.1cm]
\varepsilon & -\omega^2 - \varepsilon & 0 & 0
\end{pmatrix}
\end{equation}

The stability of $\mathbf{x}_1^e = (0,0,0,0) $ is given by the eigenvalues of the Jacobian:

\begin{equation}
J(\mathbf{x}_1^e) =
\begin{pmatrix}
0 & 0 & 1 & 0 \\[.1cm]
0 & 0 & 0 & 1 \\[.1cm]
2\sqrt{\mu} - \varepsilon & \varepsilon & 0 & 0 \\[.1cm]
\varepsilon & -\omega^2 - \varepsilon & 0 & 0
\end{pmatrix}
\label{eq:jacobian_2dof}
\end{equation}

If the system is decoupled, i.e. $\varepsilon = 0$, the eigenvalues are given by $\pm \lambda_0$ and $\pm \omega_0 \, i$, where:

\begin{equation}
\lambda_0 = \sqrt[4]{4\mu} \;,\quad \omega_0 = \omega
\end{equation}

Therefore, the origin is an index-1 saddle equilibrium point, since the linearized system has exactly one pair of real eigenvalues $ \pm \lambda_0$ and the saddle plane is spanned by their eigenvectors. We know from the Moser's generalization of the Lyapunov Subcenter Theorem \cite{wiggins2013normally,wiggins2003applied} that when the energy of the system is above that of the index-1 saddle, there is a two dimensional plane spanned by the eigenvectors of $\pm \omega_0 \, i$, known as the center invariant manifold. This invariant manifold with normal hyperbolicity is known as a NHIM and in the context of Hamiltonian systems with 2 DoF is an UPO and has the topology of a circle $S^1$. As the energy of the system is increased above the energy of the index-1 saddle at the origin, a bottleneck structure opens in phase space connecting dynamically different regions and allowing trajectories to move between them. This phenomenon results in a phase space transport mechanism. This framework of understanding chemical reactions is realized by computing the stable and unstable manifolds associated with the unstable periodic orbit. These invariant manifolds have cylindrical geometry, that is $\mathbb{R} \times S^1$, and their global behavior is referred to as \textit{tube dynamics}. The cylindrical manifolds are codimension one on the constant three-dimensional energy hypersurface and therefore, act as impenetrable barriers separating reactive and non-reactive trajectories in the phase space. Thus, they determine the initial conditions that will pass through the bottleneck in some future time (or had passed through it in the past) during their evolution~\cite{wiggins_impenetrable_2001}. The UPO provides us with the scaffolding to construct the dividing surface that separates the trapped motion in the well region of the PES and the escape to infinity of particle trajectories through the phase space bottleneck.

We look now at the linear stability of the coupled system, that is $\varepsilon \neq 0$. In this case, the reaction and bath DoF are coupled and we expect  chaotic trajectories to appear, having a significant influence on the reaction rate. However, the geometry of the NHIM and its invariant manifolds still governs this reaction mechanism. A simple analysis reveals that the square of the eigenvalues of the Jacobian matrix at the origin are given by:

\begin{equation}
\xi =\dfrac{\lambda_0^2 - \omega_0^2}{2} - \varepsilon \pm \sqrt{\left(\dfrac{\lambda_0^2 + \omega_0^2}{2}\right)^2 + \varepsilon^2} \;\;.
\label{eq:eig_ham_2dof}
\end{equation}

as a function of the coupling strength $\varepsilon$ and the squares of the eigenvalues of the uncoupled system $\lambda_0^2$ and $\omega_0^2$. Notice that when the condition $\omega_0^2 \leq \lambda_0^2$ holds, the two possible values of $\xi$ have opposite signs for all $\varepsilon > 0$. Moreover, under the assumption $\omega_0^2 > \lambda_0^2$, the two values of $\xi$ have opposite sign whenever $0 < \varepsilon < \varepsilon_c$ is satisfied, where the critical coupling strength $\varepsilon_c$ is given by Eq. \eqref{eq:crit_epsi}. If we denote $\xi_{1,\varepsilon}$ and $\xi_{2,\varepsilon}$ as the positive and negative roots respectively in Eq. \eqref{eq:eig_ham_2dof}, then the eigenvalues of the Jacobian matrix are $\pm \lambda_\varepsilon$ and $\pm \omega_\varepsilon i$ where:

\begin{equation}
\lambda_\varepsilon = \sqrt{\xi_{1,\varepsilon}} \quad,\quad \omega_\varepsilon = \sqrt{|\xi_{2,\varepsilon}|} \;.
\label{eq:eigenvalues_2dof}
\end{equation}

This argument shows that the equilibrium point at the origin is still an index-1 saddle for the coupled system under the conditions discussed above. In particular, when the coupling between the reaction and the bath DoF is weak, that is $\varepsilon \ll 1$, we have:

\begin{equation}
\lambda_\varepsilon \approx \sqrt{\lambda_0^2 - \varepsilon} \quad,\quad \omega_\varepsilon \approx \sqrt{\omega_0^2 + \varepsilon} \;,
\end{equation}

which confirms that the index-1 saddle structure of the equilibrium point at the origin persists under small perturbations. To finish the linear stability analysis of the equilibrium point at the origin, we compute the eigenvectors. Those associated with the real eigenvalues $\beta = \pm \lambda_{\varepsilon}$ correspond to the tangent directions of the unstable and stable manifolds, respectively, of the NHIM at the index-1 saddle, are given by:

\begin{equation}
\mathbf{u}_{\pm} = \left(1,\frac{\varepsilon}{\varepsilon + \left(\lambda^2_\varepsilon + \omega^2_0\right)},\pm\lambda_{\varepsilon},\pm\frac{\lambda_{\varepsilon} \, \varepsilon}{\varepsilon + \left(\lambda^2_\varepsilon + \omega^2_0\right)}\right) \;.
\label{eq:saddle_eigenv}
\end{equation}

The eigenvectors associated with the complex eigenvalues $\beta = \pm \omega_\varepsilon i$ span the center subspace of the NHIM at the index-1 saddle have the form:

\begin{equation}
\mathbf{w}_{\pm} = \left(\frac{\varepsilon}{\varepsilon - \left(\lambda^2_0 + \omega^2_\varepsilon\right)},1,\pm\frac{\omega_{\varepsilon} \, \varepsilon}{\varepsilon - \left(\lambda^2_0 + \omega^2_\varepsilon\right)}\, i,\pm\omega_{\varepsilon}\, i\right) \;.
\label{eq:center_eigenv}
\end{equation}

Therefore, we can write the solution to the linearized system at the index-1 saddle as:

\begin{equation}
\mathbf{x}(t) = C_1 e^{\lambda_\varepsilon t} \mathbf{u}_{+} + C_2 e^{-\lambda_\varepsilon t} \mathbf{u}_{-} + 2 Re\left(\eta e^{i\omega_\varepsilon t} \mathbf{w}_{+}\right)
\label{eq:geneq_lin_ham}
\end{equation}

where $C_1,C_2 \in \mathbb{R}$ and $\eta = \eta_1 + \eta_2 \, i \in \mathbb{C}$ are constants to be determined from an initial condition. This general solution to the linear system can be used in order to select an initial guess to search for the NHIM and construct its stable and unstable manifolds by using the differential correction and numerical continuation method \cite{Koon2011,naik2017geometry,naik2019b,GG2019}.

Once we have established the framework to discuss Hamiltonian saddle-node bifurcations in two DoF systems, we move on to study the changes in the geometry of the relevant phase space invariant manifolds that govern reaction dynamics in terms of the coupling strength. We do so by applyng the method of Lagrangian descriptors (LDs), see e.g. \cite{mancho2013lagrangian,lopesino2017}, and in particular we use its variable integration time version \cite{junginger2017chemical,naik2019b,GG2019} since trajectories can escape to infinity in finite time for the dynamical system we are considering. We also take advantage of the differential correction method to compute UPOs and numerical continuation and globalization for the construction of their invariant stable and unstable manifolds. For a detailed description of these nonlinear techniques, the interested reader can refer to \cite{Koon2011,naik2017geometry,naik2019b,GG2019}.

We start by fixing a total energy $H_0$ of the system. Since the model has two DoF, we know that dynamics takes place on a three-dimensional energy hypersurface embedded in the four-dimensinal phase space. This energy hypersurface is described by the set:

\begin{equation}
\begin{split}
\mathcal{S}(H_0) &= \left\lbrace (q,x,p,p_x) \in \mathbb{R}^4 \; \big|\; \dfrac{1}{2} \left(p^2 + p_x^2 \right) - \sqrt{\mu} \, q^2 + \frac{\alpha}{3} \,q^3 + \dfrac{\omega^2}{2} x^2 + \dfrac{\varepsilon}{2} \left(x-q\right)^2 = H_0 \right\rbrace
\end{split}
\end{equation}

The projection of the energy surface onto configuration space is:

\begin{equation}
\begin{split}
\mathcal{C}(H_0) &= \left\lbrace (q,x) \in \mathbb{R}^2 \; \big|\; V(q,x) \leqslant H_0 \right\rbrace \\
&= \left\lbrace (q,x) \in \mathbb{R}^2 \; \big|\; - \sqrt{\mu} \, q^2 + \frac{\alpha}{3} \,q^3 + \dfrac{\omega^2}{2} x^2 + \dfrac{\varepsilon}{2} \left(x-q\right)^2 \leqslant H_0 \right\rbrace
\end{split} \;,
\label{eq:hillsreg}
\end{equation}

which denotes configurations with positive kinetic energy, and is known in Celestial Mechanics as the \textit{Hill's region}. The boundary of $\mathcal{C}(H_0)$ is defined as the locus of points in the $(q,x)$ plane where the kinetic energy is zero, that is $H_0 = V(q,x)$, and is called the \textit{zero velocity curve}. The configuration space coordinates of the trajectories can only evolve inside the Hill's region, shown as white regions in Fig. \ref{fig:EnergySurf_Hills}. In order to identify the invariant manifolds, we take two dimensional slices of the energy surface and determine the intersection of the invariant manifolds with these low-dimensional slices. In particular, we calculate LDs and compare with qualitative understandings from Poincar\'e surfaces of section (SOSs). The isoenergetic SOSs chosen are:

\begin{align}
\mathcal{U}_{qp}^{+} &= \left\lbrace (q,x,p,p_x) \in \mathbb{R}^4 \; \big| \; x = 0 \; ,\; p_x(q,x,p;H_0) \geq 0 \right\rbrace \label{eq:sos_qp} \\[.1cm]
\mathcal{U}_{xp_x}^{+} &= \left\lbrace (q,x,p,p_x) \in \mathbb{R}^4 \; \big| \; q = q_e \; , \; p(q,x,p_x;H_0) \geq 0 \right\rbrace
\label{eq:sos_xpx}
\end{align}

where $q_e$ is the first configuration space coordinate of the center equilibrium $\mathbf{x}_2^e$ in Eq. \eqref{eq:cfSp_eqCoords}.

\label{fig:EnergySurf_Hills}
\caption{A) Phase space energy surface inside which motion takes place for three different values of the energy; B) Configuration space projection for an energy $H = -0.025$ below the barrier energy; C) for the index-1 saddle energy $H = 0$; D) for the energy $H = 0.1$, above the barrier energy. The parameter values chosen are $\mu = 0.25$, $\alpha = 2$, $\omega = 1.25$ and $\varepsilon = 0$. We have marked the energy accessible regions in white and the forbidden regions in dark grey. The equipotential in black marks the zero velocity curve for which the kinetic energy of the system is zero.}

We focus first on describing the dynamics for the uncoupled system ($\varepsilon = 0$). In this case the ''reaction'' and ''bath'' DoF are uncoupled, and therefore, the system is integrable and the trajectories are regular. The total energy can be partitioned among each DoF separately:

\begin{equation}
H(q,x,p,p_x) = \underbrace{\frac{1}{2} p^2 - \sqrt{\mu} \, q^2 + \frac{\alpha}{3} q^3}_{H_r(q,p)} + \underbrace{\frac{1}{2} \, p_x^2 + \frac{\omega^2}{2} x^2}_{H_b(x,p_x)} \;,
\end{equation}

where $H_r$ and $H_b$ are the Hamiltonians for the reaction and bath DoF respectively. Given a fixed total energy of the system $H_0$, the \textit{necessary condition} in order for reaction to take place is that the total energy is above that of the barrier of the PES located at the origin, which is zero. For $H_0 \leq 0$ the energy surface divides phase space into two disconnected regions as illustrated in Fig. \ref{fig:EnergySurf_Hills}, so that we have bounded motion in the potential well region. In Fig. \ref{fig:LD_PS_uncoupled}, we compare the bounded trajectories (regular dynamics) of the system for $H_0 = 0$ by computing LDs and Poincar\'e section on the SOS $\mathcal{U}_{qp}^{+}$ in Eq. \eqref{eq:sos_qp}. Both methods clearly recover, the trajectories on tori, as known for integrable Hamiltonian systems \cite{Meyer2009}, that foliate the energy surface. We observe that the high values (white regions in Fig. \ref{fig:LD_PS_uncoupled} in the LD contour map recover the quasiperiodic trajectories, which is a consequence of the relationship between the convergence of time averages of LDs with the Ergodic Partition Theorem as explained in \cite{lopesino2017}.

\label{fig:LD_PS_uncoupled}
\caption{Quasiperiodic trajectories describing regular motion in the potential well region of the PES for the uncoupled ($\varepsilon = 0$) Hamiltonian system with energy $H_0 = 0$. The model parameters chosen for this calculation are $\mu = 0.25$, $\alpha = 2$ and $\omega = 1.25$; A) Poincar\'e map; B) LDs obtained for an integration time $\tau = 20$. The curve in magenta depicts the energy boundary.}

When the energy of the system is above the barrier, that is $H_0 > 0$, the topology of the energy surface changes and a phase space bottleneck opens up in the barrier region allowing the escape from the well as shown in Fig.~\ref{fig:EnergySurf_Hills} D). Thus, reaction can take place when trajectories cross the bottleneck, and we can define reaction when the $q$ configuration coordinae changes sign, passing from $q > 0$ to $q < 0$ or vice-versa.	Therefore, $q=0$ is a natural choice for defining in phase space a dividing surface (DS) separating reactants (bounded motion in the potential well region) from products (escape to infinity). Hence, the isoenergetic DS is given by:

\begin{equation}
\mathcal{D}(H_0) = \left\{ (q,x,p,p_x) \in \mathbb{R}^4 \; | \; q = 0 \;,\; 2H_0 = p^2 + p_x^2 + \omega^2 x^2 \right\} \;,
\end{equation}

which has the geometry of a 2-sphere, that is $S^2$ in the three-dimensional energy hypersurface. To be precise, it is an ellipsoid with semi-major axis $\sqrt{2H_0}$ in the $p$, $p_x$ axis, and $\sqrt{2H_0}/\omega$ in the $x$ axis. This ellipsoid has two hemispheres, known as the forward and backward DS with the form:

\begin{equation}
\begin{split}
\mathcal{D}_f(H_0) &= \left\{ (q,x,p,p_x) \in \mathbb{R}^4 \; | \; q = 0 \;,\; p =- \sqrt{2H_0 -  p_x^2 - \omega^2 x^2} \right\} \\[.1cm]
\mathcal{D}_b(H_0) &= \left\{ (q,x,p,p_x) \in \mathbb{R}^4 \; | \; q = 0 \;,\; p = +\sqrt{2H_0 -  p_x^2 - \omega^2 x^2} \right\}
\end{split}
\;.
\end{equation}

Forward reaction occurs when trajectories cross $\mathcal{D}_f$ and escape from the potential well, and backward reaction when trajectories cross $\mathcal{D}_b$ and enter the well region. The forward and backward DS meet at the equator along the NHIM given by:

\begin{equation}
\mathcal{N}(H_0) = \left\{ (q,x,p,p_x) \in \mathbb{R}^4 \; | \; q = p = 0 \;,\; 2H_0 = p_x^2 + \omega^2 x^2 \right\} \;,
\end{equation}

which has the topology of $S^1$. To be precise, it is an ellipse with semiaxis $\sqrt{2H_0}$ in the $p_x$ direction and $\sqrt{2H_0}/\omega$ in the $x$ direction. As discussed earlier, for a 2 DoF Hamiltonian the NHIM is an unstable periodic orbit which extends the influence of the index-1 saddle equilibrium point of the PES, a configuration space concept, into phase space. We note that for $H_0 > 0$ the NHIM is the correct phase space structure that anchors the barriers to the reaction and carries the effect of the index-1 saddle equilibrium point to a range of energies as given by Moser's generalization of Lyapunov Subcenter Manifold Theorem \cite{wiggins2003applied}. The stable and unstable manifolds of the UPO are:

\begin{equation}
\mathcal{W}^s = \Gamma \cup \mathcal{W}^s_l \quad , \quad \mathcal{W}^u = \Gamma \cup \mathcal{W}^u_l \;,
\end{equation}

where $\Gamma$ is the homoclinic orbit:

\begin{equation}
\Gamma = \left\{  (q,x,p,p_x) \in \mathbb{R}^4 \; | \; q > 0 \;,\; H_r(q,p) = 0 \;,\; H_b(x,p_x) = H_0 \right\} \;,
\end{equation}

and the left branches are:

\begin{equation}
\begin{split}
\mathcal{W}^s_l &= \left\{ (q,x,p,p_x) \in \mathbb{R}^4 \; | \; q < 0 \:,\; p > 0 \;,\; H_r(q,p) = 0 \;,\; H_b(x,p_x) = H_0 \right\} \\[.2cm]
\mathcal{W}^u_l &= \left\{ (q,x,p,p_x) \in \mathbb{R}^4 \; | \; q < 0 \:,\; p < 0 \;,\; H_r(q,p) = 0 \;,\; H_b(x,p_x) = H_0 \right\}
\end{split}
\;.
\end{equation}

Notice that the stable and unstable manifolds have the structure of a cartesian product of a curve in the $(q,p)$ saddle space and an ellipse in the $(x,p_x)$ center space, and thus become cylindrical (or \textit{tube}) manifolds. We show the energy surface for the uncoupled case ($\varepsilon = 0$) in Fig. \ref{fig:EnergySurf_Hills} A) for three energy values, where the bottleneck only opens for $H_0 > 0$. The NHIM computed using differential correction and continuation, and its invariant manifolds computed using globalization are shown in Fig. \ref{fig:set2_manifolds_upo_energysurf}A) for $H_0 = 0.05$ and $\varepsilon = 0$.

\label{fig:set2_manifolds_upo_energysurf}
\caption{Configuration and phase space view of the energy surface and the cylindrical manifolds of the UPO associated with the index-1 saddle in the bottleneck. Blue and red denote the stable and unstable manifolds respectively, and the green surface is the isosurface of total energy $H_0 = 0.05$. The magenta curve represents the UPO. A) and B) use $\epsilon = 0$; C) and D) $\epsilon = 0.25$.}

We discuss next the dynamics of the system when the reactive and bath DoF are coupled, i.e. $\varepsilon \neq 0$, and we do so for $\varepsilon$ small. We can thus think about the resulting dynamics as a perturbation of the uncoupled case. Given an energy of the system below that of the barrier, that is $H_0 \leq 0$, the phase space bottleneck is closed and trajectories are trapped in the potential well region. However, due to the perturbation, the regular motion that existed on the tori in the uncoupled system is destroyed as given by the KAM theorem, and  therefore chaotic motion arises in some regions of the phase space. We illustrate the system's behavior for the energy $H_0 = 0$ (energy of the index-1 saddle) in Fig. \ref{fig:ps_vs_ld_1} using LDs and Poincar\'e maps on the SOSs described in Eqs. \eqref{eq:sos_qp} and \eqref{eq:sos_xpx}. We observe that there is a distinct correlation between the qualitative dynamics revealed by the Lagrangian descriptor (LD) contour maps and Poincar\'e sections. That is, chaotic regions of phase space which appear as a sea of points in the Poincar\'e section and hide the underlying structures of stable and unstable manifolds are completely resolved by LD contour maps where the tangled geometry of the manifolds is revealed by the points where the function is non-differentiable and attains a local minimum as has been shown in \cite{lopesino2017,naik2019b,GG2019}. The capability of LDs to identify the phase space structures relevant in chemical reaction dynamics can also be found in recent literature \cite{craven2016deconstructing,demian2017,Naik2019a,revuelta2019unveiling}. In addition, the generation of Poincar\'e sections relies on tracking the crossing of a 2D surface which can not be guaranteed in high dimensional phase space, while LDs just accumulate a positive scalar quantity along the trajectory and thus have potential to reveal high dimensional phase space structures.

\label{fig:ps_vs_ld_1}

\caption{Phase space structures of the coupled Hamiltonian with model parameters $\mu = 0.25$, $\alpha = 2$, $\omega = 1.25$ and $\varepsilon = 0.25$. The total energy of the system is $H_0 = 0$ (barrier energy). A) Poincar\'e map on the surface of section $\mathcal{U}^{+}_{qp}$; B) LDs calculated for $\tau = 75$ on the surface of section $\mathcal{U}^{+}_{qp}$; C) Poincar\'e map on the surface of section $\mathcal{U}^{+}_{xp_x}$; D) LDs calculated for $\tau = 75$ on the surface of section $\mathcal{U}^{+}_{xp_x}$. We have marked the energy boundary with a magenta curve.}

To finish the analysis, we consider the dynamics when the total energy is above that of the index-1 saddle, that is $H_0 > 0$, and in particular we will set $H_0 = 0.05$ in our computations. In this situation, a phase space bottleneck opens up in the barrier region, as shown in Fig. \ref{fig:EnergySurf_Hills}. In order to describe the structures that mediate reaction dynamics, that is the NHIM (or unstable periodic orbit) and its stable and unstable manifolds, we use differential correction and continuation along with globalization as described in \cite{Koon2011,naik2017geometry,naik2019b,GG2019}. In Fig. \ref{fig:set2_manifolds_upo_energysurf} panels C) and D), we show the cylindrical manifolds along with the energy surface and the unstable periodic orbit. In order to recover the homoclinic tangle geometry of the invariant manifolds, we calculate LDs on the surfaces of section $\mathcal{U}^{+}_{qp}$ and $\mathcal{U}^{+}_{xp_x}$, and compare with the direct numerical construction of these invariant manifolds. This LD based diagnostic is similar to performing a ''phase space tomography'' of the high dimensional phase space structures using a low dimensional slice. In Fig. \ref{fig:LD_NHIM_detect} we show the computation of variable time LDs for an integration time $\tau = 10$ on the slice $\mathcal{U}^{+}_{qp}$. We observe that Lagrangian descriptors clearly identify the location of the stable and unstable manifolds and the NHIM at the intersection of the invariant manifolds. Since we are using a small integration time of $\tau = 10$ to compute LDs, the complete geometry of the homoclinic tangle is not fully revealed. Therefore, in order to recover a more complete and intricate dynamical picture of the homoclinic tangle, the integration time to compute LDs has to be increased. This is shown in Fig. \ref{fig:ps_vs_ld_2}B), where $\tau = 30$ and we observe that the regular motion obtained in the middle of the Poincar\'e section displayed in Fig. \ref{fig:ps_vs_ld_2} A) corresponds to trajectories that remain trapped in the potential well region and never escape. The trapped dynamics on the tori are also captured by the LD contour map shown in Fig. \ref{fig:ps_vs_ld_2}B) which also reveals the homoclinic tangle of the stable and unstable manifolds and the resulting lobe dynamics \cite{beigie1992}. We note that in all these computations we are using the variable time definition of LDs, since the open potential surface causes trajectories to escape to infinity through the bottleneck in finite time, resulting in NaN values in the LD contour map and would hide the important underlying phase space structures making the interpretation of results difficult. In addition, when we analyze the dynamics using Poincar\'e sections, we cannot ensure that trajectories return to the surface of section when the dynamical behavior of escaping to infinity is possible in the system. This will result in blank regions in the Poincar\'e sections as shown in Fig. \ref{fig:ps_vs_ld_2}A) and C). However, we note that trapped trajectories in the potential well corresponding to regular (motion on the tori) and chaotic motion are highlighted as expected in the Poincar\'e sections. These trajectories are non-reactive and will remain so until they satisfy the \textit{sufficient} condition for reaction which is entering the cylindrical manifolds.

\label{fig:LD_NHIM_detect}

\caption{Variable time LDs calculated on the surface of section $\mathcal{U}^{+}_{qp}$ for $\tau = 10$. The NHIM and its stable and unstable manifolds are revealed as points where the LD field in non-differentiable and attains a local minimum. We have marked the energy boundary with a magenta curve.}

Identifying the regions inside the cylindrical (tube) stable and unstable manifolds can be done using globalization or using the LD contour map on appropriate isoenergetic surfaces. We compute LD contour maps to recover the intersections of these tube manifolds with the surface of section in Eq. \eqref{eq:sos_xpx}. The intersection of the invariant manifolds with the isoenergetic surface of section yields topological ellipses, known in the chemical reactions literature as \textit{reactive islands} \cite{deleon1991a,deleon1991b,deleon1992,patra2018detecting} which are of paramount importance for the computation of reaction rates/fraction. In order to illustrate the capability of LDs to recover the reactive island structure, we have compared the LD contour map and manifold intersection with the surface of section $\mathcal{U}^{+}_{xp_x}$ obtained using globalization in Fig. \ref{fig:LD_Manifolds}. We have shown the first intersection of the stable and unstable manifolds with the surface of section as a blue/red curve superimposed on the LD contour map. We observe that the points at which the LDs scalar field is non-differentiable and the manifolds intersections are in agreement, thus verifying the LD based identification of reactive islands. We also observe that successive folds and resulting intersections of the tube manifolds with the surface of section are also revealed by LDs. We note here that the first intersection of the manifolds encloses a large phase space volume in the potential well which indicates that a large portion of the potential well escapes to infinity through the bottleneck. This is despite the fact that we have chosen a very small value for the energy of the system, $H_0 = 0.05$, compared to the energy of the barrier. This is a consequence of using a high coupling strength, $\varepsilon = 0.25$, which makes the vase-like shaped potential well region small and narrow, since the center equilibrium point at the bottom of the potential well approaches the index-1 saddle equilibrium point at the origin as $\varepsilon$ is increased. Movies illustrating the change in both the shape of the energy surface and the equipotentials in configurations space as we vary the coupling strength can be found at the links https://youtu.be/kYklCrSwtls and https://youtu.be/0FNChWVM6nU respectively. We can see that the effect of increasing the coupling strength from zero is to \textit{tilt and squeeze} the vase-like shape of the energy surface that qualitatively increases the number of reactive trajectories. This action of tilting and squeezing of the vase-like container (boundary defined by the total energy surface) is to pour out its contents (the reactive trajectories) and hence the increase in reaction fraction. A quantitative investigation of this phenomenon is current work in progress and beyond the scope of this chapter.

\label{fig:ps_vs_ld_2}

\caption{Phase space structures of the coupled Hamiltonian with model parameters $\mu = 0.25$, $\alpha = 2$, $\omega = 1.25$ and $\varepsilon = 0.25$. The total energy of the system is $H_0 = 0.05$,	which is above the barrier energy. A) Poincar\'e map on the surface of section $\mathcal{U}^{+}_{qp}$; B) LDs calculated for $\tau = 30$ on the surface of section $\mathcal{U}^{+}_{qp}$; C) Poincar\'e map on the surface of section $\mathcal{U}^{+}_{xp_x}$; D) LDs calculated for $\tau = 30$ on the surface of section $\mathcal{U}^{+}_{xp_x}$. We have marked with a magenta curve the energy boundary.}

\label{fig:LD_Manifolds}
\caption{Phase space structures revealed by LDs using $\tau = 30$ for the coupled Hamiltonian with parameters $\mu = 0.25$, $\alpha = 2$, $\omega = 1.25$ and total energy $H_0 = 0.05$. A) Coupling strength $\varepsilon = 0.25$. On the top/bottom left we show the forward/backward LD and, superimposed, the first intersection of the stable/unstable manifold with $\mathcal{U}^{+}_{xp_x}$ as a blue/red curve. On the right, the total LD is depicted (addition of forward and backward LD) together with the first intersection of the stable and unstable manifolds. The magenta curve represents the energy boundary. B) same analysis using $\varepsilon = 0.125$.}

## Implications for reaction dynamics

# References

\bibliography{HamSN}