# Classificação: Classificador Bayesiano

Francisco Aparecido Rodrigues, francisco@icmc.usp.br.

Universidade de São Paulo, São Carlos, Brasil.

https://sites.icmc.usp.br/francisco

Vamos inicialmente ler os dados:

```python
import random
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
random.seed(42) # define the seed (important to reproduce the results)

#data = pd.read_csv('data/vertebralcolumn-3C.csv', header=(0))
#data = pd.read_csv('data/BreastCancer.csv', header=(0))
#data = pd.read_csv('data/Iris.csv', header=(0))
data = pd.read_csv('data/Vehicle.csv', header=(0))

data = data.dropna(axis='rows') #remove NaN
# armazena os nomes das classes
classes = np.array(pd.unique(data[data.columns[-1]]), dtype=str)

nrow, ncol = data.shape
print("Matriz de atributos: Número de linhas:", nrow, " colunas: ", ncol)
attributes = list(data.columns)
data.head(10)
```

    Matriz de atributos: Número de linhas: 846  colunas:  19

      Comp
      Circ
      D.Circ
      Rad.Ra
      Pr.Axis.Ra
      Max.L.Ra
      Scat.Ra
      Elong
      Pr.Axis.Rect
      Max.L.Rect
      Sc.Var.Maxis
      Sc.Var.maxis
      Ra.Gyr
      Skew.Maxis
      Skew.maxis
      Kurt.maxis
      Kurt.Maxis
      Holl.Ra
      Class

      0
      95
      48
      83
      178
      72
      10
      162
      42
      20
      159
      176
      379
      184
      70
      6
      16
      187
      197
      van

      1
      91
      41
      84
      141
      57
      9
      149
      45
      19
      143
      170
      330
      158
      72
      9
      14
      189
      199
      van

      2
      104
      50
      106
      209
      66
      10
      207
      32
      23
      158
      223
      635
      220
      73
      14
      9
      188
      196
      saab

      3
      93
      41
      82
      159
      63
      9
      144
      46
      19
      143
      160
      309
      127
      63
      6
      10
      199
      207
      van

      4
      85
      44
      70
      205
      103
      52
      149
      45
      19
      144
      241
      325
      188
      127
      9
      11
      180
      183
      bus

      5
      107
      57
      106
      172
      50
      6
      255
      26
      28
      169
      280
      957
      264
      85
      5
      9
      181
      183
      bus

      6
      97
      43
      73
      173
      65
      6
      153
      42
      19
      143
      176
      361
      172
      66
      13
      1
      200
      204
      bus

      7
      90
      43
      66
      157
      65
      9
      137
      48
      18
      146
      162
      281
      164
      67
      3
      3
      193
      202
      van

      8
      86
      34
      62
      140
      61
      7
      122
      54
      17
      127
      141
      223
      112
      64
      2
      14
      200
      208
      van

      9
      93
      44
      98
      197
      62
      11
      183
      36
      22
      146
      202
      505
      152
      64
      4
      14
      195
      204
      saab

Vamos construir as variáveis $X$ e $y$, sendo que o processo classificação se resume em estimar a função $f$ na relação $y = f(X) + \epsilon$, onde $\epsilon$ é o erro, que tem distribuição normal com média igual a zero e variância $\sigma^2$.

Convertemos os dados para o formato Numpy para facilitar a sua manipulação.

```python
data = data.to_numpy()
nrow,ncol = data.shape
y = data[:,-1]
X = data[:,0:ncol-1]
```

Vamos normalizar os dados, de modo a evitar o efeito da escala dos atributos.

```python
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler().fit(X)
X = scaler.transform(X)
```

```python
print('Dados transformados:')
print('Media: ', np.mean(X, axis = 0))
print('Desvio Padrao:', np.std(X, axis = 0))
```

    Dados transformados:
    Media:  [ 5.87919521e-16 -3.14956886e-16 -3.65349988e-16 -1.67977006e-16
      3.77948264e-17 -1.63777581e-16 -3.56951138e-16  1.72176431e-16
      1.67977006e-17  8.27286755e-16 -9.02876408e-17 -1.46979880e-16
      3.73748839e-16 -6.92905150e-16 -5.24928144e-17 -2.93959761e-17
     -1.31022065e-15  1.52649104e-15]
    Desvio Padrao: [1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]

Para treinar o classificador, precisamos definir o conjunto de teste e treinamento.

```python
from sklearn.model_selection import train_test_split
p = 0.8 # fracao de elementos no conjunto de treinamento
x_train, x_test, y_train, y_test = train_test_split(X, y, train_size = p, random_state = 42)
```

A partir desse conjunto de dados, podemos realizar a classificação.

## Classificador Bayesiano

Vamos considerar o caso paramétrico, assumindo que cada variável está distribuída de acordo com uma distribuição Normal. Outras distribuições também podem ser utilizadas.

Já selecionamos os conjuntos de treinamento e teste anteriormente. No conjunto de treinamento, vamos calcular a média e desvio padrão de cada atributo para cada classe. A seguir, reaizamos a classificação, dos dados usando a teoria da decisão Bayesiana, isto é: $X \in C_i$ se, e somente se, $P(C_i|X) = \max P(C_j|X)$ para todo $j$.

```python
from scipy.stats import multivariate_normal

#matrix to store the probabilities
P = pd.DataFrame(data=np.zeros((x_test.shape[0], len(classes))), columns = classes)

Pc = np.zeros(len(classes)) #fraction of elements in each class
for i in np.arange(0, len(classes)):
    elements = tuple(np.where(y_train == classes[i]))
    Pc[i] = len(elements)/len(y_train)
    Z = x_train[elements,:][0]
    m = np.mean(Z, axis = 0)
    cv = np.cov(np.transpose(Z))
    for j in np.arange(0,x_test.shape[0]):
        x = x_test[j,:]
        pj = multivariate_normal.pdf(x, mean=m, cov=cv, allow_singular=True)
        P[classes[i]][j] = pj*Pc[i]
```

```python
print(P)
```

                   van          saab           bus          opel
    0     7.795703e-12  7.779917e-16  6.276640e-05  2.579945e-14
    1     9.012929e-05  1.625794e-35  8.143083e-26  1.169996e-31
    2     2.135688e-09  1.472223e-09  3.940445e-04  5.004525e-09
    3     1.527642e-06  1.594300e-05  3.748503e-13  1.293526e-06
    4     2.346239e-07  2.441747e-06  3.046580e-02  4.047345e-05
    ..             ...           ...           ...           ...
    165   0.000000e+00  5.227464e-07  8.239158e-22  5.345531e-04
    166   0.000000e+00  8.894732e-03  2.156071e-30  4.673660e-06
    167  1.362920e-148  1.429868e-03  6.813446e-27  2.116741e-03
    168   0.000000e+00  9.599080e-03  2.677856e-33  1.454827e-04
    169   2.000502e-06  9.236692e-20  1.712805e-27  8.699291e-21

    [170 rows x 4 columns]

```python
y_pred = []
#np.array(test_x.shape[0], dtype=str)
for i in np.arange(0, x_test.shape[0]):
    c = np.argmax(np.array(P.iloc[[i]]))
    y_pred.append(classes[c])
y_pred = np.array(y_pred, dtype=str)
print(y_pred)
```

    ['bus' 'van' 'bus' 'saab' 'bus' 'van' 'van' 'saab' 'bus' 'saab' 'saab'
     'bus' 'van' 'opel' 'opel' 'van' 'saab' 'bus' 'saab' 'saab' 'van' 'van'
     'saab' 'saab' 'opel' 'opel' 'opel' 'van' 'van' 'van' 'opel' 'van' 'bus'
     'van' 'van' 'bus' 'bus' 'saab' 'bus' 'opel' 'van' 'bus' 'opel' 'van'
     'saab' 'saab' 'bus' 'van' 'bus' 'van' 'bus' 'bus' 'van' 'saab' 'bus'
     'opel' 'saab' 'opel' 'saab' 'saab' 'bus' 'van' 'saab' 'bus' 'bus' 'bus'
     'saab' 'saab' 'opel' 'opel' 'opel' 'bus' 'saab' 'bus' 'van' 'van' 'saab'
     'opel' 'saab' 'van' 'bus' 'van' 'saab' 'saab' 'opel' 'saab' 'van' 'bus'
     'van' 'saab' 'saab' 'opel' 'bus' 'bus' 'saab' 'bus' 'van' 'van' 'bus'
     'opel' 'saab' 'saab' 'opel' 'van' 'bus' 'bus' 'saab' 'van' 'saab' 'opel'
     'van' 'bus' 'van' 'bus' 'bus' 'van' 'bus' 'opel' 'bus' 'saab' 'bus'
     'opel' 'saab' 'van' 'bus' 'saab' 'bus' 'van' 'bus' 'opel' 'bus' 'van'
     'bus' 'saab' 'bus' 'bus' 'bus' 'saab' 'opel' 'bus' 'van' 'saab' 'bus'
     'bus' 'van' 'opel' 'saab' 'opel' 'van' 'saab' 'opel' 'bus' 'bus' 'saab'
     'van' 'van' 'bus' 'van' 'opel' 'bus' 'bus' 'van' 'saab' 'opel' 'opel'
     'opel' 'saab' 'opel' 'saab' 'van']

```python
from sklearn.metrics import accuracy_score
score = accuracy_score(y_pred, y_test)
print('Accuracy:', score)
```

    Accuracy: 0.8764705882352941

Código completo.

```python
import random
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from scipy.stats import multivariate_normal
from sklearn.metrics import accuracy_score

random.seed(42)

data = pd.read_csv('data/Iris.csv', header=(0))

classes = np.array(pd.unique(data[data.columns[-1]]), dtype=str)

# Converte para matriz e vetor do numpy
data = data.to_numpy()
nrow,ncol = data.shape
y = data[:,-1]
X = data[:,0:ncol-1]

# Transforma os dados para terem media igual a zero e variancia igual a 1
scaler = StandardScaler().fit(X)
X = scaler.transform(X)

# Seleciona os conjuntos de treinamento e teste
p = 0.8 # fraction of elements in the test set
x_train, x_test, y_train, y_test = train_test_split(X, y, train_size = p, random_state = 42)

####  Realiza a classificacao ####
# Matriz que armazena as probabilidades para cada classe
P = pd.DataFrame(data=np.zeros((x_train.shape[0], len(classes))), columns = classes)

Pc = np.zeros(len(classes)) # Armaze a fracao de elementos em cada classe
for i in np.arange(0, len(classes)): # Para cada classe
    elements = tuple(np.where(y_train == classes[i])) # elmentos na classe i
    Pc[i] = len(elements)/len(y_train) # Probabilidade pertencer a classe i
    Z = x_train[elements,:][0] # Elementos no conjunto de treinamento
    m = np.mean(Z, axis = 0) # Vetor media
    cv = np.cov(np.transpose(Z)) # Matriz de covariancia
    for j in np.arange(0,x_test.shape[0]): # para cada observacao no conjunto de teste
        x = x_test[j,:]
        # calcula a probabilidade pertencer a cada classe
        pj = multivariate_normal.pdf(x, mean=m, cov=cv, allow_singular=True)
        P[classes[i]][j] = pj*Pc[i]

y_pred = [] # Vetor com as classes preditas
for i in np.arange(0, x_test.shape[0]):
    c = np.argmax(np.array(P.iloc[[i]]))
    y_pred.append(classes[c])
y_pred = np.array(y_pred, dtype=str)
# calcula a acuracia
score = accuracy_score(y_pred, y_test)
print('Acuracia:', score)
```

    Acuracia: 0.9666666666666667

## Caso não paramétrico

Para o caso unidimensional, seja $(X_1,X_2, \ldots, X_n)$ uma amostra aleatória unidimensional identicamente distribuída de acordo com alguma função de distribuição $f$ não conhecida. Para estimarmos o formato de $f$, usamos um estimador (kernel density estimator):
\begin{equation}
\widehat{f}_{h}(x)={\frac {1}{n}}\sum _{i=1}^{n}K_{h}(x-x_{i})={\frac {1}{nh}}\sum _{i=1}^{n}K{\Big (}{\frac {x-x_{i}}{h}}{\Big )},
\end{equation}
onde $K$ é a função kernel.
A estimação depende do parâmetro $h$, que é um parâmetro livre e controla a abertura da função.

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

