Source code for schrodinger.application.mopac.structure_launchers

"""
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