Tuesday, 19 June 2018

Can curved molecules in flames help reduce soot pollution?

Soot is a serious problem which impacts human and the planet's health. The world health organisation estimates that in 2012 seven million people died or 1/8 total deaths around the world, from air pollution with around half from ambient air pollution and the rest from indoor air pollution from cooking on inefficient stoves (WHO). Soot emissions are also the biggest contributor to global warming after the greenhouse gases. So how do we stop soot from forming? The first step is to understand the mechanism by which soot forms, which is surprisingly still not yet fully understood. We have just published an article considering the impact of curved aromatic molecules on the formation mechanism of soot (here is a link to the preprint).

Image result for soot pollution
So what is currently understood about soot formation? The animation below will aid in illustrating soot formation. It shows the inside of a small lawn mower motor with a transparent plastic engine head allowing for a high-speed camera to capture the combustion inside the engine. 

A small spark ignites the petrol and air mixture and a blue flame is seen to spread through the cylinder. At the flame front there is enough oxygen and so the fuel burns completely to carbon dioxide and water with the blue colour (the colour is due to excited intermediates C2*, CH* and OH*). However, not all of the flame has enough oxygen to completely combust. Behind the flame front, a build-up of carbon molecules forms soot which glows yellow because it is heated by the combustion. So the yellow colour you see in the figure is soot which is being exhausted into the atmosphere. You can confirm that the yellow colour is from soot at home by placing a metal spoon into the yellow part of the flame and picking up black soot on the spoon.

From the animation inside the engine, you will also have seen the complexity of the flame. To simply the system we often use simple burners to study soot formation which produce very stable candle-like flames (diffusion flame). Below is one of the burners with a schematic to highlight the molecules involved in soot formation. The steps for soot formation are as follows:
  1. Precursor pyrolysis - the fuel breaks down and without enough oxygen a build up of carbon molecules - primarily acetylene occurs.
  2. PAH formation - The acetylene C2H2 reacts through a radical chain reaction into aromatic fused ring molecules.
  3. Inception/nucleation - Small carbon nanoparticle nuclei are formed 
  4. Growth - Some of these nuclei grow to become primary particles tens of nanometres in size.
  5. Aggregation/agglomeration - Primary particles stick together and form fractal-like stringy aggregates.

Step three (inception) is currently the most difficult to understand. Formation of small carbon nanoparticles from aromatic molecules in the flame happens too quickly to be a chemical reaction and must include some physical sticking or clustering to explain the speed of formation. My research group has previously determined that flat PAH (planar aromatics with only hexagonal rings) have been found to not be sticky enough to overcome the high temperatures where soot forms in a flame (~1000-1250°C). This thermal energy shakes the clusters apart.

So what about these curved PAH molecules? It is well known that they are present in soot, corannulene (a simple curved PAH with a pentagon ring surrounded by hexagonal rings) has been extracted from soot and if you decrease the pressure around the flame completely enclosed spherical cages of carbon (fullerenes) can be produced.

corannulene - Wiktionary           Fullerene C60
curved PAH corannulene (left and below click and drag to move the grey model around and zoom with your mouse wheel) and closed cage fullerene (right)

We have previously shown that these molecules contain a large electric polarisation which might be important. So how do these polarised cPAH effect soot formation? In this paper we considered three questions:
  1. What causes lead to curvature and how small do the molecules need to become curved?
  2. Can curved PAH cluster together in the flame where soot forms (~1000-1250°C)?
  3. What other interactions could cPAH have with other precursors (e.g. chemi-ions)?

What causes the curvature and how small do the molecules need to be to curve? Using computational chemistry we could calculate the geometry of lots of different curved aromatic molecules. We found the minimum size is 6 rings to curve a PAH with at least one ring being pentagonal.

It turns out the pentagon containing PAH are planar for larger molecules than you would expect from considering a perfect net of pentagons and hexagons. We found this is due to the bonding of electrons on the top and bottom of the molecule favouring planar geometries which are only overcome when the in-plane bonds are strong enough to pull the molecule into a curved configuration.

Can curved PAH cluster together in the flame where soot forms (~1000-1250°C)? Using another calculation method we worked out how sticky the cPAH are compared with their planar cousins (more negative binding energies means more strongly bound) shown below. Triangles indicate flat PAH and squares indicate cPAH. We found that for one or two pentagons, cPAH have similar binding energies to flat PAH. With three or greater number of pentagons the cPAH bound with less energy than the same sized flat PAH.
We have previously shown that for the size of flat PAH in soot it is unlikely to be able to stablise the small clusters in the soot inception zone. So for cPAH with up to two pentagons, we expect the situation to be similar and with three or more the nucleation is definitely not possible. 

What other interactions could cPAH have with other precursors? In the schematic near the top of the article, we also highlighted some ionic species called chemi-ions (coloured blue). There is a surprising amount of these charges in flames which form from reactions between an excited C-H molecule (which is one of the intermediates that glows blue). When excited CH  reacts with oxygen or acetylene it ejects an electron and becomes positively charged. The impact of these charges can be quite dramatic if you apply an electric field. At a given voltage you can also see the flame conduct electricity with arcs going through the flame. 

Soot formation can also be halted with a strong electric field. The picture below shows with a counterflow burner with fuel from below and air from above. With no electric field applied a yellow sooting flame is found. Applying a strong electric field removes the soot from the flame leaving a blue clean burning flame. This has been explained as charged nuclei and carbon being rapidly sucked out of the flame by the strong electric field.

Top - counterflow diffusion flame without an electric field applied. Bottom - with a strong electric field applied
Credit: Lawton and Weinberg 1969
Curved aromatics are significantly polar and might explain these electrical aspects of soot formation.  The interaction between charge and a dipolar molecule is long range and substantial. The plot below shows a range of curved PAH (covering the range of sizes we see in the flame 10-20 rings) with their binding energy to a charge. To give you a sense of what sort of energies are needed to hold something together at flame temperatures around 165 kJ/mol is usually quoted.

There is quite a lot more work needed to consider this ionic interaction and what impact soot formation here are some of the questions that need answering:
  • How many cPAH are present in early soot nanoparticles?
  • What is the polarity of the cPAH in early soot?
  • Can a cluster of cPAH be stabilised at high temperatures in a flame with a chemi-ion?
  • Is flexoelectricity involved in any other way in soot formation?
So the question Can curved molecules help reduce soot pollution? is yet to be fully answered. We will be presenting these results at the next combustion symposium in Dublin at the end of next month and have more results on the way.

Monday, 26 March 2018

Simple Quantum Chemistry: Hartree-Fock in Python


