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