Source code for schrodinger.application.jaguar.input
"""
Functions and classes for reading and creating Jaguar input files.
It allows setting and querying of keyword values in Jaguar input &gen
sections and also provides an interface to some of the mmjag library.
The JaguarInput class also provides for the running of jobs.
"""
# Copyright Schrodinger, LLC. All rights reserved.
import copy
import os
import re
import shutil
import tempfile
import schrodinger.infra.mm as mm
import schrodinger.utils.log
from schrodinger import structure
from schrodinger.application.jaguar import output
from schrodinger.application.jaguar import user_config
from schrodinger.application.jaguar.utils import basic_windows_path
from schrodinger.infra import mmbitset
from schrodinger.infra.mmcheck import MmException
from schrodinger.job import jobcontrol
from schrodinger.structutils.rmsd import superimpose
from schrodinger.utils import fileutils
__cvs_version__ = "$Revision: 1.40 $"
_version = __cvs_version__
logger = schrodinger.utils.log.get_logger("schrodinger.jaguar.input")
JAGUAR_EXE = "${SCHRODINGER}/jaguar"
INPUT_VERIF_THRESH = 0.1
MAEFILE = 'MAEFILE'
if "SCHRODINGER" not in os.environ:
    raise ImportError(
        "The SCHRODINGER environment variable must be defined to use the Jaguar input module."
    )
[docs]class ConstraintError(Exception):
    """
    Exception to be raised when Constraint is invalid, e.g. no atoms specified.
    """
    pass
[docs]class InputVerificationError(Exception):
    """
    Exception for non-matching JaguarInput instances. Generated by isEquivalentInput.
    """
    pass
[docs]def launch_file(input, wait=False, save=False, disp=None, **kwargs):
    """
    Launch a Jaguar job from the Jaguar input file 'input'. Returns a
    jobcontrol.Job object.
    Arguments
    wait (bool)
        Do not return until the job is complete.
    save (bool)
        Set to True to save the scratch directory.
    disp (str)
        Set to a valid jobcontrol disposition.
    """
    opts = []
    if save:
        opts.append("-SAVE")
    if disp:
        opts.extend(["-DISP", disp.strip()])
    # All keys and values should be strings, and we need to ensure that
    # whitespace is stripped off or it will break launch_job().
    for k, v in kwargs.items():
        opts.extend([k.strip(), v.strip()])
    job = jobcontrol.launch_job([JAGUAR_EXE, "run"] + opts + [input])
    if wait:
        job.wait()
    return job
[docs]def apply_jaguar_atom_naming(struc):
    """
    Apply jaguar unique atom naming to all atoms in the specified structure
    :param struc: The structure to apply atom naming to
    :type struc: `schrodinger.structure.Structure`
    """
    bitset = mmbitset.Bitset(size=struc.atom_total)
    bitset.fill()
    mm.mmct_ct_atom_name_jaguar_unique(struc, bitset)
[docs]def read(filename, index=1, **kwargs):
    """
    Create and return a JaguarInput object from a Jaguar .in file or a
    maestro .mae file. Additional keyword args are passed on to the
    JaguarInput constructor.
    Parameters
    filename (str)
        Can be a Jaguar input .in file, Maestro structure .mae file, or the
        basename of a file of one of these types.
    index (int)
        The index of the structure to use for an .mae input file.
    kwargs (additional keyword arguments)
        All keyword arguments are passed on to the JaguarInput constructor.
    """
    if not os.path.exists(filename):
        if os.path.exists(filename + ".in"):
            filename = filename + ".in"
        elif os.path.exists(filename + ".mae"):
            filename = filename + ".mae"
        if not os.path.exists(filename):
            raise IOError("No such file '%s'" % filename)
    root, ext = os.path.splitext(filename)
    if ext == ".in":
        ji = JaguarInput(filename, **kwargs)
    elif ext == ".mae":
        st = structure.Structure.read(filename, index)
        if "name" not in kwargs:
            kwargs["name"] = os.path.basename(root)
        ji = JaguarInput(structure=st, **kwargs)
        ji.setDirective(MAEFILE, filename)
    else:
        raise Exception("Filename '%s' must end in '.in' or '.mae'" % filename)
    return ji
[docs]def get_name_file(name):
    """
    Get the input filename and jobname, given either the input filename or
    the jobname.
    Return a tuple of (jobname, filename).
    """
    if name.endswith('.in'):
        filename = name
        jobname = os.path.basename(name[:-3])
    else:
        filename = name + '.in'
        jobname = os.path.basename(name)
    if not os.path.isabs(filename):
        filename = os.path.abspath(filename)
    return jobname, filename
[docs]def split_structure_files(files):
    """
    Given a list of structure (.in or .mae) files, split all .mae files into
    individual structures and write them back to disk with new names.
    Return the list of structure files, each of which contains a single
    structure. Any split mae files are named with either the entry ID (if
    available) or the source file basename with an index suffix.
    """
    new_files = []
    for f in files:
        root, ext = os.path.splitext(f)
        if ext == ".mae":
            if structure.count_structures(f) == 1:
                new_files.append(f)
            else:
                ix = 1
                for st in structure.StructureReader(f):
                    if "s_m_entry_id" in st.property:
                        suffix = st.property['s_m_entry_id']
                    else:
                        suffix = str(ix)
                    name = os.path.basename(root) + "_" + suffix + ".mae"
                    st.write(name)
                    new_files.append(name)
                    ix += 1
        else:
            new_files.append(f)
    return new_files
[docs]class JaguarInput(object):
    """
    A class for specifying Jaguar input.
    This is a thin wrapper to the mmjag library and carries very little
    internal information.
    """
    reload = False
    run_kwargs = {}
