Adding Methods to Driver¶
proc.py
¶
Methods that are computable by only one module should be added to the procedures
dictionary in
psi4/psi4/driver/procrouting/proc_table.py
that associates method names with functions
to run them located in psi4/psi4/driver/procrouting/proc.py.
The function should start with a declaration, as below. methodname
is
never seen by users, so it’s good to be specific to method or module.
The function must always take as arguments (name, **kwargs)
.
# energy method
def run_methodname(name, **kwargs):
# gradient method
def run_methodname_gradient(name, **kwargs):
If the function needs to test the identity of name
several times, it
can be convenient to predefine the lowercase version of the variable. The
case of all other py-side options (in kwargs) has already been handled by
energy()
, etc. in driver.py and need not be repeated here.
# include if convenient
lowername = name.lower()
# never include
kwargs = kwargs_lower(kwargs)
The function often needs to set options for the
c-side modules it calls. In order that the state of the options set by the
user remains when control is returned to the user, an
OptionsState
object is set up. See
LibOptions: globals, locals, has_changed and all that for details. All options set by the
function need to be included here, and only options set by the function
should be included. Most options should be associated with a particular
module, but a few (see below) are given without module.
# include if any options set
optstash = OptionsState(
# these and other basis options should have no associated module
['BASIS'],
['DF_BASIS_SCF'],
['DF_BASIS_MP2'],
['PUREAM'],
['FREEZE_CORE'],
# all others should have an associated module
['SCF', 'SCF_TYPE'],
['SCF', 'GUESS'],
['DFMP2', 'MP2_OS_SCALE'],
)
If options need to be set, set them anywhere here. Options should be set
locally to a module, except for those without a module in
OptionsState
.
# include if necessary as globals
psi4.set_global_option('BASIS', guessbasis)
psi4.set_global_option('DF_BASIS_SCF', guessbasisdf)
# include if necessary as locals
psi4.set_local_option('TRANSQT2', 'WFN', 'MP2')
psi4.set_local_option('CCSORT', 'WFN', 'MP2')
psi4.set_local_option('MP2', 'WFN', 'MP2')
If the regular scf module is to be run, run it through
psi4.driver.procrouting.proc.scf_helper
so that cast-up can be used. Also, add
the option to pass the reference wavefunction by pre-running scf,
then running the module with the ref_wfn
kwarg. Also, if the full
two-electron integrals are necessary for the post-scf, compute them if
only the df integrals were run previously.
# Bypass the scf call if a reference wavefunction is given
ref_wfn = kwargs.get('ref_wfn', None)
if ref_wfn is None:
ref_wfn = scf_helper(name, **kwargs)
# If the scf type is DF/CD, then the AO integrals were never written to disk
if psi4.get_option('SCF', 'SCF_TYPE') in ['DF', 'CD']:
psi4.MintsHelper(ref_wfn.basisset()).integrals()
Direct any post-scf modules to be run.
# include if further post-scf modules are needed
psi4.transqt2()
psi4.ccsort()
psi4.mp2()
If an OptionsState
object was set up, those options
need to be returned to the original user state with the following.
# include if optstash = OptionsState(...) was set up previously
optstash.restore()
Current best practice is to store as much as possible on the wavefunction, not in globals. The driver should handle interactions with globals. When QCVariables are stored on the wavefunction in the module, copy to globals with the below:
# Shove variables into global space
for k, v in dfmp2_wfn.variables().items():
core.set_variable(k, v)
The function should return the wavefunction, except for rare cases like EFP where no wavefunction available.
For now, CURRENT ENERGY
will be set by
energy()
, etc. In future, this will be extracted from the wavefunction.
# return highest or most prominent wavefunction (like dimer for SAPT)
return fnocc_wfn
Managed Methods¶
There are several conditions when a method and derivative combination should be managed:
when functionality overlaps between modules, a pattern is needed to access each route through the code;
when functionality doesn’t overlap completely, a pattern is needed to apportion defaulting among the modules, taking into account reference (RHF/UHF/ROHF), calc type (CONV/DF/CD), and possibly FREEZE_CORE state (AE/FC).
for higher-level derivatives, when, say, gradient functionality for mtd+ref+type+fcae doesn’t exactly match energy functionality, a pattern is needed to decide analytic vs. finite difference.
when default type is not available for a method (e.g., CCD governed by CC_TYPE that defaults to
CONV
but onlyDF
andCD
CCD is available), an informative error message is needed.
Managed methods handle these cases through the addition of a new
keyword QC_MODULE and a set of type keywords analogous to
MP2_TYPE: MP_TYPE,
CI_TYPE, CC_TYPE, which can have values CONV
,
DF
, and CD
. These are all global keywords, as their values are
shared among modules rather than (or in addition to) being used internally
by the module). We’re sticking with SCF_TYPE and
MP2_TYPE defaulting to DF
, while most everything higher defaults
to CONV
. (Exceptions are MP2.5 and MP3 that default to DF
.)
In psi4/psi4/driver/procrouting/proc_table.py, a managed method calls a
“select” function rather than a “run” function.
procedures = {
'energy': {
'scf' : run_scf,
'mp3' : select_mp3,
'dct' : run_dct,
Then in psi4/psi4/driver/procrouting/proc.py, the select function runs through
reference, type, and possibly freeze_core to specify the proc
function to call for any able, non-default module (e.g., mtd_type ==
'DETCI'
) or able, default module (e.g., mtd_typd == ['', 'FNOCC']
).
Don’t worry about ‘else’ statements as anything that falls through will be
caught and a readable error generated.
def select_mp3(name, **kwargs):
"""Function selecting the algorithm for a MP3 energy call
and directing to specified or best-performance default modules.
"""
reference = psi4.get_option('SCF', 'REFERENCE')
mtd_type = psi4.get_global_option('MP_TYPE')
module = psi4.get_global_option('QC_MODULE')
# Considering only [df]occ/fnocc/detci
func = None
if reference == 'RHF':
if mtd_type == 'CONV':
if module == 'DETCI':
func = run_detci
elif module == 'FNOCC':
func = run_fnocc
elif module in ['', 'OCC']:
func = run_occ
elif mtd_type == 'DF':
if module in ['', 'OCC']:
func = run_dfocc
elif mtd_type == 'CD':
if module in ['', 'OCC']:
func = run_dfocc
elif reference == 'UHF':
if mtd_type == 'CONV':
if module in ['', 'OCC']:
func = run_occ
elif mtd_type == 'DF':
if module in ['', 'OCC']:
func = run_dfocc
elif mtd_type == 'CD':
if module in ['', 'OCC']:
func = run_dfocc
elif reference == 'ROHF':
if mtd_type == 'CONV':
if module in ['DETCI']:
func = run_detci
if func is None:
raise ManagedMethodError(['select_mp3', name, 'MP_TYPE', mtd_type, reference, module])
return func(name, **kwargs)
Naturally, in the run function, you must either use the type keyword for
type switching or translate it into whatever DO_CD
-like keywords your
module uses. At run time with a closed-shell molecule,
energy('mp3')
will run OCC, while
set qc_module fnocc
energy('mp3')
will run FNOCC mp3.
A special case is DETCI that can run mp3, but oughtn’t to be used for such. So above, ROHF CONV mp3 has no default, but can still access the detci code with
set reference rohf
set qc_module detci
energy('mp3')
While the below gives an error
set reference rohf
energy('mp3')
Again, whenever a single method name needs to call multiple proc.py run functions, it should be managed. In Capabilities of , including details of overlapping modules. “✓” runs analytically. “∷” runs derivative with internal finite difference. Single underline “✓̲” or “∷̲” is default module when QC_MODULE unspecified. Double underline “✓̳” or “∷̳” is default algorithm type when type selector (e.g., CC_TYPE) unspecified. “Y” means method available in module, “D” means module is default for that method, “” mean method not available.