MayaChemTools

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