Source code for schrodinger.application.jaguar.workflow_validation

"""
Workflow keywords input validation and specialized Exceptions
"""
# Contributors: Mark A. Watson

import re

import schrodinger.application.jaguar.utils as utils
from schrodinger.application.jaguar.basis import num_functions_all_atoms
from schrodinger.application.jaguar.exceptions import JaguarUnsupportedBasisSet
from schrodinger.application.jaguar.exceptions import JaguarUserFacingException

#------------------------------------------------------------------------------


[docs]class WorkflowKeywordException(JaguarUserFacingException): """ Base exception class for all custom Workflow keyword validation errors """
[docs]class WorkflowConservationError(JaguarUserFacingException): """ Runtime error due to a failure to conserve something """
[docs]class WorkflowKeywordError(WorkflowKeywordException): """ Exception class raised when nonexistant Workflow keyword is requested """
[docs] def __init__(self, keyword, allowed_keywords): """ :type keyword: string :param keyword: input keyword :type allowed_keywords: list :param allowed_keywords: list of allowed keywords """ msg = "'%s' is not a keyword in this workflow." % keyword super().__init__(msg) self.keyword = keyword self.allowed_keywords = allowed_keywords
[docs]class WorkflowKeywordValueTypeError(WorkflowKeywordException): """ Exception class raised when Workflow keyword value has wrong type """
[docs] def __init__(self, keyword, value, valid_type): """ :type keyword: string :param keyword: input keyword :type value: depends on keyword :param value: input value :type valid_type: python type :param valid_type: types as documented in the appropriate `*_keywords.py` file """ msg = "%s of %s is not a valid type for keyword '%s'; expected %s." % \ (value, type(value), keyword, valid_type) super().__init__(msg) self.keyword = keyword self.value = value self.valid_type = valid_type
[docs]class WorkflowKeywordValueError(WorkflowKeywordException): """ Exception class raised when Workflow keyword value is invalid """
[docs] def __init__(self, keyword, value, choices): """ :type keyword: string :param keyword: input keyword :type value: depends on keyword :param value: input value :type choices: list :param choices: valid choices associated with a keyword """ self.keyword = keyword self.value = value self.allowed_values = choices msg = "%s is not an allowed value for keyword '%s'; allowed values are %s" % ( value, keyword, str(choices)) super().__init__(msg)
[docs]class WorkflowKeywordConflictError(WorkflowKeywordException): """ Exception class raised when Workflow keywords have conflicting values """
[docs] def __init__(self, mykey=None, key=None, value=None, msg=None): """ :type mykey: string :param mykey: keyword name :type key: string :param key: required keyword name :type value: any :param value: required keyword value :type msg: str :param msg: generic message to override template """ if msg is not None: super().__init__(msg) else: msg = "'%s' requires '%s' to have value '%s'" % (mykey, key, value) super().__init__(msg) self.keyword = mykey self.required_key = key self.required_value = value
[docs]class WorkflowKeywordFormatError(WorkflowKeywordException): """ Exception class raised when a string not in the keyword=value format is found """
[docs] def __init__(self, token): """ :type token: string :param token: The token that violates the keyword=value format """ msg = ( "Keyword '%s' does not conform to the required keyword=value format." % token.strip()) super().__init__(msg) self.keyword = token
[docs]class ConstraintFormatError(JaguarUserFacingException): """ Exception class raised when a string does not have the correct number of fields for a constraint """
[docs] def __init__(self, token): """ :type token: string :param token: The token that violates the format """ msg = ( "Constraint specification (%s) does not conform to acceptable format\n" % token) super().__init__(msg) self.keyword = token
[docs]class JaguarKeywordConflict(WorkflowKeywordException): """ Exception class raised when a Jaguar keyword is set that we wish to prevent in this workflow """
SCHEMA_ERRORS = { re.compile("expected.*for dictionary value.*"): WorkflowKeywordValueTypeError, re.compile("not a valid value for.*"): WorkflowKeywordValueError, re.compile("invalid list value.*"): WorkflowKeywordValueError }
[docs]def raise_voluptuous_exception(exception, kwd): """ Re-raise voluptuous Exceptions as WorkflowKeywordException's """ exps = [v for k, v in SCHEMA_ERRORS.items() if k.match(str(exception))] if exps: raise exps[0](kwd.name, kwd.value, kwd.valid_type) else: raise exception
def _calculate_num_electrons(structures): """ Return the total number of electrons in structure(s) considering charges. :param structures: structures whose electrons must be added up :type structures: Structure object or iterable of Structure objects :return: the total number of electrons :rtype: int """ if hasattr(structures, "__iter__"): n_elecs = sum([utils.get_number_electrons(st) for st in structures]) charge = sum([utils.get_total_charge(st) for st in structures]) else: n_elecs = utils.get_number_electrons(structures) charge = utils.get_total_charge(structures) return n_elecs - charge
[docs]def estate_is_physical(strs, charge, mult): """ Check whether or not the requested electronic state is plausible. This is done by ensuring the number of electrons is consistent with the requested charge/multiplicity. Raises a WorkflowConservationError :type strs: Structure object or iterable of Structure objects :param strs: the reactants or reactant complex :type charge: int :param charge: overall charge :type mult: int :type mult: overall spin multiplicity """ if hasattr(strs, "__iter__"): n_elecs = sum([utils.get_number_electrons(st) for st in strs]) else: n_elecs = utils.get_number_electrons(strs) # multiplicity - 1 = number of unpaired electrons ok = ((n_elecs - charge) - (mult - 1)) % 2 == 0 if not ok: msg = f"Charge/Multiplicity specification (charge = {charge}, multiplicity = {mult}) is not consistent with the number of electrons ({n_elecs})." raise WorkflowConservationError(msg)
[docs]def charge_is_consistent(strs, charge): """ Tests that the sum of molecular charges is consistent with the total charge. raises WorkflowConservationError if this criterion is not satisfied. :type strs: Structure or iterable of Structure objects :param strs: reactant or product structure(s) to check :type charge: int :param charge: overall charge of reaction """ if hasattr(strs, "__iter__"): q = sum(map(utils.get_total_charge, strs)) else: q = utils.get_total_charge(strs) if q != charge: q_str = str(q) charge_str = str(charge) if q > 0: q_str = '+' + q_str if charge > 0: charge_str = '+' + charge_str msg = f"Total charge specified ({charge_str}) is inconsistent with the charge of molecules ({q_str})" raise WorkflowConservationError(msg)
[docs]def basis_set_is_valid(strs, basis): """ Checks that the given basis set is defined for all atoms in the structures. A JaguarUnsupportedBasisSet is raised if the basis is not supported. :type strs: list :param strs: list of structures to check :type basis: string :param basis: name of basis set """ for st in strs: nbasis, ps_supported, basis_per_atom = num_functions_all_atoms( basis, st) if nbasis == 0: raise JaguarUnsupportedBasisSet( "Chosen basis set (%s) is not supported for all of the atom types in the input structures." % basis)