MayaChemTools

    1 #!/bin/env python
    2 #
    3 # File: Psi4VisualizeDualDescriptors.py
    4 # Author: Manish Sud <msud@san.rr.com>
    5 #
    6 # Copyright (C) 2024 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 # Add local python path to the global path and import standard library modules...
   33 import os
   34 import sys;  sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), "..", "lib", "Python"))
   35 import time
   36 import re
   37 import glob
   38 import shutil
   39 import multiprocessing as mp
   40 
   41 # Psi4 imports...
   42 if (hasattr(shutil, 'which') and shutil.which("psi4") is None):
   43     sys.stderr.write("\nWarning: Failed to find 'psi4' in your PATH indicating potential issues with your\n")
   44     sys.stderr.write("Psi4 environment. The 'import psi4' directive in the global scope of the script\n")
   45     sys.stderr.write("interferes with the multiprocessing functionality. It is imported later in the\n")
   46     sys.stderr.write("local scope during the execution of the script and may fail. Check/update your\n")
   47     sys.stderr.write("Psi4 environment and try again.\n\n")
   48 
   49 # RDKit imports...
   50 try:
   51     from rdkit import rdBase
   52     from rdkit import Chem
   53     from rdkit.Chem import AllChem
   54 except ImportError as ErrMsg:
   55     sys.stderr.write("\nFailed to import RDKit module/package: %s\n" % ErrMsg)
   56     sys.stderr.write("Check/update your RDKit environment and try again.\n\n")
   57     sys.exit(1)
   58 
   59 # MayaChemTools imports...
   60 try:
   61     from docopt import docopt
   62     import MiscUtil
   63     import Psi4Util
   64     import RDKitUtil
   65 except ImportError as ErrMsg:
   66     sys.stderr.write("\nFailed to import MayaChemTools module/package: %s\n" % ErrMsg)
   67     sys.stderr.write("Check/update your MayaChemTools environment and try again.\n\n")
   68     sys.exit(1)
   69 
   70 ScriptName = os.path.basename(sys.argv[0])
   71 Options = {}
   72 OptionsInfo = {}
   73 
   74 def main():
   75     """Start execution of the script."""
   76     
   77     MiscUtil.PrintInfo("\n%s (Psi4: Imported later; RDKit v%s; MayaChemTools v%s; %s): Starting...\n" % (ScriptName, rdBase.rdkitVersion, MiscUtil.GetMayaChemToolsVersion(), time.asctime()))
   78     
   79     (WallClockTime, ProcessorTime) = MiscUtil.GetWallClockAndProcessorTime()
   80     
   81     # Retrieve command line arguments and options...
   82     RetrieveOptions()
   83     
   84     # Process and validate command line arguments and options...
   85     ProcessOptions()
   86     
   87     # Perform actions required by the script...
   88     GenerateAndVisualizeDualDescriptors()
   89     
   90     MiscUtil.PrintInfo("\n%s: Done...\n" % ScriptName)
   91     MiscUtil.PrintInfo("Total time: %s" % MiscUtil.GetFormattedElapsedTime(WallClockTime, ProcessorTime))
   92 
   93 def GenerateAndVisualizeDualDescriptors():
   94     """Generate and visualize dual descriptors."""
   95     
   96     if OptionsInfo["GenerateCubeFiles"]:
   97         GenerateDualDescriptors()
   98         
   99     if OptionsInfo["VisualizeCubeFiles"]:
  100         VisualizeDualDescriptors()
  101 
  102 def GenerateDualDescriptors():
  103     """Generate cube files for dual descriptors."""
  104     
  105     # Setup a molecule reader...
  106     MiscUtil.PrintInfo("\nProcessing file %s..." % OptionsInfo["Infile"])
  107     Mols  = RDKitUtil.ReadMolecules(OptionsInfo["InfilePath"], **OptionsInfo["InfileParams"])
  108     
  109     MolCount, ValidMolCount, CubeFilesFailedCount = ProcessMolecules(Mols)
  110     
  111     MiscUtil.PrintInfo("\nTotal number of molecules: %d" % MolCount)
  112     MiscUtil.PrintInfo("Number of valid molecules: %d" % ValidMolCount)
  113     MiscUtil.PrintInfo("Number of molecules failed during generation of cube files: %d" % CubeFilesFailedCount)
  114     MiscUtil.PrintInfo("Number of ignored molecules: %d" % (MolCount - ValidMolCount + CubeFilesFailedCount))
  115 
  116 def ProcessMolecules(Mols):
  117     """Process and generate cube files for molecules."""
  118 
  119     if OptionsInfo["MPMode"]:
  120         return ProcessMoleculesUsingMultipleProcesses(Mols)
  121     else:
  122         return ProcessMoleculesUsingSingleProcess(Mols)
  123 
  124 def ProcessMoleculesUsingSingleProcess(Mols):
  125     """Process and generate dual descriptors cube files for molecules using a single process."""
  126     
  127     # Intialize Psi4...
  128     MiscUtil.PrintInfo("\nInitializing Psi4...")
  129     Psi4Handle = Psi4Util.InitializePsi4(Psi4RunParams = OptionsInfo["Psi4RunParams"], Psi4OptionsParams = OptionsInfo["Psi4OptionsParams"], PrintVersion = True, PrintHeader = True)
  130     OptionsInfo["psi4"] = Psi4Handle
  131 
  132     MiscUtil.PrintInfo("\nGenerating cube files for dual descriptors...")
  133     
  134     (MolCount, ValidMolCount, CubeFilesFailedCount) = [0] * 3
  135     for Mol in Mols:
  136         MolCount += 1
  137         
  138         if not CheckAndValidateMolecule(Mol, MolCount):
  139             continue
  140         
  141         # Setup a Psi4 molecule...
  142         Psi4Mol = SetupPsi4Mol(Psi4Handle, Mol, MolCount)
  143         if Psi4Mol is None:
  144             continue
  145         
  146         ValidMolCount += 1
  147         
  148         CalcStatus = GenerateMolCubeFiles(Psi4Handle, Psi4Mol, Mol, MolCount)
  149 
  150         if not CalcStatus:
  151             if not OptionsInfo["QuietMode"]:
  152                 MiscUtil.PrintWarning("Failed to generate cube files for molecule %s" % RDKitUtil.GetMolName(Mol, MolCount))
  153             
  154             CubeFilesFailedCount += 1
  155             continue
  156         
  157     return (MolCount, ValidMolCount, CubeFilesFailedCount)
  158     
  159 def ProcessMoleculesUsingMultipleProcesses(Mols):
  160     """Process and generate dual descriptors cube files for molecules using  multiple processes."""
  161 
  162     MiscUtil.PrintInfo("\nGenerating dual descriptors using multiprocessing...")
  163     
  164     MPParams = OptionsInfo["MPParams"]
  165     
  166     # Setup data for initializing a worker process...
  167     InitializeWorkerProcessArgs = (MiscUtil.ObjectToBase64EncodedString(Options), MiscUtil.ObjectToBase64EncodedString(OptionsInfo))
  168     
  169     # Setup a encoded mols data iterable for a worker process...
  170     WorkerProcessDataIterable = RDKitUtil.GenerateBase64EncodedMolStrings(Mols)
  171     
  172     # Setup process pool along with data initialization for each process...
  173     if not OptionsInfo["QuietMode"]:
  174         MiscUtil.PrintInfo("\nConfiguring multiprocessing using %s method..." % ("mp.Pool.imap()" if re.match("^Lazy$", MPParams["InputDataMode"], re.I) else "mp.Pool.map()"))
  175         MiscUtil.PrintInfo("NumProcesses: %s; InputDataMode: %s; ChunkSize: %s\n" % (MPParams["NumProcesses"], MPParams["InputDataMode"], ("automatic" if MPParams["ChunkSize"] is None else MPParams["ChunkSize"])))
  176     
  177     ProcessPool = mp.Pool(MPParams["NumProcesses"], InitializeWorkerProcess, InitializeWorkerProcessArgs)
  178     
  179     # Start processing...
  180     if re.match("^Lazy$", MPParams["InputDataMode"], re.I):
  181         Results = ProcessPool.imap(WorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"])
  182     elif re.match("^InMemory$", MPParams["InputDataMode"], re.I):
  183         Results = ProcessPool.map(WorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"])
  184     else:
  185         MiscUtil.PrintError("The value, %s, specified for \"--inputDataMode\" is not supported." % (MPParams["InputDataMode"]))
  186 
  187     # Print out Psi4 version in the main process...
  188     MiscUtil.PrintInfo("\nInitializing Psi4...\n")
  189     Psi4Handle  = Psi4Util.InitializePsi4(PrintVersion = True, PrintHeader = False)
  190     OptionsInfo["psi4"] = Psi4Handle
  191     
  192     (MolCount, ValidMolCount, CubeFilesFailedCount) = [0] * 3
  193     for Result in Results:
  194         MolCount += 1
  195         MolIndex, EncodedMol, CalcStatus = Result
  196         
  197         if EncodedMol is None:
  198             continue
  199         
  200         ValidMolCount += 1
  201 
  202         Mol = RDKitUtil.MolFromBase64EncodedMolString(EncodedMol)
  203         
  204         if not CalcStatus:
  205             if not OptionsInfo["QuietMode"]:
  206                 MiscUtil.PrintWarning("Failed to generate cube files for molecule %s" % RDKitUtil.GetMolName(Mol, MolCount))
  207             
  208             CubeFilesFailedCount += 1
  209             continue
  210         
  211     return (MolCount, ValidMolCount, CubeFilesFailedCount)
  212 
  213 def InitializeWorkerProcess(*EncodedArgs):
  214     """Initialize data for a worker process."""
  215     
  216     global Options, OptionsInfo
  217     
  218     if not OptionsInfo["QuietMode"]:
  219         MiscUtil.PrintInfo("Starting process (PID: %s)..." % os.getpid())
  220     
  221     # Decode Options and OptionInfo...
  222     Options = MiscUtil.ObjectFromBase64EncodedString(EncodedArgs[0])
  223     OptionsInfo = MiscUtil.ObjectFromBase64EncodedString(EncodedArgs[1])
  224 
  225     # Psi4 is initialized in the worker process to avoid creation of redundant Psi4
  226     # output files for each process...
  227     OptionsInfo["Psi4Initialized"]  = False
  228 
  229 def InitializePsi4ForWorkerProcess():
  230     """Initialize Psi4 for a worker process."""
  231     
  232     if OptionsInfo["Psi4Initialized"]:
  233         return
  234 
  235     OptionsInfo["Psi4Initialized"] = True
  236     
  237     # Update output file...
  238     OptionsInfo["Psi4RunParams"]["OutputFile"] = Psi4Util.UpdatePsi4OutputFileUsingPID(OptionsInfo["Psi4RunParams"]["OutputFile"], os.getpid())
  239     
  240     # Intialize Psi4...
  241     OptionsInfo["psi4"] = Psi4Util.InitializePsi4(Psi4RunParams = OptionsInfo["Psi4RunParams"], Psi4OptionsParams = OptionsInfo["Psi4OptionsParams"], PrintVersion = False, PrintHeader = True)
  242     
  243 def WorkerProcess(EncodedMolInfo):
  244     """Process data for a worker process."""
  245     
  246     if not OptionsInfo["Psi4Initialized"]:
  247         InitializePsi4ForWorkerProcess()
  248     
  249     MolIndex, EncodedMol = EncodedMolInfo
  250     
  251     CalcStatus = False
  252     
  253     if EncodedMol is None:
  254         return [MolIndex, None, CalcStatus]
  255     
  256     Mol = RDKitUtil.MolFromBase64EncodedMolString(EncodedMol)
  257     MolCount = MolIndex + 1
  258     
  259     if not CheckAndValidateMolecule(Mol, MolCount):
  260         return [MolIndex, None, CalcStatus]
  261     
  262     # Setup a Psi4 molecule...
  263     Psi4Mol = SetupPsi4Mol(OptionsInfo["psi4"], Mol, MolCount)
  264     if Psi4Mol is None:
  265         return [MolIndex, None, CalcStatus]
  266     
  267     CalcStatus = GenerateMolCubeFiles(OptionsInfo["psi4"], Psi4Mol, Mol, MolCount)
  268     
  269     return [MolIndex, RDKitUtil.MolToBase64EncodedMolString(Mol, PropertyPickleFlags = Chem.PropertyPickleOptions.MolProps | Chem.PropertyPickleOptions.PrivateProps), CalcStatus]
  270 
  271 def GenerateMolCubeFiles(Psi4Handle, Psi4Mol, Mol, MolNum = None):
  272     """Generate cube files for dual descriptors."""
  273     
  274     #  Setup reference wave function...
  275     Reference = SetupReferenceWavefunction(Mol)
  276     Psi4Handle.set_options({'Reference': Reference})
  277     
  278     # Setup method name and basis set...
  279     MethodName, BasisSet = SetupMethodNameAndBasisSet(Mol)
  280     
  281     # Calculate single point energy to setup a wavefunction...
  282     Status, Energy, WaveFunction = Psi4Util.CalculateSinglePointEnergy(Psi4Handle, Psi4Mol, MethodName, BasisSet, ReturnWaveFunction = True, Quiet = OptionsInfo["QuietMode"])
  283 
  284     if not Status:
  285         PerformPsi4Cleanup(Psi4Handle)
  286         return (False)
  287     
  288     # Generate cube files...
  289     Status = GenerateCubeFilesForDualDescriptors(Psi4Handle, WaveFunction, Mol, MolNum)
  290     
  291     # Clean up
  292     PerformPsi4Cleanup(Psi4Handle)
  293 
  294     return (True)
  295 
  296 def GenerateCubeFilesForDualDescriptors(Psi4Handle, WaveFunction, Mol, MolNum):
  297     """Generate cube files for dual descriptors."""
  298     
  299     # Setup a temporary local directory to generate cube files...
  300     Status, CubeFilesDir, CubeFilesDirPath = SetupMolCubeFilesDir(Mol, MolNum)
  301     if not Status:
  302         return False
  303     
  304     # Generate cube files using psi4.cubeprop...
  305     Status, Psi4CubeFiles = GenerateCubeFilesUsingCubeprop(Psi4Handle, WaveFunction, Mol, MolNum, CubeFilesDir, CubeFilesDirPath)
  306     if not Status:
  307         return False
  308 
  309     # Copy and rename cube files...
  310     if not MoveAndRenameCubeFiles(Mol, MolNum, Psi4CubeFiles):
  311         return False
  312 
  313     # Delete temporary local directory...
  314     if os.path.isdir(CubeFilesDir):
  315         shutil.rmtree(CubeFilesDir)
  316     
  317     if not GenerateMolFile(Mol, MolNum):
  318         return False
  319     
  320     return True
  321 
  322 def SetupMolCubeFilesDir(Mol, MolNum):
  323     """Setup a directory for generating cube files for a molecule."""
  324     
  325     MolPrefix = SetupMolPrefix(Mol, MolNum)
  326 
  327     CubeFilesDir = "%s_CubeFiles" % (MolPrefix)
  328     CubeFilesDirPath = os.path.join(os.getcwd(), CubeFilesDir)
  329     try:
  330         if os.path.isdir(CubeFilesDir):
  331             shutil.rmtree(CubeFilesDir)
  332         os.mkdir(CubeFilesDir)
  333     except Exception as ErrMsg:
  334         if not OptionsInfo["QuietMode"]:
  335             MiscUtil.PrintWarning("Failed to create cube files: %s\n" % ErrMsg)
  336             MiscUtil.PrintWarning("Ignoring molecule: %s" % RDKitUtil.GetMolName(Mol, MolNum))
  337         return (False, None, None)
  338         
  339     return (True, CubeFilesDir, CubeFilesDirPath)
  340 
  341 def GenerateCubeFilesUsingCubeprop(Psi4Handle, WaveFunction, Mol, MolNum, CubeFilesDir, CubeFilesDirPath):
  342     """Generate cube files using cubeprop."""
  343     
  344     Psi4CubeFilesParams = OptionsInfo["Psi4CubeFilesParams"]
  345 
  346     # Generate cube files...
  347     GridSpacing = Psi4CubeFilesParams["GridSpacing"]
  348     GridOverage = Psi4CubeFilesParams["GridOverage"]
  349     IsoContourThreshold = Psi4CubeFilesParams["IsoContourThreshold"]
  350     try:
  351         Psi4Handle.set_options({"CUBEPROP_TASKS": ['DUAL_DESCRIPTOR'],
  352                                 "CUBIC_GRID_SPACING": [GridSpacing, GridSpacing, GridSpacing],
  353                                 "CUBIC_GRID_OVERAGE": [GridOverage, GridOverage, GridOverage],
  354                                 "CUBEPROP_FILEPATH": CubeFilesDirPath,
  355                                 "CUBEPROP_ISOCONTOUR_THRESHOLD": IsoContourThreshold})
  356         Psi4Handle.cubeprop(WaveFunction)
  357     except Exception as ErrMsg:
  358         shutil.rmtree(CubeFilesDir)
  359         if not OptionsInfo["QuietMode"]:
  360             MiscUtil.PrintWarning("Failed to create cube files: %s\n" % ErrMsg)
  361             MiscUtil.PrintWarning("Ignoring molecule: %s" % RDKitUtil.GetMolName(Mol, MolNum))
  362         return (False, None)
  363 
  364     # Collect cube files...
  365     Psi4CubeFiles = glob.glob(os.path.join(CubeFilesDir, "DUAL*.cube"))
  366     if len(Psi4CubeFiles) == 0:
  367         shutil.rmtree(CubeFilesDir)
  368         if not OptionsInfo["QuietMode"]:
  369             MiscUtil.PrintWarning("Failed to create cube files for dual descriptors...")
  370             MiscUtil.PrintWarning("Ignoring molecule: %s" % RDKitUtil.GetMolName(Mol, MolNum))
  371         return (False, None)
  372     
  373     return (True, Psi4CubeFiles)
  374 
  375 def MoveAndRenameCubeFiles(Mol, MolNum, Psi4CubeFiles):
  376     """Move and rename cube files."""
  377 
  378     for Psi4CubeFileName in Psi4CubeFiles:
  379         try:
  380             NewCubeFileName = SetupMolCubeFileName(Mol, MolNum, Psi4CubeFileName)
  381             shutil.move(Psi4CubeFileName, NewCubeFileName)
  382         except Exception as ErrMsg:
  383             if not OptionsInfo["QuietMode"]:
  384                 MiscUtil.PrintWarning("Failed to move cube file: %s\n" % ErrMsg)
  385                 MiscUtil.PrintWarning("Ignoring molecule: %s" % RDKitUtil.GetMolName(Mol, MolNum))
  386             return False
  387     
  388     return True
  389 
  390 def GenerateMolFile(Mol, MolNum):
  391     """Generate a SD file for molecules corresponding to cube files."""
  392 
  393     Outfile = SetupMolFileName(Mol, MolNum)
  394     
  395     OutfileParams = {}
  396     Writer = RDKitUtil.MoleculesWriter(Outfile, **OutfileParams)
  397     if Writer is None:
  398         if not OptionsInfo["QuietMode"]:
  399             MiscUtil.PrintError("Failed to setup a writer for output fie %s " % Outfile)
  400         return False
  401     
  402     Writer.write(Mol)
  403 
  404     if Writer is not None:
  405         Writer.close()
  406     
  407     return True
  408 
  409 def VisualizeDualDescriptors():
  410     """Visualize cube files for dual descriptors."""
  411     
  412     MiscUtil.PrintInfo("\nVisualizing cube files for dual descriptors...")
  413 
  414     if not OptionsInfo["GenerateCubeFiles"]:
  415         MiscUtil.PrintInfo("\nInitializing Psi4...\n")
  416         Psi4Handle  = Psi4Util.InitializePsi4(PrintVersion = True, PrintHeader = False)
  417         OptionsInfo["psi4"] = Psi4Handle
  418     
  419     # Setup a molecule reader...
  420     MiscUtil.PrintInfo("Processing file %s..." % OptionsInfo["Infile"])
  421     Mols  = RDKitUtil.ReadMolecules(OptionsInfo["InfilePath"], **OptionsInfo["InfileParams"])
  422     
  423     # Setup for writing a PyMOL PML file...
  424     Outfile = OptionsInfo["Outfile"]
  425     OutFH = open(Outfile, "w")
  426     if OutFH is None:
  427         MiscUtil.PrintError("Failed to open output fie %s " % Outfile)
  428     MiscUtil.PrintInfo("\nGenerating file %s..." % Outfile)
  429 
  430     # Setup header...
  431     WritePMLHeader(OutFH, ScriptName)
  432     WritePyMOLParameters(OutFH)
  433 
  434     # Process cube files for molecules...
  435     FirstMol = True
  436     (MolCount, ValidMolCount, CubeFilesMissingCount) = [0] * 3
  437     for Mol in Mols:
  438         MolCount += 1
  439 
  440         if Mol is None:
  441             if not OptionsInfo["QuietMode"]:
  442                 MiscUtil.PrintInfo("\nProcessing molecule number %s..." % MolCount)
  443             continue
  444         ValidMolCount += 1
  445 
  446         MolName = RDKitUtil.GetMolName(Mol, MolCount)
  447         if not OptionsInfo["QuietMode"]:
  448             MiscUtil.PrintInfo("\nProcessing molecule %s..." % MolName)
  449             
  450         # Retrieve cube files...
  451         CubeFilesStatus, CubeFilesInfo = RetrieveMolCubeFilesInfo(Mol, MolCount)
  452         if not CubeFilesStatus:
  453             CubeFilesMissingCount += 1
  454             if not OptionsInfo["QuietMode"]:
  455                 MiscUtil.PrintWarning("Ignoring molecule with missing cube file...\n")
  456             continue
  457         
  458         # Setup PyMOL object names..
  459         PyMOLObjectNames = SetupPyMOLObjectNames(Mol, MolCount, CubeFilesInfo)
  460         
  461         # Setup molecule view...
  462         WriteMolView(OutFH, CubeFilesInfo, PyMOLObjectNames)
  463         
  464         # Setup dual descriptor views...
  465         WriteDualDescriptorView(OutFH, CubeFilesInfo, PyMOLObjectNames)
  466 
  467         # Setup dual descriptor level group...
  468         Enable, Action = [True, "open"]
  469         GenerateAndWritePMLForGroup(OutFH, PyMOLObjectNames["DualGroup"], PyMOLObjectNames["DualGroupMembers"], Enable, Action)
  470         
  471         # Setup mol level group...
  472         Enable, Action = [False, "close"]
  473         if FirstMol:
  474             FirstMol = False
  475             Enable, Action = [True, "open"]
  476         GenerateAndWritePMLForGroup(OutFH, PyMOLObjectNames["MolGroup"], PyMOLObjectNames["MolGroupMembers"], Enable, Action)
  477     
  478     OutFH.close()
  479     
  480     MiscUtil.PrintInfo("\nTotal number of molecules: %d" % MolCount)
  481     MiscUtil.PrintInfo("Number of valid molecules: %d" % ValidMolCount)
  482     MiscUtil.PrintInfo("Number of molecules with missing cube files: %d" % CubeFilesMissingCount)
  483     MiscUtil.PrintInfo("Number of ignored molecules: %d" % (MolCount - ValidMolCount + CubeFilesMissingCount))
  484 
  485 def WritePMLHeader(OutFH, ScriptName):
  486     """Write out PML header."""
  487 
  488     HeaderInfo = """\
  489 #
  490 # This file is automatically generated by the following Psi4 script available in
  491 # MayaChemTools: %s
  492 #
  493 cmd.reinitialize() """ % (ScriptName)
  494     
  495     OutFH.write("%s\n" % HeaderInfo)
  496 
  497 def WritePyMOLParameters(OutFH):
  498     """Write out PyMOL global parameters."""
  499 
  500     OutFH.write("""\n""\n"Setting up PyMOL gobal parameters..."\n""\n""")
  501     PMLCmds = []
  502     PMLCmds.append("""cmd.set("mesh_quality", %s)""" % (OptionsInfo["PyMOLViewParams"]["MeshQuality"]))
  503     PMLCmds.append("""cmd.set("mesh_width", %.2f)""" % (OptionsInfo["PyMOLViewParams"]["MeshWidth"]))
  504     
  505     PMLCmds.append("""cmd.set("surface_quality", %s)""" % (OptionsInfo["PyMOLViewParams"]["SurfaceQuality"]))
  506     PMLCmds.append("""cmd.set("transparency", %.2f)""" % (OptionsInfo["PyMOLViewParams"]["SurfaceTransparency"]))
  507     PML = "\n".join(PMLCmds)
  508     OutFH.write("%s\n" % PML)
  509     
  510     if OptionsInfo["PyMOLViewParams"]["SetupGlobalVolumeColorRamp"]:
  511         PyMOLViewParams = OptionsInfo["PyMOLViewParams"]
  512         GenerateAndWritePMLForVolumeColorRamp(OutFH, PyMOLViewParams["GlobalVolumeColorRampName"], PyMOLViewParams["ContourLevel1"], PyMOLViewParams["ContourColor1"], PyMOLViewParams["ContourLevel2"], PyMOLViewParams["ContourColor2"], PyMOLViewParams["VolumeContourWindowFactor"], PyMOLViewParams["VolumeColorRampOpacity"])
  513 
  514 def WriteMolView(OutFH, CubeFilesInfo, PyMOLObjectNames):
  515     """Write out PML for viewing molecules."""
  516     
  517     MolName = CubeFilesInfo["MolName"]
  518     MolFile = CubeFilesInfo["MolFile"]
  519     
  520     OutFH.write("""\n""\n"Loading %s and setting up view for molecule..."\n""\n""" % MolFile)
  521     
  522     PyMOLViewParams = OptionsInfo["PyMOLViewParams"]
  523     PMLCmds = []
  524     
  525     # Molecule...
  526     Name = PyMOLObjectNames["Mol"]
  527     PMLCmds.append("""cmd.load("%s", "%s")""" % (MolFile, Name))
  528     PMLCmds.append("""cmd.hide("everything", "%s")""" % (Name))
  529     PMLCmds.append("""util.cbag("%s", _self = cmd)""" % (Name))
  530     PMLCmds.append("""cmd.show("sticks", "%s")""" % (Name))
  531     PMLCmds.append("""cmd.set("stick_radius", %s, "%s")""" % (PyMOLViewParams["DisplayStickRadius"], Name))
  532     if PyMOLViewParams["HideHydrogens"]:
  533         PMLCmds.append("""cmd.hide("(%s and hydro)")""" % (Name))
  534     if re.match("^Sticks$", PyMOLViewParams["DisplayMolecule"]):
  535         PMLCmds.append("""cmd.enable("%s")""" % (Name))
  536     else:
  537         PMLCmds.append("""cmd.disable("%s")""" % (Name))
  538 
  539     # Molecule ball and stick...
  540     Name = PyMOLObjectNames["MolBallAndStick"]
  541     PMLCmds.append("""cmd.load("%s", "%s")""" % (MolFile, Name))
  542     PMLCmds.append("""cmd.hide("everything", "%s")""" % (Name))
  543     PMLCmds.append("""util.cbag("%s", _self = cmd)""" % (Name))
  544     PMLCmds.append("""cmd.show("sphere", "%s")""" % (Name))
  545     PMLCmds.append("""cmd.show("sticks", "%s")""" % (Name))
  546     PMLCmds.append("""cmd.set("sphere_scale", %s, "%s")""" % (PyMOLViewParams["DisplaySphereScale"], Name))
  547     PMLCmds.append("""cmd.set("stick_radius", %s, "%s")""" % (PyMOLViewParams["DisplayStickRadius"], Name))
  548     if PyMOLViewParams["HideHydrogens"]:
  549         PMLCmds.append("""cmd.hide("(%s and hydro)")""" % (Name))
  550     if re.match("^BallAndStick$", PyMOLViewParams["DisplayMolecule"]):
  551         PMLCmds.append("""cmd.enable("%s")""" % (Name))
  552     else:
  553         PMLCmds.append("""cmd.disable("%s")""" % (Name))
  554     
  555     PML = "\n".join(PMLCmds)
  556     OutFH.write("%s\n" % PML)
  557     
  558     # MolAlone group...
  559     GenerateAndWritePMLForGroup(OutFH, PyMOLObjectNames["MolAloneGroup"], PyMOLObjectNames["MolAloneGroupMembers"], True, "open")
  560 
  561 def WriteDualDescriptorView(OutFH, CubeFilesInfo, PyMOLObjectNames):
  562     """Write out PML for viewing dual descriptors in a molecule."""
  563     
  564     OutFH.write("""\n""\n"Setting up views for dual descriptor..."\n""\n""")
  565     
  566     # Cube...
  567     DualCubeFile = CubeFilesInfo["FileName"]
  568     DualCubeName = PyMOLObjectNames["DualCube"]
  569     PMLCmds = []
  570     PMLCmds.append("""cmd.load("%s", "%s")""" % (DualCubeFile, DualCubeName))
  571     PMLCmds.append("""cmd.disable("%s")""" % (DualCubeName))
  572     PMLCmds.append("")
  573     PML = "\n".join(PMLCmds)
  574     OutFH.write("%s\n" % PML)
  575 
  576     ContourLevel1 = CubeFilesInfo["ContourLevel1"]
  577     ContourLevel2 = CubeFilesInfo["ContourLevel2"]
  578     
  579     ContourColor1 = CubeFilesInfo["ContourColor1"]
  580     ContourColor2 = CubeFilesInfo["ContourColor2"]
  581     
  582     ContourWindowFactor = CubeFilesInfo["ContourWindowFactor"]
  583     ContourColorOpacity = CubeFilesInfo["ContourColorOpacity"]
  584     
  585     # Volume ramp...
  586     if CubeFilesInfo["SetupLocalVolumeColorRamp"]:
  587         GenerateAndWritePMLForVolumeColorRamp(OutFH, CubeFilesInfo["VolumeColorRampName"], ContourLevel1, ContourColor1, ContourLevel2, ContourColor2, ContourWindowFactor, ContourColorOpacity)
  588     
  589     # Volume...
  590     DualVolumeName = PyMOLObjectNames["DualVolume"]
  591     PMLCmds = []
  592     PMLCmds.append("""cmd.volume("%s", "%s", "%s")""" % (DualVolumeName, DualCubeName, CubeFilesInfo["VolumeColorRampName"]))
  593     PMLCmds.append("""cmd.disable("%s")""" % (DualVolumeName))
  594     PMLCmds.append("")
  595     PML = "\n".join(PMLCmds)
  596     OutFH.write("%s\n" % PML)
  597 
  598     # Mesh...
  599     PMLCmds = []
  600     DualMeshName = PyMOLObjectNames["DualMeshPositive"]
  601     PMLCmds.append("""cmd.isomesh("%s", "%s", "%s")""" % (DualMeshName, DualCubeName, ContourLevel2))
  602     PMLCmds.append("""util.color_deep("%s", "%s")""" % (ContourColor2, DualMeshName))
  603     PMLCmds.append("""cmd.enable("%s")""" % (DualMeshName))
  604     PMLCmds.append("")
  605     
  606     DualMeshName = PyMOLObjectNames["DualMeshNegative"]
  607     PMLCmds.append("""cmd.isomesh("%s", "%s", "%s")""" % (DualMeshName, DualCubeName, ContourLevel1))
  608     PMLCmds.append("""util.color_deep("%s", "%s")""" % (ContourColor1, DualMeshName))
  609     PMLCmds.append("""cmd.enable("%s")""" % (DualMeshName))
  610     PMLCmds.append("")
  611     PML = "\n".join(PMLCmds)
  612     OutFH.write("%s\n" % PML)
  613     
  614     GenerateAndWritePMLForGroup(OutFH, PyMOLObjectNames["DualMeshGroup"], PyMOLObjectNames["DualMeshGroupMembers"], False, "open")
  615     
  616     # Surface...
  617     PMLCmds = []
  618     DualSurfaceName = PyMOLObjectNames["DualSurfacePositive"]
  619     PMLCmds.append("""cmd.isosurface("%s", "%s", "%s")""" % (DualSurfaceName, DualCubeName, ContourLevel2))
  620     PMLCmds.append("""util.color_deep("%s", "%s")""" % (ContourColor2, DualSurfaceName))
  621     PMLCmds.append("""cmd.enable("%s")""" % (DualSurfaceName))
  622     PMLCmds.append("")
  623     
  624     DualSurfaceName = PyMOLObjectNames["DualSurfaceNegative"]
  625     PMLCmds.append("""cmd.isosurface("%s", "%s", "%s")""" % (DualSurfaceName, DualCubeName, ContourLevel1))
  626     PMLCmds.append("""util.color_deep("%s", "%s")""" % (ContourColor1, DualSurfaceName))
  627     PMLCmds.append("""cmd.enable("%s")""" % (DualSurfaceName))
  628     PMLCmds.append("")
  629     PML = "\n".join(PMLCmds)
  630     OutFH.write("%s\n" % PML)
  631     
  632     GenerateAndWritePMLForGroup(OutFH, PyMOLObjectNames["DualSurfaceGroup"], PyMOLObjectNames["DualSurfaceGroupMembers"], True, "open")
  633 
  634 def GenerateAndWritePMLForVolumeColorRamp(OutFH, ColorRampName, ContourLevel1, ContourColor1, ContourLevel2, ContourColor2, ContourWindowFactor, ContourColorOpacity):
  635     """Write out PML for volume color ramp."""
  636 
  637     RampWindowOffset1 = abs(ContourLevel1 * ContourWindowFactor)
  638     RampWindowOffset2 = abs(ContourLevel2 * ContourWindowFactor)
  639     
  640     PML = """\
  641 cmd.volume_ramp_new("%s", [%.4f, "%s", 0.00, %.4f, "%s", %s, %.4f, "%s", 0.00, %4f, "%s", 0.00, %.4f, "%s", %s, %4f, "%s", 0.00])""" % (ColorRampName, (ContourLevel1 - RampWindowOffset1), ContourColor1, ContourLevel1, ContourColor1, ContourColorOpacity, (ContourLevel1 + RampWindowOffset1), ContourColor1, (ContourLevel2 - RampWindowOffset2), ContourColor2, ContourLevel2, ContourColor2, ContourColorOpacity, (ContourLevel2 + RampWindowOffset2), ContourColor2)
  642     
  643     OutFH.write("%s\n" % PML)
  644 
  645 def GenerateAndWritePMLForGroup(OutFH, GroupName, GroupMembersList, Enable, Action):
  646     """Generate and write PML for group."""
  647     
  648     OutFH.write("""\n""\n"Setting up group %s..."\n""\n""" % GroupName)
  649     
  650     PMLCmds = []
  651     
  652     GroupMembers = " ".join(GroupMembersList)
  653     PMLCmds.append("""cmd.group("%s", "%s")""" % (GroupName, GroupMembers))
  654     
  655     if Enable is not None:
  656         if Enable:
  657             PMLCmds.append("""cmd.enable("%s")""" % GroupName)
  658         else:
  659             PMLCmds.append("""cmd.disable("%s")""" % GroupName)
  660     
  661     if Action is not None:
  662         PMLCmds.append("""cmd.group("%s", action="%s")""" % (GroupName, Action))
  663 
  664     PML = "\n".join(PMLCmds)
  665     
  666     OutFH.write("%s\n\n" % PML)
  667 
  668 def RetrieveMolCubeFilesInfo(Mol, MolNum):
  669     """Retrieve available cube files info for a molecule."""
  670 
  671     # Initialize cube files info...
  672     CubeFilesInfo = {}
  673     CubeFilesInfo["FileName"] = None
  674     CubeFilesInfo["IsocontourRangeMin"] = None
  675     CubeFilesInfo["IsocontourRangeMax"] = None
  676     
  677     CubeFilesInfo["ContourLevel1"] = None
  678     CubeFilesInfo["ContourLevel2"] = None
  679     
  680     CubeFilesInfo["ContourColor1"] = None
  681     CubeFilesInfo["ContourColor2"] = None
  682     
  683     CubeFilesInfo["ContourWindowFactor"] = None
  684     CubeFilesInfo["ContourColorOpacity"] = None
  685     
  686     CubeFilesInfo["VolumeColorRampName"] = None
  687     CubeFilesInfo["SetupLocalVolumeColorRamp"] = False
  688 
  689     # Setup cube mol info...
  690     CubeFilesInfo["MolPrefix"] = SetupMolPrefix(Mol, MolNum)
  691     CubeFilesInfo["MolName"] = SetupMolName(Mol, MolNum)
  692     CubeFilesInfo["MolFile"] = SetupMolFileName(Mol, MolNum)
  693 
  694     # Retrieve cube files...
  695     CubeFiles = glob.glob("%s*DUAL*.cube" % (CubeFilesInfo["MolPrefix"]))
  696 
  697     if len(CubeFiles) == 0:
  698         return (False, CubeFilesInfo)
  699 
  700     CubeFileName = CubeFiles[0]
  701     if  len(CubeFiles) > 1:
  702         if not OptionsInfo["QuietMode"]:
  703             MiscUtil.PrintWarning("Multiple cube files available for molecule %s; Using first cube file, %s, to visualize dual descriptor..." % (RDKitUtil.GetMolName(Mol, MolNum), CubeFileName))
  704     CubeFilesInfo["FileName"] = CubeFileName
  705     
  706     IsocontourRangeMin, IsocontourRangeMax, ContourLevel1, ContourLevel2 = SetupIsocontourRangeAndContourLevels(CubeFileName)
  707     CubeFilesInfo["IsocontourRangeMin"] = IsocontourRangeMin
  708     CubeFilesInfo["IsocontourRangeMax"] = IsocontourRangeMax
  709     
  710     CubeFilesInfo["ContourLevel1"] = ContourLevel1
  711     CubeFilesInfo["ContourLevel2"] = ContourLevel2
  712     CubeFilesInfo["ContourColor1"] = OptionsInfo["PyMOLViewParams"]["ContourColor1"]
  713     CubeFilesInfo["ContourColor2"] = OptionsInfo["PyMOLViewParams"]["ContourColor2"]
  714     
  715     CubeFilesInfo["ContourWindowFactor"] = OptionsInfo["PyMOLViewParams"]["VolumeContourWindowFactor"]
  716     CubeFilesInfo["ContourColorOpacity"] = OptionsInfo["PyMOLViewParams"]["VolumeColorRampOpacity"]
  717 
  718     VolumeColorRampName = SetupVolumeColorRampName(CubeFilesInfo)
  719     CubeFilesInfo["VolumeColorRampName"] = VolumeColorRampName
  720     CubeFilesInfo["SetupLocalVolumeColorRamp"] = False if OptionsInfo["PyMOLViewParams"]["SetupGlobalVolumeColorRamp"] else True
  721     
  722     return (True, CubeFilesInfo)
  723 
  724 def SetupVolumeColorRampName(CubeFilesInfo):
  725     """Setup volume color ramp name and status."""
  726     
  727     PyMOLViewParams = OptionsInfo["PyMOLViewParams"]
  728     
  729     # Setup info for generating local volume color ramps for individual cube files...
  730     VolumeColorRampName = PyMOLViewParams["VolumeColorRamp"]
  731     if PyMOLViewParams["VolumeColorRampAuto"]:
  732         VolumeColorRampName = SetupDefaultVolumeRampName()
  733         if PyMOLViewParams["ContourLevel1Auto"] or PyMOLViewParams["ContourLevel2Auto"]:
  734             VolumeColorRampName = "%s_%s" % (CubeFilesInfo["MolPrefix"], VolumeColorRampName)
  735     
  736     return VolumeColorRampName
  737 
  738 def SetupIsocontourRangeAndContourLevels(CubeFileName):
  739     """Setup isocontour range."""
  740 
  741     PyMOLViewParams = OptionsInfo["PyMOLViewParams"]
  742     
  743     # Setup isocontour range and contour levels...
  744     IsocontourRangeMin, IsocontourRangeMax = Psi4Util.RetrieveIsocontourRangeFromCubeFile(CubeFileName)
  745 
  746     DefaultIsocontourRange = 0.06
  747     if IsocontourRangeMin is None:
  748         IsocontourRangeMin = -DefaultIsocontourRange
  749         if not OptionsInfo["QuietMode"]:
  750             MiscUtil.PrintInfo("Failed to retrieve isocontour range from the cube file. Setting min isocontour value to %s..." % (IsocontourRangeMin))
  751     
  752     if IsocontourRangeMax is None:
  753         IsocontourRangeMax = DefaultIsocontourRange
  754         if not OptionsInfo["QuietMode"]:
  755             MiscUtil.PrintInfo("Failed to retrieve isocontour range from the cube file. Setting max isocontour value to %s..." % (IsocontourRangeMax))
  756 
  757     # Setup contour levels...
  758     ContourLevel = max(abs(IsocontourRangeMin), abs(IsocontourRangeMax)) * PyMOLViewParams["ContourLevelAutoAt"]
  759     if ContourLevel >= 0.01:
  760         ContourLevel = float("%.2f" % ContourLevel)
  761     elif ContourLevel >= 0.001:
  762         ContourLevel = float("%.3f" % ContourLevel)
  763     elif ContourLevel >= 0.0001:
  764         ContourLevel = float("%.4f" % ContourLevel)
  765     
  766     ContourLevel1 = -ContourLevel if PyMOLViewParams["ContourLevel1Auto"] else PyMOLViewParams["ContourLevel1"]
  767     ContourLevel2 = ContourLevel if PyMOLViewParams["ContourLevel2Auto"] else PyMOLViewParams["ContourLevel2"]
  768     
  769     if not OptionsInfo["QuietMode"]:
  770         if IsocontourRangeMin is not None and IsocontourRangeMax is not None:
  771             MiscUtil.PrintInfo("CubeFileName: %s; Isocontour range for %s percent of the density: %.4f to %.4f; ContourLevel1: %s; ContourLevel2: %s" % (CubeFileName, (100 * OptionsInfo["Psi4CubeFilesParams"]["IsoContourThreshold"]), IsocontourRangeMin, IsocontourRangeMax, ContourLevel1, ContourLevel2))
  772 
  773     return (IsocontourRangeMin, IsocontourRangeMax, ContourLevel1, ContourLevel2)
  774     
  775 def SetupPyMOLObjectNames(Mol, MolNum, CubeFilesInfo):
  776     """Setup PyMOL object names."""
  777     
  778     PyMOLObjectNames = {}
  779 
  780     SetupPyMOLObjectNamesForMol(Mol, MolNum, CubeFilesInfo, PyMOLObjectNames)
  781     SetupPyMOLObjectNamesForDualDescriptors(Mol, MolNum, CubeFilesInfo, PyMOLObjectNames)
  782     
  783     return PyMOLObjectNames
  784 
  785 def SetupPyMOLObjectNamesForMol(Mol, MolNum, CubeFilesInfo, PyMOLObjectNames):
  786     """Setup groups and objects for molecule."""
  787     
  788     MolFileRoot = CubeFilesInfo["MolPrefix"]
  789     
  790     MolGroupName = "%s" % MolFileRoot
  791     PyMOLObjectNames["MolGroup"] = MolGroupName
  792     PyMOLObjectNames["MolGroupMembers"] = []
  793 
  794     # Molecule alone group...
  795     MolAloneGroupName = "%s.Molecule" % (MolGroupName)
  796     PyMOLObjectNames["MolAloneGroup"] = MolAloneGroupName
  797     PyMOLObjectNames["MolGroupMembers"].append(MolAloneGroupName)
  798     
  799     PyMOLObjectNames["MolAloneGroupMembers"] = []
  800 
  801     # Molecule...
  802     MolName = "%s.Molecule" % (MolAloneGroupName)
  803     PyMOLObjectNames["Mol"] = MolName
  804     PyMOLObjectNames["MolAloneGroupMembers"].append(MolName)
  805     
  806     # BallAndStick...
  807     MolBallAndStickName = "%s.BallAndStick" % (MolAloneGroupName)
  808     PyMOLObjectNames["MolBallAndStick"] = MolBallAndStickName
  809     PyMOLObjectNames["MolAloneGroupMembers"].append(MolBallAndStickName)
  810     
  811 def SetupPyMOLObjectNamesForDualDescriptors(Mol, MolNum, CubeFilesInfo, PyMOLObjectNames):
  812     """Setup groups and objects for dual descriptors."""
  813     
  814     MolGroupName = PyMOLObjectNames["MolGroup"]
  815 
  816     # Setup dual group..
  817     DualGroupName = "%s.Dual" % (MolGroupName)
  818     PyMOLObjectNames["DualGroup"] = DualGroupName
  819     PyMOLObjectNames["MolGroupMembers"].append(DualGroupName)
  820     PyMOLObjectNames["DualGroupMembers"] = []
  821     
  822     # Cube...
  823     DualCubeName = "%s.Cube" % DualGroupName
  824     PyMOLObjectNames["DualCube"] = DualCubeName
  825     PyMOLObjectNames["DualGroupMembers"].append(DualCubeName)
  826 
  827     # Volume...
  828     DualVolumeName = "%s.Volume" % DualGroupName
  829     PyMOLObjectNames["DualVolume"] = DualVolumeName
  830     PyMOLObjectNames["DualGroupMembers"].append(DualVolumeName)
  831     
  832     # Mesh...
  833     DualMeshGroupName = "%s.Mesh" % DualGroupName
  834     DualMeshPositiveName = "%s.Positive_Electrophilic" % DualMeshGroupName
  835     DualMeshNegativeName = "%s.Negative_Nucleophilic" % DualMeshGroupName
  836 
  837     PyMOLObjectNames["DualMeshGroup"] =  DualMeshGroupName
  838     PyMOLObjectNames["DualGroupMembers"].append(DualMeshGroupName)
  839 
  840     PyMOLObjectNames["DualMeshPositive"] = DualMeshPositiveName
  841     PyMOLObjectNames["DualMeshNegative"] = DualMeshNegativeName
  842     PyMOLObjectNames["DualMeshGroupMembers"] = []
  843     PyMOLObjectNames["DualMeshGroupMembers"].append(DualMeshPositiveName)
  844     PyMOLObjectNames["DualMeshGroupMembers"].append(DualMeshNegativeName)
  845     
  846     # Surface....
  847     DualSurfaceGroupName = "%s.Surface" % DualGroupName
  848     DualSurfacePositiveName = "%s.Positive_Electrophilic" % DualSurfaceGroupName
  849     DualSurfaceNegativeName = "%s.Negative_Nucleophilic" % DualSurfaceGroupName
  850 
  851     PyMOLObjectNames["DualSurfaceGroup"] =  DualSurfaceGroupName
  852     PyMOLObjectNames["DualGroupMembers"].append(DualSurfaceGroupName)
  853 
  854     PyMOLObjectNames["DualSurfacePositive"] = DualSurfacePositiveName
  855     PyMOLObjectNames["DualSurfaceNegative"] = DualSurfaceNegativeName
  856     PyMOLObjectNames["DualSurfaceGroupMembers"] = []
  857     PyMOLObjectNames["DualSurfaceGroupMembers"].append(DualSurfacePositiveName)
  858     PyMOLObjectNames["DualSurfaceGroupMembers"].append(DualSurfaceNegativeName)
  859     
  860 def SetupMolFileName(Mol, MolNum):
  861     """Setup SD file name for a molecule. """
  862 
  863     MolPrefix = SetupMolPrefix(Mol, MolNum)
  864     MolFileName = "%s.sdf" % MolPrefix
  865 
  866     return MolFileName
  867     
  868 def SetupMolCubeFileName(Mol, MolNum, Psi4CubeFileName):
  869     """Setup cube file name for a molecule."""
  870 
  871     FileDir, FileName, FileExt = MiscUtil.ParseFileName(Psi4CubeFileName)
  872     CubeFileName = "%s.%s" % (FileName, FileExt)
  873 
  874     # Add MolPrefix...
  875     MolPrefix = SetupMolPrefix(Mol, MolNum)
  876     CubeFileName = "%s_%s" % (MolPrefix, CubeFileName)
  877     
  878     return CubeFileName
  879     
  880 def SetupMolPrefix(Mol, MolNum):
  881     """Get molecule prefix for generating files and directories."""
  882 
  883     MolNamePrefix = ''
  884     if Mol.HasProp("_Name"):
  885         MolNamePrefix = re.sub("[^a-zA-Z0-9]", "_", Mol.GetProp("_Name"))
  886     
  887     MolNumPrefix = "Mol%s" % MolNum
  888 
  889     if OptionsInfo["UseMolNumPrefix"] and OptionsInfo["UseMolNamePrefix"]:
  890         MolPrefix = MolNumPrefix
  891         if len(MolNamePrefix):
  892             MolPrefix = "%s_%s" % (MolNumPrefix, MolNamePrefix)
  893     elif OptionsInfo["UseMolNamePrefix"]:
  894         MolPrefix = MolNamePrefix if len(MolNamePrefix) else MolNumPrefix
  895     elif OptionsInfo["UseMolNumPrefix"]:
  896         MolPrefix = MolNumPrefix
  897     
  898     return MolPrefix
  899 
  900 def SetupMolName(Mol, MolNum):
  901     """Get molecule name."""
  902 
  903     # Test for creating PyMOL object name for molecule...
  904     MolNamePrefix = ''
  905     if Mol.HasProp("_Name"):
  906         MolName = re.sub("[^a-zA-Z0-9]", "_", Mol.GetProp("_Name"))
  907     else:
  908         MolName = "Mol%s" % MolNum
  909     
  910     return MolName
  911 
  912 def SetupDefaultVolumeRampName():
  913     """Setup a default name for a volume ramp."""
  914 
  915     return "psi4_cube_dual"
  916 
  917 def SetupPsi4Mol(Psi4Handle, Mol, MolCount = None):
  918     """Setup a Psi4 molecule object."""
  919     
  920     MolGeometry = RDKitUtil.GetPsi4XYZFormatString(Mol, NoCom = True, NoReorient = True)
  921     
  922     try:
  923         Psi4Mol = Psi4Handle.geometry(MolGeometry)
  924     except Exception as ErrMsg:
  925         Psi4Mol = None
  926         if not OptionsInfo["QuietMode"]:
  927             MiscUtil.PrintWarning("Failed to create Psi4 molecule from geometry string: %s\n" % ErrMsg)
  928             MolName = RDKitUtil.GetMolName(Mol, MolCount)
  929             MiscUtil.PrintWarning("Ignoring molecule: %s" % MolName)
  930     
  931     return Psi4Mol
  932 
  933 def PerformPsi4Cleanup(Psi4Handle):
  934     """Perform clean up."""
  935     
  936     # Clean up after Psi4 run ...
  937     Psi4Handle.core.clean()
  938     
  939     # Clean up any leftover scratch files...
  940     if OptionsInfo["MPMode"]:
  941         Psi4Util.RemoveScratchFiles(Psi4Handle, OptionsInfo["Psi4RunParams"]["OutputFile"])
  942 
  943 def CheckAndValidateMolecule(Mol, MolCount = None):
  944     """Validate molecule for Psi4 calculations."""
  945     
  946     if Mol is None:
  947         if not OptionsInfo["QuietMode"]:
  948             MiscUtil.PrintInfo("\nProcessing molecule number %s..." % MolCount)
  949         return False
  950     
  951     MolName = RDKitUtil.GetMolName(Mol, MolCount)
  952     if not OptionsInfo["QuietMode"]:
  953         MiscUtil.PrintInfo("\nProcessing molecule %s..." % MolName)
  954     
  955     if RDKitUtil.IsMolEmpty(Mol):
  956         if not OptionsInfo["QuietMode"]:
  957             MiscUtil.PrintWarning("Ignoring empty molecule: %s\n" % MolName)
  958         return False
  959     
  960     if not RDKitUtil.ValidateElementSymbols(RDKitUtil.GetAtomSymbols(Mol)):
  961         if not OptionsInfo["QuietMode"]:
  962             MiscUtil.PrintWarning("Ignoring molecule containing invalid element symbols: %s\n" % MolName)
  963         return False
  964     
  965     # Check for spin multiplicity to warn about open-shell systems...
  966     if RDKitUtil.GetSpinMultiplicity(Mol) > 1:
  967         if not OptionsInfo["QuietMode"]:
  968             MiscUtil.PrintWarning("Generation of dual descriptors not supported for open-shell systems. Ignoring  molecule containing unpaired electrons: %s\n" % MolName)
  969         return False
  970     
  971     # Check for 3D flag...
  972     if not Mol.GetConformer().Is3D():
  973         if not OptionsInfo["QuietMode"]:
  974             MiscUtil.PrintWarning("3D tag is not set for molecule: %s\n" % MolName)
  975     
  976     # Check for missing hydrogens...
  977     if RDKitUtil.AreHydrogensMissingInMolecule(Mol):
  978         if not OptionsInfo["QuietMode"]:
  979             MiscUtil.PrintWarning("Missing hydrogens in molecule: %s\n" % MolName)
  980 
  981     return True
  982 
  983 def SetupMethodNameAndBasisSet(Mol):
  984     """Setup method name and basis set."""
  985     
  986     MethodName = OptionsInfo["MethodName"]
  987     if OptionsInfo["MethodNameAuto"]:
  988         MethodName = "B3LYP"
  989     
  990     BasisSet = OptionsInfo["BasisSet"]
  991     if OptionsInfo["BasisSetAuto"]:
  992         BasisSet = "6-31+G**" if RDKitUtil.IsAtomSymbolPresentInMol(Mol, "S") else "6-31G**"
  993     
  994     return (MethodName, BasisSet)
  995 
  996 def SetupReferenceWavefunction(Mol):
  997     """Setup reference wavefunction."""
  998 
  999     Reference = OptionsInfo["Reference"]
 1000     if OptionsInfo["ReferenceAuto"]:
 1001         Reference = 'UHF' if (RDKitUtil.GetSpinMultiplicity(Mol) > 1) else 'RHF'
 1002     
 1003     return Reference
 1004 
 1005 def CheckAndSetupOutfilesDir():
 1006     """Check and setup outfiles directory."""
 1007     
 1008     if OptionsInfo["GenerateCubeFiles"]:
 1009         if os.path.isdir(OptionsInfo["OutfilesDir"]):
 1010             if not Options["--overwrite"]:
 1011                 MiscUtil.PrintError("The outfiles directory, %s, corresponding to output file specified, %s, for option \"-o, --outfile\" already exist. Use option \"--overwrite\" or \"--ov\"  and try again to generate and visualize cube files...\n" % (OptionsInfo["OutfilesDir"], OptionsInfo["Outfile"]))
 1012     
 1013     if OptionsInfo["VisualizeCubeFiles"]:
 1014         if not Options["--overwrite"]:
 1015             if  os.path.exists(os.path.join(OptionsInfo["OutfilesDir"], OptionsInfo["Outfile"])):
 1016                 MiscUtil.PrintError("The outfile file specified, %s, for option \"-o, --outfile\" already exist in outfiles dir, %s. Use option \"--overwrite\" or \"--ov\" to overwrite and try again to generate and visualize cube files.\n" % (OptionsInfo["Outfile"], OptionsInfo["OutfilesDir"]))
 1017                     
 1018         if not OptionsInfo["GenerateCubeFiles"]:
 1019             if not os.path.isdir(OptionsInfo["OutfilesDir"]):
 1020                 MiscUtil.PrintError("The outfiles directory, %s, corresponding to output file specified, %s, for option \"-o, --outfile\" doesn't exist. Use value, GenerateCubeFiles or Both, for option \"--mode\" and try again to generate and visualize cube files...\n" % (OptionsInfo["OutfilesDir"], OptionsInfo["Outfile"]))
 1021             
 1022             CubeFiles = glob.glob(os.path.join(OptionsInfo["OutfilesDir"], "*.cube"))
 1023             if not len(CubeFiles):
 1024                 MiscUtil.PrintError("The outfiles directory, %s, contains no cube files, \"*.cube\". Use value, GenerateCubeFiles or Both, for option \"--mode\" and try again to generate and visualize cube...\n" % (OptionsInfo["OutfilesDir"]))
 1025     
 1026     OutfilesDir = OptionsInfo["OutfilesDir"]
 1027     if OptionsInfo["GenerateCubeFiles"]:
 1028         if not os.path.isdir(OutfilesDir):
 1029             MiscUtil.PrintInfo("\nCreating directory %s..." % OutfilesDir)
 1030             os.mkdir(OutfilesDir)
 1031 
 1032     MiscUtil.PrintInfo("\nChanging directory to %s..." % OutfilesDir)
 1033     os.chdir(OutfilesDir)
 1034 
 1035 def ProcessOptions():
 1036     """Process and validate command line arguments and options."""
 1037     
 1038     MiscUtil.PrintInfo("Processing options...")
 1039     
 1040     # Validate options...
 1041     ValidateOptions()
 1042 
 1043     OptionsInfo["Infile"] = Options["--infile"]
 1044     OptionsInfo["InfilePath"] = os.path.abspath(Options["--infile"])
 1045     
 1046     ParamsDefaultInfoOverride = {"RemoveHydrogens": False}
 1047     OptionsInfo["InfileParams"] = MiscUtil.ProcessOptionInfileParameters("--infileParams", Options["--infileParams"], InfileName = Options["--infile"], ParamsDefaultInfo = ParamsDefaultInfoOverride)
 1048 
 1049     Outfile = Options["--outfile"]
 1050     FileDir, FileName, FileExt = MiscUtil.ParseFileName(Outfile)
 1051     
 1052     OptionsInfo["Outfile"] = Outfile
 1053     OptionsInfo["OutfileRoot"] = FileName
 1054     OptionsInfo["OutfileExt"] = FileExt
 1055     
 1056     OutfilesDir = Options["--outfilesDir"]
 1057     if re.match("^auto$", OutfilesDir, re.I):
 1058         OutfilesDir = "%s_DualDescriptors" % OptionsInfo["OutfileRoot"]
 1059     OptionsInfo["OutfilesDir"] = OutfilesDir
 1060     
 1061     OptionsInfo["Overwrite"] = Options["--overwrite"]
 1062     
 1063     # Method, basis set, and reference wavefunction...
 1064     OptionsInfo["BasisSet"] = Options["--basisSet"]
 1065     OptionsInfo["BasisSetAuto"] = True if re.match("^auto$", Options["--basisSet"], re.I) else False
 1066     
 1067     OptionsInfo["MethodName"] = Options["--methodName"]
 1068     OptionsInfo["MethodNameAuto"] = True if re.match("^auto$", Options["--methodName"], re.I) else False
 1069     
 1070     OptionsInfo["Reference"] = Options["--reference"]
 1071     OptionsInfo["ReferenceAuto"] = True if re.match("^auto$", Options["--reference"], re.I) else False
 1072     
 1073     # Run, options, and cube files parameters...
 1074     OptionsInfo["Psi4OptionsParams"] = Psi4Util.ProcessPsi4OptionsParameters("--psi4OptionsParams", Options["--psi4OptionsParams"])
 1075     OptionsInfo["Psi4RunParams"] = Psi4Util.ProcessPsi4RunParameters("--psi4RunParams", Options["--psi4RunParams"], InfileName = OptionsInfo["Infile"])
 1076 
 1077     ParamsDefaultInfoOverride = {}
 1078     OptionsInfo["Psi4CubeFilesParams"] = Psi4Util.ProcessPsi4CubeFilesParameters("--psi4CubeFilesParams", Options["--psi4CubeFilesParams"], ParamsDefaultInfo = ParamsDefaultInfoOverride)
 1079 
 1080     ParamsDefaultInfoOverride = {"ContourColor1": "red", "ContourColor2": "blue", "ContourLevel1": "auto", "ContourLevel2": "auto", "ContourLevelAutoAt": 0.5, "DisplayMolecule": "BallAndStick", "DisplaySphereScale": 0.2, "DisplayStickRadius": 0.1, "HideHydrogens": True,  "MeshWidth": 0.5, "MeshQuality": 2, "SurfaceQuality": 2, "SurfaceTransparency": 0.25, "VolumeColorRamp": "auto", "VolumeColorRampOpacity": 0.2, "VolumeContourWindowFactor": 0.05}
 1081     OptionsInfo["PyMOLViewParams"] = MiscUtil.ProcessOptionPyMOLCubeFileViewParameters("--pymolViewParams", Options["--pymolViewParams"], ParamsDefaultInfo = ParamsDefaultInfoOverride)
 1082     
 1083     SetupGlobalVolumeColorRamp = False
 1084     GlobalVolumeColorRampName = OptionsInfo["PyMOLViewParams"]["VolumeColorRamp"]
 1085     if OptionsInfo["PyMOLViewParams"]["VolumeColorRampAuto"]:
 1086         GlobalVolumeColorRampName = SetupDefaultVolumeRampName()
 1087         if not (OptionsInfo["PyMOLViewParams"]["ContourLevel1Auto"] or OptionsInfo["PyMOLViewParams"]["ContourLevel2Auto"]):
 1088             SetupGlobalVolumeColorRamp = True
 1089     OptionsInfo["PyMOLViewParams"]["GlobalVolumeColorRampName"] = GlobalVolumeColorRampName
 1090     OptionsInfo["PyMOLViewParams"]["SetupGlobalVolumeColorRamp"] = SetupGlobalVolumeColorRamp
 1091     
 1092     Mode = Options["--mode"]
 1093     if re.match("^GenerateCubeFiles$", Mode, re.I):
 1094         GenerateCubeFiles = True
 1095         VisualizeCubeFiles = False
 1096     elif re.match("^VisualizeCubeFiles$", Mode, re.I):
 1097         GenerateCubeFiles = False
 1098         VisualizeCubeFiles = True
 1099     else:
 1100         GenerateCubeFiles = True
 1101         VisualizeCubeFiles = True
 1102     OptionsInfo["Mode"] = Mode
 1103     OptionsInfo["GenerateCubeFiles"] = GenerateCubeFiles
 1104     OptionsInfo["VisualizeCubeFiles"] = VisualizeCubeFiles
 1105     
 1106     OptionsInfo["MPMode"] = True if re.match("^yes$", Options["--mp"], re.I) else False
 1107     OptionsInfo["MPParams"] = MiscUtil.ProcessOptionMultiprocessingParameters("--mpParams", Options["--mpParams"])
 1108     
 1109     OutfilesMolPrefix = Options["--outfilesMolPrefix"]
 1110     if re.match("^MolName$", OutfilesMolPrefix, re.I):
 1111         UseMolNamePrefix = True
 1112         UseMolNumPrefix = False
 1113     elif re.match("^MolNum$", OutfilesMolPrefix, re.I):
 1114         UseMolNamePrefix = False
 1115         UseMolNumPrefix = True
 1116     else:
 1117         UseMolNamePrefix = True
 1118         UseMolNumPrefix = True
 1119     OptionsInfo["OutfilesMolPrefix"] = OutfilesMolPrefix
 1120     OptionsInfo["UseMolNamePrefix"] = UseMolNamePrefix
 1121     OptionsInfo["UseMolNumPrefix"] = UseMolNumPrefix
 1122     
 1123     OptionsInfo["QuietMode"] = True if re.match("^yes$", Options["--quiet"], re.I) else False
 1124     
 1125     CheckAndSetupOutfilesDir()
 1126     
 1127 def RetrieveOptions():
 1128     """Retrieve command line arguments and options."""
 1129     
 1130     # Get options...
 1131     global Options
 1132     Options = docopt(_docoptUsage_)
 1133     
 1134     # Set current working directory to the specified directory...
 1135     WorkingDir = Options["--workingdir"]
 1136     if WorkingDir:
 1137         os.chdir(WorkingDir)
 1138     
 1139     # Handle examples option...
 1140     if "--examples" in Options and Options["--examples"]:
 1141         MiscUtil.PrintInfo(MiscUtil.GetExamplesTextFromDocOptText(_docoptUsage_))
 1142         sys.exit(0)
 1143 
 1144 def ValidateOptions():
 1145     """Validate option values."""
 1146     
 1147     MiscUtil.ValidateOptionFilePath("-i, --infile", Options["--infile"])
 1148     MiscUtil.ValidateOptionFileExt("-i, --infile", Options["--infile"], "sdf sd mol")
 1149 
 1150     MiscUtil.ValidateOptionFileExt("-o, --outfile", Options["--outfile"], "pml")
 1151     
 1152     MiscUtil.ValidateOptionTextValue("--mode", Options["--mode"], "GenerateCubeFiles VisualizeCubeFiles Both")
 1153     MiscUtil.ValidateOptionTextValue("--mp", Options["--mp"], "yes no")
 1154     
 1155     MiscUtil.ValidateOptionTextValue("--outfilesMolPrefix", Options["--outfilesMolPrefix"], "MolNum MolName Both")
 1156     MiscUtil.ValidateOptionTextValue("-q, --quiet", Options["--quiet"], "yes no")
 1157  
 1158 # Setup a usage string for docopt...
 1159 _docoptUsage_ = """
 1160 Psi4VisualizeDualDescriptors.py - Visualize dual descriptors for frontier orbitals
 1161 
 1162 Usage:
 1163     Psi4VisualizeDualDescriptors.py [--basisSet <text>] [--infileParams <Name,Value,...>] [--methodName <text>]
 1164                                      [--mode <GenerateCubeFiles, VisualizeCubeFiles, Both>] [--mp <yes or no>] [--mpParams <Name, Value,...>]
 1165                                      [--outfilesDir <text>] [--outfilesMolPrefix <MolNum, MolName, Both> ] [--overwrite]
 1166                                      [--psi4CubeFilesParams <Name,Value,...>] [--psi4OptionsParams <Name,Value,...>]
 1167                                      [--psi4RunParams <Name,Value,...>] [--pymolViewParams <Name,Value,...>] [--quiet <yes or no>]
 1168                                      [--reference <text>] [-w <dir>] -i <infile> -o <outfile> 
 1169     Psi4VisualizeDualDescriptors.py -h | --help | -e | --examples
 1170 
 1171 Description:
 1172     Generate and visualize dual descriptors [ Ref 151 ] corresponding to frontier
 1173     molecular orbitals for molecules in input file. A set of cube files, corresponding
 1174     to dual descriptors, is generated for molecules. The cube files are used to
 1175     create a PyMOL visualization file for viewing volumes, meshes, and surfaces
 1176     representing dual descriptors for frontier molecular orbitals. An option is
 1177     available to skip the generation of new cube files and use existing cube files
 1178     to visualize frontier molecular orbitals.
 1179     
 1180     The dual descriptor, a measure of  electrophilicity and nucleophilicity for
 1181     a molecule, is calculated by Psi4 from frontier molecular orbitals [ Ref 151]:
 1182     f2(r) = rhoLUMO(r) - rhoHOMO(r). The sign of the dual descriptor value
 1183     corresponds to electrophilic and nucleophilic sites in molecules as shown
 1184     below:
 1185         
 1186         Sign          Site              Reactivity
 1187         Positive      Electrophilic     Favored for a nucleophilic attack
 1188         Negative      Nucleophilic      Favored for a electrophilic attack
 1189         
 1190     A Psi4 XYZ format geometry string is automatically generated for each molecule
 1191     in input file. It contains atom symbols and 3D coordinates for each atom in a
 1192     molecule. In addition, the formal charge and spin multiplicity are present in the
 1193     the geometry string. These values are either retrieved from molecule properties
 1194     named 'FormalCharge' and 'SpinMultiplicty' or dynamically calculated for a
 1195     molecule.
 1196     
 1197     A set of cube and SD output files is generated for each closed-shell molecule
 1198     in input file as shown below:
 1199         
 1200         Ouput dir: <OutfileRoot>_DualDescriptors or <OutfilesDir>
 1201         
 1202         <MolIDPrefix>.sdf
 1203         <MolIDPrefix>*DUAL*.cube
 1204         
 1205     In addition, a <OutfileRoot>.pml is generated containing dual descriptors for
 1206     frontier molecular orbitals for all molecules in input file.
 1207     
 1208     The supported input file formats are: Mol (.mol), SD (.sdf, .sd)
 1209      
 1210     The supported output file formats are: PyMOL script file (.pml)
 1211     
 1212     A variety of PyMOL groups and objects are  created for visualization of dual
 1213     descriptors for closed-shell molecules as shown below:
 1214         
 1215         <MoleculeID>
 1216             .Molecule
 1217                 .Molecule
 1218                 .BallAndStick
 1219             .Dual
 1220                 .Cube
 1221                 .Volume
 1222                 .Mesh
 1223                     .Positive_Electrophilic
 1224                     .Negative_Nucleophilic
 1225                 .Surface
 1226                     .Positive_Electrophilic
 1227                     .Negative_Nucleophilic
 1228         <MoleculeID>
 1229             .Molecule
 1230                 ... ... ...
 1231             .Dual
 1232                 ... ... ...
 1233 
 1234 Options:
 1235     -b, --basisSet <text>  [default: auto]
 1236         Basis set to use for calculating single point energy before generating
 1237         cube files corresponding to dual descriptors for frontier molecular orbitals.
 1238         Default: 6-31+G** for sulfur containing molecules; Otherwise, 6-31G**
 1239         [ Ref 150 ]. The specified value must be a valid Psi4 basis set. No validation
 1240         is performed.
 1241         
 1242         The following list shows a representative sample of basis sets available
 1243         in Psi4:
 1244             
 1245             STO-3G, 6-31G, 6-31+G, 6-31++G, 6-31G*, 6-31+G*,  6-31++G*, 
 1246             6-31G**, 6-31+G**, 6-31++G**, 6-311G, 6-311+G, 6-311++G,
 1247             6-311G*, 6-311+G*, 6-311++G*, 6-311G**, 6-311+G**, 6-311++G**,
 1248             cc-pVDZ, cc-pCVDZ, aug-cc-pVDZ, cc-pVDZ-DK, cc-pCVDZ-DK, def2-SVP,
 1249             def2-SVPD, def2-TZVP, def2-TZVPD, def2-TZVPP, def2-TZVPPD
 1250             
 1251     -e, --examples
 1252         Print examples.
 1253     -h, --help
 1254         Print this help message.
 1255     -i, --infile <infile>
 1256         Input file name.
 1257     --infileParams <Name,Value,...>  [default: auto]
 1258         A comma delimited list of parameter name and value pairs for reading
 1259         molecules from files. The supported parameter names for different file
 1260         formats, along with their default values, are shown below:
 1261             
 1262             SD, MOL: removeHydrogens,no,sanitize,yes,strictParsing,yes
 1263             
 1264     -m, --methodName <text>  [default: auto]
 1265         Method to use for calculating single point energy before generating
 1266         cube files corresponding to dual descriptors for frontier molecular
 1267         orbitals. Default: B3LYP [ Ref 150 ]. The specified value must be a
 1268         valid Psi4 method name. No validation is performed.
 1269         
 1270         The following list shows a representative sample of methods available
 1271         in Psi4:
 1272             
 1273             B1LYP, B2PLYP, B2PLYP-D3BJ, B2PLYP-D3MBJ, B3LYP, B3LYP-D3BJ,
 1274             B3LYP-D3MBJ, CAM-B3LYP, CAM-B3LYP-D3BJ, HF, HF-D3BJ,  HF3c, M05,
 1275             M06, M06-2x, M06-HF, M06-L, MN12-L, MN15, MN15-D3BJ,PBE, PBE0,
 1276             PBEH3c, PW6B95, PW6B95-D3BJ, WB97, WB97X, WB97X-D, WB97X-D3BJ
 1277             
 1278     --mode <GenerateCubeFiles, VisualizeCubeFiles, or Both>  [default: Both]
 1279         Generate and visualize cube files corresponding to dual descriptors for
 1280         frontier molecular orbitals. The 'VisualizeCubes' value skips the generation
 1281         of new cube files and uses existing cube files for visualization of dual
 1282         descriptors. Multiprocessing is not supported during 'VisualizeCubeFiles'
 1283         value of '--mode' option.
 1284     --mp <yes or no>  [default: no]
 1285         Use multiprocessing.
 1286          
 1287         By default, input data is retrieved in a lazy manner via mp.Pool.imap()
 1288         function employing lazy RDKit data iterable. This allows processing of
 1289         arbitrary large data sets without any additional requirements memory.
 1290         
 1291         All input data may be optionally loaded into memory by mp.Pool.map()
 1292         before starting worker processes in a process pool by setting the value
 1293         of 'inputDataMode' to 'InMemory' in '--mpParams' option.
 1294         
 1295         A word to the wise: The default 'chunkSize' value of 1 during 'Lazy' input
 1296         data mode may adversely impact the performance. The '--mpParams' section
 1297         provides additional information to tune the value of 'chunkSize'.
 1298     --mpParams <Name,Value,...>  [default: auto]
 1299         A comma delimited list of parameter name and value pairs to configure
 1300         multiprocessing.
 1301         
 1302         The supported parameter names along with their default and possible
 1303         values are shown below:
 1304         
 1305             chunkSize, auto
 1306             inputDataMode, Lazy   [ Possible values: InMemory or Lazy ]
 1307             numProcesses, auto   [ Default: mp.cpu_count() ]
 1308         
 1309         These parameters are used by the following functions to configure and
 1310         control the behavior of multiprocessing: mp.Pool(), mp.Pool.map(), and
 1311         mp.Pool.imap().
 1312         
 1313         The chunkSize determines chunks of input data passed to each worker
 1314         process in a process pool by mp.Pool.map() and mp.Pool.imap() functions.
 1315         The default value of chunkSize is dependent on the value of 'inputDataMode'.
 1316         
 1317         The mp.Pool.map() function, invoked during 'InMemory' input data mode,
 1318         automatically converts RDKit data iterable into a list, loads all data into
 1319         memory, and calculates the default chunkSize using the following method
 1320         as shown in its code:
 1321         
 1322             chunkSize, extra = divmod(len(dataIterable), len(numProcesses) * 4)
 1323             if extra: chunkSize += 1
 1324         
 1325         For example, the default chunkSize will be 7 for a pool of 4 worker processes
 1326         and 100 data items.
 1327         
 1328         The mp.Pool.imap() function, invoked during 'Lazy' input data mode, employs
 1329         'lazy' RDKit data iterable to retrieve data as needed, without loading all the
 1330         data into memory. Consequently, the size of input data is not known a priori.
 1331         It's not possible to estimate an optimal value for the chunkSize. The default 
 1332         chunkSize is set to 1.
 1333         
 1334         The default value for the chunkSize during 'Lazy' data mode may adversely
 1335         impact the performance due to the overhead associated with exchanging
 1336         small chunks of data. It is generally a good idea to explicitly set chunkSize to
 1337         a larger value during 'Lazy' input data mode, based on the size of your input
 1338         data and number of processes in the process pool.
 1339         
 1340         The mp.Pool.map() function waits for all worker processes to process all
 1341         the data and return the results. The mp.Pool.imap() function, however,
 1342         returns the the results obtained from worker processes as soon as the
 1343         results become available for specified chunks of data.
 1344         
 1345         The order of data in the results returned by both mp.Pool.map() and 
 1346         mp.Pool.imap() functions always corresponds to the input data.
 1347     -o, --outfile <outfile>
 1348         Output file name for PyMOL PML file. The PML output file, along with cube
 1349         files, is generated in a local directory corresponding to '--outfilesDir'
 1350         option.
 1351     --outfilesDir <text>  [default: auto]
 1352         Directory name containing PML and cube files. Default:
 1353        <OutfileRoot>_DualDescriptors. This directory must be present during
 1354         'VisualizeCubeFiles' value of '--mode' option.
 1355     --outfilesMolPrefix <MolNum, MolName, Both>  [default: Both]
 1356         Molecule prefix to use for the names of cube files. Possible values:
 1357         MolNum, MolName, or Both. By default, both molecule number and name
 1358         are used. The format of molecule prefix is as follows: MolNum - Mol<Num>;
 1359         MolName - <MolName>, Both: Mol<Num>_<MolName>. Empty molecule names
 1360         are ignored. Molecule numbers are used for empty molecule names.
 1361     --overwrite
 1362         Overwrite existing files.
 1363     --psi4CubeFilesParams <Name,Value,...>  [default: auto]
 1364         A comma delimited list of parameter name and value pairs for generating
 1365         Psi4 cube files.
 1366         
 1367         The supported parameter names along with their default and possible
 1368         values are shown below:
 1369              
 1370             gridSpacing, 0.2, gridOverage, 4.0, isoContourThreshold, 0.85
 1371              
 1372         gridSpacing: Grid spacing for generating cube files. Units: Bohr. A higher
 1373         value reduces the size of the cube files on the disk. This option corresponds
 1374         to Psi4 option CUBIC_GRID_SPACING.
 1375         
 1376         gridOverage: Grid overage for generating cube files. Units: Bohr.This option
 1377         corresponds to Psi4 option CUBIC_GRID_OVERAGE.
 1378         
 1379         isoContourThreshold: IsoContour values for generating cube files that capture
 1380         specified percent of the probability density using the least amount of grid
 1381         points. Default: 0.85 (85%). This option corresponds to Psi4 option
 1382         CUBEPROP_ISOCONTOUR_THRESHOLD.
 1383     --psi4OptionsParams <Name,Value,...>  [default: none]
 1384         A comma delimited list of Psi4 option name and value pairs for setting
 1385         global and module options. The names are 'option_name' for global options
 1386         and 'module_name__option_name' for options local to a module. The
 1387         specified option names must be valid Psi4 names. No validation is
 1388         performed.
 1389         
 1390         The specified option name and  value pairs are processed and passed to
 1391         psi4.set_options() as a dictionary. The supported value types are float,
 1392         integer, boolean, or string. The float value string is converted into a float.
 1393         The valid values for a boolean string are yes, no, true, false, on, or off. 
 1394     --psi4RunParams <Name,Value,...>  [default: auto]
 1395         A comma delimited list of parameter name and value pairs for configuring
 1396         Psi4 jobs.
 1397         
 1398         The supported parameter names along with their default and possible
 1399         values are shown below:
 1400              
 1401             MemoryInGB, 1
 1402             NumThreads, 1
 1403             OutputFile, auto   [ Possible  values: stdout, quiet, or FileName ]
 1404             ScratchDir, auto   [ Possivle values: DirName]
 1405             RemoveOutputFile, yes   [ Possible values: yes, no, true, or false]
 1406             
 1407         These parameters control the runtime behavior of Psi4.
 1408         
 1409         The default file name for 'OutputFile' is <InFileRoot>_Psi4.out. The PID
 1410         is appended to output file name during multiprocessing as shown:
 1411         <InFileRoot>_Psi4_<PIDNum>.out. The 'stdout' value for 'OutputType'
 1412         sends Psi4 output to stdout. The 'quiet' or 'devnull' value suppresses
 1413         all Psi4 output.
 1414         
 1415         The default 'Yes' value of 'RemoveOutputFile' option forces the removal
 1416         of any existing Psi4 before creating new files to append output from
 1417         multiple Psi4 runs.
 1418         
 1419         The option 'ScratchDir' is a directory path to the location of scratch
 1420         files. The default value corresponds to Psi4 default. It may be used to
 1421         override the deafult path.
 1422     --pymolViewParams <Name,Value,...>  [default: auto]
 1423         A comma delimited list of parameter name and value pairs for visualizing
 1424         cube files in PyMOL.
 1425             
 1426             contourColor1, red, contourColor2, blue,
 1427             contourLevel1, auto, contourLevel2, auto,
 1428             contourLevelAutoAt, 0.5,
 1429             displayMolecule, BallAndStick, displaySphereScale, 0.2,
 1430             displayStickRadius, 0.1, hideHydrogens, yes,
 1431             meshWidth, 0.5, meshQuality, 2,
 1432             surfaceQuality, 2, surfaceTransparency, 0.25,
 1433             volumeColorRamp, auto, volumeColorRampOpacity,0.2
 1434             volumeContourWindowFactor,0.05
 1435             
 1436         contourColor1 and contourColor2: Color to use for visualizing volumes,
 1437         meshes, and surfaces corresponding to the negative and positive values
 1438         in cube files. The specified values must be valid PyMOL color names. No
 1439         validation is performed.
 1440         
 1441         contourLevel1 and contourLevel2: Contour levels to use for visualizing
 1442         volumes, meshes, and surfaces corresponding to the negative and positive
 1443         values in cube files. Default: auto. The specified values for contourLevel1
 1444         and contourLevel2 must be negative and positive numbers.
 1445         
 1446         The contour levels are automatically calculated by default. The isocontour
 1447         range for specified percent of the density is retrieved from the cube files.
 1448         The contour levels are set at 'contourLevelAutoAt' of the absolute maximum
 1449         value of the isocontour range. For example: contour levels are set to plus and
 1450         minus 0.03 at 'contourLevelAutoAt' of 0.5 for isocontour range of -0.06 to
 1451         0.06 covering specified percent of the density.
 1452         
 1453         contourLevelAutoAt: Set contour levels at specified fraction of the absolute
 1454         maximum value of the isocontour range retrieved from  the cube files. This
 1455         option is only used during the automatic calculations of the contour levels.
 1456         
 1457         displayMolecule: Display mode for molecules. Possible values: Sticks or
 1458         BallAndStick. Both displays objects are created for molecules.
 1459         
 1460         displaySphereScale: Sphere scale for displaying molecule during
 1461         BallAndStick display.
 1462         
 1463         displayStickRadius: Stick radius  for displaying molecule during Sticks
 1464         and BallAndStick display.
 1465         
 1466         hideHydrogens: Hide hydrogens in molecules. Default: yes. Possible
 1467         values: yes or no.
 1468         
 1469         meshQuality: Mesh quality for meshes to visualize cube files. The
 1470         higher values represents better quality.
 1471         
 1472         meshWidth: Line width for mesh lines to visualize cube files.
 1473         
 1474         surfaceQuality: Surface quality for surfaces to visualize cube files.
 1475         The higher values represents better quality.
 1476         
 1477         surfaceTransparency: Surface transparency for surfaces to visualize cube
 1478         files.
 1479         
 1480         volumeColorRamp: Name of a PyMOL volume color ramp to use for visualizing
 1481         cube files. Default name(s): <OutfielsMolPrefix>_psi4_cube_dual or psi4_cube_dual
 1482         The default volume color ramps are automatically generated using contour
 1483         levels and colors during 'auto' value of 'volumeColorRamp'. An explicitly
 1484         specified value must be a valid PyMOL volume color ramp. No validation is
 1485         preformed.
 1486         
 1487         VolumeColorRampOpacity: Opacity for generating volume color ramps
 1488         for visualizing cube files. This value is equivalent to 1 minus Transparency.
 1489         
 1490         volumeContourWindowFactor: Fraction of contour level representing  window
 1491         widths around contour levels during generation of volume color ramps for
 1492         visualizing cube files. For example, the value of 0.05 implies a ramp window
 1493         size of 0.0015 at contour level of 0.03.
 1494     -q, --quiet <yes or no>  [default: no]
 1495         Use quiet mode. The warning and information messages will not be printed.
 1496     -r, --reference <text>  [default: auto]
 1497         Reference wave function to use for calculating single point energy before
 1498         generating cube files for dual descriptors corresponding to frontier molecular
 1499         orbitals. Default: RHF or UHF. The default values are Restricted Hartree-Fock (RHF)
 1500         for closed-shell molecules with all electrons paired and Unrestricted artree-Fock (UHF)
 1501         for open-shell molecules with unpaired electrons.
 1502         
 1503         The specified value must be a valid Psi4 reference wave function. No validation
 1504         is performed. For example: ROHF, CUHF, RKS, etc.
 1505         
 1506         The spin multiplicity determines the default value of reference wave function
 1507         for input molecules. It is calculated from number of free radical electrons using
 1508         Hund's rule of maximum multiplicity defined as 2S + 1 where S is the total
 1509         electron spin. The total spin is 1/2 the number of free radical electrons in a 
 1510         molecule. The value of 'SpinMultiplicity' molecule property takes precedence
 1511         over the calculated value of spin multiplicity.
 1512     -w, --workingdir <dir>
 1513         Location of working directory which defaults to the current directory.
 1514 
 1515 Examples:
 1516     To generate and visualize dual descriptors from frontier orbitals based on a
 1517     single point  energy calculation using  B3LYP/6-31G** and B3LYP/6-31+G** for
 1518     non-sulfur and sulfur containing closed-shell molecules in a SD file with 3D
 1519     structures, and write a new PML file, type:
 1520 
 1521         % Psi4VisualizeDualDescriptors.py -i Psi4Sample3D.sdf
 1522           -o Psi4Sample3DOut.pml
 1523 
 1524     To run the first example to only generate cube files and skip generation of 
 1525     a PML file to visualize dual descriptors for frontier molecular orbitals, type:
 1526 
 1527         % Psi4VisualizeDualDescriptors.py --mode GenerateCubeFiles
 1528           -i Psi4Sample3D.sdf -o Psi4Sample3DOut.pml
 1529 
 1530     To run the first example to skip generation of cube files and use existing cube
 1531     files to visualize dual descriptors for frontier molecular orbitals and write out
 1532     a PML file, type:
 1533 
 1534         % Psi4VisualizeDualDescriptors.py --mode VisualizeCubeFiles
 1535           -i Psi4Sample3D.sdf -o Psi4Sample3DOut.pml
 1536 
 1537     To run the first example in multiprocessing mode on all available CPUs
 1538     without loading all data into memory and write out a PML file, type:
 1539 
 1540         % Psi4VisualizeDualDescriptors.py --mp yes -i Psi4Sample3D.sdf
 1541             -o Psi4Sample3DOut.pml
 1542 
 1543     To run the first example in multiprocessing mode on all available CPUs
 1544     by loading all data into memory and write out a PML file, type:
 1545 
 1546         % Psi4VisualizeFrontierOrbitals.py  --mp yes --mpParams "inputDataMode,
 1547             InMemory" -i Psi4Sample3D.sdf  -o Psi4Sample3DOut.pml
 1548 
 1549     To run the first example in multiprocessing mode on all available CPUs
 1550     without loading all data into memory along with multiple threads for each
 1551     Psi4 run and write out a SD file, type:
 1552 
 1553         % Psi4VisualizeDualDescriptors.py --mp yes --psi4RunParams
 1554           "NumThreads,2" -i Psi4Sample3D.sdf -o Psi4Sample3DOut.pml
 1555 
 1556     To run the first example in using a specific set of parameters to generate and
 1557     visualize dual descriptors for frontier molecular orbitals and write out a PML file,
 1558     type:
 1559 
 1560         % Psi4VisualizeDualDescriptors.py --mode both -m SCF -b aug-cc-pVDZ 
 1561           --psi4CubeFilesParams "gridSpacing, 0.2, gridOverage, 4.0"
 1562           --psi4RunParams "MemoryInGB, 2" --pymolViewParams "contourColor1,
 1563           red, contourColor2, blue,contourLevel1, -0.04, contourLevel2, 0.04,
 1564           contourLevelAutoAt, 0.75,volumeColorRamp, auto,
 1565           volumeColorRampOpacity,0.25, volumeContourWindowFactor,0.05"
 1566           -i Psi4Sample3D.sdf -o Psi4Sample3DOut.pml
 1567 
 1568 Author:
 1569     Manish Sud(msud@san.rr.com)
 1570 
 1571 See also:
 1572     Psi4PerformMinimization.py, Psi4GenerateConformers.py,
 1573     Psi4VisualizeElectrostaticPotential.py , Psi4VisualizeFrontierOrbitals.py
 1574 
 1575 Copyright:
 1576     Copyright (C) 2024 Manish Sud. All rights reserved.
 1577 
 1578     The functionality available in this script is implemented using Psi4, an
 1579     open source quantum chemistry software package, and RDKit, an open
 1580     source toolkit for cheminformatics developed by Greg Landrum.
 1581 
 1582     This file is part of MayaChemTools.
 1583 
 1584     MayaChemTools is free software; you can redistribute it and/or modify it under
 1585     the terms of the GNU Lesser General Public License as published by the Free
 1586     Software Foundation; either version 3 of the License, or (at your option) any
 1587     later version.
 1588 
 1589 """
 1590 
 1591 if __name__ == "__main__":
 1592     main()