N = 20
# gera os dados
X = np.array([1, 2, 3, 4, 12, 20,21,22,23,24,40,41, 50])
X = X.reshape((len(X), 1))
# mostra os dados
plt.figure(figsize=(12,4))
plt.plot(X[:, 0], 0.001*np.ones(X.shape[0]), 'ok')

# valores x para serem usados nas densidades
X_plot = np.linspace(np.min(X)-5, np.max(X)+5, 1000)[:, np.newaxis]

h=2
fhat = 0 # estimacao obtida
for x in X:
    # distribuição normal centrada em x
    f = (1/np.sqrt(2*np.pi*h))*np.exp(-((X_plot - x)**2)/(2*h**2))
    fhat = fhat + f # acumula as distribuições
    plt.plot(X_plot,f, '--', color = 'blue', linewidth=1)

# mostra a distribuição estimada
plt.plot(X_plot,fhat/(len(X)*np.sqrt(h)), color = 'green', linewidth=2)
plt.xlabel('x', fontsize = 20)
plt.ylabel('P(x)', fontsize = 20)
plt.xticks(fontsize=12)
plt.yticks(fontsize=12)
plt.ylim((0, 0.3))
plt.savefig('kernel-ex.eps')
plt.show(True)
```

Esse resultado pode ser obtido usando-se a função KernelDensity scikit-learn.

```python
import numpy as np
from matplotlib.pyplot import cm
from sklearn.neighbors import KernelDensity

color=['red', 'blue', 'magenta', 'gray', 'green']

N = 20
X = np.array([1, 2, 3, 4, 12, 20,21,22,23,24,40,41, 50])
X = X.reshape((len(X), 1))

plt.figure(figsize=(12,4))
plt.plot(X[:, 0], 0.001*np.ones(X.shape[0]), 'ok')

X_plot = np.linspace(np.min(X)-5, np.max(X)+5, 1000)[:, np.newaxis]

h=2
fhat = 0
for x in X:
    f = (1/np.sqrt(2*np.pi*h))*np.exp(-((X_plot - x)**2)/(2*h**2))
    fhat = fhat + f
    plt.plot(X_plot,f, '--', color = 'blue', linewidth=1)

kde = KernelDensity(kernel='gaussian', bandwidth=h).fit(X)
log_dens = np.exp(kde.score_samples(X_plot)) # score_samples() returns the log density.
plt.plot(X_plot,log_dens, color = 'red', linewidth=2, label = 'h='+str(h))
plt.xlabel('x', fontsize = 20)
plt.ylabel('P(x)', fontsize = 20)
plt.xticks(fontsize=12)
plt.yticks(fontsize=12)
plt.ylim((0, 0.3))

plt.show(True)
```

Notem que o formato da estimação depende do parâmetro livre $h$.

```python
import numpy as np
from matplotlib.pyplot import cm

color=['red', 'blue', 'gray', 'black', 'green', 'lightblue']

N = 20
X = np.array([1, 2, 3, 4, 12, 20,21,22,23,24,40,41, 50])
X = X.reshape((len(X), 1))

X_plot = np.linspace(np.min(X)-5, np.max(X)+5, 1000)[:, np.newaxis]

plt.figure(figsize=(12,4))
plt.plot(X[:, 0], 0.001*np.ones(X.shape[0]), 'ok')
c = 0
vh = [0.1, 0.5, 1, 2, 5, 10]
for h in vh:
    kde = KernelDensity(kernel='gaussian', bandwidth=h).fit(X)
    log_dens = np.exp(kde.score_samples(X_plot)) # score_samples() returns the log density.
    plt.plot(X_plot,log_dens, color = color[c], linewidth=2, label = 'h='+str(h))
    c = c + 1
plt.ylabel('P(x)', fontsize = 20)
plt.xticks(fontsize=12)
plt.yticks(fontsize=12)
#plt.ylim((0, 0.2))
plt.legend(fontsize = 10)
#plt.savefig('kernel.eps')
plt.show(True)
```

Notem que a estimação é relacionada com a estimação usando-se histogramas.

```python
import numpy as np

N = 20
X = np.array([1, 2, 3, 4, 12, 20,21,22,23,24,40,41, 50])
X = X.reshape((len(X), 1))

plt.figure(figsize=(10,5))
# Histogram
nbins = 10
plt.hist(X,bins = nbins, density = True, color='gray',alpha=0.7, rwidth=0.95)

#Kernel density estimation
X_plot = np.linspace(np.min(X)-5, np.max(X)+5, 1000)[:, np.newaxis]
kde = KernelDensity(kernel='gaussian', bandwidth=2).fit(X)
log_dens = np.exp(kde.score_samples(X_plot)) # score_samples() returns the log density.
plt.plot(X_plot,log_dens, color = 'blue', linewidth=2)
plt.plot(X[:, 0], 0.001*np.ones(X.shape[0]), 'ok')
plt.xlabel('x', fontsize = 15)
plt.ylabel('P(x)', fontsize = 15)

plt.show(True)
```

Usando o método *kernel density estimation*, podemos realizar a classificação.

```python
import random
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KernelDensity
from sklearn.metrics import accuracy_score

random.seed(42)
#data = pd.read_csv('data/Iris.csv', header=(0))
data = pd.read_csv('data/Vehicle.csv', header=(0))

classes = np.array(pd.unique(data[data.columns[-1]]), dtype=str)

# Converte para matriz e vetor do numpy
data = data.to_numpy()
nrow,ncol = data.shape
y = data[:,-1]
X = data[:,0:ncol-1]

# Transforma os dados para terem media igual a zero e variancia igual a 1
scaler = StandardScaler().fit(X)
X = scaler.transform(X)

# Seleciona os conjuntos de treinamento e teste
p = 0.8 # fraction of elements in the training set
x_train, x_test, y_train, y_test = train_test_split(X, y, train_size = p, random_state = 42)

# Matriz que armazena as probabilidades para cada classe
P = pd.DataFrame(data=np.zeros((x_test.shape[0], len(classes))), columns = classes)
Pc = np.zeros(len(classes)) # Armaze a fracao de elementos em cada classe
h = 2
for i in np.arange(0, len(classes)): # Para cada classe
    elements = tuple(np.where(y_train == classes[i])) # elmentos na classe i
    Pc[i] = len(elements)/len(y_train) # Probabilidade pertencer a classe i
    Z = x_train[elements,:][0] # Elementos no conjunto de treinamento
    kde = KernelDensity(kernel='gaussian', bandwidth=h).fit(Z)
    for j in np.arange(0,x_test.shape[0]): # para cada observacao no conjunto de teste
        x = x_test[j,:]
        x = x.reshape((1,len(x)))
        # calcula a probabilidade pertencer a cada classe
        pj = np.exp(kde.score_samples(x))
        P[classes[i]][j] = pj*Pc[i]

y_pred = [] # Vetor com as classes preditas
for i in np.arange(0, x_test.shape[0]):
    c = np.argmax(np.array(P.iloc[[i]]))
    y_pred.append(classes[c])
