
.. include:: autodoc_abbr_options_c.rst

.. _`sec:proc_py`:

Adding Methods to Driver
========================

``proc.py``
-----------

This is concerned at present with normal methods added first to the
procedures table in driver.py that associates method names with functions
to run them located in proc.py .

The function should start with a declaration, as below. ``methodname`` is
never seen by users, so it's good to be specific; if there's lots of
modules that can run mp2, call methodname modulenamemethodname, perhaps.
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
:py:func:`~driver.energy()`, etc. in driver.py and need not be repeated here. ::

    # include if convenient
    lowername = name.lower()

    # never include
    kwargs = kwargs_lower(kwargs)

It's often necessary to 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
:py:class:`~optproc.OptionsState` object is set up. See
:ref:`sec:handlingOptions_py` 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
:py:class:`~optproc.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
:py:func:`~proc.scf_helper` so that cast-up can be used. Also, add
the option to pass the reference wavefunciton 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 :py:class:`~optproc.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()

No function should return anything. ``CURRENT ENERGY`` will be set by
:py:func:`~driver.energy`, etc. ::

    # never include
    return returnvalue


Managed Methods
---------------

When functionality overlaps between modules, a pattern is needed to (1)
access each route through the code without contrivances like ``ccsd2``,
``_ccsd``, ``sdci`` and (2) apportion defaulting among the modules, taking
into account reference (RHF/UHF/ROHF) and calc type (CONV, DF, CD).
Managed methods handle both these cases through the addition of a new
keyword |globals__qc_module| and a set of type keywords analogous to
|globals__mp2_type|: |globals__mp_type|,
|globals__ci_type|, |globals__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__scf_type| and
|globals__mp2_type| defaulting to ``DF``, while everything higher defaults
to ``CONV``. In :source:`share/python/driver.py`, a managed method calls a
"select" function rather than a "run" function. ::

    procedures = {
        'energy': {
            'scf'           : run_scf,
            'mp3'           : select_mp3,
            'dcft'          : run_dcft,

Then in :source:`share/python/proc.py`, the select function runs through
reference (always outer loop) and type (inner loop) 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 :ref:`table:managedmethods` "Y" means method available in
module, "D" means module is default for that method, "" mean method not
available.

