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)