y_pred = np.array(y_pred, dtype=str)
# calcula a acuracia
score = accuracy_score(y_pred, y_test)
print('Acuracia:', score)
```

    Acuracia: 0.6764705882352942

### Exercícios

1 - Considere outras distribuições na classificação dos dados, além da Normal.

2 - Verifique como o parâmetro $h$ influencia na classificação.


# Formulation of Maxwell's Equations in Curvilinear Coordinates and Flat Spacetime- An Overview

## Author: Terrence Pierre Jacques

## This tutorial notebook constructs Maxwell's equations in a covariant form.

### This module follows from the derivations of [Tutorial-VacuumMaxwell_formulation_Cartesian](Tutorial-VacuumMaxwell_formulation_Cartesian.ipynb).

# Introduction

Following [Tutorial-VacuumMaxwell_formulation_Cartesian](Tutorial-VacuumMaxwell_formulation_Cartesian.ipynb) and [Tutorial-Start_to_Finish-Solving_Maxwells_Equations_in_Vacuum-Cartesian notebook](Tutorial-Start_to_Finish-Solving_Maxwells_Equations_in_Vacuum-Cartesian.ipynb), the user will note that solving the system of equations seems computationally wasteful. The toroidal dipole field initial data as implemented in [this tutorial](Tutorial-VacuumMaxwell_InitialData.ipynb) are axisymmetric, so one hardly needs to sample the numerical solution in the $\phi$ direction. Thus, the numerical solution requires sampling in the $r$ and $\theta$ directions. Therefore, evolving these initial data in Cartesian coordinates is wasteful, as we do not take advantages of the symmetry of the problem. To do so, we must evolve the system in spherical or cylindrical like coordinate systems, or curvilinear coordinate systems. However, we must then reformulate Maxwell's equations into a covariant form.

 ### A Note on Notation:

As is standard in NRPy+,

* Greek indices refer to four-dimensional quantities where the zeroth component indicates temporal (time) component.
* Latin indices refer to three-dimensional quantities. This is somewhat counterintuitive since Python always indexes its lists starting from 0. As a result, the zeroth component of three-dimensional quantities will necessarily indicate the first *spatial* direction.

$$\label{top}$$

# Table of Contents:

1. [Step 1](#step3): Maxwell's Equations in Vacuum and Curvilinear Coordinates
    1. [Step 1.a](#step3a): System II in Curvilinear Coordinates
    1. [Step 1.b](#step3b): System II in Curvilinear Coordinates, using the rescaled quantities $a^i$ and $e^i$
1. [Step 2](#latex_pdf_output): Output this notebook to $\LaTeX$-formatted PDF file

# Step 1: Maxwell's Equations in Vacuum and Curvilinear Coordinates \[Back to [top](#top)\]
$$\label{step3}$$

## Step 1a: System II (BSSN - like) in Curvilinear Coordinates \[Back to [top](#top)\]
$$\label{step3a}$$

Our current formulation of Maxwell's equations remains non-covariant, i.e. the forms of the equations will be altered if we move to curvilinear coordinates. To address this issue, and thus allow us to write the equations down in any arbitrary coordinate system, we must rewrite the equations to be manifestly covariant.

We begin this process by employing the so-called "comma goes to semi-colon rule," replacing all partial derivatives, which do not transform as tensorial quantities, with covariant derivatives, which do. Rewriting Maxwell's equations with only tensorial quantities will yield a covariant formulation of the equations. So, starting with System II in [this tutorial](Tutorial-VacuumMaxwell_formulation_Cartesian.ipynb),

\begin{align}
\partial_t A^i = -E^i - \partial^i \varphi &\to -E^i - \hat{\nabla}^i \varphi =  -E^i - \hat{g}^{ij}\hat{\nabla}_j \varphi, \\
\partial_t E^i = \partial^i \Gamma - \partial_j \partial^j A^i &\to \hat{\nabla}^i \Gamma - \hat{\nabla} _j \hat{\nabla}^j A^i = \hat{g}^{ij}\hat{\nabla}_j \Gamma - \hat{g}^{jk}\hat{\nabla}_j \hat{\nabla}_k A^i, \\
\partial_t \Gamma = - \partial_i \partial^i \varphi &\to - \hat{\nabla}_i \hat{\nabla}^i \varphi = - \hat{g}^{ij}\hat{\nabla}_i \hat{\nabla}_j \varphi, \\
\partial_t \varphi = -\Gamma,
\end{align}

where $\hat{\nabla}_i$ is the [covariant derivative](https://en.wikipedia.org/wiki/Covariant_derivative) associated with the reference metric $\hat{g}_{ij}$ which describes a flat space background, in an arbitrary coordinate system. Expanding the above equations using the standard covariant derivative definitions, we arrive at

\begin{align}
\partial_t A^i &= -E^i - \hat{g}^{ij}\partial_j \varphi, \\
\partial_t E^i &= \hat{g}^{ij}\partial_j \Gamma - \hat{g}^{jk}\hat{\nabla}_j \hat{\nabla}_k A^i, \\
\partial_t \Gamma &= -\hat{g}^{ij} \left(  \partial_i \partial_j \varphi - \hat{\Gamma}^k_{ji} \partial_k \varphi \right), \\
\partial_t \varphi &= -\Gamma,
\end{align}

and subject to constraints

\begin{align}
\mathcal{G} &\equiv \Gamma - \partial_i A^i - \hat{\Gamma}^i_{ji} A^j &= 0,\\
\mathcal{C} &\equiv \partial_i E^i + \hat{\Gamma}^i_{ji} E^j &= 0,
\end{align}

where $\hat{\Gamma}^i _{jk}$ is the metric connection associated with $\hat{g}_{ij}$.

In the cell below we expand the covariant laplacian within the evolution equation for $E^i$.

Based on the definition of the covariant derivative, we have
$$
\hat{\nabla}_{k} A^{i} = A^i_{,k} + \hat{\Gamma}^i_{mk} A^m
$$

Since $\hat{\nabla}_{k} A^{i}$ is a tensor, the covariant derivative of this will have the same indexing as a tensor $T_k^i$:

$$
\hat{\nabla}_{j} T^i_k = T^i_{k,j} + \hat{\Gamma}^i_{dj} T^d_k - \hat{\Gamma}^d_{kj} T^i_d.
$$

Therefore,
\begin{align}
\hat{\nabla}_{j} \left(\hat{\nabla}_{k} A^{i}\right) &= \left(A^i_{,k} + \hat{\Gamma}^i_{mk} A^m\right)_{,j} + \hat{\Gamma}^i_{dj} \left(A^d_{,k} + \hat{\Gamma}^d_{mk} A^m\right) - \hat{\Gamma}^d_{kj} \left(A^i_{,d} + \hat{\Gamma}^i_{md} A^m\right) \\
&= A^i_{,kj} + \hat{\Gamma}^i_{mk,j} A^m + \hat{\Gamma}^i_{mk} A^m_{,j} + \hat{\Gamma}^i_{dj}A^d_{,k} + \hat{\Gamma}^i_{dj}\hat{\Gamma}^d_{mk} A^m - \hat{\Gamma}^d_{kj} A^i_{,d} - \hat{\Gamma}^d_{kj} \hat{\Gamma}^i_{md} A^m \\
&= {\underbrace {\textstyle A^i_{,kj}}_{\text{Term 1}}} +
{\underbrace {\textstyle \hat{\Gamma}^i_{mk,j} A^m + \hat{\Gamma}^i_{mk} A^m_{,j} + \hat{\Gamma}^i_{dj} A^d_{,k} - \hat{\Gamma}^d_{kj} A^i_{,d}}_{\text{Term 2}}} +
{\underbrace {\textstyle \hat{\Gamma}^i_{dj}\hat{\Gamma}^d_{mk} A^m - \hat{\Gamma}^d_{kj} \hat{\Gamma}^i_{md} A^m}_{\text{Term 3}}},
\end{align}

resulting in

$$
\partial_t E^i = \hat{g}^{ij}\partial_j \Gamma - \hat{g}^{jk} \hat{\nabla}_{j} \left(\hat{\nabla}_{k} A^{i}\right) = \hat{g}^{ij}\partial_j \Gamma - \hat{g}^{jk} \left(\text{Term 1} + \text{Term 2} + \text{Term 3}\right)
$$

## Step 1b: System II in Curvilinear Coordinates, using the rescaled quantities $a^i$ and $e^i$ \[Back to [top](#top)\]
$$\label{step3b}$$

Consider an arbitrary vector $\Lambda^i$, with smooth (continous) Cartesian components $\Lambda^x$, $\Lambda^y$, and $\Lambda^z$. Transforming $\Lambda^i$ to, e.g. spherical coordinates, introduces terms that spoil the smoothness of $\Lambda^i$;

$$
\Lambda^\phi = \frac{1}{r \sin \theta} \times \left[ \text{smooth part} \right].
$$

Evolving $\Lambda^\phi$ will introduce instabilities along the $z$-axis. To avoid this, we instead evolve the _rescaled_ quantity $\lambda^i$, defined by

$$
\bar{\Lambda}^i = \frac{\lambda^i}{\text{scalefactor}[i]}.
$$

where we use the [Hadamard product](https://en.wikipedia.org/w/index.php?title=Hadamard_product_(matrices)&oldid=852272177) of matrices, and no sums are implied by the repeated indices.

Thus, we evolve the smoothed variable $\lambda^i$, via

$$
\lambda^i = \bar{\Lambda}^i \text{scalefactor}[i].
$$

Within Nrpy+, ReU[i] = 1/scalefactor[i], giving

$$
\lambda^i = \frac{\bar{\Lambda}^i}{\text{ReU}[i]}.
$$

We now define the rescaled quantities $a^i$ and $e^i$ and rewrite our formulation of Maxwell's equations in curvilinear coordinates;

\begin{align}
a^i &= \frac{A^i}{\text{ReU}[i]},\\ \\
e^i &= \frac{E^i}{\text{ReU}[i]},
\end{align}

Taking a time derivative on both sides,

\begin{align}
\partial_t a^i &= \frac{\partial_t A^i}{\text{ReU}[i]} = \frac{ -E^i - \hat{g}^{ij}\partial_j \varphi}{\text{ReU}[i]} = -e^i - \frac{\hat{g}^{ij}\partial_j \varphi}{\text{ReU}[i]},\\ \\
\partial_t e^i &= \frac{\partial_t E^i}{\text{ReU}[i]} = \frac{\hat{g}^{ij}\partial_j \Gamma - \hat{g}^{jk} \hat{\nabla}_{j} \left(\hat{\nabla}_{k} A^{i}\right)}{\text{ReU}[i]} = \frac{\hat{g}^{ij}\partial_j \Gamma}{\text{ReU}[i]} - \frac{\hat{g}^{jk} \hat{\nabla}_{j} \left(\hat{\nabla}_{k} a^{i} \text{ReU}[i] \right)}{\text{ReU}[i]},
\end{align}

Given that

$$
\partial_t E^i = {\underbrace {\textstyle \hat{g}^{ij}\partial_j \Gamma}_{\text{Term 1}}} - \hat{\gamma}^{jk} \left({\underbrace {\textstyle A^i_{,kj}}_{\text{Term 2}}} + {\underbrace {\textstyle \hat{\Gamma}^i_{mk,j} A^m + \hat{\Gamma}^i_{mk} A^m_{,j} + \hat{\Gamma}^i_{dj} A^d_{,k} - \hat{\Gamma}^d_{kj} A^i_{,d}}_{\text{Term 3}}} + {\underbrace {\textstyle \hat{\Gamma}^i_{dj}\hat{\Gamma}^d_{mk} A^m - \hat{\Gamma}^d_{kj} \hat{\Gamma}^i_{md} A^m}_{\text{Term 4}}}\right).
$$

we can make the following replacements within the above, in terms of NRPy+ code;

\begin{align}
A^i = \text{AU[i]} &\to \text{aU[i] * rfm.ReU[i]} \\
\partial_j A^i = \text{AUdD[i][j]} &\to \text{aU_dD[i][j] * rfm.ReU[i]} +\text{aU[i] * rfm.ReUdD[i][j]} \\
\partial_k \partial_j A^i = \text{AUdDD[i][j][k]} &\to
\text{aU_dDD[i][j][k] * rfm.ReU[i]} +
\text{aU_dDD[i][j] * rfm.ReUdD[i][k]} \\
&+ \text{aU_dD[i][k] * rfm.ReUdD[i][j]} +
\text{aU[i] * rfm.ReUdDD[i][j][k]}
\end{align}

The remainder of Maxwell's equations are unchanged;

$$
\partial_t \Gamma = -\hat{g}^{ij} \left(  \partial_i \partial_j \varphi - \hat{\Gamma}^k_{ji} \partial_k \varphi \right),
$$

$$
\partial_t \varphi = -\Gamma,
$$

subject to constraints

\begin{align}
\mathcal{G} &\equiv \Gamma - \partial_i A^i - \hat{\Gamma}^i_{ji} A^j &= 0,\\
\mathcal{C} &\equiv \partial_i E^i + \hat{\Gamma}^i_{ji} E^j &= 0.
\end{align}

These equations are implemement in [this tutorial](Tutorial-VacuumMaxwell_Curvilinear_RHSs.ipynb), and evolved in time in [this tutorial](Tutorial-Start_to_Finish-Solving_Maxwells_Equations_in_Vacuum-Curvilinear.ipynb).

# Step 2: Output this notebook to $\LaTeX$-formatted PDF file \[Back to [top](#top)\]
$$\label{latex_pdf_output}$$

The following code cell converts this Jupyter notebook into a proper, clickable $\LaTeX$-formatted PDF file. After the cell is successfully run, the generated PDF may be found in the root NRPy+ tutorial directory, with filename
[Tutorial-VacuumMaxwell_formulation_Curvilinear.pdf](Tutorial-VacuumMaxwell_formulation_Curvilinear.pdf) (Note that clicking on this link may not work; you may need to open the PDF file through another means.)

```python
import cmdline_helper as cmd    # NRPy+: Multi-platform Python command-line interface
cmd.output_Jupyter_notebook_to_LaTeXed_PDF("Tutorial-VacuumMaxwell_formulation_Curvilinear")
```

    Created Tutorial-VacuumMaxwell_formulation_Curvilinear.tex, and compiled
        LaTeX file to PDF file Tutorial-
        VacuumMaxwell_formulation_Curvilinear.pdf


Table of Contents

3  The One-Dimensional Particle in a Box

3.1  🥅 Learning Objectives

3.2  Cyanine Dyes

3.3  Postulate: The squared magnitude of the wavefunction is proportional to probability

3.4  The Wavefunctions of the Particle in a Box (boundary conditions)

3.5  Postulate: The wavefunction of a system is determined by solving the Schrödinger equation

3.6  The Wavefunctions of the Particle in a Box (solution)

3.7  Normalization of Wavefunctions

3.8  Normalization Check

3.9  The Energies of the Particle in a Box

3.10  Zero-Point Energy

3.11  Atomic Units

3.12  Postulate: The wavefunction contains all the physically meaningful information about a system.

3.13  Postulate: Observable Quantities Correspond to Linear, Hermitian Operators.

3.14  Heisenberg Uncertainty Principle

3.15  Double-Checking the Energy of a Particle-in-a-Box

3.16  Visualizing the Particle-in-a-Box Wavefunctions, Probabilities, etc.

3.17  🪞 Self-Reflection

3.18  🤔 Thought-Provoking Questions

3.19  🔁 Recapitulation

3.20  🔮 Next Up...

3.21  📚 References

# The One-Dimensional Particle in a Box

## 🥅 Learning Objectives
- Determine the energies and eigenfunctions of the particle-in-a-box.
- Learn how to normalize a wavefunction.
- Learn how to compute expectation values for quantum-mechanical operators.
- Learn the postulates of quantum mechanics

## Cyanine Dyes

Cyanine dye molecules are often modelled as one-dimension particles in a box. To understand why, start by thinking classically.  You learn in organic chemistry that electrons can more “freely” along alternating double bonds.  If this is true, then you can imagine that the electrons can more from one Nitrogen to the other, almost without resistance.  On the other hand, there are sp3-hybridized functional groups attached to the Nitrogen atom, so once the electron gets to Nitrogen atom, it has to turn around and go back whence it came.   A very, very, very simple model would be to imagine that the electron is totally free between the Nitrogen atoms, and totally forbidden from going much beyond the Nitrogen atoms.  This suggests modeling these systems a potential energy function like:
$$
V(x) =
\begin{cases}
    +\infty & x\leq 0\\
    0       & 0\lt x \lt a\\
    +\infty & a \leq x
\end{cases}
$$
where $a$ is the length of the box. A reasonable approximate formula for $a$ is
$$
a = \left(5.67 + 2.49 (k + 1)\right) \cdot 10^{-10} \text{ m}
$$

## Postulate: The squared magnitude of the wavefunction is proportional to probability
What is the interpretation of the wavefunction? The Born postulate indicates that the squared magnitude of the wavefunction is proportional to the probability of observing the system at that location. E.g., if $\psi(x)$ is the wavefunction for an electron as a function of $x$, then
$$ p(x) = |\psi(x)|^2 $$
is the probability of observing an electron at the point $x$. This is called the Born Postulate.

## The Wavefunctions of the Particle in a Box (boundary conditions)

The nice thing about this “particle in a box” model is that it is easy to solve the time-independent Schrödinger equation in this case.  Because there is no chance that the particle could ever “escape” an infinite box like this (such an electron would have infinite potential energy!), $|\psi(x)|^2$ must equal zero outside the box. Therefore the wavefunction can only be nonzero inside the box. In addition, the wavefunction should be zero at the edges of the box, because otherwise the wavefunction will not be continuous.  So we should have a wavefunction like
$$
\psi(x) =
\begin{cases}
    0& x\le 0\\
    \text{?????}    & 0 < x < a\\
    0 & a \le x
\end{cases}
$$

## Postulate: The wavefunction of a system is determined by solving the Schrödinger equation
How do we find the wavefunction for the particle-in-a-box or, for that matter, any other system? The wavefunction can be determined by solving the time-independent (when the potential is time-independent) or time-dependent (when the potential is time-dependent) Schrödinger equation.

## The Wavefunctions of the Particle in a Box (solution)

To find the wavefunctions for a system, one solves the Schrödinger equation. For a particle of mass $m$ in a one-dimensional box, the (time-independent) Schrödinger equation is:
$$
\left(-\frac{\hbar^2}{2m} \frac{d^2}{dx^2} + V(x) \right)\psi_n(x) = E_n \psi_n(x)
$$
where
$$
V(x) =
\begin{cases}
    +\infty & x\leq 0\\
    0       & 0\lt x \lt a\\
    +\infty & a \leq x
\end{cases}
$$
We already deduced that $\psi(x) = 0$ except when the electron is inside the box ($0 < x  Question: What function(s), when differentiated twice, are proportional to themselves?\

This suggests that the eigenfunctions of the 1-dimensional particle-in-a-box must be some linear combination of sine and cosine functions,
$$
\psi_n(x) = A \sin(cx) + B  \cos(dx)
$$
We know that the wavefunction must be zero at the edges of the box, $\psi(0) = 0$ and $\psi(a) = 0$. These are called the *boundary conditions* for the problem. Examining the first boundary condition,
$$
0 = \psi(0) = A \sin(0) + B \cos(0) = 0 + B
$$
indicates that $B=0$. The second boundary condition
$$
0 = \psi(a) = A \sin(ca)
$$
requires us to recall that $\sin(x) = 0$ whenever $x$ is an integer multiple of $\pi$. So $c=n\pi$ where $n=1,2,3,\ldots$. The wavefunction for the particle in a box is thus,
$$
\psi_n(x) = A_n \sin\left(\tfrac{n \pi x}{a}\right) \qquad \qquad n=1,2,3,\ldots
$$

## Normalization of Wavefunctions
As seen in the previous section, if a wavefunction solves the Schrödinger equation, any constant multiple of the wavefunction also solves the Schrödinger equation,
$$
\hat{H} \psi(x) = E \psi(x) \quad \longleftrightarrow \quad \hat{H} \left(A\psi(x)\right) = E \left(A\psi(x)\right)
$$
Owing to the Born postulate, the complex square of the wavefunction can be interpreted as probability. Since the probability of a particle being at *some* point in space is one, we can define the normalization constant, $A$, for the wavefunction through the requirement that:
$$
\int_{-\infty}^{\infty} \left|\psi(x)\right|^2 dx = 1.
$$
In the case of a particle in a box, this is:
\begin{align}
1 &= \int_{-\infty}^{\infty} \left|\psi_n(x)\right|^2 dx  \\
&= \int_0^a \psi_n(x) \psi_n^*(x) dx \\
&= \int_0^a A_n \sin\left(\tfrac{n \pi x}{a}\right) \left(A_n \sin\left(\tfrac{n \pi x}{a}\right) \right)^* dx \\
&= \left|A_n\right|^2\int_0^a \sin^2\left(\tfrac{n \pi x}{a}\right) dx
\end{align}
To evaluate this integral, it is useful to remember some [trigonometric identities](https://en.wikipedia.org/wiki/List_of_trigonometric_identities). (You can learn more about how I remember trigonometric identities [here](https://github.com/PaulWAyers/IntroQChem/blob/main/linkedFiles/TrigIdentities.md).) The specific identity we need here is $\sin^2 x = \tfrac{1}{2}(1-\cos 2x)$:

\begin{align}
1 &= \left|A_n\right|^2\int_0^a \sin^2\left(\tfrac{n \pi x}{a}\right) \,dx \\
 &= \left|A_n\right|^2\int_0^a \tfrac{1}{2}\left(1-\cos \left(\tfrac{2n \pi x}{a}\right)\right) \,dx \\
 &=\tfrac{\left|A_n\right|^2}{2} \left( \int_0^a 1 \,dx - \int_0^a \cos \left(\tfrac{2n \pi x}{a}\right)\,dx  \right) \\
 &=\tfrac{\left|A_n\right|^2}{2} \left( \left[ x \right]_0^a - \left[\frac{-a}{2 n \pi}\sin \left(\tfrac{2n \pi x}{a}\right) \right]_0^a  \right) \\
 &=\tfrac{\left|A_n\right|^2}{2} \left( a - 0  \right)
\end{align}
So
$$
 \left|A_n\right|^2 = \tfrac{2}{a}
$$
Note that this does not completely determine $A_n$. For example, any of the following normalization constants are allowed,
$$
A_n = \sqrt{\tfrac{2}{a}}
= - \sqrt{\tfrac{2}{a}}
= i \sqrt{\tfrac{2}{a}}
= -i \sqrt{\tfrac{2}{a}}
$$
In general, any [square root of unity](https://en.wikipedia.org/wiki/Root_of_unity) can be used,
$$
A_n = \left( k \pm i \sqrt{1-k^2} \right) \sqrt{\tfrac{2}{a}}
$$
where $k$ is any real number. The arbitrariness of the *phase* of the wavefunction is an important feature. Because the wavefunction can be imaginary (e.g., if you choose $A_n = i \sqrt{\tfrac{2}{a}}$), it is obvious that the wavefunction is not an observable property of a system. **The wavefunction is only a mathematical tool for quantum mechanics; it is not a physical object.**

Summarizing, the (normalized) wavefunction for a particle with mass $m$ confined to a one-dimensional box with length $a$ can be written as:
$$
\psi_n(x) = \sqrt{\tfrac{2}{a}} \sin\left(\tfrac{n \pi x}{a}\right) \qquad \qquad n=1,2,3,\ldots
$$

Note that in this case, the normalization condition is the same for all $n$; that is an unusual property of the particle-in-a-box wavefunction.

While this normalization convention is used 99% of the time, there are some cases where it is more convenient to make a different choice for the amplitude of the wavefunctions. I say this to remind you that normalization the wavefunction is something we do for convenience; it is not required by physics!

## Normalization Check
One advantage of using Jupyter is that we can easily check our (symbolic) mathematics. Let's confirm that the wavefunction is normalized by evaluating,
$$
\int_0^a \left| \psi_n(x) \right|^2 \, dx
$$

```python
# Execute this code block to import required objects.

