# This file is part of dftd4.
# SPDX-Identifier: LGPL-3.0-or-later
#
# dftd4 is free software: you can redistribute it and/or modify it under
# the terms of the Lesser GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# dftd4 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
# Lesser GNU General Public License for more details.
#
# You should have received a copy of the Lesser GNU General Public License
# along with dftd4. If not, see <https://www.gnu.org/licenses/>.
"""
PySCF Support
-------------
Compatibility layer for supporting DFT-D4 in `pyscf <https://pyscf.org/>`_.
"""
try:
from pyscf import lib, gto
from pyscf.grad import rhf as rhf_grad
except ModuleNotFoundError:
raise ModuleNotFoundError("This submodule requires pyscf installed")
import numpy as np
from typing import Tuple
from .interface import DispersionModel, DampingParam
GradientsBase = getattr(rhf_grad, "GradientsBase", rhf_grad.Gradients)
[docs]
class DFTD4Dispersion(lib.StreamObject):
"""
Implementation of the interface for using DFT-D4 in pyscf.
Examples
--------
>>> from pyscf import gto
>>> import dftd4.pyscf as disp
>>> mol = gto.M(
... atom='''
... C -0.755422531 -0.796459123 -1.023590391
... C 0.634274834 -0.880017014 -1.075233285
... C 1.406955202 0.199695367 -0.653144334
... C 0.798863737 1.361204515 -0.180597909
... C -0.593166787 1.434312023 -0.133597923
... C -1.376239198 0.359205222 -0.553258516
... I -1.514344238 3.173268101 0.573601106
... H 1.110906949 -1.778801728 -1.440619836
... H 1.399172302 2.197767355 0.147412751
... H 2.486417780 0.142466525 -0.689380574
... H -2.454252250 0.422581120 -0.512807958
... H -1.362353593 -1.630564523 -1.348743149
... S -3.112683203 6.289227834 1.226984439
... H -4.328789697 5.797771251 0.973373089
... C -2.689135032 6.703163830 -0.489062886
... H -1.684433029 7.115457372 -0.460265708
... H -2.683867206 5.816530502 -1.115183775
... H -3.365330613 7.451201412 -0.890098894
... '''
... )
>>> d4 = disp.DFTD4Dispersion(mol, xc="r2SCAN")
>>> d4.kernel()[0]
array(-0.0050011)
"""
def __init__(self, mol, xc: str = "hf", atm: bool = True):
self.mol = mol
self.verbose = mol.verbose
self.xc = xc
self.atm = atm
self.edisp = None
self.grads = None
[docs]
def dump_flags(self, verbose=None) -> "DFTD4Dispersion":
"""
Show options used for the DFT-D4 dispersion correction.
"""
lib.logger.info(self, "** DFTD4 parameter **")
lib.logger.info(self, "func %s", self.xc)
return self
[docs]
def kernel(self) -> Tuple[float, np.ndarray]:
"""
Compute the DFT-D4 dispersion correction.
The dispersion model as well as the parameters are created locally and
not part of the state of the instance.
Returns
-------
float, ndarray
The energy and gradient of the DFT-D4 dispersion correction.
"""
mol = self.mol
disp = DispersionModel(
mol.atom_charges(),
mol.atom_coords(),
mol.charge,
)
param = DampingParam(
method=self.xc,
atm=self.atm,
)
res = disp.get_dispersion(param=param, grad=True)
self.edisp = res.get("energy")
self.grads = res.get("gradient")
return self.edisp, self.grads
[docs]
def reset(self, mol) -> "DFTD4Dispersion":
"""
Reset mol and clean up relevant attributes for scanner mode
"""
self.mol = mol
return self
class _DFTD4:
"""
Stub class used to identify instances of the `DFTD4` class
"""
pass
class _DFTD4Grad:
"""
Stub class used to identify instances of the `DFTD4Grad` class
"""
pass
[docs]
def energy(mf):
"""
Apply DFT-D4 corrections to SCF or MCSCF methods by returning an
instance of a new class built from the original instances class.
Parameters
----------
mf
The method to which DFT-D4 corrections will be applied.
Returns
-------
The method with DFT-D4 corrections applied.
Examples
--------
>>> from pyscf import gto, scf
>>> import dftd4.pyscf as disp
>>> mol = gto.M(
... atom='''
... N -1.57871857 -0.04661102 0.00000000
... N 1.57871857 0.04661102 0.00000000
... H -2.15862174 0.13639605 0.80956529
... H -0.84947130 0.65819321 0.00000000
... H -2.15862174 0.13639605 -0.80956529
... H 2.15862174 -0.13639605 -0.80956529
... H 0.84947130 -0.65819321 0.00000000
... H 2.15862174 -0.13639605 0.80956529
... '''
... )
>>> mf = disp.energy(scf.RHF(mol)).run()
converged SCF energy = -110.917424528592
>>> mf.kernel()
-110.917424528592
"""
from pyscf.scf import hf
from pyscf.mcscf import casci
if not isinstance(mf, (hf.SCF, casci.CASCI)):
raise TypeError("mf must be an instance of SCF or CASCI")
with_dftd4 = DFTD4Dispersion(
mf.mol,
xc="hf"
if isinstance(mf, casci.CASCI)
else getattr(mf, "xc", "HF").upper().replace(" ", ""),
)
if isinstance(mf, _DFTD4):
mf.with_dftd4 = with_dftd4
return mf
class DFTD4(_DFTD4, mf.__class__):
"""
Patched SCF class including DFT-D4 corrections.
"""
def __init__(self, method, with_dftd4: DFTD4Dispersion):
self.__dict__.update(method.__dict__)
self.with_dftd4 = with_dftd4
self._keys.update(["with_dftd4"])
def dump_flags(self, verbose=None) -> "DFTD4":
mf.__class__.dump_flags(self, verbose)
if self.with_dftd4:
self.with_dftd4.dump_flags(verbose)
return self
def energy_nuc(self) -> float:
enuc = mf.__class__.energy_nuc(self)
if self.with_dftd4:
edisp = self.with_dftd4.kernel()[0]
self.scf_summary["dispersion"] = edisp
enuc += edisp
return enuc
def reset(self, mol=None) -> "DFTD4":
self.with_dftd4.reset(mol)
return mf.__class__.reset(self, mol)
def nuc_grad_method(self) -> "DFTD4Grad":
scf_grad = mf.__class__.nuc_grad_method(self)
return grad(scf_grad)
Gradients = lib.alias(nuc_grad_method, alias_name="Gradients")
return DFTD4(mf, with_dftd4)
[docs]
def grad(mfgrad: GradientsBase):
"""
Apply DFT-D4 corrections to SCF or MCSCF nuclear gradients methods
by returning an instance of a new class built from the original class.
Parameters
----------
mfgrad
The method to which DFT-D4 corrections will be applied.
Returns
-------
The method with DFT-D4 corrections applied.
Examples
--------
>>> from pyscf import gto, scf
>>> import dftd4.pyscf as disp
>>> mol = gto.M(
... atom='''
... O -1.65542061 -0.12330038 0.00000000
... O 1.24621244 0.10268870 0.00000000
... H -0.70409026 0.03193167 0.00000000
... H -2.03867273 0.75372294 0.00000000
... H 1.57598558 -0.38252146 -0.75856129
... H 1.57598558 -0.38252146 0.75856129
... '''
... )
>>> grad = disp.energy(scf.RHF(mol)).run().nuc_grad_method()
converged SCF energy = -149.939098424774
>>> g = grad.kernel()
--------------- DFTD4 gradients ---------------
x y z
0 O 0.0172438133 0.0508406920 0.0000000000
1 O 0.0380018285 -0.0460223790 -0.0000000000
2 H -0.0305058266 -0.0126478132 -0.0000000000
3 H 0.0069233858 -0.0382898692 -0.0000000000
4 H -0.0158316004 0.0230596847 0.0218908543
5 H -0.0158316004 0.0230596847 -0.0218908543
----------------------------------------------
"""
if not isinstance(mfgrad, GradientsBase):
raise TypeError("mfgrad must be an instance of Gradients")
# Ensure that the zeroth order results include DFTD4 corrections
if not getattr(mfgrad.base, "with_dftd4", None):
mfgrad.base = dftd4(mfgrad.base)
class DFTD4Grad(_DFTD4Grad, mfgrad.__class__):
"""
Patched SCF class including DFT-D4 corrections.
"""
def grad_nuc(self, mol=None, atmlst=None):
nuc_g = mfgrad.__class__.grad_nuc(self, mol, atmlst)
with_dftd4 = getattr(self.base, "with_dftd4", None)
if with_dftd4:
disp_g = with_dftd4.kernel()[1]
if atmlst is not None:
disp_g = disp_g[atmlst]
nuc_g += disp_g
return nuc_g
dgrad = DFTD4Grad.__new__(DFTD4Grad)
dgrad.__dict__.update(mfgrad.__dict__)
return dgrad