[docs]    def __init__(self,
                 input=None,
                 name=None,
                 handle=None,
                 structure=None,
                 genkeys=None,
                 reload=None,
                 run_kwargs=None,
                 text=None,
                 qsite_ok=False):
        """
        There are three main ways to create a JaguarInput instance:
        from a Jaguar input file 'input', an mmjag handle 'handle', or a
        Structure instance 'structure'.
        The 'input' parameter will provide for initialization and job name
        from an existing Jaguar input file. (If no new name is provided,
        calling the save() method will overwrite the original file.)
        Note that the structure and name parameters will modify the object
        status after loading from a file or initialization from a handle has
        completed. This can be utilized to get the settings from a Jaguar
        input file but replace the geometry for a new calculation.
        :param input: A jaguar input file; a default name will be derived from
            the input value.
        :type input: str
        :param name: A jaguar job name that will override the name derived from
            'input'.
        :type name: str
        :param handle: An mmjag handle, pointing to an existing mmjag instance
            in memory.
        :type handle: int
        :param structure: A structure that will populate a new mmjag instance.
            If specified with 'input' or 'handle' it will replace any structure
            provided by them.
        :type structure: `schrodinger.structure.Structure`
        :param genkeys: A dictionary of keyword/value pairs to set in the input
            &gen section. If keywords are specified in this mapping and the file,
            the genkeys value will be used.
        :type genkeys: dict
        :param reload: Specifies whether to reload the job from an output file
            in the run() method. Default value is False, but can be modified by
            setting JaguarInput.reload to a different value.
        :type reload: bool
        :param run_kwargs: The run_kwargs dictionary provides default keyword
            settings for the run function that is called from the launch and run
            methods. Default is to set no defaults, but this can be modified by
            setting JaguarInput.run_kwargs to a different value.
        :type run_kwargs: dict
        :param text: The complete text of a Jaguar input file.
        :type text: str
        :param qsite_ok: Allows this class to be initialized from a QSite input
            file
        :type qsite_ok: bool
        """
        mm.mmerr_initialize()
        mm.mmjag_initialize(mm.error_handler)
        # The _file attribute is not meant to be manipulated directly by the
        # user. It can be set by creating a new JaguarInput object from a
        # filename, by using the saveAs() method, or by setting an
        # instance's name attribute.
        self._file = None
        self._orig_dir = None
        self.name = ""
        if handle:
            self.handle = handle
        elif input:
            self._setNameFile(input)
            self._orig_dir = os.path.dirname(self.filename)
            user_config.preprocess_infile(self.filename, self.filename)
            if (mm.mmjag_determine_job_type(
                    self.filename) == mm.MMJAG_JAGUAR_JOB):
                self.handle = mm.mmjag_read_file(self.filename)
            elif (mm.mmjag_determine_job_type(
                    self.filename) == mm.MMJAG_QSITE_JOB):
                # See QSITE-658 for the reason behind the qsite_ok flag
                if not qsite_ok:
                    raise RuntimeError(
                        f"This file ({self.filename}) is a QSite input file! Use QSiteInput instead of JaguarInput."
                    )
                else:
                    mm.mmim_initialize(mm.error_handler)
                    self.handle, self.i_handle, self.ct1, self.ct2, self.ct3 = mm.mmjag_read_qsite_file(
                        self.filename)
        elif text:
            tmpfile = self._writeTempFile(text)
            user_config.preprocess_infile(tmpfile, tmpfile)
            if (mm.mmjag_determine_job_type(tmpfile) == mm.MMJAG_JAGUAR_JOB):
                self.handle = mm.mmjag_read_file(tmpfile)
            elif (mm.mmjag_determine_job_type(tmpfile) == mm.MMJAG_QSITE_JOB):
                # See QSITE-658 for the reason behind the qsite_ok flag
                if not qsite_ok:
                    raise RuntimeError(
                        f"This input text ({tmpfile}) is a QSite input file! Use QSiteInput instead of JaguarInput."
                    )
                else:
                    mm.mmim_initialize(mm.error_handler)
                    self.handle, self.i_handle, self.ct1, self.ct2, self.ct3 = mm.mmjag_read_qsite_file(
                        tmpfile)
            fileutils.force_remove(tmpfile)
        else:
            self.handle = mm.mmjag_new()
        if name:
            self.name = name
        ##
        # The _structures attribute stores the last Structures used to set the
        # internal mmjag structure. This allows us to avoid the round trip
        # CT -> mmjag -> CT that I'm not sure is idempotent.
        #
        self._structures = {
            mm.MMJAG_ZMAT1: None,
            mm.MMJAG_ZMAT2: None,
            mm.MMJAG_ZMAT3: None
        }
        if structure is not None:
            self.setStructure(structure)
        if genkeys:
            self.setValues(genkeys)
        if reload is not None:
            self.reload = reload
        else:
            self.reload = JaguarInput.reload
        if run_kwargs is not None:
            self.run_kwargs = run_kwargs
        else:
            self.run_kwargs = JaguarInput.run_kwargs
    def __del__(self):
        """
        Delete the mmjag handle and clean up the mmlibs.
        """
        try:
            mm.mmjag_delete(self.handle)
        except:
            pass
        mm.mmjag_terminate()
        mm.mmerr_terminate()
    def __copy__(self):
        """
        Create a copy of the JaguarInput object, setting name to
        <jobname>_copy.
        Note that this isn't really a proper copy. For example, if the original
        instance were created from a file then self.filename would be set, but the
        copy made from it will have self.filename=None. Also the name has changed.
        """
        copied = JaguarInput(handle=mm.mmjag_clone(self.handle),
                             name=self.name + "_copy")
        return copied
    def __deepcopy__(self, memo):
        cls = self.__class__
        new_copy = cls.__new__(cls)
        # These mm calls are made to ensure that the reference
        # counting is consistent with the number of JaguarInput
        # instances.
        mm.mmerr_initialize()
        mm.mmjag_initialize(mm.error_handler)
        memo[id(self)] = new_copy
        for k, v in self.__dict__.items():
            if k != 'handle':
                setattr(new_copy, k, copy.deepcopy(v, memo))
            else:
                setattr(new_copy, k, mm.mmjag_clone(v))
        return new_copy
[docs]    def launch(self, *args, **kwargs):
        """
        Save the file and launch a job. Returns a jobcontrol.Job object.
        Uses the class run_kwargs value as base for keyword arguments to the
        run function.
        """
        self.save()
        _kwargs = dict(self.run_kwargs)
        _kwargs.update(kwargs)
        return launch_file(self.filename, *args, **_kwargs)
[docs]    def run(self, **kwargs):
        """
        Save the file and launch a job with the wait option. Returns a
        JaguarOutput object. Raises an exception if the job failed.
        Set the class (or instance) attribute reload to True to load
        existing output files of the same name.
        """
        fname = basic_windows_path(self.filename)
        if fname != self.filename:
            print(
                "WARNING: The path length of this file exceeds the maximum path length on Windows."
                " Please re-run your job with a shorter filename or in a folder closer to the root of you drive."
            )
        self.saveAs(fname)
        # Setting reload for an instance will override the class default.
        if self.reload:
            if os.path.exists(self.name + ".out"):
                out = output.JaguarOutput(self.name)
                if out.status == output.JaguarOutput.OK:
                    logger.info("Reloaded %s from previous run." % self.name)
                    return out
        _kwargs = dict(self.run_kwargs)
        _kwargs.update(kwargs)
        _kwargs['wait'] = True
        logger.info("Calling launch from JaguarInput.run() for %s." % self.name)
        job = launch_file(fname, **_kwargs)
        if not job.succeeded():
            raise Exception(
                "Job '%s' from input file '%s' failed; see output for details."
                % (job.job_id, fname))
        logger.debug("Returning JaguarOutput from %s run." % self.name)
        return output.JaguarOutput(self.name)
[docs]    def save(self, follow_links=mm.MMJAG_APPEND_OFF, validate=None):
        """
        Write an input file to name.in.
        :param follow_links: Flag specifying whether to follow links in the
            structure to a Jaguar restart file, if present, to append additional
            sections.  Options are:
            - mm.MMJAG_APPEND_OFF: don't follow links to append additional
              sections (default)
            - mm.MMJAG_APPEND_OVERWRITE: append sections from link, overwriting
              any that overlap
            - mm.MMJAG_APPEND_NO_OVERWRITE: append sections from link, not
              overwriting any that overlap
            - mm.MMJAG_APPEND_X_OVERWRITE: exclusively append sections from
              link, deleting any already present
            - mm.MMJAG_APPEND_X_NO_OVERWRITE: append sections from link only if
              no such sections already present
        :type follow_links: int
        :param validate: If True, sections dependent upon geometry, molecular
            state, basis set are removed if invalid.  If not entered (None),
            default is to validate if "following links".
        :type validate: bool
        """
        if not self.filename:
            raise Exception(
                "No name has been specified for this JaguarInput object. Use the saveAs() method."
            )
        self.saveAs(self.filename, follow_links=follow_links, validate=validate)