# Note: The numpy library from autograd is imported, which behaves the same as
#       importing numpy directly. However, to make automatic differentiation work,
#       you should NOT import numpy directly by `import numpy as np`.

import autograd.numpy as np
from autograd import elementwise_grad as egrad

# import numpy as np
from scipy.integrate import trapezoid, quad
from scipy import constants

import ipywidgets as widgets

import matplotlib.pyplot as plt

# set the size of the plot
# plt.rcParams['figure.figsize'] = [10, 5]
```

```python
#The next few lines just set up the sliders for setting parameters.
#Principle quantum number slider:
n = widgets.IntSlider(
    value=1,
    min=1,
    max=10,
    step=1,
    description='n (quant. #):',
    disabled=False,
    continuous_update=True,
    orientation='horizontal',
    readout=True,
    readout_format='d')

#Box length slider:
a = widgets.FloatSlider(
    value=1,
    min=.01,
    max=10.0,
    step=0.01,
    description='a (length):',
    disabled=False,
    continuous_update=True,
    orientation='horizontal',
    readout=True,
    readout_format='.2f',
)

# Define a function for the wavefunction
def compute_wavefunction(x, n, a):
    """Compute 1-dimensional particle-in-a-box wave-function value(s).

    Parameters
    ----------
    x: float or np.ndarray
        Position of the particle.
    n: int
        Quantum number value.
    a: float
        Length of the box.
    """
    # check argument n
    if not (isinstance(n, int) and n > 0):
        raise ValueError("Argument n should be a positive integer.")
    # check argument a
    if a <= 0.0:
        raise ValueError("Argument a should be positive.")
    # check argument x
    if not (isinstance(x, float) or hasattr(x, "__iter__")):
        raise ValueError("Argument x should be a float or an array!")

    # compute wave-function
    value = np.sqrt(2 / a) * np.sin(n * np.pi * x / a)

    # set wave-function values out of the box equal to zero
    if hasattr(x, "__iter__"):
        value[x > a] = 0.0
        value[x < 0] = 0.0
    else:
        if x < 0.0 or x > a:
            value = 0.0
    return value

# Define a function for the wavefunction squared
def compute_probability(x, n, a):
    """Compute 1-dimensional particle-in-a-box probablity value(s).

    See `compute_wavefunction` parameters.
    """
    return compute_wavefunction(x, n, a)**2

#This next bit of code just prints out the normalization error
def check_normalization(a, n):
    #check the computed values of the moments against the analytic formula
    normalization,error = quad(compute_probability, 0, a, args=(n, a))
    print("Normalization of wavefunction = ", normalization)

out = widgets.interactive_output(check_normalization, {'a': a, 'n': n})

