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 toString(self): return " ".join(self._getKeywordValuePairs())
[docs] def commandLineOptions(self): opts = ["-keyword=%s" % (kvp,) for kvp in self._getKeywordValuePairs()] return opts
[docs] def isEquivalent(self, other): return self.kwdict == other.kwdict
[docs]class InvalidCDFTError(Exception): # This class intentionally left blank pass