[docs]    def saveAs(self, file_, follow_links=mm.MMJAG_APPEND_OFF, validate=None):
        """
        Write an input file to name.in and set the object's name attribute.
        :param follow_links: Flag telling whether to follow links in the
            structure to a Jaguar restart file, if present, to append additional
            sections.  Options are:
            - mm.MMJAG_APPEND_OFF: don't follow links to append additional
              sections (default)
            - mm.MMJAG_APPEND_OVERWRITE: append sections from link, overwriting
              any that overlap
            - mm.MMJAG_APPEND_NO_OVERWRITE: append sections from link, not
              overwriting any that overlap
            - mm.MMJAG_APPEND_X_OVERWRITE: exclusively append sections from
              link, deleting any already present
            - mm.MMJAG_APPEND_X_NO_OVERWRITE: append sections from link only if
              no such sections already present
        :type follow_links: int
        :param validate: If True, sections dependent upon geometry, molecular
            state, basis set are removed if invalid.  If not entered (None),
            default is to validate if "following links".
        :type validate: bool
        """
        self._setNameFile(file_)
        # Structure for optional link following
        struct_link = None
        # If any structures have been defined, write them to disk.
        structs_defined = any(
            st for st in self._structures.values() if st is not None)
        if structs_defined:
            structures = self.getStructures()
            # Raises RuntimeError in the event of a problem with the structures
            self._checkStructures(structures)
            self.writeMaefile(structs=structures)
            # Save (first) structure for optional link following
            struct_link = structures[mm.MMJAG_ZMAT1]
        # Otherwise, if the input file is being written to a new directory
        # and the old MAEFILE is relative, copy the MAEFILE to the new dir.
        #
        # FIXME: if other directives are present and not absolute paths,
        # they should be copied, too.
        elif self._orig_dir:
            orig_file = self.getDirective(MAEFILE)
            if orig_file and not os.path.isabs(orig_file):
                new_dir = os.path.abspath(os.path.dirname(self.filename))
                orig_file = os.path.join(self._orig_dir, orig_file)
                if new_dir != self._orig_dir and os.path.exists(orig_file):
                    new_file = self._formMaeFileNameFromOriginal(orig_file)
                    self.setDirective(MAEFILE, new_file)
                    new_file = os.path.join(new_dir, new_file)
                    if os.path.exists(new_file):
                        backup = new_file + "~"
                        os.rename(new_file, backup)
                    shutil.copy(orig_file, new_file)
        # Copy JaguarInput object first so that it will remain unchanged if
        # additional sections are appended from link to copy
        copy_jag = self.__copy__()
        if follow_links != mm.MMJAG_APPEND_OFF and struct_link:
            # Use source dir for link structure, if available, as backup place
            # to look for restart file.  If not available, set backup as "",
            # which will amount to using the current directory.
            backup = struct_link.property.get('s_m_Source_Path')
            if not backup:
                backup = ""
            mm.mmjag_append_sections_from_link(copy_jag.handle, struct_link,
                                               backup, follow_links)
            if validate is None:
                validate = True
        # Validate dependent sections and remove if invalid, if requested
        if validate:
            mm.mmjag_remove_invalid(copy_jag.handle, mm.MMJAG_JAGUAR_JOB, -1,
                                    mm.MMCT_INVALID_CT)
        mm.mmjag_write_file(copy_jag.handle, self.filename)
        return
[docs]    def saveAsBatch(self, file_):
        """
        Write a batch file to `file_.bat`. Note that the object's name attribute
        will not be set.
        :param `file_`: The filename to save to.  The `.bat` extension will be
            added if necessary.
        :type `file_`: str
        """
        if not file_.endswith(".bat"):
            file_ += ".bat"
        mm.mmjag_write_batch(self.handle, file_, True)
    def _formMaeFileNameFromOriginal(self, orig_file):
        """
        Form the name of the MAEFILE that orig_file will be copied to in the
        current directory.
        The result of this function is used for both the name of the Maestro
        file written with the .in file and the MAEFILE directive in the .in file
        This method may be overwritten by subclasses in order to control the
        name of the MAEFILE directive file. It is only used if setStructure
        has not been called on this instance.
        :param str orig_file: The name of the original mae file that will be
            copied into the current directory for the MAEFILE directive,
            may contain full path information
        :rtype: str
        :return: The name the MAEFILE will have without any path information
        """
        return os.path.basename(orig_file)
[docs]    def getInputText(self):
        """
        Return the text of the input file.  Note that the input file will not be
        saved to self._file.
        :return: The text of the input file
        :rtype: str
        """
        temp_name = self._getTempFile(False)
        mm.mmjag_write_file(self.handle, temp_name)
        with open(temp_name, 'r') as fh:
            input_text = fh.read()
        fileutils.force_remove(temp_name)
        return input_text
[docs]    def getZmatText(self, zmat=mm.MMJAG_ZMAT1):
        """
        Return the input file text corresponding to the specified Z matrix.
        :param zmat: The z matrix to return the text for.  Must be one of
            mm.MMJAG_ZMAT1, mm.MMJAG_ZMAT2, or mm.MMJAG_ZMAT3.
        :return: The text for the specified Z matrix
        :rtype: str
        """
        temp_name = self._getTempFile(False)
        mm.mmjag_zmat_write(self.handle, zmat, temp_name)
        with open(temp_name, 'r') as fh:
            zmat_text = fh.read()
        fileutils.force_remove(temp_name)
        return zmat_text
    def _getTempFile(self, return_handle=False):
        """
        Get a temporary file
        :param return_handle: If True, an open file handle for writing to the
            temporary file will be returned.  If False, no file handle will be
            returned.
        :type return_handle: bool
        :return: If `return_handle` is True, then a file name (str) will
            returned. This file will already exist but will be empty. If
            `return_handle` is False, then a tuple of:
              - A file name (str)
              - An open file handle for writing (file) will be returned.
        :rtype: str or tuple
        """
        temp_dir = fileutils.get_directory_path(fileutils.TEMP)
        (temp_handle, temp_name) = tempfile.mkstemp(suffix=".in", dir=temp_dir)
        if not return_handle:
            os.close(temp_handle)
            return temp_name
        else:
            wrapped_handle = os.fdopen(temp_handle, 'w')
            return wrapped_handle, temp_name
    def _writeTempFile(self, text):
        """
        Write the specified text to a temporary file
        :param text: The text to write to the file
        :type text: str
        :return: The name of the file that `text` has been written to
        :rtype: str
        """
        (temp_handle, temp_name) = self._getTempFile(True)
        temp_handle.write(text)
        temp_handle.close()
        return temp_name
    def _setNameFile(self, input):
        """
        Set the name and file properties from a named input file or jobname.
        This method keeps the filename and jobname in sync.
        """
        self._name, self._file = get_name_file(input)
    def _getName(self):
        """
        Return the name of the job.
        """
        return self._name
    def _setName(self, name):
        """
        Set the name of the job and update the filename.
        """
        self._name = name
        self._file = name + ".in"
    name = property(
        _getName, _setName,
        "Set the jobname; also updates the filename based on the jobname.")
    @property
    def filename(self):
        """
        Return the filename of the JaguarInput object.
        On Windows, with paths over the Windows max path length,
        we must prepend an extended path tag.
        """
        file_output = None
        if self._file is not None:
            file_output = fileutils.extended_windows_path(
                self._file
            )  # This function is a no-op on platforms other than Windows
        return file_output
    @filename.setter
    def filename(self, value):
        """
        We want to redirect any users who want to change the filename
        to instead change _name if they want to change the filename
        :type value: str
        :param value: New filename to swallow
        """
        print('WARNING: Directly setting .filename is unsupported.'
              ' To change the name of a file, set the "_name" property instead')
