MayaChemTools

    1 #!/bin/env python
    2 #
    3 # File: Psi4GenerateConformers.py
    4 # Author: Manish Sud <msud@san.rr.com>
    5 #
    6 # Copyright (C) 2026 Manish Sud. All rights reserved.
    7 #
    8 # The functionality available in this script is implemented using Psi4, an
    9 # open source quantum chemistry software package, and RDKit, an open
   10 # source toolkit for cheminformatics developed by Greg Landrum.
   11 #
   12 # This file is part of MayaChemTools.
   13 #
   14 # MayaChemTools is free software; you can redistribute it and/or modify it under
   15 # the terms of the GNU Lesser General Public License as published by the Free
   16 # Software Foundation; either version 3 of the License, or (at your option) any
   17 # later version.
   18 #
   19 # MayaChemTools is distributed in the hope that it will be useful, but without
   20 # any warranty; without even the implied warranty of merchantability of fitness
   21 # for a particular purpose.  See the GNU Lesser General Public License for more
   22 # details.
   23 #
   24 # You should have received a copy of the GNU Lesser General Public License
   25 # along with MayaChemTools; if not, see <http://www.gnu.org/licenses/> or
   26 # write to the Free Software Foundation Inc., 59 Temple Place, Suite 330,
   27 # Boston, MA, 02111-1307, USA.
   28 #
   29 
   30 from __future__ import print_function
   31 
   32 import os
   33 import sys
   34 import time
   35 import re
   36 import shutil
   37 import multiprocessing as mp
   38 
   39 # Psi4 imports...
   40 if hasattr(shutil, "which") and shutil.which("psi4") is None:
   41     sys.stderr.write("\nWarning: Failed to find 'psi4' in your PATH indicating potential issues with your\n")
   42     sys.stderr.write("Psi4 environment. The 'import psi4' directive in the global scope of the script\n")
   43     sys.stderr.write("interferes with the multiprocessing functionality. It is imported later in the\n")
   44     sys.stderr.write("local scope during the execution of the script and may fail. Check/update your\n")
   45     sys.stderr.write("Psi4 environment and try again.\n\n")
   46 
   47 # RDKit imports...
   48 try:
   49     from rdkit import rdBase
   50     from rdkit import Chem
   51     from rdkit.Chem import AllChem
   52 except ImportError as ErrMsg:
   53     sys.stderr.write("\nFailed to import RDKit module/package: %s\n" % ErrMsg)
   54     sys.stderr.write("Check/update your RDKit environment and try again.\n\n")
   55     sys.exit(1)
   56 
   57 # MayaChemTools imports...
   58 sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), "..", "lib", "Python"))
   59 try:
   60     from docopt import docopt
   61     import MiscUtil
   62     import Psi4Util
   63     import RDKitUtil
   64 except ImportError as ErrMsg:
   65     sys.stderr.write("\nFailed to import MayaChemTools module/package: %s\n" % ErrMsg)
   66     sys.stderr.write("Check/update your MayaChemTools environment and try again.\n\n")
   67     sys.exit(1)
   68 
   69 ScriptName = os.path.basename(sys.argv[0])
   70 Options = {}
   71 OptionsInfo = {}
   72 
   73 
   74 def main():
   75     """Start execution of the script."""
   76 
   77     MiscUtil.PrintInfo(
   78         "\n%s (Psi4: Imported later; RDKit v%s; MayaChemTools v%s; %s): Starting...\n"
   79         % (ScriptName, rdBase.rdkitVersion, MiscUtil.GetMayaChemToolsVersion(), time.asctime())
   80     )
   81 
   82     (WallClockTime, ProcessorTime) = MiscUtil.GetWallClockAndProcessorTime()
   83 
   84     # Retrieve command line arguments and options...
   85     RetrieveOptions()
   86 
   87     # Process and validate command line arguments and options...
   88     ProcessOptions()
   89 
   90     # Perform actions required by the script...
   91     GenerateConformers()
   92 
   93     MiscUtil.PrintInfo("\n%s: Done...\n" % ScriptName)
   94     MiscUtil.PrintInfo("Total time: %s" % MiscUtil.GetFormattedElapsedTime(WallClockTime, ProcessorTime))
   95 
   96 
   97 def GenerateConformers():
   98     """Generate conformers."""
   99 
  100     # Setup a molecule reader...
  101     MiscUtil.PrintInfo("\nProcessing file %s..." % OptionsInfo["Infile"])
  102     Mols = RDKitUtil.ReadMolecules(OptionsInfo["Infile"], **OptionsInfo["InfileParams"])
  103 
  104     # Set up a molecule writer...
  105     Writer = RDKitUtil.MoleculesWriter(OptionsInfo["Outfile"], **OptionsInfo["OutfileParams"])
  106     if Writer is None:
  107         MiscUtil.PrintError("Failed to setup a writer for output fie %s " % OptionsInfo["Outfile"])
  108     MiscUtil.PrintInfo("Generating file %s..." % OptionsInfo["Outfile"])
  109 
  110     MolCount, ValidMolCount, ConfGenFailedCount = ProcessMolecules(Mols, Writer)
  111 
  112     if Writer is not None:
  113         Writer.close()
  114 
  115     MiscUtil.PrintInfo("\nTotal number of molecules: %d" % MolCount)
  116     MiscUtil.PrintInfo("Number of valid molecules: %d" % ValidMolCount)
  117     MiscUtil.PrintInfo(
  118         "Number of molecules failed during conformation generation or minimization: %d" % ConfGenFailedCount
  119     )
  120     MiscUtil.PrintInfo("Number of ignored molecules: %d" % (MolCount - ValidMolCount + ConfGenFailedCount))
  121 
  122 
  123 def ProcessMolecules(Mols, Writer):
  124     """Process molecules to generate conformers."""
  125 
  126     if OptionsInfo["MPMode"]:
  127         return ProcessMoleculesUsingMultipleProcesses(Mols, Writer)
  128     else:
  129         return ProcessMoleculesUsingSingleProcess(Mols, Writer)
  130 
  131 
  132 def ProcessMoleculesUsingSingleProcess(Mols, Writer):
  133     """Process molecules to generate conformers using a single process."""
  134 
  135     # Intialize Psi4...
  136     MiscUtil.PrintInfo("\nInitializing Psi4...")
  137     Psi4Handle = Psi4Util.InitializePsi4(
  138         Psi4RunParams=OptionsInfo["Psi4RunParams"],
  139         Psi4OptionsParams=OptionsInfo["Psi4OptionsParams"],
  140         PrintVersion=True,
  141         PrintHeader=True,
  142     )
  143     OptionsInfo["psi4"] = Psi4Handle
  144 
  145     # Setup max iterations global variable...
  146     Psi4Util.UpdatePsi4OptionsParameters(Psi4Handle, {"GEOM_MAXITER": OptionsInfo["MaxIters"]})
  147 
  148     # Setup conversion factor for energy units...
  149     SetupEnergyConversionFactor(Psi4Handle)
  150 
  151     MiscUtil.PrintInfo("\nGenerating conformers and performing energy minimization...")
  152 
  153     (MolCount, ValidMolCount, ConfGenFailedCount) = [0] * 3
  154     for Mol in Mols:
  155         MolCount += 1
  156 
  157         if not CheckAndValidateMolecule(Mol, MolCount):
  158             continue
  159 
  160         # Setup 2D coordinates for SMILES input file...
  161         if OptionsInfo["SMILESInfileStatus"]:
  162             AllChem.Compute2DCoords(Mol)
  163 
  164         ValidMolCount += 1
  165 
  166         ConformerMol, CalcStatus, ConfIDs, ConfEnergies = GenerateMolConformers(Psi4Handle, Mol, MolCount)
  167 
  168         if not CalcStatus:
  169             if not OptionsInfo["QuietMode"]:
  170                 MiscUtil.PrintWarning(
  171                     "Failed to calculate energy for molecule %s" % RDKitUtil.GetMolName(Mol, MolCount)
  172                 )
  173 
  174             ConfGenFailedCount += 1
  175             continue
  176 
  177         WriteMolConformers(Writer, ConformerMol, MolCount, ConfIDs, ConfEnergies)
  178 
  179     return (MolCount, ValidMolCount, ConfGenFailedCount)
  180 
  181 
  182 def ProcessMoleculesUsingMultipleProcesses(Mols, Writer):
  183     """Process and minimize molecules using multiprocessing."""
  184 
  185     if OptionsInfo["MPLevelConformersMode"]:
  186         return ProcessMoleculesUsingMultipleProcessesAtConformersLevel(Mols, Writer)
  187     elif OptionsInfo["MPLevelMoleculesMode"]:
  188         return ProcessMoleculesUsingMultipleProcessesAtMoleculesLevel(Mols, Writer)
  189     else:
  190         MiscUtil.PrintError('The value, %s,  option "--mpLevel" is not supported.' % (OptionsInfo["MPLevel"]))
  191 
  192 
  193 def ProcessMoleculesUsingMultipleProcessesAtMoleculesLevel(Mols, Writer):
  194     """Process molecules to generate conformers using multiprocessing at molecules level."""
  195 
  196     MiscUtil.PrintInfo(
  197         "\nGenerating conformers and performing energy minimization using multiprocessing at molecules level..."
  198     )
  199 
  200     MPParams = OptionsInfo["MPParams"]
  201 
  202     # Setup data for initializing a worker process...
  203     InitializeWorkerProcessArgs = (
  204         MiscUtil.ObjectToBase64EncodedString(Options),
  205         MiscUtil.ObjectToBase64EncodedString(OptionsInfo),
  206     )
  207 
  208     # Setup a encoded mols data iterable for a worker process...
  209     WorkerProcessDataIterable = RDKitUtil.GenerateBase64EncodedMolStrings(Mols)
  210 
  211     # Setup process pool along with data initialization for each process...
  212     if not OptionsInfo["QuietMode"]:
  213         MiscUtil.PrintInfo(
  214             "\nConfiguring multiprocessing using %s method..."
  215             % ("mp.Pool.imap()" if re.match("^Lazy$", MPParams["InputDataMode"], re.I) else "mp.Pool.map()")
  216         )
  217         MiscUtil.PrintInfo(
  218             "NumProcesses: %s; InputDataMode: %s; ChunkSize: %s\n"
  219             % (
  220                 MPParams["NumProcesses"],
  221                 MPParams["InputDataMode"],
  222                 ("automatic" if MPParams["ChunkSize"] is None else MPParams["ChunkSize"]),
  223             )
  224         )
  225 
  226     ProcessPool = mp.Pool(MPParams["NumProcesses"], InitializeWorkerProcess, InitializeWorkerProcessArgs)
  227 
  228     # Start processing...
  229     if re.match("^Lazy$", MPParams["InputDataMode"], re.I):
  230         Results = ProcessPool.imap(WorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"])
  231     elif re.match("^InMemory$", MPParams["InputDataMode"], re.I):
  232         Results = ProcessPool.map(WorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"])
  233     else:
  234         MiscUtil.PrintError(
  235             'The value, %s, specified for "--inputDataMode" is not supported.' % (MPParams["InputDataMode"])
  236         )
  237 
  238     # Print out Psi4 version in the main process...
  239     MiscUtil.PrintInfo("\nInitializing Psi4...\n")
  240     Psi4Handle = Psi4Util.InitializePsi4(PrintVersion=True, PrintHeader=False)
  241     OptionsInfo["psi4"] = Psi4Handle
  242 
  243     (MolCount, ValidMolCount, ConfGenFailedCount) = [0] * 3
  244     for Result in Results:
  245         MolCount += 1
  246         MolIndex, EncodedMol, CalcStatus, ConfIDs, ConfEnergies = Result
  247 
  248         if EncodedMol is None:
  249             continue
  250         ValidMolCount += 1
  251 
  252         if not CalcStatus:
  253             ConfGenFailedCount += 1
  254             continue
  255 
  256         Mol = RDKitUtil.MolFromBase64EncodedMolString(EncodedMol)
  257         WriteMolConformers(Writer, Mol, MolCount, ConfIDs, ConfEnergies)
  258 
  259     return (MolCount, ValidMolCount, ConfGenFailedCount)
  260 
  261 
  262 def InitializeWorkerProcess(*EncodedArgs):
  263     """Initialize data for a worker process."""
  264 
  265     global Options, OptionsInfo
  266 
  267     if not OptionsInfo["QuietMode"]:
  268         MiscUtil.PrintInfo("Starting process (PID: %s)..." % os.getpid())
  269 
  270     # Decode Options and OptionInfo...
  271     Options = MiscUtil.ObjectFromBase64EncodedString(EncodedArgs[0])
  272     OptionsInfo = MiscUtil.ObjectFromBase64EncodedString(EncodedArgs[1])
  273 
  274     # Psi4 is initialized in the worker process to avoid creation of redundant Psi4
  275     # output files for each process...
  276     OptionsInfo["Psi4Initialized"] = False
  277 
  278 
  279 def WorkerProcess(EncodedMolInfo):
  280     """Process data for a worker process."""
  281 
  282     if not OptionsInfo["Psi4Initialized"]:
  283         InitializePsi4ForWorkerProcess()
  284 
  285     MolIndex, EncodedMol = EncodedMolInfo
  286     MolNum = MolIndex + 1
  287 
  288     CalcStatus = False
  289     ConfIDs = None
  290     ConfEnergies = None
  291 
  292     if EncodedMol is None:
  293         return [MolIndex, None, CalcStatus, ConfIDs, ConfEnergies]
  294 
  295     Mol = RDKitUtil.MolFromBase64EncodedMolString(EncodedMol)
  296 
  297     if not CheckAndValidateMolecule(Mol, MolNum):
  298         return [MolIndex, None, CalcStatus, ConfIDs, ConfEnergies]
  299 
  300     # Setup 2D coordinates for SMILES input file...
  301     if OptionsInfo["SMILESInfileStatus"]:
  302         AllChem.Compute2DCoords(Mol)
  303 
  304     Mol, CalcStatus, ConfIDs, ConfEnergies = GenerateMolConformers(OptionsInfo["psi4"], Mol, MolNum)
  305 
  306     return [
  307         MolIndex,
  308         RDKitUtil.MolToBase64EncodedMolString(
  309             Mol, PropertyPickleFlags=Chem.PropertyPickleOptions.MolProps | Chem.PropertyPickleOptions.PrivateProps
  310         ),
  311         CalcStatus,
  312         ConfIDs,
  313         ConfEnergies,
  314     ]
  315 
  316 
  317 def ProcessMoleculesUsingMultipleProcessesAtConformersLevel(Mols, Writer):
  318     """Process molecules to generate conformers using multiprocessing at conformers level."""
  319 
  320     MiscUtil.PrintInfo(
  321         "\nPerforming minimization with generation of conformers using multiprocessing at conformers level..."
  322     )
  323 
  324     (MolCount, ValidMolCount, ConfGenFailedCount) = [0] * 3
  325     for Mol in Mols:
  326         MolCount += 1
  327 
  328         if not CheckAndValidateMolecule(Mol, MolCount):
  329             continue
  330 
  331         # Setup 2D coordinates for SMILES input file...
  332         if OptionsInfo["SMILESInfileStatus"]:
  333             AllChem.Compute2DCoords(Mol)
  334 
  335         ValidMolCount += 1
  336 
  337         Mol, CalcStatus, ConfIDs, ConfEnergies = ProcessConformersUsingMultipleProcesses(Mol, MolCount)
  338 
  339         if not CalcStatus:
  340             ConfGenFailedCount += 1
  341             continue
  342 
  343         WriteMolConformers(Writer, Mol, MolCount, ConfIDs, ConfEnergies)
  344 
  345     return (MolCount, ValidMolCount, ConfGenFailedCount)
  346 
  347 
  348 def ProcessConformersUsingMultipleProcesses(Mol, MolNum):
  349     """Generate coformers and minimize them using multiple processes."""
  350 
  351     # Add hydrogens...
  352     Mol = Chem.AddHs(Mol)
  353 
  354     # Setup conformers...
  355     ConfIDs = EmbedMolecule(Mol, MolNum)
  356     if not len(ConfIDs):
  357         if not OptionsInfo["QuietMode"]:
  358             MolName = RDKitUtil.GetMolName(Mol, MolNum)
  359             MiscUtil.PrintWarning("Minimization couldn't be performed for molecule %s: Embedding failed...\n" % MolName)
  360         return (Mol, False, None, None)
  361 
  362     MPParams = OptionsInfo["MPParams"]
  363 
  364     # Setup data for initializing a worker process...
  365     InitializeWorkerProcessArgs = (
  366         MiscUtil.ObjectToBase64EncodedString(Options),
  367         MiscUtil.ObjectToBase64EncodedString(OptionsInfo),
  368     )
  369 
  370     # Setup a encoded mols data iterable for a worker process...
  371     MolIndex = MolNum - 1
  372     WorkerProcessDataIterable = RDKitUtil.GenerateBase64EncodedMolStringWithConfIDs(Mol, MolIndex, ConfIDs)
  373 
  374     # Setup process pool along with data initialization for each process...
  375     if not OptionsInfo["QuietMode"]:
  376         MiscUtil.PrintInfo(
  377             "\nConfiguring multiprocessing using %s method..."
  378             % ("mp.Pool.imap()" if re.match("^Lazy$", MPParams["InputDataMode"], re.I) else "mp.Pool.map()")
  379         )
  380         MiscUtil.PrintInfo(
  381             "NumProcesses: %s; InputDataMode: %s; ChunkSize: %s\n"
  382             % (
  383                 MPParams["NumProcesses"],
  384                 MPParams["InputDataMode"],
  385                 ("automatic" if MPParams["ChunkSize"] is None else MPParams["ChunkSize"]),
  386             )
  387         )
  388 
  389     ProcessPool = mp.Pool(MPParams["NumProcesses"], InitializeConformerWorkerProcess, InitializeWorkerProcessArgs)
  390 
  391     # Start processing...
  392     if re.match("^Lazy$", MPParams["InputDataMode"], re.I):
  393         Results = ProcessPool.imap(ConformerWorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"])
  394     elif re.match("^InMemory$", MPParams["InputDataMode"], re.I):
  395         Results = ProcessPool.map(ConformerWorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"])
  396     else:
  397         MiscUtil.PrintError(
  398             'The value, %s, specified for "--inputDataMode" is not supported.' % (MPParams["InputDataMode"])
  399         )
  400 
  401     CalcEnergyMap = {}
  402     CalcFailedCount = 0
  403     for Result in Results:
  404         MolIndex, EncodedMol, CalcStatus, ConfID, Energy = Result
  405 
  406         if EncodedMol is None:
  407             CalcFailedCount += 1
  408             continue
  409 
  410         if not CalcStatus:
  411             CalcFailedCount += 1
  412             continue
  413 
  414         # Retrieve minimized atom positions...
  415         MinimizedMol = RDKitUtil.MolFromBase64EncodedMolString(EncodedMol)
  416         AtomPositions = RDKitUtil.GetAtomPositions(MinimizedMol, ConfID=ConfID)
  417 
  418         # Update atom positions...
  419         RDKitUtil.SetAtomPositions(Mol, AtomPositions, ConfID=ConfID)
  420 
  421         CalcEnergyMap[ConfID] = Energy
  422 
  423     if CalcFailedCount:
  424         return (Mol, False, None, None)
  425 
  426     # Align molecules after minimization...
  427     if OptionsInfo["ConfGenerationParams"]["AlignConformers"]:
  428         AllChem.AlignMolConformers(Mol)
  429 
  430     # Filter conformers...
  431     SelectedConfIDs, SelectedConfEnergies = FilterMolConformers(Mol, MolNum, ConfIDs, CalcEnergyMap)
  432 
  433     return [Mol, True, SelectedConfIDs, SelectedConfEnergies]
  434 
  435 
  436 def InitializeConformerWorkerProcess(*EncodedArgs):
  437     """Initialize data for a conformer worker process."""
  438 
  439     global Options, OptionsInfo
  440 
  441     if not OptionsInfo["QuietMode"]:
  442         MiscUtil.PrintInfo("Starting process (PID: %s)..." % os.getpid())
  443 
  444     # Decode Options and OptionInfo...
  445     Options = MiscUtil.ObjectFromBase64EncodedString(EncodedArgs[0])
  446     OptionsInfo = MiscUtil.ObjectFromBase64EncodedString(EncodedArgs[1])
  447 
  448     # Psi4 is initialized in the worker process to avoid creation of redundant Psi4
  449     # output files for each process...
  450     OptionsInfo["Psi4Initialized"] = False
  451 
  452 
  453 def ConformerWorkerProcess(EncodedMolInfo):
  454     """Process data for a conformer worker process."""
  455 
  456     if not OptionsInfo["Psi4Initialized"]:
  457         InitializePsi4ForWorkerProcess()
  458 
  459     MolIndex, EncodedMol, ConfID = EncodedMolInfo
  460 
  461     MolNum = MolIndex + 1
  462 
  463     CalcStatus = False
  464     Energy = None
  465 
  466     if EncodedMol is None:
  467         return [MolIndex, None, CalcStatus, ConfID, Energy]
  468 
  469     Mol = RDKitUtil.MolFromBase64EncodedMolString(EncodedMol)
  470     MolName = RDKitUtil.GetMolName(Mol, MolNum)
  471 
  472     if not OptionsInfo["QuietMode"]:
  473         MiscUtil.PrintInfo("Processing conformer ID %s for molecule %s..." % (ConfID, MolName))
  474 
  475     Status, ConvergeStatus = MinimizeMoleculeUsingForceField(Mol, MolNum, ConfID)
  476     if not Status:
  477         return [MolIndex, EncodedMol, CalcStatus, ConfID, Energy]
  478 
  479     if ConvergeStatus != 0:
  480         if not OptionsInfo["QuietMode"]:
  481             MolName = RDKitUtil.GetMolName(Mol, (MolIndex + 1))
  482             MiscUtil.PrintWarning(
  483                 'Minimization using forcefield failed to converge for molecule %s in %d steps. Try using higher value for "maxIters" in "--confParams" option...\n'
  484                 % (MolName, OptionsInfo["ConfGenerationParams"]["MaxIters"])
  485             )
  486 
  487     # Perform Psi4 minimization...
  488     CalcStatus, Energy = MinimizeMoleculeUsingPsi4(OptionsInfo["psi4"], Mol, MolNum, ConfID)
  489     if not CalcStatus:
  490         if not OptionsInfo["QuietMode"]:
  491             MiscUtil.PrintWarning("Minimization couldn't be performed for molecule %s\n" % (MolName))
  492             return [MolIndex, EncodedMol, False, ConfID, Energy]
  493 
  494     return [
  495         MolIndex,
  496         RDKitUtil.MolToBase64EncodedMolString(
  497             Mol, PropertyPickleFlags=Chem.PropertyPickleOptions.MolProps | Chem.PropertyPickleOptions.PrivateProps
  498         ),
  499         CalcStatus,
  500         ConfID,
  501         Energy,
  502     ]
  503 
  504 
  505 def InitializePsi4ForWorkerProcess():
  506     """Initialize Psi4 for a worker process."""
  507 
  508     if OptionsInfo["Psi4Initialized"]:
  509         return
  510 
  511     OptionsInfo["Psi4Initialized"] = True
  512 
  513     if OptionsInfo["MPLevelConformersMode"] and re.match(
  514         "auto", OptionsInfo["Psi4RunParams"]["OutputFileSpecified"], re.I
  515     ):
  516         # Run Psi4 in quiet mode during multiprocessing at Conformers level for 'auto' OutputFile...
  517         OptionsInfo["Psi4RunParams"]["OutputFile"] = "quiet"
  518     else:
  519         # Update output file...
  520         OptionsInfo["Psi4RunParams"]["OutputFile"] = Psi4Util.UpdatePsi4OutputFileUsingPID(
  521             OptionsInfo["Psi4RunParams"]["OutputFile"], os.getpid()
  522         )
  523 
  524     # Intialize Psi4...
  525     OptionsInfo["psi4"] = Psi4Util.InitializePsi4(
  526         Psi4RunParams=OptionsInfo["Psi4RunParams"],
  527         Psi4OptionsParams=OptionsInfo["Psi4OptionsParams"],
  528         PrintVersion=False,
  529         PrintHeader=True,
  530     )
  531 
  532     # Setup max iterations global variable...
  533     Psi4Util.UpdatePsi4OptionsParameters(OptionsInfo["psi4"], {"GEOM_MAXITER": OptionsInfo["MaxIters"]})
  534 
  535     # Setup conversion factor for energy units...
  536     SetupEnergyConversionFactor(OptionsInfo["psi4"])
  537 
  538 
  539 def GenerateMolConformers(Psi4Handle, Mol, MolNum):
  540     """Generate and mininize conformers for a molecule."""
  541 
  542     # Add hydrogens..
  543     Mol = Chem.AddHs(Mol)
  544 
  545     MolName = RDKitUtil.GetMolName(Mol, MolNum)
  546 
  547     # Setup conformers...
  548     ConfIDs = EmbedMolecule(Mol, MolNum)
  549     if not len(ConfIDs):
  550         if not OptionsInfo["QuietMode"]:
  551             MiscUtil.PrintWarning(
  552                 "Conformation generation couldn't be performed for molecule %s: Embedding failed...\n" % MolName
  553             )
  554         return (Mol, False, None, None)
  555 
  556     # Minimize conformers...
  557     CalcEnergyMap = {}
  558     for ConfID in ConfIDs:
  559         if not OptionsInfo["QuietMode"]:
  560             MiscUtil.PrintInfo("Processing conformer ID %s for molecule %s..." % (ConfID, MolName))
  561 
  562         # Perform forcefield minimization...
  563         Status, ConvergeStatus = MinimizeMoleculeUsingForceField(Mol, MolNum, ConfID)
  564         if not Status:
  565             return (Mol, False, None, None)
  566 
  567         if ConvergeStatus != 0:
  568             if not OptionsInfo["QuietMode"]:
  569                 MiscUtil.PrintWarning(
  570                     'Minimization using forcefield failed to converge for molecule %s in %d steps. Try using higher value for "maxIters" in "--confParams" option...\n'
  571                     % (MolName, OptionsInfo["ConfGenerationParams"]["MaxIters"])
  572                 )
  573 
  574         # Perform Psi4 minimization...
  575         CalcStatus, Energy = MinimizeMoleculeUsingPsi4(Psi4Handle, Mol, MolNum, ConfID)
  576         if not CalcStatus:
  577             if not OptionsInfo["QuietMode"]:
  578                 MiscUtil.PrintWarning("Minimization couldn't be performed for molecule %s\n" % (MolName))
  579             return (Mol, False, None, None)
  580 
  581         CalcEnergyMap[ConfID] = Energy
  582 
  583     # Align molecules after minimization...
  584     if OptionsInfo["ConfGenerationParams"]["AlignConformers"]:
  585         AllChem.AlignMolConformers(Mol)
  586 
  587     # Filter conformers...
  588     SelectedConfIDs, SelectedConfEnergies = FilterMolConformers(Mol, MolNum, ConfIDs, CalcEnergyMap)
  589 
  590     return [Mol, True, SelectedConfIDs, SelectedConfEnergies]
  591 
  592 
  593 def FilterMolConformers(Mol, MolNum, ConfIDs, CalcEnergyMap):
  594     """Filter conformers for a molecule."""
  595 
  596     SortedConfIDs = sorted(ConfIDs, key=lambda ConfID: CalcEnergyMap[ConfID])
  597 
  598     MinEnergyConfID = SortedConfIDs[0]
  599     MinConfEnergy = CalcEnergyMap[MinEnergyConfID]
  600     EnergyWindow = OptionsInfo["EnergyWindow"]
  601 
  602     EnergyRMSDCutoff = OptionsInfo["EnergyRMSDCutoff"]
  603     ApplyEnergyRMSDCutoff = False
  604     if EnergyRMSDCutoff > 0:
  605         ApplyEnergyRMSDCutoff = True
  606 
  607     EnergyRMSDCutoffLowest = OptionsInfo["EnergyRMSDCutoffModeLowest"]
  608     EnergyRMSDCalcModeBest = OptionsInfo["EnergyRMSDCalcModeBest"]
  609 
  610     PreAligned = False
  611     if OptionsInfo["ConfGenerationParams"]["AlignConformers"]:
  612         PreAligned = True
  613 
  614     RefMol, ProbeMol = [None] * 2
  615     if EnergyRMSDCalcModeBest:
  616         # Copy molecules for best RMSD calculations to avoid change in the coordinates
  617         # of the conformations...
  618         RefMol = AllChem.Mol(Mol)
  619         ProbeMol = AllChem.Mol(Mol)
  620 
  621     # Track conformers with in the specified energy window  from the lowest
  622     # energy conformation along with applying RMSD cutoff as needed...
  623     #
  624     SelectedConfIDs = []
  625 
  626     ConfCount = 0
  627     IgnoredByEnergyConfCount = 0
  628     IgnoredByRMSDConfCount = 0
  629 
  630     FirstConf = True
  631 
  632     for ConfID in SortedConfIDs:
  633         if FirstConf:
  634             FirstConf = False
  635             ConfCount += 1
  636             SelectedConfIDs.append(ConfID)
  637             continue
  638 
  639         ConfEnergyDiff = abs(CalcEnergyMap[ConfID] - MinConfEnergy)
  640         if ConfEnergyDiff > EnergyWindow:
  641             IgnoredByEnergyConfCount += 1
  642             continue
  643 
  644         if ApplyEnergyRMSDCutoff:
  645             IgnoreConf = False
  646             if EnergyRMSDCutoffLowest:
  647                 # Compare RMSD with the lowest energy conformation...
  648                 if EnergyRMSDCalcModeBest:
  649                     CalcRMSD = AllChem.GetBestRMS(ProbeMol, RefMol, prbId=ConfID, refId=MinEnergyConfID)
  650                 else:
  651                     CalcRMSD = AllChem.GetConformerRMS(Mol, MinEnergyConfID, ConfID, prealigned=PreAligned)
  652                 if CalcRMSD < EnergyRMSDCutoff:
  653                     IgnoreConf = True
  654             else:
  655                 for SelectedConfID in SelectedConfIDs:
  656                     if EnergyRMSDCalcModeBest:
  657                         CalcRMSD = AllChem.GetBestRMS(ProbeMol, RefMol, prbId=ConfID, refId=SelectedConfID)
  658                     else:
  659                         CalcRMSD = AllChem.GetConformerRMS(Mol, SelectedConfID, ConfID, prealigned=PreAligned)
  660                     if CalcRMSD < EnergyRMSDCutoff:
  661                         IgnoreConf = True
  662                         break
  663             if IgnoreConf:
  664                 IgnoredByRMSDConfCount += 1
  665                 continue
  666 
  667         ConfCount += 1
  668         SelectedConfIDs.append(ConfID)
  669 
  670     if not OptionsInfo["QuietMode"]:
  671         MiscUtil.PrintInfo(
  672             "\nTotal Number of conformations generated for %s: %d" % (RDKitUtil.GetMolName(Mol, MolNum), ConfCount)
  673         )
  674         MiscUtil.PrintInfo(
  675             "Number of conformations ignored due to energy window cutoff: %d" % (IgnoredByEnergyConfCount)
  676         )
  677         if ApplyEnergyRMSDCutoff:
  678             MiscUtil.PrintInfo(
  679                 "Number of conformations ignored due to energy RMSD cutoff:  %d" % (IgnoredByRMSDConfCount)
  680             )
  681 
  682     SelectedConfEnergies = None
  683     if OptionsInfo["EnergyOut"]:
  684         SelectedConfEnergies = []
  685         for ConfID in SelectedConfIDs:
  686             Energy = "%.*f" % (OptionsInfo["Precision"], CalcEnergyMap[ConfID])
  687             SelectedConfEnergies.append(Energy)
  688 
  689     return [SelectedConfIDs, SelectedConfEnergies]
  690 
  691 
  692 def MinimizeMoleculeUsingPsi4(Psi4Handle, Mol, MolNum, ConfID=-1):
  693     """Minimize molecule using Psi4."""
  694 
  695     # Setup a Psi4Mol...
  696     Psi4Mol = SetupPsi4Mol(Psi4Handle, Mol, MolNum, ConfID)
  697     if Psi4Mol is None:
  698         return (False, None)
  699 
  700     #  Setup reference wave function...
  701     Reference = SetupReferenceWavefunction(Mol)
  702     Psi4Handle.set_options({"Reference": Reference})
  703 
  704     # Setup method name and basis set...
  705     MethodName, BasisSet = SetupMethodNameAndBasisSet(Mol)
  706 
  707     # Optimize geometry...
  708     Status, Energy, WaveFunction = Psi4Util.PerformGeometryOptimization(
  709         Psi4Handle, Psi4Mol, MethodName, BasisSet, ReturnWaveFunction=True, Quiet=OptionsInfo["QuietMode"]
  710     )
  711 
  712     if not Status:
  713         PerformPsi4Cleanup(Psi4Handle)
  714         return (False, None)
  715 
  716     # Update atom positions...
  717     AtomPositions = Psi4Util.GetAtomPositions(Psi4Handle, WaveFunction, InAngstroms=True)
  718     RDKitUtil.SetAtomPositions(Mol, AtomPositions, ConfID=ConfID)
  719 
  720     # Convert energy units...
  721     if OptionsInfo["ApplyEnergyConversionFactor"]:
  722         Energy = Energy * OptionsInfo["EnergyConversionFactor"]
  723 
  724     # Clean up
  725     PerformPsi4Cleanup(Psi4Handle)
  726 
  727     return (True, Energy)
  728 
  729 
  730 def MinimizeMoleculeUsingForceField(Mol, MolNum, ConfID=-1):
  731     """Minimize molecule using forcefield available in RDKit."""
  732 
  733     try:
  734         if OptionsInfo["ConfGenerationParams"]["UseUFF"]:
  735             ConvergeStatus = AllChem.UFFOptimizeMolecule(
  736                 Mol, confId=ConfID, maxIters=OptionsInfo["ConfGenerationParams"]["MaxIters"]
  737             )
  738         elif OptionsInfo["ConfGenerationParams"]["UseMMFF"]:
  739             ConvergeStatus = AllChem.MMFFOptimizeMolecule(
  740                 Mol,
  741                 confId=ConfID,
  742                 maxIters=OptionsInfo["ConfGenerationParams"]["MaxIters"],
  743                 mmffVariant=OptionsInfo["ConfGenerationParams"]["ForceFieldMMFFVariant"],
  744             )
  745         else:
  746             MiscUtil.PrintError(
  747                 "Minimization couldn't be performed: Specified forcefield, %s, is not supported"
  748                 % OptionsInfo["ConfGenerationParams"]["ForceField"]
  749             )
  750     except (ValueError, RuntimeError, Chem.rdchem.KekulizeException) as ErrMsg:
  751         if not OptionsInfo["QuietMode"]:
  752             MolName = RDKitUtil.GetMolName(Mol, MolNum)
  753             MiscUtil.PrintWarning(
  754                 "Minimization using forcefield couldn't be performed for molecule %s:\n%s\n" % (MolName, ErrMsg)
  755             )
  756         return (False, None)
  757 
  758     return (True, ConvergeStatus)
  759 
  760 
  761 def EmbedMolecule(Mol, MolNum=None):
  762     """Embed conformations"""
  763 
  764     ConfIDs = []
  765 
  766     MaxConfs = OptionsInfo["ConfGenerationParams"]["MaxConfs"]
  767     RandomSeed = OptionsInfo["ConfGenerationParams"]["RandomSeed"]
  768     EnforceChirality = OptionsInfo["ConfGenerationParams"]["EnforceChirality"]
  769     UseExpTorsionAnglePrefs = OptionsInfo["ConfGenerationParams"]["UseExpTorsionAnglePrefs"]
  770     ETVersion = OptionsInfo["ConfGenerationParams"]["ETVersion"]
  771     UseBasicKnowledge = OptionsInfo["ConfGenerationParams"]["UseBasicKnowledge"]
  772     EmbedRMSDCutoff = OptionsInfo["ConfGenerationParams"]["EmbedRMSDCutoff"]
  773 
  774     try:
  775         ConfIDs = AllChem.EmbedMultipleConfs(
  776             Mol,
  777             numConfs=MaxConfs,
  778             randomSeed=RandomSeed,
  779             pruneRmsThresh=EmbedRMSDCutoff,
  780             enforceChirality=EnforceChirality,
  781             useExpTorsionAnglePrefs=UseExpTorsionAnglePrefs,
  782             useBasicKnowledge=UseBasicKnowledge,
  783             ETversion=ETVersion,
  784         )
  785     except ValueError as ErrMsg:
  786         if not OptionsInfo["QuietMode"]:
  787             MolName = RDKitUtil.GetMolName(Mol, MolNum)
  788             MiscUtil.PrintWarning("Embedding failed  for molecule %s:\n%s\n" % (MolName, ErrMsg))
  789         ConfIDs = []
  790 
  791     if not OptionsInfo["QuietMode"]:
  792         if EmbedRMSDCutoff > 0:
  793             MiscUtil.PrintInfo(
  794                 "Generating initial conformation ensemble by distance geometry for %s - EmbedRMSDCutoff: %s; Size: %s; Size after RMSD filtering: %s"
  795                 % (RDKitUtil.GetMolName(Mol, MolNum), EmbedRMSDCutoff, MaxConfs, len(ConfIDs))
  796             )
  797         else:
  798             MiscUtil.PrintInfo(
  799                 "Generating initial conformation ensemble by distance geometry for %s - EmbedRMSDCutoff: None; Size: %s"
  800                 % (RDKitUtil.GetMolName(Mol, MolNum), len(ConfIDs))
  801             )
  802 
  803     return ConfIDs
  804 
  805 
  806 def SetupPsi4Mol(Psi4Handle, Mol, MolNum, ConfID=-1):
  807     """Setup a Psi4 molecule object."""
  808 
  809     if OptionsInfo["RecenterAndReorient"]:
  810         MolGeometry = RDKitUtil.GetPsi4XYZFormatString(Mol, ConfID=ConfID, NoCom=False, NoReorient=False)
  811     else:
  812         MolGeometry = RDKitUtil.GetPsi4XYZFormatString(Mol, ConfID=ConfID, NoCom=True, NoReorient=True)
  813 
  814     try:
  815         Psi4Mol = Psi4Handle.geometry(MolGeometry)
  816     except Exception as ErrMsg:
  817         Psi4Mol = None
  818         if not OptionsInfo["QuietMode"]:
  819             MiscUtil.PrintWarning("Failed to create Psi4 molecule from geometry string: %s\n" % ErrMsg)
  820             MolName = RDKitUtil.GetMolName(Mol, MolNum)
  821             MiscUtil.PrintWarning("Ignoring molecule: %s" % MolName)
  822 
  823     if OptionsInfo["Symmetrize"]:
  824         Psi4Mol.symmetrize(OptionsInfo["SymmetrizeTolerance"])
  825 
  826     return Psi4Mol
  827 
  828 
  829 def PerformPsi4Cleanup(Psi4Handle):
  830     """Perform clean up."""
  831 
  832     # Clean up after Psi4 run...
  833     Psi4Handle.core.clean()
  834 
  835     # Clean up any leftover scratch files...
  836     if OptionsInfo["MPMode"]:
  837         Psi4Util.RemoveScratchFiles(Psi4Handle, OptionsInfo["Psi4RunParams"]["OutputFile"])
  838 
  839 
  840 def CheckAndValidateMolecule(Mol, MolCount=None):
  841     """Validate molecule for Psi4 calculations."""
  842 
  843     if Mol is None:
  844         if not OptionsInfo["QuietMode"]:
  845             MiscUtil.PrintInfo("\nProcessing molecule number %s..." % MolCount)
  846         return False
  847 
  848     MolName = RDKitUtil.GetMolName(Mol, MolCount)
  849     if not OptionsInfo["QuietMode"]:
  850         MiscUtil.PrintInfo("\nProcessing molecule %s..." % MolName)
  851 
  852     if RDKitUtil.IsMolEmpty(Mol):
  853         if not OptionsInfo["QuietMode"]:
  854             MiscUtil.PrintWarning("Ignoring empty molecule: %s\n" % MolName)
  855         return False
  856 
  857     if not RDKitUtil.ValidateElementSymbols(RDKitUtil.GetAtomSymbols(Mol)):
  858         if not OptionsInfo["QuietMode"]:
  859             MiscUtil.PrintWarning("Ignoring molecule containing invalid element symbols: %s\n" % MolName)
  860         return False
  861 
  862     return True
  863 
  864 
  865 def SetupMethodNameAndBasisSet(Mol):
  866     """Setup method name and basis set."""
  867 
  868     MethodName = OptionsInfo["MethodName"]
  869     if OptionsInfo["MethodNameAuto"]:
  870         MethodName = "B3LYP"
  871 
  872     BasisSet = OptionsInfo["BasisSet"]
  873     if OptionsInfo["BasisSetAuto"]:
  874         BasisSet = "6-31+G**" if RDKitUtil.IsAtomSymbolPresentInMol(Mol, "S") else "6-31G**"
  875 
  876     return (MethodName, BasisSet)
  877 
  878 
  879 def SetupReferenceWavefunction(Mol):
  880     """Setup reference wavefunction."""
  881 
  882     Reference = OptionsInfo["Reference"]
  883     if OptionsInfo["ReferenceAuto"]:
  884         Reference = "UHF" if (RDKitUtil.GetSpinMultiplicity(Mol) > 1) else "RHF"
  885 
  886     return Reference
  887 
  888 
  889 def SetupEnergyConversionFactor(Psi4Handle):
  890     """Setup converstion factor for energt units. The Psi4 energy units are Hartrees."""
  891 
  892     EnergyUnits = OptionsInfo["EnergyUnits"]
  893 
  894     ApplyConversionFactor = True
  895     if re.match(r"^kcal\/mol$", EnergyUnits, re.I):
  896         ConversionFactor = Psi4Handle.constants.hartree2kcalmol
  897     elif re.match(r"^kJ\/mol$", EnergyUnits, re.I):
  898         ConversionFactor = Psi4Handle.constants.hartree2kJmol
  899     elif re.match("^eV$", EnergyUnits, re.I):
  900         ConversionFactor = Psi4Handle.constants.hartree2ev
  901     else:
  902         ApplyConversionFactor = False
  903         ConversionFactor = 1.0
  904 
  905     OptionsInfo["ApplyEnergyConversionFactor"] = ApplyConversionFactor
  906     OptionsInfo["EnergyConversionFactor"] = ConversionFactor
  907 
  908 
  909 def WriteMolConformers(Writer, Mol, MolNum, ConfIDs, ConfEnergies=None):
  910     """Write molecule coformers."""
  911 
  912     if ConfIDs is None:
  913         return True
  914 
  915     MolName = RDKitUtil.GetMolName(Mol, MolNum)
  916 
  917     for Index, ConfID in enumerate(ConfIDs):
  918         SetConfMolName(Mol, MolName, ConfID)
  919 
  920         if OptionsInfo["EnergyOut"] and ConfEnergies is not None:
  921             Mol.SetProp(OptionsInfo["EnergyDataFieldLabel"], ConfEnergies[Index])
  922 
  923             Writer.write(Mol, confId=ConfID)
  924 
  925 
  926 def SetConfMolName(Mol, MolName, ConfCount):
  927     """Set conf mol name."""
  928 
  929     ConfName = "%s_Conf%d" % (MolName, ConfCount)
  930     Mol.SetProp("_Name", ConfName)
  931 
  932 
  933 def ProcessOptions():
  934     """Process and validate command line arguments and options."""
  935 
  936     MiscUtil.PrintInfo("Processing options...")
  937 
  938     # Validate options...
  939     ValidateOptions()
  940 
  941     OptionsInfo["Infile"] = Options["--infile"]
  942     OptionsInfo["SMILESInfileStatus"] = True if MiscUtil.CheckFileExt(Options["--infile"], "smi csv tsv txt") else False
  943     ParamsDefaultInfoOverride = {"RemoveHydrogens": False}
  944     OptionsInfo["InfileParams"] = MiscUtil.ProcessOptionInfileParameters(
  945         "--infileParams",
  946         Options["--infileParams"],
  947         InfileName=Options["--infile"],
  948         ParamsDefaultInfo=ParamsDefaultInfoOverride,
  949     )
  950 
  951     OptionsInfo["Outfile"] = Options["--outfile"]
  952     OptionsInfo["OutfileParams"] = MiscUtil.ProcessOptionOutfileParameters(
  953         "--outfileParams", Options["--outfileParams"]
  954     )
  955 
  956     OptionsInfo["Overwrite"] = Options["--overwrite"]
  957 
  958     # Method, basis set, and reference wavefunction...
  959     OptionsInfo["BasisSet"] = Options["--basisSet"]
  960     OptionsInfo["BasisSetAuto"] = True if re.match("^auto$", Options["--basisSet"], re.I) else False
  961 
  962     OptionsInfo["MethodName"] = Options["--methodName"]
  963     OptionsInfo["MethodNameAuto"] = True if re.match("^auto$", Options["--methodName"], re.I) else False
  964 
  965     OptionsInfo["Reference"] = Options["--reference"]
  966     OptionsInfo["ReferenceAuto"] = True if re.match("^auto$", Options["--reference"], re.I) else False
  967 
  968     # Run and options parameters...
  969     OptionsInfo["Psi4OptionsParams"] = Psi4Util.ProcessPsi4OptionsParameters(
  970         "--psi4OptionsParams", Options["--psi4OptionsParams"]
  971     )
  972     OptionsInfo["Psi4RunParams"] = Psi4Util.ProcessPsi4RunParameters(
  973         "--psi4RunParams", Options["--psi4RunParams"], InfileName=OptionsInfo["Infile"]
  974     )
  975 
  976     # Conformer generation paramaters...
  977     ParamsDefaultInfoOverride = {"MaxConfs": 50, "MaxIters": 250}
  978     OptionsInfo["ConfGenerationParams"] = MiscUtil.ProcessOptionConformerParameters(
  979         "--confParams", Options["--confParams"], ParamsDefaultInfoOverride
  980     )
  981 
  982     # Energy parameters...
  983     OptionsInfo["EnergyOut"] = True if re.match("^yes$", Options["--energyOut"], re.I) else False
  984     OptionsInfo["EnergyUnits"] = Options["--energyUnits"]
  985 
  986     EnergyDataFieldLabel = Options["--energyDataFieldLabel"]
  987     if re.match("^auto$", EnergyDataFieldLabel, re.I):
  988         EnergyDataFieldLabel = "Psi4_Energy (%s)" % Options["--energyUnits"]
  989     OptionsInfo["EnergyDataFieldLabel"] = EnergyDataFieldLabel
  990 
  991     OptionsInfo["EnergyRMSDCalcMode"] = Options["--energyRMSDCalcMode"]
  992     OptionsInfo["EnergyRMSDCalcModeBest"] = (
  993         True if re.match("^BestRMSD$", Options["--energyRMSDCalcMode"], re.I) else False
  994     )
  995 
  996     OptionsInfo["EnergyRMSDCutoff"] = float(Options["--energyRMSDCutoff"])
  997 
  998     OptionsInfo["EnergyRMSDCutoffMode"] = Options["--energyRMSDCutoffMode"]
  999     OptionsInfo["EnergyRMSDCutoffModeLowest"] = (
 1000         True if re.match("^Lowest$", Options["--energyRMSDCutoffMode"], re.I) else False
 1001     )
 1002 
 1003     if OptionsInfo["EnergyRMSDCutoff"] > 0:
 1004         # Make sure that the alignConformers option is being used...
 1005         if not OptionsInfo["ConfGenerationParams"]["AlignConformers"]:
 1006             MiscUtil.PrintError(
 1007                 'The value for "alignConformers" specified using "--confParams" must  be set to " yes" for non-zero values of "--energyRMSDCutoff" '
 1008             )
 1009 
 1010     # Process energy window...
 1011     EnergyWindow = Options["--energyWindow"]
 1012     if re.match("^auto$", EnergyWindow, re.I):
 1013         # Set default energy window based on units...
 1014         EnergyUnits = Options["--energyUnits"]
 1015         if re.match(r"^kcal\/mol$", EnergyUnits, re.I):
 1016             EnergyWindow = 20
 1017         elif re.match(r"^kJ\/mol$", EnergyUnits, re.I):
 1018             EnergyWindow = 83.68
 1019         elif re.match("^eV$", EnergyUnits, re.I):
 1020             EnergyWindow = 0.8673
 1021         elif re.match("^Hartrees$", EnergyUnits, re.I):
 1022             EnergyWindow = 0.03188
 1023         else:
 1024             MiscUtil.PrintError(
 1025                 'Failed to set default value for "--energyWindow". The value, %s, specified for option "--energyUnits" is not valid. '
 1026                 % EnergyUnits
 1027             )
 1028     else:
 1029         EnergyWindow = float(EnergyWindow)
 1030     OptionsInfo["EnergyWindow"] = EnergyWindow
 1031 
 1032     OptionsInfo["MaxIters"] = int(Options["--maxIters"])
 1033 
 1034     OptionsInfo["MPMode"] = True if re.match("^yes$", Options["--mp"], re.I) else False
 1035     OptionsInfo["MPParams"] = MiscUtil.ProcessOptionMultiprocessingParameters("--mpParams", Options["--mpParams"])
 1036 
 1037     # Multiprocessing level...
 1038     MPLevelMoleculesMode = False
 1039     MPLevelConformersMode = False
 1040     MPLevel = Options["--mpLevel"]
 1041     if re.match("^Molecules$", MPLevel, re.I):
 1042         MPLevelMoleculesMode = True
 1043     elif re.match("^Conformers$", MPLevel, re.I):
 1044         MPLevelConformersMode = True
 1045     else:
 1046         MiscUtil.PrintError('The value, %s, specified for option "--mpLevel" is not valid. ' % MPLevel)
 1047     OptionsInfo["MPLevel"] = MPLevel
 1048     OptionsInfo["MPLevelMoleculesMode"] = MPLevelMoleculesMode
 1049     OptionsInfo["MPLevelConformersMode"] = MPLevelConformersMode
 1050 
 1051     OptionsInfo["Precision"] = int(Options["--precision"])
 1052     OptionsInfo["QuietMode"] = True if re.match("^yes$", Options["--quiet"], re.I) else False
 1053 
 1054     OptionsInfo["RecenterAndReorient"] = True if re.match("^yes$", Options["--recenterAndReorient"], re.I) else False
 1055 
 1056     Symmetrize = Options["--symmetrize"]
 1057     if re.match("^auto$", Symmetrize, re.I):
 1058         Symmetrize = True if OptionsInfo["RecenterAndReorient"] else False
 1059     else:
 1060         Symmetrize = True if re.match("^yes$", Symmetrize, re.I) else False
 1061     OptionsInfo["Symmetrize"] = Symmetrize
 1062 
 1063     OptionsInfo["SymmetrizeTolerance"] = float(Options["--symmetrizeTolerance"])
 1064 
 1065 
 1066 def RetrieveOptions():
 1067     """Retrieve command line arguments and options."""
 1068 
 1069     # Get options...
 1070     global Options
 1071     Options = docopt(_docoptUsage_)
 1072 
 1073     # Set current working directory to the specified directory...
 1074     WorkingDir = Options["--workingdir"]
 1075     if WorkingDir:
 1076         os.chdir(WorkingDir)
 1077 
 1078     # Handle examples option...
 1079     if "--examples" in Options and Options["--examples"]:
 1080         MiscUtil.PrintInfo(MiscUtil.GetExamplesTextFromDocOptText(_docoptUsage_))
 1081         sys.exit(0)
 1082 
 1083 
 1084 def ValidateOptions():
 1085     """Validate option values."""
 1086 
 1087     MiscUtil.ValidateOptionTextValue("--energyOut", Options["--energyOut"], "yes no")
 1088     MiscUtil.ValidateOptionTextValue("--energyUnits", Options["--energyUnits"], "Hartrees kcal/mol kJ/mol eV")
 1089 
 1090     MiscUtil.ValidateOptionTextValue(" --energyRMSDCalcMode", Options["--energyRMSDCalcMode"], "RMSD BestRMSD")
 1091 
 1092     MiscUtil.ValidateOptionFloatValue("--energyRMSDCutoff", Options["--energyRMSDCutoff"], {">=": 0})
 1093     MiscUtil.ValidateOptionTextValue(" --energyRMSDCutoffMode", Options["--energyRMSDCutoffMode"], "All Lowest")
 1094 
 1095     if not re.match("^auto$", Options["--energyWindow"], re.I):
 1096         MiscUtil.ValidateOptionFloatValue("--energyWindow", Options["--energyWindow"], {">": 0})
 1097 
 1098     MiscUtil.ValidateOptionFilePath("-i, --infile", Options["--infile"])
 1099     MiscUtil.ValidateOptionFileExt("-i, --infile", Options["--infile"], "sdf sd mol smi txt csv tsv")
 1100 
 1101     MiscUtil.ValidateOptionIntegerValue("--maxIters", Options["--maxIters"], {">": 0})
 1102 
 1103     MiscUtil.ValidateOptionFileExt("-o, --outfile", Options["--outfile"], "sdf sd")
 1104     MiscUtil.ValidateOptionsOutputFileOverwrite(
 1105         "-o, --outfile", Options["--outfile"], "--overwrite", Options["--overwrite"]
 1106     )
 1107     MiscUtil.ValidateOptionsDistinctFileNames(
 1108         "-i, --infile", Options["--infile"], "-o, --outfile", Options["--outfile"]
 1109     )
 1110 
 1111     MiscUtil.ValidateOptionTextValue("--mp", Options["--mp"], "yes no")
 1112     MiscUtil.ValidateOptionTextValue("--mpLevel", Options["--mpLevel"], "Molecules Conformers")
 1113 
 1114     MiscUtil.ValidateOptionIntegerValue("-p, --precision", Options["--precision"], {">": 0})
 1115     MiscUtil.ValidateOptionTextValue("-q, --quiet", Options["--quiet"], "yes no")
 1116 
 1117     MiscUtil.ValidateOptionTextValue("--recenterAndReorient", Options["--recenterAndReorient"], "yes no")
 1118     MiscUtil.ValidateOptionTextValue("--symmetrize", Options["--symmetrize"], "yes no auto")
 1119     MiscUtil.ValidateOptionFloatValue("--symmetrizeTolerance", Options["--symmetrizeTolerance"], {">": 0})
 1120 
 1121 
 1122 # Setup a usage string for docopt...
 1123 _docoptUsage_ = """
 1124 Psi4GenerateConformers.py - Generate molecular conformations
 1125 
 1126 Usage:
 1127     Psi4GenerateConformers.py [--basisSet <text>] [--confParams <Name,Value,...>] [--energyOut <yes or no>]
 1128                               [--energyDataFieldLabel <text>] [--energyUnits <text>] [--energyRMSDCalcMode <RMSD or BestRMSD>]
 1129                               [--energyRMSDCutoff <number>] [--energyRMSDCutoffMode <All or Lowest>] [--energyWindow <number>]
 1130                               [--infileParams <Name,Value,...>] [--maxIters <number>]
 1131                               [--methodName <text>] [--mp <yes or no>] [--mpLevel <Molecules or Conformers>]
 1132                               [--mpParams <Name, Value,...>] [ --outfileParams <Name,Value,...> ] [--overwrite] [--precision <number>]
 1133                               [--psi4OptionsParams <Name,Value,...>] [--psi4RunParams <Name,Value,...>]
 1134                               [--quiet <yes or no>] [--reference <text>] [--recenterAndReorient <yes or no>]
 1135                               [--symmetrize <yes or no>] [--symmetrizeTolerance <number>] [-w <dir>] -i <infile> -o <outfile> 
 1136     Psi4GenerateConformers.py -h | --help | -e | --examples
 1137 
 1138 Description:
 1139     Generate 3D conformers of molecules using a combination of distance geometry
 1140     and forcefield minimization followed by geometry optimization using a quantum
 1141     chemistry method. A set of initial 3D structures are generated for a molecule 
 1142     employing distance geometry. The 3D structures in the conformation ensemble
 1143     are sequentially minimized using forcefield and a quantum chemistry method.
 1144 
 1145     A Psi4 XYZ format geometry string is automatically generated for each molecule
 1146     in input file. It contains atom symbols and 3D coordinates for each atom in a
 1147     molecule. In addition, the formal charge and spin multiplicity are present in the
 1148     the geometry string. These values are either retrieved from molecule properties
 1149     named 'FormalCharge' and 'SpinMultiplicty' or dynamically calculated for a
 1150     molecule.
 1151 
 1152     The supported input file formats are: Mol (.mol), SD (.sdf, .sd), SMILES (.smi,
 1153     .csv, .tsv .txt)
 1154 
 1155     The supported output file formats are: SD (.sdf, .sd)
 1156 
 1157 Options:
 1158     -b, --basisSet <text>  [default: auto]
 1159         Basis set to use for energy minimization. Default: 6-31+G** for sulfur
 1160         containing molecules; Otherwise, 6-31G** [ Ref 150 ]. The specified 
 1161         value must be a valid Psi4 basis set. No validation is performed.
 1162         
 1163         The following list shows a representative sample of basis sets available
 1164         in Psi4:
 1165             
 1166             STO-3G, 6-31G, 6-31+G, 6-31++G, 6-31G*, 6-31+G*,  6-31++G*, 
 1167             6-31G**, 6-31+G**, 6-31++G**, 6-311G, 6-311+G, 6-311++G,
 1168             6-311G*, 6-311+G*, 6-311++G*, 6-311G**, 6-311+G**, 6-311++G**,
 1169             cc-pVDZ, cc-pCVDZ, aug-cc-pVDZ, cc-pVDZ-DK, cc-pCVDZ-DK, def2-SVP,
 1170             def2-SVPD, def2-TZVP, def2-TZVPD, def2-TZVPP, def2-TZVPPD
 1171             
 1172     --confParams <Name,Value,...>  [default: auto]
 1173         Generate an initial 3D conformation ensemble using distance geometry and
 1174         forcefield minimization before final geometry optimization by a specified
 1175         method name and basis set. Possible values: yes or no.
 1176         
 1177         A comma delimited list of parameter name and value pairs for generating
 1178         initial sets of 3D conformations for molecules. The 3D conformation ensemble
 1179         is generated using distance geometry and forcefield functionality available
 1180         in RDKit. The 3D structures in the conformation ensemble are subsequently
 1181         minimized by a quantum chemistry method available in Psi4.
 1182        
 1183         The supported parameter names along with their default values are shown
 1184         below:
 1185             
 1186             confMethod,ETKDGv2,
 1187             forceField,MMFF, forceFieldMMFFVariant,MMFF94,
 1188             enforceChirality,yes,alignConformers,yes, embedRMSDCutoff,0.5,
 1189             maxConfs,50,maxIters,250,randomSeed,auto
 1190             
 1191             confMethod,ETKDGv2   [ Possible values: SDG, KDG, ETDG,
 1192                 ETKDG , or ETKDGv2]
 1193             forceField, MMFF   [ Possible values: UFF or MMFF ]
 1194             forceFieldMMFFVariant,MMFF94   [ Possible values: MMFF94 or MMFF94s ]
 1195             enforceChirality,yes   [ Possible values: yes or no ]
 1196             alignConformers,yes   [ Possible values: yes or no ]
 1197             embedRMSDCutoff,0.5   [ Possible values: number or None]
 1198             
 1199         confMethod: Conformation generation methodology for generating initial 3D
 1200         coordinates. Possible values: Standard Distance Geometry (SDG), Experimental
 1201         Torsion-angle preference with Distance Geometry (ETDG), basic Knowledge-terms
 1202         with Distance Geometry (KDG) and Experimental Torsion-angle preference
 1203         along with basic Knowledge-terms with Distance Geometry (ETKDG or
 1204         ETKDGv2) [Ref 129, 167] .
 1205         
 1206         forceField: Forcefield method to use for energy minimization. Possible
 1207         values: Universal Force Field (UFF) [ Ref 81 ] or Merck Molecular Mechanics
 1208         Force Field [ Ref 83-87 ] .
 1209         
 1210         enforceChirality: Enforce chirality for defined chiral centers during
 1211         forcefield minimization.
 1212         
 1213         alignConformers: Align conformers for each molecule.
 1214         
 1215         maxConfs: Maximum number of conformations to generate for each molecule
 1216         during the generation of an initial 3D conformation ensemble using 
 1217         conformation generation methodology. The conformations are minimized
 1218         using the specified forcefield and a quantum chemistry method. The lowest
 1219         energy conformation is written to the output file.
 1220         
 1221         embedRMSDCutoff: RMSD cutoff for retaining initial set of conformers embedded
 1222         using distance geometry and before forcefield minimization. All embedded
 1223         conformers are kept for 'None' value. Otherwise, only those conformers which
 1224         are different from each other by the specified RMSD cutoff, 0.5 by default,
 1225         are kept. The first embedded conformer is always retained.
 1226         
 1227         maxIters: Maximum number of iterations to perform for each conformation
 1228         during forcefield minimization.
 1229         
 1230         randomSeed: Seed for the random number generator for reproducing initial
 1231         3D coordinates in a conformation ensemble. Default is to use a random seed.
 1232     --energyOut <yes or no>  [default: yes]
 1233         Write out energy values.
 1234     --energyDataFieldLabel <text>  [default: auto]
 1235         Energy data field label for writing energy values. Default: Psi4_Energy (<Units>). 
 1236     --energyUnits <text>  [default: kcal/mol]
 1237         Energy units. Possible values: Hartrees, kcal/mol, kJ/mol, or eV.
 1238     --energyRMSDCalcMode <RMSD or BestRMSD>  [default: RMSD]
 1239         Methodology for calculating RMSD values during the application of RMSD
 1240         cutoff for retaining conformations after the final energy minimization. Possible
 1241         values: RMSD or BestRMSD. This option is ignore during 'None' value of
 1242         '--energyRMSDCutoff' option.
 1243         
 1244         During BestRMSMode mode, the RDKit 'function AllChem.GetBestRMS' is used to
 1245         align and calculate RMSD. This function calculates optimal RMSD for aligning two
 1246         molecules, taking symmetry into account. Otherwise, the RMSD value is calculated
 1247         using 'AllChem.GetConformerRMS' without changing the atom order. A word to the
 1248         wise from RDKit documentation: The AllChem.GetBestRMS function will attempt to
 1249         align all permutations of matching atom orders in both molecules, for some molecules
 1250         it will lead to 'combinatorial explosion'.
 1251     --energyRMSDCutoff <number>  [default: 0.5]
 1252         RMSD cutoff for retaining conformations after the final energy minimization.
 1253         By default, only those conformations which are different from the lowest
 1254         energy conformation by the specified RMSD cutoff and are with in the 
 1255         specified energy window are kept. The lowest energy conformation is always
 1256         retained. A value of zero keeps all minimized conformations with in the
 1257         specified energy window from the lowest energy.
 1258     --energyRMSDCutoffMode <All or Lowest>  [default: All]
 1259         RMSD cutoff mode for  retaining conformations after the final energy
 1260         minimization. Possible values: All or Lowest. The RMSD values are compared
 1261         against all the selected conformations or the lowest energy conformation during
 1262         'All' and 'Lowest' value of '--energyRMSDCutoffMode'. This option is ignored
 1263         during 'None' value of --energyRMSDCutoff.
 1264         
 1265         By default, only those conformations which all different from all selected
 1266         conformations by the specified RMSD cutoff and are with in the specified
 1267         energy window are kept.
 1268     --energyWindow <number>  [default: auto]
 1269         Psi4 Energy window  for selecting conformers after the final energy minimization.
 1270         The default value is dependent on '--energyUnits': 20 kcal/mol, 83.68 kJ/mol,
 1271         0.8673 ev, or 0.03188 Hartrees. The specified value must be in '--energyUnits'.
 1272     -e, --examples
 1273         Print examples.
 1274     -h, --help
 1275         Print this help message.
 1276     -i, --infile <infile>
 1277         Input file name.
 1278     --infileParams <Name,Value,...>  [default: auto]
 1279         A comma delimited list of parameter name and value pairs for reading
 1280         molecules from files. The supported parameter names for different file
 1281         formats, along with their default values, are shown below:
 1282             
 1283             SD, MOL: removeHydrogens,no,sanitize,yes,strictParsing,yes
 1284             SMILES: smilesColumn,1,smilesNameColumn,2,smilesDelimiter,space,
 1285                 smilesTitleLine,auto,sanitize,yes
 1286             
 1287         Possible values for smilesDelimiter: space, comma or tab.
 1288     --maxIters <number>  [default: 50]
 1289         Maximum number of iterations to perform for each molecule or conformer
 1290         during energy minimization by a quantum chemistry method.
 1291     -m, --methodName <text>  [default: auto]
 1292         Method to use for energy minimization. Default: B3LYP [ Ref 150 ]. The
 1293         specified value must be a valid Psi4 method name. No validation is
 1294         performed.
 1295         
 1296         The following list shows a representative sample of methods available
 1297         in Psi4:
 1298             
 1299             B1LYP, B2PLYP, B2PLYP-D3BJ, B2PLYP-D3MBJ, B3LYP, B3LYP-D3BJ,
 1300             B3LYP-D3MBJ, CAM-B3LYP, CAM-B3LYP-D3BJ, HF, HF-D3BJ,  HF3c, M05,
 1301             M06, M06-2x, M06-HF, M06-L, MN12-L, MN15, MN15-D3BJ,PBE, PBE0,
 1302             PBEH3c, PW6B95, PW6B95-D3BJ, WB97, WB97X, WB97X-D, WB97X-D3BJ
 1303             
 1304     --mp <yes or no>  [default: no]
 1305         Use multiprocessing.
 1306          
 1307         By default, input data is retrieved in a lazy manner via mp.Pool.imap()
 1308         function employing lazy RDKit data iterable. This allows processing of
 1309         arbitrary large data sets without any additional requirements memory.
 1310         
 1311         All input data may be optionally loaded into memory by mp.Pool.map()
 1312         before starting worker processes in a process pool by setting the value
 1313         of 'inputDataMode' to 'InMemory' in '--mpParams' option.
 1314         
 1315         A word to the wise: The default 'chunkSize' value of 1 during 'Lazy' input
 1316         data mode may adversely impact the performance. The '--mpParams' section
 1317         provides additional information to tune the value of 'chunkSize'.
 1318     --mpLevel <Molecules or Conformers>  [default: Molecules]
 1319         Perform multiprocessing at molecules or conformers level. Possible values:
 1320         Molecules or Conformers. The 'Molecules' value starts a process pool at the
 1321         molecules level. All conformers of a molecule are processed in a single
 1322         process. The 'Conformers' value, however, starts a process pool at the 
 1323         conformers level. Each conformer of a molecule is processed in an individual
 1324         process in the process pool. The default Psi4 'OutputFile' is set to 'quiet'
 1325         using '--psi4RunParams' for 'Conformers' level. Otherwise, it may generate
 1326         a large number of Psi4 output files.
 1327     --mpParams <Name,Value,...>  [default: auto]
 1328         A comma delimited list of parameter name and value pairs to configure
 1329         multiprocessing.
 1330         
 1331         The supported parameter names along with their default and possible
 1332         values are shown below:
 1333         
 1334             chunkSize, auto
 1335             inputDataMode, Lazy   [ Possible values: InMemory or Lazy ]
 1336             numProcesses, auto   [ Default: mp.cpu_count() ]
 1337         
 1338         These parameters are used by the following functions to configure and
 1339         control the behavior of multiprocessing: mp.Pool(), mp.Pool.map(), and
 1340         mp.Pool.imap().
 1341         
 1342         The chunkSize determines chunks of input data passed to each worker
 1343         process in a process pool by mp.Pool.map() and mp.Pool.imap() functions.
 1344         The default value of chunkSize is dependent on the value of 'inputDataMode'.
 1345         
 1346         The mp.Pool.map() function, invoked during 'InMemory' input data mode,
 1347         automatically converts RDKit data iterable into a list, loads all data into
 1348         memory, and calculates the default chunkSize using the following method
 1349         as shown in its code:
 1350         
 1351             chunkSize, extra = divmod(len(dataIterable), len(numProcesses) * 4)
 1352             if extra: chunkSize += 1
 1353         
 1354         For example, the default chunkSize will be 7 for a pool of 4 worker processes
 1355         and 100 data items.
 1356         
 1357         The mp.Pool.imap() function, invoked during 'Lazy' input data mode, employs
 1358         'lazy' RDKit data iterable to retrieve data as needed, without loading all the
 1359         data into memory. Consequently, the size of input data is not known a priori.
 1360         It's not possible to estimate an optimal value for the chunkSize. The default 
 1361         chunkSize is set to 1.
 1362         
 1363         The default value for the chunkSize during 'Lazy' data mode may adversely
 1364         impact the performance due to the overhead associated with exchanging
 1365         small chunks of data. It is generally a good idea to explicitly set chunkSize to
 1366         a larger value during 'Lazy' input data mode, based on the size of your input
 1367         data and number of processes in the process pool.
 1368         
 1369         The mp.Pool.map() function waits for all worker processes to process all
 1370         the data and return the results. The mp.Pool.imap() function, however,
 1371         returns the the results obtained from worker processes as soon as the
 1372         results become available for specified chunks of data.
 1373         
 1374         The order of data in the results returned by both mp.Pool.map() and 
 1375         mp.Pool.imap() functions always corresponds to the input data.
 1376     -o, --outfile <outfile>
 1377         Output file name.
 1378     --outfileParams <Name,Value,...>  [default: auto]
 1379         A comma delimited list of parameter name and value pairs for writing
 1380         molecules to files. The supported parameter names for different file
 1381         formats, along with their default values, are shown below:
 1382             
 1383             SD: kekulize,yes,forceV3000,no
 1384             
 1385     --overwrite
 1386         Overwrite existing files.
 1387     --precision <number>  [default: 6]
 1388         Floating point precision for writing energy values.
 1389     --psi4OptionsParams <Name,Value,...>  [default: none]
 1390         A comma delimited list of Psi4 option name and value pairs for setting
 1391         global and module options. The names are 'option_name' for global options
 1392         and 'module_name__option_name' for options local to a module. The
 1393         specified option names must be valid Psi4 names. No validation is
 1394         performed.
 1395         
 1396         The specified option name and  value pairs are processed and passed to
 1397         psi4.set_options() as a dictionary. The supported value types are float,
 1398         integer, boolean, or string. The float value string is converted into a float.
 1399         The valid values for a boolean string are yes, no, true, false, on, or off. 
 1400     --psi4RunParams <Name,Value,...>  [default: auto]
 1401         A comma delimited list of parameter name and value pairs for configuring
 1402         Psi4 jobs.
 1403         
 1404         The supported parameter names along with their default and possible
 1405         values are shown below:
 1406              
 1407             MemoryInGB, 1
 1408             NumThreads, 1
 1409             OutputFile, auto   [ Possible  values: stdout, quiet, or FileName ]
 1410             ScratchDir, auto   [ Possivle values: DirName]
 1411             RemoveOutputFile, yes   [ Possible values: yes, no, true, or false]
 1412             
 1413         These parameters control the runtime behavior of Psi4.
 1414         
 1415         The default file name for 'OutputFile' is <InFileRoot>_Psi4.out. The PID
 1416         is appended to output file name during multiprocessing as shown:
 1417         <InFileRoot>_Psi4_<PIDNum>.out. The 'stdout' value for 'OutputType'
 1418         sends Psi4 output to stdout. The 'quiet' or 'devnull' value suppresses
 1419         all Psi4 output. The 'OutputFile' is set to 'quiet' for 'auto' value during 
 1420         'Conformers' of '--mpLevel' option.
 1421         
 1422         The default 'Yes' value of 'RemoveOutputFile' option forces the removal
 1423         of any existing Psi4 before creating new files to append output from
 1424         multiple Psi4 runs.
 1425         
 1426         The option 'ScratchDir' is a directory path to the location of scratch
 1427         files. The default value corresponds to Psi4 default. It may be used to
 1428         override the deafult path.
 1429     -q, --quiet <yes or no>  [default: no]
 1430         Use quiet mode. The warning and information messages will not be printed.
 1431     -r, --reference <text>  [default: auto]
 1432         Reference wave function to use for energy calculation. Default: RHF or UHF.
 1433         The default values are Restricted Hartree-Fock (RHF) for closed-shell molecules
 1434         with all electrons paired and Unrestricted Hartree-Fock (UHF) for open-shell
 1435         molecules with unpaired electrons.
 1436         
 1437         The specified value must be a valid Psi4 reference wave function. No validation
 1438         is performed. For example: ROHF, CUHF, RKS, etc.
 1439         
 1440         The spin multiplicity determines the default value of reference wave function
 1441         for input molecules. It is calculated from number of free radical electrons using
 1442         Hund's rule of maximum multiplicity defined as 2S + 1 where S is the total
 1443         electron spin. The total spin is 1/2 the number of free radical electrons in a 
 1444         molecule. The value of 'SpinMultiplicity' molecule property takes precedence
 1445         over the calculated value of spin multiplicity.
 1446     --recenterAndReorient <yes or no>  [default: yes]
 1447         Recenter and reorient a molecule during creation of a Psi4 molecule from
 1448         a geometry string.
 1449         
 1450         The 'No' values allows the minimization of a molecule in its initial 3D
 1451         coordinate space generated by RDKit.
 1452     --symmetrize <yes or no>  [default: auto]
 1453         Symmetrize molecules before energy minimization. Default: 'Yes' during
 1454         'Yes' value of '--recenterAndReorient'; Otherwise, 'No'. The psi4 function,
 1455         psi4mol.symmetrize( SymmetrizeTolerance), is called to symmetrize
 1456         the molecule before calling psi4.optimize().
 1457         
 1458         The 'No' value of '--symmetrize' during 'Yes' value of '--recenterAndReorient'
 1459         may cause psi4.optimize() to fail with a 'Point group changed...' error
 1460         message.
 1461     --symmetrizeTolerance <number>  [default: 0.01]
 1462         Symmetry tolerance for '--symmetrize'.
 1463     -w, --workingdir <dir>
 1464         Location of working directory which defaults to the current directory.
 1465 
 1466 Examples:
 1467     To generate an initial conformer ensemble of up to 50 conformations using a
 1468     combination of ETKDGv2 distance geometry methodology, applying embed RMSD
 1469     cutoff of 0.5 and MMFF forcefield minimization, followed by energy minimization
 1470     using B3LYP/6-31G** or B3LYP/6-31+G** (sulfur containing), selecting a final set
 1471     of minimized conformers for molecules in a SMILES file, applying energy RMSD
 1472     cutoff of 0.5 and energy window value value of 20 kcal/mol, and write out a SD
 1473     file containing minimized conformers, type:
 1474 
 1475         % Psi4GenerateConformers.py -i Psi4Sample.smi -o Psi4SampleOut.sdf
 1476 
 1477     To run the first example in a quiet mode and write out a SD file, type:
 1478 
 1479         % Psi4GenerateConformers.py -q yes -i Psi4Sample.smi -o
 1480           Psi4SampleOut.sdf
 1481 
 1482     To run the first example in multiprocessing mode at molecules level on all
 1483     available CPUs without loading all data into memory and write out a SD file,
 1484     type:
 1485 
 1486         % Psi4GenerateConformers.py --mp yes -i Psi4Sample.smi -o
 1487           Psi4SampleOut.sdf
 1488 
 1489     To run the first example in multiprocessing mode at conformers level on all
 1490     available CPUs without loading all data into memory and write out a SD file,
 1491     type:
 1492 
 1493         % Psi4GenerateConformers.py --mp yes --mpLevel Conformers
 1494           -i Psi4Sample.smi -o Psi4SampleOut.sdf
 1495 
 1496     To run the first example in multiprocessing mode at molecules level on specific
 1497     number of CPUs and chunk size without loading all data into memory and write
 1498     out a SD file, type:
 1499 
 1500         % Psi4GenerateConformers.py  --mp yes --mpParams "inputDataMode,Lazy,
 1501           numProcesses,4,chunkSize,8" -i Psi4Sample.smi -o Psi4SampleOut.sdf
 1502 
 1503     To run the first example by using an explicit set of specific parameters, and
 1504     write out a SD file, type
 1505 
 1506         % Psi4GenerateConformers.py --confParams "confMethod,ETKDGv2,
 1507           forceField,MMFF, forceFieldMMFFVariant,MMFF94s, maxConfs,20,
 1508           embedRMSDCutoff,0.25" --energyUnits "kJ/mol" -m B3LYP
 1509           -b "6-31+G**" --maxIters 20 -i Psi4Sample.smi -o Psi4SampleOut.sdf
 1510 
 1511     To run the first example for molecules in a CSV SMILES file, SMILES strings
 1512     in column 1, name column 2, and write out a SD file, type:
 1513 
 1514         % Psi4GenerateConformers.py --infileParams "smilesDelimiter,comma,
 1515           smilesTitleLine,yes,smilesColumn,1,smilesNameColumn,2"
 1516           -i Psi4Sample.csv -o Psi4SampleOut.sdf
 1517 
 1518 Author:
 1519 
 1520     Manish Sud(msud@san.rr.com)
 1521 
 1522 See also:
 1523     Psi4CalculateEnergy.py, Psi4CalculatePartialCharges.py, Psi4PerformMinimization.py
 1524 
 1525 Copyright:
 1526     Copyright (C) 2026 Manish Sud. All rights reserved.
 1527 
 1528     The functionality available in this script is implemented using Psi4, an
 1529     open source quantum chemistry software package, and RDKit, an open
 1530     source toolkit for cheminformatics developed by Greg Landrum.
 1531 
 1532     This file is part of MayaChemTools.
 1533 
 1534     MayaChemTools is free software; you can redistribute it and/or modify it under
 1535     the terms of the GNU Lesser General Public License as published by the Free
 1536     Software Foundation; either version 3 of the License, or (at your option) any
 1537     later version.
 1538 
 1539 """
 1540 
 1541 if __name__ == "__main__":
 1542     main()