MayaChemTools

    1 #!/bin/env python
    2 #
    3 # File: RDKitFilterTorsionStrainEnergyAlerts.py
    4 # Author: Manish Sud <msud@san.rr.com>
    5 #
    6 # Collaborator: Pat Walters
    7 #
    8 # Copyright (C) 2026 Manish Sud. All rights reserved.
    9 #
   10 # This script uses the torsion strain energy library developed by Gu, S.;
   11 # Smith, M. S.; Yang, Y.; Irwin, J. J.; Shoichet, B. K. [ Ref 153 ].
   12 #
   13 # The torsion strain enegy library is based on the Torsion Library jointly
   14 # developed by the University of Hamburg, Center for Bioinformatics,
   15 # Hamburg, Germany and F. Hoffmann-La-Roche Ltd., Basel, Switzerland.
   16 #
   17 # The functionality available in this script is implemented using RDKit, an
   18 # open source toolkit for cheminformatics developed by Greg Landrum.
   19 #
   20 # This file is part of MayaChemTools.
   21 #
   22 # MayaChemTools is free software; you can redistribute it and/or modify it under
   23 # the terms of the GNU Lesser General Public License as published by the Free
   24 # Software Foundation; either version 3 of the License, or (at your option) any
   25 # later version.
   26 #
   27 # MayaChemTools is distributed in the hope that it will be useful, but without
   28 # any warranty; without even the implied warranty of merchantability of fitness
   29 # for a particular purpose.  See the GNU Lesser General Public License for more
   30 # details.
   31 #
   32 # You should have received a copy of the GNU Lesser General Public License
   33 # along with MayaChemTools; if not, see <http://www.gnu.org/licenses/> or
   34 # write to the Free Software Foundation Inc., 59 Temple Place, Suite 330,
   35 # Boston, MA, 02111-1307, USA.
   36 #
   37 
   38 from __future__ import print_function
   39 
   40 import os
   41 import sys
   42 import time
   43 import re
   44 import glob
   45 import multiprocessing as mp
   46 import math
   47 
   48 # RDKit imports...
   49 try:
   50     from rdkit import rdBase
   51     from rdkit import Chem
   52 except ImportError as ErrMsg:
   53     sys.stderr.write("\nFailed to import RDKit module/package: %s\n" % ErrMsg)
   54     sys.stderr.write("Check/update your RDKit environment and try again.\n\n")
   55     sys.exit(1)
   56 
   57 # MayaChemTools imports...
   58 sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), "..", "lib", "Python"))
   59 try:
   60     from docopt import docopt
   61     import MiscUtil
   62     import RDKitUtil
   63     from TorsionAlerts.TorsionStrainEnergyAlerts import TorsionStrainEnergyAlerts
   64 except ImportError as ErrMsg:
   65     sys.stderr.write("\nFailed to import MayaChemTools module/package: %s\n" % ErrMsg)
   66     sys.stderr.write("Check/update your MayaChemTools environment and try again.\n\n")
   67     sys.exit(1)
   68 
   69 ScriptName = os.path.basename(sys.argv[0])
   70 Options = {}
   71 OptionsInfo = {}
   72 
   73 
   74 def main():
   75     """Start execution of the script."""
   76 
   77     MiscUtil.PrintInfo(
   78         "\n%s (RDKit v%s; MayaChemTools v%s; %s): Starting...\n"
   79         % (ScriptName, rdBase.rdkitVersion, MiscUtil.GetMayaChemToolsVersion(), time.asctime())
   80     )
   81 
   82     (WallClockTime, ProcessorTime) = MiscUtil.GetWallClockAndProcessorTime()
   83 
   84     # Retrieve command line arguments and options...
   85     RetrieveOptions()
   86 
   87     if Options["--list"]:
   88         # Handle listing of torsion library information...
   89         ProcessListTorsionLibraryOption()
   90     else:
   91         # Process and validate command line arguments and options...
   92         ProcessOptions()
   93 
   94         # Perform actions required by the script...
   95         PerformFiltering()
   96 
   97     MiscUtil.PrintInfo("\n%s: Done...\n" % ScriptName)
   98     MiscUtil.PrintInfo("Total time: %s" % MiscUtil.GetFormattedElapsedTime(WallClockTime, ProcessorTime))
   99 
  100 
  101 def PerformFiltering():
  102     """Filter molecules using SMARTS torsion rules in the torsion strain energy
  103     library file."""
  104 
  105     # Setup a molecule reader...
  106     MiscUtil.PrintInfo("\nProcessing file %s..." % OptionsInfo["Infile"])
  107     Mols = RDKitUtil.ReadMolecules(OptionsInfo["Infile"], **OptionsInfo["InfileParams"])
  108 
  109     MolCount, ValidMolCount, RemainingMolCount, WriteFailedCount = ProcessMolecules(Mols)
  110 
  111     MiscUtil.PrintInfo("\nTotal number of molecules: %d" % MolCount)
  112     MiscUtil.PrintInfo("Number of valid molecules: %d" % ValidMolCount)
  113     MiscUtil.PrintInfo("Number of molecules failed during writing: %d" % WriteFailedCount)
  114     MiscUtil.PrintInfo("Number of ignored molecules: %d" % (MolCount - ValidMolCount + WriteFailedCount))
  115 
  116     MiscUtil.PrintInfo("\nNumber of remaining molecules: %d" % RemainingMolCount)
  117     MiscUtil.PrintInfo("Number of filtered molecules: %d" % (ValidMolCount - RemainingMolCount))
  118 
  119 
  120 def ProcessMolecules(Mols):
  121     """Process and filter molecules."""
  122 
  123     if OptionsInfo["MPMode"]:
  124         return ProcessMoleculesUsingMultipleProcesses(Mols)
  125     else:
  126         return ProcessMoleculesUsingSingleProcess(Mols)
  127 
  128 
  129 def ProcessMoleculesUsingSingleProcess(Mols):
  130     """Process and filter molecules using a single process."""
  131 
  132     # Instantiate torsion strain energy alerts class...
  133     TorsionStrainEnergyAlertsHandle = InstantiateTorsionStrainEnergyAlertsClass()
  134 
  135     MiscUtil.PrintInfo("\nFiltering molecules...")
  136 
  137     OutfileFilteredMode = OptionsInfo["OutfileFilteredMode"]
  138 
  139     # Set up writers...
  140     OutfilesWriters = SetupOutfilesWriters()
  141 
  142     WriterRemaining = OutfilesWriters["WriterRemaining"]
  143     WriterFiltered = OutfilesWriters["WriterFiltered"]
  144     WriterAlertSummary = OutfilesWriters["WriterAlertSummary"]
  145 
  146     # Initialize alerts summary info...
  147     TorsionAlertsSummaryInfo = InitializeTorsionAlertsSummaryInfo()
  148 
  149     (MolCount, ValidMolCount, RemainingMolCount, WriteFailedCount, FilteredMolWriteCount) = [0] * 5
  150     for Mol in Mols:
  151         MolCount += 1
  152 
  153         if Mol is None:
  154             continue
  155 
  156         if RDKitUtil.IsMolEmpty(Mol):
  157             MiscUtil.PrintWarning("Ignoring empty molecule: %s" % RDKitUtil.GetMolName(Mol, MolCount))
  158             continue
  159 
  160         # Check for 3D flag...
  161         if not Mol.GetConformer().Is3D():
  162             MiscUtil.PrintWarning("3D tag is not set. Ignoring molecule: %s\n" % RDKitUtil.GetMolName(Mol, MolCount))
  163             continue
  164 
  165         ValidMolCount += 1
  166 
  167         # Identify torsion library alerts for rotatable bonds..
  168         RotBondsAlertsStatus, RotBondsAlertsInfo = (
  169             TorsionStrainEnergyAlertsHandle.IdentifyTorsionLibraryAlertsForRotatableBonds(Mol)
  170         )
  171 
  172         TrackTorsionAlertsSummaryInfo(TorsionAlertsSummaryInfo, RotBondsAlertsInfo)
  173 
  174         # Write out filtered and remaining molecules...
  175         WriteStatus = True
  176         if RotBondsAlertsStatus:
  177             if OutfileFilteredMode:
  178                 WriteStatus = WriteMolecule(WriterFiltered, Mol, RotBondsAlertsInfo)
  179                 if WriteStatus:
  180                     FilteredMolWriteCount += 1
  181         else:
  182             RemainingMolCount += 1
  183             WriteStatus = WriteMolecule(WriterRemaining, Mol, RotBondsAlertsInfo)
  184 
  185         if not WriteStatus:
  186             WriteFailedCount += 1
  187 
  188     WriteTorsionAlertsSummaryInfo(WriterAlertSummary, TorsionAlertsSummaryInfo)
  189     CloseOutfilesWriters(OutfilesWriters)
  190 
  191     if FilteredMolWriteCount:
  192         WriteTorsionAlertsFilteredByRulesInfo(TorsionAlertsSummaryInfo)
  193 
  194     return (MolCount, ValidMolCount, RemainingMolCount, WriteFailedCount)
  195 
  196 
  197 def ProcessMoleculesUsingMultipleProcesses(Mols):
  198     """Process and filter molecules using multiprocessing."""
  199 
  200     MiscUtil.PrintInfo("\nFiltering molecules using multiprocessing...")
  201 
  202     MPParams = OptionsInfo["MPParams"]
  203     OutfileFilteredMode = OptionsInfo["OutfileFilteredMode"]
  204 
  205     # Instantiate torsion strain energy alerts class to list torsion library information...
  206     InstantiateTorsionStrainEnergyAlertsClass()
  207 
  208     # Set up writers...
  209     OutfilesWriters = SetupOutfilesWriters()
  210 
  211     WriterRemaining = OutfilesWriters["WriterRemaining"]
  212     WriterFiltered = OutfilesWriters["WriterFiltered"]
  213     WriterAlertSummary = OutfilesWriters["WriterAlertSummary"]
  214 
  215     # Initialize alerts summary info...
  216     TorsionAlertsSummaryInfo = InitializeTorsionAlertsSummaryInfo()
  217 
  218     # Setup data for initializing a worker process...
  219     MiscUtil.PrintInfo("\nEncoding options info and rotatable bond pattern molecule...")
  220     InitializeWorkerProcessArgs = (
  221         MiscUtil.ObjectToBase64EncodedString(Options),
  222         MiscUtil.ObjectToBase64EncodedString(OptionsInfo),
  223     )
  224 
  225     # Setup a encoded mols data iterable for a worker process...
  226     WorkerProcessDataIterable = RDKitUtil.GenerateBase64EncodedMolStrings(Mols)
  227 
  228     # Setup process pool along with data initialization for each process...
  229     MiscUtil.PrintInfo(
  230         "\nConfiguring multiprocessing using %s method..."
  231         % ("mp.Pool.imap()" if re.match("^Lazy$", MPParams["InputDataMode"], re.I) else "mp.Pool.map()")
  232     )
  233     MiscUtil.PrintInfo(
  234         "NumProcesses: %s; InputDataMode: %s; ChunkSize: %s\n"
  235         % (
  236             MPParams["NumProcesses"],
  237             MPParams["InputDataMode"],
  238             ("automatic" if MPParams["ChunkSize"] is None else MPParams["ChunkSize"]),
  239         )
  240     )
  241 
  242     ProcessPool = mp.Pool(MPParams["NumProcesses"], InitializeWorkerProcess, InitializeWorkerProcessArgs)
  243 
  244     # Start processing...
  245     if re.match("^Lazy$", MPParams["InputDataMode"], re.I):
  246         Results = ProcessPool.imap(WorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"])
  247     elif re.match("^InMemory$", MPParams["InputDataMode"], re.I):
  248         Results = ProcessPool.map(WorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"])
  249     else:
  250         MiscUtil.PrintError(
  251             'The value, %s, specified for "--inputDataMode" is not supported.' % (MPParams["InputDataMode"])
  252         )
  253 
  254     (MolCount, ValidMolCount, RemainingMolCount, WriteFailedCount, FilteredMolWriteCount) = [0] * 5
  255     for Result in Results:
  256         MolCount += 1
  257         MolIndex, EncodedMol, RotBondsAlertsStatus, RotBondsAlertsInfo = Result
  258 
  259         if EncodedMol is None:
  260             continue
  261         ValidMolCount += 1
  262 
  263         Mol = RDKitUtil.MolFromBase64EncodedMolString(EncodedMol)
  264 
  265         TrackTorsionAlertsSummaryInfo(TorsionAlertsSummaryInfo, RotBondsAlertsInfo)
  266 
  267         # Write out filtered and remaining molecules...
  268         WriteStatus = True
  269         if RotBondsAlertsStatus:
  270             if OutfileFilteredMode:
  271                 WriteStatus = WriteMolecule(WriterFiltered, Mol, RotBondsAlertsInfo)
  272                 if WriteStatus:
  273                     FilteredMolWriteCount += 1
  274         else:
  275             RemainingMolCount += 1
  276             WriteStatus = WriteMolecule(WriterRemaining, Mol, RotBondsAlertsInfo)
  277 
  278         if not WriteStatus:
  279             WriteFailedCount += 1
  280 
  281     WriteTorsionAlertsSummaryInfo(WriterAlertSummary, TorsionAlertsSummaryInfo)
  282     CloseOutfilesWriters(OutfilesWriters)
  283 
  284     if FilteredMolWriteCount:
  285         WriteTorsionAlertsFilteredByRulesInfo(TorsionAlertsSummaryInfo)
  286 
  287     return (MolCount, ValidMolCount, RemainingMolCount, WriteFailedCount)
  288 
  289 
  290 def InitializeWorkerProcess(*EncodedArgs):
  291     """Initialize data for a worker process."""
  292 
  293     global Options, OptionsInfo
  294 
  295     MiscUtil.PrintInfo("Starting process (PID: %s)..." % os.getpid())
  296 
  297     # Decode Options and OptionInfo...
  298     Options = MiscUtil.ObjectFromBase64EncodedString(EncodedArgs[0])
  299     OptionsInfo = MiscUtil.ObjectFromBase64EncodedString(EncodedArgs[1])
  300 
  301     # Instantiate torsion strain energy alerts class...
  302     OptionsInfo["TorsionStrainEnergyAlertsHandle"] = InstantiateTorsionStrainEnergyAlertsClass(Quiet=True)
  303 
  304 
  305 def WorkerProcess(EncodedMolInfo):
  306     """Process data for a worker process."""
  307 
  308     MolIndex, EncodedMol = EncodedMolInfo
  309 
  310     if EncodedMol is None:
  311         return [MolIndex, None, False, None]
  312 
  313     Mol = RDKitUtil.MolFromBase64EncodedMolString(EncodedMol)
  314     if RDKitUtil.IsMolEmpty(Mol):
  315         MolName = RDKitUtil.GetMolName(Mol, (MolIndex + 1))
  316         MiscUtil.PrintWarning("Ignoring empty molecule: %s" % MolName)
  317         return [MolIndex, None, False, None]
  318 
  319     # Check for 3D flag...
  320     if not Mol.GetConformer().Is3D():
  321         MolName = RDKitUtil.GetMolName(Mol, (MolIndex + 1))
  322         MiscUtil.PrintWarning("3D tag is not set. Ignoring molecule: %s\n" % MolName)
  323         return [MolIndex, None, False, None]
  324 
  325     # Identify torsion library alerts for rotatable bonds..
  326     TorsionStrainEnergyAlertsHandle = OptionsInfo["TorsionStrainEnergyAlertsHandle"]
  327     RotBondsAlertsStatus, RotBondsAlertsInfo = (
  328         TorsionStrainEnergyAlertsHandle.IdentifyTorsionLibraryAlertsForRotatableBonds(Mol)
  329     )
  330 
  331     return [MolIndex, EncodedMol, RotBondsAlertsStatus, RotBondsAlertsInfo]
  332 
  333 
  334 def InitializeTorsionAlertsSummaryInfo():
  335     """Initialize torsion alerts summary."""
  336 
  337     if OptionsInfo["CountMode"]:
  338         return None
  339 
  340     if not OptionsInfo["TrackAlertsSummaryInfo"]:
  341         return None
  342 
  343     TorsionAlertsSummaryInfo = {}
  344     TorsionAlertsSummaryInfo["RuleIDs"] = []
  345 
  346     for DataLabel in [
  347         "SMARTSToRuleIDs",
  348         "RuleSMARTS",
  349         "HierarchyClassName",
  350         "HierarchySubClassName",
  351         "EnergyMethod",
  352         "MaxSingleEnergyAlertTypes",
  353         "MaxSingleEnergyAlertTypesMolCount",
  354     ]:
  355         TorsionAlertsSummaryInfo[DataLabel] = {}
  356 
  357     return TorsionAlertsSummaryInfo
  358 
  359 
  360 def TrackTorsionAlertsSummaryInfo(TorsionAlertsSummaryInfo, RotBondsAlertsInfo):
  361     """Track torsion alerts summary information for matched torsion rules in a
  362     molecule."""
  363 
  364     if OptionsInfo["CountMode"]:
  365         return
  366 
  367     if not OptionsInfo["TrackAlertsSummaryInfo"]:
  368         return
  369 
  370     if RotBondsAlertsInfo is None:
  371         return
  372 
  373     MolAlertsInfo = {}
  374     MolAlertsInfo["RuleIDs"] = []
  375     MolAlertsInfo["MaxSingleEnergyAlertTypes"] = {}
  376 
  377     for ID in RotBondsAlertsInfo["IDs"]:
  378         if not RotBondsAlertsInfo["MatchStatus"][ID]:
  379             continue
  380 
  381         if SkipRotatableBondAlertInfo(ID, RotBondsAlertsInfo):
  382             continue
  383 
  384         MaxSingleEnergyAlertType = SetupMaxSingleEnergyAlertStatusValue(
  385             RotBondsAlertsInfo["MaxSingleEnergyAlertStatus"][ID]
  386         )
  387 
  388         TorsionRuleNodeID = RotBondsAlertsInfo["TorsionRuleNodeID"][ID]
  389         TorsionRuleSMARTS = RotBondsAlertsInfo["TorsionRuleSMARTS"][ID]
  390 
  391         # Track data for torsion alert summary information across molecules...
  392         if TorsionRuleNodeID not in TorsionAlertsSummaryInfo["RuleSMARTS"]:
  393             TorsionAlertsSummaryInfo["RuleIDs"].append(TorsionRuleNodeID)
  394             TorsionAlertsSummaryInfo["SMARTSToRuleIDs"][TorsionRuleSMARTS] = TorsionRuleNodeID
  395 
  396             TorsionAlertsSummaryInfo["RuleSMARTS"][TorsionRuleNodeID] = TorsionRuleSMARTS
  397             TorsionAlertsSummaryInfo["HierarchyClassName"][TorsionRuleNodeID] = RotBondsAlertsInfo[
  398                 "HierarchyClassName"
  399             ][ID]
  400             TorsionAlertsSummaryInfo["HierarchySubClassName"][TorsionRuleNodeID] = RotBondsAlertsInfo[
  401                 "HierarchySubClassName"
  402             ][ID]
  403 
  404             TorsionAlertsSummaryInfo["EnergyMethod"][TorsionRuleNodeID] = RotBondsAlertsInfo["EnergyMethod"][ID]
  405 
  406             # Initialize number of alert types across all molecules...
  407             TorsionAlertsSummaryInfo["MaxSingleEnergyAlertTypes"][TorsionRuleNodeID] = {}
  408 
  409             # Initialize number of molecules flagged by each alert type...
  410             TorsionAlertsSummaryInfo["MaxSingleEnergyAlertTypesMolCount"][TorsionRuleNodeID] = {}
  411 
  412         if MaxSingleEnergyAlertType not in TorsionAlertsSummaryInfo["MaxSingleEnergyAlertTypes"][TorsionRuleNodeID]:
  413             TorsionAlertsSummaryInfo["MaxSingleEnergyAlertTypes"][TorsionRuleNodeID][MaxSingleEnergyAlertType] = 0
  414             TorsionAlertsSummaryInfo["MaxSingleEnergyAlertTypesMolCount"][TorsionRuleNodeID][
  415                 MaxSingleEnergyAlertType
  416             ] = 0
  417 
  418         TorsionAlertsSummaryInfo["MaxSingleEnergyAlertTypes"][TorsionRuleNodeID][MaxSingleEnergyAlertType] += 1
  419 
  420         # Track data for torsion alert information in a molecule...
  421         if TorsionRuleNodeID not in MolAlertsInfo["MaxSingleEnergyAlertTypes"]:
  422             MolAlertsInfo["RuleIDs"].append(TorsionRuleNodeID)
  423             MolAlertsInfo["MaxSingleEnergyAlertTypes"][TorsionRuleNodeID] = {}
  424 
  425         if MaxSingleEnergyAlertType not in MolAlertsInfo["MaxSingleEnergyAlertTypes"][TorsionRuleNodeID]:
  426             MolAlertsInfo["MaxSingleEnergyAlertTypes"][TorsionRuleNodeID][MaxSingleEnergyAlertType] = 0
  427         MolAlertsInfo["MaxSingleEnergyAlertTypes"][TorsionRuleNodeID][MaxSingleEnergyAlertType] += 1
  428 
  429     # Track number of molecules flagged by a specific torsion alert...
  430     for TorsionRuleNodeID in MolAlertsInfo["RuleIDs"]:
  431         for MaxSingleEnergyAlertType in MolAlertsInfo["MaxSingleEnergyAlertTypes"][TorsionRuleNodeID]:
  432             if MolAlertsInfo["MaxSingleEnergyAlertTypes"][TorsionRuleNodeID][MaxSingleEnergyAlertType]:
  433                 TorsionAlertsSummaryInfo["MaxSingleEnergyAlertTypesMolCount"][TorsionRuleNodeID][
  434                     MaxSingleEnergyAlertType
  435                 ] += 1
  436 
  437 
  438 def WriteTorsionAlertsSummaryInfo(Writer, TorsionAlertsSummaryInfo):
  439     """Write out torsion alerts summary informatio to a CSV file."""
  440 
  441     if OptionsInfo["CountMode"]:
  442         return
  443 
  444     if not OptionsInfo["OutfileSummaryMode"]:
  445         return
  446 
  447     if len(TorsionAlertsSummaryInfo["RuleIDs"]) == 0:
  448         return
  449 
  450     # Write headers...
  451     QuoteValues = True
  452     Values = [
  453         "TorsionRule",
  454         "HierarchyClass",
  455         "HierarchySubClass",
  456         "EnergyMethod",
  457         "MaxSingleEnergyTorsionAlertTypes",
  458         "MaxSingleEnergyTorsionAlertCount",
  459         "MaxSingleEnergyTorsionAlertMolCount",
  460     ]
  461     Writer.write("%s\n" % MiscUtil.JoinWords(Values, ",", QuoteValues))
  462 
  463     SortedRuleIDs = GetSortedTorsionAlertsSummaryInfoRuleIDs(TorsionAlertsSummaryInfo)
  464 
  465     # Write alerts information...
  466     for ID in SortedRuleIDs:
  467         # Remove any double quotes in SMARTS...
  468         RuleSMARTS = TorsionAlertsSummaryInfo["RuleSMARTS"][ID]
  469         RuleSMARTS = re.sub('"', "", RuleSMARTS, flags=re.I)
  470 
  471         HierarchyClassName = TorsionAlertsSummaryInfo["HierarchyClassName"][ID]
  472         HierarchySubClassName = TorsionAlertsSummaryInfo["HierarchySubClassName"][ID]
  473 
  474         EnergyMethod = TorsionAlertsSummaryInfo["EnergyMethod"][ID]
  475 
  476         MaxSingleEnergyAlertTypes = []
  477         MaxSingleEnergyAlertTypesCount = []
  478         MaxSingleEnergyAlertTypesMolCount = []
  479         for MaxSingleEnergyAlertType in sorted(TorsionAlertsSummaryInfo["MaxSingleEnergyAlertTypes"][ID]):
  480             MaxSingleEnergyAlertTypes.append(MaxSingleEnergyAlertType)
  481             MaxSingleEnergyAlertTypesCount.append(
  482                 "%s" % TorsionAlertsSummaryInfo["MaxSingleEnergyAlertTypes"][ID][MaxSingleEnergyAlertType]
  483             )
  484             MaxSingleEnergyAlertTypesMolCount.append(
  485                 "%s" % TorsionAlertsSummaryInfo["MaxSingleEnergyAlertTypesMolCount"][ID][MaxSingleEnergyAlertType]
  486             )
  487 
  488         Values = [
  489             RuleSMARTS,
  490             HierarchyClassName,
  491             HierarchySubClassName,
  492             EnergyMethod,
  493             "%s" % MiscUtil.JoinWords(MaxSingleEnergyAlertTypes, ","),
  494             "%s" % (MiscUtil.JoinWords(MaxSingleEnergyAlertTypesCount, ",")),
  495             "%s" % (MiscUtil.JoinWords(MaxSingleEnergyAlertTypesMolCount, ",")),
  496         ]
  497         Writer.write("%s\n" % MiscUtil.JoinWords(Values, ",", QuoteValues))
  498 
  499 
  500 def GetSortedTorsionAlertsSummaryInfoRuleIDs(TorsionAlertsSummaryInfo):
  501     """Sort torsion rule IDs by  alert types molecule count in descending order."""
  502 
  503     SortedRuleIDs = []
  504 
  505     RuleIDs = TorsionAlertsSummaryInfo["RuleIDs"]
  506     if len(RuleIDs) == 0:
  507         return SortedRuleIDs
  508 
  509     # Setup a map from AlertTypesMolCount to IDs for sorting alerts...
  510     RuleIDs = TorsionAlertsSummaryInfo["RuleIDs"]
  511     MolCountMap = {}
  512     for ID in RuleIDs:
  513         MolCount = 0
  514         for AlertType in sorted(TorsionAlertsSummaryInfo["MaxSingleEnergyAlertTypes"][ID]):
  515             MolCount += TorsionAlertsSummaryInfo["MaxSingleEnergyAlertTypesMolCount"][ID][AlertType]
  516         MolCountMap[ID] = MolCount
  517 
  518     SortedRuleIDs = sorted(RuleIDs, key=lambda ID: MolCountMap[ID], reverse=True)
  519 
  520     return SortedRuleIDs
  521 
  522 
  523 def WriteTorsionAlertsFilteredByRulesInfo(TorsionAlertsSummaryInfo):
  524     """Write out torsion alerts SD files for individual torsion rules."""
  525 
  526     if OptionsInfo["CountMode"]:
  527         return
  528 
  529     if not OptionsInfo["OutfilesFilteredByRulesMode"]:
  530         return
  531 
  532     if len(TorsionAlertsSummaryInfo["RuleIDs"]) == 0:
  533         return
  534 
  535     # Setup a molecule reader for filtered molecules...
  536     FilteredMols = RDKitUtil.ReadMolecules(OptionsInfo["OutfileFiltered"], **OptionsInfo["InfileParams"])
  537 
  538     # Get torsion rule IDs for writing out filtered SD files for individual torsion alert rules...
  539     TorsionRuleIDs = GetTorsionAlertsFilteredByRuleFilesRuleIDs(TorsionAlertsSummaryInfo)
  540 
  541     # Setup writers...
  542     ByRuleOutfilesWriters = SetupByRuleOutfilesWriters(TorsionRuleIDs)
  543 
  544     for Mol in FilteredMols:
  545         # Retrieve torsion alerts info...
  546         TorsionAlertsInfo = RetrieveTorsionAlertsInfo(Mol, TorsionAlertsSummaryInfo)
  547         if TorsionAlertsInfo is None:
  548             continue
  549 
  550         for TorsionRuleID in TorsionRuleIDs:
  551             if TorsionRuleID not in TorsionAlertsInfo["RuleSMARTS"]:
  552                 continue
  553 
  554             WriteMoleculeFilteredByRuleID(
  555                 ByRuleOutfilesWriters[TorsionRuleID], Mol, TorsionRuleID, TorsionAlertsSummaryInfo, TorsionAlertsInfo
  556             )
  557 
  558     CloseByRuleOutfilesWriters(ByRuleOutfilesWriters)
  559 
  560 
  561 def GetTorsionAlertsFilteredByRuleFilesRuleIDs(TorsionAlertsSummaryInfo):
  562     """Get torsion rule IDs for writing out individual SD files filtered by torsion alert rules."""
  563 
  564     # Get torsion rule IDs triggering torsion alerts sorted in the order from the most to
  565     # the least number of unique molecules...
  566     RuleIDs = GetSortedTorsionAlertsSummaryInfoRuleIDs(TorsionAlertsSummaryInfo)
  567 
  568     # Select torsion rule IDs for writing out SD files...
  569     if not OptionsInfo["OutfilesFilteredByRulesAllMode"]:
  570         MaxRuleIDs = OptionsInfo["OutfilesFilteredByRulesMaxCount"]
  571         if MaxRuleIDs < len(RuleIDs):
  572             RuleIDs = RuleIDs[0:MaxRuleIDs]
  573 
  574     return RuleIDs
  575 
  576 
  577 def RetrieveTorsionAlertsInfo(Mol, TorsionAlertsSummaryInfo):
  578     """Parse torsion alerts data field value to retrieve alerts information for rotatable bonds."""
  579 
  580     TorsionAlertsLabel = OptionsInfo["SDFieldIDsToLabels"]["TorsionAlertsLabel"]
  581     TorsionAlerts = Mol.GetProp(TorsionAlertsLabel) if Mol.HasProp(TorsionAlertsLabel) else None
  582 
  583     if TorsionAlerts is None or len(TorsionAlerts) == 0:
  584         return None
  585 
  586     # Initialize for tracking by rule IDs...
  587     TorsionAlertsInfo = {}
  588     TorsionAlertsInfo["RuleIDs"] = []
  589 
  590     TorsionAlertsInfo["RuleSMARTS"] = {}
  591     TorsionAlertsInfo["HierarchyClassName"] = {}
  592     TorsionAlertsInfo["HierarchySubClassName"] = {}
  593     TorsionAlertsInfo["EnergyMethod"] = {}
  594 
  595     TorsionAlertsInfo["AtomIndices"] = {}
  596     TorsionAlertsInfo["TorsionAtomIndices"] = {}
  597     TorsionAlertsInfo["TorsionAngle"] = {}
  598 
  599     TorsionAlertsInfo["Energy"] = {}
  600     TorsionAlertsInfo["EnergyLowerBound"] = {}
  601     TorsionAlertsInfo["EnergyUpperBound"] = {}
  602 
  603     TorsionAlertsInfo["AngleNotObserved"] = {}
  604     TorsionAlertsInfo["MaxSingleEnergyAlertType"] = {}
  605 
  606     TorsionAlertsInfo["AnglesNotObservedCount"] = {}
  607     TorsionAlertsInfo["MaxSingleEnergyAlertsCount"] = {}
  608 
  609     ValuesDelimiter = OptionsInfo["IntraSetValuesDelim"]
  610     TorsionAlertsSetSize = 12
  611 
  612     TorsionAlertsWords = TorsionAlerts.split()
  613     if len(TorsionAlertsWords) % TorsionAlertsSetSize:
  614         MiscUtil.PrintError(
  615             "The number of space delimited values, %s, for TorsionAlerts data field in filtered SD file must be a multiple of %s."
  616             % (len(TorsionAlertsWords), TorsionAlertsSetSize)
  617         )
  618 
  619     ID = 0
  620     for Index in range(0, len(TorsionAlertsWords), TorsionAlertsSetSize):
  621         ID += 1
  622 
  623         (
  624             RotBondIndices,
  625             TorsionIndices,
  626             TorsionAngle,
  627             Energy,
  628             EnergyLowerBound,
  629             EnergyUpperBound,
  630             HierarchyClass,
  631             HierarchySubClass,
  632             TorsionRule,
  633             EnergyMethod,
  634             AngleNotObserved,
  635             MaxSingleEnergyAlertType,
  636         ) = TorsionAlertsWords[Index : Index + TorsionAlertsSetSize]
  637         RotBondIndices = RotBondIndices.split(ValuesDelimiter)
  638         TorsionIndices = TorsionIndices.split(ValuesDelimiter)
  639 
  640         if TorsionRule not in TorsionAlertsSummaryInfo["SMARTSToRuleIDs"]:
  641             MiscUtil.PrintWarning(
  642                 "The SMARTS pattern, %s, for TorsionAlerts data field in filtered SD file doesn't map to any torsion rule..."
  643                 % TorsionRule
  644             )
  645             continue
  646         TorsionRuleNodeID = TorsionAlertsSummaryInfo["SMARTSToRuleIDs"][TorsionRule]
  647 
  648         # Track data for torsion alerts in a molecule...
  649         if TorsionRuleNodeID not in TorsionAlertsInfo["RuleSMARTS"]:
  650             TorsionAlertsInfo["RuleIDs"].append(TorsionRuleNodeID)
  651 
  652             TorsionAlertsInfo["RuleSMARTS"][TorsionRuleNodeID] = TorsionRule
  653             TorsionAlertsInfo["HierarchyClassName"][TorsionRuleNodeID] = HierarchyClass
  654             TorsionAlertsInfo["HierarchySubClassName"][TorsionRuleNodeID] = HierarchySubClass
  655             TorsionAlertsInfo["EnergyMethod"][TorsionRuleNodeID] = EnergyMethod
  656 
  657             TorsionAlertsInfo["AtomIndices"][TorsionRuleNodeID] = []
  658             TorsionAlertsInfo["TorsionAtomIndices"][TorsionRuleNodeID] = []
  659             TorsionAlertsInfo["TorsionAngle"][TorsionRuleNodeID] = []
  660 
  661             TorsionAlertsInfo["Energy"][TorsionRuleNodeID] = []
  662             TorsionAlertsInfo["EnergyLowerBound"][TorsionRuleNodeID] = []
  663             TorsionAlertsInfo["EnergyUpperBound"][TorsionRuleNodeID] = []
  664             TorsionAlertsInfo["AngleNotObserved"][TorsionRuleNodeID] = []
  665             TorsionAlertsInfo["MaxSingleEnergyAlertType"][TorsionRuleNodeID] = []
  666 
  667             TorsionAlertsInfo["AnglesNotObservedCount"][TorsionRuleNodeID] = 0
  668             TorsionAlertsInfo["MaxSingleEnergyAlertsCount"][TorsionRuleNodeID] = 0
  669 
  670         # Track multiple values for a rule ID...
  671         TorsionAlertsInfo["AtomIndices"][TorsionRuleNodeID].append(RotBondIndices)
  672         TorsionAlertsInfo["TorsionAtomIndices"][TorsionRuleNodeID].append(TorsionIndices)
  673         TorsionAlertsInfo["TorsionAngle"][TorsionRuleNodeID].append(TorsionAngle)
  674 
  675         TorsionAlertsInfo["Energy"][TorsionRuleNodeID].append(Energy)
  676         TorsionAlertsInfo["EnergyLowerBound"][TorsionRuleNodeID].append(EnergyLowerBound)
  677         TorsionAlertsInfo["EnergyUpperBound"][TorsionRuleNodeID].append(EnergyUpperBound)
  678         TorsionAlertsInfo["AngleNotObserved"][TorsionRuleNodeID].append(AngleNotObserved)
  679 
  680         TorsionAlertsInfo["MaxSingleEnergyAlertType"][TorsionRuleNodeID].append(MaxSingleEnergyAlertType)
  681 
  682         # Count angles not observer for a rule ID...
  683         if AngleNotObserved == "Yes":
  684             TorsionAlertsInfo["AnglesNotObservedCount"][TorsionRuleNodeID] += 1
  685 
  686         # Count max single energy alert for a rule ID...
  687         if MaxSingleEnergyAlertType == "Yes":
  688             TorsionAlertsInfo["MaxSingleEnergyAlertsCount"][TorsionRuleNodeID] += 1
  689 
  690     return TorsionAlertsInfo
  691 
  692 
  693 def WriteMolecule(Writer, Mol, RotBondsAlertsInfo):
  694     """Write out molecule."""
  695 
  696     if OptionsInfo["CountMode"]:
  697         return True
  698 
  699     SetupMolPropertiesForAlertsInformation(Mol, RotBondsAlertsInfo)
  700 
  701     try:
  702         Writer.write(Mol)
  703     except Exception as ErrMsg:
  704         MiscUtil.PrintWarning("Failed to write molecule %s:\n%s\n" % (RDKitUtil.GetMolName(Mol), ErrMsg))
  705         return False
  706 
  707     return True
  708 
  709 
  710 def SetupMolPropertiesForAlertsInformation(Mol, RotBondsAlertsInfo):
  711     """Setup molecule properties containing alerts information for rotatable bonds."""
  712 
  713     if not OptionsInfo["OutfileAlerts"]:
  714         return
  715 
  716     SDFieldIDsToLabels = OptionsInfo["SDFieldIDsToLabels"]
  717     Precision = OptionsInfo["Precision"]
  718 
  719     # Setup rotatable bonds count...
  720     RotBondsCount = 0
  721     if RotBondsAlertsInfo is not None:
  722         RotBondsCount = len(RotBondsAlertsInfo["IDs"])
  723     Mol.SetProp(SDFieldIDsToLabels["RotBondsCountLabel"], "%s" % RotBondsCount)
  724 
  725     if RotBondsAlertsInfo is not None:
  726         # Setup total energy along with lower and upper bounds...
  727         Mol.SetProp(
  728             SDFieldIDsToLabels["TotalEnergyLabel"],
  729             "%s" % SetupEnergyValueForSDField(RotBondsAlertsInfo["TotalEnergy"], Precision),
  730         )
  731         Mol.SetProp(
  732             SDFieldIDsToLabels["TotalEnergyLowerBoundCILabel"],
  733             "%s" % SetupEnergyValueForSDField(RotBondsAlertsInfo["TotalEnergyLowerBound"], Precision),
  734         )
  735         Mol.SetProp(
  736             SDFieldIDsToLabels["TotalEnergyUpperBoundCILabel"],
  737             "%s" % SetupEnergyValueForSDField(RotBondsAlertsInfo["TotalEnergyUpperBound"], Precision),
  738         )
  739 
  740         # Setup max single energy and alert count...
  741         if OptionsInfo["MaxSingleEnergyMode"] or OptionsInfo["TotalOrMaxSingleEnergyMode"]:
  742             Mol.SetProp(
  743                 SDFieldIDsToLabels["MaxSingleEnergyLabel"],
  744                 "%s" % SetupEnergyValueForSDField(RotBondsAlertsInfo["MaxSingleEnergy"], Precision),
  745             )
  746             Mol.SetProp(
  747                 SDFieldIDsToLabels["MaxSingleEnergyAlertsCountLabel"],
  748                 "%s"
  749                 % (
  750                     "NA"
  751                     if RotBondsAlertsInfo["MaxSingleEnergyAlertsCount"] is None
  752                     else RotBondsAlertsInfo["MaxSingleEnergyAlertsCount"]
  753                 ),
  754             )
  755 
  756         Mol.SetProp(
  757             SDFieldIDsToLabels["AnglesNotObservedCountLabel"],
  758             "%s"
  759             % (
  760                 "NA"
  761                 if RotBondsAlertsInfo["AnglesNotObservedCount"] is None
  762                 else RotBondsAlertsInfo["AnglesNotObservedCount"]
  763             ),
  764         )
  765 
  766     # Setup alert information for rotatable bonds...
  767     AlertsInfoValues = []
  768 
  769     # Delimiter for multiple values corresponding to specific set of information for
  770     # a rotatable bond. For example: TorsionAtomIndices
  771     ValuesDelim = OptionsInfo["IntraSetValuesDelim"]
  772 
  773     # Delimiter for various values for a rotatable bond...
  774     RotBondValuesDelim = OptionsInfo["InterSetValuesDelim"]
  775 
  776     # Delimiter for values corresponding to multiple rotatable bonds...
  777     AlertsInfoValuesDelim = OptionsInfo["InterSetValuesDelim"]
  778 
  779     if RotBondsAlertsInfo is not None:
  780         for ID in RotBondsAlertsInfo["IDs"]:
  781             if not RotBondsAlertsInfo["MatchStatus"][ID]:
  782                 continue
  783 
  784             if SkipRotatableBondAlertInfo(ID, RotBondsAlertsInfo):
  785                 continue
  786 
  787             RotBondValues = []
  788 
  789             # Bond atom indices...
  790             Values = ["%s" % Value for Value in RotBondsAlertsInfo["AtomIndices"][ID]]
  791             RotBondValues.append(ValuesDelim.join(Values))
  792 
  793             # Torsion atom indices...
  794             TorsionAtomIndices = SetupTorsionAtomIndicesValues(
  795                 RotBondsAlertsInfo["TorsionAtomIndices"][ID], ValuesDelim
  796             )
  797             RotBondValues.append(TorsionAtomIndices)
  798 
  799             # Torsion angle...
  800             RotBondValues.append("%.2f" % RotBondsAlertsInfo["TorsionAngle"][ID])
  801 
  802             # Energy along with its lower and upper bound confidence interval...
  803             RotBondValues.append(SetupEnergyValueForSDField(RotBondsAlertsInfo["Energy"][ID], Precision))
  804             RotBondValues.append(SetupEnergyValueForSDField(RotBondsAlertsInfo["EnergyLowerBound"][ID], Precision))
  805             RotBondValues.append(SetupEnergyValueForSDField(RotBondsAlertsInfo["EnergyUpperBound"][ID], Precision))
  806 
  807             # Hierarchy class and subclass names...
  808             RotBondValues.append("%s" % RotBondsAlertsInfo["HierarchyClassName"][ID])
  809             RotBondValues.append("%s" % RotBondsAlertsInfo["HierarchySubClassName"][ID])
  810 
  811             # Torsion rule SMARTS...
  812             RotBondValues.append("%s" % RotBondsAlertsInfo["TorsionRuleSMARTS"][ID])
  813 
  814             # Energy method...
  815             RotBondValues.append("%s" % RotBondsAlertsInfo["EnergyMethod"][ID])
  816 
  817             # Angle not observed...
  818             RotBondValues.append("%s" % SetupAngleNotObservedValue(RotBondsAlertsInfo["AngleNotObserved"][ID]))
  819 
  820             # Max single energy alert status...
  821             RotBondValues.append(
  822                 "%s" % SetupMaxSingleEnergyAlertStatusValue(RotBondsAlertsInfo["MaxSingleEnergyAlertStatus"][ID])
  823             )
  824 
  825             # Track joined values for a rotatable bond...
  826             AlertsInfoValues.append("%s" % RotBondValuesDelim.join(RotBondValues))
  827 
  828     if len(AlertsInfoValues):
  829         Mol.SetProp(
  830             OptionsInfo["SDFieldIDsToLabels"]["TorsionAlertsLabel"],
  831             "%s" % ("%s" % AlertsInfoValuesDelim.join(AlertsInfoValues)),
  832         )
  833 
  834 
  835 def WriteMoleculeFilteredByRuleID(Writer, Mol, TorsionRuleID, TorsionAlertsSummaryInfo, TorsionAlertsInfo):
  836     """Write out molecule."""
  837 
  838     if OptionsInfo["CountMode"]:
  839         return
  840 
  841     SetupMolPropertiesForFilteredByRuleIDAlertsInformation(
  842         Mol, TorsionRuleID, TorsionAlertsSummaryInfo, TorsionAlertsInfo
  843     )
  844 
  845     Writer.write(Mol)
  846 
  847 
  848 def SetupMolPropertiesForFilteredByRuleIDAlertsInformation(
  849     Mol, TorsionRuleID, TorsionAlertsSummaryInfo, TorsionAlertsInfo
  850 ):
  851     """Setup molecule properties containing alerts information for torsion alerts
  852     fileted by Rule IDs."""
  853 
  854     # Delete torsion alerts information for rotatable bonds...
  855     if Mol.HasProp(OptionsInfo["SDFieldIDsToLabels"]["TorsionAlertsLabel"]):
  856         Mol.ClearProp(OptionsInfo["SDFieldIDsToLabels"]["TorsionAlertsLabel"])
  857 
  858     # Delimiter for values...
  859     IntraSetValuesDelim = OptionsInfo["IntraSetValuesDelim"]
  860     InterSetValuesDelim = OptionsInfo["InterSetValuesDelim"]
  861 
  862     # Setup alert rule information...
  863     AlertRuleInfoValues = []
  864 
  865     AlertRuleInfoValues.append("%s" % TorsionAlertsInfo["HierarchyClassName"][TorsionRuleID])
  866     AlertRuleInfoValues.append("%s" % TorsionAlertsInfo["HierarchySubClassName"][TorsionRuleID])
  867 
  868     AlertRuleInfoValues.append("%s" % TorsionAlertsInfo["RuleSMARTS"][TorsionRuleID])
  869     AlertRuleInfoValues.append("%s" % TorsionAlertsInfo["EnergyMethod"][TorsionRuleID])
  870 
  871     Mol.SetProp(
  872         OptionsInfo["SDFieldIDsToLabels"]["TorsionRuleLabel"],
  873         "%s" % ("%s" % InterSetValuesDelim.join(AlertRuleInfoValues)),
  874     )
  875 
  876     # Setup max single energy alert count for torsion rule...
  877     Mol.SetProp(
  878         OptionsInfo["SDFieldIDsToLabels"]["TorsionRuleMaxSingleEnergyAlertsCountLabel"],
  879         "%s" % TorsionAlertsInfo["MaxSingleEnergyAlertsCount"][TorsionRuleID],
  880     )
  881 
  882     # Setup angle not observed count for torsion rule...
  883     Mol.SetProp(
  884         OptionsInfo["SDFieldIDsToLabels"]["TorsionRuleAnglesNotObservedCountLabel"],
  885         "%s" % TorsionAlertsInfo["AnglesNotObservedCount"][TorsionRuleID],
  886     )
  887 
  888     # Setup torsion rule alerts...
  889     # "TorsionRuleAlertsLabel": "TorsionRuleAlerts (RotBondIndices TorsionIndices TorsionAngle Energy EnergyLowerBoundCI EnergyUpperBoundCI EnergyMethod AngleNotObserved MaxSingleEnergyAlert)
  890     AlertsInfoValues = []
  891     for Index in range(0, len(TorsionAlertsInfo["AtomIndices"][TorsionRuleID])):
  892         RotBondInfoValues = []
  893 
  894         # Bond atom indices...
  895         Values = ["%s" % Value for Value in TorsionAlertsInfo["AtomIndices"][TorsionRuleID][Index]]
  896         RotBondInfoValues.append(IntraSetValuesDelim.join(Values))
  897 
  898         # Torsion atom indices retrieved from the filtered SD file and stored as strings...
  899         Values = ["%s" % Value for Value in TorsionAlertsInfo["TorsionAtomIndices"][TorsionRuleID][Index]]
  900         RotBondInfoValues.append(IntraSetValuesDelim.join(Values))
  901 
  902         # Torsion angle...
  903         RotBondInfoValues.append(TorsionAlertsInfo["TorsionAngle"][TorsionRuleID][Index])
  904 
  905         # Energy and its bounds...
  906         RotBondInfoValues.append(TorsionAlertsInfo["Energy"][TorsionRuleID][Index])
  907         RotBondInfoValues.append(TorsionAlertsInfo["EnergyLowerBound"][TorsionRuleID][Index])
  908         RotBondInfoValues.append(TorsionAlertsInfo["EnergyUpperBound"][TorsionRuleID][Index])
  909 
  910         # Angle not observed......
  911         RotBondInfoValues.append(TorsionAlertsInfo["AngleNotObserved"][TorsionRuleID][Index])
  912 
  913         # Max single energy alert type...
  914         RotBondInfoValues.append(TorsionAlertsInfo["MaxSingleEnergyAlertType"][TorsionRuleID][Index])
  915 
  916         # Track alerts informaiton...
  917         AlertsInfoValues.append("%s" % InterSetValuesDelim.join(RotBondInfoValues))
  918 
  919     Mol.SetProp(
  920         OptionsInfo["SDFieldIDsToLabels"]["TorsionRuleAlertsLabel"], "%s" % (InterSetValuesDelim.join(AlertsInfoValues))
  921     )
  922 
  923 
  924 def SkipRotatableBondAlertInfo(ID, RotBondsAlertsInfo):
  925     """Skip rotatble bond alert info for a specific bond during writing to output files."""
  926 
  927     if not OptionsInfo["OutfileAlertsOnly"]:
  928         return False
  929 
  930     if RotBondsAlertsInfo["RotBondsAlertsStatus"] is None:
  931         return True
  932 
  933     Status = False
  934     if OptionsInfo["TotalEnergyMode"]:
  935         if not RotBondsAlertsInfo["RotBondsAlertsStatus"]:
  936             Status = True
  937     elif OptionsInfo["MaxSingleEnergyMode"]:
  938         if (
  939             RotBondsAlertsInfo["MaxSingleEnergyAlertStatus"][ID] is None
  940             or not RotBondsAlertsInfo["MaxSingleEnergyAlertStatus"][ID]
  941         ):
  942             Status = True
  943     elif OptionsInfo["TotalOrMaxSingleEnergyMode"]:
  944         if not RotBondsAlertsInfo["RotBondsAlertsStatus"]:
  945             Status = True
  946 
  947     return Status
  948 
  949 
  950 def SetupEnergyValueForSDField(Value, Precision):
  951     """Setup energy value for SD field."""
  952 
  953     if Value is None or math.isnan(Value) or math.isinf(Value):
  954         return "NA"
  955 
  956     return "%.*f" % (Precision, Value)
  957 
  958 
  959 def SetupAngleNotObservedValue(Value):
  960     """Setup angle not observed value."""
  961 
  962     if Value is None:
  963         return "NA"
  964 
  965     return "Yes" if Value else "No"
  966 
  967 
  968 def SetupMaxSingleEnergyAlertStatusValue(Value):
  969     """Setup max single energy alert status value."""
  970 
  971     if Value is None:
  972         return "NA"
  973 
  974     return "Yes" if Value else "No"
  975 
  976 
  977 def SetupTorsionAtomIndicesValues(TorsionAtomIndicesList, ValuesDelim):
  978     """Setup torsion atom indices value for output files."""
  979 
  980     Values = ["%s" % Value for Value in TorsionAtomIndicesList]
  981 
  982     return ValuesDelim.join(Values)
  983 
  984 
  985 def SetupOutfilesWriters():
  986     """Setup molecule and summary writers."""
  987 
  988     OutfilesWriters = {"WriterRemaining": None, "WriterFiltered": None, "WriterAlertSummary": None}
  989 
  990     # Writers for SD files...
  991     WriterRemaining, WriterFiltered = SetupMoleculeWriters()
  992     OutfilesWriters["WriterRemaining"] = WriterRemaining
  993     OutfilesWriters["WriterFiltered"] = WriterFiltered
  994 
  995     # Writer for alert summary CSV file...
  996     WriterAlertSummary = SetupAlertSummaryWriter()
  997     OutfilesWriters["WriterAlertSummary"] = WriterAlertSummary
  998 
  999     return OutfilesWriters
 1000 
 1001 
 1002 def SetupMoleculeWriters():
 1003     """Setup molecule writers."""
 1004 
 1005     Writer = None
 1006     WriterFiltered = None
 1007 
 1008     if OptionsInfo["CountMode"]:
 1009         return (Writer, WriterFiltered)
 1010 
 1011     Writer = RDKitUtil.MoleculesWriter(OptionsInfo["Outfile"], **OptionsInfo["OutfileParams"])
 1012     if Writer is None:
 1013         MiscUtil.PrintError("Failed to setup a writer for output fie %s " % OptionsInfo["Outfile"])
 1014     MiscUtil.PrintInfo("\nGenerating file %s..." % OptionsInfo["Outfile"])
 1015 
 1016     if OptionsInfo["OutfileFilteredMode"]:
 1017         WriterFiltered = RDKitUtil.MoleculesWriter(OptionsInfo["OutfileFiltered"], **OptionsInfo["OutfileParams"])
 1018         if WriterFiltered is None:
 1019             MiscUtil.PrintError("Failed to setup a writer for output fie %s " % OptionsInfo["OutfileFiltered"])
 1020         MiscUtil.PrintInfo("Generating file %s..." % OptionsInfo["OutfileFiltered"])
 1021 
 1022     return (Writer, WriterFiltered)
 1023 
 1024 
 1025 def SetupAlertSummaryWriter():
 1026     """Setup a alert summary writer."""
 1027 
 1028     Writer = None
 1029 
 1030     if OptionsInfo["CountMode"]:
 1031         return Writer
 1032 
 1033     if not OptionsInfo["OutfileSummaryMode"]:
 1034         return Writer
 1035 
 1036     Outfile = OptionsInfo["OutfileSummary"]
 1037     Writer = open(Outfile, "w")
 1038     if Writer is None:
 1039         MiscUtil.PrintError("Failed to setup a writer for output fie %s " % Outfile)
 1040 
 1041     MiscUtil.PrintInfo("Generating file %s..." % Outfile)
 1042 
 1043     return Writer
 1044 
 1045 
 1046 def CloseOutfilesWriters(OutfilesWriters):
 1047     """Close outfile writers."""
 1048 
 1049     for WriterType, Writer in OutfilesWriters.items():
 1050         if Writer is not None:
 1051             Writer.close()
 1052 
 1053 
 1054 def SetupByRuleOutfilesWriters(RuleIDs):
 1055     """Setup by rule outfiles writers."""
 1056 
 1057     # Initialize...
 1058     OutfilesWriters = {}
 1059     for RuleID in RuleIDs:
 1060         OutfilesWriters[RuleID] = None
 1061 
 1062     if OptionsInfo["CountMode"]:
 1063         return OutfilesWriters
 1064 
 1065     if not OptionsInfo["OutfilesFilteredByRulesMode"]:
 1066         return OutfilesWriters
 1067 
 1068     FileDir, FileName, FileExt = MiscUtil.ParseFileName(Options["--outfile"])
 1069     OutfilesRoot = "%s_Filtered_TopRule" % FileName
 1070     OutfilesExt = "sdf"
 1071 
 1072     MsgTxt = (
 1073         "all"
 1074         if OptionsInfo["OutfilesFilteredByRulesAllMode"]
 1075         else "top %s" % OptionsInfo["OutfilesFilteredByRulesMaxCount"]
 1076     )
 1077     MiscUtil.PrintInfo(
 1078         "\nGenerating output files %s*.%s for %s torsion rules triggering alerts..."
 1079         % (OutfilesRoot, OutfilesExt, MsgTxt)
 1080     )
 1081 
 1082     # Delete any existing output files...
 1083     Outfiles = glob.glob("%s*.%s" % (OutfilesRoot, OutfilesExt))
 1084     if len(Outfiles):
 1085         MiscUtil.PrintInfo("Deleting existing output files %s*.%s..." % (OutfilesRoot, OutfilesExt))
 1086         for Outfile in Outfiles:
 1087             try:
 1088                 os.remove(Outfile)
 1089             except Exception as ErrMsg:
 1090                 MiscUtil.PrintWarning("Failed to delete file: %s" % ErrMsg)
 1091 
 1092     RuleIndex = 0
 1093     for RuleID in RuleIDs:
 1094         RuleIndex += 1
 1095         Outfile = "%s%s.%s" % (OutfilesRoot, RuleIndex, OutfilesExt)
 1096         Writer = RDKitUtil.MoleculesWriter(Outfile, **OptionsInfo["OutfileParams"])
 1097         if Writer is None:
 1098             MiscUtil.PrintError("Failed to setup a writer for output fie %s " % Outfile)
 1099 
 1100         OutfilesWriters[RuleID] = Writer
 1101 
 1102     return OutfilesWriters
 1103 
 1104 
 1105 def CloseByRuleOutfilesWriters(OutfilesWriters):
 1106     """Close by rule outfile writers."""
 1107 
 1108     for RuleID, Writer in OutfilesWriters.items():
 1109         if Writer is not None:
 1110             Writer.close()
 1111 
 1112 
 1113 def InstantiateTorsionStrainEnergyAlertsClass(Quiet=False):
 1114     """Initialize torsion strain energy alerts class."""
 1115 
 1116     try:
 1117         TorsionStrainEnergyAlertsHandle = TorsionStrainEnergyAlerts(
 1118             AlertsMode=OptionsInfo["AlertsMode"],
 1119             TotalEnergyCutoff=OptionsInfo["TotalEnergyCutoff"],
 1120             MaxSingleEnergyCutoff=OptionsInfo["MaxSingleEnergyCutoff"],
 1121             RotBondsSMARTSMode=OptionsInfo["RotBondsSMARTSMode"],
 1122             RotBondsSMARTSPattern=OptionsInfo["RotBondsSMARTSPattern"],
 1123             TorsionLibraryFilePath=OptionsInfo["TorsionEnergyLibraryFile"],
 1124             AlertTorsionsNotObserved=OptionsInfo["FilterTorsionsNotObserved"],
 1125         )
 1126     except Exception as ErrMsg:
 1127         MiscUtil.PrintError("Failed to instantiate TorsionStrainEnergyAlerts:\n%s\n" % (ErrMsg))
 1128 
 1129     if not Quiet:
 1130         MiscUtil.PrintInfo(
 1131             "\nRetrieving data from torsion strain energy library file %s..."
 1132             % TorsionStrainEnergyAlertsHandle.GetTorsionLibraryFilePath()
 1133         )
 1134         TorsionStrainEnergyAlertsHandle.ListTorsionLibraryInfo()
 1135 
 1136     return TorsionStrainEnergyAlertsHandle
 1137 
 1138 
 1139 def ProcessRotatableBondsSMARTSMode():
 1140     """ "Process SMARTS pattern for rotatable bonds."""
 1141 
 1142     RotBondsMode = OptionsInfo["RotBondsSMARTSMode"]
 1143 
 1144     RotBondsSMARTSPattern = None
 1145     RotBondsSMARTSPatternSpecified = OptionsInfo["RotBondsSMARTSPatternSpecified"]
 1146 
 1147     if re.match("^(NonStrict|SemiStrict|Strict)$", RotBondsMode, re.I):
 1148         RotBondsSMARTSPattern = None
 1149     elif re.match("Specify", RotBondsMode, re.I):
 1150         RotBondsSMARTSPatternSpecified = RotBondsSMARTSPatternSpecified.strip()
 1151         if not len(RotBondsSMARTSPatternSpecified):
 1152             MiscUtil.PrintError(
 1153                 'Empty value specified for SMILES/SMARTS pattern in  "--rotBondsSMARTSPattern" option, %s.'
 1154                 % RotBondsMode
 1155             )
 1156 
 1157         RotBondsPatternMol = Chem.MolFromSmarts(RotBondsSMARTSPatternSpecified)
 1158         if RotBondsPatternMol is None:
 1159             MiscUtil.PrintError(
 1160                 'Failed to create rotatable bonds pattern molecule. The rotatable bonds SMARTS pattern, "%s", specified using "--rotBondsSMARTSPattern" option is not valid.'
 1161                 % (RotBondsSMARTSPatternSpecified)
 1162             )
 1163     else:
 1164         MiscUtil.PrintError(
 1165             'The value, %s, specified for option "-r, --rotBondsSMARTSMode" is not valid. ' % RotBondsMode
 1166         )
 1167 
 1168     OptionsInfo["RotBondsSMARTSPattern"] = RotBondsSMARTSPattern
 1169 
 1170 
 1171 def ProcessSDFieldLabelsOption():
 1172     """Process SD data field label option."""
 1173 
 1174     ParamsOptionName = "--outfileSDFieldLabels"
 1175     ParamsOptionValue = Options["--outfileSDFieldLabels"]
 1176 
 1177     ParamsIDsToLabels = {
 1178         "RotBondsCountLabel": "RotBondsCount",
 1179         "TotalEnergyLabel": "TotalEnergy",
 1180         "TotalEnergyLowerBoundCILabel": "TotalEnergyLowerBoundCI",
 1181         "TotalEnergyUpperBoundCILabel": "TotalEnergyUpperBoundCI",
 1182         "MaxSingleEnergyLabel": "MaxSingleEnergy",
 1183         "MaxSingleEnergyAlertsCountLabel": "MaxSingleEnergyAlertsCount",
 1184         "AnglesNotObservedCountLabel": "AnglesNotObservedCount",
 1185         "TorsionAlertsLabel": "TorsionAlerts(RotBondIndices TorsionIndices TorsionAngle Energy EnergyLowerBoundCI EnergyUpperBoundCI HierarchyClass HierarchySubClass TorsionRule EnergyMethod AngleNotObserved MaxSingleEnergyAlert)",
 1186         "TorsionRuleLabel": "TorsionRule (HierarchyClass HierarchySubClass TorsionRule EnergyMethod)",
 1187         "TorsionRuleMaxSingleEnergyAlertsCountLabel": "TorsionRuleMaxSingleEnergyAlertsCount",
 1188         "TorsionRuleAnglesNotObservedCountLabel": "TorsionRuleAnglesNotObservedCount",
 1189         "TorsionRuleAlertsLabel": "TorsionRuleAlerts (RotBondIndices TorsionIndices TorsionAngle Energy EnergyLowerBoundCI EnergyUpperBoundCI AngleNotObserved MaxSingleEnergyAlert)",
 1190     }
 1191 
 1192     if re.match("^auto$", ParamsOptionValue, re.I):
 1193         OptionsInfo["SDFieldIDsToLabels"] = ParamsIDsToLabels
 1194         return
 1195 
 1196     # Setup a canonical paramater names...
 1197     ValidParamNames = []
 1198     CanonicalParamNamesMap = {}
 1199     for ParamName in sorted(ParamsIDsToLabels):
 1200         ValidParamNames.append(ParamName)
 1201         CanonicalParamNamesMap[ParamName.lower()] = ParamName
 1202 
 1203     ParamsOptionValue = ParamsOptionValue.strip()
 1204     if not ParamsOptionValue:
 1205         MiscUtil.PrintError('No valid parameter name and value pairs specified using "%s" option' % ParamsOptionName)
 1206 
 1207     ParamsOptionValueWords = ParamsOptionValue.split(",")
 1208     if len(ParamsOptionValueWords) % 2:
 1209         MiscUtil.PrintError(
 1210             'The number of comma delimited paramater names and values, %d, specified using "%s" option must be an even number.'
 1211             % (len(ParamsOptionValueWords), ParamsOptionName)
 1212         )
 1213 
 1214     # Validate paramater name and value pairs...
 1215     for Index in range(0, len(ParamsOptionValueWords), 2):
 1216         Name = ParamsOptionValueWords[Index].strip()
 1217         Value = ParamsOptionValueWords[Index + 1].strip()
 1218 
 1219         CanonicalName = Name.lower()
 1220         if CanonicalName not in CanonicalParamNamesMap:
 1221             MiscUtil.PrintError(
 1222                 'The parameter name, %s, specified using "%s" is not a valid name. Supported parameter names: %s'
 1223                 % (Name, ParamsOptionName, " ".join(ValidParamNames))
 1224             )
 1225 
 1226         ParamName = CanonicalParamNamesMap[CanonicalName]
 1227         ParamValue = Value
 1228 
 1229         # Set value...
 1230         ParamsIDsToLabels[ParamName] = ParamValue
 1231 
 1232     OptionsInfo["SDFieldIDsToLabels"] = ParamsIDsToLabels
 1233 
 1234 
 1235 def ProcessOptions():
 1236     """Process and validate command line arguments and options."""
 1237 
 1238     MiscUtil.PrintInfo("Processing options...")
 1239 
 1240     # Validate options...
 1241     ValidateOptions()
 1242 
 1243     TotalEnergyMode, MaxSingleEnergyMode, TotalOrMaxSingleEnergyMode = [False] * 3
 1244     AlertsMode = Options["--alertsMode"]
 1245     if re.match("^TotalEnergy$", AlertsMode, re.I):
 1246         TotalEnergyMode = True
 1247     elif re.match("^MaxSingleEnergy$", AlertsMode, re.I):
 1248         MaxSingleEnergyMode = True
 1249     elif re.match("^TotalOrMaxSingleEnergy$", AlertsMode, re.I):
 1250         TotalOrMaxSingleEnergyMode = True
 1251     OptionsInfo["AlertsMode"] = AlertsMode
 1252     OptionsInfo["TotalEnergyMode"] = TotalEnergyMode
 1253     OptionsInfo["MaxSingleEnergyMode"] = MaxSingleEnergyMode
 1254     OptionsInfo["TotalOrMaxSingleEnergyMode"] = TotalOrMaxSingleEnergyMode
 1255 
 1256     OptionsInfo["FilterTorsionsNotObserved"] = (
 1257         True if re.match("^yes$", Options["--filterTorsionsNotObserved"], re.I) else False
 1258     )
 1259 
 1260     OptionsInfo["MaxSingleEnergyCutoff"] = float(Options["--alertsMaxSingleEnergyCutoff"])
 1261     OptionsInfo["TotalEnergyCutoff"] = float(Options["--alertsTotalEnergyCutoff"])
 1262 
 1263     OptionsInfo["Infile"] = Options["--infile"]
 1264     ParamsDefaultInfoOverride = {"RemoveHydrogens": False}
 1265     OptionsInfo["InfileParams"] = MiscUtil.ProcessOptionInfileParameters(
 1266         "--infileParams",
 1267         Options["--infileParams"],
 1268         InfileName=Options["--infile"],
 1269         ParamsDefaultInfo=ParamsDefaultInfoOverride,
 1270     )
 1271 
 1272     OptionsInfo["Outfile"] = Options["--outfile"]
 1273     OptionsInfo["OutfileParams"] = MiscUtil.ProcessOptionOutfileParameters(
 1274         "--outfileParams", Options["--outfileParams"], Options["--infile"], Options["--outfile"]
 1275     )
 1276 
 1277     FileDir, FileName, FileExt = MiscUtil.ParseFileName(Options["--outfile"])
 1278     OutfileFiltered = "%s_Filtered.%s" % (FileName, FileExt)
 1279     OptionsInfo["OutfileFiltered"] = OutfileFiltered
 1280     OptionsInfo["OutfileFilteredMode"] = True if re.match("^yes$", Options["--outfileFiltered"], re.I) else False
 1281 
 1282     OptionsInfo["OutfileSummary"] = "%s_AlertsSummary.csv" % (FileName)
 1283 
 1284     OutfileSummaryMode = Options["--outfileSummary"]
 1285     if re.match("^auto$", OutfileSummaryMode, re.I):
 1286         OutfileSummaryMode = "yes" if re.match("^MaxSingleEnergy$", Options["--alertsMode"], re.I) else "no"
 1287     OptionsInfo["OutfileSummaryMode"] = True if re.match("^yes$", OutfileSummaryMode, re.I) else False
 1288 
 1289     if re.match("^yes$", Options["--outfileSummary"], re.I):
 1290         if not re.match("^MaxSingleEnergy$", Options["--alertsMode"], re.I):
 1291             MiscUtil.PrintError(
 1292                 'The value "%s" specified for "--outfileSummary" option is not valid. The specified value is only allowed during "MaxSingleEnergy" value of "-a, --alertsMode" option.'
 1293                 % (Options["--outfileSummary"])
 1294             )
 1295 
 1296     OutfilesFilteredByRulesMode = Options["--outfilesFilteredByRules"]
 1297     if re.match("^auto$", OutfilesFilteredByRulesMode, re.I):
 1298         OutfilesFilteredByRulesMode = "yes" if re.match("^MaxSingleEnergy$", Options["--alertsMode"], re.I) else "no"
 1299     OptionsInfo["OutfilesFilteredByRulesMode"] = True if re.match("^yes$", OutfilesFilteredByRulesMode, re.I) else False
 1300 
 1301     if re.match("^yes$", Options["--outfilesFilteredByRules"], re.I):
 1302         if not re.match("^MaxSingleEnergy$", Options["--alertsMode"], re.I):
 1303             MiscUtil.PrintError(
 1304                 'The value "%s" specified for "--outfilesFilteredByRules" option is not valid. The specified value is only allowed during "MaxSingleEnergy" value of "-a, --alertsMode" option.'
 1305                 % (Options["--outfileSummary"])
 1306             )
 1307 
 1308     OptionsInfo["TrackAlertsSummaryInfo"] = (
 1309         True if (OptionsInfo["OutfileSummaryMode"] or OptionsInfo["OutfilesFilteredByRulesMode"]) else False
 1310     )
 1311 
 1312     OutfilesFilteredByRulesMaxCount = Options["--outfilesFilteredByRulesMaxCount"]
 1313     if not re.match("^All$", OutfilesFilteredByRulesMaxCount, re.I):
 1314         OutfilesFilteredByRulesMaxCount = int(OutfilesFilteredByRulesMaxCount)
 1315     OptionsInfo["OutfilesFilteredByRulesMaxCount"] = OutfilesFilteredByRulesMaxCount
 1316     OptionsInfo["OutfilesFilteredByRulesAllMode"] = (
 1317         True if re.match("^All$", Options["--outfilesFilteredByRulesMaxCount"], re.I) else False
 1318     )
 1319 
 1320     OptionsInfo["OutfileAlerts"] = True if re.match("^yes$", Options["--outfileAlerts"], re.I) else False
 1321 
 1322     if re.match("^yes$", Options["--outfilesFilteredByRules"], re.I):
 1323         if not re.match("^yes$", Options["--outfileAlerts"], re.I):
 1324             MiscUtil.PrintError(
 1325                 'The value "%s" specified for "--outfilesFilteredByRules" option is not valid. The specified value is only allowed during "yes" value of "--outfileAlerts" option.'
 1326                 % (Options["--outfilesFilteredByRules"])
 1327             )
 1328 
 1329     OptionsInfo["OutfileAlertsMode"] = Options["--outfileAlertsMode"]
 1330     OptionsInfo["OutfileAlertsOnly"] = True if re.match("^AlertsOnly$", Options["--outfileAlertsMode"], re.I) else False
 1331 
 1332     ProcessSDFieldLabelsOption()
 1333 
 1334     OptionsInfo["Overwrite"] = Options["--overwrite"]
 1335     OptionsInfo["CountMode"] = True if re.match("^count$", Options["--mode"], re.I) else False
 1336 
 1337     OptionsInfo["Precision"] = int(Options["--precision"])
 1338 
 1339     OptionsInfo["MPMode"] = True if re.match("^yes$", Options["--mp"], re.I) else False
 1340     OptionsInfo["MPParams"] = MiscUtil.ProcessOptionMultiprocessingParameters("--mpParams", Options["--mpParams"])
 1341 
 1342     OptionsInfo["RotBondsSMARTSMode"] = Options["--rotBondsSMARTSMode"]
 1343     OptionsInfo["RotBondsSMARTSPatternSpecified"] = Options["--rotBondsSMARTSPattern"]
 1344     ProcessRotatableBondsSMARTSMode()
 1345 
 1346     OptionsInfo["TorsionEnergyLibraryFile"] = Options["--torsionEnergyLibraryFile"]
 1347 
 1348     # Setup delimiter for writing out torsion alert information to output files...
 1349     OptionsInfo["IntraSetValuesDelim"] = ","
 1350     OptionsInfo["InterSetValuesDelim"] = " "
 1351 
 1352 
 1353 def RetrieveOptions():
 1354     """Retrieve command line arguments and options."""
 1355 
 1356     # Get options...
 1357     global Options
 1358     Options = docopt(_docoptUsage_)
 1359 
 1360     # Set current working directory to the specified directory...
 1361     WorkingDir = Options["--workingdir"]
 1362     if WorkingDir:
 1363         os.chdir(WorkingDir)
 1364 
 1365     # Handle examples option...
 1366     if "--examples" in Options and Options["--examples"]:
 1367         MiscUtil.PrintInfo(MiscUtil.GetExamplesTextFromDocOptText(_docoptUsage_))
 1368         sys.exit(0)
 1369 
 1370 
 1371 def ProcessListTorsionLibraryOption():
 1372     """Process list torsion library information."""
 1373 
 1374     # Validate and process dataFile option for listing torsion library information...
 1375     OptionsInfo["TorsionEnergyLibraryFile"] = Options["--torsionEnergyLibraryFile"]
 1376     if not re.match("^auto$", Options["--torsionEnergyLibraryFile"], re.I):
 1377         MiscUtil.ValidateOptionFilePath("-t, --torsionEnergyLibraryFile", Options["--torsionEnergyLibraryFile"])
 1378 
 1379     # Instantiate TorsionStrainEnergyAlerts using defaults...
 1380     TorsionStrainEnergyAlertsHandle = TorsionStrainEnergyAlerts(
 1381         TorsionLibraryFilePath=OptionsInfo["TorsionEnergyLibraryFile"]
 1382     )
 1383     MiscUtil.PrintInfo(
 1384         "\nRetrieving data from torsion strain energy library file %s..."
 1385         % TorsionStrainEnergyAlertsHandle.GetTorsionLibraryFilePath()
 1386     )
 1387     TorsionStrainEnergyAlertsHandle.ListTorsionLibraryInfo()
 1388 
 1389 
 1390 def ValidateOptions():
 1391     """Validate option values."""
 1392 
 1393     MiscUtil.ValidateOptionTextValue(
 1394         "-a, --alertsMode", Options["--alertsMode"], "TotalEnergy MaxSingleEnergy TotalOrMaxSingleEnergy"
 1395     )
 1396 
 1397     MiscUtil.ValidateOptionFloatValue(
 1398         "--alertsMaxSingleEnergyCutoff", Options["--alertsMaxSingleEnergyCutoff"], {">": 0.0}
 1399     )
 1400     MiscUtil.ValidateOptionFloatValue("--alertsTotalEnergyCutoff", Options["--alertsTotalEnergyCutoff"], {">": 0.0})
 1401 
 1402     MiscUtil.ValidateOptionTextValue("--filterTorsionsNotObserved", Options["--filterTorsionsNotObserved"], "yes no")
 1403 
 1404     MiscUtil.ValidateOptionFilePath("-i, --infile", Options["--infile"])
 1405     MiscUtil.ValidateOptionFileExt("-i, --infile", Options["--infile"], "sdf sd mol")
 1406 
 1407     MiscUtil.ValidateOptionFileExt("-o, --outfile", Options["--outfile"], "sdf sd")
 1408     if re.match("^filter$", Options["--mode"], re.I):
 1409         MiscUtil.ValidateOptionsOutputFileOverwrite(
 1410             "-o, --outfile", Options["--outfile"], "--overwrite", Options["--overwrite"]
 1411         )
 1412         MiscUtil.ValidateOptionsDistinctFileNames(
 1413             "-i, --infile", Options["--infile"], "-o, --outfile", Options["--outfile"]
 1414         )
 1415 
 1416     MiscUtil.ValidateOptionTextValue("--outfileFiltered", Options["--outfileFiltered"], "yes no")
 1417 
 1418     MiscUtil.ValidateOptionTextValue("--outfilesFilteredByRules", Options["--outfilesFilteredByRules"], "yes no auto")
 1419     if not re.match("^All$", Options["--outfilesFilteredByRulesMaxCount"], re.I):
 1420         MiscUtil.ValidateOptionIntegerValue(
 1421             "--outfilesFilteredByRulesMaxCount", Options["--outfilesFilteredByRulesMaxCount"], {">": 0}
 1422         )
 1423 
 1424     MiscUtil.ValidateOptionTextValue("--outfileSummary", Options["--outfileSummary"], "yes no auto")
 1425     MiscUtil.ValidateOptionTextValue("--outfileAlerts", Options["--outfileAlerts"], "yes no")
 1426     MiscUtil.ValidateOptionTextValue("--outfileAlertsMode", Options["--outfileAlertsMode"], "All AlertsOnly")
 1427 
 1428     MiscUtil.ValidateOptionTextValue("-m, --mode", Options["--mode"], "filter count")
 1429     if re.match("^filter$", Options["--mode"], re.I):
 1430         if not Options["--outfile"]:
 1431             MiscUtil.PrintError(
 1432                 'The outfile must be specified using "-o, --outfile" during "filter" value of "-m, --mode" option'
 1433             )
 1434 
 1435     MiscUtil.ValidateOptionTextValue("--mp", Options["--mp"], "yes no")
 1436 
 1437     MiscUtil.ValidateOptionIntegerValue("-p, --precision", Options["--precision"], {">": 0})
 1438 
 1439     MiscUtil.ValidateOptionTextValue(
 1440         "-r, --rotBondsSMARTSMode", Options["--rotBondsSMARTSMode"], "NonStrict SemiStrict Strict Specify"
 1441     )
 1442     if re.match("^Specify$", Options["--rotBondsSMARTSMode"], re.I):
 1443         if not Options["--rotBondsSMARTSPattern"]:
 1444             MiscUtil.PrintError(
 1445                 'The SMARTS pattern must be specified using "--rotBondsSMARTSPattern" during "Specify" value of "-r, --rotBondsSMARTS" option'
 1446             )
 1447     else:
 1448         if Options["--rotBondsSMARTSPattern"]:
 1449             MiscUtil.PrintError(
 1450                 'The SMARTS pattern must not be specified using "--rotBondsSMARTSPattern" during "%s" value of "-r, --rotBondsSMARTS" option'
 1451                 % (Options["--rotBondsSMARTSMode"])
 1452             )
 1453 
 1454     if not re.match("^auto$", Options["--torsionEnergyLibraryFile"], re.I):
 1455         MiscUtil.ValidateOptionFilePath("-t, --torsionEnergyLibraryFile", Options["--torsionEnergyLibraryFile"])
 1456 
 1457 
 1458 # Setup a usage string for docopt...
 1459 _docoptUsage_ = """
 1460 RDKitFilterTorsionStrainEnergyAlerts.py - Filter torsion strain energy library alerts
 1461 
 1462 Usage:
 1463     RDKitFilterTorsionStrainEnergyAlerts.py [--alertsMode <TotalEnergy, MaxSingleEnergy, or TotalOrMaxSingleEnergy>]
 1464                                             [--alertsMaxSingleEnergyCutoff <Number>] [--alertsTotalEnergyCutoff <Number>]
 1465                                             [--filterTorsionsNotObserved <yes or no>] [--infileParams <Name,Value,...>] [--mode <filter or count>]
 1466                                             [--mp <yes or no>] [--mpParams <Name,Value,...>] [--outfileAlerts <yes or no>]
 1467                                             [--outfileAlertsMode <All or AlertsOnly>] [--outfileFiltered <yes or no>]
 1468                                             [--outfilesFilteredByRules <yes or no>] [--outfilesFilteredByRulesMaxCount <All or number>]
 1469                                             [--outfileSummary <yes or no>] [--outfileSDFieldLabels <Type,Label,...>] [--outfileParams <Name,Value,...>]
 1470                                             [--overwrite] [--precision <number>] [ --rotBondsSMARTSMode <NonStrict, SemiStrict,...>]
 1471                                             [--rotBondsSMARTSPattern <SMARTS>] [--torsionEnergyLibraryFile <FileName or auto>]
 1472                                             [-w <dir>] -i <infile> -o <outfile>
 1473     RDKitFilterTorsionStrainEnergyAlerts.py [--torsionEnergyLibraryFile <FileName or auto>] -l | --list
 1474     RDKitFilterTorsionStrainEnergyAlerts.py -h | --help | -e | --examples
 1475 
 1476 Description:
 1477     Filter strained molecules from an input file for torsion strain energy library
 1478     [ Ref 153 ] alerts by matching rotatable bonds against SMARTS patterns specified
 1479     for torsion rules in a torsion energy library file and write out appropriate
 1480     molecules to output files. The molecules must have 3D coordinates in input file.
 1481     The default torsion strain energy library file, TorsionStrainEnergyLibrary.xml,
 1482     is available under MAYACHEMTOOLS/lib/python/TorsionAlerts directory.
 1483     
 1484     The data in torsion strain energy library file is organized in a hierarchical
 1485     manner. It consists of one generic class and six specific classes at the highest
 1486     level. Each class contains multiple subclasses corresponding to named functional
 1487     groups or substructure patterns. The subclasses consist of torsion rules sorted
 1488     from specific to generic torsion patterns. The torsion rule, in turn, contains a
 1489     list of peak values for torsion angles and two tolerance values. A pair of tolerance
 1490     values define torsion bins around a torsion peak value.
 1491     
 1492     A strain energy calculation method, 'exact' or 'approximate' [ Ref 153 ], is 
 1493     associated with each torsion rule for calculating torsion strain energy. The 'exact'
 1494     stain energy calculation relies on the energy bins available under the energy histogram
 1495     consisting of 36 bins covering angles from -180 to 180. The width of each bin is 10
 1496     degree. The energy bins are are defined at the right end points. The first and the
 1497     last energy bins correspond to -170 and 180 respectively. The torsion angle is mapped
 1498     to a energy bin. An angle offset is calculated for the torsion angle from the the right
 1499     end point angle of the bin. The strain energy is estimated for the angle offset based
 1500     on the energy difference between the current and previous bins. The torsion strain
 1501     energy, in terms of torsion energy units (TEUs), corresponds to the sum of bin strain
 1502     energy and the angle offset strain energy.
 1503         
 1504         Energy = BinEnergyDiff/10.0 * BinAngleOffset + BinEnergy[BinNum]
 1505         
 1506         Where:
 1507         
 1508         BinEnergyDiff = BinEnergy[BinNum] - BinEnergy[PreviousBinNum]
 1509         BinAngleOffset = TorsionAngle - BinAngleRightSide
 1510         
 1511     The 'approximate' strain energy calculation relies on the angle difference between a
 1512     torsion angle and the torsion peaks observed for the torsion rules in the torsion
 1513     energy library. The torsion angle is matched to a torsion peak based on the value of
 1514     torsion angle difference. It must be less than or equal to the value for the second
 1515     tolerance 'tolerance2'. Otherwise, the torsion angle is not observed in the torsion
 1516     energy library and a value of 'NA' is assigned for torsion energy along with the lower
 1517     and upper bounds on energy at 95% confidence interval. The 'approximate' torsion
 1518     energy (TEUs) for observed torsion angle is calculated using the following formula:
 1519         
 1520         Energy = beta_1 * (AngleDiff ** 2) + beta_2 * (AngleDiff ** 4)
 1521         
 1522     The coefficients 'beta_1' and 'beta_2' are available for the observed angles in 
 1523     the torsion strain energy library. The 'AngleDiff' is the difference between the
 1524     torsion angle and the matched torsion peak.
 1525     
 1526     For example:
 1527         
 1528         <library>
 1529             <hierarchyClass id1="G" id2="G" name="GG">
 1530             ...
 1531             </hierarchyClass>
 1532             <hierarchyClass id1="C" id2="O" name="CO">
 1533                 <hierarchySubClass name="Ester bond I" smarts="O=[C:2][O:3]">
 1534                     <torsionRule method="exact" smarts=
 1535                         "[O:1]=[C:2]!@[O:3]~[CH0:4]">
 1536                         <angleList>
 1537                             <angle score="56.52" tolerance1="20.00"
 1538                             tolerance2="25.00" value="0.0"/>
 1539                         </angleList>
 1540                         <histogram>
 1541                             <bin count="1"/>
 1542                             ...
 1543                         </histogram>
 1544                         <histogram_shifted>
 1545                             <bin count="0"/>
 1546                             ...
 1547                         </histogram_shifted>
 1548                         <histogram_converted>
 1549                             <bin energy="4.67... lower="2.14..." upper="Inf"/>
 1550                             ...
 1551                             <bin energy="1.86..." lower="1.58..." upper="2.40..."/>
 1552                             ...
 1553                            </histogram_converted>
 1554                     </torsionRule>
 1555                     <torsionRule method="approximate" smarts=
 1556                         "[cH0:1][c:2]([cH0])!@[O:3][p:4]">
 1557                         <angleList>
 1558                         <angle beta_1="0.002..." beta_2="-7.843...e-07"
 1559                             score="27.14" theta_0="-90.0" tolerance1="30.00"
 1560                             tolerance2="45.00" value="-90.0"/>
 1561                         ...
 1562                         </angleList>
 1563                         <histogram>
 1564                             <bin count="0"/>
 1565                             ...
 1566                         </histogram>
 1567                         <histogram_shifted>
 1568                             <bin count="0"/>
 1569                             ...
 1570                         </histogram_shifted>
 1571                     </torsionRule>
 1572                 ...
 1573              ...
 1574             </hierarchyClass>
 1575             <hierarchyClass id1="N" id2="C" name="NC">
 1576             ...
 1577             </hierarchyClass>
 1578             <hierarchyClass id1="S" id2="N" name="SN">
 1579             ...
 1580             </hierarchyClass>
 1581             <hierarchyClass id1="C" id2="S" name="CS">
 1582             ...
 1583             </hierarchyClass>
 1584             <hierarchyClass id1="C" id2="C" name="CC">
 1585             ...
 1586             </hierarchyClass>
 1587             <hierarchyClass id1="S" id2="S" name="SS">
 1588              ...
 1589             </hierarchyClass>
 1590         </library>
 1591         
 1592     The rotatable bonds in a 3D molecule are identified using a default SMARTS pattern.
 1593     A custom SMARTS pattern may be optionally specified to detect rotatable bonds.
 1594     Each rotatable bond is matched to a torsion rule in the torsion strain energy library.
 1595     The strain energy is calculated for each rotatable bond using the calculation
 1596     method, 'exact' or 'approximate', associated with the matched torsion rule.
 1597     
 1598     The total strain energy (TEUs) of a molecule corresponds to the sum of  'exact' and
 1599     'approximate' strain energies calculated for all matched rotatable bonds in the
 1600     molecule. The total strain energy is set to 'NA' for molecules containing a 'approximate'
 1601     energy estimate for a torsion angle not observed in the torsion energy library. In
 1602     addition, the lower and upper bounds on energy at 95% confidence interval are
 1603     set to 'NA'.
 1604     
 1605     The following output files are generated after the filtering:
 1606         
 1607         <OutfileRoot>.sdf
 1608         <OutfileRoot>_Filtered.sdf
 1609         <OutfileRoot>_AlertsSummary.csv
 1610         <OutfileRoot>_Filtered_TopRule*.sdf
 1611         
 1612     The last two set of outfile files, <OutfileRoot>_AlertsSummary.csv and
 1613     <OutfileRoot>_<OutfileRoot>_AlertsSummary.csv, are only generated during filtering
 1614     by 'MaxSingleEnergy'.
 1615     
 1616     The supported input file formats are: Mol (.mol), SD (.sdf, .sd)
 1617     
 1618     The supported output file formats are: SD (.sdf, .sd)
 1619 
 1620 Options:
 1621     -a, --alertsMode <TotalEnergy,...>  [default: TotalEnergy]
 1622         Torsion strain energy library alert types to use for filtering molecules
 1623         containing rotatable bonds based on the calculated values for the total
 1624         torsion strain energy of a molecule and  the maximum single strain
 1625         energy of a rotatable bond in a molecule.
 1626         
 1627         Possible values: TotalEnergy, MaxSingleEnergy, or TotalOrMaxSingleEnergy
 1628         
 1629         The strain energy cutoff values in terms of torsion energy units (TEUs) are
 1630         used to filter molecules as shown below:
 1631             
 1632             AlertsMode                AlertsEnergyCutoffs (TEUs)
 1633             
 1634             TotalEnergy               >= TotalEnergyCutoff
 1635             
 1636             MaxSingleEnergy           >= MaxSingleEnergyCutoff
 1637             
 1638             TotalOrMaxSingleEnergy    >= TotalEnergyCutoff
 1639                                       or >= MaxSingleEnergyCutoff
 1640         
 1641     --alertsMaxSingleEnergyCutoff <Number>  [default: 1.8]
 1642         Maximum single strain energy (TEUs) cutoff [ Ref 153 ] for filtering molecules
 1643         based on the maximum value of a single strain energy of a rotatable bond
 1644         in  a molecule. This option is used during 'MaxSingleEnergy' or
 1645         'TotalOrMaxSingleEnergy' values of '-a, --alertsMode' option.
 1646         
 1647         The maximum single strain energy must be greater than or equal to the
 1648         specified cutoff value for filtering molecules.
 1649     --alertsTotalEnergyCutoff <Number>  [default: 6.0]
 1650         Total strain strain energy (TEUs) cutoff [ Ref 153 ] for filtering molecules
 1651         based on total strain energy for all rotatable bonds in a molecule. This
 1652         option is used during 'TotalEnergy' or 'TotalOrMaxSingleEnergy'
 1653         values of '-a, --alertsMode' option.
 1654         
 1655         The total strain energy must be greater than or equal to the specified
 1656         cutoff value for filtering molecules.
 1657     --filterTorsionsNotObserved <yes or no>  [default: no]
 1658         Filter molecules containing torsion angles not observed in torsion strain
 1659         energy library. It's not possible to calculate torsion strain energies for
 1660         these torsions during 'approximate' match to a specified torsion in the
 1661         library.
 1662         
 1663         The 'approximate' strain energy calculation relies on the angle difference
 1664         between a torsion angle and the torsion peaks observed for the torsion
 1665         rules in the torsion energy library. The torsion angle is matched to a
 1666         torsion peak based on the value of torsion angle difference. It must be
 1667         less than or equal to the value for the second tolerance 'tolerance2'.
 1668         Otherwise, the torsion angle is not observed in the torsion energy library
 1669         and a value of 'NA' is assigned for torsion energy along with the lower and
 1670         upper bounds on energy at 95% confidence interval.
 1671     -e, --examples
 1672         Print examples.
 1673     -h, --help
 1674         Print this help message.
 1675     -i, --infile <infile>
 1676         Input file name.
 1677     --infileParams <Name,Value,...>  [default: auto]
 1678         A comma delimited list of parameter name and value pairs for reading
 1679         molecules from files. The supported parameter names for different file
 1680         formats, along with their default values, are shown below:
 1681             
 1682             SD, MOL: removeHydrogens,no,sanitize,yes,strictParsing,yes
 1683             
 1684     -l, --list
 1685         List torsion library information without performing any filtering.
 1686     -m, --mode <filter or count>  [default: filter]
 1687         Specify whether to filter molecules for torsion strain energy library [ Ref 153 ]
 1688         alerts by matching rotatable bonds against SMARTS patterns specified for
 1689         torsion rules to calculate torsion strain energies and write out the rest
 1690         of the molecules to an outfile or simply count the number of matched
 1691         molecules marked for filtering.
 1692     --mp <yes or no>  [default: no]
 1693         Use multiprocessing.
 1694          
 1695         By default, input data is retrieved in a lazy manner via mp.Pool.imap()
 1696         function employing lazy RDKit data iterable. This allows processing of
 1697         arbitrary large data sets without any additional requirements memory.
 1698         
 1699         All input data may be optionally loaded into memory by mp.Pool.map()
 1700         before starting worker processes in a process pool by setting the value
 1701         of 'inputDataMode' to 'InMemory' in '--mpParams' option.
 1702         
 1703         A word to the wise: The default 'chunkSize' value of 1 during 'Lazy' input
 1704         data mode may adversely impact the performance. The '--mpParams' section
 1705         provides additional information to tune the value of 'chunkSize'.
 1706     --mpParams <Name,Value,...>  [default: auto]
 1707         A comma delimited list of parameter name and value pairs to configure
 1708         multiprocessing.
 1709         
 1710         The supported parameter names along with their default and possible
 1711         values are shown below:
 1712         
 1713             chunkSize, auto
 1714             inputDataMode, Lazy   [ Possible values: InMemory or Lazy ]
 1715             numProcesses, auto   [ Default: mp.cpu_count() ]
 1716         
 1717         These parameters are used by the following functions to configure and
 1718         control the behavior of multiprocessing: mp.Pool(), mp.Pool.map(), and
 1719         mp.Pool.imap().
 1720         
 1721         The chunkSize determines chunks of input data passed to each worker
 1722         process in a process pool by mp.Pool.map() and mp.Pool.imap() functions.
 1723         The default value of chunkSize is dependent on the value of 'inputDataMode'.
 1724         
 1725         The mp.Pool.map() function, invoked during 'InMemory' input data mode,
 1726         automatically converts RDKit data iterable into a list, loads all data into
 1727         memory, and calculates the default chunkSize using the following method
 1728         as shown in its code:
 1729         
 1730             chunkSize, extra = divmod(len(dataIterable), len(numProcesses) * 4)
 1731             if extra: chunkSize += 1
 1732         
 1733         For example, the default chunkSize will be 7 for a pool of 4 worker processes
 1734         and 100 data items.
 1735         
 1736         The mp.Pool.imap() function, invoked during 'Lazy' input data mode, employs
 1737         'lazy' RDKit data iterable to retrieve data as needed, without loading all the
 1738         data into memory. Consequently, the size of input data is not known a priori.
 1739         It's not possible to estimate an optimal value for the chunkSize. The default 
 1740         chunkSize is set to 1.
 1741         
 1742         The default value for the chunkSize during 'Lazy' data mode may adversely
 1743         impact the performance due to the overhead associated with exchanging
 1744         small chunks of data. It is generally a good idea to explicitly set chunkSize to
 1745         a larger value during 'Lazy' input data mode, based on the size of your input
 1746         data and number of processes in the process pool.
 1747         
 1748         The mp.Pool.map() function waits for all worker processes to process all
 1749         the data and return the results. The mp.Pool.imap() function, however,
 1750         returns the the results obtained from worker processes as soon as the
 1751         results become available for specified chunks of data.
 1752         
 1753         The order of data in the results returned by both mp.Pool.map() and 
 1754         mp.Pool.imap() functions always corresponds to the input data.
 1755     -o, --outfile <outfile>
 1756         Output file name.
 1757     --outfileAlerts <yes or no>  [default: yes]
 1758         Write out alerts information to SD output files.
 1759     --outfileAlertsMode <All or AlertsOnly>  [default: AlertsOnly]
 1760         Write alerts information to SD output files for all alerts or only for alerts
 1761         specified by '--AlertsMode' option. Possible values: All or AlertsOnly
 1762         This option is only valid for 'Yes' value of '--outfileAlerts' option.
 1763         
 1764         The following alerts information is added to SD output files using
 1765         'TorsionAlerts' data field:
 1766             
 1767             RotBondIndices TorsionIndices TorsionAngle
 1768             Energy EnergyLowerBoundCI EnergyUpperBoundCI
 1769             HierarchyClass HierarchySubClass TorsionRule
 1770             EnergyMethod AngleNotObserved MaxSingleEnergyAlert
 1771             
 1772         The following data filelds are added to SD output files based on the value of
 1773         '--AlertsMode' option:
 1774             
 1775             TotalEnergy
 1776             TotalEnergyLowerBoundCI
 1777             TotalEnergyUpperBoundCI
 1778             
 1779             MaxSingleEnergy
 1780             MaxSingleEnergyAlertsCount
 1781             
 1782             AnglesNotObservedCount
 1783             
 1784         The 'RotBondsCount' is always added to SD output files containing both
 1785         remaining and filtered molecules.
 1786         
 1787         Format:
 1788             
 1789             > <RotBondsCount>
 1790             Number
 1791             
 1792             > <TotalEnergy>
 1793             Number
 1794             
 1795             > <TotalEnergyLowerBoundCI>
 1796             Number
 1797             
 1798             > <TotalEnergyUpperBoundCI>
 1799             Number
 1800             
 1801             > <MaxSingleEnergy>
 1802             Number
 1803             
 1804             > <MaxSingleEnergyAlertsCount>
 1805             Number
 1806             
 1807             > <AnglesNotObservedCount>
 1808             Number
 1809             
 1810             > <TorsionAlerts (RotBondIndices TorsionIndices TorsionAngle
 1811                 Energy EnergyLowerBoundCI EnergyUpperBoundCI
 1812                 HierarchyClass HierarchySubClass TorsionRule
 1813                 EnergyMethod AngleNotObserved MaxSingleEnergyAlert)>
 1814             AtomIndex2,AtomIndex3  AtomIndex1,AtomIndex2,AtomIndex3,AtomIndex4
 1815             Angle  Energy EnergyLowerBoundCI EnergyUpperBoundCI
 1816             ClassName SubClassName SMARTS EnergyMethod Yes|No|NA Yes|No|NA
 1817             ... ... ...
 1818             ... ... ...
 1819             
 1820         A set of 12 values is written out as value of 'TorsionAlerts' data field for
 1821         each torsion in a molecule. The space character is used as a delimiter
 1822         to separate values with in a set and across set. The comma character
 1823         is used to delimit multiple values for each value in a set.
 1824         
 1825         The 'RotBondIndices' and 'TorsionIndices' contain 2 and 4 comma delimited
 1826         values representing atom indices for a rotatable bond and the matched
 1827         torsion.
 1828         
 1829         The 'Energy' value is the estimated strain energy for the matched torsion.
 1830         The 'EnergyLowerBoundCI' and 'EnergyUpperBoundCI' represent lower and
 1831         bound energy estimates at 95% confidence interval. The 'EnergyMethod',
 1832         exact or approximate, corresponds to the method employed to estimate
 1833         torsion strain energy.
 1834         
 1835         The 'AngleNotObserved' is only valid for 'approximate' value of 'EnergyMethod'.
 1836         It has three possible values: Yes, No, or NA. The 'Yes' value indicates that
 1837         the 'TorsionAngle' is outside the 'tolerance2' of all peaks for the matched
 1838         torsion rule in the torsion library.
 1839         
 1840         The 'MaxSingleEnergyAlert' is valid for the following values of '-a, --alertsMode'
 1841         option: 'MaxSingleEnergy' or 'TotalOrMaxSingleEnergy'. It has three possible
 1842         values: Yes, No, or NA. It's set to 'NA' for 'Yes' or 'NA' values of
 1843         'AngleNotObserved'. The 'Yes' value indicates that the estimated torsion
 1844         energy is greater than the specified value for '--alertsMaxSingleEnergyCutoff'
 1845         option.
 1846         
 1847         For example:
 1848             
 1849             >  <RotBondsCount>  (1) 
 1850             14
 1851             
 1852             >  <TotalEnergy>  (1) 
 1853             6.8065
 1854             
 1855             >  <TotalEnergyLowerBoundCI>  (1) 
 1856             5.9340
 1857             
 1858             >  <TotalEnergyUpperBoundCI>  (1) 
 1859             NA
 1860             
 1861             >  <MaxSingleEnergy>  (1) 
 1862             1.7108
 1863             
 1864             >  <MaxSingleEnergyAlertsCount>  (1) 
 1865             0
 1866             
 1867             >  <AnglesNotObservedCount>  (1) 
 1868             0
 1869              
 1870             >  <TorsionAlerts(RotBondIndices TorsionIndices TorsionAngle Energy
 1871                 EnergyLowerBoundCI EnergyUpperBoundCI HierarchyClass
 1872                 HierarchySubClass TorsionRule EnergyMethod AngleNotObserved
 1873                 MaxSingleEnergyAlert)>  (1) 
 1874             2,1 48,2,1,0 61.90 0.0159 -0.0320 0.0674 CO Ether [O:1][CX4:2]!
 1875             @[O:3][CX4:4] Exact NA No 2,3 1,2,3,4 109.12 1.5640 1.1175 NA CC
 1876             None/[CX4][CX3] [O:1][CX4:2]!@[CX3:3]=[O:4] Exact NA No
 1877             ... ... ...
 1878             
 1879     --outfileFiltered <yes or no>  [default: yes]
 1880         Write out a file containing filtered molecules. Its name is automatically
 1881         generated from the specified output file. Default: <OutfileRoot>_
 1882         Filtered.<OutfileExt>.
 1883     --outfilesFilteredByRules <yes or no>  [default: auto]
 1884         Write out SD files containing filtered molecules for individual torsion
 1885         rules triggering alerts in molecules. The name of SD files are automatically
 1886         generated from the specified output file. Default file names: <OutfileRoot>_
 1887         Filtered_TopRule*.sdf.
 1888         
 1889         Default value: 'yes' for 'MaxSingleEnergy' of '-a, --alertsMode' option';
 1890         otherwise, 'no'.
 1891         
 1892         The output files are only generated for 'MaxSingleEnergy' of
 1893         '-a, --alertsMode' option.
 1894         
 1895         The following alerts information is added to SD output files:
 1896             
 1897             > <RotBondsCount>
 1898             Number
 1899             
 1900             > <TotalEnergy>
 1901             Number
 1902             
 1903             > <TotalEnergyLowerBoundCI>
 1904             Number
 1905             
 1906             > <TotalEnergyUpperBoundCI>
 1907             Number
 1908             
 1909             > <MaxSingleEnergy>
 1910             Number
 1911             
 1912             > <MaxSingleEnergyAlertsCount>
 1913             Number
 1914             
 1915             > <AnglesNotObservedCount>
 1916             Number
 1917             
 1918             >  <TorsionRule (HierarchyClass HierarchySubClass TorsionRule
 1919                 EnergyMethod)> 
 1920             ClassName SubClassName EnergyMethod SMARTS
 1921              ... ... ...
 1922             
 1923             > <TorsionRuleMaxSingleEnergyAlertsCount>
 1924             Number
 1925             
 1926             > <TorsionRuleAnglesNotObservedCount>
 1927             Number
 1928             
 1929             >  <TorsionRuleAlerts (RotBondIndices TorsionIndices TorsionAngle
 1930                 Energy EnergyLowerBoundCI EnergyUpperBoundCI
 1931                 AngleNotObserved MaxSingleEnergyAlert)>
 1932             AtomIndex2,AtomIndex3  AtomIndex1,AtomIndex2,AtomIndex3,AtomIndex4
 1933             Angle Energy EnergyLowerBoundCI EnergyUpperBoundCI EnergyMethod
 1934             Yes|No|NA Yes|No|NA
 1935              ... ... ...
 1936             
 1937         For example:
 1938             
 1939             >  <RotBondsCount>  (1) 
 1940             8
 1941             
 1942             >  <TotalEnergy>  (1) 
 1943             6.1889
 1944             
 1945             >  <TotalEnergyLowerBoundCI>  (1) 
 1946             5.1940
 1947             
 1948             >  <TotalEnergyUpperBoundCI>  (1) 
 1949             NA
 1950             
 1951             >  <MaxSingleEnergy>  (1) 
 1952             1.9576
 1953             
 1954             >  <MaxSingleEnergyAlertsCount>  (1) 
 1955             1
 1956             
 1957             >  <AnglesNotObservedCount>  (1) 
 1958             0
 1959             
 1960             >  <TorsionRule (HierarchyClass HierarchySubClass TorsionRule
 1961                 EnergyMethod)>  (1) 
 1962             CC None/[CX4:2][CX4:3] [!#1:1][CX4:2]!@[CX4:3][!#1:4] Exact
 1963             
 1964             >  <TorsionRuleMaxSingleEnergyAlertsCount>  (1) 
 1965             0
 1966             
 1967             >  <TorsionRuleAnglesNotObservedCount>  (1) 
 1968             0
 1969             
 1970             >  <TorsionRuleAlerts (RotBondIndices TorsionIndices TorsionAngle
 1971                 Energy EnergyLowerBoundCI EnergyUpperBoundCI AngleNotObserved
 1972                MaxSingleEnergyAlert)>  (1) 
 1973             1,3 0,1,3,4 72.63 0.8946 0.8756 0.9145 NA No
 1974             
 1975     --outfilesFilteredByRulesMaxCount <All or number>  [default: 10]
 1976         Write out SD files containing filtered molecules for specified number of
 1977         top N torsion rules triggering alerts for the largest number of molecules
 1978         or for all torsion rules triggering alerts across all molecules.
 1979         
 1980         These output files are only generated for 'MaxSingleEnergy' value of
 1981         '-a, --alertsMode' option.
 1982     --outfileSummary <yes or no>  [default: auto]
 1983         Write out a CVS text file containing summary of torsions rules responsible
 1984         for triggering torsion alerts. Its name is automatically generated from the
 1985         specified output file. Default: <OutfileRoot>_AlertsSummary.csv.
 1986         
 1987         Default value: 'yes' for 'MaxSingleEnergy' of '-a, --alertsMode' option';
 1988         otherwise, 'no'.
 1989         
 1990         The summary output file is only generated for 'MaxSingleEnergy' of
 1991         '-a, --alertsMode' option.
 1992         
 1993         The following alerts information is written to summary text file:
 1994             
 1995             TorsionRule, HierarchyClass, HierarchySubClass, EnergyMethod,
 1996             MaxSingleEnergyTorsionAlertTypes, MaxSingleEnergyTorsionAlertCount,
 1997             MaxSingleEnergyTorsionAlertMolCount
 1998              
 1999         The double quotes characters are removed from SMART patterns before
 2000         before writing them to a CSV file. In addition, the torsion rules are sorted by
 2001         TorsionAlertMolCount.
 2002     --outfileSDFieldLabels <Type,Label,...>  [default: auto]
 2003         A comma delimited list of SD data field type and label value pairs for writing
 2004         torsion alerts information along with molecules to SD files.
 2005         
 2006         The supported SD data field label type along with their default values are
 2007         shown below:
 2008             
 2009             For all SD files:
 2010             
 2011             RotBondsCountLabel, RotBondsCount,
 2012             
 2013             TotalEnergyLabel, TotalEnergy,
 2014             TotalEnergyLowerBoundCILabel, TotalEnergyLowerBoundCI,
 2015             TotalEnergyUpperBoundCILabel, TotalEnergyUpperBoundCI,
 2016             
 2017             MaxSingleEnergyLabel, MaxSingleEnergy,
 2018             MaxSingleEnergyAlertsCountLabel,
 2019                 MaxSingleEnergyAlertsCount
 2020             
 2021             AnglesNotObservedCountLabel,
 2022                 AnglesNotObservedCount
 2023             
 2024             TorsionAlertsLabel, TorsionAlerts(RotBondIndices TorsionIndices
 2025                 TorsionAngle Energy EnergyLowerBoundCI EnergyUpperBoundCI
 2026                 HierarchyClass HierarchySubClass TorsionRule
 2027                 EnergyMethod AngleNotObserved)
 2028             
 2029             For individual SD files filtered by torsion rules:
 2030             
 2031             TorsionRuleLabel, TorsionRule (HierarchyClass HierarchySubClass
 2032                 EnergyMethod TorsionRule)
 2033             TorsionRuleMaxSingleEnergyAlertsCountLabel,
 2034                 TorsionRuleMaxSingleEnergyAlertsCount,
 2035             TorsionRuleAnglesNotObservedCountLabel,
 2036                 TorsionRuleAnglesNotObservedCount,
 2037             TorsionRuleAlertsLabel, TorsionRuleAlerts (RotBondIndices
 2038                 TorsionIndices TorsionAngle Energy EnergyLowerBoundCI
 2039                 EnergyUpperBoundCI EnergyMethod AngleObserved)
 2040             
 2041     --outfileParams <Name,Value,...>  [default: auto]
 2042         A comma delimited list of parameter name and value pairs for writing
 2043         molecules to files. The supported parameter names for different file
 2044         formats, along with their default values, are shown below:
 2045             
 2046             SD: kekulize,yes,forceV3000,no
 2047             
 2048     --overwrite
 2049         Overwrite existing files.
 2050     --precision <number>  [default: 4]
 2051         Floating point precision for writing torsion strain energy values.
 2052     -r, --rotBondsSMARTSMode <NonStrict, SemiStrict,...>  [default: SemiStrict]
 2053         SMARTS pattern to use for identifying rotatable bonds in a molecule
 2054         for matching against torsion rules in the torsion library. Possible values:
 2055         NonStrict, SemiStrict, Strict or Specify. The rotatable bond SMARTS matches
 2056         are filtered to ensure that each atom in the rotatable bond is attached to
 2057         at least two heavy atoms.
 2058         
 2059         The following SMARTS patterns are used to identify rotatable bonds for
 2060         different modes:
 2061             
 2062             NonStrict: [!$(*#*)&!D1]-&!@[!$(*#*)&!D1]
 2063             
 2064             SemiStrict:
 2065             [!$(*#*)&!D1&!$(C(F)(F)F)&!$(C(Cl)(Cl)Cl)&!$(C(Br)(Br)Br)
 2066             &!$(C([CH3])([CH3])[CH3])]-!@[!$(*#*)&!D1&!$(C(F)(F)F)
 2067             &!$(C(Cl)(Cl)Cl)&!$(C(Br)(Br)Br)&!$(C([CH3])([CH3])[CH3])]
 2068             
 2069             Strict:
 2070             [!$(*#*)&!D1&!$(C(F)(F)F)&!$(C(Cl)(Cl)Cl)&!$(C(Br)(Br)Br)
 2071             &!$(C([CH3])([CH3])[CH3])&!$([CD3](=[N,O,S])-!@[#7,O,S!D1])
 2072             &!$([#7,O,S!D1]-!@[CD3]=[N,O,S])&!$([CD3](=[N+])-!@[#7!D1])
 2073             &!$([#7!D1]-!@[CD3]=[N+])]-!@[!$(*#*)&!D1&!$(C(F)(F)F)
 2074             &!$(C(Cl)(Cl)Cl)&!$(C(Br)(Br)Br)&!$(C([CH3])([CH3])[CH3])]
 2075             
 2076         The 'NonStrict' and 'Strict' SMARTS patterns are available in RDKit. The 
 2077         'NonStrict' SMARTS pattern corresponds to original Daylight SMARTS
 2078         specification for rotatable bonds. The 'SemiStrict' SMARTS pattern is 
 2079         derived from 'Strict' SMARTS pattern for its usage in this script.
 2080         
 2081         You may use any arbitrary SMARTS pattern to identify rotatable bonds by
 2082         choosing 'Specify' value for '-r, --rotBondsSMARTSMode' option and providing its
 2083         value via '--rotBondsSMARTSPattern' option.
 2084     --rotBondsSMARTSPattern <SMARTS>
 2085         SMARTS pattern for identifying rotatable bonds. This option is only valid
 2086         for 'Specify' value of '-r, --rotBondsSMARTSMode' option.
 2087     -t, --torsionEnergyLibraryFile <FileName or auto>  [default: auto]
 2088         Specify a XML file name containing data for torsion starin energy library
 2089         hierarchy or use default file, TorsionEnergyLibrary.xml, available in
 2090         MAYACHEMTOOLS/lib/Python/TorsionAlerts directory.
 2091         
 2092         The format of data in local XML file must match format of the data in Torsion
 2093         Library [ Ref 153 ] file available in MAYACHEMTOOLS directory.
 2094     -w, --workingdir <dir>
 2095         Location of working directory which defaults to the current directory.
 2096 
 2097 Examples:
 2098     To filter molecules containing rotatable bonds with total strain energy value
 2099     of >= 6.0 (TEUs) based on torsion rules in the torsion energy library and write
 2100     write out SD files containing remaining and filtered molecules, type:
 2101 
 2102         % RDKitFilterTorsionStrainEnergyAlerts.py -i Sample3D.sdf
 2103           -o Sample3DOut.sdf
 2104 
 2105     To filter molecules containing any rotatable bonds with strain energy value of
 2106     >= 1.8 (TEUs) based on torsion rules in the torsion energy library and write out
 2107     SD files containing remaining and filtered molecules, and individual SD files for
 2108     torsion rules triggering alerts along with appropriate torsion information for
 2109     red alerts, type:
 2110 
 2111         % RDKitFilterTorsionStrainEnergyAlerts.py -a MaxSingleEnergy
 2112           -i Sample3D.sdf -o Sample3DOut.sdf
 2113 
 2114     To filter molecules containing rotatable bonds with total strain energy value
 2115     of >= 6.0 (TEUs) or any single strain energy value of >= 1.8 (TEUs) and write out
 2116     SD files containing remaining and filtered molecules, type:
 2117 
 2118         % RDKitFilterTorsionStrainEnergyAlerts.py -a TotalOrMaxSingleEnergy
 2119           -i Sample3D.sdf -o Sample3DOut.sdf
 2120 
 2121     To filter molecules containing rotatable bonds with specific cutoff values for
 2122     total or single torsion strain energy and write out SD files containing
 2123     remaining and filtered molecules, type:
 2124 
 2125         % RDKitFilterTorsionStrainEnergyAlerts.py -a TotalOrMaxSingleEnergy
 2126           -i Sample3D.sdf -o Sample3DOut.sdf --alertsTotalEnergyCutoff 6.0
 2127           --alertsMaxSingleEnergyCutoff 1.8
 2128 
 2129     To run the first example for filtering molecules and writing out torsion
 2130     information for all alert types to SD files, type:
 2131 
 2132         % RDKitFilterTorsionStrainEnergyAlerts.py -i Sample3D.sdf
 2133           -o Sample3DOut.sdf --outfileAlertsMode All
 2134 
 2135     To run the first example for filtering molecules in multiprocessing mode on
 2136     all available CPUs without loading all data into memory and write out SD files,
 2137     type:
 2138 
 2139         % RDKitFilterTorsionStrainEnergyAlerts.py --mp yes -i Sample3D.sdf
 2140          -o Sample3DOut.sdf
 2141 
 2142     To run the first example for filtering molecules in multiprocessing mode on
 2143     all available CPUs by loading all data into memory and write out a SD files,
 2144     type:
 2145 
 2146         % RDKitFilterTorsionStrainEnergyAlerts.py  --mp yes --mpParams
 2147           "inputDataMode, InMemory" -i Sample3D.sdf  -o Sample3DOut.sdf
 2148 
 2149     To run the first example for filtering molecules in multiprocessing mode on
 2150     specific number of CPUs and chunksize without loading all data into memory
 2151     and write out SD files, type:
 2152 
 2153         % RDKitFilterTorsionStrainEnergyAlerts.py --mp yes --mpParams
 2154           "inputDataMode,lazy,numProcesses,4,chunkSize,8"  -i Sample3D.sdf
 2155           -o Sample3DOut.sdf
 2156 
 2157     To list information about default torsion library file without performing any
 2158     filtering, type:
 2159 
 2160         % RDKitFilterTorsionStrainEnergyAlerts.py -l
 2161 
 2162     To list information about a local torsion library XML file without performing
 2163     any, filtering, type:
 2164 
 2165         % RDKitFilterTorsionStrainEnergyAlerts.py --torsionEnergyLibraryFile
 2166           TorsionStrainEnergyLibrary.xml -l
 2167 
 2168 Author:
 2169     Manish Sud (msud@san.rr.com)
 2170 
 2171 Collaborator:
 2172     Pat Walters
 2173 
 2174 See also:
 2175     RDKitFilterChEMBLAlerts.py, RDKitFilterPAINS.py, RDKitFilterTorsionLibraryAlerts.py,
 2176     RDKitConvertFileFormat.py, RDKitSearchSMARTS.py
 2177 
 2178 Copyright:
 2179     Copyright (C) 2026 Manish Sud. All rights reserved.
 2180 
 2181     This script uses the torsion strain energy library developed by Gu, S.;
 2182     Smith, M. S.; Yang, Y.; Irwin, J. J.; Shoichet, B. K. [ Ref 153 ].
 2183 
 2184     The torsion strain enegy library is based on the Torsion Library jointly
 2185     developed by the University of Hamburg, Center for Bioinformatics,
 2186     Hamburg, Germany and F. Hoffmann-La-Roche Ltd., Basel, Switzerland.
 2187 
 2188     The functionality available in this script is implemented using RDKit, an
 2189     open source toolkit for cheminformatics developed by Greg Landrum.
 2190 
 2191     This file is part of MayaChemTools.
 2192 
 2193     MayaChemTools is free software; you can redistribute it and/or modify it under
 2194     the terms of the GNU Lesser General Public License as published by the Free
 2195     Software Foundation; either version 3 of the License, or (at your option) any
 2196     later version.
 2197 
 2198 """
 2199 
 2200 if __name__ == "__main__":
 2201     main()