#!/usr/bin/env python
# Copyright 2021-2024 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: Xing Zhang <zhangxing.nju@gmail.com>
#
import ctypes
import numpy as np
import scipy
from pyscf import __config__
from pyscf import lib
from pyscf.lib import logger
from pyscf.gto import mole
from pyscf.pbc import tools
libpbc = lib.load_library('libpbc')
INTERPOLATION_ORDER = getattr(__config__, 'pyscf_pbc_ewald_bspline_order', 10)
def _bspline(u, n=4):
fac = 1. / scipy.special.factorial(n-1)
M = 0
for k in range(n+1):
fac1 = ((-1)**k) * scipy.special.binom(n, k)
M += fac1 * ((np.maximum(u-k, 0)) ** (n-1))
M *= fac
return M
def _bspline_grad(u, n=4):
r'''
... math::
\frac{dM}{du} = M_{n-1}(u) - M_{n-1}(u-1)
'''
dMdu = _bspline(u, n-1) - _bspline(u-1, n-1)
return dMdu
[docs]
def bspline(u, ng, n=4, deriv=0):
u = np.asarray(u).ravel()
u_floor = np.floor(u)
delta = u - u_floor
idx = []
val = []
for i in range(n):
idx.append(np.rint((u_floor - i) % ng).astype(int))
val.append(delta + i)
M = np.zeros((u.size, ng))
for i in range(n):
M[np.arange(u.size),idx[i]] += _bspline(val[i], n)
if deriv > 0:
if deriv > 1:
raise NotImplementedError
dM = np.zeros((u.size, ng))
for i in range(n):
dM[np.arange(u.size),idx[i]] += _bspline_grad(val[i], n)
M = [M, dM]
m = np.arange(ng)
b = np.exp(2*np.pi*1j*(n-1)*m/ng)
tmp = 0
for k in range(n-1):
tmp += _bspline(k+1, n) * np.exp(2*np.pi*1j*m*k/ng)
b /= tmp
if n % 2 > 0 and ng % 2 == 0 :
b[ng//2] = 0
return M, b, idx
def _get_ewald_direct(cell, ew_eta=None, ew_cut=None):
if ew_eta is None or ew_cut is None:
ew_eta, ew_cut = cell.get_ewald_params()
chargs = np.asarray(cell.atom_charges(), order='C', dtype=float)
coords = np.asarray(cell.atom_coords(), order='C')
Lall = np.asarray(cell.get_lattice_Ls(rcut=ew_cut), order='C')
natm = len(chargs)
nL = len(Lall)
ewovrl = np.zeros([1])
fun = getattr(libpbc, "get_ewald_direct")
fun(ewovrl.ctypes.data_as(ctypes.c_void_p),
chargs.ctypes.data_as(ctypes.c_void_p),
coords.ctypes.data_as(ctypes.c_void_p),
Lall.ctypes.data_as(ctypes.c_void_p),
ctypes.c_double(ew_eta), ctypes.c_double(ew_cut),
ctypes.c_int(natm), ctypes.c_int(nL))
return ewovrl[0]
def _get_ewald_direct_nuc_grad(cell, ew_eta=None, ew_cut=None):
if ew_eta is None or ew_cut is None:
ew_eta, ew_cut = cell.get_ewald_params()
chargs = np.asarray(cell.atom_charges(), order='C', dtype=float)
coords = np.asarray(cell.atom_coords(), order='C')
Lall = np.asarray(cell.get_lattice_Ls(rcut=ew_cut), order='C')
natm = len(chargs)
nL = len(Lall)
grad = np.zeros([natm,3], order='C', dtype=float)
fun = getattr(libpbc, "get_ewald_direct_nuc_grad")
fun(grad.ctypes.data_as(ctypes.c_void_p),
chargs.ctypes.data_as(ctypes.c_void_p),
coords.ctypes.data_as(ctypes.c_void_p),
Lall.ctypes.data_as(ctypes.c_void_p),
ctypes.c_double(ew_eta), ctypes.c_double(ew_cut),
ctypes.c_int(natm), ctypes.c_int(nL))
return grad
# FIXME The default interpolation order may be too high
[docs]
def particle_mesh_ewald(cell, ew_eta=None, ew_cut=None,
order=INTERPOLATION_ORDER):
if cell.dimension != 3:
raise NotImplementedError("Particle mesh ewald only works for 3D.")
chargs = cell.atom_charges()
coords = cell.atom_coords()
natm = len(coords)
if ew_eta is None or ew_cut is None:
ew_eta, ew_cut = cell.get_ewald_params()
log_precision = np.log(cell.precision / (chargs.sum()*16*np.pi**2))
ke_cutoff = -2*ew_eta**2*log_precision
mesh = cell.cutoff_to_mesh(ke_cutoff)
ewovrl = _get_ewald_direct(cell, ew_eta, ew_cut)
ewself = -.5 * np.dot(chargs,chargs) * 2 * ew_eta / np.sqrt(np.pi)
if cell.dimension == 3:
ewself += -.5 * np.sum(chargs)**2 * np.pi/(ew_eta**2 * cell.vol)
b = cell.reciprocal_vectors(norm_to=1)
u = np.dot(coords, b.T) * mesh[None,:]
Mx, bx, idx = bspline(u[:,0], mesh[0], order)
My, by, idy = bspline(u[:,1], mesh[1], order)
Mz, bz, idz = bspline(u[:,2], mesh[2], order)
idx = np.asarray(idx).T
idy = np.asarray(idy).T
idz = np.asarray(idz).T
Mx_s = Mx[np.arange(natm)[:,None], idx]
My_s = My[np.arange(natm)[:,None], idy]
Mz_s = Mz[np.arange(natm)[:,None], idz]
#:Q = np.einsum('i,ix,iy,iz->xyz', chargs, Mx, My, Mz)
Q = np.zeros([*mesh])
for ia in range(len(chargs)):
Q_s = np.einsum('x,y,z->xyz', Mx_s[ia], My_s[ia], Mz_s[ia])
Q[np.ix_(idx[ia], idy[ia], idz[ia])] += chargs[ia] * Q_s
B = np.einsum('x,y,z->xyz', bx*bx.conj(), by*by.conj(), bz*bz.conj())
Gv, Gvbase, weights = cell.get_Gv_weights(mesh)
absG2 = np.einsum('ix,ix->i', Gv, Gv)
absG2[absG2==0] = 1e200
coulG = 4*np.pi / absG2
C = weights * coulG * np.exp(-absG2/(4*ew_eta**2))
C = C.reshape(*mesh)
Q_ifft = tools.ifft(Q, mesh).reshape(*mesh)
tmp = tools.fft(B * C * Q_ifft, mesh).real.reshape(*mesh)
ewg = 0.5 * np.prod(mesh) * np.einsum('xyz,xyz->', Q, tmp)
logger.debug(cell, 'Ewald components = %.15g, %.15g, %.15g', ewovrl, ewself, ewg)
return ewovrl + ewself + ewg
[docs]
def particle_mesh_ewald_nuc_grad(cell, ew_eta=None, ew_cut=None,
order=INTERPOLATION_ORDER):
if cell.dimension != 3:
raise NotImplementedError("Particle mesh ewald only works for 3D.")
chargs = cell.atom_charges()
coords = cell.atom_coords()
if ew_eta is None or ew_cut is None:
ew_eta, ew_cut = cell.get_ewald_params()
log_precision = np.log(cell.precision / (chargs.sum()*16*np.pi**2))
ke_cutoff = -2*ew_eta**2*log_precision
mesh = cell.cutoff_to_mesh(ke_cutoff)
grad_dir = _get_ewald_direct_nuc_grad(cell, ew_eta, ew_cut)
b = cell.reciprocal_vectors(norm_to=1)
u = np.dot(coords, b.T) * mesh[None,:]
[Mx, dMx], bx, idx = bspline(u[:,0], mesh[0], order, deriv=1)
[My, dMy], by, idy = bspline(u[:,1], mesh[1], order, deriv=1)
[Mz, dMz], bz, idz = bspline(u[:,2], mesh[2], order, deriv=1)
idx = np.asarray(idx).T
idy = np.asarray(idy).T
idz = np.asarray(idz).T
Mx_s = Mx[np.indices(idx.shape)[0], idx]
My_s = My[np.indices(idy.shape)[0], idy]
Mz_s = Mz[np.indices(idz.shape)[0], idz]
dMx_s = dMx[np.indices(idx.shape)[0], idx]
dMy_s = dMy[np.indices(idy.shape)[0], idy]
dMz_s = dMz[np.indices(idz.shape)[0], idz]
Q = np.zeros([*mesh])
for ia in range(len(chargs)):
Q_s = np.einsum('x,y,z->xyz', Mx_s[ia], My_s[ia], Mz_s[ia])
Q[np.ix_(idx[ia], idy[ia], idz[ia])] += chargs[ia] * Q_s
B = np.einsum('x,y,z->xyz', bx*bx.conj(), by*by.conj(), bz*bz.conj())
Gv, Gvbase, weights = cell.get_Gv_weights(mesh)
absG2 = np.einsum('ix,ix->i', Gv, Gv)
absG2[absG2==0] = 1e200
coulG = 4*np.pi / absG2
C = weights * coulG * np.exp(-absG2/(4*ew_eta**2))
C = C.reshape(*mesh)
Q_ifft = tools.ifft(Q, mesh).reshape(*mesh)
tmp = tools.fft(B * C * Q_ifft, mesh).real.reshape(*mesh)
ng = np.prod(mesh)
bK = b * mesh[:,None]
grad_rec = np.zeros_like(grad_dir)
for ia in range(len(chargs)):
mask = np.ix_(idx[ia], idy[ia], idz[ia])
dQ_s = np.einsum('x,y,z->xyz', dMx_s[ia], My_s[ia], Mz_s[ia])
dQdr = np.einsum('x,abc->xabc', bK[0], dQ_s)
grad_rec[ia] += np.einsum('xabc,abc->x', dQdr, tmp[mask])
dQ_s = np.einsum('x,y,z->xyz', Mx_s[ia], dMy_s[ia], Mz_s[ia])
dQdr = np.einsum('x,abc->xabc', bK[1], dQ_s)
grad_rec[ia] += np.einsum('xabc,abc->x', dQdr, tmp[mask])
dQ_s = np.einsum('x,y,z->xyz', Mx_s[ia], My_s[ia], dMz_s[ia])
dQdr = np.einsum('x,abc->xabc', bK[2], dQ_s)
grad_rec[ia] += np.einsum('xabc,abc->x', dQdr, tmp[mask])
grad_rec[ia] *= chargs[ia] * ng
# reciprocal space summation does not conserve momentum
shift = -np.sum(grad_rec, axis=0) / len(grad_rec)
logger.debug(cell, f'Shift ewald nuclear gradient by {shift} to keep momentum conservation.')
grad_rec += shift[None,:]
grad = grad_dir + grad_rec
return grad
[docs]
def ewald_nuc_grad(cell, ew_eta=None, ew_cut=None):
chargs = np.asarray(cell.atom_charges(), order='C', dtype=float)
coords = np.asarray(cell.atom_coords(), order='C')
if ew_eta is None or ew_cut is None:
ew_eta, ew_cut = cell.get_ewald_params()
log_precision = np.log(cell.precision / (chargs.sum()*16*np.pi**2))
ke_cutoff = -2*ew_eta**2*log_precision
mesh = cell.cutoff_to_mesh(ke_cutoff)
if cell.dimension == 3 and cell.use_particle_mesh_ewald:
return particle_mesh_ewald_nuc_grad(cell, ew_eta=ew_eta, ew_cut=ew_cut)
grad_dir = _get_ewald_direct_nuc_grad(cell, ew_eta, ew_cut)
grad_rec = np.zeros_like(grad_dir, order="C")
Gv, _, weights = cell.get_Gv_weights(mesh)
fn = getattr(libpbc, "ewald_gs_nuc_grad")
if cell.dimension != 2 or cell.low_dim_ft_type == 'inf_vacuum':
ngrids = len(Gv)
mem_avail = cell.max_memory - lib.current_memory()[0]
if mem_avail <= 0:
logger.warn(cell, "Not enough memory for computing ewald force.")
blksize = min(ngrids, max(mesh[2], int(mem_avail*1e6 / ((2+cell.natm*2)*8))))
for ig0, ig1 in lib.prange(0, ngrids, blksize):
ngrid_sub = ig1 - ig0
Gv_sub = np.asarray(Gv[ig0:ig1], order="C")
fn(grad_rec.ctypes.data_as(ctypes.c_void_p),
Gv_sub.ctypes.data_as(ctypes.c_void_p),
chargs.ctypes.data_as(ctypes.c_void_p),
coords.ctypes.data_as(ctypes.c_void_p),
ctypes.c_double(ew_eta), ctypes.c_double(weights),
ctypes.c_int(cell.natm), ctypes.c_size_t(ngrid_sub))
else:
raise NotImplementedError
grad = grad_dir + grad_rec
return grad