Plotting of grid data
We will use the Mayavi package to plot grid data. This can be installed with
conda install -c conda-forge mayavior
mamba install mayaviIf you want to use Mayavi with Jupyter notebooks, you will need to install the ipyevents package with
conda install -c conda-forge ipyeventsor
mamba install ipyeventsIf you want to have interactive plots in your notebooks, you also need to install the x3dom Javascript extension with
jupyter nbextension install --py mayavi --user
We start by importing the necessary packages.
import numpy as np
from mayavi import mlab
If you are using Jupyter notebooks, you will need to run the following cell to initialize Mayavi:
mlab.init_notebook()
Molecular orbitals are one-electron wavefunctions. In our HF calculations, we represent them in the basis of atomic orbitals, which are themselves one-electron wavefunctions. To visualize them in 3D-space, we need to evaluate them on a spatial grid. We shall at first define a function to evaluate a contracted Gaussian basis function in space:
def evaluate_gaussian_basis(g, x, y, z):
i, j, k = g.ijk
x0, y0, z0 = g.A
g_val = 0.0
for n, d, alpha in zip(g.norm_const, g.coefs, g.exps):
g_val += n * d * (x - x0)**i * (y - y0)**j * (z - z0)**k \
* np.exp(-alpha * ((x - x0)**2 + (y - y0)**2 + (z - z0)**2))
return g_val
Of course, we have to build a spatial grid first. This can be done with the following function:
def build_grid(xlim, ylim, zlim, nx, ny, nz):
x_ = np.linspace(*xlim, nx)
y_ = np.linspace(*ylim, ny)
z_ = np.linspace(*zlim, nz)
x, y, z = np.meshgrid(x_, y_, z_, indexing='ij')
return x, y, z
Afterwards, we define a function to transform the MOs from the AO basis to the position basis:
def evaluate_mo_grid(mol, grid):
x, y, z = grid
# Build MO coefficient matrix in spin-orbit basis
n_spatial_orb = len(mol.mo_energy)
c_spin = np.zeros((n_spatial_orb * 2, n_spatial_orb * 2))
c_spin[:n_spatial_orb:, ::2] = mol.mo_coeff
c_spin[n_spatial_orb:, 1::2] = mol.mo_coeff
# Evaluate AOs on the grid
ao_grid = np.zeros((n_spatial_orb * 2,
x.shape[0], x.shape[1], x.shape[2]))
for i, g in enumerate(mol.basisfunctions):
g_grid = evaluate_gaussian_basis(g, x, y, z)
ao_grid[i] = g_grid
ao_grid[i + n_spatial_orb] = g_grid
# Transform to MOs
mo_grid = np.einsum('ij,ixyz->jxyz', c_spin, ao_grid)
return mo_grid
In the end, we can define a function to plot the MOs, along with the atoms and bonds for reference:
R_VDW = {
'H': 2.0787,
'C': 3.2125,
'N': 2.9291,
'O': 2.8724,
}
R_COV = {
'H': 0.6047,
'C': 1.4173,
'N': 1.3417,
'O': 1.1905,
}
ATOM_COLOR = {
'H': (0.8, 0.8, 0.8),
'C': (0.0, 1.0, 0.0),
'N': (0.0, 0.0, 1.0),
'O': (1.0, 0.0, 0.0),
}
def visualize_cube(mol, grid, density, isovalues, colors, figure, **kwargs):
# Draw atoms
for a in mol.atomlist:
p = mlab.points3d(
*a.coord, R_VDW[a.symbol], color=ATOM_COLOR[a.symbol],
scale_factor=0.5, figure=figure,
)
# Draw bonds
for i, a in enumerate(mol.atomlist):
ca = a.coord
sa = a.symbol
ra = R_COV[sa]
for b in mol.atomlist[:i]:
cb = b.coord
sb = b.symbol
rb = R_COV[sb]
vab = cb - ca
rab = np.linalg.norm(vab)
if rab < (ra + rb) * 1.2:
mid = ca + vab * (ra / (ra + rb))
p = mlab.plot3d(*np.vstack((ca, mid)).T, tube_radius=0.2,
color=ATOM_COLOR[sa], figure=figure)
p = mlab.plot3d(*np.vstack((cb, mid)).T, tube_radius=0.2,
color=ATOM_COLOR[sb], figure=figure)
# Draw isosurface
for ival, c in zip(isovalues, colors):
p = mlab.contour3d(*grid, density, color=c, contours=[ival],
figure=figure, **kwargs)
return p
Now, we can take a HF calculation of water
# Coordinates are in the unit of Angstrom.
o1 = Atom('O', [ 0.000, 0.000, 0.000], unit='A')
h1 = Atom('H', [ 0.758, 0.587, 0.000], unit='A')
h2 = Atom('H', [-0.758, 0.587, 0.000], unit='A')
mol = HartreeFock()
mol.set_atomlist([o1, h1, h2])
mol.get_basis('sto-3g')
mol.initialize()
e_scf = mol.run_hf(verbose=0)
construct a grid
XLIM, YLIM, ZLIM = (-5.0, 5.0), (-5.0, 5.0), (-5.0, 5.0)
NX, NY, NZ = 80, 80, 80
grid = build_grid(XLIM, YLIM, ZLIM, NX, NY, NZ)
mo_grid = evaluate_mo_grid(mol, grid)
and plot HOMO and LUMO of water:
ISOSURFACE_COLORS = [(1, 0, 0), (0, 0, 1)]
ORBITAL_ISOVALUES = [0.05, -0.05]
ihomo = (mol.nocc - 1) * 2
ilumo = mol.nocc * 2
fig_homo = mlab.figure()
visualize_cube(mol, grid, mo_grid[ihomo], ORBITAL_ISOVALUES,
ISOSURFACE_COLORS, fig_homo)
fig_lumo = mlab.figure()
visualize_cube(mol, grid, mo_grid[ilumo], ORBITAL_ISOVALUES,
ISOSURFACE_COLORS, fig_lumo)
# Remove this line if you are using Jupyter notebook
mlab.show()
While the MOs are just mathematical constructions, the electron density is a measurable quantity.
The spin-free one-electron density is defined as $$ \rho(r) = \int \Psi^*(x, x_2, \cdots, x_N) \Psi(x, x_2, \cdots, x_N) \ \mathrm{d}\sigma \mathrm{d}x_2 \cdots \mathrm{d}x_N $$ where \(r\) stands for the spatial coordinate, \(\sigma\) the spin coordinate and \(x\) a combination of both of them. For a wavefunction expressed as a Slater determinant, this expression simplifies to $$ \rho(r) = \sum_{i=1}^{N} \int \phi_i(x) \phi_i(x)\ \mathrm{d} \sigma $$ where \(\phi_i\)'s are occupied spin-orbitals.
We can calculate and visualize the density as follows:
DENSITY_COLORS = [(0.3, 0.3, 0.3)]
DENSITY_ISOVALUES = [0.05]
density_grid = np.zeros((NX, NY, NZ))
for d in mo_grid[:mol.nocc * 2]:
density_grid += np.abs(d)**2
fig_density = mlab.figure()
visualize_cube(mol, grid, density_grid, DENSITY_ISOVALUES,
DENSITY_COLORS, fig_density, opacity=0.5)
# Remove this line if you are using Jupyter notebook
mlab.show()