# Week 1

__Goals for this week__

We will talk about the organization of this course, including the weekly tasks and 3 "mini"-projects that await you during on during this semester.
We will introduce the frameworks you will use and a you will get a geniality quick course (rychlokurz geniality) in Python, required library and standard structures.

__What is this file?__

This is a Jupyter notebook. A file, that contains python scripts and also stores the binary results.

__How can I run it?__

Firs you have to run/configure your Jupyter server in your desired python environment.
Then just follow this notebook and run it cell by cell. Try to answer - code - the questions, so you'll get better grasp on python array/list/dictionary structures.

## Course Information
If nothing changes:
- __Weekly tasks [7 pts]:__ First 3 weeks (after this one), you will work on the same tasks for better understanding of how neural networks work.
- __Projects [5 pts, 13 pts, 20 pts]:__ You will work in pairs on 3 deep learning project with gradually increasing difficulty throughout the entire semester. You will present your progress during the consultations, which are marked so you will be scored continuously and for the final presentations of your solutions.
- __Midterm [15 pts]:__ This is gonna be fun
- __Exam [40 pts]__ This is gonna be a nightmare
(No worries, not for you, for me to create them)

### Feedback

- Please use
- Please fill our
- This notebook is a work in progress. If you notice a mistake, notify us, raise an issue or make a pull request on

### Python