Computational chemistry allow properties of molecules to be determined with incredible accuracy. This includes how much energy is stored in a molecule (and how much is released when you combust it), at which frequencies does it vibrate (which can be used to identify chemicals), how quickly a reaction will occur etc. . One of the most widely used method to calculate these properties as a first approximation is the Hartree-Fock (HF) method. Many very successful widely used density functional methods (B3LYP, PBE0, B97) will add in exact exhange calculated using HF to improve the calculation. One of the classic texts in computational chemistry is Modern Quantum Chemistry by Szabo and Ostlund. The book includes a simple calculation of the molecule $HeH^+$ which has the same electronic configuration as the hydrogen molecule but due to the +2 charge on the helium nucleus it is not symmetrical and must be calculated iteratively by converging on a consistent set of parameters which is why the method is often called self consistent field theory (SCF). I have rewritten this example in the python language based on the original Fortran code and try to explain how the calculations are performed and why the operations are done. I have also added in figures to show what the results look like.

Molecular Schrodinger equation

We begin with the time-independent Schrodinger equation for a molecule is

$$H\Psi = E\Psi$$

The Hamiltonian $H$ performs some mathematical operations on the function $\Psi$ and returns a number the energy and the $\Psi$ function unchanged. This equation is what is called a eigenvalue equation and turns out to be fairly straightforward to solve particularly if you make use of matrix algebra which is the method used in with the Roothmaan-Hall equations.

$$\left[ -\sum_i \frac{\nabla_i^{2}}{2} -\sum_A \frac{\nabla_A^{2}}{2} - \sum_{A,i}\frac{Z_{A}}{r_{Ai}}+\sum_{A>B}\frac{Z_A Z_B}{R_{AB}}+\sum_{i>j}\frac{1}{r_{ij}}\right]\Psi(\boldsymbol{r};\boldsymbol{R})=E\Psi(\boldsymbol{r};\boldsymbol{R})$$

or in a more compact form

$$ \left[\hat{T}_e(\boldsymbol{r}) +\hat{T}_N(\boldsymbol{R}) +\hat{V} _{eN}(\boldsymbol{r};\boldsymbol{R})+\hat{V}_{NN}(\boldsymbol{R})+\hat{V}_{ee}(\boldsymbol{r})\right]\Psi(\boldsymbol{r};\boldsymbol{R})=E\Psi(\boldsymbol{r};\boldsymbol{R})$$

You will notice that there are no units because we are making use of atomic units (a.u.) which simplifies the equations by equating these constants to one $m_e=e=h/2\pi=1/(4\pi\epsilon_{0})=1$.

The sums $\Sigma$ have electron $i,j$ and nuclei $A,B$ indices. With the electron positions $\boldsymbol{r}$ and nuclei position $\boldsymbol{R}$ being used to calculate distances between nuclei-electrons $r_{Ai}$, nuclei-nuclei $r_{AB}$ and electron-electron $r_{ij}$. Atomic charges are needed $Z_{A/B}$ in our case $Z_{He}=+2$ and $Z_{H}=+1$.

The terms are, in order;

  • Sum of each electron's kinetic energy $$\hat{T}_e(\boldsymbol{r})=-\sum_i \frac{\nabla_i^{2}}{2}$$
    • Using classical mechanics we can determine the kinetic energy from the velocity $\boldsymbol{v}$ and mass $m$ of the particle (multiplied together to give the momentum $\boldsymbol{p}=m\boldsymbol{v}$) $$E_k=T=\frac{1}{2}m\boldsymbol{v}^2=\frac{\boldsymbol{p}^2}{2m}$$.
    • This classical definition cannot be applied in quantum mechanics as you cannot define a definite position $\boldsymbol{r}$ due to the uncertainity principle which states that the momentum and position cannot be known to any degree of accuracy.
    • We must make use of the quantum mechanical momentum operator $\hat{p} =-i\hbar\nabla$ which comes from the wave description of a free particle. $$T_e = \frac{\boldsymbol{p}^2}{2m} = \frac{(-i\hbar\nabla)^2}{2m} = -\frac{\hbar^2}{2m}\nabla^2$$ ($\nabla$ is the symbol used to specify derivatives in multivariable calculus. i.e $\partial /\partial x$)
    • Think of this as a kinetic energy pressure which due to quantum uncertainity holds the electron away from the nuclei allowing it not to collapse to the positive nuclei.
    • This fluctuation is dependent inversely on the mass so the fluctuation in the light electron is much larger than the heavy nuclei which means it is only neccessary (in most cases) to consider the qunatum mechanical fluctutations of the electrons around fixed nuclei.
  • Sum of each nuclei's kinetic energy $$\hat{T}_N(\boldsymbol{R})=-\sum_A \frac{\nabla_A^{2}}{2}$$
    • Just as the electron does not have a definitive position so also the nuclei do not.
    • The uncertainity of the electron is much greater than the uncertainty of the nuclei due to the much greater mass of the later that we often will ignore the quantum uncertainity in the nuclei.
    • However, for light nuclei such as hydrogen this is important and can lead to interesting quantum tunneling effects.
  • Sum of energy from nuclei-electron attraction $$\hat{V} _{eN}(\boldsymbol{r};\boldsymbol{R})= - \sum_{A,i}\frac{Z_{A}}{r_{Ai}}$$
    • this is an attractive energy between nuclei and electrons and is just coulombs law which is usually written $E_{Coulomb} = \frac{q_A q_B}{4\pi \epsilon_0R_{AB}}$ with two charge $q_A$ and $q_B$ a distance apart $R_{AB}$
    • remember atomic units removes the $4\pi\epsilon_0$ constants, where $\epsilon_0$ is the electric permittivity.
  • Sum of energy from nuclei-nuclei repulsion $$\hat{V}_{NN}(\boldsymbol{R})=+\sum_{A>B}\frac{Z_A Z_B}{R_{AB}}$$
    • a repulsive term this is just coulombs law usually written $E_{Coulomb} = \frac{q_A q_B}{4\pi \epsilon_0R_{AB}}$ with two nuclear charges $q_A$ and $q_B$ a distance $R_{AB}$ apart
  • Sum of electron-electron repulsion $$\hat{V}_{ee}(\boldsymbol{r})=+\sum_{i>j}\frac{1}{r_{ij}}$$
    • electron-electron repulsion similar to the electrostatic term between the nuclei and electrons.

Most often, however, we only consider the electronic problem for a given set of nuclear positions $\boldsymbol{R}$ as the electrons are significantly lighter and therefore can be considered to instantanously adjust for any nuclear position. (However, the energy here does not include the nuclear repulsions which need to be added at the end to get the total energy.)

$$\left[ \hat{T}_e(\boldsymbol{r})+ \hat{V} _{eN}(\boldsymbol{r};\boldsymbol{R})+\hat{V}_{ee}(\boldsymbol{r})\right]\Psi(\boldsymbol{r})=E_{el}\Psi(\boldsymbol{r})$$

