Skip to content

Commit

Permalink
[Ready for review] Complete implementation of pbc/tdscf (pyscf#1793)
Browse files Browse the repository at this point in the history
* Bug fix for RSDF due to Mole basis sorting

* Bug fix for pbc df Jbuild for hermi == 0

* improve TDSCF init guess degeneracy handle

* complete pbc tdscf

* b3lyp -> b3lyp5

* adjust values due to v2.3

* add examples; add finalize

* remove non-gamma point exception

* fix flake8 error

* remove numer instable tests (higher roots)

* update tests

* fix wrong num of states

* bug fix in response

* keep minimal changes for pbc _response

* better handle of exxdiv, including vcut

* add NotImplementedError for rsjk + non-zero kshift

* fix shifted kpt for TDRKS/UKS

* fix flake8 error

* revert CasidaTDDFT

* fix flake error

---------

Co-authored-by: hongzhouye <>
  • Loading branch information
hongzhouye authored Aug 29, 2023
1 parent e90d834 commit 4c0998d
Show file tree
Hide file tree
Showing 41 changed files with 2,673 additions and 432 deletions.
126 changes: 76 additions & 50 deletions examples/pbc/22-k_points_tddft.py
Original file line number Diff line number Diff line change
@@ -1,65 +1,91 @@
#!/usr/bin/env python
#
# Author: Hong-Zhou Ye <[email protected]>
#

'''
TDDFT with k-point sampling or at an individual k-point
import numpy as np
from pyscf.pbc import gto, scf, tdscf
from pyscf import scf as molscf
from pyscf import lib

(This feature is in testing. We observe numerical stability problem in TDDFT
diagonalization.)

'''
TDSCF with k-point sampling
'''
atom = 'C 0 0 0; C 0.8925000000 0.8925000000 0.8925000000'
a = '''
1.7850000000 1.7850000000 0.0000000000
0.0000000000 1.7850000000 1.7850000000
1.7850000000 0.0000000000 1.7850000000
'''
basis = '''
C S
9.031436 -1.960629e-02
3.821255 -1.291762e-01
0.473725 5.822572e-01
C P
4.353457 8.730943e-02
1.266307 2.797034e-01
0.398715 5.024424e-01
''' # a trimmed DZ basis for fast test
pseudo = 'gth-hf-rev'
cell = gto.M(atom=atom, basis=basis, a=a, pseudo=pseudo).set(verbose=3)
kmesh = [2,1,1]
kpts = cell.make_kpts(kmesh)
nkpts = len(kpts)
mf = scf.KRHF(cell, kpts).rs_density_fit().run()

from pyscf.pbc import gto
from pyscf.pbc import scf
from pyscf.pbc import df
from pyscf.pbc import tdscf
log = lib.logger.new_logger(mf)

cell = gto.Cell()
cell.unit = 'B'
cell.atom = '''
C 0. 0. 0.
C 1.68506879 1.68506879 1.68506879
'''
cell.a = '''
0. 3.37013758 3.37013758
3.37013758 0. 3.37013758
3.37013758 3.37013758 0.
''' k-point TDSCF solutions can have non-zero momentum transfer between particle and hole.
This can be controlled by `td.kshift_lst`. By default, kshift_lst = [0] and only the
zero-momentum transfer solution (i.e., 'vertical' in k-space) will be solved, as
demonstrated in the example below.
'''
cell.basis = 'gth-szv'
cell.pseudo = 'gth-pade'
cell.build()
mf = scf.KRHF(cell, cell.make_kpts([2,2,2]))
mf.run()
td = mf.TDA().set(nstates=5).run()
log.note('RHF-TDA:')
for kshift,es in zip(td.kshift_lst,td.e):
log.note('kshift = %d Eex = %s', kshift, ' '.join([f'{e:.3f}' for e in es*27.2114]))

td = tdscf.KTDA(mf)
td.nstates = 5
td.verbose = 5
print(td.kernel()[0] * 27.2114)
''' If GDF/RSDF is used as the density fitting method (as in this example), solutions
with non-zero particle-hole momentum-transfer solution is also possible. The example
below demonstrates how to calculate solutions with all possible kshift.
td = tdscf.KTDDFT(mf)
td.nstates = 5
td.verbose = 5
print(td.kernel()[0] * 27.2114)
NOTE: if FFTDF is used, pyscf will set `kshift_lst` to [0].
'''
td = mf.TDHF().set(nstates=5, kshift_lst=list(range(nkpts))).run()
log.note('RHF-TDHF:')
for kshift,es in zip(td.kshift_lst,td.e):
log.note('kshift = %d Eex = %s', kshift, ' '.join([f'{e:.3f}' for e in es*27.2114]))

mf = scf.RHF(cell)
mf.kernel()
td = tdscf.TDA(mf)
td.kernel()

#
# Gamma-point RKS
#
ks = scf.RKS(cell)
ks.run()
''' TDHF at a single k-point compared to molecular TDSCF
'''
atom = '''
O 0.00000 0.00000 0.11779
H 0.00000 0.75545 -0.47116
H 0.00000 -0.75545 -0.47116
'''
a = np.eye(3) * 20 # big box to match isolated molecule
basis = 'sto-3g'
auxbasis = 'weigend'
cell = gto.M(atom=atom, basis=basis, a=a).set(verbose=3)
mol = cell.to_mol()

td = tdscf.KTDDFT(ks)
td.nstates = 5
td.verbose = 5
print(td.kernel()[0] * 27.2114)
log = lib.logger.new_logger(cell)

xc = 'b3lyp'
# pbc
mf = scf.RKS(cell).set(xc=xc).rs_density_fit(auxbasis=auxbasis).run()
pbctda = mf.TDA().run()
pbctd = mf.TDDFT().run()
# mol
molmf = molscf.RKS(cell).set(xc=xc).density_fit(auxbasis=auxbasis).run()
moltda = molmf.TDA().run()
moltd = molmf.TDDFT().run()

# TODO:
#kpt = cell.get_abs_kpts([0.25, 0.25, 0.25])
#mf = scf.RHF(cell, kpt=kpt)
#mf.kernel()
#td = tdscf.TDA(mf)
#td.kernel()
_format = lambda e: ' '.join([f'{x*27.2114:.3f}' for x in e])
log.note('PBC TDA : %s', _format(pbctda.e))
log.note('Mol TDA : %s', _format(moltda.e))
log.note('PBC TDDFT: %s', _format(pbctd.e))
log.note('Mol TDDFT: %s', _format(moltd.e))
46 changes: 46 additions & 0 deletions examples/tddft/04-tddft_missing_roots.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env python
#
# Author: Hong-Zhou Ye <[email protected]>
#


from pyscf import gto, scf, tdscf


''' TDSCF selected particle-hole pairs that have low energy difference (e.g., HOMO -> LUMO,
HOMO -> LUMO+1 etc.) as init guess for the iterative solution of the TDSCF equation.
In cases where there are many near degenerate p-h pairs, their contributions to the
final TDSCF solutions can all be significnat and all such pairs should be included.
The determination of such near degeneracies in p-h pairs is controlled by the
`deg_eia_thresh` variable: if the excitation energy of two p-h pairs differs less
than `deg_eia_thresh`, they will be considered degenerate and selected simultaneously
as init guess.
This example demonstrates the use of this variable to reduce the chance of missing
roots in TDSCF calculations. A molecule is used here for demonstration but the same
applies to periodic TDSCF calculations.
'''

atom = '''
O 0.00000 0.00000 0.11779
H 0.00000 0.75545 -0.47116
H 0.00000 -0.75545 -0.47116
O 4.00000 0.00000 0.11779
H 4.00000 0.75545 -0.47116
H 4.00000 -0.75545 -0.47116
'''
basis = 'cc-pvdz'

mol = gto.M(atom=atom, basis=basis)
mf = scf.RHF(mol).density_fit().run()

''' By default, `deg_eia_thresh` = 1e-3 (Ha) as can be seen in the output if verbose >= 4.
'''
mf.TDA().set(nroots=6).run()

''' One can check if there are missing roots by using a larger value for `deg_eia_thresh`.
Note that this will increase the number of init guess vectors and increase the cost of
solving the TDSCF equation.
'''
mf.TDA().set(nroots=6, deg_eia_thresh=0.1).run()
13 changes: 7 additions & 6 deletions pyscf/grad/test/test_tdrhf_grad.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ def fvind(x):


def setUpModule():
global mol, pmol, mf
global mol, pmol, mf, nstates
mol = gto.Mole()
mol.verbose = 5
mol.output = '/dev/null'
Expand All @@ -126,6 +126,7 @@ def setUpModule():
mol.build()
pmol = mol.copy()
mf = scf.RHF(mol).set(conv_tol=1e-12).run()
nstates = 5 # to ensure the first 3 TDSCF states are converged

def tearDownModule():
global mol, pmol, mf
Expand All @@ -134,7 +135,7 @@ def tearDownModule():

class KnownValues(unittest.TestCase):
def test_tda_singlet(self):
td = tdscf.TDA(mf).run(nstates=3)
td = tdscf.TDA(mf).run(nstates=nstates)
g1ref = tda_kernel(td.nuc_grad_method(), td.xy[2]) + mf.nuc_grad_method().kernel()

tdg = td.nuc_grad_method().as_scanner()
Expand All @@ -155,22 +156,22 @@ def test_tda_singlet(self):
mf.nuc_grad_method().kernel()).max(), 0, 8)

