MayaChemTools

    1 #!/bin/env python
    2 #
    3 # File: Psi4CalculateProperties.py
    4 # Author: Manish Sud <msud@san.rr.com>
    5 #
    6 # Copyright (C) 2026 Manish Sud. All rights reserved.
    7 #
    8 # The functionality available in this script is implemented using Psi4, an
    9 # open source quantum chemistry software package, and RDKit, an open
   10 # source toolkit for cheminformatics developed by Greg Landrum.
   11 #
   12 # This file is part of MayaChemTools.
   13 #
   14 # MayaChemTools is free software; you can redistribute it and/or modify it under
   15 # the terms of the GNU Lesser General Public License as published by the Free
   16 # Software Foundation; either version 3 of the License, or (at your option) any
   17 # later version.
   18 #
   19 # MayaChemTools is distributed in the hope that it will be useful, but without
   20 # any warranty; without even the implied warranty of merchantability of fitness
   21 # for a particular purpose.  See the GNU Lesser General Public License for more
   22 # details.
   23 #
   24 # You should have received a copy of the GNU Lesser General Public License
   25 # along with MayaChemTools; if not, see <http://www.gnu.org/licenses/> or
   26 # write to the Free Software Foundation Inc., 59 Temple Place, Suite 330,
   27 # Boston, MA, 02111-1307, USA.
   28 #
   29 
   30 from __future__ import print_function
   31 
   32 import os
   33 import sys
   34 import time
   35 import re
   36 import shutil
   37 import math
   38 import multiprocessing as mp
   39 
   40 # Psi4 imports...
   41 if hasattr(shutil, "which") and shutil.which("psi4") is None:
   42     sys.stderr.write("\nWarning: Failed to find 'psi4' in your PATH indicating potential issues with your\n")
   43     sys.stderr.write("Psi4 environment. The 'import psi4' directive in the global scope of the script\n")
   44     sys.stderr.write("interferes with the multiprocessing functionality. It is imported later in the\n")
   45     sys.stderr.write("local scope during the execution of the script and may fail. Check/update your\n")
   46     sys.stderr.write("Psi4 environment and try again.\n\n")
   47 
   48 # RDKit imports...
   49 try:
   50     from rdkit import rdBase
   51     from rdkit import Chem
   52 except ImportError as ErrMsg:
   53     sys.stderr.write("\nFailed to import RDKit module/package: %s\n" % ErrMsg)
   54     sys.stderr.write("Check/update your RDKit environment and try again.\n\n")
   55     sys.exit(1)
   56 
   57 # MayaChemTools imports...
   58 sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), "..", "lib", "Python"))
   59 try:
   60     from docopt import docopt
   61     import MiscUtil
   62     import Psi4Util
   63     import RDKitUtil
   64 except ImportError as ErrMsg:
   65     sys.stderr.write("\nFailed to import MayaChemTools module/package: %s\n" % ErrMsg)
   66     sys.stderr.write("Check/update your MayaChemTools environment and try again.\n\n")
   67     sys.exit(1)
   68 
   69 ScriptName = os.path.basename(sys.argv[0])
   70 Options = {}
   71 OptionsInfo = {}
   72 
   73 PropertyNamesMap = {}
   74 
   75 
   76 def main():
   77     """Start execution of the script."""
   78 
   79     MiscUtil.PrintInfo(
   80         "\n%s (Psi4: Imported later; RDKit v%s; MayaChemTools v%s; %s): Starting...\n"
   81         % (ScriptName, rdBase.rdkitVersion, MiscUtil.GetMayaChemToolsVersion(), time.asctime())
   82     )
   83 
   84     (WallClockTime, ProcessorTime) = MiscUtil.GetWallClockAndProcessorTime()
   85 
   86     # Retrieve command line arguments and options...
   87     RetrieveOptions()
   88 
   89     # Process and validate command line arguments and options...
   90     ProcessOptions()
   91 
   92     # Perform actions required by the script...
   93     CalculateProperties()
   94 
   95     MiscUtil.PrintInfo("\n%s: Done...\n" % ScriptName)
   96     MiscUtil.PrintInfo("Total time: %s" % MiscUtil.GetFormattedElapsedTime(WallClockTime, ProcessorTime))
   97 
   98 
   99 def CalculateProperties():
  100     """Calculate properties."""
  101 
  102     # Setup a molecule reader...
  103     MiscUtil.PrintInfo("\nProcessing file %s..." % OptionsInfo["Infile"])
  104     Mols = RDKitUtil.ReadMolecules(OptionsInfo["Infile"], **OptionsInfo["InfileParams"])
  105 
  106     # Set up a molecule writer...
  107     Writer = RDKitUtil.MoleculesWriter(OptionsInfo["Outfile"], **OptionsInfo["OutfileParams"])
  108     if Writer is None:
  109         MiscUtil.PrintError("Failed to setup a writer for output fie %s " % OptionsInfo["Outfile"])
  110     MiscUtil.PrintInfo("Generating file %s..." % OptionsInfo["Outfile"])
  111 
  112     MolCount, ValidMolCount, CalcFailedCount = ProcessMolecules(Mols, Writer)
  113 
  114     if Writer is not None:
  115         Writer.close()
  116 
  117     MiscUtil.PrintInfo("\nTotal number of molecules: %d" % MolCount)
  118     MiscUtil.PrintInfo("Number of valid molecules: %d" % ValidMolCount)
  119     MiscUtil.PrintInfo("Number of molecules failed during property calculation: %d" % CalcFailedCount)
  120     MiscUtil.PrintInfo("Number of ignored molecules: %d" % (MolCount - ValidMolCount + CalcFailedCount))
  121 
  122 
  123 def ProcessMolecules(Mols, Writer):
  124     """Process and calculate properties of molecules."""
  125 
  126     if OptionsInfo["MPMode"]:
  127         return ProcessMoleculesUsingMultipleProcesses(Mols, Writer)
  128     else:
  129         return ProcessMoleculesUsingSingleProcess(Mols, Writer)
  130 
  131 
  132 def ProcessMoleculesUsingSingleProcess(Mols, Writer):
  133     """Process and calculate properties of molecules using a single process."""
  134 
  135     # Intialize Psi4...
  136     MiscUtil.PrintInfo("\nInitializing Psi4...")
  137     Psi4Handle = Psi4Util.InitializePsi4(
  138         Psi4RunParams=OptionsInfo["Psi4RunParams"],
  139         Psi4OptionsParams=OptionsInfo["Psi4OptionsParams"],
  140         PrintVersion=True,
  141         PrintHeader=True,
  142     )
  143     OptionsInfo["psi4"] = Psi4Handle
  144 
  145     # Setup conversion factor for energy units...
  146     SetupEnergyConversionFactor(Psi4Handle)
  147 
  148     MiscUtil.PrintInfo("\nCalculating properties...")
  149 
  150     (MolCount, ValidMolCount, CalcFailedCount) = [0] * 3
  151     for Mol in Mols:
  152         MolCount += 1
  153 
  154         if not CheckAndValidateMolecule(Mol, MolCount):
  155             continue
  156 
  157         # Setup a Psi4 molecule...
  158         Psi4Mol = SetupPsi4Mol(Psi4Handle, Mol, MolCount)
  159         if Psi4Mol is None:
  160             continue
  161 
  162         ValidMolCount += 1
  163 
  164         CalcStatus, CalculatedValues = CalculateMolProperties(Psi4Handle, Psi4Mol, Mol, MolCount)
  165 
  166         if not CalcStatus:
  167             if not OptionsInfo["QuietMode"]:
  168                 MiscUtil.PrintWarning(
  169                     "Failed to calculate properties for molecule %s" % RDKitUtil.GetMolName(Mol, MolCount)
  170                 )
  171 
  172             CalcFailedCount += 1
  173             continue
  174 
  175         WritePropertyValues(Writer, Mol, CalculatedValues)
  176 
  177     return (MolCount, ValidMolCount, CalcFailedCount)
  178 
  179 
  180 def ProcessMoleculesUsingMultipleProcesses(Mols, Writer):
  181     """Process and calculate properties of molecules using  process."""
  182 
  183     MiscUtil.PrintInfo("\nCalculating properties using multiprocessing...")
  184 
  185     MPParams = OptionsInfo["MPParams"]
  186 
  187     # Setup data for initializing a worker process...
  188     InitializeWorkerProcessArgs = (
  189         MiscUtil.ObjectToBase64EncodedString(Options),
  190         MiscUtil.ObjectToBase64EncodedString(OptionsInfo),
  191     )
  192 
  193     # Setup a encoded mols data iterable for a worker process...
  194     WorkerProcessDataIterable = RDKitUtil.GenerateBase64EncodedMolStrings(Mols)
  195 
  196     # Setup process pool along with data initialization for each process...
  197     if not OptionsInfo["QuietMode"]:
  198         MiscUtil.PrintInfo(
  199             "\nConfiguring multiprocessing using %s method..."
  200             % ("mp.Pool.imap()" if re.match("^Lazy$", MPParams["InputDataMode"], re.I) else "mp.Pool.map()")
  201         )
  202         MiscUtil.PrintInfo(
  203             "NumProcesses: %s; InputDataMode: %s; ChunkSize: %s\n"
  204             % (
  205                 MPParams["NumProcesses"],
  206                 MPParams["InputDataMode"],
  207                 ("automatic" if MPParams["ChunkSize"] is None else MPParams["ChunkSize"]),
  208             )
  209         )
  210 
  211     ProcessPool = mp.Pool(MPParams["NumProcesses"], InitializeWorkerProcess, InitializeWorkerProcessArgs)
  212 
  213     # Start processing...
  214     if re.match("^Lazy$", MPParams["InputDataMode"], re.I):
  215         Results = ProcessPool.imap(WorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"])
  216     elif re.match("^InMemory$", MPParams["InputDataMode"], re.I):
  217         Results = ProcessPool.map(WorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"])
  218     else:
  219         MiscUtil.PrintError(
  220             'The value, %s, specified for "--inputDataMode" is not supported.' % (MPParams["InputDataMode"])
  221         )
  222 
  223     # Print out Psi4 version in the main process...
  224     MiscUtil.PrintInfo("\nInitializing Psi4...\n")
  225     Psi4Handle = Psi4Util.InitializePsi4(PrintVersion=True, PrintHeader=False)
  226     OptionsInfo["psi4"] = Psi4Handle
  227 
  228     (MolCount, ValidMolCount, CalcFailedCount) = [0] * 3
  229     for Result in Results:
  230         MolCount += 1
  231         MolIndex, EncodedMol, CalcStatus, CalculatedValues = Result
  232 
  233         if EncodedMol is None:
  234             continue
  235 
  236         ValidMolCount += 1
  237 
  238         Mol = RDKitUtil.MolFromBase64EncodedMolString(EncodedMol)
  239 
  240         if not CalcStatus:
  241             if not OptionsInfo["QuietMode"]:
  242                 MolName = RDKitUtil.GetMolName(Mol, MolCount)
  243                 MiscUtil.PrintWarning("Failed to calculate properties for molecule %s" % MolName)
  244 
  245             CalcFailedCount += 1
  246             continue
  247 
  248         WritePropertyValues(Writer, Mol, CalculatedValues)
  249 
  250     return (MolCount, ValidMolCount, CalcFailedCount)
  251 
  252 
  253 def InitializeWorkerProcess(*EncodedArgs):
  254     """Initialize data for a worker process."""
  255 
  256     global Options, OptionsInfo
  257 
  258     if not OptionsInfo["QuietMode"]:
  259         MiscUtil.PrintInfo("Starting process (PID: %s)..." % os.getpid())
  260 
  261     # Decode Options and OptionInfo...
  262     Options = MiscUtil.ObjectFromBase64EncodedString(EncodedArgs[0])
  263     OptionsInfo = MiscUtil.ObjectFromBase64EncodedString(EncodedArgs[1])
  264 
  265     # Psi4 is initialized in the worker process to avoid creation of redundant Psi4
  266     # output files for each process...
  267     OptionsInfo["Psi4Initialized"] = False
  268 
  269     SetupPropertyNamesInfo(PrintInfo=False)
  270 
  271 
  272 def InitializePsi4ForWorkerProcess():
  273     """Initialize Psi4 for a worker process."""
  274 
  275     if OptionsInfo["Psi4Initialized"]:
  276         return
  277 
  278     OptionsInfo["Psi4Initialized"] = True
  279 
  280     # Update output file...
  281     OptionsInfo["Psi4RunParams"]["OutputFile"] = Psi4Util.UpdatePsi4OutputFileUsingPID(
  282         OptionsInfo["Psi4RunParams"]["OutputFile"], os.getpid()
  283     )
  284 
  285     # Intialize Psi4...
  286     OptionsInfo["psi4"] = Psi4Util.InitializePsi4(
  287         Psi4RunParams=OptionsInfo["Psi4RunParams"],
  288         Psi4OptionsParams=OptionsInfo["Psi4OptionsParams"],
  289         PrintVersion=False,
  290         PrintHeader=True,
  291     )
  292 
  293     # Setup conversion factor for energy units...
  294     SetupEnergyConversionFactor(OptionsInfo["psi4"])
  295 
  296 
  297 def WorkerProcess(EncodedMolInfo):
  298     """Process data for a worker process."""
  299 
  300     if not OptionsInfo["Psi4Initialized"]:
  301         InitializePsi4ForWorkerProcess()
  302 
  303     MolIndex, EncodedMol = EncodedMolInfo
  304 
  305     CalcStatus = False
  306     CalculatedValues = None
  307 
  308     if EncodedMol is None:
  309         return [MolIndex, None, CalcStatus, CalculatedValues]
  310 
  311     Mol = RDKitUtil.MolFromBase64EncodedMolString(EncodedMol)
  312     MolCount = MolIndex + 1
  313 
  314     if not CheckAndValidateMolecule(Mol, MolCount):
  315         return [MolIndex, None, CalcStatus, CalculatedValues]
  316 
  317     # Setup a Psi4 molecule...
  318     Psi4Mol = SetupPsi4Mol(OptionsInfo["psi4"], Mol, MolCount)
  319     if Psi4Mol is None:
  320         return [MolIndex, None, CalcStatus, CalculatedValues]
  321 
  322     CalcStatus, CalculatedValues = CalculateMolProperties(OptionsInfo["psi4"], Psi4Mol, Mol, MolCount)
  323 
  324     return [
  325         MolIndex,
  326         RDKitUtil.MolToBase64EncodedMolString(
  327             Mol, PropertyPickleFlags=Chem.PropertyPickleOptions.MolProps | Chem.PropertyPickleOptions.PrivateProps
  328         ),
  329         CalcStatus,
  330         CalculatedValues,
  331     ]
  332 
  333 
  334 def CalculateMolProperties(Psi4Handle, Psi4Mol, Mol, MolNum=None):
  335     """Calculate properties."""
  336 
  337     Status = False
  338     CalculatedValues = []
  339 
  340     #  Setup reference wave function...
  341     Reference = SetupReferenceWavefunction(Mol)
  342     Psi4Handle.set_options({"Reference": Reference})
  343 
  344     # Setup method name and basis set...
  345     MethodName, BasisSet = SetupMethodNameAndBasisSet(Mol)
  346 
  347     # Calculate single point energy to setup a wavefunction...
  348     Status, Energy, WaveFunction = Psi4Util.CalculateSinglePointEnergy(
  349         Psi4Handle, Psi4Mol, MethodName, BasisSet, ReturnWaveFunction=True, Quiet=OptionsInfo["QuietMode"]
  350     )
  351 
  352     if not Status:
  353         PerformPsi4Cleanup(Psi4Handle)
  354         return (False, CalculatedValues)
  355 
  356     # Calculate properties...
  357     CalculatedValues = CalculatePropertyValues(Psi4Handle, WaveFunction, Mol, MolNum)
  358 
  359     # Clean up
  360     PerformPsi4Cleanup(Psi4Handle)
  361 
  362     return (Status, CalculatedValues)
  363 
  364 
  365 def CalculatePropertyValues(Psi4Handle, WaveFunction, Mol, MolNum):
  366     """Calculate property values."""
  367 
  368     return [
  369         PropertyNamesMap["CalcFunction"][Name](Name, Psi4Handle, WaveFunction, Mol, MolNum)
  370         for Name in OptionsInfo["SpecifiedPropertyNames"]
  371     ]
  372 
  373 
  374 def SetupPsi4Mol(Psi4Handle, Mol, MolCount=None):
  375     """Setup a Psi4 molecule object."""
  376 
  377     MolGeometry = RDKitUtil.GetPsi4XYZFormatString(Mol, NoCom=True, NoReorient=True)
  378 
  379     try:
  380         Psi4Mol = Psi4Handle.geometry(MolGeometry)
  381     except Exception as ErrMsg:
  382         Psi4Mol = None
  383         if not OptionsInfo["QuietMode"]:
  384             MiscUtil.PrintWarning("Failed to create Psi4 molecule from geometry string: %s\n" % ErrMsg)
  385             MolName = RDKitUtil.GetMolName(Mol, MolCount)
  386             MiscUtil.PrintWarning("Ignoring molecule: %s" % MolName)
  387 
  388     return Psi4Mol
  389 
  390 
  391 def PerformPsi4Cleanup(Psi4Handle):
  392     """Perform clean up."""
  393 
  394     # Clean up after Psi4 run ...
  395     Psi4Handle.core.clean()
  396 
  397     # Clean up any leftover scratch files...
  398     if OptionsInfo["MPMode"]:
  399         Psi4Util.RemoveScratchFiles(Psi4Handle, OptionsInfo["Psi4RunParams"]["OutputFile"])
  400 
  401 
  402 def CheckAndValidateMolecule(Mol, MolCount=None):
  403     """Validate molecule for Psi4 calculations."""
  404 
  405     if Mol is None:
  406         if not OptionsInfo["QuietMode"]:
  407             MiscUtil.PrintInfo("\nProcessing molecule number %s..." % MolCount)
  408         return False
  409 
  410     MolName = RDKitUtil.GetMolName(Mol, MolCount)
  411     if not OptionsInfo["QuietMode"]:
  412         MiscUtil.PrintInfo("\nProcessing molecule %s..." % MolName)
  413 
  414     if RDKitUtil.IsMolEmpty(Mol):
  415         if not OptionsInfo["QuietMode"]:
  416             MiscUtil.PrintWarning("Ignoring empty molecule: %s\n" % MolName)
  417         return False
  418 
  419     if not RDKitUtil.ValidateElementSymbols(RDKitUtil.GetAtomSymbols(Mol)):
  420         if not OptionsInfo["QuietMode"]:
  421             MiscUtil.PrintWarning("Ignoring molecule containing invalid element symbols: %s\n" % MolName)
  422         return False
  423 
  424     # Check for 3D flag...
  425     if not Mol.GetConformer().Is3D():
  426         if not OptionsInfo["QuietMode"]:
  427             MiscUtil.PrintWarning("3D tag is not set for molecule: %s\n" % MolName)
  428 
  429     # Check for missing hydrogens...
  430     if RDKitUtil.AreHydrogensMissingInMolecule(Mol):
  431         if not OptionsInfo["QuietMode"]:
  432             MiscUtil.PrintWarning("Missing hydrogens in molecule: %s\n" % MolName)
  433 
  434     return True
  435 
  436 
  437 def SetupMethodNameAndBasisSet(Mol):
  438     """Setup method name and basis set."""
  439 
  440     MethodName = OptionsInfo["MethodName"]
  441     if OptionsInfo["MethodNameAuto"]:
  442         MethodName = "B3LYP"
  443 
  444     BasisSet = OptionsInfo["BasisSet"]
  445     if OptionsInfo["BasisSetAuto"]:
  446         BasisSet = "6-31+G**" if RDKitUtil.IsAtomSymbolPresentInMol(Mol, "S") else "6-31G**"
  447 
  448     return (MethodName, BasisSet)
  449 
  450 
  451 def SetupReferenceWavefunction(Mol):
  452     """Setup reference wavefunction."""
  453 
  454     Reference = OptionsInfo["Reference"]
  455     if OptionsInfo["ReferenceAuto"]:
  456         Reference = "UHF" if (RDKitUtil.GetSpinMultiplicity(Mol) > 1) else "RHF"
  457 
  458     return Reference
  459 
  460 
  461 def SetupEnergyConversionFactor(Psi4Handle):
  462     """Setup converstion factor for energt units. The Psi4 energy units are Hartrees."""
  463 
  464     EnergyUnits = OptionsInfo["EnergyUnits"]
  465 
  466     ApplyConversionFactor = True
  467     if re.match(r"^kcal\/mol$", EnergyUnits, re.I):
  468         ConversionFactor = Psi4Handle.constants.hartree2kcalmol
  469     elif re.match(r"^kJ\/mol$", EnergyUnits, re.I):
  470         ConversionFactor = Psi4Handle.constants.hartree2kJmol
  471     elif re.match("^eV$", EnergyUnits, re.I):
  472         ConversionFactor = Psi4Handle.constants.hartree2ev
  473     else:
  474         ApplyConversionFactor = False
  475         ConversionFactor = 1.0
  476 
  477     OptionsInfo["ApplyEnergyConversionFactor"] = ApplyConversionFactor
  478     OptionsInfo["EnergyConversionFactor"] = ConversionFactor
  479 
  480 
  481 def WritePropertyValues(Writer, Mol, CalculatedValues):
  482     """Write out property values for a molecule."""
  483 
  484     if CalculatedValues is None:
  485         return
  486 
  487     for NameIndex, Name in enumerate(OptionsInfo["SpecifiedPropertyNames"]):
  488         PropertyValues = CalculatedValues[NameIndex]
  489         if re.match("^(MayerIndices|WibergLowdinIndices)$", Name, re.I):
  490             # Handle multiple lines values...
  491             Label = PropertyNamesMap["CalcValueLabels"][Name][0]
  492             FormattedValues = [" ".join(Values) for Values in PropertyValues]
  493             Mol.SetProp(Label, "\n".join(FormattedValues))
  494         else:
  495             for LabelIndex, Label in enumerate(PropertyNamesMap["CalcValueLabels"][Name]):
  496                 Mol.SetProp(Label, PropertyValues[LabelIndex])
  497 
  498     Writer.write(Mol)
  499 
  500 
  501 def CalculateDipole(PropertyName, Psi4Handle, WaveFunction, Mol, MolNum):
  502     """Calculate dipole values."""
  503 
  504     Values = ["NA"] * 4
  505 
  506     try:
  507         Title = "OEPropDipole"
  508         Psi4Handle.oeprop(WaveFunction, "DIPOLE", title=Title)
  509 
  510         DipoleX = WaveFunction.variable("%s DIPOLE X" % Title)
  511         DipoleY = WaveFunction.variable("%s DIPOLE Y" % Title)
  512         DipoleZ = WaveFunction.variable("%s DIPOLE Z" % Title)
  513         Dipole = math.sqrt(DipoleX * DipoleX + DipoleY * DipoleY + DipoleZ * DipoleZ)
  514 
  515         Values = [DipoleX, DipoleY, DipoleZ, Dipole]
  516         Values = ["%.*f" % (OptionsInfo["Precision"], Value) for Value in Values]
  517     except Exception as ErrMsg:
  518         Values = ["NA"] * 4
  519         if not OptionsInfo["QuietMode"]:
  520             MiscUtil.PrintWarning(
  521                 "Psi4 failed to calculate %s values for molecule %s:\n%s\n"
  522                 % (PropertyName, RDKitUtil.GetMolName(Mol, MolNum), ErrMsg)
  523             )
  524 
  525     return Values
  526 
  527 
  528 def CalculateQuadrupole(PropertyName, Psi4Handle, WaveFunction, Mol, MolNum):
  529     """Calculate quadrupole values."""
  530 
  531     Values = ["NA"] * 6
  532 
  533     try:
  534         Title = "OEPropQuadrupole"
  535         Psi4Handle.oeprop(WaveFunction, "QUADRUPOLE", title=Title)
  536 
  537         QuadrupoleXX = WaveFunction.variable("%s QUADRUPOLE XX" % Title)
  538         QuadrupoleYY = WaveFunction.variable("%s QUADRUPOLE YY" % Title)
  539         QuadrupoleZZ = WaveFunction.variable("%s QUADRUPOLE ZZ" % Title)
  540         QuadrupoleXY = WaveFunction.variable("%s QUADRUPOLE XY" % Title)
  541         QuadrupoleXZ = WaveFunction.variable("%s QUADRUPOLE XZ" % Title)
  542         QuadrupoleYZ = WaveFunction.variable("%s QUADRUPOLE YZ" % Title)
  543 
  544         Values = [QuadrupoleXX, QuadrupoleYY, QuadrupoleZZ, QuadrupoleXY, QuadrupoleXZ, QuadrupoleYZ]
  545         Values = ["%.*f" % (OptionsInfo["Precision"], Value) for Value in Values]
  546     except Exception as ErrMsg:
  547         Values = ["NA"] * 6
  548         if not OptionsInfo["QuietMode"]:
  549             MiscUtil.PrintWarning(
  550                 "Psi4 failed to calculate %s values for molecule %s:\n%s\n"
  551                 % (PropertyName, RDKitUtil.GetMolName(Mol, MolNum), ErrMsg)
  552             )
  553 
  554     return Values
  555 
  556 
  557 def CalculateLumoHomoEnergyGap(PropertyName, Psi4Handle, WaveFunction, Mol, MolNum):
  558     """Calculate HOMO and LUMO energy values along with the energy gap."""
  559 
  560     Values = ["NA"] * 3
  561 
  562     try:
  563         HOMOEnergy = WaveFunction.epsilon_a_subset("AO", "ALL").np[WaveFunction.nalpha()]
  564         LUMOEnergy = WaveFunction.epsilon_a_subset("AO", "ALL").np[WaveFunction.nalpha() + 1]
  565         EnergyGap = LUMOEnergy - HOMOEnergy
  566 
  567         Values = [HOMOEnergy, LUMOEnergy, EnergyGap]
  568 
  569         if OptionsInfo["ApplyEnergyConversionFactor"]:
  570             Values = [Value * OptionsInfo["EnergyConversionFactor"] for Value in Values]
  571         Values = ["%.*f" % (OptionsInfo["Precision"], Value) for Value in Values]
  572     except Exception as ErrMsg:
  573         Values = ["NA"] * 3
  574         if not OptionsInfo["QuietMode"]:
  575             MiscUtil.PrintWarning(
  576                 "Psi4 failed to calculate %s values for molecule %s:\n%s\n"
  577                 % (PropertyName, RDKitUtil.GetMolName(Mol, MolNum), ErrMsg)
  578             )
  579 
  580     return Values
  581 
  582 
  583 def CalculateMayerIndices(PropertyName, Psi4Handle, WaveFunction, Mol, MolNum):
  584     """Calculate mayer indices."""
  585 
  586     Values = ["NA"]
  587 
  588     try:
  589         Psi4Handle.oeprop(WaveFunction, "MAYER_INDICES")
  590         MayerIndicesList = WaveFunction.array_variable("MAYER_INDICES").np.tolist()
  591 
  592         # MayerIndicesList a list of lists corresponding to n x n matrix corresponding
  593         #  to number of atoms in a molecule...
  594         Values = []
  595         for IndicesRow in MayerIndicesList:
  596             IndicesRowFormatted = ["%.*f" % (OptionsInfo["Precision"], Value) for Value in IndicesRow]
  597             Values.append(IndicesRowFormatted)
  598     except Exception as ErrMsg:
  599         Values = ["NA"]
  600         if not OptionsInfo["QuietMode"]:
  601             MiscUtil.PrintWarning(
  602                 "Psi4 failed to calculate %s values for molecule %s:\n%s\n"
  603                 % (PropertyName, RDKitUtil.GetMolName(Mol, MolNum), ErrMsg)
  604             )
  605 
  606     return Values
  607 
  608 
  609 def CalculateWibergLowdinIndices(PropertyName, Psi4Handle, WaveFunction, Mol, MolNum):
  610     """Calculate mayer indices."""
  611 
  612     Values = ["NA"]
  613 
  614     try:
  615         Psi4Handle.oeprop(WaveFunction, "WIBERG_LOWDIN_INDICES")
  616         WibergLowdinIndices = WaveFunction.array_variable("WIBERG_LOWDIN_INDICES").np.tolist()
  617 
  618         # WibergLowdinIndices a list of lists corresponding to n x n matrix corresponding
  619         #  to number of atoms in a molecule...
  620         Values = []
  621         for IndicesRow in WibergLowdinIndices:
  622             IndicesRowFormatted = ["%.*f" % (OptionsInfo["Precision"], Value) for Value in IndicesRow]
  623             Values.append(IndicesRowFormatted)
  624     except Exception as ErrMsg:
  625         Values = ["NA"]
  626         if not OptionsInfo["QuietMode"]:
  627             MiscUtil.PrintWarning(
  628                 "Psi4 failed to calculate %s values for molecule %s:\n%s\n"
  629                 % (PropertyName, RDKitUtil.GetMolName(Mol, MolNum), ErrMsg)
  630             )
  631 
  632     return Values
  633 
  634 
  635 def ProcessSpecifiedPropertyNames():
  636     """Process specified property names."""
  637 
  638     SetupPropertyNamesInfo()
  639     PropertyNames = PropertyNamesMap["Names"]
  640 
  641     OptionsInfo["SpecifiedPropertyNames"] = []
  642 
  643     SpecifiedNames = re.sub(" ", "", OptionsInfo["Property"])
  644     if not SpecifiedNames:
  645         MiscUtil.PrintError('No valid property names specifed  using " -p, --property" option')
  646 
  647     if re.match("^All$", SpecifiedNames, re.I):
  648         OptionsInfo["SpecifiedPropertyNames"] = PropertyNames
  649         return
  650 
  651     # Validate propery names...
  652     CanonicalPropertyNamesMap = {}
  653     for Name in PropertyNames:
  654         CanonicalPropertyNamesMap[Name.lower()] = Name
  655 
  656     SpecifiedNamesWords = SpecifiedNames.split(",")
  657     for Name in SpecifiedNamesWords:
  658         CanonicalName = Name.lower()
  659         if CanonicalName not in CanonicalPropertyNamesMap:
  660             MiscUtil.PrintError(
  661                 'The property name specified, %s, using "-p, --property" option is not a valid name.' % Name
  662             )
  663 
  664         PropertyName = CanonicalPropertyNamesMap[CanonicalName]
  665         OptionsInfo["SpecifiedPropertyNames"].append(PropertyName)
  666 
  667 
  668 def ProcessListPropertyNames():
  669     """List available property names."""
  670 
  671     SetupPropertyNamesInfo()
  672 
  673     MiscUtil.PrintInfo("\nListing available property names...")
  674     Delimiter = ", "
  675     MiscUtil.PrintInfo("\n%s" % (Delimiter.join(PropertyNamesMap["Names"])))
  676 
  677     MiscUtil.PrintInfo("\nMultiple property values may be calculated for each propery name as shown below:\n")
  678     for PropertyName in PropertyNamesMap["Names"]:
  679         MiscUtil.PrintInfo("%s: %s" % (PropertyName, Delimiter.join(PropertyNamesMap["CalcValueLabels"][PropertyName])))
  680 
  681     MiscUtil.PrintInfo("")
  682 
  683 
  684 def SetupPropertyNamesInfo(PrintInfo=True):
  685     """Setup information for available property names."""
  686 
  687     if PrintInfo:
  688         MiscUtil.PrintInfo("\nRetrieving information for avalible property names...")
  689 
  690     PropertyNamesMap["Names"] = ["Dipole", "Quadrupole", "LumoHomoEnergyGap", "MayerIndices", "WibergLowdinIndices"]
  691 
  692     PropertyNamesMap["CalcValueLabels"] = {}
  693     PropertyNamesMap["CalcFunction"] = {}
  694 
  695     Name = "Dipole"
  696     PropertyNamesMap["CalcValueLabels"][Name] = ["DiopleX", "DiopleY", "DiopleZ", "Diople (Debye)"]
  697     PropertyNamesMap["CalcFunction"][Name] = CalculateDipole
  698 
  699     Name = "Quadrupole"
  700     PropertyNamesMap["CalcValueLabels"][Name] = [
  701         "QuadrupoleXX",
  702         "QuadrupoleYY",
  703         "QuadrupoleZZ",
  704         "QuadrupoleXY",
  705         "QuadrupoleXZ",
  706         "QuadrupoleYZ",
  707     ]
  708     PropertyNamesMap["CalcFunction"][Name] = CalculateQuadrupole
  709 
  710     Name = "LumoHomoEnergyGap"
  711     PropertyNamesMap["CalcValueLabels"][Name] = [
  712         "HOMOEnergy",
  713         "LUMOEnergy",
  714         "LUMO-HOMOGap (%s)" % OptionsInfo["EnergyUnits"],
  715     ]
  716     PropertyNamesMap["CalcFunction"][Name] = CalculateLumoHomoEnergyGap
  717 
  718     Name = "MayerIndices"
  719     PropertyNamesMap["CalcValueLabels"]["MayerIndices"] = ["MayerIndices"]
  720     PropertyNamesMap["CalcFunction"][Name] = CalculateMayerIndices
  721 
  722     Name = "WibergLowdinIndices"
  723     PropertyNamesMap["CalcValueLabels"][Name] = ["WibergLowdinIndices"]
  724     PropertyNamesMap["CalcFunction"][Name] = CalculateWibergLowdinIndices
  725 
  726 
  727 def ProcessOptions():
  728     """Process and validate command line arguments and options."""
  729 
  730     MiscUtil.PrintInfo("Processing options...")
  731 
  732     # Validate options...
  733     ValidateOptions()
  734 
  735     OptionsInfo["Infile"] = Options["--infile"]
  736     ParamsDefaultInfoOverride = {"RemoveHydrogens": False}
  737     OptionsInfo["InfileParams"] = MiscUtil.ProcessOptionInfileParameters(
  738         "--infileParams",
  739         Options["--infileParams"],
  740         InfileName=Options["--infile"],
  741         ParamsDefaultInfo=ParamsDefaultInfoOverride,
  742     )
  743 
  744     OptionsInfo["Outfile"] = Options["--outfile"]
  745     OptionsInfo["OutfileParams"] = MiscUtil.ProcessOptionOutfileParameters(
  746         "--outfileParams", Options["--outfileParams"]
  747     )
  748 
  749     OptionsInfo["Overwrite"] = Options["--overwrite"]
  750 
  751     # Method, basis set, and reference wavefunction...
  752     OptionsInfo["BasisSet"] = Options["--basisSet"]
  753     OptionsInfo["BasisSetAuto"] = True if re.match("^auto$", Options["--basisSet"], re.I) else False
  754 
  755     OptionsInfo["MethodName"] = Options["--methodName"]
  756     OptionsInfo["MethodNameAuto"] = True if re.match("^auto$", Options["--methodName"], re.I) else False
  757 
  758     OptionsInfo["Reference"] = Options["--reference"]
  759     OptionsInfo["ReferenceAuto"] = True if re.match("^auto$", Options["--reference"], re.I) else False
  760 
  761     # Energy units...
  762     OptionsInfo["EnergyUnits"] = Options["--energyUnits"]
  763 
  764     # Run and options parameters...
  765     OptionsInfo["Psi4OptionsParams"] = Psi4Util.ProcessPsi4OptionsParameters(
  766         "--psi4OptionsParams", Options["--psi4OptionsParams"]
  767     )
  768     OptionsInfo["Psi4RunParams"] = Psi4Util.ProcessPsi4RunParameters(
  769         "--psi4RunParams", Options["--psi4RunParams"], InfileName=OptionsInfo["Infile"]
  770     )
  771 
  772     OptionsInfo["MPMode"] = True if re.match("^yes$", Options["--mp"], re.I) else False
  773     OptionsInfo["MPParams"] = MiscUtil.ProcessOptionMultiprocessingParameters("--mpParams", Options["--mpParams"])
  774 
  775     OptionsInfo["Precision"] = int(Options["--precision"])
  776     OptionsInfo["QuietMode"] = True if re.match("^yes$", Options["--quiet"], re.I) else False
  777 
  778     OptionsInfo["Property"] = Options["--property"]
  779     ProcessSpecifiedPropertyNames()
  780 
  781 
  782 def RetrieveOptions():
  783     """Retrieve command line arguments and options."""
  784 
  785     # Get options...
  786     global Options
  787     Options = docopt(_docoptUsage_)
  788 
  789     # Set current working directory to the specified directory...
  790     WorkingDir = Options["--workingdir"]
  791     if WorkingDir:
  792         os.chdir(WorkingDir)
  793 
  794     # Handle examples option...
  795     if "--examples" in Options and Options["--examples"]:
  796         MiscUtil.PrintInfo(MiscUtil.GetExamplesTextFromDocOptText(_docoptUsage_))
  797         sys.exit(0)
  798 
  799     # Handle listing of propery names...
  800     if Options["--list"]:
  801         ProcessListPropertyNames()
  802         sys.exit(0)
  803 
  804 
  805 def ValidateOptions():
  806     """Validate option values."""
  807 
  808     MiscUtil.ValidateOptionTextValue("--energyUnits", Options["--energyUnits"], "Hartrees kcal/mol kJ/mol eV")
  809 
  810     MiscUtil.ValidateOptionFilePath("-i, --infile", Options["--infile"])
  811     MiscUtil.ValidateOptionFileExt("-i, --infile", Options["--infile"], "sdf sd mol")
  812 
  813     MiscUtil.ValidateOptionFileExt("-o, --outfile", Options["--outfile"], "sdf sd")
  814     MiscUtil.ValidateOptionsOutputFileOverwrite(
  815         "-o, --outfile", Options["--outfile"], "--overwrite", Options["--overwrite"]
  816     )
  817     MiscUtil.ValidateOptionsDistinctFileNames(
  818         "-i, --infile", Options["--infile"], "-o, --outfile", Options["--outfile"]
  819     )
  820 
  821     MiscUtil.ValidateOptionTextValue("--mp", Options["--mp"], "yes no")
  822     MiscUtil.ValidateOptionIntegerValue("-p, --precision", Options["--precision"], {">": 0})
  823 
  824     MiscUtil.ValidateOptionTextValue("-q, --quiet", Options["--quiet"], "yes no")
  825 
  826 
  827 # Setup a usage string for docopt...
  828 _docoptUsage_ = """
  829 Psi4CalculateProperties.py - Calculate properties
  830 
  831 Usage:
  832     Psi4CalculateProperties.py [--basisSet <text>] [--energyUnits <text>] [--infileParams <Name,Value,...>]
  833                              [--methodName <text>] [--mp <yes or no>] [--mpParams <Name, Value,...>] 
  834                              [--outfileParams <Name,Value,...> ] [--overwrite] [--property <All or Name1,Name2,Name3,...>]
  835                              [--precision <number> ] [--psi4OptionsParams <Name,Value,...>] [--psi4RunParams <Name,Value,...>]
  836                              [--quiet <yes or no>] [--reference <text>] [-w <dir>] -i <infile> -o <outfile> 
  837     Psi4CalculateProperties.py -l | --list
  838     Psi4CalculateProperties.py -h | --help | -e | --examples
  839 
  840 Description:
  841     Calculate properties for molecules using a specified method name and basis
  842     set. The molecules must have 3D coordinates in input file. The molecular
  843     geometry is not optimized before the calculation. In addition, hydrogens must
  844     be present for all molecules in input file. A single point energy calculation is 
  845     performed before calculating properties. The 3D coordinates are not modified
  846     during the calculation.
  847 
  848     A Psi4 XYZ format geometry string is automatically generated for each molecule
  849     in input file. It contains atom symbols and 3D coordinates for each atom in a
  850     molecule. In addition, the formal charge and spin multiplicity are present in the
  851     the geometry string. These values are either retrieved from molecule properties
  852     named 'FormalCharge' and 'SpinMultiplicty' or dynamically calculated for a
  853     molecule.
  854 
  855     The supported input file formats are: Mol (.mol), SD (.sdf, .sd)
  856 
  857     The supported output file formats are: SD (.sdf, .sd)
  858 
  859 Options:
  860     -b, --basisSet <text>  [default: auto]
  861         Basis set to use for calculating single point energy before calculating
  862         properties. Default: 6-31+G** for sulfur containing molecules; Otherwise,
  863         6-31G** [ Ref 150 ]. The specified value must be a valid Psi4 basis set.
  864         No validation is performed.
  865         
  866         The following list shows a representative sample of basis sets available
  867         in Psi4:
  868             
  869             STO-3G, 6-31G, 6-31+G, 6-31++G, 6-31G*, 6-31+G*,  6-31++G*, 
  870             6-31G**, 6-31+G**, 6-31++G**, 6-311G, 6-311+G, 6-311++G,
  871             6-311G*, 6-311+G*, 6-311++G*, 6-311G**, 6-311+G**, 6-311++G**,
  872             cc-pVDZ, cc-pCVDZ, aug-cc-pVDZ, cc-pVDZ-DK, cc-pCVDZ-DK, def2-SVP,
  873             def2-SVPD, def2-TZVP, def2-TZVPD, def2-TZVPP, def2-TZVPPD
  874             
  875     --energyUnits <text>  [default: kcal/mol]
  876         Energy units for writing out LUMO and HOMO energies and their energy gap.
  877         Possible values: Hartrees, kcal/mol, kJ/mol, or eV.
  878     -e, --examples
  879         Print examples.
  880     -h, --help
  881         Print this help message.
  882     -i, --infile <infile>
  883         Input file name.
  884     --infileParams <Name,Value,...>  [default: auto]
  885         A comma delimited list of parameter name and value pairs for reading
  886         molecules from files. The supported parameter names for different file
  887         formats, along with their default values, are shown below:
  888             
  889             SD, MOL: removeHydrogens,no,sanitize,yes,strictParsing,yes
  890             
  891     -l, --list
  892         List property names without performing any calculations.
  893     -m, --methodName <text>  [default: auto]
  894         Method to use for calculating single point energy before calculating
  895         properties. Default: B3LYP [ Ref 150 ]. The specified value must be a valid
  896         Psi4 method name. No validation is performed.
  897         
  898         The following list shows a representative sample of methods available
  899         in Psi4:
  900             
  901             B1LYP, B2PLYP, B2PLYP-D3BJ, B2PLYP-D3MBJ, B3LYP, B3LYP-D3BJ,
  902             B3LYP-D3MBJ, CAM-B3LYP, CAM-B3LYP-D3BJ, HF, HF-D3BJ,  HF3c, M05,
  903             M06, M06-2x, M06-HF, M06-L, MN12-L, MN15, MN15-D3BJ,PBE, PBE0,
  904             PBEH3c, PW6B95, PW6B95-D3BJ, WB97, WB97X, WB97X-D, WB97X-D3BJ
  905             
  906     --mp <yes or no>  [default: no]
  907         Use multiprocessing.
  908          
  909         By default, input data is retrieved in a lazy manner via mp.Pool.imap()
  910         function employing lazy RDKit data iterable. This allows processing of
  911         arbitrary large data sets without any additional requirements memory.
  912         
  913         All input data may be optionally loaded into memory by mp.Pool.map()
  914         before starting worker processes in a process pool by setting the value
  915         of 'inputDataMode' to 'InMemory' in '--mpParams' option.
  916         
  917         A word to the wise: The default 'chunkSize' value of 1 during 'Lazy' input
  918         data mode may adversely impact the performance. The '--mpParams' section
  919         provides additional information to tune the value of 'chunkSize'.
  920     --mpParams <Name,Value,...>  [default: auto]
  921         A comma delimited list of parameter name and value pairs to configure
  922         multiprocessing.
  923         
  924         The supported parameter names along with their default and possible
  925         values are shown below:
  926         
  927             chunkSize, auto
  928             inputDataMode, Lazy   [ Possible values: InMemory or Lazy ]
  929             numProcesses, auto   [ Default: mp.cpu_count() ]
  930         
  931         These parameters are used by the following functions to configure and
  932         control the behavior of multiprocessing: mp.Pool(), mp.Pool.map(), and
  933         mp.Pool.imap().
  934         
  935         The chunkSize determines chunks of input data passed to each worker
  936         process in a process pool by mp.Pool.map() and mp.Pool.imap() functions.
  937         The default value of chunkSize is dependent on the value of 'inputDataMode'.
  938         
  939         The mp.Pool.map() function, invoked during 'InMemory' input data mode,
  940         automatically converts RDKit data iterable into a list, loads all data into
  941         memory, and calculates the default chunkSize using the following method
  942         as shown in its code:
  943         
  944             chunkSize, extra = divmod(len(dataIterable), len(numProcesses) * 4)
  945             if extra: chunkSize += 1
  946         
  947         For example, the default chunkSize will be 7 for a pool of 4 worker processes
  948         and 100 data items.
  949         
  950         The mp.Pool.imap() function, invoked during 'Lazy' input data mode, employs
  951         'lazy' RDKit data iterable to retrieve data as needed, without loading all the
  952         data into memory. Consequently, the size of input data is not known a priori.
  953         It's not possible to estimate an optimal value for the chunkSize. The default 
  954         chunkSize is set to 1.
  955         
  956         The default value for the chunkSize during 'Lazy' data mode may adversely
  957         impact the performance due to the overhead associated with exchanging
  958         small chunks of data. It is generally a good idea to explicitly set chunkSize to
  959         a larger value during 'Lazy' input data mode, based on the size of your input
  960         data and number of processes in the process pool.
  961         
  962         The mp.Pool.map() function waits for all worker processes to process all
  963         the data and return the results. The mp.Pool.imap() function, however,
  964         returns the the results obtained from worker processes as soon as the
  965         results become available for specified chunks of data.
  966         
  967         The order of data in the results returned by both mp.Pool.map() and 
  968         mp.Pool.imap() functions always corresponds to the input data.
  969     -o, --outfile <outfile>
  970         Output file name.
  971     --outfileParams <Name,Value,...>  [default: auto]
  972         A comma delimited list of parameter name and value pairs for writing
  973         molecules to files. The supported parameter names for different file
  974         formats, along with their default values, are shown below:
  975             
  976             SD: kekulize,yes,forceV3000,no
  977             
  978     --overwrite
  979         Overwrite existing files.
  980     -p, --property <All or Name1,Name2,Name3,...>  [default: Dipole]
  981         Comma delimited lists of properties properties to calculate. Default:
  982         'Dipole'. The following properties may be calculated for molecules:
  983             
  984             Dipole,Quadrupole,LumoHomoEnergyGap,MayerIndices,
  985             WibergLowdinIndices
  986             
  987         Multiple property values are calculated for a single property name as
  988         shown below:
  989             
  990             Dipole: DipoleX, DipoleY, DipoleZ,Dipole
  991             Quadrupole: QuadrupoleXX, QuadrupoleYY, QuadrupoleZZ,
  992                 QuadrupoleXY, QuadrupoleXZ, QuadrupoleYZ
  993             LumoHomoEnergyGap: HOMOEnergy, LUMOEnergy, LUMO-HOMOGap
  994             
  995     --precision <number>  [default: 4]
  996         Floating point precision for writing values of calculated properties.
  997     --psi4OptionsParams <Name,Value,...>  [default: none]
  998         A comma delimited list of Psi4 option name and value pairs for setting
  999         global and module options. The names are 'option_name' for global options
 1000         and 'module_name__option_name' for options local to a module. The
 1001         specified option names must be valid Psi4 names. No validation is
 1002         performed.
 1003         
 1004         The specified option name and  value pairs are processed and passed to
 1005         psi4.set_options() as a dictionary. The supported value types are float,
 1006         integer, boolean, or string. The float value string is converted into a float.
 1007         The valid values for a boolean string are yes, no, true, false, on, or off. 
 1008     --psi4RunParams <Name,Value,...>  [default: auto]
 1009         A comma delimited list of parameter name and value pairs for configuring
 1010         Psi4 jobs.
 1011         
 1012         The supported parameter names along with their default and possible
 1013         values are shown below:
 1014              
 1015             MemoryInGB, 1
 1016             NumThreads, 1
 1017             OutputFile, auto   [ Possible  values: stdout, quiet, or FileName ]
 1018             ScratchDir, auto   [ Possivle values: DirName]
 1019             RemoveOutputFile, yes   [ Possible values: yes, no, true, or false]
 1020             
 1021         These parameters control the runtime behavior of Psi4.
 1022         
 1023         The default file name for 'OutputFile' is <InFileRoot>_Psi4.out. The PID
 1024         is appended to output file name during multiprocessing as shown:
 1025         <InFileRoot>_Psi4_<PIDNum>.out. The 'stdout' value for 'OutputType'
 1026         sends Psi4 output to stdout. The 'quiet' or 'devnull' value suppresses
 1027         all Psi4 output.
 1028         
 1029         The default 'Yes' value of 'RemoveOutputFile' option forces the removal
 1030         of any existing Psi4 before creating new files to append output from
 1031         multiple Psi4 runs.
 1032         
 1033         The option 'ScratchDir' is a directory path to the location of scratch
 1034         files. The default value corresponds to Psi4 default. It may be used to
 1035         override the deafult path.
 1036     -q, --quiet <yes or no>  [default: no]
 1037         Use quiet mode. The warning and information messages will not be printed.
 1038     -r, --reference <text>  [default: auto]
 1039         Reference wave function to use for energy calculation. Default: RHF or UHF.
 1040         The default values are Restricted Hartree-Fock (RHF) for closed-shell molecules
 1041         with all electrons paired and Unrestricted Hartree-Fock (UHF) for open-shell
 1042         molecules with unpaired electrons.
 1043         
 1044         The specified value must be a valid Psi4 reference wave function. No validation
 1045         is performed. For example: ROHF, CUHF, RKS, etc.
 1046         
 1047         The spin multiplicity determines the default value of reference wave function
 1048         for input molecules. It is calculated from number of free radical electrons using
 1049         Hund's rule of maximum multiplicity defined as 2S + 1 where S is the total
 1050         electron spin. The total spin is 1/2 the number of free radical electrons in a 
 1051         molecule. The value of 'SpinMultiplicity' molecule property takes precedence
 1052         over the calculated value of spin multiplicity.
 1053     -w, --workingdir <dir>
 1054         Location of working directory which defaults to the current directory.
 1055 
 1056 Examples:
 1057     To calculate dipole based on a single point energy calculation using  B3LYP/6-31G**
 1058     and B3LYP/6-31+G** for non-sulfur and sulfur containing molecules in a SD file
 1059     with 3D structures, use RHF and UHF for closed-shell and open-shell molecules,
 1060     and write a new SD file, type:
 1061 
 1062         % Psi4CalculateProperties.py -i Psi4Sample3D.sdf -o Psi4Sample3DOut.sdf
 1063 
 1064     To run the first example for calculating all available properties and write out a 
 1065     SD file, type:
 1066 
 1067         % Psi4CalculateProperties.py -p All -i Psi4Sample3D.sdf
 1068           -o Psi4Sample3DOut.sdf
 1069 
 1070     To run the first example in multiprocessing mode on all available CPUs
 1071     without loading all data into memory and write out a SD file, type:
 1072 
 1073         % Psi4CalculateProperties.py --mp yes -i Psi4Sample3D.sdf
 1074           -o Psi4Sample3DOut.sdf
 1075 
 1076     To run the first example in multiprocessing mode on all available CPUs
 1077     by loading all data into memory and write out a SD file, type:
 1078 
 1079         % Psi4CalculateProperties.py  --mp yes --mpParams "inputDataMode,
 1080           InMemory" -i Psi4Sample3D.sdf  -o Psi4Sample3DOut.sdf
 1081 
 1082     To run the first example in multiprocessing mode on all available CPUs
 1083     without loading all data into memory along with multiple threads for each
 1084     Psi4 run and write out a SD file, type:
 1085 
 1086         % Psi4CalculateProperties.py --mp yes --psi4RunParams "NumThreads,2"
 1087            -i Psi4Sample3D.sdf -o Psi4Sample3DOut.sdf
 1088 
 1089     To calculate a specific set of properties based on a single point energy using
 1090     a specific method and basis set for molecules in a SD containing 3D structures
 1091     and write a new SD file, type:
 1092 
 1093         % Psi4CalculateProperties.py  -p "Dipole,LumoHomoEnergyGap" -m SCF
 1094           -b aug-cc-pVDZ -i Psi4Sample3D.sdf -o Psi4Sample3DOut.sdf
 1095 
 1096 Author:
 1097     Manish Sud(msud@san.rr.com)
 1098 
 1099 See also:
 1100     Psi4CalculatePartialCharges.py, Psi4PerformMinimization.py, Psi4GenerateConformers.py
 1101 
 1102 Copyright:
 1103     Copyright (C) 2026 Manish Sud. All rights reserved.
 1104 
 1105     The functionality available in this script is implemented using Psi4, an
 1106     open source quantum chemistry software package, and RDKit, an open
 1107     source toolkit for cheminformatics developed by Greg Landrum.
 1108 
 1109     This file is part of MayaChemTools.
 1110 
 1111     MayaChemTools is free software; you can redistribute it and/or modify it under
 1112     the terms of the GNU Lesser General Public License as published by the Free
 1113     Software Foundation; either version 3 of the License, or (at your option) any
 1114     later version.
 1115 
 1116 """
 1117 
 1118 if __name__ == "__main__":
 1119     main()