Source code for pyscf.pbc.grad.krks_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.

r'''
The energy derivatives for the strain tensor e_ij is

                1  d E
    sigma_ij = --- ------
                V  d e_ij

The strain tesnor e_ij describes the transformation for real space coordinates
in the crystal

    \sum_j [\deta_ij + e_ij] R_j  [for j = x, y, z]

The strain tensor is generally not a symmetric tensor. Symmetrization

    [e1   e6/2 e5/2]
    [e6/2 e2   e4/2]
    [e5/2 e4/2 e3  ]

is applied to form 6 independent component.

    e1 = e_11
    e2 = e_22
    e3 = e_33
    e6 = e_12 + e_21
    e5 = e_13 + e_31
    e4 = e_32 + e_23

The 6 component strain is then used to define the symmetric stress tensor.

               1  d E
    sigma_i = --- ------  for i = 1 .. 6
               V  d e_i

The symmetric stress tensor represented in the 6 Voigt notation can be
transformed from the asymmetric stress tensor sigma_ij

    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
'''

import numpy as np
from pyscf import lib
from pyscf import gto
from pyscf.lib import logger
from pyscf.pbc.tools import pbc as pbctools
from pyscf.pbc.lib.kpts_helper import is_zero
from pyscf.pbc.dft.gen_grid import UniformGrids
from pyscf.pbc.gto import pseudo
from pyscf.pbc.df import FFTDF, ft_ao
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 import krks as krks_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_ovlp(cell, kpts): '''Strain derivatives for overlap matrix ''' disp = 1e-5 scaled_kpts = kpts.dot(cell.lattice_vectors().T) s = [] 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)) s1 = np.asarray(cell1.pbc_intor('int1e_ovlp', kpts=kpts1)) s2 = np.asarray(cell2.pbc_intor('int1e_ovlp', kpts=kpts2)) s.append((s1 - s2) / (2*disp)) return s
[docs] def get_kin(cell, kpts): '''Strain derivatives for kinetic matrix ''' disp = 1e-5 scaled_kpts = kpts.dot(cell.lattice_vectors().T) t = [] 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 = np.asarray(cell1.pbc_intor('int1e_kin', kpts=kpts1)) t2 = np.asarray(cell2.pbc_intor('int1e_kin', kpts=kpts2)) t.append((t1 - t2) / (2*disp)) return t
[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 == 3 nkpts, nao = dm_kpts.shape[:2] 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((nvar, ngrids)) rho1 = np.zeros((3,3, nvar, ngrids)) def partial_dot(bra, ket): '''conj(ig),ig->g''' # Adapt to the _contract_rho function 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 = np.asarray(coords.T, order='C') for k, dm in enumerate(dm_kpts): 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 grids' response ao1 += np.einsum('xig,yg->xyig', ao[1:4], coordsT) c0 = dm.T.dot(ao[0]) rho0[0,p0:p1] += partial_dot(ao[0], c0).real rho1[:,:,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,ij->xjg', ao[:4], dm) for i in range(4): rho0[i,p0:p1] += partial_dot(ao[0], c0[i]).real rho1[:,:, : ,p0:p1] += np.einsum('xynig,ig->xyng', ao_strain, c0[0].conj()).real rho1[:,:,1:4,p0:p1] += np.einsum('xyig,nig->xyng', ao_strain[:,:,0], c0[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,ij->xjg', ao[:4], dm) for i in range(4): rho0[i,p0:p1] += partial_dot(ao[0], c0[i]).real rho0[4,p0:p1] += partial_dot(ao[1], c0[1]).real rho0[4,p0:p1] += partial_dot(ao[2], c0[2]).real rho0[4,p0:p1] += partial_dot(ao[3], c0[3]).real rho1[:,:, :4,p0:p1] += np.einsum('xynig,ig->xyng', ao_strain, c0[0].conj()).real rho1[:,:,1:4,p0:p1] += np.einsum('xyig,nig->xyng', ao_strain[:,:,0], c0[1:4].conj()).real rho1[:,:,4,p0:p1] += np.einsum('xynig,nig->xyg', ao_strain[:,:,1:4], c0[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=0)[:2] out += np.einsum('xyng,ng->xy', rho1, vxc).real * weight_0 out += np.einsum('g,g->', rho0[0], exc).real * weight_1 Gv = cell.get_Gv(mesh) coulG_0, coulG_1 = _get_coulG_strain_derivatives(cell, Gv) rhoG = pbctools.fft(rho0[0], mesh) if with_j: vR = pbctools.ifft(rhoG * coulG_0, mesh) EJ = np.einsum('xyg,g->xy', rho1[:,:,0], vR).real * weight_0 * 2 EJ += np.einsum('g,g->', rho0[0], vR).real * weight_1 EJ += np.einsum('g,xyg,g->xy', rhoG.conj(), coulG_1, 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[:,:,0], 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, 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[:,:,0], vR).real Ene += np.einsum('g,xyg,g->xy', rhoG.conj(), coulG_1, 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, krks_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() dme0 = mf_grad.make_rdm1e() 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) 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 _get_first_order_local_orbitals(cell, minao_ref='MINAO', kpts=None): if isinstance(minao_ref, str): pcell = reference_mol(cell, minao_ref) else: pcell = minao_ref scaled_kpts = kpts.dot(cell.lattice_vectors().T) nkpts = len(kpts) nao = cell.nao naop = pcell.nao if is_zero(kpts): dtype = np.float64 else: dtype = np.complex128 C1_minao = np.empty((3, 3, nkpts, nao, naop), dtype=dtype) disp = 1e-5 for x in range(3): for y in range(3): cell1, cell2 = _finite_diff_cells(cell, x, y, disp) pcell1, pcell2 = _finite_diff_cells(pcell, x, y, disp) kpts1 = scaled_kpts.dot(cell1.reciprocal_vectors(norm_to=1)) kpts2 = scaled_kpts.dot(cell2.reciprocal_vectors(norm_to=1)) C1 = np.asarray(_make_minao_lo(cell1, pcell1, kpts=kpts1), dtype=dtype) C2 = np.asarray(_make_minao_lo(cell2, pcell2, kpts=kpts2), dtype=dtype) C1_minao[x,y] = (C1 - C2) / (2*disp) return C1_minao 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 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)] 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]) dm_deriv1 = lib.einsum('pj,xyjq->xypq', C_inv[k].dot(dm[k]), SC1) i0 = i1 = 0 for idx, val in zip(U_idx, U_val): i0, i1 = i1, i1 + len(idx) P0 = dm_deriv0[k][i0:i1,i0:i1] P1 = dm_deriv1[:,:,i0:i1,i0:i1] sigma += weight * (val * 0.5) * ( np.einsum('xyii->xy', P1).real * 2 # *2 for P1+P1.T - np.einsum('xyij,ji->xy', P1, P0).real * 2) return sigma