def test_tda_triplet(self):
td = tdscf.TDA(mf).run(singlet=False, nstates=3)
td = tdscf.TDA(mf).run(singlet=False, nstates=nstates)
tdg = td.nuc_grad_method()
# [[ 0 0 -2.81048403e-01]
# [ 0 0 2.81048403e-01]]
self.assertAlmostEqual(lib.fp(tdg.kernel(state=1)), 0.19667995802487931, 6)

g1 = tdg.kernel(state=3)
self.assertAlmostEqual(g1[0,2], -0.47296513687621511, 8)
self.assertAlmostEqual(g1[0,2], -0.472965206465775, 6)

td_solver = td.as_scanner()
e1 = td_solver(pmol.set_geom_('H 0 0 1.805; F 0 0 0', unit='B'))
e2 = td_solver(pmol.set_geom_('H 0 0 1.803; F 0 0 0', unit='B'))
self.assertAlmostEqual((e1[2]-e2[2])/.002, g1[0,2], 5)

def test_tdhf_singlet(self):
td = tdscf.TDDFT(mf).run(nstates=3)
td = tdscf.TDDFT(mf).run(nstates=nstates)
tdg = td.nuc_grad_method()
# [[ 0 0 -2.71041021e-01]
# [ 0 0 2.71041021e-01]]
Expand All @@ -185,7 +186,7 @@ def test_tdhf_singlet(self):
self.assertAlmostEqual((e1[2]-e2[2])/.002, g1[0,2], 6)

