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