 ILI285 - Computación Científica I / INF285 - Computación Científica

 Least Squares

  [S]cientific [C]omputing [T]eam

 Version: 1.27

## Table of Contents
* [Introduction](#intro)
* [QR Factorization](#qr)
* [Examples](#ex)
* [Inconsistents Systems](#in)
* [A Survey of Models](#sm)
* [Acknowledgements](#acknowledgements)

```python
import numpy as np
import matplotlib.pyplot as plt
import scipy.linalg as spla
%matplotlib inline
# https://scikit-learn.org/stable/modules/classes.html#module-sklearn.datasets
from sklearn import datasets
import ipywidgets as widgets
import matplotlib as mpl
mpl.rcParams['font.size'] = 14
mpl.rcParams['axes.labelsize'] = 20
mpl.rcParams['xtick.labelsize'] = 14
mpl.rcParams['ytick.labelsize'] = 14
M=8
```

## Introduction

We have learned about square linear system of equations.
However, How can we solve a non-square system? (More equations that unknowns!) Well, we need to find a least squares approximation.

# QR Factorization

## Gram-Schmidt Orthogonalization

This algorithm orthogonalize a set of input vectors, returning an **orthogonal set** that spans the same column space.
We will only consider now that the input set of vectors are **linearly independent**.

Let $A=[\mathbf{a}_1\, ...., \mathbf{a}_n]$ a matrix with linearly independent column vectors $\in\mathbb{R}^m$ and $n \le m$.

We know the following for the orthogonal set:

\begin{align*}
    \mathbf{q}_i^*\,\mathbf{q}_i & = \|\mathbf{q}_i\|_2^2= 1\\
    \mathbf{q}_i^*\,\mathbf{q}_j & = 0, \, \text{ for } i\neq j
\end{align*}

Then the Gram-Schmidt orthogonalization finds the $\mathbf{q}_i$ and $r_{ij}$ from the following set of equations and considering the previous constraints:
\begin{align*}
    \mathbf{a}_1 &= r_{11}\,\mathbf{q}_1\\
    r_{11} &= \|\mathbf{a}_1\|_2\\
    \mathbf{q}_1 &= \dfrac{\mathbf{a}_1}{r_{11}}\\
    \mathbf{a}_2 &= r_{12}\,\mathbf{q}_1+r_{22}\,\mathbf{q}_2\\
    r_{12} &= \mathbf{q}_1^*\,\mathbf{a}_2\\
    r_{22} &= \|\mathbf{a}_2-r_{12}\,\mathbf{q}_1\|_2\\
    \mathbf{q}_2 &= \dfrac{\mathbf{a}_2-r_{12}\,\mathbf{q}_1}{r_{22}}\\
    \vdots &=  \vdots\\
    \mathbf{a}_j &= \sum_{i=1}^j r_{ij}\,\mathbf{q}_i\\
    r_{ij} &= \mathbf{q}_i^*\,\mathbf{a}_j, \, \text{ for } i<j\\
    r_{jj} &= \left\|\mathbf{a}_j-\sum_{i=1}^{j-1} r_{ij}\,\mathbf{q}_i\right\|_2\\
    \mathbf{q}_j &= \dfrac{\mathbf{a}_j-\sum_{i=1}^{j-1} r_{ij}\,\mathbf{q}_i}{r_jj}\\
    \vdots &=  \vdots\\
    \mathbf{a}_n &= \sum_{i=1}^n r_{in}\,\mathbf{q}_i\\
    r_{in} &= \mathbf{q}_i^*\,\mathbf{a}_n, \, \text{ for } i<n\\
    r_{nn} &= \left\|\mathbf{a}_n-\sum_{i=1}^{n-1} r_{in}\,\mathbf{q}_i\right\|_2\\
    \mathbf{q}_n &= \dfrac{\mathbf{a}_n-\sum_{i=1}^{n-1} r_{in}\,\mathbf{q}_i}{r_{nn}}
\end{align*}

Thus, we obtain the QR decomposition as follows:

\begin{equation}
                 \mathbf{a}_{m\times n} = Q_{m\times n}R_{n\times n}\\
\end{equation}

Where $Q$ is a matrix of vectors $\mathbf{q}_{n}$, and $R$ is an upper-triangular matrix, with the coefficients $r_{ij}$:

This is known as the **Reduced QR Factorization**.

_**[IMPORTANT]** What is then a **full** QR decomposition?_

```python
# Inputs:
# A: A set of linearly independent columns
# type_factorization: reduced or full
# type_gram_schmidt: classic or modified
def QR(A, type_factorization = 'reduced', type_gram_schmidt='classic'):
    A.astype('float')
    if type_factorization == 'reduced':
        Q = np.zeros(A.shape)
        R = np.zeros((A.shape[1],A.shape[1]))
    elif type_factorization == 'full':
        Q = np.zeros((A.shape[0],A.shape[0]))
        R = np.zeros(A.shape)
    for j in np.arange(A.shape[1]):
        y = A[:,j]
        for i in np.arange(j):
            if type_gram_schmidt == 'classic':
                R[i,j] = np.dot(Q[:,i],A[:,j])
            elif type_gram_schmidt == 'modified':
                R[i,j] = np.dot(Q[:,i],y)
            y=y-R[i,j]*Q[:,i]
        R[j,j] = np.linalg.norm(y)
        Q[:,j] = y/np.linalg.norm(R[j,j])
    # The following lines must be completed by you!
    #if type_factorization == 'full':
        # (1) We need to add 0's to the R matrix so it is of the same shape as the matrix A,
        # fortunately this was already done!
        # (2) We need to add orthogonal vectors to Q so it is square,
        # how do we do this?
    return Q,R
```

```python
A = np.array([[1,-4],[2,3],[2,2]])
Qa, Ra = QR(A, type_factorization ='reduced', type_gram_schmidt='classic')
print(A)
print(np.dot(Qa,Ra))
print(Qa)
print(Ra)
```

    [[ 1 -4]
     [ 2  3]
     [ 2  2]]
    [[ 1. -4.]
     [ 2.  3.]
     [ 2.  2.]]
    [[ 0.33333333 -0.93333333]
     [ 0.66666667  0.33333333]
     [ 0.66666667  0.13333333]]
    [[3. 2.]
     [0. 5.]]

This method let us resolve a system of equations. However, exists a **Full QR Factorization**, creating the next system:

\begin{equation}
                 A_{m\times n} = Q_{m\times m}R_{m\times n}\\
\end{equation}

Q is a square orthogonal matrix, adding $m-n$ columns and R grows adding $m-n$ zero rows.

#### Theorem
A square matrix $Q$ is orthogonal if $Q^*\, = Q^{-1}$

## Examples

### Normal vs Modified Gram-Schmidt

```python
d = 1e-10
A = np.array([[1,1,1],[d,0,0],[0,d,0],[0,0,d]])
print(A)

Q1,R1 = QR(A, type_gram_schmidt = 'classic')
Q2,R2 = QR(A, type_gram_schmidt = 'modified')

# What are the Q's?
print('What are the Q\'s?')
print(Q1)
print(Q2)
# Are truly orthogonal the Q's?
print('Are truly orthogonal the Q\'s?')
print(np.dot(Q1.transpose(),Q1)) # Warning: We are just using the transpose since the matrices are real!
print(np.dot(Q2.transpose(),Q2))
# Do we recover A?
print('Showing Q1 and Q2')
print(np.dot(Q1,R1)-A)
print(np.dot(Q2,R2)-A)
```

    [[1.e+00 1.e+00 1.e+00]
     [1.e-10 0.e+00 0.e+00]
     [0.e+00 1.e-10 0.e+00]
     [0.e+00 0.e+00 1.e-10]]
    What are the Q's?
    [[ 1.00000000e+00  0.00000000e+00  0.00000000e+00]
     [ 1.00000000e-10 -7.07106781e-01 -7.07106781e-01]
     [ 0.00000000e+00  7.07106781e-01  0.00000000e+00]
     [ 0.00000000e+00  0.00000000e+00  7.07106781e-01]]
    [[ 1.00000000e+00  0.00000000e+00  0.00000000e+00]
     [ 1.00000000e-10 -7.07106781e-01 -4.08248290e-01]
     [ 0.00000000e+00  7.07106781e-01 -4.08248290e-01]
     [ 0.00000000e+00  0.00000000e+00  8.16496581e-01]]
    Are truly orthogonal the Q's?
    [[ 1.00000000e+00 -7.07106781e-11 -7.07106781e-11]
     [-7.07106781e-11  1.00000000e+00  5.00000000e-01]
     [-7.07106781e-11  5.00000000e-01  1.00000000e+00]]
    [[ 1.00000000e+00 -7.07106781e-11 -4.08248290e-11]
     [-7.07106781e-11  1.00000000e+00 -1.66533454e-16]
     [-4.08248290e-11 -1.66533454e-16  1.00000000e+00]]
    Showing Q1 and Q2
    [[ 0.00000000e+00  0.00000000e+00  0.00000000e+00]
     [ 0.00000000e+00  0.00000000e+00 -3.62324963e-27]
     [ 0.00000000e+00  0.00000000e+00  0.00000000e+00]
     [ 0.00000000e+00  0.00000000e+00  0.00000000e+00]]
    [[ 0.00000000e+00  0.00000000e+00  0.00000000e+00]
     [ 0.00000000e+00  0.00000000e+00 -1.77879510e-27]
     [ 0.00000000e+00  0.00000000e+00 -3.12420006e-27]
     [ 0.00000000e+00  0.00000000e+00  0.00000000e+00]]

## Overdetermined Linear Systems of Equations

There is cases where the number of equations is greater than variables. Many times, those systems don't have an exact solution (inconsistent system). Then, in this case we needs an approximation closest to the data. Based in orthogonality, the shortest distance from a point to plane. The orthogonal distance represents the error which would be minimum.

\begin{equation}
                 \mathbf{b} - A\,\mathbf{x} = \mathbf{r}\\
                 \mathbf{b} - A\,\mathbf{x} \perp \{A\,\mathbf{x}\, |\, \mathbf{x} \in \mathbb{R}^m\}
\end{equation}

The idea is that $\mathbf{r}$ would be closest to zero. We need to apply orthogonality to find the vector that satisfied this condition.

\begin{equation}
                 (A\,\mathbf{x})^*\,(\mathbf{b}-A\,\overline{\mathbf{x}})=0, \hspace{1cm} \text{for all } \mathbf{x} \in \mathbb{R}^n\\
                 \mathbf{x}^*\, A^*\,(\mathbf{b}-A\,\overline{\mathbf{x}})=0, \hspace{1cm} \text{for all } \mathbf{x} \in \mathbb{R}^n\\
                 A^*\,(\mathbf{b}-A\,\overline{\mathbf{x}})=\mathbf{0} \\
                 A^*\,A\,\overline{\mathbf{x}}= A^*\,\mathbf{b} \\
\end{equation}

This last equation gives us a new square $n\times n$ matrix, which let us resolve the equation system.
This linear system of equations is known as the **Normal Equations**.

```python
def least_squares(A,b):
    Q,R = QR(A,type_gram_schmidt='modified')
    return spla.solve_triangular(R,np.dot(Q.T,b))

def solve_model(M):
    A=M['A']
    b=M['b']
    M['x_bar']=least_squares(A,b)
    return M

def create_model(data, type_model='linear'):
    if type_model == 'linear': # f(x)=a0+a1*x
        A = np.ones((data.shape[0],2))
        A[:,1] = data[:,0]
        b = data[:,1]
    if type_model == 'parabollic': # f(x)=a0+a1*x+a_2*x^2
        A = np.ones((data.shape[0],3))
        A[:,1] = data[:,0]
        A[:,2] = data[:,0]**2
        b = data[:,1]
    if type_model == 'exponential': #f(x)=a0 \exp(a1*x) = \exp(\log(a0)+a1*x) -> log(f(x))=log(a0)+a1*x = A0+a1+x (it is linear now!)
        A = np.ones((data.shape[0],2))
        A[:,1] = data[:,0]
        b = np.log(data[:,1])
    M = {'A':A,
         'b':b,
         'type_model':type_model}
    M=solve_model(M)
    return M

def evaluate_model(M,x):
    x_bar=M['x_bar']
    if M['type_model'] == 'linear':
        return x_bar[0] + x_bar[1]*x
    if M['type_model'] == 'parabollic':
        return x_bar[0] + x_bar[1]*x + x_bar[2]*x**2
    if M['type_model'] == 'exponential':
        return np.exp(x_bar[0]+x_bar[1]*x)
```

# Adjusting some models

```python
def generate_data(type_of_data='linear'):
    n=40
    np.random.seed(0)
    x = np.linspace(0,10,n)
    y = np.random.rand(n)
    x = np.concatenate((x,x,y),axis=0)
    n = 3*n
    if type_of_data=='linear':
        y = x+0.1*np.random.normal(0,1,n)+1.5
    elif type_of_data=='parabollic':
        y = 4*x**2+0.1*x*np.random.normal(0,1,n)+1.5
    elif type_of_data=='exponential':
        y = np.exp(x+0.1*np.random.normal(0,1,n)+1.5)
    elif type_of_data=='sinusoidal':
        y = np.sin(2*np.pi*x/10)+0.1*np.random.normal(0,1,n)+1.5
    elif type_of_data=='random':
        y = 0.1*np.random.normal(0,1,n)+1.5
    elif type_of_data=='boston house-prices':
        x,y=datasets.load_boston(return_X_y=True)
        x=x[:,5]
    elif type_of_data=='diabetes':
        x,y=datasets.load_diabetes(return_X_y=True)
        x=x[:,2]
    data = np.stack((x, y)).T
    return data
```

```python
def looking_at_data(type_of_data='diabetes'):
    data=generate_data(type_of_data)
    Ml = create_model(data, type_model='linear')
    Mp = create_model(data, type_model='parabollic')
    Me = create_model(data, type_model='exponential')
    xx=np.linspace(np.min(data[:,0])-0.1,np.max(data[:,0])+0.1,1000)
    yyl=evaluate_model(Ml,xx)
    yyp=evaluate_model(Mp,xx)
    yye=evaluate_model(Me,xx)

    error_l=data[:,1]-evaluate_model(Ml,data[:,0])
    error_p=data[:,1]-evaluate_model(Mp,data[:,0])
    error_e=data[:,1]-evaluate_model(Me,data[:,0])

    plt.figure(figsize=(2*M,M))

    plt.subplot(1, 2, 1)
    plt.plot(xx,yyl,'k-',linewidth=5,label='linear model')
    plt.plot(xx,yyp,'y-',linewidth=20,label='parabollic model',alpha=0.4)
    plt.plot(xx,yye,'g-',linewidth=5,label='exponential model')
    plt.plot(data[:,0],data[:,1],'.b',markersize=20,label='original data',alpha=0.3)
    plt.grid(True)
    plt.xlabel(r'$x$')
    plt.ylabel(r'$y$')
    plt.legend(loc='best')
    #plt.ylim(0,10)

    plt.subplot(1, 2, 2)
    plt.title('What does this histogram tell us?')
    three_errors=np.vstack((error_l, error_p, error_e)).T
    plt.hist(three_errors, bins=20,
             label=['linear','parabollic','exponential'],
             color=['k','y','g'], alpha=0.5)
    plt.legend(loc='best')
    plt.grid(True)

    plt.show()
widgets.interact(looking_at_data,type_of_data=['linear','parabollic','exponential','sinusoidal','random','boston house-prices','diabetes'])
```

    interactive(children=(Dropdown(description='type_of_data', index=6, options=('linear', 'parabollic', 'exponent…

    <function __main__.looking_at_data(type_of_data='diabetes')>

## References

### Numpy Least Squares
http://docs.scipy.org/doc/numpy/reference/generated/numpy.linalg.lstsq.html

### Numpy QR Factorization
http://docs.scipy.org/doc/numpy/reference/generated/numpy.linalg.qr.html

### Scikit Learn Datasets
https://scikit-learn.org/stable/modules/classes.html#module-sklearn.datasets

# Acknowledgements
* _Material created by professor Claudio Torres_ (`ctorres@inf.utfsm.cl`) _and assistans: Laura Bermeo, Alvaro Salinas, Axel Símonsen and Martín Villanueva. DI UTFSM. April 2016._
* _Material updated by professor Claudio Torres_ (`ctorres@inf.utfsm.cl`) DI UTFSM. June 2017.
* _Material updated by professor Claudio Torres_ (`ctorres@inf.utfsm.cl`) DI UTFSM. July 2019.
* _Material updated by professor Claudio Torres_ (`ctorres@inf.utfsm.cl`) DI UTFSM. August 2019.
* _Update July 2020 - v1.27 - C.Torres_ : Fixing formatting issues.

```python

```


# Quantization of Signals

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

## Oversampling

[Oversampling](https://en.wikipedia.org/wiki/Oversampling) is a technique which is applied in [analog-to-digital converters](https://en.wikipedia.org/wiki/Analog-to-digital_converter) to lower the average power of the quantization error. It requires a joint consideration of sampling and quantization.

### Ideal Analog-to-Digital Conversion

Let's consider the ideal sampling of a signal followed by its quantization, as given by the following block diagram

Ideal sampling is modeled by multiplying the continuous signal $x(t)$ with a series of equidistant Dirac impulse, resulting in the discrete signal $x[k] = x(k T)$ where $T$ denotes the sampling interval. The discrete signal $x[k]$ is then quantized. The output of the ideal analog-to-digital converter is the quantized discrete signal $x_\text{Q}[k]$.

### Nyquist Sampling

Sampling of the continuous signal $x(t)$ leads to repetitions of the spectrum $X(j \omega) = \mathcal{F} \{ x(t) \}$ at multiples of $\omega_\text{S} = \frac{2 \pi}{T}$. We limit ourselves to a continuous real-valued $x(t) \in \mathbb{R}$, band-limited signal $| X(j \omega) | = 0$ for $|\omega| > \omega_\text{C}$ where $\omega_\text{C}$ denotes its cut-off frequency. The spectral repetitions due to sampling do not overlap if the [sampling theorem](https://en.wikipedia.org/wiki/Nyquist%E2%80%93Shannon_sampling_theorem) $\omega_\text{S} \geq  2 \cdot \omega_\text{C}$ is fulfilled. In the case of Nyquist (critical) sampling, the sampling frequency is chosen as $\omega_\text{S} =  2 \cdot \omega_\text{C}$.

### Oversampling

The basic idea of oversampling is to sample the input signal at frequencies which are significantly higher than the Nyquist criterion dictates. After quantization, the signal is low-pass filtered by a discrete filter $H_\text{LP}(\mathrm{e}^{\,\mathrm{j}\,\Omega})$ and resampled back to the Nyquist rate. In order to avoid aliasing due to the resampling this filter has to be chosen as an ideal low-pass

\begin{equation}
H_\text{LP}(\mathrm{e}^{\,\mathrm{j}\,\Omega}) = \text{rect} \left( \frac{\Omega}{2 \, \Omega_\text{C}} \right)
\end{equation}

where $\Omega_\text{C} = \omega_\text{C} \cdot T$. For an oversampling of factor $L \in \mathbb{N}$ we have $\omega_\text{S} = L \cdot 2 \omega_\text{C}$. For this case, the resampling can be realized by keeping only every $L$-th sample which is known as decimation. The following block diagram illustrates the building blocks of oversampled digital-to-analog conversion, $\downarrow L$ denotes decimation by a factor of $L$

In order the conclude on the benefits of oversampling we have to derive the average power of the overall quantization error. According to our [model of the quantization error](linear_uniform_quantization_error.ipynb#Model-for-the-Quantization-Error), the quantization error $e[k]$ can be modeled as uniformly distributed white noise. Its power spectral density (PSD) is given as

\begin{equation}
\Phi_{ee}(\mathrm{e}^{\,\mathrm{j}\,\Omega}) = \frac{Q^2}{12}
\end{equation}

where $Q$ denotes the quantization step. Before the discrete low-pass filter $H_\text{LP}(\mathrm{e}^{\,\mathrm{j}\,\Omega})$, the power of the quantization error is uniformly distributed over the entire frequency range $-\pi < \Omega \leq \pi$. However, after the ideal low-pass filter its frequency range is limited to $- \frac{\pi}{L} < \Omega \leq \frac{\pi}{L}$. The average power of the quantization error is then given as

\begin{equation}
\sigma_{e, \text{LP}}^2 = \frac{1}{2 \pi} \int\limits_{- \frac{\pi}{L}}^{\frac{\pi}{L}} \Phi_{ee}(\mathrm{e}^{\,\mathrm{j}\,\Omega})  \; \mathrm{d}\Omega = \frac{1}{L} \cdot \frac{Q^2}{12}
\end{equation}

The average power $\sigma_x^2$ of the sampled signal $x[k]$ is not affected, since the cutoff frequency of the low-pass filter has been chosen as the upper frequency limit $\omega_\text{C}$ of the input signal $x(t)$.

In order to calculate the SNR of the oversampled analog-to-digital converter we assume that the input signal is drawn from a wide-sense stationary (WSS) uniformly distributed zero-mean random process with $x_\text{min} \leq x[k] < x_\text{max}$. With the results from our discussion of [linear uniform quantization](linear_uniform_quantization_error.ipynb#Uniformly-Distributed-Signal) and $\sigma_{e, \text{LP}}^2$ from above we get

\begin{equation}
SNR = 10 \cdot \log_{10} \left( 2^{2 w} \right) + 10 \cdot \log_{10} \left( L \right) \approx 6.02 \, w + 10 \cdot \log_{10} \left( L \right) \quad \text{in dB}
\end{equation}

where $w$ denotes the number of bits used for a binary representation of the quantization index. Hence, oversampling by a factor of $L$ brings a plus of $10 \cdot \log_{10} \left( L \right)$ dB in terms of SNR. For instance, an oversampling by a factor of $L = 4$ results in a SNR which is approximately 6 dB higher. For equal SNR the quantization step $Q$ can be chosen larger. In terms of the wordlength of a quantizer this accounts to a reduction by one bit. Consequently, there is a trade-off between accuracy of the quantizer and its sampling frequency.

### Example

The following numerical simulation illustrates the benefit in terms of SNR for an oversampled linear uniform quantizer with $w = 16$ for the quantization of the harmonic signal $x[k] = \cos[\Omega_0 k]$.

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

w = 16  # wordlength of the quantized signal
L = 2**np.arange(1, 10)  # oversampling factors

N = 8192  # length of signal
Om0 = 100*2*np.pi/N  # frequency of harmonic signal
Q = 1/(2**(w-1))  # quantization step

def uniform_midtread_quantizer(x, Q):
    '''Uniform mid-tread qantizer with limiter.'''
    # limiter
    x = np.copy(x)
    idx = np.where(x <= -1)
    x[idx] = -1
    idx = np.where(x > 1 - Q)
    x[idx] = 1 - Q
    # linear uniform quantization
    xQ = Q * np.floor(x/Q + 1/2)

    return xQ

def SNR_oversampled_ADC(L):
    '''Estimate SNR of oversampled analog-to-digital converter.'''
    x = (1-Q)*np.cos(Om0*np.arange(N))
    xu = (1-Q)*np.cos(Om0*np.arange(N*L)/L)
    # quantize signal
    xQu = uniform_midtread_quantizer(xu, Q)
    # low-pass filtering and decimation
    xQ = sig.resample(xQu, N)
    # estimate SNR
    e = xQ - x

    return 10*np.log10((np.var(x)/np.var(e)))

# compute SNR for oversampled ADC
SNR = [SNR_oversampled_ADC(l) for l in L]

# plot result
plt.figure(figsize=(10, 4))
plt.semilogx(L, SNR, label='SNR with oversampling')
plt.plot(L, (6.02*w+1.76)*np.ones(L.shape), label='SNR w/o oversampling')
plt.xlabel(r'oversampling factor $L$')
plt.ylabel(r'SNR in dB')
plt.legend(loc='upper left')
plt.grid()
```

**Exercise**

* What SNR can be achieved for an oversampling factor of $L=16$?
* By how many bits could the word length $w$ be reduced in order to gain the same SNR as without oversampling?

Solution: The SNR for the quantization of a uniformly distributed input signal without oversampling is $\text{SNR} \approx 6.02 w \approx 96$ dB and with 16 times oversampling $\text{SNR}_{L} \approx 6.02 w + 10 \cdot \log_{10} (16)  \approx 96 + 12$ dB. Since the [quantization of a harmonic signal](linear_uniform_quantization_error.ipynb#Harmonic-Signal) is considered an offset of $1.76$ dB has to added to both. The wordlength could be reduced by 2 bits according to these numbers.

### Anti-Aliasing Filter

Besides an increased SNR, oversampling has also another benefit. In order to ensure that the input signal $x(t)$ is band-limited before sampling, a low-pass filter $H_\text{LP}(\mathrm{j}\,\omega)$ is applied in typical analog-to-digital converters. This is illustrated in the following

The filter $H_\text{LP}(\mathrm{j}\,\omega)$ is also known as [anti-aliasing filter](https://en.wikipedia.org/wiki/Anti-aliasing_filter). The ideal low-pass filter is given as $H_\text{LP}(\mathrm{j}\,\omega) = \text{rect}\left( \frac{\omega}{\omega_\text{S}}  \right)$. The ideal $H_\text{LP}(\mathrm{j}\,\omega)$ can only be approximated in the analog domain. Since the sampling rate is higher than the Nyquist rate, there is no need for a steep slope of the filter in order to avoid aliasing. However, the pass-band of the filter within $|\omega| < |\omega_\text{C}|$ has to be flat.

Before decimation, the discrete filter $H_\text{LP}(\mathrm{e}^{\,\mathrm{j}\,\Omega})$ has to remove the spectral contributions that may lead to aliasing. However, a discrete filter $H_\text{LP}(\mathrm{e}^{\,\mathrm{j}\,\Omega})$ with steep slope can be realized much easier than in the analog domain.

**Copyright**

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


# CHEM 1000 - Spring 2022
Prof. Geoffrey Hutchison, University of Pittsburgh

## Graded Homework 3

For this homework, we'll focus on:
- vector arithmetic
- scalar dot product
- vector cross product
- simple operators
---

As a reminder, you do not need to use Python to solve the problems. If you want, you can use other methods, just put your answers in the appropriate places.

To turn in, either download as Notebook (.ipynb) or Print to PDF and upload to Gradescope.

Make sure you fill in any place that says YOUR CODE HERE or "YOUR ANSWER HERE", as well as your name and collaborators (i.e., anyone you discussed this with) below:

```python
NAME = ""
COLLABORATORS = ""
```

### Vector Arithmetic (4 points)

We're going to use some vectors on a more complicated molecule, biphenyl:

```python
import py3Dmol

# 7095 is the PubChem Compound ID (CID) for biphenyl:
# https://pubchem.ncbi.nlm.nih.gov/compound/Biphenyl
view = py3Dmol.view(width=400,height=400,query='cid:7095')
view.setStyle({'stick':{}})
view.addSurface(py3Dmol.VDW,{'opacity':0.7,'color':'white'})
view.zoomTo()
view.show()
```

You appear to be running in JupyterLab (or JavaScript failed to load for some other reason).  You need to install the 3dmol extension:

        <tt>jupyter labextension install jupyterlab_3dmol</tt>

```python
# here are the coordinates of the indicated atoms
import numpy as np

c1 = np.array([ 1.441, -1.132, 0.414])
c2 = np.array([ 0.742,  0.001, 0.004])
c3 = np.array([-0.742,  0.000, 0.002])
c4 = np.array([-1.443,  1.133, 0.412])

# find the vector for c1-c2 bond by subtraction
c1c2 =
c2c3 =
c3c4 =

# print the length of the c1c2 bond (i.e., the magnitude or 'norm' in numpy)
print( round(np.linalg.norm(c1c2), 3) )

# print the length of the c2c3 bond
print( round(np.linalg.norm(c2c3), 3) )

# print the length of the c3c4 bond
print( round(np.linalg.norm(c3c4), 3) )
```

**Concept**: Are the bonds all the same length or not? Why?

YOUR ANSWER HERE

## Center of Geometry

Find the 'centroid' or center point for the atoms c1, c2, c3, c4. (This should obviously be the center of the molecule, and between atoms c3 and c4).

```python
# YOUR CODE HERE
centroid = # FIXME
print(centroid)
```

**Concept**: Are the coordinates of this molecule exactly at the origin 0,0,0?

YOUR ANSWER HERE

## Scalar Dot Product

Use the scalar dot product to find the angle between c1c2 and c2c3 (i.e., the c1-c2-c3 angle)

As a reminder:

$$
\cos (\theta)=\frac{\mathbf{u} \cdot \mathbf{v}}{|\mathbf{u}||\mathbf{v}|}
$$

```python
# YOUR CODE HERE
mag_c1c2 = np.linalg.norm(c1c2)
mag_c2c3 = np.linalg.norm(c2c3)

theta = np.arccos( # FIXME )
print( round(np.degrees(theta), 3) )
```

**Concept:** Does your answer above match your expectation? What do you think the angle should be based on chemistry?

YOUR ANSWER HERE

### Vector Cross Product

Let's say we want to know the angle between the two benzene rings in biphenyl. That's the *dihedral* or torsion angle around c1-c2-c3-c4.

We know that c1-c2-c3 form a plane (i.e., three points determine a plane) and that the cross product will give us the vector perpendicular to that plane.

So we'll need to take two cross products (and be careful to take them in the right order)

```python
# YOUR CODE HERE
# find the cross product between c1c2 and c2c3 bonds
c1c2c3 =
print(c1c2c3)

# find the cross product between c2c3 and c3c4 bonds
c2c3c4 =
print(c2c3c4)

# the following code should get the torsion angle for you
mag_c1c2c3 = np.linalg.norm(c1c2c3)
mag_c2c3c4 = np.linalg.norm(c2c3c4)
torsion = np.arccos(np.dot(c1c2c3, c2c3c4) / (mag_c1c2c3 * mag_c2c3c4))
print( round(np.degrees(torsion), 3) )
```

**Concept** What happens if you reverse the order of the bonds in the cross product? Will it change your answer for the angle?

Explain:
YOUR ANSWER HERE

### Operators

Unfortunately, what we've learned about operators is a bit abstract still.
(We'll design some operators that tell us things about energies, forces, etc. on Monday.)

If you find these hard, they can be. You can either do this with Sympy, or create a PDF of this notebook and add pages with your work.

5.3 Consider the linear momentum operator
$$
\hat{p} \equiv-i \hbar \frac{d}{d x}
$$
where $\hbar$ is a constant. For the function $\psi(x)=\mathrm{e}^{i k x},$ show that $\hat{p} \psi(x)=\hbar k \psi(x)$

5.4 Consider the kinetic energy operator
$$
\hat{H} \equiv-\frac{h^{2}}{8 \pi^{2} m} \frac{d^{2}}{d x^{2}}
$$
For the function $\psi(x)=\sin \left(\frac{\pi x}{L}\right),$ show that $\hat{H} \psi(x)=\frac{h^{2}}{8 m L^{2}} \psi(x)$

```python
from sympy import init_session
init_session()

i, hbar = symbols('i hbar')
h, m, L = symbols('h m L')
```

```python
# YOUR CODE HERE
# if you have an error, make sure you run the cell above this
psi = exp(i*k*x)
p =

print(p)
```

```python
# YOUR CODE HERE
psi = sin(pi*x/L)
H =

print(H)
```


###### Content under Creative Commons Attribution license CC-BY 4.0, code under BSD 3-Clause License © 2017 L.A. Barba, N.C. Clementi

# Step to the future

Welcome to Lesson 2 of the course module "Tour the dynamics of change and motion," in _Engineering Computations_. The previous lesson, [Catch things in motion](http://go.gwu.edu/engcomp3lesson1), showed you how to compute velocity and acceleration of a moving body whose positions were known.

Time history of position can be captured on a long-exposure photograph (using a strobe light), or on video. But digitizing the positions from images can be a bit tedious, and error-prone. Luckily, we found online a data set from a fancy motion-capture experiment of a falling ball, with high resolution [1]. You computed acceleration and found that it was not only smaller than the theoretical value of $9.8 \rm{m/s}^2$, but it _decreased_ over time. The effect is due to air resistance and is what leads to objects reaching a _terminal velocity_ in freefall.

In general, not only is [motion capture](https://en.wikipedia.org/wiki/Motion_capture) (a.k.a., _mo-cap_) expensive, but it's inappropriate for many physical scenarios. Take a roller-coaster ride, for example: during design of the ride, it's more likely that the engineers will use an _accelerometer_. It really is the acceleration that makes a roller-coaster ride exciting, and they only rarely go faster than highway speeds (say, 60 mph) [2].
How would an engineer analyze data captured with an accelerometer?

## A roller-coaster ride

Prof. Anders Malthe-Sorenssen has a file with accelerometer data for a roller-coaster ride called "The Rocket" (we don't know if it's real or made up!). He has kindly given permission to use his data. So let's load it and have a look. We'll first need our favorite numerical Python libraries, of course.

```python
import numpy
from matplotlib import pyplot
```

```python
%matplotlib inline

pyplot.rc('font', family='serif', size='14')
```

If you don't have the data file in the location we assume below, you can get it by adding a code cell and executing this code in it, then commenting or deleting the `filename` assignment before the call to `numpy.loadtxt()`.

```Python
from urllib.request import urlretrieve
URL = 'http://go.gwu.edu/engcomp3data2?accessType=DOWNLOAD'
urlretrieve(URL, 'therocket.txt')
```

```python
filename = '../data/therocket.txt'
t, a = numpy.loadtxt(filename, usecols=[0,1], unpack=True)
```

We'll take a peek at the data by printing the first five pairs of $(t, a)$ values, then plot the whole set below. Time is given in units of seconds, while acceleration is in $\rm{m/s}^2$.

```python
for i in range(0,5): print(t[i],a[i])
```

    0.0 0.2731644
    0.1 1.4411079
    0.2 2.6693138
    0.3 4.2383806
    0.4 5.6499504

```python
# plot the acceleration over time
fig = pyplot.figure(figsize=(8, 3))

pyplot.plot(t, a, color='#004065', linestyle='-', linewidth=1)
pyplot.title('Acceleration of roller-coster ride data, from [1]. \n')
pyplot.xlabel('Time [s]')
pyplot.ylabel('[m/s$^2$]');
```

### Set things up to compute velocity and position

Our challenge now is to find the motion description—the position $x(t)$—from the acceleration data. In the [previous lesson](http://go.gwu.edu/engcomp3lesson1), we did the opposite: with position data, get the velocity and acceleration, using _numerical derivatives_:

\begin{equation}
v(t_i) = \frac{dx}{dt} \approx \frac{x(t_i+\Delta t)-x(t_i)}{\Delta t}
\end{equation}

\begin{equation}
a(t_i) = \frac{dv}{dt} \approx \frac{v(t_i+\Delta t)-v(t_i)}{\Delta t}
\end{equation}

Since this time we're dealing with horizontal acceleration, we swapped the position variable from $y$ to $x$ in the equation for velocity, above.

The key to our problem is realizing that if we have the initial velocity, we can use the acceleration data to find the velocity after a short interval of time. And if we have the initial position, we can use the known velocity to find the new position after a short interval of time. Let's rearrange the equation for acceleration above, by solving for the velocity at $t_i + \Delta t$:

\begin{equation}
 v(t_i+\Delta t)   \approx  v(t_i) + a(t_i) \Delta t
\end{equation}

We need to know the velocity and acceleration at some initial time, $t_0$, and then we can compute the velocity $v(t_i + \Delta t)$. For the roller-coaster ride, it's natural to assume that the initial velocity is zero, and the initial position is zero with respect to a convenient reference system. We're actually ready to solve this!

Let's save the time increment for our data set in a variable named `dt`, and compute the number of time increments in the data. Then, we'll initialize new arrays of velocity and position to all-zero values, with the intention of updating these to the computed values.

```python
#time increment
dt = t[1]-t[0]
dt
```

    0.10000000000000001

```python
#number of time increments
N = len(t)
N
```

    151

```python
#initialize v and x arrays to zero
v = numpy.zeros(N)
x = numpy.zeros(N)
```

### Step through time

In the code cell below, we use a `for` statement to step through the sequence of acceleration values, each time computing the velocity and position at the subsequent time instant. We are applying the equation for $v(t_i + \Delta t)$ above, and a similar equation for position:

\begin{equation}
 x(t_i+\Delta t)   \approx  x(t_i) + v(t_i) \Delta t
\end{equation}

```python
for i in range(N-1):
    v[i+1] = v[i] + a[i]*dt
    x[i+1] = x[i] + v[i]*dt
```

And there you have it. You have computed the velocity and position over time from the acceleration data. We can now make plots of the computed variables. Note that we use the Matplotlib [`subplot()`](https://matplotlib.org/api/_as_gen/matplotlib.pyplot.subplot.html?highlight=matplotlib%20pyplot%20subplot#matplotlib.pyplot.subplot) function to get the two plots in one figure. The argument to `subplot()` is a set of three digits, corresponding to the number of rows, number of columns, and plot number in a matrix of sub-plots.

```python
# plot velocity and position over time
fig = pyplot.figure(figsize=(10,6))

pyplot.subplot(211)
pyplot.plot(t,  v, color='#0096d6', linestyle='-', linewidth=1)
pyplot.title('Velocity and position of roller-coster ride (data from [1]). \n')
pyplot.ylabel('$v$ [m/s] ')

pyplot.subplot(212)
pyplot.plot(t,  x, color='#008367', linestyle='-', linewidth=1)
pyplot.xlabel('Time [s]')
pyplot.ylabel('$x$ [m]');
```

## Euler's method

The method we used above to compute the velocity and position from acceleration data is known as _Euler's method_. The eminent Swiss mathematician Leonhard Euler presented it in his book _"Institutionum calculi integralis,"_ published around 1770 [3].

You can understand why it works by writing out a Taylor expansion for $x(t)$:

\begin{equation}
x(t+\Delta t) = x(t) + \frac{d x}{dt}\Delta t + \frac{d^2 x}{dt^2}\frac{\Delta t^2}{2} + \frac{d^3 x}{dt^3}\frac{\Delta t^3}{3!}+\cdots
\end{equation}

With $v=dx/dt$, you can see that the first two terms on the right-hand side correspond to what we used in the code above. That means that Euler's method makes an approximation by throwing away the terms $\frac{d^2 x}{dt^2}\frac{\Delta t^2}{2} + \frac{d^3 x}{dt^3}\frac{\Delta t^3}{3!}+\cdots$. So the error made in _one step_ of Euler's method is proportional to $\Delta t^2$. Since we take $N=T/\Delta t$ steps (for a final time instant $T$), we conclude that the error overall is proportional to $\Delta t$.

#### **Euler's method is a first-order method** because the error in the approximation goes with the first power of the time increment $\Delta t$.

## Initial-value problems

To get velocity and position from the acceleration data, we needed to know the _initial values_ of the velocity and position. Then we could apply Euler's method to _step in time_ starting at $t_0$, with time increment $\Delta t$. This setting corresponds to the numerical solution of _initial-value problems_.
(We follow here the presentation in [4], p.86.)

Consider the differential equation corresponding to an object in free fall:

\begin{equation}
\ddot{y}=-g,
\end{equation}

where the dot above a variable represents the time derivative, and $g$ is the acceleration of gravity. Introducing the velocity as intermediary variable, we can write:

\begin{eqnarray}
\dot{y} &=& v \nonumber\\
\dot{v} &=& -g
\end{eqnarray}

The above is a system of two ordinary differential equations, with time as the independent variable. For its numerical solution, we need two initial conditions, and Euler's method:

\begin{eqnarray}
y(t_0) = y_0, \qquad y_{i+1} &=& y_i + \dot{y} \Delta t \nonumber\\
v(t_0) = v_0, \qquad v_{i+1} &=& v_i + \dot{v} \Delta t
\end{eqnarray}

It's so neatly symmetrical that it's just asking for a vectorized equation! Combine the two dependent variables into a vector of unknowns, $\mathbf{y}$:

\begin{equation}
\mathbf{y} = \begin{bmatrix}
y \\ v
\end{bmatrix},
\end{equation}

and write the differential equation in vector form, as follows:

\begin{equation}
\dot{\mathbf{y}} = \begin{bmatrix}
v \\ -g
\end{bmatrix}.
\end{equation}

Equation (9) above represents the _state_ of the system, at any given instant in time. A code design for the numerical solution that generalizes to other changing systems (or _dynamical systems_) is to write one function that computes the right-hand side of the differential equation (the derivatives of the state variables), and another function that takes a state and applies the numerical method for each time increment. The solution is then computed in one `for` statement that calls these functions.

Study the code below: the function `freefall()` computes the right-hands side of the equation, and the function `eulerstep()` takes the state and applies Euler's method to update it one time increment.

```python
def freefall(state):
    '''Computes the right-hand side of the freefall differential
    equation, in SI units.

    Arguments
    ----------
    state : array of two dependent variables [y v]^T

    Returns
    -------
    derivs: array of two derivatives [v -g]^T
    '''

    derivs = numpy.array([state[1], -9.8])
    return derivs
```

```python
def eulerstep(state, rhs, dt):
    '''Uses Euler's method to update a state to the next one.

    Arguments
    ---------
    state: array of two dependent variables [y v]^T
    rhs  : function that computes the right hand side of the
           differential equation.
    dt   : float, time increment.

    Returns
    -------
    next_state: array, updated state after one time increment.
    '''

    next_state = state + rhs(state) * dt
    return next_state
```

## Numerical solution vs. experiment

Here's an idea! Let's use the `freefall()` and `eulerstep()` functions to obtain a numerical solution with the same initial conditions as the falling-ball experiment from [Lesson 1](http://go.gwu.edu/engcomp3lesson1), and compare with the experimental data.

You can grab the data from its location online running the following code in a new cell:
```Python
filename = 'fallingtennisball02.txt'
url = 'http://go.gwu.edu/engcomp3data1'
urlretrieve(url, filename)
```

You already imported `urlretrieve` above. Remember to then comment the assignment of the `filename` variable below. We'll load it from our local copy.

```python
filename = '../data/fallingtennisball02.txt'
t, y = numpy.loadtxt(filename, usecols=[0,1], unpack=True)
```

We'll need to use the same time increment, so let's compute it from two time samples. The initial position is the first value of the `y` array, while the initial velocity is zero. And we'll only look at the section of data before the ball bounces from the ground, which gives us the number of time steps.

```python
#time increment
dt = t[1]-t[0]
```

```python
y0 = y[0] #initial position
v0 = 0    #initial velocity
N = 576   #number of steps
```

Now, let's create a new array, called `num_sol`, to hold the results of the numerical solution. The array has dimensions `Nx2`, with each two-element row holding the state variables, $(y,v)$, at a given time instant. After saving the initial conditions in the solution array, we are ready to start stepping in time in a `for` statement. Study the code below.

```python
#initialize array
num_sol = numpy.zeros([N,2])
```

```python
#Set intial conditions
num_sol[0,0] = y0
num_sol[0,1] = v0
```

```python
for i in range(N-1):
    num_sol[i+1] = eulerstep(num_sol[i], freefall, dt)
```

Did it work? Exciting! Let's plot in the same figure both the numerical solution and the experimental data.

```python
fig = pyplot.figure(figsize=(6,4))
pyplot.plot(t[:N], num_sol[:,0], linewidth=2, linestyle='--', label='Numerical solution')
pyplot.plot(t[:N], y[:N], linewidth=2, alpha=0.8, label='Experimental data')
pyplot.xlabel('Time [s]')
pyplot.ylabel('$y$ [m]')
pyplot.title('Free fall tennis ball (no air resistance) \n')
pyplot.legend();
```

The two lines look very close… but let's plot the difference to get an idea of the error.

```python
fig = pyplot.figure(figsize=(6,4))
pyplot.plot(t[:N], y[:N]-num_sol[:,0])
pyplot.title('Difference between numerical solution and experimental data.\n')
pyplot.xlabel('Time [s]')
pyplot.ylabel('$y$ [m]');
```

## Air resistance

In [Lesson 1](http://go.gwu.edu/engcomp3lesson1) of this module, we computed the acceleration of gravity and got a value less than the theoretical $9.8 \rm{m/s}^2$, even when using high-resolution experimental data. Did you figure out why?

We were missing the effect of air resistance! When an object moves in a fluid, like air, it applies a force on the fluid, and consequently the fluid applies an equal and opposite force on the object (Newton's third law).

This force is the *drag* of the fuid, and it opposes the direction of travel. The drag force depends on the object's geometry, and its velocity: for a sphere, its magnitude is given by:

\begin{equation}
    F_d = \frac{1}{2} \pi R^2 \rho C_d v^2,
\end{equation}

where $R$ is the radius of the sphere, $\rho$ the density of the fluid, $C_d$ the drag coefficient of a sphere, and $v$ is the velocity.

Since we have another force involved, we'll have to rethink the problem formulation. The state variables are still the same (position and velocity):

\begin{equation}
\mathbf{y} = \begin{bmatrix}
y \\ v
\end{bmatrix}.
\end{equation}

But we'll adjust the differential equation to add the effect of air resistance. In vector form, we can write it as follows:

\begin{equation}
\dot{\mathbf{y}} = \begin{bmatrix}
v \\ a_y
\end{bmatrix},
\end{equation}

where $a_y$ now includes the acceleration due to the drag force:

\begin{equation}
 a_y = -g + a_{\text{drag}}
\end{equation}

With $F_{\text{drag}} = m a_{\text{drag}}$:

\begin{equation}
    a_{\text{drag}} = \frac{1}{2m} \pi R^2 \rho C_d v^2
\end{equation}

Finally, we can write our differential equation as:

\begin{equation}
\dot{\mathbf{y}} = \begin{bmatrix}
v \\ -g + a_{\text{drag}}
\end{bmatrix}.
\end{equation}

Let's write a new function for this modified right-hand side of a falling tennis ball with air resistance.

##### Note:

According to the International Tennis Federation, [ITF](http://www.itftennis.com/home.aspx), the diameter of a tennis ball has to be in the range of $6.54$–$6.86 \rm{cm}$, and its mass in the range of $56.0$–$59.4 \rm{g}$. We chose a value in the middle of the range for each quantity.

```python
def fall_drag(state):
    '''Computes the right-hand side of the differential equation
    for the fall of a ball, with drag, in SI units.

    Arguments
    ----------
    state : array of two dependent variables [y v]^T

    Returns
    -------
    derivs: array of two derivatives [v (-g+a_drag)]^T
    '''
    R = 0.0661/2 # radius in meters
    m = 0.0577   # mass in kilograms
    rho = 1.22   # air density kg/m^3
    C_d = 0.47   # drag coefficient for a sphere
    pi = numpy.pi

    a_drag = 1/(2*m) * pi * R**2 * rho * C_d * (state[1])**2

    derivs = numpy.array([state[1], -9.8 + a_drag])
    return derivs
```

Assume the same initial conditions as before:

```python
y0 = y[0] # initial position
v0 = 0    # initial velocity
N = 576   # number of steps
```

```python
# initialize array
num_sol_drag = numpy.zeros([N,2])
```

```python
# Set intial conditions
num_sol_drag[0,0] = y0
num_sol_drag[0,1] = v0
```

```python
for i in range(N-1):
    num_sol_drag[i+1] = eulerstep(num_sol_drag[i], fall_drag, dt)
```

Time to plot and see how it looks! Would you expect the results to be better than in the previous case? Let's plot the three cases and check the differences.

```python
fig = pyplot.figure(figsize=(6,4))
pyplot.plot(t[:N], num_sol[:,0], linewidth=2, linestyle='--', label='Num-solution no drag')
pyplot.plot(t[:N], y[:N], linewidth=2, alpha=0.6, label='Experimental data')
pyplot.plot(t[:N], num_sol_drag[:,0], linewidth=2, linestyle='--', label='Num-solution drag')

pyplot.title('Free fall tennis ball \n')

pyplot.xlabel('Time [s]')
pyplot.ylabel('$y$ [m]')
pyplot.legend();
```

All the lines look very close… but let's plot the differences with the experimental data in both cases, to get an idea of the error.

```python
fig = pyplot.figure(figsize=(6,4))
pyplot.plot(t[:N], y[:N]-num_sol[:,0], label='No drag')
pyplot.plot(t[:N], y[:N]-num_sol_drag[:,0], label='With drag')
pyplot.title('Difference between numerical solution and experimental data.\n')
pyplot.xlabel('Time [s]')
pyplot.ylabel('$y$ [m]')
pyplot.legend();
```

##### Discuss with your neighbor

* What do you see in the plot of the difference between the numerical solution and the experimental data?

## What we've learned

* Integrating an equation of motion numerically.
* Drawing multiple plots in one figure,
* Solving initial-value problems numerically
* Using Euler's method.
* Euler's method is a first-order method.
* Freefall with air resistance is a more realistic model.

## References

1. _Elementary Mechanics Using Python_ (2015), Anders Malthe-Sorenssen, Undergraduate Lecture Notes in Physics, Springer. Data at http://folk.uio.no/malthe/mechbook/

2. _The Physics Hyptertextbook_ (n/a), Glenn Elert, [Acceleration](https://physics.info/acceleration/)

3. Euler method. (2017, October 13). In Wikipedia, The Free Encyclopedia. Retrieved 01:21, November 10, 2017, from https://en.wikipedia.org/w/index.php?title=Euler_method&oldid=805120184

4. _Computational Physics with Python_, lecture notes by Eric Ayars, California State University, Chico. Available online on the author's website: https://physics.csuchico.edu/ayars/312/handouts/comp-phys-python.pdf

```python
# Execute this cell to load the notebook's style sheet, then ignore it
from IPython.core.display import HTML
css_file = '../style/custom.css'
HTML(open(css_file, "r").read())
```

<link href="https://fonts.googleapis.com/css?family=Merriweather:300,300i,400,400i,700,700i,900,900i" rel='stylesheet' >
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,300i,400,400i,700,700i" rel='stylesheet' >
<link href='http://fonts.googleapis.com/css?family=Source+Code+Pro:300,400' rel='stylesheet' >
<style>

@font-face {
    font-family: "Computer Modern";
    src: url('http://mirrors.ctan.org/fonts/cm-unicode/fonts/otf/cmunss.otf');
}

#notebook_panel { /* main background */
    background: rgb(245,245,245);
}

div.cell { /* set cell width */
    width: 800px;
}

div #notebook { /* centre the content */
    background: #fff; /* white background for content */
    width: 1000px;
    margin: auto;
    padding-left: 0em;
}

#notebook li { /* More space between bullet points */
margin-top:0.5em;
}

/* draw border around running cells */
div.cell.border-box-sizing.code_cell.running {
    border: 1px solid #111;
}

/* Put a solid color box around each cell and its output, visually linking them*/
div.cell.code_cell {
    background-color: rgb(256,256,256);
    border-radius: 0px;
    padding: 0.5em;
    margin-left:1em;
    margin-top: 1em;
}

div.text_cell_render{
    font-family: 'Source Sans Pro', sans-serif;
    line-height: 140%;
    font-size: 110%;
    width:680px;
    margin-left:auto;
    margin-right:auto;
}

/* Formatting for header cells */
.text_cell_render h1 {
    font-family: 'Merriweather', serif;
    font-style:regular;
    font-weight: bold;
    font-size: 250%;
    line-height: 100%;
    color: #004065;
    margin-bottom: 1em;
    margin-top: 0.5em;
    display: block;
}
.text_cell_render h2 {
    font-family: 'Merriweather', serif;
    font-weight: bold;
    font-size: 180%;
    line-height: 100%;
    color: #0096d6;
    margin-bottom: 0.5em;
    margin-top: 0.5em;
    display: block;
}

.text_cell_render h3 {
    font-family: 'Merriweather', serif;
	font-size: 150%;
    margin-top:12px;
    margin-bottom: 3px;
    font-style: regular;
    color: #008367;
}

.text_cell_render h4 {    /*Use this for captions*/
    font-family: 'Merriweather', serif;
    font-weight: 300;
    font-size: 100%;
    line-height: 120%;
    text-align: left;
    width:500px;
    margin-top: 1em;
    margin-bottom: 2em;
    margin-left: 80pt;
    font-style: regular;
}

.text_cell_render h5 {  /*Use this for small titles*/
    font-family: 'Source Sans Pro', sans-serif;
    font-weight: regular;
    font-size: 130%;
    color: #e31937;
    font-style: italic;
    margin-bottom: .5em;
    margin-top: 1em;
    display: block;
}

.text_cell_render h6 { /*use this for copyright note*/
    font-family: 'Source Code Pro', sans-serif;
    font-weight: 300;
    font-size: 9pt;
    line-height: 100%;
    color: grey;
    margin-bottom: 1px;
    margin-top: 1px;
}

    .CodeMirror{
            font-family: "Source Code Pro";
			font-size: 90%;
    }
/*    .prompt{
        display: None;
    }*/

    .warning{
        color: rgb( 240, 20, 20 )
        }
</style>


Back in October, Greg Wilson [blogged about testing scientific software](http://software-carpentry.org/blog/2014/10/why-we-dont-teach-testing.html). He specifically points to [a lesson introducing the Euler method](http://nbviewer.ipython.org/github/numerical-mooc/numerical-mooc/blob/master/lessons/01_phugoid/01_03_PhugoidFullModel.ipynb) as part of the [Practical Numerical Methods with Python MOOC](http://openedx.seas.gwu.edu/courses/GW/MAE6286/2014_fall/about), and asks why a certain number (the measured convergence rate) is *close enough* to the expected value.

The big question of testing in scientific software, how this should be done, and how it is being done, has led to the [*Close Enough for Scientific Work*](https://github.com/swcarpentry/close-enough-for-scientific-work) project, as launched and explained by Greg Wilson in [this blog post](http://software-carpentry.org/blog/2014/11/close-enough-for-scientific-work.html). If you're reading this, you should be following that project! As I've helped out on the MOOC, which is led by [Lorena Barba](http://lorenabarba.com/), I've been (over)thinking what sort of answer we could, or should, give. Well aware that we're heading for the [academic version of the balloon joke](https://www2.bc.edu/~radinr/Management_Humor/jokes.htm) (an answer that is correct, of little use, and took a long time to arrive), I've outlined some answers in the posts linked below.

## Background

$$
\newcommand{\dt}{\Delta t}
\newcommand{\udt}[1]{u^{({#1})}(T)}
$$

We have a system of differential equations to solve: the solution will be $u(t)$. The numerical solution we get will depend on one parameter, the timestep $\dt$. The resulting numerical approximation, at the point $t=T$, will be $\udt{\dt}$: the exact solution is $u(T)$.

The method used is Euler's method, also [introduced in an earlier lesson in the MOOC](http://nbviewer.ipython.org/github/numerical-mooc/numerical-mooc/blob/master/lessons/01_phugoid/01_02_Phugoid_Oscillation.ipynb), and [explained in detail by Lorena in this screencast](https://www.youtube.com/watch?v=6i6qhqDCViA). The key result derived there is that Euler's method is *first order*, so that

$$
\begin{equation}
  u(T) - \udt{\dt} = c_1 \dt + c_2 \dt^2 + {\cal O}(\dt^3) \simeq c_1 \dt.
\end{equation}
$$

For sufficiently small $\dt$ the error is proportional to $\dt$.

As we can't (usually) know the exact solution $u(T)$ ahead of time, a standard check is *grid convergence*: compute three solutions with different timesteps (eg, $\dt, 2\dt, 4\dt$) and compare them. The comparison to use is

$$
\begin{equation}
  s_m = \log_2 \left( \frac{\udt{4\dt} - \udt{2\dt}}{\udt{2\dt} - \udt{\dt}} \right) \simeq 1.
\end{equation}
$$

The symbol $s_m$ is used as it's the *(measured) slope* of the best fit line through the errors, as shown on the [original lesson](http://nbviewer.ipython.org/github/numerical-mooc/numerical-mooc/blob/master/lessons/01_phugoid/01_03_PhugoidFullModel.ipynb):

####Figure 1. Convergence test plot taken from the original lesson.

The [original lesson](http://nbviewer.ipython.org/github/numerical-mooc/numerical-mooc/blob/master/lessons/01_phugoid/01_03_PhugoidFullModel.ipynb) does precisely this comparison (using the phugoid model, with specific parameter values, and $\dt = 0.001$), finding a measured slope $s_m = 1.014$. Is this *close enough* to 1?

## TL;DR: Answer

Answer**s**

1. [*I don't care about the algorithm: I just want the answer to be right*](http://ianhawke.github.io/blog/close-enough-part-1.html): $0.585 \lesssim s_m \lesssim 1.585$ is close enough for Euler's method.
2. [*I don't care about the answer: I just want the algorithm behaviour to be right*](http://ianhawke.github.io/blog/close-enough-part-2.html): $s_m=1.014$ with $\dt=0.001$ is close enough if $1 \le s_m \le 1.0093$ with $\dt=0.0005$.
3. [*I don't care about the behaviour: I just want to know I've implemented Euler's method*](http://ianhawke.github.io/blog/close-enough-part-3.html): measure the local truncation error instead, and check *both* the convergence rate *and* the leading order constant. $s_m$ by itself is useless.

## And yet more

The accuracy of the convergence rate was just one small point in [Greg's original post](http://software-carpentry.org/blog/2014/10/why-we-dont-teach-testing.html). The more substantial question was on testing a "real" code, such as [this n-body simulation code](http://benchmarksgame.alioth.debian.org/u32/program.php?test=nbody&lang=python3&id=1). To illustrate how far you can go with the approaches described above, [here's my attempt at testing that code](http://ianhawke.github.io/blog/close-enough-part-4.html).

###### Acknowledgements

Aside from the obvious debts to Lorena Barba and Greg Wilson, both for starting this off and for later comments, I've also benefitted from comments from and discussions with Hans Fangohr and Ashwin Srinath.


## About Python

- Python is a general purpose programming language
- Python is free and open source
- Python is supported by a vast collection of standart and external software libraries
- Python is one of the most popular programming languages
- Python stepped in almost any field such as:
    - Game programming
    - CGI and GUI programming
    - Web development
    - Scientific programming
    - Data Science
    - Machine Learning
    - Communications
- Python is used extensively by Internet service and high tech companies:
    - Google
    - Dropbox
    - Reddit
    - Youtube
    - Walt Disney Animation
- Python is __high level language__ suitable for rapid development
- Supported with many libraries community built
- Python is __multiparadigm language__ (procedural, object-oriented, functional programming)
- Python is __interpreted language__ rather than compiled

#### Scientific Python

- Python has become one of the core languages of scientific computing.

__Numerical Programming:__ Fundamental matrix and array processing capabilities are provided by the excellent ```NumPy``` library. NumPy provides the basic array data type plus some simple processing operations

```python
import numpy as np # load the library
a = np.linspace(-np.pi, np.pi, 100) # Create array
b = np.cos(a) # Apply cosine to each element of a
c = np.ones(25) # An array of 25 ones
np.dot(c,c) # Compute inner product
```

    25.0

The SciPy library is built on top of NumPy and provides additional functionality.

```python
from scipy.stats import norm
from scipy.integrate import quad
phi = norm()
value, error = quad(phi.pdf, -2,2) # Integrate using Gaussian quadrature
value
```

    0.9544997361036417

SciPy includes many of the standard routines used in:

- linear algebra
- integration
- interpolation
- optimization
- distributios and random number generation
- signal processing

The most popular and comprehensive Python library for creating figures and graphs is ```Matplotlib```.

- Plots, histograms, contour images, 3D, bar charts, etc., etc.
- Output in many formats (PDF, PNG, EPS, etc.)
- LaTeX integration

To see more plots you can see [here](http://matplotlib.org/gallery.html)

Other graphics libraries include:

- Plotly
- Bokeh
- VPython -- 3D graphics and animations

Symbolic Algebra, it's useful to be able to manipulate symbolic expressions, as in Mathematica or Maple. The SymPy library provides this functionality from within the Python shell

```python
from sympy import Symbol
x, y = Symbol('x'), Symbol('y') # Treat 'x' and 'y' as algebraic symbols
print(x + x + x + y)
```

    3*x + y

```python
expression = (x+y)**2
expression.expand()
```

    x**2 + 2*x*y + y**2

```python
from sympy import solve
# Solving polynomials
solve(x**2 + x + 2)
```

    [-1/2 - sqrt(7)*I/2, -1/2 + sqrt(7)*I/2]

```python
from sympy import limit, sin, diff
limit(1 / x, x, 0)
```

    oo

```python
limit(sin(x)/x , x, 0)
```

    1

```python
diff(sin(x),x)
```

    cos(x)

Python’s data manipulation and statistics libraries have improved rapidly over the last few years. ```Pandas``` one of the most popular libraries for working with data. Pandas is fast, efficient, and well designed.

```python
import pandas as pd
import scipy as sp
data = sp.randn(5, 2) # Create 5x2 matrix of random numbers
dates = pd.date_range('28/12/2010', periods=5)
df = pd.DataFrame(data, columns=('price', 'weight'), index=dates)
print(df)
```

                   price    weight
    2010-12-28  1.018554 -0.696254
    2010-12-29  0.122694 -0.907565
    2010-12-30 -0.446334  1.657745
    2010-12-31 -0.305365 -0.597863
    2011-01-01  0.895197  0.195587

```python
df.mean()
```

    price     0.256949
    weight   -0.069670
    dtype: float64

__Other Useful Statistics Libraries__:

- ```statsmodel``` - various statistical routines
- ```scikit-learn``` - machine learning in Python
- ```pyMC``` - for bayesian data analysis
- ```pystan``` - Bayesian analysis based on stan

Python has many libraries for studying graphs, One well-known example is ```NetworkX```

Here’s some example code that generates and plots a random graph, with node color determined by shortest path length from a central node

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

G = nx.random_geometric_graph(200,0.12) # Generate random graph
pos = nx.get_node_attributes(G, 'pos') # Get positions of nodes
# Find node nearest the center point (0.5, 0.5)
dists = [(x - 0.5)**2 + (y - 0.5)**2 for x,y in list(pos.values())]
ncenter = np.argmin(dists)
```

```python
# Plot graph, coloring by path length from central node
p = nx.single_source_shortest_path_length(G, ncenter)
plt.figure()
nx.draw_networkx_edges(G, pos, alpha=0.4)
nx.draw_networkx_nodes(G, pos, nodelist=list(p.keys()), node_size=120, alpha=0.5,
                          node_color=list(p.values()), cmap=plt.cm.jet_r)

plt.show()
```

Running your Python code on massive servers in the cloud is becoming easier and easier.

A nice example is [Wakari]()

As you can see in this small preview of what is possible with Python, Python is quite capable of doing almost anaything nowadays. In the journey towards becoming data science experts we will be using Python, hence we will be masters of Python Language and it's scientific stack.

In the next notebook we will go ahead and talk about how to setup our Python environment.

```python

```


# Variational Principle  using Symbolic Mathematics in Python

## 1. Introduction

The variational principle tells us that we can use a trial wavefunction to solve the Schrodinger equation using the following theorem:

$${{\int {{\Psi ^*}\hat H{\rm{ }}\Psi } d\tau } \over {\int {{\Psi ^*}\Psi } d\tau }} \ge {E_0}$$

We will use Sympy to solve the particle in a box problem by guessing a trial wavefunction using variational principle

```python
import sympy as sym
```

This exercise is a bit more self-guided than the other notebooks we have done. One of the most useful things you can do is **open last week's notebook to remember the commands in sympy**. Also, remember that google is your friend:

1. [Sympy tutorial](https://docs.sympy.org/latest/tutorial/index.html)
2. [Stack Overflow](https://stackoverflow.com/search?q=sympy+)
3. [Stack Exchange](https://stackexchange.com/)

## 2. Particle in a box

The wave function that we pick for a particle in a box needs to have the following properties

1. single valued
1. normalizable
1. function and its first derivative are continuous
1. boundary condition that the wave function goes to zero at the ends of the box

Particle in a box: a is a classical particle, red is real part, blue is imaginary part.

This particle only expericnes kinetic energy between the box, so the Hamiltonian for this system is

$$\hat H = {{ - {\hbar ^2}} \over {2m}}{{{d^2}} \over {d{x^2}}} + \left\{ {\matrix{{V(x) = 0} & {0 < x < a}  \cr {V(x) = \infty } & {x < 0\text{ }{\rm{ or}}\;x > a}  \cr } } \right.$$

For our purposes, that means we can consider the Hamiltonian to be

$$\hat H = {{ - {\hbar ^2}} \over {2m}}{{{d^2}} \over {d{x^2}}}$$

as long as we keep the limits of integration to be $(0,a)$

### 2.1 Trial Wave function

Although the particle in box has a well known solution

[https://en.wikipedia.org/wiki/Particle_in_a_box](https://en.wikipedia.org/wiki/Particle_in_a_box)

(or check your favorite pchem book)

We are going to guess a trial wave function:

$$\Phi (x) = \left( {{x \over a} - {{{x^3}} \over a}} \right) + \alpha \left( {{{{x^5}} \over {{a^5}}} - {1 \over 2}\left( {{{{x^7}} \over {{a^7}}} + {{{x^7}} \over {{a^7}}}} \right)} \right)$$

### 2.2 Exercise: Variational Theorem

We are going to follow the following plan:

1. Solve for the energy of the trial wave function above

$${E_{trial}} = {{\int\limits_0^a {\Phi (x){{ - {\hbar ^2}} \over {2m}}{{{d^2}} \over {d{x^2}}}\Phi (x)dx} } \over {\int\limits_0^a {\Phi {{(x)}^2}dx} }}$$

Your answer will be a function of $ m,a,\text{and } \alpha$ We will use $\alpha$ as the parameter we vary to minimize the energy and make a new trial wave function.

2. Minimize the trial energy
We will use a first derivative of the trial energy $${d \over {d\alpha }}{E_{trial}}(\alpha )$$ to find the value of $\alpha$ that gives you the lowest energy

3. Plot your new wavefunction compared to the ground state particle in a box: $${\psi _{true}}(x) = {\left( {{2 \over a}} \right)^{1/2}}\sin {{n\pi x} \over a}$$ Plot as a function of $x/a$ from $0$ to $1$. Assuming this has $m=m_e$, and $a=a_0$ use atomic (theorist) units to plot the function.

4. Compare your trial energy to the actual energy (using atomic units)
$${E_{true}}(n = 1) = {{{\hbar ^2}{\pi ^2}} \over {2m{a^2}}}$$

```python
# Your code here
```

Your descriptions/explanations here

### 2.3 Exercise: New trial wavefunction

Determine the minimum energy of the particle in a box using a new trial wavefunction $$x^\alpha(x-a)^\alpha$$

1. Find the minimum energy, $E_{trial}$
2. Plot the new trial wavefunction and compare it to the true solution and the wavefunction you found above
3. Compare you new energy to the trial energy you found above
4. Which wavefunction is better? How do you know?

```python
# Your code here
```

Your descriptions/explanations here

### 2.4 Exercise: Design your own wavefunction!

**Now you get to make your own wavefunction!**

The only guidance I would give you is that it make sense mathematically and that it include $\alpha$ so that you can minimize the energy.

Remember that $a$ and $x$ are both length units, and that trigonometric, logarithmic, and exponential functions are all unitless

Using your new wavefunction:

1. Find the minimum energy, $E_{trial}$
2. Plot the new trial wavefunction and compare it to the true solution and the wavefunction you found above
3. Compare you new energy to the trial energy you found above
4. Which wavefunction is better? How do you know?

```python
# Your code here
```

Your descriptions/explanations here

# Reading Homework

Read the following sections in Kramer

- 4.2.3 Born-Oppenheimer approximation
- 4.3.2 Secular equation
- All of 4.5

For each subsection
- write down the subchapter name
- what was the most important idea
- draw an idea digram of the main idea

**Make sure to upload this to the assignment repository**

Example idea diagram:

```python

```


# Analysing Hillslope Morphology at the Landscape Scale

Hillslope morphology reflects the competition between surface uplift and river erosion, which sets the baselevel conditions for hillslopes. In lab 2 you ran a simple numerical model in order to explore how changes in uplift and erosion influence hillslope development. In this lab, you will look at some hillslope morphology data from a real landscape in order to try and interpret the distribution of erosion in order to infer rates of tectonic processes. The data has already been generated for you by running software developed by Hurst et al. (2012) and Grieve et al (2016) to extract information about hillslope morphology from digitial elevation models (DEMs). This software is slow and was run on a very large dataset so the results have been provided in the data download from Moodle. The aims of this lab are:
* Follow and execute python code to analyse hillslope morphology extracted from a DEM.
* Further understand the concept of steady-state and transience in hillslope morphology
* Quantify metrics of dimensionless hillslope morphology
* Analyse hillslope morphology at a landscape scale to evaluate the distribution of erosion/uplift.
* Create and explain figures that show variation in hillslope form in a tectonically active landscape

## Bolinas
The landscape you will work on is the Bolinas Ridgeline, which runs adjacent to the San Andreas Fault in California, just north of San Francisco, over the Golden Gate Bridge. You will analyse hillslope morphology in 40 catchments along the fault that drain towards the fault. These catchments are of similar size, but relatively small, no more than  ~4 km in length. The elevation of the landscape generally increases from NW to SE.

The bedrock beneath these catchments is uniform, consisting of poorly-consolidated metasedimentary rocks (sandstones) of the Franciscan Fm. The key point here is that there should be little variation in channel or hillslope form due to differences in bedrock type. The hillslopes here are all soil-mantled, and thus you can assume transport-limited conditions and that the nonlinear sediment flux law is appropriate to govern downslope soil flux on the hillslopes.

Previous work by Kirby et al (2007) has examined the morphology of the river profiles and found that rivers get progressively steeper, with increasing $M_{\chi}$ from NW to SE. The parameter $M_{\chi}$ is an estimate of channel slope normalised to drainage area, and is considered a good proxy for erosion rate.

Kirby et al (2007) suggested that channel erosion rates increased from NW to SE and thus inferred that there was an increase in tectonic uplift rate along this landform. This is important because the high uplift rates make this site a potential nucleus for future earthquakes and San Francisco has a prominent history of being hit by large earthquakes. Millions of people live in the Bay Area, which also has one of the USA's biggest ports, and there is a great deal of industry near by from hi-tech manufacturing to financial services. We will explore whether hillslope morphology reflects the patterns of erosion inferred from the channel network.

## Python
The programming language we are using in this lab is called Python.  No prior knowledge of programming is required for this lab. Learning how to be a programmer is not the aim! However, this sort of scientific computing is becoming more common place in research and consultancy, so it won't do you any harm to see it in action. Python is multifunctional, for example it can interface with ArcGIS (the software used in the previous lab) to automate workflows. In the last lab we saw how it can be used to construct numerical models and visualise results. In this lab we'll use it to visualise the results of analyses performed on topographic data.

**To run a code block, click in a cell, hold down shift, and press enter.** An asterisk in square brackets `In [*]:` will appear while the code is being executed, and this will change to a number `In [1]:` when the code is finished. *The order in which you execute the code blocks matters, they must be run in sequence.*

Inside blocks of python code there are comments indicated by lines that start with `#`. These lines are not computer code but rather comments providing information about what the code is doing to help you follow along. Before we get started we need to tell python which tools we want to use (these are called modules):

```python
# import modules for numerical calculations, for plotting, and for data manipulation
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cm as cm
import pandas as pd

# import operating system functions
import os

# tell python to allow plotting to occur within the page
%matplotlib inline

# Customise figure style, changing the font to arial and the size to 16pt
from matplotlib import rc
rc('font',**{'family':'sans-serif','sans-serif':['Arial']})
rc('font',size=12)
```

Before we get started we need to define the workspace we are working in so that python knows where to go to look for files and data.

```python
# Get the current directory
# DataDirectory = os.getcwd()+"\\data\\" # use this line for windows
DataDirectory = os.getcwd()+"/data/" # use this line if on a mac or on binder

# Set the project name
ProjectName = "Bolinas"
```

## Load the hillslope data
The file named **Bolinas_HilltopData.csv** contains the results of all the hillslope traces collected along the Bolinas ridge. Each trace starts at a hilltop and follows the steepest path down to an adjacent channel. Open up this file in excel and take a look at the data table. Information about the location of the hilltop ($X$,$Y$,$i$,$j$) are included, as are the location where the traces stopped in the channel network ($a$,$b$,*StreamID*). For our purposes, the hilltop curvature $C_{HT}$, hillslope length $L_H$, hillslope relief $R$ and average hillslope gradient $S$ are all recorded, and this is the data we wil be using mostly. The code below reads this data into Python so that we can explore it, and then checks the data for errors.

```python
# load the hillslopes data
Filename = ProjectName+'_HilltopData.csv'
HillslopeData = pd.read_csv(DataDirectory+Filename)

# drop any rows with no data (hillslope traces to outside the study domain)
# or with value of -9999 for Basin ID
HillslopeData = HillslopeData.dropna()
HillslopeData = HillslopeData[HillslopeData.BasinID != -9999]
```

## Determine *SC*
The critical slope gradient *SC* is an important parameter to constrain. This can be done based on topographic data following the work of Grieve et al. (2016). Recall from equation X that *SC* is the gradient toward which hillslope sediment transport accelerates rapidly toward infinity. Therefore it should not be possible for a hillslope to be steeper than *SC*.

We can determine *SC* by looking at the percentage of hillslopes that are steeper than a range of *SC* values. We will arbritarily assume that *SC* is the value at which >99.5% of hillslopes in the entire Bolinas landscape have a lower slope.

```python
# set up range of S_c values to test from 0.5 to 1.
Sc_test = np.arange(0.5,1.,0.01)

# set up empty array for results
Percent_Less_Than = np.zeros(len(Sc_test))

NoHillslopes = len(HillslopeData)

#loop across the
for i, Sc in enumerate(Sc_test):

    # get max hillslope relief
    R_test = HillslopeData.Lh*Sc

    #compare to actual hillslope relief and count how many fall below max line
    #first for segments
    BelowMask = HillslopeData.R < R_test
    NumberBelow = np.sum(BelowMask)
    Percent_Less_Than[i] = 100.*float(NumberBelow)/float(NoHillslopes)

# find the location of the 99.5th percentile
ind = np.argmin(np.abs(99-Percent_Less_Than))

# SET S_C BASED ON THE RESULTS
Sc = Sc_test[ind]

# plot the resulting curve
plt.plot(Sc_test,Percent_Less_Than,'k-',color=[0.5,0.5,0.5])

# plot the percentile
plt.plot([0.5,Sc_test[ind],Sc_test[ind]],[99,99,0.],'r--',lw=0.5)

#plot the value of Sc
plt.text(Sc_test[ind],85,"$S_C = $"+str(Sc_test[ind]),rotation=-90)

#label the axes
plt.xlabel("$S_C$")
plt.ylabel("Percent with lower Relief")
plt.ylim(70,100)
plt.xlim(0.5,1.0)
plt.tight_layout()
plt.savefig(ProjectName+"Sc_Percent.pdf")
```

## Hillslope morphology within an individual catchment
During the first part of this lab, you will explore hillslope morphology within an individual catchment along the Bolinas ridge. There are 40 catchments to choose from so you can explore the catchments to decide which you think might be the most interesting. The catchments are numbered from NW to SE.

### Hillslope Traces

The data you will be working with consists of individual hillslope traces from every hilltop down the path of steepest descent to a neigbouring channel pixel. Only the hilltops falling within or on the boundary of the 41 catchments pictured above have been extracted. There are over 150,000 individual hillslope traces, so this is alot of data to analyse and will require us to group the data spatially and calculate summary statistics. The image below shows you some examples of individual hillslope traces across a DEM. The hilltops are marked in black and the channels in blue, the hillslope traces are depicted with red lines.

### Plot Histograms of Hillslope Morphology

You will start by looking at the distribution of hillslope morphology within your catchments by plotting histograms of the important hillslope metrics: hillslope length, slope, relief and hilltop curvature:

```python
# SET THE BASIN NUMBER YOU WISH TO WORK WITH HERE
Basin = 24

# get the hillslope data only for the basin you are working on
Basins = np.sort(HillslopeData.BasinID.unique())
BasinHillslopeData = HillslopeData[HillslopeData.BasinID == Basins[Basin]]

# setup the figure
# you can modify the figure size here if you wish
fig_width_inches = 6.6
fig_height_inches = 6.6
plt.figure(figsize=(fig_width_inches,fig_height_inches))

# this figure is going to have four parts, called subplots
# we create a subplot by providing a 3 digit number,
# the first two digits refer to the dimensions of the plot (2x2 subplots)
# the third number refers to which of these we want to populate

# HILLSLOPE LENGTH
plt.subplot(221)
# generate the frequency data for a histogram for hillslope length
freq, bin_edges = np.histogram(BasinHillslopeData.Lh,bins=np.arange(0,400,20.))
# normalise the frequency data to the largest value in the dataset
freq_norm = freq.astype(np.float)/float(np.max(freq))
# plot the results as a bar chart
plt.bar(bin_edges[:-1],freq_norm,width=20,align='edge',edgecolor='k',linewidth=0.5,color='steelblue')
plt.xlabel("Hillslope Length (m)")
plt.ylabel("Normalised Frequency")
plt.xlim(0,400)

# HILLSLOPE GRADIENT
plt.subplot(222)
freq, bin_edges = np.histogram(BasinHillslopeData.S,bins=np.arange(0,0.8,0.04))
freq_norm = freq.astype(np.float)/float(np.max(freq))
plt.bar(bin_edges[:-1],freq_norm,width=0.04,align='edge',edgecolor='k',linewidth=0.5,color='thistle')
plt.xlabel("Mean Slope (m/m)")
plt.ylabel("Normalised Frequency")
plt.xlim(0,0.8)

# HILLTOP CURVATURE
plt.subplot(223)
freq, bin_edges = np.histogram(BasinHillslopeData.R,bins=np.arange(0,200.,10.))
freq_norm = freq.astype(np.float)/float(np.max(freq))
plt.bar(bin_edges[:-1],freq_norm,width=10.,align='edge',edgecolor='k',linewidth=0.5,color='sandybrown')
plt.xlabel("Hillslope Relief (m)")
plt.ylabel("Normalised Frequency")
plt.xlim(0,200)

# HILLSLOPE RELIEF
plt.subplot(224)
freq, bin_edges = np.histogram(BasinHillslopeData.Cht,bins=np.arange(-0.1,0,0.005))
freq_norm = freq.astype(np.float)/float(np.max(freq))
plt.bar(bin_edges[:-1],freq_norm,width=0.005,align='edge',edgecolor='k',linewidth=0.5,color='salmon')
plt.xlabel("Hilltop Curvature (m$^{-1}$)")
plt.ylabel("Normalised Frequency")
plt.xlim(0,-0.1)

# FINALISE THE FIGURE AND SAVE
# add a figure title
plt.suptitle("Basin "+str(Basin)+" Hillslopes")
# check the layout, leave some white space around the edges
plt.tight_layout(rect=[0, 0.03, 1, 0.95])
# save the figure
plt.savefig(ProjectName + "_Basin" + "%02d" % Basin + "_HillslopeHists.png", dpi=300)
```

### Calculate the dimensionless hillslope morphology within the basin

Recall from the lecture that hillslopes with different lengths will have different relief. Therefore it is useful to calculate the dimensionless properties of hillslopes so that we can compare between different landscapes.

The nonlinear sediment transport equation predicts a relationship between dimensionless erosion rate and dimensionless relief **for steady state hillslopes** of the form:
$$
\begin{equation}
R^* = {{1}\over{E^*}}\left(\sqrt{1+(E^*)^2} - \ln \left[ {{1}\over{2}} \left( 1+\sqrt{1+(E^*)^2}\right) \right] -1 \right)
\end{equation}
$$

```python
# Calculate Theoretical Steady-State E* vs R*

# First declare a range of E* values from 10^-1 (0.1) to 10^3 (1000)
EStarSS = np.logspace(-1,3,1000)

# Calculate RStar as a function of EStar for steady-state hillslopes from equation X
RStarSS = (1./EStarSS)*(np.sqrt(1.+(EStarSS**2.)) - np.log(0.5*(1.+np.sqrt(1+EStarSS**2.))) - 1.)
```

When $E^*$ < 1, the model predictions are similar to those of a linear sediment flux law, but for $E^*$ > 1 hillslope relief is limited and hillslope gradients approach the critical value $S_C$ and the hillslopes become steep and planar. Conveniently, the relationship between $E^*$ and $R^*$ can also be cast in terms of properties of hillslopes that are measureable from topography. This means that by measuring these properties we can test whether steady-state hillslopes conform with this flux law, or if we are confident the flux law is appropriate we can test whether hillslopes are at steady state:

Now we will calculate $E^*$ and $R^*$ based on the observed topography from our hillslope traces.

**This next cell of code has been intentionally left blank so that you can complete it yourself.**

You can access the data for hillslope length with `BasinHillslopeData.Lh`, hilltop curvature with `BasinHillslopeData.Cht` and hillslope gradient with `BasinHillslopeData.S`. The equations for these are:

$$
\begin{equation}
E^* = {{- 2 \:C_{HT} \:L_H}\over{S_C}} %\tag{}
\end{equation}
$$

$$
\begin{equation}
R^* = {{S}\over{S_C}} %\tag{}
\end{equation}
$$

**Complete the cells below to calculate $E^*$ and $R^*$ for your basin**
You may need to look back at the histogram code to figure out how to access the specific data you need within the BasinHillslopeData object.

```python
#### YOU NEED TO COMPLETE THIS CELL

# Calculate E* vs R* for our hillslope data
EStar =
RStar =
```

Now we will plot the results so we can compare the observed hillslope morphology to the steady-state predictions:

```python
# setup the figure
# you can modify the figure size here if you wish
fig_width_inches = 6.6
fig_height_inches = 5
fig = plt.figure(figsize=(fig_width_inches,fig_height_inches))
ax = plt.subplot()

# Set up the plot to have a lograthmic scale on both axes
plt.loglog()

# Plot analytical steady-state relationship as a dashed line
ax.plot(EStarSS,RStarSS,'k--',zorder=10)

# Plot the observed hillslope data as a grey scatter plot
ax.plot(EStar,RStar,'.',color=[0.5,0.5,0.5])

# Finalise the figure
plt.xlabel('$E^*={{-2\:C_{HT}\:L_H}/{S_C}}$')
plt.ylabel('$R^*=S/S_C$')
plt.xlim(0.1,1000.)
plt.ylim(0.01,1.)
plt.suptitle("Basin "+str(Basin)+" Dimensionless Hillslope Morphology")
plt.tight_layout(rect=[0, 0.03, 1, 0.95])

```

The results look quite messy. Some of the data sit near the line, and some of the data sit quite far away. **Remember** these are thousands of individual hillslope traces! To try and make a bit more sense of this data we will do some grouping. The hillslope traces record where in the river network the hillslope terminates, and the channel network has been split into 100 m long segments. We will collect all the hillslope data belonging to each channel segment and conduct some statistics. Because we do not know whether the data is normally distributed, you will use the median as our central tendency indicator (instead of mean). To get an idea of the spread of the data, you will use the 16th and 84th percentiles (instead of standard deviation). These concepts should be familiar to you by now but please ask if you're not sure why this is important.

```python
# get a list of the unique channel segments in the hillslope data
Segments = BasinHillslopeData.StreamID.unique()

# we will create a new dataset to store the results in
# this sets up the new dataset and give names to the columns
SegmentsData = pd.DataFrame(columns=['SegmentNo','EStar','EStarLower','EStarUpper','RStar','RStarLower','RStarUpper','NTraces'])

# loop across all segments in the dataset
for i, Segment in enumerate(Segments):

    # Get segment hillslope data as a new data object
    SegmentHillslopeData = BasinHillslopeData[BasinHillslopeData.StreamID == float(Segment)]

    # Calculate EStar and RStar for the current segment
    TempEs = (-2.*SegmentHillslopeData.Cht*SegmentHillslopeData.Lh)/Sc
    TempRs = SegmentHillslopeData.S/Sc

    # Get the median and quartiles for EStar and RStar
    EStarSeg = TempEs.quantile(0.5)
    EStarSegUpper = TempEs.quantile(0.84)
    EStarSegLower = TempEs.quantile(0.16)
    RStarSeg = TempRs.quantile(0.5)
    RStarSegUpper = TempRs.quantile(0.84)
    RStarSegLower = TempRs.quantile(0.16)

    NTraces = SegmentHillslopeData.size

    #add to the new data frame
    SegmentsData.loc[i] = [Segment,EStarSeg,EStarSegLower,EStarSegUpper,RStarSeg,RStarSegLower,RStarSegUpper,NTraces]

# only keep segments with more than 100 hillslope traces so that we have a good size dataset
SegmentsData = SegmentsData[SegmentsData.NTraces > 100]
```

Now that you have segmented the hillslope data, let's plot this instead of/on top of the raw hillslope data and see if it provides a clearer picture of what is going on:

```python
# Plot symbols with error bars with colours but faded (increase alpha)
for i, row in SegmentsData.iterrows():
    EStarErr = np.array([[row.EStarLower],[row.EStarUpper]])
    RStarErr = np.array([[row.RStarLower],[row.RStarUpper]])
    ax.plot([row.EStar,row.EStar],RStarErr,'k-', lw=1,alpha=0.5,zorder=9)
    ax.plot(EStarErr,[row.RStar,row.RStar],'k-', lw=1,alpha=0.5,zorder=9)
    ax.plot(row.EStar,row.RStar,'ko',ms=4,zorder=32)

# save the figure
plt.savefig(ProjectName + "_Basin" + "%02d" % Basin + "_EStarRStar.png", dpi=300)

# display the updated plot
fig
```

## Hillslope morphology along the Bolinas Ridge

You have perhaps observed that hillslope morphology data is quite noisy and quite varied. In order to see through the variability within a basin you will now make a series of plots designed to show how hillslope morphology varies from basin to basin. To do this you will calculate the median, 16th and 84th percentiles of the various hillslope metrics and also the channel steepness data derived by Kirby et al. (2007):

```python
# we have already loaded the hillslope data, now we need the channel data
Filename = ProjectName+'_MChiSegmented.csv'
ChannelData = pd.read_csv(DataDirectory+Filename)

# Get rid of sections where there is no data
Segments2Remove = ChannelData[ChannelData.chi == -9999].segment_number.unique()
ChannelData = ChannelData[~ChannelData.segment_number.isin(Segments2Remove)]

# get list of basins
Basins = HillslopeData.BasinID.unique()
Basins.sort()

# create a new data object
BasinData = pd.DataFrame(columns=['Basin','CHT','CHT16','CHT84','LH','LH16','LH84','S','S16','S84','MChi','MChi16','MChi84'])

# calculate catchment summary data for each basin
for i, Basin in enumerate(Basins):

    # isolate hillslope data for this basin
    BasinHillslopeData = HillslopeData[HillslopeData.BasinID == Basin]

    # isolate channel data for this basin
    BasinChannelData = ChannelData[ChannelData.basin_key == i]

    # curvature stats
    CHT = BasinHillslopeData.Cht.quantile(0.5)
    CHT16 = BasinHillslopeData.Cht.quantile(0.16)
    CHT84 = BasinHillslopeData.Cht.quantile(0.84)

    # hillslope length stats
    LH = BasinHillslopeData.Lh.quantile(0.5)
    LH16 = BasinHillslopeData.Lh.quantile(0.16)
    LH84 = BasinHillslopeData.Lh.quantile(0.84)

    # slope stats
    S = BasinHillslopeData.S.quantile(0.5)
    S16 = BasinHillslopeData.S.quantile(0.16)
    S84 = BasinHillslopeData.S.quantile(0.84)

    # Channel Steepness stats
    MChi = BasinChannelData.m_chi.quantile(0.5)
    MChi16 = BasinChannelData.m_chi.quantile(0.16)
    MChi84 = BasinChannelData.m_chi.quantile(0.84)

    # add stats to the new data frame
    BasinData.loc[i] = [i,CHT,CHT16,CHT84,LH,LH16,LH84,S,S16,S84,MChi,MChi16,MChi84]

```

### Compare hillslope morphology along the length of the Bolinas Ridge

Now you will make some plots to show how the hillslope and channel morphology vary along the length of the Bolinas Ridge. In the lecture, we identified that hilltop curvature should be a proxy for erosion rates if the hillslopes are adjusted to their baselevel conditions. Hillslope gradient is also expected to increase with erosion rates, but become limited ***IF*** a process transition to more frequent landsliding has occurred. Think about what sort of relationship you might expect between hilltop curvature and hillslope gradient if this is the case. I suggest sketching out the expected relationship before you make the next plot.

Now go ahead and create a plot of hilltop curvature against hillslope gradient:

```python
# setup the figure
# you can modify the figure size here if you wish
fig_width_inches = 6.6
fig_height_inches = 5
fig = plt.figure(figsize=(fig_width_inches,fig_height_inches))
ax = plt.subplot()

#set up a colour map for showing basin number
ColourMap = cm.viridis

# Plot symbols with error bars with colours but faded (increase alpha)
for i, row in BasinData.iterrows():
    CHTErr = np.array([[row.CHT16],[row.CHT84]])
    SErr = np.array([[row.S16],[row.S84]])
    Colour = float(i)/float(len(BasinData))
    ax.plot([row.CHT,row.CHT],SErr,'-',color=ColourMap(Colour),lw=1,alpha=0.5,zorder=9)
    ax.plot(CHTErr,[row.S,row.S],'-',color=ColourMap(Colour),lw=1,alpha=0.5,zorder=9)
    ax.plot(row.CHT,row.S,'o',color=ColourMap(Colour),ms=5,zorder=32)

# label the axes
plt.xlabel("Hilltop Curvature (m$^{-1}$)")
plt.ylabel("Hillslope Gradient (m/m)")

# add the colour bar
m = cm.ScalarMappable(cmap=ColourMap)
m.set_array(BasinData.Basin)
cbar = plt.colorbar(m)
cbar.set_label("Basin No.")

# save the figure
plt.savefig(ProjectName + "_Cht_S.png", dpi=300)
```

The results are interesting, and have been colour-coded by basin number from NW to SE to help you to interpret the results. But these results don't allow us to compare the hillslope morphology to the theoretical predictions of the nonlinear sediment flux equation. To do that, we again need to calculate $E^*$ and $R^*$.

```python
# Calculate E* vs R* for our hillslope data
EStar = (-2*BasinData.CHT*BasinData.LH)/(Sc)
RStar = BasinData.S/Sc

# Calculate E* vs R* range
EStar16 = (-2*BasinData.CHT16*BasinData.LH16)/(Sc)
RStar16 = BasinData.S16/Sc
EStar84 = (-2*BasinData.CHT84*BasinData.LH84)/(Sc)
RStar84 = BasinData.S84/Sc

# setup the figure
# you can modify the figure size here if you wish
fig_width_inches = 6.6
fig_height_inches = 4.4
fig = plt.figure(figsize=(fig_width_inches,fig_height_inches))
ax = plt.subplot()

#make a logarithmic plot
plt.loglog()

#plot the theoretical relationship between EStar and RStar
ax.plot(EStarSS,RStarSS,'k--',zorder=10)

#set up a colour map for showing basin number
ColourMap = cm.viridis

# Plot symbols with error bars with colours but faded (increase alpha)
for i in range(0,len(Basins)):
    EStarErr = np.array([[EStar16[i]],[EStar84[i]]])
    RStarErr = np.array([[RStar16[i]],[RStar84[i]]])
    Colour = float(i)/float(len(BasinData))
    ax.plot([EStar[i],EStar[i]],RStarErr,'-',color=ColourMap(Colour),lw=1,alpha=0.5,zorder=9)
    ax.plot(EStarErr,[RStar[i],RStar[i]],'-',color=ColourMap(Colour),lw=1,alpha=0.5,zorder=9)
    ax.plot(EStar[i],RStar[i],'o',color=ColourMap(Colour),ms=5,zorder=32)

# label the axes and limit
plt.xlabel('$E^*={{-2\:C_{HT}\:L_H}/{S_C}}$')
plt.ylabel('$R^*=S/S_C$')
plt.xlim(0.1,100.)
plt.ylim(0.02,1.)

# add the colour bar
m = cm.ScalarMappable(cmap=ColourMap)
m.set_array(BasinData.Basin)
cbar = plt.colorbar(m)
cbar.set_label("Basin No.")

# save the figure
plt.savefig(ProjectName + "_Estar_Rstar.png", dpi=300)
```

It would seem that hillslope morphology is varying systematically along the length of the Bolinas Ridge. The boundary conditions for the hillslopes are set erosion in the adjacent streams. Kirby et al. (2007) suggested the steepness of the channels increasing along the Bolinas Ridge was reflecting increased erosion in response to a gradient in tectonic uplift rates. Let's use our hillslope data to investigate whether hillslope and channel metrics for erosion rates are comparable by plotting channel steepness ($M_\chi$) against $E^*$:

```python
# setup the figure
# you can modify the figure size here if you wish
fig_width_inches = 6.6
fig_height_inches = 4.4
fig = plt.figure(figsize=(fig_width_inches,fig_height_inches))
ax = plt.subplot()

#set up a colour map for showing basin number
ColourMap = cm.viridis

# Get MChi data
MChi = BasinData.MChi
MChi16 = BasinData.MChi16
MChi84 = BasinData.MChi84

# Plot symbols with error bars with colours but faded (increase alpha)
for i in range(0,len(Basins)):
    EStarErr = np.array([[EStar16[i]],[EStar84[i]]])
    MChiErr = np.array([[MChi16[i]],[MChi84[i]]])
    Colour = float(i)/float(len(BasinData))
    ax.plot([MChi[i],MChi[i]],EStarErr,'-',color=ColourMap(Colour),lw=1,alpha=0.5,zorder=9)
    ax.plot(MChiErr,[EStar[i],EStar[i]],'-',color=ColourMap(Colour),lw=1,alpha=0.5,zorder=9)
    ax.plot(MChi[i],EStar[i],'o',color=ColourMap(Colour),ms=5,zorder=32)

# label the axes and limit
plt.xlabel('Channel Steepness Index $M_{Chi}$')
plt.ylabel('$E^*={{-2\:C_{HT}\:L_H}/{S_C}}$')

# add the colour bar
m = cm.ScalarMappable(cmap=ColourMap)
m.set_array(BasinData.Basin)
cbar = plt.colorbar(m)
cbar.set_label("Basin No.")

# save the figure
plt.savefig(ProjectName + "_MChi_EStar.png", dpi=300)
```

ASSESSMENT TASK

Describe the distribution of hillslope morphology within an individual catchment and along the length of the Bolinas Ridge. Evaluate how hillslope morphology compares to the steady-state predictions of the non-linear sediment transport equation and to the boundary conditions set by the channel steepness. You may wish to refer to existing academic literature on this site, and on other locations where hillslope morphology has been analysed in this way to help you. You can choose which figures to include, and  remember that your figures will need to have a figure caption. You should approach this task as a short report/essay.


```python
'''
TODO:
0. Do hurtwiza wklep mianownik "L_K*L_G+M_K*M_G".
Jest to rozwinięty mianownik K_Z,
więc hurtwiz w tym momencie nam wypluje stabilnosc petli zamknietej

1. Mamy petle otwarta i zamknieta
    1.1 Wyswietl transmitancje dla obu petli nazwij K_Z i K_O

2. Sprawdzamy stabilnosc K*G Hurwitz'em

3. Rysujemy Nyquista, dla 1+K*G

4. Zależnie od stabilności K*G oceniamy na podstawie 1+K*G, oceniamy
stabilnosc całego układu zamknietego
'''
import os
import subprocess
import numpy as np
import pandas as pd
import matplotlib as plt
import sympy as sp
import hurwitz as hr

# Tworzymy plik do latexa, ale na poczatku sprawdzamy czy plik o takiej samej nazwie istnieje
# jak tak to go usuwamy

# Ustawiamy s oraz k w SymPy
s = sp.Symbol('s', rational=True)
k = sp.Symbol('k', rational=True, real=True)

# Nasze transmitancje i ich postacie w latexu
K_a = 1/((s+1)*(s+2))
K_b = k
K_c = (s+2)/((s+1)*(s+2))
K_d = 1/(s**2+4*s+5)

LATEX_K_LIST = [str(sp.latex(K_a)), str(sp.latex(K_b)),
                str(sp.latex(K_c)), str(sp.latex(K_d))]
K_LIST = [(K_a), (K_b),
          (K_c), (K_d)]
G_a = k
G_b = 1/((s+1)*(s+2))
G_c = 1/(s+3)
G_d = 1/(s+3)

G_LIST = [(G_a), (G_b),
          (G_c), (G_d)]
LATEX_G_LIST = [str(sp.latex(G_a)), str(sp.latex(G_b)),
                str(sp.latex(G_c)), str(sp.latex(G_d))]

# Podział na licznik i mianownik (symbolicznie)
L_K_a, M_K_a = sp.fraction(K_a)
L_K_b, M_K_b = sp.fraction(K_b)
L_K_c, M_K_c = sp.fraction(K_c)
L_K_d, M_K_d = sp.fraction(K_d)

K_L_LIST = []
K_M_LIST = []
for x in K_LIST:
    K_L_LIST.append(sp.fraction(x)[0])
    K_M_LIST.append(sp.fraction(x)[1])

L_G_a, M_G_a = sp.fraction(G_a)
L_G_b, M_G_b = sp.fraction(G_b)
L_G_c, M_G_c = sp.fraction(G_c)
L_G_d, M_G_d = sp.fraction(G_d)

G_L_LIST = []
G_M_LIST = []
for x in G_LIST:
    G_L_LIST.append(sp.fraction(x)[0])
    G_M_LIST.append(sp.fraction(x)[1])

# Kazde K_Z jako latex w liscie
LATEX_K_Z_LIST = [str(sp.latex((L_K_a*M_G_a)/(L_K_a*L_G_a+M_K_a*M_G_a))), str(sp.latex((L_K_b*M_G_b/(L_K_b*L_G_b+M_K_b*M_G_b)))),
                  str(sp.latex((L_K_c*M_G_c)/(L_K_c*L_G_c+M_K_c*M_G_c))), str(sp.latex((L_K_d*M_G_d/(L_K_d*L_G_d+M_K_d*M_G_d))))]
# Kazde K_Z jako expr
K_Z_LIST = []
for x in range(4):
    K_Z_LIST.append((K_L_LIST[x]*G_M_LIST[x])/(K_L_LIST[x]*G_L_LIST[x]+K_M_LIST[x]*G_M_LIST[x]))
# Dzieli K_Z na mianownik i licznik
K_Z_L_LIST=[]
K_Z_M_LIST=[]
for x in K_Z_LIST:
    K_Z_L_LIST.append(sp.fraction(x)[0])
    K_Z_M_LIST.append(sp.fraction(x)[1])
# Hurwitz, dla kazdego przykladu, dla petli zamknietej
# Dostajemy booleana czy jest stabilny czy nie
# oraz wersje latexowa(string) K_z
K_Z_H_MATRIX = []
K_Z_H_DETS = []
for x in K_Z_LIST:
    K_Z_H_MATRIX.append(hr.Hurwitz_sp(x)[0])
    K_Z_H_DETS.append(hr.Hurwitz_sp(x)[1])
# Licznik i Mianownik K'atych (jako listy wspolczynnikow przy s'ach)
L_KG_LIST_a, M_KG_LIST_a = [[float(i) for i in sp.Poly(
    i, s).all_coeffs()] for i in (K_a*G_a).replace(k, 1).as_numer_denom()]
L_KG_LIST_b, M_KG_LIST_b = [[float(i) for i in sp.Poly(
    i, s).all_coeffs()] for i in (K_b*G_b).replace(k, 1).as_numer_denom()]
L_KG_LIST_c, M_KG_LIST_c = [[float(i) for i in sp.Poly(
    i, s).all_coeffs()] for i in (K_c*G_c).replace(k, 1).as_numer_denom()]
L_KG_LIST_d, M_KG_LIST_d = [[float(i) for i in sp.Poly(
    i, s).all_coeffs()] for i in (K_d*G_d).replace(k, 1).as_numer_denom()]

'''
Tworzymy plik txt, ktory bedzie handlerem wyniku stabilnosci ktory otrzymamy od matlaba.
Dodatkowo za pomoca modulu os wykonamy odpowiednie, dla systemu komendy, aby matlab wyprodukowal nam
wysokiej jakosci plot'y Nyquista
# '''
# if os.path.exists("stabs.txt"):
#     os.remove("stabs.txt")
# if os.path.exists("end_of_mat.txt"):
#     os.remove("end_of_mat.txt")

# f = open("stabs.txt", "w+")
# f.close()
# END_NAME_FILE = "end_of_mat.txt"
# # dlmwrite('./stabs.txt',[1 2 3 4],'delimiter',',')
# if os.name == "nt":  # if is Windows
#     print("Detected OS: Windows")
#     COMMAND_START = "cmd /c matlab -nodesktop -r \"nyquist_plot_with_k("+str(repr(L_KG_LIST_a))+","+str(
#         repr(M_KG_LIST_a))+"," + "\'"+"a"+"\');nyquist_plot_with_k("+str(repr(L_KG_LIST_b))+","+str(
#         repr(M_KG_LIST_b))+"," + "\'"+"b"+"\');nyquist_plot("+str(repr(L_KG_LIST_c))+","+str(
#         repr(M_KG_LIST_c))+"," + "\'"+"c"+"\');nyquist_plot("+str(repr(L_KG_LIST_d))+","+str(
#         repr(M_KG_LIST_d))+"," + "\'"+"d"+"\');finish(\'"+END_NAME_FILE+"\');exit;\""

# elif os.name == "posix":  # if is Linux
#     print("Detected OS: Linux")
#     COMMAND_START = "/usr/local/MATLAB/R2018a/bin/matlab -softwareopengl -nodesktop -r \"nyquist_plot_with_k("+str(repr(L_KG_LIST_a))+","+str(
#         repr(M_KG_LIST_a))+"," + "\'"+"a"+"\');nyquist_plot_with_k("+str(repr(L_KG_LIST_b))+","+str(
#         repr(M_KG_LIST_b))+"," + "\'"+"b"+"\');nyquist_plot("+str(repr(L_KG_LIST_c))+","+str(
#         repr(M_KG_LIST_c))+"," + "\'"+"c"+"\');nyquist_plot("+str(repr(L_KG_LIST_d))+","+str(
#         repr(M_KG_LIST_d))+"," + "\'"+"d"+"\');finish(\'"+END_NAME_FILE+"\');exit;\""
# else:
#     print("Error: Deteced OS: Not known!")
#     raise SystemExit

# print("Command/s to be run: \n"+COMMAND_START)
# os.system(COMMAND_START)

# print("Waiting for Matlab to finish:")
# i = 0
# while not os.path.exists("end_of_mat.txt"):
#     if i == 1:  # to sie i tak nie wykona
#         print("wtf")

# print("Matlab has finished", end="")

# print("FINISH")
# df = pd.read_csv("stabs.txt", header=None)
# STABS_KG = df[0].to_list()
# STABS_1_KG = df[1].to_list()

print("Finished analyzing stability")
print("-"*50)
# if os.path.exists("stabs.txt"):
#     os.remove("stabs.txt")
# if os.path.exists("end_of_mat.txt"):
#     os.remove("end_of_mat.txt")

# DO LATEXA !!!!!!!!!!!!!!
# for x in range(1):
#     print("\\textbf{Przykład }\n$$K="+sp.latex(K_LIST[x])+"$$\n"+"$$G="+sp.latex(G_LIST[x])+"$$")
#     print("Transmitancja CLS:\n$$K_Z=\\frac{k}{1+KG}=\\frac{" + sp.latex(K_LIST[x]) + "}{1+" + sp.latex(K_LIST[x]) + sp.latex(G_LIST[x]) + "}=" +sp.latex(K_Z_LIST[x])+"$$")
#     print("Transmitancja OLS:\n$$K_O=K\\cdot G="+sp.latex(K_LIST[x]*G_LIST[x])+"$$")
#     print("\\textbf{Sprawdzamy stabilność CLS z pomocą kryterium Hurwitza:}\\newline")
#     print("Wielomian charakterystyczny CLS:\n$$M_Z=" + sp.latex(K_Z_M_LIST[x]) + "$$")
#     print("Macierz Hurwitza na podstawie tego wielomianu oraz wartości wyznacznika i podwyznaczników:")
#     print("$$" + str((hr.Hurwitz_sp(K_Z_LIST(x)))[0])+ "$$\n Tabela z wyznacznikami\n$$" + str((hr.Hurwitz_sp(K_Z_LIST(x)))[1])+ "$$")
#     print("%oceniam stabilnosc recznie")
#     print("\\newline\\textbf{Sprawdzam stabilność CLS z pomocą kryterium Nyquista}\\newline")
#     print("Wykres Nyquista dla 1+KG:\n\\includegraphics[width=12cm]{}")
#     print("%oceniam stabilnosc recznie\n\\newpage")
```

```python
K_Z_LIST
```

```python
K_Z_LIST[0]
```

```python

```