Coverage for /builds/ase/ase/ase/calculators/mopac.py : 57.53%

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"""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
15from packaging import version
16import numpy as np
18from ase import Atoms
19from ase.calculators.calculator import FileIOCalculator, ReadError, Parameters
20from ase.units import kcal, mol, Debye
23class MOPAC(FileIOCalculator):
24 implemented_properties = ['energy', 'forces', 'dipole',
25 'magmom', 'free_energy']
26 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 >>> atoms = molecule('O2')
57 >>> atoms.calc = MOPAC(label='O2')
58 >>> atoms.get_potential_energy()
59 >>> eigs = atoms.calc.get_eigenvalues()
60 >>> somos = atoms.calc.get_somo_levels()
61 >>> homo, lumo = atoms.calc.get_homo_lumo_levels()
63 Use the internal geometry optimization of Mopac:
65 >>> atoms = molecule('H2')
66 >>> atoms.calc = MOPAC(label='H2', task='GRADIENTS')
67 >>> atoms.get_potential_energy()
69 Read in and start from output file:
71 >>> atoms = MOPAC.read_atoms('H2')
72 >>> atoms.calc.get_homo_lumo_levels()
74 """
75 FileIOCalculator.__init__(self, restart, ignore_bad_restart_file,
76 label, atoms, **kwargs)
78 def write_input(self, atoms, properties=None, system_changes=None):
79 FileIOCalculator.write_input(self, atoms, properties, system_changes)
80 p = Parameters(self.parameters.copy())
82 # Ensure DISP so total energy is available
83 if 'DISP' not in p.task.split():
84 p.task = p.task + ' DISP'
86 # Build string to hold .mop input file:
87 s = f'{p.method} {p.task} '
89 if p.relscf:
90 s += 'RELSCF={0} '.format(p.relscf)
92 # Write charge:
93 if p.charge is None:
94 charge = atoms.get_initial_charges().sum()
95 else:
96 charge = p.charge
98 if charge != 0:
99 s += 'CHARGE={0} '.format(int(round(charge)))
101 magmom = int(round(abs(atoms.get_initial_magnetic_moments().sum())))
102 if magmom:
103 s += (['DOUBLET', 'TRIPLET', 'QUARTET', 'QUINTET'][magmom - 1] +
104 ' UHF ')
106 s += '\nTitle: ASE calculation\n\n'
108 # Write coordinates:
109 for xyz, symbol in zip(atoms.positions, atoms.get_chemical_symbols()):
110 s += ' {0:2} {1} 1 {2} 1 {3} 1\n'.format(symbol, *xyz)
112 for v, pbc in zip(atoms.cell, atoms.pbc):
113 if pbc:
114 s += 'Tv {0} {1} {2}\n'.format(*v)
116 with open(self.label + '.mop', 'w') as fd:
117 fd.write(s)
119 def get_spin_polarized(self):
120 return self.nspins == 2
122 def get_index(self, lines, pattern):
123 for i, line in enumerate(lines):
124 if line.find(pattern) != -1:
125 return i
127 def read(self, label):
128 FileIOCalculator.read(self, label)
129 if not os.path.isfile(self.label + '.out'):
130 raise ReadError
132 with open(self.label + '.out') as fd:
133 lines = fd.readlines()
135 self.parameters = Parameters(task='', method='')
136 p = self.parameters
137 parm_line = self.read_parameters_from_file(lines)
138 for keyword in parm_line.split():
139 if 'RELSCF' in keyword:
140 p.relscf = float(keyword.split('=')[-1])
141 elif keyword in self.methods:
142 p.method = keyword
143 else:
144 p.task += keyword + ' '
146 p.task = p.task.rstrip()
147 if 'charge' not in p:
148 p.charge = None
150 self.atoms = self.read_atoms_from_file(lines)
151 self.read_results()
153 def read_atoms_from_file(self, lines):
154 """Read the Atoms from the output file stored as list of str in lines.
155 Parameters:
157 lines: list of str
158 """
159 # first try to read from final point (last image)
160 i = self.get_index(lines, 'FINAL POINT AND DERIVATIVES')
161 if i is None: # XXX should we read it from the input file?
162 assert 0, 'Not implemented'
164 lines1 = lines[i:]
165 i = self.get_index(lines1, 'CARTESIAN COORDINATES')
166 j = i + 2
167 symbols = []
168 positions = []
169 while not lines1[j].isspace(): # continue until we hit a blank line
170 l = lines1[j].split()
171 symbols.append(l[1])
172 positions.append([float(c) for c in l[2: 2 + 3]])
173 j += 1
175 return Atoms(symbols=symbols, positions=positions)
177 def read_parameters_from_file(self, lines):
178 """Find and return the line that defines a Mopac calculation
180 Parameters:
182 lines: list of str
183 """
184 for i, line in enumerate(lines):
185 if line.find('CALCULATION DONE:') != -1:
186 break
188 lines1 = lines[i:]
189 for i, line in enumerate(lines1):
190 if line.find('****') != -1:
191 return lines1[i + 1]
193 def read_results(self):
194 """Read the results, such as energy, forces, eigenvalues, etc.
195 """
196 FileIOCalculator.read(self, self.label)
197 if not os.path.isfile(self.label + '.out'):
198 raise ReadError
200 with open(self.label + '.out') as fd:
201 lines = fd.readlines()
203 self.results['version'] = self.get_version_from_file(lines)
205 total_energy_regex = re.compile(
206 r'^\s+TOTAL ENERGY\s+\=\s+(-?\d+\.\d+) EV')
207 final_heat_regex = re.compile(
208 r'^\s+FINAL HEAT OF FORMATION\s+\=\s+(-?\d+\.\d+) KCAL/MOL')
210 for i, line in enumerate(lines):
211 if total_energy_regex.match(line):
212 self.results['total_energy'] = float(
213 total_energy_regex.match(line).groups()[0])
214 elif final_heat_regex.match(line):
215 self.results['final_hof'] = float(final_heat_regex.match(line)
216 .groups()[0]) * kcal / mol
217 elif line.find('NO. OF FILLED LEVELS') != -1:
218 self.nspins = 1
219 self.no_occ_levels = int(line.split()[-1])
220 elif line.find('NO. OF ALPHA ELECTRON') != -1:
221 self.nspins = 2
222 self.no_alpha_electrons = int(line.split()[-1])
223 self.no_beta_electrons = int(lines[i + 1].split()[-1])
224 self.results['magmom'] = abs(self.no_alpha_electrons -
225 self.no_beta_electrons)
226 elif line.find('FINAL POINT AND DERIVATIVES') != -1:
227 forces = [-float(line.split()[6])
228 for line in lines[i + 3:i + 3 + 3 * len(self.atoms)]]
229 self.results['forces'] = np.array(
230 forces).reshape((-1, 3)) * kcal / mol
231 elif line.find('EIGENVALUES') != -1:
232 if line.find('ALPHA') != -1:
233 j = i + 1
234 eigs_alpha = []
235 while not lines[j].isspace():
236 eigs_alpha += [float(eps) for eps in lines[j].split()]
237 j += 1
238 elif line.find('BETA') != -1:
239 j = i + 1
240 eigs_beta = []
241 while not lines[j].isspace():
242 eigs_beta += [float(eps) for eps in lines[j].split()]
243 j += 1
244 eigs = np.array([eigs_alpha, eigs_beta]).reshape(2, 1, -1)
245 self.eigenvalues = eigs
246 else:
247 eigs = []
248 j = i + 1
249 while not lines[j].isspace():
250 eigs += [float(e) for e in lines[j].split()]
251 j += 1
252 self.eigenvalues = np.array(eigs).reshape(1, 1, -1)
253 elif line.find('DIPOLE ') != -1:
254 self.results['dipole'] = np.array(
255 lines[i + 3].split()[1:1 + 3], float) * Debye
257 # Developers recommend using final HOF as it includes dispersion etc.
258 # For backward compatibility we won't change the results of old MOPAC
259 # calculations... yet
260 if version.parse(self.results['version']) >= version.parse('22'):
261 self.results['energy'] = self.results['final_hof']
262 else:
263 warn("Using a version of MOPAC lower than v22: ASE will use "
264 "TOTAL ENERGY as the potential energy. In future, "
265 "FINAL HEAT OF FORMATION will be preferred for all versions.")
266 self.results['energy'] = self.results['total_energy']
267 self.results['free_energy'] = self.results['energy']
269 @staticmethod
270 def get_version_from_file(lines: Sequence[str]):
271 version_regex = re.compile(r'^ \*\*\s+MOPAC (v[\.\d]+)\s+\*\*\s$')
272 for line in lines:
273 match = version_regex.match(line)
274 if match:
275 return match.groups()[0]
276 else:
277 return ValueError('Version number was not found in MOPAC output')
279 def get_eigenvalues(self, kpt=0, spin=0):
280 return self.eigenvalues[spin, kpt]
282 def get_homo_lumo_levels(self):
283 eigs = self.eigenvalues
284 if self.nspins == 1:
285 nocc = self.no_occ_levels
286 return np.array([eigs[0, 0, nocc - 1], eigs[0, 0, nocc]])
287 else:
288 na = self.no_alpha_electrons
289 nb = self.no_beta_electrons
290 if na == 0:
291 return None, self.eigenvalues[1, 0, nb - 1]
292 elif nb == 0:
293 return self.eigenvalues[0, 0, na - 1], None
294 else:
295 eah, eal = eigs[0, 0, na - 1: na + 1]
296 ebh, ebl = eigs[1, 0, nb - 1: nb + 1]
297 return np.array([max(eah, ebh), min(eal, ebl)])
299 def get_somo_levels(self):
300 assert self.nspins == 2
301 na, nb = self.no_alpha_electrons, self.no_beta_electrons
302 if na == 0:
303 return None, self.eigenvalues[1, 0, nb - 1]
304 elif nb == 0:
305 return self.eigenvalues[0, 0, na - 1], None
306 else:
307 return np.array([self.eigenvalues[0, 0, na - 1],
308 self.eigenvalues[1, 0, nb - 1]])
310 def get_final_heat_of_formation(self):
311 """Final heat of formation as reported in the Mopac output file"""
312 warn("This method is deprecated, please use "
313 "MOPAC.results['final_hof']", DeprecationWarning)
314 return self.results['final_hof']
316 @property
317 def final_hof(self):
318 warn("This property is deprecated, please use "
319 "MOPAC.results['final_hof']", DeprecationWarning)
320 return self.results['final_hof']
322 @final_hof.setter
323 def final_hof(self, new_hof):
324 warn("This property is deprecated, please use "
325 "MOPAC.results['final_hof']", DeprecationWarning)
326 self.results['final_hof'] = new_hof