```python
import numpy as np
import sympy as sp
import pandas as pd
import math

import matplotlib.pyplot as plt

# Needed only in Jupyter to render properly in-notebook
%matplotlib inline
```

# Homework 4

## Chinmai Raman

### Using pandas on Global Historical Climatology Network data

```python
df = pd.read_csv("598354.csv")
df.head()
```

      STATION
      STATION_NAME
      DATE
      MDPR
      DAPR
      PRCP
      TMAX
      TMIN
      TOBS

      0
      GHCND:USR0000CTHO
      THOMES CREEK CALIFORNIA CA US
      20140101
      -9999
      -9999
      -9999
      217
      83
      -9999

      1
      GHCND:USR0000CTHO
      THOMES CREEK CALIFORNIA CA US
      20140102
      -9999
      -9999
      -9999
      217
      72
      -9999

      2
      GHCND:USR0000CTHO
      THOMES CREEK CALIFORNIA CA US
      20140103
      -9999
      -9999
      -9999
      206
      78
      -9999

      3
      GHCND:USR0000CTHO
      THOMES CREEK CALIFORNIA CA US
      20140104
      -9999
      -9999
      -9999
      206
      94
      -9999

      4
      GHCND:USR0000CTHO
      THOMES CREEK CALIFORNIA CA US
      20140105
      -9999
      -9999
      -9999
      206
      100
      -9999

The data provided only contains data from locations around California. In many instances, the temperature
(max and min) is provided, but not the precipitation, or vice-versa, especially in the month of June, 2015.
Most of the data points for precipitation in the month of June, 2015 are 0, which is not surprising given
that California experiences mostly Winter rains, and we have also been experiencing a drought for a few
years. I chose Blue Canyon Airport as my location because it contains both precipiation and temperature
data, and the area experienced some rain during June of 2015.

### Data for Blue Canyon Airport, California

```python
data = df.ix[383819:384427]
new_data = pd.DataFrame(data, columns = ['a', 'b', 'DATE', 'c', 'd', 'e', 'PRCP', 'TMAX', 'TMIN', 'f'])
newer_data = new_data.drop(['a', 'b', 'c', 'd', 'e', 'f'], axis = 1)
newer_data
```

      DATE
      PRCP
      TMAX
      TMIN

      383819
      20140103
      0
      150
      78

      383820
      20140104
      0
      128
      56

      383821
      20140105
      0
      161
      56

      383822
      20140106
      0
      111
      67

      383823
      20140107
      0
      94
      44

      383824
      20140108
      0
      72
      22

      383825
      20140109
      0
      61
      17

      383826
      20140110
      0
      122
      22

      383827
      20140111
      104
      89
      0

      383828
      20140112
      0
      83
      -16

      383829
      20140113
      0
      172
      50

      383830
      20140114
      0
      189
      94

      383831
      20140115
      0
      189
      94

      383832
      20140116
      0
      183
      89

      383833
      20140117
      0
      167
      111

      383834
      20140118
      0
      167
      100

      383835
      20140119
      0
      150
      94

      383836
      20140120
      0
      156
      94

      383837
      20140121
      0
      150
      83

      383838
      20140122
      0
      183
      78

      383839
      20140123
      0
      106
      44

      383840
      20140124
      0
      150
      50

      383841
      20140125
      0
      156
      83

      383842
      20140126
      0
      133
      78

      383843
      20140127
      0
      144
      83

      383844
      20140128
      0
      128
      78

      383845
      20140129
      445
      100
      22

      383846
      20140130
      13
      56
      0

      383847
      20140131
      5
      44
      -21

      383848
      20140201
      0
      44
      -32

      ...
      ...
      ...
      ...
      ...

      384398
      20150805
      0
      239
      122

      384399
      20150806
      0
      233
      172

      384400
      20150807
      0
      228
      144

      384401
      20150808
      0
      233
      161

      384402
      20150809
      0
      256
      167

      384403
      20150810
      0
      244
      161

      384404
      20150811
      0
      222
      144

      384405
      20150812
      0
      233
      150

      384406
      20150813
      0
      250
      139

      384407
      20150814
      0
      239
      150

      384408
      20150815
      0
      272
      172

      384409
      20150816
      0
      294
      206

      384410
      20150817
      0
      -9999
      -9999

      384411
      20150818
      0
      -9999
      -9999

      384412
      20150819
      0
      278
      206

      384413
      20150820
      0
      272
      194

      384414
      20150821
      0
      261
      178

      384415
      20150822
      0
      267
      178

      384416
      20150823
      0
      261
      172

      384417
      20150824
      0
      267
      183

      384418
      20150825
      0
      278
      189

      384419
      20150826
      0
      278
      178

      384420
      20150827
      0
      278
      194

      384421
      20150828
      0
      272
      172

      384422
      20150829
      0
      206
      117

      384423
      20150830
      0
      217
      122

      384424
      20150831
      0
      244
      150

      384425
      20150901
      0
      239
      150

      384426
      20140108
      8
      -9999
      -9999

      384427
      20140130
      53
      -9999
      -9999

609 rows × 4 columns

### Maximum and Minimum temperatures in January of 2014

```python
pd.DataFrame(newer_data[1:29]).plot('DATE', 'TMAX', kind = 'bar')
```

```python
pd.DataFrame(newer_data[1:29]).plot('DATE', 'TMIN', kind = 'bar')
```

```python
idx = newer_data[newer_data['TMIN'] == - 9999].index.tolist()
my_data = newer_data.drop(idx)
```

### Histograms of the Maximum and Minimum temperatures over their respective ranges

```python
my_data['TMIN'].hist(bins=20)
```

```python
my_data['TMAX'].hist(bins=20)
```

### Recorded Rainfall in June, 2015

```python
rain_data = my_data.drop(['TMAX', 'TMIN'], axis = 1)
rain_data.ix[20150601:20150630].plot(kind = 'bar')
```


