Source code for pyscf.pbc.grad.kuks_stress

#!/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 KNumInt, _contract_rho
from pyscf.pbc.dft.krkspu import _set_U, _make_minao_lo, reference_mol
from pyscf.pbc.grad.krks_stress import get_ovlp, _get_first_order_local_orbitals
from pyscf.pbc.grad import kuks as kuks_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_kpts, kpts, 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_kpts is None: dm_kpts = mf.make_rdm1() assert cell.low_dim_ft_type != 'inf_vacuum' assert cell.dimension != 1 ni = mf._numint assert isinstance(ni, KNumInt) 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 kpts.ndim == 2 assert dm_kpts.ndim == 4 nkpts, nao = dm_kpts.shape[1:3] assert nkpts == len(kpts) 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_ks, _, mask, weight, coords \ in ni.block_loop(cell, grids, nao, deriv+1, kpts=kpts): p0, p1 = p1, p1 + weight.size ao_ks_strain = _eval_ao_strain_derivatives(cell, coords, kpts, deriv=deriv) coordsT = coords.T for k in range(nkpts): dm = dm_kpts[:,k] ao = ao_ks[k].transpose(0,2,1) ao_strain = ao_ks_strain[k].transpose(0,1,2,4,3) 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 rho0 *= 1./nkpts # *2 for rho1 because the derivatives were applied to the bra only rho1 *= 2./nkpts 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_kpts.sum(axis=0), kpts) 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, kuks_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') 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) kpts = mf.kpts scaled_kpts = kpts.dot(cell.lattice_vectors().T) nkpts = len(kpts) disp = 1e-5 for x in range(3): for y in range(3): cell1, cell2 = _finite_diff_cells(cell, x, y, disp) kpts1 = scaled_kpts.dot(cell1.reciprocal_vectors(norm_to=1)) kpts2 = scaled_kpts.dot(cell2.reciprocal_vectors(norm_to=1)) t1 = cell1.pbc_intor('int1e_kin', kpts=kpts1) t2 = cell2.pbc_intor('int1e_kin', kpts=kpts2) t1 = sum(np.einsum('ij,ji->', x, d).real for x, d in zip(t1, dm0)) t2 = sum(np.einsum('ij,ji->', x, d).real for x, d in zip(t2, dm0)) sigma[x,y] += (t1 - t2) / (2*disp) / nkpts s1 = cell1.pbc_intor('int1e_ovlp', kpts=kpts1) s2 = cell2.pbc_intor('int1e_ovlp', kpts=kpts2) s1 = sum(np.einsum('ij,ji->', x, d).real for x, d in zip(s1, dme0)) s2 = sum(np.einsum('ij,ji->', x, d).real for x, d in zip(s2, dme0)) sigma[x,y] -= (s1 - s2) / (2*disp) / nkpts t0 = log.timer_debug1('hcore derivatives', *t0) dm0 = mf.make_rdm1() sigma += get_vxc(mf_grad, cell, dm0, kpts=kpts, with_j=True, with_nuc=True) t0 = log.timer_debug1('Vxc and Coulomb derivatives', *t0) if hasattr(mf, 'U_idx'): sigma += _hubbard_U_deriv1(mf, dm0, kpts) log.timer_debug1('DFT+U') sigma /= cell.vol if log.verbose >= logger.DEBUG: log.debug('Asymmetric strain tensor') log.debug('%s', sigma) return sigma
def _hubbard_U_deriv1(mf, dm=None, kpts=None): assert mf.alpha is None assert mf.C_ao_lo is None assert mf.minao_ref is not None if dm is None: dm = mf.make_rdm1() if kpts is None: kpts = mf.kpts.reshape(-1, 3) nkpts = len(kpts) cell = mf.cell # Construct orthogonal minao local orbitals. pcell = reference_mol(cell, mf.minao_ref) C_ao_lo = _make_minao_lo(cell, pcell, kpts=kpts) U_idx, U_val = _set_U(cell, pcell, mf.U_idx, mf.U_val)[:2] U_idx_stack = np.hstack(U_idx) C0 = [C_k[:,U_idx_stack] for C_k in C_ao_lo] C1_ao_lo = _get_first_order_local_orbitals(cell, pcell, kpts) C1 = [C_k[:,:,:,U_idx_stack] for C_k in C1_ao_lo.transpose(2,0,1,3,4)] ovlp0 = cell.pbc_intor('int1e_ovlp', kpts=kpts) ovlp1 = np.asarray(get_ovlp(cell, kpts)) nao = cell.nao ovlp1 = ovlp1.reshape(3,3,nkpts,nao,nao).transpose(2,0,1,3,4) C_inv = [C_k.conj().T.dot(S_k) for C_k, S_k in zip(C0, ovlp0)] dm_deriv0 = [ [C_k.dot(dm_k).dot(C_k.conj().T) for C_k, dm_k in zip(C_inv, dm_s)] for dm_s in dm ] sigma = np.zeros((3, 3)) weight = 1. / nkpts for k in range(nkpts): SC1 = lib.einsum('pq,xyqi->xypi', ovlp0[k], C1[k]) SC1 += lib.einsum('xypq,qi->xypi', ovlp1[k], C0[k]) for s in range(2): dm_deriv1 = lib.einsum('pj,xyjq->xypq', C_inv[k].dot(dm[s,k]), SC1) i0 = i1 = 0 for idx, val in zip(U_idx, U_val): i0, i1 = i1, i1 + len(idx) P0 = dm_deriv0[s][k][i0:i1,i0:i1] P1 = dm_deriv1[:,:,i0:i1,i0:i1] sigma += weight * (val * 0.5) * ( np.einsum('xyii->xy', P1).real * 2 - np.einsum('xyij,ji->xy', P1, P0).real * 4) return sigma