Repository: pyscf/pyscf — pull request #2531 (python)
Title: Fix CCSD two-particle density matrix for complex orbitals

* Fix #2519 
* Improve the doc-strings for two-particle density matrix functions.

Resolved issues:
title: Incorrect conjugation in 2RDM transformations
body: PySCF typically uses the following conventions for DMs
```
dm1[p, q] = <q* p>
dm2[p, q, r, s] = <p* r* s q>
```

From Helgaker (3.2), when transforming operators under unitary $U$, the annihilation operator transforms under the conjugate $U^*$, and the creation operator transforms under $U$. This means that following the PySCF conventions, the 1RDM transforms as the Hamiltonian does i.e.
```python
dm1_ao = einsum("ij,pi,qj->pq", dm1_mo, c, c.conj())
```
but the 2RDM should transform as
```python
dm2_ao = einsum("ijkl,pi,qj,rk,sl->pqrs", dm2_mo, c.conj(), c, c.conj(), c)
```
whereas in PySCF the 2RDM is also assumed to transform as the Hamiltonian (the conjugations are reversed):
https://github.com/pyscf/pyscf/blob/2c7f7625fd2719b8f3e9715e40c3937cab65c06e/pyscf/cc/ccsd_rdm.py#L379-L381

This can be verified by trying to recover correlated energies using the RDMs in the AO basis via `ao_repr=True`, which fails, but works with the reverse transformation. See the following script for an example:
https://gist.github.com/obackhouse/855724a176c7f9cad580dc2d39e7c63d
We're aware in this case that the coefficients are not actually a unitary, but this is the best way I could find to show this as an issue in PySCF, and the issue seems to apply at least to the MO->AO transformation.

I am happy to fix but wanted to check if we are missing something first?

Hints:

Fix patch:
diff --git a/pyscf/cc/ccsd_rdm.py b/pyscf/cc/ccsd_rdm.py
index de1b38f2b4..9353dcf3fe 100644
--- a/pyscf/cc/ccsd_rdm.py
+++ b/pyscf/cc/ccsd_rdm.py
@@ -377,8 +377,13 @@ def _make_rdm2(mycc, d1, d2, with_dm1=True, with_frozen=True, ao_repr=False):
 
 
 def _rdm2_mo2ao(dm2, mo):
+    '''
+    Back transform the two-particle density matrices to AO representation, where
+    the dm2 is defined in accordance with the chemist's ERI notation:
+    E = einsum('pqrs,pqrs', eri, rdm2)
+    '''
     mo_C = mo.conj()
-    return lib.einsum('ijkl,pi,qj,rk,sl->pqrs', dm2, mo, mo_C, mo, mo_C)
+    return lib.einsum('ijkl,pi,qj,rk,sl->pqrs', dm2, mo_C, mo, mo_C, mo)
 
 
 if __name__ == '__main__':
diff --git a/pyscf/cc/uccsd_rdm.py b/pyscf/cc/uccsd_rdm.py
index 7e096973f0..8c2bada6ee 100644
--- a/pyscf/cc/uccsd_rdm.py
+++ b/pyscf/cc/uccsd_rdm.py
@@ -625,8 +625,8 @@ def _make_rdm2(mycc, d1, d2, with_dm1=True, with_frozen=True, ao_repr=False):
 
 
 def _dm2ab_mo2ao(dm2, mo_a, mo_b):
-    return lib.einsum('ijkl,pi,qj,rk,sl->pqrs', dm2, mo_a, mo_a.conj(),
-                      mo_b, mo_b.conj())
+    return lib.einsum('ijkl,pi,qj,rk,sl->pqrs', dm2, mo_a.conj(), mo_a,
+                      mo_b.conj(), mo_b)
 
 
 if __name__ == '__main__':
