MayaChemTools

   1 #
   2 # File: TorsionAlertsUtil.py
   3 # Author: Manish Sud <msud@san.rr.com>
   4 #
   5 # Copyright (C) 2026 Manish Sud. All rights reserved.
   6 #
   7 # This file is part of MayaChemTools.
   8 #
   9 # MayaChemTools is free software; you can redistribute it and/or modify it under
  10 # the terms of the GNU Lesser General Public License as published by the Free
  11 # Software Foundation; either version 3 of the License, or (at your option) any
  12 # later version.
  13 #
  14 # MayaChemTools is distributed in the hope that it will be useful, but without
  15 # any warranty; without even the implied warranty of merchantability of fitness
  16 # for a particular purpose.  See the GNU Lesser General Public License for more
  17 # details.
  18 #
  19 # You should have received a copy of the GNU Lesser General Public License
  20 # along with MayaChemTools; if not, see <http://www.gnu.org/licenses/> or
  21 # write to the Free Software Foundation Inc., 59 Temple Place, Suite 330,
  22 # Boston, MA, 02111-1307, USA.
  23 #
  24 
  25 import sys
  26 import re
  27 import xml.etree.ElementTree as ET
  28 
  29 from rdkit import Chem
  30 
  31 __all__ = [
  32     "CalculateTorsionAngleDifference",
  33     "DoesSMARTSContainsMappedAtoms",
  34     "DoesSMARTSContainValidSubClassMappedAtoms",
  35     "DoesSMARTSContainValidTorsionRuleMappedAtoms",
  36     "FilterSubstructureMatchesByAtomMapNumbers",
  37     "GetAtomPositions",
  38     "GetGenericHierarchyClassElementNode",
  39     "GetHeavyAtomNeighbors",
  40     "IdentifyRotatableBondsForTorsionLibraryMatch",
  41     "IsSpecificHierarchyClass",
  42     "ListTorsionLibraryInfo",
  43     "RemoveLastHierarchyClassElementNodeFromTracking",
  44     "RemoveLastHierarchySubClassElementNodeFromTracking",
  45     "RetrieveTorsionLibraryInfo",
  46     "SetupHierarchyClassAndSubClassNamesForRotatableBond",
  47     "SetupHierarchySubClassElementPatternMol",
  48     "SetupTorsionRuleElementPatternMol",
  49     "SetupTorsionLibraryInfoForMatchingRotatableBonds",
  50     "TrackHierarchyClassElementNode",
  51     "TrackHierarchySubClassElementNode",
  52 ]
  53 
  54 
  55 def RetrieveTorsionLibraryInfo(TorsionLibraryFilePath, Quiet=True):
  56     """Retrieve torsion library information.
  57 
  58     Arguments:
  59         TorsionLibraryFilePath (str):  Torsion library XML file path.
  60 
  61     Returns:
  62         object: An object returned by xml.etree.ElementTree.parse function.
  63 
  64     Notes:
  65         The XML file is parsed using xml.etree.ElementTree.parse function and
  66         object created by the parse function is simply returned.
  67 
  68     """
  69 
  70     if not Quiet:
  71         _PrintInfoMsg("\nRetrieving data from torsion library file %s..." % TorsionLibraryFilePath)
  72 
  73     TorsionLibElementTree = ET.parse(TorsionLibraryFilePath)
  74 
  75     return TorsionLibElementTree
  76 
  77 
  78 def ListTorsionLibraryInfo(TorsionLibElementTree):
  79     """List torsion library information using XML tree object. The following
  80     information is listed:
  81 
  82         Summary:
  83 
  84             Total number of HierarchyClass nodes: <Number>
  85             Total number of HierarchyClassSubClass nodes: <Number
  86             Total number of TorsionRule nodes: <Number
  87 
  88         Details:
  89 
  90             HierarchyClass: <Name>; HierarchySubClass nodes: <Number>;
  91                 TorsionRule nodes: <SMARTS>
  92              ... ... ...
  93 
  94     Arguments:
  95         TorsionLibElementTree (object): XML tree object.
  96 
  97     Returns:
  98         Nothing.
  99 
 100     """
 101     HierarchyClassesInfo = {}
 102     HierarchyClassesInfo["HierarchyClassNames"] = []
 103     HierarchyClassesInfo["HierarchySubClassCount"] = {}
 104     HierarchyClassesInfo["TorsionRuleCount"] = {}
 105 
 106     HierarchyClassCount, HierarchySubClassCount, TorsionRuleCount = [0] * 3
 107 
 108     for HierarchyClassNode in TorsionLibElementTree.findall("hierarchyClass"):
 109         HierarchyClassCount += 1
 110         HierarchyClassName = HierarchyClassNode.get("name")
 111         if HierarchyClassName in HierarchyClassesInfo["HierarchyClassNames"]:
 112             _PrintWarningMsg("Hierarchy class name, %s, already exists..." % HierarchyClassName)
 113         HierarchyClassesInfo["HierarchyClassNames"].append(HierarchyClassName)
 114 
 115         SubClassCount = 0
 116         for HierarchySubClassNode in HierarchyClassNode.iter("hierarchySubClass"):
 117             SubClassCount += 1
 118         HierarchyClassesInfo["HierarchySubClassCount"][HierarchyClassName] = SubClassCount
 119         HierarchySubClassCount += SubClassCount
 120 
 121         RuleCount = 0
 122         for TorsionRuleNode in HierarchyClassNode.iter("torsionRule"):
 123             RuleCount += 1
 124 
 125         HierarchyClassesInfo["TorsionRuleCount"][HierarchyClassName] = RuleCount
 126         TorsionRuleCount += RuleCount
 127 
 128     _PrintInfoMsg("\nTotal number of HierarchyClass nodes: %s" % HierarchyClassCount)
 129     _PrintInfoMsg("Total number of HierarchyClassSubClass nodes: %s" % HierarchySubClassCount)
 130     _PrintInfoMsg("Total number of TorsionRule nodes: %s" % TorsionRuleCount)
 131 
 132     # List info for each hierarchyClass...
 133     _PrintInfoMsg("")
 134 
 135     # Generic class first...
 136     GenericClassName = "GG"
 137     if GenericClassName in HierarchyClassesInfo["HierarchyClassNames"]:
 138         _PrintInfoMsg(
 139             "HierarchyClass: %s; HierarchySubClass nodes: %s; TorsionRule nodes: %s"
 140             % (
 141                 GenericClassName,
 142                 HierarchyClassesInfo["HierarchySubClassCount"][GenericClassName],
 143                 HierarchyClassesInfo["TorsionRuleCount"][GenericClassName],
 144             )
 145         )
 146 
 147     for HierarchyClassName in sorted(HierarchyClassesInfo["HierarchyClassNames"]):
 148         if HierarchyClassName == GenericClassName:
 149             continue
 150         _PrintInfoMsg(
 151             "HierarchyClass: %s; HierarchySubClass nodes: %s; TorsionRule nodes: %s"
 152             % (
 153                 HierarchyClassName,
 154                 HierarchyClassesInfo["HierarchySubClassCount"][HierarchyClassName],
 155                 HierarchyClassesInfo["TorsionRuleCount"][HierarchyClassName],
 156             )
 157         )
 158 
 159 
 160 def SetupTorsionLibraryInfoForMatchingRotatableBonds(TorsionLibraryInfo):
 161     """Setup torsion  library information for matching rotatable bonds. The
 162     following information is initialized and updated in torsion library
 163     dictionary for matching rotatable bonds:
 164 
 165             TorsionLibraryInfo["GenericClass"] = None
 166             TorsionLibraryInfo["GenericClassElementNode"] = None
 167 
 168             TorsionLibraryInfo["SpecificClasses"] = {}
 169             TorsionLibraryInfo["SpecificClasses"]["Names"] = []
 170             TorsionLibraryInfo["SpecificClasses"]["ElementNode"] = {}
 171 
 172             TorsionLibraryInfo["HierarchyClassNodes"] = []
 173             TorsionLibraryInfo["HierarchySubClassNodes"] = []
 174 
 175             TorsionLibraryInfo["DataCache"] = {}
 176             TorsionLibraryInfo["DataCache"]["SubClassPatternMol"] = {}
 177 
 178             TorsionLibraryInfo["DataCache"]["TorsionRulePatternMol"] = {}
 179             TorsionLibraryInfo["DataCache"]["TorsionRuleAnglesInfo"] = {}
 180 
 181     Arguments:
 182         TorsionLibraryInfo (dict): A dictionary containing root node for
 183             torsion library element tree.
 184 
 185     Returns:
 186         Nonthing. The torsion library information dictionary is updated.
 187 
 188     """
 189     _SetupTorsionLibraryHierarchyClassesInfoForMatchingRotatableBonds(TorsionLibraryInfo)
 190     _SetupTorsionLibraryDataCacheInfoForMatchingRotatableBonds(TorsionLibraryInfo)
 191 
 192 
 193 def _SetupTorsionLibraryHierarchyClassesInfoForMatchingRotatableBonds(TorsionLibraryInfo):
 194     """Setup  hierarchy classes information for generic and specific classes."""
 195 
 196     RootElementNode = TorsionLibraryInfo["TorsionLibElementTree"]
 197 
 198     TorsionLibraryInfo["GenericClass"] = None
 199     TorsionLibraryInfo["GenericClassElementNode"] = None
 200 
 201     TorsionLibraryInfo["SpecificClasses"] = {}
 202     TorsionLibraryInfo["SpecificClasses"]["Names"] = []
 203     TorsionLibraryInfo["SpecificClasses"]["ElementNode"] = {}
 204 
 205     # Class name stacks for tracking names during processing of torsion rules..
 206     TorsionLibraryInfo["HierarchyClassNodes"] = []
 207     TorsionLibraryInfo["HierarchySubClassNodes"] = []
 208 
 209     ElementNames = []
 210     for ElementNode in RootElementNode.findall("hierarchyClass"):
 211         ElementName = ElementNode.get("name")
 212         if ElementName in ElementNames:
 213             _PrintWarningMsg("Hierarchy class name, %s, already exists. Ignoring duplicate name..." % ElementName)
 214             continue
 215 
 216         ElementNames.append(ElementName)
 217 
 218         if re.match("^GG$", ElementName, re.I):
 219             TorsionLibraryInfo["GenericClass"] = ElementName
 220             TorsionLibraryInfo["GenericClassElementNode"] = ElementNode
 221         else:
 222             TorsionLibraryInfo["SpecificClasses"]["Names"].append(ElementName)
 223             TorsionLibraryInfo["SpecificClasses"]["ElementNode"][ElementName] = ElementNode
 224 
 225 
 226 def _SetupTorsionLibraryDataCacheInfoForMatchingRotatableBonds(TorsionLibraryInfo):
 227     """Setup information for caching molecules for hierarchy subclass and torsion rule patterns."""
 228 
 229     TorsionLibElementTree = TorsionLibraryInfo["TorsionLibElementTree"]
 230 
 231     # Initialize data cache for pattern molecules corresponding to SMARTS patterns for
 232     # hierarchy subclasses and torsion rules. The pattern mols are generated and cached
 233     # later.
 234     TorsionLibraryInfo["DataCache"] = {}
 235     TorsionLibraryInfo["DataCache"]["SubClassPatternMol"] = {}
 236 
 237     TorsionLibraryInfo["DataCache"]["TorsionRulePatternMol"] = {}
 238     TorsionLibraryInfo["DataCache"]["TorsionRuleLonePairMapNumber"] = {}
 239     TorsionLibraryInfo["DataCache"]["TorsionRuleAnglesInfo"] = {}
 240 
 241     HierarchyClassID, HierarchySubClassID, TorsionRuleID = [0] * 3
 242 
 243     for HierarchyClassNode in TorsionLibElementTree.findall("hierarchyClass"):
 244         HierarchyClassID += 1
 245 
 246         for HierarchySubClassNode in HierarchyClassNode.iter("hierarchySubClass"):
 247             HierarchySubClassID += 1
 248             # Add unique ID to node...
 249             HierarchySubClassNode.set("NodeID", HierarchySubClassID)
 250 
 251         for TorsionRuleNode in HierarchyClassNode.iter("torsionRule"):
 252             TorsionRuleID += 1
 253             # Add unique ID to node...
 254             TorsionRuleNode.set("NodeID", TorsionRuleID)
 255 
 256 
 257 def IdentifyRotatableBondsForTorsionLibraryMatch(TorsionLibraryInfo, Mol, RotBondsPatternMol):
 258     """Identify rotatable bonds in a molecule for torsion library match.
 259 
 260     Arguments:
 261         TorsionLibraryInfo (dict): A dictionary containing information for
 262             matching rotatable bonds.
 263         Mol (object): RDKit molecule object.
 264         RotBondsPatternMol (object): RDKit molecule object for SMARTS pattern
 265             corresponding to rotatable bonds.
 266 
 267     Returns:
 268         bool: True - Rotatable bonds present in molecule; Otherwise, false.
 269         None or dict: None - For no rotatable bonds in molecule; otherwise, a
 270             dictionary containing the following informations for rotatable bonds
 271             matched to RotBondsPatternMol:
 272 
 273                 RotBondsInfo["IDs"] = []
 274                 RotBondsInfo["AtomIndices"] = {}
 275                 RotBondsInfo["HierarchyClass"] = {}
 276 
 277     """
 278 
 279     # Match rotatable bonds...
 280     RotBondsMatches = FilterSubstructureMatchesByAtomMapNumbers(
 281         Mol, RotBondsPatternMol, Mol.GetSubstructMatches(RotBondsPatternMol, useChirality=False)
 282     )
 283 
 284     #  Check and filter rotatable bond matches...
 285     RotBondsMatches = _FilterRotatableBondMatches(Mol, RotBondsMatches)
 286 
 287     if not len(RotBondsMatches):
 288         return False, None
 289 
 290     # Initialize rotatable bonds info...
 291     RotBondsInfo = {}
 292     RotBondsInfo["IDs"] = []
 293     RotBondsInfo["AtomIndices"] = {}
 294     RotBondsInfo["HierarchyClass"] = {}
 295 
 296     # Setup rotatable bonds info...
 297     ID = 0
 298     for RotBondAtomIndices in RotBondsMatches:
 299         ID += 1
 300 
 301         RotBondAtoms = [Mol.GetAtomWithIdx(RotBondAtomIndices[0]), Mol.GetAtomWithIdx(RotBondAtomIndices[1])]
 302         RotBondAtomSymbols = [RotBondAtoms[0].GetSymbol(), RotBondAtoms[1].GetSymbol()]
 303 
 304         ClassID = "%s%s" % (RotBondAtomSymbols[0], RotBondAtomSymbols[1])
 305         if ClassID not in TorsionLibraryInfo["SpecificClasses"]["Names"]:
 306             ReverseClassID = "%s%s" % (RotBondAtomSymbols[1], RotBondAtomSymbols[0])
 307             if ReverseClassID in TorsionLibraryInfo["SpecificClasses"]["Names"]:
 308                 ClassID = ReverseClassID
 309                 # Reverse atom indices and related information...
 310                 RotBondAtomIndices = list(reversed(RotBondAtomIndices))
 311                 RotBondAtoms = list(reversed(RotBondAtoms))
 312                 RotBondAtomSymbols = list(reversed(RotBondAtomSymbols))
 313 
 314         # Track information...
 315         RotBondsInfo["IDs"].append(ID)
 316         RotBondsInfo["AtomIndices"][ID] = RotBondAtomIndices
 317         RotBondsInfo["HierarchyClass"][ID] = ClassID
 318 
 319     return True, RotBondsInfo
 320 
 321 
 322 def FilterSubstructureMatchesByAtomMapNumbers(Mol, PatternMol, AtomIndicesList):
 323     """Filter a list of lists containing matched atom indices by map atom numbers
 324     present in a pattern molecule. The list of atom indices correspond to a list retrieved by
 325     RDKit function GetSubstructureMatches using SMILES/SMARTS pattern. The
 326     atom map numbers are mapped to appropriate atom indices during the generation
 327     of molecules. For example: [O:1]=[S:2](=[O])[C:3][C:4].
 328 
 329     Arguments:
 330         Mol (object): RDKit molecule object.
 331         PatternMol (object): RDKit molecule object for a SMILES/SMARTS pattern.
 332         AtomIndicesList (list): A list of lists containing atom indices.
 333 
 334     Returns:
 335         list : A list of lists containing filtered atom indices.
 336 
 337     """
 338 
 339     AtomMapIndices = _GetAtomMapIndices(PatternMol)
 340 
 341     MatchedAtomIndicesList = []
 342     for AtomIndices in AtomIndicesList:
 343         MatchedAtomIndicesList.append(
 344             _FilterSubstructureMatchByAtomMapNumbers(Mol, PatternMol, AtomIndices, AtomMapIndices)
 345         )
 346 
 347     return MatchedAtomIndicesList
 348 
 349 
 350 def _GetAtomMapIndices(Mol):
 351     """Get a list of available atom indices corresponding to sorted atom map
 352     numbers present in a SMILES/SMARTS pattern used for creating a molecule.
 353     """
 354 
 355     AtomMapIndices, AtomMapNumbers = _GetAtomMapIndicesAndMapNumbers(Mol)
 356 
 357     return AtomMapIndices
 358 
 359 
 360 def _GetAtomMapIndicesAndMapNumbers(Mol):
 361     """Get a list of available atom indices and atom map numbers present
 362     in  a SMILES/SMARTS pattern used for creating a molecule. Both lists
 363     are sorted in ascending order by atom map numbers.
 364     """
 365 
 366     # Setup a atom map number to atom indices map..
 367     AtomMapNumToIndices = {}
 368     for Atom in Mol.GetAtoms():
 369         AtomMapNum = Atom.GetAtomMapNum()
 370 
 371         if AtomMapNum:
 372             AtomMapNumToIndices[AtomMapNum] = Atom.GetIdx()
 373 
 374     # Setup atom indices corresponding to sorted atom map numbers...
 375     AtomMapIndices = None
 376     AtomMapNumbers = None
 377     if len(AtomMapNumToIndices):
 378         AtomMapNumbers = sorted(AtomMapNumToIndices)
 379         AtomMapIndices = [AtomMapNumToIndices[AtomMapNum] for AtomMapNum in AtomMapNumbers]
 380 
 381     return (AtomMapIndices, AtomMapNumbers)
 382 
 383 
 384 def _FilterSubstructureMatchByAtomMapNumbers(Mol, PatternMol, AtomIndices, AtomMapIndices):
 385     """Filter substructure match atom indices by atom map indices corresponding to
 386     atom map numbers.
 387     """
 388 
 389     if AtomMapIndices is None:
 390         return list(AtomIndices)
 391 
 392     return [AtomIndices[Index] for Index in AtomMapIndices]
 393 
 394 
 395 def _FilterRotatableBondMatches(Mol, RotBondsMatches):
 396     """Filter rotatable bond matches to ensure that each rotatable bond atom
 397     is attached to at least two heavy atoms. Otherwise, the torsion rules might match
 398     hydrogens."""
 399 
 400     FilteredRotBondMatches = []
 401 
 402     # Go over rotatable bonds...
 403     for RotBondMatch in RotBondsMatches:
 404         SkipRotBondMatch = False
 405         for AtomIndex in RotBondMatch:
 406             Atom = Mol.GetAtomWithIdx(AtomIndex)
 407 
 408             HeavyAtomNbrCount = len(GetHeavyAtomNeighbors(Atom))
 409             if HeavyAtomNbrCount <= 1:
 410                 SkipRotBondMatch = True
 411                 break
 412 
 413         if not SkipRotBondMatch:
 414             FilteredRotBondMatches.append(RotBondMatch)
 415 
 416     return FilteredRotBondMatches
 417 
 418 
 419 def SetupHierarchySubClassElementPatternMol(TorsionLibraryInfo, ElementNode):
 420     """Setup pattern molecule for SMARTS pattern in hierarchy subclass element.
 421 
 422     Arguments:
 423         TorsionLibraryInfo (dict): A dictionary containing information for
 424             matching rotatable bonds.
 425         ElementNode (object): A hierarchy sub class element node being matched
 426            in torsion library XML tree.
 427 
 428     Returns:
 429         object: RDKit molecule object corresponding to SMARTS pattern for
 430             hierarchy sub class element node.
 431 
 432     """
 433 
 434     # Check data cache...
 435     SubClassNodeID = ElementNode.get("NodeID")
 436     if SubClassNodeID in TorsionLibraryInfo["DataCache"]["SubClassPatternMol"]:
 437         return TorsionLibraryInfo["DataCache"]["SubClassPatternMol"][SubClassNodeID]
 438 
 439     # Setup and track pattern mol...
 440     SubClassSMARTSPattern = ElementNode.get("smarts")
 441     SubClassPatternMol = Chem.MolFromSmarts(SubClassSMARTSPattern)
 442 
 443     if SubClassPatternMol is None:
 444         _PrintWarningMsg(
 445             "Ignoring hierachical subclass, %s, containing invalid SMARTS pattern %s"
 446             % (ElementNode.get("name"), SubClassSMARTSPattern)
 447         )
 448 
 449     if not DoesSMARTSContainValidSubClassMappedAtoms(SubClassSMARTSPattern):
 450         SubClassPatternMol = None
 451         _PrintWarningMsg(
 452             "Ignoring hierachical subclass, %s, containing invalid map atom numbers in SMARTS pattern %s"
 453             % (ElementNode.get("name"), SubClassSMARTSPattern)
 454         )
 455 
 456     TorsionLibraryInfo["DataCache"]["SubClassPatternMol"][SubClassNodeID] = SubClassPatternMol
 457 
 458     return SubClassPatternMol
 459 
 460 
 461 def SetupTorsionRuleElementPatternMol(TorsionLibraryInfo, ElementNode, TorsionRuleNodeID, TorsionSMARTSPattern):
 462     """Setup pattern molecule for SMARTS pattern in torsion rule element.
 463 
 464     Arguments:
 465         TorsionLibraryInfo (dict): A dictionary containing information for
 466             matching rotatable bonds.
 467         ElementNode (object): A torsion rule element node being matched in
 468            torsion library XML tree.
 469         TorsionRuleNodeID (int): Torsion rule element node ID.
 470         TorsionSMARTSPattern (str): SMARTS pattern for torsion rule element node.
 471 
 472     Returns:
 473         object: RDKit molecule object corresponding to SMARTS pattern for
 474             torsion rule element node.
 475 
 476     """
 477 
 478     if TorsionRuleNodeID in TorsionLibraryInfo["DataCache"]["TorsionRulePatternMol"]:
 479         return TorsionLibraryInfo["DataCache"]["TorsionRulePatternMol"][TorsionRuleNodeID]
 480 
 481     TorsionPatternMol = Chem.MolFromSmarts(TorsionSMARTSPattern)
 482     if TorsionPatternMol is None:
 483         _PrintWarningMsg("Ignoring torsion rule element containing invalid SMARTS pattern %s" % TorsionSMARTSPattern)
 484 
 485     if not DoesSMARTSContainValidTorsionRuleMappedAtoms(TorsionSMARTSPattern):
 486         TorsionPatternMol = None
 487         _PrintWarningMsg(
 488             "Ignoring torsion rule element containing invalid map atoms numbers in SMARTS pattern %s"
 489             % TorsionSMARTSPattern
 490         )
 491 
 492     TorsionLibraryInfo["DataCache"]["TorsionRulePatternMol"][TorsionRuleNodeID] = TorsionPatternMol
 493 
 494     return TorsionPatternMol
 495 
 496 
 497 def SetupHierarchyClassAndSubClassNamesForRotatableBond(TorsionLibraryInfo):
 498     """Setup hierarchy class and subclass names for a rotatable bond matched to
 499     a torsion rule element node.
 500 
 501     Returns:
 502         TorsionLibraryInfo (dict): A dictionary containing information for
 503             matching rotatable bonds.
 504 
 505     Returns:
 506         str: A back slash delimited string containing hierarchy class names at
 507             the level of torsion rule element node.
 508         str: A back slash delimited string containing hierarchy sub class names
 509           at the level of torsion rule element node.
 510 
 511     """
 512 
 513     HierarchyClassName, HierarchyClassSubName = ["None"] * 2
 514 
 515     # Setup hierarchy class name...
 516     if len(TorsionLibraryInfo["HierarchyClassNodes"]):
 517         HierarchyClassElementNode = TorsionLibraryInfo["HierarchyClassNodes"][-1]
 518         HierarchyClassName = HierarchyClassElementNode.get("name")
 519         if len(HierarchyClassName) == 0:
 520             HierarchyClassName = "None"
 521 
 522     # Setup hierarchy class name...
 523     if len(TorsionLibraryInfo["HierarchySubClassNodes"]):
 524         HierarchySubClassNames = []
 525         for ElementNode in TorsionLibraryInfo["HierarchySubClassNodes"]:
 526             Name = ElementNode.get("name")
 527             if len(Name) == 0:
 528                 Name = "None"
 529             HierarchySubClassNames.append(Name)
 530 
 531         HierarchyClassSubName = "/".join(HierarchySubClassNames)
 532 
 533     # Replace spaces by underscores in class and subclass names...
 534     if HierarchyClassName is not None:
 535         if " " in HierarchyClassName:
 536             HierarchyClassName = HierarchyClassName.replace(" ", "_")
 537 
 538     if HierarchyClassSubName is not None:
 539         if " " in HierarchyClassSubName:
 540             HierarchyClassSubName = HierarchyClassSubName.replace(" ", "_")
 541 
 542     return (HierarchyClassName, HierarchyClassSubName)
 543 
 544 
 545 def SetupTorsionRuleAnglesInfo(TorsionLibraryInfo, TorsionRuleElementNode):
 546     """Setup torsion angles and energy info for matching a torsion rule.
 547 
 548     Arguments:
 549         TorsionLibraryInfo (dict): A dictionary containing information for
 550             matching rotatable bonds.
 551         TorsionRuleElementNode (object): A torsion rule element node being
 552            matched in torsion library XML tree.
 553 
 554     Returns:
 555         dict: A dictionary containing the following information for torsion rule
 556             being matched to a rotatable bond:
 557 
 558             RuleAnglesInfo = {}
 559 
 560             RuleAnglesInfo["IDs"] = []
 561             RuleAnglesInfo["Value"] = {}
 562             RuleAnglesInfo["Score"] = {}
 563             RuleAnglesInfo["Tolerance1"] = {}
 564             RuleAnglesInfo["Tolerance2"] = {}
 565 
 566             RuleAnglesInfo["ValuesList"] = []
 567             RuleAnglesInfo["ValuesIn360RangeList"] = []
 568             RuleAnglesInfo["Tolerances1List"] = []
 569             RuleAnglesInfo["Tolerances2List"] = []
 570 
 571             # Strain energy calculations...
 572             RuleAnglesInfo["EnergyMethod"] = None
 573             RuleAnglesInfo["EnergyMethodExact"] = None
 574             RuleAnglesInfo["EnergyMethodApproximate"] = None
 575 
 576             # For approximate strain energy calculation...
 577             RuleAnglesInfo["Beta1"] = {}
 578             RuleAnglesInfo["Beta2"] = {}
 579             RuleAnglesInfo["Theta0"] = {}
 580 
 581             # For exact strain energy calculation...
 582             RuleAnglesInfo["HistogramEnergy"] = []
 583             RuleAnglesInfo["HistogramEnergyLowerBound"] = []
 584             RuleAnglesInfo["HistogramEnergyUpperBound"] = []
 585 
 586     """
 587 
 588     # Check data cache...
 589     TorsionRuleNodeID = TorsionRuleElementNode.get("NodeID")
 590     if TorsionRuleNodeID in TorsionLibraryInfo["DataCache"]["TorsionRuleAnglesInfo"]:
 591         return TorsionLibraryInfo["DataCache"]["TorsionRuleAnglesInfo"][TorsionRuleNodeID]
 592 
 593     # Initialize rule angles info...
 594     RuleAnglesInfo = {}
 595 
 596     RuleAnglesInfo["IDs"] = []
 597     RuleAnglesInfo["Value"] = {}
 598     RuleAnglesInfo["Score"] = {}
 599     RuleAnglesInfo["Tolerance1"] = {}
 600     RuleAnglesInfo["Tolerance2"] = {}
 601 
 602     RuleAnglesInfo["ValuesList"] = []
 603     RuleAnglesInfo["ValuesIn360RangeList"] = []
 604     RuleAnglesInfo["Tolerances1List"] = []
 605     RuleAnglesInfo["Tolerances2List"] = []
 606 
 607     # Strain energy calculations...
 608     RuleAnglesInfo["EnergyMethod"] = None
 609     RuleAnglesInfo["EnergyMethodExact"] = None
 610     RuleAnglesInfo["EnergyMethodApproximate"] = None
 611 
 612     # For approximate strain energy calculation....
 613     RuleAnglesInfo["Beta1"] = {}
 614     RuleAnglesInfo["Beta2"] = {}
 615     RuleAnglesInfo["Theta0"] = {}
 616 
 617     # For exact strain energy calculation...
 618     RuleAnglesInfo["HistogramEnergy"] = []
 619     RuleAnglesInfo["HistogramEnergyLowerBound"] = []
 620     RuleAnglesInfo["HistogramEnergyUpperBound"] = []
 621 
 622     # Setup strain energy calculation information...
 623     EnergyMethod, EnergyMethodExact, EnergyMethodApproximate = [None] * 3
 624     EnergyMethod = TorsionRuleElementNode.get("method")
 625     if EnergyMethod is not None:
 626         EnergyMethodExact = True if re.match("^exact$", EnergyMethod, re.I) else False
 627         EnergyMethodApproximate = True if re.match("^approximate$", EnergyMethod, re.I) else False
 628 
 629     RuleAnglesInfo["EnergyMethod"] = EnergyMethod
 630     RuleAnglesInfo["EnergyMethodExact"] = EnergyMethodExact
 631     RuleAnglesInfo["EnergyMethodApproximate"] = EnergyMethodApproximate
 632 
 633     # Setup angles information....
 634     AngleID = 0
 635     for AngleListElementNode in TorsionRuleElementNode.findall("angleList"):
 636         for AngleNode in AngleListElementNode.iter("angle"):
 637             AngleID += 1
 638             Value = float(AngleNode.get("value"))
 639             Tolerance1 = float(AngleNode.get("tolerance1"))
 640             Tolerance2 = float(AngleNode.get("tolerance2"))
 641             Score = float(AngleNode.get("score"))
 642 
 643             # Track values...
 644             RuleAnglesInfo["IDs"].append(AngleID)
 645             RuleAnglesInfo["Value"][AngleID] = Value
 646             RuleAnglesInfo["Score"][AngleID] = Score
 647             RuleAnglesInfo["Tolerance1"][AngleID] = Tolerance1
 648             RuleAnglesInfo["Tolerance2"][AngleID] = Tolerance2
 649 
 650             RuleAnglesInfo["ValuesList"].append(Value)
 651             RuleAnglesInfo["Tolerances1List"].append(Tolerance1)
 652             RuleAnglesInfo["Tolerances2List"].append(Tolerance2)
 653 
 654             # Map value to 0 to 360 range...
 655             MappedValue = Value + 360 if Value < 0 else Value
 656             RuleAnglesInfo["ValuesIn360RangeList"].append(MappedValue)
 657 
 658             # Approximate strain energy calculation information...
 659             if EnergyMethodApproximate:
 660                 Beta1 = float(AngleNode.get("beta_1"))
 661                 Beta2 = float(AngleNode.get("beta_2"))
 662                 Theta0 = float(AngleNode.get("theta_0"))
 663 
 664                 RuleAnglesInfo["Beta1"][AngleID] = Beta1
 665                 RuleAnglesInfo["Beta2"][AngleID] = Beta2
 666                 RuleAnglesInfo["Theta0"][AngleID] = Theta0
 667 
 668     #  Exact energy method  information...
 669     if EnergyMethodExact:
 670         for HistogramBinNode in TorsionRuleElementNode.find("histogram_converted").iter("bin"):
 671             Energy = float(HistogramBinNode.get("energy"))
 672             Lower = float(HistogramBinNode.get("lower"))
 673             Upper = float(HistogramBinNode.get("upper"))
 674 
 675             RuleAnglesInfo["HistogramEnergy"].append(Energy)
 676             RuleAnglesInfo["HistogramEnergyLowerBound"].append(Lower)
 677             RuleAnglesInfo["HistogramEnergyUpperBound"].append(Upper)
 678 
 679     if len(RuleAnglesInfo["IDs"]) == 0:
 680         RuleAnglesInfo = None
 681 
 682     # Cache data...
 683     TorsionLibraryInfo["DataCache"]["TorsionRuleAnglesInfo"][TorsionRuleNodeID] = RuleAnglesInfo
 684 
 685     return RuleAnglesInfo
 686 
 687 
 688 def DoesSMARTSContainValidSubClassMappedAtoms(SMARTS):
 689     """Check for the presence of two central mapped atoms in SMARTS pattern.
 690     A valid SMARTS pattern must contain only two mapped atoms corresponding
 691     to map atom numbers ':2' and ':3'.
 692 
 693     Arguments:
 694         SMARTS (str): SMARTS pattern for sub class in torsion library XML tree.
 695 
 696     Returns:
 697         bool: True - A valid pattern; Otherwise, false.
 698 
 699     """
 700 
 701     MatchedMappedAtoms = re.findall(":[0-9]", SMARTS, re.I)
 702     if len(MatchedMappedAtoms) < 2 or len(MatchedMappedAtoms) > 4:
 703         return False
 704 
 705     # Check for the presence of two central atom map numbers for a torsion...
 706     for MapAtomNum in [":2", ":3"]:
 707         if MapAtomNum not in MatchedMappedAtoms:
 708             return False
 709 
 710     return True
 711 
 712 
 713 def DoesSMARTSContainValidTorsionRuleMappedAtoms(SMARTS):
 714     """Check for the presence of four mapped atoms in a SMARTS pattern.
 715     A valid SMARTS pattern must contain only four mapped atoms corresponding
 716     to map atom numbers ':1', ':2', ':3' and ':4'.
 717 
 718     Arguments:
 719         SMARTS (str): SMARTS pattern for torsion rule in torsion library XML
 720             tree.
 721 
 722     Returns:
 723         bool: True - A valid pattern; Otherwise, false.
 724 
 725     """
 726 
 727     MatchedMappedAtoms = re.findall(":[0-9]", SMARTS, re.I)
 728     if len(MatchedMappedAtoms) != 4:
 729         return False
 730 
 731     # Check for the presence of four atom map numbers for a torsion...
 732     for MapAtomNum in [":1", ":2", ":3", ":4"]:
 733         if MapAtomNum not in MatchedMappedAtoms:
 734             return False
 735 
 736     return True
 737 
 738 
 739 def DoesSMARTSContainsMappedAtoms(SMARTS, MappedAtomNumsList):
 740     """Check for the presence of specified mapped atoms in SMARTS pattern.
 741     The mapped atom numbers in the list are specified as ':1', ':2', ':3' etc.
 742 
 743     Arguments:
 744         SMARTS (str): SMARTS pattern in torsion library XML tree.
 745         MappedAtoms (list): Mapped atom numbers as ":1", ":2" etc.
 746 
 747     Returns:
 748         bool: True - All mapped atoms present in pattern; Otherwise, false.
 749 
 750     """
 751 
 752     MatchedMappedAtoms = re.findall(":[0-9]", SMARTS, re.I)
 753     if len(MatchedMappedAtoms) == 0:
 754         return False
 755 
 756     # Check for the presence of specified mapped atoms in pattern...
 757     for MapAtomNum in MappedAtomNumsList:
 758         if MapAtomNum not in MatchedMappedAtoms:
 759             return False
 760 
 761     return True
 762 
 763 
 764 def IsSpecificHierarchyClass(TorsionLibraryInfo, HierarchyClass):
 765     """Check whether it's a specific hierarchy class.
 766 
 767     Arguments:
 768         TorsionLibraryInfo (dict): A dictionary containing information for
 769             matching rotatable bonds.
 770         HierarchyClass (str): Hierarchy class name.
 771 
 772     Returns:
 773         bool: True - A valid hierarchy class name; Otherwise, false.
 774 
 775     """
 776     return True if HierarchyClass in TorsionLibraryInfo["SpecificClasses"]["ElementNode"] else False
 777 
 778 
 779 def GetGenericHierarchyClassElementNode(TorsionLibraryInfo):
 780     """Get generic hierarchy class element node.
 781 
 782     Arguments:
 783         TorsionLibraryInfo (dict): A dictionary containing information for
 784             matching rotatable bonds.
 785 
 786     Returns:
 787         object: Generic hierarchy class element node in torsion library XML
 788             tree.
 789 
 790     """
 791     return TorsionLibraryInfo["GenericClassElementNode"]
 792 
 793 
 794 def TrackHierarchyClassElementNode(TorsionLibraryInfo, ElementNode):
 795     """Track hierarchy class element node using a stack.
 796 
 797     Arguments:
 798         TorsionLibraryInfo (dict): A dictionary containing information for
 799             matching rotatable bonds.
 800         ElementNode (object): Hierarchy class element node in torsion library
 801             XML tree.
 802 
 803     Returns:
 804         Nothing. The torsion library info is updated.
 805 
 806     """
 807     TorsionLibraryInfo["HierarchyClassNodes"].append(ElementNode)
 808 
 809 
 810 def RemoveLastHierarchyClassElementNodeFromTracking(TorsionLibraryInfo):
 811     """Remove last hierarchy class element node from tracking by removing it
 812     from a stack.
 813 
 814     Arguments:
 815         TorsionLibraryInfo (dict): A dictionary containing information for
 816             matching rotatable bonds.
 817 
 818     Returns:
 819         Nothing. The torsion library info is updated.
 820 
 821     """
 822     TorsionLibraryInfo["HierarchyClassNodes"].pop()
 823 
 824 
 825 def TrackHierarchySubClassElementNode(TorsionLibraryInfo, ElementNode):
 826     """Track hierarchy sub class element node using a stack.
 827 
 828     Arguments:
 829         TorsionLibraryInfo (dict): A dictionary containing information for
 830             matching rotatable bonds.
 831         ElementNode (object): Hierarchy sub class element node in torsion
 832             library XML tree.
 833 
 834     Returns:
 835         Nothing. The torsion library info is updated.
 836 
 837     """
 838     TorsionLibraryInfo["HierarchySubClassNodes"].append(ElementNode)
 839 
 840 
 841 def RemoveLastHierarchySubClassElementNodeFromTracking(TorsionLibraryInfo):
 842     """Remove last hierarchy sub class element node from tracking by removing it
 843     from a stack.
 844 
 845     Arguments:
 846         TorsionLibraryInfo (dict): A dictionary containing information for
 847             matching rotatable bonds.
 848 
 849     Returns:
 850         Nothing. The torsion library info is updated.
 851 
 852     """
 853     TorsionLibraryInfo["HierarchySubClassNodes"].pop()
 854 
 855 
 856 def CalculateTorsionAngleDifference(TorsionAngle1, TorsionAngle2):
 857     """Calculate torsion angle difference in the range from 0 to 180.
 858 
 859     Arguments:
 860         TorsionAngle1 (float): First torsion angle.
 861         TorsionAngle2 (float): Second torsion angle.
 862 
 863     Returns:
 864         float: Difference between first and second torsion angle.
 865 
 866     """
 867 
 868     # Map angles to 0 to 360 range...
 869     if TorsionAngle1 < 0:
 870         TorsionAngle1 = TorsionAngle1 + 360
 871     if TorsionAngle2 < 0:
 872         TorsionAngle2 = TorsionAngle2 + 360
 873 
 874     # Calculate and map angle difference in the range from 0 to 180 range...
 875     TorsionAngleDiff = abs(TorsionAngle1 - TorsionAngle2)
 876     if TorsionAngleDiff > 180.0:
 877         TorsionAngleDiff = abs(TorsionAngleDiff - 360)
 878 
 879     return TorsionAngleDiff
 880 
 881 
 882 def _PrintInfoMsg(Msg=""):
 883     """Print message to stderr along with flushing stderr."""
 884 
 885     print(Msg, sep=" ", end="\n", file=sys.stderr)
 886     sys.stderr.flush()
 887 
 888 
 889 def _PrintWarningMsg(Msg):
 890     """Print message to stderr along with flushing stderr. An `Warning` prefix
 891     is placed before the message."""
 892 
 893     _PrintInfoMsg("Warning: %s" % Msg)
 894 
 895 
 896 def GetHeavyAtomNeighbors(Atom):
 897     """Get a list of heavy atom neighbors.
 898 
 899     Arguments:
 900         Atom (object): RDKit atom object.
 901 
 902     Returns:
 903         list : List of heavy atom neighbors.
 904 
 905     """
 906 
 907     AtomNeighbors = []
 908     for AtomNbr in Atom.GetNeighbors():
 909         if AtomNbr.GetAtomicNum() > 1:
 910             AtomNeighbors.append(AtomNbr)
 911 
 912     return AtomNeighbors
 913 
 914 
 915 def GetAtomPositions(Mol, ConfID=-1):
 916     """Retrieve a list of lists containing coordinates of all atoms in a
 917     molecule.
 918 
 919     Arguments:
 920         Mol (object): RDKit molecule object.
 921         ConfID (int): Conformer number.
 922 
 923     Returns:
 924         list : List of lists containing atom positions.
 925 
 926     Examples:
 927 
 928         for AtomPosition in TorsionAlertsUtil.GetAtomPositions(Mol):
 929             print("X: %s; Y: %s; Z: %s" % (AtomPosition[0], AtomPosition[1], AtomPosition[2]))
 930 
 931     """
 932 
 933     return Mol.GetConformer(id=ConfID).GetPositions().tolist()