[docs]    def getAtomCount(self, zmat=mm.MMJAG_ZMAT1):
        """
        Return the number of atoms for the specified zmat.
        """
        return mm.mmjag_zmat_atom_count(self.handle, zmat)
[docs]    def getValue(self, key):
        """
        Return the &gen section value for keyword 'key'.
        The return type is as defined by mmjag_key_type().
        """
        try:
            type_ = mm.mmjag_key_type(self.handle, key)
        except MmException:
            # If the key type is not known, return it as a string, since an int
            # or float cast may not work
            type_ = None
        if type_ == mm.MMJAG_INT:
            return mm.mmjag_key_int_get(self.handle, key)
        elif type_ == mm.MMJAG_REAL:
            return mm.mmjag_key_real_get(self.handle, key)
        else:
            return mm.mmjag_key_char_get(self.handle, key)
[docs]    def getDefault(self, key):
        """
        Return the default value for &gen section keyword 'key'.
        """
        type_ = mm.mmjag_key_type(self.handle, key)
        if type_ == mm.MMJAG_INT:
            return mm.mmjag_key_int_def(self.handle, key)
        elif type_ == mm.MMJAG_REAL:
            return mm.mmjag_key_real_def(self.handle, key)
        else:
            return mm.mmjag_key_char_def(self.handle, key)
[docs]    def setValue(self, key, value):
        """
        Set the &gen section keyword 'key' to the value provided.
        If value is None, the keyword will be unset.
        """
        if value is None:
            mm.mmjag_key_delete(self.handle, key)
            return
        if isinstance(value, int):
            mm.mmjag_key_int_set(self.handle, key, value)
        elif isinstance(value, float):
            mm.mmjag_key_real_set(self.handle, key, value)
        elif isinstance(value, str):
            # Strip any leading or trailing whitespace, and do not set value
            # if value is an empty string after stripping
            if value.strip() != '':
                mm.mmjag_key_char_set(self.handle, key, value.strip())
        else:
            err = f"value type, {type(value).__name__}, not valid. Please use int, float, or str."
            raise RuntimeError(err)
        return
[docs]    def setValues(self, dict_):
        """
        Set multiple &gen section keywords from the provided dictionary
        Note that one easy way to specify the `dict_` argument is via the
        `"dict(basis='6-31g**', igeopt=1)"` syntax for constructing a
        dictionary.
        """
        for k, v in dict_.items():
            self.setValue(k, v)
    __getitem__ = getValue
    __setitem__ = setValue
    def __delitem__(self, key):
        """
        Remove a key from the &gen section.  Note that `deleteKey` will raise
        an error if `key` is not recognized as a Jaguar keyword.  This function
        will successfully delete the key.
        """
        mm.mmjag_key_clear(self.handle, key)
[docs]    def deleteKey(self, key):
        """
        Remove a key from the &gen section.
        """
        mm.mmjag_key_delete(self.handle, key)
[docs]    def deleteKeys(self, keys):
        """
        Remove a list of keys from the &gen section.
        """
        for k in keys:
            mm.mmjag_key_delete(self.handle, k)
[docs]    def getNonDefault(self):
        """
        Return a dictionary of all non-default keys except 'multip' and
        'molchg', which must be retrieved explicitly since they are
        connected to the geometry.
        """
        dict_ = {}
        for i in range(1, mm.mmjag_key_nondef_count(self.handle) + 1):
            key = mm.mmjag_key_nondef_get(self.handle, i)
            if key in [mm.MMJAG_IKEY_MOLCHG, mm.MMJAG_IKEY_MULTIP]:
                continue
            try:
                dict_[key] = self.getValue(key)
            except mm.MmException as e:
                raise ValueError(f"Encountered error for Jaguar key {key}.")
        return dict_
[docs]    def isNonDefault(self, key):
        """
        Has the specified key been set to a non-default value?
        :param key: The key to check
        :type key: str
        :return: True if the specified key is set to a non-default value.  False
            otherwise.
        :rtype: bool
        """
        is_non_default = mm.mmjag_key_defined(self.handle, key)
        return bool(is_non_default)
[docs]    def setDirective(self, name, value):
        """
        Set a file specification directive.
        """
        mm.mmjag_directive_set(self.handle, name, value)
[docs]    def getDirective(self, name):
        """
        Get a file specification directive.
        """
        return mm.mmjag_directive_get(self.handle, name)
[docs]    def getDirectives(self):
        """
        Get all file specification directives, except for MAEFILE, which
        is weeded out by the mmjag function itself.
        """
        k, v = mm.mmjag_directives_list(self.handle)
        dict_ = dict(list(zip(k, v)))
        return dict_
[docs]    def writeMaefile(self, filename=None, structs=None):
        """
        Write an associated .mae file and set the MAEFILE directive. If no
        name is provided, use jobname.mae. If no structs are provided, use
        self.getStructure().
        If an absolute filename is not provided but the input file is known,
        write the mae file relative to the input file.
        If no filename is given and no jobname is set, a random filename will be
        generated in the current directory.
        :param structs: A list of structures
        :type structures: list
        """
        if not filename:
            if hasattr(self, "name") and self.name:
                filename = self.name + ".mae"
            else:
                (handle, filename) = tempfile.mkstemp(".mae", "jaguar_temp_",
                                                      ".")
                os.close(handle)
        if not structs:
            structs = [self.getStructure()]
        if not os.path.isabs(filename) and self.filename:
            filepath = os.path.join(os.path.dirname(self.filename), filename)
        else:
            filepath = filename
        filepath = fileutils.extended_windows_path(filepath)
        with structure.StructureWriter(filepath) as writer:
            writer.extend(structs)
        self.setDirective(MAEFILE, filename)
        return filepath
[docs]    def getMaefilename(self, dont_create=False):
        """
        Get the filename of the Maestro file containing the input structure.  If
        no such file exists, it will be created unless c{dont_create} is True.
        :param dont_create: If False, a Maestro file will be created if one does
            not yet exist.  If True, None will be returned if no file exists.
        :type dont_create: bool
        :return: The requested filename as an absolute path.  If no file exists
            and `dont_create` is True, None will be returned.
        :rtype: str or NoneType
        :raise RuntimeError: If no structure is present.
        """
        maefile = self.getDirective(MAEFILE)
        if maefile:
            if os.path.isabs(maefile):
                return maefile
            elif self.filename:
                dirname = os.path.dirname(self.filename)
                return os.path.join(dirname, maefile)
            else:
                dirname = os.getcwd()
                return os.path.join(dirname, maefile)
        elif dont_create:
            return None
        elif not self.getAtomCount():
            raise RuntimeError("No structure present.")
        else:
            return self.writeMaefile()
    # Restart name
    def _getRestart(self):
        """
        Get the restart name associated with the input file.
        """
        return output.restart_name(self.name)
    restart = property(_getRestart,
                       doc="Get the restart jobname based on the current name.")
    # Structure/zmat translation
