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