Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2authors: Ben Comer (Georgia Tech), Xiangyun (Ray) Lei (Georgia Tech)
4"""
5from io import StringIO
6from ase.calculators.calculator import Calculator, all_changes
7from ase.calculators.calculator import InputError, ReadError
8from ase.calculators.calculator import CalculatorSetupError
9import multiprocessing
10from ase import io
11import numpy as np
12import json
13from ase.units import Bohr, Hartree
14import warnings
15import os
18class Psi4(Calculator):
19 """
20 An ase calculator for the popular open source Q-chem code
21 psi4.
22 method is the generic input for whatever method you wish to use, thus
23 and quantum chemistry method implemented in psi4 can be input
24 (i.e. ccsd(t))
26 also note that you can always use the in-built psi4 module through:
27 calc.psi4
28 """
29 implemented_properties = ['energy', 'forces']
30 discard_results_on_any_change = True
32 default_parameters = {
33 "basis": "aug-cc-pvtz",
34 "method": "hf",
35 'symmetry': 'c1'}
37 def __init__(self, restart=None, ignore_bad_restart=False,
38 label='psi4-calc', atoms=None, command=None,
39 **kwargs):
40 Calculator.__init__(self, restart=restart,
41 ignore_bad_restart=ignore_bad_restart, label=label,
42 atoms=atoms, command=command, **kwargs)
43 import psi4
44 self.psi4 = psi4
45 # perform initial setup of psi4 python API
46 self.set_psi4(atoms=atoms)
48 def set_psi4(self, atoms=None):
49 """
50 This function sets the imported psi4 module to the settings the user
51 defines
52 """
54 # Set the scrath directory for electronic structure files.
55 # The default is /tmp
56 if 'PSI_SCRATCH' in os.environ:
57 pass
58 elif self.parameters.get('PSI_SCRATCH'):
59 os.environ['PSI_SCRATCH'] = self.parameters['PSI_SCRATCH']
61 # Input spin settings
62 if self.parameters.get('reference') is not None:
63 self.psi4.set_options({'reference':
64 self.parameters['reference']})
65 # Memory
66 if self.parameters.get('memory') is not None:
67 self.psi4.set_memory(self.parameters['memory'])
69 # Threads
70 nthreads = self.parameters.get('num_threads', 1)
71 if nthreads == 'max':
72 nthreads = multiprocessing.cpu_count()
73 self.psi4.set_num_threads(nthreads)
75 # deal with some ASE specific inputs
76 if 'kpts' in self.parameters:
77 raise InputError('psi4 is a non-periodic code, and thus does not'
78 ' require k-points. Please remove this '
79 'argument.')
81 if self.parameters['method'] == 'LDA':
82 # svwn is equivalent to LDA
83 self.parameters['method'] = 'svwn'
85 if 'nbands' in self.parameters:
86 raise InputError('psi4 does not support the keyword "nbands"')
88 if 'smearing' in self.parameters:
89 raise InputError('Finite temperature DFT is not implemented in'
90 ' psi4 currently, thus a smearing argument '
91 'cannot be utilized. please remove this '
92 'argument')
94 if 'xc' in self.parameters:
95 raise InputError('psi4 does not accept the `xc` argument please'
96 ' use the `method` argument instead')
98 if atoms is None:
99 if self.atoms is None:
100 return None
101 else:
102 atoms = self.atoms
103 if self.atoms is None:
104 self.atoms = atoms
105 # Generate the atomic input
106 geomline = '{}\t{:.15f}\t{:.15f}\t{:.15f}'
107 geom = [geomline.format(atom.symbol, *atom.position) for atom in atoms]
108 geom.append('symmetry {}'.format(self.parameters['symmetry']))
109 geom.append('units angstrom')
111 charge = self.parameters.get('charge')
112 mult = self.parameters.get('multiplicity')
113 if mult is None:
114 mult = 1
115 if charge is not None:
116 warnings.warn('A charge was provided without a spin '
117 'multiplicity. A multiplicity of 1 is assumed')
118 if charge is None:
119 charge = 0
121 geom.append('{} {}'.format(charge, mult))
122 geom.append('no_reorient')
124 if not os.path.isdir(self.directory):
125 os.mkdir(self.directory)
126 self.molecule = self.psi4.geometry('\n'.join(geom))
128 def read(self, label):
129 """Read psi4 outputs made from this ASE calculator
130 """
131 filename = label + '.dat'
132 if not os.path.isfile(filename):
133 raise ReadError('Could not find the psi4 output file: ' + filename)
135 with open(filename, 'r') as fd:
136 txt = fd.read()
137 if '!ASE Information\n' not in txt:
138 raise Exception('The output file {} could not be read because '
139 'the file does not contain the "!ASE Information"'
140 ' lines inserted by this calculator. This likely'
141 ' means the output file was not made using this '
142 'ASE calculator or has since been modified and '
143 'thus cannot be read.'.format(filename))
144 info = txt.split('!ASE Information\n')[1]
145 info = info.split('!')[0]
146 saved_dict = json.loads(info)
147 # use io read to recode atoms
148 with StringIO(str(saved_dict['atoms'])) as g:
149 self.atoms = io.read(g, format='json')
150 self.parameters = saved_dict['parameters']
151 self.results = saved_dict['results']
152 # convert forces to numpy array
153 if 'forces' in self.results:
154 self.results['forces'] = np.array(self.results['forces'])
156 def calculate(self, atoms=None, properties=['energy'],
157 system_changes=all_changes, symmetry='c1'):
159 Calculator.calculate(self, atoms=atoms)
160 if self.atoms is None:
161 raise CalculatorSetupError('An Atoms object must be provided to '
162 'perform a calculation')
163 atoms = self.atoms
165 if atoms.get_initial_magnetic_moments().any():
166 self.parameters['reference'] = 'uhf'
167 self.parameters['multiplicity'] = None
168 # this inputs all the settings into psi4
169 self.set_psi4(atoms=atoms)
170 self.psi4.core.set_output_file(self.label + '.dat',
171 False)
173 # Set up the method
174 method = self.parameters['method']
175 basis = self.parameters['basis']
177 # Do the calculations
178 if 'forces' in properties:
179 grad, wf = self.psi4.driver.gradient('{}/{}'.format(method, basis),
180 return_wfn=True)
181 # energy comes for free
182 energy = wf.energy()
183 self.results['energy'] = energy * Hartree
184 # convert to eV/A
185 # also note that the gradient is -1 * forces
186 self.results['forces'] = -1 * np.array(grad) * Hartree / Bohr
187 elif 'energy' in properties:
188 energy = self.psi4.energy('{}/{}'.format(method, basis),
189 molecule=self.molecule)
190 # convert to eV
191 self.results['energy'] = energy * Hartree
193 # dump the calculator info to the psi4 file
194 save_atoms = self.atoms.copy()
195 # use io.write to encode atoms
196 with StringIO() as fd:
197 io.write(fd, save_atoms, format='json')
198 json_atoms = fd.getvalue()
199 # convert forces to list for json storage
200 save_results = self.results.copy()
201 if 'forces' in save_results:
202 save_results['forces'] = save_results['forces'].tolist()
203 save_dict = {'parameters': self.parameters,
204 'results': save_results,
205 'atoms': json_atoms}
206 self.psi4.core.print_out('!ASE Information\n')
207 self.psi4.core.print_out(json.dumps(save_dict))
208 self.psi4.core.print_out('!')