Hartree-Fock equation

One of the challenges in solving this equation is that you do not know where the electron resides only where it is statisically likely to be if you took a measurement - called the many-body effect. This makes the terms involving the electron repulsions ($+\sum_{i>j}\frac{1}{r_{ij}}$) difficult to calculate.

In the Hartree-Fock approach the first approximation used is to treat each electron separately interacting with an averaged distribution of the other electrons (mean field approach).

$$\left[ -\sum_i \frac{\nabla_i^{2}}{2}- \sum_{A,i}\frac{Z_{A}}{r_{Ai}}+v_{HF}(i)\right]\Psi(\boldsymbol{r})=\epsilon_{ij}\Psi(\boldsymbol{r})$$

The functions chosen to represent each electron is based on the hydrogen atomic solutions, which are exact, for an electron around an atom of hydrogen the atomic orbitals.

So we consider a product of one-electron wavefunctions $\psi_i(\boldsymbol{r})$ interacting with a mean field of all the other electrons. $$\Psi(\boldsymbol{r}) = \psi_1(\boldsymbol{r}_1)\psi_2(\boldsymbol{r}_2)\psi_3(\boldsymbol{r}_3)...=\prod_i \psi_i(\boldsymbol{r}_i)$$

However this does not have the correct symmetry requirements which means if you swap electrons the sign of the wavefunction inverts. This requirement means that the Fermions (electrons) cannot occupy the same quantum states and do not collapse towards the nucleus. We can construct something called a slater determinant which does have the correct symmetry properties.

$$\Psi(\mathbf{r}_1, \mathbf{r}_2, \ldots, \mathbf{r}_N) = \frac{1}{\sqrt{N!}} \left| \begin{matrix} \psi_1(\mathbf{r}_1) & \psi_2(\mathbf{r}_1) & \cdots & \psi_N(\mathbf{r}_1) \\ \psi_1(\mathbf{r}_2) & \psi_2(\mathbf{r}_2) & \cdots & \psi_N(\mathbf{r}_2) \\ \vdots & \vdots & \ddots & \vdots \\ \psi_1(\mathbf{r}_N) & \psi_2(\mathbf{r}_N) & \cdots & \psi_N(\mathbf{r}_N) \end{matrix} \right|\equiv \left|\psi _1,\psi _2,\cdots ,\psi _N\right.\rangle$$

$\left|\psi _1,\psi _2,\cdots ,\psi _N\right.\rangle $ is often written simply as $\psi_i(\boldsymbol{r}_i)$

This means we can write a set of $i$ equations which can be solved for a set of one-electron wavefunctions.

$$\left[ -\sum_i \frac{\nabla_i^{2}}{2}- \sum_{A,i}\frac{Z_{A}}{r_{Ai}}+v_{HF}(i)\right]\psi_i(\boldsymbol{r}_i)=\epsilon_{ij}\psi_i(\boldsymbol{r}_i)$$

What then is $v_{HF}(i)$ we can consider two interactions a coulomb interaction $\mathscr{J}$ (repulsion between the "averaged out" one-electron orbitals) and an exchange interactions $\mathscr{K}$ which is not classical and is due to electrons of the same spin being indistinguishable which lowers the energy of the system.

$$ v_{HF}(i) = \sum_j \left[ 2\mathscr{J}_j - \mathscr{K}_j\right] $$

Where the sum is over all of the other $j$ electrons. There is a double contribution from the coulomb operator as we are considering closed shell systems where there are two electrons per orbital but the exhange operator is only considered once as this interaction can only be between electrons with like-spins. This means for our HeH+ system it will not contribute as we have

$$ \mathscr{J}_j = \int \psi_j(\boldsymbol{r}_j)^* \frac{1}{r_{i,j}}\psi_j(\boldsymbol{r}_j) dr$$$$ \mathscr{K}_j\psi_i(\boldsymbol{r}_i) = \left[\int \psi_j(\boldsymbol{r}_j)^* \frac{1}{r_{i,j}}\psi_i(\boldsymbol{r}_i)dr\right]\psi_j(\boldsymbol{r}_j) $$

You will notice the indices have switched for the exchange operator.

The first two terms are often considered as the core terms as they are the kinetic energy and potential energy for an isolated system such as the hydrogen atom and therefore they are refered to as $\mathscr{H}^{CORE}$

$$ \mathscr{H}^{CORE} = -\sum_i \frac{\nabla_i^{2}}{2}- \sum_{A,i}\frac{Z_{A}}{r_{Ai}} $$

Putting this all together we get the Hartree-Fock equation

$$ \left( \mathscr{H}^{CORE} + \sum_j \left[ 2\mathscr{J}_j - \mathscr{K}_j\right] \right)\psi_i(\boldsymbol{r}_i) = \sum_j \epsilon_{ij}\psi_i(\boldsymbol{r}_i) $$

Or simply

$$ \mathscr{f}_i\psi_i(\boldsymbol{r}_i) = \epsilon_{i}\psi_i(\boldsymbol{r}_i) $$

where $\mathscr{f}_i$ are the fock operators


The most successful method to construct these one-electron wavefunctions $\psi_i$ is to consider them delocalised over the whole molecule. Therefore there will be a set of orbitals, one for each electron, which are spread over the whole molecule. We call this type of treatment molecular orbital theory. The question then is what functions do we use to treat these one-electron delocalised orbitals. We know the solutions exactly for a hydrogen like atom so we can use atomic orbitals $\phi_i(\boldsymbol{r}_i)$ centred at each atom $\boldsymbol{r}_i$ to be the basis for our delocalised molecular orbitals. We need to be able to optimise the amounts of each of these atomic orbitals in each of our one-electron delocalised orbitals so we consider a linear combination of atomic orbitals (LCAO).

$$\psi_i(\boldsymbol{r}_i) = \sum_{\mu} c_{\mu} \phi_{\mu}(\boldsymbol{r}_i)$$

where the constants $c_i$ are going to be optimised to minimise the energy.

The method relies on the vartionial principle which is true for the Hartree-Fock equations which is that any approximation you make (i.e. describing the electrons as a LCAO) will raise the energy above the real energy of the system. The advantage of a variational method is that you can systematically improve the wavefunction and improve your result knowing that you will be able to converge upon a value (which will be above the actual energy).

The atomic orbitals are called the slater type orbitals for the 1s orbital this can be written as

$$ \phi^{SLA}\left( \boldsymbol{r}\right) = \left( \zeta^3/\pi \right)^{1/2}e^{-\zeta \boldsymbol{r}} $$

Let's have a look at the function by plotting it.

