MayaChemTools

    1 #
    2 # File: Psi4Util.py
    3 # Author: Manish Sud <msud@san.rr.com>
    4 #
    5 # Copyright (C) 2026 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__ = [
   38     "CalculateSinglePointEnergy",
   39     "InitializePsi4",
   40     "JoinMethodNameAndBasisSet",
   41     "ListPsi4RunParamaters",
   42     "RetrieveIsocontourRangeFromCubeFile",
   43     "RetrieveMinAndMaxValueFromCubeFile",
   44     "PerformGeometryOptimization",
   45     "ProcessPsi4ConstrainTorsionsParameters",
   46     "ProcessPsi4CubeFilesParameters",
   47     "ProcessPsi4OptionsParameters",
   48     "ProcessPsi4RunParameters",
   49     "ProcessPsi4DDXSolvationParameters",
   50     "RemoveScratchFiles",
   51     "SetupPsi4DDXSolvationOptions",
   52     "UpdatePsi4OptionsParameters",
   53     "UpdatePsi4RunParameters",
   54     "UpdatePsi4OutputFileUsingPID",
   55 ]
   56 
   57 
   58 def InitializePsi4(Psi4RunParams=None, Psi4OptionsParams=None, PrintVersion=False, PrintHeader=False):
   59     """Import Psi4 module and configure it for running Psi4 jobs.
   60 
   61     Arguments:
   62         Psi4RunParams (dict): Runtime parameter name and value pairs.
   63         Psi4OptionsParams (dict): Option name and value pairs. This is simply
   64             passed to ps4.set_options().
   65         PrintVersion (bool): Print version number.
   66         PrintHeader (bool): Print header information.
   67 
   68     Returns:
   69         Object: Psi4 module reference.
   70 
   71     """
   72 
   73     # Import Psi4...
   74     try:
   75         import psi4
   76     except ImportError as ErrMsg:
   77         sys.stderr.write("\nFailed to import Psi4 module/package: %s\n" % ErrMsg)
   78         sys.stderr.write("Check/update your Psi4 environment and try again.\n\n")
   79         sys.exit(1)
   80 
   81     Psi4Handle = psi4
   82 
   83     if PrintVersion:
   84         MiscUtil.PrintInfo("Importing Psi4 module (Psi4 v%s)...\n" % (Psi4Handle.__version__))
   85 
   86     # Update Psi4 run paramaters...
   87     if Psi4RunParams is not None:
   88         UpdatePsi4RunParameters(Psi4Handle, Psi4RunParams)
   89 
   90     # Update Psi4 options...
   91     if Psi4OptionsParams is not None:
   92         UpdatePsi4OptionsParameters(Psi4Handle, Psi4OptionsParams)
   93 
   94     # Print header after updating Psi4 run parameters...
   95     if PrintHeader:
   96         Psi4Handle.print_header()
   97 
   98     return Psi4Handle
   99 
  100 
  101 def CalculateSinglePointEnergy(psi4, Molecule, Method, BasisSet, ReturnWaveFunction=False, Quiet=False):
  102     """Calculate single point electronic energy in Hartrees using a specified
  103     method and basis set.
  104 
  105     Arguments:
  106         psi4 (Object): Psi4 module reference.
  107         Molecule (Object): Psi4 molecule object.
  108         Method (str): A valid method name.
  109         BasisSet (str): A valid basis set.
  110         ReturnWaveFunction (bool): Return wave function.
  111         Quiet (bool): Flag to print error message.
  112 
  113     Returns:
  114         float: Total electronic energy in Hartrees.
  115         (float, psi4 object): Energy and wavefuction.
  116 
  117     """
  118 
  119     Status = False
  120     Energy, WaveFunction = [None] * 2
  121 
  122     try:
  123         MethodAndBasisSet = JoinMethodNameAndBasisSet(Method, BasisSet)
  124         if ReturnWaveFunction:
  125             Energy, WaveFunction = psi4.energy(MethodAndBasisSet, molecule=Molecule, return_wfn=True)
  126         else:
  127             Energy = psi4.energy(MethodAndBasisSet, molecule=Molecule, return_wfn=False)
  128         Status = True
  129     except Exception as ErrMsg:
  130         if not Quiet:
  131             MiscUtil.PrintWarning("Psi4Util.CalculateSinglePointEnergy: Failed to calculate energy:\n%s\n" % ErrMsg)
  132 
  133     return (Status, Energy, WaveFunction) if ReturnWaveFunction else (Status, Energy)
  134 
  135 
  136 def PerformGeometryOptimization(psi4, Molecule, Method, BasisSet, ReturnWaveFunction=True, Quiet=False):
  137     """Perform geometry optimization using a specified method and basis set.
  138 
  139     Arguments:
  140         psi4 (Object): Psi4 module reference.
  141         Molecule (Object): Psi4 molecule object.
  142         Method (str): A valid method name.
  143         BasisSet (str): A valid basis set.
  144         ReturnWaveFunction (bool): Return wave function.
  145         Quiet (bool): Flag to print error message.
  146 
  147     Returns:
  148         float: Total electronic energy in Hartrees.
  149         (float, psi4 object): Energy and wavefuction.
  150 
  151     """
  152 
  153     Status = False
  154     Energy, WaveFunction = [None] * 2
  155 
  156     try:
  157         MethodAndBasisSet = JoinMethodNameAndBasisSet(Method, BasisSet)
  158         if ReturnWaveFunction:
  159             Energy, WaveFunction = psi4.optimize(MethodAndBasisSet, molecule=Molecule, return_wfn=True)
  160         else:
  161             Energy = psi4.optimize(MethodAndBasisSet, molecule=Molecule, return_wfn=False)
  162         Status = True
  163     except Exception as ErrMsg:
  164         if not Quiet:
  165             MiscUtil.PrintWarning(
  166                 "Psi4Util.PerformGeometryOptimization: Failed to perform geometry optimization:\n%s\n" % ErrMsg
  167             )
  168 
  169     return (Status, Energy, WaveFunction) if ReturnWaveFunction else (Status, Energy)
  170 
  171 
  172 def JoinMethodNameAndBasisSet(MethodName, BasisSet):
  173     """Join method name and basis set using a backslash delimiter.
  174     An empty basis set specification is ignored.
  175 
  176     Arguments:
  177         MethodName (str): A valid method name.
  178         BasisSet (str): A valid basis set or an empty string.
  179 
  180     Returns:
  181         str: MethodName/BasisSet or MethodName
  182 
  183     """
  184 
  185     return MethodName if MiscUtil.IsEmpty(BasisSet) else "%s/%s" % (MethodName, BasisSet)
  186 
  187 
  188 def GetAtomPositions(psi4, WaveFunction, InAngstroms=True):
  189     """Retrieve a list of lists containing coordinates of all atoms in the
  190     molecule available in Psi4 wave function. By default, the atom positions
  191     are returned in Angstroms. The Psi4 default is Bohr.
  192 
  193     Arguments:
  194         psi4 (Object): Psi4 module reference.
  195         WaveFunction (Object): Psi4 wave function reference.
  196         InAngstroms (bool): True - Positions in Angstroms; Otherwise, in Bohr.
  197 
  198     Returns:
  199         None or list : List of lists containing atom positions.
  200 
  201     Examples:
  202 
  203         for AtomPosition in Psi4Util.GetAtomPositions(Psi4Handle, WaveFunction):
  204             print("X: %s; Y: %s; Z: %s" % (AtomPosition[0], AtomPosition[1],
  205                 AtomPosition[2]))
  206 
  207     """
  208 
  209     if WaveFunction is None:
  210         return None
  211 
  212     AtomPositions = WaveFunction.molecule().geometry().to_array()
  213     if InAngstroms:
  214         AtomPositions = AtomPositions * psi4.constants.bohr2angstroms
  215 
  216     return AtomPositions.tolist()
  217 
  218 
  219 def ListPsi4RunParamaters(psi4):
  220     """List values for a key set of the following Psi4 runtime parameters:
  221     Memory, NumThreads, OutputFile, ScratchDir, DataDir.
  222 
  223     Arguments:
  224         psi4 (object): Psi4 module reference.
  225 
  226     Returns:
  227         None
  228 
  229     """
  230 
  231     MiscUtil.PrintInfo("\nListing Psi4 run options:")
  232 
  233     # Memory in bytes...
  234     Memory = psi4.get_memory()
  235     MiscUtil.PrintInfo("Memory: %s (B); %s (MB)" % (Memory, Memory / (1024 * 1024)))
  236 
  237     # Number of threads...
  238     NumThreads = psi4.get_num_threads()
  239     MiscUtil.PrintInfo("NumThreads: %s " % (NumThreads))
  240 
  241     # Output file...
  242     OutputFile = psi4.core.get_output_file()
  243     MiscUtil.PrintInfo("OutputFile: %s " % (OutputFile))
  244 
  245     # Scratch dir...
  246     psi4_io = psi4.core.IOManager.shared_object()
  247     ScratchDir = psi4_io.get_default_path()
  248     MiscUtil.PrintInfo("ScratchDir: %s " % (ScratchDir))
  249 
  250     # Data dir...
  251     DataDir = psi4.core.get_datadir()
  252     MiscUtil.PrintInfo("DataDir: %s " % (DataDir))
  253 
  254 
  255 def UpdatePsi4OptionsParameters(psi4, OptionsInfo):
  256     """Update Psi4 options using psi4.set_options().
  257 
  258     Arguments:
  259         psi4 (object): Psi4 module reference.
  260         OptionsInfo (dictionary) : Option name and value pairs for setting
  261             global and module options.
  262 
  263     Returns:
  264         None
  265 
  266     """
  267     if OptionsInfo is None:
  268         return
  269 
  270     if len(OptionsInfo) == 0:
  271         return
  272 
  273     try:
  274         psi4.set_options(OptionsInfo)
  275     except Exception as ErrMsg:
  276         MiscUtil.PrintWarning("Psi4Util.UpdatePsi4OptionsParameters: Failed to set Psi4 options\n%s\n" % ErrMsg)
  277 
  278 
  279 def UpdatePsi4RunParameters(psi4, RunParamsInfo):
  280     """Update Psi4 runtime parameters. The supported parameter names along with
  281     their default values are as follows: MemoryInGB: 1; NumThreads: 1, OutputFile:
  282     stdout; ScratchDir: auto; RemoveOutputFile: True.
  283 
  284     Arguments:
  285         psi4 (object): Psi4 module reference.
  286         RunParamsInfo (dictionary) : Parameter name and value pairs for
  287             configuring Psi4 jobs.
  288 
  289     Returns:
  290         None
  291 
  292     """
  293 
  294     # Set default values for possible arguments...
  295     Psi4RunParams = {
  296         "MemoryInGB": 1,
  297         "NumThreads": 1,
  298         "OutputFile": "stdout",
  299         "ScratchDir": "auto",
  300         "RemoveOutputFile": True,
  301     }
  302 
  303     # Set specified values for possible arguments...
  304     for Param in Psi4RunParams:
  305         if Param in RunParamsInfo:
  306             Psi4RunParams[Param] = RunParamsInfo[Param]
  307 
  308     # Memory...
  309     Memory = int(Psi4RunParams["MemoryInGB"] * 1024 * 1024 * 1024)
  310     psi4.core.set_memory_bytes(Memory, True)
  311 
  312     # Number of threads...
  313     psi4.core.set_num_threads(Psi4RunParams["NumThreads"], quiet=True)
  314 
  315     # Output file...
  316     OutputFile = Psi4RunParams["OutputFile"]
  317     if not re.match("^stdout$", OutputFile, re.I):
  318         # Possible values: stdout, quiet, devnull, or filename
  319         if re.match("^(quiet|devnull)$", OutputFile, re.I):
  320             # Psi4 output is redirected to /dev/null after call to be_quiet function...
  321             psi4.core.be_quiet()
  322         else:
  323             # Delete existing output file at the start of the first Psi4 run...
  324             if Psi4RunParams["RemoveOutputFile"]:
  325                 if os.path.isfile(OutputFile):
  326                     os.remove(OutputFile)
  327 
  328             # Append to handle output from multiple Psi4 runs for molecules in
  329             # input file...
  330             Append = True
  331             psi4.core.set_output_file(OutputFile, Append)
  332 
  333     # Scratch directory...
  334     ScratchDir = Psi4RunParams["ScratchDir"]
  335     if not re.match("^auto$", ScratchDir, re.I):
  336         if not os.path.isdir(ScratchDir):
  337             MiscUtil.PrintError("ScratchDir is not a directory: %s" % ScratchDir)
  338         psi4.core.IOManager.shared_object().set_default_path(os.path.abspath(os.path.expanduser(ScratchDir)))
  339 
  340 
  341 def ProcessPsi4OptionsParameters(ParamsOptionName, ParamsOptionValue):
  342     """Process parameters for setting up Psi4 options and return a map
  343     containing processed parameter names and values.
  344 
  345     ParamsOptionValue is a comma delimited list of Psi4 option name and value
  346     pairs for setting global and module options. The names are 'option_name'
  347     for global options and 'module_name__option_name' for options local to a
  348     module. The specified option names must be valid Psi4 names. No validation
  349     is performed.
  350 
  351     The specified option name and  value pairs are processed and passed to
  352     psi4.set_options() as a dictionary. The supported value types are float,
  353     integer, boolean, or string. The float value string is converted into a float.
  354     The valid values for a boolean string are yes, no, true, false, on, or off.
  355 
  356     Arguments:
  357         ParamsOptionName (str): Command line input parameters option name.
  358         ParamsOptionValue (str): Comma delimited list of parameter name and value pairs.
  359 
  360     Returns:
  361         dictionary: Processed parameter name and value pairs.
  362 
  363     """
  364 
  365     OptionsInfo = {}
  366 
  367     if re.match("^(auto|none)$", ParamsOptionValue, re.I):
  368         return None
  369 
  370     ParamsOptionValue = ParamsOptionValue.strip()
  371     if not ParamsOptionValue:
  372         MiscUtil.PrintError('No valid parameter name and value pairs specified using "%s" option' % ParamsOptionName)
  373 
  374     ParamsOptionValueWords = ParamsOptionValue.split(",")
  375     if len(ParamsOptionValueWords) % 2:
  376         MiscUtil.PrintError(
  377             'The number of comma delimited paramater names and values, %d, specified using "%s" option must be an even number.'
  378             % (len(ParamsOptionValueWords), ParamsOptionName)
  379         )
  380 
  381     # Validate paramater name and value pairs...
  382     for Index in range(0, len(ParamsOptionValueWords), 2):
  383         Name = ParamsOptionValueWords[Index].strip()
  384         Value = ParamsOptionValueWords[Index + 1].strip()
  385 
  386         if MiscUtil.IsInteger(Value):
  387             Value = int(Value)
  388         elif MiscUtil.IsFloat(Value):
  389             Value = float(Value)
  390 
  391         OptionsInfo[Name] = Value
  392 
  393     return OptionsInfo
  394 
  395 
  396 def ProcessPsi4RunParameters(ParamsOptionName, ParamsOptionValue, InfileName=None, ParamsDefaultInfo=None):
  397     """Process parameters for Psi4 runs and return a map containing processed
  398     parameter names and values.
  399 
  400     ParamsOptionValue a comma delimited list of parameter name and value pairs
  401     for configuring Psi4 jobs.
  402 
  403     The supported parameter names along with their default and possible
  404     values are shown below:
  405 
  406     MemoryInGB,1,NumThreads,1,OutputFile,auto,ScratchDir,auto,
  407     RemoveOutputFile,yes
  408 
  409     Possible  values: OutputFile - stdout, quiet, or FileName; ScratchDir -
  410     DirName; RemoveOutputFile - yes, no, true, or false
  411 
  412     These parameters control the runtime behavior of Psi4.
  413 
  414     The default for 'OutputFile' is a file name <InFileRoot>_Psi4.out. The PID
  415     is appened the output file name during multiprocessing. The 'stdout' value
  416     for 'OutputType' sends Psi4 output to stdout. The 'quiet' or 'devnull' value
  417     suppresses all Psi4 output.
  418 
  419     The default 'Yes' value of 'RemoveOutputFile' option forces the removal
  420     of any existing Psi4 before creating new files to append output from
  421     multiple Psi4 runs.
  422 
  423     The option 'ScratchDir' is a directory path to the location of scratch
  424     files. The default value corresponds to Psi4 default. It may be used to
  425     override the deafult path.
  426 
  427     Arguments:
  428         ParamsOptionName (str): Command line Psi4 run parameters option name.
  429         ParamsOptionValues (str): Comma delimited list of parameter name and value pairs.
  430         InfileName (str): Name of input file.
  431         ParamsDefaultInfo (dict): Default values to override for selected parameters.
  432 
  433     Returns:
  434         dictionary: Processed parameter name and value pairs.
  435 
  436     Notes:
  437         The parameter name and values specified in ParamsOptionValues are validated before
  438         returning them in a dictionary.
  439 
  440     """
  441 
  442     ParamsInfo = {
  443         "MemoryInGB": 1,
  444         "NumThreads": 1,
  445         "OutputFile": "auto",
  446         "ScratchDir": "auto",
  447         "RemoveOutputFile": True,
  448     }
  449 
  450     # Setup a canonical paramater names...
  451     ValidParamNames = []
  452     CanonicalParamNamesMap = {}
  453     for ParamName in sorted(ParamsInfo):
  454         ValidParamNames.append(ParamName)
  455         CanonicalParamNamesMap[ParamName.lower()] = ParamName
  456 
  457     # Update default values...
  458     if ParamsDefaultInfo is not None:
  459         for ParamName in ParamsDefaultInfo:
  460             if ParamName not in ParamsInfo:
  461                 MiscUtil.PrintError(
  462                     'The default parameter name, %s, specified using "%s" to function ProcessPsi4RunParameters is not a valid name. Supported parameter names: %s'
  463                     % (ParamName, ParamsDefaultInfo, " ".join(ValidParamNames))
  464                 )
  465             ParamsInfo[ParamName] = ParamsDefaultInfo[ParamName]
  466 
  467     if re.match("^auto$", ParamsOptionValue, re.I):
  468         # No specific parameters to process except for parameters with possible auto value...
  469         _ProcessPsi4RunAutoParameters(ParamsInfo, ParamsOptionName, ParamsOptionValue, InfileName)
  470         return ParamsInfo
  471 
  472     ParamsOptionValue = ParamsOptionValue.strip()
  473     if not ParamsOptionValue:
  474         MiscUtil.PrintError('No valid parameter name and value pairs specified using "%s" option' % ParamsOptionName)
  475 
  476     ParamsOptionValueWords = ParamsOptionValue.split(",")
  477     if len(ParamsOptionValueWords) % 2:
  478         MiscUtil.PrintError(
  479             'The number of comma delimited paramater names and values, %d, specified using "%s" option must be an even number.'
  480             % (len(ParamsOptionValueWords), ParamsOptionName)
  481         )
  482 
  483     # Validate paramater name and value pairs...
  484     for Index in range(0, len(ParamsOptionValueWords), 2):
  485         Name = ParamsOptionValueWords[Index].strip()
  486         Value = ParamsOptionValueWords[Index + 1].strip()
  487 
  488         CanonicalName = Name.lower()
  489         if CanonicalName not in CanonicalParamNamesMap:
  490             MiscUtil.PrintError(
  491                 'The parameter name, %s, specified using "%s" is not a valid name. Supported parameter names: %s'
  492                 % (Name, ParamsOptionName, " ".join(ValidParamNames))
  493             )
  494 
  495         ParamName = CanonicalParamNamesMap[CanonicalName]
  496         ParamValue = Value
  497 
  498         if re.match("^MemoryInGB$", ParamName, re.I):
  499             Value = float(Value)
  500             if Value <= 0:
  501                 MiscUtil.PrintError(
  502                     'The parameter value, %s, specified for parameter name, %s, using "%s" option is not a valid value. Supported values: > 0'
  503                     % (Value, Name, ParamsOptionName)
  504                 )
  505             ParamValue = Value
  506         elif re.match("^NumThreads$", ParamName, re.I):
  507             Value = int(Value)
  508             if Value <= 0:
  509                 MiscUtil.PrintError(
  510                     'The parameter value, %s, specified for parameter name, %s, using "%s" option is not a valid value. Supported values: > 0'
  511                     % (Value, Name, ParamsOptionName)
  512                 )
  513             ParamValue = Value
  514         elif re.match("^ScratchDir$", ParamName, re.I):
  515             if not re.match("^auto$", Value, re.I):
  516                 if not os.path.isdir(Value):
  517                     MiscUtil.PrintError(
  518                         'The parameter value, %s, specified for parameter name, %s, using "%s" option is not a valid value. It must be a directory name.'
  519                         % (Value, Name, ParamsOptionName)
  520                     )
  521             ParamValue = Value
  522         elif re.match("^RemoveOutputFile$", ParamName, re.I):
  523             if re.match("^(yes|true)$", Value, re.I):
  524                 Value = True
  525             elif re.match("^(no|false)$", Value, re.I):
  526                 Value = False
  527             else:
  528                 MiscUtil.PrintError(
  529                     'The parameter value, %s, specified for parameter name, %s, using "%s" option is not a valid value. Supported values: yes, no, true, or false'
  530                     % (Value, Name, ParamsOptionName)
  531                 )
  532             ParamValue = Value
  533 
  534         # Set value...
  535         ParamsInfo[ParamName] = ParamValue
  536 
  537     # Handle paramaters with possible auto values...
  538     _ProcessPsi4RunAutoParameters(ParamsInfo, ParamsOptionName, ParamsOptionValue, InfileName)
  539 
  540     return ParamsInfo
  541 
  542 
  543 def _ProcessPsi4RunAutoParameters(ParamsInfo, ParamsOptionName, ParamsOptionValue, InfileName):
  544     """Process parameters with possible auto values."""
  545 
  546     Value = ParamsInfo["OutputFile"]
  547     ParamValue = Value
  548     if re.match("^auto$", Value, re.I):
  549         if InfileName is not None:
  550             # Use InfileName to setup output file. The OutputFile name is automatically updated using
  551             # PID during multiprocessing...
  552             InfileDir, InfileRoot, InfileExt = MiscUtil.ParseFileName(InfileName)
  553             OutputFile = "%s_Psi4.out" % (InfileRoot)
  554         else:
  555             OutputFile = "Psi4.out"
  556     elif re.match("^(devnull|quiet)$", Value, re.I):
  557         OutputFile = "quiet"
  558     else:
  559         # It'll be treated as a filename and processed later...
  560         OutputFile = Value
  561 
  562     ParamsInfo["OutputFile"] = OutputFile
  563 
  564     # OutputFileSpecified is used to track the specified value of the paramater.
  565     # It may be used by the calling function to dynamically override the value of
  566     # OutputFile to suprress the Psi4 output based on the initial value.
  567     ParamsInfo["OutputFileSpecified"] = ParamValue
  568 
  569 
  570 def ProcessPsi4ConstrainTorsionsParameters(ParamsOptionName, ParamsOptionValue, ParamsDefaultInfo=None):
  571     """Process parameters for Psi4 constrain torsions around rotatable bonds and
  572     return a map containing processed parameter names and values.
  573 
  574     ParamsOptionValue is a comma delimited list of parameter name and value pairs
  575     for generating cube files.
  576 
  577     The supported parameter names along with their default and possible
  578     values are shown below:
  579 
  580     ignoreHydrogens, yes, rotBondsSMARTSMode, NonStrict,
  581     rotBondsSMARTSPattern, Auto
  582 
  583     Arguments:
  584         ParamsOptionName (str): Command line Psi4 constrain torsions option name.
  585         ParamsOptionValues (str): Comma delimited list of parameter name and value pairs.
  586         ParamsDefaultInfo (dict): Default values to override for selected parameters.
  587 
  588     Returns:
  589         dictionary: Processed parameter name and value pairs.
  590 
  591     """
  592 
  593     ParamsInfo = {"IgnoreHydrogens": True, "RotBondsSMARTSMode": "SemiStrict", "RotBondsSMARTSPattern": "auto"}
  594 
  595     # Setup a canonical paramater names...
  596     ValidParamNames = []
  597     CanonicalParamNamesMap = {}
  598     for ParamName in sorted(ParamsInfo):
  599         ValidParamNames.append(ParamName)
  600         CanonicalParamNamesMap[ParamName.lower()] = ParamName
  601 
  602     # Update default values...
  603     if ParamsDefaultInfo is not None:
  604         for ParamName in ParamsDefaultInfo:
  605             if ParamName not in ParamsInfo:
  606                 MiscUtil.PrintError(
  607                     'The default parameter name, %s, specified using "%s" to function ProcessPsi4ConstrainTorsionsParameters not a valid name. Supported parameter names: %s'
  608                     % (ParamName, ParamsDefaultInfo, " ".join(ValidParamNames))
  609                 )
  610             ParamsInfo[ParamName] = ParamsDefaultInfo[ParamName]
  611 
  612     if re.match("^auto$", ParamsOptionValue, re.I):
  613         # No specific parameters to process except for parameters with possible auto value...
  614         _ProcessPsi4ConstrainTorsionsAutoParameters(ParamsInfo, ParamsOptionName, ParamsOptionValue)
  615         return ParamsInfo
  616 
  617     ParamsOptionValue = ParamsOptionValue.strip()
  618     if not ParamsOptionValue:
  619         MiscUtil.PrintError('No valid parameter name and value pairs specified using "%s" option' % ParamsOptionName)
  620 
  621     ParamsOptionValueWords = ParamsOptionValue.split(",")
  622     if len(ParamsOptionValueWords) % 2:
  623         MiscUtil.PrintError(
  624             'The number of comma delimited paramater names and values, %d, specified using "%s" option must be an even number.'
  625             % (len(ParamsOptionValueWords), ParamsOptionName)
  626         )
  627 
  628     # Validate paramater name and value pairs...
  629     for Index in range(0, len(ParamsOptionValueWords), 2):
  630         Name = ParamsOptionValueWords[Index].strip()
  631         Value = ParamsOptionValueWords[Index + 1].strip()
  632 
  633         CanonicalName = Name.lower()
  634         if CanonicalName not in CanonicalParamNamesMap:
  635             MiscUtil.PrintError(
  636                 'The parameter name, %s, specified using "%s" is not a valid name. Supported parameter names: %s'
  637                 % (Name, ParamsOptionName, " ".join(ValidParamNames))
  638             )
  639 
  640         ParamName = CanonicalParamNamesMap[CanonicalName]
  641         ParamValue = Value
  642 
  643         if re.match("^IgnoreHydrogens$", ParamName, re.I):
  644             if re.match("^(yes|true)$", Value, re.I):
  645                 Value = True
  646             elif re.match("^(no|false)$", Value, re.I):
  647                 Value = False
  648             else:
  649                 MiscUtil.PrintError(
  650                     'The parameter value, %s, specified for parameter name, %s, using "%s" option is not a valid value. Supported values: yes, no, true, or false'
  651                     % (Value, Name, ParamsOptionName)
  652                 )
  653             ParamValue = Value
  654         elif re.match("^RotBondsSMARTSMode$", ParamName, re.I):
  655             if not re.match("^(NonStrict|SemiStrict|Strict|Specify)$", Value, re.I):
  656                 MiscUtil.PrintError(
  657                     'The parameter value, %s, specified for parameter name, %s, using "%s" option is not a valid value. Supported values: NonStrict, SemiStrict, Strict, or Specify'
  658                     % (Value, Name, ParamsOptionName)
  659                 )
  660             ParamValue = Value
  661 
  662         # Set value...
  663         ParamsInfo[ParamName] = ParamValue
  664 
  665     # Handle paramaters with possible auto values...
  666     _ProcessPsi4ConstrainTorsionsAutoParameters(ParamsInfo, ParamsOptionName, ParamsOptionValue)
  667 
  668     return ParamsInfo
  669 
  670 
  671 def _ProcessPsi4ConstrainTorsionsAutoParameters(ParamsInfo, ParamsOptionName, ParamsOptionValue):
  672     """Process parameters with possible auto values."""
  673 
  674     if re.match("^Specify$", ParamsInfo["RotBondsSMARTSMode"], re.I):
  675         if re.match("^auto$", ParamsInfo["RotBondsSMARTSPattern"], re.I):
  676             MiscUtil.PrintError(
  677                 'The parameter value, auto, specified for parameter name, RotBondsSMARTSPattern, using "%s" is not allowed during, specify, value for parameter name, RotBondsSMARTSMode. You must specify a valid SMARTS pattern using parameter name, RotBondsSMARTSPattern.'
  678                 % ParamsOptionName
  679             )
  680     else:
  681         if not re.match("^auto$", ParamsInfo["RotBondsSMARTSPattern"], re.I):
  682             MiscUtil.PrintError(
  683                 'The parameter value, %s, specified for parameter name, RotBondsSMARTSPattern, using "%s" is not allowed during, %s, value for parameter name, RotBondsSMARTSMode.'
  684                 % (ParamsInfo["RotBondsSMARTSPattern"], ParamsOptionName, ParamsInfo["RotBondsSMARTSMode"])
  685             )
  686 
  687     # Setup default SMARTS pattern...
  688     Name = "RotBondsSMARTSMode"
  689     Value = ParamsInfo[Name]
  690     if re.match("^NonStrict$", Value, re.I):
  691         RotBondsSMARTSPattern = "[!$(*#*)&!D1]-&!@[!$(*#*)&!D1]"
  692     elif re.match("^SemiStrict$", Value, re.I):
  693         RotBondsSMARTSPattern = "[!$(*#*)&!D1&!$(C(F)(F)F)&!$(C(Cl)(Cl)Cl)&!$(C(Br)(Br)Br)&!$(C([CH3])([CH3])[CH3])]-!@[!$(*#*)&!D1&!$(C(F)(F)F)&!$(C(Cl)(Cl)Cl)&!$(C(Br)(Br)Br)&!$(C([CH3])([CH3])[CH3])]"
  694     elif re.match("^Strict$", Value, re.I):
  695         RotBondsSMARTSPattern = "[!$(*#*)&!D1&!$(C(F)(F)F)&!$(C(Cl)(Cl)Cl)&!$(C(Br)(Br)Br)&!$(C([CH3])([CH3])[CH3])&!$([CD3](=[N,O,S])-!@[#7,O,S!D1])&!$([#7,O,S!D1]-!@[CD3]=[N,O,S])&!$([CD3](=[N+])-!@[#7!D1])&!$([#7!D1]-!@[CD3]=[N+])]-!@[!$(*#*)&!D1&!$(C(F)(F)F)&!$(C(Cl)(Cl)Cl)&!$(C(Br)(Br)Br)&!$(C([CH3])([CH3])[CH3])]"
  696     elif re.match("^Specify$", Value, re.I):
  697         RotBondsSMARTSPattern = ParamsInfo["RotBondsSMARTSPattern"].strip()
  698         if not len(RotBondsSMARTSPattern):
  699             MiscUtil.PrintError(
  700                 'Empty value specified using parameter name, RotBondsSMARTSPattern, using "%s" option'
  701                 % ParamsOptionName
  702             )
  703     else:
  704         MiscUtil.PrintError(
  705             'The parameter value, %s, specified for parameter name, %s, using "%s" option is not a valid value. Supported values: NonStrict, SemiStrict, Strict, or Specify'
  706             % (Value, Name, ParamsOptionName)
  707         )
  708 
  709     ParamsInfo["RotBondsSMARTSPattern"] = RotBondsSMARTSPattern
  710 
  711     return
  712 
  713 
  714 def ProcessPsi4DDXSolvationParameters(ParamsOptionName, ParamsOptionValue, ParamsDefaultInfo=None):
  715     """Process parameters for Psi4 DDX solvation and return a map containing
  716     processed parameter names and values.
  717 
  718     ParamsOptionValue is a space delimited list of parameter name and value pairs
  719     for solvation energy calculations.
  720 
  721     The supported parameter names along with their default and possible
  722     values are shown below:
  723 
  724     SolvationModel PCM Solvent water solventEpsilon None radiiSet UFF
  725 
  726     solvationModel: Solvation model for calculating solvation energy. The
  727     corresponding Psi4 option is DDX_MODEL.
  728 
  729     solvent: Solvent to use. The corresponding Ps4 option is DDX_SOLVENT.
  730 
  731     solventEpsilon: Dielectric constant of the solvent. The corresponding
  732     Psi4 option is DDX_SOLVENT_EPSILON.
  733 
  734     radiiSet: Radius set for cavity spheres. The corresponding Psi option is
  735     DDX_RADII_SET.
  736 
  737     Arguments:
  738         ParamsOptionName (str): Command line Psi4 DDX solvation option name.
  739         ParamsOptionValues (str): Space delimited list of parameter name and value pairs.
  740         ParamsDefaultInfo (dict): Default values to override for selected parameters.
  741 
  742     Returns:
  743         dictionary: Processed parameter name and value pairs.
  744 
  745     """
  746 
  747     ParamsInfo = {
  748         "SolvationModel": "PCM",
  749         "Solvent": "water",
  750         "SolventEpsilon": None,
  751         "RadiiSet": "UFF",
  752         "RadiiScaling": "auto",
  753     }
  754 
  755     # Setup a canonical paramater names...
  756     ValidParamNames = []
  757     CanonicalParamNamesMap = {}
  758     for ParamName in sorted(ParamsInfo):
  759         ValidParamNames.append(ParamName)
  760         CanonicalParamNamesMap[ParamName.lower()] = ParamName
  761 
  762     # Update default values...
  763     if ParamsDefaultInfo is not None:
  764         for ParamName in ParamsDefaultInfo:
  765             if ParamName not in ParamsInfo:
  766                 MiscUtil.PrintError(
  767                     'The default parameter name, %s, specified using "%s" to function ProcessPsi4DDXSolvationParameters not a valid name. Supported parameter names: %s'
  768                     % (ParamName, ParamsDefaultInfo, " ".join(ValidParamNames))
  769                 )
  770             ParamsInfo[ParamName] = ParamsDefaultInfo[ParamName]
  771 
  772     ParamsOptionValue = ParamsOptionValue.strip()
  773     if not ParamsOptionValue:
  774         MiscUtil.PrintError('No valid parameter name and value pairs specified using "%s" option' % ParamsOptionName)
  775 
  776     if re.match("^auto$", ParamsOptionValue, re.I):
  777         _ProcessPsi4DDXSolvationAutoParameters(ParamsInfo, ParamsOptionName, ParamsOptionValue)
  778         return ParamsInfo
  779 
  780     ParamsOptionValueWords = ParamsOptionValue.split()
  781     if len(ParamsOptionValueWords) % 2:
  782         MiscUtil.PrintError(
  783             'The number of comma delimited paramater names and values, %d, specified using "%s" option must be an even number.'
  784             % (len(ParamsOptionValueWords), ParamsOptionName)
  785         )
  786 
  787     for Index in range(0, len(ParamsOptionValueWords), 2):
  788         Name = ParamsOptionValueWords[Index].strip()
  789         Value = ParamsOptionValueWords[Index + 1].strip()
  790 
  791         CanonicalName = Name.lower()
  792         if CanonicalName not in CanonicalParamNamesMap:
  793             MiscUtil.PrintError(
  794                 'The parameter name, %s, specified using "%s" is not a valid name. Supported parameter names: %s'
  795                 % (Name, ParamsOptionName, " ".join(ValidParamNames))
  796             )
  797 
  798         ParamName = CanonicalParamNamesMap[CanonicalName]
  799         ParamValue = Value
  800 
  801         if re.match("^SolvationModel$", ParamName, re.I):
  802             if not re.match("^(COSMO|PCM)$", Value, re.I):
  803                 MiscUtil.PrintError(
  804                     'The parameter value, %s, specified for parameter name, %s, using "%s" option is not a valid value. Supported values: COSMO or PCM'
  805                     % (Value, Name, ParamsOptionName)
  806                 )
  807             ParamValue = Value
  808         elif re.match("^Solvent$", ParamName, re.I):
  809             if MiscUtil.IsEmpty(Value):
  810                 MiscUtil.PrintError(
  811                     'The parameter value, %s, specified for parameter name, %s, using "%s" option is empty.'
  812                     % (Value, Name, ParamsOptionName)
  813                 )
  814             ParamValue = Value
  815         elif re.match("^SolventEpsilon$", ParamName, re.I):
  816             if re.match("^none$", Value, re.I):
  817                 Value = None
  818             else:
  819                 if not MiscUtil.IsNumber(Value):
  820                     MiscUtil.PrintError(
  821                         'The parameter value, %s, specified for parameter name, %s, using "%s" option must be a float.'
  822                         % (Value, Name, ParamsOptionName)
  823                     )
  824                 Value = float(Value)
  825                 if Value <= 0:
  826                     MiscUtil.PrintError(
  827                         'The parameter value, %s, specified for parameter name, %s, using "%s" option is not a valid value. Supported values: >= 0'
  828                         % (Value, Name, ParamsOptionName)
  829                     )
  830             ParamValue = Value
  831         elif re.match("^RadiiSet$", ParamName, re.I):
  832             if not re.match("^(UFF|Bondi)$", Value, re.I):
  833                 MiscUtil.PrintError(
  834                     'The parameter value, %s, specified for parameter name, %s, using "%s" option is not a valid value. Supported values: UFF or Bondi'
  835                     % (Value, Name, ParamsOptionName)
  836                 )
  837             ParamValue = Value
  838         elif re.match("^RadiiScaling$", ParamName, re.I):
  839             if not re.match("^auto$", Value, re.I):
  840                 if not MiscUtil.IsNumber(Value):
  841                     MiscUtil.PrintError(
  842                         'The parameter value, %s, specified for parameter name, %s, using "%s" option must be a float.'
  843                         % (Value, Name, ParamsOptionName)
  844                     )
  845                 Value = float(Value)
  846                 if Value <= 0:
  847                     MiscUtil.PrintError(
  848                         'The parameter value, %s, specified for parameter name, %s, using "%s" option is not a valid value. Supported values: >= 0'
  849                         % (Value, Name, ParamsOptionName)
  850                     )
  851                 ParamValue = Value
  852         else:
  853             ParamValue = Value
  854 
  855         # Set value...
  856         ParamsInfo[ParamName] = ParamValue
  857 
  858     SolventEpsilon = ParamsInfo["SolventEpsilon"]
  859     if SolventEpsilon is not None and SolventEpsilon > 0.0:
  860         Solvent = ParamsInfo["Solvent"]
  861         if not MiscUtil.IsEmpty(Solvent):
  862             MiscUtil.PrintWarning(
  863                 ' 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...'
  864                 % (ParamsOptionName, Solvent)
  865             )
  866 
  867     # Handle paramaters with possible auto values...
  868     _ProcessPsi4DDXSolvationAutoParameters(ParamsInfo, ParamsOptionName, ParamsOptionValue)
  869     return ParamsInfo
  870 
  871 
  872 def _ProcessPsi4DDXSolvationAutoParameters(ParamsInfo, ParamsOptionName, ParamsOptionValue):
  873     """Process parameters with possible auto values."""
  874 
  875     ParamValue = "%s" % ParamsInfo["RadiiScaling"]
  876     if re.match("^auto$", ParamValue, re.I):
  877         if re.match("^UFF$", ParamsInfo["RadiiSet"], re.I):
  878             ParamValue = 1.1
  879         elif re.match("^Bondi$", ParamsInfo["RadiiSet"], re.I):
  880             ParamValue = 1.2
  881         else:
  882             ParamValue = 0.0
  883     else:
  884         ParamValue = float(ParamValue)
  885     ParamsInfo["RadiiScaling"] = ParamValue
  886 
  887     return
  888 
  889 
  890 def SetupPsi4DDXSolvationOptions(SolvationMode, ParamsInfo):
  891     """Setup Psi4 options for calculating solvation energy using DDX module.
  892 
  893     Arguments:
  894         SolvationMode (bool): Set DDX option for solvation calculation.
  895         ParamsInfo (dict): Psi4 DDX parameter name and value pairs.
  896 
  897     Returns:
  898         dictionary: Psi4 Option name and value pairs.
  899 
  900     """
  901 
  902     # Initialize DDX solvation options...
  903     DDXOptionsInfo = {}
  904     DDXOptionsInfo["DDX"] = True if SolvationMode else False
  905 
  906     # Setup DDX solvation options...
  907     ParamNameToDDXOptionID = {
  908         "SolvationModel": "DDX_MODEL",
  909         "Solvent": "DDX_SOLVENT",
  910         "SolventEpsilon": "DDX_SOLVENT_EPSILON",
  911         "RadiiSet": "DDX_RADII_SET",
  912         "RadiiScaling": "DDX_RADII_SCALING",
  913     }
  914 
  915     for ParamName in ParamNameToDDXOptionID:
  916         DDXOptionID = ParamNameToDDXOptionID[ParamName]
  917         DDXOptionsInfo[DDXOptionID] = ParamsInfo[ParamName]
  918 
  919     # Check for the presence fo both solvent and solvent epsilon parameters...
  920     if DDXOptionsInfo["DDX_SOLVENT_EPSILON"] is None:
  921         DDXOptionsInfo.pop("DDX_SOLVENT_EPSILON", None)
  922     else:
  923         DDXOptionsInfo.pop("DDX_SOLVENT", None)
  924 
  925     return DDXOptionsInfo
  926 
  927 
  928 def ProcessPsi4CubeFilesParameters(ParamsOptionName, ParamsOptionValue, ParamsDefaultInfo=None):
  929     """Process parameters for Psi4 runs and return a map containing processed
  930     parameter names and values.
  931 
  932     ParamsOptionValue is a comma delimited list of parameter name and value pairs
  933     for generating cube files.
  934 
  935     The supported parameter names along with their default and possible
  936     values are shown below:
  937 
  938     GridSpacing, 0.2, GridOverage, 4.0, IsoContourThreshold, 0.85
  939 
  940     GridSpacing: Units: Bohr. A higher value reduces the size of the cube files
  941     on the disk. This option corresponds to Psi4 option CUBIC_GRID_SPACING.
  942 
  943     GridOverage: Units: Bohr.This option corresponds to Psi4 option
  944     CUBIC_GRID_OVERAGE.
  945 
  946     IsoContourThreshold captures specified percent of the probability density
  947     using the least amount of grid points. This option corresponds to Psi4 option
  948     CUBEPROP_ISOCONTOUR_THRESHOLD.
  949 
  950     Arguments:
  951         ParamsOptionName (str): Command line Psi4 cube files option name.
  952         ParamsOptionValues (str): Comma delimited list of parameter name and value pairs.
  953         ParamsDefaultInfo (dict): Default values to override for selected parameters.
  954 
  955     Returns:
  956         dictionary: Processed parameter name and value pairs.
  957 
  958     """
  959 
  960     ParamsInfo = {"GridSpacing": 0.2, "GridOverage": 4.0, "IsoContourThreshold": 0.85}
  961 
  962     # Setup a canonical paramater names...
  963     ValidParamNames = []
  964     CanonicalParamNamesMap = {}
  965     for ParamName in sorted(ParamsInfo):
  966         ValidParamNames.append(ParamName)
  967         CanonicalParamNamesMap[ParamName.lower()] = ParamName
  968 
  969     # Update default values...
  970     if ParamsDefaultInfo is not None:
  971         for ParamName in ParamsDefaultInfo:
  972             if ParamName not in ParamsInfo:
  973                 MiscUtil.PrintError(
  974                     'The default parameter name, %s, specified using "%s" to function ProcessPsi4CubeFilesParameters not a valid name. Supported parameter names: %s'
  975                     % (ParamName, ParamsDefaultInfo, " ".join(ValidParamNames))
  976                 )
  977             ParamsInfo[ParamName] = ParamsDefaultInfo[ParamName]
  978 
  979     if re.match("^auto$", ParamsOptionValue, re.I):
  980         # No specific parameters to process except for parameters with possible auto value...
  981         _ProcessPsi4CubeFilesAutoParameters(ParamsInfo, ParamsOptionName, ParamsOptionValue)
  982         return ParamsInfo
  983 
  984     ParamsOptionValue = ParamsOptionValue.strip()
  985     if not ParamsOptionValue:
  986         MiscUtil.PrintError('No valid parameter name and value pairs specified using "%s" option' % ParamsOptionName)
  987 
  988     ParamsOptionValueWords = ParamsOptionValue.split(",")
  989     if len(ParamsOptionValueWords) % 2:
  990         MiscUtil.PrintError(
  991             'The number of comma delimited paramater names and values, %d, specified using "%s" option must be an even number.'
  992             % (len(ParamsOptionValueWords), ParamsOptionName)
  993         )
  994 
  995     # Validate paramater name and value pairs...
  996     for Index in range(0, len(ParamsOptionValueWords), 2):
  997         Name = ParamsOptionValueWords[Index].strip()
  998         Value = ParamsOptionValueWords[Index + 1].strip()
  999 
 1000         CanonicalName = Name.lower()
 1001         if CanonicalName not in CanonicalParamNamesMap:
 1002             MiscUtil.PrintError(
 1003                 'The parameter name, %s, specified using "%s" is not a valid name. Supported parameter names: %s'
 1004                 % (Name, ParamsOptionName, " ".join(ValidParamNames))
 1005             )
 1006 
 1007         ParamName = CanonicalParamNamesMap[CanonicalName]
 1008         ParamValue = Value
 1009 
 1010         if re.match("^(GridSpacing|GridOverage)$", ParamName, re.I):
 1011             if not MiscUtil.IsFloat(Value):
 1012                 MiscUtil.PrintError(
 1013                     'The parameter value, %s, specified for parameter name, %s, using "%s" option must be a float.'
 1014                     % (Value, Name, ParamsOptionName)
 1015                 )
 1016             Value = float(Value)
 1017             if Value <= 0:
 1018                 MiscUtil.PrintError(
 1019                     'The parameter value, %s, specified for parameter name, %s, using "%s" option is not a valid value. Supported values: > 0'
 1020                     % (Value, Name, ParamsOptionName)
 1021                 )
 1022             ParamValue = Value
 1023         elif re.match("^IsoContourThreshold$", ParamName, re.I):
 1024             if not MiscUtil.IsFloat(Value):
 1025                 MiscUtil.PrintError(
 1026                     'The parameter value, %s, specified for parameter name, %s, using "%s" option must be a float.'
 1027                     % (Value, Name, ParamsOptionName)
 1028                 )
 1029             Value = float(Value)
 1030             if Value <= 0 or Value > 1:
 1031                 MiscUtil.PrintError(
 1032                     'The parameter value, %s, specified for parameter name, %s, using "%s" option is not a valid value. Supported values: >= 0 and <= 1'
 1033                     % (Value, Name, ParamsOptionName)
 1034                 )
 1035             ParamValue = Value
 1036 
 1037         # Set value...
 1038         ParamsInfo[ParamName] = ParamValue
 1039 
 1040     # Handle paramaters with possible auto values...
 1041     _ProcessPsi4CubeFilesAutoParameters(ParamsInfo, ParamsOptionName, ParamsOptionValue)
 1042 
 1043     return ParamsInfo
 1044 
 1045 
 1046 def _ProcessPsi4CubeFilesAutoParameters(ParamsInfo, ParamsOptionName, ParamsOptionValue):
 1047     """Process parameters with possible auto values."""
 1048 
 1049     # No auto parameters to process
 1050     return
 1051 
 1052 
 1053 def RetrieveIsocontourRangeFromCubeFile(CubeFileName):
 1054     """Retrieve isocontour range values from the cube file. The range
 1055     values are retrieved from the second line in the cube file after
 1056     the string 'Isocontour range'.
 1057 
 1058     Arguments:
 1059         CubeFileName (str): Cube file name.
 1060 
 1061     Returns:
 1062         float: Minimum range value.
 1063         float: Maximum range value.
 1064 
 1065     """
 1066 
 1067     IsocontourRangeMin, IsocontourRangeMax = [None] * 2
 1068 
 1069     CubeFH = open(CubeFileName, "r")
 1070     if CubeFH is None:
 1071         MiscUtil.PrintError("Couldn't open cube file: %s.\n" % (CubeFileName))
 1072 
 1073     # Look for isocontour range in the first 2 comments line...
 1074     RangeLine = None
 1075     LineCount = 0
 1076     for Line in CubeFH:
 1077         LineCount += 1
 1078         Line = Line.rstrip()
 1079         if re.search("Isocontour range", Line, re.I):
 1080             RangeLine = Line
 1081             break
 1082 
 1083         if LineCount >= 2:
 1084             break
 1085     CubeFH.close()
 1086 
 1087     if RangeLine is None:
 1088         return (IsocontourRangeMin, IsocontourRangeMax)
 1089 
 1090     LineWords = RangeLine.split(":")
 1091 
 1092     ContourRangeWord = LineWords[-1]
 1093     ContourRangeWord = re.sub(r"(\(|\)| )", "", ContourRangeWord)
 1094 
 1095     ContourLevel1, ContourLevel2 = ContourRangeWord.split(",")
 1096     ContourLevel1 = float(ContourLevel1)
 1097     ContourLevel2 = float(ContourLevel2)
 1098 
 1099     if ContourLevel1 < ContourLevel2:
 1100         IsocontourRangeMin = ContourLevel1
 1101         IsocontourRangeMax = ContourLevel2
 1102     else:
 1103         IsocontourRangeMin = ContourLevel2
 1104         IsocontourRangeMax = ContourLevel1
 1105 
 1106     return (IsocontourRangeMin, IsocontourRangeMax)
 1107 
 1108 
 1109 def RetrieveMinAndMaxValueFromCubeFile(CubeFileName):
 1110     """Retrieve minimum and maxmimum grid values from the cube file.
 1111 
 1112     Arguments:
 1113         CubeFileName (str): Cube file name.
 1114 
 1115     Returns:
 1116         float: Minimum value.
 1117         float: Maximum value.
 1118 
 1119     """
 1120 
 1121     MinValue, MaxValue = [sys.float_info.max, sys.float_info.min]
 1122 
 1123     CubeFH = open(CubeFileName, "r")
 1124     if CubeFH is None:
 1125         MiscUtil.PrintError("Couldn't open cube file: %s.\n" % (CubeFileName))
 1126 
 1127     # Ignore first two comments lines:
 1128     #
 1129     # The first two lines of the header are comments, they are generally ignored by parsing packages or used as two default labels.
 1130     #
 1131     # Ignore lines upto the last section of the header lines:
 1132     #
 1133     # The third line has the number of atoms included in the file followed by the position of the origin of the volumetric data.
 1134     # The next three lines give the number of voxels along each axis (x, y, z) followed by the axis vector.
 1135     # The last section in the header is one line for each atom consisting of 5 numbers, the first is the atom number, the second
 1136     # is the charge, and the last three are the x,y,z coordinates of the atom center.
 1137     #
 1138     Line = CubeFH.readline()
 1139     Line = CubeFH.readline()
 1140     Line = CubeFH.readline()
 1141     CubeFH.close()
 1142 
 1143     Line = Line.strip()
 1144     LineWords = Line.split()
 1145     NumOfAtoms = int(LineWords[0])
 1146 
 1147     HeaderLinesCount = 6 + NumOfAtoms
 1148 
 1149     # Ignore header lines...
 1150     CubeFH = open(CubeFileName, "r")
 1151     LineCount = 0
 1152     for Line in CubeFH:
 1153         LineCount += 1
 1154         if LineCount >= HeaderLinesCount:
 1155             break
 1156 
 1157     # Process values....
 1158     for Line in CubeFH:
 1159         Line = Line.strip()
 1160         for Value in Line.split():
 1161             Value = float(Value)
 1162 
 1163             if Value < MinValue:
 1164                 MinValue = Value
 1165             if Value > MaxValue:
 1166                 MaxValue = Value
 1167 
 1168     return (MinValue, MaxValue)
 1169 
 1170 
 1171 def UpdatePsi4OutputFileUsingPID(OutputFile, PID=None):
 1172     """Append PID to output file name. The PID is automatically retrieved
 1173     during None value of PID.
 1174 
 1175     Arguments:
 1176         OutputFile (str): Output file name.
 1177         PID (int): Process ID or None.
 1178 
 1179     Returns:
 1180         str: Update output file name. Format: <OutFieRoot>_<PID>.<OutFileExt>
 1181 
 1182     """
 1183 
 1184     if re.match("stdout|devnull|quiet", OutputFile, re.I):
 1185         return OutputFile
 1186 
 1187     if PID is None:
 1188         PID = os.getpid()
 1189 
 1190     FileDir, FileRoot, FileExt = MiscUtil.ParseFileName(OutputFile)
 1191     OutputFile = "%s_PID%s.%s" % (FileRoot, PID, FileExt)
 1192 
 1193     return OutputFile
 1194 
 1195 
 1196 def RemoveScratchFiles(psi4, OutputFile, PID=None):
 1197     """Remove any leftover scratch files associated with the specified output
 1198     file. The file specification, <OutfileRoot>.*<PID>.* is used to collect and
 1199     remove files from the scratch directory. In addition, the file
 1200     psi.<PID>.clean, in current directory is removed.
 1201 
 1202     Arguments:
 1203         psi4 (object): psi4 module reference.
 1204         OutputFile (str): Output file name.
 1205         PID (int): Process ID or None.
 1206 
 1207     Returns:
 1208         None
 1209 
 1210     """
 1211 
 1212     if re.match("stdout|devnull|quiet", OutputFile, re.I):
 1213         # Scratch files are associated to stdout prefix...
 1214         OutputFile = "stdout"
 1215 
 1216     if PID is None:
 1217         PID = os.getpid()
 1218 
 1219     OutfileDir, OutfileRoot, OutfileExt = MiscUtil.ParseFileName(OutputFile)
 1220 
 1221     ScratchOutfilesSpec = os.path.join(
 1222         psi4.core.IOManager.shared_object().get_default_path(), "%s.*%s.*" % (OutfileRoot, PID)
 1223     )
 1224     for ScratchFile in glob.glob(ScratchOutfilesSpec):
 1225         os.remove(ScratchFile)
 1226 
 1227     # Remove any psi.<PID>.clean in the current directory...
 1228     ScratchFile = os.path.join(os.getcwd(), "psi.%s.clean" % (PID))
 1229     if os.path.isfile(ScratchFile):
 1230         os.remove(ScratchFile)