[docs]    def getStructure(self, zmat=mm.MMJAG_ZMAT1):
        """
        Return a Structure representation of the specified zmat section.
        Note that if the JaguarInput instance was created from a Jaguar
        input file that has no associated Maestro file (MAEFILE), the Lewis
        structure is determined automatically based on atom distances.
        Parameters
        zmat (mmjag enum)
            The zmat to return (MMJAG_ZMAT1, MMJAG_ZMAT2, or MMJAG_ZMAT3).
        """
        struct = self._structures.get(zmat)
        if not struct:
            handle = mm.mmjag_zmat_ct_get(self.handle, zmat)
            struct = structure.Structure(handle)
        return struct
[docs]    def getStructures(self):
        """
        Return a list of all available structure representations for zmat
        sections
        The first call to getStructure is required because getStructure is
        guaranteed to return a structure, which might have been set in a
        different way
        """
        zmats = [mm.MMJAG_ZMAT1, mm.MMJAG_ZMAT2, mm.MMJAG_ZMAT3]
        return [self.getStructure(zmat) for zmat in zmats]
[docs]    def setStructure(self, struct, zmat=mm.MMJAG_ZMAT1, set_molchg_multip=True):
        """
        Set one of the zmat sections from the provided Structure (or MMCT
        handle).
        If set_molchg_multip is True, calling this method will update
        the values of molchg and multip. molchg will be consistent
        with the sum of formal charges in the provided CT, while
        multip will be set according to the CT-level
        i_m_Spin_multiplicity property.
        Note one may call self.clearAllConstraints() to remove any unwanted
        constraints or scan-coordinates after updating the Structure. By
        default, such info will be preserved if the new Structure has the
        same atoms and connectivity as the original Structure; only XYZs
        (and charges/multiplicity) will be updated.
        Parameters
        struct (schrodinger.structure.Structure)
            The structure to use for setting.
        zmat (mmjag enum)
            The zmat to set (MMJAG_ZMAT1, MMJAG_ZMAT2, or MMJAG_ZMAT3).
        set_molchg_multip
            Whether to update molecular charge and multiplicity
            (default is yes)
        """
        self._structures[zmat] = struct
        if set_molchg_multip:
            # CT-level charge has preference over sum of atomic charges
            # (JAGUAR-5790 and JAGUAR-5604)
            if 'i_m_Molecular_charge' in struct.property:
                self.setValue(mm.MMJAG_IKEY_MOLCHG,
                              struct.property['i_m_Molecular_charge'])
            else:
                self.setValue(mm.MMJAG_IKEY_MOLCHG, struct.formal_charge)
            if 'i_m_Spin_multiplicity' in struct.property:
                self.setValue(mm.MMJAG_IKEY_MULTIP,
                              struct.property['i_m_Spin_multiplicity'])
        # This call resets &coord block if struct is different
        mm.mmjag_zmat_ct_set(self.handle, zmat, struct)
    def _checkStructures(self, structures):
        """
        Raise RuntimeError if structure with a lower number zmat structure is
        undefined when a higher number zmat structure is defined (e.g., zmat2
        being defined when zmat1 is not).
        """
        empty_struc = None
        for i, cur_struc in enumerate(structures):
            if cur_struc.atom_total == 0:
                empty_struc = i + 1
            elif empty_struc is not None:
                err = "zmat%i is defined but not zmat%i" % (i + 1, empty_struc)
                raise RuntimeError(err)
[docs]    def hasStructure(self, zmat=mm.MMJAG_ZMAT1):
        """
        Does this handle have a structure for the specified zmat section?
        :param zmat: The zmat to check (MMJAG_ZMAT1, MMJAG_ZMAT2, or
            MMJAG_ZMAT3)
        :type zmat: int
        :return: True if the specified structure is present.  False otherwise.
        :rtype: bool
        """
        defined = mm.mmjag_zmat_defined(self.handle, zmat)
        return bool(defined)
[docs]    def resetStructure(self, struct, molchg, multip, zmat=mm.MMJAG_ZMAT1):
        """
        Redefine the connectivity and formal charges for the input CT,
        and store the new structure in the current mmjag object, in the
        &zmat section indicated by the zmat argument.
        This function is used when the molecular geometry has changed such
        that it may not be consistent with its original CT description in
        terms of bond orders and formal charges, and we want to force the
        creation of a new Lewis structure.
        Parameters
        struct (schrodinger.structure.Structure)
            The structure to use for setting.
        molchg (integer)
            The value to use for the net molecular charge.
        multip (integer)
            The value to use for the net molecular spin.
        zmat (mmjag enum)
            The zmat to set (MMJAG_ZMAT1, MMJAG_ZMAT2, or MMJAG_ZMAT3).
        """
        geostring = ""
        for at in struct.atom:
            geostring += "%s %s %s %s\n" % (at.atom_name, str(at.x), str(
                at.y), str(at.z))
        mm.mmjag_zmat_from_geostring(self.handle, zmat, molchg,
                                     mm.MMJAG_ANGSTROM_DEGREE, geostring)
        newct = structure.Structure(mm.mmjag_ct_from_zmat(self.handle, zmat))
        self._structures[zmat] = newct
        self.setValue(mm.MMJAG_IKEY_MOLCHG, molchg)
        self.setValue(mm.MMJAG_IKEY_MULTIP, multip)
[docs]    def deleteStructure(self, zmat=mm.MMJAG_ZMAT1):
        """
        Delete the specified structure
        :param zmat: The z matrix to delete.  Must be one of mm.MMJAG_ZMAT1,
            mm.MMJAG_ZMAT2, or mm.MMJAG_ZMAT3.
        """
        self._structures[zmat] = None
        mm.mmjag_zmat_delete(self.handle, zmat)
    # Atom properties
    def _setCounterpoise(self, atom, value, zmat=mm.MMJAG_ZMAT1):
        """
        Set the counterpoise status (True or False) for the specified atom.
        Parameters
        atom (int)
            The index of the atom to modify.
        value (bool)
            Use True to make it counterpoise, False to make it real.
        zmat (mmjag enum)
            The zmatrix to modify.
        """
        mm.mmjag_zmat_atom_cp_set(self.handle, zmat, atom, value)
[docs]    def preflight(self):
        """
        Run a preflight check and return any warnings.
        :return: A string containing any warnings raised by the preflight check.
            If there were no warnings, an empty string is returned.
        :rtype: str
        """
        with mm.CaptureMMErr(mm.mmjag_get_errhandler()) as captured:
            mm.mmjag_preflight(self.handle)
        return captured.messages
[docs]    def makeInternalCoords(self, zmat=mm.MMJAG_ZMAT1):
        """
        Convert the specified Z-matrix to internal coordinates
        :param zmat: The Z-matrix to modify.  Must be one of mm.MMJAG_ZMAT1,
            mm.MMJAG_ZMAT2, or mm.MMJAG_ZMAT3.
        :type zmat: int
        """
        mm.mmjag_zmat_makeint(self.handle, zmat)
[docs]    def makeCartesianCoords(self, zmat=mm.MMJAG_ZMAT1):
        """
        Convert the specified Z-matrix to Cartesian coordinates
        :param zmat: The Z-matrix to modify.  Must be one of mm.MMJAG_ZMAT1,
            mm.MMJAG_ZMAT2, or mm.MMJAG_ZMAT3.
        :type zmat: int
        """
        mm.mmjag_zmat_makecart(self.handle, zmat)