widgets.HBox([widgets.VBox([a, n]),out])
```

    HBox(children=(VBox(children=(FloatSlider(value=1.0, description='a (length):', max=10.0, min=0.01, step=0.01)…

## The Energies of the Particle in a Box
How do we compute the energy of a particle in a box? All we need to do is substitute the eigenfunctions of the Hamiltonian, $\psi_n(x)$ back into the Schrödinger equation to determine the eigenenergies, $E_n$. That is, from
$$
\hat{H} \psi_n(x) = E_n \psi_n(x)
$$
we deduce
\begin{align}
-\frac{\hbar^2}{2m} \frac{d^2}{dx^2} \left( A_n \sin \left( \frac{n \pi x}{a}\right) \right)
 &= E_n  \left( A_n \sin \left( \frac{n \pi x}{a}\right) \right) \\
 -A_n \frac{\hbar^2}{2m} \frac{d}{dx} \left( \frac{n \pi}{a} \cos \left( \frac{n \pi x}{a}\right) \right)
 &= E_n  \left( A_n \sin \left( \frac{n \pi x}{a}\right) \right) \\
  A_n \frac{\hbar^2}{2m} \left( \frac{n \pi}{a} \right)^2 \sin \left( \frac{n \pi x}{a}\right)
 &= E_n  \left( A_n \sin \left( \frac{n \pi x}{a}\right) \right) \\
   \frac{\hbar^2 n^2 \pi^2}{2ma^2}
 &= E_n
\end{align}
Using the definition of $\hbar$, we can rearrange this to:
\begin{align}
E_n &= \frac{\hbar^2 n^2 \pi^2}{2ma^2} \qquad \qquad n=1,2,3,\ldots\\
&= \frac{h^2 n^2}{8ma^2}
\end{align}
Notice that only certain energies are allowed. This is a fundamental principle of quantum mechanics, and it is related to the "waviness" of particles. Certain "frequencies" are resonant, and other "frequencies" cannot be observed. *The **only** energies that can be observed for a particle-in-a-box are the ones given by the above formula.*

## Zero-Point Energy
Naïvely, you might expect that the lowest-energy state of a particle in a box has zero energy. (The potential in the box is zero, after all, so shouldn't the lowest-energy state be the state with zero kinetic energy? And if the kinetic energy were zero and the potential energy were zero, then the total energy would be zero.)

But this doesn't happen. It turns out that you can never "stop" a quantum particle; it always has a zero-point motion, typically a resonant oscillation about the lowest-potential-energy location(s). Indeed, the more you try to confine a particle to stop it, the bigger its kinetic energy becomes. This is clear in the particle-in-a-box, which has only kinetic energy. There the (kinetic) energy increases rapidly, as $a^{-2}$, as the box becomes smaller:
$$
T_n = E_n = \frac{h^2n^2}{8ma^2}
$$
The residual energy in the electronic ground state is called the **zero-point energy**,
$$
E_{\text{zero-point energy}} = \frac{h^2}{8ma^2}
$$
The existence of the zero-point energy, and the fact that zero-point kinetic energy is always positive, is a general feature of quantum mechanics.
> **Zero-Point Energy Principle:** Let $V(x)$ be a nonnegative potential. The ground-state energy is always greater than zero.

More generally, for any potential that is bound from below,
$$
V_{\text{min}}= \min_x V(x)
$$
the ground-state energy of the system satisfies $E_{\text{zero-point energy}} > V_{\text{min}}$.
>Nuance: There is a tiny mathematical footnote here; there are some $V(x)$ for which there are *no* bound states. In such cases, e.g., $V(x) = \text{constant}$, it is possible for $E = V_{\text{min}}$.)

## Atomic Units
Because Planck's constant and the electron mass are tiny numbers, it is often useful to use [atomic units](https://en.wikipedia.org/wiki/Hartree_atomic_units) when performing calculations. We'll learn more about atomic units later but, for now, we only need to know that, in atomic units, $\hbar$, the mass of the electron, $m_e$, the charge of the electron, $e$, and the average (mean) distance of an electron from the nucleus in the Hydrogen atom, $a_0$, are all defined to be equal to 1.0 in atomic units.
\begin{align}
\hbar &= \frac{h}{2 \pi} = 1.0545718 \times 10^{-34} \text{ J s}= 1 \text{ a.u.} \\
m_e &= 9.10938356 \times 10^{-31} \text{ kg} = 1 \text{ a.u.} \\
e &= 1.602176634 \times 10^-19 \text{ C} = 1  \text{ a.u.} \\
a_0 &= 	5.291772109 \times 10^{-11} \text{ m} = 1 \text{ Bohr} = 1 \text{ a.u.}
\end{align}
The unit of energy in atomic units is called the Hartree,
$$
E_h =4.359744722 \times 10^{-18} \text{ J} = 1 \text{ Hartree} = 1 \text{ a.u.}
$$
and the ground-state (zero-point) energy of the Hydrogen atom is $-\tfrac{1}{2} E_h$.

We can now define functions for the eigenenergies of the 1-dimensional particle in a box:

```python
#The next few lines just set up the sliders for setting parameters.
#Principle quantum number slider:
n = widgets.IntSlider(
    value=1,
    min=1,
    max=10,
    step=1,
    description='n (quant. #):',
    disabled=False,
    continuous_update=True,
    orientation='horizontal',
    readout=True,
    readout_format='d')

#Box length slider:
a = widgets.FloatSlider(
    value=1,
    min=.01,
    max=10.0,
    step=0.01,
    description='a (length):',
    disabled=False,
    continuous_update=True,
    orientation='horizontal',
    readout=True,
    readout_format='.2f',
)

# Define a function for the energy of a particle in a box
# with length a and quantum number n [in atomic units!]
# The length is input in Bohr (atomic units)
def compute_energy(n, a):
    "Compute 1-dimensional particle-in-a-box energy."
    return n**2 * np.pi**2 / (2 * a**2)

# Define a function for the energy of an electron in a box
# with length a and quantum number n [in SI units!].
# The length is input in meters.
def compute_energy_si(n, a):
    "Compute 1-dimensional particle-in-a-box energy."
    return n**2 * constants.h**2 / (8 * constants.m_e* a**2)

#Define variable for atomic unit of length in meters
a0 = constants.value('atomic unit of length')

#This next bit of code just prints out the energy in atomic and SI units
def print_energy(a, n):
    print(f'The energy of an electron in a box of length {a:.2f} a.u. with '
       f'quantum number {n} is {compute_energy(n, a):.2f} a.u..')
    print(f'The energy of an electron in a box of length {a*a0:.2e} m with '
       f'quantum number {n} is {compute_energy_si(n, a*a0):.2e} Joules.')

out = widgets.interactive_output(print_energy, {'a': a, 'n': n})

widgets.VBox([widgets.VBox([a, n]),out])
```

    VBox(children=(VBox(children=(FloatSlider(value=1.0, description='a (length):', max=10.0, min=0.01, step=0.01)…

## Postulate: The wavefunction contains all the physically meaningful information about a system.
While the wavefunction is not itself observable, all observable properties of a system can be determined from the wavefunction. However, just because the wavefunction encapsulates all the *observable* properties of a system does not mean that it contains *all information* about a system. In quantum mechanics, some things are not observable. Consider that for the ground ($n=1$) state of the particle in a box, the root-mean-square average momentum,
$$
\bar{p}_{rms} = \sqrt{2m \cdot T} = \sqrt{(2m)\frac{h^2n^2}{8ma^2}} = \frac{hn}{2a}
$$
increases as you squeeze the box. That is, the more you try to constrain the particle in space, the faster it moves. You can't "stop" the particle no matter how hard you squeeze it, so it's impossible to exactly know where the particle is located. You can only determine its *average* position.

## Postulate: Observable Quantities Correspond to Linear, Hermitian Operators.
The *correspondence* principle says that for every classical observable there is a linear, Hermitian, operator that allows computation of the quantum-mechanical observable. An operator, $\hat{C}$ is linear if for any complex numbers $a$ and $b$, and any wavefunctions $\psi_1(x)$ and $\psi_2(x)$,
$$
\hat{C} \left(a \psi_1(x,t) + b \psi_2(x,t) \right) = a \hat{C} \psi_1(x,t) + b \hat{C} \psi_2(x,t)
$$
Similarly, an operator is Hermitian if it satisfies the relation,
$$
\int \psi_1^*(x,t) \hat{C} \psi_2(x,t) \, dx = \int \left( \hat{C}\psi_1(x,t) \right)^*  \psi_2(x,t) \, dx
$$
or, equivalently,
$$
\int \psi_1^*(x,t) \left( \hat{C} \psi_2(x,t) \right) \, dx = \int \psi_2(x,t)\left( \hat{C} \psi_1(x,t) \right)^* \, dx
$$
That is for a linear operator, the linear operator applied to a sum of wavefunctions is equal to the sum of the linear operators directly applied to the wavefunctions separately, and the linear operator applied to a constant times a wavefunction is the constant times the linear operator applied directly to the wavefunction. A Hermitian operator can apply forward (towards $\psi_2(x,t)$) or backwards (towards $\psi_1(x,t)$). This is very useful, because sometimes it is much easier to apply an operator in one direction.

We've already been introduced to the quantum-mechanical operators for the momentum,
$$
\hat{p} = -i \hbar \tfrac{d}{dx}
$$
and the kinetic energy,
$$
\hat{T} = -\tfrac{\hbar^2}{2m} \tfrac{d^2}{dx^2}
$$
These operators are linear because the derivative of a sum is the sum of the derivatives, and the derivative of a constant times a function is that constant times the derivative of the function. These operators are also Hermitian. For example, to show that the momentum operator is Hermitian:

\begin{align}
\int_{-\infty}^{\infty} \psi_1^*(x,t) \hat{p} \psi_2(x,t) dx &= \int_{-\infty}^{\infty} \psi_1^*(x,t) \left( -i \hbar \tfrac{d}{dx} \right) \psi_2(x,t) dx \\
&= -i \hbar \int_{-\infty}^{\infty} \tfrac{d}{dx} \left(\psi_1^*(x,t)\psi_2(x,t)  \right) - \left(
 \psi_2(x,t) \tfrac{d}{dx} \psi_1^*(x,t)\right)  dx
\end{align}
Here we used the product rule for derivatives, $f(x)\tfrac{dg}{dx} = \tfrac{d f(x) g(x)}{dx} - g(x) \tfrac{df}{dx}$. Using the fundamental theorem of calculus and the fact the probability of observing a particle at $\pm \infty$ is zero, and therefore the wavefunctions at infinity are also zero, one knows that
$$
\int_{-\infty}^{\infty} \tfrac{d}{dx} \left(\psi_1^*(x,t)\psi_2(x,t)  \right) = \left[  \psi_1^*(x,t)\psi_2(x,t)\right]_{-\infty}^{\infty} = 0
$$
Therefore the above equation can be simplified to
\begin{align}
\int_{-\infty}^{\infty} \psi_1^*(x,t) \hat{p} \psi_2(x,t) dx &=i \hbar \int_{-\infty}^{\infty}   \psi_2(x,t) \tfrac{d}{dx} \psi_1^*(x,t)  dx \\
&= \int_{-\infty}^{\infty}   \psi_2(x,t) i \hbar  \tfrac{d}{dx} \psi_1^*(x,t) dx \\
&= \int_{-\infty}^{\infty}  \psi_2(x,t) \left( -i \hbar  \tfrac{d}{dx} \psi_1^*(x,t)\right)  dx \\
&= \int_{-\infty}^{\infty}  \psi_2(x,t) \left( \hat{p} \psi_1(x,t)\right)^*  dx
\end{align}

The expectation value of the momentum of a particle-in-a-box is always zero. This is intuitive, since electrons (on average) are neither moving to the right nor to the left inside the box: if they were, then the box itself would need to be moving. Indeed, for any real wavefunction, the average momentum is always zero. This follows directly from the previous derivation with $\psi_1^*(x,t) = \psi_2(x,t)$. Thus:
\begin{align}
\int_{-\infty}^{\infty} \psi_2(x,t) \hat{p} \psi_2(x,t) dx
&=i \hbar \int_{-\infty}^{\infty}   \psi_2(x,t) \tfrac{d}{dx} \psi_2(x,t)  dx \\
&=-i \hbar \int_{-\infty}^{\infty}  \psi_2(x,t) \left( \tfrac{d}{dx} \psi_2(x,t)\right)  dx \\
&= 0
\end{align}
The last line follows because the only number that is equal to its negative is zero. (That is, $x=-x$ if and only if $x=0$.) It is a subtle feature that the eigenfunctions of a real-valued Hamiltonian operator can always be chosen to be real-valued themselves, so their average momentum is clearly zero. We often denote quantum-mechanical expectation values with the shorthand,
$$
\langle \hat{p} \rangle =0
$$

The momentum-squared of the particle-in-a-box is easily computed from the kinetic energy,
$$
\langle \hat{p}^2 \rangle = \int_0^a \psi_n(x) \hat{p}^2 \psi_n(x) dx = \int_0^a \psi_n(x) \left(2m\hat{T}\right) \psi_n(x) dx = 2m E_n = \frac{h^2n^2}{4a^2}
$$

Intuitively, since the box is symmetric about $x=\tfrac{a}{2}$, the particle has equal probability of being in the first-half and the second-half of the box. So the average position is expected to be
$$ \langle x \rangle =\tfrac{a}{2}$$

We can confirm this by explicit integration,

\begin{align}
\langle x \rangle &= \int_0^a \psi_n^*(x)\, x \,\psi_n(x) dx \\
&= \int_0^a \left(\sqrt{\tfrac{2}{a}} \sin\left(\tfrac{n \pi x}{a} \right)\right) x \left(\sqrt{\tfrac{2}{a}}\sin\left(\tfrac{n \pi x}{a} \right)\right) dx \\
&= \tfrac{2}{a} \int_0^a  x \sin^2\left(\tfrac{n \pi x}{a} \right) dx \\
&= \tfrac{2}{a} \left[ \tfrac{x^2}{4} - x \tfrac{\sin \tfrac{2n \pi x}{a}}{\tfrac{4 n \pi}{a}}
- \tfrac{\cos \tfrac{2n \pi x}{a}}{\tfrac{8 n^2 \pi^2}{a^2}}
 \right]_0^a \\
 &= \tfrac{2}{a} \left[ \tfrac{a^2}{4} - 0 - 0 \right] \\
 &= \tfrac{a}{2}
\end{align}

Similarly, we expect that the expectation value of $\langle x^2 \rangle$ will be proportional to $a^2$. We can confirm this by explicit integration,
\begin{align}
\langle x^2 \rangle &= \int_0^a \psi_n^*(x)\, x^2 \,\psi_n(x) dx \\
&= \int_0^a \left(\sqrt{\tfrac{2}{a}} \sin\left(\tfrac{n \pi x}{a} \right)\right) x^2 \left(\sqrt{\tfrac{2}{a}}\sin\left(\tfrac{n \pi x}{a} \right)\right) dx \\
&= \tfrac{2}{a} \int_0^a  x^2 \sin^2\left(\tfrac{n \pi x}{a} \right) dx \\
&= \tfrac{2}{a} \left[ \tfrac{x^3}{6}
 - x^2 \tfrac{\sin \tfrac{2n \pi x}{a}}{\tfrac{4 n \pi}{a}}
 - x \tfrac{\cos \tfrac{2n \pi x}{a}}{\tfrac{4 n^2 \pi^2}{a^2}}
 - \tfrac{\sin \tfrac{2n \pi x}{a}}{\tfrac{8 n^3 \pi^3}{a^3}}
 \right]_0^a \\
 &= \tfrac{2}{a} \left[ \tfrac{a^3}{6} - 0 - \tfrac{a}{{\tfrac{4 n^2 \pi^2}{a^2}}} - 0 \right] \\
  &= \tfrac{2}{a} \left[ \tfrac{a^3}{6} - \tfrac{a^3}{4 n^2 \pi^2} \right] \\
  &= a^2\left[ \tfrac{1}{3} - \tfrac{1}{2 n^2 \pi^2} \right]
\end{align}

We can verify these formulas by explicit integration.

```python
#The next few lines just set up the sliders for setting parameters.
#Principle quantum number slider:
n = widgets.IntSlider(
    value=1,
    min=1,
    max=10,
    step=1,
    description='n (quant. #):',
    disabled=False,
    continuous_update=True,
    orientation='horizontal',
    readout=True,
    readout_format='d')

