3. Tutorial

This tutorial shows the usage of the var_mesh package. The code snippets can be executed in a Python shell when going from top to bottom. When looking at specific examples, some previous inputs may be necessary to run them. To run the example separately, the script can be found at the bottom of the section.
The prerequisite for this tutorial is a successful installation of the package. Test the installation with the following command in your Python shell.
>>> import var_mesh

3.1. Minimal example

With the goal in mind to optimize meshes for further DFT calculations, a simple example calculation for water can be done. For this, the necessary pyscf and var_mesh modules need to be imported.

>>> from pyscf import dft, gto
>>> from var_mesh import var_mesh

Next, the water mol object can be created, along with the RKS object for the restricted Kohn-Sham SCF calculation.

>>> mol = gto.M(atom='O 0 0 0; H 0 0 0.95691; H 0.95691 0 -0.23987')
>>> mf = dft.RKS(mol)

Calling the function var_mesh() and overwriting the existing Grids object will create the optimized mesh for the calculation, which can be started afterwards.

>>> mf.grids = var_mesh(mf.grids)
Atom grids = {'H': (30, 110), 'O': (60, 302)}
>>> mf.kernel()
converged SCF energy = -74.7350158141935

The output from var_mesh() can be reused for future calculations.

>>> mf.grids.atom_grid = {'H': (30, 110), 'O': (60, 302)}
>>> mf.kernel()
converged SCF energy = -74.7350158141935

The script for this example can be downloaded here.

3.2. Custom grids

Instead of default PySCF grid levels, custom radial or angular grids can be used as well. For this, the parameters rad or ang can be overwritten with dictionaries that have the atom type identifiers as keys, with lists of the respective number of grids as values.

>>> from var_mesh import gen_mesh
>>> gen_mesh.rad = {'H': list(range(10, 100, 15)), 'O': list(range(20, 200, 20))}
>>> print(gen_mesh.rad)
{'H': [10, 25, 40, 55, 70, 85], 'O': [20, 40, 60, 80, 100, 120, 140, 160, 180]}

The angular grids have to follow the Lebedev order. The array ang_grids can be used for this purpose and contains all possible grid numbers.

>>> gen_mesh.ang = {'H': gen_mesh.ang_grids[15:20], 'O': gen_mesh.ang_grids[20:25]}
>>> print(gen_mesh.ang)
{'H': array([350, 434, 590, 770, 974]), 'O': array([1202, 1454, 1730, 2030, 2354])}

Different attributes of the Grids class can also freely be used and will be respected in the optimizations. See the documentation for more details.

>>> mesh = dft.Grids(mol)
>>> mesh.prune = None

Changing the attribute verbose will change the amount of output of the var_mesh() function, with the maximum output at level 6. The error threshold can be changed as well.

>>> mesh.verbose = 5
>>> mesh = var_mesh(mesh, thres=1e-7)
Start coarse grid search.
[1/5] Error = 5.79931e-04
[2/5] Error = 1.44722e-07
[3/5] Error = 2.00122e-08
Error condition met.
Level = 2
Start fine grid search.
[1/6] Error = 3.13417e-05
[2/6] Error = 1.49391e-08
Error condition met.
Levels per atom type:
'H' = 2
'O' = 1
Atom grids = {'H': (40, 590), 'O': (40, 1454)}
>>> print('Mesh points = %d' % len(mesh.coords))
Mesh points = 105360

One can see that only five combinations in the coarse grid search will be tested. Because the custom angular grid levels for hydrogen has the shortest list of grid numbers, only the first five elements will be used for every other atomic species.

The script for this example can be downloaded here.

3.3. Helper functions

The package comes with functions to visualize meshes. The function plot_mesh_3d() will open an interactive 3d plot with grid points colored in green, and the atoms colored in their respective CPK color. The grid points can be scaled by their respective weights.

>>> from var_mesh import plot_mesh_3d
>>> mesh = dft.Grids(mol)
>>> mesh.level = 0
>>> mesh.build()
>>> plot_mesh_3d(mesh, weight=True)
_images/plot_mesh_3d.png

The grid can be projected to a given plane, and the grid points can be scaled by a given integer.

>>> from var_mesh import plot_mesh_2d
>>> plot_mesh_2d(mesh, weight=5, plane='xz')
_images/plot_mesh_2d.png

The script for this example can be downloaded here.

3.4. Precise option

The fine grid search is enabled by default but can be disabled with the parameter precise. Disabling this option will result in a faster grid generation process, but the resulting grid may be larger.

>>> from timeit import default_timer
>>> start = default_timer()
... mesh = var_mesh(mf.grids, thres=1e-8, precise=False)
... end = default_timer()
... print('Time spent = %f seconds' % (end - start))
... print('Mesh points = %d' % len(mesh.coords))
Atom grids = {'H': (60, 434), 'O': (90, 590)}
Time spent = 1.228876 seconds
Mesh points = 60828

This can be compared to the output when the parameter precise is set to True

>>> start = default_timer()
... mesh = var_mesh(mf.grids, thres=1e-8, precise=True)
... end = default_timer()
... print('Time spent = %f seconds' % (end - start))
... print('Mesh points = %d' % len(mesh.coords))
Atom grids = {'H': (50, 302), 'O': (90, 590)}
Time spent = 3.657851 seconds
Mesh points = 48500

The script for this example can be downloaded here.

3.5. Mode option

The mode parameter will change the format of the final grid output. These outputs can be reused in the respective codes. Currently, the mode 'pyscf', 'erkale', and 'gamess' are supported (case-insensitive).