diff --git a/pyscf/scf/hf.py b/pyscf/scf/hf.py
index 645cfee1f4..c3f30d93eb 100644
--- a/pyscf/scf/hf.py
+++ b/pyscf/scf/hf.py
@@ -853,6 +853,16 @@ def make_rdm1(mo_coeff, mo_occ, **kwargs):
 def make_rdm2(mo_coeff, mo_occ, **kwargs):
     '''Two-particle density matrix in AO representation
 
+    NOTE the indices of the two-particle density matrix is ordered to
+
+    dm2[p,q,r,s] = <q^+ s^+ r p>.
+
+    HF energy can be computed
+    E = einsum('pq,qp', hcore, 1pdm) + einsum('pqrs,pqrs', eri, 2pdm) / 2
+    where h1[p,q] = <p|h|q> and eri[p,q,r,s] = (pq|rs)
+to make the density matrix consistent with the density matrix obtained
+    from post-HF methods,
+
     Args:
         mo_coeff : 2D ndarray
             Orbital coefficients. Each column is one orbital.
diff --git a/pyscf/tdscf/_lr_eig.py b/pyscf/tdscf/_lr_eig.py
index 28beab7ffd..0eaa964bf5 100644
--- a/pyscf/tdscf/_lr_eig.py
+++ b/pyscf/tdscf/_lr_eig.py
@@ -524,10 +524,7 @@ def real_eig(aop, x0, precond, tol_residual=1e-5, nroots=1, x0sym=None, pick=Non
     assert pick is None
     assert callable(precond)
 
-    if isinstance(verbose, logger.Logger):
-        log = verbose
-    else:
-        log = logger.Logger(sys.stdout, verbose)
+    log = logger.new_logger(verbose)
 
     assert x0.ndim == 2
     A_size = x0.shape[1] // 2


Test patch:
diff --git a/pyscf/cc/test/test_gccsd.py b/pyscf/cc/test/test_gccsd.py
index 393e8bbced..d0f5123642 100644
--- a/pyscf/cc/test/test_gccsd.py
+++ b/pyscf/cc/test/test_gccsd.py
@@ -285,6 +285,7 @@ def test_rdm_complex(self):
         eri = eri + eri.transpose(1,0,3,2).conj()
         eri = eri + eri.transpose(2,3,0,1)
         eri *= .1
+        mf._eri = eri
 
         def get_jk(mol, dm, *args,**kwargs):
             vj = numpy.einsum('ijkl,lk->ij', eri, dm)
@@ -294,17 +295,19 @@ def get_veff(mol, dm, *args, **kwargs):
             vj, vk = get_jk(mol, dm)
             return vj - vk
         def ao2mofn(mos):
-            return eri
+            c = mos
+            return lib.einsum('pqrs,pi,qj,rk,sl->ijkl', eri, c.conj(), c, c.conj(), c)
 
         mf.get_jk = get_jk
         mf.get_veff = get_veff
         hcore = numpy.random.random((nmo,nmo)) * .2 + numpy.random.random((nmo,nmo))* .2j
-        hcore = hcore + hcore.T.conj() + numpy.diag(range(nmo))*2
+        hcore = hcore + hcore.T.conj() + numpy.diag(numpy.arange(nmo)*2)
         mf.get_hcore = lambda *args: hcore
         mf.get_ovlp = lambda *args: numpy.eye(nmo)
         orbspin = numpy.zeros(nmo, dtype=int)
         orbspin[1::2] = 1
-        mf.mo_coeff = lib.tag_array(numpy.eye(nmo) + 0j, orbspin=orbspin)
+        u = numpy.linalg.eigh(hcore)[1]
+        mf.mo_coeff = lib.tag_array(u, orbspin=orbspin)
         mf.mo_occ = numpy.zeros(nmo)
         mf.mo_occ[:nocc] = 1
 
@@ -313,8 +316,8 @@ def ao2mofn(mos):
         mycc.ao2mo = lambda *args, **kwargs: eris
         mycc.kernel(eris=eris)
         mycc.solve_lambda(eris=eris)
-        dm1 = mycc.make_rdm1()
-        dm2 = mycc.make_rdm2()
+        dm1 = mycc.make_rdm1(ao_repr=True)
+        dm2 = mycc.make_rdm2(ao_repr=True)
 
         e1 = numpy.einsum('ij,ji', hcore, dm1)
         e1+= numpy.einsum('ijkl,ijkl', eri, dm2) * .5