Python Interface¶
The Python interface enables DFTB calculations to be performed by external programs. Results such as system energy, atomic forces and Mulliken charges can be extracted directly, along with performing tasks like defining calculations in external potentials or updating the geometry. With the pythonapi (version 0.1) an existing input file (dftb_in.hsd), which contains settings in addition to the geometry, is required for the initialization of DFTB+.
Regarding the units: Just like DFTB+, the interface expects and delivers values in atomic units!
Setting up DFTB+¶
For this special use case, DFTB+ needs to be compiled as a shared library with API support enabled. At this point, a basic understanding of how to build DFTB+ is assumed (all necessary steps are explained in the INSTALL.rst file in the top level directory of the DFTB+ repository). The CMake configuration for this case can be done by setting the WITH_API and BUILD_SHARED_LIBS flags to be TRUE in the file config.cmake, before starting the configuration and compilation process. Alternatively, if you do not want to modify files, a construct like the following is a convenient way to specify these flags on the command line while configuring with CMake:
cmake -DBUILD_SHARED_LIBS=1 -DWITH_API=1 ..
If only the pure compilation process is carried out, the resulting library is
located inside the buid directory at prog/dftb+/. If make install
was
executed, there is also a copy placed in the CMAKE_INSTALL_PREFIX path, which
defaults to install/lib/ inside the build directory. Within these locations,
the library files libdftbplus.* will be installed.
The path to the resulting shared library must be passed to the interface, so you will need to know where the libdftbplus.* files ends up.
Setting up the interface¶
You can install the associated Python package via the standard ‘python setup’
mechanism. If you want to install it system-wide into your normal python
installation, with the appropriate permissions you can simply issue python
setup.py
in the directory tools/pythonapi/. Alternatively, to install it
locally in your home space, use python setup.py install --user
. If the local
Python installation directory is not in your PATH, you should add it
accordingly.
Providing the input for DFTB+¶
[Input: recipes/interfaces/pyapi/]
To initialize an instance of the dftbplus calculator, you will need an initial file formatted according to the HSD structure for the usual dftb_in.hsd input of DFTB+.
Although the geometry of the structure to be calculated can be later replaced by your main script, it is necessary first to supply a dummy geometry (suitable for the desired number of atoms and boundary conditions). It is important to ensure that the total number of coordinates matches the number of atoms of the real geometry and that any specified lattice vectors are linearly independent, otherwise DFTB+ will return an error message. In the case of the supplied TiO2 example the following geometry block of dftb_in.hsd would be appropriate:
Geometry = GenFormat {
6 S
Ti O
1 1 0.0000000000E+00 0.0000000000E+00 0.0000000000E+00
2 1 0.0000000000E+00 0.0000000000E+00 0.0000000000E+00
3 2 0.0000000000E+00 0.0000000000E+00 0.0000000000E+00
4 2 0.0000000000E+00 0.0000000000E+00 0.0000000000E+00
5 2 0.0000000000E+00 0.0000000000E+00 0.0000000000E+00
6 2 0.0000000000E+00 0.0000000000E+00 0.0000000000E+00
0.0000000000E+00 0.0000000000E+00 0.0000000000E+00
1.0000000000E+02 0.0000000000E+00 0.0000000000E+00
0.0000000000E+00 1.0000000000E+02 0.0000000000E+00
0.0000000000E+00 0.0000000000E+00 1.0000000000E+02
}
The section Hamiltonian
contains the usual entries for the calculation
of a TiO2 supercell:
Hamiltonian = DFTB {
Scc = Yes
MaxSccIterations = 100
SccTolerance = 1.00e-05
SlaterKosterFiles = Type2FileNames {
Prefix = "./slakos/mio-ext/"
Separator = "-"
Suffix = ".skf"
}
MaxAngularMomentum {
Ti = "d"
O = "p"
}
KPointsAndWeights = SupercellFolding {
4 0 0
0 4 0
0 0 4
0.5 0.5 0.5
}
}
To be able to also extract the forces via the interface, it is advisable to
specify the corresponding entry in the Analysis{}
block. Otherwise DFTB+
will issue an error message, asking the user to do so. To ensure backwards
compatibility of the input, the parser version should also be specified:
Analysis {
CalculateForces = Yes
}
ParserOptions {
ParserVersion = 8
}
Main script¶
The script shown here serves to illustrate the use of the Python interface, based on the calculation of TiO2.
In order to be able to use the interface, the package dftbplus must be imported as the first step. The path LIB_PATH to the DFTB+ shared library is defined (note that the name prefix of the libray file name should also be part of the path), as well as conversion factors to convert the atom coordinates we will list in from Ångström into the atomic units (Bohr) required by the interface.
import numpy as np
import dftbplus
LIB_PATH = '/home/user/libdftbplus'
# DFTB+ conversion factors
# (according to prog/dftb+/lib_common/constants.F90)
BOHR__AA = 0.529177249
AA__BOHR = 1 / BOHR__AA
At the beginning of the main()
function, the atom coordinates and lattice
vectors are defined. In this case, a conversion to atomic units is necessary,
since a .gen block is used whose values are usually in units of Ångström.
def main():
'''Main driver routine.'''
# coordinates of TiO2, in Ångström
coords = np.array([
[-0.016726922839251, 0.016725329441158, -0.000003204152532],
[-0.016726505918979, 1.920201169305565, -7.297102897292027],
[ 0.017412997824265, -0.024318617967798, 2.005339137853385],
[ 1.920770753428742, -0.024319922392223, -4.437737763954652],
[ 0.024319174400169, -0.017404302527510, -2.005347277168561],
[ 0.024317270342179, 1.886164739806594, -5.291732430733527]])
# lattice vectors of TiO2, in Ångström
latvecs = np.array([
[-1.903471721000000, 1.903471721000000, 4.864738245000000],
[ 1.903471721000000, -1.903471721000000, 4.864738245000000],
[ 1.903471721000000, 1.903471721000000, -4.864738245000000]])
# conversion to atomic units
coords *= AA__BOHR
latvecs *= AA__BOHR
An object of the DftbPlus class is instantiated, which requires the location of the shared library libpath, the HSD input file hsdpath and the name of the log file logfile to be optionally specified. These keywords have default values ‘./libdftbplus’, ‘./dftb_in.hsd’ and None if not set explicitly. Note, that adding the shared library extension to libpath is not essential. Since the extension can be system dependent, it is guessed by the interface if missing. If logfile=None is specified, the output of the calculation gets printed to stdout.
After instantiation, the geometry can set or replaced; for periodic structures, lattice vectors can be specified in addition to the absolute coordinates.
The DFTB+ calculations are carried out automatically, as soon as the
corresponding get_* methods are called. To correctly finalize the DFTB+ object,
use the close()
method.
cdftb = dftbplus.DftbPlus(libpath=LIB_PATH,
hsdpath='dftb_in.hsd',
logfile='TiO2.log')
# set geometry
cdftb.set_geometry(coords, latvecs=latvecs)
# get number of atoms
natoms = cdftb.get_nr_atoms()
# calculate energy, gradients and Gross charges
merminen = cdftb.get_energy()
gradients = cdftb.get_gradients()
grosschg = cdftb.get_gross_charges()
# finalize DFTB+ and clean up
cdftb.close()
if __name__ == "__main__":
main()
As always, please consult the archive to obtain the complete, connected script. To do so, follow the path mentioned above.
The Interface is also capable of defining a population (in)dependent external potential. This is covered in the following two sections (extpot, qdepextpot).
Using a population independent external potential¶
[Input: recipes/interfaces/pyapi/extpot/]
A external potential which does not depend on the Mulliken charges in the
calculation can be included with only a small addition in the script. The DFTB+
object has a method set_external_potential()
, which should be relatively
self-explanatory. The external potential at the position of the QM-atoms is
given as a positional argument. If forces are required, the gradient of the
external potential at each atom can be passed additionally as the keyword
argument extpotgrad.
Therefore, after the initialization of the DFTB+ object, the following code is inserted:
# example values of extpot and extpotgrad used here were
# taken from file: test/api/mm/testers/test_extpot.f90
extpot = np.array([-0.025850198503435,
-0.005996294763958,
-0.022919371690684])
extpotgrad = np.array([
[0.035702717378527, 0.011677956375860, 0.009766745155626],
[0.023243271928971, -0.000046945156575, 0.004850533043745],
[0.016384005706180, 0.004608295375551, 0.005401080774962]])
# set external potential and its gradients
cdftb.set_external_potential(extpot, extpotgrad=extpotgrad)
Population dependent external potential¶
[Input: recipes/interfaces/pyapi/qdepextpot/]
This section deals with the capability of the interface to run calculations with a population dependent external potential, i.e. arrising in cases like polarizable surroundings where the applied field responds to the state of the QM calculation. Since in general only the user knows how to calculate this type of potential, callback functions can be defined which will then be executed at runtime.
The DFTB+ object provides a method register_ext_pot_generator()
that takes
care of the registration of the callback functions. As the first positional
argument of this method, an arbitrary pointer can be specified. DFTB+ will pass
back this pointer unaltered when calling the registered functions. You can
typically use it to pass a pointer to the data or a Python object (class) which
contains the necessary data for the potential calculation. If your data is in
the global space and you do not need it, pass None (or equivalent). The second
and third positional arguments have to be the function that provides the
external potential and its gradients.
Furthermore, the auxiliary class PotentialCalculator
is defined to perform
the actual calculation of the external potential and its gradients. The
structure of a script required for the calculation is explained below, using a
trivial example in which the external potential and gradient are assumed to be
zero. Therefore, this should not change the results of the calculation.
import numpy as np
import dftbplus
LIB_PATH = '/home/user/libdftbplus'
class PotentialCalculator:
'''
Auxiliary class for calculating the population dependent external
potential and its gradients. An instance of this class gets handed over
to DFTB+ via the ctypes interface, to handle the necessary callbacks.
'''
def __init__(self, qmcoords, mmcoords, mmcharges):
'''Initializes a PotentialCalculator object.
Args:
qmcoords (2darray): coordinates of QM-atoms
(shape: [qmatoms, 3])
mmcoords (2darray): coordinates of MM-atoms
(shape: [mmatoms, 3])
mmcharges (1darray): charges of MM-atoms
(shape: [mmatoms, 1])
'''
self._qmcoords = qmcoords
self._mmcoords = mmcoords
self._qmatoms = np.shape(self._qmcoords)[0]
self._mmatoms = np.shape(self._mmcoords)[0]
self._mmcharges = mmcharges
def calc_extpot(self, dqatom):
'''Calculates the current external potential
using the properties of the MM- and QM-atoms.
Args:
dqatom (1darray): population difference with respect to
reference population (usually the neutral atom)
Note: population means electrons, so a
positive number indicates electron excess
Returns:
extpot (1darray): updated external potential
at the position of each QM-atom
'''
# Note: Some types of potential require knowledge of the
# current atomic populations, which is provided by dqatom.
extpot = np.zeros(self._qmatoms)
return extpot
def calc_extpotgrad(self, dqatom):
'''Calculates the current gradients of the external
potential using the properties of the MM- and QM-atoms.
Args:
dqatom (1darray): population difference with respect to
reference population (usually the neutral atom)
Note: population means electrons, so a ositive number
indicates electron excess
Returns:
extpotgrad (2darray): updated potential gradient
at the position of each QM-atom
'''
# Note: Some types of potential require knowledge of the
# current atomic populations, which is provided by dqatom.
extpotgrad = np.zeros((self._qmatoms, 3))
return extpotgrad
def get_extpot(potcalc, dqatom, extpotatom):
'''Queries the external potential.
Args:
potcalc (pyobject): instance of a class that provides methods for
calculating the external potential and its gradients
dqatom (1darray): population difference with respect to reference
population (usually the neutral atom)
Note: population means electrons, so a positive number indicates
electron excess
extpotatom (1darray): potential at the position of each QM-atom
Note: it should be the potential as felt by an electron
(negative potential value means attraction for an electron)
'''
extpotatom[:] = potcalc.calc_extpot(dqatom)
def get_extpotgrad(potcalc, dqatom, extpotatomgrad):
'''Queries the external potentials gradients.
Args:
potcalc (pyobject): instance of a class that provides methods for
calculating the external potential and its gradients
dqatom (1darray): population difference with respect to referenc
population (usually the neutral atom)
Note: population means electrons, so a positive number indicates
electron excess
extpotatomgrad (2darray): potential gradient at the position of each
QM-atom
Note: it should be the gradient of the potential as felt by an
electron (negative potential value means attraction for an
electron)
'''
extpotatomgrad[:, :] = potcalc.calc_extpotgrad(dqatom)
The initialization of the calculator and the definition of the geometry is completely analogous to the above explanations. Only the registration of the callback functions is still missing:
# register callback functions for a qdepextpot calculation
cdftb.register_ext_pot_generator(potcalc, get_extpot, get_extpotgrad)
Please consult the associated archive with this tutorial to obtain the full corresponding example.