#!/usr/bin/env python
# Copyright 2025 The PySCF Developers. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Author: Chenghan Li <lch004218@gmail.com>
#
import numpy as np
import pyscf
from pyscf import lib
from pyscf import gto
from pyscf import df
from pyscf import scf
from pyscf import grad
from pyscf.lib import logger
from pyscf.qmmm.pbc import mm_mole
from scipy.special import erfc, erf
[docs]
def add_mm_charges(scf_method, atoms_or_coords, a, charges, radii=None,
rcut_ewald=None, rcut_hcore=None, unit=None):
'''Embedding the one-electron (non-relativistic) potential generated by MM
point (or gaussian-distributed) charges into QM Hamiltonian.
The total energy includes the regular QM energy, the interaction between
the nuclei in QM region and the MM charges, and the static Coulomb
interaction between the electron density and the MM charges. The electrostatic
interactions between reference cell and periodic images are also computed.
It does not include the static Coulomb interactions of the MM charges,
the MM energy, the vdw interaction or other
bonding/non-bonding effects between QM region and MM particles.
Args:
scf_method : a HF or DFT object
atoms_or_coords : 2D array, shape (N,3)
MM particle coordinates
charges : 1D array
MM particle charges
a : 2D array, shape (3,3)
Lattice vectors
Kwargs:
radii : 1D array
The Gaussian charge distribution radii of MM atoms.
rcut_ewald: Float
Ewald real-space cutoff
rcut_hcore: Float
Real-space cutoff for exact QM-MM coupling
unit : str
Bohr, AU, Ang (case insensitive). Default is the same to mol.unit
Returns:
Same method object as the input scf_method with modified 1e Hamiltonian
Examples:
>>> mol = gto.M(atom='H 0 0 0; F 0 0 1', basis='ccpvdz', verbose=0)
>>> mf = add_mm_charges(dft.RKS(mol), [(0.5,0.6,0.8)], np.eye(3)*10, [-0.3])
>>> mf.kernel()
'''
mol = scf_method.mol
if unit is None:
unit = mol.unit
mm_mol = mm_mole.create_mm_mol(atoms_or_coords, a, charges, radii=radii,
rcut_ewald=rcut_ewald, rcut_hcore=rcut_hcore, unit=unit)
return qmmm_for_scf(scf_method, mm_mol)
[docs]
def qmmm_for_scf(method, mm_mol):
'''Add the potential of MM particles to SCF (HF and DFT) method
then generate the corresponding QM/MM method for the QM system.
Args:
mm_mol : MM Mole object
'''
if isinstance(method, scf.hf.SCF):
# Avoid to initialize QMMM twice
if isinstance(method, QMMM):
method.mm_mol = mm_mol
method.s1r = None
method.s1rr = None
method.mm_ewald_pot = None
method.qm_ewald_hess = None
method.e_nuc = None
return method
cls = QMMMSCF
else:
# post-HF methods
raise NotImplementedError()
return lib.set_class(cls(method, mm_mol), (cls, method.__class__))
[docs]
class QMMM:
__name_mixin__ = 'QMMM'
[docs]
class QMMMSCF(QMMM):
_keys = {'mm_mol', 's1r', 's1rr', 'mm_ewald_pot', 'qm_ewald_hess', 'e_nuc'}
to_gpu = NotImplemented
as_scanner = NotImplemented
def __init__(self, method, mm_mol):
self.__dict__.update(method.__dict__)
self.mm_mol = mm_mol
self.s1r = None
self.s1rr = None
self.mm_ewald_pot = None
self.qm_ewald_hess = None
self.e_nuc = None
[docs]
def dump_flags(self, verbose=None):
super().dump_flags(verbose)
logger.info(self, '** Add background charges for %s **',
self.__class__.__name__)
_a = self.mm_mol.lattice_vectors()
logger.info(self, 'lattice vectors a1 [%.9f, %.9f, %.9f]', *_a[0])
logger.info(self, ' a2 [%.9f, %.9f, %.9f]', *_a[1])
logger.info(self, ' a3 [%.9f, %.9f, %.9f]', *_a[2])
if self.verbose >= logger.DEBUG2:
logger.debug2(self, 'Charge Location')
coords = self.mm_mol.atom_coords()
charges = self.mm_mol.atom_charges()
for i, z in enumerate(charges):
logger.debug2(self, '%.9g %s', z, coords[i])
return self
[docs]
def get_mm_ewald_pot(self, mol, mm_mol):
return self.mm_mol.get_ewald_pot(
mol.atom_coords(),
mm_mol.atom_coords(), mm_mol.atom_charges())
[docs]
def get_qm_ewald_pot(self, mol, dm, qm_ewald_hess=None):
# hess = d^2 E / dQ_i dQ_j, d^2 E / dQ_i dD_ja, d^2 E / dDia dDjb, d^2 E/ dQ_i dO_jab
if qm_ewald_hess is None:
qm_ewald_hess = self.mm_mol.get_ewald_pot(mol.atom_coords())
self.qm_ewald_hess = qm_ewald_hess
charges = self.get_qm_charges(dm)
dips = self.get_qm_dipoles(dm)
quads = self.get_qm_quadrupoles(dm)
ewpot0 = lib.einsum('ij,j->i', qm_ewald_hess[0], charges)
ewpot0 += lib.einsum('ijx,jx->i', qm_ewald_hess[1], dips)
ewpot0 += lib.einsum('ijxy,jxy->i', qm_ewald_hess[3], quads)
ewpot1 = lib.einsum('ijx,i->jx', qm_ewald_hess[1], charges)
ewpot1 += lib.einsum('ijxy,jy->ix', qm_ewald_hess[2], dips)
ewpot2 = lib.einsum('ijxy,j->ixy', qm_ewald_hess[3], charges)
return ewpot0, ewpot1, ewpot2
[docs]
def get_hcore(self, mol=None):
cput0 = (logger.process_clock(), logger.perf_counter())
mm_mol = self.mm_mol
if mol is None:
mol = self.mol
rcut_hcore = mm_mol.rcut_hcore
h1e = super().get_hcore(mol)
Ls = mm_mol.get_lattice_Ls()
qm_center = np.mean(mol.atom_coords(), axis=0)
mask = np.linalg.norm(Ls, axis=-1) < 1e-12
Ls[mask] = [np.inf] * 3
r_qm = (mol.atom_coords() - qm_center)[None,:,:] - Ls[:,None,:]
r_qm = np.einsum('Lix,Lix->Li', r_qm, r_qm)
assert rcut_hcore**2 < np.min(r_qm), \
"QM image is within rcut_hcore of QM center. " + \
f"rcut_hcore = {rcut_hcore} >= min(r_qm) = {np.sqrt(np.min(r_qm))}"
Ls[Ls == np.inf] = 0.0
r_qm = mol.atom_coords() - qm_center
r_qm = np.einsum('ix,ix->i', r_qm, r_qm)
assert rcut_hcore**2 > np.max(r_qm), \
"Not all QM atoms are within rcut_hcore of QM center. " + \
f"rcut_hcore = {rcut_hcore} <= max(r_qm) = {np.sqrt(np.max(r_qm))}"
r_qm = None
all_coords = lib.direct_sum('ix+Lx->Lix',
mm_mol.atom_coords(), Ls).reshape(-1,3)
all_charges = np.hstack([mm_mol.atom_charges()] * len(Ls))
dist2 = all_coords - qm_center
dist2 = lib.einsum('ix,ix->i', dist2, dist2)
# charges within rcut_hcore exactly go into hcore
mask = dist2 <= rcut_hcore**2
charges = all_charges[mask]
coords = all_coords[mask]
logger.note(self, '%d MM charges see directly QM density'%charges.shape[0])
nao = mol.nao
max_memory = self.max_memory - lib.current_memory()[0]
blksize = int(min(max_memory*1e6/8/nao**2, 200))
blksize = max(blksize, 1)
if mm_mol.charge_model == 'gaussian' and len(coords) != 0:
expnts = np.hstack([mm_mol.get_zetas()] * len(Ls))[mask]
if mol.cart:
intor = 'int3c2e_cart'
else:
intor = 'int3c2e_sph'
cintopt = gto.moleintor.make_cintopt(mol._atm, mol._bas,
mol._env, intor)
v = 0
for i0, i1 in lib.prange(0, charges.size, blksize):
fakemol = gto.fakemol_for_charges(coords[i0:i1], expnts[i0:i1])
j3c = df.incore.aux_e2(mol, fakemol, intor=intor,
aosym='s2ij', cintopt=cintopt)
v += lib.einsum('xk,k->x', j3c, -charges[i0:i1])
if type(v) is not int:
v = lib.unpack_tril(v)
h1e += v
elif mm_mol.charge_model == 'point' and len(coords) != 0:
for i0, i1 in lib.prange(0, charges.size, blksize):
j3c = mol.intor('int1e_grids', hermi=1, grids=coords[i0:i1])
h1e += lib.einsum('kpq,k->pq', j3c, -charges[i0:i1])
else: # no MM charges
pass
logger.timer(self, 'get_hcore', *cput0)
return h1e
[docs]
def get_qm_charges(self, dm):
aoslices = self.mol.aoslice_by_atom()
chg = self.mol.atom_charges()
dmS = lib.dot(dm, self.get_ovlp())
qm_charges = list()
for iatm in range(self.mol.natm):
p0, p1 = aoslices[iatm, 2:]
qm_charges.append(chg[iatm] - np.trace(dmS[p0:p1, p0:p1]))
return np.asarray(qm_charges)
[docs]
def get_s1r(self):
if self.s1r is None:
cput0 = (logger.process_clock(), logger.perf_counter())
self.s1r = list()
mol = self.mol
bas_atom = mol._bas[:,gto.ATOM_OF]
for i in range(self.mol.natm):
b0, b1 = np.where(bas_atom == i)[0][[0,-1]]
shls_slice = (0, mol.nbas, b0, b1+1)
with mol.with_common_orig(mol.atom_coord(i)):
self.s1r.append(
mol.intor('int1e_r', shls_slice=shls_slice))
logger.timer(self, 'get_s1r', *cput0)
return self.s1r
[docs]
def get_qm_dipoles(self, dm, s1r=None):
if s1r is None:
s1r = self.get_s1r()
aoslices = self.mol.aoslice_by_atom()
qm_dipoles = list()
for iatm in range(self.mol.natm):
p0, p1 = aoslices[iatm, 2:]
qm_dipoles.append(
-lib.einsum('uv,xvu->x', dm[p0:p1], s1r[iatm]))
return np.asarray(qm_dipoles)
[docs]
def get_s1rr(self):
'''
int phi_u phi_v [3(r-Rc)otimes(r-Rc) - |r-Rc|^2] /2 dr
'''
if self.s1rr is None:
cput0 = (logger.process_clock(), logger.perf_counter())
self.s1rr = list()
mol = self.mol
nao = mol.nao
bas_atom = mol._bas[:,gto.ATOM_OF]
for i in range(self.mol.natm):
b0, b1 = np.where(bas_atom == i)[0][[0,-1]]
shls_slice = (0, mol.nbas, b0, b1+1)
with mol.with_common_orig(mol.atom_coord(i)):
s1rr_ = mol.intor('int1e_rr', shls_slice=shls_slice)
s1rr_ = s1rr_.reshape((3,3,nao,-1))
s1rr_trace = lib.einsum('xxuv->uv', s1rr_)
s1rr_ = 3/2 * s1rr_
for k in range(3):
s1rr_[k,k] -= 0.5 * s1rr_trace
self.s1rr.append(s1rr_)
logger.timer(self, 'get_s1rr', *cput0)
return self.s1rr
[docs]
def get_qm_quadrupoles(self, dm, s1rr=None):
if s1rr is None:
s1rr = self.get_s1rr()
aoslices = self.mol.aoslice_by_atom()
qm_quadrupoles = list()
for iatm in range(self.mol.natm):
p0, p1 = aoslices[iatm, 2:]
qm_quadrupoles.append(
-lib.einsum('uv,xyvu->xy', dm[p0:p1], s1rr[iatm]))
return np.asarray(qm_quadrupoles)
[docs]
def get_vdiff(self, mol, ewald_pot):
'''
vdiff_uv = d Q_I / d dm_uv ewald_pot[0]_I
+ d D_Ix / d dm_uv ewald_pot[1]_Ix
+ d O_Ixy / d dm_uv ewald_pot[2]_Ixy
'''
vdiff = np.zeros((mol.nao, mol.nao))
ovlp = self.get_ovlp()
s1r = self.get_s1r()
s1rr = self.get_s1rr()
aoslices = mol.aoslice_by_atom()
for iatm in range(mol.natm):
v0 = ewald_pot[0][iatm]
v1 = ewald_pot[1][iatm]
v2 = ewald_pot[2][iatm]
p0, p1 = aoslices[iatm, 2:]
vdiff[:,p0:p1] -= v0 * ovlp[:,p0:p1]
vdiff[:,p0:p1] -= lib.einsum('x,xuv->uv', v1, s1r[iatm])
vdiff[:,p0:p1] -= lib.einsum('xy,xyuv->uv', v2, s1rr[iatm])
vdiff = (vdiff + vdiff.T) / 2
return vdiff
[docs]
def get_veff(self, mol=None, dm=None, dm_last=0, vhf_last=0, hermi=1,
mm_ewald_pot=None, qm_ewald_pot=None):
if mol is None:
mol = self.mol
mm_mol = self.mm_mol
if mm_ewald_pot is None:
if self.mm_ewald_pot is not None:
mm_ewald_pot = self.mm_ewald_pot
else:
cput0 = (logger.process_clock(), logger.perf_counter())
mm_ewald_pot = self.get_mm_ewald_pot(mol, mm_mol)
self.mm_ewald_pot = mm_ewald_pot
logger.timer(self, 'get_mm_ewald_pot', *cput0)
if qm_ewald_pot is None:
if self.qm_ewald_hess is not None:
qm_ewald_pot = self.get_qm_ewald_pot(
mol, dm, self.qm_ewald_hess)
else:
cput0 = (logger.process_clock(), logger.perf_counter())
qm_ewald_pot = self.get_qm_ewald_pot(mol, dm)
logger.timer(self, 'get_qm_ewald_pot', *cput0)
ewald_pot = \
mm_ewald_pot[0] + qm_ewald_pot[0], \
mm_ewald_pot[1] + qm_ewald_pot[1], \
mm_ewald_pot[2] + qm_ewald_pot[2]
vdiff = self.get_vdiff(mol, ewald_pot)
if vhf_last is not None and isinstance(vhf_last, lib.NPArrayWithTag):
vhf_last = vhf_last.veff_rs
veff = super().get_veff(mol, dm, dm_last, vhf_last, hermi)
if isinstance(veff, lib.NPArrayWithTag):
metadata = veff.__dict__
veff = lib.tag_array(veff + vdiff, veff_rs=veff, **metadata)
else:
veff = lib.tag_array(veff + vdiff, veff_rs=veff)
return veff
[docs]
def energy_elec(self, dm=None, h1e=None, vhf=None):
if vhf is None:
# use the original veff to compute energy
vhf = super().get_veff(self.mol, dm)
return super().energy_elec(dm=dm, h1e=h1e, vhf=vhf)
else:
return super().energy_elec(dm=dm, h1e=h1e, vhf=vhf.veff_rs)
[docs]
def energy_ewald(self, dm=None, mm_ewald_pot=None, qm_ewald_pot=None):
# QM-QM and QM-MM pbc correction
if dm is None:
dm = self.make_rdm1()
if mm_ewald_pot is None:
if self.mm_ewald_pot is not None:
mm_ewald_pot = self.mm_ewald_pot
else:
mm_ewald_pot = self.get_mm_ewald_pot(self.mol, self.mm_mol)
if qm_ewald_pot is None:
qm_ewald_pot = self.get_qm_ewald_pot(self.mol, dm, self.qm_ewald_hess)
ewald_pot = mm_ewald_pot[0] + qm_ewald_pot[0] / 2
e = lib.einsum('i,i->', ewald_pot, self.get_qm_charges(dm))
ewald_pot = mm_ewald_pot[1] + qm_ewald_pot[1] / 2
e += lib.einsum('ix,ix->', ewald_pot, self.get_qm_dipoles(dm))
ewald_pot = mm_ewald_pot[2] + qm_ewald_pot[2] / 2
e += lib.einsum('ixy,ixy->', ewald_pot, self.get_qm_quadrupoles(dm))
# TODO add energy correction if sum(charges) !=0 ?
return e
[docs]
def energy_nuc(self):
if self.e_nuc is not None:
return self.e_nuc
else:
cput0 = (logger.process_clock(), logger.perf_counter())
# gas phase nuc energy
nuc = self.mol.energy_nuc() # qm_nuc - qm_nuc
# select mm atoms within rcut_hcore
mol = self.mol
Ls = self.mm_mol.get_lattice_Ls()
qm_center = np.mean(mol.atom_coords(), axis=0)
all_coords = lib.direct_sum('ix+Lx->Lix',
self.mm_mol.atom_coords(), Ls).reshape(-1,3)
all_charges = np.hstack([self.mm_mol.atom_charges()] * len(Ls))
all_expnts = np.hstack([np.sqrt(self.mm_mol.get_zetas())] * len(Ls))
dist2 = all_coords - qm_center
dist2 = lib.einsum('ix,ix->i', dist2, dist2)
mask = dist2 <= self.mm_mol.rcut_hcore**2
charges = all_charges[mask]
coords = all_coords[mask]
expnts = all_expnts[mask]
# qm_nuc - mm_pc
for j in range(self.mol.natm):
q2, r2 = self.mol.atom_charge(j), self.mol.atom_coord(j)
r = lib.norm(r2-coords, axis=1)
nuc += q2*(charges * erf(expnts*r) /r).sum()
logger.timer(self, 'energy_nuc', *cput0)
self.e_nuc = nuc
return nuc
[docs]
def energy_tot(self, dm=None, h1e=None, vhf=None, mm_ewald_pot=None, qm_ewald_pot=None):
nuc = self.energy_nuc()
ewald = self.energy_ewald(dm=dm, mm_ewald_pot=mm_ewald_pot, qm_ewald_pot=qm_ewald_pot)
e_tot = self.energy_elec(dm, h1e, vhf)[0] + nuc + ewald
self.scf_summary['nuc'] = nuc.real
self.scf_summary['ewald'] = ewald
return e_tot
[docs]
def nuc_grad_method(self):
scf_grad = super().nuc_grad_method()
return qmmm_grad_for_scf(scf_grad)
Gradients = nuc_grad_method
[docs]
def add_mm_charges_grad(scf_grad, atoms_or_coords, a, charges, radii=None,
rcut_ewald=None, rcut_hcore=None, unit=None):
'''Apply the MM charges in the QM gradients' method. It affects both the
electronic and nuclear parts of the QM fragment.
Args:
scf_grad : a HF or DFT gradient object (grad.HF or grad.RKS etc)
coords : 2D array, shape (N,3)
MM particle coordinates
charges : 1D array
MM particle charges
a : 2D array, shape (3,3)
Lattice vectors
Kwargs:
radii : 1D array
The Gaussian charge distribution radii of MM atoms.
unit : str
Bohr, AU, Ang (case insensitive). Default is the same to mol.unit
Returns:
Same gradeints method object as the input scf_grad method
'''
assert (isinstance(scf_grad, grad.rhf.Gradients))
mol = scf_grad.mol
if unit is None:
unit = mol.unit
mm_mol = mm_mole.create_mm_mol(atoms_or_coords, a, charges, radii=radii,
rcut_ewald=rcut_ewald, rcut_hcore=rcut_hcore, unit=unit)
mm_grad = qmmm_grad_for_scf(scf_grad)
mm_grad.base.mm_mol = mm_mol
return mm_grad
# Define method mm_charge_grad for backward compatibility
mm_charge_grad = add_mm_charges_grad
[docs]
def qmmm_grad_for_scf(scf_grad):
'''Add the potential of MM particles to SCF (HF and DFT) object and then
generate the corresponding QM/MM gradients method for the QM system.
'''
if getattr(scf_grad.base, 'with_x2c', None):
raise NotImplementedError('X2C with QM/MM charges')
# Avoid to initialize QMMMGrad twice
if isinstance(scf_grad, QMMMGrad):
return scf_grad
assert (isinstance(scf_grad.base, scf.hf.SCF) and
isinstance(scf_grad.base, QMMM))
scf_grad.de_ewald_mm = None
scf_grad.de_nuc_mm = None
return scf_grad.view(lib.make_class((QMMMGrad, scf_grad.__class__)))
[docs]
class QMMMGrad:
__name_mixin__ = 'QMMM'
_keys = {'de_ewald_mm', 'de_nuc_mm'}
to_gpu = NotImplemented
def __init__(self, scf_grad):
self.__dict__.update(scf_grad.__dict__)
[docs]
def dump_flags(self, verbose=None):
super().dump_flags(verbose)
logger.info(self, '** Add background charges for %s **', self.__class__.__name__)
_a = self.base.mm_mol.lattice_vectors()
logger.info(self, 'lattice vectors a1 [%.9f, %.9f, %.9f]', *_a[0])
logger.info(self, ' a2 [%.9f, %.9f, %.9f]', *_a[1])
logger.info(self, ' a3 [%.9f, %.9f, %.9f]', *_a[2])
if self.verbose >= logger.DEBUG2:
logger.debug2(self, 'Charge Location')
coords = self.base.mm_mol.atom_coords()
charges = self.base.mm_mol.atom_charges()
for i, z in enumerate(charges):
logger.debug2(self, '%.9g %s', z, coords[i])
return self
[docs]
def grad_ewald(self, dm=None, with_mm=False, mm_ewald_pot=None, qm_ewald_pot=None):
'''
pbc correction energy grad w.r.t. qm and mm atom positions
'''
cput0 = (logger.process_clock(), logger.perf_counter())
if dm is None: dm = self.base.make_rdm1()
mol = self.base.mol
cell = self.base.mm_mol
assert cell.dimension == 3
qm_charges = self.base.get_qm_charges(dm)
qm_dipoles = self.base.get_qm_dipoles(dm)
qm_quads = self.base.get_qm_quadrupoles(dm)
qm_coords = self.base.mol.atom_coords()
mm_charges = self.base.mm_mol.atom_charges()
mm_coords = self.base.mm_mol.atom_coords()
aoslices = mol.aoslice_by_atom()
# nuc grad due to qm multipole change due to ovlp change
qm_multipole_grad = np.zeros_like(qm_coords)
if mm_ewald_pot is None:
if self.base.mm_ewald_pot is not None:
mm_ewald_pot = self.base.mm_ewald_pot
else:
mm_ewald_pot = self.base.get_mm_ewald_pot(mol, cell)
if qm_ewald_pot is None:
qm_ewald_pot = self.base.get_qm_ewald_pot(mol, dm, self.base.qm_ewald_hess)
ewald_pot = \
mm_ewald_pot[0] + qm_ewald_pot[0], \
mm_ewald_pot[1] + qm_ewald_pot[1], \
mm_ewald_pot[2] + qm_ewald_pot[2]
dEds = np.zeros((mol.nao, mol.nao))
dEdsr = np.zeros((3, mol.nao, mol.nao))
dEdsrr = np.zeros((3, 3, mol.nao, mol.nao))
s1 = self.get_ovlp(mol) # = -mol.intor('int1e_ipovlp')
s1r = list()
s1rr = list()
bas_atom = mol._bas[:,gto.ATOM_OF]
for iatm in range(mol.natm):
v0 = ewald_pot[0][iatm]
v1 = ewald_pot[1][iatm]
v2 = ewald_pot[2][iatm]
p0, p1 = aoslices[iatm, 2:]
dEds[p0:p1] -= v0 * dm[p0:p1]
dEdsr[:,p0:p1] -= lib.einsum('x,uv->xuv', v1, dm[p0:p1])
dEdsrr[:,:,p0:p1] -= lib.einsum('xy,uv->xyuv', v2, dm[p0:p1])
b0, b1 = np.where(bas_atom == iatm)[0][[0,-1]]
shlslc = (b0, b1+1, 0, mol.nbas)
with mol.with_common_orig(qm_coords[iatm]):
# s1r[a,x,u,v] = \int phi_u (r_a-Ri_a) (-\nabla_x phi_v) dr
s1r.append(
np.asarray(-mol.intor('int1e_irp', shls_slice=shlslc).
reshape(3, 3, -1, mol.nao)))
# s1rr[a,b,x,u,v] =
# \int phi_u [3/2*(r_a-Ri_a)(r_b-Ri_b)-1/2*(r-Ri)^2 delta_ab] (-\nable_x phi_v) dr
s1rr_ = np.asarray(-mol.intor('int1e_irrp', shls_slice=shlslc).
reshape(3, 3, 3, -1, mol.nao))
s1rr_trace = lib.einsum('aaxuv->xuv', s1rr_)
s1rr_ *= 3 / 2
for k in range(3):
s1rr_[k,k] -= 0.5 * s1rr_trace
s1rr.append(s1rr_)
for jatm in range(mol.natm):
p0, p1 = aoslices[jatm, 2:]
# d E_qm_pc / d Ri with fixed ewald_pot
qm_multipole_grad[jatm] += \
lib.einsum('uv,xuv->x', dEds[p0:p1], s1[:,p0:p1]) \
- lib.einsum('uv,xuv->x', dEds[:,p0:p1], s1[:,:,p0:p1])
# d E_qm_dip / d Ri
qm_multipole_grad[jatm] -= \
lib.einsum('auv,axuv->x', dEdsr[:,p0:p1], s1r[jatm])
s1r_ = list()
for iatm in range(mol.natm):
s1r_.append(s1r[iatm][...,p0:p1])
s1r_ = np.concatenate(s1r_, axis=-2)
qm_multipole_grad[jatm] += lib.einsum('auv,axuv->x', dEdsr[...,p0:p1], s1r_)
# d E_qm_quad / d Ri
qm_multipole_grad[jatm] -= \
lib.einsum('abuv,abxuv->x', dEdsrr[:,:,p0:p1], s1rr[jatm])
s1rr_ = list()
for iatm in range(mol.natm):
s1rr_.append(s1rr[iatm][...,p0:p1])
s1rr_ = np.concatenate(s1rr_, axis=-2)
qm_multipole_grad[jatm] += lib.einsum('abuv,abxuv->x', dEdsrr[...,p0:p1], s1rr_)
cput1 = logger.timer(self, 'grad_ewald pulay', *cput0)
s1 = s1r = s1rr = dEds = dEdsr = dEdsrr = None
ew_eta, ew_cut = cell.get_ewald_params()
# ---------------------------------------------- #
# -------- Ewald real-space gradient ----------- #
# ---------------------------------------------- #
Lall = cell.get_lattice_Ls()
from pyscf import pbc
rmax_qm = max(np.linalg.norm(qm_coords - np.mean(qm_coords, axis=0), axis=-1))
qm_ewovrl_grad = np.zeros_like(qm_coords)
def grad_Tij(R, r):
Rij = R
Tij = 1 / r
# Tija = \nabla_a Tij
# Tijab = \nabla_a Tijb
# Tijabc = \nabla_a Tijbc
Tija = -lib.einsum('ijx,ij->ijx', Rij, Tij**3)
Tijab = 3 * lib.einsum('ija,ijb->ijab', Rij, Rij)
Tijab = lib.einsum('ijab,ij->ijab', Tijab, Tij**5)
Tijab -= lib.einsum('ij,ab->ijab', Tij**3, np.eye(3))
Tijabc = -15 * lib.einsum('ija,ijb,ijc->ijabc', Rij, Rij, Rij)
Tijabc = lib.einsum('ijabc,ij->ijabc', Tijabc, Tij**7)
Tijabc += 3 * lib.einsum('ija,bc,ij->ijabc', Rij, np.eye(3), Tij**5)
Tijabc += 3 * lib.einsum('ijb,ac,ij->ijabc', Rij, np.eye(3), Tij**5)
Tijabc += 3 * lib.einsum('ijc,ab,ij->ijabc', Rij, np.eye(3), Tij**5)
return Tija, Tijab, Tijabc
def grad_kTij(R, r, eta):
if isinstance(eta, float):
Tij = erfc(eta * r) / r
ekR = np.exp(-eta**2 * r**2)
Tij = erfc(eta * r) / r
invr3 = (Tij + 2*eta/np.sqrt(np.pi) * ekR) / r**2
Tija = -lib.einsum('ijx,ij->ijx', R, invr3)
Tijab = 3 * lib.einsum('ija,ijb,ij->ijab', R, R, 1/r**2)
Tijab -= lib.einsum('ij,ab->ijab', np.ones_like(r), np.eye(3))
invr5 = invr3 + 4/3*eta**3/np.sqrt(np.pi) * ekR # NOTE this is invr5 * r**2
Tijab = lib.einsum('ijab,ij->ijab', Tijab, invr5)
Tijab += 4/3*eta**3/np.sqrt(np.pi)*lib.einsum('ij,ab->ijab', ekR, np.eye(3))
invr7 = invr5 / r**2 + 8/15 / np.sqrt(np.pi) * eta**5 * ekR # NOTE this is invr7 * r**2
Tijabc = -15 * lib.einsum('ija,ijb,ijc->ijabc', R, R, R)
Tijabc = lib.einsum('ijabc,ij->ijabc', Tijabc, 1/r**2)
Tijabc += 3 * lib.einsum('ija,bc->ijabc', R, np.eye(3))
Tijabc += 3 * lib.einsum('ijb,ac->ijabc', R, np.eye(3))
Tijabc += 3 * lib.einsum('ijc,ab->ijabc', R, np.eye(3))
Tijabc = lib.einsum('ijabc,ij->ijabc', Tijabc, invr7)
Tijabc -= 8/5 / np.sqrt(np.pi) * eta**5 * lib.einsum('ij,ija,bc->ijabc', ekR, R, np.eye(3))
Tijabc -= 8/5 / np.sqrt(np.pi) * eta**5 * lib.einsum('ij,ijb,ac->ijabc', ekR, R, np.eye(3))
Tijabc -= 8/5 / np.sqrt(np.pi) * eta**5 * lib.einsum('ij,ijc,ab->ijabc', ekR, R, np.eye(3))
return Tija, Tijab, Tijabc
else:
Tij = erfc(eta * r) / r
ekR = np.exp(-eta**2 * r**2)
Tij = erfc(eta * r) / r
invr3 = (Tij + 2*eta/np.sqrt(np.pi) * ekR) / r**2
Tija = -lib.einsum('ijx,ij->ijx', R, invr3)
Tijab = 3 * lib.einsum('ija,ijb,ij->ijab', R, R, 1/r**2)
Tijab -= lib.einsum('ij,ab->ijab', np.ones_like(r), np.eye(3))
invr5 = invr3 + 4/3*eta**3/np.sqrt(np.pi) * ekR # NOTE this is invr5 * r**2
Tijab = lib.einsum('ijab,ij->ijab', Tijab, invr5)
Tijab += 4/3/np.sqrt(np.pi)*lib.einsum('j,ij,ab->ijab', eta**3, ekR, np.eye(3))
invr7 = invr5 / r**2 + 8/15 / np.sqrt(np.pi) * eta**5 * ekR # NOTE this is invr7 * r**2
Tijabc = -15 * lib.einsum('ija,ijb,ijc->ijabc', R, R, R)
Tijabc = lib.einsum('ijabc,ij->ijabc', Tijabc, 1/r**2)
Tijabc += 3 * lib.einsum('ija,bc->ijabc', R, np.eye(3))
Tijabc += 3 * lib.einsum('ijb,ac->ijabc', R, np.eye(3))
Tijabc += 3 * lib.einsum('ijc,ab->ijabc', R, np.eye(3))
Tijabc = lib.einsum('ijabc,ij->ijabc', Tijabc, invr7)
Tijabc -= 8/5 / np.sqrt(np.pi) * lib.einsum('j,ij,ija,bc->ijabc', eta**5, ekR, R, np.eye(3))
Tijabc -= 8/5 / np.sqrt(np.pi) * lib.einsum('j,ij,ijb,ac->ijabc', eta**5, ekR, R, np.eye(3))
Tijabc -= 8/5 / np.sqrt(np.pi) * lib.einsum('j,ij,ijc,ab->ijabc', eta**5, ekR, R, np.eye(3))
return Tija, Tijab, Tijabc
#------ qm - mm classical ewald energy gradient ------#
all_mm_coords = lib.direct_sum('jx-Lx->Ljx', mm_coords, Lall).reshape(-1,3)
all_mm_charges = np.hstack([mm_charges] * len(Lall))
dist2 = lib.direct_sum('jx-x->jx', all_mm_coords, np.mean(qm_coords, axis=0))
dist2 = lib.einsum('jx,jx->j', dist2, dist2)
if with_mm:
mm_ewovrl_grad = np.zeros_like(all_mm_coords)
mem_avail = self.max_memory - lib.current_memory()[0] # in MB
blksize = int(mem_avail/81/(8e-6*len(all_mm_coords)))
if blksize == 0:
raise RuntimeError(f"Not enough memory, mem_avail = {mem_avail}, blkszie = {blksize}")
for i0, i1 in lib.prange(0, mol.natm, blksize):
R = qm_coords[i0:i1,None,:] - all_mm_coords[None,:,:]
r = np.linalg.norm(R, axis=-1)
r[r<1e-16] = np.inf
# substract the real-space Coulomb within rcut_hcore
mask = dist2 <= cell.rcut_hcore**2
Tija, Tijab, Tijabc = grad_Tij(R[:,mask], r[:,mask])
qm_ewovrl_grad[i0:i1] -= lib.einsum('i,ijx,j->ix', qm_charges[i0:i1], Tija, all_mm_charges[mask])
qm_ewovrl_grad[i0:i1] -= lib.einsum('ia,ijxa,j->ix', qm_dipoles[i0:i1], Tijab, all_mm_charges[mask])
qm_ewovrl_grad[i0:i1] -= lib.einsum('iab,ijxab,j->ix', qm_quads[i0:i1], Tijabc, all_mm_charges[mask]) / 3
if with_mm:
mm_ewovrl_grad[mask] += lib.einsum('i,ijx,j->jx', qm_charges[i0:i1], Tija, all_mm_charges[mask])
mm_ewovrl_grad[mask] += lib.einsum('ia,ijxa,j->jx', qm_dipoles[i0:i1], Tijab, all_mm_charges[mask])
mm_ewovrl_grad[mask] += lib.einsum('iab,ijxab,j->jx', qm_quads[i0:i1], Tijabc, all_mm_charges[mask]) / 3
# difference between MM gaussain charges and MM point charges
mask = dist2 > cell.rcut_hcore**2
min_expnt = np.min(cell.get_zetas())
max_ewrcut = pbc.gto.cell._estimate_rcut(min_expnt, 0, 1., cell.precision)
cut2 = (max_ewrcut + rmax_qm)**2
mask = mask & (dist2 <= cut2)
expnts = np.hstack([np.sqrt(cell.get_zetas())] * len(Lall))[mask]
Tija, Tijab, Tijabc = grad_kTij(R[:,mask], r[:,mask], expnts)
qm_ewovrl_grad[i0:i1] -= lib.einsum('i,ijx,j->ix', qm_charges[i0:i1], Tija, all_mm_charges[mask])
qm_ewovrl_grad[i0:i1] -= lib.einsum('ia,ijxa,j->ix', qm_dipoles[i0:i1], Tijab, all_mm_charges[mask])
qm_ewovrl_grad[i0:i1] -= lib.einsum('iab,ijxab,j->ix', qm_quads[i0:i1], Tijabc, all_mm_charges[mask]) / 3
if with_mm:
mm_ewovrl_grad[mask] += lib.einsum('i,ijx,j->jx', qm_charges[i0:i1], Tija, all_mm_charges[mask])
mm_ewovrl_grad[mask] += lib.einsum('ia,ijxa,j->jx', qm_dipoles[i0:i1], Tijab, all_mm_charges[mask])
mm_ewovrl_grad[mask] += lib.einsum('iab,ijxab,j->jx', qm_quads[i0:i1], Tijabc, all_mm_charges[mask]) / 3
# ewald real-space sum
cut2 = (ew_cut + rmax_qm)**2
mask = dist2 <= cut2
Tija, Tijab, Tijabc = grad_kTij(R[:,mask], r[:,mask], ew_eta)
qm_ewovrl_grad[i0:i1] += lib.einsum('i,ijx,j->ix', qm_charges[i0:i1], Tija, all_mm_charges[mask])
qm_ewovrl_grad[i0:i1] += lib.einsum('ia,ijxa,j->ix', qm_dipoles[i0:i1], Tijab, all_mm_charges[mask])
qm_ewovrl_grad[i0:i1] += lib.einsum('iab,ijxab,j->ix', qm_quads[i0:i1], Tijabc, all_mm_charges[mask]) / 3
if with_mm:
mm_ewovrl_grad[mask] -= lib.einsum('i,ijx,j->jx', qm_charges[i0:i1], Tija, all_mm_charges[mask])
mm_ewovrl_grad[mask] -= lib.einsum('ia,ijxa,j->jx', qm_dipoles[i0:i1], Tijab, all_mm_charges[mask])
mm_ewovrl_grad[mask] -= lib.einsum('iab,ijxab,j->jx', qm_quads[i0:i1], Tijabc, all_mm_charges[mask]) / 3
if with_mm:
mm_ewovrl_grad = mm_ewovrl_grad.reshape(len(Lall), -1, 3)
mm_ewovrl_grad = np.sum(mm_ewovrl_grad, axis=0)
all_mm_coords = all_mm_charges = None
#------ qm - qm classical ewald energy gradient ------#
R = lib.direct_sum('ix-jx->ijx', qm_coords, qm_coords)
r = np.sqrt(lib.einsum('ijx,ijx->ij', R, R))
r[r<1e-16] = 1e100
# substract the real-space Coulomb within rcut_hcore
Tija, Tijab, Tijabc = grad_Tij(R, r)
qm_ewovrl_grad -= lib.einsum('i,ijx,j->ix', qm_charges, Tija, qm_charges)
qm_ewovrl_grad += lib.einsum('i,ijxa,ja->ix', qm_charges, Tijab, qm_dipoles)
qm_ewovrl_grad -= lib.einsum('i,ijxa,ja->jx', qm_charges, Tijab, qm_dipoles) #
qm_ewovrl_grad += lib.einsum('ia,ijxab,jb->ix', qm_dipoles, Tijabc, qm_dipoles)
qm_ewovrl_grad -= lib.einsum('i,ijxab,jab->ix', qm_charges, Tijabc, qm_quads) / 3
qm_ewovrl_grad += lib.einsum('i,ijxab,jab->jx', qm_charges, Tijabc, qm_quads) / 3 #
# ewald real-space sum
# NOTE here I assume ewald real-space sum is over all qm images
# consistent with mm_mole.get_ewald_pot
R = (R[:,:,None,:] - Lall[None,None]).reshape(len(qm_coords), len(Lall)*len(qm_coords), 3)
r = np.sqrt(lib.einsum('ijx,ijx->ij', R, R))
r[r<1e-16] = 1e100
Tija, Tijab, Tijabc = grad_kTij(R, r, ew_eta)
Tija = Tija.reshape(len(qm_coords), len(qm_coords), len(Lall), 3)
Tijab = Tijab.reshape(len(qm_coords), len(qm_coords), len(Lall), 3, 3)
Tijabc = Tijabc.reshape(len(qm_coords), len(qm_coords), len(Lall), 3, 3, 3)
qm_ewovrl_grad += lib.einsum('i,ijLx,j->ix', qm_charges, Tija, qm_charges)
qm_ewovrl_grad -= lib.einsum('i,ijLxa,ja->ix', qm_charges, Tijab, qm_dipoles)
qm_ewovrl_grad += lib.einsum('i,ijLxa,ja->jx', qm_charges, Tijab, qm_dipoles) #
qm_ewovrl_grad -= lib.einsum('ia,ijLxab,jb->ix', qm_dipoles, Tijabc, qm_dipoles)
qm_ewovrl_grad += lib.einsum('i,ijLxab,jab->ix', qm_charges, Tijabc, qm_quads) / 3
qm_ewovrl_grad -= lib.einsum('i,ijLxab,jab->jx', qm_charges, Tijabc, qm_quads) / 3 #
cput2 = logger.timer(self, 'grad_ewald real-space', *cput1)
# ---------------------------------------------- #
# ---------- Ewald k-space gradient ------------ #
# ---------------------------------------------- #
mesh = cell.mesh
Gv, Gvbase, weights = cell.get_Gv_weights(mesh)
absG2 = lib.einsum('gi,gi->g', Gv, Gv)
absG2[absG2==0] = 1e200
coulG = 4*np.pi / absG2
coulG *= weights
Gpref = np.exp(-absG2/(4*ew_eta**2)) * coulG
GvRmm = lib.einsum('gx,ix->ig', Gv, mm_coords)
cosGvRmm = np.cos(GvRmm)
sinGvRmm = np.sin(GvRmm)
zcosGvRmm = lib.einsum("i,ig->g", mm_charges, cosGvRmm)
zsinGvRmm = lib.einsum("i,ig->g", mm_charges, sinGvRmm)
GvRqm = lib.einsum('gx,ix->ig', Gv, qm_coords)
cosGvRqm = np.cos(GvRqm)
sinGvRqm = np.sin(GvRqm)
zcosGvRqm = lib.einsum("i,ig->g", qm_charges, cosGvRqm)
zsinGvRqm = lib.einsum("i,ig->g", qm_charges, sinGvRqm)
DGcosGvRqm = lib.einsum("ia,ga,ig->g", qm_dipoles, Gv, cosGvRqm)
DGsinGvRqm = lib.einsum("ia,ga,ig->g", qm_dipoles, Gv, sinGvRqm)
TGGcosGvRqm = lib.einsum("iab,ga,gb,ig->g", qm_quads, Gv, Gv, cosGvRqm)
TGGsinGvRqm = lib.einsum("iab,ga,gb,ig->g", qm_quads, Gv, Gv, sinGvRqm)
qm_ewg_grad = np.zeros_like(qm_coords)
if with_mm:
mm_ewg_grad = np.zeros_like(mm_coords)
# qm pc - mm pc
p = ['einsum_path', (3, 4), (1, 3), (1, 2), (0, 1)]
qm_ewg_grad -= lib.einsum('i,gx,ig,g,g->ix', qm_charges, Gv, sinGvRqm, zcosGvRmm, Gpref, optimize=p)
qm_ewg_grad += lib.einsum('i,gx,ig,g,g->ix', qm_charges, Gv, cosGvRqm, zsinGvRmm, Gpref, optimize=p)
if with_mm:
p = ['einsum_path', (0, 2), (1, 2), (0, 2), (0, 1)]
mm_ewg_grad -= lib.einsum('i,gx,ig,g,g->ix', mm_charges, Gv, sinGvRmm, zcosGvRqm, Gpref, optimize=p)
mm_ewg_grad += lib.einsum('i,gx,ig,g,g->ix', mm_charges, Gv, cosGvRmm, zsinGvRqm, Gpref, optimize=p)
# qm dip - mm pc
p = ['einsum_path', (4, 5), (1, 4), (0, 1), (0, 2), (0, 1)]
qm_ewg_grad -= lib.einsum('ia,gx,ga,ig,g,g->ix', qm_dipoles, Gv, Gv, sinGvRqm, zsinGvRmm, Gpref, optimize=p)
qm_ewg_grad -= lib.einsum('ia,gx,ga,ig,g,g->ix', qm_dipoles, Gv, Gv, cosGvRqm, zcosGvRmm, Gpref, optimize=p)
if with_mm:
p = ['einsum_path', (1, 3), (0, 2), (0, 2), (0, 1)]
mm_ewg_grad += lib.einsum('g,j,gx,jg,g->jx', DGcosGvRqm, mm_charges, Gv, cosGvRmm, Gpref, optimize=p)
mm_ewg_grad += lib.einsum('g,j,gx,jg,g->jx', DGsinGvRqm, mm_charges, Gv, sinGvRmm, Gpref, optimize=p)
# qm quad - mm pc
p = ['einsum_path', (5, 6), (0, 5), (0, 2), (2, 3), (1, 2), (0, 1)]
qm_ewg_grad += lib.einsum('ga,gb,iab,gx,ig,g,g->ix', Gv, Gv, qm_quads,
Gv, sinGvRqm, zcosGvRmm, Gpref, optimize=p) / 3
qm_ewg_grad -= lib.einsum('ga,gb,iab,gx,ig,g,g->ix', Gv, Gv, qm_quads,
Gv, cosGvRqm, zsinGvRmm, Gpref, optimize=p) / 3
if with_mm:
p = ['einsum_path', (1, 3), (0, 2), (0, 2), (0, 1)]
mm_ewg_grad += lib.einsum('g,j,gx,jg,g->jx', TGGcosGvRqm, mm_charges, Gv, sinGvRmm, Gpref, optimize=p) / 3
mm_ewg_grad -= lib.einsum('g,j,gx,jg,g->jx', TGGsinGvRqm, mm_charges, Gv, cosGvRmm, Gpref, optimize=p) / 3
# qm pc - qm pc
p = ['einsum_path', (3, 4), (1, 3), (1, 2), (0, 1)]
qm_ewg_grad -= lib.einsum('i,gx,ig,g,g->ix', qm_charges, Gv, sinGvRqm, zcosGvRqm, Gpref, optimize=p)
qm_ewg_grad += lib.einsum('i,gx,ig,g,g->ix', qm_charges, Gv, cosGvRqm, zsinGvRqm, Gpref, optimize=p)
# qm pc - qm dip
qm_ewg_grad += lib.einsum('i,gx,ig,g,g->ix', qm_charges, Gv, cosGvRqm, DGcosGvRqm, Gpref, optimize=p)
qm_ewg_grad += lib.einsum('i,gx,ig,g,g->ix', qm_charges, Gv, sinGvRqm, DGsinGvRqm, Gpref, optimize=p)
p = ['einsum_path', (3, 5), (1, 4), (1, 3), (1, 2), (0, 1)]
qm_ewg_grad -= lib.einsum('ja,ga,gx,g,jg,g->jx', qm_dipoles, Gv, Gv, zsinGvRqm, sinGvRqm, Gpref, optimize=p)
qm_ewg_grad -= lib.einsum('ja,ga,gx,g,jg,g->jx', qm_dipoles, Gv, Gv, zcosGvRqm, cosGvRqm, Gpref, optimize=p)
# qm dip - qm dip
p = ['einsum_path', (4, 5), (1, 4), (1, 3), (1, 2), (0, 1)]
qm_ewg_grad -= lib.einsum('ia,ga,gx,ig,g,g->ix', qm_dipoles, Gv, Gv, sinGvRqm, DGcosGvRqm, Gpref, optimize=p)
qm_ewg_grad += lib.einsum('ia,ga,gx,ig,g,g->ix', qm_dipoles, Gv, Gv, cosGvRqm, DGsinGvRqm, Gpref, optimize=p)
# qm pc - qm quad
p = ['einsum_path', (3, 4), (1, 3), (1, 2), (0, 1)]
qm_ewg_grad += lib.einsum('i,gx,ig,g,g->ix', qm_charges, Gv, sinGvRqm, TGGcosGvRqm, Gpref, optimize=p) / 3
qm_ewg_grad -= lib.einsum('i,gx,ig,g,g->ix', qm_charges, Gv, cosGvRqm, TGGsinGvRqm, Gpref, optimize=p) / 3
p = ['einsum_path', (4, 6), (1, 5), (1, 2), (2, 3), (1, 2), (0, 1)]
qm_ewg_grad += lib.einsum('jab,ga,gb,gx,g,jg,g->jx', qm_quads, Gv, Gv,
Gv, zcosGvRqm, sinGvRqm, Gpref, optimize=p) / 3
qm_ewg_grad -= lib.einsum('jab,ga,gb,gx,g,jg,g->jx', qm_quads, Gv, Gv,
Gv, zsinGvRqm, cosGvRqm, Gpref, optimize=p) / 3
logger.timer(self, 'grad_ewald k-space', *cput2)
logger.timer(self, 'grad_ewald', *cput0)
if not with_mm:
return qm_multipole_grad + qm_ewovrl_grad + qm_ewg_grad
else:
return qm_multipole_grad + qm_ewovrl_grad + qm_ewg_grad, \
mm_ewovrl_grad + mm_ewg_grad
[docs]
def get_hcore(self, mol=None):
cput0 = (logger.process_clock(), logger.perf_counter())
mm_mol = self.base.mm_mol
if mol is None:
mol = self.mol
rcut_hcore = mm_mol.rcut_hcore
coords = mm_mol.atom_coords()
charges = mm_mol.atom_charges()
Ls = mm_mol.get_lattice_Ls()
qm_center = np.mean(mol.atom_coords(), axis=0)
all_coords = lib.direct_sum('ix+Lx->Lix',
mm_mol.atom_coords(), Ls).reshape(-1,3)
all_charges = np.hstack([mm_mol.atom_charges()] * len(Ls))
dist2 = all_coords - qm_center
dist2 = lib.einsum('ix,ix->i', dist2, dist2)
# charges within rcut_hcore exactly go into hcore
mask = dist2 <= rcut_hcore**2
charges = all_charges[mask]
coords = all_coords[mask]
nao = mol.nao
max_memory = self.max_memory - lib.current_memory()[0]
blksize = int(min(max_memory*1e6/8/nao**2/3, 200))
blksize = max(blksize, 1)
g_qm = super().get_hcore(mol)
if mm_mol.charge_model == 'gaussian':
expnts = np.hstack([mm_mol.get_zetas()] * len(Ls))[mask]
if mol.cart:
intor = 'int3c2e_ip1_cart'
else:
intor = 'int3c2e_ip1_sph'
cintopt = gto.moleintor.make_cintopt(mol._atm, mol._bas,
mol._env, intor)
v = 0
for i0, i1 in lib.prange(0, charges.size, blksize):
fakemol = gto.fakemol_for_charges(coords[i0:i1], expnts[i0:i1])
j3c = df.incore.aux_e2(mol, fakemol, intor, aosym='s1',
comp=3, cintopt=cintopt)
v += lib.einsum('ipqk,k->ipq', j3c, charges[i0:i1])
g_qm += v
else:
for i0, i1 in lib.prange(0, charges.size, blksize):
j3c = mol.intor('int1e_grids_ip', grids=coords[i0:i1])
g_qm += lib.einsum('ikpq,k->ipq', j3c, charges[i0:i1])
logger.timer(self, 'get_hcore', *cput0)
return g_qm
[docs]
def grad_hcore_mm(self, dm, mol=None):
r'''Nuclear gradients of the electronic energy
'''
cput0 = (logger.process_clock(), logger.perf_counter())
mm_mol = self.base.mm_mol
if mol is None:
mol = self.mol
rcut_hcore = mm_mol.rcut_hcore
coords = mm_mol.atom_coords()
charges = mm_mol.atom_charges()
expnts = mm_mol.get_zetas()
Ls = mm_mol.get_lattice_Ls()
qm_center = np.mean(mol.atom_coords(), axis=0)
all_coords = lib.direct_sum('ix+Lx->Lix',
mm_mol.atom_coords(), Ls).reshape(-1,3)
all_charges = np.hstack([mm_mol.atom_charges()] * len(Ls))
all_expnts = np.hstack([expnts] * len(Ls))
dist2 = all_coords - qm_center
dist2 = lib.einsum('ix,ix->i', dist2, dist2)
# charges within rcut_hcore exactly go into hcore
mask = dist2 <= rcut_hcore**2
charges = all_charges[mask]
coords = all_coords[mask]
expnts = all_expnts[mask]
intor = 'int3c2e_ip2'
nao = mol.nao
max_memory = self.max_memory - lib.current_memory()[0]
blksize = int(min(max_memory*1e6/8/nao**2/3, 200))
blksize = max(blksize, 1)
cintopt = gto.moleintor.make_cintopt(mol._atm, mol._bas,
mol._env, intor)
g = np.zeros_like(all_coords)
g_ = np.zeros_like(coords)
for i0, i1 in lib.prange(0, charges.size, blksize):
fakemol = gto.fakemol_for_charges(coords[i0:i1], expnts[i0:i1])
j3c = df.incore.aux_e2(mol, fakemol, intor, aosym='s1',
comp=3, cintopt=cintopt)
g_[i0:i1] = lib.einsum('ipqk,qp->ik', j3c * charges[i0:i1], dm).T
g[mask] = g_
g = g.reshape(len(Ls), -1, 3)
g = np.sum(g, axis=0)
logger.timer(self, 'grad_hcore_mm', *cput0)
return g
contract_hcore_mm = grad_hcore_mm
[docs]
def grad_nuc(self, mol=None, atmlst=None):
cput0 = (logger.process_clock(), logger.perf_counter())
assert atmlst is None # atmlst needs to be full for computing g_mm
if mol is None: mol = self.mol
mm_mol = self.base.mm_mol
coords = mm_mol.atom_coords()
charges = mm_mol.atom_charges()
Ls = mm_mol.get_lattice_Ls()
qm_center = np.mean(mol.atom_coords(), axis=0)
all_coords = lib.direct_sum('ix+Lx->Lix',
mm_mol.atom_coords(), Ls).reshape(-1,3)
all_charges = np.hstack([mm_mol.atom_charges()] * len(Ls))
all_expnts = np.hstack([np.sqrt(mm_mol.get_zetas())] * len(Ls))
dist2 = all_coords - qm_center
dist2 = lib.einsum('ix,ix->i', dist2, dist2)
mask = dist2 <= mm_mol.rcut_hcore**2
charges = all_charges[mask]
coords = all_coords[mask]
expnts = all_expnts[mask]
g_qm = super().grad_nuc(mol, atmlst)
g_mm = np.zeros_like(all_coords)
for i in range(mol.natm):
q1 = mol.atom_charge(i)
r1 = mol.atom_coord(i)
r = lib.norm(r1-coords, axis=1)
g_mm_ = q1 * lib.einsum('i,ix,i->ix', charges, r1-coords, erf(expnts*r)/r**3)
g_mm_ -= q1 * lib.einsum('i,ix,i->ix', charges * expnts * 2 / np.sqrt(np.pi),
r1-coords, np.exp(-expnts**2 * r**2)/r**2)
g_mm[mask] += g_mm_
g_qm[i] -= np.sum(g_mm_, axis=0)
g_mm = g_mm.reshape(len(Ls), -1, 3)
g_mm = np.sum(g_mm, axis=0)
self.de_nuc_mm = g_mm
logger.timer(self, 'grad_nuc', *cput0)
return g_qm
[docs]
def grad_nuc_mm(self, mol=None):
if self.de_nuc_mm is not None:
return self.de_nuc_mm
cput0 = (logger.process_clock(), logger.perf_counter())
if mol is None:
mol = self.mol
mm_mol = self.base.mm_mol
coords = mm_mol.atom_coords()
charges = mm_mol.atom_charges()
Ls = mm_mol.get_lattice_Ls()
qm_center = np.mean(mol.atom_coords(), axis=0)
all_coords = lib.direct_sum('ix+Lx->Lix',
mm_mol.atom_coords(), Ls).reshape(-1,3)
all_charges = np.hstack([mm_mol.atom_charges()] * len(Ls))
all_expnts = np.hstack([np.sqrt(mm_mol.get_zetas())] * len(Ls))
dist2 = all_coords - qm_center
dist2 = lib.einsum('ix,ix->i', dist2, dist2)
mask = dist2 <= mm_mol.rcut_hcore**2
charges = all_charges[mask]
coords = all_coords[mask]
expnts = all_expnts[mask]
g_mm = np.zeros_like(all_coords)
for i in range(mol.natm):
q1 = mol.atom_charge(i)
r1 = mol.atom_coord(i)
r = lib.norm(r1-coords, axis=1)
g_mm[mask] += q1 * lib.einsum('i,ix,i->ix', charges, r1-coords, erf(expnts*r)/r**3)
g_mm[mask] -= q1 * lib.einsum('i,ix,i->ix', charges * expnts * 2 / np.sqrt(np.pi),
r1-coords, np.exp(-expnts**2 * r**2)/r**2)
g_mm = g_mm.reshape(len(Ls), -1, 3)
g_mm = np.sum(g_mm, axis=0)
logger.timer(self, 'grad_nuc_mm', *cput0)
return g_mm
def _finalize(self):
g_ewald_qm, self.de_ewald_mm = self.grad_ewald(with_mm=True)
self.de += g_ewald_qm
super()._finalize()