#!/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.
import numpy as np
from pyscf import lib
from pyscf.lib import logger
from pyscf.pbc.tools import pbc as pbctools
from pyscf.pbc.dft.gen_grid import UniformGrids
from pyscf.pbc.df import FFTDF
from pyscf.pbc.dft.numint import NumInt, _contract_rho
from pyscf.pbc.grad import uks as uks_grad
from pyscf.pbc.grad.rks_stress import (
strain_tensor_dispalcement,
_finite_diff_cells,
_get_weight_strain_derivatives,
_get_coulG_strain_derivatives,
_eval_ao_strain_derivatives,
_get_vpplocG_strain_derivatives,
_get_pp_nonloc_strain_derivatives,
ewald)
[docs]
def get_vxc(ks_grad, cell, dm, with_j=False, with_nuc=False):
'''Strain derivatives for Coulomb and XC at gamma point
Kwargs:
with_j : Whether to include the electron-electron Coulomb interactions
with_nuc : Whether to include the electron-nuclear Coulomb interactions
'''
mf = ks_grad.base
if dm is None: dm = mf.make_rdm1()
assert cell.low_dim_ft_type != 'inf_vacuum'
assert cell.dimension != 1
ni = mf._numint
assert isinstance(ni, NumInt)
if ks_grad.grids is not None:
grids = ks_grad.grids
else:
grids = mf.grids
assert isinstance(grids, UniformGrids)
xc_code = mf.xc
xctype = ni._xc_type(xc_code)
if xctype == 'LDA':
deriv = 0
nvar = 1
elif xctype == 'GGA':
deriv = 1
nvar = 4
elif xctype == 'MGGA':
deriv = 1
nvar = 5
else:
raise NotImplementedError
assert dm.ndim == 3
nao = dm.shape[1]
coords = grids.coords
ngrids = len(coords)
mesh = grids.mesh
weight_0, weight_1 = _get_weight_strain_derivatives(cell, grids)
out = np.zeros((3,3))
rho0 = np.zeros((2, nvar, ngrids))
rho1 = np.zeros((3,3, 2, nvar, ngrids))
def partial_dot(bra, ket):
'''conj(ig),ig->g'''
return _contract_rho(bra.T, ket.T)
XY, YY, ZY, XZ, YZ, ZZ = 5, 7, 8, 6, 8, 9
p1 = 0
for ao, _, mask, weight, coords \
in ni.block_loop(cell, grids, nao, deriv+1):
p0, p1 = p1, p1 + weight.size
ao_strain = _eval_ao_strain_derivatives(cell, coords, deriv=deriv)[0]
ao = ao.transpose(0,2,1)
ao_strain = ao_strain.transpose(0,1,2,4,3)
coordsT = np.asarray(coords.T, order='C')
if xctype == 'LDA':
ao1 = ao_strain[:,:,0]
# Adding the response of the grids
ao1 += np.einsum('xig,yg->xyig', ao[1:4], coordsT)
for s in range(2):
c0 = dm[s].T.dot(ao[0])
rho0[s,0,p0:p1] += partial_dot(ao[0], c0).real
rho1[:,:,s,0,p0:p1] += np.einsum('xyig,ig->xyg', ao1, c0.conj()).real
elif xctype == 'GGA':
ao_strain[:,:,0] += np.einsum('xig,yg->xyig', ao[1:4], coordsT)
ao_strain[:,:,1] += np.einsum('xig,yg->xyig', ao[4:7], coordsT)
ao_strain[0,:,2] += np.einsum('ig,yg->yig', ao[XY], coordsT)
ao_strain[1,:,2] += np.einsum('ig,yg->yig', ao[YY], coordsT)
ao_strain[2,:,2] += np.einsum('ig,yg->yig', ao[ZY], coordsT)
ao_strain[0,:,3] += np.einsum('ig,yg->yig', ao[XZ], coordsT)
ao_strain[1,:,3] += np.einsum('ig,yg->yig', ao[YZ], coordsT)
ao_strain[2,:,3] += np.einsum('ig,yg->yig', ao[ZZ], coordsT)
c0 = lib.einsum('xig,sij->sxjg', ao[:4], dm)
for s in range(2):
for i in range(4):
rho0[s,i,p0:p1] += partial_dot(ao[0], c0[s,i]).real
# TODO: computing density derivatives using FFT
rho1[:,:,s, : ,p0:p1] += np.einsum('xynig,ig->xyng', ao_strain, c0[s,0].conj()).real
rho1[:,:,s,1:4,p0:p1] += np.einsum('xyig,nig->xyng', ao_strain[:,:,0], c0[s,1:4].conj()).real
else: # MGGA
ao_strain[:,:,0] += np.einsum('xig,yg->xyig', ao[1:4], coordsT)
ao_strain[:,:,1] += np.einsum('xig,yg->xyig', ao[4:7], coordsT)
ao_strain[0,:,2] += np.einsum('ig,yg->yig', ao[XY], coordsT)
ao_strain[1,:,2] += np.einsum('ig,yg->yig', ao[YY], coordsT)
ao_strain[2,:,2] += np.einsum('ig,yg->yig', ao[ZY], coordsT)
ao_strain[0,:,3] += np.einsum('ig,yg->yig', ao[XZ], coordsT)
ao_strain[1,:,3] += np.einsum('ig,yg->yig', ao[YZ], coordsT)
ao_strain[2,:,3] += np.einsum('ig,yg->yig', ao[ZZ], coordsT)
c0 = lib.einsum('xig,sij->sxjg', ao[:4], dm)
for s in range(2):
for i in range(4):
rho0[s,i,p0:p1] += partial_dot(ao[0], c0[s,i]).real
rho0[s,4,p0:p1] += partial_dot(ao[1], c0[s,1]).real
rho0[s,4,p0:p1] += partial_dot(ao[2], c0[s,2]).real
rho0[s,4,p0:p1] += partial_dot(ao[3], c0[s,3]).real
rho1[:,:,s, :4,p0:p1] += np.einsum('xynig,ig->xyng', ao_strain, c0[s,0].conj()).real
rho1[:,:,s,1:4,p0:p1] += np.einsum('xyig,nig->xyng', ao_strain[:,:,0], c0[s,1:4].conj()).real
rho1[:,:,s,4,p0:p1] += np.einsum('xynig,nig->xyg', ao_strain[:,:,1:4], c0[s,1:4].conj()).real
if xctype == 'LDA':
pass
elif xctype == 'GGA':
rho0[:,1:4] *= 2 # dm should be hermitian
else: # MGGA
rho0[:,1:4] *= 2 # dm should be hermitian
rho0[:,4] *= .5 # factor 1/2 for tau
rho1[:,:,:,4] *= .5
# *2 for rho1 because the derivatives were applied to the bra only
rho1 *= 2.
exc, vxc = ni.eval_xc_eff(xc_code, rho0, 1, xctype=xctype, spin=1)[:2]
out += np.einsum('xysng,sng->xy', rho1, vxc).real * weight_0
rho0 = rho0[:,0].sum(axis=0)
rho1 = rho1[:,:,:,0].sum(axis=2)
out += np.einsum('g,g->', rho0, exc.ravel()).real * weight_1
Gv = cell.get_Gv(mesh)
coulG_0, coulG_1 = _get_coulG_strain_derivatives(cell, Gv)
rhoG = pbctools.fft(rho0, mesh)
if with_j:
vR = pbctools.ifft(rhoG * coulG_0, mesh)
EJ = np.einsum('xyg,g->xy', rho1, vR).real * weight_0 * 2
EJ += np.einsum('g,g->', rho0, vR).real * weight_1
EJ += np.einsum('xyg,g->xy', coulG_1, rhoG.conj()*rhoG).real * (weight_0/ngrids)
out += .5 * EJ
if with_nuc:
if cell._pseudo:
vpplocG_0, vpplocG_1 = _get_vpplocG_strain_derivatives(cell, mesh)
vpplocR = pbctools.ifft(vpplocG_0, mesh).real
Ene = np.einsum('xyg,g->xy', rho1, vpplocR).real
Ene += np.einsum('g,xyg->xy', rhoG.conj(), vpplocG_1).real * (1./ngrids)
Ene += _get_pp_nonloc_strain_derivatives(cell, mesh, dm.sum(axis=0))
else:
charge = -cell.atom_charges()
# SI corresponds to Fourier components of the fractional atomic
# positions within the cell. It does not respond to the strain
# transformation
SI = cell.get_SI(mesh=mesh)
ZG = np.dot(charge, SI)
vR = pbctools.ifft(ZG * coulG_0, mesh).real
Ene = np.einsum('xyg,g->xy', rho1, vR).real
Ene += np.einsum('xyg,g->xy', coulG_1, rhoG.conj()*ZG).real * (1./ngrids)
out += Ene
return out
[docs]
def kernel(mf_grad):
'''Compute the energy derivatives for strain tensor (e_ij)
1 d E
sigma_ij = --- ------
V d e_ij
sigma is a asymmetric 3x3 matrix. The symmetric stress tensor in the 6 Voigt
notation can be transformed from the asymmetric stress tensor
sigma1 = sigma_11
sigma2 = sigma_22
sigma3 = sigma_33
sigma6 = (sigma_12 + sigma_21)/2
sigma5 = (sigma_13 + sigma_31)/2
sigma4 = (sigma_23 + sigma_32)/2
See K. Doll, Mol Phys (2010), 108, 223
'''
assert isinstance(mf_grad, uks_grad.Gradients)
mf = mf_grad.base
with_df = mf.with_df
assert isinstance(with_df, FFTDF)
ni = mf._numint
if ni.libxc.is_hybrid_xc(mf.xc):
raise NotImplementedError('Stress tensor for hybrid DFT')
if hasattr(mf, 'U_idx'):
raise NotImplementedError('Stress tensor for DFT+U')
log = logger.new_logger(mf_grad)
t0 = (logger.process_clock(), logger.perf_counter())
log.debug('Computing stress tensor')
cell = mf.cell
dm0 = mf.make_rdm1().sum(axis=0)
dme0 = mf_grad.make_rdm1e().sum(axis=0)
sigma = ewald(cell)
disp = 1e-5
for x in range(3):
for y in range(3):
cell1, cell2 = _finite_diff_cells(cell, x, y, disp)
t1 = cell1.pbc_intor('int1e_kin')
t2 = cell2.pbc_intor('int1e_kin')
t1 = np.einsum('ij,ji->', t1, dm0)
t2 = np.einsum('ij,ji->', t2, dm0)
sigma[x,y] += (t1 - t2) / (2*disp)
s1 = cell1.pbc_intor('int1e_ovlp')
s2 = cell2.pbc_intor('int1e_ovlp')
s1 = np.einsum('ij,ji->', s1, dme0)
s2 = np.einsum('ij,ji->', s2, dme0)
sigma[x,y] -= (s1 - s2) / (2*disp)
t0 = log.timer_debug1('hcore derivatives', *t0)
dm0 = mf.make_rdm1()
sigma += get_vxc(mf_grad, cell, dm0, with_j=True, with_nuc=True)
t0 = log.timer_debug1('Vxc and Coulomb derivatives', *t0)
sigma /= cell.vol
if log.verbose >= logger.DEBUG:
log.debug('Asymmetric strain tensor')
log.debug('%s', sigma)
return sigma