Repository: pyscf/pyscf — pull request #2373 (python)
Title: Fix transition dipoles of triplet tddft states

fix #2370

Resolved issues:
title: TDDFT reports non-zero oscillator strengths for spin-forbidden transitions
body: TDDFT (w/ or w/o TDA) will report non-zero oscillator strengths for triplet excited states when the ground state is a singlet:

```
from pyscf import gto

mol = gto.M(atom='''
  H      1.2194     -0.1652      2.1600
  C      0.6825     -0.0924      1.2087
  C     -0.7075     -0.0352      1.1973
  H     -1.2644     -0.0630      2.1393
  C     -1.3898      0.0572     -0.0114
  H     -2.4836      0.1021     -0.0204
  C     -0.6824      0.0925     -1.2088
  H     -1.2194      0.1652     -2.1599
  C      0.7075      0.0352     -1.1973
  H      1.2641      0.0628     -2.1395
  C      1.3899     -0.0572      0.0114
  H      2.4836     -0.1022      0.0205
''',
              basis='sto-3g')

mf = mol.RKS(xc='cam-b3lyp').run()
td = mf.TDA()
td.singlet = False
td.kernel()
td.analyze()
```

Output:
```
** Triplet excitation energies and oscillator strengths **
Excited State   1:      4.59810 eV    269.64 nm  f=0.0000
Excited State   2:      5.75758 eV    215.34 nm  f=1.0200
Excited State   3:      5.75772 eV    215.34 nm  f=1.0198
```

Hints:

Fix patch:
diff --git a/pyscf/tdscf/rhf.py b/pyscf/tdscf/rhf.py
index b1b680b69f..d2cb63e086 100644
--- a/pyscf/tdscf/rhf.py
+++ b/pyscf/tdscf/rhf.py
@@ -530,16 +530,20 @@ def _charge_center(mol):
     return numpy.einsum('z,zr->r', charges, coords)/charges.sum()
 
 def _contract_multipole(tdobj, ints, hermi=True, xy=None):
+    '''ints is the integral tensor of a spin-independent operator'''
     if xy is None: xy = tdobj.xy
+    nstates = len(xy)
+    pol_shape = ints.shape[:-2]
+    nao = ints.shape[-1]
+
+    if not tdobj.singlet:
+        return numpy.zeros((nstates,) + pol_shape)
+
     mo_coeff = tdobj._scf.mo_coeff
     mo_occ = tdobj._scf.mo_occ
     orbo = mo_coeff[:,mo_occ==2]
     orbv = mo_coeff[:,mo_occ==0]
 
-    nstates = len(xy)
-    pol_shape = ints.shape[:-2]
-    nao = ints.shape[-1]
-
     #Incompatible to old numpy version
     #ints = numpy.einsum('...pq,pi,qj->...ij', ints, orbo.conj(), orbv)
     ints = lib.einsum('xpq,pi,qj->xij', ints.reshape(-1,nao,nao), orbo.conj(), orbv)


Test patch:
diff --git a/pyscf/tdscf/test/test_tdrhf.py b/pyscf/tdscf/test/test_tdrhf.py
index 243d897bb2..b2b5ed6218 100644
--- a/pyscf/tdscf/test/test_tdrhf.py
+++ b/pyscf/tdscf/test/test_tdrhf.py
@@ -15,6 +15,7 @@
 #
 
 import unittest
+import numpy as np
 from pyscf import lib, gto, scf, tdscf
 
 def setUpModule():
@@ -41,6 +42,8 @@ def test_tda_singlet(self):
         e = td.kernel()[0]
         ref = [11.90276464, 11.90276464, 16.86036434]
         self.assertAlmostEqual(abs(e[:len(ref)] * 27.2114 - ref).max(), 0, 5)
+        dip = td.transition_dipole()
+        self.assertAlmostEqual(lib.fp(np.linalg.norm(dip, axis=1)), -0.65616659, 5)
 
     def test_tda_triplet(self):
         td = mf.TDA().set(nstates=nstates)
@@ -48,12 +51,16 @@ def test_tda_triplet(self):
         e = td.kernel()[0]
         ref = [11.01747918, 11.01747918, 13.16955056]
         self.assertAlmostEqual(abs(e[:len(ref)] * 27.2114 - ref).max(), 0, 5)
+        dip = td.transition_dipole()
+        self.assertAlmostEqual(abs(dip).max(), 0, 8)
 
     def test_tdhf_singlet(self):
         td = mf.TDHF().set(nstates=nstates)
         e = td.kernel()[0]
         ref = [11.83487199, 11.83487199, 16.66309285]
         self.assertAlmostEqual(abs(e[:len(ref)] * 27.2114 - ref).max(), 0, 5)
+        dip = td.transition_dipole()
+        self.assertAlmostEqual(lib.fp(np.linalg.norm(dip, axis=1)), -0.64009191, 5)
 
     def test_tdhf_triplet(self):
         td = mf.TDHF().set(nstates=nstates)
@@ -61,6 +68,8 @@ def test_tdhf_triplet(self):
         e = td.kernel()[0]
         ref = [10.8919234, 10.8919234, 12.63440705]
         self.assertAlmostEqual(abs(e[:len(ref)] * 27.2114 - ref).max(), 0, 5)
+        dip = td.transition_dipole()
+        self.assertAlmostEqual(abs(dip).max(), 0, 8)
 
 
 if __name__ == "__main__":