Tutorial: Write a Abinitio-Interface

Abinitio-Interfaces are plugins for the SurfacePointProvider and are used to perform (electronic structure) calculations on molecular systems. In the following example implementations are presented in order to

Example: Adopter to the Atomic Simulation Environment

According to the documentation, the Atomic Simulation Environment (ASE_) is a set of tools and Python modules for setting up, manipulating, running, visualizing and analyzing atomistic simulations under GNU LPGL licence. Hereby, the ASE provides so called Calculators, which are similar to the Abinitio-Interfaces used in the SurfacePointProvider. In the following, we are going to construct a adopter class to use the ASE calculators within out framework.

Step 0: Basic Setup

To implement a Abinitio-Interface using the ASE we need to import some objects/methods. We will use from the ASE:

  • Atoms: which defines the basic molecule

-calculators: which gives access to the available calculators

From PySurf we will import the Abinitio class which is the baseclass of all Abinitio interfaces

1from ase import Atoms
2from ase import calculators
3from pysurf import Abinitio

Step 1: Abstractmethods in Abinitio

1 class ASEInterface(Abinitio):
2     implemented = ['energy', 'gradient']
3
4     @classmethod
5     def from_config(cls, config, atomids, nstates):
6         """used to initialize the class using userinput"""
7
8     def get(self, request):
9         """Compute requested properties and set results"""

The Abinitio baseclass defines two abstractmethods that need to be set:

  • from_config:

    used to initialize the class using the userinput

  • get:

    which is used to answer the request

implement:

states which properties are implemented, for runtime check do not lie on that…

If you write a new qm interface, those are the methods you have to implement.

The next thing is to add user configurations.

Step 2: Adding userinput for the Plugin

Before we are going to implement the abstractmethods we are going to add some custom user-input that we want to use in our Plugin. Herefor, we use the question DSL of Colt and add our own questions to our class.

 1 class ASEInterface(Abinitio):
 2
 3     _questions =  """
 4     calculator = qchem
 5
 6     [calculator(qchem)]
 7     method = b3lyp
 8     basis = 6-31g
 9
10     [calculator(psi4)]
11     method = b3lyp
12     memory = 500MB
13     basis = 6-31g
14     """
15
16     implemented = ['energy', 'gradient']
17
18     @classmethod
19     def from_config(cls, config, atomids, nstates):
20         """used to initialize the class using userinput"""
21         if nstates != 1:
22             raise Exception("ASE does not support excited states")
23         return cls(config['calculator'], atomids)
24
25     def __init__(self, calculator, atomids):
26         # define the molecule in a basic manner
27         self.molecule = Atoms(numbers=atomids)
28         self.calculator = self._select_calculator(calculator)
29
30     def _select_calculator(self, calculator):
31         if calculator == 'psi4':
32             return calculators.psi4.Psi4(atoms=self.molecule, method=calculator['method'],
33                                          memory=Calculator['memory'], basis=calculator['basis'])
34         if calculator == 'qchem':
35             return calculators.qchem.QChem(atoms=self.molecule, method=calculator['method'],
36                                            basis=calculator['basis'])
37         raise NotImplementedError("calculator not implemented")

For education purpose we only show two calculators, the one for qchem and the one for psi4.

Step 3: Implementing get

With that it is now trivial to implement the get function

 1 class ASEInterface(Abinitio):
 2     ...
 3
 4     def get(self, request):
 5         """Compute requested properties and set results"""
 6         # set the coordinates
 7         self.molecule.positions = request.crd
 8         # compute energy
 9         if 'energy' in request:
10             request['energy'].set(self.calculator.get_forces())
11         # compute gradient
12         if 'gradient'in request:
13             request['gradient'].set(self.calculator.get_forces())
14         return request

With that done, we have a new Abinitio-Interface

Example: PySCF interface:

In this example we show how to write an interface for the PySCF program package, which also supports excited states.

Step 0: Basic Setup

To implement a Abinitio-Interface using PySCF we need to import some objects/methods. We will use from the PySCF

  • gto: which defines the basic molecule

  • dft, grad, tddft: which allow to perform dft and tddft calculations for energies and gradients