#Box length slider:
a = widgets.FloatSlider(
    value=1,
    min=.01,
    max=10.0,
    step=0.01,
    description='a (length):',
    disabled=False,
    continuous_update=True,
    orientation='horizontal',
    readout=True,
    readout_format='.2f',
)

#Compute <x^power>, the expectation value of x^power
def compute_moment(x, n, a, power):
    """Compute the x^power moment of the 1-dimensional particle-in-a-box.

    See `compute_wavefunction` parameters.
    """
    return compute_probability(x, n, a)*x**power

#This next bit of code just prints out the values.
def check_moments(a, n):
    #check the computed values of the moments against the analytic formula
    avg_r,error = quad(compute_moment, 0, a, args=(n, a, 1))
    avg_r2,error = quad(compute_moment, 0, a, args=(n, a, 2))
    print(f"<r> computed = {avg_r:.5f}")
    print(f"<r> analytic = {a/2:.5f}")
    print(f"<r^2> computed = {avg_r2:.5f}")
    print(f"<r^2> analytic = {a**2*(1/3 - 1./(2*n**2*np.pi**2)):.5f}")

out = widgets.interactive_output(check_moments, {'a': a, 'n': n})

widgets.HBox([widgets.VBox([a, n]),out])
```

    HBox(children=(VBox(children=(FloatSlider(value=1.0, description='a (length):', max=10.0, min=0.01, step=0.01)…

## Heisenberg Uncertainty Principle
The previous example gives a first example of the more general [Heisenberg Uncertainty Principle](https://en.wikipedia.org/wiki/Uncertainty_principle). One specific manifestation of the Heisenberg Uncertaity Principle is that the variance of the position, $\sigma_x^2 = \langle x^2 \rangle - \langle x \rangle^2$, times the variance of the momentum. $\sigma_p^2 = \langle \hat{p}^2 \rangle - \langle \hat{p} \rangle^2$ is greater than $\tfrac{\hbar^2}{4}$. We can verify this formula for the particle in a box.
\begin{align}
\tfrac{\hbar^2}{4} &\le \sigma_x^2 \sigma_p^2 \\
&= \left( a^2\left[ \tfrac{1}{3} - \tfrac{1}{2 n^2 \pi^2} \right] - \tfrac{a^2}{4} \right)
 \left( \frac{h^2n^2}{4a^2} - 0 \right) \\
&= \left( a^2\left[ \tfrac{1}{3} - \tfrac{1}{2 n^2 \pi^2} \right] - \tfrac{a^2}{4} \right)
 \left( \frac{\hbar^2 \pi^2 n^2}{a^2} \right) \\
&= \hbar^2 \pi^2 n^2 \left(\tfrac{1}{12} - \tfrac{1}{2 n^2 \pi^2} \right)
\end{align}
The right-hand-side gets larger and larger as $n$ increases, so the largest value occurs where:
\begin{align}
\tfrac{\hbar^2}{4} &\le \hbar^2 \pi^2 \left(\tfrac{1}{12} - \tfrac{1}{2 \pi^2} \right) \\
&= 0.32247 \hbar^2
\end{align}

## Double-Checking the Energy of a Particle-in-a-Box
To check the energy of the particle in a box, we can compute the kinetic energy density, then integrate it over all space. That is, we define:
$$
\tau_n(x) = \psi_n^*(x) \left(-\tfrac{\hbar^2}{2m}\tfrac{d^2}{dx^2}\right) \psi_n(x)
$$
and then the kinetic energy (which is the energy for the particle in a box) is
$$
T_n = \int_0^a \tau_n(x) dx
$$

> *Note:* In fact there are several different equivalent definitions of the kinetic energy density, but this is not very important in an introductory quantum chemistry course. All of the kinetic energy densities give the same total kinetic energy. However, because the kinetic energy density, $\tau(x)$, represents the kinetic energy of a particle at the point $x$, and it is impossible to know the momentum (or the momentum-squared, ergo the kinetic energy) exactly at a given point in space according to the Heisenberg uncertainty principle), there can be no unique definition for $\tau(x)$.

```python
#The next few lines just set up the sliders for setting parameters.
#Principle quantum number slider:
n = widgets.IntSlider(
    value=1,
    min=1,
    max=10,
    step=1,
    description='n (quant. #):',
    disabled=False,
    continuous_update=True,
    orientation='horizontal',
    readout=True,
    readout_format='d')

#Box length slider:
a = widgets.FloatSlider(
    value=1,
    min=.01,
    max=10.0,
    step=0.01,
    description='a (length):',
    disabled=False,
    continuous_update=True,
    orientation='horizontal',
    readout=True,
    readout_format='.2f',
)

#The next few lines just set up the sliders for setting parameters.
#Principle quantum number slider:

def compute_wavefunction_derivative(x, n, a, order=1):
    """Compute 1-dimensional particle-in-a-box kinetic energy density.
    """
    if not (isinstance(order, int) and n > 0):
        raise ValueError("Argument order is expected to be a positive integer!")

    def wavefunction(x):
        v = np.sqrt(2 / a) * np.sin(n * np.pi * x / a)
        return v

    # compute derivative
    deriv = egrad(wavefunction)
    for _ in range(order - 1):
        deriv = egrad(deriv)

    # return zero for x values out of the box
    deriv = deriv(x)
#     deriv[x < 0] = 0.0
#     deriv[x > a] = 0.0

    if hasattr(x, "__iter__"):
        deriv[x > a] = 0.0
        deriv[x < 0] = 0.0
    else:
        if x < 0.0 or x > a:
            deriv = 0.0

    return deriv

def compute_kinetic_energy_density(x, n, a):
    """Compute 1-dimensional particle-in-a-box kinetic energy density.

    See `compute_wavefunction` parameters.
    """
    # evaluate wave-function and its 2nd derivative w.r.t. x
    wf = compute_wavefunction(x, n, a)
    d2 = compute_wavefunction_derivative(x, n, a, order=2)
    return -0.5 * wf * d2

#This next bit of code just prints out the values.
def check_energy(a, n):
    #check the computed values of the moments against the analytic formula
    ke,error = quad(compute_kinetic_energy_density, 0, a, args=(n, a))
    energy = compute_energy(n, a)
    print(f"The energy computed by integrating the k.e. density is {ke:.5f}")
    print(f"The energy computed directly is {energy:.5f}")

out = widgets.interactive_output(check_energy, {'a': a, 'n': n})

widgets.VBox([widgets.VBox([a, n]),out])
```

    VBox(children=(VBox(children=(FloatSlider(value=1.0, description='a (length):', max=10.0, min=0.01, step=0.01)…

## Visualizing the Particle-in-a-Box Wavefunctions, Probabilities, etc.
In the next code block, the wavefunction, probability density, derivative, second-derivative, and kinetic energy density for the particle-in-a-box are shown. Notice that the kinetic-energy-density is proportional to the probability density, and that the first and second derivatives are not zero at the edge of the box, but the wavefunction and probability density are. It's useful to change the parameters in the below figures to build your intuition for the particle-in-a-box.

```python
#The next few lines just set up the sliders for setting parameters.
#Principle quantum number slider:
n = widgets.IntSlider(
    value=1,
    min=1,
    max=10,
    step=1,
    description='n (quant. #):',
    disabled=False,
    continuous_update=True,
    orientation='horizontal',
    readout=True,
    readout_format='d')

#Box length slider:
a = widgets.FloatSlider(
    value=1,
    min=.01,
    max=10.0,
    step=0.01,
    description='a (length):',
    disabled=False,
    continuous_update=True,
    orientation='horizontal',
    readout=True,
    readout_format='.2f',
)

#This next bit of code makes the plots and prints out the energy
def make_plots(a, n):
    #check the computed values of the moments against the analytic formula
    energy = compute_energy(n, a)
    print(f"The energy computed directly is {energy:.5f}")
    # sample x coordinates
    x = np.arange(-0.6, a + 0.6, 0.01)

    # evaluate wave-function & probability
    wf = compute_wavefunction(x, n, a)
    pr = compute_probability(x, n, a)

    # evaluate 1st & 2nd derivative of wavefunction w.r.t. x
    d1 = compute_wavefunction_derivative(x, n, a, order=1)
    d2 = compute_wavefunction_derivative(x, n, a, order=2)

    # evaluate kinetic energy density
    kin = compute_kinetic_energy_density(x, n, a)
    #print("Integrate KED = ", trapezoid(kin, x))

    # set the size of the plot
    plt.rcParams['figure.figsize'] = [15, 10]
    plt.rcParams['font.family'] = 'serif'
    plt.rcParams['font.serif'] = ['Times New Roman']
    plt.rcParams['mathtext.fontset'] = 'stix'
    plt.rcParams['xtick.labelsize'] = 15
    plt.rcParams['ytick.labelsize'] = 15

    # define four subplots
    fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2)
    fig.suptitle(f'a={a}  n={n}  E={compute_energy(n, a):.4f} a.u.', fontsize=35, fontweight='bold')

    # plot 1
    ax1.plot(x, wf, linestyle='--', label=r'$\psi(x)$', linewidth=3)
    ax1.plot(x, pr, linestyle='-', label=r'$\|\psi(x)\|^2$', linewidth=3)
    ax1.legend(frameon=False, fontsize=20)
    ax1.set_xlabel('x coordinates', fontsize=20)

    # plot 2
    ax2.plot(x, d1, linewidth=3, c='k')
    ax2.set_xlabel('x coordinates', fontsize=20)
    ax2.set_ylabel(r'$\frac{d\psi(x)}{dx}$', fontsize=30, rotation=0, labelpad=25)

    # plot 3
    ax3.plot(x, d2, linewidth=3, c='g', )
    ax3.set_xlabel('x coordinates', fontsize=20)
    ax3.set_ylabel(r'$\frac{d^2\psi(x)}{dx^2}$', fontsize=25, rotation=0, labelpad=25)

    # plot 4
    ax4.plot(x, kin, linewidth=3, c='r')
    ax4.set_xlabel('x coordinates', fontsize=20)
    ax4.set_ylabel('Kinetic Energy Density', fontsize=16)

    # adjust spacing between plots
    plt.subplots_adjust(left=0.125,
                    bottom=0.1,
                    right=0.9,
                    top=0.9,
                    wspace=0.35,
                    hspace=0.35)
    #Show Plot
    plt.show()

