Coverage for /builds/kinetik161/ase/ase/calculators/mopac.py: 85.48%
186 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-12-10 11:04 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-12-10 11:04 +0000
1"""This module defines an ASE interface to MOPAC.
3Set $ASE_MOPAC_COMMAND to something like::
5 LD_LIBRARY_PATH=/path/to/lib/ \
6 MOPAC_LICENSE=/path/to/license \
7 /path/to/MOPAC2012.exe PREFIX.mop 2> /dev/null
9"""
10import os
11import re
12from typing import Sequence
13from warnings import warn
15import numpy as np
16from packaging import version
18from ase import Atoms
19from ase.calculators.calculator import FileIOCalculator, Parameters, ReadError
20from ase.units import Debye, kcal, mol
23class MOPAC(FileIOCalculator):
24 implemented_properties = ['energy', 'forces', 'dipole',
25 'magmom', 'free_energy']
26 _legacy_default_command = 'mopac PREFIX.mop 2> /dev/null'
27 discard_results_on_any_change = True
29 default_parameters = dict(
30 method='PM7',
31 task='1SCF GRADIENTS',
32 charge=None,
33 relscf=0.0001)
35 methods = ['AM1', 'MNDO', 'MNDOD', 'PM3', 'PM6', 'PM6-D3', 'PM6-DH+',
36 'PM6-DH2', 'PM6-DH2X', 'PM6-D3H4', 'PM6-D3H4X', 'PMEP', 'PM7',
37 'PM7-TS', 'RM1']
39 def __init__(self, restart=None,
40 ignore_bad_restart_file=FileIOCalculator._deprecated,
41 label='mopac', atoms=None, **kwargs):
42 """Construct MOPAC-calculator object.
44 Parameters:
46 label: str
47 Prefix for filenames (label.mop, label.out, ...)
49 Examples:
51 Use default values to do a single SCF calculation and print
52 the forces (task='1SCF GRADIENTS'):
54 >>> from ase.build import molecule
55 >>> from ase.calculators.mopac import MOPAC
56 >>>
57 >>> atoms = molecule('O2')
58 >>> atoms.calc = MOPAC(label='O2')
59 >>> atoms.get_potential_energy()
60 >>> eigs = atoms.calc.get_eigenvalues()
61 >>> somos = atoms.calc.get_somo_levels()
62 >>> homo, lumo = atoms.calc.get_homo_lumo_levels()
64 Use the internal geometry optimization of Mopac:
66 >>> atoms = molecule('H2')
67 >>> atoms.calc = MOPAC(label='H2', task='GRADIENTS')
68 >>> atoms.get_potential_energy()
70 Read in and start from output file:
72 >>> atoms = MOPAC.read_atoms('H2')
73 >>> atoms.calc.get_homo_lumo_levels()
75 """
76 FileIOCalculator.__init__(self, restart, ignore_bad_restart_file,
77 label, atoms, **kwargs)
79 def write_input(self, atoms, properties=None, system_changes=None):
80 FileIOCalculator.write_input(self, atoms, properties, system_changes)
81 p = Parameters(self.parameters.copy())
83 # Ensure DISP so total energy is available
84 if 'DISP' not in p.task.split():
85 p.task = p.task + ' DISP'
87 # Build string to hold .mop input file:
88 s = f'{p.method} {p.task} '
90 if p.relscf:
91 s += f'RELSCF={p.relscf} '
93 # Write charge:
94 if p.charge is None:
95 charge = atoms.get_initial_charges().sum()
96 else:
97 charge = p.charge
99 if charge != 0:
100 s += f'CHARGE={int(round(charge))} '
102 magmom = int(round(abs(atoms.get_initial_magnetic_moments().sum())))
103 if magmom:
104 s += (['DOUBLET', 'TRIPLET', 'QUARTET', 'QUINTET'][magmom - 1] +
105 ' UHF ')
107 s += '\nTitle: ASE calculation\n\n'
109 # Write coordinates:
110 for xyz, symbol in zip(atoms.positions, atoms.get_chemical_symbols()):
111 s += ' {:2} {} 1 {} 1 {} 1\n'.format(symbol, *xyz)
113 for v, pbc in zip(atoms.cell, atoms.pbc):
114 if pbc:
115 s += 'Tv {} {} {}\n'.format(*v)
117 with open(self.label + '.mop', 'w') as fd:
118 fd.write(s)
120 def get_spin_polarized(self):
121 return self.nspins == 2
123 def get_index(self, lines, pattern):
124 for i, line in enumerate(lines):
125 if line.find(pattern) != -1:
126 return i
128 def read(self, label):
129 FileIOCalculator.read(self, label)
130 if not os.path.isfile(self.label + '.out'):
131 raise ReadError
133 with open(self.label + '.out') as fd:
134 lines = fd.readlines()
136 self.parameters = Parameters(task='', method='')
137 p = self.parameters
138 parm_line = self.read_parameters_from_file(lines)
139 for keyword in parm_line.split():
140 if 'RELSCF' in keyword:
141 p.relscf = float(keyword.split('=')[-1])
142 elif keyword in self.methods:
143 p.method = keyword
144 else:
145 p.task += keyword + ' '
147 p.task = p.task.rstrip()
148 if 'charge' not in p:
149 p.charge = None
151 self.atoms = self.read_atoms_from_file(lines)
152 self.read_results()
154 def read_atoms_from_file(self, lines):
155 """Read the Atoms from the output file stored as list of str in lines.
156 Parameters:
158 lines: list of str
159 """
160 # first try to read from final point (last image)
161 i = self.get_index(lines, 'FINAL POINT AND DERIVATIVES')
162 if i is None: # XXX should we read it from the input file?
163 assert 0, 'Not implemented'
165 lines1 = lines[i:]
166 i = self.get_index(lines1, 'CARTESIAN COORDINATES')
167 j = i + 2
168 symbols = []
169 positions = []
170 while not lines1[j].isspace(): # continue until we hit a blank line
171 l = lines1[j].split()
172 symbols.append(l[1])
173 positions.append([float(c) for c in l[2: 2 + 3]])
174 j += 1
176 return Atoms(symbols=symbols, positions=positions)
178 def read_parameters_from_file(self, lines):
179 """Find and return the line that defines a Mopac calculation
181 Parameters:
183 lines: list of str
184 """
185 for i, line in enumerate(lines):
186 if line.find('CALCULATION DONE:') != -1:
187 break
189 lines1 = lines[i:]
190 for i, line in enumerate(lines1):
191 if line.find('****') != -1:
192 return lines1[i + 1]
194 def read_results(self):
195 """Read the results, such as energy, forces, eigenvalues, etc.
196 """
197 FileIOCalculator.read(self, self.label)
198 if not os.path.isfile(self.label + '.out'):
199 raise ReadError
201 with open(self.label + '.out') as fd:
202 lines = fd.readlines()
204 self.results['version'] = self.get_version_from_file(lines)
206 total_energy_regex = re.compile(
207 r'^\s+TOTAL ENERGY\s+\=\s+(-?\d+\.\d+) EV')
208 final_heat_regex = re.compile(
209 r'^\s+FINAL HEAT OF FORMATION\s+\=\s+(-?\d+\.\d+) KCAL/MOL')
211 for i, line in enumerate(lines):
212 if total_energy_regex.match(line):
213 self.results['total_energy'] = float(
214 total_energy_regex.match(line).groups()[0])
215 elif final_heat_regex.match(line):
216 self.results['final_hof'] = float(final_heat_regex.match(line)
217 .groups()[0]) * kcal / mol
218 elif line.find('NO. OF FILLED LEVELS') != -1:
219 self.nspins = 1
220 self.no_occ_levels = int(line.split()[-1])
221 elif line.find('NO. OF ALPHA ELECTRON') != -1:
222 self.nspins = 2
223 self.no_alpha_electrons = int(line.split()[-1])
224 self.no_beta_electrons = int(lines[i + 1].split()[-1])
225 self.results['magmom'] = abs(self.no_alpha_electrons -
226 self.no_beta_electrons)
227 elif line.find('FINAL POINT AND DERIVATIVES') != -1:
228 forces = [-float(line.split()[6])
229 for line in lines[i + 3:i + 3 + 3 * len(self.atoms)]]
230 self.results['forces'] = np.array(
231 forces).reshape((-1, 3)) * kcal / mol
232 elif line.find('EIGENVALUES') != -1:
233 if line.find('ALPHA') != -1:
234 j = i + 1
235 eigs_alpha = []
236 while not lines[j].isspace():
237 eigs_alpha += [float(eps) for eps in lines[j].split()]
238 j += 1
239 elif line.find('BETA') != -1:
240 j = i + 1
241 eigs_beta = []
242 while not lines[j].isspace():
243 eigs_beta += [float(eps) for eps in lines[j].split()]
244 j += 1
245 eigs = np.array([eigs_alpha, eigs_beta]).reshape(2, 1, -1)
246 self.eigenvalues = eigs
247 else:
248 eigs = []
249 j = i + 1
250 while not lines[j].isspace():
251 eigs += [float(e) for e in lines[j].split()]
252 j += 1
253 self.eigenvalues = np.array(eigs).reshape(1, 1, -1)
254 elif line.find('DIPOLE ') != -1:
255 self.results['dipole'] = np.array(
256 lines[i + 3].split()[1:1 + 3], float) * Debye
258 # Developers recommend using final HOF as it includes dispersion etc.
259 # For backward compatibility we won't change the results of old MOPAC
260 # calculations... yet
261 if version.parse(self.results['version']) >= version.parse('22'):
262 self.results['energy'] = self.results['final_hof']
263 else:
264 warn("Using a version of MOPAC lower than v22: ASE will use "
265 "TOTAL ENERGY as the potential energy. In future, "
266 "FINAL HEAT OF FORMATION will be preferred for all versions.")
267 self.results['energy'] = self.results['total_energy']
268 self.results['free_energy'] = self.results['energy']
270 @staticmethod
271 def get_version_from_file(lines: Sequence[str]):
272 version_regex = re.compile(r'^ \*\*\s+MOPAC (v[\.\d]+)\s+\*\*\s$')
273 for line in lines:
274 match = version_regex.match(line)
275 if match:
276 return match.groups()[0]
277 else:
278 return ValueError('Version number was not found in MOPAC output')
280 def get_eigenvalues(self, kpt=0, spin=0):
281 return self.eigenvalues[spin, kpt]
283 def get_homo_lumo_levels(self):
284 eigs = self.eigenvalues
285 if self.nspins == 1:
286 nocc = self.no_occ_levels
287 return np.array([eigs[0, 0, nocc - 1], eigs[0, 0, nocc]])
288 else:
289 na = self.no_alpha_electrons
290 nb = self.no_beta_electrons
291 if na == 0:
292 return None, self.eigenvalues[1, 0, nb - 1]
293 elif nb == 0:
294 return self.eigenvalues[0, 0, na - 1], None
295 else:
296 eah, eal = eigs[0, 0, na - 1: na + 1]
297 ebh, ebl = eigs[1, 0, nb - 1: nb + 1]
298 return np.array([max(eah, ebh), min(eal, ebl)])
300 def get_somo_levels(self):
301 assert self.nspins == 2
302 na, nb = self.no_alpha_electrons, self.no_beta_electrons
303 if na == 0:
304 return None, self.eigenvalues[1, 0, nb - 1]
305 elif nb == 0:
306 return self.eigenvalues[0, 0, na - 1], None
307 else:
308 return np.array([self.eigenvalues[0, 0, na - 1],
309 self.eigenvalues[1, 0, nb - 1]])
311 def get_final_heat_of_formation(self):
312 """Final heat of formation as reported in the Mopac output file"""
313 warn("This method is deprecated, please use "
314 "MOPAC.results['final_hof']", DeprecationWarning)
315 return self.results['final_hof']
317 @property
318 def final_hof(self):
319 warn("This property is deprecated, please use "
320 "MOPAC.results['final_hof']", DeprecationWarning)
321 return self.results['final_hof']
323 @final_hof.setter
324 def final_hof(self, new_hof):
325 warn("This property is deprecated, please use "
326 "MOPAC.results['final_hof']", DeprecationWarning)
327 self.results['final_hof'] = new_hof