[[<- back to pattern_classification](https://github.com/rasbt/pattern_classification)]

```python
%load_ext watermark
```

```python
%watermark -a "Sebastian Raschka" -d -v
```

    UsageError: Line magic function `%watermark` not found.

# Hierarchical Agglomerative Clustering - Complete Linkage Clustering

### - A quick Python tutorial

## Sections

- [Generating some Sample Data](#Generating-some-Sample-Data)
- [Pair-wise Distance Matrix, Rows](#Pair-wise-Distance-Matrix,-Rows)
- [Apply Clustering - Complete Linkage](#Apply-Clustering---Complete-Linkage)
- [Heatmaps](#Heatmaps)
    - [Heatmap of the Original Data](#Heatmap-of-the-Original-Data)
    - [Heatmap after Row-Clustering](#Heatmap-after-Row-Clustering)
    - [Heatmap plus Row-Dendrogram](#[Heatmap-plus-Row-Dendrogram)
    - [Adding a Column Dendrogram](#Adding-a-Column-Dendrogram)
- [Important Warning About Adding Dendrograms](#Important-Warning-About-Adding-Dendrograms)

**This is a more technical-oriented tutorial that was born out of necessity to plot a heatmap including the dendrograms from a complete linkage clustering. Since I couldn't find any good resource online, I thought that it might be worthwhile sharing it. **

**Therefore, the theoretical aspects are a little bit brief, but nonetheless, here is a short introduction of what it is all about.**

Complete linkage is one implementation of *hierarchical agglomerative clustering*. The principle of hierarchical agglomerative clustering is to start with a singleton cluster, and clusters are iteratively merged until one single cluster remains. This results in a "cluster tree," which is also called *dendrogram*. The opposite approach -- starting with one cluster and divide into clusters until only singleton clusters remain -- is called *divisive hierarchical clustering*.

The algorithm can be summarized via the following pseudocode

1: Compute a distance or similarity matrix.
2: Each data point is represented as a singleton cluster.
3: **Repeat**
4:         Merge two closest clusters (e.g., based on distance between most similar or dissimilar members).
5:         Update the distance (or similarity) matrix.
6: **Until** one single cluster remains.

Complete linkage compares the most dissimilar members between clusters in each iteration. The two clusters which have the most similar *dissimilar members* are merged into a new cluster.
\begin{equation}
d(C,D) = \max[dist(C_i, D_j)]
\end{equation}
for all $i$  points in cluster $C$ and $j$ points in cluster $D$.

In contrast, the *single linkage* algorithm compares the two most similar members instead of the most dissimilar ones.
\begin{equation}
d(C,D) = \min[dist(C_i, D_j)]
\end{equation}
for all $i$  points in cluster $C$ and $j$ points in cluster $D$.

For more implementations, e.g., `centroid`, `average` etc., please see the [`scipy.cluster.hierarchy.linkage` documentation](http://docs.scipy.org/doc/scipy/reference/generated/scipy.cluster.hierarchy.linkage.html#scipy.cluster.hierarchy.linkage).

**(A more detailed article about prototype-based, hierarchical, and density-based clustering is in the planning stage)**

### Generating some Sample Data

[[back to top](#Sections)]

First, we generate some random sample data to work with. Like in a typical application, the rows represent different observations (Samples 1-5), and the columns are the different features (*X, Y, Z*).

```python
import pandas as pd
import numpy as np

np.random.seed(123)

variables = ['X', 'Y', 'Z']
labels = ['ID_0','ID_1','ID_2','ID_3','ID_4']

X = np.random.random_sample([5,3])*10
df = pd.DataFrame(X, columns=variables, index=labels)
df
```

      X
      Y
      Z

      ID_0
      6.964692
      2.861393
      2.268515

      ID_1
      5.513148
      7.194690
      4.231065

      ID_2
      9.807642
      6.848297
      4.809319

      ID_3
      3.921175
      3.431780
      7.290497

      ID_4
      4.385722
      0.596779
      3.980443

### Pair-wise Distance Matrix, Rows

[[back to top](#Sections)]

First, we calculate the pair-wise distances for every row (i.e, between every sample across the different variables). We will use the default euclidean distance measure. The other available distance measures are listed in the [`scipy.spatial.distance.pdist` documentation](http://docs.scipy.org/doc/scipy-0.14.0/reference/generated/scipy.spatial.distance.pdist.html#scipy.spatial.distance.pdist) with a nice and short explanatory paragraph. In addition, we use the [`squareform`](http://docs.scipy.org/doc/scipy-0.14.0/reference/generated/scipy.spatial.distance.squareform.html#scipy.spatial.distance.squareform) function to return a symmetrical matrix of the pair-wise distances.

```python
from scipy.spatial.distance import pdist,squareform

row_dist = pd.DataFrame(squareform(pdist(df, metric='euclidean')), columns=labels, index=labels)
row_dist
```

      ID_0
      ID_1
      ID_2
      ID_3
      ID_4

      ID_0
      0.000000
      4.973534
      5.516653
      5.899885
      3.835396

      ID_1
      4.973534
      0.000000
      4.347073
      5.104311
      6.698233

      ID_2
      5.516653
      4.347073
      0.000000
      7.244262
      8.316594

      ID_3
      5.899885
      5.104311
      7.244262
      0.000000
      4.382864

      ID_4
      3.835396
      6.698233
      8.316594
      4.382864
      0.000000

### Apply Clustering - Complete Linkage

[[back to top](#Sections)]

When we apply the complete linkage agglomeration to our clusters, the `linkage` function returns a so-called `linkage matrix`.
This `linkage matrix` consists of several rows where each row consists of 1 merge. The first and second column denote the most dissimilar members in each cluster, and the third row reports the distance between those members. The last column returns the count of members in the clusters.

However, before we call the `linkage` function, let us take a careful look at its [documentation](http://docs.scipy.org/doc/scipy/reference/generated/scipy.cluster.hierarchy.linkage.html#scipy.cluster.hierarchy.linkage).

> Parameters:
**y** : ndarray
    A condensed or redundant distance matrix. A condensed distance matrix is a flat array containing the upper triangular of the distance matrix. This is the form that pdist returns. Alternatively, a collection of m observation vectors in n dimensions may be passed as an m by n array.

> **method** : str, optional
The linkage algorithm to use. See the Linkage Methods section below for full descriptions.

> **metric** : str, optional
The distance metric to use. See the distance.pdist function for a list of valid distance metrics.

> Returns:
**Z** : ndarray
The hierarchical clustering encoded as a linkage matrix.

Thus, we can either pass a condensed distance matrix (upper triangular) from the `pdist` function, or we can pass the "original" data array and define the `'euclidean'` metric as  function argument in `linkage`. However, we shouldn't pass the `squareform` distance metrics, which would yield incorrect distance values although the overall clustering could be the same.

#### a) Squareform distance matrix (wrong)

```python
from scipy.cluster.hierarchy import linkage

row_clusters = linkage(row_dist, method='complete', metric='euclidean')
pd.DataFrame(row_clusters,
             columns=['row label 1', 'row label 2', 'distance', 'no. of items in clust.'],
             index=['cluster %d' %(i+1) for i in range(row_clusters.shape[0])])
```

    /srv/venv/lib/python3.6/site-packages/ipykernel_launcher.py:3: ClusterWarning: scipy.cluster: The symmetric non-negative hollow observation matrix looks suspiciously like an uncondensed distance matrix
      This is separate from the ipykernel package so we can avoid doing imports until

      row label 1
      row label 2
      distance
      no. of items in clust.

      cluster 1
      0.0
      4.0
      6.521973
      2.0

      cluster 2
      1.0
      2.0
      6.729603
      2.0

      cluster 3
      3.0
      5.0
      8.539247
      3.0

      cluster 4
      6.0
      7.0
      12.444824
      5.0

#### b) Condensed distance matrix (correct)

```python
row_clusters = linkage(pdist(df, metric='euclidean'), method='complete')
pd.DataFrame(row_clusters,
             columns=['row label 1', 'row label 2', 'distance', 'no. of items in clust.'],
             index=['cluster %d' %(i+1) for i in range(row_clusters.shape[0])])
```

      row label 1
      row label 2
      distance
      no. of items in clust.

      cluster 1
      0.0
      4.0
      3.835396
      2.0

      cluster 2
      1.0
      2.0
      4.347073
      2.0

      cluster 3
      3.0
      5.0
      5.899885
      3.0

      cluster 4
      6.0
      7.0
      8.316594
      5.0

#### c) Input sample matrix (correct)

```python
row_clusters = linkage(df.values, method='complete', metric='euclidean')
pd.DataFrame(row_clusters,
             columns=['row label 1', 'row label 2', 'distance', 'no. of items in clust.'],
             index=['cluster %d' %(i+1) for i in range(row_clusters.shape[0])])
```

      row label 1
      row label 2
      distance
      no. of items in clust.

      cluster 1
      0.0
      4.0
      3.835396
      2.0

      cluster 2
      1.0
      2.0
      4.347073
      2.0

      cluster 3
      3.0
      5.0
      5.899885
      3.0

      cluster 4
      6.0
      7.0
      8.316594
      5.0

```python
import matplotlib.pyplot as plt
%matplotlib inline
from scipy.cluster.hierarchy import dendrogram

row_dendr = dendrogram(row_clusters, labels=labels)
```

## Heatmaps

[[back to top](#Sections)]

This section is about the visualization of our hierarchical clustering. Here, we will plot simple heatmaps for each scenario: The original data, the data after row clustering, and eventually the data after we applied row and column clustering.

### Heatmap of the Original Data

[[back to top](#Sections)]

```python
fig = plt.figure()

ax = fig.add_subplot(111)

cax = ax.matshow(df, interpolation='nearest', cmap='hot_r')
fig.colorbar(cax)

ax.set_xticklabels([''] + list(df.columns))
ax.set_yticklabels([''] + list(df.index))

plt.show()
```

### Heatmap after Row-Clustering

[[back to top](#Sections)]

The `dendrogram` function returns a dictionary with various items which are explained in detail in the scipy documenation [here](http://docs.scipy.org/doc/scipy/reference/generated/scipy.cluster.hierarchy.dendrogram.html). The dendrogram leave order can then be accessed via the `'leaves'` key, e.g.,

```python
row_dendr['leaves']
```

    [1, 2, 3, 0, 4]

Thus, in order to sort the DataFrame according to the clustering, we can simply use the `'leaves'` as indices like so:

    df.ix[row_dendr['leaves']]

```python
# reorder rows with respect to the clustering
row_dendr = dendrogram(row_clusters, labels=labels, no_plot=True)
df_rowclust = df.ix[row_dendr['leaves']]

# plot
fig = plt.figure()
ax = fig.add_subplot(111)

cax = ax.matshow(df_rowclust, interpolation='nearest', cmap='hot_r')
fig.colorbar(cax)

ax.set_xticklabels([''] + list(df_rowclust.columns))
ax.set_yticklabels([''] + list(df_rowclust.index))

plt.show()
```

### Heatmap plus Row-Dendrogram

[[back to top](#Sections)]

Now, we can rotate the dendrogram via the by setting the `orientation` parameter to `'right'`, but note that
we now have to sort the clustered data in reverse order to match the row labels in the heatmap via

    df.ix[row_dendr['leaves'][::-1]]

```python
from scipy.cluster import hierarchy
# makes dendrogram black (1)
hierarchy.set_link_color_palette(['black'])

# plot row dendrogram
fig = plt.figure(figsize=(8,8))
axd = fig.add_axes([0.09,0.1,0.2,0.6])
row_dendr = dendrogram(row_clusters, orientation='right',
                   color_threshold=np.inf, ) # makes dendrogram black (2))

# reorder data with respect to clustering
df_rowclust = df.ix[row_dendr['leaves'][::-1]]

axd.set_xticks([])
axd.set_yticks([])

# remove axes spines from dendrogram
for i in axd.spines.values():
        i.set_visible(False)

# reorder rows with respect to the clustering
df_rowclust = df.ix[row_dendr['leaves'][::-1]]

# plot heatmap
axm = fig.add_axes([0.26,0.1,0.6,0.6]) # x-pos, y-pos, width, height
cax = axm.matshow(df_rowclust, interpolation='nearest', cmap='hot_r')
fig.colorbar(cax)
axm.set_xticklabels([''] + list(df_rowclust.columns))
axm.set_yticklabels([''] + list(df_rowclust.index))

plt.show()
```

### Adding a Column Dendrogram

[[back to top](#Sections)]

Also, we can add a an additional dendrogram for the column clustering at the top. Here, we first sort the indexes after clustering the rows

    df_rowclust = df.ix[row_dendr['leaves'][::-1]]

And then we can use the indices from the column clustering to reorder the columns:

    df_rowclust.columns = [df_rowclust.columns[col_dendr['leaves']]]

```python
# Compute pairwise distances for columns
col_dists = pdist(df.T, metric='euclidean')
col_clusters = linkage(col_dists, method='complete')

# plot column dendrogram
fig = plt.figure(figsize=(8,8))

axd2 = fig.add_axes([0.38,0.74,0.36,0.10])
col_dendr = dendrogram(col_clusters, orientation='top',
                       color_threshold=np.inf) # makes dendrogram black)
axd2.set_xticks([])
axd2.set_yticks([])

# plot row dendrogram
axd1 = fig.add_axes([0.09,0.1,0.2,0.6])
row_dendr = dendrogram(row_clusters, orientation='right',
                       count_sort='ascending',
                       color_threshold=np.inf) # makes dendrogram black
axd1.set_xticks([])
axd1.set_yticks([])

# remove axes spines from dendrogram
for i,j in zip(axd1.spines.values(), axd2.spines.values()):
        i.set_visible(False)
        j.set_visible(False)

# reorder columns and rows with respect to the clustering
df_rowclust = df.ix[row_dendr['leaves'][::-1]]
df_rowclust.columns = [df_rowclust.columns[col_dendr['leaves']]]

# plot heatmap
axm = fig.add_axes([0.26,0.1,0.6,0.6])
cax = axm.matshow(df_rowclust, interpolation='nearest', cmap='hot_r')
fig.colorbar(cax)
axm.set_xticklabels([''] + list(df_rowclust.columns))
axm.set_yticklabels([''] + list(df_rowclust.index))

plt.show()
```

# Important Warning About Adding Dendrograms

[[back to top](#Sections)]

It is very important that you always check that the labels on the heatmap and the dendrogram match when you create the first plot! For example, we can provide the `labels` to the `labels` parameter of the dendrogram and **DON'T set `axd.set_yticks([])`** to show the labels on the dendrogram to compare them to the heatmap labels.

```python
from scipy.cluster import hierarchy
# makes dendrogram black (1)
hierarchy.set_link_color_palette(['black'])

# plot row dendrogram
fig = plt.figure(figsize=(8,8))
axd = fig.add_axes([0.09,0.1,0.2,0.6])
row_dendr = dendrogram(row_clusters, orientation='right',
                   labels=labels,
                   color_threshold=np.inf, ) # makes dendrogram black (2))
axd.set_xticks([])

# uncomment to hide dendrogram labels
#axd.set_yticks([])

# remove axes spines from dendrogram
for i in axd.spines.values():
        i.set_visible(False)

# reorder columns and rows with respect to the clustering
df_rowclust = df.ix[row_dendr['leaves'][::-1]]

# plot heatmap
axm = fig.add_axes([0.26,0.1,0.6,0.6]) # x-pos, y-pos, width, height
cax = axm.matshow(df_rowclust, interpolation='nearest', cmap='hot_r')
fig.colorbar(cax)
axm.set_xticklabels([''] + list(df_rowclust.columns))
axm.set_yticklabels([''] + list(df_rowclust.index))

plt.show()
```

```python

```

```python

```

```python

```

```python
test complete; Gopala
```


# Symbolic Partial Derivative Routine

## Authors: Zach Etienne & Tyler Knowles

## This module contains a routine for computing partial derivatives of a mathematical expression that is written as several subexpressions.

**Notebook Status:**  Validated

**Validation Notes:** This tutorial notebook has been confirmed to be self-consistent with its corresponding NRPy+ module, as documented [below](#code_validation). Additionally, this notebook has been validated by checking that results are consistent with exact derivative expressions used in the SEOBNRv3_opt approixment of [LALSuite](https://git.ligo.org/lscsoft/lalsuite).

### NRPy+ Source Code for this module: [SEOBNR_Derivative_Routine.py](../edit/SEOBNR/SEOBNR_Derivative_Routine.py)

## Introduction
$$\label{intro}$$

This notebook documents the symbolic partial derivative routine used to generate analytic derivatives of the [SEOBNRv3](https://git.ligo.org/lscsoft/lalsuite) Hamiltonian (documented [here](../Tutorial-SEOBNR_v3_Hamiltonian.ipynb)) and described in [this article](https://arxiv.org/abs/1803.06346).  In general, this notebook takes as input a file of inter-dependent mathematical expressions (in SymPy syntax), a file listing the names of values within those expressions, and a file listing all variables with which to take partial derivatives of each expression.  The output is a text file containing the original expression and those for each partial derivative computation.  The intention is to perform CSE on these expressions to create efficient partial derivative code!

# Table of Contents
$$\label{toc}$$

This notebook is organized as follows

1. [Step 1](#initializenrpy): Initialize core Python/NRPy+ modules
1. [Step 2:](#read_expressions) Read in Hamiltonian expressions from `Hamstring.txt`
1. [Step 3:](#list_constants) Specify constants and variables in Hamiltonian expression
1. [Step 4:](#list_free_symbols) Extract free symbols
1. [Step 5:](#convert_to_func) Convert variables to function notation; e.g., `var` goes to `var(xx)`
1. [Step 6:](#differentiate) Differentiate with respect to `xx`
1. [Step 7:](#remove_zeros) Remove derivatives (of constants) that evaluate to zero, simplifying derivative expressions
1. [Step 8:](#partial_derivative) Simplify derivatives with respect to a specific variable
1. [Step 9:](#store_results) Store partial derivatives to SymPy notebook `partial_derivatives.txt-VALIDATION.txt`
1. [Step 10:](#code_validation) Validate against LALSuite and trusted `SEOBNR_Derivative_Routine` NRPy+ module
1. [Step 11:](#latex_pdf_output) Output this notebook to $\LaTeX$-formatted PDF file

# Step 1: Initialize core Python/NRPy+ modules \[Back to [top](#toc)\]
$$\label{initializenrpy}$$

Let's start by importing all the needed modules from Python/NRPy+ and creating the output directory (if it does not already exist):

```python
# Step 1.a: import all needed modules from Python/NRPy+:
import sympy as sp                # SymPy: The Python computer algebra package upon which NRPy+ depends
import sys, os                    # Standard Python modules for multiplatform OS-level functions
from outputC import superfast_uniq, lhrh      # Remove duplicate entries from a Python array; store left- and right-
                                              #   hand sides of mathematical expressions

# Step 1.b: Check for a sufficiently new version of SymPy (for validation)
# Ignore the rc's and b's for release candidates & betas.
sympy_version = sp.__version__.replace('rc', '...').replace('b', '...')
sympy_version_decimal = float(sympy_version.split(".")[0]) + float(sympy_version.split(".")[1])/10.0
if sympy_version_decimal < 1.2:
    print('Error: NRPy+ does not support SymPy < 1.2')
    sys.exit(1)

# Step 1.c: Name of the directory containing the input file
inputdir = "SEOBNR"
```

# Step 2: Read in Hamiltonian expressions from `Hamstring.txt` \[Back to [top](#toc)\]
$$\label{read_expressions}$$

We read in the expressions of which we will compute partial derivatives in a single large string before splitting the string by line (carriage return) and by "=".  Doing so allows us to manipulate the right- and left-hand sides of the expressions appropriately.  We store the left- and right-hand sides in the array `lr`, which consists of `lhrh` arrays with left-hand sides `lhs` and right-hand sides `rhs`.  Note that `Lambda` is a protected keyword in Python, so the variable $\Lambda$ in the Hamiltonian is renamed `Lamb`.

```python
# Step 2.a: Read in expressions as a (single) string
with open(os.path.join(inputdir,'Hamstring.txt'), 'r') as file:
    expressions_as_lines = file.readlines()

# Step 2.b: Create and populate the "lr" array, which separates each line into left- and right-hand sides
#   Each entry is a string of the form lhrh(lhs='',rhs='')
lr = []

for i in range(len(expressions_as_lines)):
    # Ignore lines with 2 or fewer characters and those starting with #
    if len(expressions_as_lines[i]) > 2 and expressions_as_lines[i][0] != "#":
        # Split each line by its equals sign
        split_line = expressions_as_lines[i].split("=")
        # Append the line to "lr", removing spaces, "sp." prefixes, and replacing Lambda->Lamb
        #   (Lambda is a protected keyword):
        lr.append(lhrh(lhs=split_line[0].replace(" ","").replace("Lambda","Lamb"),
                       rhs=split_line[1].replace(" ","").replace("sp.","").replace("Lambda","Lamb")))

# Step 2.c: Separate and sympify right- and left-hand sides into separate arrays
lhss = []
rhss = []

# As of April 2021, "sp.sympify("Q+1")" fails because Q is a reserved keyword.
#   This is the workaround, courtesy Ken Sible.
custom_global_dict = {}
exec('from sympy import *', custom_global_dict)
del custom_global_dict['Q']
if sympy_version_decimal >= 1.6:
    custom_parse_expr = lambda expr: sp.parse_expr(expr, global_dict=custom_global_dict)
else:
    custom_parse_expr = lambda expr: sp.sympify(expr)

for i in range(len(lr)):
    lhss.append(custom_parse_expr(lr[i].lhs))
    rhss.append(custom_parse_expr(lr[i].rhs))
```

# Step 3: Specify constants and variables in Hamiltonian expression \[Back to [top](#toc)\]
$$\label{list_constants}$$

We read in and declare as SymPy symbols the constant values; derivatives with respect to these variables will be set to zero.  We then read in the variables with respect to which we want to take derivatives and declare those as SymPy variables as well.

```python
# Step 3.a: Create `input_constants` array and populate with SymPy symbols
m1,m2,tortoise,eta,KK,k0,k1,EMgamma,d1v2,dheffSSv2 = sp.symbols('m1 m2 tortoise eta KK k0 k1 EMgamma d1v2 dheffSSv2',
                                                                real=True)
input_constants = [m1,m2,tortoise,eta,KK,k0,k1,EMgamma,d1v2,dheffSSv2]

# Step 3.b: Create `dynamic_variables` array and populate with SymPy symbols
x,y,z,px,py,pz,s1x,s1y,s1z,s2x,s2y,s2z = sp.symbols('x y z px py pz s1x s1y s1z s2x s2y s2z', real=True)
dynamic_variables = [x,y,z,px,py,pz,s1x,s1y,s1z,s2x,s2y,s2z]
```

# Step 4: Extract free symbols \[Back to [top](#toc)\]
$$\label{list_free_symbols}$$

By ''free symbols'' we mean the variables in the right-hand sides.  We first create a list of all such terms (using SymPy's built-in free_symbol attribute), including duplicates, and then strip the duplicates.  We then remove input constants from the symbol list.

```python
# Step 4.a: Prepare array of "free symbols" in the right-hand side expressions
full_symbol_list_with_dups = []
for i in range(len(lr)):
    for variable in rhss[i].free_symbols:
        full_symbol_list_with_dups.append(variable)

# Step 4.b: Remove duplicate free symbols
full_symbol_list = superfast_uniq(full_symbol_list_with_dups)

# Step 4.c: Remove input constants from symbol list
for inputconst in input_constants:
    for symbol in full_symbol_list:
        if str(symbol) == str(inputconst):
            full_symbol_list.remove(symbol)
```

# Step 5: Convert variables to function notation; e.g., `var` goes to `var(xx)` \[Back to [top](#toc)\]
$$\label{convert_to_func}$$

In order to compute the partial derivative of each right-hand side, we mark each variable (left-hand side) and each free symbol (in right-hand sides) as a function with argument $\texttt{xx}$.

```python
# Step 5.a: Convert each left-hand side to function notation
#   while separating and simplifying left- and right-hand sides
xx = sp.Symbol('xx',real=True)
func = []
for i in range(len(lr)):
    func.append(sp.sympify(sp.Function(lr[i].lhs,real=True)(xx)))

# Step 5.b: Mark each free variable as a function with argument xx
full_function_list = []
for symb in full_symbol_list:
    func = sp.sympify(sp.Function(str(symb),real=True)(xx))
    full_function_list.append(func)
    for i in range(len(rhss)):
        for var in rhss[i].free_symbols:
            if str(var) == str(symb):
                rhss[i] = rhss[i].subs(var,func)
```

# Step 6: Differentiate with respect to `xx` \[Back to [top](#toc)\]
$$\label{differentiate}$$

Now we differentiate the right-hand expressions with respect to `xx`.  We use the SymPy $\texttt{diff}$ command, differentiating with respect to $\texttt{xx}$.  After so doing, we remove $\texttt{(xx)}$ and "Derivative" (which is output by $\texttt{diff}$), and use "prm" suffix to denote the derivative with respect to $\texttt{xx}$.

```python
# Step 6: Use SymPy's diff function to differentiate right-hand sides with respect to xx
#   and append "prm" notation to left-hand sides
lhss_deriv = []
rhss_deriv = []
for i in range(len(rhss)):
    lhss_deriv.append(custom_parse_expr(str(lhss[i])+"prm"))
    newrhs = custom_parse_expr(str(sp.diff(rhss[i],xx)).replace("(xx)","").replace(", xx","prm").replace("Derivative",""))
    rhss_deriv.append(newrhs)
```

# Step 7: Remove derivatives (of constants) that evaluate to zero, simplifying derivative expressions \[Back to [top](#toc)\]
$$\label{remove_zeros}$$

We declare a function to simply the derivative expressions.  In particular, we want to remove terms equal to zero.

```python
# Step 7.a: Define derivative simplification function
def simplify_deriv(lhss_deriv,rhss_deriv):
    # Copy expressions into another array
    lhss_deriv_simp = []
    rhss_deriv_simp = []
    for i in range(len(rhss_deriv)):
        lhss_deriv_simp.append(lhss_deriv[i])
        rhss_deriv_simp.append(rhss_deriv[i])
    # If a right-hand side is 0, substitute value 0 for the corresponding left-hand side in later terms
    for i in range(len(rhss_deriv_simp)):
        if rhss_deriv_simp[i] == 0:
            for j in range(i+1,len(rhss_deriv_simp)):
                for var in rhss_deriv_simp[j].free_symbols:
                    if str(var) == str(lhss_deriv_simp[i]):
                        rhss_deriv_simp[j] = rhss_deriv_simp[j].subs(var,0)
    zero_elements_to_remove = []
    # Create array of indices for expressions that are zero
    for i in range(len(rhss_deriv_simp)):
        if rhss_deriv_simp[i] == sp.sympify(0):
            zero_elements_to_remove.append(i)

    # When removing terms that are zero, we need to take into account their new index (after each removal)
    count = 0
    for i in range(len(zero_elements_to_remove)):
        del lhss_deriv_simp[zero_elements_to_remove[i]+count]
        del rhss_deriv_simp[zero_elements_to_remove[i]+count]
        count -= 1
    return lhss_deriv_simp,rhss_deriv_simp

# Step 7.b: Call the simplication function and then copy results
lhss_deriv_simp,rhss_deriv_simp = simplify_deriv(lhss_deriv,rhss_deriv)
lhss_deriv = lhss_deriv_simp
rhss_deriv = rhss_deriv_simp
```

# Step 8: Simplify derivatives with respect to a specific variable \[Back to [top](#toc)\]
$$\label{partial_derivative}$$

In [Step 6](#differentiate) we took a generic derivative of each expression, assuming all variables were functions of `xx`.  We now define a function that will select a specific dynamic variable (element of `dynamic_variables`) and set the derivative of the variable to 1 and all others to 0.

```python
# Step 8.a: Define onevar derivative function
def deriv_onevar(lhss_deriv,rhss_deriv,variable_list,index):
    # Denote each variable with prm
    variableprm_list = []
    for variable in variable_list:
        variableprm_list.append(str(variable)+"prm")

    # Copy expressions into another array
    lhss_deriv_new = []
    rhss_deriv_new = []
    for i in range(len(rhss_deriv)):
        lhss_deriv_new.append(lhss_deriv[i])
        rhss_deriv_new.append(rhss_deriv[i])
    # For each free symbol's derivative, replace it with:
    #   1, if we are differentiating with respect to the variable, or
    #   0, if we are note differentiating with respect to that variable
    for i in range(len(rhss_deriv_new)):
        for var in variableprm_list:
            if variableprm_list.index(str(var))==index:
                rhss_deriv_new[i] = rhss_deriv_new[i].subs(var,1)
            else:
                rhss_deriv_new[i] = rhss_deriv_new[i].subs(var,0)
    # Simplify derivative expressions again
    lhss_deriv_simp,rhss_deriv_simp = simplify_deriv(lhss_deriv_new,rhss_deriv_new)
    return lhss_deriv_simp,rhss_deriv_simp

# Step 8.b: Call the derivative function and populate dictionaries with the result
lhss_derivative = {}
rhss_derivative = {}
for index in range(len(dynamic_variables)):
    lhss_temp,rhss_temp = deriv_onevar(lhss_deriv,rhss_deriv,dynamic_variables,index)
    lhss_derivative[dynamic_variables[index]] = lhss_temp
    rhss_derivative[dynamic_variables[index]] = rhss_temp
```

# Step 9: Store partial derivatives to SymPy notebook `partial_derivatives.txt-VALIDATION.txt` \[Back to [top](#toc)\]
$$\label{store_results}$$

We write the resulting derivatives in SymPy syntax.  Each partial derivative is output in its own file, in a similar format to the input expressions.

```python
# Step 9: Output original expression and each partial derivative expression in SymPy snytax
with open(os.path.join(inputdir,'partial_derivatives.txt-VALIDATION'), 'w') as output:
    for i in range(len(lr)):
        right_side = lr[i].rhs
        right_side_in_sp = right_side.replace("sqrt(","sp.sqrt(").replace("log(","sp.log(").replace("pi",
                                                "sp.pi").replace("sign(","sp.sign(").replace("Abs(",
                                                "sp.Abs(").replace("Rational(","sp.Rational(")
        output.write(str(lr[i].lhs)+" = "+right_side_in_sp)
    for var in dynamic_variables:
        for i in range(len(lhss_derivative[var])):
            right_side = str(rhss_derivative[var][i])
            right_side_in_sp = right_side.replace("sqrt(","sp.sqrt(").replace("log(","sp.log(").replace("pi",
                                                "sp.pi").replace("sign(","sp.sign(").replace("Abs(",
                                                "sp.Abs(").replace("Rational(","sp.Rational(").replace("prm",
                                                "prm_"+str(var))
            output.write(str(lhss_derivative[var][i]).replace("prm","prm_"+str(var))+" = "+right_side_in_sp+"\n")
```

# Step 10: Validate against LALSuite and trusted `SEOBNR_Derivative_Routine` NRPy+ module \[Back to [top](#toc)\]
$$\label{code_validation}$$

We validate the output of this notebook against known LALSuite values of the Hamiltonian partial derivatives and the output of the `SEOBNR_Derivative_Routine` NRPy+ module.  We note that due to cancellations in the deriavtive terms, various versions of SymPy may result in relative errors that differ as much as an order of magnitude.  Furthermore, even changing the set of input pararameters can affect the relative error by as many as two orders of magnitude.  Therefore we look for agreement with LALSuite to at least 10 significant digits.

When comparing the notebook output to that of the NRPy+ module, we compare term-by-term using SymPy to check that each right-hand side side is equivalent.

```python
# Define a function to return a set of reasonable input parameters
# This function contains three distinct sets of input parameters, and index differentiates between them
def reset_values(tort_value, index):
    # Check that a reasonable tortoise value has been passed
    if tort_value!=1 and tort_value!=2:
        print("Error: tortoise must be 1 or 2.")
        sys.exit(1)
    # Each index corresponds to a specific set of input parameters
    if index==0:#-M 13 -m 11 -X 0.1 -Y -0.2 -Z 0.3 -x -0.3 -y 0.2 -z -0.1
        values = {'m1': 1.300000000000000e+01, 'm2': 1.100000000000000e+01, 'eta': 2.482638888888889e-01,
                  'x': 1.658426645098320e+01, 'y': 3.975021008701605e-02, 'z': -1.820682538442627e-07,
                  's1x': 2.934751675254397e-02, 's1y': -5.867672205485316e-02, 's1z': 8.802097562761332e-02,
                  's2x': -6.302678133897792e-02, 's2y': 4.200490780215727e-02, 's2z': -2.100705983874398e-02,
                  'KK': 3.913980338468737e-01, 'k0': -7.447639215330089e-01, 'k1': -6.380586501824999e-01,
                  'd1v2': -7.476323019145448e+01,'dheffSSv2':2.105103187692902e+01,
                  'EMgamma': 0.577215664901532860606512090082402431}
        # Note that we transform the momentum based on the tortoise values
        if tort_value==1:
            values.update({'px': -1.517631642228534e-03, 'py': 2.693180445886167e-01, 'pz': -1.320499830947482e-04,
                          'tortoise': 1})
        else:
            values.update({'px': -1.633028076483384e-03, 'py': 2.693177679992048e-01, 'pz': -1.320499918278832e-04,
                          'tortoise': 2})
    elif index==1:#-M 25 -m 10 -X 0.1 -Y -0.0 -Z 0.1 -x -0.2 -y 0.0 -z -0.2
        values = {'m1': 2.500000000000000e+01, 'm2': 1.000000000000000e+01, 'eta': 2.040816326530612e-01,
                  'x': 1.289689003662444e+01, 'y': 5.495441315063273e-03, 'z': -1.717482806041791e-11,
                  's1x': 5.102040816179230e-02, 's1y': 9.846215537206260e-07, 's1z': 5.102040816473832e-02,
                  's2x': -1.632653061189792e-02, 's2y': -6.762952223804450e-07, 's2z': -1.632653061259188e-02,
                  'KK': 5.642540639599580e-01, 'k0': -1.063532077165767e+00, 'k1': -8.835684149841774e-01,
                  'd1v2': -8.041179092044979e+01,'dheffSSv2':1.125986130778842e+01,
                  'EMgamma': 0.577215664901532860606512090082402431}
        if tort_value==1:
            values.update({'px': -1.898773926867491e-03, 'py': 3.160984442121970e-01, 'pz': 1.171602901570564e-07,
                           'tortoise': 1})
        else:
            values.update({'px': -2.209215477700561e-03, 'py': 3.160983119312114e-01, 'pz': 1.171602905704723e-07,
                          'tortoise': 2})
    elif index==2:#-M 7 -m 5 -X 0.01 -Y -0.5 -Z 0.03 -x -0.04 -y 0.05 -z -0.06
        values = {'m1': 7.000000000000000e+00, 'm2': 5.000000000000000e+00, 'eta': 2.430555555555556e-01,
                  'x': 2.633506161699224e+01, 'y': 7.574563213724998e-02, 'z': -2.789625823248071e-08,
                      's1x': 3.417297286269225e-03, 's1y': -1.701385963191495e-01, 's1z': 1.020835932957879e-02,
                      's2x': -6.945454346305877e-03, 's2y': 8.679766617922793e-03, 's2z': -1.041665076794264e-02,
                      'KK': 4.052853693162246e-01, 'k0': -7.706473492549312e-01, 'k1': -6.587426366263742e-01,
                      'd1v2': -7.555647472993827e+01,'dheffSSv2':1.972817669753086e+01,
                      'EMgamma': 0.577215664901532860606512090082402431}
        if tort_value==1:
            values.update({'px': -7.883793607066706e-04, 'py': 2.068742709904638e-01, 'pz': -7.338789145500886e-04,
                          'tortoise': 1})
        else:
            values.update({'px': -8.039726989861640e-04, 'py': 2.068742261404732e-01, 'pz': -7.338789145335709e-04,
                           'tortoise': 2})
    else:
        # If an improper index is passed, exit
        print("Error: invalid index (only three sets of input parameters available).")
        sys.exit(1)
    # Return the input values
    return values

# Numerically evaluate right-hand sides using input values
def evaluate_expression(left_sides,right_sides,input_values):
    new_right_sides = []
    for i in range(len(right_sides)):
        term = custom_parse_expr(str(right_sides[i]).replace("(xx)",""))
        # Only look for the free variables in each expression to reduce computation time
        free_vars = term.free_symbols
        for variable in free_vars:
            term = term.subs(variable, input_values[str(variable)])
        # Evaluate each term to reduce computation time
        new_right_sides.append(sp.sympify(term.evalf()))
        # Store each subexpression in values numerically
        input_values[str(left_sides[i])] = new_right_sides[i]
    # Return the input values dictionary with all numerical right-hand added
    return input_values

# Create array of trusted LALSuite derivative values
# Note that position in the array corresponds to the index of the corresponding input values
LALSuite_validated_values = []
#-M 13 -m 11 -X 0.1 -Y -0.2 -Z 0.3 -x -0.3 -y 0.2 -z -0.1
LALSuite_validated_values.append({'Hreal': 9.928923110195770e-01,'dHreal_dx': 9.932484846748471e-04,
                                  'dHreal_dy': 2.813294366789505e-06, 'dHreal_dz': 1.926378549762488e-06,
                                  'dHreal_dpx': -3.710666135737856e-04, 'dHreal_dpy': 6.116199124763537e-02,
                                  'dHreal_dpz': -5.600910364542288e-07, 'dHreal_ds1x': -1.438467658934620e-05,
                                  'dHreal_ds1y': -1.319462868057848e-06, 'dHreal_ds1z': 7.665413183773232e-04,
                                  'dHreal_ds2x': -2.075691477548065e-05,'dHreal_ds2y': 2.456427688083135e-06,
                                  'dHreal_ds2z': 8.762835349889455e-04})
#-M 25 -m 10 -X 0.1 -Y -0.0 -Z 0.1 -x -0.2 -y 0.0 -z -0.2
LALSuite_validated_values.append({'Hreal': 9.926852598351464e-01, 'dHreal_dx': 1.397519118422771e-03,
                                  'dHreal_dy': 1.928133240540033e-06, 'dHreal_dz': -1.215449398950413e-06,
                                  'dHreal_dpx': -4.004159849919695e-04, 'dHreal_dpy': 5.701850933742150e-02,
                                  'dHreal_dpz': 4.329487960716782e-08, 'dHreal_ds1x': 2.259457049322466e-06,
                                  'dHreal_ds1y': -2.544122765762015e-09, 'dHreal_ds1z': 9.834156257814124e-04,
                                  'dHreal_ds2x': 5.185557993931246e-06,'dHreal_ds2y': 2.437768415468806e-10,
                                  'dHreal_ds2z': 2.111169766641698e-03})
#-M 7 -m 5 -X 0.01 -Y -0.5 -Z 0.03 -x -0.04 -y 0.05 -z -0.06
LALSuite_validated_values.append({'Hreal': 9.955293642650920e-01, 'dHreal_dx': 3.734697245297603e-04,
                                  'dHreal_dy': 1.105998063449349e-06, 'dHreal_dz': 5.367207414282669e-08,
                                  'dHreal_dpx': -1.848412708548443e-04, 'dHreal_dpy': 4.754239153769983e-02,
                                  'dHreal_dpz': -3.549083643069269e-08, 'dHreal_ds1x': -4.819261725948465e-07,
                                  'dHreal_ds1y': 3.333280059627902e-06, 'dHreal_ds1z': 2.201786563823208e-04,
                                  'dHreal_ds2x': -7.576810957551029e-07,'dHreal_ds2y': 6.818093508597533e-06,
                                  'dHreal_ds2z': 2.922663340179887e-04})

# Sort variables by which tortoise value we use to compute the derivatives
variables_tort2 = [x,y,z]
variables_tort1 = [px,py,pz,s1x,s1y,s1z,s2x,s2y,s2z]

# Call evaluation function
print("Computing the difference between notebook output and trusted LALSuite derivative values...")
for index in range(3):
    values = reset_values(2,index)
    values = evaluate_expression(lhss,rhss,values)
    Hreal = values['Hreal']
    trusted_Hreal = LALSuite_validated_values[index]['Hreal']
    relative_difference = (trusted_Hreal - Hreal)/trusted_Hreal
    if abs(relative_difference) > 1e-9:
        print("The relative difference in Hreal is too large: %.15e" % relative_difference)
        sys.exit(1)

    for var in variables_tort2:
        Hrealprm = evaluate_expression(lhss_derivative[var],rhss_derivative[var],values)['Hrealprm']
        trusted_Hrealprm = LALSuite_validated_values[index]['dHreal_d'+str(var)]
        relative_difference = (trusted_Hrealprm - Hrealprm)/trusted_Hrealprm
        if abs(relative_difference) > 1e-9:
            print("The relative difference in dHreal_d%s is too large: %.15e" % (var,relative_difference))
            sys.exit(1)

    values = reset_values(1,index)
    values = evaluate_expression(lhss,rhss,values)

    for var in variables_tort1:
        Hrealprm = evaluate_expression(lhss_derivative[var],rhss_derivative[var],values)['Hrealprm']
        trusted_Hrealprm = LALSuite_validated_values[index]['dHreal_d'+str(var)]
        relative_difference = (trusted_Hrealprm - Hrealprm)/trusted_Hrealprm
        if abs(relative_difference) > 1e-9:
            print("The relative difference in dHreal_d%s is too large: %.15e" % (var,relative_difference))
            sys.exit(1)
print("Test passed: the notebook agrees with LALSuite to at least 10 significant digits!")

print("Printing difference between notebook output and trusted NRPy+ module output...")
# Open the files to compare
file = 'partial_derivatives.txt'
outfile = 'partial_derivatives.txt-VALIDATION'

print("Checking file " + outfile)
with open(os.path.join(inputdir,file), "r") as file1, open(os.path.join(inputdir,outfile), "r") as file2:
    # Read the lines of each file
    file1_lines = file1.readlines()
    file2_lines = file2.readlines()
    # Compare right-hand sides of the expressions by computing the difference between them
    num_diffs = 0
    for i in range(len(file1_lines)):
        expr_new = custom_parse_expr(file1_lines[i].split("=")[1].replace("sp.",""))
        expr_validated = custom_parse_expr(file2_lines[i].split("=")[1].replace("sp.",""))
        difference = sp.simplify(expr_new - expr_validated)
        if difference != 0:
            num_diffs += 1
            print(difference)
    if num_diffs == 0:
        print("No difference. TEST PASSED!")
    else:
        print("ERROR: Disagreement found with the trusted file. See differences above.")
        sys.exit(1)
```

    Computing the difference between notebook output and trusted LALSuite derivative values...
    Test passed: the notebook agrees with LALSuite to at least 10 significant digits!
    Printing difference between notebook output and trusted NRPy+ module output...
    Checking file partial_derivatives.txt-VALIDATION
    No difference. TEST PASSED!

# Step 11: Output this notebook to $\LaTeX$-formatted PDF file \[Back to [top](#toc)\]
$$\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-SEOBNR_Derivative_Routine.pdf](Tutorial-SEOBNR_Derivative_Routine.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-SEOBNR_Derivative_Routine")
```

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


# Project 2, Variational Monte Carlo studies of electronic systems. Deadline May 31

**[Computational Physics II FYS4411/FYS9411](http://www.uio.no/studier/emner/matnat/fys/FYS4411/index-eng.html)**, Department of Physics, University of Oslo, Norway

Date: **Jan 11, 2019**

Copyright 1999-2019, [Computational Physics II FYS4411/FYS9411](http://www.uio.no/studier/emner/matnat/fys/FYS4411/index-eng.html). Released under CC Attribution-NonCommercial 4.0 license

## Introduction

The aim of this project is to use the Variational Monte
Carlo (VMC) method to evaluate
the ground state energy, onebody densities, expectation values of the kinetic and potential energies
 and single-particle denisties of
quantum dots with $N=2$, $N=6$, $N=12$ and $N=20$ electrons. These are so-called closed shell systems.

## Theoretical background and description of the physical system

We consider a system of electrons confined in a pure two-dimensional
isotropic harmonic oscillator potential, with an idealized  total Hamiltonian given by

$$
\begin{equation}
\label{eq:finalH} \tag{1}
\hat{H}=\sum_{i=1}^{N} \left(  -\frac{1}{2} \nabla_i^2 + \frac{1}{2} \omega^2r_i^2  \right)+\sum_{i<j}\frac{1}{r_{ij}},
\end{equation}
$$

where natural units ($\hbar=c=e=m_e=1$) are used and all energies are in so-called atomic units a.u. We will study systems of many electrons $N$ as functions of the oscillator frequency  $\omega$ using the above Hamiltonian.  The Hamiltonian includes a standard harmonic oscillator part

$$
\hat{H}_0=\sum_{i=1}^{N} \left(  -\frac{1}{2} \nabla_i^2 + \frac{1}{2} \omega^2r_i^2  \right),
$$

and the repulsive interaction between two electrons given by

$$
\hat{H}_1=\sum_{i<j}\frac{1}{r_{ij}},
$$

with the distance between electrons given by $r_{ij}=\vert \boldsymbol{r}_1-\boldsymbol{r}_2\vert$. We define the
modulus of the positions of the electrons (for a given electron $i$) as $r_i = \sqrt{r_{i_x}^2+r_{i_y}^2}$.

### Project 2 a):

In exercises a-f we will deal only with a system of
two electrons in a quantum dot with a frequency of $\hbar\omega = 1$.
The reason for this is that we have exact closed form expressions
for the ground state energy from Taut's work for selected values of $\omega$,
see M. Taut, Phys. Rev. A **48**, 3561 (1993).
The energy is given by $3$ a.u.  (atomic units) when the interaction between the electrons is included.
If only the harmonic oscillator part of the Hamiltonian is included,
the so-called unperturbed part,

$$
\hat{H}_0=\sum_{i=1}^{N} \left(  -\frac{1}{2} \nabla_i^2 + \frac{1}{2} \omega^2r_i^2  \right),
$$

the energy is $2$ a.u.
The wave function for one electron in an oscillator potential in two dimensions is

$$
\phi_{n_x,n_y}(x,y) = A H_{n_x}(\sqrt{\omega}x)H_{n_y}(\sqrt{\omega}y)\exp{(-\omega(x^2+y^2)/2}.
$$

The functions $H_{n_x}(\sqrt{\omega}x)$ are so-called Hermite polynomials, discussed in connection with project 1  while $A$ is a normalization constant.
For the lowest-lying state we have $n_x=n_y=0$ and an energy $\epsilon_{n_x,n_y}=\omega(n_x+n_y+1) = \omega$.
Convince yourself that the lowest-lying energy for the two-electron system  is simply $2\omega$.

The unperturbed wave function for the ground state of the two-electron system is given by

$$
\Phi(\boldsymbol{r}_1,\boldsymbol{r}_2) = C\exp{\left(-\omega(r_1^2+r_2^2)/2\right)},
$$

with $C$ being a normalization constant and $r_i = \sqrt{r_{i_x}^2+r_{i_y}^2}$. Note that the vector $\boldsymbol{r}_i$
refers to the $x$ and $y$ position for a given particle.
What is the total spin of this wave function? Find arguments for why the ground state should have
this specific total spin.

### Project 2 b):

We want to perform  a Variational Monte Carlo calculation of the ground state of two electrons in a quantum dot well with different oscillator energies, assuming total spin $S=0$ using the Hamiltonian of
Eq. ([1](#eq:finalH)).
Our trial wave function which has the following form

$$
\begin{equation}
   \psi_{T}(\boldsymbol{r}_1,\boldsymbol{r}_2) =
   C\exp{\left(-\alpha\omega(r_1^2+r_2^2)/2\right)}
   \exp{\left(\frac{ar_{12}}{(1+\beta r_{12})}\right)},
\label{eq:trial} \tag{2}
\end{equation}
$$

where $a$ is equal to one when the two electrons have anti-parallel spins and $1/3$ when the spins are parallel. Finally, $\alpha$ and $\beta$ are our variational parameters. Note well the dependence on $\alpha$ for the single-particle part of the trial function. It is important to remember this when you use higher-order Hermite polynomials.
Find the analytical expressions for the local energy.

### Project 2 c):

Your task is to perform a Variational Monte Carlo calculation
using the Metropolis algorithm to compute the integral

$$
\begin{equation}
   \langle E \rangle =
   \frac{\int d\boldsymbol{r}_1d\boldsymbol{r}_2\psi^{\ast}_T(\boldsymbol{r}_1,\boldsymbol{r}_2)\hat{H}(\boldsymbol{r}_1,\boldsymbol{r}_2)\psi_T(\boldsymbol{r}_1,\boldsymbol{r}_2)}
        {\int d\boldsymbol{r}_1d\boldsymbol{r}_2\psi^{\ast}_T(\boldsymbol{r}_1,\boldsymbol{r}_2)\psi_T(\boldsymbol{r}_1,\boldsymbol{r}_2)}.
\label{_auto1} \tag{3}
\end{equation}
$$

Compute the expectation value of the energy using both the analytical expression for the local energy and numerical derivation of the kinetic energy. Compare the time usage between the two approaches.
Perform these calculations without importance sampling and also without the Jastrow factor. For the calculations without the Jastrow factor and repulsive Coulomb potential, your energy should equal 2.0 a.u. and your variance should be exactly equal to zero.

### Project 2 d):

Add now importance sampling and repeat the calculations from the previous exercise but use only
the analytical expression for the local energy. Perform also a blocking analysis in order to obtain the optimal
standard deviation. Compare your results with the those without importance sampling and comment your results.

### Project 2 e):

Using either the steepest descent method or the conjugate gradient method, find the optimal variational
parameters and perform your Monte Carlo calculations using these.
In addition, you should parallelize your program using MPI and set it up to run on Smaug.

### Project 2 f):

Finally, we wil now analyze and interpret our results for the two-electron systems.
Find the  energy minimum and discuss your results compared with the analytical solution from
Taut's work, see reference [1] below. Compute also the mean distance
$r_{12}=\vert \boldsymbol{r}_1-\boldsymbol{r}_2\vert$ (with $r_i = \sqrt{r_{i_x}^2+r_{i_y}^2}$) between the two electrons for the optimal set of the variational parameters.
With the optimal parameters for the ground state wave function, compute the onebody density. Discuss your results and compare the results with those obtained with a pure harmonic oscillator wave functions. Run a Monte Carlo calculations without the Jastrow factor as well
and compute the same quantities. How important are the correlations induced by the Jastrow factor?
Compute also the expectation value of the kinetic energy and potential energy using $\omega=0.01$, $\omega=0.05$,
$\omega=0.1$, $\omega=0.5$ and $\omega=1.0$. Comment your results. Hint, think of the virial theorem.

Discuss also your results with those obtained with Hartree-Fock theory for two electrons from project 1.

### Project 2 g):

The previous exercises have prepared you for extending your calculational machinery  to other systems.
Here we will focus on quantum dots with $N=6$ and $N=12$ electrons.

The new item you need to pay attention to is the calculation of the Slater Determinant. This is an additional complication
to your VMC calculations.
If we stick to harmonic oscillator like wave functions,
the trial wave function for say an $N=6$ electron quantum dot can be written as

$$
\begin{equation}
   \psi_{T}(\boldsymbol{r}_1,\boldsymbol{r}_2,\dots, \boldsymbol{r}_6) =
   Det\left(\phi_{1}(\boldsymbol{r}_1),\phi_{2}(\boldsymbol{r}_2),
   \dots,\phi_{6}(\boldsymbol{r}_6)\right)
   \prod_{i<j}^{6}\exp{\left(\frac{a r_{ij}}{(1+\beta r_{ij})}\right)},
\label{_auto2} \tag{4}
\end{equation}
$$

where $Det$ is a Slater determinant and the single-particle wave functions
are the harmonic oscillator wave functions for the $n_x=0,1$ and $n_y=0,1$ orbitals.
Similarly, for the $N=12$ quantum dot, the trial wave function can take the form

$$
\begin{equation}
   \psi_{T}(\boldsymbol{r}_1,\boldsymbol{r}_2, \dots,\boldsymbol{r}_{12}) =
   Det\left(\phi_{1}(\boldsymbol{r}_1),\phi_{2}(\boldsymbol{r}_2),
   \dots,\phi_{12}(\boldsymbol{r}_{12})\right)
   \prod_{i<j}^{12}\exp{\left(\frac{ar_{ij}}{(1+\beta r_{ij})}\right)},
\label{_auto3} \tag{5}
\end{equation}
$$

In this case you need to include the $n_x=2$ and $n_y=2$ wave functions as well.
Observe that $r_i = \sqrt{r_{i_x}^2+r_{i_y}^2}$.  Use the Hermite polynomials defined in project 1. Reference [5] gives benchmark results for closed-shell systems up to $N=20$.

Write a function which sets up the Slater determinant. Find the Hermite polynomials which are needed for $n_x=0,1,2$ and obviously $n_y$ as well.  Compare the results you obtain with those from project 1.
Compute the ground state energies of quantum dots for $N=6$ and $N=12$ electrons, following the same set up as in the previous exercises for $\omega=0.01$, $\omega=0.05$,
$\omega=0.1$, $\omega=0.5$, and $\omega=1.0$.
The calculations should include  parallelization, blocking, importance sampling and energy minimization using the conjugate gradient approach or similar approaches.
To test your Slater determinant code, you should reproduce the unperturbed single-particle energies
when the electron-electron repulsion is switched off. Convince yourself that the unperturbed ground state energies for $N=6$ is $10\omega$ and for $N=12$ we obtain $28\omega$. What is the expected total
spin of the ground states?

### Project 2 h):

With the optimal parameters for the ground state wave function, compute again the onebody density. Discuss your results and compare the results with those obtained with a pure harmonic oscillator
wave functions. Run a Monte Carlo calculations without the Jastrow factor as well
and compute the same quantities. How important are the correlations induced by the Jastrow factor?
Compute also the expectation value of the kinetic energy and potential energy using $\omega=0.01$,
$\omega=0.05$, $\omega=0.1$, $\omega=0.5$, and $\omega=1.0$. Comment your results.

### Project 2 i):

The last exercise  is a performance analysis of your code(s) for the case of $N=6$ electrons. Make a performance analysis by timing your serial code
with and without vectorization. Perform several runs with the same number of Monte carlo cycles and compute an average timing analysis
with and without vectorization. Comment your results. Use at least $10^6$ Monte Carlo samples.

Compare thereafter your serial code(s)  with the speedup you get by parallelizing your code, running either OpenMP or MPI or both.
Do you get a near $100\%$ speedup with the parallel version? Comment again your results and perform timing benchmarks several times in order
to extract  an average performance time.

### Literature

1. M. Taut, Phys. Rev. A **48**, 3561 - 3566 (1993).

2. B. L. Hammond, W. A. Lester and P. J. Reynolds, *Monte Carlo methods in Ab Initio Quantum Chemistry*, World Scientific, Singapore, 1994, chapters 2-5 and appendix B.

3. B. H. Bransden and C. J. Joachain, Physics of Atoms and molecules, Longman, 1986. Chapters 6, 7 and 9.

4. A. K.  Rajagopal and J. C. Kimball, see Phys. Rev. B **15**, 2819 (1977).

5. M. L. Pedersen, G. Hagen, M. Hjorth-Jensen, S. Kvaal,  and F. Pederiva, Phys. Rev. B **84**, 115302 (2011)

## Introduction to numerical projects

Here follows a brief recipe and recommendation on how to write a report for each
project.

  * Give a short description of the nature of the problem and the eventual  numerical methods you have used.

  * Describe the algorithm you have used and/or developed. Here you may find it convenient to use pseudocoding. In many cases you can describe the algorithm in the program itself.

  * Include the source code of your program. Comment your program properly.

  * If possible, try to find analytic solutions, or known limits in order to test your program when developing the code.

  * Include your results either in figure form or in a table. Remember to label your results. All tables and figures should have relevant captions and labels on the axes.

  * Try to evaluate the reliabilty and numerical stability/precision of your results. If possible, include a qualitative and/or quantitative discussion of the numerical stability, eventual loss of precision etc.

  * Try to give an interpretation of you results in your answers to  the problems.

  * Critique: if possible include your comments and reflections about the  exercise, whether you felt you learnt something, ideas for improvements and  other thoughts you've made when solving the exercise. We wish to keep this course at the interactive level and your comments can help us improve it.

  * Try to establish a practice where you log your work at the  computerlab. You may find such a logbook very handy at later stages in your work, especially when you don't properly remember  what a previous test version  of your program did. Here you could also record  the time spent on solving the exercise, various algorithms you may have tested or other topics which you feel worthy of mentioning.

## Format for electronic delivery of report and programs

The preferred format for the report is a PDF file. You can also use DOC or postscript formats or as an ipython notebook file.  As programming language we prefer that you choose between C/C++, Fortran2008 or Python. The following prescription should be followed when preparing the report:

  * Use Devilry to hand in your projects, log in  at  <http://devilry.ifi.uio.no> with your normal UiO username and password.

  * Upload **only** the report file!  For the source code file(s) you have developed please provide us with your link to your github domain.  The report file should include all of your discussions and a list of the codes you have developed.  The full version of the codes should be in your github repository.

  * In your github repository, please include a folder which contains selected results. These can be in the form of output from your code for a selected set of runs and input parameters.

  * Still in your github make a folder where you place your codes.

  * In this and all later projects, you should include tests (for example unit tests) of your code(s).

  * Comments  from us on your projects, approval or not, corrections to be made  etc can be found under your Devilry domain and are only visible to you and the teachers of the course.

Finally,
we encourage you to work two and two together. Optimal working groups consist of
2-3 students. You can then hand in a common report.