out = widgets.interactive_output(make_plots, {'a': a, 'n': n})

widgets.VBox([widgets.VBox([a, n]),out])

```

    VBox(children=(VBox(children=(FloatSlider(value=1.0, description='a (length):', max=10.0, min=0.01, step=0.01)…

## 🪞 Self-Reflection
- Can you think of other physical or chemical systems where the particle-in-a-box Hamiltonian would be appropriate?
- Can you think of another property density, besides the kinetic energy density, that cannot be uniquely defined in quantum mechanics?

## 🤔 Thought-Provoking Questions
- How would the wavefunction and ground-state energy change if you made a small change in the particle-in a box Hamiltonian, so that the right-hand-side of the box was a little higher than the left-hand-side?
- Any system with a zero-point energy of zero is classical. Why?
- How would you compute the probability of observing an electron at the center of a box if the box contained 2 electrons? If it contained 4 electrons? If it contained 8 electrons? The probability of observing an electron at the center of a box with 3 electrons is sometimes *lower* than the probability of observing an electron at the center of a box with 2 electrons. Why?
- Demonstrate that the kinetic energy operator is linear and Hermitian.

## 🔁 Recapitulation
- Write the Hamiltonian, time-independent Schrödinger equation, eigenfunctions, and eigenvalues for the one-dimensional particle in a box.
- Play around with the eigenfunctions and eigenenergies of the particle-in-a-box to build intuition for them.
- How would you compute the uncertainty in $x^4$?
- Practice your calculus by explicitly computing, using integration by parts, $\langle x \rangle$ and $\langle x^2 \rangle$.

## 🔮 Next Up...
- Postulates of Quantum Mechanics
- Multielectron particle-in-a-box.
- Multidimensional particle-in-a-box.
- Harmonic Oscillator

## 📚 References
My favorite sources for this material are:
- [Randy's book](https://github.com/PaulWAyers/IntroQChem/blob/main/documents/DumontBook.pdf?raw=true) has an excellent treatment of the particle-in-a-box model, including several extensions to the material covered here. (Chapter 3)
- Also see my (pdf) class [notes]
(https://github.com/PaulWAyers/IntroQChem/blob/main/documents/PinBox.pdf?raw=true).
- Also see my notes on the [mathematical structure of quantum mechanics](https://github.com/PaulWAyers/IntroQChem/blob/main/documents/LinAlgAnalogy.pdf?raw=true).
- D. A. MacQuarrie, Quantum Chemistry (University Science Books, Mill Valley California, 1983)
- [An excellent explanation of the link to the spectrum of cyanine dyes](https://pubs.acs.org/doi/10.1021/ed084p1840)
- Chemistry Libre Text: [one dimensional](https://chem.libretexts.org/Bookshelves/Physical_and_Theoretical_Chemistry_Textbook_Maps/Map%3A_Physical_Chemistry_for_the_Biosciences_(Chang)/11%3A_Quantum_Mechanics_and_Atomic_Structure/11.08%3A_Particle_in_a_One-Dimensional_Box)
and [multi-dimensional](https://chem.libretexts.org/Bookshelves/Physical_and_Theoretical_Chemistry_Textbook_Maps/Supplemental_Modules_(Physical_and_Theoretical_Chemistry)/Quantum_Mechanics/05.5%3A_Particle_in_Boxes/Particle_in_a_3-Dimensional_box)
- [McQuarrie and Simon summary](https://chem.libretexts.org/Bookshelves/Physical_and_Theoretical_Chemistry_Textbook_Maps/Map%3A_Physical_Chemistry_(McQuarrie_and_Simon)/03%3A_The_Schrodinger_Equation_and_a_Particle_in_a_Box)
- [Mathematical Features of Quantum Mechanics]() (my notes).

There are also some excellent wikipedia articles:
- [Particle in a Box](https://en.wikipedia.org/wiki/Particle_in_a_box)
- [Particle on a Ring](https://en.wikipedia.org/wiki/Particle_in_a_ring)
- [Postulates of Quantum Mechanics](https://en.wikipedia.org/wiki/Mathematical_formulation_of_quantum_mechanics#Postulates_of_quantum_mechanics)

```python

```


# Ikeda for many ships
The method developed in: ([01.03_ikeda_many_dev](06_ikeda/01.03_ikeda_many_dev.ipynb)) will now be attempted for many ships.

```python
# %load ../../imports.py
"""
These is the standard setup for the notebooks.
"""

%matplotlib inline
%load_ext autoreload
%autoreload 2

#from jupyterthemes import jtplot
#jtplot.style(theme='onedork', context='notebook', ticks=True, grid=False)

import pandas as pd
pd.options.display.max_rows = 999
pd.options.display.max_columns = 999
pd.set_option("display.max_columns", None)
import numpy as np
import os
import matplotlib.pyplot as plt
#plt.style.use('paper')

#import data
import copy
from rolldecay.bis_system import BisSystem
from rolldecay import database
from mdldb.tables import Run

from sklearn.pipeline import Pipeline
from rolldecayestimators.transformers import CutTransformer, LowpassFilterDerivatorTransformer, ScaleFactorTransformer, OffsetTransformer
from rolldecayestimators.direct_estimator_cubic import EstimatorQuadraticB, EstimatorCubic
from rolldecayestimators.ikeda_estimator import IkedaQuadraticEstimator
import rolldecayestimators.equations as equations
import rolldecayestimators.lambdas as lambdas
from rolldecayestimators.substitute_dynamic_symbols import lambdify
import rolldecayestimators.symbols as symbols
import sympy as sp

from sklearn.metrics import r2_score
import rolldecay.paper_writing as paper_writing

```

```python
from pyscores2.indata import Indata
from pyscores2.runScores2 import Calculation
from pyscores2.output import OutputFile
from pyscores2 import TDPError
import pyscores2
from rolldecayestimators.ikeda import Ikeda, IkedaR
from rolldecayestimators.simplified_ikeda_class import SimplifiedIkeda
import subprocess
```

```python
df_all_sections_id = pd.read_csv('all_sections.csv', sep=';')
df_all_sections_id.head()
```

```python
section_groups=df_all_sections_id.groupby(by='loading_condition_id')
```

```python
loading_condition_ids = df_all_sections_id['loading_condition_id'].unique()
mask=pd.notnull(loading_condition_ids)
loading_condition_ids=loading_condition_ids[mask]
```

```python
df_rolldecay = database.load(rolldecay_table_name='rolldecay_quadratic_b', limit_score=0.99,
                             exclude_table_name='rolldecay_exclude')
```

```python
mask=df_rolldecay['loading_condition_id'].isin(loading_condition_ids)
df=df_rolldecay.loc[mask].copy()
```

```python
def add_cScores(sections):
    sections=sections.copy()
    sections['cScores']=sections['area']/(sections['b']*sections['t'])
    mask=sections['cScores']>1
    sections.loc[mask,'cScores']=1
    return sections

def cut_sections(sections, draught):
    sections=sections.copy()
    mask = sections['t']>draught
    sections.loc[mask,'t']=draught
    sections.loc[mask,'area']-=draught*sections['b'].max()  # Assuming rectangular shape
    return sections

def remove_duplicate_sections(sections):
    sections=sections.copy()
    mask=~sections['x'].duplicated()
    sections=sections.loc[mask]
    assert sections['x'].is_unique
    return sections

def too_small_sections(sections):
    sections=sections.copy()
    small = 0.1
    mask=sections['b']==0
    sections.loc[mask,'b']=small
    mask=sections['t']==0
    sections.loc[mask,'t']=small
    mask=sections['area']==0
    sections.loc[mask,'area']=small
    return sections
```

```python
from scipy.integrate import simps
def calculate_lcb(x, area, **kwargs):
    """
    Calculate lcb from AP
    """
    return simps(y=area*x,x=x)/np.trapz(y=area,x=x)

def calculate_dispacement(x, area, **kwargs):
    """
    Calculate displacement
    """
    return np.trapz(y=area,x=x)
```

```python
class DraughtError(ValueError): pass

def define_indata(row, sections, rho=1000,  g=9.81):

    indata = Indata()

    draught=(row.TA+row.TF)/2
    indata.draught=draught
    if draught<=sections['t'].max():
        sections = cut_sections(sections, draught)
    else:
        raise DraughtError('Draught is too large for sections')

    sections=add_cScores(sections)

    indata.cScores=np.array(sections['cScores'])
    indata.ts=np.array(sections['t'])
    indata.bs=np.array(sections['b'])
    indata.zbars=np.zeros_like(sections['b'])  # Guessing...

    beam=sections['b'].max()
    indata.lpp=sections['x'].max()-sections['x'].min()
    #indata.displacement=row.Volume
    indata.displacement=calculate_dispacement(**sections)

    indata.g=g
    indata.kxx=row.KXX
    indata.kyy=row.lpp*0.4
    lcb=calculate_lcb(x=sections['x'], area=sections['area'])
    indata.lcb=lcb-row.lpp/2
    indata.lpp=row.lpp
    indata.projectName='loading_condition_id_%i' % row.loading_condition_id

    indata.rho=rho
    indata.zcg=row.kg-draught
    #indata.waveFrequenciesMin=0.2
    #indata.waveFrequenciesMax=0.5
    #indata.waveFrequenciesIncrement=0.006
    w=row.omega0/np.sqrt(row.scale_factor)
    indata.waveFrequenciesMin=w*0.5
    indata.waveFrequenciesMax=w*2.0
    N=40
    indata.waveFrequenciesIncrement=(indata.waveFrequenciesMax-indata.waveFrequenciesMin)/N
    indata.runOptions["IE"].set_value(1)

    return indata,sections
```

```python
def create_ikeda(row, indata, output_file, fi_a):

    w = row.omega0
    scale_factor=row.scale_factor
    V = row.ship_speed*1.852/3.6/np.sqrt(scale_factor)
    R = 0.01*row.beam/scale_factor
    lBK=row.BKL/scale_factor
    bBK=row.BKB/scale_factor
    ikeda = Ikeda.load_scoresII(V=V, w=w, fi_a=fi_a, indata=indata, output_file=output_file,
                                scale_factor=scale_factor, lBK=lBK, bBK=bBK)

    ikeda.R = R
    return ikeda
```

```python
def calculate_ikeda(ikeda):

    output = {}
    output['B_44_hat'] = ikeda.calculate_B44()[0]
    output['B_W0_hat'] =ikeda.calculate_B_W0()[0]
    output['B_W_hat'] =ikeda.calculate_B_W()[0]
    output['B_F_hat'] =ikeda.calculate_B_F()[0]
    output['B_E_hat'] =ikeda.calculate_B_E()[0]
    output['B_BK_hat'] =ikeda.calculate_B_BK()[0]
    output['B_L_hat'] =ikeda.calculate_B_L()[0]
    output['Bw_div_Bw0'] =ikeda.calculate_Bw_div_Bw0()[0]
    return output
