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