MayaChemTools

    1 #!/bin/env python
    2 #
    3 # File: RDKitPerformTorsionScan.py
    4 # Author: Manish Sud <msud@san.rr.com>
    5 #
    6 # Acknowledgment: Pat Walters
    7 #
    8 # Copyright (C) 2026 Manish Sud. All rights reserved.
    9 #
   10 # The functionality available in this script is implemented using RDKit, an
   11 # open source toolkit for cheminformatics developed by Greg Landrum.
   12 #
   13 # This file is part of MayaChemTools.
   14 #
   15 # MayaChemTools is free software; you can redistribute it and/or modify it under
   16 # the terms of the GNU Lesser General Public License as published by the Free
   17 # Software Foundation; either version 3 of the License, or (at your option) any
   18 # later version.
   19 #
   20 # MayaChemTools is distributed in the hope that it will be useful, but without
   21 # any warranty; without even the implied warranty of merchantability of fitness
   22 # for a particular purpose.  See the GNU Lesser General Public License for more
   23 # details.
   24 #
   25 # You should have received a copy of the GNU Lesser General Public License
   26 # along with MayaChemTools; if not, see <http://www.gnu.org/licenses/> or
   27 # write to the Free Software Foundation Inc., 59 Temple Place, Suite 330,
   28 # Boston, MA, 02111-1307, USA.
   29 #
   30 
   31 from __future__ import print_function
   32 
   33 import os
   34 import sys
   35 import time
   36 import re
   37 import glob
   38 import multiprocessing as mp
   39 
   40 import matplotlib.pyplot as plt
   41 import seaborn as sns
   42 
   43 # RDKit imports...
   44 try:
   45     from rdkit import rdBase
   46     from rdkit import Chem
   47     from rdkit.Chem import AllChem
   48     from rdkit.Chem import rdMolTransforms
   49 except ImportError as ErrMsg:
   50     sys.stderr.write("\nFailed to import RDKit module/package: %s\n" % ErrMsg)
   51     sys.stderr.write("Check/update your RDKit environment and try again.\n\n")
   52     sys.exit(1)
   53 
   54 # MayaChemTools imports...
   55 sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), "..", "lib", "Python"))
   56 try:
   57     from docopt import docopt
   58     import MiscUtil
   59     import RDKitUtil
   60 except ImportError as ErrMsg:
   61     sys.stderr.write("\nFailed to import MayaChemTools module/package: %s\n" % ErrMsg)
   62     sys.stderr.write("Check/update your MayaChemTools environment and try again.\n\n")
   63     sys.exit(1)
   64 
   65 ScriptName = os.path.basename(sys.argv[0])
   66 Options = {}
   67 OptionsInfo = {}
   68 
   69 
   70 def main():
   71     """Start execution of the script."""
   72 
   73     MiscUtil.PrintInfo(
   74         "\n%s (RDKit v%s; MayaChemTools v%s; %s): Starting...\n"
   75         % (ScriptName, rdBase.rdkitVersion, MiscUtil.GetMayaChemToolsVersion(), time.asctime())
   76     )
   77 
   78     (WallClockTime, ProcessorTime) = MiscUtil.GetWallClockAndProcessorTime()
   79 
   80     # Retrieve command line arguments and options...
   81     RetrieveOptions()
   82 
   83     # Process and validate command line arguments and options...
   84     ProcessOptions()
   85 
   86     # Perform actions required by the script...
   87     PerformTorsionScan()
   88 
   89     MiscUtil.PrintInfo("\n%s: Done...\n" % ScriptName)
   90     MiscUtil.PrintInfo("Total time: %s" % MiscUtil.GetFormattedElapsedTime(WallClockTime, ProcessorTime))
   91 
   92 
   93 def PerformTorsionScan():
   94     """Perform torsion scan."""
   95 
   96     # Setup a molecule reader for input file...
   97     MiscUtil.PrintInfo("\nProcessing file %s..." % OptionsInfo["Infile"])
   98     OptionsInfo["InfileParams"]["AllowEmptyMols"] = True
   99     Mols = RDKitUtil.ReadMolecules(OptionsInfo["Infile"], **OptionsInfo["InfileParams"])
  100 
  101     PlotExt = OptionsInfo["OutPlotParams"]["OutExt"]
  102     FileDir, FileName, FileExt = MiscUtil.ParseFileName(Options["--outfile"])
  103     MiscUtil.PrintInfo(
  104         "Generating output files %s_*.sdf, %s_*Torsion*Match*.sdf, %s_*Torsion*Match*Energies.csv, %s_*Torsion*Match*Plot.%s, %s_*Torsion*Match*Viewer.html..."
  105         % (FileName, FileName, FileName, FileName, PlotExt, FileName)
  106     )
  107 
  108     MolCount, ValidMolCount, MinimizationFailedCount, TorsionsMissingCount, TorsionsScanFailedCount = ProcessMolecules(
  109         Mols
  110     )
  111 
  112     MiscUtil.PrintInfo("\nTotal number of molecules: %d" % MolCount)
  113     MiscUtil.PrintInfo("Number of valid molecules: %d" % ValidMolCount)
  114     MiscUtil.PrintInfo("Number of molecules failed during initial minimization: %d" % MinimizationFailedCount)
  115     MiscUtil.PrintInfo("Number of molecules without any matched torsions: %d" % TorsionsMissingCount)
  116     MiscUtil.PrintInfo("Number of molecules failed during torsion scan: %d" % TorsionsScanFailedCount)
  117     MiscUtil.PrintInfo(
  118         "Number of ignored molecules: %d"
  119         % (MolCount - ValidMolCount + TorsionsMissingCount + MinimizationFailedCount + TorsionsScanFailedCount)
  120     )
  121 
  122 
  123 def ProcessMolecules(Mols):
  124     """Process molecules to perform torsion scan."""
  125 
  126     if OptionsInfo["MPMode"]:
  127         return ProcessMoleculesUsingMultipleProcesses(Mols)
  128     else:
  129         return ProcessMoleculesUsingSingleProcess(Mols)
  130 
  131 
  132 def ProcessMoleculesUsingSingleProcess(Mols):
  133     """Process molecules to perform torsion scan using a single process."""
  134 
  135     MolInfoText = "first molecule"
  136     if not OptionsInfo["FirstMolMode"]:
  137         MolInfoText = "all molecules"
  138 
  139     if OptionsInfo["TorsionMinimize"]:
  140         MiscUtil.PrintInfo(
  141             "\nPerforming torsion scan on %s by generating conformation ensembles for specific torsion angles and constrained energy minimization of the ensembles..."
  142             % (MolInfoText)
  143         )
  144     else:
  145         MiscUtil.PrintInfo(
  146             "\nPerforming torsion scan on %s by skipping generation of conformation ensembles for specific torsion angles and constrained energy minimization of the ensembles..."
  147             % (MolInfoText)
  148         )
  149 
  150     SetupTorsionsPatternsInfo()
  151 
  152     (MolCount, ValidMolCount, TorsionsMissingCount, MinimizationFailedCount, TorsionsScanFailedCount) = [0] * 5
  153 
  154     for Mol in Mols:
  155         MolCount += 1
  156 
  157         if OptionsInfo["FirstMolMode"] and MolCount > 1:
  158             MolCount -= 1
  159             break
  160 
  161         if Mol is None:
  162             continue
  163 
  164         if RDKitUtil.IsMolEmpty(Mol):
  165             if not OptionsInfo["QuietMode"]:
  166                 MolName = RDKitUtil.GetMolName(Mol, MolCount)
  167                 MiscUtil.PrintWarning("Ignoring empty molecule: %s" % MolName)
  168             continue
  169         ValidMolCount += 1
  170 
  171         Mol, MinimizationCalcStatus, TorsionsMatchStatus, TorsionsScanCalcStatus = PerformMinimizationAndTorsionScan(
  172             Mol, MolCount
  173         )
  174 
  175         if not MinimizationCalcStatus:
  176             MinimizationFailedCount += 1
  177             continue
  178 
  179         if not TorsionsMatchStatus:
  180             TorsionsMissingCount += 1
  181             continue
  182 
  183         if not TorsionsScanCalcStatus:
  184             TorsionsScanFailedCount += 1
  185             continue
  186 
  187     return (MolCount, ValidMolCount, MinimizationFailedCount, TorsionsMissingCount, TorsionsScanFailedCount)
  188 
  189 
  190 def ProcessMoleculesUsingMultipleProcesses(Mols):
  191     """Process and minimize molecules using multiprocessing."""
  192 
  193     if OptionsInfo["MPLevelTorsionAnglesMode"]:
  194         return ProcessMoleculesUsingMultipleProcessesAtTorsionAnglesLevel(Mols)
  195     elif OptionsInfo["MPLevelMoleculesMode"]:
  196         return ProcessMoleculesUsingMultipleProcessesAtMoleculesLevel(Mols)
  197     else:
  198         MiscUtil.PrintError('The value, %s,  option "--mpLevel" is not supported.' % (OptionsInfo["MPLevel"]))
  199 
  200 
  201 def ProcessMoleculesUsingMultipleProcessesAtMoleculesLevel(Mols):
  202     """Process molecules to perform torsion scan using multiprocessing at molecules level."""
  203 
  204     MolInfoText = "first molecule"
  205     if not OptionsInfo["FirstMolMode"]:
  206         MolInfoText = "all molecules"
  207 
  208     if OptionsInfo["TorsionMinimize"]:
  209         MiscUtil.PrintInfo(
  210             "\nPerforming torsion scan on %s using multiprocessing at molecules level by generating conformation ensembles for specific torsion angles and constrained energy minimization of the ensembles..."
  211             % (MolInfoText)
  212         )
  213     else:
  214         MiscUtil.PrintInfo(
  215             "\nPerforming torsion scan %s using multiprocessing at molecules level by skipping generation of conformation ensembles for specific torsion angles and constrained energy minimization of the ensembles..."
  216             % (MolInfoText)
  217         )
  218 
  219     MPParams = OptionsInfo["MPParams"]
  220 
  221     # Setup data for initializing a worker process...
  222     MiscUtil.PrintInfo("\nEncoding options info...")
  223 
  224     InitializeWorkerProcessArgs = (
  225         MiscUtil.ObjectToBase64EncodedString(Options),
  226         MiscUtil.ObjectToBase64EncodedString(OptionsInfo),
  227     )
  228 
  229     if OptionsInfo["FirstMolMode"]:
  230         Mol = Mols[0]
  231         Mols = [Mol]
  232 
  233     # Setup a encoded mols data iterable for a worker process...
  234     WorkerProcessDataIterable = RDKitUtil.GenerateBase64EncodedMolStrings(Mols)
  235 
  236     # Setup process pool along with data initialization for each process...
  237     MiscUtil.PrintInfo(
  238         "\nConfiguring multiprocessing using %s method..."
  239         % ("mp.Pool.imap()" if re.match("^Lazy$", MPParams["InputDataMode"], re.I) else "mp.Pool.map()")
  240     )
  241     MiscUtil.PrintInfo(
  242         "NumProcesses: %s; InputDataMode: %s; ChunkSize: %s\n"
  243         % (
  244             MPParams["NumProcesses"],
  245             MPParams["InputDataMode"],
  246             ("automatic" if MPParams["ChunkSize"] is None else MPParams["ChunkSize"]),
  247         )
  248     )
  249 
  250     ProcessPool = mp.Pool(MPParams["NumProcesses"], InitializeWorkerProcess, InitializeWorkerProcessArgs)
  251 
  252     # Start processing...
  253     if re.match("^Lazy$", MPParams["InputDataMode"], re.I):
  254         Results = ProcessPool.imap(WorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"])
  255     elif re.match("^InMemory$", MPParams["InputDataMode"], re.I):
  256         Results = ProcessPool.map(WorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"])
  257     else:
  258         MiscUtil.PrintError(
  259             'The value, %s, specified for "--inputDataMode" is not supported.' % (MPParams["InputDataMode"])
  260         )
  261 
  262     (MolCount, ValidMolCount, TorsionsMissingCount, MinimizationFailedCount, TorsionsScanFailedCount) = [0] * 5
  263 
  264     for Result in Results:
  265         MolCount += 1
  266 
  267         MolIndex, EncodedMol, MinimizationCalcStatus, TorsionsMatchStatus, TorsionsScanCalcStatus = Result
  268 
  269         if EncodedMol is None:
  270             continue
  271         ValidMolCount += 1
  272 
  273         if not MinimizationCalcStatus:
  274             MinimizationFailedCount += 1
  275             continue
  276 
  277         if not TorsionsMatchStatus:
  278             TorsionsMissingCount += 1
  279             continue
  280 
  281         if not TorsionsScanCalcStatus:
  282             TorsionsScanFailedCount += 1
  283             continue
  284 
  285         Mol = RDKitUtil.MolFromBase64EncodedMolString(EncodedMol)
  286 
  287     return (MolCount, ValidMolCount, MinimizationFailedCount, TorsionsMissingCount, TorsionsScanFailedCount)
  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     # Initialize torsion patterns info...
  302     SetupTorsionsPatternsInfo()
  303 
  304 
  305 def WorkerProcess(EncodedMolInfo):
  306     """Process data for a worker process."""
  307 
  308     MolIndex, EncodedMol = EncodedMolInfo
  309 
  310     (MinimizationCalcStatus, TorsionsMatchStatus, TorsionsScanCalcStatus) = [False] * 3
  311 
  312     if EncodedMol is None:
  313         return [MolIndex, None, MinimizationCalcStatus, TorsionsMatchStatus, TorsionsScanCalcStatus]
  314 
  315     Mol = RDKitUtil.MolFromBase64EncodedMolString(EncodedMol)
  316     if RDKitUtil.IsMolEmpty(Mol):
  317         if not OptionsInfo["QuietMode"]:
  318             MolName = RDKitUtil.GetMolName(Mol, (MolIndex + 1))
  319             MiscUtil.PrintWarning("Ignoring empty molecule: %s" % MolName)
  320         return [MolIndex, None, MinimizationCalcStatus, TorsionsMatchStatus, TorsionsScanCalcStatus]
  321 
  322     Mol, MinimizationCalcStatus, TorsionsMatchStatus, TorsionsScanCalcStatus = PerformMinimizationAndTorsionScan(
  323         Mol, (MolIndex + 1)
  324     )
  325 
  326     return [
  327         MolIndex,
  328         RDKitUtil.MolToBase64EncodedMolString(
  329             Mol, PropertyPickleFlags=Chem.PropertyPickleOptions.MolProps | Chem.PropertyPickleOptions.PrivateProps
  330         ),
  331         MinimizationCalcStatus,
  332         TorsionsMatchStatus,
  333         TorsionsScanCalcStatus,
  334     ]
  335 
  336 
  337 def ProcessMoleculesUsingMultipleProcessesAtTorsionAnglesLevel(Mols):
  338     """Process molecules to perform torsion scan using multiprocessing at torsion angles level."""
  339 
  340     MolInfoText = "first molecule"
  341     if not OptionsInfo["FirstMolMode"]:
  342         MolInfoText = "all molecules"
  343 
  344     if OptionsInfo["TorsionMinimize"]:
  345         MiscUtil.PrintInfo(
  346             "\nPerforming torsion scan on %s using multiprocessing at torsion angles level by generating conformation ensembles for specific torsion angles and constrained energy minimization of the ensembles..."
  347             % (MolInfoText)
  348         )
  349     else:
  350         MiscUtil.PrintInfo(
  351             "\nPerforming torsion scan %s using multiprocessing at torsion angles level by skipping generation of conformation ensembles for specific torsion angles and constrained energy minimization of the ensembles..."
  352             % (MolInfoText)
  353         )
  354 
  355     SetupTorsionsPatternsInfo()
  356 
  357     (MolCount, ValidMolCount, TorsionsMissingCount, MinimizationFailedCount, TorsionsScanFailedCount) = [0] * 5
  358 
  359     for Mol in Mols:
  360         MolCount += 1
  361 
  362         if OptionsInfo["FirstMolMode"] and MolCount > 1:
  363             MolCount -= 1
  364             break
  365 
  366         if Mol is None:
  367             continue
  368 
  369         if RDKitUtil.IsMolEmpty(Mol):
  370             if not OptionsInfo["QuietMode"]:
  371                 MolName = RDKitUtil.GetMolName(Mol, MolCount)
  372                 MiscUtil.PrintWarning("Ignoring empty molecule: %s" % MolName)
  373             continue
  374         ValidMolCount += 1
  375 
  376         Mol, MinimizationCalcStatus, TorsionsMatchStatus, TorsionsScanCalcStatus = PerformMinimizationAndTorsionScan(
  377             Mol, MolCount, UseMultiProcessingAtTorsionAnglesLevel=True
  378         )
  379 
  380         if not MinimizationCalcStatus:
  381             MinimizationFailedCount += 1
  382             continue
  383 
  384         if not TorsionsMatchStatus:
  385             TorsionsMissingCount += 1
  386             continue
  387 
  388         if not TorsionsScanCalcStatus:
  389             TorsionsScanFailedCount += 1
  390             continue
  391 
  392     return (MolCount, ValidMolCount, MinimizationFailedCount, TorsionsMissingCount, TorsionsScanFailedCount)
  393 
  394 
  395 def ScanSingleTorsionInMolUsingMultipleProcessesAtTorsionAnglesLevel(
  396     Mol, TorsionID, TorsionPattern, TorsionPatternMol, TorsionMatches, MolNum
  397 ):
  398     """Perform torsion scan for a molecule using multiple processses at torsion angles
  399     level along with constrained energy minimization.
  400     """
  401 
  402     if OptionsInfo["MPLevelMoleculesMode"]:
  403         MiscUtil.PrintError(
  404             "Single torison scanning for a molecule is not allowed in multiprocessing mode at molecules level.\n"
  405         )
  406 
  407     Mols, Angles = SetupMolsForSingleTorsionScanInMol(Mol, TorsionMatches, MolNum)
  408 
  409     MPParams = OptionsInfo["MPParams"]
  410 
  411     # Setup data for initializing a worker process...
  412     MiscUtil.PrintInfo("\nEncoding options info...")
  413 
  414     # Track and avoid encoding TorsionsPatternsInfo as it contains RDKit molecule object...
  415     TorsionsPatternsInfo = OptionsInfo["TorsionsPatternsInfo"]
  416     OptionsInfo["TorsionsPatternsInfo"] = None
  417 
  418     InitializeWorkerProcessArgs = (
  419         MiscUtil.ObjectToBase64EncodedString(Options),
  420         MiscUtil.ObjectToBase64EncodedString(OptionsInfo),
  421     )
  422 
  423     # Restore TorsionsPatternsInfo...
  424     OptionsInfo["TorsionsPatternsInfo"] = TorsionsPatternsInfo
  425 
  426     # Setup a encoded mols data iterable for a worker process...
  427     WorkerProcessDataIterable = GenerateBase64EncodedMolStringsWithTorsionScanInfo(
  428         Mol, (MolNum - 1), Mols, Angles, TorsionID, TorsionPattern, TorsionPatternMol, TorsionMatches
  429     )
  430 
  431     # Setup process pool along with data initialization for each process...
  432     MiscUtil.PrintInfo(
  433         "\nConfiguring multiprocessing using %s method..."
  434         % ("mp.Pool.imap()" if re.match("^Lazy$", MPParams["InputDataMode"], re.I) else "mp.Pool.map()")
  435     )
  436     MiscUtil.PrintInfo(
  437         "NumProcesses: %s; InputDataMode: %s; ChunkSize: %s\n"
  438         % (
  439             MPParams["NumProcesses"],
  440             MPParams["InputDataMode"],
  441             ("automatic" if MPParams["ChunkSize"] is None else MPParams["ChunkSize"]),
  442         )
  443     )
  444 
  445     ProcessPool = mp.Pool(MPParams["NumProcesses"], InitializeTorsionAngleWorkerProcess, InitializeWorkerProcessArgs)
  446 
  447     # Start processing...
  448     if re.match("^Lazy$", MPParams["InputDataMode"], re.I):
  449         Results = ProcessPool.imap(TorsionAngleWorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"])
  450     elif re.match("^InMemory$", MPParams["InputDataMode"], re.I):
  451         Results = ProcessPool.map(TorsionAngleWorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"])
  452     else:
  453         MiscUtil.PrintError(
  454             'The value, %s, specified for "--inputDataMode" is not supported.' % (MPParams["InputDataMode"])
  455         )
  456 
  457     TorsionMols = []
  458     TorsionEnergies = []
  459     TorsionAngles = []
  460 
  461     for Result in Results:
  462         EncodedTorsionMol, CalcStatus, Angle, Energy = Result
  463 
  464         if not CalcStatus:
  465             return (Mol, False, None, None, None)
  466 
  467         if EncodedTorsionMol is None:
  468             return (Mol, False, None, None, None)
  469         TorsionMol = RDKitUtil.MolFromBase64EncodedMolString(EncodedTorsionMol)
  470 
  471         if OptionsInfo["RemoveHydrogens"]:
  472             TorsionMol = Chem.RemoveHs(TorsionMol)
  473 
  474         TorsionMols.append(TorsionMol)
  475         TorsionEnergies.append(Energy)
  476         TorsionAngles.append(Angle)
  477 
  478     return (Mol, True, TorsionMols, TorsionEnergies, TorsionAngles)
  479 
  480 
  481 def InitializeTorsionAngleWorkerProcess(*EncodedArgs):
  482     """Initialize data for a worker process."""
  483 
  484     global Options, OptionsInfo
  485 
  486     MiscUtil.PrintInfo("Starting process (PID: %s)..." % os.getpid())
  487 
  488     # Decode Options and OptionInfo...
  489     Options = MiscUtil.ObjectFromBase64EncodedString(EncodedArgs[0])
  490     OptionsInfo = MiscUtil.ObjectFromBase64EncodedString(EncodedArgs[1])
  491 
  492     # Initialize torsion patterns info...
  493     SetupTorsionsPatternsInfo()
  494 
  495 
  496 def TorsionAngleWorkerProcess(EncodedMolInfo):
  497     """Process data for a worker process."""
  498 
  499     (
  500         MolIndex,
  501         EncodedMol,
  502         EncodedTorsionMol,
  503         TorsionAngle,
  504         TorsionID,
  505         TorsionPattern,
  506         EncodedTorsionPatternMol,
  507         TorsionMatches,
  508     ) = EncodedMolInfo
  509 
  510     if EncodedMol is None or EncodedTorsionMol is None or EncodedTorsionPatternMol is None:
  511         return (None, False, None, None)
  512 
  513     Mol = RDKitUtil.MolFromBase64EncodedMolString(EncodedMol)
  514     TorsionMol = RDKitUtil.MolFromBase64EncodedMolString(EncodedTorsionMol)
  515     TorsionPatternMol = RDKitUtil.MolFromBase64EncodedMolString(EncodedTorsionPatternMol)
  516 
  517     TorsionMol, CalcStatus, Energy = MinimizeCalculateEnergyForTorsionMol(
  518         Mol, TorsionMol, TorsionAngle, TorsionID, TorsionPattern, TorsionPatternMol, TorsionMatches, (MolIndex + 1)
  519     )
  520 
  521     return (
  522         RDKitUtil.MolToBase64EncodedMolString(
  523             TorsionMol,
  524             PropertyPickleFlags=Chem.PropertyPickleOptions.MolProps | Chem.PropertyPickleOptions.PrivateProps,
  525         ),
  526         CalcStatus,
  527         TorsionAngle,
  528         Energy,
  529     )
  530 
  531 
  532 def GenerateBase64EncodedMolStringsWithTorsionScanInfo(
  533     Mol,
  534     MolIndex,
  535     TorsionMols,
  536     TorsionAngles,
  537     TorsionID,
  538     TorsionPattern,
  539     TorsionPatternMol,
  540     TorsionMatches,
  541     PropertyPickleFlags=Chem.PropertyPickleOptions.AllProps,
  542 ):
  543     """Set up an iterator for generating base64 encoded molecule string for
  544     a torsion in a molecule along with appropriate trosion scan information.
  545     """
  546 
  547     for Index, TorsionMol in enumerate(TorsionMols):
  548         yield (
  549             [MolIndex, None, None, TorsionAngles[Index], TorsionID, TorsionPattern, None, TorsionMatches]
  550             if (Mol is None or TorsionMol is None)
  551             else [
  552                 MolIndex,
  553                 RDKitUtil.MolToBase64EncodedMolString(Mol, PropertyPickleFlags),
  554                 RDKitUtil.MolToBase64EncodedMolString(TorsionMol, PropertyPickleFlags),
  555                 TorsionAngles[Index],
  556                 TorsionID,
  557                 TorsionPattern,
  558                 RDKitUtil.MolToBase64EncodedMolString(TorsionPatternMol, PropertyPickleFlags),
  559                 TorsionMatches,
  560             ]
  561         )
  562 
  563 
  564 def PerformMinimizationAndTorsionScan(Mol, MolNum, UseMultiProcessingAtTorsionAnglesLevel=False):
  565     """Perform minimization and torsions scan."""
  566 
  567     if not OptionsInfo["QuietMode"]:
  568         MiscUtil.PrintInfo("\nProcessing molecule %s..." % (RDKitUtil.GetMolName(Mol, MolNum)))
  569 
  570     Mol = AddHydrogens(Mol)
  571 
  572     if not OptionsInfo["Infile3D"]:
  573         Mol, MinimizationCalcStatus = MinimizeMolecule(Mol, MolNum)
  574         if not MinimizationCalcStatus:
  575             return (Mol, False, False, False)
  576 
  577     TorsionsMolInfo = SetupTorsionsMolInfo(Mol, MolNum)
  578     if TorsionsMolInfo["NumOfMatches"] == 0:
  579         return (Mol, True, False, False)
  580 
  581     Mol, ScanCalcStatus = ScanAllTorsionsInMol(Mol, TorsionsMolInfo, MolNum, UseMultiProcessingAtTorsionAnglesLevel)
  582     if not ScanCalcStatus:
  583         return (Mol, True, True, False)
  584 
  585     return (Mol, True, True, True)
  586 
  587 
  588 def ScanAllTorsionsInMol(Mol, TorsionsMolInfo, MolNum, UseMultiProcessingAtTorsionAnglesLevel=False):
  589     """Perform scans on all torsions in a molecule."""
  590 
  591     if TorsionsMolInfo["NumOfMatches"] == 0:
  592         return Mol, True
  593 
  594     MolName = RDKitUtil.GetMolName(Mol, MolNum)
  595 
  596     FirstTorsionMode = OptionsInfo["FirstTorsionMode"]
  597     TorsionsPatternsInfo = OptionsInfo["TorsionsPatternsInfo"]
  598 
  599     TorsionPatternCount, TorsionScanCount, TorsionMatchCount = [0] * 3
  600     TorsionMaxMatches = OptionsInfo["TorsionMaxMatches"]
  601 
  602     for TorsionID in TorsionsPatternsInfo["IDs"]:
  603         TorsionPatternCount += 1
  604         TorsionPattern = TorsionsPatternsInfo["Pattern"][TorsionID]
  605         TorsionPatternMol = TorsionsPatternsInfo["Mol"][TorsionID]
  606 
  607         TorsionsMatches = TorsionsMolInfo["Matches"][TorsionID]
  608 
  609         if TorsionsMatches is None:
  610             continue
  611 
  612         if FirstTorsionMode and TorsionPatternCount > 1:
  613             if not OptionsInfo["QuietMode"]:
  614                 MiscUtil.PrintWarning(
  615                     'Already scaned first torsion pattern, "%s" for molecule %s during "%s" value of "--modeTorsions" option . Abandoning torsion scan...\n'
  616                     % (TorsionPattern, MolName, OptionsInfo["ModeTorsions"])
  617                 )
  618             break
  619 
  620         for Index, TorsionMatches in enumerate(TorsionsMatches):
  621             TorsionMatchNum = Index + 1
  622             TorsionMatchCount += 1
  623 
  624             if TorsionMatchCount > TorsionMaxMatches:
  625                 if not OptionsInfo["QuietMode"]:
  626                     MiscUtil.PrintWarning(
  627                         'Already scaned a maximum of %s torsion matches for molecule %s specified by "--torsionMaxMatches" option. Abandoning torsion scan...\n'
  628                         % (TorsionMaxMatches, MolName)
  629                     )
  630                 break
  631 
  632             TmpMol, TorsionScanStatus, TorsionMols, TorsionEnergies, TorsionAngles = ScanSingleTorsionInMol(
  633                 Mol,
  634                 TorsionID,
  635                 TorsionPattern,
  636                 TorsionPatternMol,
  637                 TorsionMatches,
  638                 TorsionMatchNum,
  639                 MolNum,
  640                 UseMultiProcessingAtTorsionAnglesLevel,
  641             )
  642             if not TorsionScanStatus:
  643                 continue
  644 
  645             TorsionScanCount += 1
  646             GenerateOutputFiles(Mol, MolNum, TorsionID, TorsionMatchNum, TorsionMols, TorsionEnergies, TorsionAngles)
  647 
  648         if TorsionMatchCount > TorsionMaxMatches:
  649             break
  650 
  651     if OptionsInfo["RemoveHydrogens"]:
  652         Mol = Chem.RemoveHs(Mol)
  653 
  654     if TorsionScanCount:
  655         GenerateStartingTorsionScanStructureOutfile(Mol, MolNum)
  656 
  657     Status = True if TorsionScanCount else False
  658 
  659     return (Mol, Status)
  660 
  661 
  662 def ScanSingleTorsionInMol(
  663     Mol,
  664     TorsionID,
  665     TorsionPattern,
  666     TorsionPatternMol,
  667     TorsionMatches,
  668     TorsionMatchNum,
  669     MolNum,
  670     UseMultiProcessingAtTorsionAnglesLevel,
  671 ):
  672     """Perform torsion scan for a molecule along with constrained energy minimization."""
  673 
  674     if not OptionsInfo["QuietMode"]:
  675         MiscUtil.PrintInfo(
  676             "Processing torsion pattern, %s, match number, %s, in molecule %s..."
  677             % (TorsionPattern, TorsionMatchNum, RDKitUtil.GetMolName(Mol, MolNum))
  678         )
  679 
  680     if UseMultiProcessingAtTorsionAnglesLevel:
  681         return ScanSingleTorsionInMolUsingMultipleProcessesAtTorsionAnglesLevel(
  682             Mol, TorsionID, TorsionPattern, TorsionPatternMol, TorsionMatches, MolNum
  683         )
  684     else:
  685         return ScanSingleTorsionInMolUsingSingleProcess(
  686             Mol, TorsionID, TorsionPattern, TorsionPatternMol, TorsionMatches, MolNum
  687         )
  688 
  689 
  690 def ScanSingleTorsionInMolUsingSingleProcess(Mol, TorsionID, TorsionPattern, TorsionPatternMol, TorsionMatches, MolNum):
  691     """Perform torsion scan for a molecule using single processs along with constrained
  692     energy minimization."""
  693 
  694     TorsionMols = []
  695     TorsionEnergies = []
  696     TorsionAngles = []
  697 
  698     Mols, Angles = SetupMolsForSingleTorsionScanInMol(Mol, TorsionMatches, MolNum)
  699 
  700     for Index, Angle in enumerate(Angles):
  701         TorsionMol = Mols[Index]
  702         TorsionMol, CalcStatus, Energy = MinimizeCalculateEnergyForTorsionMol(
  703             Mol, TorsionMol, Angle, TorsionID, TorsionPattern, TorsionPatternMol, TorsionMatches, MolNum
  704         )
  705 
  706         if not CalcStatus:
  707             return (Mol, False, None, None, None)
  708 
  709         if OptionsInfo["RemoveHydrogens"]:
  710             TorsionMol = Chem.RemoveHs(TorsionMol)
  711 
  712         TorsionMols.append(TorsionMol)
  713         TorsionEnergies.append(Energy)
  714         TorsionAngles.append(Angle)
  715 
  716     return (Mol, True, TorsionMols, TorsionEnergies, TorsionAngles)
  717 
  718 
  719 def SetupMolsForSingleTorsionScanInMol(Mol, TorsionMatches, MolNum=None):
  720     """Setup molecules corresponding to all torsion angles in a molecule."""
  721 
  722     AtomIndex1, AtomIndex2, AtomIndex3, AtomIndex4 = TorsionMatches
  723 
  724     TorsionMols = []
  725     TorsionAngles = OptionsInfo["TorsionAngles"]
  726 
  727     for Angle in TorsionAngles:
  728         TorsionMol = Chem.Mol(Mol)
  729         TorsionMolConf = TorsionMol.GetConformer(0)
  730 
  731         rdMolTransforms.SetDihedralDeg(TorsionMolConf, AtomIndex1, AtomIndex2, AtomIndex3, AtomIndex4, Angle)
  732         TorsionMols.append(TorsionMol)
  733 
  734     return (TorsionMols, TorsionAngles)
  735 
  736 
  737 def MinimizeCalculateEnergyForTorsionMol(
  738     Mol, TorsionMol, TorsionAngle, TorsionID, TorsionPattern, TorsionPatternMol, TorsionMatches, MolNum
  739 ):
  740     """ "Calculate energy of a torsion molecule by performing an optional constrained
  741     energy minimzation.
  742     """
  743 
  744     if OptionsInfo["TorsionMinimize"]:
  745         # Perform constrained minimization...
  746         TorsionMatchesMol = RDKitUtil.MolFromSubstructureMatch(TorsionMol, TorsionPatternMol, TorsionMatches)
  747         TorsionMol, CalcStatus, Energy = ConstrainAndMinimizeMolecule(
  748             TorsionMol, TorsionMatchesMol, TorsionMatches, MolNum
  749         )
  750 
  751         if not CalcStatus:
  752             if not OptionsInfo["QuietMode"]:
  753                 MolName = RDKitUtil.GetMolName(Mol, MolNum)
  754                 MiscUtil.PrintWarning(
  755                     "Failed to perform constrained minimization for molecule %s with torsion angle set to %s during torsion scan for torsion pattern %s. Abandoning torsion scan..."
  756                     % (MolName, TorsionAngle, TorsionPattern)
  757                 )
  758             return (TorsionMol, False, None)
  759     else:
  760         # Calculate energy...
  761         CalcStatus, Energy = GetEnergy(TorsionMol)
  762         if not CalcStatus:
  763             if not OptionsInfo["QuietMode"]:
  764                 MolName = RDKitUtil.GetMolName(Mol, MolNum)
  765                 MiscUtil.PrintWarning(
  766                     "Failed to retrieve calculated energy for molecule %s with torsion angle set to %s during torsion scan for torsion pattern %s. Abandoning torsion scan..."
  767                     % (MolName, TorsionAngle, TorsionPattern)
  768                 )
  769             return (TorsionMol, False, None)
  770 
  771     return (TorsionMol, CalcStatus, Energy)
  772 
  773 
  774 def SetupTorsionsMolInfo(Mol, MolNum=None):
  775     """Setup torsions info for a molecule."""
  776 
  777     TorsionsPatternsInfo = OptionsInfo["TorsionsPatternsInfo"]
  778 
  779     # Initialize...
  780     TorsionsMolInfo = {}
  781     TorsionsMolInfo["IDs"] = []
  782     TorsionsMolInfo["NumOfMatches"] = 0
  783     TorsionsMolInfo["Matches"] = {}
  784     for TorsionID in TorsionsPatternsInfo["IDs"]:
  785         TorsionsMolInfo["IDs"].append(TorsionID)
  786         TorsionsMolInfo["Matches"][TorsionID] = None
  787 
  788     MolName = RDKitUtil.GetMolName(Mol, MolNum)
  789     UseChirality = OptionsInfo["UseChirality"]
  790 
  791     for TorsionID in TorsionsPatternsInfo["IDs"]:
  792         # Match torsions..
  793         TorsionPattern = TorsionsPatternsInfo["Pattern"][TorsionID]
  794         TorsionPatternMol = TorsionsPatternsInfo["Mol"][TorsionID]
  795         TorsionsMatches = RDKitUtil.FilterSubstructureMatchesByAtomMapNumbers(
  796             Mol, TorsionPatternMol, Mol.GetSubstructMatches(TorsionPatternMol, useChirality=UseChirality)
  797         )
  798 
  799         # Validate tosion matches...
  800         ValidTorsionsMatches = []
  801         for Index, TorsionMatch in enumerate(TorsionsMatches):
  802             if len(TorsionMatch) != 4:
  803                 if not OptionsInfo["QuietMode"]:
  804                     MiscUtil.PrintWarning(
  805                         "Ignoring invalid torsion match to atom indices, %s, for torsion pattern, %s, in molecule %s: It must match exactly 4 atoms."
  806                         % (TorsionMatch, TorsionPattern, MolName)
  807                     )
  808                 continue
  809 
  810             if not RDKitUtil.AreAtomIndicesSequentiallyConnected(Mol, TorsionMatch):
  811                 if not OptionsInfo["QuietMode"]:
  812                     MiscUtil.PrintInfo("")
  813                     MiscUtil.PrintWarning(
  814                         "Invalid torsion match to atom indices, %s, for torsion pattern, %s, in molecule %s: Matched atom indices must be sequentially connected."
  815                         % (TorsionMatch, TorsionPattern, MolName)
  816                     )
  817                     MiscUtil.PrintWarning("Reordering matched atom indices in a sequentially connected manner...")
  818 
  819                 Status, ReorderdTorsionMatch = RDKitUtil.ReorderAtomIndicesInSequentiallyConnectedManner(
  820                     Mol, TorsionMatch
  821                 )
  822                 if Status:
  823                     TorsionMatch = ReorderdTorsionMatch
  824                     if not OptionsInfo["QuietMode"]:
  825                         MiscUtil.PrintWarning(
  826                             "Successfully reordered torsion match to atom indices, %s, for torsion pattern, %s, in molecule %s: Matched atom indices are now sequentially connected."
  827                             % (TorsionMatch, TorsionPattern, MolName)
  828                         )
  829                 else:
  830                     if not OptionsInfo["QuietMode"]:
  831                         MiscUtil.PrintWarning(
  832                             "Ignoring torsion match. Failed to reorder torsion match to atom indices, %s, for torsion pattern, %s, in molecule %s: Matched atom indices are not sequentially connected."
  833                             % (TorsionMatch, TorsionPattern, MolName)
  834                         )
  835                     continue
  836 
  837             Bond = Mol.GetBondBetweenAtoms(TorsionMatch[1], TorsionMatch[2])
  838             if Bond.IsInRing():
  839                 if not OptionsInfo["QuietMode"]:
  840                     MiscUtil.PrintWarning(
  841                         "Ignoring invalid torsion match to atom indices, %s, for torsion pattern, %s, in molecule %s: Matched atom indices, %s and %s, are not allowed to be in a ring."
  842                         % (TorsionMatch, TorsionPattern, MolName, TorsionMatch[1], TorsionMatch[2])
  843                     )
  844                 continue
  845 
  846             # Filter matched torsions...
  847             if OptionsInfo["FilterTorsionsByAtomIndicesMode"]:
  848                 InvalidAtomIndices = []
  849                 for AtomIndex in TorsionMatch:
  850                     if AtomIndex not in OptionsInfo["TorsionsFilterByAtomIndicesList"]:
  851                         InvalidAtomIndices.append(AtomIndex)
  852                 if len(InvalidAtomIndices):
  853                     if not OptionsInfo["QuietMode"]:
  854                         MiscUtil.PrintWarning(
  855                             'Ignoring invalid torsion match to atom indices, %s, for torsion pattern, %s, in molecule %s: Matched atom indices, %s,  must be present in the list, %s, specified using option "--torsionsFilterbyAtomIndices".'
  856                             % (
  857                                 TorsionMatch,
  858                                 TorsionPattern,
  859                                 MolName,
  860                                 InvalidAtomIndices,
  861                                 OptionsInfo["TorsionsFilterByAtomIndicesList"],
  862                             )
  863                         )
  864                     continue
  865 
  866             ValidTorsionsMatches.append(TorsionMatch)
  867 
  868         # Track valid matches...
  869         if len(ValidTorsionsMatches):
  870             TorsionsMolInfo["NumOfMatches"] += len(ValidTorsionsMatches)
  871             TorsionsMolInfo["Matches"][TorsionID] = ValidTorsionsMatches
  872 
  873     if TorsionsMolInfo["NumOfMatches"] == 0:
  874         if not OptionsInfo["QuietMode"]:
  875             MiscUtil.PrintWarning("Failed to match any torsions  in molecule %s" % (MolName))
  876 
  877     return TorsionsMolInfo
  878 
  879 
  880 def SetupTorsionsPatternsInfo():
  881     """Setup torsions patterns info."""
  882 
  883     TorsionsPatternsInfo = {}
  884     TorsionsPatternsInfo["IDs"] = []
  885     TorsionsPatternsInfo["Pattern"] = {}
  886     TorsionsPatternsInfo["Mol"] = {}
  887 
  888     TorsionID = 0
  889     for TorsionPattern in OptionsInfo["TorsionPatternsList"]:
  890         TorsionID += 1
  891 
  892         TorsionMol = Chem.MolFromSmarts(TorsionPattern)
  893         if TorsionMol is None:
  894             MiscUtil.PrintError(
  895                 'Failed to create torsion pattern molecule. The torsion SMILES/SMARTS pattern, "%s", specified using "-t, --torsions" option is not valid.'
  896                 % (TorsionPattern)
  897             )
  898 
  899         TorsionsPatternsInfo["IDs"].append(TorsionID)
  900         TorsionsPatternsInfo["Pattern"][TorsionID] = TorsionPattern
  901         TorsionsPatternsInfo["Mol"][TorsionID] = TorsionMol
  902 
  903     OptionsInfo["TorsionsPatternsInfo"] = TorsionsPatternsInfo
  904 
  905 
  906 def MinimizeMolecule(Mol, MolNum=None):
  907     """Generate and minimize conformers for a molecule to get the lowest energy conformer."""
  908 
  909     if not OptionsInfo["QuietMode"]:
  910         MiscUtil.PrintInfo("Minimizing molecule %s..." % (RDKitUtil.GetMolName(Mol, MolNum)))
  911 
  912     ConfIDs = EmbedMolecule(Mol, MolNum)
  913     if not len(ConfIDs):
  914         if not OptionsInfo["QuietMode"]:
  915             MolName = RDKitUtil.GetMolName(Mol, MolNum)
  916             MiscUtil.PrintWarning("Minimization couldn't be performed for molecule %s: Embedding failed...\n" % MolName)
  917         return (Mol, False)
  918 
  919     CalcEnergyMap = {}
  920     for ConfID in ConfIDs:
  921         try:
  922             if OptionsInfo["UseUFF"]:
  923                 Status = AllChem.UFFOptimizeMolecule(Mol, confId=ConfID, maxIters=OptionsInfo["MaxIters"])
  924             elif OptionsInfo["UseMMFF"]:
  925                 Status = AllChem.MMFFOptimizeMolecule(
  926                     Mol, confId=ConfID, maxIters=OptionsInfo["MaxIters"], mmffVariant=OptionsInfo["MMFFVariant"]
  927                 )
  928             else:
  929                 MiscUtil.PrintError(
  930                     "Minimization couldn't be performed: Specified forcefield, %s, is not supported"
  931                     % OptionsInfo["ForceField"]
  932                 )
  933         except (ValueError, RuntimeError, Chem.rdchem.KekulizeException) as ErrMsg:
  934             if not OptionsInfo["QuietMode"]:
  935                 MolName = RDKitUtil.GetMolName(Mol, MolNum)
  936                 MiscUtil.PrintWarning("Minimization couldn't be performed for molecule %s:\n%s\n" % (MolName, ErrMsg))
  937             return (Mol, False)
  938 
  939         EnergyStatus, Energy = GetEnergy(Mol, ConfID)
  940         if not EnergyStatus:
  941             if not OptionsInfo["QuietMode"]:
  942                 MolName = RDKitUtil.GetMolName(Mol, MolNum)
  943                 MiscUtil.PrintWarning(
  944                     "Failed to retrieve calculated energy for conformation number %d of molecule %s. Try again after removing any salts or cleaing up the molecule...\n"
  945                     % (ConfID, MolName)
  946                 )
  947             return (Mol, False)
  948 
  949         if Status != 0:
  950             if not OptionsInfo["QuietMode"]:
  951                 MolName = RDKitUtil.GetMolName(Mol, MolNum)
  952                 MiscUtil.PrintWarning(
  953                     'Minimization failed to converge for conformation number %d of molecule %s in %d steps. Try using higher value for "--maxIters" option...\n'
  954                     % (ConfID, MolName, OptionsInfo["MaxIters"])
  955                 )
  956 
  957         CalcEnergyMap[ConfID] = Energy
  958 
  959     SortedConfIDs = sorted(ConfIDs, key=lambda ConfID: CalcEnergyMap[ConfID])
  960     MinEnergyConfID = SortedConfIDs[0]
  961 
  962     for ConfID in [Conf.GetId() for Conf in Mol.GetConformers()]:
  963         if ConfID == MinEnergyConfID:
  964             continue
  965         Mol.RemoveConformer(ConfID)
  966 
  967     # Set ConfID to 0 for MinEnergyConf...
  968     Mol.GetConformer(MinEnergyConfID).SetId(0)
  969 
  970     return (Mol, True)
  971 
  972 
  973 def ConstrainAndMinimizeMolecule(Mol, RefMolCore, RefMolMatches=None, MolNum=None):
  974     """Constrain and Minimize molecule."""
  975 
  976     # Setup forcefield function to use for constrained minimization...
  977     ForceFieldFunction = None
  978     ForceFieldName = None
  979     if OptionsInfo["UseUFF"]:
  980         ForceFieldFunction = lambda mol, confId=-1: AllChem.UFFGetMoleculeForceField(mol, confId=confId)
  981         ForceFieldName = "UFF"
  982     else:
  983         ForceFieldFunction = lambda mol, confId=-1: AllChem.MMFFGetMoleculeForceField(
  984             mol, AllChem.MMFFGetMoleculeProperties(mol, mmffVariant=OptionsInfo["MMFFVariant"]), confId=confId
  985         )
  986         ForceFieldName = "MMFF"
  987 
  988     if ForceFieldFunction is None:
  989         if not OptionsInfo["QuietMode"]:
  990             MiscUtil.PrintWarning(
  991                 "Failed to setup forcefield %s for molecule: %s\n" % (ForceFieldName, RDKitUtil.GetMolName(Mol, MolNum))
  992             )
  993         return (None, False, None)
  994 
  995     MaxConfs = OptionsInfo["MaxConfsTorsion"]
  996     EnforceChirality = OptionsInfo["EnforceChirality"]
  997     UseExpTorsionAnglePrefs = OptionsInfo["UseExpTorsionAnglePrefs"]
  998     ETVersion = OptionsInfo["ETVersion"]
  999     UseBasicKnowledge = OptionsInfo["UseBasicKnowledge"]
 1000     UseTethers = OptionsInfo["UseTethers"]
 1001 
 1002     CalcEnergyMap = {}
 1003     MolConfsMap = {}
 1004     ConfIDs = [ConfID for ConfID in range(0, MaxConfs)]
 1005 
 1006     for ConfID in ConfIDs:
 1007         try:
 1008             MolConf = Chem.Mol(Mol)
 1009             RDKitUtil.ConstrainAndEmbed(
 1010                 MolConf,
 1011                 RefMolCore,
 1012                 coreMatchesMol=RefMolMatches,
 1013                 useTethers=UseTethers,
 1014                 coreConfId=-1,
 1015                 randomseed=ConfID,
 1016                 getForceField=ForceFieldFunction,
 1017                 enforceChirality=EnforceChirality,
 1018                 useExpTorsionAnglePrefs=UseExpTorsionAnglePrefs,
 1019                 useBasicKnowledge=UseBasicKnowledge,
 1020                 ETversion=ETVersion,
 1021             )
 1022         except (ValueError, RuntimeError, Chem.rdchem.KekulizeException) as ErrMsg:
 1023             if not OptionsInfo["QuietMode"]:
 1024                 MolName = RDKitUtil.GetMolName(Mol, MolNum)
 1025                 MiscUtil.PrintWarning(
 1026                     "Constrained embedding couldn't  be performed for molecule %s:\n%s\n"
 1027                     % (RDKitUtil.GetMolName(Mol, MolNum), ErrMsg)
 1028                 )
 1029             return (None, False, None)
 1030 
 1031         EnergyStatus, Energy = GetEnergy(MolConf)
 1032 
 1033         if not EnergyStatus:
 1034             if not OptionsInfo["QuietMode"]:
 1035                 MolName = RDKitUtil.GetMolName(Mol, MolNum)
 1036                 MiscUtil.PrintWarning(
 1037                     "Failed to retrieve calculated energy for conformation number %d of molecule %s. Try again after removing any salts or cleaing up the molecule...\n"
 1038                     % (ConfID, MolName)
 1039                 )
 1040             return (None, False, None)
 1041 
 1042         CalcEnergyMap[ConfID] = Energy
 1043         MolConfsMap[ConfID] = MolConf
 1044 
 1045     SortedConfIDs = sorted(ConfIDs, key=lambda ConfID: CalcEnergyMap[ConfID])
 1046     MinEnergyConfID = SortedConfIDs[0]
 1047 
 1048     MinEnergy = CalcEnergyMap[MinEnergyConfID]
 1049     MinEnergyMolConf = MolConfsMap[MinEnergyConfID]
 1050 
 1051     MinEnergyMolConf.ClearProp("EmbedRMS")
 1052 
 1053     return (MinEnergyMolConf, True, MinEnergy)
 1054 
 1055 
 1056 def GetEnergy(Mol, ConfID=None):
 1057     """Calculate energy."""
 1058 
 1059     Status = True
 1060     Energy = None
 1061 
 1062     if ConfID is None:
 1063         ConfID = -1
 1064 
 1065     if OptionsInfo["UseUFF"]:
 1066         UFFMoleculeForcefield = AllChem.UFFGetMoleculeForceField(Mol, confId=ConfID)
 1067         if UFFMoleculeForcefield is None:
 1068             Status = False
 1069         else:
 1070             Energy = UFFMoleculeForcefield.CalcEnergy()
 1071     elif OptionsInfo["UseMMFF"]:
 1072         MMFFMoleculeProperties = AllChem.MMFFGetMoleculeProperties(Mol, mmffVariant=OptionsInfo["MMFFVariant"])
 1073         MMFFMoleculeForcefield = AllChem.MMFFGetMoleculeForceField(Mol, MMFFMoleculeProperties, confId=ConfID)
 1074         if MMFFMoleculeForcefield is None:
 1075             Status = False
 1076         else:
 1077             Energy = MMFFMoleculeForcefield.CalcEnergy()
 1078     else:
 1079         MiscUtil.PrintError(
 1080             "Couldn't retrieve conformer energy: Specified forcefield, %s, is not supported" % OptionsInfo["ForceField"]
 1081         )
 1082 
 1083     return (Status, Energy)
 1084 
 1085 
 1086 def EmbedMolecule(Mol, MolNum=None):
 1087     """Embed conformations."""
 1088 
 1089     ConfIDs = []
 1090 
 1091     MaxConfs = OptionsInfo["MaxConfs"]
 1092     RandomSeed = OptionsInfo["RandomSeed"]
 1093     EnforceChirality = OptionsInfo["EnforceChirality"]
 1094     UseExpTorsionAnglePrefs = OptionsInfo["UseExpTorsionAnglePrefs"]
 1095     ETVersion = OptionsInfo["ETVersion"]
 1096     UseBasicKnowledge = OptionsInfo["UseBasicKnowledge"]
 1097 
 1098     try:
 1099         ConfIDs = AllChem.EmbedMultipleConfs(
 1100             Mol,
 1101             numConfs=MaxConfs,
 1102             randomSeed=RandomSeed,
 1103             enforceChirality=EnforceChirality,
 1104             useExpTorsionAnglePrefs=UseExpTorsionAnglePrefs,
 1105             useBasicKnowledge=UseBasicKnowledge,
 1106             ETversion=ETVersion,
 1107         )
 1108     except ValueError as ErrMsg:
 1109         if not OptionsInfo["QuietMode"]:
 1110             MolName = RDKitUtil.GetMolName(Mol, MolNum)
 1111             MiscUtil.PrintWarning("Embedding failed  for molecule %s:\n%s\n" % (MolName, ErrMsg))
 1112         ConfIDs = []
 1113 
 1114     return ConfIDs
 1115 
 1116 
 1117 def GenerateStartingTorsionScanStructureOutfile(Mol, MolNum):
 1118     """Write out the structure of molecule used for starting tosion scan."""
 1119 
 1120     FileDir, FileName, FileExt = MiscUtil.ParseFileName(Options["--outfile"])
 1121     MolName = GetOutputFileMolName(Mol, MolNum)
 1122 
 1123     Outfile = "%s_%s.%s" % (FileName, MolName, FileExt)
 1124 
 1125     # Set up a molecule writer...
 1126     Writer = RDKitUtil.MoleculesWriter(Outfile, **OptionsInfo["OutfileParams"])
 1127     if Writer is None:
 1128         MiscUtil.PrintWarning("Failed to setup a writer for output fie %s " % Outfile)
 1129         return
 1130 
 1131     Writer.write(Mol)
 1132 
 1133     if Writer is not None:
 1134         Writer.close()
 1135 
 1136 
 1137 def GenerateOutputFiles(Mol, MolNum, TorsionID, TorsionMatchNum, TorsionMols, TorsionEnergies, TorsionAngles):
 1138     """Generate output files."""
 1139 
 1140     StructureOutfile, EnergyTextOutfile, PlotOutfile, ViewerHTMLOutfile = SetupOutputFileNames(
 1141         Mol, MolNum, TorsionID, TorsionMatchNum
 1142     )
 1143 
 1144     GenerateScannedTorsionsStructureOutfile(
 1145         StructureOutfile, Mol, MolNum, TorsionID, TorsionMatchNum, TorsionMols, TorsionEnergies, TorsionAngles
 1146     )
 1147     GenerateEnergyTextOutfile(
 1148         EnergyTextOutfile, Mol, MolNum, TorsionID, TorsionMatchNum, TorsionMols, TorsionEnergies, TorsionAngles
 1149     )
 1150     GeneratePlotOutfile(
 1151         PlotOutfile, Mol, MolNum, TorsionID, TorsionMatchNum, TorsionMols, TorsionEnergies, TorsionAngles
 1152     )
 1153     GenerateViewerHTMLOutfile(
 1154         ViewerHTMLOutfile, Mol, MolNum, TorsionID, TorsionMatchNum, TorsionMols, TorsionEnergies, TorsionAngles
 1155     )
 1156 
 1157 
 1158 def GenerateScannedTorsionsStructureOutfile(
 1159     Outfile, Mol, MolNum, TorsionID, TorsionMatchNum, TorsionMols, TorsionEnergies, TorsionAngles
 1160 ):
 1161     """Write out structures generated after torsion scan along with associated data."""
 1162 
 1163     # Set up a molecule writer...
 1164     Writer = RDKitUtil.MoleculesWriter(Outfile, **OptionsInfo["OutfileParams"])
 1165     if Writer is None:
 1166         MiscUtil.PrintWarning("Failed to setup a writer for output fie %s " % Outfile)
 1167         return
 1168 
 1169     MolName = RDKitUtil.GetMolName(Mol, MolNum)
 1170 
 1171     RelativeTorsionEnergies = SetupRelativeEnergies(TorsionEnergies)
 1172     for Index, TorsionMol in enumerate(TorsionMols):
 1173         TorsionAngle = "%s" % TorsionAngles[Index]
 1174         TorsionMol.SetProp("Torsion_Angle", TorsionAngle)
 1175 
 1176         TorsionEnergy = "%.2f" % TorsionEnergies[Index]
 1177         TorsionMol.SetProp(OptionsInfo["EnergyLabel"], TorsionEnergy)
 1178 
 1179         RelativeTorsionEnergy = "%.2f" % RelativeTorsionEnergies[Index]
 1180         TorsionMol.SetProp(OptionsInfo["RelativeEnergyLabel"], RelativeTorsionEnergy)
 1181 
 1182         TorsionMolName = "%s_Deg%s" % (MolName, TorsionAngle)
 1183         TorsionMol.SetProp("_Name", TorsionMolName)
 1184 
 1185         Writer.write(TorsionMol)
 1186 
 1187     if Writer is not None:
 1188         Writer.close()
 1189 
 1190 
 1191 def GenerateEnergyTextOutfile(
 1192     Outfile, Mol, MolNum, TorsionID, TorsionMatchNum, TorsionMols, TorsionEnergies, TorsionAngles
 1193 ):
 1194     """Write out torsion angles and energies."""
 1195 
 1196     # Setup a writer...
 1197     Writer = open(Outfile, "w")
 1198     if Writer is None:
 1199         MiscUtil.PrintError("Failed to setup a writer for output fie %s " % Outfile)
 1200 
 1201     # Write headers...
 1202     Writer.write("TorsionAngle,%s,%s\n" % (OptionsInfo["EnergyLabel"], OptionsInfo["RelativeEnergyLabel"]))
 1203 
 1204     RelativeTorsionEnergies = SetupRelativeEnergies(TorsionEnergies)
 1205     for Index, TorsionAngle in enumerate(TorsionAngles):
 1206         Writer.write("%d,%.2f,%.2f\n" % (TorsionAngle, TorsionEnergies[Index], RelativeTorsionEnergies[Index]))
 1207 
 1208     if Writer is not None:
 1209         Writer.close()
 1210 
 1211 
 1212 def GenerateViewerHTMLOutfile(
 1213     Outfile, Mol, MolNum, TorsionID, TorsionMatchNum, TorsionMols, TorsionEnergies, TorsionAngles
 1214 ):
 1215     """Write out a HTML file for viewing torsion scan along with associated data."""
 1216 
 1217     # Setup a writer...
 1218     Writer = open(Outfile, "w")
 1219     if Writer is None:
 1220         MiscUtil.PrintError("Failed to setup a writer for output fie %s " % Outfile)
 1221 
 1222     # Setup HTML for torsion scan viewer...
 1223     MolName = RDKitUtil.GetMolName(Mol, MolNum)
 1224     RelativeTorsionEnergies = SetupRelativeEnergies(TorsionEnergies)
 1225 
 1226     Units = "kcal/mol"
 1227     PlotHeight = OptionsInfo["OutPlotTorsionViewerHeight"]
 1228 
 1229     ForceField = OptionsInfo["MMFFVariant"] if OptionsInfo["UseMMFF"] else OptionsInfo["ForceField"]
 1230     TitleLine = "%s (%s)" % (OptionsInfo["OutPlotParams"]["Title"], ForceField)
 1231     if OptionsInfo["OutPlotTitleTorsionSpec"]:
 1232         TorsionPattern = OptionsInfo["TorsionsPatternsInfo"]["Pattern"][TorsionID]
 1233         TitleLine = "%s: %s" % (TitleLine, TorsionPattern)
 1234 
 1235     ViwerHTMLText = RDKitUtil.SetupHTMLForTorsionScanViewer(
 1236         MolName, TorsionMols, TorsionEnergies, RelativeTorsionEnergies, TorsionAngles, Units, PlotHeight, TitleLine
 1237     )
 1238 
 1239     # Write out HTML for torsion scan viewer...
 1240     Writer.write("%s" % ViwerHTMLText)
 1241 
 1242     Writer.close()
 1243 
 1244 
 1245 def GeneratePlotOutfile(Outfile, Mol, MolNum, TorsionID, TorsionMatchNum, TorsionMols, TorsionEnergies, TorsionAngles):
 1246     """Generate a plot corresponding to torsion angles and energies."""
 1247 
 1248     OutPlotParams = OptionsInfo["OutPlotParams"]
 1249 
 1250     # Initialize seaborn and matplotlib paramaters...
 1251     if not OptionsInfo["OutPlotInitialized"]:
 1252         OptionsInfo["OutPlotInitialized"] = True
 1253         RCParams = {
 1254             "figure.figsize": (OutPlotParams["Width"], OutPlotParams["Height"]),
 1255             "axes.titleweight": OutPlotParams["TitleWeight"],
 1256             "axes.labelweight": OutPlotParams["LabelWeight"],
 1257         }
 1258         sns.set(
 1259             context=OutPlotParams["Context"],
 1260             style=OutPlotParams["Style"],
 1261             palette=OutPlotParams["Palette"],
 1262             font=OutPlotParams["Font"],
 1263             font_scale=OutPlotParams["FontScale"],
 1264             rc=RCParams,
 1265         )
 1266 
 1267     # Create a new figure...
 1268     plt.figure()
 1269 
 1270     if OptionsInfo["OutPlotRelativeEnergy"]:
 1271         TorsionEnergies = SetupRelativeEnergies(TorsionEnergies)
 1272 
 1273     # Draw plot...
 1274     PlotType = OutPlotParams["Type"]
 1275     if re.match("linepoint", PlotType, re.I):
 1276         Axis = sns.lineplot(x=TorsionAngles, y=TorsionEnergies, marker="o", legend=False)
 1277     elif re.match("scatter", PlotType, re.I):
 1278         Axis = sns.scatterplot(x=TorsionAngles, y=TorsionEnergies, legend=False)
 1279     elif re.match("line", PlotType, re.I):
 1280         Axis = sns.lineplot(x=TorsionAngles, y=TorsionEnergies, legend=False)
 1281     else:
 1282         MiscUtil.PrintError(
 1283             'The value, %s, specified for "type" using option "--outPlotParams" is not supported. Valid plot types: linepoint, scatter or line'
 1284             % (PlotType)
 1285         )
 1286 
 1287     # Setup title and labels...
 1288     Title = OutPlotParams["Title"]
 1289     if OptionsInfo["OutPlotTitleTorsionSpec"]:
 1290         TorsionPattern = OptionsInfo["TorsionsPatternsInfo"]["Pattern"][TorsionID]
 1291         Title = "%s: %s" % (OutPlotParams["Title"], TorsionPattern)
 1292 
 1293     # Set labels and title...
 1294     Axis.set(xlabel=OutPlotParams["XLabel"], ylabel=OutPlotParams["YLabel"], title=Title)
 1295 
 1296     # Save figure...
 1297     plt.savefig(Outfile)
 1298 
 1299     # Close the plot...
 1300     plt.close()
 1301 
 1302 
 1303 def SetupRelativeEnergies(Energies):
 1304     """Set up a list of relative energies."""
 1305 
 1306     SortedEnergies = sorted(Energies)
 1307     MinEnergy = SortedEnergies[0]
 1308     RelativeEnergies = [(Energy - MinEnergy) for Energy in Energies]
 1309 
 1310     return RelativeEnergies
 1311 
 1312 
 1313 def SetupOutputFileNames(Mol, MolNum, TorsionID, TorsionMatchNum):
 1314     """Setup names of output files."""
 1315 
 1316     FileDir, FileName, FileExt = MiscUtil.ParseFileName(Options["--outfile"])
 1317     MolName = GetOutputFileMolName(Mol, MolNum)
 1318 
 1319     OutfileRoot = "%s_%s_Torsion%s_Match%s" % (FileName, MolName, TorsionID, TorsionMatchNum)
 1320 
 1321     StructureOutfile = "%s.%s" % (OutfileRoot, FileExt)
 1322     EnergyTextOutfile = "%s_Energies.csv" % (OutfileRoot)
 1323 
 1324     PlotExt = OptionsInfo["OutPlotParams"]["OutExt"]
 1325     PlotOutfile = "%s_Plot.%s" % (OutfileRoot, PlotExt)
 1326 
 1327     ViewerHTMLOutfile = "%s_Viewer.html" % (OutfileRoot)
 1328 
 1329     return (StructureOutfile, EnergyTextOutfile, PlotOutfile, ViewerHTMLOutfile)
 1330 
 1331 
 1332 def GetOutputFileMolName(Mol, MolNum):
 1333     """Get output file prefix."""
 1334 
 1335     MolName = "Mol%s" % MolNum
 1336     if OptionsInfo["OutfileMolName"]:
 1337         MolName = re.sub("[^a-zA-Z0-9]", "_", RDKitUtil.GetMolName(Mol, MolNum), flags=re.I)
 1338 
 1339     return MolName
 1340 
 1341 
 1342 def AddHydrogens(Mol, AddCoords=True):
 1343     """Check and add hydrogens."""
 1344 
 1345     if not OptionsInfo["AddHydrogens"]:
 1346         return Mol
 1347 
 1348     return Chem.AddHs(Mol, addCoords=AddCoords)
 1349 
 1350 
 1351 def ProcessTorsionRangeOptions():
 1352     """Process tosion range options."""
 1353 
 1354     TosionRangeMode = Options["--torsionRangeMode"]
 1355     OptionsInfo["TosionRangeMode"] = TosionRangeMode
 1356 
 1357     if re.match("^Range$", TosionRangeMode, re.I):
 1358         ProcessTorsionRangeValues()
 1359     elif re.match("^Angles$", TosionRangeMode, re.I):
 1360         ProcessTorsionAnglesValues()
 1361     else:
 1362         MiscUtil.PrintError('The value, %s,  option "--torsionRangeMode" is not supported.' % TosionRangeMode)
 1363 
 1364 
 1365 def ProcessTorsionRangeValues():
 1366     """Process tosion range values."""
 1367 
 1368     TorsionRange = Options["--torsionRange"]
 1369     if re.match("^auto$", TorsionRange, re.I):
 1370         TorsionRange = "0,360,5"
 1371     TorsionRangeWords = TorsionRange.split(",")
 1372 
 1373     TorsionStart = int(TorsionRangeWords[0])
 1374     TorsionStop = int(TorsionRangeWords[1])
 1375     TorsionStep = int(TorsionRangeWords[2])
 1376 
 1377     if TorsionStart >= TorsionStop:
 1378         MiscUtil.PrintError(
 1379             'The start value, %d, specified for option "--torsionRange" in string "%s" must be less than stop value, %s.'
 1380             % (TorsionStart, Options["--torsionRange"], TorsionStop)
 1381         )
 1382     if TorsionStep == 0:
 1383         MiscUtil.PrintError(
 1384             'The step value, %d, specified for option "--torsonRange" in string "%s" must be > 0.'
 1385             % (TorsionStep, Options["--torsionRange"])
 1386         )
 1387     if TorsionStep >= (TorsionStop - TorsionStart):
 1388         MiscUtil.PrintError(
 1389             'The step value, %d, specified for option "--torsonRange" in string "%s" must be less than, %s.'
 1390             % (TorsionStep, Options["--torsionRange"], (TorsionStop - TorsionStart))
 1391         )
 1392 
 1393     if TorsionStart < 0:
 1394         if TorsionStart < -180:
 1395             MiscUtil.PrintError(
 1396                 'The start value, %d, specified for option "--torsionRange" in string "%s" must be  >= -180 to use scan range from -180 to 180.'
 1397                 % (TorsionStart, Options["--torsionRange"])
 1398             )
 1399         if TorsionStop > 180:
 1400             MiscUtil.PrintError(
 1401                 'The stop value, %d, specified for option "--torsionRange" in string "%s" must be <= 180 to use scan range from -180 to 180.'
 1402                 % (TorsionStop, Options["--torsionRange"])
 1403             )
 1404     else:
 1405         if TorsionStop > 360:
 1406             MiscUtil.PrintError(
 1407                 'The stop value, %d, specified for option "--torsionRange" in string "%s" must be  <= 360 to use scan range from 0 to 360.'
 1408                 % (TorsionStop, Options["--torsionRange"])
 1409             )
 1410 
 1411     TorsionAngles = [Angle for Angle in range(TorsionStart, TorsionStop, TorsionStep)]
 1412     TorsionAngles.append(TorsionStop)
 1413 
 1414     OptionsInfo["TorsionRange"] = TorsionRange
 1415     OptionsInfo["TorsionStart"] = TorsionStart
 1416     OptionsInfo["TorsionStop"] = TorsionStop
 1417     OptionsInfo["TorsionStep"] = TorsionStep
 1418 
 1419     OptionsInfo["TorsionAngles"] = TorsionAngles
 1420 
 1421 
 1422 def ProcessTorsionAnglesValues():
 1423     """Process tosion angle values."""
 1424 
 1425     TorsionRange = Options["--torsionRange"]
 1426     if re.match("^auto$", TorsionRange, re.I):
 1427         MiscUtil.PrintError('The value specified, %s, for option "--torsionRange" is not valid.' % (TorsionRange))
 1428 
 1429     TorsionAngles = []
 1430 
 1431     for TorsionAngle in TorsionRange.split(","):
 1432         TorsionAngle = int(TorsionAngle)
 1433 
 1434         if TorsionAngle < -180:
 1435             MiscUtil.PrintError(
 1436                 'The stop value, %d, specified for option "--torsionRange" in string "%s" must be  >= -180.'
 1437                 % (TorsionAngle, TorsionRange)
 1438             )
 1439 
 1440         if TorsionAngle > 360:
 1441             MiscUtil.PrintError(
 1442                 'The stop value, %d, specified for option "--torsionRange" in string "%s" must be  <= 360.'
 1443                 % (TorsionAngle, TorsionRange)
 1444             )
 1445 
 1446         TorsionAngles.append(TorsionAngle)
 1447 
 1448     OptionsInfo["TorsionRange"] = TorsionRange
 1449     OptionsInfo["TorsionStart"] = None
 1450     OptionsInfo["TorsionStop"] = None
 1451     OptionsInfo["TorsionStep"] = None
 1452 
 1453     OptionsInfo["TorsionAngles"] = sorted(TorsionAngles)
 1454 
 1455 
 1456 def ProcesssConformerGeneratorOption():
 1457     """Process comformer generator option."""
 1458 
 1459     ConfGenParams = MiscUtil.ProcessOptionConformerGenerator("--conformerGenerator", Options["--conformerGenerator"])
 1460 
 1461     OptionsInfo["ConformerGenerator"] = ConfGenParams["ConformerGenerator"]
 1462     OptionsInfo["UseBasicKnowledge"] = ConfGenParams["UseBasicKnowledge"]
 1463     OptionsInfo["UseExpTorsionAnglePrefs"] = ConfGenParams["UseExpTorsionAnglePrefs"]
 1464     OptionsInfo["ETVersion"] = ConfGenParams["ETVersion"]
 1465 
 1466 
 1467 def ProcessOptions():
 1468     """Process and validate command line arguments and options."""
 1469 
 1470     MiscUtil.PrintInfo("Processing options...")
 1471 
 1472     # Validate options...
 1473     ValidateOptions()
 1474 
 1475     OptionsInfo["ModeMols"] = Options["--modeMols"]
 1476     OptionsInfo["FirstMolMode"] = True if re.match("^First$", Options["--modeMols"], re.I) else False
 1477 
 1478     OptionsInfo["ModeTorsions"] = Options["--modeTorsions"]
 1479     OptionsInfo["FirstTorsionMode"] = True if re.match("^First$", Options["--modeTorsions"], re.I) else False
 1480 
 1481     OptionsInfo["Infile"] = Options["--infile"]
 1482     OptionsInfo["InfileParams"] = MiscUtil.ProcessOptionInfileParameters(
 1483         "--infileParams", Options["--infileParams"], Options["--infile"]
 1484     )
 1485     OptionsInfo["Infile3D"] = True if re.match("^yes$", Options["--infile3D"], re.I) else False
 1486 
 1487     OptionsInfo["Outfile"] = Options["--outfile"]
 1488     OptionsInfo["OutfileParams"] = MiscUtil.ProcessOptionOutfileParameters(
 1489         "--outfileParams", Options["--outfileParams"]
 1490     )
 1491 
 1492     OptionsInfo["OutfileMolName"] = True if re.match("^yes$", Options["--outfileMolName"], re.I) else False
 1493 
 1494     OptionsInfo["OutPlotRelativeEnergy"] = (
 1495         True if re.match("^yes$", Options["--outPlotRelativeEnergy"], re.I) else False
 1496     )
 1497     OptionsInfo["OutPlotTitleTorsionSpec"] = (
 1498         True if re.match("^yes$", Options["--outPlotTitleTorsionSpec"], re.I) else False
 1499     )
 1500     OptionsInfo["OutPlotTorsionViewerHeight"] = int(Options["--outPlotTorsionViewerHeight"])
 1501 
 1502     # The default width and height, 10.0 and 5.6, map to aspect raito of 16/9 (1.778)...
 1503     DefaultValues = {
 1504         "Type": "linepoint",
 1505         "Width": 10.0,
 1506         "Height": 5.6,
 1507         "Title": "RDKit Torsion Scan",
 1508         "XLabel": "Torsion Angle (degrees)",
 1509         "YLabel": "Energy (kcal/mol)",
 1510     }
 1511     if OptionsInfo["OutPlotRelativeEnergy"]:
 1512         DefaultValues["YLabel"] = "Relative Energy (kcal/mol)"
 1513     OptionsInfo["OutPlotParams"] = MiscUtil.ProcessOptionSeabornPlotParameters(
 1514         "--outPlotParams", Options["--outPlotParams"], DefaultValues
 1515     )
 1516     if not re.match("^(linepoint|scatter|Line)$", OptionsInfo["OutPlotParams"]["Type"], re.I):
 1517         MiscUtil.PrintError(
 1518             'The value, %s, specified for "type" using option "--outPlotParams" is not supported. Valid plot types: linepoint, scatter or line'
 1519             % (OptionsInfo["OutPlotParams"]["Type"])
 1520         )
 1521 
 1522     OptionsInfo["OutPlotInitialized"] = False
 1523 
 1524     # Procsss and validate specified SMILES/SMARTS torsion patterns...
 1525     TorsionPatterns = Options["--torsions"]
 1526     TorsionPatternsList = []
 1527     for TorsionPattern in TorsionPatterns.split(","):
 1528         TorsionPattern = TorsionPattern.strip()
 1529         if not len(TorsionPattern):
 1530             MiscUtil.PrintError(
 1531                 'Empty value specified for SMILES/SMARTS pattern in  "-t, --torsions" option: %s' % TorsionPatterns
 1532             )
 1533 
 1534         TorsionMol = Chem.MolFromSmarts(TorsionPattern)
 1535         if TorsionMol is None:
 1536             MiscUtil.PrintError(
 1537                 'Failed to create torsion pattern molecule. The torsion SMILES/SMARTS pattern, "%s", specified using "-t, --torsions" option, "%s",  is not valid.'
 1538                 % (TorsionPattern, TorsionPatterns)
 1539             )
 1540         TorsionPatternsList.append(TorsionPattern)
 1541 
 1542     OptionsInfo["TorsionPatterns"] = TorsionPatterns
 1543     OptionsInfo["TorsionPatternsList"] = TorsionPatternsList
 1544 
 1545     # Process and validate any specified torsion atom indices for filtering torsion matches...
 1546     TorsionsFilterByAtomIndices = Options["--torsionsFilterbyAtomIndices"]
 1547     TorsionsFilterByAtomIndicesList = []
 1548     if not re.match("^None$", TorsionsFilterByAtomIndices, re.I):
 1549         for AtomIndex in TorsionsFilterByAtomIndices.split(","):
 1550             AtomIndex = AtomIndex.strip()
 1551             if not MiscUtil.IsInteger(AtomIndex):
 1552                 MiscUtil.PrintError(
 1553                     'The value specified, %s, for option "--torsionsFilterbyAtomIndices" must be an integer.'
 1554                     % AtomIndex
 1555                 )
 1556             AtomIndex = int(AtomIndex)
 1557             if AtomIndex < 0:
 1558                 MiscUtil.PrintError(
 1559                     'The value specified, %s, for option "--torsionsFilterbyAtomIndices" must be >= 0.' % AtomIndex
 1560                 )
 1561             TorsionsFilterByAtomIndicesList.append(AtomIndex)
 1562 
 1563         if len(TorsionsFilterByAtomIndicesList) < 4:
 1564             MiscUtil.PrintError(
 1565                 'The number of values, %s,  specified, %s, for option "--torsionsFilterbyAtomIndices" must be >=4.'
 1566                 % (len(TorsionsFilterByAtomIndicesList), TorsionsFilterByAtomIndices)
 1567             )
 1568 
 1569     OptionsInfo["TorsionsFilterByAtomIndices"] = TorsionsFilterByAtomIndices
 1570     OptionsInfo["TorsionsFilterByAtomIndicesList"] = TorsionsFilterByAtomIndicesList
 1571     OptionsInfo["FilterTorsionsByAtomIndicesMode"] = True if len(TorsionsFilterByAtomIndicesList) > 0 else False
 1572 
 1573     OptionsInfo["Overwrite"] = Options["--overwrite"]
 1574 
 1575     OptionsInfo["AddHydrogens"] = True if re.match("^yes$", Options["--addHydrogens"], re.I) else False
 1576 
 1577     ProcesssConformerGeneratorOption()
 1578 
 1579     if re.match("^UFF$", Options["--forceField"], re.I):
 1580         ForceField = "UFF"
 1581         UseUFF = True
 1582         UseMMFF = False
 1583     elif re.match("^MMFF$", Options["--forceField"], re.I):
 1584         ForceField = "MMFF"
 1585         UseUFF = False
 1586         UseMMFF = True
 1587     else:
 1588         MiscUtil.PrintError(
 1589             'The value, %s, specified for "--forceField" is not supported.' % (Options["--forceField"],)
 1590         )
 1591 
 1592     MMFFVariant = "MMFF94" if re.match("^MMFF94$", Options["--forceFieldMMFFVariant"], re.I) else "MMFF94s"
 1593 
 1594     OptionsInfo["ForceField"] = ForceField
 1595     OptionsInfo["MMFFVariant"] = MMFFVariant
 1596     OptionsInfo["UseMMFF"] = UseMMFF
 1597     OptionsInfo["UseUFF"] = UseUFF
 1598 
 1599     if UseMMFF:
 1600         OptionsInfo["EnergyLabel"] = "%s_Energy" % MMFFVariant
 1601         OptionsInfo["RelativeEnergyLabel"] = "%s_Relative_Energy" % MMFFVariant
 1602     else:
 1603         OptionsInfo["EnergyLabel"] = "%s_Energy" % ForceField
 1604         OptionsInfo["RelativeEnergyLabel"] = "%s_Relative_Energy" % ForceField
 1605 
 1606     OptionsInfo["EnforceChirality"] = True if re.match("^yes$", Options["--enforceChirality"], re.I) else False
 1607 
 1608     OptionsInfo["MaxConfs"] = int(Options["--maxConfs"])
 1609     OptionsInfo["MaxConfsTorsion"] = int(Options["--maxConfsTorsion"])
 1610     OptionsInfo["MaxIters"] = int(Options["--maxIters"])
 1611 
 1612     OptionsInfo["MPMode"] = True if re.match("^yes$", Options["--mp"], re.I) else False
 1613     OptionsInfo["MPParams"] = MiscUtil.ProcessOptionMultiprocessingParameters("--mpParams", Options["--mpParams"])
 1614 
 1615     # Multiprocessing level...
 1616     MPLevelMoleculesMode = False
 1617     MPLevelTorsionAnglesMode = False
 1618     MPLevel = Options["--mpLevel"]
 1619     if re.match("^Molecules$", MPLevel, re.I):
 1620         MPLevelMoleculesMode = True
 1621     elif re.match("^TorsionAngles$", MPLevel, re.I):
 1622         MPLevelTorsionAnglesMode = True
 1623     else:
 1624         MiscUtil.PrintError('The value, %s, specified for option "--mpLevel" is not valid. ' % MPLevel)
 1625     OptionsInfo["MPLevel"] = MPLevel
 1626     OptionsInfo["MPLevelMoleculesMode"] = MPLevelMoleculesMode
 1627     OptionsInfo["MPLevelTorsionAnglesMode"] = MPLevelTorsionAnglesMode
 1628 
 1629     OptionsInfo["QuietMode"] = True if re.match("^yes$", Options["--quiet"], re.I) else False
 1630 
 1631     RandomSeed = -1
 1632     if not re.match("^auto$", Options["--randomSeed"], re.I):
 1633         RandomSeed = int(Options["--randomSeed"])
 1634     OptionsInfo["RandomSeed"] = RandomSeed
 1635 
 1636     OptionsInfo["RemoveHydrogens"] = True if re.match("^yes$", Options["--removeHydrogens"], re.I) else False
 1637 
 1638     OptionsInfo["TorsionMaxMatches"] = int(Options["--torsionMaxMatches"])
 1639     OptionsInfo["TorsionMinimize"] = True if re.match("^yes$", Options["--torsionMinimize"], re.I) else False
 1640 
 1641     ProcessTorsionRangeOptions()
 1642 
 1643     OptionsInfo["UseTethers"] = True if re.match("^yes$", Options["--useTethers"], re.I) else False
 1644     OptionsInfo["UseChirality"] = True if re.match("^yes$", Options["--useChirality"], re.I) else False
 1645 
 1646 
 1647 def RetrieveOptions():
 1648     """Retrieve command line arguments and options."""
 1649 
 1650     # Get options...
 1651     global Options
 1652     Options = docopt(_docoptUsage_)
 1653 
 1654     # Set current working directory to the specified directory...
 1655     WorkingDir = Options["--workingdir"]
 1656     if WorkingDir:
 1657         os.chdir(WorkingDir)
 1658 
 1659     # Handle examples option...
 1660     if "--examples" in Options and Options["--examples"]:
 1661         MiscUtil.PrintInfo(MiscUtil.GetExamplesTextFromDocOptText(_docoptUsage_))
 1662         sys.exit(0)
 1663 
 1664 
 1665 def ValidateOptions():
 1666     """Validate option values."""
 1667 
 1668     MiscUtil.ValidateOptionTextValue("-a, --addHydrogens", Options["--addHydrogens"], "yes no")
 1669     MiscUtil.ValidateOptionTextValue(
 1670         "-c, --conformerGenerator", Options["--conformerGenerator"], "SDG KDG ETDG ETKDG ETKDGv2"
 1671     )
 1672 
 1673     MiscUtil.ValidateOptionTextValue("-f, --forceField", Options["--forceField"], "UFF MMFF")
 1674     MiscUtil.ValidateOptionTextValue(" --forceFieldMMFFVariant", Options["--forceFieldMMFFVariant"], "MMFF94 MMFF94s")
 1675 
 1676     MiscUtil.ValidateOptionTextValue("--enforceChirality ", Options["--enforceChirality"], "yes no")
 1677 
 1678     MiscUtil.ValidateOptionFilePath("-i, --infile", Options["--infile"])
 1679     MiscUtil.ValidateOptionFileExt("-i, --infile", Options["--infile"], "sdf sd mol smi txt csv tsv")
 1680     MiscUtil.ValidateOptionTextValue("--infile3D", Options["--infile3D"], "yes no")
 1681 
 1682     MiscUtil.ValidateOptionFileExt("-o, --outfile", Options["--outfile"], "sdf sd")
 1683     MiscUtil.ValidateOptionsOutputFileOverwrite(
 1684         "-o, --outfile", Options["--outfile"], "--overwrite", Options["--overwrite"]
 1685     )
 1686     MiscUtil.ValidateOptionsDistinctFileNames(
 1687         "-i, --infile", Options["--infile"], "-o, --outfile", Options["--outfile"]
 1688     )
 1689 
 1690     if not Options["--overwrite"]:
 1691         FileDir, FileName, FileExt = MiscUtil.ParseFileName(Options["--outfile"])
 1692         FileNames = glob.glob("%s_*" % FileName)
 1693         if len(FileNames):
 1694             MiscUtil.PrintError(
 1695                 'The outfile names, %s_*, generated from file specified, %s, for option "-o, --outfile" already exist. Use option "--overwrite" or "--ov"  and try again.\n'
 1696                 % (FileName, Options["--outfile"])
 1697             )
 1698 
 1699     MiscUtil.ValidateOptionTextValue("--outPlotRelativeEnergy", Options["--outPlotRelativeEnergy"], "yes no")
 1700     MiscUtil.ValidateOptionTextValue("--outPlotTitleTorsionSpec", Options["--outPlotTitleTorsionSpec"], "yes no")
 1701     MiscUtil.ValidateOptionIntegerValue(
 1702         "--outPlotTorsionViewerHeight", Options["--outPlotTorsionViewerHeight"], {">": 0}
 1703     )
 1704 
 1705     MiscUtil.ValidateOptionTextValue("--outfileMolName ", Options["--outfileMolName"], "yes no")
 1706 
 1707     MiscUtil.ValidateOptionTextValue("--modeMols", Options["--modeMols"], "First All")
 1708     MiscUtil.ValidateOptionTextValue("--modeTorsions", Options["--modeTorsions"], "First All")
 1709 
 1710     MiscUtil.ValidateOptionIntegerValue("--maxConfs", Options["--maxConfs"], {">": 0})
 1711     MiscUtil.ValidateOptionIntegerValue("--maxConfsTorsion", Options["--maxConfsTorsion"], {">": 0})
 1712     MiscUtil.ValidateOptionIntegerValue("--maxIters", Options["--maxIters"], {">": 0})
 1713 
 1714     MiscUtil.ValidateOptionTextValue("--mp", Options["--mp"], "yes no")
 1715     MiscUtil.ValidateOptionTextValue("--mpLevel", Options["--mpLevel"], "Molecules TorsionAngles")
 1716     MiscUtil.ValidateOptionTextValue("-q, --quiet", Options["--quiet"], "yes no")
 1717 
 1718     if not re.match("^auto$", Options["--randomSeed"], re.I):
 1719         MiscUtil.ValidateOptionIntegerValue("--randomSeed", Options["--randomSeed"], {})
 1720 
 1721     MiscUtil.ValidateOptionTextValue("-r, --removeHydrogens", Options["--removeHydrogens"], "yes no")
 1722 
 1723     MiscUtil.ValidateOptionIntegerValue("--torsionMaxMatches", Options["--torsionMaxMatches"], {">": 0})
 1724     MiscUtil.ValidateOptionTextValue("--torsionMinimize", Options["--torsionMinimize"], "yes no")
 1725 
 1726     MiscUtil.ValidateOptionTextValue("--torsionRangeMode", Options["--torsionRangeMode"], "Range or Angles")
 1727     TorsionRange = Options["--torsionRange"]
 1728     if re.match("^Range$", Options["--torsionRangeMode"], re.I):
 1729         if not re.match("^auto$", TorsionRange, re.I):
 1730             MiscUtil.ValidateOptionNumberValues("--torsionRange", TorsionRange, 3, ",", "integer", {})
 1731     else:
 1732         if re.match("^auto$", TorsionRange, re.I):
 1733             MiscUtil.PrintError(
 1734                 'The value, %s, specified for option "-torsionRange" is not valid for "%s" value of "--torsionRangeMode" option. You must specify a torsion angle or a comma delimited list of torsion angles.'
 1735                 % (TorsionRange, Options["--torsionRangeMode"])
 1736             )
 1737         TorsionAngles = []
 1738         for TorsionAngle in TorsionRange.split(","):
 1739             TorsionAngle = TorsionAngle.strip()
 1740             if not MiscUtil.IsInteger(TorsionAngle):
 1741                 MiscUtil.PrintError(
 1742                     'The value specified, %s, for option "--torsionRange" in string "%s" must be an integer.'
 1743                     % (TorsionAngle, TorsionRange)
 1744                 )
 1745             if TorsionAngle in TorsionAngles:
 1746                 MiscUtil.PrintError(
 1747                     'The value specified, %s, for option "--torsionRange" in string "%s" is a duplicate value.'
 1748                     % (TorsionAngle, TorsionRange)
 1749                 )
 1750             TorsionAngles.append(TorsionAngle)
 1751 
 1752     MiscUtil.ValidateOptionTextValue("--useChirality", Options["--useChirality"], "yes no")
 1753     MiscUtil.ValidateOptionTextValue("--useTethers", Options["--useTethers"], "yes no")
 1754 
 1755 
 1756 # Setup a usage string for docopt...
 1757 _docoptUsage_ = """
 1758 RDKitPerformTorsionScan.py - Perform torsion scan
 1759 
 1760 Usage:
 1761     RDKitPerformTorsionScan.py [--addHydrogens <yes or no>] [--conformerGenerator <SDG, KDG, ETDG, ETKDG, ETKDGv2>]
 1762                                [--forceField <UFF, or MMFF>] [--forceFieldMMFFVariant <MMFF94 or MMFF94s>]
 1763                                [--enforceChirality <yes or no>] [--infile3D <yes or no>] [--infileParams <Name,Value,...>]
 1764                                [--modeMols  <First or All>] [--modeTorsions  <First or All> ] [--maxConfs <number>]
 1765                                [--maxConfsTorsion <number>] [--maxIters <number>] [--mp <yes or no>]
 1766                                [--mpLevel <Molecules or TorsionAngles>] [--mpParams <Name,Value,...>]
 1767                                [--outfileMolName  <yes or no>] [--outfileParams <Name,Value,...>] [--outPlotParams <Name,Value,...>]
 1768                                [--outPlotRelativeEnergy <yes or no>] [--outPlotTitleTorsionSpec <yes or no>] [--outPlotTorsionViewerHeight <number>]
 1769                                [--overwrite]  [--quiet <yes or no>] [--removeHydrogens <yes or no>] [--randomSeed <number>]
 1770                                [--torsionsFilterbyAtomIndices <Index1, Index2, ...>] [--torsionMaxMatches <number>] [--torsionMinimize <yes or no>]
 1771                                [--torsionRangeMode <Range or Angles>] [--torsionRange <Start,Stop,Step or Angle1,Angle2,...>]
 1772                                [--useChirality <yes or no>] [--useTethers  <yes or no>] [-w <dir>] -t <torsions> -i <infile>  -o <outfile> 
 1773     RDKitPerformTorsionScan.py -h | --help | -e | --examples
 1774 
 1775 Description:
 1776     Perform torsion scan for molecules around torsion angles specified using
 1777     SMILES/SMARTS patterns. A molecule is optionally minimized before performing
 1778     a torsion scan. A set of initial 3D structures are generated for a molecule
 1779     by scanning the torsion angle across the specified range and updating the 3D
 1780     coordinates of the molecule. A conformation ensemble is optionally generated
 1781     for each 3D structure representing a specific torsion angle. The conformation
 1782     with the lowest energy is selected to represent the torsion angle. An option
 1783     is available to skip the generation of the conformation ensemble and simply
 1784     calculate the energy for the initial 3D structure for a specific torsion angle.
 1785 
 1786     The torsions are specified using SMILES or SMARTS patterns. A substructure match
 1787     is performed to select torsion atoms in a molecule. The SMILES pattern match must
 1788     correspond to four torsion atoms. The SMARTS patterns containing atom map numbers
 1789     may match  more than four atoms. The atom map numbers, however, must match
 1790     exactly four torsion atoms. For example: [s:1][c:2]([aX2,cH1])!@[CX3:3](O)=[O:4] for
 1791     thiophene esters and carboxylates as specified in Torsion Library (TorLib) [Ref 146].
 1792 
 1793     A set of five output files is generated for each torsion match in each
 1794     molecule. The names of the output files are generated using the root of
 1795     the specified output file. They may either contain sequential molecule
 1796     numbers or molecule names as shown below:
 1797         
 1798         <OutfileRoot>_Mol<Num>.sdf
 1799         <OutfileRoot>_Mol<Num>_Torsion<Num>_Match<Num>.sdf
 1800         <OutfileRoot>_Mol<Num>_Torsion<Num>_Match<Num>_Energies.csv
 1801         <OutfileRoot>_Mol<Num>_Torsion<Num>_Match<Num>_Plot.<ImgExt>
 1802         <OutfileRoot>_Mol<Num>_Torsion<Num>_Match<Num>_Viewer.html
 1803         
 1804         or
 1805         
 1806         <OutfileRoot>_<MolName>.sdf
 1807         <OutfileRoot>_<MolName>_Torsion<Num>_Match<Num>.sdf
 1808         <OutfileRoot>_<MolName>_Torsion<Num>_Match<Num>_Energies.csv
 1809         <OutfileRoot>_<MolName>_Torsion<Num>_Match<Num>_Plot.<ImgExt>
 1810         <OutfileRoot>_<MolName>_Torsion<Num>_Match<Num>_Viewer.html
 1811         
 1812     The supported input file formats are: Mol (.mol), SD (.sdf, .sd), SMILES (.smi,
 1813     .csv, .tsv, .txt)
 1814 
 1815     The supported output file formats are: SD (.sdf, .sd)
 1816 
 1817 Options:
 1818     -a, --addHydrogens <yes or no>  [default: yes]
 1819         Add hydrogens before minimization.
 1820     -c, --conformerGenerator <text>  [default: ETKDGv2]
 1821         Conformation generation methodology for generating initial 3D structure
 1822         of a molecule and conformation ensemble representing a specific torsion
 1823         angle. No conformation ensemble is generated for 'No' value of
 1824         '--torsionMinimize' option.
 1825         
 1826         The possible values along with a brief description are shown below:
 1827             
 1828             SDG: Standard Distance Geometry
 1829             KDG: basic Knowledge-terms with Distance Geometry
 1830             ETDG: Experimental Torsion-angle preference with Distance Geometry
 1831             ETKDG: Experimental Torsion-angle preference along with basic
 1832                 Knowledge-terms and Distance Geometry [Ref 129]
 1833             ETKDGv2: Experimental Torsion-angle preference along with basic
 1834                 Knowledge-terms and Distance Geometry [Ref 167]
 1835     -f, --forceField <UFF, MMFF>  [default: MMFF]
 1836         Forcefield method to use for  energy minimization of initial 3D structure
 1837         of a molecule and conformation ensemble representing a specific torsion.
 1838         No conformation ensemble is generated during for 'No' value of '--torsionMinimze'
 1839         option and constrained energy minimization is not performed. Possible values:
 1840         Universal Force Field (UFF) [ Ref 81 ] or Merck Molecular Mechanics Force
 1841         Field [ Ref 83-87 ] .
 1842     --forceFieldMMFFVariant <MMFF94 or MMFF94s>  [default: MMFF94]
 1843         Variant of MMFF forcefield to use for energy minimization.
 1844     --enforceChirality <yes or no>  [default: Yes]
 1845         Enforce chirality for defined chiral centers during generation of conformers.
 1846     -e, --examples
 1847         Print examples.
 1848     -h, --help
 1849         Print this help message.
 1850     -i, --infile <infile>
 1851         Input file name.
 1852     --infile3D <yes or no>  [default: no]
 1853         Skip generation and minimization of initial 3D structures for molecules in
 1854         input file containing 3D coordinates.
 1855     --infileParams <Name,Value,...>  [default: auto]
 1856         A comma delimited list of parameter name and value pairs for reading
 1857         molecules from files. The supported parameter names for different file
 1858         formats, along with their default values, are shown below:
 1859             
 1860             SD, MOL: removeHydrogens,yes,sanitize,yes,strictParsing,yes
 1861             
 1862             SMILES: smilesColumn,1,smilesNameColumn,2,smilesDelimiter,space,
 1863                 smilesTitleLine,auto,sanitize,yes
 1864             
 1865         Possible values for smilesDelimiter: space, comma or tab.
 1866     --modeMols <First or All>  [default: First]
 1867         Perform torsion scan for the first molecule or all molecules in input
 1868         file.
 1869     --modeTorsions <First or All>  [default: First]
 1870         Perform torsion scan for the first or all specified torsion pattern in
 1871         molecules up to a maximum number of matches for each torsion
 1872         specification as indicated by '--torsionMaxMatches' option. 
 1873     --maxConfs <number>  [default: 250]
 1874         Maximum number of conformations to generate for initial 3D structure of a
 1875         molecule. The lowest energy conformation is written to the output file.
 1876     --maxConfsTorsion <number>  [default: 50]
 1877         Maximum number of conformations to generate for conformation ensemble
 1878         representing a specific torsion. A constrained minimization is performed
 1879         using the coordinates of the specified torsion and the lowest energy
 1880         conformation is written to the output file.
 1881     --maxIters <number>  [default: 500]
 1882         Maximum number of iterations to perform for a molecule during minimization
 1883         to generation initial 3D structures. This option is ignored during 'yes' value
 1884         of  '--infile3D' option.
 1885     --mp <yes or no>  [default: no]
 1886         Use multiprocessing.
 1887          
 1888         By default, input data is retrieved in a lazy manner via mp.Pool.imap()
 1889         function employing lazy RDKit data iterable. This allows processing of
 1890         arbitrary large data sets without any additional requirements memory.
 1891         
 1892         All input data may be optionally loaded into memory by mp.Pool.map()
 1893         before starting worker processes in a process pool by setting the value
 1894         of 'inputDataMode' to 'InMemory' in '--mpParams' option.
 1895         
 1896         A word to the wise: The default 'chunkSize' value of 1 during 'Lazy' input
 1897         data mode may adversely impact the performance. The '--mpParams' section
 1898         provides additional information to tune the value of 'chunkSize'.
 1899     --mpLevel <Molecules or TorsionAngles>  [default: Molecules]
 1900         Perform multiprocessing at molecules or torsion angles level. Possible values:
 1901         Molecules or TorsionAngles. The 'Molecules' value starts a process pool at the
 1902         molecules level. All torsion angles of a molecule are processed in a single
 1903         process. The 'TorsionAngles' value, however, starts a process pool at the 
 1904         torsion angles level. Each torsion angle in a torsion match for a molecule is
 1905         processed in an individual process in the process pool.
 1906     --mpParams <Name,Value,...>  [default: auto]
 1907         A comma delimited list of parameter name and value pairs to configure
 1908         multiprocessing.
 1909         
 1910         The supported parameter names along with their default and possible
 1911         values are shown below:
 1912         
 1913             chunkSize, auto
 1914             inputDataMode, Lazy   [ Possible values: InMemory or Lazy ]
 1915             numProcesses, auto   [ Default: mp.cpu_count() ]
 1916         
 1917         These parameters are used by the following functions to configure and
 1918         control the behavior of multiprocessing: mp.Pool(), mp.Pool.map(), and
 1919         mp.Pool.imap().
 1920         
 1921         The chunkSize determines chunks of input data passed to each worker
 1922         process in a process pool by mp.Pool.map() and mp.Pool.imap() functions.
 1923         The default value of chunkSize is dependent on the value of 'inputDataMode'.
 1924         
 1925         The mp.Pool.map() function, invoked during 'InMemory' input data mode,
 1926         automatically converts RDKit data iterable into a list, loads all data into
 1927         memory, and calculates the default chunkSize using the following method
 1928         as shown in its code:
 1929         
 1930             chunkSize, extra = divmod(len(dataIterable), len(numProcesses) * 4)
 1931             if extra: chunkSize += 1
 1932         
 1933         For example, the default chunkSize will be 7 for a pool of 4 worker processes
 1934         and 100 data items.
 1935         
 1936         The mp.Pool.imap() function, invoked during 'Lazy' input data mode, employs
 1937         'lazy' RDKit data iterable to retrieve data as needed, without loading all the
 1938         data into memory. Consequently, the size of input data is not known a priori.
 1939         It's not possible to estimate an optimal value for the chunkSize. The default 
 1940         chunkSize is set to 1.
 1941         
 1942         The default value for the chunkSize during 'Lazy' data mode may adversely
 1943         impact the performance due to the overhead associated with exchanging
 1944         small chunks of data. It is generally a good idea to explicitly set chunkSize to
 1945         a larger value during 'Lazy' input data mode, based on the size of your input
 1946         data and number of processes in the process pool.
 1947         
 1948         The mp.Pool.map() function waits for all worker processes to process all
 1949         the data and return the results. The mp.Pool.imap() function, however,
 1950         returns the the results obtained from worker processes as soon as the
 1951         results become available for specified chunks of data.
 1952         
 1953         The order of data in the results returned by both mp.Pool.map() and 
 1954         mp.Pool.imap() functions always corresponds to the input data.
 1955     -o, --outfile <outfile>
 1956         Output file name. The output file root is used for generating the names
 1957         of the output files corresponding to structures, energies, and plots during
 1958         the torsion scan.
 1959     --outfileMolName <yes or no>  [default: no]
 1960         Append molecule name to output file root during the generation of the names
 1961         for output files. The default is to use <MolNum>. The non alphabetical
 1962         characters in molecule names are replaced by underscores.
 1963     --outfileParams <Name,Value,...>  [default: auto]
 1964         A comma delimited list of parameter name and value pairs for writing
 1965         molecules to files. The supported parameter names for different file
 1966         formats, along with their default values, are shown below:
 1967             
 1968             SD: kekulize,yes,forceV3000,no
 1969             
 1970     --outPlotParams <Name,Value,...>  [default: auto]
 1971         A comma delimited list of parameter name and value pairs for generating
 1972         plots using Seaborn module. The supported parameter names along with their
 1973         default values are shown below:
 1974             
 1975             type,linepoint,outExt,svg,width,10,height,5.6,
 1976             title,auto,xlabel,auto,ylabel,auto,titleWeight,bold,labelWeight,bold
 1977             style,darkgrid,palette,deep,font,sans-serif,fontScale,1,
 1978             context,notebook
 1979             
 1980         Possible values:
 1981             
 1982             type: linepoint, scatter, or line. Both points and lines are drawn
 1983                 for linepoint plot type.
 1984             outExt: Any valid format supported by Python module Matplotlib.
 1985                 For example: PDF (.pdf), PNG (.png), PS (.ps), SVG (.svg)
 1986             titleWeight, labelWeight: Font weight for title and axes labels.
 1987                 Any valid value.
 1988             style: darkgrid, whitegrid, dark, white, ticks
 1989             palette: deep, muted, pastel, dark, bright, colorblind
 1990             font: Any valid font name
 1991             context: paper, notebook, talk, poster, or any valid name
 1992             
 1993     --outPlotRelativeEnergy <yes or no>  [default: yes]
 1994         Plot relative energies in the torsion plot. The minimum energy value is
 1995         subtracted from energy values to calculate relative energies. This option
 1996         is not used during the generation of interactive energy plot for torsion
 1997         scan viewer, which always plots relative energy.
 1998     --outPlotTitleTorsionSpec <yes or no>  [default: yes]
 1999         Append torsion specification to the title of the torsion plot.
 2000     --outPlotTorsionViewerHeight <number>  [default: 430]
 2001         Plot height in pixels for interactive relative energy plot generated in
 2002         torsion scan viewer. This is different from the width and height specified
 2003         using '--outPlotParams' for the standalone plots.
 2004     --overwrite
 2005         Overwrite existing files.
 2006     -q, --quiet <yes or no>  [default: no]
 2007         Use quiet mode. The warning and information messages will not be printed.
 2008     --randomSeed <number>  [default: auto]
 2009         Seed for the random number generator for generating initial 3D coordinates.
 2010         Default is to use a random seed.
 2011     --removeHydrogens <yes or no>  [default: Yes]
 2012         Remove hydrogens after minimization.
 2013     -t, --torsions <SMILES/SMARTS,...,...>
 2014         SMILES/SMARTS patterns corresponding to torsion specifications. It's a 
 2015         comma delimited list of valid SMILES/SMART patterns.
 2016         
 2017         A substructure match is performed to select torsion atoms in a molecule.
 2018         The SMILES pattern match must correspond to four torsion atoms. The
 2019         SMARTS patterns containing atom map numbers  may match  more than four
 2020         atoms. The atom map numbers, however, must match exactly four torsion
 2021         atoms. For example: [s:1][c:2]([aX2,cH1])!@[CX3:3](O)=[O:4] for thiophene
 2022         esters and carboxylates as specified in Torsion Library (TorLib) [Ref 146].
 2023     --torsionsFilterbyAtomIndices <Index1, Index2, ...>  [default: none]
 2024         Comma delimited list of atom indices for filtering torsion matches
 2025         corresponding to torsion specifications  "-t, --torsions". The atom indices
 2026         must be valid. No explicit validation is performed. The list must contain at
 2027         least 4 atom indices.
 2028         
 2029         The torsion atom indices, matched by "-t, --torsions" specifications, must be
 2030         present in the list. Otherwise, the torsion matches are ignored.
 2031     --torsionMaxMatches <number>  [default: 5]
 2032         Maximum number of torsions to match for each torsion specification in a
 2033         molecule.
 2034     --torsionMinimize <yes or no>  [default: no]
 2035         Perform constrained energy minimization on a conformation ensemble
 2036         for  a specific torsion angle and select the lowest energy conformation
 2037         representing the torsion angle.
 2038     --torsionRangeMode <Range or Angles>  [default: Range]
 2039         Perform torsion scan using torsion angles corresponding to a torsion range
 2040         or an explicit list of torsion angles. Possible values: Range or Angles. You
 2041         may use '--torsionRange' option to specify values for torsion angle or
 2042         torsion angles.
 2043     --torsionRange <Start,Stop,Step or Angle1,Angle2...>  [default: auto]
 2044         Start, stop, and step size angles or a comma delimited list of angles in
 2045         degrees for a torsion scan.
 2046         
 2047         This value is '--torsionRangeMode' specific. It must be a triplet corresponding
 2048         to 'start,Stop,Step' for 'Range' value of '--torsionRange' option. Otherwise, it
 2049         is comma delimited list of one or more torsion angles for 'Angles' value of
 2050         '--torsionRange' option.
 2051         
 2052         The default values, based on '--torsionRangeMode' option, are shown below:
 2053             
 2054             TorsionRangeMode       Default value
 2055             Range                  0,360,5
 2056             Angles                 None
 2057             
 2058         You must explicitly provide a list of  torsion angle(s) for 'Angles' of
 2059         '--torsionRangeMode' option.
 2060     --useChirality <yes or no>  [default: no]
 2061         Use chirrality during substructure matches for identification of torsions.
 2062      --useTethers <yes or no>  [default: yes]
 2063         Use tethers to optimize the final conformation by applying a series of extra forces
 2064         to align matching atoms to the positions of the core atoms. Otherwise, use simple
 2065         distance constraints during the optimization.
 2066     -w, --workingdir <dir>
 2067         Location of working directory which defaults to the current directory.
 2068 
 2069 Examples:
 2070     To perform a torsion scan from 0 to 360 degrees with a stepsize of 5 on the
 2071     first molecule in a SMILES file using a minimum energy structure of the molecule
 2072     selected from an ensemble of conformations, skipping generation of conformation
 2073     ensembles for specific torsion angles and constrained energy minimization of the
 2074     ensemble, generate output files corresponding to structure, energy and torsion
 2075     plot, type:
 2076     
 2077         % RDKitPerformTorsionScan.py  -t "O=CNC" -i SampleSeriesD3R.smi 
 2078           -o SampleOut.sdf
 2079 
 2080     To run the previous example for performing a torsion scan using a specific list
 2081     of torsion angles, type:
 2082     
 2083         % RDKitPerformTorsionScan.py  -t "O=CNC" -i SampleSeriesD3R.smi 
 2084           -o SampleOut.sdf --torsionRangleMode Angles
 2085           --torsionRange "160,220,280"
 2086     
 2087     To run the previous example on all molecules in a SD file, type:
 2088     
 2089         % RDKitPerformTorsionScan.py  -t "O=CNC" --modeMols All
 2090           -i SampleSeriesD3R.sdf -o SampleOut.sdf
 2091 
 2092     To perform a torsion scan on the first molecule in a SMILES file using a minimum
 2093     energy structure of the molecule selected from an ensemble of conformations,
 2094     generation of conformation ensembles for specific torsion angles and constrained
 2095     energy minimization of the ensemble, generate output files corresponding to
 2096     structure, energy and torsion plot, type:
 2097     
 2098         % RDKitPerformTorsionScan.py  -t "O=CNC" --torsionMinimize Yes
 2099            -i SampleSeriesD3R.smi -o SampleOut.sdf
 2100 
 2101     To run the previous example on all molecules in a SD file, type:
 2102     
 2103         % RDKitPerformTorsionScan.py  -t "O=CNC" --modeMols All
 2104            --torsionMinimize Yes -i SampleSeriesD3R.sdf -o SampleOut.sdf
 2105 
 2106     To run the previous example in multiprocessing mode at molecules level
 2107     on all available CPUs without loading all data into memory and write out
 2108     a SD file, type:
 2109 
 2110         % RDKitPerformTorsionScan.py  -t "O=CNC" -i SampleSeriesD3R.smi 
 2111           -o SampleOut.sdf --modeMols All --torsionMinimize Yes --mp yes
 2112     
 2113     To run the previous example in multiprocessing mode at torsion angles level
 2114     on all available CPUs without loading all data into memory and write out
 2115     a SD file, type:
 2116 
 2117         % RDKitPerformTorsionScan.py  -t "O=CNC" -i SampleSeriesD3R.smi 
 2118           -o SampleOut.sdf --modeMols All --torsionMinimize Yes --mp yes
 2119           --mpLevel TorsionAngles
 2120     
 2121     To run the previous example in multiprocessing mode on all available CPUs
 2122     by loading all data into memory and write out a SD file, type:
 2123 
 2124         % RDKitPerformTorsionScan.py  -t "O=CNC" -i SampleSeriesD3R.smi 
 2125           -o SampleOut.sdf --modeMols All --torsionMinimize Yes --mp yes
 2126           --mpParams "inputDataMode,InMemory"
 2127     
 2128     To run the previous example in multiprocessing mode on specific number of
 2129     CPUs and chunk size without loading all data into memory and write out a SD file,
 2130     type:
 2131 
 2132         % RDKitPerformTorsionScan.py  -t "O=CNC" -i SampleSeriesD3R.smi 
 2133           -o SampleOut.sdf --modeMols All --torsionMinimize Yes --mp yes
 2134           --mpParams "inputDataMode,Lazy,numProcesses,4,chunkSize,8"
 2135 
 2136     To perform a torsion scan on first molecule in a SD file containing 3D coordinates,
 2137     skipping generation of conformation ensembles for specific torsion angles and
 2138     constrained energy minimization of the ensemble, generate output files
 2139     corresponding to structure, energy and torsion plot, type:
 2140     
 2141         % RDKitPerformTorsionScan.py  -t "O=CNC"  --infile3D yes
 2142           -i SampleSeriesD3R3D.sdf -o SampleOut.sdf
 2143 
 2144     To perform a torsion scan using multiple torsion specifications on all molecules in
 2145     a SD file containing 3D coordinates, generation of conformation ensembles for specific
 2146     torsion angles and constrained energy minimization of the ensemble, generate output files
 2147     corresponding to structure, energy and torsion plot, type:
 2148     
 2149         % RDKitPerformTorsionScan.py  -t "O=CNC,[O:1]=[C:2](c)[N:3][C:4]"
 2150           --infile3D yes --modeMols All  --modeTorsions All
 2151           --torsionMinimize Yes -i SampleSeriesD3R3D.sdf -o SampleOut.sdf
 2152 
 2153     To run the previous example using a specific torsion scan range, type:
 2154 
 2155         % RDKitPerformTorsionScan.py  -t "O=CNC,[O:1]=[C:2](c)[N:3][C:4]"
 2156           --infile3D yes --modeMols All --modeTorsions All --torsionMinimize
 2157           Yes --torsionRange 0,360,10 -i SampleSeriesD3R.smi -o SampleOut.sdf
 2158 
 2159 Author:
 2160     Manish Sud(msud@san.rr.com)
 2161 
 2162 Acknowledgment:
 2163     Pat Walters
 2164 
 2165 See also:
 2166     RDKitCalculateRMSD.py, RDKitCalculateMolecularDescriptors.py, RDKitCompareMoleculeShapes.py,
 2167     RDKitConvertFileFormat.py, RDKitPerformConstrainedMinimization.py
 2168 
 2169 Copyright:
 2170     Copyright (C) 2026 Manish Sud. All rights reserved.
 2171 
 2172     The functionality available in this script is implemented using RDKit, an
 2173     open source toolkit for cheminformatics developed by Greg Landrum.
 2174 
 2175     This file is part of MayaChemTools.
 2176 
 2177     MayaChemTools is free software; you can redistribute it and/or modify it under
 2178     the terms of the GNU Lesser General Public License as published by the Free
 2179     Software Foundation; either version 3 of the License, or (at your option) any
 2180     later version.
 2181 
 2182 """
 2183 
 2184 if __name__ == "__main__":
 2185     main()