Coverage for /builds/ase/ase/ase/calculators/qchem.py: 54.22%
83 statements
« prev ^ index » next coverage.py v7.5.3, created at 2025-08-02 00:12 +0000
« prev ^ index » next coverage.py v7.5.3, created at 2025-08-02 00:12 +0000
1# fmt: off
3import numpy as np
5import ase.units
6from ase.calculators.calculator import FileIOCalculator, SCFError
9class QChem(FileIOCalculator):
10 """
11 QChem calculator
12 """
13 name = 'QChem'
15 implemented_properties = ['energy', 'forces']
16 _legacy_default_command = 'qchem PREFIX.inp PREFIX.out'
18 # Following the minimal requirements given in
19 # http://www.q-chem.com/qchem-website/manual/qchem43_manual/sect-METHOD.html
20 default_parameters = {'method': 'hf',
21 'basis': '6-31G*',
22 'jobtype': None,
23 'charge': 0}
25 def __init__(self, restart=None,
26 ignore_bad_restart_file=FileIOCalculator._deprecated,
27 label='qchem', scratch=None, np=1, nt=1, pbs=False,
28 basisfile=None, ecpfile=None, atoms=None, **kwargs):
29 """
30 The scratch directory, number of processor and threads as well as a few
31 other command line options can be set using the arguments explained
32 below. The remaining kwargs are copied as options to the input file.
33 The calculator will convert these options to upper case
34 (Q-Chem standard) when writing the input file.
36 scratch: str
37 path of the scratch directory
38 np: int
39 number of processors for the -np command line flag
40 nt: int
41 number of threads for the -nt command line flag
42 pbs: boolean
43 command line flag for pbs scheduler (see Q-Chem manual)
44 basisfile: str
45 path to file containing the basis. Use in combination with
46 basis='gen' keyword argument.
47 ecpfile: str
48 path to file containing the effective core potential. Use in
49 combination with ecp='gen' keyword argument.
50 """
52 FileIOCalculator.__init__(self, restart, ignore_bad_restart_file,
53 label, atoms, **kwargs)
55 # Augment the command by various flags
56 if pbs:
57 self.command = 'qchem -pbs '
58 else:
59 self.command = 'qchem '
60 if np != 1:
61 self.command += '-np %d ' % np
62 if nt != 1:
63 self.command += '-nt %d ' % nt
64 self.command += 'PREFIX.inp PREFIX.out'
65 if scratch is not None:
66 self.command += f' {scratch}'
68 self.basisfile = basisfile
69 self.ecpfile = ecpfile
71 def read(self, label):
72 raise NotImplementedError
74 def read_results(self):
75 filename = self.label + '.out'
77 with open(filename) as fileobj:
78 lineiter = iter(fileobj)
79 for line in lineiter:
80 if 'SCF failed to converge' in line:
81 raise SCFError()
82 elif 'ERROR: alpha_min' in line:
83 # Even though it is not technically a SCFError:
84 raise SCFError()
85 elif ' Total energy in the final basis set =' in line:
86 convert = ase.units.Hartree
87 self.results['energy'] = float(line.split()[8]) * convert
88 elif ' Gradient of SCF Energy' in line:
89 # Read gradient as 3 by N array and transpose at the end
90 gradient = [[] for _ in range(3)]
91 # Skip first line containing atom numbering
92 next(lineiter)
93 while True:
94 # Loop over the three Cartesian coordinates
95 for i in range(3):
96 # Cut off the component numbering and remove
97 # trailing characters ('\n' and stuff)
98 line = next(lineiter)[5:].rstrip()
99 # Cut in chunks of 12 symbols and convert into
100 # strings. This is preferred over string.split() as
101 # the fields may overlap for large gradients
102 gradient[i].extend(list(map(
103 float, [line[i:i + 12]
104 for i in range(0, len(line), 12)])))
106 # After three force components we expect either a
107 # separator line, which we want to skip, or the end of
108 # the gradient matrix which is characterized by the
109 # line ' Max gradient component'.
110 # Maybe change stopping criterion to be independent of
111 # next line. Eg. if not lineiter.next().startswith(' ')
112 if ' Max gradient component' in next(lineiter):
113 # Minus to convert from gradient to force
114 self.results['forces'] = np.array(gradient).T * (
115 -ase.units.Hartree / ase.units.Bohr)
116 break
118 def write_input(self, atoms, properties=None, system_changes=None):
119 FileIOCalculator.write_input(self, atoms, properties, system_changes)
120 filename = self.label + '.inp'
122 with open(filename, 'w') as fileobj:
123 fileobj.write('$comment\n ASE generated input file\n$end\n\n')
125 fileobj.write('$rem\n')
126 if self.parameters['jobtype'] is None:
127 if 'forces' in properties:
128 fileobj.write(' %-25s %s\n' % ('JOBTYPE', 'FORCE'))
129 else:
130 fileobj.write(' %-25s %s\n' % ('JOBTYPE', 'SP'))
132 for prm in self.parameters:
133 if prm not in ['charge', 'multiplicity']:
134 if self.parameters[prm] is not None:
135 fileobj.write(' %-25s %s\n' % (
136 prm.upper(), self.parameters[prm].upper()))
138 # Not even a parameters as this is an absolute necessity
139 fileobj.write(' %-25s %s\n' % ('SYM_IGNORE', 'TRUE'))
140 fileobj.write('$end\n\n')
142 fileobj.write('$molecule\n')
143 # Following the example set by the gaussian calculator
144 if ('multiplicity' not in self.parameters):
145 tot_magmom = atoms.get_initial_magnetic_moments().sum()
146 mult = tot_magmom + 1
147 else:
148 mult = self.parameters['multiplicity']
149 # Default charge of 0 is defined in default_parameters
150 fileobj.write(' %d %d\n' % (self.parameters['charge'], mult))
151 for a in atoms:
152 fileobj.write(' {} {:f} {:f} {:f}\n'.format(a.symbol,
153 a.x, a.y, a.z))
154 fileobj.write('$end\n\n')
156 if self.basisfile is not None:
157 with open(self.basisfile) as f_in:
158 basis = f_in.readlines()
159 fileobj.write('$basis\n')
160 fileobj.writelines(basis)
161 fileobj.write('$end\n\n')
163 if self.ecpfile is not None:
164 with open(self.ecpfile) as f_in:
165 ecp = f_in.readlines()
166 fileobj.write('$ecp\n')
167 fileobj.writelines(ecp)
168 fileobj.write('$end\n\n')