[docs]    def getUnknownKeywords(self):
        """
        Return a dictionary of all unknown keywords and their values
        :return: A dictionary of all unknown keywords and their values
        :rtype: dict
        """
        unknowns = {}
        for i in range(1, mm.mmjag_key_nondef_count(self.handle) + 1):
            key = mm.mmjag_key_nondef_get(self.handle, i)
            try:
                mm.mmjag_key_type(self.handle, key)
            except MmException:
                # mmjag_key_type will fail for all unknown keywords
                val = mm.mmjag_key_char_get(self.handle, key)
                unknowns[key] = val
        return unknowns
[docs]    def sectionDefined(self, sect):
        """
        Determine if the specified section is defined
        :param sect: The section to check for
        :type sect: str
        :return: True if the specified section is defined.  False otherwise.
        :rtype: bool
        """
        defined = mm.mmjag_sect_defined(self.handle, sect)
        return bool(defined)
[docs]    def createSection(self, sect):
        """
        Create the specified section
        :param sect: The section to create
        :type sect: str
        """
        mm.mmjag_sect_create(self.handle, sect)
[docs]    def deleteSection(self, sect):
        """
        Delete the specified section
        :param sect: The section to delete
        :type sect: str
        :raise ValueError: If the specified section does not exist
        """
        try:
            mm.mmjag_sect_delete(self.handle, sect)
        except MmException as err:
            raise ValueError(str(err))
[docs]    def clearAllConstraints(self):
        """
        Delete all constraints and their associated coord entries.  (Note that
        mm.mmjag_constraint_delete_all() does not delete the associated coord
        entries.)
        """
        num_constraints = mm.mmjag_constraint_count(self.handle)
        for i in range(num_constraints, 0, -1):
            constraint_data = mm.mmjag_constraint_type_get(self.handle, i)
            (constraint_type, coord_type, atom1, atom2, atom3, atom4) = \
                                                                constraint_data
            mm.mmjag_constraint_delete(self.handle, coord_type, atom1, atom2,
                                       atom3, atom4, 1)
[docs]    def scanCount(self):
        """
        This function returns the total number of scan coordinates.
        :return: scan count
        :rtype: int
        """
        return mm.mmjag_scan_count(self.handle)
[docs]    def getScanCoordinate(self, i):
        """
        This function returns i-th scan coordinate.
        :return: tuple that contains coordinate type, list of
            atoms, initial and final coordinate values, number of
            steps and step.
        :rtype: tuple
        """
        (coord_type, atom1, atom2, atom3, atom4, initial, final, num_steps,
         step) = mm.mmjag_scan_get(self.handle, i)
        if coord_type == mm.MMJAG_COORD_CART_X or \
            coord_type == mm.MMJAG_COORD_CART_Y or \
            coord_type == mm.MMJAG_COORD_CART_Z:
            atoms = [atom1]
        elif coord_type == mm.MMJAG_COORD_DISTANCE:
            atoms = [atom1, atom2]
        elif coord_type == mm.MMJAG_COORD_ANGLE:
            atoms = [atom1, atom2, atom3]
        elif coord_type == mm.MMJAG_COORD_TORSION:
            atoms = [atom1, atom2, atom3, atom4]
        return (coord_type, atoms, initial, final, num_steps, step)
[docs]    def setScanCoordinate(self, coord_type, atoms, initial_value, final_value,
                          num_steps, step):
        """
        This function defines scan coordinate. If atoms list size is less
        than 4, we add zeros to the list.
        :param coord_type: coordinate type
        :type coord_type: int
        :param atoms: list of atom indices
        :type atoms: list
        :param initial_value: coordinate initial value
        :type initial_value: float
        :param final_value: coordinate final value
        :type final_value: float
        :param num_steps: number of steps
        :type num_steps: int
        :param step: step value
        :type step: float
        """
        # Copy atoms so we don't modify the original list
        atoms = atoms[:]
        while len(atoms) < 4:
            atoms.append(0)
        mm.mmjag_scan_set(self.handle, coord_type, atoms[0], atoms[1], atoms[2],
                          atoms[3], initial_value, final_value, num_steps, step)
[docs]    def constrainAtomXYZ(self, index):
        """
        Constrain the XYZ coordinates of an atom.
        :type index: int
        :param index: The index of the atom to constrain
        """
        unused = 0
        # The line below calls a method that is general for setting XYZ, bond,
        # angle & torsion constraints, therefore it takes 4 atoms numbers.  For
        # XYZ constraints, we only need specify 1 atom.  The other atom numbers
        # are unused.
        mm.mmjag_general_constraint_set(self.handle, mm.MMJAG_COORD_CART_XYZ,
                                        index, unused, unused, unused,
                                        mm.MMJAG_FIXED_CONSTRAINT)
[docs]    def constrainInternal(self, atom1, atom2, atom3=0, atom4=0):
        """
        Constrain an internal coordinate (bond, angle or torsion)
        :type atom1: int
        :param atom1: The index of the first atom in the internal coordinate
            definition
        :type atom2: int
        :param atom2: The index of the second atom in the internal coordinate
            definition
        :type atom3: int
        :param atom3: The index of the third atom in the internal coordinate
            definition (0 if this coordinate is a bond)
        :type atom4: int
        :param atom4: The index of the fourth atom in the internal coordinate
            definition (0 if this coordinate is a bond or angle)
        """
        if atom4:
            ctype = mm.MMJAG_COORD_TORSION
        elif atom3:
            ctype = mm.MMJAG_COORD_ANGLE
        else:
            ctype = mm.MMJAG_COORD_DISTANCE
        # The line below calls a method that is general for setting XYZ, bond,
        # angle & torsion constraints, therefore it takes 4 atoms numbers.
        # Any excess atom numbers are unused.
        mm.mmjag_general_constraint_set(self.handle, ctype, atom1, atom2, atom3,
                                        atom4, mm.MMJAG_FIXED_CONSTRAINT)
[docs]    def constraintCount(self):
        """
        This function returns the total number of constraints.
        :return: constraint count
        :rtype: int
        """
        return mm.mmjag_constraint_count(self.handle)
[docs]    def constraints(self):
        """
        Generator function that yields constraints instead of
        returning a list.
        """
        nmax = self.constraintCount()
        for i in range(1, nmax + 1):
            yield self.getConstraint(i)
[docs]    def getConstraint(self, i):
        """
        This function returns i-th constraint.
        :return: tuple that contains coordinate type, list of atoms and
            target value (may be None if constraint is not dynamic). If
            constraint type is MMJAG_SCAN_CONSTRAINT return None, so that
            we scan coordinates don't appear as constraints.
        :rtype: tuple or None
        :raises: if constraint index is out of range, a MmException is raised
                 if the atom list is empty, a ConstraintError is raised
        """
        constraint_data = mm.mmjag_constraint_type_get(self.handle, i)
        (constraint_type, coord_type, atom1, atom2, atom3, atom4) = \
                                                                constraint_data
        if constraint_type == mm.MMJAG_SCAN_CONSTRAINT:
            return None
        # The API for mm.mmjag_constraint_type_get() guarantees that atom
        # numbers are 0 if not used.
        atoms = [x for x in (atom1, atom2, atom3, atom4) if x != 0]
        value = None
        if constraint_type == mm.MMJAG_TARGET_CONSTRAINT:
            value = mm.mmjag_target_constraint_get(self.handle, i)
        elif constraint_type == mm.MMJAG_HARMONIC_CONSTRAINT:
            k, a, c, has_c = mm.mmjag_harmonic_constraint_get(self.handle, i)
            value = (k, a, c)
        if not atoms:
            msg = f'No atoms defined for constraint {i}.'
            raise ConstraintError(msg)
        return (coord_type, atoms, value)