In [1]:
# Need to import some libraries to have the maths functions and plotting
import numpy as np
import scipy.special as sp
import matplotlib.pyplot as plt
%matplotlib notebook # Allows plotting in the notebook

x = np.linspace(-5,5,num=1000)
r = abs(x)

zeta = 1.0

psi_STO = (zeta**3/np.pi)**(0.5)*np.exp(-zeta*r)

[<matplotlib.lines.Line2D at 0x1065af0b8>]

Slater type orbitals (STO) are the exact solutions for the hydrogen atom and provide an accurate basis set for many electron molecules however the calculations of the integrals are expensive as their is no simple exact solution for the integrals. One way around this is to approximate the Slater type orbitals using a sum of contracted Gaussian functions (CGF). There are simple analytical expressions for the integrals between two Gaussians so this can save a lot of computing time. Let's look at this for the case of the 1s orbital

$$\phi^{GF}(\alpha)=(2\alpha/\pi)^{3/4}exp(-\alpha r^{2}) $$$$\phi^{CGF}\left( \boldsymbol{r}\right) = \sum_n d_n\phi^{GF}_n(\alpha)$$

We will make use of three Gaussians to approximate the slater type orbitals.

$$\phi^{CGF}_{STO-3G}\left( \boldsymbol{r}\right) = \sum^3_n d_n\phi^{GF}_n(\alpha)$$
In [19]:
# Coeff is the d_n variable in the equation above
Coeff = np.array([[1.00000,0.0000000,0.000000],

# Expon is the alpha variable in the equation above
Expon = np.array([[0.270950,0.000000,0.000000],

psi_CGF_STO1G = Coeff[0,0]*(2*Expon[0,0]/np.pi)**(0.75)*np.exp(-Expon[0,0]*r**2)
psi_CGF_STO2G = Coeff[1,0]*(2*Expon[1,0]/np.pi)**(0.75)*np.exp(-Expon[1,0]*r**2) \
                + Coeff[1,1]*(2*Expon[1,1]/np.pi)**(0.75)*np.exp(-Expon[1,1]*r**2) \
                + Coeff[1,2]*(2*Expon[1,2]/np.pi)**(0.75)*np.exp(-Expon[1,2]*r**2)
psi_CGF_STO3G = Coeff[2,0]*(2*Expon[2,0]/np.pi)**(0.75)*np.exp(-Expon[2,0]*r**2) \
                + Coeff[2,1]*(2*Expon[2,1]/np.pi)**(0.75)*np.exp(-Expon[2,1]*r**2) \
                + Coeff[2,2]*(2*Expon[2,2]/np.pi)**(0.75)*np.exp(-Expon[2,2]*r**2)
# Plot the three functions
plt.title("Approximations to a STO with CGF")
<matplotlib.legend.Legend at 0x106ddda58>

As the number of Gaussians is increased the function more closely describes the slater type orbitals. You will also see that nearest the centre (x=0) the approximation is poorest. This region is called the cusp.

We will use this very poor basis set for our HeH+ molecule for ease of calculation but for accurate calculations at the very least a 6-311G(d,p) basis set will used which would have 6 contracted Gaussian functions to describe hydrogen and helium 1s orbital.

Roothaan-Hall equations

The Hartree-Fock equations need to be converted into a tabular form (matrix form) using the basis set of atomic orbitals to allow for a solution to be determined using the computer. We insert our basis set of orbitals $\sum_i c_i \phi^{CGF}_i(\boldsymbol{r}_i)$ (we will drop the superscript and just have the orbitals denoted as $\phi_i$) We are also going to be building up a table by combining different basis set indices so will use the subscripts $\nu$ and $\mu$ to denotes the indices as we build up the table/matrix. We will also replace $(\boldsymbol{r}_\nu)$ with $(1)$ for ease of reading.

$$ \mathscr{f}_i\psi_i(\boldsymbol{r}_i) = \epsilon_{i}\psi_i(\boldsymbol{r}_i) $$$$ \mathscr{f}_i\sum^K_{\nu =1} c_{\nu 1} \phi_{\nu}(1) = \epsilon_{i}\sum^K_{\nu =1} c_{\nu i} \phi_{\nu}(1) $$

We left multiply the other basis set index of our table and integrate.

$$ \sum^K_{\nu =1}c_{\nu 1} \int d\nu_1 \phi_{\mu}(1) \mathscr{f}_i \phi_{\nu}(1) = \epsilon_{i}\sum^K_{\nu =1} c_{\nu i}\int d\nu_1 \phi_{\mu}(1)\phi_{\nu}(1) $$

This last term $\int d\nu_1 \phi_{\mu}(1)\phi_{\nu}(1)$ is called the overlap integral written as $S_{\mu \nu}$. This term will help to illustrate the population of a table if we want to know how much the 1s orbital on the He interacts with the 1s orbital on the H atom we would calculate the values on the diagonal of the overlap matrix (for our calculation below this is 0.45077041 in the code the variable is called S12). However, the overlap of the basis function with itself is one by definition.

$$S_{\mu \nu}= \left[ \begin{matrix} 1.0 & 0.45077041 \\ 0.45077041 & 1.0 \end{matrix}\right] $$

We have another matrix called the Fock matrix $F_{\mu \nu}$ which is made up of the core contributions $\mathscr{H}^{CORE}$ and the effective potential $v_{HF}$ lets see how the matrices are constructed from these.

$$ \int d\nu_1 \phi_{\mu}(1)\mathscr{H}^{CORE}\phi_{\nu}(1) = \int d\nu_1 \phi_{\mu}(1)\left[ -\sum_i \frac{\nabla_i^{2}}{2}- \sum_{A,i}\frac{Z_{A}}{r_{Ai}} \right]\phi_{\nu}(1) = H^{CORE}_{\mu \nu} $$

Where $H^{CORE}_{\mu \nu}$ is another matrix that has to be calculated containing a kinetic energy and potential energy contribution. These are called one electron integrals (as they are the interactions of a single electron in a molecular orbital). In the code the core contributions are broken up into kinetic terms (T11, T12 and T22, note we do not need T21 as the matrix is symmetrical) and potential terms (V11A, V12A, V22A for atom one and V11B, V12B, V22B for the second atom, note the sum is also over the nuclei for this term)

The effective Hartree potential is a bit tricker to calculate.

$$ \int d\nu_1 \phi_{\mu}(1)v_{HF}\phi_{\nu}(1) = \sum_{j=1}^{N/2}\int d\nu_1 \phi_{\mu}(1)\left[ 2\mathscr{J}_j - \mathscr{K}_j \right]\phi_{\nu}(1) = G^{2e}_{\mu \nu \lambda \sigma} $$

Where $N$ is the total number of electrons and for closed shell molecules we want half of these electrons, due to two electrons residing in each occupied molecular orbital. This integrate is tricky as it involves the impact of exchange which requires that electrons are indistingishable and therefore instead of a matrix we have 3D matrix or a cube of values (often called a tensor in mathematics). We therefore have to introduce two new indices $\lambda$ and $\sigma$ to denote these indistinguishable versions of the electrons. Which creates quite a construction.

$$ G^{2e}_{\mu \nu \lambda \sigma} = \sum^{N/2}_{j=1}\sum^K_{\lambda=1}\sum^K_{\sigma=1}c_{\lambda j}c_{\sigma j}\left[ \begin{matrix} 2\int d\nu_1d\nu_2\phi_\mu(1)\phi_\nu(1)\frac{1}{r_{12}}\phi_\lambda(2)\phi_\sigma(2)\\ -\int d\nu_1\nu_2 \phi_\mu(1)\phi_\lambda(1)\frac{1}{r_{12}}\phi_{\nu}(2)\phi_\sigma(2) \end{matrix} \right] $$

Or in short hand

$$ G^{2e}_{\mu \nu \lambda \sigma} = \sum^{N/2}_{j=1}\sum^K_{\lambda=1}\sum^K_{\sigma=1}c_{\lambda j}c_{\sigma j}\left[2(\mu\nu|\lambda\sigma)-(\mu\lambda|\nu\sigma) \right] $$

The code uses the variable names V1111, V2111, V2121, V2211, V2221 and V2222 and then constructs $G^{2e}_{\mu \nu \lambda \sigma}$ with the variable name G.

Another shorthand that will be helpful is to consider the charge density matrix $P$

$$ P_{\mu \nu}=2\sum^{N/2}_{i=1}c_{\mu i}c_{\nu i}\; and\; P_{\lambda \sigma}=2\sum^{N/2}_{i=1}c_{\lambda i}c_{\sigma i} $$

The electron density can be determined from the density matrix as

$$ \rho(\boldsymbol{r})=\sum^K_{\mu=1}\sum^K_{\nu=1}c_{\mu}(\boldsymbol{r})c_{\nu}(\boldsymbol{r}) $$

The Fock matrix $F_{\mu \nu}$ is then calculated as for closed shell molecules

$$ F_{\mu \nu} = H^{CORE}_{\mu \nu} + \sum^K_{\lambda=1}\sum^K_{\sigma=1}P_{\lambda \sigma}\left[2(\mu\nu|\lambda\sigma)-(\mu\lambda|\nu\sigma) \right] = H^{CORE}_{\mu \nu} + G^{2e}_{\mu \nu \lambda \sigma} $$

In the code G is used when calculating this term

Putting this back into the Hartree-Fock equation we have now transformed into five matrices/tables or the Roothaan-Hall equations

$$ \boldsymbol{F} \boldsymbol{C} = \boldsymbol{S} \boldsymbol{C} \boldsymbol{E} $$

The coefficient matrix $\boldsymbol{C}$ is a table with $K\times K$ entries with coefficients where $K$ are the number of electrons.

$$ \boldsymbol{C} = \left( \begin{matrix} c_{1,1} & c_{1,2} & ... & c_{1,K} \\ c_{2,1} & c_{2,2} & ... & c_{2,K} \\ \vdots & \vdots & & \vdots \\ c_{K,1} & c_{K,2} & ... & c_{K,K} \end{matrix} \right) $$

$\boldsymbol{E}$ is a diagonal matrix of the energies of the molecular orbitals

$$ \boldsymbol{E} = \left( \begin{matrix} \epsilon_1 & 0 & ... & 0 \\ 0 & \epsilon_2 & ... & 0 \\ \vdots & \vdots & & \vdots \\ 0 & 0 & ... & \epsilon_K \end{matrix} \right) $$

We are trying to find out the values of $\boldsymbol{C}$ which minimise the energy however $\boldsymbol{C}$ is on both sides of the equation so we have to iterative solve the equations until the $\boldsymbol{C}$ on both sides are equal and the energy is the lowest possible this is called the self consistent field method or SCF for short.

Computers are best at solving equations in the form of $\boldsymbol{FC}=\boldsymbol{CE}$, however, that overlap integral is a bit of a problem ($\boldsymbol{F} \boldsymbol{C} = \boldsymbol{S} \boldsymbol{C} \boldsymbol{E}$) so we have to do some matrix algebra to rearrange the equation into an equivalent equation $\boldsymbol{F'C'}=\boldsymbol{C'E}$ (where $\boldsymbol{F'}=\boldsymbol{S}^{-1/2}\boldsymbol{F}\boldsymbol{S}^{-1/2}$ and $\boldsymbol{C'}=\boldsymbol{S}^{-1/2}\boldsymbol{C}$ (In the code $\boldsymbol{S}^{-1/2}$ and $\boldsymbol{S}^{1/2}$ are denoted as X and X.T )


The computation of the various integrals can be calculated as simple functions when Gaussians are used (for a detailed description I recommend the appendix in Modern Quantum Chemistry).

In [2]:
def S_int(A,B,Rab2):
    Calculates the overlap between two gaussian functions 
    return (np.pi/(A+B))**1.5*np.exp(-A*B*Rab2/(A+B))
In [3]:
def T_int(A,B,Rab2):
    Calculates the kinetic energy integrals for un-normalised primitives
    return A*B/(A+B)*(3.0-2.0*A*B*Rab2/(A+B))*(np.pi/(A+B))**1.5*np.exp(-A*B*Rab2/(A+B))
In [4]:
def V_int(A,B,Rab2,Rcp2,Zc):
    Calculates the un-normalised nuclear attraction integrals
    V = 2.0*np.pi/(A+B)*F0((A+B)*Rcp2)*np.exp(-A*B*Rab2/(A+B))
    return -V*Zc
In [5]:
# Mathematical functions

def F0(t):
    F function for 1s orbital
    if (t<1e-6):
        return 1.0-t/3.0
        return 0.5*(np.pi/t)**0.5*sp.erf(t**0.5)
def erf(t):
    Approximation for the error function
    P = 0.3275911
    A = [0.254829592,-0.284496736,1.421413741,-1.453152027,1.061405429]
    T = 1.0/(1+P*t)
    Poly = A[0]*Tn
    for i in range(1,5):
    return 1.0-Poly*np.exp(-t*t)
In [6]:
def TwoE(A,B,C,D,Rab2,Rcd2,Rpq2):
    Calculate two electron integrals
    A,B,C,D are the exponents alpha, beta, etc.
    Rab2 equals squared distance between centre A and centre B
    return 2.0*(np.pi**2.5)/((A+B)*(C+D)*np.sqrt(A+B+C+D))*F0((A+B)*(C+D)*Rpq2/(A+B+C+D))*np.exp(-A*B*Rab2/(A+B)-C*D*Rcd2/(C+D))
In [7]:
def Intgrl(N,R,Zeta1,Zeta2,Za,Zb):
    Declares the variables and compiles the integrals.
    global S12,T11,T12,T22,V11A,V12A,V22A,V11B,V12B,V22B,V1111,V2111,V2121,V2211,V2221,V2222
    S12 = 0.0
    T11 = 0.0
    T12 = 0.0
    T22 = 0.0
    V11A = 0.0
    V12A = 0.0
    V22A = 0.0
    V11B = 0.0
    V12B = 0.0
    V22B = 0.0
    V1111 = 0.0
    V2111 = 0.0
    V2121 = 0.0
    V2211 = 0.0
    V2221 = 0.0
    V2222 = 0.0
    R2 = R*R
    # The coefficients for the contracted Gaussian functions are below
    Coeff = np.array([[1.00000,0.0000000,0.000000],
    Expon = np.array([[0.270950,0.000000,0.000000],
    D1 = np.zeros([3])
    A1 = np.zeros([3])
    D2 = np.zeros([3])
    A2 = np.zeros([3])
    # This loop constructs the contracted Gaussian functions
    for i in range(N):
        A1[i] = Expon[N-1,i]*(Zeta1**2)
        D1[i] = Coeff[N-1,i]*((2.0*A1[i]/np.pi)**0.75)
        A2[i] = Expon[N-1,i]*(Zeta2**2)
        D2[i] = Coeff[N-1,i]*((2.0*A2[i]/np.pi)**0.75)
    # Calculate one electron integrals 
    # Centre A is first atom centre B is second atom
    # Origin is on second atom
    # V12A - off diagonal nuclear attraction to centre A etc.
    for i in range(N):
        for j in range(N):
            # Rap2 - squared distance between centre A and centre P
            Rap = A2[j]*R/(A1[i]+A2[j])
            Rap2 = Rap**2
            Rbp2 = (R-Rap)**2
            S12 = S12 + S_int(A1[i],A2[j],R2)*D1[i]*D2[j]
            T11 = T11 + T_int(A1[i],A1[j],0.0)*D1[i]*D1[j]
            T12 = T12 + T_int(A1[i],A2[j],R2)*D1[i]*D2[j]
            T22 = T22 + T_int(A2[i],A2[j],0.0)*D2[i]*D2[j]
            V11A = V11A + V_int(A1[i],A1[j],0.0,0.0,Za)*D1[i]*D1[j]
            V12A = V12A + V_int(A1[i],A2[j],R2,Rap2,Za)*D1[i]*D2[j]
            V22A = V22A + V_int(A2[i],A2[j],0.0,R2,Za)*D2[i]*D2[j]
            V11B = V11B + V_int(A1[i],A1[j],0.0,R2,Zb)*D1[i]*D1[j]
            V12B = V12B + V_int(A1[i],A2[j],R2,Rbp2,Zb)*D1[i]*D2[j]
            V22B = V22B + V_int(A2[i],A2[j],0.0,0.0,Zb)*D2[i]*D2[j]
    # Calculate two electron integrals
    for i in range(N):
        for j in range(N):
            for k in range(N):
                for l in range(N):
                    Rap = A2[i]*R/(A2[i]+A1[j])
                    Rbp = R - Rap
                    Raq = A2[k]*R/(A2[k]+A1[l])
                    Rbq = R - Raq
                    Rpq = Rap - Raq
                    Rap2 = Rap*Rap
                    Rbp2 = Rbp*Rbp
                    Raq2 = Raq*Raq
                    Rbq2 = Rbq*Rbq
                    Rpq2 = Rpq*Rpq
                    V1111 = V1111 + TwoE(A1[i],A1[j],A1[k],A1[l],0.0,0.0,0.0)*D1[i]*D1[j]*D1[k]*D1[l]
                    V2111 = V2111 + TwoE(A2[i],A1[j],A1[k],A1[l],R2,0.0,Rap2)*D2[i]*D1[j]*D1[k]*D1[l]
                    V2121 = V2121 + TwoE(A2[i],A1[j],A2[k],A1[l],R2,R2,Rpq2)*D2[i]*D1[j]*D2[k]*D1[l]
                    V2211 = V2211 + TwoE(A2[i],A2[j],A1[k],A1[l],0.0,0.0,R2)*D2[i]*D2[j]*D1[k]*D1[l]
                    V2221 = V2221 + TwoE(A2[i],A2[j],A2[k],A1[l],0.0,R2,Rbq2)*D2[i]*D2[j]*D2[k]*D1[l]
                    V2222 = V2222 + TwoE(A2[i],A2[j],A2[k],A2[l],0.0,0.0,0.0)*D2[i]*D2[j]*D2[k]*D2[l]
In [8]:
def Colect(N,R,Zeta1,Zeta2,Za,Zb):
    Takes the basic integrals and assembles the relevant matrices, 
    that are S,H,X,XT and Two electron integrals
    # Form core hamiltonian
    H[0,0] = T11+V11A+V11B
    H[0,1] = T12+V12A+V12B
    H[1,0] = H[0,1]
    H[1,1] = T22+V22A+V22B

    # Form overlap matrix
    S[0,0] = 1.0
    S[0,1] = S12
    S[1,0] = S12
    S[1,1] = 1.0
    # This is S^-1/2
    X[0,0] = 1.0/np.sqrt(2.0*(1.0+S12))
    X[1,0] = X[0,0]
    X[0,1] = 1.0/np.sqrt(2.0*(1.0-S12))
    X[1,1] = -X[0,1]
    # This is the coulomb and exchange term (aa|bb) and (ab|ba)
    TT[0,0,0,0] = V1111
    TT[1,0,0,0] = V2111
    TT[0,1,0,0] = V2111
    TT[0,0,1,0] = V2111
    TT[0,0,0,1] = V2111
    TT[1,0,1,0] = V2121
    TT[0,1,1,0] = V2121
    TT[1,0,0,1] = V2121
    TT[0,1,0,1] = V2121
    TT[1,1,0,0] = V2211
    TT[0,0,1,1] = V2211
    TT[1,1,1,0] = V2221
    TT[1,1,0,1] = V2221
    TT[1,0,1,1] = V2221
    TT[0,1,1,1] = V2221
    TT[1,1,1,1] = V2222

Self consistent field calculation

A common method to solve these equations is to follow the steps below

  1. Guess an initial density matrix $\boldsymbol{P}$ (for this example will use the initial guess P=0)
  2. From $\boldsymbol{P}$ calculate the Fock matrix
  3. Calculate $\boldsymbol{F'}=\boldsymbol{S}^{-1/2}\boldsymbol{F}\boldsymbol{S}^{1/2}$
  4. Solve the eigenvalue problem using the secular equation $|\boldsymbol{F'}-\boldsymbol{E}\boldsymbol{I}|=0$ giving the eigenvalues $\boldsymbol{E}$ and eigenvectors $\boldsymbol{C'}$ which can be solved by diagonalising $\boldsymbol{F'}$
  5. Calculate the molecular orbitals coefficients by reverting $\boldsymbol{C'}=\boldsymbol{S}^{-1/2}\boldsymbol{C}$
  6. Calculate the new density matrix $\boldsymbol{P}$ from the matrix $\boldsymbol{C}$
  7. Check to see if the energy has converged if not then head back to step 6 and repeat.

I have labelled the steps in the program below using the tag ######## STEP 1. ########

In [143]:
def SCF(N,R,Zeta1,Zeta2,Za,Zb,G):
    Performs the SCF iterations
    Crit = 1e-11 # Convergence critera
    Maxit = 250 # Maximum number of iterations
    ######## STEP 1. Guess an initial density matrix ########
    # Use core hamiltonian for initial guess of F, I.E. (P=0)
    P = np.zeros([2,2])
    Energy = 0.0
    while (Iter<Maxit):
        Iter += 1
        ######## STEP 2. calculate the Fock matrix ########
        # Form two electron part of Fock matrix from P
        G = np.zeros([2,2]) # This is the two electron contribution in the equations above
        for i in range(2):
            for j in range(2):
                for k in range(2):
                    for l in range(2):

        # Add core hamiltonian H^CORE to get fock matrix
        F = H+G
        # Calculate the electronic energy
        Energy = np.sum(0.5*P*(H+F))
        print('Electronic energy = ',Energy)
        ######## STEP 3. Calculate F' (remember S^-1/2 is X and S^1/2 is X.T) ########
        G = np.matmul(F,X)
        Fprime = np.matmul(X.T,G)
        ######## STEP 4. Solve the eigenvalue problem ########
        # Diagonalise transformed Fock matrix
        ######## STEP 5. Calculate the molecular orbitals coefficients ########
        # Transform eigen vectors to get matrix C
        C = np.matmul(X,Cprime)
        ######## STEP 6. Calculate the new density matrix from the old P ########
        Oldp = np.array(P)
        P= np.zeros([2,2])
        # Form new density matrix
        for i in range(2):
            for j in range(2):
                #Save present density matrix before creating a new one
                for k in range(1):
                    P[i,j] += 2.0*C[i,k]*C[j,k]

        ######## STEP 7. Check to see if the energy has converged ########
        Delta = 0.0
        # Calculate delta the difference between the old density matrix Old P and the new P
        Delta = (P-Oldp)
        Delta = np.sqrt(np.sum(Delta**2)/4.0)
        #Check for convergence
        if (Delta<Crit):
            # Add nuclear repulsion to get the total energy
            Energytot = Energy+Za*Zb/R
            print("Calculation converged with electronic energy:",Energy)
            print("Calculation converged with total energy:",Energytot)
            print("Density matrix", P)
            print("Mulliken populations",np.matmul(P,S))
In [144]:
def FormG():
    Calculate the G matrix from the density matrix and two electron integals
    for i in range(2):
        for j in range(2):
            for k in range(2):
                for l in range(2):
def Mult(A,B,C_,IM,M):
    Multiples two square matrices A and B to get C
    for i in range(M):
        for j in range(M):
            for k in range(M):
                C_[i,j] = A[i,j]*B[i,j]
def Diag(Fprime,Cprime,E):
    Diagonalises F to give eigenvectors in C and eigen values in E, theta is the angle describing the solution
    import math
    # Angle for heteronuclear diatonic
    Theta = 0.5*math.atan(2.0*Fprime[0,1]/(Fprime[0,0]-Fprime[1,1]))
    #print('Theta', Theta)
    Cprime[0,0] = np.cos(Theta)
    Cprime[1,0] = np.sin(Theta)
    Cprime[0,1] = np.sin(Theta)
    Cprime[1,1] = -np.cos(Theta)
    E[0,0] = Fprime[0,0]*np.cos(Theta)**2+Fprime[1,1]*np.sin(Theta)**2+Fprime[0,1]*np.sin(2.0*Theta)
    E[1,1] = Fprime[1,1]*np.cos(Theta)**2+Fprime[0,0]*np.sin(Theta)**2-Fprime[0,1]*np.sin(2.0*Theta)
    if (E[1,1] <= E[0,0]):
        Temp = E[1,1]
        E[1,1] = E[0,0]
        E[0,0] = Temp
        Temp = Cprime[0,1]
        Cprime[0,1] = Cprime[0,0]
        Cprime[0,0] = Temp
        Temp = Cprime[1,1]
In [145]:
def HFCALC(N,R,Zeta1,Zeta2,Za,Zb,G):
    Calculates the integrals constructs the matrices and then runs the SCF calculation
    # Calculate one and two electron integrals
    # Put all integals into array
    # Perform the SCF calculation
In [146]:
Let's set up the variables and perform the calculations
global H,S,X,XT,TT,G,C,P,Oldp,F,Fprime,Cprime,E,Zb

H = np.zeros([2,2])
S = np.zeros([2,2])
X = np.zeros([2,2])
XT = np.zeros([2,2])
TT = np.zeros([2,2,2,2])
G = np.zeros([2,2])
C = np.zeros([2,2])

P = np.zeros([2,2])
Oldp = np.zeros([2,2])
F = np.zeros([2,2])
Fprime = np.zeros([2,2])
Cprime = np.zeros([2,2])
E = np.zeros([2,2])

Energy = 0.0
Delta = 0.0

N = 3
R = 1.4632
Zeta1 = 2.0925
Zeta2 = 1.24
Za = 2.0
Zb = 1.0
Electronic energy =  0.0
Delta 0.882866853014
Electronic energy =  -4.14186287613
Delta 0.42637666285
Electronic energy =  -4.21250515815
Delta 0.169751444038
Electronic energy =  -4.22491713481
Delta 0.0726766265559
Electronic energy =  -4.22705796346
Delta 0.0305577508572
Electronic energy =  -4.22744516532
Delta 0.0129689170661
Electronic energy =  -4.22751420209
Delta 0.00548417493289
Electronic energy =  -4.22752659937
Delta 0.00232279034282
Electronic energy =  -4.22752881931
Delta 0.000983151834497
Electronic energy =  -4.22752921732
Delta 0.000416249714185
Electronic energy =  -4.22752928864
Delta 0.000176211995738
Electronic energy =  -4.22752930143
Delta 7.46000228255e-05
Electronic energy =  -4.22752930372
Delta 3.15815291915e-05
Electronic energy =  -4.22752930413
Delta 1.33699962565e-05
Electronic energy =  -4.2275293042
Delta 5.66014751815e-06
Electronic energy =  -4.22752930421
Delta 2.3962102418e-06
Electronic energy =  -4.22752930422
Delta 1.01442931622e-06
Electronic energy =  -4.22752930422
Delta 4.29456113385e-07
Electronic energy =  -4.22752930422
Delta 1.81809149079e-07
Electronic energy =  -4.22752930422
Delta 7.69684434551e-08
Electronic energy =  -4.22752930422
Delta 3.2584395203e-08
Electronic energy =  -4.22752930422
Delta 1.37945213101e-08
Electronic energy =  -4.22752930422
Delta 5.83987605061e-09
Electronic energy =  -4.22752930422
Delta 2.47229696259e-09
Electronic energy =  -4.22752930422
Delta 1.04664054128e-09
Electronic energy =  -4.22752930422
Delta 4.43092627596e-10
Electronic energy =  -4.22752930422
Delta 1.87582070145e-10
Electronic energy =  -4.22752930422
Delta 7.94123469356e-11
Electronic energy =  -4.22752930422
Delta 3.36188760201e-11
Electronic energy =  -4.22752930422
Delta 1.42322864473e-11
Electronic energy =  -4.22752930422
Delta 6.02525212134e-12
Calculation converged with electronic energy: -4.22752930422
Calculation converged with total energy: -2.8606621637
Density matrix [[ 1.28614168  0.54017322]
 [ 0.54017322  0.22687011]]
Mulliken populations [[ 1.52963579  1.11992783]
 [ 0.64243955  0.47036421]]
Coeffients [[ 0.80191698 -0.78226577]
 [ 0.33680121  1.06844537]]


Let's have a look at what the energy comes out to be (units in Hartrees)

$E_{HF}^{Current}(STO-3G)$ = -2.8606621637

$E_{HF}^{PySCF}(STO-3G)$ = -2.8418364990824458

$E_{HF}^{PySCF}(6-31G(d))$ = -2.9098394146425748

$E_{HF}^{PySCF}(cc-pVTZ)$ = -2.9322482557926945

$E_{HF}^{PySCF}(aug-cc-pVTZ)$ = -2.9322713663802804

$E_{HF}^{PySCF}(aug-cc-pVQZ)$ = -2.932878077558255

Comparing these in a bar plot we have.

In [135]:
<Container object of 6 artists>

The energy calculated is getting close to the Hartree-Fock limited which is not the real energy of the molecule as it can get even lower if the correlation energy is included. These are the many-body effects which are being ignored in the mean-field approximation to speed up the calculations. The most accurate energy for HeH+ is __. So the mean-field approximation is getting very close to the full energy.

Molecular orbitals

Let's consider the shape of the molecular orbtials and electron density for this molecule along the bond length.

In [247]:
C = np.matmul(X,Cprime)
P = np.array([[ 1.28614168,0.54017322],
 [ 0.54017322 ,0.22687011]])

x = np.linspace(-8,8,num=1000)
r1 = abs(x+R/2)
r2 = abs(x-R/2)

psi_CGF_STO3G_He = Coeff[2,0]*(2*Expon[2,0]/np.pi)**(0.75)*np.exp(-Expon[2,0]*r1**2) \
                + Coeff[2,1]*(2*Expon[2,1]/np.pi)**(0.75)*np.exp(-Expon[2,1]*r1**2) \
                + Coeff[2,2]*(2*Expon[2,2]/np.pi)**(0.75)*np.exp(-Expon[2,2]*r1**2)
psi_CGF_STO3G_H = Coeff[2,0]*(2*Expon[2,0]/np.pi)**(0.75)*np.exp(-Expon[2,0]*r2**2) \
                + Coeff[2,1]*(2*Expon[2,1]/np.pi)**(0.75)*np.exp(-Expon[2,1]*r2**2) \
                + Coeff[2,2]*(2*Expon[2,2]/np.pi)**(0.75)*np.exp(-Expon[2,2]*r2**2)

density = np.zeros(x.shape)
density = density + P[0,0]*psi_CGF_STO3G_He*psi_CGF_STO3G_He
density = density + P[1,1]*psi_CGF_STO3G_H*psi_CGF_STO3G_H
density = density + 2*P[0,1]*psi_CGF_STO3G_He*psi_CGF_STO3G_H
In [193]:
plt.title("Molecular orbitals")

<matplotlib.legend.Legend at 0x111c98400>
In [248]:
plt.title("Electron density $\Psi^{2}$")
[<matplotlib.lines.Line2D at 0x114e35400>]

Let's compare these results with a larger basis set. This is the input script for the program Gaussian which can use much larger basis sets.

#p RHF/aug-cc-pVQZ SP


1 1 He -1.41702 0.00000 0.00000 H -0.82048 0.00000 0.00000

Output energy is -2.932878077558184 which is close to pySCF solution.

Let's plot the wavefunctions and the electron density.

In [250]:
LUMO = np.genfromtxt('LUMOHeH+.txt')
HOMO = np.genfromtxt('HOMOHeH+.txt')
density = np.genfromtxt('densityHeH+.txt')

plt.title("Molecular orbitals RHF/aug-cc-pVQZ")
plt.title("Electron density $e\AA^3$ RHF/aug-cc-pVQZ")
[<matplotlib.lines.Line2D at 0x1162b8198>]

The larger basis set provides more electron density on the Helium atom. You can also see how well the cusp is modelled with many basis sets added together.

If we grab the fchk file we can plot the molecular orbitals in three-dimensions using Avogadro software. Here is the occupied molecular orbital HOMO the surface is the isosurface at the 0.02 density value.

Occupied orbital HOMO

And the unoccupied orbital LUMO

We can also plot the electron density at the 0.002 $e$Å$^3$ value which is roughly where the exchange energy is very strong and you have repuslion between the molecules.


You may be considering that these calculations overly complicated for a simple diatomic such as HHe+ and attempting anything larger would be a nightmare. Fortunately many free publically accessible codes are available for these calculations. If you want to get your hands on something right now there is the website called http://molcalc.org that allows you to do calculations on simple molecules right from your web browser. If you want to do some proper calculations I would recommend getting a copy of the software packages NWChem or GAMESS. To construct your own geometries you will need a program like Avogadro which can be used to build molecules and the input text files you need as input to these quantum chemistry codes.

In [108]:
# Stylings from http://lorenabarba.com
from IPython.core.display import HTML
def css_styling():
    styles = open("custom.css", "r").read()
    return HTML(styles)