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.