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 
  395         """
  396 
  397         # Identify rotatable bonds...
  398         RotBondsStatus, RotBondsInfo = TorsionAlertsUtil.IdentifyRotatableBondsForTorsionLibraryMatch(self.TorsionLibraryInfo, Mol, self.RotBondsPatternMol)
  399         if not RotBondsStatus:
  400             return (False, None)
  401 
  402         # Identify alerts for rotatable bonds...
  403         RotBondsAlertsStatus, RotBondsAlertsInfo = self._MatchRotatableBondsToTorsionLibrary(Mol, RotBondsInfo)
  404 
  405         return (RotBondsAlertsStatus, RotBondsAlertsInfo)
  406 
  407 
  408     def _MatchRotatableBondsToTorsionLibrary(self, Mol, RotBondsInfo):
  409         """Match rotatable bond to torsion library."""
  410 
  411         # Initialize...
  412         RotBondsAlertsInfo = self._InitializeRotatableBondsAlertsInfo()
  413 
  414         # Match rotatable bonds to torsion library...
  415         for ID in RotBondsInfo["IDs"]:
  416             AtomIndices = RotBondsInfo["AtomIndices"][ID]
  417             HierarchyClass = RotBondsInfo["HierarchyClass"][ID]
  418 
  419             MatchStatus, MatchInfo = self._MatchRotatableBondToTorsionLibrary(Mol, AtomIndices, HierarchyClass)
  420 
  421             if MatchInfo is None or len(MatchInfo) == 0:
  422                 AlertType, TorsionAtomIndices, TorsionAngle, TorsionAngleViolation, HierarchyClassName, HierarchySubClassName, TorsionRuleNodeID, TorsionRulePeaks, TorsionRuleTolerances1, TorsionRuleTolerances2, TorsionRuleSMARTS = [None] * 11
  423             else:
  424                 AlertType, TorsionAtomIndices, TorsionAngle, TorsionAngleViolation, HierarchyClassName, HierarchySubClassName, TorsionRuleNodeID, TorsionRulePeaks, TorsionRuleTolerances1, TorsionRuleTolerances2, TorsionRuleSMARTS = MatchInfo
  425 
  426             # Track alerts information...
  427             RotBondsAlertsInfo["IDs"].append(ID)
  428             RotBondsAlertsInfo["MatchStatus"][ID] = MatchStatus
  429             RotBondsAlertsInfo["AlertTypes"][ID] = AlertType
  430             RotBondsAlertsInfo["AtomIndices"][ID] = AtomIndices
  431             RotBondsAlertsInfo["TorsionAtomIndices"][ID] = TorsionAtomIndices
  432             RotBondsAlertsInfo["TorsionAngles"][ID] = TorsionAngle
  433             RotBondsAlertsInfo["TorsionAngleViolations"][ID] = TorsionAngleViolation
  434             RotBondsAlertsInfo["HierarchyClassNames"][ID] = HierarchyClassName
  435             RotBondsAlertsInfo["HierarchySubClassNames"][ID] = HierarchySubClassName
  436             RotBondsAlertsInfo["TorsionRuleNodeID"][ID] = TorsionRuleNodeID
  437             RotBondsAlertsInfo["TorsionRulePeaks"][ID] = TorsionRulePeaks
  438             RotBondsAlertsInfo["TorsionRuleTolerances1"][ID] = TorsionRuleTolerances1
  439             RotBondsAlertsInfo["TorsionRuleTolerances2"][ID] = TorsionRuleTolerances2
  440             RotBondsAlertsInfo["TorsionRuleSMARTS"][ID] = TorsionRuleSMARTS
  441 
  442             #  Count alert types...
  443             if AlertType is not None:
  444                 if AlertType not in RotBondsAlertsInfo["Count"]:
  445                     RotBondsAlertsInfo["Count"][AlertType] = 0
  446                 RotBondsAlertsInfo["Count"][AlertType] += 1
  447 
  448         # Setup alert status for rotatable bonds...
  449         RotBondsAlertsStatus = False
  450         AlertsCount = 0
  451         for ID in RotBondsInfo["IDs"]:
  452             if RotBondsAlertsInfo["AlertTypes"][ID] in self.SpecifiedAlertsModeList:
  453                 AlertsCount += 1
  454                 if AlertsCount >= self.MinAlertsCount:
  455                     RotBondsAlertsStatus = True
  456                     break
  457 
  458         return (RotBondsAlertsStatus, RotBondsAlertsInfo)
  459 
  460 
  461     def _InitializeRotatableBondsAlertsInfo(self, ):
  462         """Initialize alerts information for rotatable bonds."""
  463 
  464         RotBondsAlertsInfo = {}
  465         RotBondsAlertsInfo["IDs"] = []
  466 
  467         for DataLabel in ["MatchStatus", "AlertTypes", "AtomIndices", "TorsionAtomIndices", "TorsionAngles", "TorsionAngleViolations", "HierarchyClassNames", "HierarchySubClassNames", "TorsionRuleNodeID", "TorsionRulePeaks", "TorsionRuleTolerances1", "TorsionRuleTolerances2", "TorsionRuleSMARTS", "Count"]:
  468             RotBondsAlertsInfo[DataLabel] = {}
  469 
  470         return RotBondsAlertsInfo
  471 
  472 
  473     def _MatchRotatableBondToTorsionLibrary(self, Mol, RotBondAtomIndices, RotBondHierarchyClass):
  474         """Match rotatable bond to torsion library."""
  475 
  476         if TorsionAlertsUtil.IsSpecificHierarchyClass(self.TorsionLibraryInfo, RotBondHierarchyClass):
  477             MatchStatus, MatchInfo = self._MatchRotatableBondAgainstSpecificHierarchyClass(Mol, RotBondAtomIndices, RotBondHierarchyClass)
  478             if not MatchStatus:
  479                 MatchStatus, MatchInfo = self._MatchRotatableBondAgainstGenericHierarchyClass(Mol, RotBondAtomIndices, RotBondHierarchyClass)
  480         else:
  481             MatchStatus, MatchInfo = self._MatchRotatableBondAgainstGenericHierarchyClass(Mol, RotBondAtomIndices, RotBondHierarchyClass)
  482 
  483         return (MatchStatus, MatchInfo)
  484 
  485 
  486     def _MatchRotatableBondAgainstSpecificHierarchyClass(self, Mol, RotBondAtomIndices, RotBondHierarchyClass):
  487         """Match rotatable bond against a specific hierarchy class."""
  488 
  489         TorsionLibraryInfo = self.TorsionLibraryInfo
  490 
  491         HierarchyClassElementNode = None
  492         if RotBondHierarchyClass in TorsionLibraryInfo["SpecificClasses"]["ElementNode"]:
  493             HierarchyClassElementNode = TorsionLibraryInfo["SpecificClasses"]["ElementNode"][RotBondHierarchyClass]
  494 
  495         if HierarchyClassElementNode is None:
  496             return (False, None, None, None)
  497 
  498         TorsionAlertsUtil.TrackHierarchyClassElementNode(TorsionLibraryInfo, HierarchyClassElementNode)
  499         MatchStatus, MatchInfo = self._ProcessElementForRotatableBondMatch(Mol, RotBondAtomIndices, HierarchyClassElementNode)
  500         TorsionAlertsUtil.RemoveLastHierarchyClassElementNodeFromTracking(TorsionLibraryInfo)
  501 
  502         return (MatchStatus, MatchInfo)
  503 
  504 
  505     def _MatchRotatableBondAgainstGenericHierarchyClass(self, Mol, RotBondAtomIndices, RotBondHierarchyClass):
  506         """Match rotatable bond against a generic hierarchy class."""
  507 
  508         TorsionLibraryInfo = self.TorsionLibraryInfo
  509 
  510         HierarchyClassElementNode = TorsionAlertsUtil.GetGenericHierarchyClassElementNode(TorsionLibraryInfo)
  511         if HierarchyClassElementNode is None:
  512             return (False, None)
  513 
  514         TorsionAlertsUtil.TrackHierarchyClassElementNode(TorsionLibraryInfo, HierarchyClassElementNode)
  515 
  516         #  Match hierarchy subclasses before matching torsion rules...
  517         MatchStatus, MatchInfo = self._MatchRotatableBondAgainstGenericHierarchySubClasses(Mol, RotBondAtomIndices, HierarchyClassElementNode)
  518 
  519         if not MatchStatus:
  520             MatchStatus, MatchInfo = self._MatchRotatableBondAgainstGenericHierarchyTorsionRules(Mol, RotBondAtomIndices, HierarchyClassElementNode)
  521 
  522         TorsionAlertsUtil.RemoveLastHierarchyClassElementNodeFromTracking(TorsionLibraryInfo)
  523 
  524         return (MatchStatus, MatchInfo)
  525 
  526 
  527     def _MatchRotatableBondAgainstGenericHierarchySubClasses(self, Mol, RotBondAtomIndices, HierarchyClassElementNode):
  528         """Match rotatable bond againat generic hierarchy subclasses."""
  529 
  530         for ElementChildNode in HierarchyClassElementNode:
  531             if ElementChildNode.tag != "hierarchySubClass":
  532                 continue
  533 
  534             SubClassMatchStatus = self._ProcessHierarchySubClassElementForRotatableBondMatch(Mol, RotBondAtomIndices, ElementChildNode)
  535 
  536             if SubClassMatchStatus:
  537                 MatchStatus, MatchInfo = self._ProcessElementForRotatableBondMatch(Mol, RotBondAtomIndices, ElementChildNode)
  538 
  539                 if MatchStatus:
  540                     return (MatchStatus, MatchInfo)
  541 
  542         return(False, None)
  543 
  544 
  545     def _MatchRotatableBondAgainstGenericHierarchyTorsionRules(self, Mol, RotBondAtomIndices, HierarchyClassElementNode):
  546         """Match rotatable bond againat torsion rules generic hierarchy class."""
  547 
  548         for ElementChildNode in HierarchyClassElementNode:
  549             if ElementChildNode.tag != "torsionRule":
  550                 continue
  551 
  552             MatchStatus, MatchInfo = self._ProcessTorsionRuleElementForRotatableBondMatch(Mol, RotBondAtomIndices, ElementChildNode)
  553 
  554             if MatchStatus:
  555                 return (MatchStatus, MatchInfo)
  556 
  557         return(False, None)
  558 
  559 
  560     def _ProcessElementForRotatableBondMatch(self, Mol, RotBondAtomIndices, ElementNode):
  561         """Process element node to recursively match rotatable bond against hierarchy
  562         subclasses and torsion rules."""
  563 
  564         TorsionLibraryInfo = self.TorsionLibraryInfo
  565 
  566         for ElementChildNode in ElementNode:
  567             if ElementChildNode.tag == "hierarchySubClass":
  568                 SubClassMatchStatus = self._ProcessHierarchySubClassElementForRotatableBondMatch(Mol, RotBondAtomIndices, ElementChildNode)
  569 
  570                 if SubClassMatchStatus:
  571                     TorsionAlertsUtil.TrackHierarchySubClassElementNode(TorsionLibraryInfo, ElementChildNode)
  572 
  573                     MatchStatus, MatchInfo = self._ProcessElementForRotatableBondMatch(Mol, RotBondAtomIndices, ElementChildNode)
  574                     if MatchStatus:
  575                         TorsionAlertsUtil.RemoveLastHierarchySubClassElementNodeFromTracking(TorsionLibraryInfo)
  576                         return (MatchStatus, MatchInfo)
  577 
  578                     TorsionAlertsUtil.RemoveLastHierarchySubClassElementNodeFromTracking(TorsionLibraryInfo)
  579 
  580             elif ElementChildNode.tag == "torsionRule":
  581                 MatchStatus, MatchInfo = self._ProcessTorsionRuleElementForRotatableBondMatch(Mol, RotBondAtomIndices, ElementChildNode)
  582 
  583                 if MatchStatus:
  584                     return (MatchStatus, MatchInfo)
  585 
  586         return (False, None)
  587 
  588 
  589     def _ProcessHierarchySubClassElementForRotatableBondMatch(self, Mol, RotBondAtomIndices, ElementNode):
  590         """Process hierarchy subclass element to match rotatable bond."""
  591 
  592         # Setup subclass SMARTS pattern mol...
  593         SubClassPatternMol = TorsionAlertsUtil.SetupHierarchySubClassElementPatternMol(self.TorsionLibraryInfo, ElementNode)
  594         if SubClassPatternMol is None:
  595             return False
  596 
  597         # Match SMARTS pattern...
  598         SubClassPatternMatches = TorsionAlertsUtil.FilterSubstructureMatchesByAtomMapNumbers(Mol, SubClassPatternMol, Mol.GetSubstructMatches(SubClassPatternMol, useChirality = False))
  599         if len(SubClassPatternMatches) == 0:
  600             return False
  601 
  602         # Match rotatable bond indices...
  603         RotBondAtomIndex1, RotBondAtomIndex2 = RotBondAtomIndices
  604         MatchStatus = False
  605         for SubClassPatternMatch in SubClassPatternMatches:
  606             if len(SubClassPatternMatch) == 2:
  607                 # Matched to pattern containing map atom numbers ":2" and ":3"...
  608                 CentralAtomsIndex1, CentralAtomsIndex2 = SubClassPatternMatch
  609             elif len(SubClassPatternMatch) == 4:
  610                 # Matched to pattern containing map atom numbers ":1", ":2", ":3" and ":4"...
  611                 CentralAtomsIndex1 = SubClassPatternMatch[1]
  612                 CentralAtomsIndex2 = SubClassPatternMatch[2]
  613             elif len(SubClassPatternMatch) == 3:
  614                 SubClassSMARTSPattern = ElementNode.get("smarts")
  615                 if TorsionAlertsUtil.DoesSMARTSContainsMappedAtoms(SubClassSMARTSPattern, [":2", ":3", ":4"]):
  616                     # Matched to pattern containing map atom numbers ":2", ":3" and ":4"...
  617                     CentralAtomsIndex1 = SubClassPatternMatch[0]
  618                     CentralAtomsIndex2 = SubClassPatternMatch[1]
  619                 else:
  620                     # Matched to pattern containing map atom numbers ":1", ":2" and ":3"...
  621                     CentralAtomsIndex1 = SubClassPatternMatch[1]
  622                     CentralAtomsIndex2 = SubClassPatternMatch[2]
  623             else:
  624                 continue
  625 
  626             if CentralAtomsIndex1 != CentralAtomsIndex2:
  627                 if ((CentralAtomsIndex1 == RotBondAtomIndex1 and CentralAtomsIndex2 == RotBondAtomIndex2) or (CentralAtomsIndex1 == RotBondAtomIndex2 and CentralAtomsIndex2 == RotBondAtomIndex1)):
  628                     MatchStatus = True
  629                     break
  630 
  631         return (MatchStatus)
  632 
  633 
  634     def _ProcessTorsionRuleElementForRotatableBondMatch(self, Mol, RotBondAtomIndices, ElementNode):
  635         """Process torsion rule element to match rotatable bond."""
  636 
  637         TorsionLibraryInfo = self.TorsionLibraryInfo
  638 
  639         #  Retrieve torsion matched to rotatable bond...
  640         TorsionAtomIndices, TorsionAngle = self._MatchTorsionRuleToRotatableBond(Mol, RotBondAtomIndices, ElementNode)
  641         if TorsionAtomIndices is None:
  642             return (False, None)
  643 
  644         # Setup torsion angles info for matched torsion rule...
  645         TorsionAnglesInfo = TorsionAlertsUtil.SetupTorsionRuleAnglesInfo(TorsionLibraryInfo, ElementNode)
  646         if TorsionAnglesInfo is None:
  647             return (False, None)
  648 
  649         #   Setup torsion alert type and angle violation...
  650         AlertType, TorsionAngleViolation = self._SetupTorsionAlertTypeForRotatableBond(TorsionAnglesInfo, TorsionAngle)
  651 
  652         # Setup hierarchy class and subclass names...
  653         HierarchyClassName, HierarchySubClassName = TorsionAlertsUtil.SetupHierarchyClassAndSubClassNamesForRotatableBond(TorsionLibraryInfo)
  654 
  655         # Setup rule node ID...
  656         TorsionRuleNodeID = ElementNode.get("NodeID")
  657 
  658         # Setup SMARTS...
  659         TorsionRuleSMARTS = ElementNode.get("smarts")
  660         if " " in TorsionRuleSMARTS:
  661             TorsionRuleSMARTS = TorsionRuleSMARTS.replace(" ", "")
  662 
  663         # Setup torsion peaks and tolerances...
  664         TorsionRulePeaks = TorsionAnglesInfo["ValuesList"]
  665         TorsionRuleTolerances1 = TorsionAnglesInfo["Tolerances1List"]
  666         TorsionRuleTolerances2 = TorsionAnglesInfo["Tolerances2List"]
  667 
  668         MatchInfo = [AlertType, TorsionAtomIndices, TorsionAngle, TorsionAngleViolation, HierarchyClassName, HierarchySubClassName, TorsionRuleNodeID, TorsionRulePeaks, TorsionRuleTolerances1, TorsionRuleTolerances2, TorsionRuleSMARTS]
  669 
  670         # Setup match status...
  671         MatchStatus = True
  672 
  673         return (MatchStatus, MatchInfo)
  674 
  675 
  676     def _MatchTorsionRuleToRotatableBond(self, Mol, RotBondAtomIndices, ElementNode):
  677         """Retrieve matched torsion for torsion rule matched to rotatable bond."""
  678 
  679         # Get torsion matches...
  680         TorsionMatches = self._GetMatchesForTorsionRule(Mol, ElementNode)
  681         if TorsionMatches is None or len(TorsionMatches) == 0:
  682             return (None, None)
  683 
  684         # Identify the first torsion match corresponding to central atoms in RotBondAtomIndices...
  685         RotBondAtomIndex1, RotBondAtomIndex2 = RotBondAtomIndices
  686         for TorsionMatch in TorsionMatches:
  687             CentralAtomIndex1 = TorsionMatch[1]
  688             CentralAtomIndex2 = TorsionMatch[2]
  689 
  690             if ((CentralAtomIndex1 == RotBondAtomIndex1 and CentralAtomIndex2 == RotBondAtomIndex2) or (CentralAtomIndex1 == RotBondAtomIndex2 and CentralAtomIndex2 == RotBondAtomIndex1)):
  691                 TorsionAngle = self._CalculateTorsionAngle(Mol, TorsionMatch)
  692 
  693                 return (TorsionMatch, TorsionAngle)
  694 
  695         return (None, None)
  696 
  697 
  698     def _CalculateTorsionAngle(self, Mol, TorsionMatch):
  699         """Calculate torsion angle."""
  700 
  701         if type(TorsionMatch[3]) is list:
  702             return self._CalculateTorsionAngleUsingNitrogenLonePairPosition(Mol, TorsionMatch)
  703 
  704         # Calculate torsion angle using torsion atom indices..
  705         MolConf = Mol.GetConformer(0)
  706         TorsionAngle = rdMolTransforms.GetDihedralDeg(MolConf, TorsionMatch[0], TorsionMatch[1], TorsionMatch[2], TorsionMatch[3])
  707         TorsionAngle = round(TorsionAngle, 2)
  708 
  709         return TorsionAngle
  710 
  711 
  712     def _CalculateTorsionAngleUsingNitrogenLonePairPosition(self, Mol, TorsionMatch):
  713         """Calculate torsion angle using nitrogen lone pair positon."""
  714 
  715         # Setup a carbon atom as position holder for lone pair position...
  716         TmpMol = Chem.RWMol(Mol)
  717         LonePairAtomIndex = TmpMol.AddAtom(Chem.Atom(6))
  718 
  719         TmpMolConf = TmpMol.GetConformer(0)
  720         TmpMolConf.SetAtomPosition(LonePairAtomIndex, TorsionMatch[3])
  721 
  722         TorsionAngle = rdMolTransforms.GetDihedralDeg(TmpMolConf, TorsionMatch[0], TorsionMatch[1], TorsionMatch[2], LonePairAtomIndex)
  723         TorsionAngle = round(TorsionAngle, 2)
  724 
  725         return TorsionAngle
  726 
  727 
  728     def _GetMatchesForTorsionRule(self, Mol, ElementNode):
  729         """Get matches for torsion rule."""
  730 
  731         # Match torsions...
  732         TorsionMatches = None
  733         if self._IsNitogenLonePairTorsionRule(ElementNode):
  734             TorsionMatches = self._GetSubstructureMatchesForNitrogenLonePairTorsionRule(Mol, ElementNode)
  735         else:
  736             TorsionMatches = self._GetSubstructureMatchesForTorsionRule(Mol, ElementNode)
  737 
  738         if TorsionMatches is None or len(TorsionMatches) == 0:
  739             return TorsionMatches
  740 
  741         # Filter torsion matches...
  742         FiltertedTorsionMatches = []
  743         for TorsionMatch in TorsionMatches:
  744             if len(TorsionMatch) != 4:
  745                 continue
  746 
  747             # Ignore matches containing hydrogen atoms as first or last atom...
  748             if Mol.GetAtomWithIdx(TorsionMatch[0]).GetAtomicNum() == 1:
  749                 continue
  750             if type(TorsionMatch[3]) is int:
  751                 # May contains a list for type two nitrogen lone pair match...
  752                 if Mol.GetAtomWithIdx(TorsionMatch[3]).GetAtomicNum() == 1:
  753                     continue
  754             FiltertedTorsionMatches.append(TorsionMatch)
  755 
  756         return FiltertedTorsionMatches
  757 
  758 
  759     def _GetSubstructureMatchesForTorsionRule(self, Mol, ElementNode):
  760         """Get substructure matches for a torsion rule."""
  761 
  762         # Setup torsion rule SMARTS pattern mol....
  763         TorsionRuleNodeID = ElementNode.get("NodeID")
  764         TorsionSMARTSPattern = ElementNode.get("smarts")
  765         TorsionPatternMol = TorsionAlertsUtil.SetupTorsionRuleElementPatternMol(self.TorsionLibraryInfo, ElementNode, TorsionRuleNodeID, TorsionSMARTSPattern)
  766         if TorsionPatternMol is None:
  767             return None
  768 
  769         # Match torsions...
  770         TorsionMatches = TorsionAlertsUtil.FilterSubstructureMatchesByAtomMapNumbers(Mol, TorsionPatternMol, Mol.GetSubstructMatches(TorsionPatternMol, useChirality = False))
  771 
  772         return TorsionMatches
  773 
  774 
  775     def _GetSubstructureMatchesForNitrogenLonePairTorsionRule(self, Mol, ElementNode):
  776         """Get substructure matches for a torsion rule containing N_lp."""
  777 
  778         if self._IsTypeOneNitogenLonePairTorsionRule(ElementNode):
  779             return self._GetSubstructureMatchesForTypeOneNitrogenLonePairTorsionRule(Mol, ElementNode)
  780         elif self._IsTypeTwoNitogenLonePairTorsionRule(ElementNode):
  781             return self._GetSubstructureMatchesForTypeTwoNitrogenLonePairTorsionRule(Mol, ElementNode)
  782 
  783         return None
  784 
  785 
  786     def _GetSubstructureMatchesForTypeOneNitrogenLonePairTorsionRule(self, Mol, ElementNode):
  787         """Get substructure matches for a torsion rule containing N_lp and four mapped atoms."""
  788 
  789         # For example:
  790         #    [CX4:1][CX4H2:2]!@[NX3;"N_lp":3][CX4:4]
  791         #    [C:1][CX4H2:2]!@[NX3;"N_lp":3][C:4]
  792         #    ... ... ...
  793 
  794         TorsionRuleNodeID = ElementNode.get("NodeID")
  795         TorsionPatternMol, LonePairMapNumber = self._SetupNitrogenLonePairTorsionRuleElementInfo(ElementNode, TorsionRuleNodeID)
  796 
  797         if TorsionPatternMol is None:
  798             return None
  799 
  800         if LonePairMapNumber is None:
  801             return None
  802 
  803         # Match torsions...
  804         TorsionMatches = TorsionAlertsUtil.FilterSubstructureMatchesByAtomMapNumbers(Mol, TorsionPatternMol, Mol.GetSubstructMatches(TorsionPatternMol, useChirality = False))
  805 
  806         # Filter matches...
  807         FiltertedTorsionMatches = []
  808         for TorsionMatch in TorsionMatches:
  809             if len(TorsionMatch) != 4:
  810                 continue
  811 
  812             # Check for Nitogen atom at LonePairMapNumber...
  813             LonePairNitrogenAtom = Mol.GetAtomWithIdx(TorsionMatch[LonePairMapNumber - 1])
  814             if LonePairNitrogenAtom.GetSymbol() != "N":
  815                 continue
  816 
  817             # Make sure LonePairNitrogenAtom is planar...
  818             #  test
  819             PlanarityStatus = self._IsLonePairNitrogenAtomPlanar(Mol, LonePairNitrogenAtom)
  820             if PlanarityStatus is None:
  821                 continue
  822 
  823             if  not PlanarityStatus:
  824                 continue
  825 
  826             FiltertedTorsionMatches.append(TorsionMatch)
  827 
  828         return FiltertedTorsionMatches
  829 
  830 
  831     def _GetSubstructureMatchesForTypeTwoNitrogenLonePairTorsionRule(self, Mol, ElementNode):
  832         """Get substructure matches for a torsion rule containing N_lp and three mapped atoms."""
  833 
  834         # For example:
  835         # [!#1:1][CX4:2]!@[NX3;"N_lp":3]
  836         # [!#1:1][$(S(=O)=O):2]!@["N_lp":3]@[Cr3]
  837         # [C:1][$(S(=O)=O):2]!@["N_lp":3]
  838         # [c:1][$(S(=O)=O):2]!@["N_lp":3]
  839         # [!#1:1][$(S(=O)=O):2]!@["N_lp":3]
  840         #    ... ... ...
  841 
  842         TorsionRuleNodeID = ElementNode.get("NodeID")
  843         TorsionPatternMol, LonePairMapNumber = self._SetupNitrogenLonePairTorsionRuleElementInfo(ElementNode, TorsionRuleNodeID)
  844 
  845         if TorsionPatternMol is None:
  846             return None
  847 
  848         if not self._IsValidTypeTwoNitrogenLonePairMapNumber(LonePairMapNumber):
  849             return None
  850 
  851         # Match torsions...
  852         TorsionMatches = TorsionAlertsUtil.FilterSubstructureMatchesByAtomMapNumbers(Mol, TorsionPatternMol, Mol.GetSubstructMatches(TorsionPatternMol, useChirality = False))
  853 
  854         # Filter matches...
  855         FiltertedTorsionMatches = []
  856         for TorsionMatch in TorsionMatches:
  857             if len(TorsionMatch) != 3:
  858                 continue
  859 
  860             # Check for Nitogen atom at LonePairMapNumber...
  861             LonePairNitrogenAtom = Mol.GetAtomWithIdx(TorsionMatch[LonePairMapNumber - 1])
  862             if LonePairNitrogenAtom.GetSymbol() != "N":
  863                 continue
  864 
  865             # Make sure LonePairNitrogenAtom is not planar...
  866             PlanarityStatus = self._IsLonePairNitrogenAtomPlanar(Mol, LonePairNitrogenAtom)
  867 
  868             if PlanarityStatus is None:
  869                 continue
  870 
  871             if  PlanarityStatus:
  872                 continue
  873 
  874             # Calculate lone pair coordinates for a non-planar nitrogen...
  875             LonePairPosition = self._CalculateLonePairCoordinatesForNitrogenAtom(Mol, LonePairNitrogenAtom)
  876             if LonePairPosition is None:
  877                 continue
  878 
  879             # Append lone pair coodinate list to list of torsion match containing atom indices...
  880             TorsionMatch.append(LonePairPosition)
  881 
  882             # Track torsion matches...
  883             FiltertedTorsionMatches.append(TorsionMatch)
  884 
  885         return FiltertedTorsionMatches
  886 
  887 
  888     def _SetupNitrogenLonePairTorsionRuleElementInfo(self, ElementNode, TorsionRuleNodeID):
  889         """Setup pattern molecule and lone pair map number for type one and type
  890         two nitrogen lone pair rules."""
  891 
  892         TorsionLibraryInfo = self.TorsionLibraryInfo
  893         TorsionPatternMol, LonePairMapNumber = [None] * 2
  894 
  895         if TorsionRuleNodeID in TorsionLibraryInfo["DataCache"]["TorsionRulePatternMol"]:
  896             TorsionPatternMol = TorsionLibraryInfo["DataCache"]["TorsionRulePatternMol"][TorsionRuleNodeID]
  897             LonePairMapNumber = TorsionLibraryInfo["DataCache"]["TorsionRuleLonePairMapNumber"][TorsionRuleNodeID]
  898         else:
  899             # Setup torsion pattern...
  900             TorsionSMARTSPattern = ElementNode.get("smarts")
  901             TorsionSMARTSPattern, LonePairMapNumber = self._ProcessSMARTSForNitrogenLonePairTorsionRule(TorsionSMARTSPattern)
  902 
  903             # Setup torsion pattern mol...
  904             TorsionPatternMol = Chem.MolFromSmarts(TorsionSMARTSPattern)
  905             if TorsionPatternMol is None:
  906                 print("Warning: Ignoring torsion rule element containing invalid map atoms numbers in SMARTS pattern %s" % TorsionSMARTSPattern)
  907 
  908             # Cache data...
  909             TorsionLibraryInfo["DataCache"]["TorsionRulePatternMol"][TorsionRuleNodeID] = TorsionPatternMol
  910             TorsionLibraryInfo["DataCache"]["TorsionRuleLonePairMapNumber"][TorsionRuleNodeID] = LonePairMapNumber
  911 
  912         return (TorsionPatternMol, LonePairMapNumber)
  913 
  914 
  915     def _IsLonePairNitrogenAtomPlanar(self, Mol, NitrogenAtom):
  916         """Check for the planarity of nitrogen atom and its three neighbors."""
  917 
  918         AllowHydrogenNbrs = self.NitrogenLonePairAllowHydrogenNbrs
  919         Tolerance = self.NitrogenLonePairPlanarityTolerance
  920 
  921         # Get neighbors...
  922         if AllowHydrogenNbrs:
  923             AtomNeighbors = NitrogenAtom.GetNeighbors()
  924         else:
  925             AtomNeighbors = TorsionAlertsUtil.GetHeavyAtomNeighbors(NitrogenAtom)
  926 
  927         if len(AtomNeighbors) != 3:
  928             return None
  929 
  930         # Setup atom positions...
  931         AtomPositions = []
  932         MolAtomsPositions = TorsionAlertsUtil.GetAtomPositions(Mol)
  933 
  934         # Neighbor positions...
  935         for AtomNbr in AtomNeighbors:
  936             AtomNbrIndex = AtomNbr.GetIdx()
  937             AtomPositions.append(MolAtomsPositions[AtomNbrIndex])
  938 
  939         # Nitrogen position...
  940         NitrogenAtomIndex = NitrogenAtom.GetIdx()
  941         AtomPositions.append(MolAtomsPositions[NitrogenAtomIndex])
  942 
  943         Status =  self._AreFourPointsCoplanar(AtomPositions[0], AtomPositions[1], AtomPositions[2], AtomPositions[3], Tolerance)
  944 
  945         return Status
  946 
  947 
  948     def _AreFourPointsCoplanar(self, Point1, Point2, Point3, Point4, Tolerance = 1.0):
  949         """Check whether four points are coplanar with in the threshold of 1 degree."""
  950 
  951         # Setup  normalized direction vectors...
  952         VectorP2P1 = self._NormalizeVector(np.subtract(Point2, Point1))
  953         VectorP3P1 = self._NormalizeVector(np.subtract(Point3, Point1))
  954         VectorP1P4 = self._NormalizeVector(np.subtract(Point1, Point4))
  955 
  956         # Calculate angle between VectorP1P4 and normal to vectors VectorP2P1 and VectorP3P1...
  957         PlaneP1P2P3Normal = self._NormalizeVector(np.cross(VectorP2P1, VectorP3P1))
  958         PlanarityAngle = np.arccos(np.clip(np.dot(PlaneP1P2P3Normal, VectorP1P4), -1.0, 1.0))
  959 
  960         Status = math.isclose(PlanarityAngle, math.radians(90), abs_tol=math.radians(Tolerance))
  961 
  962         return Status
  963 
  964 
  965     def _NormalizeVector(self, Vector):
  966         """Normalize vector."""
  967 
  968         Norm = np.linalg.norm(Vector)
  969 
  970         return Vector if math.isclose(Norm, 0.0, abs_tol = 1e-08) else Vector/Norm
  971 
  972 
  973     def _CalculateLonePairCoordinatesForNitrogenAtom(self, Mol, NitrogenAtom):
  974         """Calculate approximate lone pair coordinates for non-plannar nitrogen atom."""
  975 
  976         AllowHydrogenNbrs = self.NitrogenLonePairAllowHydrogenNbrs
  977 
  978         # Get neighbors...
  979         if AllowHydrogenNbrs:
  980             AtomNeighbors = NitrogenAtom.GetNeighbors()
  981         else:
  982             AtomNeighbors = TorsionAlertsUtil.GetHeavyAtomNeighbors(NitrogenAtom)
  983 
  984         if len(AtomNeighbors) != 3:
  985             return None
  986 
  987         # Setup positions for nitrogen and its neghbors...
  988         MolAtomsPositions = TorsionAlertsUtil.GetAtomPositions(Mol)
  989 
  990         NitrogenPosition = MolAtomsPositions[NitrogenAtom.GetIdx()]
  991         NbrPositions = []
  992         for AtomNbr in AtomNeighbors:
  993             NbrPositions.append(MolAtomsPositions[AtomNbr.GetIdx()])
  994         Nbr1Position, Nbr2Position, Nbr3Position = NbrPositions
  995 
  996         # Setup  normalized direction vectors...
  997         VectorP2P1 = self._NormalizeVector(np.subtract(Nbr2Position, Nbr1Position))
  998         VectorP3P1 = self._NormalizeVector(np.subtract(Nbr3Position, Nbr1Position))
  999         VectorP1P4 = self._NormalizeVector(np.subtract(Nbr1Position, NitrogenPosition))
 1000 
 1001         # Calculate angle between VectorP1P4 and normal to vectors VectorP2P1 and VectorP3P1...
 1002         PlaneP1P2P3Normal = self._NormalizeVector(np.cross(VectorP2P1, VectorP3P1))
 1003         PlanarityAngle = np.arccos(np.clip(np.dot(PlaneP1P2P3Normal, VectorP1P4), -1.0, 1.0))
 1004 
 1005         # Check for reversing the direction of the normal...
 1006         if PlanarityAngle < math.radians(90):
 1007             PlaneP1P2P3Normal = PlaneP1P2P3Normal * -1
 1008 
 1009         # Add normal to nitrogen cooridnates for the approximate coordinates of the
 1010         # one pair. The exact VSEPR coordinates of the lone pair are not necessary to
 1011         # calculate the torsion angle...
 1012         LonePairPosition = NitrogenPosition + PlaneP1P2P3Normal
 1013 
 1014         return list(LonePairPosition)
 1015 
 1016 
 1017     def _ProcessSMARTSForNitrogenLonePairTorsionRule(self, SMARTSPattern):
 1018         """Process SMARTS pattern for a torion rule containing N_lp."""
 1019 
 1020         LonePairMapNumber = self._GetNitrogenLonePairMapNumber(SMARTSPattern)
 1021 
 1022         # Remove double quotes around N_lp..
 1023         SMARTSPattern = re.sub("\"N_lp\"", "N_lp", SMARTSPattern, re.I)
 1024 
 1025         # Remove N_lp specification from SMARTS pattern for torsion rule...
 1026         if re.search("\[N_lp", SMARTSPattern, re.I):
 1027             # Handle missing NX3...
 1028             SMARTSPattern = re.sub("\[N_lp", "[NX3", SMARTSPattern)
 1029         else:
 1030             SMARTSPattern = re.sub(";N_lp", "", SMARTSPattern)
 1031 
 1032         return (SMARTSPattern, LonePairMapNumber)
 1033 
 1034 
 1035     def _GetNitrogenLonePairMapNumber(self, SMARTSPattern):
 1036         """Get atom map number for nitrogen involved in N_lp."""
 1037 
 1038         LonePairMapNumber = None
 1039 
 1040         SMARTSPattern = re.sub("\"N_lp\"", "N_lp", SMARTSPattern, re.I)
 1041         MatchedMappedAtoms = re.findall("N_lp:[0-9]", SMARTSPattern, re.I)
 1042 
 1043         if len(MatchedMappedAtoms) == 1:
 1044             LonePairMapNumber = int(re.sub("N_lp:", "", MatchedMappedAtoms[0]))
 1045 
 1046         return LonePairMapNumber
 1047 
 1048 
 1049     def _IsNitogenLonePairTorsionRule(self, ElementNode):
 1050         """Check for the presence of N_lp in SMARTS pattern for a torsion rule."""
 1051 
 1052         if "N_lp" not  in ElementNode.get("smarts"):
 1053             return False
 1054 
 1055         LonePairMatches = re.findall("N_lp", ElementNode.get("smarts"), re.I)
 1056 
 1057         return True if len(LonePairMatches) == 1 else False
 1058 
 1059 
 1060     def _IsTypeOneNitogenLonePairTorsionRule(self, ElementNode):
 1061         """Check for the presence four mapped atoms in a SMARTS pattern containing
 1062         N_lp for a torsion rule."""
 1063 
 1064         # For example:
 1065         #    [CX4:1][CX4H2:2]!@[NX3;"N_lp":3][CX4:4]
 1066         #    [C:1][CX4H2:2]!@[NX3;"N_lp":3][C:4]
 1067         #    ... ... ...
 1068 
 1069         MatchedMappedAtoms = re.findall(":[0-9]", ElementNode.get("smarts"), re.I)
 1070 
 1071         return True if len(MatchedMappedAtoms) == 4 else False
 1072 
 1073 
 1074     def _IsTypeTwoNitogenLonePairTorsionRule(self, ElementNode):
 1075         """Check for the presence three mapped atoms in a SMARTS pattern containing
 1076         N_lp for a torsion rule."""
 1077 
 1078         # For example:
 1079         # [!#1:1][CX4:2]!@[NX3;"N_lp":3]
 1080         # [!#1:1][$(S(=O)=O):2]!@["N_lp":3]@[Cr3]
 1081         # [C:1][$(S(=O)=O):2]!@["N_lp":3]
 1082         # [c:1][$(S(=O)=O):2]!@["N_lp":3]
 1083         # [!#1:1][$(S(=O)=O):2]!@["N_lp":3]
 1084         #
 1085 
 1086         MatchedMappedAtoms = re.findall(":[0-9]", ElementNode.get("smarts"), re.I)
 1087 
 1088         return True if len(MatchedMappedAtoms) == 3 else False
 1089 
 1090 
 1091     def _IsValidTypeTwoNitogenLonePairTorsionRule(self, ElementNode):
 1092         """Validate atom map number for nitrogen involved in N_lp for type two nitrogen
 1093         lone pair torsion rule."""
 1094 
 1095         LonePairMapNumber = self._GetNitrogenLonePairMapNumber(ElementNode.get("smarts"))
 1096 
 1097         return self._IsValidTypeTwoNitrogenLonePairMapNumber(LonePairMapNumber)
 1098 
 1099 
 1100     def _IsValidTypeTwoNitrogenLonePairMapNumber(self, LonePairMapNumber):
 1101         """Check that  the atom map number is 3."""
 1102 
 1103         return True if LonePairMapNumber is not None and LonePairMapNumber == 3 else False
 1104 
 1105 
 1106     def _SetupTorsionAlertTypeForRotatableBond(self, TorsionAnglesInfo, TorsionAngle):
 1107         """Setup torsion alert type and angle violation for a rotatable bond."""
 1108 
 1109         TorsionCategory, TorsionAngleViolation = [None, None]
 1110 
 1111         for ID in TorsionAnglesInfo["IDs"]:
 1112             if self._IsTorsionAngleInWithinTolerance(TorsionAngle, TorsionAnglesInfo["Value"][ID], TorsionAnglesInfo["Tolerance1"][ID]):
 1113                 TorsionCategory = "Green"
 1114                 TorsionAngleViolation = 0.0
 1115                 break
 1116 
 1117             if self._IsTorsionAngleInWithinTolerance(TorsionAngle, TorsionAnglesInfo["Value"][ID], TorsionAnglesInfo["Tolerance2"][ID]):
 1118                 TorsionCategory = "Orange"
 1119                 TorsionAngleViolation = self._CalculateTorsionAngleViolation(TorsionAngle, TorsionAnglesInfo["ValuesIn360RangeList"], TorsionAnglesInfo["Tolerances1List"])
 1120                 break
 1121 
 1122         if TorsionCategory is None:
 1123             TorsionCategory = "Red"
 1124             TorsionAngleViolation = self._CalculateTorsionAngleViolation(TorsionAngle, TorsionAnglesInfo["ValuesIn360RangeList"], TorsionAnglesInfo["Tolerances2List"])
 1125 
 1126         return (TorsionCategory, TorsionAngleViolation)
 1127 
 1128 
 1129     def _IsTorsionAngleInWithinTolerance(self, TorsionAngle, TorsionPeak, TorsionTolerance):
 1130         """Check torsion angle against torsion tolerance."""
 1131 
 1132         TorsionAngleDiff = TorsionAlertsUtil.CalculateTorsionAngleDifference(TorsionPeak, TorsionAngle)
 1133 
 1134         return True if (abs(TorsionAngleDiff) <= TorsionTolerance) else False
 1135 
 1136 
 1137     def _CalculateTorsionAngleViolation(self, TorsionAngle, TorsionPeaks, TorsionTolerances):
 1138         """Calculate torsion angle violation."""
 1139 
 1140         TorsionAngleViolation = None
 1141 
 1142         # Map angle to 0 to 360 range. TorsionPeaks values must be in this range...
 1143         if TorsionAngle < 0:
 1144             TorsionAngle = TorsionAngle + 360
 1145 
 1146         # Identify the closest torsion peak index...
 1147         if len(TorsionPeaks) == 1:
 1148             NearestPeakIndex = 0
 1149         else:
 1150             NearestPeakIndex = min(range(len(TorsionPeaks)), key=lambda Index: abs(TorsionPeaks[Index] - TorsionAngle))
 1151 
 1152         # Calculate torsion angle violation from the nearest peak and its tolerance value...
 1153         TorsionAngleDiff = TorsionAlertsUtil.CalculateTorsionAngleDifference(TorsionPeaks[NearestPeakIndex], TorsionAngle)
 1154         TorsionAngleViolation = abs(abs(TorsionAngleDiff) - TorsionTolerances[NearestPeakIndex])
 1155 
 1156         return TorsionAngleViolation