From PySurf we will import the Abinitio class which is the baseclass of all Abinitio interfaces

1from pysurf import Abinitio
2from pyscf import gto, dft, tddft, grad

Step 1: Abstractmethods in Abinitio

 1 class PySCF(Abinitio):
 2
 3     methods = {}
 4     implemented = []
 5
 6     @classmethod
 7     def from_config(cls, config, atomids, nstates):
 8         """used to initialize the class using userinput"""
 9
10     def get(self, request):
11         """Compute requested properties and set results"""

The Abinitio baseclass defines two abstractmethods that need to be set:

  • from_config:

    used to initialize the class using the userinput

  • get:

    which is used to answer the request

implement:

states which properties are implemented, for runtime check do not lie on that…

If you write a new qm interface, those are the methods you have to implement.

The next thing is to add user configurations.

Step 2: Adding userinput for the Plugin

Before we are going to implement the abstractmethods we are going to add some custom user-input that we want to use in our Plugin. Herefor, we use the question DSL of Colt and add our own questions to our class.

For each method a separate class is implemented, the calculator classes. These calculator classes have to have methods with the name do_prop where prop stands for all the implemented properties, e.g. do_energy. Moreover it has to have a property implemented which is copied to the PySCF class. PySurf will check the implemented property, whether the interaface provides all necessary properties that are needed in the calculation.

 1 class PySCF(Abinitio):
 2
 3     _questions =  """
 4     basis = 631g*
 5     # Calculation Method
 6     method = DFT/TDDFT :: str :: [DFT/TDDFT]
 7     """
 8
 9     # implemented has to be overwritten by the individual classes for the methods
10     implemented = []
11
12     # dictionary containing the keywords for the method and the corresponding classes
13     methods = {'DFT/TDDFT': DFT}
14
15     @classmethod
16     def _extend_questions(cls, questions):
17         questions.generate_cases("method", {name: method.questions
18                                              for name, method in cls.methods.items()})
19
20     @classmethod
21     def from_config(cls, config, atomids, nstates):
22         method = config['method'].value
23         basis = config['basis']
24         config_method = config['method']
25         return cls(basis, method, atomids, nstates, config_method)
26
27
28     def __init__(self, basis, method, atomids, nstates, config_method):
29         """ """
30         self.mol = self._generate_pyscf_mol(basis, atomids)
31         self.nstates = nstates
32         self.atomids = atomids
33         self.basis = basis
34         # initializing the class for the corresponding method
35         self.calculator = self.methods[method].from_config(config_method, self.mol, nstates)
36         # update the implemented property
37         self.implemented = self.calculator.implemented

The code for the _generate_pyscf_mol function is shown in the next section. It is a PySCF specific function that creates the molecule object for PySCF.

Step 3: Implementing get

The get function calls the corresponding functions of the calculator class. The _generate_pyscf_mol function generates the basic molecule object of Pyscf.

 1 class PySCF(Abinitio):
 2     ...
 3    # update coordinates
 4     self.mol = self._generate_pyscf_mol(self.basis, self.atomids, request.crd)
 5     for prop in request:
 6         func = getattr(self.calculator, 'do_' + prop)
 7         func(request, self.mol)
 8     #
 9     return request
10
11     @staticmethod
12     def _generate_pyscf_mol(basis, atomids, crds=None):
13         """ helper function to generate the mol object for Pyscf """
14         if crds is None:
15             crds = np.zeros((len(atomids), 3))
16         mol = gto.M(atom=[[atom, crd] for atom, crd in zip(atomids, crds)],
17         basis = basis, unit='Bohr')
18         return mol

Step 4: Implementing the DFT calculator class

For educational purposes we restrict to the calculation of energies. Like in all Colt classes questions can be added, which are asked through the _extend_questions method of the PySCF class. Answers are passed to the __init__ function via the from_config classmethod. At the initialization the dft and tddft scanners are set up to make sure that calculations are started from the last converged result. In the do_energy function the request is filled with the energies.