Forte Tutorial 5: Visualizing orbitals in Jupyter notebooks


In this tutorial we are going to explore how to generate orbitals and visualize them in Jupyter notebooks using the Python API.

Let's import the psi4 and forte modules, including the forte.utils submodule


In [ ]:
import psi4
import forte
import forte.utils

Note: in this tutorial we do not call forte.startup()/cleanup() because we are not going to do any computation using forte

We will start by generating SCF orbitals via psi4 using the forte.util


In [ ]:
xyz = """
0 1
C      0.722300    0.722300    0.000000
C      0.722300   -0.722300    0.000000
C     -0.722300    0.722300    0.000000
C     -0.722300   -0.722300    0.000000
H      1.483571    1.483571    0.000000
H      1.483571   -1.483571    0.000000
H     -1.483571    1.483571    0.000000
H     -1.483571   -1.483571    0.000000
symmetry c1"""

E_scf, wfn = forte.utils.psi4_scf(xyz, 'sto-3g', 'rhf', functional = 'hf')

Generating the cube files using the forte.utils.psi4_cubeprop function

Next we use a convenience function that generates cube files. Here is some information on it:

def psi4_cubeprop(wfn, path = '.', orbs = [], nocc = 3, nvir = 3, load = False):
    """
    Run a psi4 cubeprop computation to generate cube files from a given Wavefunction object
    By default this function plots from the HOMO -2 to the LUMO + 2

    Parameters
    ----------
    wfn : psi4Wavefunction
        A psi4 Wavefunction object
    path : str
        The path of the directory that will contain the cube files
    orbs : list
        The list of orbitals to convert to cube files (one based).
    nocc : int
        The number of occupied orbitals
    nvir : int
        The number of virtual orbitals

We want to plot two occupied and two virtual orbitals so we pass the arguments nocc=2 and nvir=2.


In [ ]:
forte.utils.psi4_cubeprop(wfn,path='cubes',nocc=2,nvir=2)

After we execute this command, four cube files are generated in the folder ./cubes. We can list them with the os.listdir function:


In [ ]:
import os
os.listdir('./cubes')

Reading cube files

To load these cube files we will use the CubeFile class. This class reads a cube file and lets us manipulate them. Let's load the HOMO, which is orbital 15 (the cube files use 1-based indexing)


In [ ]:
cube = forte.CubeFile('cubes/Psi_a_15_15-A.cube')

A CubeFile object cannot be directly visualized. However, we can access all the information about the cube file.


In [ ]:
# number of atoms
print(f'cube.natoms() -> {cube.natoms()}')

# number of grid points along each direction
print(f'cube.num() -> {cube.num()}')

The CubeFile class provides a convenient method (compute_levels) to determine the isocontour levels that contain a given fraction of the electron density. For example, to compute the isovalues that encompass 85% of the density we call compute_levels('mo',0.85), where the argument 'mo' indicates that the cube file stores the values of a molecular orbital (this is necessary, because the levels are computed differently for the density)


In [ ]:
levels85 = cube.compute_levels('mo',0.85)
levels85

This information is also generated and stored by psi4 in the cube file (this is not standard, only psi4 implements this feature)


In [ ]:
with open('cubes/Psi_a_15_15-A.cube') as f:
    print("".join(f.readlines()[:6]))

Rendering cube files

Forte implements a simple 3D rederer based on the pythreejs module. To install it via conda type in the command line: conda install -c conda-forge pythreejs. The renderer is an object of the Pt3JSRendered class (a python class)


In [ ]:
renderer = forte.utils.Py3JSRenderer(width=400, height=400)

Once created, we can add it a CubeFile object and display it. This is a Python widget and it is interactive. Try to interact with it by clicking and dragging the pointer around. You can also zoom the molecule in and out.


In [ ]:
renderer.add_cubefile(cube)
renderer.display()

The CubeFile class will find the isolevels that correspond to 85% of the density. However, we can select a different value by passing the sumlevel argument.


In [ ]:
renderer2 = forte.utils.Py3JSRenderer(width=400, height=400)
renderer2.add_cubefile(cube, sumlevel=0.5)
renderer2.display()

We can even specify custom levels and colors via the levels and colors parameters


In [ ]:
renderer3 = forte.utils.Py3JSRenderer(width=400, height=400)
renderer3.add_cubefile(cube, levels = [0.06,-0.04],colors = ['#4412B3', '#0DC2FF'])
renderer3.display()

In [ ]:
cube_lumo = forte.CubeFile('cubes/Psi_a_16_16-A.cube')
renderer_lumo = forte.utils.Py3JSRenderer(width=400, height=400)
renderer_lumo.add_cubefile(cube_lumo)
renderer_lumo.display()

Rendering many cube files

Forte implements a few trick to plot several orbitals all at once. For example, if we pass the argument load=True to the function forte.utils.psi4_cubeprop we can automatically load all the cube file generated


In [ ]:
cubes = forte.utils.psi4_cubeprop(wfn,path='cubes',nocc=2,nvir=2,load=True)

There is also a convenience function (cube_viewer) that allows to plot several cube files al at once. This can be invoked with


In [ ]:
forte.utils.cube_viewer(cubes)

This function is customizable, for example we can change the size and select a different font (or even not display the text)


In [ ]:
forte.utils.cube_viewer(cubes, width=500, height=500, font_size = 20, font_family = 'Times New Roman')

Manipulating cube files

The CubeFile class supports three type of operations:

  • scale(double factor): scale all the values on the grid by factor: $$ \phi(\mathbf{r}_i) \leftarrow \mathtt{factor} * \phi(\mathbf{r}_i) $$
  • add(CubeFile cube): add to this cube file the grid values stored in cube: $$ \phi(\mathbf{r}_i) \leftarrow \phi(\mathbf{r}_i) + \psi(\mathbf{r}_i) $$
  • pointwise_product(CubeFile cube): multiply each point of this cube file with the values stored in cube: $$ \phi(\mathbf{r}_i) \leftarrow \phi(\mathbf{r}_i) * \psi(\mathbf{r}_i) $$

To evaluate the density corresponing to the HOMO we can do just compute the pointwise product with itself


In [ ]:
dens = forte.CubeFile(cube)
dens.pointwise_product(dens)

Then we can render it using the compute_levels function to find the appropriate levels. Here we cannot let the renderer determine the levels because it cannot tell that this is a density.


In [ ]:
dens_renderer = forte.utils.Py3JSRenderer(width=400, height=400)
dens_levels = dens.compute_levels('density',0.6)
dens_renderer.add_cubefile(dens, levels = dens_levels) # need to specify a sensible rendering level
dens_renderer.display()

If we want, we can also save a cubefile to disk using the save function


In [ ]:
dens.save('cubes/dens.cube')