def test_tdhf_triplet(self):
td = tdscf.TDDFT(mf).run(singlet=False, nstates=3)
td = tdscf.TDDFT(mf).run(singlet=False, nstates=nstates)
tdg = td.nuc_grad_method()
# [[ 0 0 -2.86250870e-01]
# [ 0 0 2.86250870e-01]]
Expand Down
21 changes: 11 additions & 10 deletions pyscf/grad/test/test_tdrks_grad.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from pyscf.grad import tdrks as tdrks_grad

def setUpModule():
global mol, mf_lda, mf_gga
global mol, mf_lda, mf_gga, nstates
mol = gto.Mole()
mol.verbose = 0
mol.output = None
Expand All @@ -42,20 +42,21 @@ def setUpModule():
mf_gga.grids.prune = False
mf_gga.conv_tol = 1e-10
mf_gga.kernel()
nstates = 5 # to ensure the first 3 TDSCF states are converged

def tearDownModule():
global mol, mf_lda, mf_gga
del mol, mf_lda, mf_gga

class KnownValues(unittest.TestCase):
def test_tda_singlet_lda(self):
td = tdscf.TDA(mf_lda).run(nstates=3)
td = tdscf.TDA(mf_lda).run(nstates=nstates)
tdg = td.nuc_grad_method()
g1 = tdg.kernel(td.xy[2])
self.assertAlmostEqual(g1[0,2], -9.23916667e-02, 6)

def test_tda_triplet_lda(self):
td = tdscf.TDA(mf_lda).run(singlet=False, nstates=3)
td = tdscf.TDA(mf_lda).run(singlet=False, nstates=nstates)
tdg = td.nuc_grad_method()
g1 = tdg.kernel(state=3)
self.assertAlmostEqual(g1[0,2], -0.3311324654, 6)
Expand All @@ -67,7 +68,7 @@ def test_tda_triplet_lda(self):
self.assertAlmostEqual(abs((e1[2]-e2[2])/.002 - g1[0,2]).max(), 0, 4)

def test_tda_singlet_b88(self):
td = tdscf.TDA(mf_gga).run(nstates=3)
td = tdscf.TDA(mf_gga).run(nstates=nstates)
tdg = td.nuc_grad_method()
g1 = tdg.kernel(state=3)
self.assertAlmostEqual(g1[0,2], -9.32506535e-02, 6)
Expand All @@ -81,7 +82,7 @@ def test_tda_singlet_b3lyp_xcfun(self):
mf.scf()

