MayaChemTools

    1 #!/bin/env python
    2 #
    3 # File: Psi4PerformTorsionScan.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 Psi4, an
   11 # open source quantum chemistry software package, and RDKit, an open
   12 # source toolkit for cheminformatics developed by Greg Landrum.
   13 #
   14 # This file is part of MayaChemTools.
   15 #
   16 # MayaChemTools is free software; you can redistribute it and/or modify it under
   17 # the terms of the GNU Lesser General Public License as published by the Free
   18 # Software Foundation; either version 3 of the License, or (at your option) any
   19 # later version.
   20 #
   21 # MayaChemTools is distributed in the hope that it will be useful, but without
   22 # any warranty; without even the implied warranty of merchantability of fitness
   23 # for a particular purpose.  See the GNU Lesser General Public License for more
   24 # details.
   25 #
   26 # You should have received a copy of the GNU Lesser General Public License
   27 # along with MayaChemTools; if not, see <http://www.gnu.org/licenses/> or
   28 # write to the Free Software Foundation Inc., 59 Temple Place, Suite 330,
   29 # Boston, MA, 02111-1307, USA.
   30 #
   31 #
   32 
   33 from __future__ import print_function
   34 
   35 import os
   36 import sys
   37 import time
   38 import re
   39 import glob
   40 import shutil
   41 import multiprocessing as mp
   42 
   43 import matplotlib.pyplot as plt
   44 import seaborn as sns
   45 
   46 # Psi4 imports...
   47 if hasattr(shutil, "which") and shutil.which("psi4") is None:
   48     sys.stderr.write("\nWarning: Failed to find 'psi4' in your PATH indicating potential issues with your\n")
   49     sys.stderr.write("Psi4 environment. The 'import psi4' directive in the global scope of the script\n")
   50     sys.stderr.write("interferes with the multiprocessing functionality. It is imported later in the\n")
   51     sys.stderr.write("local scope during the execution of the script and may fail. Check/update your\n")
   52     sys.stderr.write("Psi4 environment and try again.\n\n")
   53 
   54 # RDKit imports...
   55 try:
   56     from rdkit import rdBase
   57     from rdkit import Chem
   58     from rdkit.Chem import AllChem
   59     from rdkit.Chem import rdMolAlign
   60     from rdkit.Chem import rdMolTransforms
   61 except ImportError as ErrMsg:
   62     sys.stderr.write("\nFailed to import RDKit module/package: %s\n" % ErrMsg)
   63     sys.stderr.write("Check/update your RDKit environment and try again.\n\n")
   64     sys.exit(1)
   65 
   66 # MayaChemTools imports...
   67 sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), "..", "lib", "Python"))
   68 try:
   69     from docopt import docopt
   70     import MiscUtil
   71     import Psi4Util
   72     import RDKitUtil
   73 except ImportError as ErrMsg:
   74     sys.stderr.write("\nFailed to import MayaChemTools module/package: %s\n" % ErrMsg)
   75     sys.stderr.write("Check/update your MayaChemTools environment and try again.\n\n")
   76     sys.exit(1)
   77 
   78 ScriptName = os.path.basename(sys.argv[0])
   79 Options = {}
   80 OptionsInfo = {}
   81 
   82 
   83 def main():
   84     """Start execution of the script."""
   85 
   86     MiscUtil.PrintInfo(
   87         "\n%s (Psi4: Imported later; RDKit v%s; MayaChemTools v%s; %s): Starting...\n"
   88         % (ScriptName, rdBase.rdkitVersion, MiscUtil.GetMayaChemToolsVersion(), time.asctime())
   89     )
   90 
   91     (WallClockTime, ProcessorTime) = MiscUtil.GetWallClockAndProcessorTime()
   92 
   93     # Retrieve command line arguments and options...
   94     RetrieveOptions()
   95 
   96     # Process and validate command line arguments and options...
   97     ProcessOptions()
   98 
   99     # Perform actions required by the script...
  100     PerformTorsionScan()
  101 
  102     MiscUtil.PrintInfo("\n%s: Done...\n" % ScriptName)
  103     MiscUtil.PrintInfo("Total time: %s" % MiscUtil.GetFormattedElapsedTime(WallClockTime, ProcessorTime))
  104 
  105 
  106 def PerformTorsionScan():
  107     """Perform torsion scan."""
  108 
  109     # Setup a molecule reader for input file...
  110     MiscUtil.PrintInfo("\nProcessing file %s..." % OptionsInfo["Infile"])
  111     OptionsInfo["InfileParams"]["AllowEmptyMols"] = True
  112     Mols = RDKitUtil.ReadMolecules(OptionsInfo["Infile"], **OptionsInfo["InfileParams"])
  113 
  114     PlotExt = OptionsInfo["OutPlotParams"]["OutExt"]
  115     FileDir, FileName, FileExt = MiscUtil.ParseFileName(Options["--outfile"])
  116     MiscUtil.PrintInfo(
  117         "Generating output files %s_*.sdf, %s_*Torsion*Match*.sdf, %s_*Torsion*Match*Energies.csv, %s_*Torsion*Match*Plot.%s, %s_*Torsion*Match*Viewer.html..."
  118         % (FileName, FileName, FileName, FileName, PlotExt, FileName)
  119     )
  120 
  121     MolCount, ValidMolCount, MinimizationFailedCount, TorsionsMissingCount, TorsionsScanFailedCount = ProcessMolecules(
  122         Mols
  123     )
  124 
  125     MiscUtil.PrintInfo("\nTotal number of molecules: %d" % MolCount)
  126     MiscUtil.PrintInfo("Number of valid molecules: %d" % ValidMolCount)
  127     MiscUtil.PrintInfo("Number of molecules failed during initial minimization: %d" % MinimizationFailedCount)
  128     MiscUtil.PrintInfo("Number of molecules without any matched torsions: %d" % TorsionsMissingCount)
  129     MiscUtil.PrintInfo("Number of molecules failed during torsion scan: %d" % TorsionsScanFailedCount)
  130     MiscUtil.PrintInfo(
  131         "Number of ignored molecules: %d"
  132         % (MolCount - ValidMolCount + TorsionsMissingCount + MinimizationFailedCount + TorsionsScanFailedCount)
  133     )
  134 
  135 
  136 def ProcessMolecules(Mols):
  137     """Process molecules to perform torsion scan."""
  138 
  139     if OptionsInfo["MPMode"]:
  140         return ProcessMoleculesUsingMultipleProcesses(Mols)
  141     else:
  142         return ProcessMoleculesUsingSingleProcess(Mols)
  143 
  144 
  145 def ProcessMoleculesUsingSingleProcess(Mols):
  146     """Process molecules to perform torsion scan using a single process."""
  147 
  148     # Intialize Psi4...
  149     MiscUtil.PrintInfo("\nInitializing Psi4...")
  150     Psi4Handle = Psi4Util.InitializePsi4(
  151         Psi4RunParams=OptionsInfo["Psi4RunParams"],
  152         Psi4OptionsParams=OptionsInfo["Psi4OptionsParams"],
  153         PrintVersion=True,
  154         PrintHeader=True,
  155     )
  156     OptionsInfo["psi4"] = Psi4Handle
  157 
  158     # Setup max iterations global variable...
  159     Psi4Util.UpdatePsi4OptionsParameters(Psi4Handle, {"GEOM_MAXITER": OptionsInfo["MaxIters"]})
  160 
  161     # Setup conversion factor for energy units...
  162     SetupEnergyConversionFactor(Psi4Handle)
  163 
  164     MolInfoText = "first molecule"
  165     if not OptionsInfo["FirstMolMode"]:
  166         MolInfoText = "all molecules"
  167 
  168     if OptionsInfo["TorsionMinimize"]:
  169         MiscUtil.PrintInfo(
  170             "\nPerforming torsion scan on %s by generating conformation ensembles for specific torsion angles and constrained energy minimization of the ensembles..."
  171             % (MolInfoText)
  172         )
  173     else:
  174         MiscUtil.PrintInfo(
  175             "\nPerforming torsion scan on %s by skipping generation of conformation ensembles for specific torsion angles and constrained energy minimization of the ensembles..."
  176             % (MolInfoText)
  177         )
  178 
  179     SetupTorsionsPatternsInfo()
  180 
  181     (MolCount, ValidMolCount, TorsionsMissingCount, MinimizationFailedCount, TorsionsScanFailedCount) = [0] * 5
  182 
  183     for Mol in Mols:
  184         MolCount += 1
  185 
  186         if OptionsInfo["FirstMolMode"] and MolCount > 1:
  187             MolCount -= 1
  188             break
  189 
  190         if not CheckAndValidateMolecule(Mol, MolCount):
  191             continue
  192 
  193         # Setup 2D coordinates for SMILES input file...
  194         if OptionsInfo["SMILESInfileStatus"]:
  195             AllChem.Compute2DCoords(Mol)
  196 
  197         ValidMolCount += 1
  198 
  199         Mol, MinimizationCalcStatus, TorsionsMatchStatus, TorsionsScanCalcStatus = PerformMinimizationAndTorsionScan(
  200             Mol, MolCount
  201         )
  202 
  203         if not MinimizationCalcStatus:
  204             MinimizationFailedCount += 1
  205             continue
  206 
  207         if not TorsionsMatchStatus:
  208             TorsionsMissingCount += 1
  209             continue
  210 
  211         if not TorsionsScanCalcStatus:
  212             TorsionsScanFailedCount += 1
  213             continue
  214 
  215     return (MolCount, ValidMolCount, MinimizationFailedCount, TorsionsMissingCount, TorsionsScanFailedCount)
  216 
  217 
  218 def ProcessMoleculesUsingMultipleProcesses(Mols):
  219     """Process and minimize molecules using multiprocessing."""
  220 
  221     if OptionsInfo["MPLevelTorsionAnglesMode"]:
  222         return ProcessMoleculesUsingMultipleProcessesAtTorsionAnglesLevel(Mols)
  223     elif OptionsInfo["MPLevelMoleculesMode"]:
  224         return ProcessMoleculesUsingMultipleProcessesAtMoleculesLevel(Mols)
  225     else:
  226         MiscUtil.PrintError('The value, %s,  option "--mpLevel" is not supported.' % (OptionsInfo["MPLevel"]))
  227 
  228 
  229 def ProcessMoleculesUsingMultipleProcessesAtMoleculesLevel(Mols):
  230     """Process molecules to perform torsion scan using multiprocessing at molecules level."""
  231 
  232     MolInfoText = "first molecule"
  233     if not OptionsInfo["FirstMolMode"]:
  234         MolInfoText = "all molecules"
  235 
  236     if OptionsInfo["TorsionMinimize"]:
  237         MiscUtil.PrintInfo(
  238             "\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..."
  239             % (MolInfoText)
  240         )
  241     else:
  242         MiscUtil.PrintInfo(
  243             "\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..."
  244             % (MolInfoText)
  245         )
  246 
  247     MPParams = OptionsInfo["MPParams"]
  248 
  249     # Setup data for initializing a worker process...
  250     InitializeWorkerProcessArgs = (
  251         MiscUtil.ObjectToBase64EncodedString(Options),
  252         MiscUtil.ObjectToBase64EncodedString(OptionsInfo),
  253     )
  254 
  255     if OptionsInfo["FirstMolMode"]:
  256         Mol = Mols[0]
  257         Mols = [Mol]
  258 
  259     # Setup a encoded mols data iterable for a worker process...
  260     WorkerProcessDataIterable = RDKitUtil.GenerateBase64EncodedMolStrings(Mols)
  261 
  262     # Setup process pool along with data initialization for each process...
  263     MiscUtil.PrintInfo(
  264         "\nConfiguring multiprocessing using %s method..."
  265         % ("mp.Pool.imap()" if re.match("^Lazy$", MPParams["InputDataMode"], re.I) else "mp.Pool.map()")
  266     )
  267     MiscUtil.PrintInfo(
  268         "NumProcesses: %s; InputDataMode: %s; ChunkSize: %s\n"
  269         % (
  270             MPParams["NumProcesses"],
  271             MPParams["InputDataMode"],
  272             ("automatic" if MPParams["ChunkSize"] is None else MPParams["ChunkSize"]),
  273         )
  274     )
  275 
  276     ProcessPool = mp.Pool(MPParams["NumProcesses"], InitializeWorkerProcess, InitializeWorkerProcessArgs)
  277 
  278     # Start processing...
  279     if re.match("^Lazy$", MPParams["InputDataMode"], re.I):
  280         Results = ProcessPool.imap(WorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"])
  281     elif re.match("^InMemory$", MPParams["InputDataMode"], re.I):
  282         Results = ProcessPool.map(WorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"])
  283     else:
  284         MiscUtil.PrintError(
  285             'The value, %s, specified for "--inputDataMode" is not supported.' % (MPParams["InputDataMode"])
  286         )
  287 
  288     (MolCount, ValidMolCount, TorsionsMissingCount, MinimizationFailedCount, TorsionsScanFailedCount) = [0] * 5
  289 
  290     for Result in Results:
  291         MolCount += 1
  292 
  293         MolIndex, EncodedMol, MinimizationCalcStatus, TorsionsMatchStatus, TorsionsScanCalcStatus = Result
  294 
  295         if EncodedMol is None:
  296             continue
  297         ValidMolCount += 1
  298 
  299         if not MinimizationCalcStatus:
  300             MinimizationFailedCount += 1
  301             continue
  302 
  303         if not TorsionsMatchStatus:
  304             TorsionsMissingCount += 1
  305             continue
  306 
  307         if not TorsionsScanCalcStatus:
  308             TorsionsScanFailedCount += 1
  309             continue
  310 
  311         Mol = RDKitUtil.MolFromBase64EncodedMolString(EncodedMol)
  312 
  313     return (MolCount, ValidMolCount, MinimizationFailedCount, TorsionsMissingCount, TorsionsScanFailedCount)
  314 
  315 
  316 def InitializeWorkerProcess(*EncodedArgs):
  317     """Initialize data for a worker process."""
  318 
  319     global Options, OptionsInfo
  320 
  321     if not OptionsInfo["QuietMode"]:
  322         MiscUtil.PrintInfo("Starting process (PID: %s)..." % os.getpid())
  323 
  324     # Decode Options and OptionInfo...
  325     Options = MiscUtil.ObjectFromBase64EncodedString(EncodedArgs[0])
  326     OptionsInfo = MiscUtil.ObjectFromBase64EncodedString(EncodedArgs[1])
  327 
  328     # Initialize torsion patterns info...
  329     SetupTorsionsPatternsInfo()
  330 
  331     # Psi4 is initialized in the worker process to avoid creation of redundant Psi4
  332     # output files for each process...
  333     OptionsInfo["Psi4Initialized"] = False
  334 
  335 
  336 def WorkerProcess(EncodedMolInfo):
  337     """Process data for a worker process."""
  338 
  339     if not OptionsInfo["Psi4Initialized"]:
  340         InitializePsi4ForWorkerProcess()
  341 
  342     MolIndex, EncodedMol = EncodedMolInfo
  343 
  344     MolNum = MolIndex + 1
  345     (MinimizationCalcStatus, TorsionsMatchStatus, TorsionsScanCalcStatus) = [False] * 3
  346 
  347     if EncodedMol is None:
  348         return [MolIndex, None, MinimizationCalcStatus, TorsionsMatchStatus, TorsionsScanCalcStatus]
  349 
  350     Mol = RDKitUtil.MolFromBase64EncodedMolString(EncodedMol)
  351     if not CheckAndValidateMolecule(Mol, MolNum):
  352         return [MolIndex, None, MinimizationCalcStatus, TorsionsMatchStatus, TorsionsScanCalcStatus]
  353 
  354     # Setup 2D coordinates for SMILES input file...
  355     if OptionsInfo["SMILESInfileStatus"]:
  356         AllChem.Compute2DCoords(Mol)
  357 
  358     Mol, MinimizationCalcStatus, TorsionsMatchStatus, TorsionsScanCalcStatus = PerformMinimizationAndTorsionScan(
  359         Mol, MolNum
  360     )
  361 
  362     return [
  363         MolIndex,
  364         RDKitUtil.MolToBase64EncodedMolString(
  365             Mol, PropertyPickleFlags=Chem.PropertyPickleOptions.MolProps | Chem.PropertyPickleOptions.PrivateProps
  366         ),
  367         MinimizationCalcStatus,
  368         TorsionsMatchStatus,
  369         TorsionsScanCalcStatus,
  370     ]
  371 
  372 
  373 def ProcessMoleculesUsingMultipleProcessesAtTorsionAnglesLevel(Mols):
  374     """Process molecules to perform torsion scan using multiprocessing at torsion angles level."""
  375 
  376     MolInfoText = "first molecule"
  377     if not OptionsInfo["FirstMolMode"]:
  378         MolInfoText = "all molecules"
  379 
  380     if OptionsInfo["TorsionMinimize"]:
  381         MiscUtil.PrintInfo(
  382             "\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..."
  383             % (MolInfoText)
  384         )
  385     else:
  386         MiscUtil.PrintInfo(
  387             "\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..."
  388             % (MolInfoText)
  389         )
  390 
  391     SetupTorsionsPatternsInfo()
  392 
  393     (MolCount, ValidMolCount, TorsionsMissingCount, MinimizationFailedCount, TorsionsScanFailedCount) = [0] * 5
  394 
  395     for Mol in Mols:
  396         MolCount += 1
  397 
  398         if OptionsInfo["FirstMolMode"] and MolCount > 1:
  399             MolCount -= 1
  400             break
  401 
  402         if not CheckAndValidateMolecule(Mol, MolCount):
  403             continue
  404 
  405         # Setup 2D coordinates for SMILES input file...
  406         if OptionsInfo["SMILESInfileStatus"]:
  407             AllChem.Compute2DCoords(Mol)
  408 
  409         ValidMolCount += 1
  410 
  411         Mol, MinimizationCalcStatus, TorsionsMatchStatus, TorsionsScanCalcStatus = PerformMinimizationAndTorsionScan(
  412             Mol, MolCount, UseMultiProcessingAtTorsionAnglesLevel=True
  413         )
  414 
  415         if not MinimizationCalcStatus:
  416             MinimizationFailedCount += 1
  417             continue
  418 
  419         if not TorsionsMatchStatus:
  420             TorsionsMissingCount += 1
  421             continue
  422 
  423         if not TorsionsScanCalcStatus:
  424             TorsionsScanFailedCount += 1
  425             continue
  426 
  427     return (MolCount, ValidMolCount, MinimizationFailedCount, TorsionsMissingCount, TorsionsScanFailedCount)
  428 
  429 
  430 def ScanSingleTorsionInMolUsingMultipleProcessesAtTorsionAnglesLevel(
  431     Mol, TorsionID, TorsionPattern, TorsionPatternMol, TorsionMatches, MolNum
  432 ):
  433     """Perform torsion scan for a molecule using multiple processses at torsion angles
  434     level along with constrained energy minimization.
  435     """
  436 
  437     if OptionsInfo["MPLevelMoleculesMode"]:
  438         MiscUtil.PrintError(
  439             "Single torison scanning for a molecule is not allowed in multiprocessing mode at molecules level.\n"
  440         )
  441 
  442     Mols, Angles = SetupMolsForSingleTorsionScanInMol(Mol, TorsionMatches, MolNum)
  443 
  444     MPParams = OptionsInfo["MPParams"]
  445 
  446     # Setup data for initializing a worker process...
  447 
  448     # Track and avoid encoding TorsionsPatternsInfo as it contains RDKit molecule object...
  449     TorsionsPatternsInfo = OptionsInfo["TorsionsPatternsInfo"]
  450     OptionsInfo["TorsionsPatternsInfo"] = None
  451 
  452     InitializeWorkerProcessArgs = (
  453         MiscUtil.ObjectToBase64EncodedString(Options),
  454         MiscUtil.ObjectToBase64EncodedString(OptionsInfo),
  455     )
  456 
  457     # Restore TorsionsPatternsInfo...
  458     OptionsInfo["TorsionsPatternsInfo"] = TorsionsPatternsInfo
  459 
  460     # Setup a encoded mols data iterable for a worker process...
  461     WorkerProcessDataIterable = GenerateBase64EncodedMolStringsWithTorsionScanInfo(
  462         Mol, (MolNum - 1), Mols, Angles, TorsionID, TorsionPattern, TorsionPatternMol, TorsionMatches
  463     )
  464 
  465     # Setup process pool along with data initialization for each process...
  466     MiscUtil.PrintInfo(
  467         "\nConfiguring multiprocessing using %s method..."
  468         % ("mp.Pool.imap()" if re.match("^Lazy$", MPParams["InputDataMode"], re.I) else "mp.Pool.map()")
  469     )
  470     MiscUtil.PrintInfo(
  471         "NumProcesses: %s; InputDataMode: %s; ChunkSize: %s\n"
  472         % (
  473             MPParams["NumProcesses"],
  474             MPParams["InputDataMode"],
  475             ("automatic" if MPParams["ChunkSize"] is None else MPParams["ChunkSize"]),
  476         )
  477     )
  478 
  479     ProcessPool = mp.Pool(MPParams["NumProcesses"], InitializeTorsionAngleWorkerProcess, InitializeWorkerProcessArgs)
  480 
  481     # Start processing...
  482     if re.match("^Lazy$", MPParams["InputDataMode"], re.I):
  483         Results = ProcessPool.imap(TorsionAngleWorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"])
  484     elif re.match("^InMemory$", MPParams["InputDataMode"], re.I):
  485         Results = ProcessPool.map(TorsionAngleWorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"])
  486     else:
  487         MiscUtil.PrintError(
  488             'The value, %s, specified for "--inputDataMode" is not supported.' % (MPParams["InputDataMode"])
  489         )
  490 
  491     TorsionMols = []
  492     TorsionEnergies = []
  493     TorsionAngles = []
  494 
  495     for Result in Results:
  496         EncodedTorsionMol, CalcStatus, Angle, Energy = Result
  497 
  498         if not CalcStatus:
  499             return (Mol, False, None, None, None)
  500 
  501         if EncodedTorsionMol is None:
  502             return (Mol, False, None, None, None)
  503         TorsionMol = RDKitUtil.MolFromBase64EncodedMolString(EncodedTorsionMol)
  504 
  505         TorsionMols.append(TorsionMol)
  506         TorsionEnergies.append(Energy)
  507         TorsionAngles.append(Angle)
  508 
  509     return (Mol, True, TorsionMols, TorsionEnergies, TorsionAngles)
  510 
  511 
  512 def InitializeTorsionAngleWorkerProcess(*EncodedArgs):
  513     """Initialize data for a worker process."""
  514 
  515     global Options, OptionsInfo
  516 
  517     if not OptionsInfo["QuietMode"]:
  518         MiscUtil.PrintInfo("Starting process (PID: %s)..." % os.getpid())
  519 
  520     # Decode Options and OptionInfo...
  521     Options = MiscUtil.ObjectFromBase64EncodedString(EncodedArgs[0])
  522     OptionsInfo = MiscUtil.ObjectFromBase64EncodedString(EncodedArgs[1])
  523 
  524     # Initialize torsion patterns info...
  525     SetupTorsionsPatternsInfo()
  526 
  527     # Psi4 is initialized in the worker process to avoid creation of redundant Psi4
  528     # output files for each process...
  529     OptionsInfo["Psi4Initialized"] = False
  530 
  531 
  532 def TorsionAngleWorkerProcess(EncodedMolInfo):
  533     """Process data for a worker process."""
  534 
  535     if not OptionsInfo["Psi4Initialized"]:
  536         InitializePsi4ForWorkerProcess()
  537 
  538     (
  539         MolIndex,
  540         EncodedMol,
  541         EncodedTorsionMol,
  542         TorsionAngle,
  543         TorsionID,
  544         TorsionPattern,
  545         EncodedTorsionPatternMol,
  546         TorsionMatches,
  547     ) = EncodedMolInfo
  548 
  549     if EncodedMol is None or EncodedTorsionMol is None or EncodedTorsionPatternMol is None:
  550         return (None, False, None, None)
  551 
  552     Mol = RDKitUtil.MolFromBase64EncodedMolString(EncodedMol)
  553     TorsionMol = RDKitUtil.MolFromBase64EncodedMolString(EncodedTorsionMol)
  554     TorsionPatternMol = RDKitUtil.MolFromBase64EncodedMolString(EncodedTorsionPatternMol)
  555 
  556     TorsionMol, CalcStatus, Energy = MinimizeCalculateEnergyForTorsionMol(
  557         Mol, TorsionMol, TorsionAngle, TorsionID, TorsionPattern, TorsionPatternMol, TorsionMatches, (MolIndex + 1)
  558     )
  559 
  560     return (
  561         RDKitUtil.MolToBase64EncodedMolString(
  562             TorsionMol,
  563             PropertyPickleFlags=Chem.PropertyPickleOptions.MolProps | Chem.PropertyPickleOptions.PrivateProps,
  564         ),
  565         CalcStatus,
  566         TorsionAngle,
  567         Energy,
  568     )
  569 
  570 
  571 def GenerateBase64EncodedMolStringsWithTorsionScanInfo(
  572     Mol,
  573     MolIndex,
  574     TorsionMols,
  575     TorsionAngles,
  576     TorsionID,
  577     TorsionPattern,
  578     TorsionPatternMol,
  579     TorsionMatches,
  580     PropertyPickleFlags=Chem.PropertyPickleOptions.AllProps,
  581 ):
  582     """Set up an iterator for generating base64 encoded molecule string for
  583     a torsion in a molecule along with appropriate trosion scan information.
  584     """
  585 
  586     for Index, TorsionMol in enumerate(TorsionMols):
  587         yield (
  588             [MolIndex, None, None, TorsionAngles[Index], TorsionID, TorsionPattern, None, TorsionMatches]
  589             if (Mol is None or TorsionMol is None)
  590             else [
  591                 MolIndex,
  592                 RDKitUtil.MolToBase64EncodedMolString(Mol, PropertyPickleFlags),
  593                 RDKitUtil.MolToBase64EncodedMolString(TorsionMol, PropertyPickleFlags),
  594                 TorsionAngles[Index],
  595                 TorsionID,
  596                 TorsionPattern,
  597                 RDKitUtil.MolToBase64EncodedMolString(TorsionPatternMol, PropertyPickleFlags),
  598                 TorsionMatches,
  599             ]
  600         )
  601 
  602 
  603 def InitializePsi4ForWorkerProcess():
  604     """Initialize Psi4 for a worker process."""
  605 
  606     if OptionsInfo["Psi4Initialized"]:
  607         return
  608 
  609     OptionsInfo["Psi4Initialized"] = True
  610 
  611     if OptionsInfo["MPLevelTorsionAnglesMode"] and re.match(
  612         "auto", OptionsInfo["Psi4RunParams"]["OutputFileSpecified"], re.I
  613     ):
  614         # Run Psi4 in quiet mode during multiprocessing at Torsions level for 'auto' OutputFile...
  615         OptionsInfo["Psi4RunParams"]["OutputFile"] = "quiet"
  616     else:
  617         # Update output file...
  618         OptionsInfo["Psi4RunParams"]["OutputFile"] = Psi4Util.UpdatePsi4OutputFileUsingPID(
  619             OptionsInfo["Psi4RunParams"]["OutputFile"], os.getpid()
  620         )
  621 
  622     # Intialize Psi4...
  623     OptionsInfo["psi4"] = Psi4Util.InitializePsi4(
  624         Psi4RunParams=OptionsInfo["Psi4RunParams"],
  625         Psi4OptionsParams=OptionsInfo["Psi4OptionsParams"],
  626         PrintVersion=False,
  627         PrintHeader=True,
  628     )
  629 
  630     # Setup max iterations global variable...
  631     Psi4Util.UpdatePsi4OptionsParameters(OptionsInfo["psi4"], {"GEOM_MAXITER": OptionsInfo["MaxIters"]})
  632 
  633     # Setup conversion factor for energy units...
  634     SetupEnergyConversionFactor(OptionsInfo["psi4"])
  635 
  636 
  637 def PerformMinimizationAndTorsionScan(Mol, MolNum, UseMultiProcessingAtTorsionAnglesLevel=False):
  638     """Perform minimization and torsions scan."""
  639 
  640     if not OptionsInfo["Infile3D"]:
  641         # Add hydrogens...
  642         Mol = Chem.AddHs(Mol, addCoords=True)
  643 
  644         Mol, MinimizationCalcStatus = MinimizeMolecule(Mol, MolNum)
  645         if not MinimizationCalcStatus:
  646             return (Mol, False, False, False)
  647 
  648     TorsionsMolInfo = SetupTorsionsMolInfo(Mol, MolNum)
  649     if TorsionsMolInfo["NumOfMatches"] == 0:
  650         return (Mol, True, False, False)
  651 
  652     Mol, ScanCalcStatus = ScanAllTorsionsInMol(Mol, TorsionsMolInfo, MolNum, UseMultiProcessingAtTorsionAnglesLevel)
  653     if not ScanCalcStatus:
  654         return (Mol, True, True, False)
  655 
  656     return (Mol, True, True, True)
  657 
  658 
  659 def ScanAllTorsionsInMol(Mol, TorsionsMolInfo, MolNum, UseMultiProcessingAtTorsionAnglesLevel=False):
  660     """Perform scans on all torsions in a molecule."""
  661 
  662     if TorsionsMolInfo["NumOfMatches"] == 0:
  663         return Mol, True
  664 
  665     MolName = RDKitUtil.GetMolName(Mol, MolNum)
  666 
  667     FirstTorsionMode = OptionsInfo["FirstTorsionMode"]
  668     TorsionsPatternsInfo = OptionsInfo["TorsionsPatternsInfo"]
  669 
  670     TorsionPatternCount, TorsionScanCount, TorsionMatchCount = [0] * 3
  671     TorsionMaxMatches = OptionsInfo["TorsionMaxMatches"]
  672 
  673     for TorsionID in TorsionsPatternsInfo["IDs"]:
  674         TorsionPatternCount += 1
  675         TorsionPattern = TorsionsPatternsInfo["Pattern"][TorsionID]
  676         TorsionPatternMol = TorsionsPatternsInfo["Mol"][TorsionID]
  677 
  678         TorsionsMatches = TorsionsMolInfo["Matches"][TorsionID]
  679 
  680         if TorsionsMatches is None:
  681             continue
  682 
  683         if FirstTorsionMode and TorsionPatternCount > 1:
  684             if not OptionsInfo["QuietMode"]:
  685                 MiscUtil.PrintWarning(
  686                     'Already scaned first torsion pattern, "%s" for molecule %s during "%s" value of "--modeTorsions" option . Abandoning torsion scan...\n'
  687                     % (TorsionPattern, MolName, OptionsInfo["ModeTorsions"])
  688                 )
  689             break
  690 
  691         for Index, TorsionMatches in enumerate(TorsionsMatches):
  692             TorsionMatchNum = Index + 1
  693             TorsionMatchCount += 1
  694 
  695             if TorsionMatchCount > TorsionMaxMatches:
  696                 if not OptionsInfo["QuietMode"]:
  697                     MiscUtil.PrintWarning(
  698                         'Already scaned a maximum of %s torsion matches for molecule %s specified by "--torsionMaxMatches" option. Abandoning torsion scan...\n'
  699                         % (TorsionMaxMatches, MolName)
  700                     )
  701                 break
  702 
  703             TmpMol, TorsionScanStatus, TorsionMols, TorsionEnergies, TorsionAngles = ScanSingleTorsionInMol(
  704                 Mol,
  705                 TorsionID,
  706                 TorsionPattern,
  707                 TorsionPatternMol,
  708                 TorsionMatches,
  709                 TorsionMatchNum,
  710                 MolNum,
  711                 UseMultiProcessingAtTorsionAnglesLevel,
  712             )
  713             if not TorsionScanStatus:
  714                 continue
  715 
  716             TorsionScanCount += 1
  717             GenerateOutputFiles(Mol, MolNum, TorsionID, TorsionMatchNum, TorsionMols, TorsionEnergies, TorsionAngles)
  718 
  719         if TorsionMatchCount > TorsionMaxMatches:
  720             break
  721 
  722     if TorsionScanCount:
  723         GenerateStartingTorsionScanStructureOutfile(Mol, MolNum)
  724 
  725     Status = True if TorsionScanCount else False
  726 
  727     return (Mol, Status)
  728 
  729 
  730 def ScanSingleTorsionInMol(
  731     Mol,
  732     TorsionID,
  733     TorsionPattern,
  734     TorsionPatternMol,
  735     TorsionMatches,
  736     TorsionMatchNum,
  737     MolNum,
  738     UseMultiProcessingAtTorsionAnglesLevel,
  739 ):
  740     """Perform torsion scan for a molecule along with constrained energy minimization."""
  741 
  742     if not OptionsInfo["QuietMode"]:
  743         MiscUtil.PrintInfo(
  744             "\nProcessing torsion pattern, %s, match number, %s, in molecule %s..."
  745             % (TorsionPattern, TorsionMatchNum, RDKitUtil.GetMolName(Mol, MolNum))
  746         )
  747 
  748         if OptionsInfo["TorsionMinimize"]:
  749             MiscUtil.PrintInfo(
  750                 "Generating initial ensemble of constrained conformations by distance geometry and forcefield followed by Psi4 constraned minimization to select the lowest energy structure at specific torsion angles for molecule %s..."
  751                 % (RDKitUtil.GetMolName(Mol, MolNum))
  752             )
  753         else:
  754             MiscUtil.PrintInfo(
  755                 "Calculating single point energy using Psi4 for molecule, %s, at specific torsion angles..."
  756                 % (RDKitUtil.GetMolName(Mol, MolNum))
  757             )
  758 
  759     if UseMultiProcessingAtTorsionAnglesLevel:
  760         return ScanSingleTorsionInMolUsingMultipleProcessesAtTorsionAnglesLevel(
  761             Mol, TorsionID, TorsionPattern, TorsionPatternMol, TorsionMatches, MolNum
  762         )
  763     else:
  764         return ScanSingleTorsionInMolUsingSingleProcess(
  765             Mol, TorsionID, TorsionPattern, TorsionPatternMol, TorsionMatches, MolNum
  766         )
  767 
  768 
  769 def ScanSingleTorsionInMolUsingSingleProcess(Mol, TorsionID, TorsionPattern, TorsionPatternMol, TorsionMatches, MolNum):
  770     """Perform torsion scan for a molecule using single processs along with constrained
  771     energy minimization."""
  772 
  773     TorsionMols = []
  774     TorsionEnergies = []
  775     TorsionAngles = []
  776 
  777     Mols, Angles = SetupMolsForSingleTorsionScanInMol(Mol, TorsionMatches, MolNum)
  778 
  779     for Index, Angle in enumerate(Angles):
  780         TorsionMol = Mols[Index]
  781         TorsionMol, CalcStatus, Energy = MinimizeCalculateEnergyForTorsionMol(
  782             Mol, TorsionMol, Angle, TorsionID, TorsionPattern, TorsionPatternMol, TorsionMatches, MolNum
  783         )
  784 
  785         if not CalcStatus:
  786             return (Mol, False, None, None, None)
  787 
  788         TorsionMols.append(TorsionMol)
  789         TorsionEnergies.append(Energy)
  790         TorsionAngles.append(Angle)
  791 
  792     return (Mol, True, TorsionMols, TorsionEnergies, TorsionAngles)
  793 
  794 
  795 def SetupMolsForSingleTorsionScanInMol(Mol, TorsionMatches, MolNum=None):
  796     """Setup molecules corresponding to all torsion angles in a molecule."""
  797 
  798     AtomIndex1, AtomIndex2, AtomIndex3, AtomIndex4 = TorsionMatches
  799 
  800     TorsionMols = []
  801     TorsionAngles = OptionsInfo["TorsionAngles"]
  802 
  803     for Angle in TorsionAngles:
  804         TorsionMol = Chem.Mol(Mol)
  805         TorsionMolConf = TorsionMol.GetConformer(0)
  806 
  807         rdMolTransforms.SetDihedralDeg(TorsionMolConf, AtomIndex1, AtomIndex2, AtomIndex3, AtomIndex4, Angle)
  808         TorsionMols.append(TorsionMol)
  809 
  810     return (TorsionMols, TorsionAngles)
  811 
  812 
  813 def MinimizeCalculateEnergyForTorsionMol(
  814     Mol, TorsionMol, TorsionAngle, TorsionID, TorsionPattern, TorsionPatternMol, TorsionMatches, MolNum
  815 ):
  816     """ "Calculate energy of a torsion molecule by performing an optional constrained
  817     energy minimzation.
  818     """
  819 
  820     if OptionsInfo["TorsionMinimize"]:
  821         if not OptionsInfo["QuietMode"]:
  822             MolName = RDKitUtil.GetMolName(Mol, MolNum)
  823             MiscUtil.PrintInfo("\nProcessing torsion angle %s for molecule %s..." % (TorsionAngle, MolName))
  824 
  825         # Perform constrained minimization...
  826         TorsionMatchesMol = RDKitUtil.MolFromSubstructureMatch(TorsionMol, TorsionPatternMol, TorsionMatches)
  827         TorsionMol, CalcStatus, Energy = ConstrainAndMinimizeMolecule(
  828             TorsionMol, TorsionAngle, TorsionMatchesMol, TorsionMatches, MolNum
  829         )
  830 
  831         if not CalcStatus:
  832             if not OptionsInfo["QuietMode"]:
  833                 MolName = RDKitUtil.GetMolName(Mol, MolNum)
  834                 MiscUtil.PrintWarning(
  835                     "Failed to perform constrained minimization for molecule %s with torsion angle set to %s during torsion scan for torsion pattern %s. Abandoning torsion scan..."
  836                     % (MolName, TorsionAngle, TorsionPattern)
  837                 )
  838             return (TorsionMol, False, None)
  839     else:
  840         # Setup a Psi4 molecule...
  841         Psi4Mol = SetupPsi4Mol(OptionsInfo["psi4"], TorsionMol, MolNum)
  842         if Psi4Mol is None:
  843             return (TorsionMol, False, None)
  844 
  845         # Calculate single point Psi4 energy...
  846         CalcStatus, Energy = CalculateEnergyUsingPsi4(OptionsInfo["psi4"], Psi4Mol, TorsionMol, MolNum)
  847         if not CalcStatus:
  848             if not OptionsInfo["QuietMode"]:
  849                 MolName = RDKitUtil.GetMolName(Mol, MolNum)
  850                 MiscUtil.PrintWarning(
  851                     "Failed to calculate Psi4 energy for molecule %s with torsion angle set to %s during torsion scan for torsion pattern %s. Abandoning torsion scan..."
  852                     % (MolName, TorsionAngle, TorsionPattern)
  853                 )
  854             return (TorsionMol, False, None)
  855 
  856     return (TorsionMol, CalcStatus, Energy)
  857 
  858 
  859 def SetupTorsionsMolInfo(Mol, MolNum=None):
  860     """Setup torsions info for a molecule."""
  861 
  862     TorsionsPatternsInfo = OptionsInfo["TorsionsPatternsInfo"]
  863 
  864     # Initialize...
  865     TorsionsMolInfo = {}
  866     TorsionsMolInfo["IDs"] = []
  867     TorsionsMolInfo["NumOfMatches"] = 0
  868     TorsionsMolInfo["Matches"] = {}
  869     for TorsionID in TorsionsPatternsInfo["IDs"]:
  870         TorsionsMolInfo["IDs"].append(TorsionID)
  871         TorsionsMolInfo["Matches"][TorsionID] = None
  872 
  873     MolName = RDKitUtil.GetMolName(Mol, MolNum)
  874     UseChirality = OptionsInfo["UseChirality"]
  875 
  876     for TorsionID in TorsionsPatternsInfo["IDs"]:
  877         # Match torsions..
  878         TorsionPattern = TorsionsPatternsInfo["Pattern"][TorsionID]
  879         TorsionPatternMol = TorsionsPatternsInfo["Mol"][TorsionID]
  880         TorsionsMatches = RDKitUtil.FilterSubstructureMatchesByAtomMapNumbers(
  881             Mol, TorsionPatternMol, Mol.GetSubstructMatches(TorsionPatternMol, useChirality=UseChirality)
  882         )
  883 
  884         # Validate tosion matches...
  885         ValidTorsionsMatches = []
  886         for Index, TorsionMatch in enumerate(TorsionsMatches):
  887             if len(TorsionMatch) != 4:
  888                 if not OptionsInfo["QuietMode"]:
  889                     MiscUtil.PrintWarning(
  890                         "Ignoring invalid torsion match to atom indices, %s, for torsion pattern, %s, in molecule %s: It must match exactly 4 atoms."
  891                         % (TorsionMatch, TorsionPattern, MolName)
  892                     )
  893                 continue
  894 
  895             if not RDKitUtil.AreAtomIndicesSequentiallyConnected(Mol, TorsionMatch):
  896                 if not OptionsInfo["QuietMode"]:
  897                     MiscUtil.PrintInfo("")
  898                     MiscUtil.PrintWarning(
  899                         "Invalid torsion match to atom indices, %s, for torsion pattern, %s, in molecule %s: Matched atom indices must be sequentially connected."
  900                         % (TorsionMatch, TorsionPattern, MolName)
  901                     )
  902                     MiscUtil.PrintWarning("Reordering matched atom indices in a sequentially connected manner...")
  903 
  904                 Status, ReorderdTorsionMatch = RDKitUtil.ReorderAtomIndicesInSequentiallyConnectedManner(
  905                     Mol, TorsionMatch
  906                 )
  907                 if Status:
  908                     TorsionMatch = ReorderdTorsionMatch
  909                     if not OptionsInfo["QuietMode"]:
  910                         MiscUtil.PrintWarning(
  911                             "Successfully reordered torsion match to atom indices, %s, for torsion pattern, %s, in molecule %s: Matched atom indices are now sequentially connected."
  912                             % (TorsionMatch, TorsionPattern, MolName)
  913                         )
  914                 else:
  915                     if not OptionsInfo["QuietMode"]:
  916                         MiscUtil.PrintWarning(
  917                             "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."
  918                             % (TorsionMatch, TorsionPattern, MolName)
  919                         )
  920                     continue
  921 
  922             Bond = Mol.GetBondBetweenAtoms(TorsionMatch[1], TorsionMatch[2])
  923             if Bond.IsInRing():
  924                 if not OptionsInfo["QuietMode"]:
  925                     MiscUtil.PrintWarning(
  926                         "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."
  927                         % (TorsionMatch, TorsionPattern, MolName, TorsionMatch[1], TorsionMatch[2])
  928                     )
  929                 continue
  930 
  931             # Filter matched torsions...
  932             if OptionsInfo["FilterTorsionsByAtomIndicesMode"]:
  933                 InvalidAtomIndices = []
  934                 for AtomIndex in TorsionMatch:
  935                     if AtomIndex not in OptionsInfo["TorsionsFilterByAtomIndicesList"]:
  936                         InvalidAtomIndices.append(AtomIndex)
  937                 if len(InvalidAtomIndices):
  938                     if not OptionsInfo["QuietMode"]:
  939                         MiscUtil.PrintWarning(
  940                             '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".'
  941                             % (
  942                                 TorsionMatch,
  943                                 TorsionPattern,
  944                                 MolName,
  945                                 InvalidAtomIndices,
  946                                 OptionsInfo["TorsionsFilterByAtomIndicesList"],
  947                             )
  948                         )
  949                     continue
  950 
  951             ValidTorsionsMatches.append(TorsionMatch)
  952 
  953         # Track valid matches...
  954         if len(ValidTorsionsMatches):
  955             TorsionsMolInfo["NumOfMatches"] += len(ValidTorsionsMatches)
  956             TorsionsMolInfo["Matches"][TorsionID] = ValidTorsionsMatches
  957 
  958     if TorsionsMolInfo["NumOfMatches"] == 0:
  959         if not OptionsInfo["QuietMode"]:
  960             MiscUtil.PrintWarning("Failed to match any torsions  in molecule %s" % (MolName))
  961 
  962     return TorsionsMolInfo
  963 
  964 
  965 def SetupTorsionsPatternsInfo():
  966     """Setup torsions patterns info."""
  967 
  968     TorsionsPatternsInfo = {}
  969     TorsionsPatternsInfo["IDs"] = []
  970     TorsionsPatternsInfo["Pattern"] = {}
  971     TorsionsPatternsInfo["Mol"] = {}
  972 
  973     TorsionID = 0
  974     for TorsionPattern in OptionsInfo["TorsionPatternsList"]:
  975         TorsionID += 1
  976 
  977         TorsionMol = Chem.MolFromSmarts(TorsionPattern)
  978         if TorsionMol is None:
  979             MiscUtil.PrintError(
  980                 'Failed to create torsion pattern molecule. The torsion SMILES/SMARTS pattern, "%s", specified using "-t, --torsions" option is not valid.'
  981                 % (TorsionPattern)
  982             )
  983 
  984         TorsionsPatternsInfo["IDs"].append(TorsionID)
  985         TorsionsPatternsInfo["Pattern"][TorsionID] = TorsionPattern
  986         TorsionsPatternsInfo["Mol"][TorsionID] = TorsionMol
  987 
  988     OptionsInfo["TorsionsPatternsInfo"] = TorsionsPatternsInfo
  989 
  990 
  991 def MinimizeMolecule(Mol, MolNum=None):
  992     """Minimize molecule."""
  993 
  994     return GenerateAndMinimizeConformersUsingForceField(Mol, MolNum)
  995 
  996 
  997 def GenerateAndMinimizeConformersUsingForceField(Mol, MolNum=None):
  998     """Generate and minimize conformers for a molecule to get the lowest energy conformer
  999     as the minimized structure."""
 1000 
 1001     MolName = RDKitUtil.GetMolName(Mol, MolNum)
 1002 
 1003     # Setup conformers...
 1004     ConfIDs = EmbedMolecule(Mol, MolNum)
 1005     if not len(ConfIDs):
 1006         if not OptionsInfo["QuietMode"]:
 1007             MiscUtil.PrintWarning("Minimization couldn't be performed for molecule %s: Embedding failed...\n" % MolName)
 1008         return (Mol, False)
 1009 
 1010     if not OptionsInfo["QuietMode"]:
 1011         MiscUtil.PrintInfo(
 1012             "Performing initial minimization of molecule, %s, using forcefield by generating a conformation ensemble and selecting the lowest energy conformer - EmbedRMSDCutoff: %s; Size: %s; Size after RMSD filtering: %s"
 1013             % (
 1014                 MolName,
 1015                 OptionsInfo["ConfGenerationParams"]["EmbedRMSDCutoff"],
 1016                 OptionsInfo["ConfGenerationParams"]["MaxConfs"],
 1017                 len(ConfIDs),
 1018             )
 1019         )
 1020 
 1021     # Minimize conformers...
 1022     CalcEnergyMap = {}
 1023     for ConfID in ConfIDs:
 1024         # Perform forcefield minimization...
 1025         Status, ConvergeStatus = MinimizeMoleculeUsingForceField(Mol, MolNum, ConfID)
 1026         if not Status:
 1027             return (Mol, False)
 1028 
 1029         EnergyStatus, Energy = CalculateEnergyUsingForceField(Mol, ConfID)
 1030         if not EnergyStatus:
 1031             if not OptionsInfo["QuietMode"]:
 1032                 MolName = RDKitUtil.GetMolName(Mol, MolNum)
 1033                 MiscUtil.PrintWarning(
 1034                     "Failed to retrieve calculated energy for conformation number %d of molecule %s. Try again after removing any salts or cleaing up the molecule...\n"
 1035                     % (ConfID, MolName)
 1036                 )
 1037             return (Mol, False)
 1038 
 1039         if ConvergeStatus != 0:
 1040             if not OptionsInfo["QuietMode"]:
 1041                 MiscUtil.PrintWarning(
 1042                     'Minimization using forcefield failed to converge for molecule %s in %d steps. Try using higher value for "maxIters" in "--confParams" option...\n'
 1043                     % (MolName, OptionsInfo["ConfGenerationParams"]["MaxIters"])
 1044                 )
 1045 
 1046         CalcEnergyMap[ConfID] = Energy
 1047 
 1048     SortedConfIDs = sorted(ConfIDs, key=lambda ConfID: CalcEnergyMap[ConfID])
 1049     MinEnergyConfID = SortedConfIDs[0]
 1050 
 1051     for ConfID in [Conf.GetId() for Conf in Mol.GetConformers()]:
 1052         if ConfID == MinEnergyConfID:
 1053             continue
 1054         Mol.RemoveConformer(ConfID)
 1055 
 1056     # Set ConfID to 0 for MinEnergyConf...
 1057     Mol.GetConformer(MinEnergyConfID).SetId(0)
 1058 
 1059     return (Mol, True)
 1060 
 1061 
 1062 def ConstrainAndMinimizeMolecule(Mol, TorsionAngle, RefMolCore, RefMolMatches, MolNum=None):
 1063     """Constrain and minimize molecule."""
 1064 
 1065     # TorsionMol, CalcStatus, Energy
 1066     MolName = RDKitUtil.GetMolName(Mol, MolNum)
 1067 
 1068     # Setup constrained conformers...
 1069     MolConfs, MolConfsStatus = ConstrainEmbedAndMinimizeMoleculeUsingRDKit(Mol, RefMolCore, RefMolMatches, MolNum)
 1070     if not MolConfsStatus:
 1071         if not OptionsInfo["QuietMode"]:
 1072             MiscUtil.PrintWarning(
 1073                 "Conformation generation couldn't be performed for molecule %s: Constrained embedding failed...\n"
 1074                 % MolName
 1075             )
 1076         return (Mol, False, None)
 1077 
 1078     # Minimize conformers...
 1079     ConfNums = []
 1080     CalcEnergyMap = {}
 1081     MolConfsMap = {}
 1082 
 1083     for ConfNum, MolConf in enumerate(MolConfs):
 1084         if not OptionsInfo["QuietMode"]:
 1085             MiscUtil.PrintInfo(
 1086                 "\nPerforming constrained minimization using Psi4 for molecule, %s, conformer number, %s, at torsion angle %s..."
 1087                 % (MolName, ConfNum, TorsionAngle)
 1088             )
 1089 
 1090         CalcStatus, Energy = ConstrainAndMinimizeMoleculeUsingPsi4(
 1091             OptionsInfo["psi4"], MolConf, RefMolCore, RefMolMatches, MolNum
 1092         )
 1093         if not CalcStatus:
 1094             if not OptionsInfo["QuietMode"]:
 1095                 MiscUtil.PrintWarning("Minimization couldn't be performed for molecule %s\n" % (MolName))
 1096             return (Mol, False, None)
 1097 
 1098         ConfNums.append(ConfNum)
 1099         CalcEnergyMap[ConfNum] = Energy
 1100         MolConfsMap[ConfNum] = MolConf
 1101 
 1102     SortedConfNums = sorted(ConfNums, key=lambda ConfNum: CalcEnergyMap[ConfNum])
 1103     MinEnergyConfNum = SortedConfNums[0]
 1104 
 1105     MinEnergy = CalcEnergyMap[MinEnergyConfNum]
 1106     MinEnergyMolConf = MolConfsMap[MinEnergyConfNum]
 1107 
 1108     MinEnergyMolConf.ClearProp("EmbedRMS")
 1109 
 1110     return (MinEnergyMolConf, True, MinEnergy)
 1111 
 1112 
 1113 def ConstrainAndMinimizeMoleculeUsingPsi4(Psi4Handle, Mol, RefMolCore, RefMolMatches, MolNum, ConfID=-1):
 1114     """Minimize molecule using Psi4."""
 1115 
 1116     # Setup a list for constrained atoms...
 1117     ConstrainedAtomIndices = SetupConstrainedAtomIndicesForPsi4(Mol, RefMolCore, RefMolMatches)
 1118     if len(ConstrainedAtomIndices) == 0:
 1119         return (False, None)
 1120 
 1121     # Setup a Psi4Mol...
 1122     Psi4Mol = SetupPsi4Mol(Psi4Handle, Mol, MolNum, ConfID)
 1123     if Psi4Mol is None:
 1124         return (False, None)
 1125 
 1126     #  Setup reference wave function...
 1127     Reference = SetupReferenceWavefunction(Mol)
 1128     Psi4Handle.set_options({"Reference": Reference})
 1129 
 1130     # Setup method name and basis set...
 1131     MethodName, BasisSet = SetupMethodNameAndBasisSet(Mol)
 1132 
 1133     # Setup freeze list for constrained torsion...
 1134     FreezeDihedralList = [("%s" % AtomIdex) for AtomIdex in ConstrainedAtomIndices]
 1135     FreezeDihedralString = "%s" % " ".join(FreezeDihedralList)
 1136     Psi4Handle.set_options({"OPTKING__frozen_dihedral": FreezeDihedralString})
 1137 
 1138     # Optimize geometry...
 1139     Status, Energy, WaveFunction = Psi4Util.PerformGeometryOptimization(
 1140         Psi4Handle, Psi4Mol, MethodName, BasisSet, ReturnWaveFunction=True, Quiet=OptionsInfo["QuietMode"]
 1141     )
 1142 
 1143     if not Status:
 1144         PerformPsi4Cleanup(Psi4Handle)
 1145         return (False, None)
 1146 
 1147     # Update atom positions...
 1148     AtomPositions = Psi4Util.GetAtomPositions(Psi4Handle, WaveFunction, InAngstroms=True)
 1149     RDKitUtil.SetAtomPositions(Mol, AtomPositions, ConfID=ConfID)
 1150 
 1151     # Convert energy units...
 1152     if OptionsInfo["ApplyEnergyConversionFactor"]:
 1153         Energy = Energy * OptionsInfo["EnergyConversionFactor"]
 1154 
 1155     # Clean up
 1156     PerformPsi4Cleanup(Psi4Handle)
 1157 
 1158     return (True, Energy)
 1159 
 1160 
 1161 def ConstrainEmbedAndMinimizeMoleculeUsingRDKit(Mol, RefMolCore, RefMolMatches, MolNum=None):
 1162     """Constrain, embed, and minimize molecule."""
 1163 
 1164     # Setup forcefield function to use for constrained minimization...
 1165     ForceFieldFunction = None
 1166     ForceFieldName = None
 1167     if OptionsInfo["ConfGenerationParams"]["UseUFF"]:
 1168         ForceFieldFunction = lambda mol, confId=-1: AllChem.UFFGetMoleculeForceField(mol, confId=confId)
 1169         ForceFieldName = "UFF"
 1170     else:
 1171         ForceFieldFunction = lambda mol, confId=-1: AllChem.MMFFGetMoleculeForceField(
 1172             mol,
 1173             AllChem.MMFFGetMoleculeProperties(
 1174                 mol, mmffVariant=OptionsInfo["ConfGenerationParams"]["ForceFieldMMFFVariant"]
 1175             ),
 1176             confId=confId,
 1177         )
 1178         ForceFieldName = "MMFF"
 1179 
 1180     if ForceFieldFunction is None:
 1181         if not OptionsInfo["QuietMode"]:
 1182             MiscUtil.PrintWarning(
 1183                 "Failed to setup forcefield %s for molecule: %s\n" % (ForceFieldName, RDKitUtil.GetMolName(Mol, MolNum))
 1184             )
 1185         return (None, False)
 1186 
 1187     MaxConfs = OptionsInfo["ConfGenerationParams"]["MaxConfsTorsions"]
 1188     EnforceChirality = OptionsInfo["ConfGenerationParams"]["EnforceChirality"]
 1189     UseExpTorsionAnglePrefs = OptionsInfo["ConfGenerationParams"]["UseExpTorsionAnglePrefs"]
 1190     ETVersion = OptionsInfo["ConfGenerationParams"]["ETVersion"]
 1191     UseBasicKnowledge = OptionsInfo["ConfGenerationParams"]["UseBasicKnowledge"]
 1192     UseTethers = OptionsInfo["ConfGenerationParams"]["UseTethers"]
 1193 
 1194     MolConfs = []
 1195     ConfIDs = [ConfID for ConfID in range(0, MaxConfs)]
 1196 
 1197     for ConfID in ConfIDs:
 1198         try:
 1199             MolConf = Chem.Mol(Mol)
 1200             RDKitUtil.ConstrainAndEmbed(
 1201                 MolConf,
 1202                 RefMolCore,
 1203                 coreMatchesMol=RefMolMatches,
 1204                 useTethers=UseTethers,
 1205                 coreConfId=-1,
 1206                 randomseed=ConfID,
 1207                 getForceField=ForceFieldFunction,
 1208                 enforceChirality=EnforceChirality,
 1209                 useExpTorsionAnglePrefs=UseExpTorsionAnglePrefs,
 1210                 useBasicKnowledge=UseBasicKnowledge,
 1211                 ETversion=ETVersion,
 1212             )
 1213         except (ValueError, RuntimeError, Chem.rdchem.KekulizeException) as ErrMsg:
 1214             if not OptionsInfo["QuietMode"]:
 1215                 MiscUtil.PrintWarning(
 1216                     "Constrained embedding couldn't  be performed for molecule %s:\n%s\n"
 1217                     % (RDKitUtil.GetMolName(Mol, MolNum), ErrMsg)
 1218                 )
 1219             return (None, False)
 1220 
 1221         MolConfs.append(MolConf)
 1222 
 1223     return FilterConstrainedConformationsByRMSD(Mol, MolConfs, MolNum)
 1224 
 1225 
 1226 def FilterConstrainedConformationsByRMSD(Mol, MolConfs, MolNum=None):
 1227     """Filter contarained conformations by RMSD."""
 1228 
 1229     EmbedRMSDCutoff = OptionsInfo["ConfGenerationParams"]["EmbedRMSDCutoff"]
 1230     if EmbedRMSDCutoff is None or EmbedRMSDCutoff <= 0:
 1231         if not OptionsInfo["QuietMode"]:
 1232             MiscUtil.PrintInfo(
 1233                 "\nGenerating initial ensemble of  constrained conformations by distance geometry  and forcefield for %s - EmbedRMSDCutoff: None; Size: %s"
 1234                 % (RDKitUtil.GetMolName(Mol, MolNum), len(MolConfs))
 1235             )
 1236         return (MolConfs, True)
 1237 
 1238     FirstMolConf = True
 1239     SelectedMolConfs = []
 1240     for MolConfIndex, MolConf in enumerate(MolConfs):
 1241         if FirstMolConf:
 1242             FirstMolConf = False
 1243             SelectedMolConfs.append(MolConf)
 1244             continue
 1245 
 1246         # Compare RMSD against all selected conformers...
 1247         ProbeMolConf = Chem.RemoveHs(Chem.Mol(MolConf))
 1248         IgnoreConf = False
 1249         for SelectedMolConfIndex, SelectedMolConf in enumerate(SelectedMolConfs):
 1250             RefMolConf = Chem.RemoveHs(Chem.Mol(SelectedMolConf))
 1251             CalcRMSD = rdMolAlign.AlignMol(ProbeMolConf, RefMolConf)
 1252 
 1253             if CalcRMSD < EmbedRMSDCutoff:
 1254                 IgnoreConf = True
 1255                 break
 1256 
 1257         if IgnoreConf:
 1258             continue
 1259 
 1260         SelectedMolConfs.append(MolConf)
 1261 
 1262     if not OptionsInfo["QuietMode"]:
 1263         MiscUtil.PrintInfo(
 1264             "\nGenerating initial ensemble of constrained conformations by distance geometry and forcefield for %s - EmbedRMSDCutoff: %s; Size: %s; Size after RMSD filtering: %s"
 1265             % (RDKitUtil.GetMolName(Mol, MolNum), EmbedRMSDCutoff, len(MolConfs), len(SelectedMolConfs))
 1266         )
 1267 
 1268     return (SelectedMolConfs, True)
 1269 
 1270 
 1271 def EmbedMolecule(Mol, MolNum=None):
 1272     """Embed conformations."""
 1273 
 1274     ConfIDs = []
 1275 
 1276     MaxConfs = OptionsInfo["ConfGenerationParams"]["MaxConfs"]
 1277     RandomSeed = OptionsInfo["ConfGenerationParams"]["RandomSeed"]
 1278     EnforceChirality = OptionsInfo["ConfGenerationParams"]["EnforceChirality"]
 1279     UseExpTorsionAnglePrefs = OptionsInfo["ConfGenerationParams"]["UseExpTorsionAnglePrefs"]
 1280     ETVersion = OptionsInfo["ConfGenerationParams"]["ETVersion"]
 1281     UseBasicKnowledge = OptionsInfo["ConfGenerationParams"]["UseBasicKnowledge"]
 1282     EmbedRMSDCutoff = OptionsInfo["ConfGenerationParams"]["EmbedRMSDCutoff"]
 1283 
 1284     try:
 1285         ConfIDs = AllChem.EmbedMultipleConfs(
 1286             Mol,
 1287             numConfs=MaxConfs,
 1288             randomSeed=RandomSeed,
 1289             pruneRmsThresh=EmbedRMSDCutoff,
 1290             enforceChirality=EnforceChirality,
 1291             useExpTorsionAnglePrefs=UseExpTorsionAnglePrefs,
 1292             useBasicKnowledge=UseBasicKnowledge,
 1293             ETversion=ETVersion,
 1294         )
 1295     except ValueError as ErrMsg:
 1296         if not OptionsInfo["QuietMode"]:
 1297             MolName = RDKitUtil.GetMolName(Mol, MolNum)
 1298             MiscUtil.PrintWarning("Embedding failed  for molecule %s:\n%s\n" % (MolName, ErrMsg))
 1299         ConfIDs = []
 1300 
 1301     return ConfIDs
 1302 
 1303 
 1304 def MinimizeMoleculeUsingForceField(Mol, MolNum, ConfID=-1):
 1305     """Minimize molecule using forcefield available in RDKit."""
 1306 
 1307     try:
 1308         if OptionsInfo["ConfGenerationParams"]["UseUFF"]:
 1309             ConvergeStatus = AllChem.UFFOptimizeMolecule(
 1310                 Mol, confId=ConfID, maxIters=OptionsInfo["ConfGenerationParams"]["MaxIters"]
 1311             )
 1312         elif OptionsInfo["ConfGenerationParams"]["UseMMFF"]:
 1313             ConvergeStatus = AllChem.MMFFOptimizeMolecule(
 1314                 Mol,
 1315                 confId=ConfID,
 1316                 maxIters=OptionsInfo["ConfGenerationParams"]["MaxIters"],
 1317                 mmffVariant=OptionsInfo["ConfGenerationParams"]["ForceFieldMMFFVariant"],
 1318             )
 1319         else:
 1320             MiscUtil.PrintError(
 1321                 "Minimization couldn't be performed: Specified forcefield, %s, is not supported"
 1322                 % OptionsInfo["ConfGenerationParams"]["ForceField"]
 1323             )
 1324     except (ValueError, RuntimeError, Chem.rdchem.KekulizeException) as ErrMsg:
 1325         if not OptionsInfo["QuietMode"]:
 1326             MolName = RDKitUtil.GetMolName(Mol, MolNum)
 1327             MiscUtil.PrintWarning(
 1328                 "Minimization using forcefield couldn't be performed for molecule %s:\n%s\n" % (MolName, ErrMsg)
 1329             )
 1330         return (False, None)
 1331 
 1332     return (True, ConvergeStatus)
 1333 
 1334 
 1335 def CalculateEnergyUsingForceField(Mol, ConfID=None):
 1336     """Calculate energy."""
 1337 
 1338     Status = True
 1339     Energy = None
 1340 
 1341     if ConfID is None:
 1342         ConfID = -1
 1343 
 1344     if OptionsInfo["ConfGenerationParams"]["UseUFF"]:
 1345         UFFMoleculeForcefield = AllChem.UFFGetMoleculeForceField(Mol, confId=ConfID)
 1346         if UFFMoleculeForcefield is None:
 1347             Status = False
 1348         else:
 1349             Energy = UFFMoleculeForcefield.CalcEnergy()
 1350     elif OptionsInfo["ConfGenerationParams"]["UseMMFF"]:
 1351         MMFFMoleculeProperties = AllChem.MMFFGetMoleculeProperties(
 1352             Mol, mmffVariant=OptionsInfo["ConfGenerationParams"]["ForceFieldMMFFVariant"]
 1353         )
 1354         MMFFMoleculeForcefield = AllChem.MMFFGetMoleculeForceField(Mol, MMFFMoleculeProperties, confId=ConfID)
 1355         if MMFFMoleculeForcefield is None:
 1356             Status = False
 1357         else:
 1358             Energy = MMFFMoleculeForcefield.CalcEnergy()
 1359     else:
 1360         MiscUtil.PrintError(
 1361             "Couldn't retrieve conformer energy: Specified forcefield, %s, is not supported"
 1362             % OptionsInfo["ConfGenerationParams"]["ForceField"]
 1363         )
 1364 
 1365     return (Status, Energy)
 1366 
 1367 
 1368 def CalculateEnergyUsingPsi4(Psi4Handle, Psi4Mol, Mol, MolNum=None):
 1369     """Calculate single point energy using Psi4."""
 1370 
 1371     Status = False
 1372     Energy = None
 1373 
 1374     #  Setup reference wave function...
 1375     Reference = SetupReferenceWavefunction(Mol)
 1376     Psi4Handle.set_options({"Reference": Reference})
 1377 
 1378     # Setup method name and basis set...
 1379     MethodName, BasisSet = SetupMethodNameAndBasisSet(Mol)
 1380 
 1381     Status, Energy = Psi4Util.CalculateSinglePointEnergy(
 1382         Psi4Handle, Psi4Mol, MethodName, BasisSet, Quiet=OptionsInfo["QuietMode"]
 1383     )
 1384 
 1385     # Convert energy units...
 1386     if Status:
 1387         if OptionsInfo["ApplyEnergyConversionFactor"]:
 1388             Energy = Energy * OptionsInfo["EnergyConversionFactor"]
 1389 
 1390     # Clean up
 1391     PerformPsi4Cleanup(Psi4Handle)
 1392 
 1393     return (Status, Energy)
 1394 
 1395 
 1396 def SetupConstrainedAtomIndicesForPsi4(Mol, RefMolCore, RefMolMatches):
 1397     """Setup a list of atom indices to be constrained during Psi4 minimizaiton."""
 1398 
 1399     AtomIndices = []
 1400 
 1401     if RefMolMatches is None:
 1402         return AtomIndices
 1403     else:
 1404         ConstrainAtomIndices = RefMolMatches
 1405 
 1406     # Atom indices start from 1 for Psi4 instead 0 for RDKit...
 1407     AtomIndices = [AtomIndex + 1 for AtomIndex in ConstrainAtomIndices]
 1408 
 1409     return AtomIndices
 1410 
 1411 
 1412 def SetupPsi4Mol(Psi4Handle, Mol, MolNum, ConfID=-1):
 1413     """Setup a Psi4 molecule object."""
 1414 
 1415     # Turn off recentering and reorientation to perform optimization in the
 1416     # constrained coordinate space...
 1417     MolGeometry = RDKitUtil.GetPsi4XYZFormatString(Mol, ConfID=ConfID, NoCom=True, NoReorient=True)
 1418 
 1419     try:
 1420         Psi4Mol = Psi4Handle.geometry(MolGeometry)
 1421     except Exception as ErrMsg:
 1422         Psi4Mol = None
 1423         if not OptionsInfo["QuietMode"]:
 1424             MiscUtil.PrintWarning("Failed to create Psi4 molecule from geometry string: %s\n" % ErrMsg)
 1425             MolName = RDKitUtil.GetMolName(Mol, MolNum)
 1426             MiscUtil.PrintWarning("Ignoring molecule: %s" % MolName)
 1427 
 1428     return Psi4Mol
 1429 
 1430 
 1431 def PerformPsi4Cleanup(Psi4Handle):
 1432     """Perform clean up."""
 1433 
 1434     # Clean up after Psi4 run...
 1435     Psi4Handle.core.clean()
 1436 
 1437     # Clean up any leftover scratch files...
 1438     if OptionsInfo["MPMode"]:
 1439         Psi4Util.RemoveScratchFiles(Psi4Handle, OptionsInfo["Psi4RunParams"]["OutputFile"])
 1440 
 1441 
 1442 def CheckAndValidateMolecule(Mol, MolCount=None):
 1443     """Validate molecule for Psi4 calculations."""
 1444 
 1445     if Mol is None:
 1446         if not OptionsInfo["QuietMode"]:
 1447             MiscUtil.PrintInfo("\nProcessing molecule number %s..." % MolCount)
 1448         return False
 1449 
 1450     MolName = RDKitUtil.GetMolName(Mol, MolCount)
 1451     if not OptionsInfo["QuietMode"]:
 1452         MiscUtil.PrintInfo("\nProcessing molecule %s..." % MolName)
 1453 
 1454     if RDKitUtil.IsMolEmpty(Mol):
 1455         if not OptionsInfo["QuietMode"]:
 1456             MiscUtil.PrintWarning("Ignoring empty molecule: %s\n" % MolName)
 1457         return False
 1458 
 1459     if not RDKitUtil.ValidateElementSymbols(RDKitUtil.GetAtomSymbols(Mol)):
 1460         if not OptionsInfo["QuietMode"]:
 1461             MiscUtil.PrintWarning("Ignoring molecule containing invalid element symbols: %s\n" % MolName)
 1462         return False
 1463 
 1464     if OptionsInfo["Infile3D"]:
 1465         if not Mol.GetConformer().Is3D():
 1466             if not OptionsInfo["QuietMode"]:
 1467                 MiscUtil.PrintWarning("3D tag is not set for molecule: %s\n" % MolName)
 1468 
 1469     if OptionsInfo["Infile3D"]:
 1470         # Otherwise, Hydrogens are always added...
 1471         if RDKitUtil.AreHydrogensMissingInMolecule(Mol):
 1472             if not OptionsInfo["QuietMode"]:
 1473                 MiscUtil.PrintWarning("Missing hydrogens in molecule: %s\n" % MolName)
 1474 
 1475     return True
 1476 
 1477 
 1478 def SetupMethodNameAndBasisSet(Mol):
 1479     """Setup method name and basis set."""
 1480 
 1481     MethodName = OptionsInfo["MethodName"]
 1482     if OptionsInfo["MethodNameAuto"]:
 1483         MethodName = "B3LYP"
 1484 
 1485     BasisSet = OptionsInfo["BasisSet"]
 1486     if OptionsInfo["BasisSetAuto"]:
 1487         BasisSet = "6-31+G**" if RDKitUtil.IsAtomSymbolPresentInMol(Mol, "S") else "6-31G**"
 1488 
 1489     return (MethodName, BasisSet)
 1490 
 1491 
 1492 def SetupReferenceWavefunction(Mol):
 1493     """Setup reference wavefunction."""
 1494 
 1495     Reference = OptionsInfo["Reference"]
 1496     if OptionsInfo["ReferenceAuto"]:
 1497         Reference = "UHF" if (RDKitUtil.GetSpinMultiplicity(Mol) > 1) else "RHF"
 1498 
 1499     return Reference
 1500 
 1501 
 1502 def SetupEnergyConversionFactor(Psi4Handle):
 1503     """Setup converstion factor for energt units. The Psi4 energy units are Hartrees."""
 1504 
 1505     EnergyUnits = OptionsInfo["EnergyUnits"]
 1506 
 1507     ApplyConversionFactor = True
 1508     if re.match(r"^kcal\/mol$", EnergyUnits, re.I):
 1509         ConversionFactor = Psi4Handle.constants.hartree2kcalmol
 1510     elif re.match(r"^kJ\/mol$", EnergyUnits, re.I):
 1511         ConversionFactor = Psi4Handle.constants.hartree2kJmol
 1512     elif re.match("^eV$", EnergyUnits, re.I):
 1513         ConversionFactor = Psi4Handle.constants.hartree2ev
 1514     else:
 1515         ApplyConversionFactor = False
 1516         ConversionFactor = 1.0
 1517 
 1518     OptionsInfo["ApplyEnergyConversionFactor"] = ApplyConversionFactor
 1519     OptionsInfo["EnergyConversionFactor"] = ConversionFactor
 1520 
 1521 
 1522 def GenerateStartingTorsionScanStructureOutfile(Mol, MolNum):
 1523     """Write out the structure of molecule used for starting tosion scan."""
 1524 
 1525     FileDir, FileName, FileExt = MiscUtil.ParseFileName(Options["--outfile"])
 1526     MolName = GetOutputFileMolName(Mol, MolNum)
 1527 
 1528     Outfile = "%s_%s.%s" % (FileName, MolName, FileExt)
 1529 
 1530     # Set up a molecule writer...
 1531     Writer = RDKitUtil.MoleculesWriter(Outfile, **OptionsInfo["OutfileParams"])
 1532     if Writer is None:
 1533         MiscUtil.PrintWarning("Failed to setup a writer for output fie %s " % Outfile)
 1534         return
 1535 
 1536     Writer.write(Mol)
 1537 
 1538     if Writer is not None:
 1539         Writer.close()
 1540 
 1541 
 1542 def GenerateOutputFiles(Mol, MolNum, TorsionID, TorsionMatchNum, TorsionMols, TorsionEnergies, TorsionAngles):
 1543     """Generate output files."""
 1544 
 1545     StructureOutfile, EnergyTextOutfile, PlotOutfile, ViewerHTMLOutfile = SetupOutputFileNames(
 1546         Mol, MolNum, TorsionID, TorsionMatchNum
 1547     )
 1548 
 1549     GenerateScannedTorsionsStructureOutfile(
 1550         StructureOutfile, Mol, MolNum, TorsionID, TorsionMatchNum, TorsionMols, TorsionEnergies, TorsionAngles
 1551     )
 1552     GenerateEnergyTextOutfile(
 1553         EnergyTextOutfile, Mol, MolNum, TorsionID, TorsionMatchNum, TorsionMols, TorsionEnergies, TorsionAngles
 1554     )
 1555     GeneratePlotOutfile(
 1556         PlotOutfile, Mol, MolNum, TorsionID, TorsionMatchNum, TorsionMols, TorsionEnergies, TorsionAngles
 1557     )
 1558     GenerateViewerHTMLOutfile(
 1559         ViewerHTMLOutfile, Mol, MolNum, TorsionID, TorsionMatchNum, TorsionMols, TorsionEnergies, TorsionAngles
 1560     )
 1561 
 1562 
 1563 def GenerateScannedTorsionsStructureOutfile(
 1564     Outfile, Mol, MolNum, TorsionID, TorsionMatchNum, TorsionMols, TorsionEnergies, TorsionAngles
 1565 ):
 1566     """Write out structures generated after torsion scan along with associated data."""
 1567 
 1568     # Set up a molecule writer...
 1569     Writer = RDKitUtil.MoleculesWriter(Outfile, **OptionsInfo["OutfileParams"])
 1570     if Writer is None:
 1571         MiscUtil.PrintWarning("Failed to setup a writer for output fie %s " % Outfile)
 1572         return
 1573 
 1574     MolName = RDKitUtil.GetMolName(Mol, MolNum)
 1575 
 1576     RelativeTorsionEnergies = SetupRelativeEnergies(TorsionEnergies)
 1577     for Index, TorsionMol in enumerate(TorsionMols):
 1578         TorsionAngle = "%s" % TorsionAngles[Index]
 1579         TorsionMol.SetProp("Torsion_Angle", TorsionAngle)
 1580 
 1581         TorsionEnergy = "%.*f" % (OptionsInfo["Precision"], TorsionEnergies[Index])
 1582         TorsionMol.SetProp(OptionsInfo["EnergyDataFieldLabel"], TorsionEnergy)
 1583 
 1584         RelativeTorsionEnergy = "%.*f" % (OptionsInfo["Precision"], RelativeTorsionEnergies[Index])
 1585         TorsionMol.SetProp(OptionsInfo["EnergyRelativeDataFieldLabel"], RelativeTorsionEnergy)
 1586 
 1587         TorsionMolName = "%s_Deg%s" % (MolName, TorsionAngle)
 1588         TorsionMol.SetProp("_Name", TorsionMolName)
 1589 
 1590         Writer.write(TorsionMol)
 1591 
 1592     if Writer is not None:
 1593         Writer.close()
 1594 
 1595 
 1596 def GenerateEnergyTextOutfile(
 1597     Outfile, Mol, MolNum, TorsionID, TorsionMatchNum, TorsionMols, TorsionEnergies, TorsionAngles
 1598 ):
 1599     """Write out torsion angles and energies."""
 1600 
 1601     # Setup a writer...
 1602     Writer = open(Outfile, "w")
 1603     if Writer is None:
 1604         MiscUtil.PrintError("Failed to setup a writer for output fie %s " % Outfile)
 1605 
 1606     # Write headers...
 1607     Writer.write(
 1608         "TorsionAngle,%s,%s\n" % (OptionsInfo["EnergyDataFieldLabel"], OptionsInfo["EnergyRelativeDataFieldLabel"])
 1609     )
 1610 
 1611     RelativeTorsionEnergies = SetupRelativeEnergies(TorsionEnergies)
 1612     for Index, TorsionAngle in enumerate(TorsionAngles):
 1613         TorsionEnergy = "%.*f" % (OptionsInfo["Precision"], TorsionEnergies[Index])
 1614         RelativeTorsionEnergy = "%.*f" % (OptionsInfo["Precision"], RelativeTorsionEnergies[Index])
 1615         Writer.write("%d,%s,%s\n" % (TorsionAngle, TorsionEnergy, RelativeTorsionEnergy))
 1616 
 1617     if Writer is not None:
 1618         Writer.close()
 1619 
 1620 
 1621 def GenerateViewerHTMLOutfile(
 1622     Outfile, Mol, MolNum, TorsionID, TorsionMatchNum, TorsionMols, TorsionEnergies, TorsionAngles
 1623 ):
 1624     """Write out a HTML file for viewing torsion scan along with associated data."""
 1625 
 1626     # Setup a writer...
 1627     Writer = open(Outfile, "w")
 1628     if Writer is None:
 1629         MiscUtil.PrintError("Failed to setup a writer for output fie %s " % Outfile)
 1630 
 1631     # Setup HTML for torsion scan viewer...
 1632     MolName = RDKitUtil.GetMolName(Mol, MolNum)
 1633     RelativeTorsionEnergies = SetupRelativeEnergies(TorsionEnergies)
 1634 
 1635     Units = OptionsInfo["EnergyUnits"]
 1636     PlotHeight = OptionsInfo["OutPlotTorsionViewerHeight"]
 1637 
 1638     MethodName, BasisSet = SetupMethodNameAndBasisSet(Mol)
 1639     TitleLine = "%s (%s/%s)" % (OptionsInfo["OutPlotParams"]["Title"], MethodName, BasisSet)
 1640     if OptionsInfo["OutPlotTitleTorsionSpec"]:
 1641         TorsionPattern = OptionsInfo["TorsionsPatternsInfo"]["Pattern"][TorsionID]
 1642         TitleLine = "%s: %s" % (TitleLine, TorsionPattern)
 1643 
 1644     ViwerHTMLText = RDKitUtil.SetupHTMLForTorsionScanViewer(
 1645         MolName, TorsionMols, TorsionEnergies, RelativeTorsionEnergies, TorsionAngles, Units, PlotHeight, TitleLine
 1646     )
 1647 
 1648     # Write out HTML for torsion scan viewer...
 1649     Writer.write("%s" % ViwerHTMLText)
 1650 
 1651     Writer.close()
 1652 
 1653 
 1654 def GeneratePlotOutfile(Outfile, Mol, MolNum, TorsionID, TorsionMatchNum, TorsionMols, TorsionEnergies, TorsionAngles):
 1655     """Generate a plot corresponding to torsion angles and energies."""
 1656 
 1657     OutPlotParams = OptionsInfo["OutPlotParams"]
 1658 
 1659     # Initialize seaborn and matplotlib paramaters...
 1660     if not OptionsInfo["OutPlotInitialized"]:
 1661         OptionsInfo["OutPlotInitialized"] = True
 1662         RCParams = {
 1663             "figure.figsize": (OutPlotParams["Width"], OutPlotParams["Height"]),
 1664             "axes.titleweight": OutPlotParams["TitleWeight"],
 1665             "axes.labelweight": OutPlotParams["LabelWeight"],
 1666         }
 1667         sns.set(
 1668             context=OutPlotParams["Context"],
 1669             style=OutPlotParams["Style"],
 1670             palette=OutPlotParams["Palette"],
 1671             font=OutPlotParams["Font"],
 1672             font_scale=OutPlotParams["FontScale"],
 1673             rc=RCParams,
 1674         )
 1675 
 1676     # Create a new figure...
 1677     plt.figure()
 1678 
 1679     if OptionsInfo["OutPlotRelativeEnergy"]:
 1680         TorsionEnergies = SetupRelativeEnergies(TorsionEnergies)
 1681 
 1682     # Draw plot...
 1683     PlotType = OutPlotParams["Type"]
 1684     if re.match("linepoint", PlotType, re.I):
 1685         Axis = sns.lineplot(x=TorsionAngles, y=TorsionEnergies, marker="o", legend=False)
 1686     elif re.match("scatter", PlotType, re.I):
 1687         Axis = sns.scatterplot(x=TorsionAngles, y=TorsionEnergies, legend=False)
 1688     elif re.match("line", PlotType, re.I):
 1689         Axis = sns.lineplot(x=TorsionAngles, y=TorsionEnergies, legend=False)
 1690     else:
 1691         MiscUtil.PrintError(
 1692             'The value, %s, specified for "type" using option "--outPlotParams" is not supported. Valid plot types: linepoint, scatter or line'
 1693             % (PlotType)
 1694         )
 1695 
 1696     # Setup title and labels...
 1697     Title = OutPlotParams["Title"]
 1698     if OptionsInfo["OutPlotTitleTorsionSpec"]:
 1699         TorsionPattern = OptionsInfo["TorsionsPatternsInfo"]["Pattern"][TorsionID]
 1700         Title = "%s: %s" % (OutPlotParams["Title"], TorsionPattern)
 1701 
 1702     # Set labels and title...
 1703     Axis.set(xlabel=OutPlotParams["XLabel"], ylabel=OutPlotParams["YLabel"], title=Title)
 1704 
 1705     # Save figure...
 1706     plt.savefig(Outfile)
 1707 
 1708     # Close the plot...
 1709     plt.close()
 1710 
 1711 
 1712 def SetupRelativeEnergies(Energies):
 1713     """Set up a list of relative energies."""
 1714 
 1715     SortedEnergies = sorted(Energies)
 1716     MinEnergy = SortedEnergies[0]
 1717     RelativeEnergies = [(Energy - MinEnergy) for Energy in Energies]
 1718 
 1719     return RelativeEnergies
 1720 
 1721 
 1722 def SetupOutputFileNames(Mol, MolNum, TorsionID, TorsionMatchNum):
 1723     """Setup names of output files."""
 1724 
 1725     FileDir, FileName, FileExt = MiscUtil.ParseFileName(Options["--outfile"])
 1726     MolName = GetOutputFileMolName(Mol, MolNum)
 1727 
 1728     OutfileRoot = "%s_%s_Torsion%s_Match%s" % (FileName, MolName, TorsionID, TorsionMatchNum)
 1729 
 1730     StructureOutfile = "%s.%s" % (OutfileRoot, FileExt)
 1731     EnergyTextOutfile = "%s_Energies.csv" % (OutfileRoot)
 1732 
 1733     PlotExt = OptionsInfo["OutPlotParams"]["OutExt"]
 1734     PlotOutfile = "%s_Plot.%s" % (OutfileRoot, PlotExt)
 1735 
 1736     ViewerHTMLOutfile = "%s_Viewer.html" % (OutfileRoot)
 1737 
 1738     return (StructureOutfile, EnergyTextOutfile, PlotOutfile, ViewerHTMLOutfile)
 1739 
 1740 
 1741 def GetOutputFileMolName(Mol, MolNum):
 1742     """Get output file prefix."""
 1743 
 1744     MolName = "Mol%s" % MolNum
 1745     if OptionsInfo["OutfileMolName"]:
 1746         MolName = re.sub("[^a-zA-Z0-9]", "_", RDKitUtil.GetMolName(Mol, MolNum), flags=re.I)
 1747 
 1748     return MolName
 1749 
 1750 
 1751 def ProcessTorsionRangeOptions():
 1752     """Process tosion range options."""
 1753 
 1754     TosionRangeMode = Options["--torsionRangeMode"]
 1755     OptionsInfo["TosionRangeMode"] = TosionRangeMode
 1756 
 1757     if re.match("^Range$", TosionRangeMode, re.I):
 1758         ProcessTorsionRangeValues()
 1759     elif re.match("^Angles$", TosionRangeMode, re.I):
 1760         ProcessTorsionAnglesValues()
 1761     else:
 1762         MiscUtil.PrintError('The value, %s,  option "--torsionRangeMode" is not supported.' % TosionRangeMode)
 1763 
 1764 
 1765 def ProcessTorsionRangeValues():
 1766     """Process tosion range values."""
 1767 
 1768     TorsionRange = Options["--torsionRange"]
 1769     if re.match("^auto$", TorsionRange, re.I):
 1770         TorsionRange = "0,360,5"
 1771     TorsionRangeWords = TorsionRange.split(",")
 1772 
 1773     TorsionStart = int(TorsionRangeWords[0])
 1774     TorsionStop = int(TorsionRangeWords[1])
 1775     TorsionStep = int(TorsionRangeWords[2])
 1776 
 1777     if TorsionStart >= TorsionStop:
 1778         MiscUtil.PrintError(
 1779             'The start value, %d, specified for option "--torsionRange" in string "%s" must be less than stop value, %s.'
 1780             % (TorsionStart, Options["--torsionRange"], TorsionStop)
 1781         )
 1782     if TorsionStep == 0:
 1783         MiscUtil.PrintError(
 1784             'The step value, %d, specified for option "--torsonRange" in string "%s" must be > 0.'
 1785             % (TorsionStep, Options["--torsionRange"])
 1786         )
 1787     if TorsionStep >= (TorsionStop - TorsionStart):
 1788         MiscUtil.PrintError(
 1789             'The step value, %d, specified for option "--torsonRange" in string "%s" must be less than, %s.'
 1790             % (TorsionStep, Options["--torsionRange"], (TorsionStop - TorsionStart))
 1791         )
 1792 
 1793     if TorsionStart < 0:
 1794         if TorsionStart < -180:
 1795             MiscUtil.PrintError(
 1796                 'The start value, %d, specified for option "--torsionRange" in string "%s" must be  >= -180 to use scan range from -180 to 180.'
 1797                 % (TorsionStart, Options["--torsionRange"])
 1798             )
 1799         if TorsionStop > 180:
 1800             MiscUtil.PrintError(
 1801                 'The stop value, %d, specified for option "--torsionRange" in string "%s" must be <= 180 to use scan range from -180 to 180.'
 1802                 % (TorsionStop, Options["--torsionRange"])
 1803             )
 1804     else:
 1805         if TorsionStop > 360:
 1806             MiscUtil.PrintError(
 1807                 'The stop value, %d, specified for option "--torsionRange" in string "%s" must be  <= 360 to use scan range from 0 to 360.'
 1808                 % (TorsionStop, Options["--torsionRange"])
 1809             )
 1810 
 1811     TorsionAngles = [Angle for Angle in range(TorsionStart, TorsionStop, TorsionStep)]
 1812     TorsionAngles.append(TorsionStop)
 1813 
 1814     OptionsInfo["TorsionRange"] = TorsionRange
 1815     OptionsInfo["TorsionStart"] = TorsionStart
 1816     OptionsInfo["TorsionStop"] = TorsionStop
 1817     OptionsInfo["TorsionStep"] = TorsionStep
 1818 
 1819     OptionsInfo["TorsionAngles"] = TorsionAngles
 1820 
 1821 
 1822 def ProcessTorsionAnglesValues():
 1823     """Process tosion angle values."""
 1824 
 1825     TorsionRange = Options["--torsionRange"]
 1826     if re.match("^auto$", TorsionRange, re.I):
 1827         MiscUtil.PrintError('The value specified, %s, for option "--torsionRange" is not valid.' % (TorsionRange))
 1828 
 1829     TorsionAngles = []
 1830 
 1831     for TorsionAngle in TorsionRange.split(","):
 1832         TorsionAngle = int(TorsionAngle)
 1833 
 1834         if TorsionAngle < -180:
 1835             MiscUtil.PrintError(
 1836                 'The stop value, %d, specified for option "--torsionRange" in string "%s" must be  >= -180.'
 1837                 % (TorsionAngle, TorsionRange)
 1838             )
 1839 
 1840         if TorsionAngle > 360:
 1841             MiscUtil.PrintError(
 1842                 'The stop value, %d, specified for option "--torsionRange" in string "%s" must be  <= 360.'
 1843                 % (TorsionAngle, TorsionRange)
 1844             )
 1845 
 1846         TorsionAngles.append(TorsionAngle)
 1847 
 1848     OptionsInfo["TorsionRange"] = TorsionRange
 1849     OptionsInfo["TorsionStart"] = None
 1850     OptionsInfo["TorsionStop"] = None
 1851     OptionsInfo["TorsionStep"] = None
 1852 
 1853     OptionsInfo["TorsionAngles"] = sorted(TorsionAngles)
 1854 
 1855 
 1856 def ProcessOptions():
 1857     """Process and validate command line arguments and options."""
 1858 
 1859     MiscUtil.PrintInfo("Processing options...")
 1860 
 1861     # Validate options...
 1862     ValidateOptions()
 1863 
 1864     OptionsInfo["ModeMols"] = Options["--modeMols"]
 1865     OptionsInfo["FirstMolMode"] = True if re.match("^First$", Options["--modeMols"], re.I) else False
 1866 
 1867     OptionsInfo["ModeTorsions"] = Options["--modeTorsions"]
 1868     OptionsInfo["FirstTorsionMode"] = True if re.match("^First$", Options["--modeTorsions"], re.I) else False
 1869 
 1870     OptionsInfo["Infile"] = Options["--infile"]
 1871     OptionsInfo["SMILESInfileStatus"] = True if MiscUtil.CheckFileExt(Options["--infile"], "smi csv tsv txt") else False
 1872     ParamsDefaultInfoOverride = {"RemoveHydrogens": False}
 1873     OptionsInfo["InfileParams"] = MiscUtil.ProcessOptionInfileParameters(
 1874         "--infileParams",
 1875         Options["--infileParams"],
 1876         InfileName=Options["--infile"],
 1877         ParamsDefaultInfo=ParamsDefaultInfoOverride,
 1878     )
 1879     OptionsInfo["Infile3D"] = True if re.match("^yes$", Options["--infile3D"], re.I) else False
 1880 
 1881     OptionsInfo["Outfile"] = Options["--outfile"]
 1882     OptionsInfo["OutfileParams"] = MiscUtil.ProcessOptionOutfileParameters(
 1883         "--outfileParams", Options["--outfileParams"]
 1884     )
 1885 
 1886     # Method, basis set, and reference wavefunction...
 1887     OptionsInfo["BasisSet"] = Options["--basisSet"]
 1888     OptionsInfo["BasisSetAuto"] = True if re.match("^auto$", Options["--basisSet"], re.I) else False
 1889 
 1890     OptionsInfo["MethodName"] = Options["--methodName"]
 1891     OptionsInfo["MethodNameAuto"] = True if re.match("^auto$", Options["--methodName"], re.I) else False
 1892 
 1893     OptionsInfo["Reference"] = Options["--reference"]
 1894     OptionsInfo["ReferenceAuto"] = True if re.match("^auto$", Options["--reference"], re.I) else False
 1895 
 1896     # Run and options parameters...
 1897     OptionsInfo["Psi4OptionsParams"] = Psi4Util.ProcessPsi4OptionsParameters(
 1898         "--psi4OptionsParams", Options["--psi4OptionsParams"]
 1899     )
 1900     OptionsInfo["Psi4RunParams"] = Psi4Util.ProcessPsi4RunParameters(
 1901         "--psi4RunParams", Options["--psi4RunParams"], InfileName=OptionsInfo["Infile"]
 1902     )
 1903 
 1904     # Conformer generation paramaters...
 1905     ParamsDefaultInfoOverride = {"MaxConfs": 250, "MaxConfsTorsions": 50}
 1906     OptionsInfo["ConfGenerationParams"] = MiscUtil.ProcessOptionConformerParameters(
 1907         "--confParams", Options["--confParams"], ParamsDefaultInfoOverride
 1908     )
 1909 
 1910     # Energy units and label...
 1911     OptionsInfo["EnergyUnits"] = Options["--energyUnits"]
 1912 
 1913     EnergyDataFieldLabel = Options["--energyDataFieldLabel"]
 1914     if re.match("^auto$", EnergyDataFieldLabel, re.I):
 1915         EnergyDataFieldLabel = "Psi4_Energy (%s)" % Options["--energyUnits"]
 1916     OptionsInfo["EnergyDataFieldLabel"] = EnergyDataFieldLabel
 1917 
 1918     EnergyRelativeDataFieldLabel = Options["--energyRelativeDataFieldLabel"]
 1919     if re.match("^auto$", EnergyRelativeDataFieldLabel, re.I):
 1920         EnergyRelativeDataFieldLabel = "Psi4_Relative_Energy (%s)" % Options["--energyUnits"]
 1921     OptionsInfo["EnergyRelativeDataFieldLabel"] = EnergyRelativeDataFieldLabel
 1922 
 1923     # Plot parameters...
 1924     OptionsInfo["OutfileMolName"] = True if re.match("^yes$", Options["--outfileMolName"], re.I) else False
 1925     OptionsInfo["OutPlotRelativeEnergy"] = (
 1926         True if re.match("^yes$", Options["--outPlotRelativeEnergy"], re.I) else False
 1927     )
 1928     OptionsInfo["OutPlotTitleTorsionSpec"] = (
 1929         True if re.match("^yes$", Options["--outPlotTitleTorsionSpec"], re.I) else False
 1930     )
 1931     OptionsInfo["OutPlotTorsionViewerHeight"] = int(Options["--outPlotTorsionViewerHeight"])
 1932 
 1933     # The default width and height, 10.0 and 7.5, map to aspect raito of 16/9 (1.778)...
 1934     if OptionsInfo["OutPlotRelativeEnergy"]:
 1935         EnergyLabel = "Relative Energy (%s)" % OptionsInfo["EnergyUnits"]
 1936     else:
 1937         EnergyLabel = "Energy (%s)" % OptionsInfo["EnergyUnits"]
 1938 
 1939     DefaultValues = {
 1940         "Type": "linepoint",
 1941         "Width": 10.0,
 1942         "Height": 5.6,
 1943         "Title": "Psi4 Torsion Scan",
 1944         "XLabel": "Torsion Angle (degrees)",
 1945         "YLabel": EnergyLabel,
 1946     }
 1947     OptionsInfo["OutPlotParams"] = MiscUtil.ProcessOptionSeabornPlotParameters(
 1948         "--outPlotParams", Options["--outPlotParams"], DefaultValues
 1949     )
 1950     if not re.match("^(linepoint|scatter|Line)$", OptionsInfo["OutPlotParams"]["Type"], re.I):
 1951         MiscUtil.PrintError(
 1952             'The value, %s, specified for "type" using option "--outPlotParams" is not supported. Valid plot types: linepoint, scatter or line'
 1953             % (OptionsInfo["OutPlotParams"]["Type"])
 1954         )
 1955 
 1956     OptionsInfo["OutPlotInitialized"] = False
 1957 
 1958     OptionsInfo["Overwrite"] = Options["--overwrite"]
 1959 
 1960     OptionsInfo["MaxIters"] = int(Options["--maxIters"])
 1961 
 1962     OptionsInfo["MPMode"] = True if re.match("^yes$", Options["--mp"], re.I) else False
 1963     OptionsInfo["MPParams"] = MiscUtil.ProcessOptionMultiprocessingParameters("--mpParams", Options["--mpParams"])
 1964 
 1965     # Multiprocessing level...
 1966     MPLevelMoleculesMode = False
 1967     MPLevelTorsionAnglesMode = False
 1968     MPLevel = Options["--mpLevel"]
 1969     if re.match("^Molecules$", MPLevel, re.I):
 1970         MPLevelMoleculesMode = True
 1971     elif re.match("^TorsionAngles$", MPLevel, re.I):
 1972         MPLevelTorsionAnglesMode = True
 1973     else:
 1974         MiscUtil.PrintError('The value, %s, specified for option "--mpLevel" is not valid. ' % MPLevel)
 1975     OptionsInfo["MPLevel"] = MPLevel
 1976     OptionsInfo["MPLevelMoleculesMode"] = MPLevelMoleculesMode
 1977     OptionsInfo["MPLevelTorsionAnglesMode"] = MPLevelTorsionAnglesMode
 1978 
 1979     OptionsInfo["Precision"] = int(Options["--precision"])
 1980     OptionsInfo["QuietMode"] = True if re.match("^yes$", Options["--quiet"], re.I) else False
 1981 
 1982     # Procsss and validate specified SMILES/SMARTS torsion patterns...
 1983     TorsionPatterns = Options["--torsions"]
 1984     TorsionPatternsList = []
 1985     for TorsionPattern in TorsionPatterns.split(","):
 1986         TorsionPattern = TorsionPattern.strip()
 1987         if not len(TorsionPattern):
 1988             MiscUtil.PrintError(
 1989                 'Empty value specified for SMILES/SMARTS pattern in  "-t, --torsions" option: %s' % TorsionPatterns
 1990             )
 1991 
 1992         TorsionMol = Chem.MolFromSmarts(TorsionPattern)
 1993         if TorsionMol is None:
 1994             MiscUtil.PrintError(
 1995                 'Failed to create torsion pattern molecule. The torsion SMILES/SMARTS pattern, "%s", specified using "-t, --torsions" option, "%s",  is not valid.'
 1996                 % (TorsionPattern, TorsionPatterns)
 1997             )
 1998         TorsionPatternsList.append(TorsionPattern)
 1999 
 2000     OptionsInfo["TorsionPatterns"] = TorsionPatterns
 2001     OptionsInfo["TorsionPatternsList"] = TorsionPatternsList
 2002 
 2003     # Process and validate any specified torsion atom indices for filtering torsion matches...
 2004     TorsionsFilterByAtomIndices = Options["--torsionsFilterbyAtomIndices"]
 2005     TorsionsFilterByAtomIndicesList = []
 2006     if not re.match("^None$", TorsionsFilterByAtomIndices, re.I):
 2007         for AtomIndex in TorsionsFilterByAtomIndices.split(","):
 2008             AtomIndex = AtomIndex.strip()
 2009             if not MiscUtil.IsInteger(AtomIndex):
 2010                 MiscUtil.PrintError(
 2011                     'The value specified, %s, for option "--torsionsFilterbyAtomIndices" must be an integer.'
 2012                     % AtomIndex
 2013                 )
 2014             AtomIndex = int(AtomIndex)
 2015             if AtomIndex < 0:
 2016                 MiscUtil.PrintError(
 2017                     'The value specified, %s, for option "--torsionsFilterbyAtomIndices" must be >= 0.' % AtomIndex
 2018                 )
 2019             TorsionsFilterByAtomIndicesList.append(AtomIndex)
 2020 
 2021         if len(TorsionsFilterByAtomIndicesList) < 4:
 2022             MiscUtil.PrintError(
 2023                 'The number of values, %s,  specified, %s, for option "--torsionsFilterbyAtomIndices" must be >=4.'
 2024                 % (len(TorsionsFilterByAtomIndicesList), TorsionsFilterByAtomIndices)
 2025             )
 2026 
 2027     OptionsInfo["TorsionsFilterByAtomIndices"] = TorsionsFilterByAtomIndices
 2028     OptionsInfo["TorsionsFilterByAtomIndicesList"] = TorsionsFilterByAtomIndicesList
 2029     OptionsInfo["FilterTorsionsByAtomIndicesMode"] = True if len(TorsionsFilterByAtomIndicesList) > 0 else False
 2030 
 2031     OptionsInfo["TorsionMaxMatches"] = int(Options["--torsionMaxMatches"])
 2032     OptionsInfo["TorsionMinimize"] = True if re.match("^yes$", Options["--torsionMinimize"], re.I) else False
 2033 
 2034     ProcessTorsionRangeOptions()
 2035 
 2036     OptionsInfo["UseChirality"] = True if re.match("^yes$", Options["--useChirality"], re.I) else False
 2037 
 2038 
 2039 def RetrieveOptions():
 2040     """Retrieve command line arguments and options."""
 2041 
 2042     # Get options...
 2043     global Options
 2044     Options = docopt(_docoptUsage_)
 2045 
 2046     # Set current working directory to the specified directory...
 2047     WorkingDir = Options["--workingdir"]
 2048     if WorkingDir:
 2049         os.chdir(WorkingDir)
 2050 
 2051     # Handle examples option...
 2052     if "--examples" in Options and Options["--examples"]:
 2053         MiscUtil.PrintInfo(MiscUtil.GetExamplesTextFromDocOptText(_docoptUsage_))
 2054         sys.exit(0)
 2055 
 2056 
 2057 def ValidateOptions():
 2058     """Validate option values."""
 2059 
 2060     MiscUtil.ValidateOptionTextValue("--energyUnits", Options["--energyUnits"], "Hartrees kcal/mol kJ/mol eV")
 2061 
 2062     MiscUtil.ValidateOptionFilePath("-i, --infile", Options["--infile"])
 2063     MiscUtil.ValidateOptionFileExt("-i, --infile", Options["--infile"], "sdf sd mol smi txt csv tsv")
 2064     MiscUtil.ValidateOptionTextValue("--infile3D", Options["--infile3D"], "yes no")
 2065 
 2066     MiscUtil.ValidateOptionFileExt("-o, --outfile", Options["--outfile"], "sdf sd")
 2067     MiscUtil.ValidateOptionsOutputFileOverwrite(
 2068         "-o, --outfile", Options["--outfile"], "--overwrite", Options["--overwrite"]
 2069     )
 2070     MiscUtil.ValidateOptionsDistinctFileNames(
 2071         "-i, --infile", Options["--infile"], "-o, --outfile", Options["--outfile"]
 2072     )
 2073 
 2074     if not Options["--overwrite"]:
 2075         FileDir, FileName, FileExt = MiscUtil.ParseFileName(Options["--outfile"])
 2076         FileNames = glob.glob("%s_*" % FileName)
 2077         if len(FileNames):
 2078             MiscUtil.PrintError(
 2079                 'The outfile names, %s_*, generated from file specified, %s, for option "-o, --outfile" already exist. Use option "--overwrite" or "--ov"  and try again.\n'
 2080                 % (FileName, Options["--outfile"])
 2081             )
 2082 
 2083     MiscUtil.ValidateOptionTextValue("--outPlotRelativeEnergy", Options["--outPlotRelativeEnergy"], "yes no")
 2084     MiscUtil.ValidateOptionTextValue("--outPlotTitleTorsionSpec", Options["--outPlotTitleTorsionSpec"], "yes no")
 2085     MiscUtil.ValidateOptionIntegerValue(
 2086         "--outPlotTorsionViewerHeight", Options["--outPlotTorsionViewerHeight"], {">": 0}
 2087     )
 2088 
 2089     MiscUtil.ValidateOptionTextValue("--outfileMolName ", Options["--outfileMolName"], "yes no")
 2090 
 2091     MiscUtil.ValidateOptionTextValue("--modeMols", Options["--modeMols"], "First All")
 2092     MiscUtil.ValidateOptionTextValue("--modeTorsions", Options["--modeTorsions"], "First All")
 2093 
 2094     MiscUtil.ValidateOptionIntegerValue("--maxIters", Options["--maxIters"], {">": 0})
 2095 
 2096     MiscUtil.ValidateOptionTextValue("--mp", Options["--mp"], "yes no")
 2097     MiscUtil.ValidateOptionTextValue("--mpLevel", Options["--mpLevel"], "Molecules TorsionAngles")
 2098 
 2099     MiscUtil.ValidateOptionIntegerValue("-p, --precision", Options["--precision"], {">": 0})
 2100     MiscUtil.ValidateOptionTextValue("-q, --quiet", Options["--quiet"], "yes no")
 2101 
 2102     MiscUtil.ValidateOptionIntegerValue("--torsionMaxMatches", Options["--torsionMaxMatches"], {">": 0})
 2103     MiscUtil.ValidateOptionTextValue("--torsionMinimize", Options["--torsionMinimize"], "yes no")
 2104 
 2105     MiscUtil.ValidateOptionTextValue("--torsionRangeMode", Options["--torsionRangeMode"], "Range or Angles")
 2106     TorsionRange = Options["--torsionRange"]
 2107     if re.match("^Range$", Options["--torsionRangeMode"], re.I):
 2108         if not re.match("^auto$", TorsionRange, re.I):
 2109             MiscUtil.ValidateOptionNumberValues("--torsionRange", TorsionRange, 3, ",", "integer", {})
 2110     else:
 2111         if re.match("^auto$", TorsionRange, re.I):
 2112             MiscUtil.PrintError(
 2113                 '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.'
 2114                 % (TorsionRange, Options["--torsionRangeMode"])
 2115             )
 2116         TorsionAngles = []
 2117         for TorsionAngle in TorsionRange.split(","):
 2118             TorsionAngle = TorsionAngle.strip()
 2119             if not MiscUtil.IsInteger(TorsionAngle):
 2120                 MiscUtil.PrintError(
 2121                     'The value specified, %s, for option "--torsionRange" in string "%s" must be an integer.'
 2122                     % (TorsionAngle, TorsionRange)
 2123                 )
 2124             if TorsionAngle in TorsionAngles:
 2125                 MiscUtil.PrintError(
 2126                     'The value specified, %s, for option "--torsionRange" in string "%s" is a duplicate value.'
 2127                     % (TorsionAngle, TorsionRange)
 2128                 )
 2129             TorsionAngles.append(TorsionAngle)
 2130 
 2131     MiscUtil.ValidateOptionTextValue("--useChirality", Options["--useChirality"], "yes no")
 2132 
 2133 
 2134 # Setup a usage string for docopt...
 2135 _docoptUsage_ = """
 2136 Psi4PerformTorsionScan.py - Perform torsion scan
 2137 
 2138 Usage:
 2139     Psi4PerformTorsionScan.py [--basisSet <text>] [--confParams <Name,Value,...>] [--energyDataFieldLabel <text>]
 2140                               [--energyRelativeDataFieldLabel <text>] [--energyUnits <text>] [--infile3D <yes or no>]
 2141                               [--infileParams <Name,Value,...>] [--maxIters <number>] [--methodName <text>]
 2142                               [--modeMols <First or All>] [--modeTorsions <First or All>] [--mp <yes or no>]
 2143                               [--mpLevel <Molecules or TorsionAngles>] [--mpParams <Name,Value,...>]
 2144                               [--outfileMolName <yes or no>] [--outfileParams <Name,Value,...>] [--outPlotParams <Name,Value,...>]
 2145                               [--outPlotRelativeEnergy <yes or no>] [--outPlotTitleTorsionSpec <yes or no>] [--outPlotTorsionViewerHeight <number>]
 2146                               [--overwrite] [--precision <number>] [--psi4OptionsParams <Name,Value,...>] [--psi4RunParams <Name,Value,...>]
 2147                               [--quiet <yes or no>] [--reference <text>]  [--torsionsFilterbyAtomIndices <Index1, Index2, ...>]
 2148                               [--torsionMaxMatches <number>] [--torsionMinimize <yes or no>] [--torsionRangeMode <Range or Angles>]
 2149                               [--torsionRange <Start,Stop,Step or Angle1,Angle2,...>] [--useChirality <yes or no>]
 2150                               [-w <dir>] -t <torsions> -i <infile>  -o <outfile> 
 2151     Psi4PerformTorsionScan.py -h | --help | -e | --examples
 2152 
 2153 Description:
 2154     Perform torsion scan for molecules around torsion angles specified using
 2155     SMILES/SMARTS patterns. A molecule is optionally minimized before performing
 2156     a torsion scan using a forcefield. A set of initial 3D structures are generated for
 2157     a molecule by scanning the torsion angle across the specified range and updating
 2158     the 3D coordinates of the molecule. A conformation ensemble is optionally generated
 2159     for each 3D structure representing a specific torsion angle using a combination of
 2160     distance geometry and forcefield followed by constrained geometry optimization
 2161     using a quantum chemistry method. The conformation with the lowest energy is
 2162     selected to represent the torsion angle. An option is available to skip the generation
 2163     of the conformation ensemble and simply calculate the energy for the initial 3D
 2164     structure for a specific torsion torsion angle using a quantum chemistry method.
 2165     
 2166     The torsions are specified using SMILES or SMARTS patterns. A substructure match
 2167     is performed to select torsion atoms in a molecule. The SMILES pattern match must
 2168     correspond to four torsion atoms. The SMARTS patterns containing atom map numbers
 2169     may match  more than four atoms. The atom map numbers, however, must match
 2170     exactly four torsion atoms. For example: [s:1][c:2]([aX2,cH1])!@[CX3:3](O)=[O:4] for
 2171     thiophene esters and carboxylates as specified in Torsion Library (TorLib) [Ref 146].
 2172 
 2173     A Psi4 XYZ format geometry string is automatically generated for each molecule
 2174     in input file. It contains atom symbols and 3D coordinates for each atom in a
 2175     molecule. In addition, the formal charge and spin multiplicity are present in the
 2176     the geometry string. These values are either retrieved from molecule properties
 2177     named 'FormalCharge' and 'SpinMultiplicty' or dynamically calculated for a
 2178     molecule.
 2179     
 2180     A set of five output files is generated for each torsion match in each
 2181     molecule. The names of the output files are generated using the root of
 2182     the specified output file. They may either contain sequential molecule
 2183     numbers or molecule names as shown below:
 2184         
 2185         <OutfileRoot>_Mol<Num>.sdf
 2186         <OutfileRoot>_Mol<Num>_Torsion<Num>_Match<Num>.sdf
 2187         <OutfileRoot>_Mol<Num>_Torsion<Num>_Match<Num>_Energies.csv
 2188         <OutfileRoot>_Mol<Num>_Torsion<Num>_Match<Num>_Plot.<ImgExt>
 2189         <OutfileRoot>_Mol<Num>_Torsion<Num>_Match<Num>_Viewer.html
 2190         
 2191         or
 2192         
 2193         <OutfileRoot>_<MolName>.sdf
 2194         <OutfileRoot>_<MolName>_Torsion<Num>_Match<Num>.sdf
 2195         <OutfileRoot>_<MolName>_Torsion<Num>_Match<Num>_Energies.csv
 2196         <OutfileRoot>_<MolName>_Torsion<Num>_Match<Num>_Plot.<ImgExt>
 2197         <OutfileRoot>_<MolName>_Torsion<Num>_Match<Num>_Viewer.html
 2198         
 2199     The supported input file formats are: Mol (.mol), SD (.sdf, .sd), SMILES (.smi,
 2200     .csv, .tsv, .txt)
 2201 
 2202     The supported output file formats are: SD (.sdf, .sd)
 2203 
 2204 Options:
 2205     -b, --basisSet <text>  [default: auto]
 2206         Basis set to use for energy calculation or constrained energy minimization.
 2207         Default: 6-31+G** for sulfur containing molecules; Otherwise, 6-31G** [ Ref 150 ].
 2208         The specified value must be a valid Psi4 basis set. No validation is performed.
 2209         
 2210         The following list shows a representative sample of basis sets available
 2211         in Psi4:
 2212             
 2213             STO-3G, 6-31G, 6-31+G, 6-31++G, 6-31G*, 6-31+G*,  6-31++G*, 
 2214             6-31G**, 6-31+G**, 6-31++G**, 6-311G, 6-311+G, 6-311++G,
 2215             6-311G*, 6-311+G*, 6-311++G*, 6-311G**, 6-311+G**, 6-311++G**,
 2216             cc-pVDZ, cc-pCVDZ, aug-cc-pVDZ, cc-pVDZ-DK, cc-pCVDZ-DK, def2-SVP,
 2217             def2-SVPD, def2-TZVP, def2-TZVPD, def2-TZVPP, def2-TZVPPD
 2218             
 2219     --confParams <Name,Value,...>  [default: auto]
 2220         A comma delimited list of parameter name and value pairs for generating
 2221         initial 3D coordinates for molecules in input file at specific torsion angles. A
 2222         conformation ensemble is optionally generated for each 3D structure
 2223         representing a specific torsion angle using a combination of distance geometry
 2224         and forcefield followed by constrained geometry optimization using a quantum
 2225         chemistry method. The conformation with the lowest energy is selected to
 2226         represent the torsion angle.
 2227         
 2228         The supported parameter names along with their default values are shown
 2229         below:
 2230             
 2231             confMethod,ETKDGv2,
 2232             forceField,MMFF, forceFieldMMFFVariant,MMFF94,
 2233             enforceChirality,yes,embedRMSDCutoff,0.5,maxConfs,250,
 2234             maxConfsTorsions,50,useTethers,yes
 2235             
 2236             confMethod,ETKDGv2   [ Possible values: SDG, KDG, ETDG,
 2237                 ETKDG , or ETKDGv2]
 2238             forceField, MMFF   [ Possible values: UFF or MMFF ]
 2239             forceFieldMMFFVariant,MMFF94   [ Possible values: MMFF94 or MMFF94s ]
 2240             enforceChirality,yes   [ Possible values: yes or no ]
 2241             useTethers,yes   [ Possible values: yes or no ]
 2242             
 2243         confMethod: Conformation generation methodology for generating initial 3D
 2244         coordinates. Possible values: Standard Distance Geometry (SDG), Experimental
 2245         Torsion-angle preference with Distance Geometry (ETDG), basic Knowledge-terms
 2246         with Distance Geometry (KDG) and Experimental Torsion-angle preference
 2247         along with basic Knowledge-terms and Distance Geometry (ETKDG or
 2248         ETKDGv2) [Ref 129, 167] .
 2249         
 2250         forceField: Forcefield method to use for energy minimization. Possible values:
 2251         Universal Force Field (UFF) [ Ref 81 ] or Merck Molecular Mechanics Force
 2252         Field [ Ref 83-87 ] .
 2253         
 2254         enforceChirality: Enforce chirality for defined chiral centers during
 2255         forcefield minimization.
 2256         
 2257         maxConfs: Maximum number of conformations to generate for each molecule
 2258         during the generation of an initial 3D conformation ensemble using a conformation
 2259         generation methodology. The conformations are minimized using the specified
 2260         forcefield. The lowest energy structure is selected for performing the torsion scan.
 2261         
 2262         maxConfsTorsion: Maximum number of 3D conformations to generate for
 2263         conformation ensemble representing a specific torsion. The conformations are
 2264         constrained at specific torsions angles and minimized using the specified forcefield
 2265         and a quantum chemistry method. The lowest energy conformation is selected to
 2266         calculate final torsion energy and written to the output file.
 2267         
 2268         embedRMSDCutoff: RMSD cutoff for retaining initial set of conformers embedded
 2269         using distance geometry and forcefield minimization. All embedded conformers
 2270         are kept for 'None' value. Otherwise, only those conformers which are different
 2271         from each other by the specified RMSD cutoff, 0.5 by default, are kept. The first
 2272         embedded conformer is always retained.
 2273         
 2274         useTethers: Use tethers to optimize the final embedded conformation by
 2275         applying a series of extra forces to align matching atoms to the positions of
 2276         the core atoms. Otherwise, use simple distance constraints during the
 2277         optimization.
 2278     --energyDataFieldLabel <text>  [default: auto]
 2279         Energy data field label for writing energy values. Default: Psi4_Energy (<Units>). 
 2280     --energyRelativeDataFieldLabel <text>  [default: auto]
 2281         Relative energy data field label for writing energy values. Default:
 2282         Psi4_Relative_Energy (<Units>). 
 2283     --energyUnits <text>  [default: kcal/mol]
 2284         Energy units. Possible values: Hartrees, kcal/mol, kJ/mol, or eV.
 2285     -e, --examples
 2286         Print examples.
 2287     -h, --help
 2288         Print this help message.
 2289     -i, --infile <infile>
 2290         Input file name.
 2291     --infile3D <yes or no>  [default: no]
 2292         Skip generation and minimization of initial 3D structures for molecules in
 2293         input file containing 3D coordinates.
 2294     --infileParams <Name,Value,...>  [default: auto]
 2295         A comma delimited list of parameter name and value pairs for reading
 2296         molecules from files. The supported parameter names for different file
 2297         formats, along with their default values, are shown below:
 2298             
 2299             SD, MOL: removeHydrogens,no,sanitize,yes,strictParsing,yes
 2300             
 2301             SMILES: smilesColumn,1,smilesNameColumn,2,smilesDelimiter,space,
 2302                 smilesTitleLine,auto,sanitize,yes
 2303             
 2304         Possible values for smilesDelimiter: space, comma or tab.
 2305     --maxIters <number>  [default: 50]
 2306         Maximum number of iterations to perform for each molecule or conformer
 2307         during constrained energy minimization by a quantum chemistry method.
 2308     -m, --methodName <text>  [default: auto]
 2309         Method to use for energy calculation or constrained energy minimization.
 2310         Default: B3LYP [ Ref 150 ]. The specified value must be a valid Psi4 method
 2311         name. No validation is performed.
 2312         
 2313         The following list shows a representative sample of methods available
 2314         in Psi4:
 2315             
 2316             B1LYP, B2PLYP, B2PLYP-D3BJ, B2PLYP-D3MBJ, B3LYP, B3LYP-D3BJ,
 2317             B3LYP-D3MBJ, CAM-B3LYP, CAM-B3LYP-D3BJ, HF, HF-D3BJ,  HF3c, M05,
 2318             M06, M06-2x, M06-HF, M06-L, MN12-L, MN15, MN15-D3BJ,PBE, PBE0,
 2319             PBEH3c, PW6B95, PW6B95-D3BJ, WB97, WB97X, WB97X-D, WB97X-D3BJ
 2320             
 2321     --modeMols <First or All>  [default: First]
 2322         Perform torsion scan for the first molecule or all molecules in input
 2323         file.
 2324     --modeTorsions <First or All>  [default: First]
 2325         Perform torsion scan for the first or all specified torsion pattern in
 2326         molecules up to a maximum number of matches for each torsion
 2327         specification as indicated by '--torsionMaxMatches' option. 
 2328     --mp <yes or no>  [default: no]
 2329         Use multiprocessing.
 2330          
 2331         By default, input data is retrieved in a lazy manner via mp.Pool.imap()
 2332         function employing lazy RDKit data iterable. This allows processing of
 2333         arbitrary large data sets without any additional requirements memory.
 2334         
 2335         All input data may be optionally loaded into memory by mp.Pool.map()
 2336         before starting worker processes in a process pool by setting the value
 2337         of 'inputDataMode' to 'InMemory' in '--mpParams' option.
 2338         
 2339         A word to the wise: The default 'chunkSize' value of 1 during 'Lazy' input
 2340         data mode may adversely impact the performance. The '--mpParams' section
 2341         provides additional information to tune the value of 'chunkSize'.
 2342     --mpLevel <Molecules or TorsionAngles>  [default: Molecules]
 2343         Perform multiprocessing at molecules or torsion angles level. Possible values:
 2344         Molecules or TorsionAngles. The 'Molecules' value starts a process pool at the
 2345         molecules level. All torsion angles of a molecule are processed in a single
 2346         process. The 'TorsionAngles' value, however, starts a process pool at the 
 2347         torsion angles level. Each torsion angle in a torsion match for a molecule is
 2348         processed in an individual process in the process pool.
 2349     --mpParams <Name,Value,...>  [default: auto]
 2350         A comma delimited list of parameter name and value pairs to configure
 2351         multiprocessing.
 2352         
 2353         The supported parameter names along with their default and possible
 2354         values are shown below:
 2355         
 2356             chunkSize, auto
 2357             inputDataMode, Lazy   [ Possible values: InMemory or Lazy ]
 2358             numProcesses, auto   [ Default: mp.cpu_count() ]
 2359         
 2360         These parameters are used by the following functions to configure and
 2361         control the behavior of multiprocessing: mp.Pool(), mp.Pool.map(), and
 2362         mp.Pool.imap().
 2363         
 2364         The chunkSize determines chunks of input data passed to each worker
 2365         process in a process pool by mp.Pool.map() and mp.Pool.imap() functions.
 2366         The default value of chunkSize is dependent on the value of 'inputDataMode'.
 2367         
 2368         The mp.Pool.map() function, invoked during 'InMemory' input data mode,
 2369         automatically converts RDKit data iterable into a list, loads all data into
 2370         memory, and calculates the default chunkSize using the following method
 2371         as shown in its code:
 2372         
 2373             chunkSize, extra = divmod(len(dataIterable), len(numProcesses) * 4)
 2374             if extra: chunkSize += 1
 2375         
 2376         For example, the default chunkSize will be 7 for a pool of 4 worker processes
 2377         and 100 data items.
 2378         
 2379         The mp.Pool.imap() function, invoked during 'Lazy' input data mode, employs
 2380         'lazy' RDKit data iterable to retrieve data as needed, without loading all the
 2381         data into memory. Consequently, the size of input data is not known a priori.
 2382         It's not possible to estimate an optimal value for the chunkSize. The default 
 2383         chunkSize is set to 1.
 2384         
 2385         The default value for the chunkSize during 'Lazy' data mode may adversely
 2386         impact the performance due to the overhead associated with exchanging
 2387         small chunks of data. It is generally a good idea to explicitly set chunkSize to
 2388         a larger value during 'Lazy' input data mode, based on the size of your input
 2389         data and number of processes in the process pool.
 2390         
 2391         The mp.Pool.map() function waits for all worker processes to process all
 2392         the data and return the results. The mp.Pool.imap() function, however,
 2393         returns the the results obtained from worker processes as soon as the
 2394         results become available for specified chunks of data.
 2395         
 2396         The order of data in the results returned by both mp.Pool.map() and 
 2397         mp.Pool.imap() functions always corresponds to the input data.
 2398     -o, --outfile <outfile>
 2399         Output file name. The output file root is used for generating the names
 2400         of the output files corresponding to structures, energies, and plots during
 2401         the torsion scan.
 2402     --outfileMolName <yes or no>  [default: no]
 2403         Append molecule name to output file root during the generation of the names
 2404         for output files. The default is to use <MolNum>. The non alphabetical
 2405         characters in molecule names are replaced by underscores.
 2406     --outfileParams <Name,Value,...>  [default: auto]
 2407         A comma delimited list of parameter name and value pairs for writing
 2408         molecules to files. The supported parameter names for different file
 2409         formats, along with their default values, are shown below:
 2410             
 2411             SD: kekulize,yes,forceV3000,no
 2412             
 2413     --outPlotParams <Name,Value,...>  [default: auto]
 2414         A comma delimited list of parameter name and value pairs for generating
 2415         plots using Seaborn module. The supported parameter names along with their
 2416         default values are shown below:
 2417             
 2418             type,linepoint,outExt,svg,width,10,height,5.6,
 2419             title,auto,xlabel,auto,ylabel,auto,titleWeight,bold,labelWeight,bold
 2420             style,darkgrid,palette,deep,font,sans-serif,fontScale,1,
 2421             context,notebook
 2422             
 2423         Possible values:
 2424             
 2425             type: linepoint, scatter, or line. Both points and lines are drawn
 2426                 for linepoint plot type.
 2427             outExt: Any valid format supported by Python module Matplotlib.
 2428                 For example: PDF (.pdf), PNG (.png), PS (.ps), SVG (.svg)
 2429             titleWeight, labelWeight: Font weight for title and axes labels.
 2430                 Any valid value.
 2431             style: darkgrid, whitegrid, dark, white, ticks
 2432             palette: deep, muted, pastel, dark, bright, colorblind
 2433             font: Any valid font name
 2434             context: paper, notebook, talk, poster, or any valid name
 2435             
 2436     --outPlotRelativeEnergy <yes or no>  [default: yes]
 2437         Plot relative energies in the torsion plot. The minimum energy value is
 2438         subtracted from energy values to calculate relative energies. This option
 2439         is not used during the generation of interactive energy plot for torsion
 2440         scan viewer, which always plots relative energy.
 2441     --outPlotTitleTorsionSpec <yes or no>  [default: yes]
 2442         Append torsion specification to the title of the torsion plot.
 2443     --outPlotTorsionViewerHeight <number>  [default: 430]
 2444         Plot height in pixels for interactive relative energy plot generated in
 2445         torsion scan viewer. This is different from the width and height specified
 2446         using '--outPlotParams' for the standalone plots.
 2447     --overwrite
 2448         Overwrite existing files.
 2449     --precision <number>  [default: 6]
 2450         Floating point precision for writing energy values.
 2451     --psi4OptionsParams <Name,Value,...>  [default: none]
 2452         A comma delimited list of Psi4 option name and value pairs for setting
 2453         global and module options. The names are 'option_name' for global options
 2454         and 'module_name__option_name' for options local to a module. The
 2455         specified option names must be valid Psi4 names. No validation is
 2456         performed.
 2457         
 2458         The specified option name and  value pairs are processed and passed to
 2459         psi4.set_options() as a dictionary. The supported value types are float,
 2460         integer, boolean, or string. The float value string is converted into a float.
 2461         The valid values for a boolean string are yes, no, true, false, on, or off. 
 2462     --psi4RunParams <Name,Value,...>  [default: auto]
 2463         A comma delimited list of parameter name and value pairs for configuring
 2464         Psi4 jobs.
 2465         
 2466         The supported parameter names along with their default and possible
 2467         values are shown below:
 2468              
 2469             MemoryInGB, 1
 2470             NumThreads, 1
 2471             OutputFile, auto   [ Possible  values: stdout, quiet, or FileName ]
 2472             ScratchDir, auto   [ Possivle values: DirName]
 2473             RemoveOutputFile, yes   [ Possible values: yes, no, true, or false]
 2474             
 2475         These parameters control the runtime behavior of Psi4.
 2476         
 2477         The default file name for 'OutputFile' is <InFileRoot>_Psi4.out. The PID
 2478         is appended to output file name during multiprocessing as shown:
 2479         <InFileRoot>_Psi4_<PIDNum>.out. The 'stdout' value for 'OutputType'
 2480         sends Psi4 output to stdout. The 'quiet' or 'devnull' value suppresses
 2481         all Psi4 output. The 'OutputFile' is set to 'quiet' for 'auto' value during 
 2482         'Conformers' of '--mpLevel' option.
 2483         
 2484         The default 'Yes' value of 'RemoveOutputFile' option forces the removal
 2485         of any existing Psi4 before creating new files to append output from
 2486         multiple Psi4 runs.
 2487         
 2488         The option 'ScratchDir' is a directory path to the location of scratch
 2489         files. The default value corresponds to Psi4 default. It may be used to
 2490         override the deafult path.
 2491     -q, --quiet <yes or no>  [default: no]
 2492         Use quiet mode. The warning and information messages will not be printed.
 2493     --reference <text>  [default: auto]
 2494         Reference wave function to use for energy calculation or constrained energy
 2495         minimization. Default: RHF or UHF. The default values are Restricted Hartree-Fock
 2496         (RHF) for closed-shell molecules with all electrons paired and Unrestricted
 2497         Hartree-Fock (UHF) for open-shell molecules with unpaired electrons.
 2498         
 2499         The specified value must be a valid Psi4 reference wave function. No validation
 2500         is performed. For example: ROHF, CUHF, RKS, etc.
 2501         
 2502         The spin multiplicity determines the default value of reference wave function
 2503         for input molecules. It is calculated from number of free radical electrons using
 2504         Hund's rule of maximum multiplicity defined as 2S + 1 where S is the total
 2505         electron spin. The total spin is 1/2 the number of free radical electrons in a 
 2506         molecule. The value of 'SpinMultiplicity' molecule property takes precedence
 2507         over the calculated value of spin multiplicity.
 2508     -t, --torsions <SMILES/SMARTS,...,...>
 2509         SMILES/SMARTS patterns corresponding to torsion specifications. It's a 
 2510         comma delimited list of valid SMILES/SMART patterns.
 2511         
 2512         A substructure match is performed to select torsion atoms in a molecule.
 2513         The SMILES pattern match must correspond to four torsion atoms. The
 2514         SMARTS patterns containing atom map numbers  may match  more than four
 2515         atoms. The atom map numbers, however, must match exactly four torsion
 2516         atoms. For example: [s:1][c:2]([aX2,cH1])!@[CX3:3](O)=[O:4] for thiophene
 2517         esters and carboxylates as specified in Torsion Library (TorLib) [Ref 146].
 2518     --torsionsFilterbyAtomIndices <Index1, Index2, ...>  [default: none]
 2519         Comma delimited list of atom indices for filtering torsion matches
 2520         corresponding to torsion specifications  "-t, --torsions". The atom indices
 2521         must be valid. No explicit validation is performed. The list must contain at
 2522         least 4 atom indices.
 2523         
 2524         The torsion atom indices, matched by "-t, --torsions" specifications, must be
 2525         present in the list. Otherwise, the torsion matches are ignored.
 2526     --torsionMaxMatches <number>  [default: 5]
 2527         Maximum number of torsions to match for each torsion specification in a
 2528         molecule.
 2529     --torsionMinimize <yes or no>  [default: no]
 2530         Perform constrained energy minimization on a conformation ensemble
 2531         for  a specific torsion angle and select the lowest energy conformation
 2532         representing the torsion angle. A conformation ensemble is generated for
 2533         each 3D structure representing a specific torsion angle using a combination
 2534         of distance geometry and forcefield followed by constrained geometry
 2535         optimization using a quantum chemistry method.
 2536     --torsionRangeMode <Range or Angles>  [default: Range]
 2537         Perform torsion scan using torsion angles corresponding to a torsion range
 2538         or an explicit list of torsion angles. Possible values: Range or Angles. You
 2539         may use '--torsionRange' option to specify values for torsion angle or
 2540         torsion angles.
 2541     --torsionRange <Start,Stop,Step or Angle1,Angle2...>  [default: auto]
 2542         Start, stop, and step size angles or a comma delimited list of angles in
 2543         degrees for a torsion scan.
 2544         
 2545         This value is '--torsionRangeMode' specific. It must be a triplet corresponding
 2546         to 'start,Stop,Step' for 'Range' value of '--torsionRange' option. Otherwise, it
 2547         is comma delimited list of one or more torsion angles for 'Angles' value of
 2548         '--torsionRange' option.
 2549         
 2550         The default values, based on '--torsionRangeMode' option, are shown below:
 2551             
 2552             TorsionRangeMode       Default value
 2553             Range                  0,360,5
 2554             Angles                 None
 2555             
 2556         You must explicitly provide a list of  torsion angle(s) for 'Angles' of
 2557         '--torsionRangeMode' option.
 2558     --useChirality <yes or no>  [default: no]
 2559         Use chirrality during substructure matches for identification of torsions.
 2560     -w, --workingdir <dir>
 2561         Location of working directory which defaults to the current directory.
 2562 
 2563 Examples:
 2564     To perform a torsion scan on the first molecule in a SMILES file using a minimum
 2565     energy structure of the molecule selected from an initial ensemble of conformations
 2566     generated using distance geometry and forcefield, skip generation of conformation
 2567     ensembles for specific torsion angles and constrained energy minimization of the
 2568     ensemble, calculating single point at a specific torsion angle energy using B3LYP/6-31G**
 2569     and B3LYP/6-31+G** for non-sulfur and sulfur containing molecules, generate output files
 2570     corresponding to structure, energy and torsion plot, type:
 2571     
 2572         % Psi4PerformTorsionScan.py  -t "CCCC" -i Psi4SampleTorsionScan.smi 
 2573           -o SampleOut.sdf
 2574 
 2575     To run the previous example for performing a torsion scan using a specific list
 2576     of torsion angles, type:
 2577     
 2578         % Psi4PerformTorsionScan.py  -t "CCCC" -i Psi4SampleTorsionScan.smi 
 2579           -o SampleOut.sdf --torsionRangeMode Angles
 2580           --torsionRange "0,180,360"
 2581 
 2582     To run the previous example on the first molecule in a SD file containing 3D
 2583     coordinates and skip the generations of initial 3D structure, type: 
 2584     
 2585         % Psi4PerformTorsionScan.py  -t "CCCC"  --infile3D yes
 2586           -i Psi4SampleTorsionScan3D.sdf  -o SampleOut.sdf
 2587 
 2588     To run the first example on all molecules in a SD file, type:
 2589     
 2590         % Psi4PerformTorsionScan.py  -t "CCCC" --modeMols All
 2591           -i Psi4SampleTorsionScan.sdf -o SampleOut.sdf
 2592 
 2593     To run the first example on all molecules in a SD file containing 3D
 2594     coordinates and skip the generation of initial 3D structures, type: 
 2595     
 2596         % Psi4PerformTorsionScan.py  -t "CCCC"  --infile3D yes
 2597           --modeMols All -i Psi4SampleTorsionScan3D.sdf  -o SampleOut.sdf
 2598 
 2599     To perform a torsion scan on the first molecule in a SMILES file using a minimum
 2600     energy structure of the molecule selected from an initial ensemble of conformations
 2601     generated using distance geometry and forcefield,  generate up to 50 conformations
 2602     for specific torsion angles using ETKDGv2 methodology followed by initial MMFF
 2603     forcefield minimization and final energy minimization using B3LYP/6-31G** and
 2604     B3LYP/6-31+G** for non-sulfur and sulfur containing molecules, generate output files
 2605     corresponding to minimum energy structure, energy and torsion plot, type:
 2606 
 2607         % Psi4PerformTorsionScan.py  -t "CCCC" --torsionMinimize Yes
 2608            -i Psi4SampleTorsionScan.smi -o SampleOut.sdf
 2609 
 2610     To run the previous example on all molecules in a SD file, type:
 2611     
 2612         % Psi4PerformTorsionScan.py  -t "CCCC" --modeMols All
 2613            --torsionMinimize Yes -i Psi4SampleTorsionScan.sdf -o SampleOut.sdf
 2614 
 2615     To run the previous example on all molecules in a SD file containing 3D
 2616     coordinates and skip the generation of initial 3D structures, type:
 2617     
 2618         % Psi4PerformTorsionScan.py  -t "CCCC" --modeMols All
 2619            --infile3D yes --modeMols All  --torsionMinimize Yes
 2620            -i Psi4SampleTorsionScan.sdf -o SampleOut.sdf
 2621 
 2622     To run the previous example in multiprocessing mode at molecules level
 2623     on all available CPUs without loading all data into memory and write out
 2624     a SD file, type:
 2625 
 2626         % Psi4PerformTorsionScan.py  -t "CCCC" -i Psi4SampleTorsionScan.smi 
 2627           -o SampleOut.sdf --modeMols All --torsionMinimize Yes --mp yes
 2628     
 2629     To run the previous example in multiprocessing mode at torsion angles level
 2630     on all available CPUs without loading all data into memory and write out
 2631     a SD file, type:
 2632 
 2633         % Psi4PerformTorsionScan.py  -t "CCCC" -i Psi4SampleTorsionScan.smi 
 2634           -o SampleOut.sdf --modeMols All --torsionMinimize Yes --mp yes
 2635           --mpLevel TorsionAngles
 2636     
 2637     To run the previous example in multiprocessing mode on all available CPUs
 2638     by loading all data into memory and write out a SD file, type:
 2639 
 2640         % Psi4PerformTorsionScan.py  -t "CCCC" -i Psi4SampleTorsionScan.smi 
 2641           -o SampleOut.sdf --modeMols All --torsionMinimize Yes --mp yes
 2642           --mpParams "inputDataMode,InMemory"
 2643     
 2644     To run the previous example in multiprocessing mode on specific number of
 2645     CPUs and chunk size without loading all data into memory and write out a SD file,
 2646     type:
 2647 
 2648         % Psi4PerformTorsionScan.py  -t "CCCC" -i Psi4SampleTorsionScan.smi 
 2649           -o SampleOut.sdf --modeMols All --torsionMinimize Yes --mp yes
 2650           --mpParams "inputDataMode,Lazy,numProcesses,4,chunkSize,8"
 2651 
 2652 Author:
 2653     Manish Sud(msud@san.rr.com)
 2654 
 2655 Acknowledgment:
 2656     Pat Walters
 2657 
 2658 See also:
 2659     Psi4CalculateEnergy.py, Psi4GenerateConformers.py,
 2660     Psi4GenerateConstrainedConformers.py, Psi4PerformConstrainedMinimization.py
 2661 
 2662 Copyright:
 2663     Copyright (C) 2026 Manish Sud. All rights reserved.
 2664 
 2665     The functionality available in this script is implemented using RDKit, an
 2666     open source toolkit for cheminformatics developed by Greg Landrum.
 2667 
 2668     This file is part of MayaChemTools.
 2669 
 2670     MayaChemTools is free software; you can redistribute it and/or modify it under
 2671     the terms of the GNU Lesser General Public License as published by the Free
 2672     Software Foundation; either version 3 of the License, or (at your option) any
 2673     later version.
 2674 
 2675 """
 2676 
 2677 if __name__ == "__main__":
 2678     main()