Coverage for ase / io / gamess_us.py: 75.39%
191 statements
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-04 10:20 +0000
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-04 10:20 +0000
1"""Module for GAMESS US IO."""
3from __future__ import annotations
5import os
6import re
7from copy import deepcopy
8from io import TextIOBase
9from subprocess import TimeoutExpired, call
11import numpy as np
13from ase import Atoms
14from ase.calculators.singlepoint import SinglePointCalculator
15from ase.units import Bohr, Debye, Hartree
16from ase.utils import reader, workdir, writer
19def _format_value(val: bool | str) -> str:
20 if isinstance(val, bool):
21 return '.t.' if val else '.f.'
22 return str(val).upper()
25def _write_block(name: str, args: dict[str, bool | str]) -> str:
26 out = [f' ${name.upper()}']
27 for key, val in args.items():
28 out.append(f' {key.upper()}={_format_value(val)}')
29 out.append(' $END')
30 return '\n'.join(out)
33def _write_geom(atoms: Atoms, basis_spec: dict[str | int, str] | None) -> str:
34 out = [' $DATA', atoms.get_chemical_formula(), 'C1']
35 for i, atom in enumerate(atoms):
36 out.append(
37 '{:<3} {:>3} {:20.13e} {:20.13e} {:20.13e}'.format(
38 atom.symbol, atom.number, *atom.position
39 )
40 )
41 if basis_spec is not None:
42 basis = basis_spec.get(i)
43 if basis is None:
44 basis = basis_spec.get(atom.symbol)
45 if basis is None:
46 msg = f'Could not find an appropriate basis set for atom {i}'
47 raise ValueError(msg)
48 out += [basis, '']
49 out.append(' $END')
50 return '\n'.join(out)
53def _write_ecp(atoms: Atoms, ecp: dict[int | str, str]) -> str:
54 out = [' $ECP']
55 for i, symbol in enumerate(atoms.symbols):
56 if i in ecp:
57 out.append(ecp[i])
58 elif symbol in ecp:
59 out.append(ecp[symbol])
60 else:
61 msg = f'Could not find an appropriate ECP for atom {i}'
62 raise ValueError(msg)
63 out.append(' $END')
64 return '\n'.join(out)
67_xc = dict(LDA='SVWN')
70@writer
71def write_gamess_us_in(fd: TextIOBase, atoms: Atoms, properties=None, **params):
72 params = deepcopy(params)
74 if properties is None:
75 properties = ['energy']
77 # set RUNTYP from properties iff value not provided by the user
78 contrl = params.pop('contrl', {})
79 if 'runtyp' not in contrl:
80 if 'forces' in properties:
81 contrl['runtyp'] = 'gradient'
82 else:
83 contrl['runtyp'] = 'energy'
85 # Support xc keyword for functional specification
86 xc = params.pop('xc', None)
87 if xc is not None and 'dfttyp' not in contrl:
88 contrl['dfttyp'] = _xc.get(xc.upper(), xc.upper())
90 # Automatically determine multiplicity from magnetic moment
91 magmom_tot = int(round(atoms.get_initial_magnetic_moments().sum()))
92 if 'mult' not in contrl:
93 contrl['mult'] = abs(magmom_tot) + 1
95 # Since we're automatically determining multiplicity, we also
96 # need to automatically switch to UHF when the multiplicity
97 # is not 1
98 if 'scftyp' not in contrl:
99 contrl['scftyp'] = 'rhf' if contrl['mult'] == 1 else 'uhf'
101 # effective core potentials
102 ecp = params.pop('ecp', None)
103 if ecp is not None and 'pp' not in contrl:
104 contrl['pp'] = 'READ'
106 # If no basis set is provided, use 3-21G by default.
107 basis_spec = None
108 if 'basis' not in params:
109 params['basis'] = dict(gbasis='N21', ngauss=3)
110 else:
111 keys = set(params['basis'])
112 # Check if the user is specifying a literal per-atom basis.
113 # We assume they are passing a per-atom basis if the keys of the
114 # basis dict are atom symbols, or if they are atom indices, or
115 # a mixture of both.
116 if keys.intersection(set(atoms.symbols)) or any(
117 map(lambda x: isinstance(x, int), keys)
118 ):
119 basis_spec = params.pop('basis')
121 out = [_write_block('contrl', contrl)]
122 out += [_write_block(*item) for item in params.items()]
123 out.append(_write_geom(atoms, basis_spec))
124 if ecp is not None:
125 out.append(_write_ecp(atoms, ecp))
126 fd.write('\n\n'.join(out))
129_geom_re = re.compile(r'^\s*ATOM\s+ATOMIC\s+COORDINATES')
130_atom_re = re.compile(r'^\s*(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s*\n')
131_energy_re = re.compile(r'^\s*FINAL [\S\s]+ ENERGY IS\s+(\S+) AFTER')
132_grad_re = re.compile(r'^\s*GRADIENT OF THE ENERGY\s*')
133_charges_re = re.compile(r'^\s*TOTAL MULLIKEN AND LOWDIN ATOMIC POPULATIONS\s*')
134_dipole_re = re.compile(r'^\s+DX\s+DY\s+DZ\s+\/D\/\s+\(DEBYE\)')
137@reader
138def read_gamess_us_out(fd):
139 atoms = None
140 energy = None
141 forces = None
142 charges = None
143 dipole = None
144 for line in fd:
145 # Geometry
146 if _geom_re.match(line):
147 fd.readline()
148 symbols = []
149 pos = []
150 while True:
151 atom = _atom_re.match(fd.readline())
152 if atom is None:
153 break
154 symbol, _, x, y, z = atom.groups()
155 symbols.append(symbol.capitalize())
156 pos.append(list(map(float, [x, y, z])))
157 atoms = Atoms(symbols, np.array(pos) * Bohr)
158 continue
160 # Energy
161 ematch = _energy_re.match(line)
162 if ematch is not None:
163 energy = float(ematch.group(1)) * Hartree
165 # MPn energy. Supplants energy parsed above.
166 elif line.strip().startswith('TOTAL ENERGY'):
167 energy = float(line.strip().split()[-1]) * Hartree
169 # Higher-level energy (e.g. coupled cluster)
170 # Supplants energies parsed above.
171 elif line.strip().startswith('THE FOLLOWING METHOD AND ENERGY'):
172 energy = float(fd.readline().strip().split()[-1]) * Hartree
174 elif _charges_re.match(line):
175 fd.readline()
176 charges = np.array(
177 [float(fd.readline().split()[3]) for _ in symbols],
178 )
180 # Gradients
181 elif _grad_re.match(line):
182 for _ in range(3):
183 fd.readline()
184 grad = []
185 while True:
186 atom = _atom_re.match(fd.readline())
187 if atom is None:
188 break
189 grad.append(list(map(float, atom.groups()[2:])))
190 forces = -np.array(grad) * Hartree / Bohr
191 elif _dipole_re.match(line):
192 dipole = np.array(list(map(float, fd.readline().split()[:3])))
193 dipole *= Debye
195 atoms.calc = SinglePointCalculator(
196 atoms,
197 energy=energy,
198 forces=forces,
199 charges=charges,
200 dipole=dipole,
201 )
202 return atoms
205@reader
206def read_gamess_us_punch(fd):
207 atoms = None
208 energy = None
209 forces = None
210 charges = None
211 dipole = None
212 for line in fd:
213 if line.strip() == '$DATA':
214 symbols = []
215 pos = []
216 while line.strip() != '$END':
217 line = fd.readline()
218 atom = _atom_re.match(line)
219 if atom is None:
220 # The basis set specification is interlaced with the
221 # molecular geometry. We don't care about the basis
222 # set, so ignore lines that don't match the pattern.
223 continue
224 symbols.append(atom.group(1).capitalize())
225 pos.append(list(map(float, atom.group(3, 4, 5))))
226 atoms = Atoms(symbols, np.array(pos))
227 elif line.startswith('E('):
228 energy = float(line.split()[1][:-1]) * Hartree
229 elif line.strip().startswith('POPULATION ANALYSIS'):
230 # Mulliken charges
231 charges = np.array(
232 [float(fd.readline().split()[2]) for _ in symbols],
233 )
234 elif line.strip().startswith('DIPOLE'):
235 dipole = np.array(list(map(float, line.split()[1:]))) * Debye
236 elif line.strip() == '$GRAD':
237 # The gradient block also contains the energy, which we prefer
238 # over the energy obtained above because it is more likely to
239 # be consistent with the gradients. It probably doesn't actually
240 # make a difference though.
241 energy = float(fd.readline().split()[1]) * Hartree
242 grad = []
243 while line.strip() != '$END':
244 line = fd.readline()
245 atom = _atom_re.match(line)
246 if atom is None:
247 continue
248 grad.append(list(map(float, atom.group(3, 4, 5))))
249 forces = -np.array(grad) * Hartree / Bohr
251 atoms.calc = SinglePointCalculator(
252 atoms,
253 energy=energy,
254 forces=forces,
255 charges=charges,
256 dipole=dipole,
257 )
259 return atoms
262def clean_userscr(userscr: str, prefix: str) -> None:
263 for fname in os.listdir(userscr):
264 tokens = fname.split('.')
265 if tokens[0] == prefix and tokens[-1] != 'bak':
266 fold = os.path.join(userscr, fname)
267 os.rename(fold, fold + '.bak')
270def get_userscr(prefix: str, command: str) -> str | None:
271 prefix_test = prefix + '_test'
272 command = command.replace('PREFIX', prefix_test)
273 with workdir(prefix_test, mkdir=True):
274 try:
275 call(command, shell=True, timeout=2)
276 except TimeoutExpired:
277 pass
279 try:
280 with open(f'{prefix_test}.log', encoding='utf-8') as fd:
281 for line in fd:
282 if line.startswith('GAMESS supplementary output files'):
283 return ' '.join(line.split(' ')[8:]).strip()
284 except FileNotFoundError:
285 return None
287 return None