"""
Currently, we assume this code is universal for all versions of MOPAC
we support. i.e. on adding a support for a future MOPAC release,
it should not be necessary to (substantially) modify this module.
Copyright Schrodinger, LLC. All rights reserved.
"""
# Contributors: Mike Beachy, Mark A. Watson
import os
import re
import shutil
import textwrap
from io import StringIO
import schrodinger.application.mopac.utils as utils
MOPAC_MODE = 'mopac'
TITLE_START = 'Input for structure '
TITLE_END = 'created by Schrodinger semiempirical NDDO python module.'
[docs]class StructureLauncherError(Exception):
pass
[docs]class StructureLauncher(object):
"""
A class for running MOPAC calculations on a Structure object.
Typically, this should not be instantiated manually, but instead
used via the MopacAPI class using the get_launcher() method etc.
"""
CHARGE = "CHARGE"
MMOK = "MMOK"
NOMM = "NOMM"
[docs] def __init__(self,
mopac_launcher,
method,
minimize=True,
settings=None,
keywords=''):
"""
:type mopac_launcher: MopacLauncher object
:param mopac_launcher: API to MOPAC backend.
:type method: module level constant
:param method:
The semi-empirical method to use for the calculation.
:type minimize: bool
:param minimize:
If True, minimize the molecule, otherwise calculate a single point
energy.
:type settings: dict
:param settings:
A settings dictionary for MOPAC input settings. If a MOPAC
keyword does not take a value, set the dictionary value to True.
:type keywords: str
:param keywords:
A string of space-separated keywords to use directly in the
MOPAC input file. Use of the settings argument is recommended
over setting the keyword string directly.
"""
# Index of the structure being written to the input file. The index is
# included in the title so we can see which structures are missing from
# output files
self.structure_index = 1
self.mopac_launcher = mopac_launcher
if method not in self.mopac_launcher.valid_methods:
raise RuntimeError("Unrecognized semi-empirical method: '%s'" %
method)
self._settings = dict()
self.method = method
self.minimize = minimize
self.keywords = keywords
self.setKeyword(method, raise_conflicts=True)
if not minimize:
self.setKeyword('1SCF')
# New MOPAC2016 binary pointed to in MOPAC-267 doesn't print out Total Energy unless
# ENPART is set. Since we've historically reported this property, set ENPART by default
self.setKeyword('ENPART')
if settings:
for key, value in settings.items():
self.setKeyword(key, value, raise_conflicts=True)
[docs] def setKeyword(self, key, value=True, raise_conflicts=False):
"""
Set the MOPAC keyword to the provided value. If the keyword doesn't
take a value, use True. The keyword will be stored as uppercase
regardless of the argument case.
:param raise_conflicts:
If True, raise an exception when conflicting keywords are
detected. Otherwise, have the current keyword take precedence.
"""
if key == StructureLauncher.NOMM:
if self.hasKeyword(StructureLauncher.MMOK):
if raise_conflicts:
raise StructureLauncherError(
"MMOK and NOMM were both "
"specified, but are contradictory settings.")
self.delKeyword(StructureLauncher.MMOK)
elif key == StructureLauncher.MMOK:
if self.hasKeyword(StructureLauncher.NOMM):
if raise_conflicts:
raise StructureLauncherError(
"MMOK and NOMM were both "
"specified, but are contradictory settings.")
self.delKeyword(StructureLauncher.NOMM)
elif key in self.mopac_launcher.valid_methods:
for m in self.mopac_launcher.valid_methods:
if self.hasKeyword(m) and m != key:
if raise_conflicts:
raise StructureLauncherError(
"Multiple calculation "
"methods (%s and %s) have been specified." %
(key, m))
self.delKeyword(m)
if self.hasKeyword(key):
self.delKeyword(key)
self._settings[key.upper()] = value
[docs] def getValue(self, key):
"""
Get the value for the provided keyword, whether it was provided a
key/value pair or in a MOPAC keywords string. Note that if the
keyword and value are specified as part of the keywords string, the
value is always returned as a string.
If the keyword is specified but has no value, True is returned.
Raise a KeyError if the keyword isn't defined.
"""
ku = key.upper()
if ku in self._settings:
return self._settings[ku]
# Look for a keyword optionally followed by an equals sign and a
# value.
keyword_re = re.compile(r"\b%s\b(\s*=\s*(?P<value>\S+))?" % key,
re.IGNORECASE)
match = keyword_re.search(self.keywords)
if match:
if match.group('value'):
return match.group('value')
else:
return True
raise KeyError("Key '%s' not found." % ku)
[docs] def delKeyword(self, key):
"""
Delete the keyword from the keywords specification and the kwdict
dictionary.
"""
# Look for a keyword optionally followed by an equals sign and a
# value, and grab up any surrounding whitespace.
keyword_re = re.compile(r"\s*\b%s\b(\s*=\s*\S+)?\s*" % key,
re.IGNORECASE)
match = keyword_re.search(self.keywords)
if match:
# Remove the matching part, cleaning up spaces as we can.
pre = self.keywords[:match.start()]
post = self.keywords[match.end():]
self.keywords = (pre + " " + post).strip()
ku = key.upper()
if ku in self._settings:
del (self._settings[ku])
[docs] def get_mopfile_text(self, structure):
"""
Write a MOPAC input file to a StringIO buffer based on the current
settings and the provided Structure object.
:type structure: schrodinger.structure.Structure
:param structure:
The structure to use in writing the file.
return StringIO buffer
"""
settings = []
for key, value in self._settings.items():
if type(value) is bool:
settings.append(key)
else:
settings.append("%s=%s" % (key, value))
mopac_keywords = self.keywords
added_settings = []
if self.mopac_launcher.extra_keywords:
added_settings.extend(self.mopac_launcher.extra_keywords)
if not self.hasKeyword(StructureLauncher.NOMM):
added_settings.append(StructureLauncher.MMOK)
# If the structure has a formal charge, set the CHARGE keyword. If
# the user has specified a CHARGE keyword, use it in preference to
# the structure's formal charge. If the user-specified CHARGE is
# different from the structure formal charge, warn about this mismatch.
structure_charge = structure.formal_charge
if structure_charge:
if self.hasKeyword(StructureLauncher.CHARGE):
charge = int(self.getValue(StructureLauncher.CHARGE))
if charge != structure_charge:
print("WARNING: User-specified charge of %d does "
"not match the structure-determined charge "
"of %d." % (charge, structure_charge))
else:
added_settings.append(
"%s=%d" %
(StructureLauncher.CHARGE, structure.formal_charge))
all_settings = "%s %s %s" % (mopac_keywords, " ".join(settings),
" ".join(added_settings))
buff = StringIO()
# Wrap long lines. MOPAC has a 240 character input limit - three
# lines of 80 chars each.
lines = textwrap.wrap(all_settings, 77, break_long_words=False)
# Add a "+" to all lines but the last one.
for ix in range(len(lines) - 1):
buff.write("%s +\n" % lines[ix])
buff.write(lines[-1] + "\n")
# Add structure index to the title line so we can know which structures
# are missing from output files
buff.write(TITLE_START + f"{self.structure_index}, " + TITLE_END + "\n")
buff.write("\n")
self.structure_index += 1
opt = int(self.minimize)
if 'GRADIENTS' in self.keywords:
opt = 1
for at in structure.atom:
if at.element:
buff.write("%s %13.6f %d %13.6f %d %13.6f %d\n" %
(at.element, at.x, opt, at.y, opt, at.z, opt))
else: # dummy atom
buff.write("XX %13.6f %d %13.6f %d %13.6f %d\n" %
(at.x, opt, at.y, opt, at.z, opt))
return buff
[docs] def write(self, structure, filename):
"""
Write a MOPAC input file based on the current keyword argument
settings and the provided Structure object.
:type structure: schrodinger.structure.Structure
:param structure: The structure to use in writing the file.
:type filename: str
:param filename: The name of the file to be written.
"""
with open(filename, 'w') as fh:
buff = self.get_mopfile_text(structure)
shutil.copyfileobj(buff, fh)
return
[docs] def hasKeyword(self, keyword):
"""
If the given keyword was set in the constructor keywords string or
as a key in the kwdict dictionary, return True, otherwise return
False.
"""
# Prepend a space so that we can search for " " + keyword. This
# mirrors the way the MOPAC code looks for keywords.
keywords = " " + self.keywords.upper()
ku = keyword.upper()
if keywords.find(" " + ku) != -1:
return True
if ku in self._settings:
return True
return False
[docs] def run(self,
structure,
jobname=None,
input_file=False,
tmpdir='',
scratch_cleanup=utils.REMOVE,
save_output_file=False):
"""
Run a MOPAC calculation on the provided structure given the
object's current settings.
"""
# It seems like it should be possible to make the settings directly
# into the MOPAC Fortran modules and avoid explicitly writing an input
# file. Writing an input file is the expedient route for now.
start_dir = os.getcwd()
if not jobname:
if structure.title:
jobname = re.sub(r"\W", "", structure.title) + "_semi_emp"
else:
jobname = "semi_emp"
inputfile = jobname + ".mop"
scr_dir = utils.make_scratch_dir(tmpdir, jobname)
external = None
results = None
try:
try:
external = os.path.abspath(self.getValue('EXTERNAL'))
shutil.copy(external, scr_dir)
self.setKeyword("EXTERNAL", os.path.basename(external))
except KeyError:
pass
os.chdir(scr_dir)
self.write(structure, inputfile)
if input_file:
shutil.copy(inputfile, start_dir)
results = self.mopac_launcher.run(jobname, structure)
finally:
if external:
# Restore original EXTERNAL value.
self.setKeyword("EXTERNAL", external)
utils.run_cleanup(results, start_dir, scr_dir, jobname,
save_output_file, scratch_cleanup)
return results