|
| 1 | +# This file is part of s-dftd3. |
| 2 | +# SPDX-Identifier: LGPL-3.0-or-later |
| 3 | +# |
| 4 | +# s-dftd3 is free software: you can redistribute it and/or modify it under |
| 5 | +# the terms of the Lesser GNU General Public License as published by |
| 6 | +# the Free Software Foundation, either version 3 of the License, or |
| 7 | +# (at your option) any later version. |
| 8 | +# |
| 9 | +# s-dftd3 is distributed in the hope that it will be useful, |
| 10 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | +# Lesser GNU General Public License for more details. |
| 13 | +# |
| 14 | +# You should have received a copy of the Lesser GNU General Public License |
| 15 | +# along with s-dftd3. If not, see <https://www.gnu.org/licenses/>. |
| 16 | +""" |
| 17 | +Compatibility layer for supporting DFT-D3 in `pyscf <https://pyscf.org/>`_. |
| 18 | +
|
| 19 | +Example |
| 20 | +------- |
| 21 | +>>> from pyscf import gto, scf |
| 22 | +>>> from pyscf import scf |
| 23 | +>>> import dftd3.pyscf as disp |
| 24 | +>>> mol = gto.Mole() |
| 25 | +>>> mol.atom = ''' O 0.00000000 0.00000000 -0.11081188 |
| 26 | +... H -0.00000000 -0.84695236 0.59109389 |
| 27 | +... H -0.00000000 0.89830571 0.52404783 ''' |
| 28 | +>>> mol.basis = 'cc-pvdz' |
| 29 | +>>> _ = mol.build() |
| 30 | +>>> mf = disp.dftd3(scf.RHF(mol)) |
| 31 | +>>> print(mf.kernel()) |
| 32 | +-75.99396273778923 |
| 33 | +""" |
| 34 | + |
| 35 | +import numpy as np |
| 36 | + |
| 37 | +try: |
| 38 | + from pyscf import lib, gto |
| 39 | +except ModuleNotFoundError: |
| 40 | + raise ModuleNotFoundError("This submodule requires pyscf installed") |
| 41 | + |
| 42 | +from .interface import ( |
| 43 | + DispersionModel, |
| 44 | + RationalDampingParam, |
| 45 | + ZeroDampingParam, |
| 46 | + ModifiedRationalDampingParam, |
| 47 | + ModifiedZeroDampingParam, |
| 48 | + OptimizedPowerDampingParam, |
| 49 | +) |
| 50 | + |
| 51 | +_damping_param = { |
| 52 | + "d3bj": RationalDampingParam, |
| 53 | + "d3zero": ZeroDampingParam, |
| 54 | + "d3bjm": ModifiedRationalDampingParam, |
| 55 | + "d3mbj": ModifiedRationalDampingParam, |
| 56 | + "d3zerom": ModifiedZeroDampingParam, |
| 57 | + "d3mzero": ModifiedZeroDampingParam, |
| 58 | + "d3op": OptimizedPowerDampingParam, |
| 59 | +} |
| 60 | + |
| 61 | + |
| 62 | +def dftd3(mf): |
| 63 | + """Apply DFT-D3 corrections to SCF or MCSCF methods""" |
| 64 | + |
| 65 | + from pyscf.scf import hf |
| 66 | + from pyscf.mcscf import casci |
| 67 | + |
| 68 | + if not isinstance(mf, (hf.SCF, casci.CASCI)): |
| 69 | + raise TypeError("mf must be an instance of SCF or CASCI") |
| 70 | + |
| 71 | + with_dftd3 = DFTD3Dispersion( |
| 72 | + mf.mol, |
| 73 | + xc="hf" |
| 74 | + if isinstance(mf, casci.CASCI) |
| 75 | + else getattr(mf, "xc", "HF").upper().replace(" ", ""), |
| 76 | + ) |
| 77 | + |
| 78 | + if isinstance(mf, _DFTD3): |
| 79 | + mf.with_dftd3 = with_dftd3 |
| 80 | + return mf |
| 81 | + |
| 82 | + class DFTD3(_DFTD3, mf.__class__): |
| 83 | + def __init__(self, method, with_dftd3): |
| 84 | + self.__dict__.update(method.__dict__) |
| 85 | + self.with_dftd3 = with_dftd3 |
| 86 | + self._keys.update(["with_dftd3"]) |
| 87 | + |
| 88 | + def dump_flags(self, verbose=None): |
| 89 | + mf.__class__.dump_flags(self, verbose) |
| 90 | + if self.with_dftd3: |
| 91 | + self.with_dftd3.dump_flags(verbose) |
| 92 | + return self |
| 93 | + |
| 94 | + def energy_nuc(self): |
| 95 | + enuc = mf.__class__.energy_nuc(self) |
| 96 | + if self.with_dftd3: |
| 97 | + enuc += self.with_dftd3.kernel()[0] |
| 98 | + return enuc |
| 99 | + |
| 100 | + def reset(self, mol=None): |
| 101 | + self.with_dftd3.reset(mol) |
| 102 | + return mf.__class__.reset(self, mol) |
| 103 | + |
| 104 | + def nuc_grad_method(self): |
| 105 | + scf_grad = mf.__class__.nuc_grad_method(self) |
| 106 | + return grad(scf_grad) |
| 107 | + |
| 108 | + Gradients = lib.alias(nuc_grad_method, alias_name="Gradients") |
| 109 | + |
| 110 | + return DFTD3(mf, with_dftd3) |
| 111 | + |
| 112 | + |
| 113 | +def grad(scf_grad): |
| 114 | + """ |
| 115 | + Apply DFT-D3 corrections to SCF or MCSCF nuclear gradients methods |
| 116 | + """ |
| 117 | + from pyscf.grad import rhf as rhf_grad |
| 118 | + |
| 119 | + if not isinstance(scf_grad, rhf_grad.Gradients): |
| 120 | + raise TypeError("scf_grad must be an instance of Gradients") |
| 121 | + |
| 122 | + # Ensure that the zeroth order results include DFTD3 corrections |
| 123 | + if not getattr(scf_grad.base, "with_dftd3", None): |
| 124 | + scf_grad.base = dftd3(scf_grad.base) |
| 125 | + |
| 126 | + class DFTD3Grad(_DFTD3Grad, scf_grad.__class__): |
| 127 | + def grad_nuc(self, mol=None, atmlst=None): |
| 128 | + nuc_g = scf_grad.__class__.grad_nuc(self, mol, atmlst) |
| 129 | + with_dftd3 = getattr(self.base, "with_dftd3", None) |
| 130 | + if with_dftd3: |
| 131 | + disp_g = with_dftd3.kernel()[1] |
| 132 | + if atmlst is not None: |
| 133 | + disp_g = disp_g[atmlst] |
| 134 | + nuc_g += disp_g |
| 135 | + return nuc_g |
| 136 | + |
| 137 | + mfgrad = DFTD3Grad.__new__(DFTD3Grad) |
| 138 | + mfgrad.__dict__.update(scf_grad.__dict__) |
| 139 | + return mfgrad |
| 140 | + |
| 141 | + |
| 142 | +class DFTD3Dispersion(lib.StreamObject): |
| 143 | + def __init__(self, mol, xc="hf", version="d3bj", atm=False): |
| 144 | + self.mol = mol |
| 145 | + self.verbose = mol.verbose |
| 146 | + self.xc = xc |
| 147 | + self.atm = atm |
| 148 | + self.version = version |
| 149 | + self.edisp = None |
| 150 | + self.grads = None |
| 151 | + |
| 152 | + def dump_flags(self, verbose=None): |
| 153 | + lib.logger.info(self, "** DFTD3 parameter **") |
| 154 | + lib.logger.info(self, "func %s", self.xc) |
| 155 | + lib.logger.info( |
| 156 | + self, "version %s", self.version + "-atm" if self.atm else self.version |
| 157 | + ) |
| 158 | + return self |
| 159 | + |
| 160 | + def kernel(self): |
| 161 | + mol = self.mol |
| 162 | + |
| 163 | + disp = DispersionModel( |
| 164 | + np.array([gto.charge(mol.atom_symbol(ia)) for ia in range(mol.natm)]), |
| 165 | + mol.atom_coords(), |
| 166 | + ) |
| 167 | + |
| 168 | + param = _damping_param[self.version]( |
| 169 | + method=self.xc, |
| 170 | + atm=self.atm, |
| 171 | + ) |
| 172 | + |
| 173 | + res = disp.get_dispersion(param=param, grad=True) |
| 174 | + |
| 175 | + self.edisp = res.get("energy") |
| 176 | + self.grads = res.get("gradient") |
| 177 | + return self.edisp, self.grads |
| 178 | + |
| 179 | + def reset(self, mol): |
| 180 | + """Reset mol and clean up relevant attributes for scanner mode""" |
| 181 | + self.mol = mol |
| 182 | + return self |
| 183 | + |
| 184 | + |
| 185 | +class _DFTD3: |
| 186 | + """ |
| 187 | + Stub class used to identify instances of the `DFTD3` class |
| 188 | + """ |
| 189 | + |
| 190 | + pass |
| 191 | + |
| 192 | + |
| 193 | +class _DFTD3Grad: |
| 194 | + """ |
| 195 | + Stub class used to identify instances of the `DFTD3Grad` class |
| 196 | + """ |
| 197 | + |
| 198 | + pass |
0 commit comments