Hartree-Fock Self Consistent Field Procedure
Quick links to the program and files you’ll need

SCF program: `scf.py`

Overlap integrals: `s.dat`

,

Kinetic Energy integrals:`t.dat`

,

Potential Energy integrals: `v.dat`

,

Nuclear Repulsion: `enuc.dat`

,

Electron Repulsion integrals: `eri.dat`

I do quite a bit of development work as a graduate student, and one of the most helpful parts of my education was writing a Hartree-Fock SCF (self-consistent field) program from scratch. The SCF procedure is the workhorse of most computational chemistry software packages, as it is generally the first step before doing more advanced calculations (such as MP2, etc). It is also the conceptual basis for molecular orbital theory . I’ve shared my code below, in case you want to try it out for yourself. I wrote it in Python 2.7 (it won’t work in Python 3), utilizing the NumPy package. I find it to be very readable, which makes testing out ideas fast and (relatively) intuitive. The program is written to give an idea how SCF calculations work, so it isn’t that efficient – but I only use it for two electron systems anyway so who cares!

#!/usr/bin/python
####################################
#
# SELF CONSISTENT FIELD METHOD
#
####################################
from __future__ import division
import sys
import math
import numpy as np
from numpy import genfromtxt
import csv
####################################
#
# FUNCTIONS
#
####################################
# Symmetrize a matrix given a triangular one
def symmetrize ( a ):
return a + a . T - np . diag ( a . diagonal ())
# Return compund index given four indices
def eint ( a , b , c , d ):
if a > b : ab = a * ( a + 1 ) / 2 + b
else : ab = b * ( b + 1 ) / 2 + a
if c > d : cd = c * ( c + 1 ) / 2 + d
else : cd = d * ( d + 1 ) / 2 + c
if ab > cd : abcd = ab * ( ab + 1 ) / 2 + cd
else : abcd = cd * ( cd + 1 ) / 2 + ab
return abcd
# Return Value of two electron integral
# Example: (12\vert 34) = tei(1,2,3,4)
# I use chemists notation for the SCF procedure.
def tei ( a , b , c , d ):
return twoe . get ( eint ( a , b , c , d ), 0.0 )
# Put Fock matrix in Orthonormal AO basis
def fprime ( X , F ):
return np . dot ( np . transpose ( X ), np . dot ( F , X ))
# Diagonalize a matrix. Return Eigenvalues
# and non orthogonal Eigenvectors in separate 2D arrays.
def diagonalize ( M ):
e , Cprime = np . linalg . eigh ( M )
#e=np.diag(e)
C = np . dot ( S_minhalf , Cprime )
return e , C
# Make Density Matrix
# and store old one to test for convergence
def makedensity ( C , P , dim , Nelec ):
OLDP = np . zeros (( dim , dim ))
for mu in range ( 0 , dim ):
for nu in range ( 0 , dim ):
OLDP [ mu , nu ] = P [ mu , nu ]
P [ mu , nu ] = 0.0e0
for m in range ( 0 , Nelec // 2 ):
P [ mu , nu ] = P [ mu , nu ] + 2 * C [ mu , m ] * C [ nu , m ]
return P , OLDP
# Make Fock Matrix
def makefock ( Hcore , P , dim ):
F = np . zeros (( dim , dim ))
for i in range ( 0 , dim ):
for j in range ( 0 , dim ):
F [ i , j ] = Hcore [ i , j ]
for k in range ( 0 , dim ):
for l in range ( 0 , dim ):
F [ i , j ] = F [ i , j ] + P [ k , l ] * ( tei ( i + 1 , j + 1 , k + 1 , l + 1 ) - 0.5e0 * tei ( i + 1 , k + 1 , j + 1 , l + 1 ))
return F
# Calculate change in density matrix
def deltap ( P , OLDP ):
DELTA = 0.0e0
for i in range ( 0 , dim ):
for j in range ( 0 , dim ):
DELTA = DELTA + (( P [ i , j ] - OLDP [ i , j ]) ** 2 )
DELTA = ( DELTA / 4 ) ** ( 0.5 )
return DELTA
# Calculate energy at iteration
def currentenergy ( P , Hcore , F , dim ):
EN = 0.0e0
for mu in range ( 0 , dim ):
for nu in range ( 0 , dim ):
EN = EN + 0.5 * P [ mu , nu ] * ( Hcore [ mu , nu ] + F [ mu , nu ])
return EN
####################################
#
# FORM CORE HAMILTONIAN
#
####################################
Nelec = 2 # The number of electrons in our system
# ENUC = nuclear repulsion, Sraw is overlap matrix, Traw is kinetic energy matrix,
# Vraw is potential energy matrix
ENUC = genfromtxt ( './enuc.dat' , dtype = float , delimiter = ',' )
Sraw = genfromtxt ( './s.dat' , dtype = None )
Traw = genfromtxt ( './t.dat' , dtype = None )
Vraw = genfromtxt ( './v.dat' , dtype = None )
# dim is the number of basis functions
dim = int (( np . sqrt ( 8 * len ( Sraw ) + 1 ) - 1 ) / 2 )
# Initialize integrals, and put them in convenient Numpy array format
S = np . zeros (( dim , dim ))
T = np . zeros (( dim , dim ))
V = np . zeros (( dim , dim ))
for i in Sraw : S [ i [ 0 ] - 1 , i [ 1 ] - 1 ] = i [ 2 ]
for i in Traw : T [ i [ 0 ] - 1 , i [ 1 ] - 1 ] = i [ 2 ]
for i in Vraw : V [ i [ 0 ] - 1 , i [ 1 ] - 1 ] = i [ 2 ]
# The matrices are stored triangularly. For convenience, we fill
# the whole matrix. The function is defined above.
S = symmetrize ( S )
V = symmetrize ( V )
T = symmetrize ( T )
Hcore = T + V
#print Hcore
#####################################
#
# TWO ELECTRON INTEGRALS
#
#####################################
# Like the core hamiltonian, we need to grab the integrals from the
# separate file, and put into ERIraw (ERI = electron repulsion integrals).
# I chose to store the two electron integrals in a python dictionary.
# The function 'eint' generates a unique compund index for the unique two
# electron integral, and maps this index to the corresponding integral value.
# 'twoe' is the name of the dictionary containing these.
ERIraw = genfromtxt ( './eri.dat' , dtype = None )
twoe = { eint ( row [ 0 ], row [ 1 ], row [ 2 ], row [ 3 ]) : row [ 4 ] for row in ERIraw }
#####################################
#
# SCF PROCEDURE
#
#####################################
# Step 1: orthogonalize the basis (I used symmetric orthogonalization,
# which uses the S^(-1/2) as the transformation matrix. See Szabo and Ostlund
# p 143 for more details.
SVAL , SVEC = np . linalg . eig ( S )
SVAL_minhalf = ( np . diag ( SVAL ** ( - 0.5 )))
S_minhalf = np . dot ( SVEC , np . dot ( SVAL_minhalf , np . transpose ( SVEC )))
# This is the main loop. See Szabo and Ostlund p 146.
# Try uncommenting the print lines to see step by step
# output. Look to see the functions defined above for a better
# understanding of the process.
P = np . zeros (( dim , dim )) # P is density matrix, set intially to zero.
DELTA = 1.0
convergence = 0.00000001
G = np . zeros (( dim , dim )) # The G matrix is used to make the Fock matrix
while DELTA > convergence :
F = makefock ( Hcore , P , dim )
# print "F = \n", F
Fprime = fprime ( S_minhalf , F )
# print "Fprime = \n", Fprime
E , Cprime = np . linalg . eigh ( Fprime )
C = np . dot ( S_minhalf , Cprime )
E , C = diagonalize ( Fprime )
# print "C = \n", C
P , OLDP = makedensity ( C , P , dim , Nelec )
# print "P = \n", P
# test for convergence. if meets criteria, exit loop and calculate properties of interest
DELTA = deltap ( P , OLDP )
#print "E= ",currentenergy(P,Hcore,F,dim)+ENUC
#print "Delta = ", DELTA,"\n"
EN = currentenergy ( P , Hcore , F , dim )
print "TOTAL E(SCF) = \n " , EN + ENUC
#print "C = \n", C

To use it, download the `scf.py`

file to
your computer, and put it in a folder along with `s.dat`

, `t.dat`

,
`v.dat`

, and `enuc.dat`

. These are the integral values it needs to
run: `s.dat`

is the overlap
matrix values,
`t.dat`

is the kinetic energy matrix, `v.dat`

is the potential
energy
matrix, and `enuc.dat`

contains the nuclear repulsion energy. You’ll also need
the `eri.dat`

, which contains the two electron integrals. All values were taken from `Gaussian09`

for HeH+ at a bond length of 0.9295 Angstrom with an STO-3G basis set. Once you have those downloaded, fire up your terminal and give it a try! See the example of my terminal below for usage. Just navigate to the folder you have everything in, and type `python scf.py`

then hit Enter!