[docs]    def setConstraint(self, coordinate_type, atoms, value=None):
        """
        This function defines static, dynamic, and natural torsion constraints.
        If atoms list size is less than 4, we add zeros to the list. If the
        coordinate_type is a natural torsion, a ConstraintError is raised if
        the number of atoms supplied is not 2.
        Otherwise, if value is not None, we set this constraint as 'dynamic'.
        :param coordinate_type: coordinate type
        :type coordinate_type: int
        :param atoms: list of atom indices
        :type atoms: list
        :param value: target value (for dynamic constraints only)
        :type value: float
        """
        if len(atoms
              ) != 2 and coordinate_type == mm.MMJAG_COORD_NATURAL_TORSION:
            raise ConstraintError(
                "Incorrect number of atoms specified for natural torsion constraint"
            )
        while len(atoms) < 4:
            atoms.append(0)
        if coordinate_type == mm.MMJAG_COORD_NATURAL_TORSION:
            mm.mmjag_general_constraint_set(self.handle, coordinate_type,
                                            atoms[0], atoms[1], atoms[2],
                                            atoms[3],
                                            mm.MMJAG_NATURAL_TORSION_CONSTRAINT)
        else:
            if value is None:
                mm.mmjag_general_constraint_set(self.handle, coordinate_type,
                                                atoms[0], atoms[1], atoms[2],
                                                atoms[3],
                                                mm.MMJAG_FIXED_CONSTRAINT)
            else:
                mm.mmjag_target_constraint_set(self.handle, coordinate_type,
                                               atoms[0], atoms[1], atoms[2],
                                               atoms[3], value)
[docs]    def setMonomialConstraint(self,
                              coordinate_type,
                              atoms,
                              fc,
                              width,
                              center=None):
        """
        Defines a monomial (e.g. harmonic) constraint.
        If atoms list size is less than 4, we add zeros to the list.
        :param coordinate_type: coordinate type
        :type coordinate_type: int
        :param atoms: list of atom indices
        :type atoms: list
        :param fc: force constant for potential
        :type fc: float
        :param width: half-width for flat bottom of potential
        :type width: float
        :param center: center for internal coordinate constraints. If None, Jaguar
                    will not use the center
        :type center: float or None
        """
        while len(atoms) < 4:
            atoms.append(0)
        has_c = center is not None
        if not has_c:
            center = 0
        mm.mmjag_harmonic_constraint_set(self.handle, coordinate_type, atoms[0],
                                         atoms[1], atoms[2], atoms[3], fc,
                                         width, center, has_c)
[docs]    def setActiveCoord(self, coordinate_type, atoms):
        """
        This function defines an active coordinate.
        If atoms list size is less than 4,
        we add zeros to the list.
        :param coordinate_type: coordinate type
        :type coordinate_type: int
        :param atoms: list of atom indices
        :type atoms: list
        """
        while len(atoms) < 4:
            atoms.append(0)
        mm.mmjag_general_constraint_set(self.handle, coordinate_type, atoms[0],
                                        atoms[1], atoms[2], atoms[3],
                                        mm.MMJAG_ACTIVE_COORD)
[docs]    def anyConstraintsActive(self):
        """
        Check if any coordinate constraints are active
        :return: Are any coordinate constraints active
        :rtype: bool
        """
        active_indices = self.getConstraintIndicesByType(mm.MMJAG_ACTIVE_COORD)
        return bool(active_indices)
[docs]    def allConstraintsActive(self):
        """
        Check if all coordinate constraints are active
        :return: True if all coordinate constraints are active otherwise
            returns False. Will return False if there are no constraints set.
        :rtype: bool
        """
        if not self.constraintCount():
            return False
        active_indices = self.getConstraintIndicesByType(mm.MMJAG_ACTIVE_COORD)
        return len(active_indices) == self.constraintCount()
[docs]    def getConstraintIndicesByType(self, reference_type):
        """
        Returns a list of constraint indices (1 based indexing) that are of
        the given constraint type
        :param reference_type: Check for constraints with this type
        :type reference_type: mm.MMjag_constraint_type
        :return: A list of indices with the given type, 1 indexed
        :rtype: list(int)
        """
        num_constraints = self.constraintCount()
        indices = []
        for idx in range(1, num_constraints + 1):
            constraint_data = mm.mmjag_constraint_type_get(self.handle, idx)
            constraint_type = constraint_data[0]
            if constraint_type == reference_type:
                indices.append(idx)
        return indices
[docs]    def setAtomicBasis(self, atom_num, basis):
        """
        Set a per-atom basis set
        :param atom_num: The atom number to set the basis set for
        :type atom_num: int
        :param basis: The basis set
        :type basis: str
        """
        try:
            mm.mmjag_atomic_char_set(self.handle, -1, mm.MMJAG_ATOMIC_BASIS,
                                     mm.MMJAG_JAGUAR_JOB, atom_num, basis)
        except mm.MmException as err:
            if "Invaild atom number" in str(err):
                raise ValueError("Invalid atom number")
            else:
                raise
[docs]    def getAtomicBasis(self, atom_num):
        """
        Get the per-atom basis set for the specified atom
        :param atom_num: The atom index to get the basis set for
        :type atom_num: int
        :return: The basis set, or None if no basis set has been set for this
            atom
        :rtype: str or NoneType
        """
        try:
            basis = mm.mmjag_atomic_char_get(self.handle, -1,
                                             mm.MMJAG_ATOMIC_BASIS,
                                             mm.MMJAG_JAGUAR_JOB, atom_num)
        except mm.MmException as err:
            if "Invaild atom number" in str(err):
                raise ValueError("Invalid atom number")
            else:
                raise
        if not basis:
            basis = None
        return basis
[docs]    def getAllAtomicBases(self):
        """
        Get all per-atom basis sets
        :return: A dictionary of {atom index: basis set}.  Atoms which do not
            have a per-atom basis set are not included in this dictionary.
        :rtype: dict
        """
        struc = self.getStructure()
        bases = {}
        for atom_num in range(1, struc.atom_total + 1):
            cur_basis = self.getAtomicBasis(atom_num)
            if cur_basis is not None:
                bases[atom_num] = cur_basis
        return bases
[docs]    def clearAtomicBases(self):
        """
        Clear all per-atom basis sets
        """
        struc = self.getStructure()
        for atom_num in range(1, struc.atom_total + 1):
            mm.mmjag_atomic_char_set(self.handle, -1, mm.MMJAG_ATOMIC_BASIS,
                                     mm.MMJAG_JAGUAR_JOB, atom_num, "")