We will use _Python 3.7_ compatible code in these notebooks, however it may work with older versions.
We assume that you have seen Python code before. If you have not, the quick course may help and also you should learn the basics as soon as possible (e.g. [W3Schools tutorial](https://www.w3schools.com/python/default.asp))).
Your first task is to configure python environment, Tensorflow (CPU or GPU based on you HW), PyTorch, and to practice python concepts in the scripts below.
You should understand it fully, otherwise review your knowledge before you proceed. You will need it in the following weeks.

```python
# Line comment as you
"""
Block comment
"""

# input and output
name=input()
print("Hello, "+name)

```

    Hello, Petko a Denko

```python
# variables don't need explicit type declaration
var = 'Neural Networks rule!'
var = 2021
var = 20.21
var = True
var = [29,1,2021]
var = {'NN':'Rule!', 'year':2021}

# Basic types
2021            # integer
36.5            # float
'cool'          # string
True, False     # boolean operands
None            # null-like operand
# python specific
[12,34,'hello','world']     # list
{123:456,'DL':'NN'}   # dictionary

# Type conversion
float('36.5')
int(36.5)
str(3.65)

# Basic operations
a = 2 + 5 - 2.5
a += 3
b = 2 ** 3  # exponentiation
print(a, b)
print(5 / 2)
print(5 // 2)  # Notice the difference between these two
print('DL' + 'NN')
# All compound assignment operators available
# including += -= *= **= /= //=
# pre/post in/decrementers not available (++ --)

# F-strings
print(f'1 + 2 = {1 + 2}, a = {a}')
print('1 + 2 = {1 + 2}, a = {a}')  # We need the f at the start for {} to work as expression wrappers.

```

    7.5 8
    2.5
    2
    DLNN
    1 + 2 = 3, a = 7.5
    1 + 2 = {1 + 2}, a = {a}

```python
# Conditions
if a > 4 and b < 3:  # and, or and not are the basic logical operators
    print('a')   # Indentation by spaces or tabs tells us where the statement belongs. print('a') is in the if.
elif b > 5:
    print('b')   # Indentation by spaces or tabs tells us where the statement belongs. print('b') is in the elif.
else:
    print('c')   # Indentation by spaces or tabs tells us where the statement belongs. print('c') is in the else.
print('d')       # But print('d') is outside, it will print every time.

# Loops
while a < 10:
    if b > 3:
        a += 1  # More indentation for code that is "deeper"
    else:
        a += 2
print(f'a = {a}')

# 'while' loops are not considered 'pythonic'. 'for' loops are more common
for char in 'string':
    print(char)

```

    b
    d
    a = 10.5
    s
    t
    r
    i
    n
    g

```python
# Lists - work like C arrays, but they are not dependent on element type
a = [1, 2.4, 10e-7, 0b1001, 'some "text"'] # embedded "" in '', works vice versa
len(a)  # Length
print(a[1])  # Second element
print(a[1:3])  # Second to third element
a.append(4)  # Adding at the end
del a[3]  # Removing at the index
print([])  # Empty array
print(a)

# This is why for loops are used more often
for el in a:
# but be careful with iterations over list which contain different var types - it is your responsibility
    print(el + 1)

```

```python
a = [1, 2.4, 10e-7, 0b1001]
# We can define lists with list comprehension statements
b = [el + 2 for el in a]
print(b)

```

    [3, 4.4, 2.000001, 11]

```python
# Dictionaries - key-based structures
a = {
    'layer_1': 'dense',
    5: 'five',
    4: 'four',
    'result': [1, 2, 3]
}
print(a['layer_1'])
a['layer_2'] = 'ReLU'
if 'result' in a:  # Does key exist?
    print(a['result'])
{}  # Empty
del a[5]  # Remove record

print()
print('Keys:')
for key in a:
    print(key)

print()
print('Keys and values:')
for key, value in a.items():
    print(key, ':', value)

# Dictionaries can be also defined via comprehension statement
a = {i: i**2 for i in [1, 2, 3, 4]}
print(a)

```

    dense
    [1, 2, 3]

    Keys:
    layer_1
    4
    result
    layer_2

    Keys and values:
    layer_1 : dense
    4 : four
    result : [1, 2, 3]
    layer_2 : ReLU
    {1: 1, 2: 4, 3: 9, 4: 16}

```python
# Most common and useful iterators
print('range(start, stop, step)')
for i in range(10,20,2):
    print(i)

print()
print('enumerate')
lowercase = ['a', 'b', 'c']
for i, el in enumerate(lowercase): # iterates over elements attaching the index order
    print(i, el)

print()
print('zip') # Like a zip - side-by-side merges lists together for iteration
uppercase = ['A', 'B', 'C']
numbers = [1,2,3]
for n, a, b in zip(numbers,lowercase, uppercase):
    print(n, a, b)

```

    range(start, stop, step)
    10
    12
    14
    16
    18

    enumerate
    0 a
    1 b
    2 c

    zip
    1 a A
    2 b B
    3 c C

```python
# Functions
def example_function(a, b=1, c=1):  # b and c have default values
    return a*b, a*c  # we return two values at the same time

a, b = example_function(1, 2, 3)  # and we can assign both values at the same time as well
print(a, b)
print(example_function(4))
print(example_function(5, 2))
print(example_function(5, c=2))  # Notice how do the arguments behave

# Classes
class A:

    def __init__(self, b):  # Constructor
        self.b = b  # Object variable

    def add_to_b(self, c):  # self is always the first argument and it references the object itself
        self.b += c

    def sub_from_b(self, c):
        self.add_to_b(-c)  # Calling object method

    def __str__(self): # every python class contain several default methods that start and end with __
        return f'Class A, b={self.b}'

    # be careful with naming and using underscores _
    # private, protected and public is expressed by underscores
    # default is public
    def foo(self):
        print("I'm public")

    def _bar(self):
        print("I'm protected")
    def __nope(self):

        print("You shall not print me, I'm private")

a = A(5)
a.add_to_b(1)
print(a.b)
a.sub_from_b(2)
print(a.b)
print(a)
a.foo()
a.bar()
a.nope()
```

### Linear Algebra

Neural network models can be defined using vectors and matrices, i.e. concepts from linear algebra.
The _DeepLearningBook_ dedicates first pages for linear algebra, we will be using a bit of it during this semester, therefore you should know how basic linear operations work. Some of the concepts were covered during your _Algebra and Discrete Mathematics_ course. Read the provided links to review necessary topics (note that there are some questions at the end of each page) and solve the exercises in this notebook.

#### Vectors
- [On vectors](https://www.mathsisfun.com/algebra/vectors.html)
- [On dot product](https://www.mathsisfun.com/algebra/vectors-dot-product.html)

In these labs we use _DeepLearningBook_ notation: simple italic for scalars $x$, lowercase bold italic for vectors $\boldsymbol{x}$ and uppercase bold italics for matrices $\boldsymbol{X}$.
Please, keep this notation in mind.

### NumPy

[Numpy](https://numpy.org/) is a popular Python library for scientific computation. It provides a convenient way of working with vectors and matrices.
Ideally try to use NumPy to solve these exercises.

```python
import numpy as np
```

__Exercise 1.1:__ Calculate the following:

$
\begin{align}
\boldsymbol{a} = \begin{bmatrix}0 \\ 1 \\ 3 \end{bmatrix} \ \
\boldsymbol{b} = \begin{bmatrix}2 \\ 4 \\ 1 \end{bmatrix}
\end{align}
$

$5\boldsymbol{a} = ?$

$\boldsymbol{a} + \boldsymbol{b} = ?$

$\boldsymbol{a} \cdot \boldsymbol{b} = ?$

$||\boldsymbol{a}|| = ?$

TODO

```python
# Init vectors
a = np.array([0, 1, 3])
b = np.array([2, 4, 1])

# Basic operations, results for E 1.1
print(5*a)
print(a + b)
print(np.dot(a, b))
print(np.linalg.norm(a))
```

    [ 0  5 15]
    [2 5 4]
    7
    3.1622776601683795

__Exercise 1.2:__ Determine quickly whether or not two vectors (e.g. $\boldsymbol{a}$ and $\boldsymbol{b}$) are orthogonal (perpendicular)?

TODO

```python
# Perpendicularity (similarity) of vectors
np.linalg.norm(a - b)
```

    4.123105625617661

__Exercise 1.3:__ Compute which vector is longer, $\boldsymbol{a}$ or $\boldsymbol{b}$?

```python
# Length of vectors
len_a = np.linalg.norm(a)
len_b = np.linalg.norm(b)

print('a' if len_a > len_b else 'b')
```

    b

#### Matrices
- [On matrices](https://www.mathsisfun.com/algebra/matrix-introduction.html)
- [On matrix multiplication](https://www.mathsisfun.com/algebra/matrix-multiplying.html)

__INFO: NumPy array indexing__

```python
# Indexing, i.e. selecting elements from an array / vector / matrix

W = np.array([
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
])

W[1, 1]  # Element from second row of second column
W[0]  # First row
W[[0, 2]]  # First AND third row
W[:, 0]  # First column
W[1, [0, 2]]  # First AND third column of second row

# Access array slices by index
a = np.zeros([10,10])
a[:3] = 1
a[:, :3] = 2
a[:3, :3] = 3
rows = [4,6,7]
cols = [9,3,5]
a[rows, cols] = 4
print(a)

# transposition
a = np.arange(24).reshape(2,3,4)
print(a.shape)
print(a)
a=np.transpose(a, (2,1,0))
# swap 0th and 2nd axes
print(a.shape)
print(a)

```

    [[3. 3. 3. 1. 1. 1. 1. 1. 1. 1.]
     [3. 3. 3. 1. 1. 1. 1. 1. 1. 1.]
     [3. 3. 3. 1. 1. 1. 1. 1. 1. 1.]
     [2. 2. 2. 0. 0. 0. 0. 0. 0. 0.]
     [2. 2. 2. 0. 0. 0. 0. 0. 0. 4.]
     [2. 2. 2. 0. 0. 0. 0. 0. 0. 0.]
     [2. 2. 2. 4. 0. 0. 0. 0. 0. 0.]
     [2. 2. 2. 0. 0. 4. 0. 0. 0. 0.]
     [2. 2. 2. 0. 0. 0. 0. 0. 0. 0.]
     [2. 2. 2. 0. 0. 0. 0. 0. 0. 0.]]
    (2, 3, 4)
    [[[ 0  1  2  3]
      [ 4  5  6  7]
      [ 8  9 10 11]]

     [[12 13 14 15]
      [16 17 18 19]
      [20 21 22 23]]]
    (4, 3, 2)
    [[[ 0 12]
      [ 4 16]
      [ 8 20]]

     [[ 1 13]
      [ 5 17]
      [ 9 21]]

     [[ 2 14]
      [ 6 18]
      [10 22]]

     [[ 3 15]
      [ 7 19]
      [11 23]]]

__Exercise 1.4:__ Calculate the following. Vectors are columns by default.

$
\boldsymbol{C} = \begin{bmatrix}0 & 2 & 4\\ 1 & 2 & 5 \end{bmatrix}
\boldsymbol{d} = \begin{bmatrix} 1 & 7 \end{bmatrix}
\boldsymbol{E} = \begin{bmatrix} 1 & 2 \\ 3 & 4 \\ \end{bmatrix}
$

$\boldsymbol{C}\boldsymbol{d} = ?$

$\boldsymbol{C}\boldsymbol{E} = ?$

$\boldsymbol{d}^T \boldsymbol{C} - \boldsymbol{d}^T = ?$

$\boldsymbol{C}^T\boldsymbol{d} = ?$

$\boldsymbol{C}\boldsymbol{d}^T = ?$

$\boldsymbol{d}\boldsymbol{E} = ?$

```python
# Init matrices
# One way:
C = np.array([
    [0, 2, 4],
    [1, 2, 5]
])

d = np.array([1, 7])

# Other way:
E = np.arange(4).reshape(2, 2)

print(C.T * d)
print(C.T @ E)
print(d * C.T - d)
```

    [[ 0  7]
     [ 2 14]
     [ 4 35]]
    [[ 2  3]
     [ 4  8]
     [10 19]]
    [[-1  0]
     [ 1  7]
     [ 3 28]]

__Exercise 1.5:__ We can express the result of general matrix-vector product $\boldsymbol{Ex}_1$ as a vector of dot products.
Is it possible to do the same with $\boldsymbol{x}_2^T\boldsymbol{E}$?

```python
# There is a difference between a 1-D vector and a column matrix in numpy:
x1 = np.array([1, 2])  # This is a vector
x2 = np.array([  # This is a matrix
    [1],
    [2]
])

# First let's see the dimensions of these two
print(x1.shape)
print(x2.shape)

# Matrix - vector multiplicataion
# Then we can multiply them with E using np.matmul or @ matrix multiplication operator
print(E @ x1)

# TODO x_2^T \times E

```

    (2,)
    (2, 1)
    [2 8]
    [[2]
     [8]]

__Exercise 1.6:__ What is the difference between the two results from previous code cell?

TODO actually, just think about the answer ;)

### Derivatives

The final topic to cover are derivatives.
Almost all training algorithms of neural networks in practice are based on calculating the derivatives with respect to (w.r.t.) parameters of the model.
You should know the basics from your _Calculus course_ (Matematická analýza), but just in case, we recommend you to read the following to refresh your memory:

- [On derivatives](https://www.mathsisfun.com/calculus/derivatives-introduction.html)

You won't need to use derivatives during this course, so you won't need to learn all the [derivative rules](https://www.mathsisfun.com/calculus/derivatives-rules.html). However we need you to have an intuition about what derivatives are and what is their geometric interpretation. In essence, we need you to understand that a derivative tells us what is the slope of the tangent at given point. You should understand what is happening in the gif below:

```python
import matplotlib.pyplot as plt
plt.figure(figsize=(6,6))
tangents = plt.imread('images/tangents.gif')
plt.imshow(tangents)
```

License: en:User:Dino, User:Lfahlberg CC BY-SA 3.0, via Wikimedia Commons

Partial derivatives are a concept you might have not head about before.
It is applied when we derive a function with more than one variable.
In such case we can actually derive a function in any direction.

Read the following link:

- [On partial derivatives](https://www.mathsisfun.com/calculus/derivatives-partial.html)

Function of one variable (1D) is a curve, and of two variables (2D) is a topographical relief.
2D function can be visualized by a 3D graph.
In this graph you can pick a point and then ask, what is a slope of the tangent in any direction.
Most commonly you would calculate the slope along axes of both variables (let's say $x,y$): $\frac{df}{dx}$ and $\frac{df}{dy}$.

Vector of derivatives w.r.t. all the parameters is called a _gradient_.
Generally for function $f$ with arbitrary number of parameters $x_1. x_2, ..., x_N = \boldsymbol{x}$, the gradient $\triangledown f$ is defined as:

\begin{equation}
\triangledown f(\boldsymbol{x}) = \frac{df}{d\boldsymbol{x}} = \begin{bmatrix}\frac{df}{dx_1} \\ \frac{df}{dx_2} \\ \vdots \\ \frac{df}{dx_N} \end{bmatrix}
\end{equation}

Gradient is the most important concept from this week's lab. The gradient is a vector quantity that tells us the _direction of steepest ascent_ at each point. This is a very important property, which we will often use in the following weeks. The magnitude of this vector tells us how steep this ascent is, i.e. what is the slope of the tangent in the direction of the gradient.

To cpmpare _derivative_ and _gradient_:

- _Derivative_ is a quantity that tells us, what is the rate of change in given direction.
- _Gradient_ is a quantity that tells us what is the direction of the steepest rate of change, along with the rate of this change.

Observe the difference between these two concepts in the Figure below. All the plots show the same function $F(x,y) = \sin(x) \cos(y)$. In first two plots we shot the derivatives w.r.t $y$ and $x$ respectively. These are shown as white arrows. Notice that they all point in one direction. On the other hand in the last plot we show the gradients. If we interpret the derivatives from the two previous plots as vectors, these gradients are in fact their sum.

```python
from backstage import plots
plots.derivatives_plot()
```

__Exercise 1.8:__ With the following derivative rules:
- $(af)' = af'$
- $(f + g)' = f' + g'$
- $(x^k)' = kx^{k-1}$

Calculate the following:

$f(x^2 + y^2 + 2x)$

$\frac{df}{dx}=?$

$\frac{df}{dy}=?$

$\triangledown f(x, y) = ?$

$g(x_1, x_2, \dots, x_N) = g(\boldsymbol{x}) = \boldsymbol{a} \cdot \boldsymbol{x}$

$\triangledown g(\boldsymbol{x}) = ?$

```python
# TODO Derivatives ... by hand... on your paper
```

### Correct Answers

__E 1.4:__

The term $\boldsymbol{C}\boldsymbol{d}^T$ is not valid. You can not multiply two matrices with dimensions $3 \times 2$ and $1 \times 2$.
Also the term $\boldsymbol{E}\boldsymbol{d} is also invalid.

__E 1.7:__

$\frac{df}{dx}= 2x + 2$

$\frac{df}{dy}= 2y$

$\triangledown f(x, y)  = \begin{bmatrix}2x + 2 \\ 2y \end{bmatrix} $

$\triangledown g(\boldsymbol{x})  = \boldsymbol{a}$

```python

```

Created in Deepnote


```python
"""Intermolecular Interactions and Symmetry-Adapted Perturbation Theory"""

__authors__ = "Konrad Patkowski"
__email__   = ["patkowsk@auburn.edu"]

__copyright__ = "(c) 2008-2020, The Psi4Education Developers"
__license__   = "BSD-3-Clause"
__date__      = "2020-07-16"
```

This lab activity is designed to teach students about weak intermolecular interactions, and the calculation and interpretation of the interaction energy between two molecules. The interaction energy can be broken down into physically meaningful contributions (electrostatics, induction, dispersion, and exchange) using symmetry-adapted perturbation theory (SAPT). In this exercise, we will calculate complete interaction energies and their SAPT decomposition using the procedures from the Psi4 software package, processing and analyzing the data with NumPy and Matplotlib.

Prerequisite knowledge: the Hartree-Fock method, molecular orbitals, electron correlation and the MP2 theory. The lab also assumes all the standard Python prerequisites of all Psi4Education labs.

Learning Objectives:
1. Recognize and appreciate the ubiquity and diversity of intermolecular interactions.
2. Compare and contrast the supermolecular and perturbative methods of calculating interaction energy.
3. Analyze and interpret the electrostatic, induction, dispersion, and exchange SAPT contributions at different intermolecular separations.

Author: Konrad Patkowski, Auburn University (patkowsk@auburn.edu; ORCID: 0000-0002-4468-207X)

Copyright: Psi4Education Project, 2020

# Weak intermolecular interactions

In this activity, you will examine some properties of weak interactions between molecules. As the molecular subunits are not connected by any covalent (or ionic) bonds, we often use the term *noncovalent interactions*. Suppose we want to calculate the interaction energy between molecule A and molecule B for a certain geometry of the A-B complex (obviously, this interaction energy depends on how far apart the molecules are and how they are oriented). The simplest way of doing so is by subtraction (in the so-called *supermolecular approach*):

\begin{equation}
E_{\rm int}=E_{\rm A-B}-E_{\rm A}-E_{\rm B}
\end{equation}

where $E_{\rm X}$ is the total energy of system X, computed using our favorite electronic structure theory and basis set. A negative value of $E_{\rm int}$ means that A and B have a lower energy when they are together than when they are apart, so they do form a weakly bound complex that might be stable at least at very low temperatures. A positive value of $E_{\rm int}$ means that the A-B complex is unbound - it is energetically favorable for A and B to go their separate ways.

Let's consider a simple example of two interacting helium atoms and calculate $E_{\rm int}$ at a few different interatomic distances $R$. You will use Psi4 to calculate the total energies that you need to perform subtraction. When you do so for a couple different $R$, you will be able to sketch the *potential energy curve* - the graph of $E_{\rm int}(R)$ as a function of $R$.

OK, but how should you pick the electronic structure method to calculate $E_{\rm A-B}$, $E_{\rm A}$, and $E_{\rm B}$? Let's start with the simplest choice and try out the Hartree-Fock (HF) method. In case HF is not accurate enough, we will also try the coupled-cluster method with single, double, and perturbative triple excitations - CCSD(T). If you haven't heard about CCSD(T) before, let's just state that it is **(1)** usually very accurate (it's even called the *gold standard* of electronic structure theory) and **(2)** very expensive for larger molecules. For the basis set, let's pick the augmented correlation consistent triple-zeta (aug-cc-pVTZ) basis of Dunning which should be quite OK for both HF and CCSD(T).

```python
# A simple Psi4 input script to compute the potential energy curve for two helium atoms

%matplotlib notebook
import time
import numpy as np
import scipy
from scipy.optimize import *
np.set_printoptions(precision=5, linewidth=200, threshold=2000, suppress=True)
import psi4
import matplotlib.pyplot as plt

# Set Psi4 & NumPy Memory Options
psi4.set_memory('2 GB')
psi4.core.set_output_file('output.dat', False)

numpy_memory = 2

psi4.set_options({'basis': 'aug-cc-pVTZ',
              'e_convergence': 1e-10,
              'd_convergence': 1e-10,
              'INTS_TOLERANCE': 1e-15})

```

We need to collect some data points to graph the function $E_{\rm int}(R)$. Therefore, we set up a list of distances $R$ for which we will run the calculations (we go with 11 of them). For each distance, we need to remember three values ($E_{\rm A-B}$, $E_{\rm A}$, and $E_{\rm B}$). For this purpose, we will prepare two $11\times 3$ NumPy arrays to hold the HF and CCSD(T) results.

```python
distances = [4.0,4.5,5.0,5.3,5.6,6.0,6.5,7.0,8.0,9.0,10.0]
ehf = np.zeros((11,3))
eccsdt = np.zeros((11,3))

```

We are almost ready to crunch some numbers! One question though: how are we going to tell Psi4 whether we want $E_{\rm A-B}$, $E_{\rm A}$, or $E_{\rm B}$?
We need to define three different geometries. The $E_{\rm A-B}$ one has two helium atoms $R$ atomic units from each other - we can place one atom at $(0,0,0)$ and the other at $(0,0,R)$. The other two geometries involve one actual helium atom, with a nucleus and two electrons, and one *ghost atom* in place of the other one. A ghost atom does not have a nucleus or electrons, but it does carry the same basis functions as an actual atom - we need to calculate all energies in the same basis set, with functions centered at both $(0,0,0)$ and $(0,0,R)$, to prevent the so-called *basis set superposition error*. In Psi4, the syntax `Gh(X)` denotes a ghost atom where basis functions for atom type X are located.

Using ghost atoms, we can now easily define geometries for the $E_{\rm A}$ and $E_{\rm B}$ calculations.

```python
for i in range(len(distances)):
  dimer = psi4.geometry("""
  He 0.0 0.0 0.0
  --
  He 0.0 0.0 """+str(distances[i])+"""
  units bohr
  symmetry c1
  """)

  psi4.energy('ccsd(t)')   #HF will be calculated along the way
  ehf[i,0] = psi4.variable('HF TOTAL ENERGY')
  eccsdt[i,0] = psi4.variable('CCSD(T) TOTAL ENERGY')
  psi4.core.clean()

  monomerA = psi4.geometry("""
  He 0.0 0.0 0.0
  --
  Gh(He) 0.0 0.0 """+str(distances[i])+"""
  units bohr
  symmetry c1
  """)

  psi4.energy('ccsd(t)')   #HF will be calculated along the way
  ehf[i,1] = psi4.variable('HF TOTAL ENERGY')
  eccsdt[i,1] = psi4.variable('CCSD(T) TOTAL ENERGY')
  psi4.core.clean()

  monomerB = psi4.geometry("""
  Gh(He) 0.0 0.0 0.0
  --
  He 0.0 0.0 """+str(distances[i])+"""
  units bohr
  symmetry c1
  """)

  psi4.energy('ccsd(t)')   #HF will be calculated along the way
  ehf[i,2] = psi4.variable('HF TOTAL ENERGY')
  eccsdt[i,2] = psi4.variable('CCSD(T) TOTAL ENERGY')
  psi4.core.clean()

```

We have completed the $E_{\rm A-B}$, $E_{\rm A}$, or $E_{\rm B}$ calculations for all 11 distances $R$ (it didn't take that long, did it?). We will now perform the subtraction to form NumPy arrays with $E_{\rm int}(R)$ values for each method, converted from atomic units (hartrees) to kcal/mol, and graph the resulting potential energy curves using the matplotlib library.

```python
#COMPLETE the two lines below to generate interaction energies. Convert them from atomic units to kcal/mol.
einthf =
eintccsdt =

print ('HF PEC',einthf)
print ('CCSD(T) PEC',eintccsdt)

plt.plot(distances,einthf,'r+',linestyle='-',label='HF')
plt.plot(distances,eintccsdt,'bo',linestyle='-',label='CCSD(T)')
plt.hlines(0.0,4.0,10.0)
plt.legend(loc='upper right')
plt.show()

```

*Questions*
1. Which curve makes more physical sense?
2. Why does helium form a liquid at very low temperatures?
3. You learned in freshman chemistry that two helium atoms do not form a molecule because there are two electrons on a bonding orbital and two electrons on an antibonding orbital. How does this information relate to the behavior of HF (which does assume a molecular orbital for every electron) and CCSD(T) (which goes beyond the molecular orbital picture)?
4. When you increase the size of the interacting molecules, the CCSD(T) method quickly gets much more expensive and your calculation might take weeks instead of seconds. It gets especially expensive for the calculation of $E_{\rm A-B}$ because A-B has more electrons than either A or B. Your friend suggests to use CCSD(T) only for the easier terms $E_{\rm A}$ and $E_{\rm B}$ and subtract them from $E_{\rm A-B}$ calculated with a different, cheaper method such as HF. Why is this a really bad idea?

*To answer the questions above, please double click this Markdown cell to edit it. When you are done entering your answers, run this cell as if it was a code cell, and your Markdown source will be recompiled.*

A nice feature of the supermolecular approach is that it is very easy to use - you just need to run three standard energy calculations, and modern quantum chemistry codes such as Psi4 give you a lot of methods to choose from. However, the accuracy of subtraction hinges on error cancellation, and we have to be careful to ensure that the errors do cancel between $E_{\rm A-B}$ and $E_{\rm A}+E_{\rm B}$. Another drawback of the supermolecular approach is that it is not particularly rich in physical insight. All that we get is a single number $E_{\rm int}$ that tells us very little about the underlying physics of the interaction. Therefore, one may want to find an alternative approach where $E_{\rm int}$ is computed directly, without subtraction, and it is obtained as a sum of distinct, physically meaningful terms. Symmetry-adapted perturbation theory (SAPT) is such an alternative approach.

# Symmetry-Adapted Perturbation Theory (SAPT)

SAPT is a perturbation theory aimed specifically at calculating the interaction energy between two molecules. Contrary to the supermolecular approach, SAPT obtains the interaction energy directly - no subtraction of similar terms is needed. Moreover, the result is obtained as a sum of separate corrections accounting for the electrostatic, induction, dispersion, and exchange contributions to interaction energy, so the SAPT decomposition facilitates the understanding and physical interpretation of results.
- *Electrostatic energy* arises from the Coulomb interaction between charge densities of isolated molecules.
- *Induction energy* is the energetic effect of mutual polarization between the two molecules.
- *Dispersion energy* is a consequence of intermolecular electron correlation, usually explained in terms of correlated fluctuations of electron density on both molecules.
- *Exchange energy* is a short-range repulsive effect that is a consequence of the Pauli exclusion principle.

In this activity, we will explore the simplest level of the SAPT theory called SAPT0 (see [Parker:2014] for the definitions of different levels of SAPT). A particular SAPT correction $E^{(nk)}$ corresponds to effects that are of $n$th order in the intermolecular interaction and $k$th order in the intramolecular electron correlation. In SAPT0, intramolecular correlation is neglected, and intermolecular interaction is included through second order:

\begin{equation}
E_{\rm int}^{\rm SAPT0}=E^{(10)}_{\rm elst}+E^{(10)}_{\rm exch}+E^{(20)}_{\rm ind,resp}+E^{(20)}_{\rm exch-ind,resp}+E^{(20)}_{\rm disp}+E^{(20)}_{\rm exch-disp}+\delta E^{(2)}_{\rm HF}
\end{equation}

In this equation, the consecutive corrections account for the electrostatic, first-order exchange, induction, exchange induction, dispersion, and exchange dispersion effects, respectively. The additional subscript ''resp'' denotes that these corrections are computed including response effects - the HF orbitals of each molecule are relaxed in the electric field generated by the other molecule. The last term $\delta E^{(2)}_{\rm HF}$ approximates third- and higher-order induction and exchange induction effects and is taken from a supermolecular HF calculation.

Sticking to our example of two helium atoms, let's now calculate the SAPT0 interaction energy contributions using Psi4. In the results that follow, we will group $E^{(20)}_{\rm ind,resp}$, $E^{(20)}_{\rm exch-ind,resp}$, and $\delta E^{(2)}_{\rm HF}$ to define the total induction effect (including its exchange quenching), and group $E^{(20)}_{\rm disp}$ with $E^{(20)}_{\rm exch-disp}$ to define the total dispersion effect.

```python
distances = [4.0,4.5,5.0,5.3,5.6,6.0,6.5,7.0,8.0,9.0,10.0]
eelst = np.zeros((11))
eexch = np.zeros((11))
eind = np.zeros((11))
edisp = np.zeros((11))
esapt = np.zeros((11))

for i in range(len(distances)):
  dimer = psi4.geometry("""
  He 0.0 0.0 0.0
  --
  He 0.0 0.0 """+str(distances[i])+"""
  units bohr
  symmetry c1
  """)

  psi4.energy('sapt0')
  eelst[i] = psi4.variable('SAPT ELST ENERGY') * 627.509
  eexch[i] = psi4.variable('SAPT EXCH ENERGY') * 627.509
  eind[i] = psi4.variable('SAPT IND ENERGY') * 627.509
  edisp[i] = psi4.variable('SAPT DISP ENERGY') * 627.509
  esapt[i] = psi4.variable('SAPT TOTAL ENERGY') * 627.509
  psi4.core.clean()

plt.close()
plt.ylim(-0.2,0.4)
plt.plot(distances,eelst,'r+',linestyle='-',label='SAPT0 elst')
plt.plot(distances,eexch,'bo',linestyle='-',label='SAPT0 exch')
plt.plot(distances,eind,'g^',linestyle='-',label='SAPT0 ind')
plt.plot(distances,edisp,'mx',linestyle='-',label='SAPT0 disp')
plt.plot(distances,esapt,'k*',linestyle='-',label='SAPT0 total')
plt.hlines(0.0,4.0,10.0)
plt.legend(loc='upper right')
plt.show()

```

*Questions*
1. What is the origin of attraction between two helium atoms?
2. For the interaction of two helium atoms, which SAPT terms are *long-range* (vanish with distance like some inverse power of $R$) and which are *short-range* (vanish exponentially with $R$ just like the overlap of molecular orbitals)?
3. The dispersion energy decays at large $R$ like $R^{-n}$. Find the value of $n$ by fitting a function to the five largest-$R$ results. You can use `scipy.optimize.curve_fit` to perform the fitting, but you have to define the appropriate function first.
Does the optimal exponent $n$ obtained by your fit agree with what you know about van der Waals dispersion forces? Is the graph of dispersion energy shaped like the $R^{-n}$ graph for large $R$? What about intermediate $R$?

*Do you know how to calculate $R^{-n}$ if you have an array with $R$ values? If not, look it up in the NumPy documentation!*

```python
#COMPLETE the definition of function f below.
def f

ndisp = scipy.optimize.curve_fit(f,distances[-5:],edisp[-5:])
print ("Optimal dispersion exponent:",ndisp[0][0])

```

# Interaction between two water molecules

For the next part, you will perform the same analysis and obtain the supermolecular and SAPT0 data for the interaction of two water molecules. We now have many more degrees of freedom: in addition to the intermolecular distance $R$, we can change the relative orientation of two molecules, or even their internal geometries (O-H bond lengths and H-O-H angles). In this way, the potential energy curve becomes a multidimensional *potential energy surface*. It is hard to graph functions of more than two variables, so we will stick to the distance dependence of the interaction energies. Therefore, we will assume one particular orientation of two water molecules (a hydrogen-bonded one) and vary the intermolecular distance $R$ while keeping the orientation, and molecular geometries, constant. The geometry of the A-B complex has been defined for you, but you have to request all the necessary Psi4 calculations and extract the numbers that you need. To save time, we will downgrade the basis set to aug-cc-pVDZ and use MP2 (an approximate method that captures most of electron correlation) in place of CCSD(T).

*Hints:* To prepare the geometries for the individual water molecules A and B, copy and paste the A-B geometry, but use the Gh(O2)... syntax to define the appropriate ghost atoms. Remember to run `psi4.core.clean()` after each calculation.

```python
distances_h2o = [2.7,3.0,3.5,4.0,4.5,5.0,6.0,7.0,8.0,9.0]
ehf_h2o = np.zeros((10,3))
emp2_h2o = np.zeros((10,3))
psi4.set_options({'basis': 'aug-cc-pVDZ'})

for i in range(len(distances_h2o)):
  dimer = psi4.geometry("""
  O1
  H1 O1 0.96
  H2 O1 0.96 H1 104.5
  --
  O2 O1 """+str(distances_h2o[i])+""" H1 5.0 H2 0.0
  X O2 1.0 O1 120.0 H2 180.0
  H3 O2 0.96 X 52.25 O1 90.0
  H4 O2 0.96 X 52.25 O1 -90.0
  units angstrom
  symmetry c1
  """)

#COMPLETE the MP2 energy calculations for A-B, A, and B, and prepare the data for the graph.
#Copy and paste the A-B geometry, but use the Gh(O2)... syntax to define the appropriate ghost atoms for the A and B calculations.
#Remember to run psi4.core.clean() after each calculation.

print ('HF PEC',einthf_h2o)
print ('MP2 PEC',eintmp2_h2o)

plt.close()
plt.plot(distances_h2o,einthf_h2o,'r+',linestyle='-',label='HF')
plt.plot(distances_h2o,eintmp2_h2o,'bo',linestyle='-',label='MP2')
plt.hlines(0.0,2.5,9.0)
plt.legend(loc='upper right')
plt.show()

```

```python
eelst_h2o = np.zeros((10))
eexch_h2o = np.zeros((10))
eind_h2o = np.zeros((10))
edisp_h2o = np.zeros((10))
esapt_h2o = np.zeros((10))

#COMPLETE the SAPT calculations for 10 distances to prepare the data for the graph.

plt.close()
plt.ylim(-10.0,10.0)
plt.plot(distances_h2o,eelst_h2o,'r+',linestyle='-',label='SAPT0 elst')
plt.plot(distances_h2o,eexch_h2o,'bo',linestyle='-',label='SAPT0 exch')
plt.plot(distances_h2o,eind_h2o,'g^',linestyle='-',label='SAPT0 ind')
plt.plot(distances_h2o,edisp_h2o,'mx',linestyle='-',label='SAPT0 disp')
plt.plot(distances_h2o,esapt_h2o,'k*',linestyle='-',label='SAPT0 total')
plt.hlines(0.0,2.5,9.0)
plt.legend(loc='upper right')
plt.show()

```

Before we proceed any further, let us check one thing about your first MP2 water-water interaction energy calculation, the one that produced `eintmp2_h2o[0]`. Here's the geometry of that complex again:

```python
#all x,y,z in Angstroms
atomtypes = ["O1","H1","H2","O2","H3","H4"]
coordinates = np.array([[0.116724185090,     1.383860971547,     0.000000000000],
                        [0.116724185090,     0.423860971547,     0.000000000000],
                        [-0.812697549673,    1.624225775439,     0.000000000000],
                        [-0.118596320329,   -1.305864713301,     0.000000000000],
                        [0.362842754701,    -1.642971982825,    -0.759061990794],
                        [0.362842754701,    -1.642971982825,     0.759061990794]])

```

First, write the code to compute the four O-H bond lengths and two H-O-H bond angles in the two molecules. *(Hint: if the angles look weird, maybe they are still in radians - don't forget to convert them to degrees.)* Are the two water molecules identical?

Then, check the values of the MP2 energy for these two molecules (the numbers $E_{\rm A}$ and $E_{\rm B}$ that you subtracted to get the interaction energy). If the molecules are the same, why are the MP2 energies close but not the same?

*Hints:* The most elegant way to write this code is to define functions `distance(point1,point2)` for the distance between two points $(x_1,y_1,z_1)$ and $(x_2,y_2,z_2)$, and `angle(vec1,vec2)` for the angle between two vectors $(x_{v1},y_{v1},z_{v1})$ and $(x_{v2},y_{v2},z_{v2})$. Recall that the cosine of this angle is related to the dot product $(x_{v1},y_{v1},z_{v1})\cdot(x_{v2},y_{v2},z_{v2})$. If needed, check the documentation on how to calculate the dot product of two NumPy vectors.

When you are parsing the NumPy array with the coordinates, remember that `coordinates[k,:]` is the vector of $(x,y,z)$ values for atom number $k$, $k=0,1,2,\ldots,N_{\rm atoms}-1$.

```python

#COMPLETE the distance and angle calculations below.
ro1h1 =
ro1h2 =
ro2h3 =
ro2h4 =
ah1o1h2 =
ah3o2h4 =
print ('O-H distances: %5.3f %5.3f %5.3f %5.3f' % (ro1h1,ro1h2,ro2h3,ro2h4))
print ('H-O-H angles: %6.2f %6.2f' % (ah1o1h2,ah3o2h4))
print ('MP2 energy of molecule 1: %18.12f hartrees' % emp2_h2o[0,1])
print ('MP2 energy of molecule 2: %18.12f hartrees' % emp2_h2o[0,2])

```

We can now proceed with the analysis of the SAPT0 energy components for the complex of two water molecules. *Please edit this Markdown cell to write your answers.*
1. Which of the four SAPT terms are long-range, and which are short-range this time?
2. For the terms that are long-range and decay with $R$ like $R^{-n}$, estimate $n$ by fitting a proper function to the 5 data points with the largest $R$, just like you did for the two interacting helium atoms (using `scipy.optimize.curve_fit`). How would you explain the power $n$ that you obtained for the electrostatic energy?

```python
#COMPLETE the optimizations below.
nelst_h2o =
nind_h2o =
ndisp_h2o =
print ("Optimal electrostatics exponent:",nelst_h2o[0][0])
print ("Optimal induction exponent:",nind_h2o[0][0])
print ("Optimal dispersion exponent:",ndisp_h2o[0][0])

```

The water molecules are polar - each one has a nonzero dipole moment, and at large distances we expect the electrostatic energy to be dominated by the dipole-dipole interaction (at short distances, when the orbitals of two molecules overlap, the multipole approximation is not valid and the electrostatic energy contains the short-range *charge penetration* effects). Let's check if this is indeed the case. In preparation for this, we first find the HF dipole moment vector for each water molecule.

```python
waterA = psi4.geometry("""
O 0.116724185090 1.383860971547 0.000000000000
H 0.116724185090 0.423860971547 0.000000000000
H -0.812697549673 1.624225775439 0.000000000000
units angstrom
noreorient
nocom
symmetry c1
""")

comA = waterA.center_of_mass()
comA = np.array([comA[0],comA[1],comA[2]])
E, wfn = psi4.energy('HF',return_wfn=True)
dipoleA = np.array([psi4.variable('SCF DIPOLE X'),psi4.variable('SCF DIPOLE Y'),
                    psi4.variable('SCF DIPOLE Z')])*0.393456   # conversion from Debye to a.u.
psi4.core.clean()
print("COM A in a.u.",comA)
print("Dipole A in a.u.",dipoleA)

waterB = psi4.geometry("""
O -0.118596320329 -1.305864713301 0.000000000000
H 0.362842754701 -1.642971982825 -0.759061990794
H 0.362842754701 -1.642971982825 0.759061990794
units angstrom
noreorient
nocom
symmetry c1
""")

comB = waterB.center_of_mass()
comB = np.array([comB[0],comB[1],comB[2]])
E, wfn = psi4.energy('HF',return_wfn=True)
dipoleB = np.array([psi4.variable('SCF DIPOLE X'),psi4.variable('SCF DIPOLE Y'),
                    psi4.variable('SCF DIPOLE Z')])*0.393456   # conversion from Debye to a.u.
psi4.core.clean()
print("COM B in a.u.",comB)
print("Dipole B in a.u.",dipoleB)

comA_to_comB = comB - comA
print("Vector from COMA to COMB:",comA_to_comB)

```

Our goal now is to plot the electrostatic energy from SAPT against the interaction energy between two dipoles $\boldsymbol{\mu_A}$ and $\boldsymbol{\mu_B}$:

\begin{equation}
E_{\rm dipole-dipole}=\frac{\boldsymbol{\mu_A}\cdot\boldsymbol{\mu_B}}{R^3}-\frac{3(\boldsymbol{\mu_A}\cdot{\mathbf R})(\boldsymbol{\mu_B}\cdot{\mathbf R})}{R^5}
\end{equation}

Program this formula in the `dipole_dipole` function below, taking ${\mathbf R}$, $\boldsymbol{\mu_A}$, and $\boldsymbol{\mu_B}$ in atomic units and calculating the dipole-dipole interaction energy, also in atomic units (which we will later convert to kcal/mol).
With your new function, we can populate the `edipdip` array of dipole-dipole interaction energies for all intermolecular separations, and plot these energies alongside the actual electrostatic energy data from SAPT.

Note that ${\mathbf R}$ is the vector from the center of mass of molecule A to the center of mass of molecule B. For the shortest intermolecular distance, the atomic coordinates are listed in the code above, so `R = comA_to_comB`. For any other distance, we obtained the geometry of the complex by shifting one water molecule away from the other along the O-O direction, so we need to shift the center of mass of the second molecule in the same way.

```python
#the geometries are related to each other by a shift of 1 molecule along the O-O vector:
OA_to_OB = (np.array([-0.118596320329,-1.305864713301,0.000000000000])-np.array(
    [0.116724185090,1.383860971547,0.000000000000]))/0.529177249
OA_to_OB_unit = OA_to_OB/np.sqrt(np.sum(OA_to_OB*OA_to_OB))
print("Vector from OA to OB:",OA_to_OB,OA_to_OB_unit)

def dipole_dipole(R,dipA,dipB):
#COMPLETE the definition of the dipole-dipole energy. All your data are in atomic units.

edipdip = []
for i in range(len(distances_h2o)):
  shiftlength = (distances_h2o[i]-distances_h2o[0])/0.529177249
  R = comA_to_comB + shiftlength*OA_to_OB_unit
  edipdip.append(dipole_dipole(R,dipoleA,dipoleB)*627.509)

edipdip = np.array(edipdip)
print (edipdip)

plt.close()
plt.ylim(-10.0,10.0)
plt.plot(distances_h2o,eelst_h2o,'r+',linestyle='-',label='SAPT0 elst')
plt.plot(distances_h2o,edipdip,'bo',linestyle='-',label='dipole-dipole')
plt.hlines(0.0,2.5,9.0)
plt.legend(loc='upper right')
plt.show()

```

We clearly have a favorable dipole-dipole interaction, which results in negative (attractive) electrostatic energy. This is how the origins of hydrogen bonding might have been explained to you in your freshman chemistry class: two polar molecules have nonzero dipole moments and the dipole-dipole interaction can be strongly attractive. However, your SAPT components show you that it's not a complete explanation: the two water molecules are bound not only by electrostatics, but by two other SAPT components as well. Can you quantify the relative (percentage) contributions of electrostatics, induction, and dispersion to the overall interaction energy at the van der Waals minimum? This minimum is the second point on your curve, so, for example, `esapt_h2o[1]` is the total SAPT interaction energy.

```python
#now let's examine the SAPT0 contributions at the van der Waals minimum, which is the 2nd point on the curve
#COMPLETE the calculation of percentages.
percent_elst =
percent_ind  =
percent_disp =
print ('At the van der Waals minimum, electrostatics, induction, and dispersion')
print (' contribute %5.1f, %5.1f, and %5.1f percent of interaction energy, respectively.'
 % (percent_elst,percent_ind,percent_disp))

```

You have now completed some SAPT calculations and analyzed the meaning of different corrections. Can you complete the table below to indicate whether different SAPT corrections can be positive (repulsive), negative (attractive), or both, and why?

```python
#Type in your answers below.
#COMPLETE this table. Do not remove the comment (#) signs.
#
#SAPT term       Positive/Negative/Both?     Why?
#Electrostatics
#Exchange
#Induction
#Dispersion

```

# Ternary diagrams

Higher levels of SAPT calculations can give very accurate interaction energies, but are more computationally expensive than SAPT0. SAPT0 is normally sufficient for qualitative accuracy and basic understanding of the interaction physics. One important use of SAPT0 is to *classify different intermolecular complexes according to the type of interaction*, and a nice way to display the results of this classification is provided by a *ternary diagram*.

The relative importance of attractive electrostatic, induction, and dispersion contributions to a SAPT interaction energy for a particular structure can be marked as a point inside a triangle, with the distance to each vertex of the triangle depicting the relative contribution of a given type (the more dominant a given contribution is, the closer the point lies to the corresponding vertex). If the electrostatic contribution is repulsive, we can display the relative magnitudes of electrostatic, induction, and dispersion terms in the same way, but we need the second triangle (the left one). The combination of two triangles forms the complete diagram and we can mark lots of different points corresponding to different complexes and geometries.

Let's now mark all your systems on a ternary diagram, in blue for two helium atoms and in red for two water molecules. What kinds of interaction are represented? Compare your diagram with the one pictured below, prepared for 2510 different geometries of the complex of two water molecules, with all kinds of intermolecular distances and orientations (this graph is taken from [Smith:2016]). What conclusions can you draw about the interaction of two water molecules at *any* orientation?

```python
def ternary(sapt, title='', labeled=True, view=True, saveas=None, relpath=False, graphicsformat=['pdf']):
#Adapted from the QCDB ternary diagram code by Lori Burns
    """Takes array of arrays *sapt* in form [elst, indc, disp] and builds formatted
    two-triangle ternary diagrams. Either fully-readable or dotsonly depending
    on *labeled*.
    """
    from matplotlib.path import Path
    import matplotlib.patches as patches

    # initialize plot
    plt.close()
    fig, ax = plt.subplots(figsize=(6, 3.6))
    plt.xlim([-0.75, 1.25])
    plt.ylim([-0.18, 1.02])
    plt.xticks([])
    plt.yticks([])
    ax.set_aspect('equal')

    if labeled:
        # form and color ternary triangles
        codes = [Path.MOVETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY]
        pathPos = Path([(0., 0.), (1., 0.), (0.5, 0.866), (0., 0.)], codes)
        pathNeg = Path([(0., 0.), (-0.5, 0.866), (0.5, 0.866), (0., 0.)], codes)
        ax.add_patch(patches.PathPatch(pathPos, facecolor='white', lw=2))
        ax.add_patch(patches.PathPatch(pathNeg, facecolor='#fff5ee', lw=2))

        # label corners
        ax.text(1.0,
                -0.15,
                u'Elst (−)',
                verticalalignment='bottom',
                horizontalalignment='center',
                family='Times New Roman',
                weight='bold',
                fontsize=18)
        ax.text(0.5,
                0.9,
                u'Ind (−)',
                verticalalignment='bottom',
                horizontalalignment='center',
                family='Times New Roman',
                weight='bold',
                fontsize=18)
        ax.text(0.0,
                -0.15,
                u'Disp (−)',
                verticalalignment='bottom',
                horizontalalignment='center',
                family='Times New Roman',
                weight='bold',
                fontsize=18)
        ax.text(-0.5,
                0.9,
                u'Elst (+)',
                verticalalignment='bottom',
                horizontalalignment='center',
                family='Times New Roman',
                weight='bold',
                fontsize=18)

    xvals = []
    yvals = []
    cvals = []
    geomindex = 0 # first 11 points are He-He, the next 10 are H2O-H2O
    for sys in sapt:
        [elst, indc, disp] = sys

        # calc ternary posn and color
        Ftop = abs(indc) / (abs(elst) + abs(indc) + abs(disp))
        Fright = abs(elst) / (abs(elst) + abs(indc) + abs(disp))
        xdot = 0.5 * Ftop + Fright
        ydot = 0.866 * Ftop
        if geomindex <= 10:
          cdot = 'b'
        else:
          cdot = 'r'
        if elst > 0.:
            xdot = 0.5 * (Ftop - Fright)
            ydot = 0.866 * (Ftop + Fright)
        #print elst, indc, disp, '', xdot, ydot, cdot

        xvals.append(xdot)
        yvals.append(ydot)
        cvals.append(cdot)
        geomindex += 1

    sc = ax.scatter(xvals, yvals, c=cvals, s=15, marker="o",
                    edgecolor='none', vmin=0, vmax=1, zorder=10)

    # remove figure outline
    ax.spines['top'].set_visible(False)
    ax.spines['right'].set_visible(False)
    ax.spines['bottom'].set_visible(False)
    ax.spines['left'].set_visible(False)

    # save and show
    plt.show()
    return 1

sapt = []
for i in range(11):
  sapt.append([eelst[i],eind[i],edisp[i]])
for i in range(10):
  sapt.append([eelst_h2o[i],eind_h2o[i],edisp_h2o[i]])
idummy = ternary(sapt)
from IPython.display import Image
Image(filename='water2510.png')

```

# Some further reading:

1. How is the calculation of SAPT corrections actually programmed? The Psi4NumPy projects has some tutorials on this topic: https://github.com/psi4/psi4numpy/tree/master/Tutorials/07_Symmetry_Adapted_Perturbation_Theory
2. A classic (but recently updated) book on the theory of interactions between molecules: "The Theory of Intermolecular Forces"
	> [[Stone:2013](https://www.worldcat.org/title/theory-of-intermolecular-forces/oclc/915959704)] A. Stone, Oxford University Press, 2013
3. The classic review paper on SAPT: "Perturbation Theory Approach to Intermolecular Potential Energy Surfaces of van der Waals Complexes"
	> [[Jeziorski:1994](http://pubs.acs.org/doi/abs/10.1021/cr00031a008)] B. Jeziorski, R. Moszynski, and K. Szalewicz, *Chem. Rev.* **94**, 1887 (1994)
4. A brand new (as of 2020) review of SAPT, describing new developments and inprovements to the theory: "Recent developments in symmetry‐adapted perturbation theory"
	> [[Patkowski:2020](https://onlinelibrary.wiley.com/doi/abs/10.1002/wcms.1452)] K. Patkowski, *WIREs Comput. Mol. Sci.* **10**, e1452 (2020)
5. The definitions and practical comparison of different levels of SAPT: "Levels of symmetry adapted perturbation theory (SAPT). I. Efficiency and performance for interaction energies"
	> [[Parker:2014](http://aip.scitation.org/doi/10.1063/1.4867135)] T. M. Parker, L. A. Burns, R. M. Parrish, A. G. Ryno, and C. D. Sherrill, *J. Chem. Phys.* **140**, 094106 (2014)
6. An example study making use of the SAPT0 classification of interaction types, with lots of ternary diagrams in the paper and in the supporting information: "Revised Damping Parameters for the D3 Dispersion Correction to Density Functional Theory"
	> [[Smith:2016](https://pubs.acs.org/doi/abs/10.1021/acs.jpclett.6b00780)] D. G. A. Smith, L. A. Burns, K. Patkowski, and C. D. Sherrill, *J. Phys. Chem. Lett.* **7**, 2197 (2016).