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