td = mf.TDA()
td.nstates = 3
td.nstates = nstates
e, z = td.kernel()
tdg = td.Gradients()
g1 = tdg.kernel(state=3)
Expand All @@ -99,7 +100,7 @@ def test_tda_triplet_b3lyp(self):
mf.xc = 'b3lyp5'
mf.conv_tol = 1e-14
mf.kernel()
td = tdscf.TDA(mf).run(singlet=False, nstates=3)
td = tdscf.TDA(mf).run(singlet=False, nstates=nstates)
tdg = td.nuc_grad_method()
g1 = tdg.kernel(state=3)
self.assertAlmostEqual(g1[0,2], -0.36333834, 6)
Expand All @@ -115,7 +116,7 @@ def test_tda_singlet_mgga(self):
mf.xc = 'm06l'
mf.conv_tol = 1e-14
mf.kernel()
td = mf.TDA().run(nstates=3)
td = mf.TDA().run(nstates=nstates)
tdg = td.Gradients()
g1 = tdg.kernel(state=3)
self.assertAlmostEqual(g1[0,2], -0.1461843283, 6)
Expand All @@ -128,7 +129,7 @@ def test_tda_singlet_mgga(self):
self.assertAlmostEqual(abs((e1[2]-e2[2])/.002 - g1[0,2]).max(), 0, 3)

def test_tddft_lda(self):
td = tdscf.TDDFT(mf_lda).run(nstates=3)
td = tdscf.TDDFT(mf_lda).run(nstates=nstates)
tdg = td.nuc_grad_method()
g1 = tdg.kernel(state=3)
self.assertAlmostEqual(g1[0,2], -1.31315477e-01, 6)
Expand All @@ -145,7 +146,7 @@ def test_tddft_b3lyp_high_cost(self):
mf._numint.libxc = dft.xcfun
mf.grids.prune = False
mf.scf()
td = tdscf.TDDFT(mf).run(nstates=3)
td = tdscf.TDDFT(mf).run(nstates=nstates)
tdg = td.nuc_grad_method()
g1 = tdg.kernel(state=3)
self.assertAlmostEqual(g1[0,2], -1.55778110e-01, 6)
Expand All @@ -154,7 +155,7 @@ def test_range_separated_high_cost(self):
mol = gto.M(atom="H; H 1 1.", basis='631g', verbose=0)
mf = dft.RKS(mol).set(xc='CAMB3LYP')
mf._numint.libxc = dft.xcfun
td = mf.apply(tdscf.TDA)
td = mf.apply(tdscf.TDA).set(nstates=nstates)
tdg_scanner = td.nuc_grad_method().as_scanner().as_scanner()
g = tdg_scanner(mol, state=3)[1]
self.assertAlmostEqual(lib.fp(g), 0.60109310253094916, 6)
Expand Down
9 changes: 5 additions & 4 deletions pyscf/grad/test/test_tduhf_grad.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@


def setUpModule():
global mol, pmol, mf
global mol, pmol, mf, nstates
mol = gto.Mole()
mol.verbose = 5
mol.output = '/dev/null'
Expand All @@ -38,6 +38,7 @@ def setUpModule():
mol.build()
pmol = mol.copy()
mf = scf.UHF(mol).set(conv_tol=1e-12).run()
nstates = 5 # to ensure the first 3 TDSCF states are converged

def tearDownModule():
global mol, pmol, mf
Expand All @@ -46,7 +47,7 @@ def tearDownModule():

class KnownValues(unittest.TestCase):
def test_tda(self):
td = tdscf.TDA(mf).run(nstates=3)
td = tdscf.TDA(mf).run(nstates=nstates)
tdg = td.nuc_grad_method()
g1 = tdg.kernel(state=3)
self.assertAlmostEqual(g1[0,2], -0.78246882668628404, 6)
Expand All @@ -57,10 +58,10 @@ def test_tda(self):
self.assertAlmostEqual((e1[2]-e2[2])/.002, g1[0,2], 4)

def test_tdhf(self):
td = tdscf.TDDFT(mf).run(nstates=3)
td = tdscf.TDDFT(mf).run(nstates=nstates)
tdg = td.nuc_grad_method()
g1 = tdg.kernel(td.xy[2])
self.assertAlmostEqual(g1[0,2], -0.78969714300299776, 6)
self.assertAlmostEqual(g1[0,2], -0.78969714300299776, 5)

td_solver = td.as_scanner()
e1 = td_solver(pmol.set_geom_('H 0 0 1.805; F 0 0 0', unit='B'))
Expand Down
Loading

0 comments on commit 4c0998d

Please sign in to comment.