MayaChemTools

    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