Source code for psi4.driver.procrouting.empirical_dispersion

#
# @BEGIN LICENSE
#
# Psi4: an open-source quantum chemistry software package
#
# Copyright (c) 2007-2019 The Psi4 Developers.
#
# The copyrights for code used from other parties are included in
# the corresponding files.
#
# This file is part of Psi4.
#
# Psi4 is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, version 3.
#
# Psi4 is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with Psi4; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# @END LICENSE
#

import collections

import numpy as np

from psi4 import core
from psi4.driver import p4util
from psi4.driver import driver_findif
from psi4.driver.p4util.exceptions import ValidationError
from psi4.driver.qcdb import intf_dftd3
from psi4.driver.qcdb import interface_gcp as gcp

_engine_can_do = collections.OrderedDict([('libdisp', ['d1', 'd2', 'chg', 'das2009', 'das2010']),
                                          ('dftd3', ['d2', 'd3zero', 'd3bj', 'd3mzero', 'd3mbj']),
                                          ('nl', ['nl']),
                                        ]) # yapf: disable

_capable_engines_for_disp = collections.defaultdict(list)
for eng, disps in _engine_can_do.items():
    for disp in disps:
        _capable_engines_for_disp[disp].append(eng)


[docs]class EmpiricalDispersion(object): """Lightweight unification of empirical dispersion calculation modes. Attributes ---------- dashlevel: {'d1', 'd2', 'd3zero', 'd3bj', 'd3mzero', 'd3mbj', 'chg', 'das2009', 'das2010', 'nl'} Name of dispersion correction to be applied. Resolved from `name_hint` and/or `level_hint` into a key of `dashparam.dashcoeff`. dashparams : dict Complete (number and parameter names vary by `dashlevel`) set of parameter values defining the flexible parts of `dashlevel`. Resolved into a complete set (keys of dashcoeff[dashlevel]['default']) from `name_hint` and/or `dashcoeff_supplement` and/or user `param_tweaks`. fctldash : str If `dashparams` for `dashlevel` corresponds to a defined, named, untweaked "functional-dashlevel" set, then that functional. Otherwise, empty string. description : str Tagline for dispersion `dashlevel`. dashlevel_citation : str Literature reference for dispersion `dashlevel` in general, *not necessarily* for `dashparams`. dashparams_citation : str Literature reference for dispersion parameters, if `dashparams` corresponds to a defined, named, untweaked "functional-dashlevel" set with a citation. Otherwise, empty string. dashcoeff_supplement : dict See description in `qcdb.intf_dftd3.dashparam.from_arrays`. Used here to "bless" the dispersion definitions attached to the procedures/dft/*_functionals-defined dictionaries as legit, non-custom, and of equal validity to `qcdb.intf_dftd3.dashparam.dashcoeff` itself for purposes of validating `fctldash`. engine : {'libdisp', 'dftd3', 'nl'} Compute engine for dispersion. One of Psi4's internal libdisp library, Grimme's DFTD3 executable, or nl. disp : psi4.core.Dispersion Only present for `engine=libdisp`. Psi4 class instance prepared to compute dispersion. ordered_params : list Fixed-order list of relevant parameters for `dashlevel`. Matches DFT_DISPERSION_PARAMETERS ordering. Used for printing. Parameters ---------- name_hint : str, optional Name of functional (func only, func & disp, or disp only) for which to compute dispersion (e.g., blyp, BLYP-D2, blyp-d3bj, blyp-d3(bj), hf+d). Any or all parameters initialized from `dashcoeff[dashlevel][functional-without-dashlevel]` or `dashcoeff_supplement[dashlevel][functional-with-dashlevel] can be overwritten via `param_tweaks`. level_hint : str, optional Name of dispersion correction to be applied (e.g., d, D2, d3(bj), das2010). Must be key in `dashcoeff` or "alias" or "formal" to one. param_tweaks : list or dict, optional Values for the same keys as `dashcoeff[dashlevel]['default']` (and same order if list) used to override any or all values initialized by `name_hint`. Extra parameters will error. engine : str, optional Override which code computes dispersion. See above for allowed values. Really only relevant for -D2, which can be computed by libdisp or dftd3. """ def __init__(self, name_hint=None, level_hint=None, param_tweaks=None, **kwargs): from .dft import dashcoeff_supplement self.dashcoeff_supplement = dashcoeff_supplement resolved = intf_dftd3.from_arrays( name_hint=name_hint, level_hint=level_hint, param_tweaks=param_tweaks, dashcoeff_supplement=self.dashcoeff_supplement) self.fctldash = resolved['fctldash'] self.dashlevel = resolved['dashlevel'] self.dashparams = resolved['dashparams'] self.description = intf_dftd3.dashcoeff[self.dashlevel]['description'] self.ordered_params = intf_dftd3.dashcoeff[self.dashlevel]['default'].keys() self.dashlevel_citation = intf_dftd3.dashcoeff[self.dashlevel]['citation'] self.dashparams_citation = resolved['dashparams_citation'] engine = kwargs.pop('engine', None) if engine is None: self.engine = _capable_engines_for_disp[self.dashlevel][0] else: if self.dashlevel in _engine_can_do[engine]: self.engine = engine else: raise ValidationError("""This little engine ({}) can't ({})""".format(engine, self.dashlevel)) if self.engine == 'libdisp': self.disp = core.Dispersion.build(self.dashlevel, **resolved['dashparams'])
[docs] def print_out(self): """Format dispersion parameters of `self` for output file.""" text = [] text.append(" => %s: Empirical Dispersion <=" % (self.fctldash.upper() if self.fctldash.upper() else 'Custom')) text.append('') text.append(self.description) text.append(self.dashlevel_citation.rstrip()) if self.dashparams_citation: text.append(" Parametrisation from:{}".format(self.dashparams_citation.rstrip())) text.append('') for op in self.ordered_params: text.append(" %6s = %14.6f" % (op, self.dashparams[op])) text.append('\n') core.print_out('\n'.join(text))
[docs] def compute_energy(self, molecule): """Compute dispersion energy based on engine, dispersion level, and parameters in `self`. Parameters ---------- molecule : psi4.core.Molecule System for which to compute empirical dispersion correction. Returns ------- float Dispersion energy [Eh]. Notes ----- DISPERSION CORRECTION ENERGY Disp always set. Overridden in SCF finalization, but that only changes for "-3C" methods. self.fctldash + DISPERSION CORRECTION ENERGY Set if `fctldash` nonempty. """ if self.engine == 'dftd3': jobrec = intf_dftd3.run_dftd3_from_arrays( molrec=molecule.to_dict(np_out=False), name_hint=self.fctldash, level_hint=self.dashlevel, param_tweaks=self.dashparams, dashcoeff_supplement=self.dashcoeff_supplement, ptype='energy', verbose=1) dashd_part = float(jobrec['qcvars']['DISPERSION CORRECTION ENERGY'].data) for k, qca in jobrec['qcvars'].items(): if not isinstance(qca.data, np.ndarray): core.set_variable(k, qca.data) if self.fctldash in ['hf3c', 'pbeh3c']: gcp_part = gcp.run_gcp(molecule, self.fctldash, verbose=False, dertype=0) dashd_part += gcp_part return dashd_part else: ene = self.disp.compute_energy(molecule) core.set_variable('DISPERSION CORRECTION ENERGY', ene) if self.fctldash: core.set_variable('{} DISPERSION CORRECTION ENERGY'.format(self.fctldash), ene) return ene
[docs] def compute_gradient(self, molecule): """Compute dispersion gradient based on engine, dispersion level, and parameters in `self`. Parameters ---------- molecule : psi4.core.Molecule System for which to compute empirical dispersion correction. Returns ------- psi4.core.Matrix (nat, 3) dispersion gradient [Eh/a0]. """ if self.engine == 'dftd3': jobrec = intf_dftd3.run_dftd3_from_arrays( molrec=molecule.to_dict(np_out=False), name_hint=self.fctldash, level_hint=self.dashlevel, param_tweaks=self.dashparams, dashcoeff_supplement=self.dashcoeff_supplement, ptype='gradient', verbose=1) dashd_part = core.Matrix.from_array(jobrec['qcvars']['DISPERSION CORRECTION GRADIENT'].data) for k, qca in jobrec['qcvars'].items(): if not isinstance(qca.data, np.ndarray): core.set_variable(k, qca.data) if self.fctldash in ['hf3c', 'pbeh3c']: gcp_part = gcp.run_gcp(molecule, self.fctldash, verbose=False, dertype=1) dashd_part.add(gcp_part) return dashd_part else: return self.disp.compute_gradient(molecule)
[docs] def compute_hessian(self, molecule): """Compute dispersion Hessian based on engine, dispersion level, and parameters in `self`. Uses finite difference, as no dispersion engine has analytic second derivatives. Parameters ---------- molecule : psi4.core.Molecule System for which to compute empirical dispersion correction. Returns ------- psi4.core.Matrix (3*nat, 3*nat) dispersion Hessian [Eh/a0/a0]. """ optstash = p4util.OptionsState(['PRINT']) core.set_global_option('PRINT', 0) core.print_out("\n\n Analytical Dispersion Hessians are not supported by dftd3 or gcp.\n") core.print_out(" Computing the Hessian through finite difference of gradients.\n\n") # Setup the molecule molclone = molecule.clone() molclone.reinterpret_coordentry(False) molclone.fix_orientation(True) molclone.fix_com(True) # Record undisplaced symmetry for projection of diplaced point groups core.set_parent_symmetry(molecule.schoenflies_symbol()) findif_meta_dict = driver_findif.hessian_from_gradient_geometries(molclone, -1) for displacement in findif_meta_dict["displacements"].values(): geom_array = np.reshape(displacement["geometry"], (-1, 3)) molclone.set_geometry(core.Matrix.from_array(geom_array)) molclone.update_geometry() displacement["gradient"] = self.compute_gradient(molclone).np.ravel().tolist() H = driver_findif.compute_hessian_from_gradients(findif_meta_dict, -1) optstash.restore() return core.Matrix.from_array(H)