```

```python
results = pd.DataFrame()
fi_a = np.deg2rad(10)
for run_name, row in df.iterrows():
    loading_condition_id=row['loading_condition_id']
    sections = section_groups.get_group(loading_condition_id)
    sections=remove_duplicate_sections(sections)
    sections=too_small_sections(sections)

    try:
        indata,sections_ = define_indata(row, sections)
    except DraughtError as e:
        print('Draught is too large for sections, this loading condition is skipped.')
        continue

    save_name='%s.in' % row.loading_condition_id
    save_path=os.path.join('scores2',save_name)
    indata.save(save_path)

    calculation = Calculation(outDataDirectory='scores2/result')

    # Run scoresII:
    try:
        calculation.run(indata=indata, b_div_t_max=None, timeout=1.0)
    except TDPError:
        print('Dissregarding the TDPError')
        continue
    except pyscores2.LcgError as e:
        print('Disregarded')
        print(e)
        continue
    except subprocess.TimeoutExpired:
        print('Disregarded, scoresII got stuck...')
        continue

    output_file = OutputFile(filePath=calculation.outDataPath)
    ikeda = create_ikeda(row=row, indata=indata, output_file=output_file, fi_a=fi_a)
    result_data = calculate_ikeda(ikeda)
    result=pd.Series(data=result_data, name=row.name)
    results=results.append(result)

```

```python
results
```

## Also run Simplified Ikeda for comparison

```python
def calculate_si(si):

    output = pd.DataFrame()
    output['B_44_hat'] = si.calculate_B44()
    output['B_W0_hat'] =si.calculate_B_W0()
    output['B_W_hat'] =si.calculate_B_W()
    output['B_F_hat'] =si.calculate_B_F()
    output['B_E_hat'] =si.calculate_B_E()
    output['B_BK_hat'] =si.calculate_B_BK()
    output['B_L_hat'] =si.calculate_B_L()
    output['Bw_div_Bw0'] =si.calculate_Bw_div_Bw0()
    return output
```

```python
inputs_si=pd.DataFrame()
inputs_si['w']=df['omega0']  # Already model scale
scale_factor=df['scale_factor']
inputs_si['V']=df['ship_speed']*1.852/3.6/np.sqrt(scale_factor)
inputs_si['fi_a']=fi_a
inputs_si['beam']=df['beam']/scale_factor
inputs_si['lpp']=df['lpp']/scale_factor
inputs_si['kg']=df['kg']/scale_factor
inputs_si['volume']=df['Volume']/(scale_factor**3)
draught=(df['TA']+df['TF'])/2
inputs_si['draught']=draught/scale_factor
inputs_si['A0']=df['A0']
inputs_si['lBK']=df['BKL']/scale_factor
inputs_si['bBK']=df['BKB']/scale_factor
si = SimplifiedIkeda(**inputs_si)
results_si = calculate_si(si)
results_si.index=df.index
```

## Make comparison with model tests

```python
B_e = lambdas.B_e_lambda(B_1=df['B_1'], B_2=df['B_2'], phi_a=fi_a,
                   omega0=df['omega0'])

scale_factor = df['scale_factor']
Volume = df['Volume']/(scale_factor**3)
beam = df['beam']/scale_factor
g=9.81
rho=1000

df['B_e_hat'] = lambdas.B_e_hat_lambda(B_e=B_e, Disp=Volume, beam=beam,
                                 g=g, rho=rho)
```

```python
df_results = pd.merge(left=results, right=results_si, how='inner', left_index=True, right_index=True,
                     suffixes=('_ikeda','_si'))
```

```python
mask = df_results['B_44_hat_ikeda'].notnull()
df_results = df_results.loc[mask].copy()

df_compare = pd.merge(left=df, right=df_results, how='inner', left_index=True, right_index=True,
                     suffixes=('','_y'))

```

```python
fig,ax=plt.subplots()
df_compare.plot(x='B_44_hat_ikeda', y='B_44_hat_si', ax=ax, style='o')

ax.set_xlabel(r'$\hat{B_{44}}$ (Ikeda)')
ax.set_ylabel(r'$\hat{B_{44}}$ (SI)')

xlim = ax.get_xlim()
ylim = ax.get_ylim()
lim = np.max([xlim[1],ylim[1]])
ax.set_xlim(0,lim)
ax.set_ylim(0,lim)
ax.plot([0,lim],[0,lim],'r-')

ax.grid(True)
ax.set_aspect('equal', 'box')
ax.get_legend().remove()
```

```python
size=2.5
with plt.style.context('paper'):
    fig,ax=plt.subplots()
    fig.set_size_inches(size,size)
    df_compare.plot(x='B_e_hat', y='B_44_hat_ikeda', ax=ax, style='.',
                    label=r'$\hat{B_{e}}$ Ikeda')

    df_compare.plot(x='B_e_hat', y='B_44_hat_si', ax=ax, style='+',
                    label=r'$\hat{B_{e}}$ SI')

    ax.set_xlabel(r'$\hat{B_{e}}$ (model test)')
    ax.set_ylabel(r'$\hat{B_{e}}$ (prediction)')

    xlim = ax.get_xlim()
    ylim = ax.get_ylim()
    lim = np.max([xlim[1],ylim[1]])
    ax.set_xlim(0,lim)
    ax.set_ylim(0,lim)
    #ax.set_title('Total roll damping for Ikeda, Simplified Ikeda and model tests')
    ax.plot([0,lim],[0,lim],'r-')

    ax.grid(True)
    ax.set_aspect('equal', 'box')
    ax.legend();
    paper_writing.save_fig(fig=fig, name='si_ikeda_model')
```

```python
df_compare['B_44_fraction_si_ikeda'] = df_compare['B_44_hat_si']/df_compare['B_44_hat_ikeda']

fig,ax=plt.subplots()
df_compare.plot(x='B_e_hat', y='B_44_fraction_si_ikeda', ax=ax, style='.')
ax.set_xlabel(r'$\hat{B_{44}}$')
ax.set_xlabel(r'$\frac{B_{44}(SI)}{B_{44}(Ikeda)}$')

ax.get_legend().remove()
```

```python
r2_score(y_true=df_compare['B_e_hat'], y_pred=df_compare['B_44_hat_ikeda'])
```

```python
r2_score(y_true=df_compare['B_e_hat'], y_pred=df_compare['B_44_hat_si'])
```

## Investigating the residuals

```python
def calculate_residuals(suffix_true='_ikeda', suffix_prediction='_si'):
    prefixes = ['B_44_hat',
                'B_W0_hat',
                'B_W_hat',
                'B_F_hat',
                'B_E_hat',
                'B_BK_hat',
                'B_L_hat',
                'Bw_div_Bw0',]

    for prefix in prefixes:
        residual_name = '%s_residual%s%s' % (prefix, suffix_prediction, suffix_true)
        name_true='%s%s' % (prefix, suffix_true)
        name_prediction='%s%s' % (prefix, suffix_prediction)

        df_compare[residual_name] = df_compare[name_prediction] - df_compare[name_true]

```

```python
calculate_residuals()
df_compare['B_44_residual_si_model'] = df_compare['B_44_hat_si'] - df_compare['B_e_hat']
```

```python
import seaborn as sns;
#sns.set_theme()
```

```python
df_compare['draught']=(df_compare['TA'] + df_compare['TF'])/2
df_compare['OG']=df_compare['draught']-df_compare['kg']
df_compare['beam/draught']=df_compare['beam']/df_compare['draught']
df_compare['V']=df_compare['ship_speed']*1.852
df_compare['Fn']=df_compare['V']/(np.sqrt(df_compare['lpp']*g))
df_compare[r'OG/d']=df_compare['OG']/df_compare['draught']
df_compare[r'LBK/Lpp']=df_compare['BKL']/df_compare['lpp']
df_compare[r'BBK/beam']=df_compare['BKB']/df_compare['beam']
df_compare['omega_hat']=lambdas.omega_hat(beam=df_compare['beam'], g=g, omega0=df_compare['omega0'])

df_compare['Cb']=df_compare['Volume']/(df_compare['lpp']*df_compare['beam']*df_compare['draught'])
interesting=('Cb','A0','OG/d','LBK/Lpp','BBK/beam','omega_hat',r'beam/draught', 'Fn')
#sns.lmplot(data=df_compare,y='B_44_hat_residual_si_ikeda', x=interesting, aspect=0.6);
sns.pairplot(df_compare,y_vars='B_44_hat_residual_si_ikeda', x_vars=interesting, aspect=0.6);
```

```python
with plt.style.context('paper'):

    y='B_44_residual_si_model'

    ylabel=r'$\hat{B}_{e}^{SI}-\hat{B}_{e}^{Model}$'
    fig,ax=plt.subplots()
    fig.set_size_inches(size,size)

    df_compare.plot(x=r'beam/draught',y=y, ax=ax, style='.')

    ax.set_ylabel(ylabel)
    ax.set_xlabel(r'$\frac{beam}{T}$ [-]')
    ax.set_ylabel(ylabel)
    ax.grid(True)
    ax.get_legend().remove()
    paper_writing.save_fig(fig, name='beam_T_residual')
```

```python
labels={
'Cb' : r'$C_b$ [-]',
r'beam/draught' : r'$\frac{beam}{T}$ [-]',
r'OG/d' : r'$\frac{\overline{OG}}{T}$ [-]',
'A0' : r'$A_{0}$ [-]',
r'BBK/beam' : r'$\frac{BK_B}{beam}$ [-]',
r'LBK/Lpp' : r'$\frac{BK_L}{L_{pp}}$ [-]',
r'omega_hat' : r'$\hat{\omega}$ [-]',
r'fi_a' : r'$\phi_a$ [rad]',
r'Fn' : r'$F_n$ [-]',

}

with plt.style.context('paper'):
    fig,ax=plt.subplots()
    fig.set_size_inches(size,size)

    df_compare['B_44_residual_si_model_abs'] = df_compare['B_44_residual_si_model'].abs()

    for y in interesting:

        df_=df_compare.sort_values(by=y)
        y_=df_[y].abs()
        y_-=y_.min()

        ax.plot(df_['B_44_residual_si_model_abs'], y_, '.', label=labels[y])

    ax.set_xlabel(r'$|\hat{B}_{e}^{SI}-\hat{B}_{e}^{Model}|$')
    ax.set_ylabel('change')

    ax.grid(True)
    ax.legend(loc='upper center', bbox_to_anchor=(1.30, 0.8),
          ncol=1)

    paper_writing.save_fig(fig, name='parameter_residual')
```

## Comparing damping contributions SI vs. Ikeda

```python
suffix_true='_ikeda'
suffix_prediction='_si'
prefixes = [
    'B_W_hat',
    'B_F_hat',
    'B_E_hat',
    'B_BK_hat',
    'B_L_hat',]

labels={
    'B_W_hat' : r'$\hat{B_{W}}$',
    'B_F_hat' : r'$\hat{B_{F}}$',
    'B_E_hat' : r'$\hat{B_{E}}$',
    'B_BK_hat' : r'$\hat{B_{BK}}$',
    'B_L_hat' : r'$\hat{B_{L}}$',
}

lim = np.max([df_compare['B_44_hat_ikeda'].max(),
              df_compare['B_44_hat_si'].max(),
             ])
with plt.style.context('paper'):

    fig,ax=plt.subplots()
    fig.set_size_inches(size,size)

    for prefix in prefixes:

        name_true='%s%s' % (prefix, suffix_true)
        name_prediction='%s%s' % (prefix, suffix_prediction)

        y_=(df_compare[name_prediction]-df_compare[name_true]).abs()
        #y_-=y_.min()

        ax.plot(df_compare['B_44_residual_si_model_abs'], y_, '.', label=labels[prefix])
        ax.set_xlabel(r'$|\hat{B}_{e}^{SI}-\hat{B}_{e}^{Ikeda}|$')

    ax.legend()
    ax.grid(True)
    ax.set_ylabel(r'$\hat{B}$ [-]')

    paper_writing.save_fig(fig, name='component_residual')

```

```python
fig,ax = plt.subplots()
df_compare['B_W_fraction_si_ikeda'] = df_compare['B_W_hat_si']/df_compare['B_W_hat_ikeda']

df_compare.plot(x='Fn', y='B_W_fraction_si_ikeda', ax=ax, style='o');
ax.set_ylabel(r'$\frac{\hat{B_W}(SI)}{\hat{B_W}(Ikeda)}$')
ax.grid(True)
ax.get_legend().remove()

```

```python

```

```python

```

```python

```

```python

```

```python

```