MayaChemTools

    1 #
    2 # File: TorsionStrainEnergyAlerts.py
    3 # Author: Manish Sud <msud@san.rr.com>
    4 #
    5 # Collaborator: Pat Walters
    6 #
    7 # Copyright (C) 2026 Manish Sud. All rights reserved.
    8 #
    9 # This module uses the torsion strain energy library developed by Gu, S.;
   10 # Smith, M. S.; Yang, Y.; Irwin, J. J.; Shoichet, B. K. [ Ref 153 ].
   11 #
   12 # The torsion strain enegy library is based on the Torsion Library jointly
   13 # developed by the University of Hamburg, Center for Bioinformatics,
   14 # Hamburg, Germany and F. Hoffmann-La-Roche Ltd., Basel, Switzerland.
   15 #
   16 # This file is part of MayaChemTools.
   17 #
   18 # MayaChemTools is free software; you can redistribute it and/or modify it under
   19 # the terms of the GNU Lesser General Public License as published by the Free
   20 # Software Foundation; either version 3 of the License, or (at your option) any
   21 # later version.
   22 #
   23 # MayaChemTools is distributed in the hope that it will be useful, but without
   24 # any warranty; without even the implied warranty of merchantability of fitness
   25 # for a particular purpose.  See the GNU Lesser General Public License for more
   26 # details.
   27 #
   28 # You should have received a copy of the GNU Lesser General Public License
   29 # along with MayaChemTools; if not, see <http://www.gnu.org/licenses/> or
   30 # write to the Free Software Foundation Inc., 59 Temple Place, Suite 330,
   31 # Boston, MA, 02111-1307, USA.
   32 #
   33 
   34 import os
   35 import re
   36 import math
   37 
   38 from rdkit import Chem
   39 from rdkit.Chem import rdMolTransforms
   40 
   41 from . import TorsionAlertsUtil
   42 
   43 
   44 class TorsionStrainEnergyAlerts:
   45     def __init__(
   46         self,
   47         AlertsMode="TotalEnergy",
   48         TotalEnergyCutoff=6.0,
   49         MaxSingleEnergyCutoff=1.8,
   50         RotBondsSMARTSMode="SemiStrict",
   51         RotBondsSMARTSPattern=None,
   52         TorsionLibraryFilePath="auto",
   53         AlertTorsionsNotObserved=False,
   54     ):
   55         """Identify strained molecules based on torsion strain energy library [ Ref 153 ]
   56         alerts by matching rotatable bonds against SMARTS patterns specified for
   57         torsion rules in a torsion energy library file. The molecules must have 3D coordinates.
   58         The default torsion strain energy library file, TorsionStrainEnergyLibrary.xml, is
   59         ia available in the directory containing this file.
   60 
   61         The data in torsion strain energy library file is organized in a hierarchical
   62         manner. It consists of one generic class and six specific classes at the highest
   63         level. Each class contains multiple subclasses corresponding to named functional
   64         groups or substructure patterns. The subclasses consist of torsion rules sorted
   65         from specific to generic torsion patterns. The torsion rule, in turn, contains a
   66         list of peak values for torsion angles and two tolerance values. A pair of tolerance
   67         values define torsion bins around a torsion peak value.
   68 
   69         A strain energy calculation method, 'exact' or 'approximate' [ Ref 153 ], is
   70         associated with each torsion rule for calculating torsion strain energy. The 'exact'
   71         stain energy calculation relies on the energy bins available under the energy histogram
   72         consisting of 36 bins covering angles from -180 to 180. The width of each bin is 10
   73         degree. The energy bins are are defined at the right end points. The first and the
   74         last energy bins correspond to -170 and 180 respectively. The torsion angle is mapped
   75         to a energy bin. An angle offset is calculated for the torsion angle from the the right
   76         end point angle of the bin. The strain energy is estimated for the angle offset based
   77         on the energy difference between the current and previous bins. The torsion strain
   78         energy, in terms of torsion energy units (TEUs), corresponds to the sum of bin strain
   79         energy and the angle offset strain energy.
   80 
   81             Energy = BinEnergyDiff/10.0 * BinAngleOffset + BinEnergy[BinNum]
   82 
   83             Where:
   84 
   85             BinEnergyDiff = BinEnergy[BinNum] - BinEnergy[PreviousBinNum]
   86             BinAngleOffset = TorsionAngle - BinAngleRightSide
   87 
   88         The 'approximate' strain energy calculation relies on the angle difference between a
   89         torsion angle and the torsion peaks observed for the torsion rules in the torsion
   90         energy library. The torsion angle is matched to a torsion peak based on the value of
   91         torsion angle difference. It must be less than or equal to the value for the second
   92         tolerance 'tolerance2'. Otherwise, the torsion angle is not observed in the torsion
   93         energy library and a value of 'NA' is assigned for torsion energy along with the lower
   94         and upper bounds on energy at 95% confidence interval. The 'approximate' torsion
   95         energy (TEUs) for observed torsion angle is calculated using the following formula:
   96 
   97             Energy = beta_1 * (AngleDiff ** 2) + beta_2 * (AngleDiff ** 4)
   98 
   99         The coefficients 'beta_1' and 'beta_2' are available for the observed angles in
  100         the torsion strain energy library. The 'AngleDiff' is the difference between the
  101         torsion angle and the matched torsion peak.
  102 
  103         For example:
  104 
  105             <library>
  106                 <hierarchyClass id1="G" id2="G" name="GG">
  107                 ...
  108                 </hierarchyClass>
  109                 <hierarchyClass id1="C" id2="O" name="CO">
  110                     <hierarchySubClass name="Ester bond I" smarts="O=[C:2][O:3]">
  111                         <torsionRule method="exact" smarts=
  112                             "[O:1]=[C:2]!@[O:3]~[CH0:4]">
  113                             <angleList>
  114                                 <angle score="56.52" tolerance1="20.00"
  115                                 tolerance2="25.00" value="0.0"/>
  116                             </angleList>
  117                             <histogram>
  118                                 <bin count="1"/>
  119                                 ...
  120                             </histogram>
  121                             <histogram_shifted>
  122                                 <bin count="0"/>
  123                                 ...
  124                             </histogram_shifted>
  125                             <histogram_converted>
  126                                 <bin energy="4.67... lower="2.14..." upper="Inf"/>
  127                                 ...
  128                                 <bin energy="1.86..." lower="1.58..." upper="2.40..."/>
  129                                 ...
  130                                </histogram_converted>
  131                         </torsionRule>
  132                         <torsionRule method="approximate" smarts=
  133                             "[cH0:1][c:2]([cH0])!@[O:3][p:4]">
  134                             <angleList>
  135                             <angle beta_1="0.002..." beta_2="-7.843...e-07"
  136                                 score="27.14" theta_0="-90.0" tolerance1="30.00"
  137                                 tolerance2="45.00" value="-90.0"/>
  138                             ...
  139                             </angleList>
  140                             <histogram>
  141                                 <bin count="0"/>
  142                                  ...
  143                             </histogram>
  144                             <histogram_shifted>
  145                                 <bin count="0"/>
  146                                 ...
  147                             </histogram_shifted>
  148                         </torsionRule>
  149                     ...
  150                  ...
  151                 </hierarchyClass>
  152                  <hierarchyClass id1="N" id2="C" name="NC">
  153                  ...
  154                 </hierarchyClass>
  155                 <hierarchyClass id1="S" id2="N" name="SN">
  156                 ...
  157                 </hierarchyClass>
  158                 <hierarchyClass id1="C" id2="S" name="CS">
  159                 ...
  160                 </hierarchyClass>
  161                 <hierarchyClass id1="C" id2="C" name="CC">
  162                 ...
  163                 </hierarchyClass>
  164                 <hierarchyClass id1="S" id2="S" name="SS">
  165                  ...
  166                 </hierarchyClass>
  167             </library>
  168 
  169         The rotatable bonds in a 3D molecule are identified using a default SMARTS pattern.
  170         A custom SMARTS pattern may be optionally specified to detect rotatable bonds.
  171         Each rotatable bond is matched to a torsion rule in the torsion strain energy library.
  172         The strain energy is calculated for each rotatable bond using the calculation
  173         method, 'exact' or 'approximate', associated with the matched torsion rule.
  174 
  175         The total strain energy (TEUs) of a molecule corresponds to the sum of  'exact' and
  176         'approximate' strain energies calculated for all matched rotatable bonds in the
  177         molecule. The total strain energy is set to 'NA' for molecules containing a 'approximate'
  178         energy estimate for a torsion angle not observed in the torsion energy library. In
  179         addition, the lower and upper bounds on energy at 95% confidence interval are
  180         set to 'NA'.
  181 
  182         Arguments:
  183             AlertsMode (str): Torsion strain energy library alert types to use
  184                 for issuing alerts. Possible values: TotalEnergy,
  185                 MaxSingleEnergy, or TotalOrMaxSingleEnergy.
  186             TotalEnergyCutoff (float): Total strain strain energy (TEUs) cutoff.
  187             MaxSingleEnergyCutoff (float): Maximum single strain energy (TEUs)
  188                 cutoff.
  189             RotBondsSMARTSMode (str): SMARTS pattern to use for identifying
  190                 rotatable bonds in a molecule. Possible values: NonStrict,
  191                 SemiStrict, Strict or Specify.
  192             RotBondsSMARTSPattern (str):SMARTS pattern for identifying rotatable
  193                 bonds. This paramater is only valid for 'Specify' value
  194                 'RotBondsSMARTSMode'.
  195             TorsionLibraryFilePath (str): A XML file name containing data for
  196                 torsion starin energy library.
  197             AlertTorsionsNotObserved (bool): Issue alerts about molecules
  198                 containing torsion angles not observed in torsion strain energy
  199                 library.
  200 
  201         Returns:
  202             object: An instantiated class object.
  203 
  204         Examples:
  205 
  206             StrainEnergyAlertsHandle = TorsionStrainEnergyAlerts()
  207 
  208             StrainEnergyAlertsHandle = TorsionStrainEnergyAlerts(AlertsMode = "TotalEnergy",
  209                 TotalEnergyCutoff = 6.0, MaxSingleEnergyCutoff = 1.8, RotBondsSMARTSMode = "SemiStrict",
  210                 RotBondsSMARTSPattern = None, TorsionLibraryFilePath = "auto", AlertTorsionsNotObserved = False)
  211 
  212         Notes:
  213             The following sections provide additional details for the parameters.
  214 
  215             AlertsMode: Torsion strain energy library alert types to use for issuing
  216             alerts about molecules containing rotatable bonds based on the calculated
  217             values for the total torsion strain energy of a molecule and  the maximum
  218             single strain energy of a rotatable bond in a molecule.
  219 
  220             Possible values: TotalEnergy, MaxSingleEnergy, or TotalOrMaxSingleEnergy
  221 
  222             The strain energy cutoff values in terms of torsion energy units (TEUs) are
  223             used to filter molecules as shown below:
  224 
  225                 AlertsMode                AlertsEnergyCutoffs (TEUs)
  226 
  227                 TotalEnergy               >= TotalEnergyCutoff
  228 
  229                 MaxSingleEnergy           >= MaxSingleEnergyCutoff
  230 
  231                 TotalOrMaxSingleEnergy    >= TotalEnergyCutoff
  232                                           or >= MaxSingleEnergyCutoff
  233 
  234             TotalEnergyCutoff: Total strain strain energy (TEUs) cutoff [ Ref 153 ] for
  235             issuing alerts based on total strain energy for all rotatable bonds in a
  236             molecule. This option is used during 'TotalEnergy' or 'TotalOrMaxSingleEnergy'
  237             values of 'AlertsMode' parameter.
  238 
  239             The total strain energy must be greater than or equal to the specified
  240             cutoff value to identify a molecule containing strained torsions.
  241 
  242             MaxSingleEnergyCutoff: Maximum single strain energy (TEUs) cutoff [ Ref 153 ]
  243             for issuing alerts based on the maximum value of a single strain energy of a
  244             rotatable bond in  a molecule. This option is used during 'MaxSingleEnergy' or
  245             'TotalOrMaxSingleEnergy' values of 'AlertsMode' parameter.
  246 
  247             The maximum single strain energy must be greater than or equal to the
  248             specified cutoff valueo to identify a molecule containing strained torsions.
  249 
  250             RotBondsSMARTSMode: SMARTS pattern to use for identifying rotatable bonds in
  251             a molecule for matching against torsion rules in the torsion library. Possible values:
  252             NonStrict, SemiStrict, Strict or Specify. The rotatable bond SMARTS matches
  253             are filtered to ensure that each atom in the rotatable bond is attached to
  254             at least two heavy atoms.
  255 
  256             The following SMARTS patterns are used to identify rotatable bonds for
  257             different modes:
  258 
  259                 NonStrict: [!$(*#*)&!D1]-&!@[!$(*#*)&!D1]
  260 
  261                 SemiStrict:
  262                 [!$(*#*)&!D1&!$(C(F)(F)F)&!$(C(Cl)(Cl)Cl)&!$(C(Br)(Br)Br)
  263                 &!$(C([CH3])([CH3])[CH3])]-!@[!$(*#*)&!D1&!$(C(F)(F)F)
  264                 &!$(C(Cl)(Cl)Cl)&!$(C(Br)(Br)Br)&!$(C([CH3])([CH3])[CH3])]
  265 
  266                 Strict:
  267                 [!$(*#*)&!D1&!$(C(F)(F)F)&!$(C(Cl)(Cl)Cl)&!$(C(Br)(Br)Br)
  268                 &!$(C([CH3])([CH3])[CH3])&!$([CD3](=[N,O,S])-!@[#7,O,S!D1])
  269                 &!$([#7,O,S!D1]-!@[CD3]=[N,O,S])&!$([CD3](=[N+])-!@[#7!D1])
  270                 &!$([#7!D1]-!@[CD3]=[N+])]-!@[!$(*#*)&!D1&!$(C(F)(F)F)
  271                 &!$(C(Cl)(Cl)Cl)&!$(C(Br)(Br)Br)&!$(C([CH3])([CH3])[CH3])]
  272 
  273             The 'NonStrict' and 'Strict' SMARTS patterns are available in RDKit. The
  274             'NonStrict' SMARTS pattern corresponds to original Daylight SMARTS
  275             specification for rotatable bonds. The 'SemiStrict' SMARTS pattern is
  276             derived from 'Strict' SMARTS pattern.
  277 
  278             TorsionLibraryFilePath (str): Specify a XML file name containing data for
  279             torsion starin energy library hierarchy or use default file,
  280             TorsionEnergyLibrary.xml, available in the directory containing this file.
  281 
  282             AlertTorsionsNotObserved: Issue alerts abpout molecules con ntaining torsion
  283             angles not observed in torsion strain energy library. It's not possible to
  284             calculate torsion strain energies for these torsions during 'approximate'
  285             match to a specified torsion in the library.
  286 
  287             The 'approximate' strain energy calculation relies on the angle difference
  288             between a torsion angle and the torsion peaks observed for the torsion
  289             rules in the torsion energy library. The torsion angle is matched to a
  290             torsion peak based on the value of torsion angle difference. It must be
  291             less than or equal to the value for the second tolerance 'tolerance2'.
  292             Otherwise, the torsion angle is not observed in the torsion energy library
  293             and a value of 'NA' is assigned for torsion energy along with the lower and
  294             upper bounds on energy at 95% confidence interval.
  295 
  296         """
  297 
  298         self._ProcessTorsionStrainEnergyAlertsParameters(
  299             AlertsMode,
  300             TotalEnergyCutoff,
  301             MaxSingleEnergyCutoff,
  302             RotBondsSMARTSMode,
  303             RotBondsSMARTSPattern,
  304             TorsionLibraryFilePath,
  305             AlertTorsionsNotObserved,
  306         )
  307 
  308         self._InitializeTorsionStrainEnergyAlerts()
  309 
  310     def _ProcessTorsionStrainEnergyAlertsParameters(
  311         self,
  312         AlertsMode,
  313         TotalEnergyCutoff,
  314         MaxSingleEnergyCutoff,
  315         RotBondsSMARTSMode,
  316         RotBondsSMARTSPattern,
  317         TorsionLibraryFilePath,
  318         AlertTorsionsNotObserved,
  319     ):
  320         """Process torsion strain energy alerts paramaters."""
  321 
  322         # Process AltertsMode paramater...
  323         self._ProcessAlertsModeParameter(AlertsMode)
  324 
  325         # Process TotalEnergyCutoff parameter...
  326         if TotalEnergyCutoff <= 0.0:
  327             raise ValueError(
  328                 "The value, %s, specified for TotalEnergyCutoff parameter is not valid. Supported value: > 0.0"
  329                 % TotalEnergyCutoff
  330             )
  331         self.TotalEnergyCutoff = TotalEnergyCutoff
  332 
  333         # Process  MaxSingleEnergyCutoff parameter...
  334         if MaxSingleEnergyCutoff <= 0.0:
  335             raise ValueError(
  336                 "The value, %s, specified for MaxSingleEnergyCutoff parameter is not valid. Supported value: > 0.0"
  337                 % MaxSingleEnergyCutoff
  338             )
  339         self.MaxSingleEnergyCutoff = MaxSingleEnergyCutoff
  340 
  341         # Process RotBondsSMARTSMode and RotBondsSMARTSPattern parameters...
  342         self._ProcessRotBondsParameters(RotBondsSMARTSMode, RotBondsSMARTSPattern)
  343 
  344         # Process TorsionLibraryFilePath parameter...
  345         self._ProcessTorsionLibraryFilePathParameter(TorsionLibraryFilePath)
  346 
  347         # AlertTorsionsNotObserved parameter...
  348         self.AlertTorsionsNotObserved = AlertTorsionsNotObserved
  349 
  350     def _InitializeTorsionStrainEnergyAlerts(self):
  351         """Initialize tosion strain energy alerts."""
  352 
  353         # Retrieve and setup torsion library info for matching rotatable bonds...
  354         TorsionLibraryInfo = {}
  355 
  356         TorsionLibElementTree = TorsionAlertsUtil.RetrieveTorsionLibraryInfo(self.TorsionLibraryFilePath)
  357         TorsionLibraryInfo["TorsionLibElementTree"] = TorsionLibElementTree
  358 
  359         TorsionAlertsUtil.SetupTorsionLibraryInfoForMatchingRotatableBonds(TorsionLibraryInfo)
  360 
  361         self.TorsionLibraryInfo = TorsionLibraryInfo
  362 
  363     def _ProcessAlertsModeParameter(self, AlertsMode):
  364         """Process AlertsMode parameter."""
  365 
  366         TotalEnergyMode, MaxSingleEnergyMode, TotalOrMaxSingleEnergyMode = [False] * 3
  367         if re.match("^TotalEnergy$", AlertsMode, re.I):
  368             TotalEnergyMode = True
  369         elif re.match("^MaxSingleEnergy$", AlertsMode, re.I):
  370             MaxSingleEnergyMode = True
  371         elif re.match("^TotalOrMaxSingleEnergy$", AlertsMode, re.I):
  372             TotalOrMaxSingleEnergyMode = True
  373         else:
  374             raise ValueError(
  375                 "Invalid value, %s, specified for AlertsMode parameter. Valid values: TotalEnergy, MaxSingleEnergy, or TotalOrMaxSingleEnergy"
  376                 % (AlertsMode)
  377             )
  378 
  379         self.AltersMode = AlertsMode
  380         self.TotalEnergyMode = TotalEnergyMode
  381         self.MaxSingleEnergyMode = MaxSingleEnergyMode
  382         self.TotalOrMaxSingleEnergyMode = TotalOrMaxSingleEnergyMode
  383 
  384     def _ProcessRotBondsParameters(self, RotBondsSMARTSMode, RotBondsSMARTSPattern):
  385         """Process  RotBondsSMARTSMode and RotBondsSMARTSPattern parameters."""
  386 
  387         if re.match("^NonStrict$", RotBondsSMARTSMode, re.I):
  388             RotBondsSMARTSPattern = "[!$(*#*)&!D1]-&!@[!$(*#*)&!D1]"
  389         elif re.match("^SemiStrict$", RotBondsSMARTSMode, re.I):
  390             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])]"
  391         elif re.match("^Strict$", RotBondsSMARTSMode, re.I):
  392             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])]"
  393         elif re.match("^Specify$", RotBondsSMARTSMode, re.I):
  394             if RotBondsSMARTSPattern is None:
  395                 raise ValueError(
  396                     "The RotBondsSMARTSPattern parameter value must be specified during Specify value for RotBondsSMARTSMode parameter."
  397                 )
  398             RotBondsSMARTSPattern = RotBondsSMARTSPattern.strip()
  399             if not len(RotBondsSMARTSPattern):
  400                 raise ValueError("Empty value specified for  RotBondsSMARTSPattern parameter.")
  401         else:
  402             raise ValueError(
  403                 "Invalid value, %s, specified for RotBondsSMARTSMode parameter. Valid values: NonStrict, SemiStrict, Strict or Specify"
  404                 % (RotBondsSMARTSMode)
  405             )
  406 
  407         RotBondsPatternMol = Chem.MolFromSmarts(RotBondsSMARTSPattern)
  408         if RotBondsPatternMol is None:
  409             if re.match("Specify", RotBondsSMARTSMode, re.I):
  410                 raise ValueError(
  411                     'Failed to create rotatable bonds pattern molecule. The rotatable bonds SMARTS pattern, "%s", specified using RotBondsSMARTSPattern parameter is not valid.'
  412                     % (RotBondsSMARTSPattern)
  413                 )
  414             else:
  415                 raise ValueError(
  416                     'Failed to create rotatable bonds pattern molecule. The default rotatable bonds SMARTS pattern, "%s", used for %s value of RotBondsSMARTSMode parameter is not valid.'
  417                     % (RotBondsSMARTSPattern, RotBondsSMARTSMode)
  418                 )
  419 
  420         self.RotBondsSMARTSMode = RotBondsSMARTSMode
  421         self.RotBondsSMARTSPattern = RotBondsSMARTSPattern
  422         self.RotBondsPatternMol = RotBondsPatternMol
  423 
  424     def _ProcessTorsionLibraryFilePathParameter(self, TorsionLibraryFilePath):
  425         """Process torsion library path."""
  426 
  427         # TorsionLibraryFilePath parameter...
  428         if re.match("^auto$", TorsionLibraryFilePath):
  429             TorsionLibraryFile = "TorsionStrainEnergyLibrary.xml"
  430             TorsionLibraryFilePath = os.path.join(os.path.dirname(os.path.abspath(__file__)), TorsionLibraryFile)
  431             if not os.path.isfile(TorsionLibraryFilePath):
  432                 raise ValueError("The default torsion alerts library file %s doesn't exist." % TorsionLibraryFilePath)
  433         else:
  434             if not os.path.isfile(TorsionLibraryFilePath):
  435                 raise ValueError(
  436                     "The file specified, %s, for parameter TorsionLibraryFilePath doesn't exist."
  437                     % TorsionLibraryFilePath
  438                 )
  439             TorsionLibraryFilePath = os.path.abspath(TorsionLibraryFilePath)
  440 
  441         self.TorsionLibraryFilePath = TorsionLibraryFilePath
  442 
  443     def GetTorsionLibraryFilePath(self):
  444         """Get torsion strain library file path.
  445 
  446         Arguments:
  447             None
  448 
  449         Returns:
  450             FilePath (str): Torsion strain library path.
  451 
  452         """
  453 
  454         return self.TorsionLibraryFilePath
  455 
  456     def ListTorsionLibraryInfo(self):
  457         """List torsion strain library information.
  458 
  459         Arguments:
  460             None
  461 
  462         Returns:
  463             None
  464 
  465         """
  466 
  467         return TorsionAlertsUtil.ListTorsionLibraryInfo(self.TorsionLibraryInfo["TorsionLibElementTree"])
  468 
  469     def IdentifyTorsionLibraryAlertsForRotatableBonds(self, Mol):
  470         """Identify torsion strain library alerts for a molecule by matching rotatable
  471         bonds against SMARTS patterns specified for torsion rules in torsion energy
  472         library file.
  473 
  474         Arguments:
  475             Mol (object): RDKit molecule object.
  476 
  477         Returns:
  478             bool: True - Molecule contains strained torsions; False - Molecule
  479                 contains no strained torsions or rotatable bonds.
  480             dict or None: Torsion alerts information regarding matching of
  481                 rotatable bonds to torsion strain library.
  482 
  483         Examples:
  484 
  485             from TorsionAlerts.TorsionStrainEnergyAlerts import
  486                 TorsionStrainEnergyAlerts
  487 
  488             StrainEnergyAlerts = TorsionStrainEnergyAlerts()
  489             AlertsStatus, AlertsInfo = StrainEnergyAlerts.
  490                 IdentifyTorsionLibraryAlertsForRotatableBonds(RDKitMol)
  491 
  492             RotBondsAlertsStatus = AlertsInfo["RotBondsAlertsStatus"]
  493             TotalEnergy = AlertsInfo["TotalEnergy"]
  494             TotalEnergyLowerBound = AlertsInfo["TotalEnergyLowerBound"]
  495             TotalEnergyUpperBound = AlertsInfo["TotalEnergyUpperBound"]
  496             AnglesNotObservedCount = AlertsInfo["AnglesNotObservedCount"]
  497             MaxSingleEnergy = AlertsInfo["MaxSingleEnergy"])
  498             MaxSingleEnergyAlertsCount = AlertsInfo[
  499                 "MaxSingleEnergyAlertsCount"]
  500 
  501             # List of rotatable bond IDs...
  502             RotatableBondIDs = AlertsInfo["IDs"]
  503 
  504             # Dictionaries containing information for rotatable bonds by using
  505             # bond ID as key...
  506             for ID in AlertsInfo["IDs"]:
  507                 MatchStatus = AlertsInfo["MatchStatus"][ID]
  508                 MaxSingleEnergyAlertStatus = AlertsInfo[
  509                     "MaxSingleEnergyAlertStatus"][ID]
  510                 AtomIndices = AlertsInfo["AtomIndices"][ID]
  511                 TorsionAtomIndices = AlertsInfo["TorsionAtomIndices"][ID]
  512                 TorsionAngle = AlertsInfo["TorsionAngle"][ID]
  513                 HierarchyClassName = AlertsInfo["HierarchyClassName"][ID]
  514                 HierarchySubClassName = AlertsInfo["HierarchySubClassName"][ID]
  515                 TorsionRuleNodeID = AlertsInfo["TorsionRuleNodeID"][ID]
  516                 TorsionRuleSMARTS = AlertsInfo["TorsionRuleSMARTS"][ID]
  517                 EnergyMethod = AlertsInfo["EnergyMethod"][ID]
  518                 AngleNotObserved = AlertsInfo["AngleNotObserved"][ID]
  519                 Energy = AlertsInfo["Energy"][ID]
  520                 EnergyLowerBound = AlertsInfo["EnergyLowerBound"][ID]
  521                 EnergyUpperBound = AlertsInfo["EnergyUpperBound"][ID]
  522 
  523         """
  524 
  525         # Identify rotatable bonds...
  526         RotBondsStatus, RotBondsInfo = TorsionAlertsUtil.IdentifyRotatableBondsForTorsionLibraryMatch(
  527             self.TorsionLibraryInfo, Mol, self.RotBondsPatternMol
  528         )
  529 
  530         if not RotBondsStatus:
  531             return (False, None)
  532 
  533         # Identify alerts for rotatable bonds...
  534         RotBondsAlertsStatus, RotBondsAlertsInfo = self._MatchRotatableBondsToTorsionLibrary(Mol, RotBondsInfo)
  535 
  536         return (RotBondsAlertsStatus, RotBondsAlertsInfo)
  537 
  538     def _MatchRotatableBondsToTorsionLibrary(self, Mol, RotBondsInfo):
  539         """Match rotatable bond to torsion library."""
  540 
  541         # Initialize...
  542         RotBondsAlertsInfo = self._InitializeRotatableBondsAlertsInfo()
  543 
  544         # Match rotatable bonds to torsion library...
  545         for ID in RotBondsInfo["IDs"]:
  546             AtomIndices = RotBondsInfo["AtomIndices"][ID]
  547             HierarchyClass = RotBondsInfo["HierarchyClass"][ID]
  548 
  549             MatchStatus, MatchInfo = self._MatchRotatableBondToTorsionLibrary(Mol, AtomIndices, HierarchyClass)
  550             self._TrackRotatableBondsAlertsInfo(RotBondsAlertsInfo, ID, AtomIndices, MatchStatus, MatchInfo)
  551 
  552         RotBondsAlertsStatus = self._SetupRotatableBondsAlertStatusTotalStrainEnergiesInfo(RotBondsAlertsInfo)
  553 
  554         return (RotBondsAlertsStatus, RotBondsAlertsInfo)
  555 
  556     def _InitializeRotatableBondsAlertsInfo(self):
  557         """Initialize alerts information for rotatable bonds."""
  558 
  559         RotBondsAlertsInfo = {}
  560         RotBondsAlertsInfo["IDs"] = []
  561 
  562         for DataLabel in [
  563             "RotBondsAlertsStatus",
  564             "TotalEnergy",
  565             "TotalEnergyLowerBound",
  566             "TotalEnergyUpperBound",
  567             "AnglesNotObservedCount",
  568             "MaxSingleEnergy",
  569             "MaxSingleEnergyAlertsCount",
  570         ]:
  571             RotBondsAlertsInfo[DataLabel] = None
  572 
  573         for DataLabel in [
  574             "MatchStatus",
  575             "MaxSingleEnergyAlertStatus",
  576             "AtomIndices",
  577             "TorsionAtomIndices",
  578             "TorsionAngle",
  579             "HierarchyClassName",
  580             "HierarchySubClassName",
  581             "TorsionRuleNodeID",
  582             "TorsionRuleSMARTS",
  583             "EnergyMethod",
  584             "AngleNotObserved",
  585             "Energy",
  586             "EnergyLowerBound",
  587             "EnergyUpperBound",
  588         ]:
  589             RotBondsAlertsInfo[DataLabel] = {}
  590 
  591         return RotBondsAlertsInfo
  592 
  593     def _TrackRotatableBondsAlertsInfo(self, RotBondsAlertsInfo, ID, AtomIndices, MatchStatus, MatchInfo):
  594         """Track alerts information for rotatable bonds."""
  595 
  596         if MatchInfo is None or len(MatchInfo) == 0:
  597             (
  598                 TorsionAtomIndices,
  599                 TorsionAngle,
  600                 HierarchyClassName,
  601                 HierarchySubClassName,
  602                 TorsionRuleNodeID,
  603                 TorsionRuleSMARTS,
  604                 EnergyMethod,
  605                 AngleNotObserved,
  606                 Energy,
  607                 EnergyLowerBound,
  608                 EnergyUpperBound,
  609             ) = [None] * 11
  610         else:
  611             (
  612                 TorsionAtomIndices,
  613                 TorsionAngle,
  614                 HierarchyClassName,
  615                 HierarchySubClassName,
  616                 TorsionRuleNodeID,
  617                 TorsionRuleSMARTS,
  618                 EnergyMethod,
  619                 AngleNotObserved,
  620                 Energy,
  621                 EnergyLowerBound,
  622                 EnergyUpperBound,
  623             ) = MatchInfo
  624 
  625         # Track torsion match information...
  626         RotBondsAlertsInfo["IDs"].append(ID)
  627         RotBondsAlertsInfo["MatchStatus"][ID] = MatchStatus
  628         RotBondsAlertsInfo["AtomIndices"][ID] = AtomIndices
  629         RotBondsAlertsInfo["TorsionAtomIndices"][ID] = TorsionAtomIndices
  630         RotBondsAlertsInfo["TorsionAngle"][ID] = TorsionAngle
  631         RotBondsAlertsInfo["HierarchyClassName"][ID] = HierarchyClassName
  632         RotBondsAlertsInfo["HierarchySubClassName"][ID] = HierarchySubClassName
  633         RotBondsAlertsInfo["TorsionRuleNodeID"][ID] = TorsionRuleNodeID
  634         RotBondsAlertsInfo["TorsionRuleSMARTS"][ID] = TorsionRuleSMARTS
  635         RotBondsAlertsInfo["EnergyMethod"][ID] = EnergyMethod
  636         RotBondsAlertsInfo["AngleNotObserved"][ID] = AngleNotObserved
  637         RotBondsAlertsInfo["Energy"][ID] = Energy
  638         RotBondsAlertsInfo["EnergyLowerBound"][ID] = EnergyLowerBound
  639         RotBondsAlertsInfo["EnergyUpperBound"][ID] = EnergyUpperBound
  640 
  641     def _SetupRotatableBondsAlertStatusTotalStrainEnergiesInfo(self, RotBondsAlertsInfo):
  642         """Setup rotatable bonds alert status along with total strain energies."""
  643 
  644         # Initialize...
  645         RotBondsAlertsStatus = False
  646         TotalEnergy, TotalEnergyLowerBound, TotalEnergyUpperBound, AnglesNotObservedCount = [None, None, None, None]
  647         MaxSingleEnergy, MaxSingleEnergyAlertsCount = [None, None]
  648 
  649         # Initialize max single energy alert status...
  650         for ID in RotBondsAlertsInfo["IDs"]:
  651             RotBondsAlertsInfo["MaxSingleEnergyAlertStatus"][ID] = None
  652 
  653         # Check for torsion angles not obervered in the strain library...
  654         AnglesNotObservedCount = 0
  655         for ID in RotBondsAlertsInfo["IDs"]:
  656             AngleNotObserved = RotBondsAlertsInfo["AngleNotObserved"][ID]
  657             if AngleNotObserved is not None and AngleNotObserved:
  658                 AnglesNotObservedCount += 1
  659 
  660         # Setup alert status for rotable bonds...
  661         if AnglesNotObservedCount > 0:
  662             if self.AlertTorsionsNotObserved:
  663                 RotBondsAlertsStatus = True
  664         else:
  665             TotalEnergy = 0.0
  666             for ID in RotBondsAlertsInfo["IDs"]:
  667                 Energy = RotBondsAlertsInfo["Energy"][ID]
  668                 TotalEnergy += Energy
  669                 if self.TotalEnergyMode:
  670                     if TotalEnergy > self.TotalEnergyCutoff:
  671                         RotBondsAlertsStatus = True
  672                         break
  673                 elif self.MaxSingleEnergyMode:
  674                     if Energy > self.MaxSingleEnergyCutoff:
  675                         RotBondsAlertsStatus = True
  676                         break
  677                 elif self.TotalOrMaxSingleEnergyMode:
  678                     if TotalEnergy > self.TotalEnergyCutoff or Energy > self.MaxSingleEnergyCutoff:
  679                         RotBondsAlertsStatus = True
  680                         break
  681 
  682             # Setup energy infomation...
  683             TotalEnergy, TotalEnergyLowerBound, TotalEnergyUpperBound = [0.0, 0.0, 0.0]
  684             if self.MaxSingleEnergyMode or self.TotalOrMaxSingleEnergyMode:
  685                 MaxSingleEnergy, MaxSingleEnergyAlertsCount = [0.0, 0]
  686 
  687             for ID in RotBondsAlertsInfo["IDs"]:
  688                 Energy = RotBondsAlertsInfo["Energy"][ID]
  689 
  690                 # Setup total energy along with the lower and upper bounds...
  691                 TotalEnergy += Energy
  692                 TotalEnergyLowerBound += RotBondsAlertsInfo["EnergyLowerBound"][ID]
  693                 TotalEnergyUpperBound += RotBondsAlertsInfo["EnergyUpperBound"][ID]
  694 
  695                 # Setup max single energy and max single energy alerts count...
  696                 if self.MaxSingleEnergyMode or self.TotalOrMaxSingleEnergyMode:
  697                     MaxSingleEnergyAlertStatus = False
  698 
  699                     if Energy > MaxSingleEnergy:
  700                         MaxSingleEnergy = Energy
  701                         if Energy > self.MaxSingleEnergyCutoff:
  702                             MaxSingleEnergyAlertStatus = True
  703                             MaxSingleEnergyAlertsCount += 1
  704 
  705                     RotBondsAlertsInfo["MaxSingleEnergyAlertStatus"][ID] = MaxSingleEnergyAlertStatus
  706 
  707         RotBondsAlertsInfo["RotBondsAlertsStatus"] = RotBondsAlertsStatus
  708 
  709         RotBondsAlertsInfo["TotalEnergy"] = TotalEnergy
  710         RotBondsAlertsInfo["TotalEnergyLowerBound"] = TotalEnergyLowerBound
  711         RotBondsAlertsInfo["TotalEnergyUpperBound"] = TotalEnergyUpperBound
  712 
  713         RotBondsAlertsInfo["AnglesNotObservedCount"] = AnglesNotObservedCount
  714 
  715         RotBondsAlertsInfo["MaxSingleEnergy"] = MaxSingleEnergy
  716         RotBondsAlertsInfo["MaxSingleEnergyAlertsCount"] = MaxSingleEnergyAlertsCount
  717 
  718         return RotBondsAlertsStatus
  719 
  720     def _MatchRotatableBondToTorsionLibrary(self, Mol, RotBondAtomIndices, RotBondHierarchyClass):
  721         """Match rotatable bond to torsion library."""
  722 
  723         if TorsionAlertsUtil.IsSpecificHierarchyClass(self.TorsionLibraryInfo, RotBondHierarchyClass):
  724             MatchStatus, MatchInfo = self._MatchRotatableBondAgainstSpecificHierarchyClass(
  725                 Mol, RotBondAtomIndices, RotBondHierarchyClass
  726             )
  727             if not MatchStatus:
  728                 MatchStatus, MatchInfo = self._MatchRotatableBondAgainstGenericHierarchyClass(
  729                     Mol, RotBondAtomIndices, RotBondHierarchyClass
  730                 )
  731         else:
  732             MatchStatus, MatchInfo = self._MatchRotatableBondAgainstGenericHierarchyClass(
  733                 Mol, RotBondAtomIndices, RotBondHierarchyClass
  734             )
  735 
  736         return (MatchStatus, MatchInfo)
  737 
  738     def _MatchRotatableBondAgainstSpecificHierarchyClass(self, Mol, RotBondAtomIndices, RotBondHierarchyClass):
  739         """Match rotatable bond against a specific hierarchy class."""
  740 
  741         TorsionLibraryInfo = self.TorsionLibraryInfo
  742 
  743         HierarchyClassElementNode = None
  744         if RotBondHierarchyClass in TorsionLibraryInfo["SpecificClasses"]["ElementNode"]:
  745             HierarchyClassElementNode = TorsionLibraryInfo["SpecificClasses"]["ElementNode"][RotBondHierarchyClass]
  746 
  747         if HierarchyClassElementNode is None:
  748             return (False, None, None, None)
  749 
  750         TorsionAlertsUtil.TrackHierarchyClassElementNode(TorsionLibraryInfo, HierarchyClassElementNode)
  751         MatchStatus, MatchInfo = self._ProcessElementForRotatableBondMatch(
  752             Mol, RotBondAtomIndices, HierarchyClassElementNode
  753         )
  754         TorsionAlertsUtil.RemoveLastHierarchyClassElementNodeFromTracking(TorsionLibraryInfo)
  755 
  756         return (MatchStatus, MatchInfo)
  757 
  758     def _MatchRotatableBondAgainstGenericHierarchyClass(self, Mol, RotBondAtomIndices, RotBondHierarchyClass):
  759         """Match rotatable bond against a generic hierarchy class."""
  760 
  761         TorsionLibraryInfo = self.TorsionLibraryInfo
  762 
  763         HierarchyClassElementNode = TorsionAlertsUtil.GetGenericHierarchyClassElementNode(TorsionLibraryInfo)
  764         if HierarchyClassElementNode is None:
  765             return (False, None)
  766 
  767         TorsionAlertsUtil.TrackHierarchyClassElementNode(TorsionLibraryInfo, HierarchyClassElementNode)
  768 
  769         #  Match hierarchy subclasses before matching torsion rules...
  770         MatchStatus, MatchInfo = self._MatchRotatableBondAgainstGenericHierarchySubClasses(
  771             Mol, RotBondAtomIndices, HierarchyClassElementNode
  772         )
  773 
  774         if not MatchStatus:
  775             MatchStatus, MatchInfo = self._MatchRotatableBondAgainstGenericHierarchyTorsionRules(
  776                 Mol, RotBondAtomIndices, HierarchyClassElementNode
  777             )
  778 
  779         TorsionAlertsUtil.RemoveLastHierarchyClassElementNodeFromTracking(TorsionLibraryInfo)
  780 
  781         return (MatchStatus, MatchInfo)
  782 
  783     def _MatchRotatableBondAgainstGenericHierarchySubClasses(self, Mol, RotBondAtomIndices, HierarchyClassElementNode):
  784         """Match rotatable bond againat generic hierarchy subclasses."""
  785 
  786         for ElementChildNode in HierarchyClassElementNode:
  787             if ElementChildNode.tag != "hierarchySubClass":
  788                 continue
  789 
  790             SubClassMatchStatus = self._ProcessHierarchySubClassElementForRotatableBondMatch(
  791                 Mol, RotBondAtomIndices, ElementChildNode
  792             )
  793 
  794             if SubClassMatchStatus:
  795                 MatchStatus, MatchInfo = self._ProcessElementForRotatableBondMatch(
  796                     Mol, RotBondAtomIndices, ElementChildNode
  797                 )
  798 
  799                 if MatchStatus:
  800                     return (MatchStatus, MatchInfo)
  801 
  802         return (False, None)
  803 
  804     def _MatchRotatableBondAgainstGenericHierarchyTorsionRules(
  805         self, Mol, RotBondAtomIndices, HierarchyClassElementNode
  806     ):
  807         """Match rotatable bond againat torsion rules generic hierarchy class."""
  808 
  809         for ElementChildNode in HierarchyClassElementNode:
  810             if ElementChildNode.tag != "torsionRule":
  811                 continue
  812 
  813             MatchStatus, MatchInfo = self._ProcessTorsionRuleElementForRotatableBondMatch(
  814                 Mol, RotBondAtomIndices, ElementChildNode
  815             )
  816 
  817             if MatchStatus:
  818                 return (MatchStatus, MatchInfo)
  819 
  820         return (False, None)
  821 
  822     def _ProcessElementForRotatableBondMatch(self, Mol, RotBondAtomIndices, ElementNode):
  823         """Process element node to recursively match rotatable bond against hierarchy
  824         subclasses and torsion rules."""
  825 
  826         TorsionLibraryInfo = self.TorsionLibraryInfo
  827 
  828         for ElementChildNode in ElementNode:
  829             if ElementChildNode.tag == "hierarchySubClass":
  830                 SubClassMatchStatus = self._ProcessHierarchySubClassElementForRotatableBondMatch(
  831                     Mol, RotBondAtomIndices, ElementChildNode
  832                 )
  833 
  834                 if SubClassMatchStatus:
  835                     TorsionAlertsUtil.TrackHierarchySubClassElementNode(TorsionLibraryInfo, ElementChildNode)
  836 
  837                     MatchStatus, MatchInfo = self._ProcessElementForRotatableBondMatch(
  838                         Mol, RotBondAtomIndices, ElementChildNode
  839                     )
  840                     if MatchStatus:
  841                         TorsionAlertsUtil.RemoveLastHierarchySubClassElementNodeFromTracking(TorsionLibraryInfo)
  842                         return (MatchStatus, MatchInfo)
  843 
  844                     TorsionAlertsUtil.RemoveLastHierarchySubClassElementNodeFromTracking(TorsionLibraryInfo)
  845 
  846             elif ElementChildNode.tag == "torsionRule":
  847                 MatchStatus, MatchInfo = self._ProcessTorsionRuleElementForRotatableBondMatch(
  848                     Mol, RotBondAtomIndices, ElementChildNode
  849                 )
  850 
  851                 if MatchStatus:
  852                     return (MatchStatus, MatchInfo)
  853 
  854         return (False, None)
  855 
  856     def _ProcessHierarchySubClassElementForRotatableBondMatch(self, Mol, RotBondAtomIndices, ElementNode):
  857         """Process hierarchy subclass element to match rotatable bond."""
  858 
  859         # Setup subclass SMARTS pattern mol...
  860         SubClassPatternMol = TorsionAlertsUtil.SetupHierarchySubClassElementPatternMol(
  861             self.TorsionLibraryInfo, ElementNode
  862         )
  863         if SubClassPatternMol is None:
  864             return False
  865 
  866         # Match SMARTS pattern...
  867         SubClassPatternMatches = TorsionAlertsUtil.FilterSubstructureMatchesByAtomMapNumbers(
  868             Mol, SubClassPatternMol, Mol.GetSubstructMatches(SubClassPatternMol, useChirality=False)
  869         )
  870         if len(SubClassPatternMatches) == 0:
  871             return False
  872 
  873         # Match rotatable bond indices...
  874         RotBondAtomIndex1, RotBondAtomIndex2 = RotBondAtomIndices
  875         MatchStatus = False
  876         for SubClassPatternMatch in SubClassPatternMatches:
  877             if len(SubClassPatternMatch) == 2:
  878                 # Matched to pattern containing map atom numbers ":2" and ":3"...
  879                 CentralAtomsIndex1, CentralAtomsIndex2 = SubClassPatternMatch
  880             elif len(SubClassPatternMatch) == 4:
  881                 # Matched to pattern containing map atom numbers ":1", ":2", ":3" and ":4"...
  882                 CentralAtomsIndex1 = SubClassPatternMatch[1]
  883                 CentralAtomsIndex2 = SubClassPatternMatch[2]
  884             elif len(SubClassPatternMatch) == 3:
  885                 SubClassSMARTSPattern = ElementNode.get("smarts")
  886                 if TorsionAlertsUtil.DoesSMARTSContainsMappedAtoms(SubClassSMARTSPattern, [":2", ":3", ":4"]):
  887                     # Matched to pattern containing map atom numbers ":2", ":3" and ":4"...
  888                     CentralAtomsIndex1 = SubClassPatternMatch[0]
  889                     CentralAtomsIndex2 = SubClassPatternMatch[1]
  890                 else:
  891                     # Matched to pattern containing map atom numbers ":1", ":2" and ":3"...
  892                     CentralAtomsIndex1 = SubClassPatternMatch[1]
  893                     CentralAtomsIndex2 = SubClassPatternMatch[2]
  894             else:
  895                 continue
  896 
  897             if CentralAtomsIndex1 != CentralAtomsIndex2:
  898                 if (CentralAtomsIndex1 == RotBondAtomIndex1 and CentralAtomsIndex2 == RotBondAtomIndex2) or (
  899                     CentralAtomsIndex1 == RotBondAtomIndex2 and CentralAtomsIndex2 == RotBondAtomIndex1
  900                 ):
  901                     MatchStatus = True
  902                     break
  903 
  904         return MatchStatus
  905 
  906     def _ProcessTorsionRuleElementForRotatableBondMatch(self, Mol, RotBondAtomIndices, ElementNode):
  907         """Process torsion rule element to match rotatable bond."""
  908 
  909         TorsionLibraryInfo = self.TorsionLibraryInfo
  910 
  911         #  Retrieve torsions matched to rotatable bond...
  912         TorsionAtomIndicesList, TorsionAnglesList = self._MatchTorsionRuleToRotatableBond(
  913             Mol, RotBondAtomIndices, ElementNode
  914         )
  915         if TorsionAtomIndicesList is None:
  916             return (False, None)
  917 
  918         # Setup torsion angles and enery bin information for matched torsion rule...
  919         TorsionRuleAnglesInfo = TorsionAlertsUtil.SetupTorsionRuleAnglesInfo(TorsionLibraryInfo, ElementNode)
  920         if TorsionRuleAnglesInfo is None:
  921             return (False, None)
  922 
  923         # Setup highest strain energy for matched torsions...
  924         TorsionAtomIndices, TorsionAngle, EnergyMethod, AngleNotObserved, Energy, EnergyLowerBound, EnergyUpperBound = (
  925             self._SelectHighestStrainEnergyTorsionForRotatableBond(
  926                 TorsionRuleAnglesInfo, TorsionAtomIndicesList, TorsionAnglesList
  927             )
  928         )
  929 
  930         # Setup hierarchy class and subclass names...
  931         HierarchyClassName, HierarchySubClassName = (
  932             TorsionAlertsUtil.SetupHierarchyClassAndSubClassNamesForRotatableBond(TorsionLibraryInfo)
  933         )
  934 
  935         # Setup rule node ID...
  936         TorsionRuleNodeID = ElementNode.get("NodeID")
  937 
  938         # Setup SMARTS...
  939         TorsionRuleSMARTS = ElementNode.get("smarts")
  940         if " " in TorsionRuleSMARTS:
  941             TorsionRuleSMARTS = TorsionRuleSMARTS.replace(" ", "")
  942 
  943         # Setup match info...
  944         MatchInfo = [
  945             TorsionAtomIndices,
  946             TorsionAngle,
  947             HierarchyClassName,
  948             HierarchySubClassName,
  949             TorsionRuleNodeID,
  950             TorsionRuleSMARTS,
  951             EnergyMethod,
  952             AngleNotObserved,
  953             Energy,
  954             EnergyLowerBound,
  955             EnergyUpperBound,
  956         ]
  957 
  958         # Setup match status...
  959         MatchStatus = True
  960 
  961         return (MatchStatus, MatchInfo)
  962 
  963     def _SelectHighestStrainEnergyTorsionForRotatableBond(
  964         self, TorsionRuleAnglesInfo, TorsionAtomIndicesList, TorsionAnglesList
  965     ):
  966         """Select highest strain energy torsion matched to a rotatable bond."""
  967 
  968         TorsionAtomIndices, TorsionAngle, EnergyMethod, AngleNotObserved, Energy, EnergyLowerBound, EnergyUpperBound = [
  969             None
  970         ] * 7
  971         ValidEnergyValue, ValidCurrentEnergyValue = [False] * 2
  972 
  973         FirstTorsion = True
  974         for Index in range(0, len(TorsionAtomIndicesList)):
  975             CurrentTorsionAtomIndices = TorsionAtomIndicesList[Index]
  976             CurrentTorsionAngle = TorsionAnglesList[Index]
  977 
  978             if FirstTorsion:
  979                 FirstTorsion = False
  980                 EnergyMethod, AngleNotObserved, Energy, EnergyLowerBound, EnergyUpperBound = (
  981                     self._SetupStrainEnergyForRotatableBond(TorsionRuleAnglesInfo, CurrentTorsionAngle)
  982                 )
  983                 TorsionAtomIndices = CurrentTorsionAtomIndices
  984                 TorsionAngle = CurrentTorsionAngle
  985                 ValidEnergyValue = self._IsEnergyValueValid(Energy)
  986                 continue
  987 
  988             # Select highest strain energy...
  989             (
  990                 CurrentEnergyMethod,
  991                 CurrentAngleNotObserved,
  992                 CurrentEnergy,
  993                 CurrentEnergyLowerBound,
  994                 CurrentEnergyUpperBound,
  995             ) = self._SetupStrainEnergyForRotatableBond(TorsionRuleAnglesInfo, CurrentTorsionAngle)
  996             ValidCurrentEnergyValue = self._IsEnergyValueValid(CurrentEnergy)
  997 
  998             UpdateValues = False
  999             if ValidEnergyValue and ValidCurrentEnergyValue:
 1000                 if CurrentEnergy > Energy:
 1001                     UpdateValues = True
 1002             elif ValidCurrentEnergyValue:
 1003                 if not ValidEnergyValue:
 1004                     UpdateValues = True
 1005 
 1006             if UpdateValues:
 1007                 EnergyMethod, AngleNotObserved, Energy, EnergyLowerBound, EnergyUpperBound = [
 1008                     CurrentEnergyMethod,
 1009                     CurrentAngleNotObserved,
 1010                     CurrentEnergy,
 1011                     CurrentEnergyLowerBound,
 1012                     CurrentEnergyUpperBound,
 1013                 ]
 1014                 TorsionAtomIndices = CurrentTorsionAtomIndices
 1015                 TorsionAngle = CurrentTorsionAngle
 1016 
 1017         return (
 1018             TorsionAtomIndices,
 1019             TorsionAngle,
 1020             EnergyMethod,
 1021             AngleNotObserved,
 1022             Energy,
 1023             EnergyLowerBound,
 1024             EnergyUpperBound,
 1025         )
 1026 
 1027     def _IsEnergyValueValid(self, Value):
 1028         """Check for valid energy value."""
 1029 
 1030         return False if (Value is None or math.isnan(Value) or math.isinf(Value)) else True
 1031 
 1032     def _SetupStrainEnergyForRotatableBond(self, TorsionRuleAnglesInfo, TorsionAngle):
 1033         """Setup strain energy for rotatable bond."""
 1034 
 1035         if TorsionRuleAnglesInfo["EnergyMethodExact"]:
 1036             return self._SetupStrainEnergyForRotatableBondByExactMethod(TorsionRuleAnglesInfo, TorsionAngle)
 1037         elif TorsionRuleAnglesInfo["EnergyMethodApproximate"]:
 1038             return self._SetupStrainEnergyForRotatableBondByApproximateMethod(TorsionRuleAnglesInfo, TorsionAngle)
 1039         else:
 1040             EnergyMethod, AngleNotObserved, Energy, EnergyLowerBound, EnergyUpperBound = [None, None, None, None, None]
 1041             return (EnergyMethod, AngleNotObserved, Energy, EnergyLowerBound, EnergyUpperBound)
 1042 
 1043     def _SetupStrainEnergyForRotatableBondByExactMethod(self, TorsionRuleAnglesInfo, TorsionAngle):
 1044         """Setup strain energy for rotatable bond by exact method."""
 1045 
 1046         EnergyMethod, AngleNotObserved, Energy, EnergyLowerBound, EnergyUpperBound = ["Exact", None, None, None, None]
 1047 
 1048         # Map angle to energy bin numbers...
 1049         BinNum = math.ceil(TorsionAngle / 10) + 17
 1050         PreviousBinNum = (BinNum + 35) % 36
 1051 
 1052         # Bin angle from -170 to 180 by the right end points...
 1053         BinAngleRightSide = (BinNum - 17) * 10
 1054 
 1055         # Angle offset towards the left of the bin from the right end point...
 1056         AngleOffset = TorsionAngle - BinAngleRightSide
 1057 
 1058         BinEnergy = TorsionRuleAnglesInfo["HistogramEnergy"][BinNum]
 1059         PreviousBinEnergy = TorsionRuleAnglesInfo["HistogramEnergy"][PreviousBinNum]
 1060         Energy = BinEnergy + (BinEnergy - PreviousBinEnergy) / 10.0 * AngleOffset
 1061 
 1062         BinEnergyLowerBound = TorsionRuleAnglesInfo["HistogramEnergyLowerBound"][BinNum]
 1063         PreviousBinEnergyLowerBound = TorsionRuleAnglesInfo["HistogramEnergyLowerBound"][PreviousBinNum]
 1064         EnergyLowerBound = (
 1065             BinEnergyLowerBound + (BinEnergyLowerBound - PreviousBinEnergyLowerBound) / 10.0 * AngleOffset
 1066         )
 1067 
 1068         BinEnergyUpperBound = TorsionRuleAnglesInfo["HistogramEnergyUpperBound"][BinNum]
 1069         PreviousBinEnergyUpperBound = TorsionRuleAnglesInfo["HistogramEnergyUpperBound"][PreviousBinNum]
 1070         EnergyUpperBound = (
 1071             BinEnergyUpperBound + (BinEnergyUpperBound - PreviousBinEnergyUpperBound) / 10.0 * AngleOffset
 1072         )
 1073 
 1074         return (EnergyMethod, AngleNotObserved, Energy, EnergyLowerBound, EnergyUpperBound)
 1075 
 1076     def _SetupStrainEnergyForRotatableBondByApproximateMethod(self, TorsionRuleAnglesInfo, TorsionAngle):
 1077         """Setup strain energy for rotatable bond by approximate method."""
 1078 
 1079         EnergyMethod, AngleNotObserved, Energy, EnergyLowerBound, EnergyUpperBound = [
 1080             "Approximate",
 1081             True,
 1082             None,
 1083             None,
 1084             None,
 1085         ]
 1086 
 1087         for AngleID in TorsionRuleAnglesInfo["IDs"]:
 1088             Tolerance2 = TorsionRuleAnglesInfo["Tolerance2"][AngleID]
 1089             Theta0 = TorsionRuleAnglesInfo["Theta0"][AngleID]
 1090 
 1091             AngleDiff = TorsionAlertsUtil.CalculateTorsionAngleDifference(TorsionAngle, Theta0)
 1092             if abs(AngleDiff) <= Tolerance2:
 1093                 Beta1 = TorsionRuleAnglesInfo["Beta1"][AngleID]
 1094                 Beta2 = TorsionRuleAnglesInfo["Beta2"][AngleID]
 1095 
 1096                 Energy = Beta1 * (AngleDiff**2) + Beta2 * (AngleDiff**4)
 1097 
 1098                 # Estimates of lower and upper bound are not available for
 1099                 # approximate method...
 1100                 EnergyLowerBound = Energy
 1101                 EnergyUpperBound = Energy
 1102 
 1103                 AngleNotObserved = False
 1104 
 1105                 break
 1106 
 1107         return (EnergyMethod, AngleNotObserved, Energy, EnergyLowerBound, EnergyUpperBound)
 1108 
 1109     def _MatchTorsionRuleToRotatableBond(self, Mol, RotBondAtomIndices, ElementNode):
 1110         """Retrieve matched torsions for torsion rule matched to rotatable bond."""
 1111 
 1112         # Get torsion matches...
 1113         TorsionMatches = self._GetMatchesForTorsionRule(Mol, ElementNode)
 1114         if TorsionMatches is None or len(TorsionMatches) == 0:
 1115             return (None, None)
 1116 
 1117         # Identify all torsion matches corresponding to central atoms in RotBondAtomIndices...
 1118         RotBondAtomIndex1, RotBondAtomIndex2 = RotBondAtomIndices
 1119         RotBondTorsionMatches, RotBondTorsionAngles = [None] * 2
 1120 
 1121         for TorsionMatch in TorsionMatches:
 1122             CentralAtomIndex1 = TorsionMatch[1]
 1123             CentralAtomIndex2 = TorsionMatch[2]
 1124 
 1125             if (CentralAtomIndex1 == RotBondAtomIndex1 and CentralAtomIndex2 == RotBondAtomIndex2) or (
 1126                 CentralAtomIndex1 == RotBondAtomIndex2 and CentralAtomIndex2 == RotBondAtomIndex1
 1127             ):
 1128                 TorsionAngle = self._CalculateTorsionAngle(Mol, TorsionMatch)
 1129                 if RotBondTorsionMatches is None:
 1130                     RotBondTorsionMatches = []
 1131                     RotBondTorsionAngles = []
 1132                 RotBondTorsionMatches.append(TorsionMatch)
 1133                 RotBondTorsionAngles.append(TorsionAngle)
 1134 
 1135         return (RotBondTorsionMatches, RotBondTorsionAngles)
 1136 
 1137     def _CalculateTorsionAngle(self, Mol, TorsionMatch):
 1138         """Calculate torsion angle."""
 1139 
 1140         # Calculate torsion angle using torsion atom indices..
 1141         MolConf = Mol.GetConformer(0)
 1142         TorsionAngle = rdMolTransforms.GetDihedralDeg(
 1143             MolConf, TorsionMatch[0], TorsionMatch[1], TorsionMatch[2], TorsionMatch[3]
 1144         )
 1145         TorsionAngle = round(TorsionAngle, 2)
 1146 
 1147         return TorsionAngle
 1148 
 1149     def _GetMatchesForTorsionRule(self, Mol, ElementNode):
 1150         """Get matches for torsion rule."""
 1151 
 1152         # Match torsions...
 1153         TorsionMatches = self._GetSubstructureMatchesForTorsionRule(Mol, ElementNode)
 1154 
 1155         if TorsionMatches is None or len(TorsionMatches) == 0:
 1156             return TorsionMatches
 1157 
 1158         # Filter torsion matches...
 1159         FiltertedTorsionMatches = []
 1160         for TorsionMatch in TorsionMatches:
 1161             if len(TorsionMatch) != 4:
 1162                 continue
 1163 
 1164             # Ignore matches containing hydrogen atoms as first or last atom...
 1165             if Mol.GetAtomWithIdx(TorsionMatch[0]).GetAtomicNum() == 1:
 1166                 continue
 1167             if Mol.GetAtomWithIdx(TorsionMatch[3]).GetAtomicNum() == 1:
 1168                 continue
 1169 
 1170             FiltertedTorsionMatches.append(TorsionMatch)
 1171 
 1172         return FiltertedTorsionMatches
 1173 
 1174     def _GetSubstructureMatchesForTorsionRule(self, Mol, ElementNode):
 1175         """Get substructure matches for a torsion rule."""
 1176 
 1177         # Setup torsion rule SMARTS pattern mol....
 1178         TorsionRuleNodeID = ElementNode.get("NodeID")
 1179         TorsionSMARTSPattern = ElementNode.get("smarts")
 1180         TorsionPatternMol = TorsionAlertsUtil.SetupTorsionRuleElementPatternMol(
 1181             self.TorsionLibraryInfo, ElementNode, TorsionRuleNodeID, TorsionSMARTSPattern
 1182         )
 1183         if TorsionPatternMol is None:
 1184             return None
 1185 
 1186         # Match torsions...
 1187         TorsionMatches = TorsionAlertsUtil.FilterSubstructureMatchesByAtomMapNumbers(
 1188             Mol, TorsionPatternMol, Mol.GetSubstructMatches(TorsionPatternMol, useChirality=False)
 1189         )
 1190 
 1191         return TorsionMatches