[docs]    def getChargeConstraints(self):
        """
        Parse CDFT input file section to get charge constraints.
        Assume &cdft section takes the form:
        &cdft
        net-charge
        weight1 first-atom1 last-atom1
        weight2 first-atom2 last-atom2
        net-charge
        weight3 first-atom3 last-atom3
        weight4 first-atom4 last-atom4
        &
        :rtype constraints: list
        :return constraints: list of CDFT constraints in the form
                [ (charge1, weights1), (charge2, weights2), ...]
            where:
                charge: The target charge value for the specified atoms
                weights: A dictionary of {atom index: weight}
        Return empty list if keyword icdft!=1 (i.e. not CDFT done)
        :raise InvalidCDFTError if &cdft section invalid or not found
                when icdft keyword is 1
        """
        # Get &cdft section from mmjag handle
        txt = ''
        if self.getValue(mm.MMJAG_IKEY_ICDFT) == mm.MMJAG_ICDFT_ON:
            try:
                txt = mm.mmjag_get_sect_text(self.handle, 'cdft')
            except mm.MmException as err:
                raise InvalidCDFTError(str(err))
            lines = txt.strip().split('\n')
        else:
            return []
        # Strip off &cdft and &
        if lines[0].strip() != '&cdft':
            raise InvalidCDFTError('Error: invalid &cdft section')
        if lines[-1].strip() != '&':
            raise InvalidCDFTError('Error: invalid &cdft section')
        del lines[0]
        del lines[-1]
        # Parse charges and weights
        charge = None
        constraints = []
        weights = {}
        for line in lines:
            if not line.strip():
                # Ignore empty lines
                continue
            try:
                tokens = line.split()
                if len(tokens) == 1:
                    # Get total charge
                    if charge is not None:
                        constraints.append((charge, weights))
                    charge = float(tokens[0])
                    weights = {}
                elif len(tokens) == 3:
                    # Get weights and atom indices
                    for idx in range(int(tokens[1]), int(tokens[2]) + 1):
                        weights[idx] = float(tokens[0])
                else:
                    msg = 'Cannot parse &cdft section line %s' % line
                    raise InvalidCDFTError(msg)
            except ValueError:
                msg = 'Cannot parse &cdft section line %s' % line
                raise InvalidCDFTError(msg)
        if charge is not None:
            constraints.append((charge, weights))
        return constraints
[docs]    def appendChargeConstraints(self, charge, weights):
        """
        Set charge constraints for CDFT.
        Append to existing constraints if previously set.
        :param charge: The target charge value for the specified atoms
        :type charge: float
        :param weights: A dictionary of {atom index: weight}
        :type weights: dict
        """
        # Append new constraint to previous constraints
        constraints = self.getChargeConstraints()
        constraints.append((charge, weights))
        # Update mmjag handle
        self.setChargeConstraints(constraints)
[docs]    def setChargeConstraints(self, constraints):
        """
        Set charge constraints for CDFT.
        Overwrite existing constraints if previously set.
        :param contraints: List of CDFT constraints. Each item of the list
            should be a (charge, weights) tuple, where weights is a dictionary with
            atom index as key and weight as value.
        :type constraints: list
        """
        # Create new &cdft text section
        txt = '&cdft\n'
        for chg, wts in constraints:
            txt += '%.6f\n' % chg
            sorted_weights = sorted(wts.items())
            for atom_num, cur_weight in sorted_weights:
                txt += '%.6f %i %i\n' % (cur_weight, atom_num, atom_num)
        txt += '&\n'
        # Update mmjag handle
        self.setValue(mm.MMJAG_IKEY_ICDFT, mm.MMJAG_ICDFT_ON)
        mm.mmjag_sect_append_wrapper(self.handle, txt)
[docs]    def clearChargeConstraints(self):
        """
        Clear all CDFT charge constraints
        """
        self.deleteKey(mm.MMJAG_IKEY_ICDFT)
        mm.mmjag_sect_check_and_delete(self.handle, 'cdft')
[docs]    def isEquivalentInput(self, other, thresh=INPUT_VERIF_THRESH):
        """
        Checks whether two JaguarInput instances are describing essentially the same job.
        The comparison checks:
        1) that the non-default Jaguar keywords are the same
        2) that the number of atoms are the same
        3) that the structures can be superposed on each other to within a given threshold
        4) that all atoms identities (elements and isotopes) are the same.
        NB: this means that renumberings of the same structure will be parsed as non-matching.
        If any of the tests fail, an InputVerificationError is raised (with a message indicating
        the failed test), otherwise True is returned.
        """
        def dict_diff(a, b):
            return dict(set(a.items()) - set(b.items()))
        self_kwds = self.getNonDefault()
        other_kwds = other.getNonDefault()
        if self_kwds != other_kwds:
            raise InputVerificationError(
                "Inequivalent inputs with differing non-default keys:\n"
                f"in self but not other: {dict_diff(self_kwds, other_kwds)}\n"
                f"in other but not self: {dict_diff(other_kwds, self_kwds)}\n")
        ref_st = self.getStructure()
        trial_st = other.getStructure()
        if ref_st.atom_total != trial_st.atom_total:
            raise InputVerificationError(
                "Inputs not equivalent: there are differing numbers of atoms in the structures"
            )
        atlist = list(range(1, ref_st.atom_total + 1))
        rms = superimpose(ref_st, atlist, trial_st, atlist)
        if rms > thresh:
            error = "Inputs not equivalent: the new structure deviates too much (%.4f) from the reference" % rms
            raise InputVerificationError(error)
        for ref_at, try_at in zip(ref_st.atom, trial_st.atom):
            if ref_at.atomic_weight != try_at.atomic_weight:
                raise InputVerificationError(
                    "Inputs not equivalent: the atom lists contains different elements/isotopes"
                )
        return True
[docs]class GenOptions(object):
    """
    A class to convert keyword value pairs defined in a single string into a
    data structure, and allow them to be converted back into a string.
    Here are some example strings::
        'igeopt=1 mp2=3'
        'igeopt=1 maxitg=1 iacc=1'
    """
    eq_re = re.compile(r"\s*=\s*")
    OK, PARTIAL, ERROR = list(range(3))
[docs]    def __init__(self, kwdict=None):
        self.kwdict = {}
        self.keys = []
        if kwdict:
            for k, v in kwdict.items():
                self.__setitem__(k, v)
    def __setitem__(self, key, value):
        key = str(key).strip()
        if key not in self.kwdict:
            self.keys.append(key)
        self.kwdict[key] = str(value).strip()
    def __getitem__(self, key):
        return self.kwdict[key]
    @staticmethod
    def _splitString(string):
        compressed = GenOptions.eq_re.sub("=", string)
        kvps = []
        for kvp in compressed.split():
            kvps.append(GenOptions.eq_re.split(kvp))
        return kvps
[docs]    @staticmethod
    def fromString(string):
        """
        Create a GenOptions instance from the provided string.
        """
        opts = GenOptions()
        retval = GenOptions.testString(string, opts)
        if retval == GenOptions.PARTIAL:
            raise ValueError("The provided option string was incomplete.")
        if retval == GenOptions.ERROR:
            raise ValueError("The provided option string was ill-defined.")
        return opts
[docs]    @staticmethod
    def testString(string, gen_options=None):
        """
        Test the state of the provided string. If gen_options is provided,
        set its values based on the keyword value pairs in the string.
        Parameters
        string (str)
            Input string to read for settings.
        gen_options (GenOptions)
            A gen_options instance to modify according to the values in
            'string'.
        """
        if not string:
            return GenOptions.OK
        kvps = GenOptions._splitString(string)
        if len(kvps[-1]) < 2 or not kvps[-1][1]:
            return GenOptions.PARTIAL
        for kvp in kvps:
            if len(kvp) != 2 or not kvp[0]:
                return GenOptions.ERROR
            if gen_options:
                gen_options[kvp[0]] = kvp[1]
        return GenOptions.OK
    def _getKeywordValuePairs(self):
        kvs = []
        for k in self.keys:
            kvs.append("%s=%s" % (k, self.kwdict[k]))
        return kvs
[docs]    def commandLineOptions(self):
        opts = ["-keyword=%s" % (kvp,) for kvp in self._getKeywordValuePairs()]
        return opts