MayaChemTools

   1 #!/bin/env python
   2 # File: Psi4Util.py
   3 # Author: Manish Sud <msud@san.rr.com>
   4 #
   5 # Copyright (C) 2024 Manish Sud. All rights reserved.
   6 #
   7 # The functionality available in this file is implemented using Psi4, an open
   8 # source quantum chemistry software package.
   9 #
  10 # This file is part of MayaChemTools.
  11 #
  12 # MayaChemTools is free software; you can redistribute it and/or modify it under
  13 # the terms of the GNU Lesser General Public License as published by the Free
  14 # Software Foundation; either version 3 of the License, or (at your option) any
  15 # later version.
  16 #
  17 # MayaChemTools is distributed in the hope that it will be useful, but without
  18 # any warranty; without even the implied warranty of merchantability of fitness
  19 # for a particular purpose.  See the GNU Lesser General Public License for more
  20 # details.
  21 #
  22 # You should have received a copy of the GNU Lesser General Public License
  23 # along with MayaChemTools; if not, see <http://www.gnu.org/licenses/> or
  24 # write to the Free Software Foundation Inc., 59 Temple Place, Suite 330,
  25 # Boston, MA, 02111-1307, USA.
  26 #
  27 
  28 from __future__ import print_function
  29 
  30 import os
  31 import sys
  32 import re
  33 import glob
  34 
  35 import MiscUtil
  36 
  37 __all__ = ["CalculateSinglePointEnergy", "InitializePsi4", "JoinMethodNameAndBasisSet", "ListPsi4RunParamaters", "RetrieveIsocontourRangeFromCubeFile", "RetrieveMinAndMaxValueFromCubeFile", "PerformGeometryOptimization", "ProcessPsi4CubeFilesParameters", "ProcessPsi4OptionsParameters", "ProcessPsi4RunParameters", "ProcessPsi4DDXSolvationParameters", "RemoveScratchFiles", "SetupPsi4DDXSolvationOptions", "UpdatePsi4OptionsParameters", "UpdatePsi4RunParameters", "UpdatePsi4OutputFileUsingPID"]
  38 
  39 def InitializePsi4(Psi4RunParams = None,  Psi4OptionsParams = None, PrintVersion = False, PrintHeader = False):
  40     """Import Psi4 module and configure it for running Psi4 jobs.
  41     
  42     Arguments:
  43         Psi4RunParams (dict): Runtime parameter name and value pairs.
  44         Psi4OptionsParams (dict): Option name and value pairs. This is simply
  45             passed to ps4.set_options().      
  46         PrintVersion (bool): Print version number.
  47         PrintHeader (bool): Print header information.
  48 
  49     Returns:
  50         Object: Psi4 module reference.
  51 
  52     """
  53     
  54     # Import Psi4...
  55     try:
  56         import psi4
  57     except ImportError as ErrMsg:
  58         sys.stderr.write("\nFailed to import Psi4 module/package: %s\n" % ErrMsg)
  59         sys.stderr.write("Check/update your Psi4 environment and try again.\n\n")
  60         sys.exit(1)
  61 
  62     Psi4Handle = psi4
  63     
  64     if PrintVersion:
  65         MiscUtil.PrintInfo("Importing Psi4 module (Psi4 v%s)...\n" % (Psi4Handle.__version__))
  66 
  67     # Update Psi4 run paramaters...
  68     if Psi4RunParams is not None:
  69         UpdatePsi4RunParameters(Psi4Handle, Psi4RunParams)
  70 
  71     # Update Psi4 options...
  72     if Psi4OptionsParams is not None:
  73         UpdatePsi4OptionsParameters(Psi4Handle, Psi4OptionsParams)
  74         
  75     # Print header after updating Psi4 run parameters...
  76     if PrintHeader:
  77         Psi4Handle.print_header()
  78     
  79     return Psi4Handle
  80     
  81 def CalculateSinglePointEnergy(psi4, Molecule, Method, BasisSet, ReturnWaveFunction = False, Quiet = False):
  82     """Calculate single point electronic energy in Hartrees using a specified
  83     method and basis set.
  84 
  85     Arguments:
  86         psi4 (Object): Psi4 module reference.
  87         Molecule (Object): Psi4 molecule object.
  88         Method (str): A valid method name.
  89         BasisSet (str): A valid basis set.
  90         ReturnWaveFunction (bool): Return wave function.
  91         Quiet (bool): Flag to print error message.
  92 
  93     Returns:
  94         float: Total electronic energy in Hartrees.
  95         (float, psi4 object): Energy and wavefuction.
  96 
  97     """
  98     
  99     Status = False
 100     Energy, WaveFunction = [None] * 2
 101 
 102     try:
 103         MethodAndBasisSet = JoinMethodNameAndBasisSet(Method, BasisSet)
 104         if ReturnWaveFunction:
 105             Energy, WaveFunction = psi4.energy(MethodAndBasisSet, molecule = Molecule, return_wfn = True)
 106         else:
 107             Energy = psi4.energy(MethodAndBasisSet, molecule = Molecule, return_wfn = False)
 108         Status = True
 109     except Exception as ErrMsg:
 110         if not Quiet:
 111             MiscUtil.PrintWarning("Psi4Util.CalculateSinglePointEnergy: Failed to calculate energy:\n%s\n" % ErrMsg)
 112     
 113     return (Status, Energy, WaveFunction) if ReturnWaveFunction else (Status, Energy)
 114     
 115 def PerformGeometryOptimization(psi4, Molecule, Method, BasisSet, ReturnWaveFunction = True, Quiet = False):
 116     """Perform geometry optimization using a specified method and basis set.
 117     
 118     Arguments:
 119         psi4 (Object): Psi4 module reference.
 120         Molecule (Object): Psi4 molecule object.
 121         Method (str): A valid method name.
 122         BasisSet (str): A valid basis set.
 123         ReturnWaveFunction (bool): Return wave function.
 124         Quiet (bool): Flag to print error message.
 125 
 126     Returns:
 127         float: Total electronic energy in Hartrees.
 128         (float, psi4 object): Energy and wavefuction.
 129 
 130     """
 131     
 132     Status = False
 133     Energy, WaveFunction = [None] * 2
 134 
 135     try:
 136         MethodAndBasisSet = JoinMethodNameAndBasisSet(Method, BasisSet)
 137         if ReturnWaveFunction:
 138             Energy, WaveFunction = psi4.optimize(MethodAndBasisSet, molecule = Molecule, return_wfn = True)
 139         else:
 140             Energy = psi4.optimize(MethodAndBasisSet, molecule = Molecule, return_wfn = False)
 141         Status = True
 142     except Exception as ErrMsg:
 143         if not Quiet:
 144             MiscUtil.PrintWarning("Psi4Util.PerformGeometryOptimization: Failed to perform geometry optimization:\n%s\n" % ErrMsg)
 145     
 146     return (Status, Energy, WaveFunction) if ReturnWaveFunction else (Status, Energy)
 147     
 148 def JoinMethodNameAndBasisSet(MethodName, BasisSet):
 149     """Join method name and basis set using a backslash delimiter.
 150     An empty basis set specification is ignored.
 151 
 152     Arguments:
 153         MethodName (str): A valid method name.
 154         BasisSet (str): A valid basis set or an empty string.
 155 
 156     Returns:
 157         str: MethodName/BasisSet or MethodName
 158 
 159     """
 160     
 161     return MethodName if MiscUtil.IsEmpty(BasisSet) else "%s/%s" % (MethodName, BasisSet)
 162     
 163 def GetAtomPositions(psi4, WaveFunction, InAngstroms = True):
 164     """Retrieve a list of lists containing coordinates of all atoms in the
 165     molecule available in Psi4 wave function. By default, the atom positions
 166     are returned in Angstroms. The Psi4 default is Bohr.
 167 
 168     Arguments:
 169         psi4 (Object): Psi4 module reference.
 170         WaveFunction (Object): Psi4 wave function reference.
 171         InAngstroms (bool): True - Positions in Angstroms; Otherwise, in Bohr.
 172 
 173     Returns:
 174         None or list : List of lists containing atom positions.
 175 
 176     Examples:
 177 
 178         for AtomPosition in Psi4Util.GetAtomPositions(Psi4Handle, WaveFunction):
 179             print("X: %s; Y: %s; Z: %s" % (AtomPosition[0], AtomPosition[1],
 180                 AtomPosition[2]))
 181 
 182     """
 183 
 184     if WaveFunction is None:
 185         return None
 186     
 187     AtomPositions = WaveFunction.molecule().geometry().to_array()
 188     if InAngstroms:
 189         AtomPositions = AtomPositions * psi4.constants.bohr2angstroms
 190     
 191     return AtomPositions.tolist()
 192 
 193 def ListPsi4RunParamaters(psi4):
 194     """List values for a key set of the following Psi4 runtime parameters:
 195     Memory, NumThreads, OutputFile, ScratchDir, DataDir.
 196     
 197     Arguments:
 198         psi4 (object): Psi4 module reference.
 199 
 200     Returns:
 201         None
 202 
 203     """
 204     
 205     MiscUtil.PrintInfo("\nListing Psi4 run options:")
 206     
 207     # Memory in bytes...
 208     Memory = psi4.get_memory()
 209     MiscUtil.PrintInfo("Memory: %s (B); %s (MB)" % (Memory, Memory/(1024*1024)))
 210     
 211     # Number of threads...
 212     NumThreads = psi4.get_num_threads()
 213     MiscUtil.PrintInfo("NumThreads: %s " % (NumThreads))
 214     
 215     # Output file...
 216     OutputFile = psi4.core.get_output_file()
 217     MiscUtil.PrintInfo("OutputFile: %s " % (OutputFile))
 218     
 219     # Scratch dir...
 220     psi4_io = psi4.core.IOManager.shared_object()
 221     ScratchDir = psi4_io.get_default_path()
 222     MiscUtil.PrintInfo("ScratchDir: %s " % (ScratchDir))
 223     
 224     # Data dir...
 225     DataDir = psi4.core.get_datadir()
 226     MiscUtil.PrintInfo("DataDir: %s " % (DataDir))
 227 
 228 def UpdatePsi4OptionsParameters(psi4, OptionsInfo):
 229     """Update Psi4 options using psi4.set_options().
 230     
 231     Arguments:
 232         psi4 (object): Psi4 module reference.
 233         OptionsInfo (dictionary) : Option name and value pairs for setting
 234             global and module options.
 235 
 236     Returns:
 237         None
 238 
 239     """
 240     if OptionsInfo is None:
 241         return
 242 
 243     if len(OptionsInfo) == 0:
 244         return
 245 
 246     try:
 247         psi4.set_options(OptionsInfo)
 248     except Exception as ErrMsg:
 249         MiscUtil.PrintWarning("Psi4Util.UpdatePsi4OptionsParameters: Failed to set Psi4 options\n%s\n" % ErrMsg)
 250 
 251 def UpdatePsi4RunParameters(psi4, RunParamsInfo):
 252     """Update Psi4 runtime parameters. The supported parameter names along with
 253     their default values are as follows: MemoryInGB: 1; NumThreads: 1, OutputFile:
 254     stdout; ScratchDir: auto; RemoveOutputFile: True.
 255 
 256     Arguments:
 257         psi4 (object): Psi4 module reference.
 258         RunParamsInfo (dictionary) : Parameter name and value pairs for
 259             configuring Psi4 jobs.
 260 
 261     Returns:
 262         None
 263 
 264     """
 265 
 266     # Set default values for possible arguments...
 267     Psi4RunParams = {"MemoryInGB": 1, "NumThreads": 1, "OutputFile": "stdout",  "ScratchDir" : "auto", "RemoveOutputFile": True}
 268     
 269     # Set specified values for possible arguments...
 270     for Param in Psi4RunParams:
 271         if Param in RunParamsInfo:
 272             Psi4RunParams[Param] = RunParamsInfo[Param]
 273     
 274     # Memory...
 275     Memory = int(Psi4RunParams["MemoryInGB"]*1024*1024*1024)
 276     psi4.core.set_memory_bytes(Memory, True)
 277     
 278     # Number of threads...
 279     psi4.core.set_num_threads(Psi4RunParams["NumThreads"], quiet = True)
 280 
 281     # Output file...
 282     OutputFile = Psi4RunParams["OutputFile"]
 283     if not re.match("^stdout$", OutputFile, re.I):
 284         # Possible values: stdout, quiet, devnull, or filename
 285         if re.match("^(quiet|devnull)$", OutputFile, re.I):
 286             # Psi4 output is redirected to /dev/null after call to be_quiet function...
 287             psi4.core.be_quiet()
 288         else:
 289             # Delete existing output file at the start of the first Psi4 run...
 290             if Psi4RunParams["RemoveOutputFile"]:
 291                 if os.path.isfile(OutputFile):
 292                     os.remove(OutputFile)
 293                     
 294             # Append to handle output from multiple Psi4 runs for molecules in
 295             # input file...
 296             Append = True
 297             psi4.core.set_output_file(OutputFile, Append)
 298 
 299     # Scratch directory...
 300     ScratchDir = Psi4RunParams["ScratchDir"]
 301     if not re.match("^auto$", ScratchDir, re.I):
 302         if not os.path.isdir(ScratchDir):
 303             MiscUtil.PrintError("ScratchDir is not a directory: %s" % ScratchDir)
 304         psi4.core.IOManager.shared_object().set_default_path(os.path.abspath(os.path.expanduser(ScratchDir)))
 305 
 306 def ProcessPsi4OptionsParameters(ParamsOptionName, ParamsOptionValue):
 307     """Process parameters for setting up Psi4 options and return a map
 308     containing processed parameter names and values.
 309     
 310     ParamsOptionValue is a comma delimited list of Psi4 option name and value
 311     pairs for setting global and module options. The names are 'option_name'
 312     for global options and 'module_name__option_name' for options local to a
 313     module. The specified option names must be valid Psi4 names. No validation
 314     is performed.
 315     
 316     The specified option name and  value pairs are processed and passed to
 317     psi4.set_options() as a dictionary. The supported value types are float,
 318     integer, boolean, or string. The float value string is converted into a float.
 319     The valid values for a boolean string are yes, no, true, false, on, or off. 
 320 
 321     Arguments:
 322         ParamsOptionName (str): Command line input parameters option name.
 323         ParamsOptionValue (str): Comma delimited list of parameter name and value pairs.
 324 
 325     Returns:
 326         dictionary: Processed parameter name and value pairs.
 327 
 328     """
 329 
 330     OptionsInfo = {}
 331     
 332     if re.match("^(auto|none)$", ParamsOptionValue, re.I):
 333         return None
 334 
 335     ParamsOptionValue = ParamsOptionValue.strip()
 336     if not ParamsOptionValue:
 337         PrintError("No valid parameter name and value pairs specified using \"%s\" option" % ParamsOptionName)
 338     
 339     ParamsOptionValueWords = ParamsOptionValue.split(",")
 340     if len(ParamsOptionValueWords) % 2:
 341         MiscUtil.PrintError("The number of comma delimited paramater names and values, %d, specified using \"%s\" option must be an even number." % (len(ParamsOptionValueWords), ParamsOptionName))
 342     
 343     # Validate paramater name and value pairs...
 344     for Index in range(0, len(ParamsOptionValueWords), 2):
 345         Name = ParamsOptionValueWords[Index].strip()
 346         Value = ParamsOptionValueWords[Index + 1].strip()
 347         
 348         if  MiscUtil.IsInteger(Value):
 349             Value = int(Value)
 350         elif MiscUtil.IsFloat(Value):
 351             Value = float(Value)
 352         
 353         OptionsInfo[Name] = Value
 354 
 355     return OptionsInfo
 356 
 357 def ProcessPsi4RunParameters(ParamsOptionName, ParamsOptionValue, InfileName = None, ParamsDefaultInfo = None):
 358     """Process parameters for Psi4 runs and return a map containing processed
 359     parameter names and values.
 360     
 361     ParamsOptionValue a comma delimited list of parameter name and value pairs
 362     for configuring Psi4 jobs.
 363     
 364     The supported parameter names along with their default and possible
 365     values are shown below:
 366     
 367     MemoryInGB,1,NumThreads,1,OutputFile,auto,ScratchDir,auto,
 368     RemoveOutputFile,yes
 369             
 370     Possible  values: OutputFile - stdout, quiet, or FileName; OutputFile -
 371     DirName; RemoveOutputFile - yes, no, true, or false
 372     
 373     These parameters control the runtime behavior of Psi4.
 374     
 375     The default for 'OutputFile' is a file name <InFileRoot>_Psi4.out. The PID
 376     is appened the output file name during multiprocessing. The 'stdout' value
 377     for 'OutputType' sends Psi4 output to stdout. The 'quiet' or 'devnull' value
 378     suppresses all Psi4 output.
 379     
 380     The default 'Yes' value of 'RemoveOutputFile' option forces the removal
 381     of any existing Psi4 before creating new files to append output from
 382     multiple Psi4 runs.
 383     
 384     The option 'ScratchDir' is a directory path to the location of scratch
 385     files. The default value corresponds to Psi4 default. It may be used to
 386     override the deafult path.
 387 
 388     Arguments:
 389         ParamsOptionName (str): Command line Psi4 run parameters option name.
 390         ParamsOptionValues (str): Comma delimited list of parameter name and value pairs.
 391         InfileName (str): Name of input file.
 392         ParamsDefaultInfo (dict): Default values to override for selected parameters.
 393 
 394     Returns:
 395         dictionary: Processed parameter name and value pairs.
 396 
 397     Notes:
 398         The parameter name and values specified in ParamsOptionValues are validated before
 399         returning them in a dictionary.
 400 
 401     """
 402 
 403     ParamsInfo = {"MemoryInGB": 1, "NumThreads": 1, "OutputFile": "auto",  "ScratchDir" : "auto", "RemoveOutputFile": True}
 404     
 405     # Setup a canonical paramater names...
 406     ValidParamNames = []
 407     CanonicalParamNamesMap = {}
 408     for ParamName in sorted(ParamsInfo):
 409         ValidParamNames.append(ParamName)
 410         CanonicalParamNamesMap[ParamName.lower()] = ParamName
 411     
 412     # Update default values...
 413     if ParamsDefaultInfo is not None:
 414         for ParamName in ParamsDefaultInfo:
 415             if ParamName not in ParamsInfo:
 416                 MiscUtil.PrintError("The default parameter name, %s, specified using \"%s\" to function ProcessPsi4RunParameters is not a valid name. Supported parameter names: %s" % (ParamName, ParamsDefaultInfo, " ".join(ValidParamNames)))
 417             ParamsInfo[ParamName] = ParamsDefaultInfo[ParamName]
 418     
 419     if re.match("^auto$", ParamsOptionValue, re.I):
 420         # No specific parameters to process except for parameters with possible auto value...
 421         _ProcessPsi4RunAutoParameters(ParamsInfo, ParamsOptionName, ParamsOptionValue, InfileName)
 422         return ParamsInfo
 423     
 424     ParamsOptionValue = ParamsOptionValue.strip()
 425     if not ParamsOptionValue:
 426         PrintError("No valid parameter name and value pairs specified using \"%s\" option" % ParamsOptionName)
 427     
 428     ParamsOptionValueWords = ParamsOptionValue.split(",")
 429     if len(ParamsOptionValueWords) % 2:
 430         MiscUtil.PrintError("The number of comma delimited paramater names and values, %d, specified using \"%s\" option must be an even number." % (len(ParamsOptionValueWords), ParamsOptionName))
 431     
 432     # Validate paramater name and value pairs...
 433     for Index in range(0, len(ParamsOptionValueWords), 2):
 434         Name = ParamsOptionValueWords[Index].strip()
 435         Value = ParamsOptionValueWords[Index + 1].strip()
 436 
 437         CanonicalName = Name.lower()
 438         if  not CanonicalName in CanonicalParamNamesMap:
 439             MiscUtil.PrintError("The parameter name, %s, specified using \"%s\" is not a valid name. Supported parameter names: %s" % (Name, ParamsOptionName, " ".join(ValidParamNames)))
 440 
 441         ParamName = CanonicalParamNamesMap[CanonicalName]
 442         ParamValue = Value
 443         
 444         if re.match("^MemoryInGB$", ParamName, re.I):
 445             Value = float(Value)
 446             if Value <= 0:
 447                 MiscUtil.PrintError("The parameter value, %s, specified for parameter name, %s, using \"%s\" option is not a valid value. Supported values: > 0" % (Value, Name, ParamsOptionName))
 448             ParamValue = Value
 449         elif re.match("^NumThreads$", ParamName, re.I):
 450             Value = int(Value)
 451             if Value <= 0:
 452                 MiscUtil.PrintError("The parameter value, %s, specified for parameter name, %s, using \"%s\" option is not a valid value. Supported values: > 0" % (Value, Name, ParamsOptionName))
 453             ParamValue = Value
 454         elif re.match("^ScratchDir$", ParamName, re.I):
 455             if not re.match("^auto$", Value, re.I):
 456                 if not os.path.isdir(Value):
 457                     MiscUtil.PrintError("The parameter value, %s, specified for parameter name, %s, using \"%s\" option is not a valid value. It must be a directory name." % (Value, Name, ParamsOptionName))
 458             ParamValue = Value
 459         elif re.match("^RemoveOutputFile$", ParamName, re.I):
 460             if re.match("^(yes|true)$", Value, re.I):
 461                 Value = True
 462             elif re.match("^(no|false)$", Value, re.I):
 463                 Value = False
 464             else:
 465                 MiscUtil.PrintError("The parameter value, %s, specified for parameter name, %s, using \"%s\" option is not a valid value. Supported values: yes, no, true, or false" % (Value, Name, ParamsOptionName))
 466             ParamValue = Value
 467             
 468         # Set value...
 469         ParamsInfo[ParamName] = ParamValue
 470     
 471     # Handle paramaters with possible auto values...
 472     _ProcessPsi4RunAutoParameters(ParamsInfo, ParamsOptionName, ParamsOptionValue, InfileName)
 473 
 474     return ParamsInfo
 475 
 476 def _ProcessPsi4RunAutoParameters(ParamsInfo, ParamsOptionName, ParamsOptionValue, InfileName):
 477     """Process parameters with possible auto values.
 478     """
 479     
 480     Value = ParamsInfo["OutputFile"]
 481     ParamValue = Value
 482     if re.match("^auto$", Value, re.I):
 483         if InfileName is not None:
 484             # Use InfileName to setup output file. The OutputFile name is automatically updated using
 485             # PID during multiprocessing...
 486             InfileDir, InfileRoot, InfileExt = MiscUtil.ParseFileName(InfileName)
 487             OutputFile = "%s_Psi4.out" % (InfileRoot)
 488         else:
 489             OutputFile = "Psi4.out"
 490     elif re.match("^(devnull|quiet)$", Value, re.I):
 491         OutputFile = "quiet"
 492     else:
 493         # It'll be treated as a filename and processed later...
 494         OutputFile = Value
 495     
 496     ParamsInfo["OutputFile"] = OutputFile
 497 
 498     # OutputFileSpecified is used to track the specified value of the paramater.
 499     # It may be used by the calling function to dynamically override the value of
 500     # OutputFile to suprress the Psi4 output based on the initial value.
 501     ParamsInfo["OutputFileSpecified"] = ParamValue
 502     
 503 def ProcessPsi4DDXSolvationParameters(ParamsOptionName, ParamsOptionValue, ParamsDefaultInfo = None):
 504     """Process parameters for Psi4 DDX solvation and return a map containing
 505     processed parameter names and values.
 506     
 507     ParamsOptionValue is a space delimited list of parameter name and value pairs
 508     for solvation energy calculatios.
 509     
 510     The supported parameter names along with their default and possible
 511     values are shown below:
 512     
 513     SolvationModel PCM Solvent water solventEpsilon None radiiSet UFF
 514     
 515     solvationModel: Solvation model for calculating solvation energy. The
 516     corresponding Psi4 option is DDX_MODEL.
 517     
 518     solvent: Solvent to use. The corresponding Ps4 option is DDX_SOLVENT.
 519             
 520     solventEpsilon: Dielectric constant of the solvent. The corresponding
 521     Psi4 option is DDX_SOLVENT_EPSILON.
 522             
 523     radiiSet: Radius set for cavity spheres. The corresponding Psi option is
 524     DDX_RADII_SET.
 525 
 526     Arguments:
 527         ParamsOptionName (str): Command line Psi4 DDX solvation option name.
 528         ParamsOptionValues (str): Space delimited list of parameter name and value pairs.
 529         ParamsDefaultInfo (dict): Default values to override for selected parameters.
 530 
 531     Returns:
 532         dictionary: Processed parameter name and value pairs.
 533 
 534     """
 535     
 536     ParamsInfo = {"SolvationModel": "PCM", "Solvent": "water", "SolventEpsilon": None, "RadiiSet": "UFF", "RadiiScaling": "auto"}
 537     
 538     # Setup a canonical paramater names...
 539     ValidParamNames = []
 540     CanonicalParamNamesMap = {}
 541     for ParamName in sorted(ParamsInfo):
 542         ValidParamNames.append(ParamName)
 543         CanonicalParamNamesMap[ParamName.lower()] = ParamName
 544     
 545     # Update default values...
 546     if ParamsDefaultInfo is not None:
 547         for ParamName in ParamsDefaultInfo:
 548             if ParamName not in ParamsInfo:
 549                 MiscUtil.PrintError("The default parameter name, %s, specified using \"%s\" to function ProcessPsi4CubeFilesParameters not a valid name. Supported parameter names: %s" % (ParamName, ParamsDefaultInfo, " ".join(ValidParamNames)))
 550             ParamsInfo[ParamName] = ParamsDefaultInfo[ParamName]
 551     
 552     ParamsOptionValue = ParamsOptionValue.strip()
 553     if not ParamsOptionValue:
 554         MiscUtil.PrintError("No valid parameter name and value pairs specified using \"%s\" option" % ParamsOptionName)
 555     
 556     if re.match("^auto$", ParamsOptionValue, re.I):
 557         _ProcessPsi4DDXSolvationAutoParameters(ParamsInfo, ParamsOptionName, ParamsOptionValue)
 558         return ParamsInfo
 559 
 560     ParamsOptionValueWords = ParamsOptionValue.split()
 561     if len(ParamsOptionValueWords) % 2:
 562         MiscUtil.PrintError("The number of comma delimited paramater names and values, %d, specified using \"%s\" option must be an even number." % (len(ParamsOptionValueWords), ParamsOptionName))
 563 
 564     for Index in range(0, len(ParamsOptionValueWords), 2):
 565         Name = ParamsOptionValueWords[Index].strip()
 566         Value = ParamsOptionValueWords[Index + 1].strip()
 567 
 568         CanonicalName = Name.lower()
 569         if  not CanonicalName in CanonicalParamNamesMap:
 570             MiscUtil.PrintError("The parameter name, %s, specified using \"%s\" is not a valid name. Supported parameter names: %s" % (Name, ParamsOptionName, " ".join(ValidParamNames)))
 571 
 572         ParamName = CanonicalParamNamesMap[CanonicalName]
 573         ParamValue = Value
 574         
 575         if re.match("^SolvationModel$", ParamName, re.I):
 576             if not re.match("^(COSMO|PCM)$", Value, re.I):
 577                 MiscUtil.PrintError("The parameter value, %s, specified for parameter name, %s, using \"%s\" option is not a valid value. Supported values: COSMO or PCM" % (Value, Name, ParamsOptionName))
 578             ParamValue = Value
 579         elif re.match("^Solvent$", ParamName, re.I):
 580             if MiscUtil.IsEmpty(Value):
 581                 MiscUtil.PrintError("The parameter value, %s, specified for parameter name, %s, using \"%s\" option is empty." % (Value, Name, ParamsOptionName))
 582             ParamValue = Value
 583         elif re.match("^SolventEpsilon$", ParamName, re.I):
 584             if  re.match("^none$", Value, re.I):
 585                 Value = None
 586             else:
 587                 if not MiscUtil.IsNumber(Value):
 588                     MiscUtil.PrintError("The parameter value, %s, specified for parameter name, %s, using \"%s\" option must be a float." % (Value, Name, ParamsOptionName))
 589                 Value = float(Value)
 590                 if Value <= 0:
 591                     MiscUtil.PrintError("The parameter value, %s, specified for parameter name, %s, using \"%s\" option is not a valid value. Supported values: >= 0" % (Value, Name, ParamsOptionName))
 592             ParamValue = Value
 593         elif re.match("^RadiiSet$", ParamName, re.I):
 594             if not re.match("^(UFF|Bondi)$", Value, re.I):
 595                 MiscUtil.PrintError("The parameter value, %s, specified for parameter name, %s, using \"%s\" option is not a valid value. Supported values: UFF or Bondi" % (Value, Name, ParamsOptionName))
 596             ParamValue = Value
 597         elif re.match("^RadiiScaling$", ParamName, re.I):
 598             if not re.match("^auto$", Value, re.I):
 599                 if not MiscUtil.IsNumber(Value):
 600                     MiscUtil.PrintError("The parameter value, %s, specified for parameter name, %s, using \"%s\" option must be a float." % (Value, Name, ParamsOptionName))
 601                 Value = float(Value)
 602                 if Value <= 0:
 603                     MiscUtil.PrintError("The parameter value, %s, specified for parameter name, %s, using \"%s\" option is not a valid value. Supported values: >= 0" % (Value, Name, ParamsOptionName))
 604                 ParamValue = Value
 605         else:
 606             ParamValue = Value
 607         
 608         # Set value...
 609         ParamsInfo[ParamName] = ParamValue
 610     
 611     SolventEpsilon = ParamsInfo["SolventEpsilon"]
 612     if SolventEpsilon is not None and SolventEpsilon > 0.0:
 613         Solvent = ParamsInfo["Solvent"]
 614         if not MiscUtil.IsEmpty(Solvent):
 615             MiscUtil.PrintWarning(" You've specified values for both \"solvent\"  and \"solventEpsilon\" parameters using \"%s\" option. The parameter value, %s, specified for paramater name \"solvent\" is being ignored..." % (ParamsOptionName, Solvent))
 616     
 617     # Handle paramaters with possible auto values...
 618     _ProcessPsi4DDXSolvationAutoParameters(ParamsInfo, ParamsOptionName, ParamsOptionValue)
 619     return ParamsInfo
 620 
 621 def _ProcessPsi4DDXSolvationAutoParameters(ParamsInfo, ParamsOptionName, ParamsOptionValue):
 622     """Process parameters with possible auto values.
 623     """
 624     
 625     ParamValue = "%s" % ParamsInfo["RadiiScaling"]
 626     if re.match("^auto$", ParamValue, re.I):
 627         if re.match("^UFF$", ParamsInfo["RadiiSet"], re.I):
 628             ParamValue = 1.1
 629         elif re.match("^Bondi$", ParamsInfo["RadiiSet"], re.I):
 630             ParamValue = 1.2
 631         else:
 632             ParamValue = 0.0
 633     else:
 634         ParamValue = float(ParamValue)
 635     ParamsInfo["RadiiScaling"] = ParamValue
 636     
 637     return
 638 
 639 def SetupPsi4DDXSolvationOptions(SolvationMode, ParamsInfo):
 640     """Setup Psi4 options for calculating solvation energy using DDX module.
 641     
 642     Arguments:
 643         SolvationMode (bool): Set DDX option for solvation calculation.
 644         ParamsInfo (dict): Psi4 DDX parameter name and value pairs.
 645 
 646     Returns:
 647         dictionary: Psi4 Option name and value pairs.
 648 
 649     """
 650     
 651     # Initialize DDX solvation options...
 652     DDXOptionsInfo = {}
 653     DDXOptionsInfo["DDX"] = True if SolvationMode else False
 654     
 655     # Setup DDX solvation options...
 656     ParamNameToDDXOptionID = {"SolvationModel": "DDX_MODEL", "Solvent": "DDX_SOLVENT", "SolventEpsilon": "DDX_SOLVENT_EPSILON", "RadiiSet": "DDX_RADII_SET", "RadiiScaling": "DDX_RADII_SCALING"}
 657     
 658     for ParamName in ParamNameToDDXOptionID:
 659         DDXOptionID = ParamNameToDDXOptionID[ParamName]
 660         DDXOptionsInfo[DDXOptionID] = ParamsInfo[ParamName]
 661     
 662     # Check for the presence fo both solvent and solvent epsilon parameters...
 663     if DDXOptionsInfo["DDX_SOLVENT_EPSILON"] is None:
 664         DDXOptionsInfo.pop("DDX_SOLVENT_EPSILON", None)
 665     else:
 666         DDXOptionsInfo.pop("DDX_SOLVENT", None)
 667     
 668     return DDXOptionsInfo
 669 
 670 def ProcessPsi4CubeFilesParameters(ParamsOptionName, ParamsOptionValue, ParamsDefaultInfo = None):
 671     """Process parameters for Psi4 runs and return a map containing processed
 672     parameter names and values.
 673     
 674     ParamsOptionValue is a comma delimited list of parameter name and value pairs
 675     for generating cube files.
 676     
 677     The supported parameter names along with their default and possible
 678     values are shown below:
 679     
 680     GridSpacing, 0.2, GridOverage, 4.0, IsoContourThreshold, 0.85
 681     
 682     GridSpacing: Units: Bohr. A higher value reduces the size of the cube files
 683     on the disk. This option corresponds to Psi4 option CUBIC_GRID_SPACING.
 684     
 685     GridOverage: Units: Bohr.This option corresponds to Psi4 option
 686     CUBIC_GRID_OVERAGE.
 687     
 688     IsoContourThreshold captures specified percent of the probability density
 689     using the least amount of grid points. This option corresponds to Psi4 option
 690     CUBEPROP_ISOCONTOUR_THRESHOLD.
 691 
 692     Arguments:
 693         ParamsOptionName (str): Command line Psi4 cube files option name.
 694         ParamsOptionValues (str): Comma delimited list of parameter name and value pairs.
 695         ParamsDefaultInfo (dict): Default values to override for selected parameters.
 696 
 697     Returns:
 698         dictionary: Processed parameter name and value pairs.
 699 
 700     """
 701 
 702     ParamsInfo = {"GridSpacing": 0.2, "GridOverage":  4.0, "IsoContourThreshold": 0.85}
 703     
 704     # Setup a canonical paramater names...
 705     ValidParamNames = []
 706     CanonicalParamNamesMap = {}
 707     for ParamName in sorted(ParamsInfo):
 708         ValidParamNames.append(ParamName)
 709         CanonicalParamNamesMap[ParamName.lower()] = ParamName
 710     
 711     # Update default values...
 712     if ParamsDefaultInfo is not None:
 713         for ParamName in ParamsDefaultInfo:
 714             if ParamName not in ParamsInfo:
 715                 MiscUtil.PrintError("The default parameter name, %s, specified using \"%s\" to function ProcessPsi4CubeFilesParameters not a valid name. Supported parameter names: %s" % (ParamName, ParamsDefaultInfo, " ".join(ValidParamNames)))
 716             ParamsInfo[ParamName] = ParamsDefaultInfo[ParamName]
 717     
 718     if re.match("^auto$", ParamsOptionValue, re.I):
 719         # No specific parameters to process except for parameters with possible auto value...
 720         _ProcessPsi4CubeFilesAutoParameters(ParamsInfo, ParamsOptionName, ParamsOptionValue)
 721         return ParamsInfo
 722     
 723     ParamsOptionValue = ParamsOptionValue.strip()
 724     if not ParamsOptionValue:
 725         PrintError("No valid parameter name and value pairs specified using \"%s\" option" % ParamsOptionName)
 726     
 727     ParamsOptionValueWords = ParamsOptionValue.split(",")
 728     if len(ParamsOptionValueWords) % 2:
 729         MiscUtil.PrintError("The number of comma delimited paramater names and values, %d, specified using \"%s\" option must be an even number." % (len(ParamsOptionValueWords), ParamsOptionName))
 730     
 731     # Validate paramater name and value pairs...
 732     for Index in range(0, len(ParamsOptionValueWords), 2):
 733         Name = ParamsOptionValueWords[Index].strip()
 734         Value = ParamsOptionValueWords[Index + 1].strip()
 735 
 736         CanonicalName = Name.lower()
 737         if  not CanonicalName in CanonicalParamNamesMap:
 738             MiscUtil.PrintError("The parameter name, %s, specified using \"%s\" is not a valid name. Supported parameter names: %s" % (Name, ParamsOptionName, " ".join(ValidParamNames)))
 739 
 740         ParamName = CanonicalParamNamesMap[CanonicalName]
 741         ParamValue = Value
 742         
 743         if re.match("^(GridSpacing|GridOverage)$", ParamName, re.I):
 744             if not MiscUtil.IsFloat(Value):
 745                 MiscUtil.PrintError("The parameter value, %s, specified for parameter name, %s, using \"%s\" option must be a float." % (Value, Name, ParamsOptionName))
 746             Value = float(Value)
 747             if Value <= 0:
 748                 MiscUtil.PrintError("The parameter value, %s, specified for parameter name, %s, using \"%s\" option is not a valid value. Supported values: > 0" % (Value, Name, ParamsOptionName))
 749             ParamValue = Value
 750         elif re.match("^IsoContourThreshold$", ParamName, re.I):
 751             if not MiscUtil.IsFloat(Value):
 752                 MiscUtil.PrintError("The parameter value, %s, specified for parameter name, %s, using \"%s\" option must be a float." % (Value, Name, ParamsOptionName))
 753             Value = float(Value)
 754             if Value <= 0 or Value > 1:
 755                 MiscUtil.PrintError("The parameter value, %s, specified for parameter name, %s, using \"%s\" option is not a valid value. Supported values: >= 0 and <= 1" % (Value, Name, ParamsOptionName))
 756             ParamValue = Value
 757         
 758         # Set value...
 759         ParamsInfo[ParamName] = ParamValue
 760     
 761     # Handle paramaters with possible auto values...
 762     _ProcessPsi4CubeFilesAutoParameters(ParamsInfo, ParamsOptionName, ParamsOptionValue)
 763 
 764     return ParamsInfo
 765 
 766 def _ProcessPsi4CubeFilesAutoParameters(ParamsInfo, ParamsOptionName, ParamsOptionValue):
 767     """Process parameters with possible auto values.
 768     """
 769     
 770     # No auto parameters to process
 771     return
 772 
 773 def RetrieveIsocontourRangeFromCubeFile(CubeFileName):
 774     """Retrieve isocontour range values from the cube file. The range
 775     values are retrieved from the second line in the cube file after
 776     the string 'Isocontour range'.
 777     
 778     Arguments:
 779         CubeFileName (str): Cube file name.
 780 
 781     Returns:
 782         float: Minimum range value.
 783         float: Maximum range value.
 784 
 785     """
 786 
 787     IsocontourRangeMin, IsocontourRangeMax = [None] * 2
 788     
 789     CubeFH = open(CubeFileName, "r")
 790     if CubeFH is None:
 791         MiscUtil.PrintError("Couldn't open cube file: %s.\n" % (CubeFileName))
 792 
 793     # Look for isocontour range in the first 2 comments line...
 794     RangeLine = None
 795     LineCount = 0
 796     for Line in CubeFH:
 797         LineCount += 1
 798         Line = Line.rstrip()
 799         if re.search("Isocontour range", Line, re.I):
 800             RangeLine = Line
 801             break
 802         
 803         if LineCount >= 2:
 804             break
 805     CubeFH.close()
 806 
 807     if RangeLine is None:
 808         return (IsocontourRangeMin, IsocontourRangeMax)
 809     
 810     LineWords = RangeLine.split(":")
 811     
 812     ContourRangeWord = LineWords[-1]
 813     ContourRangeWord = re.sub("(\(|\)| )", "", ContourRangeWord)
 814 
 815     ContourLevel1, ContourLevel2 = ContourRangeWord.split(",")
 816     ContourLevel1 = float(ContourLevel1)
 817     ContourLevel2 = float(ContourLevel2)
 818     
 819     if ContourLevel1 < ContourLevel2:
 820         IsocontourRangeMin = ContourLevel1
 821         IsocontourRangeMax = ContourLevel2
 822     else:
 823         IsocontourRangeMin = ContourLevel2
 824         IsocontourRangeMax = ContourLevel1
 825         
 826     return (IsocontourRangeMin, IsocontourRangeMax)
 827     
 828 def RetrieveMinAndMaxValueFromCubeFile(CubeFileName):
 829     """Retrieve minimum and maxmimum grid values from the cube file.
 830     
 831     Arguments:
 832         CubeFileName (str): Cube file name.
 833 
 834     Returns:
 835         float: Minimum value.
 836         float: Maximum value.
 837 
 838     """
 839 
 840     MinValue, MaxValue = [sys.float_info.max, sys.float_info.min]
 841     
 842     CubeFH = open(CubeFileName, "r")
 843     if CubeFH is None:
 844         MiscUtil.PrintError("Couldn't open cube file: %s.\n" % (CubeFileName))
 845 
 846     # Ignore first two comments lines:
 847     #
 848     # The first two lines of the header are comments, they are generally ignored by parsing packages or used as two default labels.
 849     #
 850     # Ignore lines upto the last section of the header lines:
 851     #
 852     # The third line has the number of atoms included in the file followed by the position of the origin of the volumetric data.
 853     # The next three lines give the number of voxels along each axis (x, y, z) followed by the axis vector.
 854     # The last section in the header is one line for each atom consisting of 5 numbers, the first is the atom number, the second
 855     # is the charge, and the last three are the x,y,z coordinates of the atom center.
 856     #
 857     Line = CubeFH.readline()
 858     Line = CubeFH.readline()
 859     Line = CubeFH.readline()
 860     CubeFH.close()
 861     
 862     Line = Line.strip()
 863     LineWords = Line.split()
 864     NumOfAtoms = int(LineWords[0])
 865 
 866     HeaderLinesCount = 6 + NumOfAtoms
 867 
 868     # Ignore header lines...
 869     CubeFH = open(CubeFileName, "r")
 870     LineCount = 0
 871     for Line in CubeFH:
 872         LineCount += 1
 873         if LineCount >= HeaderLinesCount:
 874             break
 875     
 876     # Process values....
 877     for Line in CubeFH:
 878         Line = Line.strip()
 879         for Value in Line.split():
 880             Value = float(Value)
 881             
 882             if Value < MinValue:
 883                 MinValue = Value
 884             if Value > MaxValue:
 885                 MaxValue = Value
 886     
 887     return (MinValue, MaxValue)
 888 
 889 def UpdatePsi4OutputFileUsingPID(OutputFile, PID = None):
 890     """Append PID to output file name. The PID is automatically retrieved
 891     during None value of PID.
 892     
 893     Arguments:
 894         OutputFile (str): Output file name.
 895         PID (int): Process ID or None.
 896 
 897     Returns:
 898         str: Update output file name. Format: <OutFieRoot>_<PID>.<OutFileExt>
 899 
 900     """
 901     
 902     if re.match("stdout|devnull|quiet", OutputFile, re.I):
 903         return OutputFile
 904 
 905     if PID is None:
 906         PID = os.getpid()
 907     
 908     FileDir, FileRoot, FileExt = MiscUtil.ParseFileName(OutputFile)
 909     OutputFile = "%s_PID%s.%s" % (FileRoot, PID, FileExt)
 910     
 911     return OutputFile
 912     
 913 def RemoveScratchFiles(psi4, OutputFile, PID = None):
 914     """Remove any leftover scratch files associated with the specified output
 915     file. The file specification, <OutfileRoot>.*<PID>.* is used to collect and
 916     remove files from the scratch directory. In addition, the file
 917     psi.<PID>.clean, in current directory is removed.
 918     
 919     Arguments:
 920         psi4 (object): psi4 module reference.
 921         OutputFile (str): Output file name.
 922         PID (int): Process ID or None.
 923 
 924     Returns:
 925         None
 926 
 927     """
 928     
 929     if re.match("stdout|devnull|quiet", OutputFile, re.I):
 930         # Scratch files are associated to stdout prefix...
 931         OutputFile = "stdout"
 932     
 933     if PID is None:
 934         PID = os.getpid()
 935     
 936     OutfileDir, OutfileRoot, OutfileExt = MiscUtil.ParseFileName(OutputFile)
 937     
 938     ScratchOutfilesSpec = os.path.join(psi4.core.IOManager.shared_object().get_default_path(), "%s.*%s.*" % (OutfileRoot, PID))
 939     for ScratchFile in glob.glob(ScratchOutfilesSpec):
 940         os.remove(ScratchFile)
 941 
 942     # Remove any psi.<PID>.clean in the current directory...
 943     ScratchFile = os.path.join(os.getcwd(), "psi.%s.clean" % (PID))
 944     if os.path.isfile(ScratchFile):
 945         os.remove(ScratchFile)