1 #!/bin/env python 2 # File: TorsionLibraryAlerts.py 3 # Author: Manish Sud <msud@san.rr.com> 4 # 5 # Collaborator: Pat Walters 6 # 7 # Acknowledgments: Wolfgang Guba, Patrick Penner, and Levi Pierce 8 # 9 # Copyright (C) 2024 Manish Sud. All rights reserved. 10 # 11 # This module uses the Torsion Library jointly developed by the University 12 # of Hamburg, Center for Bioinformatics, Hamburg, Germany and 13 # F. Hoffmann-La-Roche Ltd., Basel, Switzerland. 14 # 15 # This file is part of MayaChemTools. 16 # 17 # MayaChemTools is free software; you can redistribute it and/or modify it under 18 # the terms of the GNU Lesser General Public License as published by the Free 19 # Software Foundation; either version 3 of the License, or (at your option) any 20 # later version. 21 # 22 # MayaChemTools is distributed in the hope that it will be useful, but without 23 # any warranty; without even the implied warranty of merchantability of fitness 24 # for a particular purpose. See the GNU Lesser General Public License for more 25 # details. 26 # 27 # You should have received a copy of the GNU Lesser General Public License 28 # along with MayaChemTools; if not, see <http://www.gnu.org/licenses/> or 29 # write to the Free Software Foundation Inc., 59 Temple Place, Suite 330, 30 # Boston, MA, 02111-1307, USA. 31 # 32 33 import os 34 import re 35 import math 36 import numpy as np 37 38 from rdkit import Chem 39 from rdkit.Chem import rdMolTransforms 40 41 from . import TorsionAlertsUtil 42 43 class TorsionLibraryAlerts(): 44 def __init__(self, AlertsMode = "Red", MinAlertsCount = 1, NitrogenLonePairAllowHydrogenNbrs = True, NitrogenLonePairPlanarityTolerance = 1.0, RotBondsSMARTSMode = "SemiStrict", RotBondsSMARTSPattern = None, TorsionLibraryFilePath = "auto"): 45 """Identify strained molecules from an input file for torsion library [ Ref 146, 152, 159 ] 46 alerts by matching rotatable bonds against SMARTS patterns specified for torsion 47 rules in a torsion library file, The molecules must have 3D coordinates. The default 48 torsion library file, TorsionLibrary.xml, is available in the directory containing this file. 49 50 The data in torsion library file is organized in a hierarchical manner. It consists 51 of one generic class and six specific classes at the highest level. Each class 52 contains multiple subclasses corresponding to named functional groups or 53 substructure patterns. The subclasses consist of torsion rules sorted from 54 specific to generic torsion patterns. The torsion rule, in turn, contains a list 55 of peak values for torsion angles and two tolerance values. A pair of tolerance 56 values define torsion bins around a torsion peak value. For example: 57 58 <library> 59 <hierarchyClass name="GG" id1="G" id2="G"> 60 ... 61 </hierarchyClass> 62 <hierarchyClass name="CO" id1="C" id2="O"> 63 <hierarchySubClass name="Ester bond I" smarts="O=[C:2][O:3]"> 64 <torsionRule smarts="[O:1]=[C:2]!@[O:3]~[CH0:4]"> 65 <angleList> 66 <angle value="0.0" tolerance1="20.00" 67 tolerance2="25.00" score="56.52"/> 68 </angleList> 69 </torsionRule> 70 ... 71 ... 72 ... 73 </hierarchyClass> 74 <hierarchyClass name="NC" id1="N" id2="C"> 75 ... 76 </hierarchyClass> 77 <hierarchyClass name="SN" id1="S" id2="N"> 78 ... 79 </hierarchyClass> 80 <hierarchyClass name="CS" id1="C" id2="S"> 81 ... 82 </hierarchyClass> 83 <hierarchyClass name="CC" id1="C" id2="C"> 84 ... 85 </hierarchyClass> 86 <hierarchyClass name="SS" id1="S" id2="S"> 87 ... 88 </hierarchyClass> 89 </library> 90 91 The rotatable bonds in a 3D molecule are identified using a default SMARTS pattern. 92 A custom SMARTS pattern may be optionally specified to detect rotatable bonds. 93 Each rotatable bond is matched to a torsion rule in the torsion library and 94 assigned one of the following three alert categories: Green, Orange or Red. The 95 rotatable bond is marked Green or Orange for the measured angle of the torsion 96 pattern within the first or second tolerance bins around a torsion peak. 97 Otherwise, it's marked Red implying that the measured angle is not observed in 98 the structure databases employed to generate the torsion library. 99 100 Arguments: 101 AlertsMode(str): Torsion library alert types to use for issuing 102 alerts about molecules. 103 MinAlertsCount(int): Minimum number of alerts allowed in a molecule. 104 NitrogenLonePairAllowHydrogenNbrs (bool): Use hydrogen neighbors 105 attached to nitrogen during the determination of its planarity. 106 NitrogenLonePairPlanarityTolerance (float): Angle tolerance in 107 degrees allowed for nitrogen to be considered coplanar with its 108 three neighbors. 109 RotBondsSMARTSMode (str): SMARTS pattern to use for identifying 110 rotatable bonds in a molecule. Possible values: NonStrict, 111 SemiStrict, Strict or Specify. 112 RotBondsSMARTSPattern (str):SMARTS pattern for identifying rotatable 113 bonds. This paramater is only valid for 'Specify' value 114 'RotBondsSMARTSMode'. 115 TorsionLibraryFilePath (str): A XML file name containing data for 116 torsion library. 117 118 Returns: 119 object: An instantiated class object. 120 121 Notes: 122 The following sections provide additional details for the parameters. 123 124 AlertsMode: Torsion library alert types to use for issuing alerts about 125 molecules containing rotatable bonds marked with Green, Orange, or 126 Red alerts. Possible values: Red or RedAndOrange. 127 128 MinAlertsCount: Minimum number of rotatable bond alerts allowed in a 129 molecule. 130 131 NitrogenLonePairAllowHydrogenNbrs, NitrogenLonePairPlanarityTolerance: 132 These parameters are used during the matching of torsion rules containing 133 'N_lp' in their SMARTS patterns. The 'allowHydrogensNbrs' allows the use 134 hydrogen neighbors attached to nitrogen during the determination of its 135 planarity. The 'planarityTolerance' in degrees represents the tolerance 136 allowed for nitrogen to be considered coplanar with its three neighbors. 137 138 The torsion rules containing 'N_lp' in their SMARTS patterns are categorized 139 into the following two types of rules: 140 141 TypeOne: 142 143 [CX4:1][CX4H2:2]!@[NX3;"N_lp":3][CX4:4] 144 [C:1][CX4H2:2]!@[NX3;"N_lp":3][C:4] 145 ... ... ... 146 147 TypeTwo: 148 149 [!#1:1][CX4:2]!@[NX3;"N_lp":3] 150 [C:1][$(S(=O)=O):2]!@["N_lp":3] 151 ... ... ... 152 153 The torsions are matched to torsion rules containing 'N_lp' using specified 154 SMARTS patterns without the 'N_lp' along with additional constraints using 155 the following methodology: 156 157 TypeOne: 158 159 . SMARTS pattern must contain four mapped atoms and the third 160 mapped atom must be a nitrogen matched with 'NX3:3' 161 . Nitrogen atom must have 3 neighbors. The 'allowHydrogens' 162 parameter controls inclusion of hydrogens as its neighbors. 163 . Nitrogen atom and its 3 neighbors must be coplanar. 164 'planarityTolerance' parameter provides tolerance in degrees 165 for nitrogen to be considered coplanar with its 3 neighbors. 166 167 TypeTwo: 168 169 . SMARTS pattern must contain three mapped atoms and the third 170 mapped atom must be a nitrogen matched with 'NX3:3'. The 171 third mapped atom may contain only 'N_lp:3' The missing 'NX3' 172 is automatically detected. 173 . Nitrogen atom must have 3 neighbors. 'allowHydrogens' 174 parameter controls inclusion of hydrogens as neighbors. 175 . Nitrogen atom and its 3 neighbors must not be coplanar. 176 'planarityTolerance' parameter provides tolerance in degrees 177 for nitrogen to be considered coplanar with its 3 neighbors. 178 . Nitrogen lone pair position equivalent to VSEPR theory is 179 determined based on the position of nitrogen and its neighbors. 180 A vector normal to 3 nitrogen neighbors is calculated and added 181 to the coordinates of nitrogen atom to determine the approximate 182 position of the lone pair. It is used as the fourth position to 183 calculate the torsion angle. 184 185 RotBondsSMARTSMode: SMARTS pattern to use for identifying rotatable bonds in 186 a molecule for matching against torsion rules in the torsion library. Possible 187 values: NonStrict, SemiStrict, Strict or Specify. The rotatable bond SMARTS 188 matches are filtered to ensure that each atom in the rotatable bond is attached 189 to at least two heavy atoms. 190 191 The following SMARTS patterns are used to identify rotatable bonds for 192 different modes: 193 194 NonStrict: [!$(*#*)&!D1]-&!@[!$(*#*)&!D1] 195 196 SemiStrict: 197 [!$(*#*)&!D1&!$(C(F)(F)F)&!$(C(Cl)(Cl)Cl)&!$(C(Br)(Br)Br) 198 &!$(C([CH3])([CH3])[CH3])]-!@[!$(*#*)&!D1&!$(C(F)(F)F) 199 &!$(C(Cl)(Cl)Cl)&!$(C(Br)(Br)Br)&!$(C([CH3])([CH3])[CH3])] 200 201 Strict: 202 [!$(*#*)&!D1&!$(C(F)(F)F)&!$(C(Cl)(Cl)Cl)&!$(C(Br)(Br)Br) 203 &!$(C([CH3])([CH3])[CH3])&!$([CD3](=[N,O,S])-!@[#7,O,S!D1]) 204 &!$([#7,O,S!D1]-!@[CD3]=[N,O,S])&!$([CD3](=[N+])-!@[#7!D1]) 205 &!$([#7!D1]-!@[CD3]=[N+])]-!@[!$(*#*)&!D1&!$(C(F)(F)F) 206 &!$(C(Cl)(Cl)Cl)&!$(C(Br)(Br)Br)&!$(C([CH3])([CH3])[CH3])] 207 208 The 'NonStrict' and 'Strict' SMARTS patterns are available in RDKit. The 209 'NonStrict' SMARTS pattern corresponds to original Daylight SMARTS 210 specification for rotatable bonds. The 'SemiStrict' SMARTS pattern is 211 derived from 'Strict' SMARTS pattern. 212 213 TorsionLibraryFilePath: Specify a XML file name containing data for 214 torsion library hierarchy or use default file, TorsionLibrary.xml, available in 215 the directory containing this file. 216 217 """ 218 219 self._ProcessTorsionLibraryAlertsParameters(AlertsMode, MinAlertsCount, NitrogenLonePairAllowHydrogenNbrs, NitrogenLonePairPlanarityTolerance, RotBondsSMARTSMode, RotBondsSMARTSPattern, TorsionLibraryFilePath) 220 221 self._InitializeTorsionLibraryAlerts() 222 223 224 def _ProcessTorsionLibraryAlertsParameters(self, AlertsMode, MinAlertsCount, NitrogenLonePairAllowHydrogenNbrs, NitrogenLonePairPlanarityTolerance, RotBondsSMARTSMode, RotBondsSMARTSPattern, TorsionLibraryFilePath): 225 """Process torsion library alerts paramaters. """ 226 227 # Process AltertsMode paramater... 228 self._ProcessAlertsModeParameter(AlertsMode) 229 230 # Process MinAlertsCount... 231 if MinAlertsCount < 1: 232 raise ValueError("The value, %s, specified for AltertsMinCount parameter is not valid. Supported value: >=1" % MinAlertsCount) 233 self.MinAlertsCount = MinAlertsCount 234 235 # Process nitrogen lone pair paramaters... 236 self.NitrogenLonePairAllowHydrogenNbrs = NitrogenLonePairAllowHydrogenNbrs 237 if NitrogenLonePairPlanarityTolerance < 0: 238 raise ValueError("The value, %s, specified for NitrogenLonePairPlanarityTolerance parameter is not valid. Supported value: >= 0" % NitrogenLonePairPlanarityTolerance) 239 self.NitrogenLonePairPlanarityTolerance = NitrogenLonePairPlanarityTolerance 240 241 # Process RotBondsSMARTSMode and RotBondsSMARTSPattern parameters... 242 self._ProcessRotBondsParameters(RotBondsSMARTSMode, RotBondsSMARTSPattern) 243 244 # Process TorsionLibraryFilePath parameter... 245 self._ProcessTorsionLibraryFilePathParameter(TorsionLibraryFilePath) 246 247 248 def _InitializeTorsionLibraryAlerts(self): 249 """Initialize tosion library alerts.""" 250 251 # Retrieve and setup torsion library info for matching rotatable bonds... 252 TorsionLibraryInfo = {} 253 254 TorsionLibElementTree = TorsionAlertsUtil.RetrieveTorsionLibraryInfo(self.TorsionLibraryFilePath) 255 TorsionLibraryInfo["TorsionLibElementTree"] = TorsionLibElementTree 256 257 TorsionAlertsUtil.SetupTorsionLibraryInfoForMatchingRotatableBonds(TorsionLibraryInfo) 258 259 self.TorsionLibraryInfo = TorsionLibraryInfo 260 261 262 def _ProcessAlertsModeParameter(self, AlertsMode): 263 """Process AlertsMode parameter. """ 264 265 SpecifiedAlertsModeList = [] 266 if re.match("^Red$", AlertsMode, re.I): 267 SpecifiedAlertsModeList.append("Red") 268 elif re.match("^RedAndOrange$", AlertsMode, re.I): 269 SpecifiedAlertsModeList.append("Red") 270 SpecifiedAlertsModeList.append("Orange") 271 else: 272 raise ValueError("Invalid value, %s, specified for AlertsMode parameter. Valid values: Red, or RedAndOrange" % (AlertsMode)) 273 274 self.AltersMode = AlertsMode 275 self.SpecifiedAlertsModeList = SpecifiedAlertsModeList 276 277 278 def _ProcessRotBondsParameters(self, RotBondsSMARTSMode, RotBondsSMARTSPattern): 279 """Process RotBondsSMARTSMode and RotBondsSMARTSPattern parameters. """ 280 281 if re.match("^NonStrict$", RotBondsSMARTSMode, re.I): 282 RotBondsSMARTSPattern = "[!$(*#*)&!D1]-&!@[!$(*#*)&!D1]" 283 elif re.match("^SemiStrict$", RotBondsSMARTSMode, re.I): 284 RotBondsSMARTSPattern = "[!$(*#*)&!D1&!$(C(F)(F)F)&!$(C(Cl)(Cl)Cl)&!$(C(Br)(Br)Br)&!$(C([CH3])([CH3])[CH3])]-!@[!$(*#*)&!D1&!$(C(F)(F)F)&!$(C(Cl)(Cl)Cl)&!$(C(Br)(Br)Br)&!$(C([CH3])([CH3])[CH3])]" 285 elif re.match("^Strict$", RotBondsSMARTSMode, re.I): 286 RotBondsSMARTSPattern = "[!$(*#*)&!D1&!$(C(F)(F)F)&!$(C(Cl)(Cl)Cl)&!$(C(Br)(Br)Br)&!$(C([CH3])([CH3])[CH3])&!$([CD3](=[N,O,S])-!@[#7,O,S!D1])&!$([#7,O,S!D1]-!@[CD3]=[N,O,S])&!$([CD3](=[N+])-!@[#7!D1])&!$([#7!D1]-!@[CD3]=[N+])]-!@[!$(*#*)&!D1&!$(C(F)(F)F)&!$(C(Cl)(Cl)Cl)&!$(C(Br)(Br)Br)&!$(C([CH3])([CH3])[CH3])]" 287 elif re.match("^Specify$", RotBondsSMARTSMode, re.I): 288 if RotBondsSMARTSPattern is None: 289 raise ValueError("The RotBondsSMARTSPattern parameter value must be specified during Specify value for RotBondsSMARTSMode parameter.") 290 RotBondsSMARTSPattern = RotBondsSMARTSPattern.strip() 291 if not len(RotBondsSMARTSPattern): 292 raise ValueError("Empty value specified for RotBondsSMARTSPattern parameter.") 293 else: 294 raise ValueError("Invalid value, %s, specified for RotBondsSMARTSMode parameter. Valid values: NonStrict, SemiStrict, Strict or Specify" % (RotBondsSMARTSMode)) 295 296 RotBondsPatternMol = Chem.MolFromSmarts(RotBondsSMARTSPattern) 297 if RotBondsPatternMol is None: 298 if re.match("Specify", RotBondsSMARTSMode, re.I): 299 raise ValueError("Failed to create rotatable bonds pattern molecule. The rotatable bonds SMARTS pattern, \"%s\", specified using RotBondsSMARTSPattern parameter is not valid." % (RotBondsSMARTSPattern)) 300 else: 301 raise ValueError("Failed to create rotatable bonds pattern molecule. The default rotatable bonds SMARTS pattern, \"%s\", used for %s value of RotBondsSMARTSMode parameter is not valid." % (RotBondsSMARTSPattern, RotBondsSMARTSMode)) 302 303 self.RotBondsSMARTSMode = RotBondsSMARTSMode 304 self.RotBondsSMARTSPattern = RotBondsSMARTSPattern 305 self.RotBondsPatternMol = RotBondsPatternMol 306 307 308 def _ProcessTorsionLibraryFilePathParameter(self, TorsionLibraryFilePath): 309 """ Process torsion library path parameter. """ 310 311 # TorsionLibraryFilePath parameter... 312 if re.match("^auto$", TorsionLibraryFilePath): 313 TorsionLibraryFile = "TorsionLibrary.xml" 314 TorsionLibraryFilePath = os.path.join(os.path.dirname(os.path.abspath(__file__)), TorsionLibraryFile) 315 if not os.path.isfile(TorsionLibraryFilePath): 316 raise ValueError("The default torsion alerts library file %s doesn't exist." % TorsionLibraryFilePath) 317 else: 318 if not os.path.isfile(TorsionLibraryFilePath): 319 raise ValueError("The file specified, %s, for parameter TorsionLibraryFilePath doesn't exist." % TorsionLibraryFilePath) 320 TorsionLibraryFilePath = os.path.abspath(TorsionLibraryFilePath) 321 322 self.TorsionLibraryFilePath = TorsionLibraryFilePath 323 324 325 def GetTorsionLibraryFilePath(self): 326 """Get torsion strain library file path. 327 328 Arguments: 329 Nothing. 330 331 Returns: 332 FilePath (str): Torsion strain library path. 333 334 """ 335 336 return self.TorsionLibraryFilePath 337 338 339 def ListTorsionLibraryInfo(self): 340 """List torsion strain library information. 341 342 Arguments: 343 Nothing. 344 345 Returns: 346 Nothing. The torsion library information is printed. 347 348 """ 349 350 return TorsionAlertsUtil.ListTorsionLibraryInfo(self.TorsionLibraryInfo["TorsionLibElementTree"]) 351 352 353 def IdentifyTorsionLibraryAlertsForRotatableBonds(self, Mol): 354 """Identify torsion library alerts for a molecule by matching rotatable bonds 355 against SMARTS patterns specified for torsion rules in torsion energy library 356 file. 357 358 Arguments: 359 Mol (object): RDKit molecule object. 360 361 Returns: 362 bool: True - Molecule contains strained torsions; False - Molecule 363 contains no strained torsions or rotatable bonds. 364 dict or None: Torsion alerts information regarding matching of 365 rotatable bonds to torsion strain library. 366 367 Examples: 368 369 from TorsionAlerts.TorsionLibraryAlerts import TorsionLibraryAlerts 370 371 LibraryAlerts = TorsionLibraryAlerts() 372 AlertsStatus, AlertsInfo = LibraryAlerts. 373 IdentifyTorsionLibraryAlertsForRotatableBonds(RDKitMol) 374 375 # List of rotatable bond IDs... 376 RotatableBondIDs = AlertsInfo["IDs"] 377 378 # Dictionaries containing information for rotatable bonds by using 379 # bond ID as key... 380 for ID in AlertsInfo["IDs"]: 381 MatchStatus = AlertsInfo["MatchStatus"][ID] 382 AlertTypes = AlertsInfo["AlertTypes"][ID] 383 AtomIndices = AlertsInfo["AtomIndices"][ID] 384 TorsionAtomIndices = AlertsInfo["TorsionAtomIndices"][ID] 385 TorsionAngles = AlertsInfo["TorsionAngles"][ID] 386 TorsionAngleViolations = AlertsInfo["TorsionAngleViolations"][ID] 387 HierarchyClassNames = AlertsInfo["HierarchyClassNames"][ID] 388 HierarchySubClassNames = AlertsInfo["HierarchySubClassNames"][ID] 389 TorsionRuleNodeID = AlertsInfo["TorsionRuleNodeID"][ID] 390 TorsionRulePeaks = AlertsInfo["TorsionRulePeaks"][ID] 391 TorsionRuleTolerances1 = AlertsInfo["TorsionRuleTolerances1"][ID] 392 TorsionRuleTolerances2 = AlertsInfo["TorsionRuleTolerances2"][ID] 393 TorsionRuleSMARTS = AlertsInfo["TorsionRuleSMARTS"][ID] 394 Count = AlertsInfo["Count"][ID] 395 396 """ 397 398 # Identify rotatable bonds... 399 RotBondsStatus, RotBondsInfo = TorsionAlertsUtil.IdentifyRotatableBondsForTorsionLibraryMatch(self.TorsionLibraryInfo, Mol, self.RotBondsPatternMol) 400 if not RotBondsStatus: 401 return (False, None) 402 403 # Identify alerts for rotatable bonds... 404 RotBondsAlertsStatus, RotBondsAlertsInfo = self._MatchRotatableBondsToTorsionLibrary(Mol, RotBondsInfo) 405 406 return (RotBondsAlertsStatus, RotBondsAlertsInfo) 407 408 409 def _MatchRotatableBondsToTorsionLibrary(self, Mol, RotBondsInfo): 410 """Match rotatable bond to torsion library.""" 411 412 # Initialize... 413 RotBondsAlertsInfo = self._InitializeRotatableBondsAlertsInfo() 414 415 # Match rotatable bonds to torsion library... 416 for ID in RotBondsInfo["IDs"]: 417 AtomIndices = RotBondsInfo["AtomIndices"][ID] 418 HierarchyClass = RotBondsInfo["HierarchyClass"][ID] 419 420 MatchStatus, MatchInfo = self._MatchRotatableBondToTorsionLibrary(Mol, AtomIndices, HierarchyClass) 421 422 if MatchInfo is None or len(MatchInfo) == 0: 423 AlertType, TorsionAtomIndices, TorsionAngle, TorsionAngleViolation, HierarchyClassName, HierarchySubClassName, TorsionRuleNodeID, TorsionRulePeaks, TorsionRuleTolerances1, TorsionRuleTolerances2, TorsionRuleSMARTS = [None] * 11 424 else: 425 AlertType, TorsionAtomIndices, TorsionAngle, TorsionAngleViolation, HierarchyClassName, HierarchySubClassName, TorsionRuleNodeID, TorsionRulePeaks, TorsionRuleTolerances1, TorsionRuleTolerances2, TorsionRuleSMARTS = MatchInfo 426 427 # Track alerts information... 428 RotBondsAlertsInfo["IDs"].append(ID) 429 RotBondsAlertsInfo["MatchStatus"][ID] = MatchStatus 430 RotBondsAlertsInfo["AlertTypes"][ID] = AlertType 431 RotBondsAlertsInfo["AtomIndices"][ID] = AtomIndices 432 RotBondsAlertsInfo["TorsionAtomIndices"][ID] = TorsionAtomIndices 433 RotBondsAlertsInfo["TorsionAngles"][ID] = TorsionAngle 434 RotBondsAlertsInfo["TorsionAngleViolations"][ID] = TorsionAngleViolation 435 RotBondsAlertsInfo["HierarchyClassNames"][ID] = HierarchyClassName 436 RotBondsAlertsInfo["HierarchySubClassNames"][ID] = HierarchySubClassName 437 RotBondsAlertsInfo["TorsionRuleNodeID"][ID] = TorsionRuleNodeID 438 RotBondsAlertsInfo["TorsionRulePeaks"][ID] = TorsionRulePeaks 439 RotBondsAlertsInfo["TorsionRuleTolerances1"][ID] = TorsionRuleTolerances1 440 RotBondsAlertsInfo["TorsionRuleTolerances2"][ID] = TorsionRuleTolerances2 441 RotBondsAlertsInfo["TorsionRuleSMARTS"][ID] = TorsionRuleSMARTS 442 443 # Count alert types... 444 if AlertType is not None: 445 if AlertType not in RotBondsAlertsInfo["Count"]: 446 RotBondsAlertsInfo["Count"][AlertType] = 0 447 RotBondsAlertsInfo["Count"][AlertType] += 1 448 449 # Setup alert status for rotatable bonds... 450 RotBondsAlertsStatus = False 451 AlertsCount = 0 452 for ID in RotBondsInfo["IDs"]: 453 if RotBondsAlertsInfo["AlertTypes"][ID] in self.SpecifiedAlertsModeList: 454 AlertsCount += 1 455 if AlertsCount >= self.MinAlertsCount: 456 RotBondsAlertsStatus = True 457 break 458 459 return (RotBondsAlertsStatus, RotBondsAlertsInfo) 460 461 462 def _InitializeRotatableBondsAlertsInfo(self, ): 463 """Initialize alerts information for rotatable bonds.""" 464 465 RotBondsAlertsInfo = {} 466 RotBondsAlertsInfo["IDs"] = [] 467 468 for DataLabel in ["MatchStatus", "AlertTypes", "AtomIndices", "TorsionAtomIndices", "TorsionAngles", "TorsionAngleViolations", "HierarchyClassNames", "HierarchySubClassNames", "TorsionRuleNodeID", "TorsionRulePeaks", "TorsionRuleTolerances1", "TorsionRuleTolerances2", "TorsionRuleSMARTS", "Count"]: 469 RotBondsAlertsInfo[DataLabel] = {} 470 471 return RotBondsAlertsInfo 472 473 474 def _MatchRotatableBondToTorsionLibrary(self, Mol, RotBondAtomIndices, RotBondHierarchyClass): 475 """Match rotatable bond to torsion library.""" 476 477 if TorsionAlertsUtil.IsSpecificHierarchyClass(self.TorsionLibraryInfo, RotBondHierarchyClass): 478 MatchStatus, MatchInfo = self._MatchRotatableBondAgainstSpecificHierarchyClass(Mol, RotBondAtomIndices, RotBondHierarchyClass) 479 if not MatchStatus: 480 MatchStatus, MatchInfo = self._MatchRotatableBondAgainstGenericHierarchyClass(Mol, RotBondAtomIndices, RotBondHierarchyClass) 481 else: 482 MatchStatus, MatchInfo = self._MatchRotatableBondAgainstGenericHierarchyClass(Mol, RotBondAtomIndices, RotBondHierarchyClass) 483 484 return (MatchStatus, MatchInfo) 485 486 487 def _MatchRotatableBondAgainstSpecificHierarchyClass(self, Mol, RotBondAtomIndices, RotBondHierarchyClass): 488 """Match rotatable bond against a specific hierarchy class.""" 489 490 TorsionLibraryInfo = self.TorsionLibraryInfo 491 492 HierarchyClassElementNode = None 493 if RotBondHierarchyClass in TorsionLibraryInfo["SpecificClasses"]["ElementNode"]: 494 HierarchyClassElementNode = TorsionLibraryInfo["SpecificClasses"]["ElementNode"][RotBondHierarchyClass] 495 496 if HierarchyClassElementNode is None: 497 return (False, None, None, None) 498 499 TorsionAlertsUtil.TrackHierarchyClassElementNode(TorsionLibraryInfo, HierarchyClassElementNode) 500 MatchStatus, MatchInfo = self._ProcessElementForRotatableBondMatch(Mol, RotBondAtomIndices, HierarchyClassElementNode) 501 TorsionAlertsUtil.RemoveLastHierarchyClassElementNodeFromTracking(TorsionLibraryInfo) 502 503 return (MatchStatus, MatchInfo) 504 505 506 def _MatchRotatableBondAgainstGenericHierarchyClass(self, Mol, RotBondAtomIndices, RotBondHierarchyClass): 507 """Match rotatable bond against a generic hierarchy class.""" 508 509 TorsionLibraryInfo = self.TorsionLibraryInfo 510 511 HierarchyClassElementNode = TorsionAlertsUtil.GetGenericHierarchyClassElementNode(TorsionLibraryInfo) 512 if HierarchyClassElementNode is None: 513 return (False, None) 514 515 TorsionAlertsUtil.TrackHierarchyClassElementNode(TorsionLibraryInfo, HierarchyClassElementNode) 516 517 # Match hierarchy subclasses before matching torsion rules... 518 MatchStatus, MatchInfo = self._MatchRotatableBondAgainstGenericHierarchySubClasses(Mol, RotBondAtomIndices, HierarchyClassElementNode) 519 520 if not MatchStatus: 521 MatchStatus, MatchInfo = self._MatchRotatableBondAgainstGenericHierarchyTorsionRules(Mol, RotBondAtomIndices, HierarchyClassElementNode) 522 523 TorsionAlertsUtil.RemoveLastHierarchyClassElementNodeFromTracking(TorsionLibraryInfo) 524 525 return (MatchStatus, MatchInfo) 526 527 528 def _MatchRotatableBondAgainstGenericHierarchySubClasses(self, Mol, RotBondAtomIndices, HierarchyClassElementNode): 529 """Match rotatable bond againat generic hierarchy subclasses.""" 530 531 for ElementChildNode in HierarchyClassElementNode: 532 if ElementChildNode.tag != "hierarchySubClass": 533 continue 534 535 SubClassMatchStatus = self._ProcessHierarchySubClassElementForRotatableBondMatch(Mol, RotBondAtomIndices, ElementChildNode) 536 537 if SubClassMatchStatus: 538 MatchStatus, MatchInfo = self._ProcessElementForRotatableBondMatch(Mol, RotBondAtomIndices, ElementChildNode) 539 540 if MatchStatus: 541 return (MatchStatus, MatchInfo) 542 543 return(False, None) 544 545 546 def _MatchRotatableBondAgainstGenericHierarchyTorsionRules(self, Mol, RotBondAtomIndices, HierarchyClassElementNode): 547 """Match rotatable bond againat torsion rules generic hierarchy class.""" 548 549 for ElementChildNode in HierarchyClassElementNode: 550 if ElementChildNode.tag != "torsionRule": 551 continue 552 553 MatchStatus, MatchInfo = self._ProcessTorsionRuleElementForRotatableBondMatch(Mol, RotBondAtomIndices, ElementChildNode) 554 555 if MatchStatus: 556 return (MatchStatus, MatchInfo) 557 558 return(False, None) 559 560 561 def _ProcessElementForRotatableBondMatch(self, Mol, RotBondAtomIndices, ElementNode): 562 """Process element node to recursively match rotatable bond against hierarchy 563 subclasses and torsion rules.""" 564 565 TorsionLibraryInfo = self.TorsionLibraryInfo 566 567 for ElementChildNode in ElementNode: 568 if ElementChildNode.tag == "hierarchySubClass": 569 SubClassMatchStatus = self._ProcessHierarchySubClassElementForRotatableBondMatch(Mol, RotBondAtomIndices, ElementChildNode) 570 571 if SubClassMatchStatus: 572 TorsionAlertsUtil.TrackHierarchySubClassElementNode(TorsionLibraryInfo, ElementChildNode) 573 574 MatchStatus, MatchInfo = self._ProcessElementForRotatableBondMatch(Mol, RotBondAtomIndices, ElementChildNode) 575 if MatchStatus: 576 TorsionAlertsUtil.RemoveLastHierarchySubClassElementNodeFromTracking(TorsionLibraryInfo) 577 return (MatchStatus, MatchInfo) 578 579 TorsionAlertsUtil.RemoveLastHierarchySubClassElementNodeFromTracking(TorsionLibraryInfo) 580 581 elif ElementChildNode.tag == "torsionRule": 582 MatchStatus, MatchInfo = self._ProcessTorsionRuleElementForRotatableBondMatch(Mol, RotBondAtomIndices, ElementChildNode) 583 584 if MatchStatus: 585 return (MatchStatus, MatchInfo) 586 587 return (False, None) 588 589 590 def _ProcessHierarchySubClassElementForRotatableBondMatch(self, Mol, RotBondAtomIndices, ElementNode): 591 """Process hierarchy subclass element to match rotatable bond.""" 592 593 # Setup subclass SMARTS pattern mol... 594 SubClassPatternMol = TorsionAlertsUtil.SetupHierarchySubClassElementPatternMol(self.TorsionLibraryInfo, ElementNode) 595 if SubClassPatternMol is None: 596 return False 597 598 # Match SMARTS pattern... 599 SubClassPatternMatches = TorsionAlertsUtil.FilterSubstructureMatchesByAtomMapNumbers(Mol, SubClassPatternMol, Mol.GetSubstructMatches(SubClassPatternMol, useChirality = False)) 600 if len(SubClassPatternMatches) == 0: 601 return False 602 603 # Match rotatable bond indices... 604 RotBondAtomIndex1, RotBondAtomIndex2 = RotBondAtomIndices 605 MatchStatus = False 606 for SubClassPatternMatch in SubClassPatternMatches: 607 if len(SubClassPatternMatch) == 2: 608 # Matched to pattern containing map atom numbers ":2" and ":3"... 609 CentralAtomsIndex1, CentralAtomsIndex2 = SubClassPatternMatch 610 elif len(SubClassPatternMatch) == 4: 611 # Matched to pattern containing map atom numbers ":1", ":2", ":3" and ":4"... 612 CentralAtomsIndex1 = SubClassPatternMatch[1] 613 CentralAtomsIndex2 = SubClassPatternMatch[2] 614 elif len(SubClassPatternMatch) == 3: 615 SubClassSMARTSPattern = ElementNode.get("smarts") 616 if TorsionAlertsUtil.DoesSMARTSContainsMappedAtoms(SubClassSMARTSPattern, [":2", ":3", ":4"]): 617 # Matched to pattern containing map atom numbers ":2", ":3" and ":4"... 618 CentralAtomsIndex1 = SubClassPatternMatch[0] 619 CentralAtomsIndex2 = SubClassPatternMatch[1] 620 else: 621 # Matched to pattern containing map atom numbers ":1", ":2" and ":3"... 622 CentralAtomsIndex1 = SubClassPatternMatch[1] 623 CentralAtomsIndex2 = SubClassPatternMatch[2] 624 else: 625 continue 626 627 if CentralAtomsIndex1 != CentralAtomsIndex2: 628 if ((CentralAtomsIndex1 == RotBondAtomIndex1 and CentralAtomsIndex2 == RotBondAtomIndex2) or (CentralAtomsIndex1 == RotBondAtomIndex2 and CentralAtomsIndex2 == RotBondAtomIndex1)): 629 MatchStatus = True 630 break 631 632 return (MatchStatus) 633 634 635 def _ProcessTorsionRuleElementForRotatableBondMatch(self, Mol, RotBondAtomIndices, ElementNode): 636 """Process torsion rule element to match rotatable bond.""" 637 638 TorsionLibraryInfo = self.TorsionLibraryInfo 639 640 # Retrieve torsion matched to rotatable bond... 641 TorsionAtomIndices, TorsionAngle = self._MatchTorsionRuleToRotatableBond(Mol, RotBondAtomIndices, ElementNode) 642 if TorsionAtomIndices is None: 643 return (False, None) 644 645 # Setup torsion angles info for matched torsion rule... 646 TorsionAnglesInfo = TorsionAlertsUtil.SetupTorsionRuleAnglesInfo(TorsionLibraryInfo, ElementNode) 647 if TorsionAnglesInfo is None: 648 return (False, None) 649 650 # Setup torsion alert type and angle violation... 651 AlertType, TorsionAngleViolation = self._SetupTorsionAlertTypeForRotatableBond(TorsionAnglesInfo, TorsionAngle) 652 653 # Setup hierarchy class and subclass names... 654 HierarchyClassName, HierarchySubClassName = TorsionAlertsUtil.SetupHierarchyClassAndSubClassNamesForRotatableBond(TorsionLibraryInfo) 655 656 # Setup rule node ID... 657 TorsionRuleNodeID = ElementNode.get("NodeID") 658 659 # Setup SMARTS... 660 TorsionRuleSMARTS = ElementNode.get("smarts") 661 if " " in TorsionRuleSMARTS: 662 TorsionRuleSMARTS = TorsionRuleSMARTS.replace(" ", "") 663 664 # Setup torsion peaks and tolerances... 665 TorsionRulePeaks = TorsionAnglesInfo["ValuesList"] 666 TorsionRuleTolerances1 = TorsionAnglesInfo["Tolerances1List"] 667 TorsionRuleTolerances2 = TorsionAnglesInfo["Tolerances2List"] 668 669 MatchInfo = [AlertType, TorsionAtomIndices, TorsionAngle, TorsionAngleViolation, HierarchyClassName, HierarchySubClassName, TorsionRuleNodeID, TorsionRulePeaks, TorsionRuleTolerances1, TorsionRuleTolerances2, TorsionRuleSMARTS] 670 671 # Setup match status... 672 MatchStatus = True 673 674 return (MatchStatus, MatchInfo) 675 676 677 def _MatchTorsionRuleToRotatableBond(self, Mol, RotBondAtomIndices, ElementNode): 678 """Retrieve matched torsion for torsion rule matched to rotatable bond.""" 679 680 # Get torsion matches... 681 TorsionMatches = self._GetMatchesForTorsionRule(Mol, ElementNode) 682 if TorsionMatches is None or len(TorsionMatches) == 0: 683 return (None, None) 684 685 # Identify the first torsion match corresponding to central atoms in RotBondAtomIndices... 686 RotBondAtomIndex1, RotBondAtomIndex2 = RotBondAtomIndices 687 for TorsionMatch in TorsionMatches: 688 CentralAtomIndex1 = TorsionMatch[1] 689 CentralAtomIndex2 = TorsionMatch[2] 690 691 if ((CentralAtomIndex1 == RotBondAtomIndex1 and CentralAtomIndex2 == RotBondAtomIndex2) or (CentralAtomIndex1 == RotBondAtomIndex2 and CentralAtomIndex2 == RotBondAtomIndex1)): 692 TorsionAngle = self._CalculateTorsionAngle(Mol, TorsionMatch) 693 694 return (TorsionMatch, TorsionAngle) 695 696 return (None, None) 697 698 699 def _CalculateTorsionAngle(self, Mol, TorsionMatch): 700 """Calculate torsion angle.""" 701 702 if type(TorsionMatch[3]) is list: 703 return self._CalculateTorsionAngleUsingNitrogenLonePairPosition(Mol, TorsionMatch) 704 705 # Calculate torsion angle using torsion atom indices.. 706 MolConf = Mol.GetConformer(0) 707 TorsionAngle = rdMolTransforms.GetDihedralDeg(MolConf, TorsionMatch[0], TorsionMatch[1], TorsionMatch[2], TorsionMatch[3]) 708 TorsionAngle = round(TorsionAngle, 2) 709 710 return TorsionAngle 711 712 713 def _CalculateTorsionAngleUsingNitrogenLonePairPosition(self, Mol, TorsionMatch): 714 """Calculate torsion angle using nitrogen lone pair positon.""" 715 716 # Setup a carbon atom as position holder for lone pair position... 717 TmpMol = Chem.RWMol(Mol) 718 LonePairAtomIndex = TmpMol.AddAtom(Chem.Atom(6)) 719 720 TmpMolConf = TmpMol.GetConformer(0) 721 TmpMolConf.SetAtomPosition(LonePairAtomIndex, TorsionMatch[3]) 722 723 TorsionAngle = rdMolTransforms.GetDihedralDeg(TmpMolConf, TorsionMatch[0], TorsionMatch[1], TorsionMatch[2], LonePairAtomIndex) 724 TorsionAngle = round(TorsionAngle, 2) 725 726 return TorsionAngle 727 728 729 def _GetMatchesForTorsionRule(self, Mol, ElementNode): 730 """Get matches for torsion rule.""" 731 732 # Match torsions... 733 TorsionMatches = None 734 if self._IsNitogenLonePairTorsionRule(ElementNode): 735 TorsionMatches = self._GetSubstructureMatchesForNitrogenLonePairTorsionRule(Mol, ElementNode) 736 else: 737 TorsionMatches = self._GetSubstructureMatchesForTorsionRule(Mol, ElementNode) 738 739 if TorsionMatches is None or len(TorsionMatches) == 0: 740 return TorsionMatches 741 742 # Filter torsion matches... 743 FiltertedTorsionMatches = [] 744 for TorsionMatch in TorsionMatches: 745 if len(TorsionMatch) != 4: 746 continue 747 748 # Ignore matches containing hydrogen atoms as first or last atom... 749 if Mol.GetAtomWithIdx(TorsionMatch[0]).GetAtomicNum() == 1: 750 continue 751 if type(TorsionMatch[3]) is int: 752 # May contains a list for type two nitrogen lone pair match... 753 if Mol.GetAtomWithIdx(TorsionMatch[3]).GetAtomicNum() == 1: 754 continue 755 FiltertedTorsionMatches.append(TorsionMatch) 756 757 return FiltertedTorsionMatches 758 759 760 def _GetSubstructureMatchesForTorsionRule(self, Mol, ElementNode): 761 """Get substructure matches for a torsion rule.""" 762 763 # Setup torsion rule SMARTS pattern mol.... 764 TorsionRuleNodeID = ElementNode.get("NodeID") 765 TorsionSMARTSPattern = ElementNode.get("smarts") 766 TorsionPatternMol = TorsionAlertsUtil.SetupTorsionRuleElementPatternMol(self.TorsionLibraryInfo, ElementNode, TorsionRuleNodeID, TorsionSMARTSPattern) 767 if TorsionPatternMol is None: 768 return None 769 770 # Match torsions... 771 TorsionMatches = TorsionAlertsUtil.FilterSubstructureMatchesByAtomMapNumbers(Mol, TorsionPatternMol, Mol.GetSubstructMatches(TorsionPatternMol, useChirality = False)) 772 773 return TorsionMatches 774 775 776 def _GetSubstructureMatchesForNitrogenLonePairTorsionRule(self, Mol, ElementNode): 777 """Get substructure matches for a torsion rule containing N_lp.""" 778 779 if self._IsTypeOneNitogenLonePairTorsionRule(ElementNode): 780 return self._GetSubstructureMatchesForTypeOneNitrogenLonePairTorsionRule(Mol, ElementNode) 781 elif self._IsTypeTwoNitogenLonePairTorsionRule(ElementNode): 782 return self._GetSubstructureMatchesForTypeTwoNitrogenLonePairTorsionRule(Mol, ElementNode) 783 784 return None 785 786 787 def _GetSubstructureMatchesForTypeOneNitrogenLonePairTorsionRule(self, Mol, ElementNode): 788 """Get substructure matches for a torsion rule containing N_lp and four mapped atoms.""" 789 790 # For example: 791 # [CX4:1][CX4H2:2]!@[NX3;"N_lp":3][CX4:4] 792 # [C:1][CX4H2:2]!@[NX3;"N_lp":3][C:4] 793 # ... ... ... 794 795 TorsionRuleNodeID = ElementNode.get("NodeID") 796 TorsionPatternMol, LonePairMapNumber = self._SetupNitrogenLonePairTorsionRuleElementInfo(ElementNode, TorsionRuleNodeID) 797 798 if TorsionPatternMol is None: 799 return None 800 801 if LonePairMapNumber is None: 802 return None 803 804 # Match torsions... 805 TorsionMatches = TorsionAlertsUtil.FilterSubstructureMatchesByAtomMapNumbers(Mol, TorsionPatternMol, Mol.GetSubstructMatches(TorsionPatternMol, useChirality = False)) 806 807 # Filter matches... 808 FiltertedTorsionMatches = [] 809 for TorsionMatch in TorsionMatches: 810 if len(TorsionMatch) != 4: 811 continue 812 813 # Check for Nitogen atom at LonePairMapNumber... 814 LonePairNitrogenAtom = Mol.GetAtomWithIdx(TorsionMatch[LonePairMapNumber - 1]) 815 if LonePairNitrogenAtom.GetSymbol() != "N": 816 continue 817 818 # Make sure LonePairNitrogenAtom is planar... 819 # test 820 PlanarityStatus = self._IsLonePairNitrogenAtomPlanar(Mol, LonePairNitrogenAtom) 821 if PlanarityStatus is None: 822 continue 823 824 if not PlanarityStatus: 825 continue 826 827 FiltertedTorsionMatches.append(TorsionMatch) 828 829 return FiltertedTorsionMatches 830 831 832 def _GetSubstructureMatchesForTypeTwoNitrogenLonePairTorsionRule(self, Mol, ElementNode): 833 """Get substructure matches for a torsion rule containing N_lp and three mapped atoms.""" 834 835 # For example: 836 # [!#1:1][CX4:2]!@[NX3;"N_lp":3] 837 # [!#1:1][$(S(=O)=O):2]!@["N_lp":3]@[Cr3] 838 # [C:1][$(S(=O)=O):2]!@["N_lp":3] 839 # [c:1][$(S(=O)=O):2]!@["N_lp":3] 840 # [!#1:1][$(S(=O)=O):2]!@["N_lp":3] 841 # ... ... ... 842 843 TorsionRuleNodeID = ElementNode.get("NodeID") 844 TorsionPatternMol, LonePairMapNumber = self._SetupNitrogenLonePairTorsionRuleElementInfo(ElementNode, TorsionRuleNodeID) 845 846 if TorsionPatternMol is None: 847 return None 848 849 if not self._IsValidTypeTwoNitrogenLonePairMapNumber(LonePairMapNumber): 850 return None 851 852 # Match torsions... 853 TorsionMatches = TorsionAlertsUtil.FilterSubstructureMatchesByAtomMapNumbers(Mol, TorsionPatternMol, Mol.GetSubstructMatches(TorsionPatternMol, useChirality = False)) 854 855 # Filter matches... 856 FiltertedTorsionMatches = [] 857 for TorsionMatch in TorsionMatches: 858 if len(TorsionMatch) != 3: 859 continue 860 861 # Check for Nitogen atom at LonePairMapNumber... 862 LonePairNitrogenAtom = Mol.GetAtomWithIdx(TorsionMatch[LonePairMapNumber - 1]) 863 if LonePairNitrogenAtom.GetSymbol() != "N": 864 continue 865 866 # Make sure LonePairNitrogenAtom is not planar... 867 PlanarityStatus = self._IsLonePairNitrogenAtomPlanar(Mol, LonePairNitrogenAtom) 868 869 if PlanarityStatus is None: 870 continue 871 872 if PlanarityStatus: 873 continue 874 875 # Calculate lone pair coordinates for a non-planar nitrogen... 876 LonePairPosition = self._CalculateLonePairCoordinatesForNitrogenAtom(Mol, LonePairNitrogenAtom) 877 if LonePairPosition is None: 878 continue 879 880 # Append lone pair coodinate list to list of torsion match containing atom indices... 881 TorsionMatch.append(LonePairPosition) 882 883 # Track torsion matches... 884 FiltertedTorsionMatches.append(TorsionMatch) 885 886 return FiltertedTorsionMatches 887 888 889 def _SetupNitrogenLonePairTorsionRuleElementInfo(self, ElementNode, TorsionRuleNodeID): 890 """Setup pattern molecule and lone pair map number for type one and type 891 two nitrogen lone pair rules.""" 892 893 TorsionLibraryInfo = self.TorsionLibraryInfo 894 TorsionPatternMol, LonePairMapNumber = [None] * 2 895 896 if TorsionRuleNodeID in TorsionLibraryInfo["DataCache"]["TorsionRulePatternMol"]: 897 TorsionPatternMol = TorsionLibraryInfo["DataCache"]["TorsionRulePatternMol"][TorsionRuleNodeID] 898 LonePairMapNumber = TorsionLibraryInfo["DataCache"]["TorsionRuleLonePairMapNumber"][TorsionRuleNodeID] 899 else: 900 # Setup torsion pattern... 901 TorsionSMARTSPattern = ElementNode.get("smarts") 902 TorsionSMARTSPattern, LonePairMapNumber = self._ProcessSMARTSForNitrogenLonePairTorsionRule(TorsionSMARTSPattern) 903 904 # Setup torsion pattern mol... 905 TorsionPatternMol = Chem.MolFromSmarts(TorsionSMARTSPattern) 906 if TorsionPatternMol is None: 907 print("Warning: Ignoring torsion rule element containing invalid map atoms numbers in SMARTS pattern %s" % TorsionSMARTSPattern) 908 909 # Cache data... 910 TorsionLibraryInfo["DataCache"]["TorsionRulePatternMol"][TorsionRuleNodeID] = TorsionPatternMol 911 TorsionLibraryInfo["DataCache"]["TorsionRuleLonePairMapNumber"][TorsionRuleNodeID] = LonePairMapNumber 912 913 return (TorsionPatternMol, LonePairMapNumber) 914 915 916 def _IsLonePairNitrogenAtomPlanar(self, Mol, NitrogenAtom): 917 """Check for the planarity of nitrogen atom and its three neighbors.""" 918 919 AllowHydrogenNbrs = self.NitrogenLonePairAllowHydrogenNbrs 920 Tolerance = self.NitrogenLonePairPlanarityTolerance 921 922 # Get neighbors... 923 if AllowHydrogenNbrs: 924 AtomNeighbors = NitrogenAtom.GetNeighbors() 925 else: 926 AtomNeighbors = TorsionAlertsUtil.GetHeavyAtomNeighbors(NitrogenAtom) 927 928 if len(AtomNeighbors) != 3: 929 return None 930 931 # Setup atom positions... 932 AtomPositions = [] 933 MolAtomsPositions = TorsionAlertsUtil.GetAtomPositions(Mol) 934 935 # Neighbor positions... 936 for AtomNbr in AtomNeighbors: 937 AtomNbrIndex = AtomNbr.GetIdx() 938 AtomPositions.append(MolAtomsPositions[AtomNbrIndex]) 939 940 # Nitrogen position... 941 NitrogenAtomIndex = NitrogenAtom.GetIdx() 942 AtomPositions.append(MolAtomsPositions[NitrogenAtomIndex]) 943 944 Status = self._AreFourPointsCoplanar(AtomPositions[0], AtomPositions[1], AtomPositions[2], AtomPositions[3], Tolerance) 945 946 return Status 947 948 949 def _AreFourPointsCoplanar(self, Point1, Point2, Point3, Point4, Tolerance = 1.0): 950 """Check whether four points are coplanar with in the threshold of 1 degree.""" 951 952 # Setup normalized direction vectors... 953 VectorP2P1 = self._NormalizeVector(np.subtract(Point2, Point1)) 954 VectorP3P1 = self._NormalizeVector(np.subtract(Point3, Point1)) 955 VectorP1P4 = self._NormalizeVector(np.subtract(Point1, Point4)) 956 957 # Calculate angle between VectorP1P4 and normal to vectors VectorP2P1 and VectorP3P1... 958 PlaneP1P2P3Normal = self._NormalizeVector(np.cross(VectorP2P1, VectorP3P1)) 959 PlanarityAngle = np.arccos(np.clip(np.dot(PlaneP1P2P3Normal, VectorP1P4), -1.0, 1.0)) 960 961 Status = math.isclose(PlanarityAngle, math.radians(90), abs_tol=math.radians(Tolerance)) 962 963 return Status 964 965 966 def _NormalizeVector(self, Vector): 967 """Normalize vector.""" 968 969 Norm = np.linalg.norm(Vector) 970 971 return Vector if math.isclose(Norm, 0.0, abs_tol = 1e-08) else Vector/Norm 972 973 974 def _CalculateLonePairCoordinatesForNitrogenAtom(self, Mol, NitrogenAtom): 975 """Calculate approximate lone pair coordinates for non-plannar nitrogen atom.""" 976 977 AllowHydrogenNbrs = self.NitrogenLonePairAllowHydrogenNbrs 978 979 # Get neighbors... 980 if AllowHydrogenNbrs: 981 AtomNeighbors = NitrogenAtom.GetNeighbors() 982 else: 983 AtomNeighbors = TorsionAlertsUtil.GetHeavyAtomNeighbors(NitrogenAtom) 984 985 if len(AtomNeighbors) != 3: 986 return None 987 988 # Setup positions for nitrogen and its neghbors... 989 MolAtomsPositions = TorsionAlertsUtil.GetAtomPositions(Mol) 990 991 NitrogenPosition = MolAtomsPositions[NitrogenAtom.GetIdx()] 992 NbrPositions = [] 993 for AtomNbr in AtomNeighbors: 994 NbrPositions.append(MolAtomsPositions[AtomNbr.GetIdx()]) 995 Nbr1Position, Nbr2Position, Nbr3Position = NbrPositions 996 997 # Setup normalized direction vectors... 998 VectorP2P1 = self._NormalizeVector(np.subtract(Nbr2Position, Nbr1Position)) 999 VectorP3P1 = self._NormalizeVector(np.subtract(Nbr3Position, Nbr1Position)) 1000 VectorP1P4 = self._NormalizeVector(np.subtract(Nbr1Position, NitrogenPosition)) 1001 1002 # Calculate angle between VectorP1P4 and normal to vectors VectorP2P1 and VectorP3P1... 1003 PlaneP1P2P3Normal = self._NormalizeVector(np.cross(VectorP2P1, VectorP3P1)) 1004 PlanarityAngle = np.arccos(np.clip(np.dot(PlaneP1P2P3Normal, VectorP1P4), -1.0, 1.0)) 1005 1006 # Check for reversing the direction of the normal... 1007 if PlanarityAngle < math.radians(90): 1008 PlaneP1P2P3Normal = PlaneP1P2P3Normal * -1 1009 1010 # Add normal to nitrogen cooridnates for the approximate coordinates of the 1011 # one pair. The exact VSEPR coordinates of the lone pair are not necessary to 1012 # calculate the torsion angle... 1013 LonePairPosition = NitrogenPosition + PlaneP1P2P3Normal 1014 1015 return list(LonePairPosition) 1016 1017 1018 def _ProcessSMARTSForNitrogenLonePairTorsionRule(self, SMARTSPattern): 1019 """Process SMARTS pattern for a torion rule containing N_lp.""" 1020 1021 LonePairMapNumber = self._GetNitrogenLonePairMapNumber(SMARTSPattern) 1022 1023 # Remove double quotes around N_lp.. 1024 SMARTSPattern = re.sub("\"N_lp\"", "N_lp", SMARTSPattern, re.I) 1025 1026 # Remove N_lp specification from SMARTS pattern for torsion rule... 1027 if re.search("\[N_lp", SMARTSPattern, re.I): 1028 # Handle missing NX3... 1029 SMARTSPattern = re.sub("\[N_lp", "[NX3", SMARTSPattern) 1030 else: 1031 SMARTSPattern = re.sub(";N_lp", "", SMARTSPattern) 1032 1033 return (SMARTSPattern, LonePairMapNumber) 1034 1035 1036 def _GetNitrogenLonePairMapNumber(self, SMARTSPattern): 1037 """Get atom map number for nitrogen involved in N_lp.""" 1038 1039 LonePairMapNumber = None 1040 1041 SMARTSPattern = re.sub("\"N_lp\"", "N_lp", SMARTSPattern, re.I) 1042 MatchedMappedAtoms = re.findall("N_lp:[0-9]", SMARTSPattern, re.I) 1043 1044 if len(MatchedMappedAtoms) == 1: 1045 LonePairMapNumber = int(re.sub("N_lp:", "", MatchedMappedAtoms[0])) 1046 1047 return LonePairMapNumber 1048 1049 1050 def _IsNitogenLonePairTorsionRule(self, ElementNode): 1051 """Check for the presence of N_lp in SMARTS pattern for a torsion rule.""" 1052 1053 if "N_lp" not in ElementNode.get("smarts"): 1054 return False 1055 1056 LonePairMatches = re.findall("N_lp", ElementNode.get("smarts"), re.I) 1057 1058 return True if len(LonePairMatches) == 1 else False 1059 1060 1061 def _IsTypeOneNitogenLonePairTorsionRule(self, ElementNode): 1062 """Check for the presence four mapped atoms in a SMARTS pattern containing 1063 N_lp for a torsion rule.""" 1064 1065 # For example: 1066 # [CX4:1][CX4H2:2]!@[NX3;"N_lp":3][CX4:4] 1067 # [C:1][CX4H2:2]!@[NX3;"N_lp":3][C:4] 1068 # ... ... ... 1069 1070 MatchedMappedAtoms = re.findall(":[0-9]", ElementNode.get("smarts"), re.I) 1071 1072 return True if len(MatchedMappedAtoms) == 4 else False 1073 1074 1075 def _IsTypeTwoNitogenLonePairTorsionRule(self, ElementNode): 1076 """Check for the presence three mapped atoms in a SMARTS pattern containing 1077 N_lp for a torsion rule.""" 1078 1079 # For example: 1080 # [!#1:1][CX4:2]!@[NX3;"N_lp":3] 1081 # [!#1:1][$(S(=O)=O):2]!@["N_lp":3]@[Cr3] 1082 # [C:1][$(S(=O)=O):2]!@["N_lp":3] 1083 # [c:1][$(S(=O)=O):2]!@["N_lp":3] 1084 # [!#1:1][$(S(=O)=O):2]!@["N_lp":3] 1085 # 1086 1087 MatchedMappedAtoms = re.findall(":[0-9]", ElementNode.get("smarts"), re.I) 1088 1089 return True if len(MatchedMappedAtoms) == 3 else False 1090 1091 1092 def _IsValidTypeTwoNitogenLonePairTorsionRule(self, ElementNode): 1093 """Validate atom map number for nitrogen involved in N_lp for type two nitrogen 1094 lone pair torsion rule.""" 1095 1096 LonePairMapNumber = self._GetNitrogenLonePairMapNumber(ElementNode.get("smarts")) 1097 1098 return self._IsValidTypeTwoNitrogenLonePairMapNumber(LonePairMapNumber) 1099 1100 1101 def _IsValidTypeTwoNitrogenLonePairMapNumber(self, LonePairMapNumber): 1102 """Check that the atom map number is 3.""" 1103 1104 return True if LonePairMapNumber is not None and LonePairMapNumber == 3 else False 1105 1106 1107 def _SetupTorsionAlertTypeForRotatableBond(self, TorsionAnglesInfo, TorsionAngle): 1108 """Setup torsion alert type and angle violation for a rotatable bond.""" 1109 1110 TorsionCategory, TorsionAngleViolation = [None, None] 1111 1112 for ID in TorsionAnglesInfo["IDs"]: 1113 if self._IsTorsionAngleInWithinTolerance(TorsionAngle, TorsionAnglesInfo["Value"][ID], TorsionAnglesInfo["Tolerance1"][ID]): 1114 TorsionCategory = "Green" 1115 TorsionAngleViolation = 0.0 1116 break 1117 1118 if self._IsTorsionAngleInWithinTolerance(TorsionAngle, TorsionAnglesInfo["Value"][ID], TorsionAnglesInfo["Tolerance2"][ID]): 1119 TorsionCategory = "Orange" 1120 TorsionAngleViolation = self._CalculateTorsionAngleViolation(TorsionAngle, TorsionAnglesInfo["ValuesIn360RangeList"], TorsionAnglesInfo["Tolerances1List"]) 1121 break 1122 1123 if TorsionCategory is None: 1124 TorsionCategory = "Red" 1125 TorsionAngleViolation = self._CalculateTorsionAngleViolation(TorsionAngle, TorsionAnglesInfo["ValuesIn360RangeList"], TorsionAnglesInfo["Tolerances2List"]) 1126 1127 return (TorsionCategory, TorsionAngleViolation) 1128 1129 1130 def _IsTorsionAngleInWithinTolerance(self, TorsionAngle, TorsionPeak, TorsionTolerance): 1131 """Check torsion angle against torsion tolerance.""" 1132 1133 TorsionAngleDiff = TorsionAlertsUtil.CalculateTorsionAngleDifference(TorsionPeak, TorsionAngle) 1134 1135 return True if (abs(TorsionAngleDiff) <= TorsionTolerance) else False 1136 1137 1138 def _CalculateTorsionAngleViolation(self, TorsionAngle, TorsionPeaks, TorsionTolerances): 1139 """Calculate torsion angle violation.""" 1140 1141 TorsionAngleViolation = None 1142 1143 # Map angle to 0 to 360 range. TorsionPeaks values must be in this range... 1144 if TorsionAngle < 0: 1145 TorsionAngle = TorsionAngle + 360 1146 1147 # Identify the closest torsion peak index... 1148 if len(TorsionPeaks) == 1: 1149 NearestPeakIndex = 0 1150 else: 1151 NearestPeakIndex = min(range(len(TorsionPeaks)), key=lambda Index: abs(TorsionPeaks[Index] - TorsionAngle)) 1152 1153 # Calculate torsion angle violation from the nearest peak and its tolerance value... 1154 TorsionAngleDiff = TorsionAlertsUtil.CalculateTorsionAngleDifference(TorsionPeaks[NearestPeakIndex], TorsionAngle) 1155 TorsionAngleViolation = abs(abs(TorsionAngleDiff) - TorsionTolerances[NearestPeakIndex]) 1156 1157 return TorsionAngleViolation