>>> mesh = dft.Grids(mol)
>>> print('mode=PySCF:')
>>> var_mesh(mesh, mode='pyscf')
mode=PySCF:
PySCF grid: atom_grid = {'H': (30, 110), 'O': (60, 302)}

If the code does not support different grids for different atom types, the precise option has no effect. If it is turned on, a warning will be generated.

>>> print('mode=ERKALE:')
>>> var_mesh(mesh, precise=True, mode='erkale')
mode=ERKALE:

WARN: The precise parameter has no effect when using 'gamess' mode.

ERKALE grid: DFTGrid 60 -302

Also, only the largest atom grid will be used in an optimization step. Thus, the atom grids will differ from the default 'pyscf' mode and will be the same for every atom type.

>>> print('mode=GAMESS:')
>>> mesh = var_mesh(mesh, precise=False, mode='gamess')
mode=GAMESS:
GAMESS grid: $dft nrad=60 nleb=302 $end
>>> print('GAMESS grid: %s' % mesh.atom_grid)
GAMESS atom grids: {'H': (60, 302), 'O': (60, 302)}

The script for this example can be downloaded here.

3.6. Mesh time

A more sophisticated way to show the time difference would be to time both options for a set of different thresholds.

>>> import numpy as np
>>> errors = 10.0**(np.arange(-1, -9, -1))
>>> print(errors)
[1.e-01 1.e-02 1.e-03 1.e-04 1.e-05 1.e-06 1.e-07 1.e-08]

A code to time the different options can look like the following

>>> times_false = []
>>> times_true = []
>>> for i in range(len(errors)): \
...     t1 = default_timer() \
...     mesh = var_mesh(mesh, thres=errors[i], precise=False) \
...     t2 = default_timer() \
...     mesh = var_mesh(mesh, thres=errors[i], precise=True) \
...     t3 = default_timer() \
...     times_false.append(t2 - t1) \
...     times_true.append(t3 - t2)

These result can be plotted afterwards.

>>> import matplotlib.pyplot as plt
>>> plt.plot(errors, times_false, label='precise=False')
>>> plt.plot(errors, times_true, label='precise=True')
>>> plt.xlabel('Mesh error')
>>> plt.ylabel('Time [s]')
>>> plt.xscale('log')
>>> plt.gca().invert_xaxis()
>>> plt.legend()
>>> plt.show()
_images/mesh_time.png

The script for this example can be downloaded here.

3.7. Calculation time

Another interesting aspect might be the grid generation time in relation to the DFT calculation time.

>>> mf = dft.RKS(mol)
>>> mf.verbose = 0
>>> mf.grids.verbose = 0
>>> time_mesh = []
>>> time_scf = []
>>> for i in range(len(errors)): \
...     t1 = default_timer() \
...     mf.grids = var_mesh(mf.grids, thres=errors[i], precise=True) \
...     t2 = default_timer() \
...     mf.kernel() \
...     t3 = default_timer() \
...     time_mesh.append(t2 - t1) \
...     time_scf.append(t3 - t2)

These result can be plotted as well.

>>> plt.plot(errors, time_mesh, label='VarMesh')
>>> plt.plot(errors, time_scf, label='SCF')
>>> plt.xlabel('Mesh error')
>>> plt.ylabel('Time [s]')
>>> plt.xscale('log')
>>> plt.gca().invert_xaxis()
>>> plt.legend()
>>> plt.show()
_images/calculation_time.png

The script for this example can be downloaded here.

3.8. PyFLOSIC example

This package can be used with the pyflosic package, too. Since pyflosic only supports Python 3, this example can not be executed with Python 2. The package ase is required as well.

>>> from ase.io import read
>>> from flosic_os import ase2pyscf, xyz_to_nuclei_fod
>>> from flosic_scf import FLOSIC
At first, a geometry file is needed: H2.xyz
Next, the calculation details have to be set up.
>>> molecule = read('H2.xyz')
>>> geo, nuclei, fod1, fod2, included = xyz_to_nuclei_fod(molecule)
>>> mol = gto.M(atom=ase2pyscf(nuclei), basis='6-311++Gss', spin=0, charge=0)
>>> sic_object = FLOSIC(mol, xc='lda,pw', fod1=fod1, fod2=fod2, ham_sic='HOO')
>>> sic_object.max_cycle = 300
>>> sic_object.conv_tol = 1e-7

By default a grid level of 3 will be used. Here we compare the mesh size before and after the optimization.

>>> mesh_size = len(sic_object.calc_uks.grids.coords)
>>> print('Mesh size before: %d' % mesh_size)
Mesh size before: 28186
>>> sic_object.calc_uks.grids = var_mesh(sic_object.calc_uks.grids)
>>> print('Mesh size after: %d' % len(sic_object.calc_uks.grids.coords))
Mesh size after: 6600

Finally, start the FLO-SIC calculation.

>>> sic_object.kernel()
ESIC = -0.045866
ESIC = -0.045129
ESIC = -0.045133
ESIC = -0.045130
ESIC = -0.045129
ESIC = -0.045129
ESIC = -0.045129
ESIC = -0.045129
ESIC = -0.045129
ESIC = -0.045129
ESIC = -0.045129
ESIC = -0.045129
ESIC = -0.045129
converged SCF energy = -1.18118690828491  <S^2> = 6.6613381e-16  2S+1 = 1

The script for this example can be downloaded here.