fix phase error for k-point symmetry (pyscf/pyscf#1584)

KsymAdaptedRCCSD Not Converge

Dear developers,

I'm testing the latest CCSD with k-point symmetries for periodic systems. However, the following calculation for diamond primitive cell with 3x3x3 k-point sampling does not converge. Please see the scripts and corresponding outputs below. Thank you!

## Version information
```
Python 3.8.13
GCC 7.5.0
PySCF version: 2.1.1
GIT HEAD      7bcae2e3044bf4259f1c6e6b03829912ce0ae1a0
```

## CCSD without k-point symmetry
The calculation without symmetries ended gracefully.

Here is the CCSD input without k-point symmetries:
```python
from pyscf.pbc import gto, df, scf, cc
from pyscf import scf as mol_scf
from time import time

start_time = time()

cell = gto.Cell()
cell.atom = "C 0.0000 0.0000 0.0000; C 0.8900 0.8900 0.8900"
cell.a = [[0.0000, 1.7800, 1.7800], [1.7800, 0.0000, 1.7800], [1.7800, 1.7800, 0.0000]]
cell.basis = "gth-szv"
cell.pseudo = "gth-pade"
cell.max_memory = 512000
cell.verbose = 4
# cell.precision = 1e-4

cell.build()

kmesh = [3,3,3]
kpts = cell.make_kpts(kmesh)
khf = scf.KRHF(cell, kpts)
khf = mol_scf.addons.remove_linear_dep_(khf)
khf.kernel()
print(f"** HF total energy [Ha]: {khf.e_tot} **")

kcc = cc.KRCCSD(khf)
kcc.conv_tol = 1e-4
kcc.conv_tol_normt = 1e-3
kcc.kernel()

print(f"** MP2 total energy [Ha]: {kcc.emp2 + khf.e_tot} **")
print(f"** MP2 correlation [Ha]: {kcc.emp2} **")
print(f"** CCSD total energy [Ha]: {kcc.e_tot} **")
print(f"** CCSD correlation [Ha]: {kcc.e_corr} **")

end_time = time()

print(f"#####################################################")
print(f"# Finished in {end_time - start_time:10.2f} seconds. #")
print(f"#####################################################")
```

Here is the output:
```python
******** <class 'pyscf.pbc.cc.kccsd_rhf.RCCSD'> ********
CC2 = 0
CCSD nocc = 4, nmo = 8
max_cycle = 50
direct = 1
conv_tol = 0.0001
conv_tol_normt = 0.001
diis_space = 6
diis_start_cycle = 0
diis_start_energy_diff = 1e+09
max_memory 512000 MB (current use 229 MB)
using incore ERI storage
Init t2, MP2 energy (with fock eigenvalue shift) = -0.101136246587875
Init E_corr(CCSD) = -0.101136246587875
cycle = 1  E_corr(CCSD) = -0.11302371985953  dE = -0.0118874733  norm(t1,t2) = 0.199481
cycle = 2  E_corr(CCSD) = -0.116486053728869  dE = -0.00346233387  norm(t1,t2) = 0.0672819
cycle = 3  E_corr(CCSD) = -0.11809151506564  dE = -0.00160546134  norm(t1,t2) = 0.0298103
cycle = 4  E_corr(CCSD) = -0.118301235613774  dE = -0.000209720548  norm(t1,t2) = 0.00626899
cycle = 5  E_corr(CCSD) = -0.118298883933205  dE = 2.35168057e-06  norm(t1,t2) = 0.0019157
cycle = 6  E_corr(CCSD) = -0.118295531043058  dE = 3.35289015e-06  norm(t1,t2) = 0.000438101
RCCSD converged
E(RCCSD) = -11.11828707024064  E_corr = -0.1182955310430577
** MP2 total energy [Ha]: -11.101127785785454 **
** MP2 correlation [Ha]: -0.10113624658787514 **
** CCSD total energy [Ha]: -11.118287070240635 **
** CCSD correlation [Ha]: -0.11829553104305766 **
#####################################################
# Finished in    3442.58 seconds. #
#####################################################
```

## CCSD with k-point symmetries
Here is the input file
```python
from pyscf.pbc import gto, df, scf, cc
from pyscf import scf as mol_scf
from time import time

start_time = time()

cell = gto.Cell()
cell.atom = "C 0.0000 0.0000 0.0000; C 0.8900 0.8900 0.8900"
cell.a = [[0.0000, 1.7800, 1.7800], [1.7800, 0.0000, 1.7800], [1.7800, 1.7800, 0.0000]]
cell.basis = "gth-szv"
cell.pseudo = "gth-pade"
cell.max_memory = 512000
cell.verbose = 4
# cell.precision = 1e-8
cell.space_group_symmetry = True
cell.build()

kmesh = [3,3,3]
kpts = cell.make_kpts(kmesh, with_gamma_point=True, space_group_symmetry=True)
khf = scf.KRHF(cell, kpts)
khf = mol_scf.addons.remove_linear_dep_(khf)
khf.kernel()
print(f"** HF total energy [Ha]: {khf.e_tot} **")

kcc = cc.KsymAdaptedRCCSD(khf)
kcc.conv_tol = 1e-4
kcc.conv_tol_normt = 1e-3
kcc.kernel()

print(f"** MP2 total energy [Ha]: {kcc.emp2 + khf.e_tot} **")
print(f"** MP2 correlation [Ha]: {kcc.emp2} **")
print(f"** CCSD total energy [Ha]: {kcc.e_tot} **")
print(f"** CCSD correlation [Ha]: {kcc.e_corr} **")

end_time = time()

print(f"#####################################################")
print(f"# Finished in {end_time - start_time:10.2f} seconds. #")
print(f"#####################################################")
```
But unfortunately, the CCSD calculation does not converge:
```python
******** <class 'pyscf.pbc.cc.kccsd_rhf_ksymm.KsymAdaptedRCCSD'> ********
CC2 = 0
CCSD nocc = 4, nmo = 8
max_cycle = 50
direct = 1
conv_tol = 0.0001
conv_tol_normt = 0.001
diis_space = 6
diis_start_cycle = 0
diis_start_energy_diff = 1e+09
max_memory 512000 MB (current use 269 MB)
Incore memory estimation: 999.240704 MB
using incore ERI storage
Init t2, MP2 energy (with fock eigenvalue shift) = 115.220187475672
Init E_corr(CCSD) = 115.220187475672
cycle = 1  E_corr(CCSD) = 1105912122.99569  dE = 1.10591201e+09  norm(t1,t2) = 1.26556e+07

WARN: Non-zero imaginary part found in KRCCSD energy (1.6764182260999362e+23-16275017475.528364j)

cycle = 2  E_corr(CCSD) = 1.67641822609994e+23  dE = 1.67641823e+23  norm(t1,t2) = 6.48812e+21

WARN: Non-zero imaginary part found in KRCCSD energy (-7.192569978591114e+74-3.580017490652341e+62j)

cycle = 3  E_corr(CCSD) = -7.19256997859111e+74  dE = -7.19256998e+74  norm(t1,t2) = 2.42369e+51

WARN: Non-zero imaginary part found in KRCCSD energy (-1.2621487897532068e+225-1.8297827444511553e+213j)

cycle = 4  E_corr(CCSD) = -1.26214878975321e+225  dE = -1.26214879e+225  norm(t1,t2) = 1.95135e+152
cycle = 5  E_corr(CCSD) = nan  dE = nan  norm(t1,t2) = nan
...
```

diff --git a/pyscf/pbc/symm/symmetry.py b/pyscf/pbc/symm/symmetry.py
index 41d8a0606d..3618039c8f 100644
--- a/pyscf/pbc/symm/symmetry.py
+++ b/pyscf/pbc/symm/symmetry.py
@@ -235,7 +235,7 @@ def _get_phase(cell, op, kpt_scaled, ignore_phase=False, tol=SYMPREC):
         # remove numerical noise, important for symmetry adaptation
         Lshift = Lshift.round()
         if not ignore_phase:
-            phase[iatm] = np.exp(-1j * np.dot(kpt_scaled, Lshift) * 2.0 * np.pi)
+            phase[iatm] = np.exp(1j * np.dot(kpt_scaled, Lshift) * 2.0 * np.pi)
     return atm_map, phase

 def _get_rotation_mat(cell, kpt_scaled_ibz, mo_coeff_or_dm, op, Dmats,

diff --git a/pyscf/pbc/lib/test/test_kpts_ksymm.py b/pyscf/pbc/lib/test/test_kpts_ksymm.py
index 5e7a0b2ddd..9de0d07bcb 100644
--- a/pyscf/pbc/lib/test/test_kpts_ksymm.py
+++ b/pyscf/pbc/lib/test/test_kpts_ksymm.py
@@ -36,15 +36,16 @@ def setUpModule():
               [2.6935121974, 2.6935121974, 0.0]]
     cell.basis = 'gth-szv'
     cell.pseudo  = 'gth-pade'
-    cell.mesh = [24,24,24]
+    cell.mesh = [20,] * 3
     cell.space_group_symmetry = True
     cell.build()

     cell1 = cell.copy()
     cell1.build(symmorphic=True)

-    kpts0 = cell.make_kpts([2,2,2])
+    kpts0 = cell.make_kpts([3,3,3])
     kmf = scf.KRKS(cell, kpts0)
+    kmf.max_cycle=1
     kmf.kernel()

 def tearDownModule():
@@ -91,12 +92,12 @@ def test_transform(self):
         kpts = libkpts.make_kpts(cell, kpts0, space_group_symmetry=True, time_reversal_symmetry=True)
         dms_ibz = kmf.make_rdm1()[kpts.ibz2bz]
         dms_bz = kpts.transform_dm(dms_ibz)
-        self.assertAlmostEqual(abs(dms_bz - kmf.make_rdm1()).max(), 0, 9)
+        self.assertAlmostEqual(abs(dms_bz - kmf.make_rdm1()).max(), 0, 7)

         mo_coeff_ibz = np.asarray(kmf.mo_coeff)[kpts.ibz2bz]
         mo_coeff_bz = kpts.transform_mo_coeff(mo_coeff_ibz)
         dms_bz = khf.make_rdm1(mo_coeff_bz, kmf.mo_occ)
-        self.assertAlmostEqual(abs(dms_bz - kmf.make_rdm1()).max(), 0, 9)
+        self.assertAlmostEqual(abs(dms_bz - kmf.make_rdm1()).max(), 0, 7)

         mo_occ_ibz = kpts.check_mo_occ_symmetry(kmf.mo_occ)
         mo_occ_bz = kpts.transform_mo_occ(mo_occ_ibz)
@@ -104,17 +105,17 @@ def test_transform(self):

         mo_energy_ibz = np.asarray(kmf.mo_energy)[kpts.ibz2bz]
         mo_energy_bz = kpts.transform_mo_energy(mo_energy_ibz)
-        self.assertAlmostEqual(abs(mo_energy_bz - np.asarray(kmf.mo_energy)).max(), 0 , 9)
+        self.assertAlmostEqual(abs(mo_energy_bz - np.asarray(kmf.mo_energy)).max(), 0 , 7)

         fock_ibz = kmf.get_fock()[kpts.ibz2bz]
         fock_bz = kpts.transform_fock(fock_ibz)
-        self.assertAlmostEqual(abs(fock_bz - kmf.get_fock()).max(), 0, 9)
+        self.assertAlmostEqual(abs(fock_bz - kmf.get_fock()).max(), 0, 7)

         kumf = kmf.to_uhf()
         mo_coeff_ibz = np.asarray(kumf.mo_coeff)[:,kpts.ibz2bz]
         mo_coeff_bz = kpts.transform_mo_coeff(mo_coeff_ibz)
         dms_bz = kuhf.make_rdm1(mo_coeff_bz, kumf.mo_occ)
-        self.assertAlmostEqual(abs(dms_bz - kumf.make_rdm1()).max(), 0, 9)
+        self.assertAlmostEqual(abs(dms_bz - kumf.make_rdm1()).max(), 0, 7)

         mo_occ_ibz = np.asarray(kumf.mo_occ)[:,kpts.ibz2bz]
         mo_occ_bz = kpts.transform_mo_occ(mo_occ_ibz)
@@ -122,11 +123,11 @@ def test_transform(self):

         mo_energy_ibz = np.asarray(kumf.mo_energy)[:,kpts.ibz2bz]
         mo_energy_bz = kpts.transform_mo_energy(mo_energy_ibz)
-        self.assertAlmostEqual(abs(mo_energy_bz - np.asarray(kumf.mo_energy)).max(), 0 , 9)
+        self.assertAlmostEqual(abs(mo_energy_bz - np.asarray(kumf.mo_energy)).max(), 0 , 7)

         fock_ibz = kumf.get_fock()[:,kpts.ibz2bz]
         fock_bz = kpts.transform_fock(fock_ibz)
-        self.assertAlmostEqual(abs(fock_bz - kumf.get_fock()).max(), 0, 9)
+        self.assertAlmostEqual(abs(fock_bz - kumf.get_fock()).max(), 0, 7)

     def test_symmetrize_density(self):
         rho0 = kmf.get_rho()


Fix k-point symmetry adapted KS-DFT with Newton solver (pyscf/pyscf#1594)

Newton method for krks_ksymm fails

Hi,
Second order SCF solver can be used for k-symmetry adapted HF class but fails for DFT calculations.
```
import numpy
from pyscf.pbc import gto, scf, dft

cell = gto.M(
    a = numpy.asarray([[0.0, 2.6935121974, 2.6935121974],
                       [2.6935121974, 0.0, 2.6935121974],
                       [2.6935121974, 2.6935121974, 0.0]]),
    atom = '''Si  0.0000000000 0.0000000000 0.0000000000
              Si  1.3467560987 1.3467560987 1.3467560987''',
    basis = 'gth-szv',
    pseudo = 'gth-pade',
    mesh = [24,24,24],
    verbose = 5,)

nk = [2,2,2]
kpts = cell.make_kpts(nk,
                      space_group_symmetry=True,
                      time_reversal_symmetry=True,
                      symmorphic=True)

kmf = scf.KRKS(cell, kpts)
kmf.xc = 'pbe'
kmf = kmf.newton()
kmf.kernel()
```
The calculation will fail because of not correctly recognizing KPoints class.

diff --git a/pyscf/pbc/dft/numint.py b/pyscf/pbc/dft/numint.py
index a8ca103c28..74ba8349fe 100644
--- a/pyscf/pbc/dft/numint.py
+++ b/pyscf/pbc/dft/numint.py
@@ -27,6 +27,7 @@
 from pyscf.dft.gen_grid import NBINS, CUTOFF, ALIGNMENT_UNIT
 from pyscf.pbc.dft.gen_grid import make_mask, BLKSIZE
 from pyscf.pbc.lib.kpts_helper import member, is_zero
+from pyscf.pbc.lib.kpts import KPoints

 def eval_ao(cell, coords, kpt=numpy.zeros(3), deriv=0, relativity=0, shls_slice=None,
@@ -554,6 +555,8 @@ def nr_rks_fxc(ni, cell, grids, xc_code, dm0, dms, relativity=0, hermi=0,
     '''
     if kpts is None:
         kpts = numpy.zeros((1,3))
+    if isinstance(kpts, KPoints):
+        kpts = kpts.kpts_ibz
     xctype = ni._xc_type(xc_code)
     if xctype == 'LDA':
         ao_deriv = 0
@@ -843,6 +846,10 @@ def cache_xc_kernel(ni, cell, grids, xc_code, mo_coeff, mo_occ, spin=0,
     '''
     if kpts is None:
         kpts = numpy.zeros((1,3))
+    if isinstance(kpts, KPoints):
+        mo_coeff = kpts.transform_mo_coeff(mo_coeff)
+        mo_occ = kpts.transform_mo_occ(mo_occ)
+        kpts = kpts.kpts
     xctype = ni._xc_type(xc_code)
     if xctype == 'GGA':
         ao_deriv = 1

diff --git a/pyscf/pbc/dft/test/test_krks_ksym.py b/pyscf/pbc/dft/test/test_krks_ksym.py
index caa5db1573..f55e78a43b 100644
--- a/pyscf/pbc/dft/test/test_krks_ksym.py
+++ b/pyscf/pbc/dft/test/test_krks_ksym.py
@@ -152,6 +152,20 @@ def test_gga_df(self):
         kmf.kernel()
         self.assertAlmostEqual(kmf.e_tot, kmf0.e_tot, 9)

+    def test_gga_df_newton(self):
+        kpts0 = He.make_kpts(nk, with_gamma_point=False)
+        kmf0 = krks.KRKS(He, kpts=kpts0).density_fit()
+        kmf0.xc = 'pbe'
+        kmf0 = kmf0.newton()
+        kmf0.kernel()
+
+        kpts = He.make_kpts(nk, with_gamma_point=False, space_group_symmetry=True,time_reversal_symmetry=True)
+        kmf = pscf.KRKS(He, kpts=kpts).density_fit()
+        kmf.xc = 'pbe'
+        kmf = kmf.newton()
+        kmf.kernel()
+        self.assertAlmostEqual(kmf.e_tot, kmf0.e_tot, 9)
+
     def test_rsh_df(self):
         kpts0 = He.make_kpts(nk, with_gamma_point=False)
         kmf0 = krks.KRKS(He, kpts=kpts0).density_fit()


Fix Mole.decontract_basis method (Fix issue #1609) (pyscf/pyscf#1620)

Issue with x2c and using cc-pCVXZ basis sets in at least PySCF 1.7.6 and newer

Hi all,

I've come across what I believe to be a bug when trying to run a relativistic exact two-component PySCF calculation using a core-valence correlation consistent basis set (cc-pCVDZ) at the SCF/mean-field level. I have included two input files that do (c6h4_load.py) and do not (c6h4_parse.py) have the bug I describe below:

When running a calculation in which I request to use the cc-pCVDZ basis set, the output file lists the correct number of primitive GTOs, and the initial energy in the SCF procedure is reasonable (approx. -230 a.u.), but then the first step of the SCF cycle has an energy that is two orders of magnitude greater (approx. -50000 a.u.) and the SCF procedure does not converge.

The issue only occurs if I load the cc-pCVDZ basis set as it is defined within PySCF. Two actions fix this problem:
1) Switching to the non-core-valence variant (cc-pCVDZ -> cc-pVDZ); SCF converges normally without any complications.

2) If I manually specify the SAME cc-pCVDZ basis set and use the 'parse' function from the 'gto' module, the same exact x2c mean-field calculation no longer produces these massive energy errors in the SCF energy and convergence is smooth and quick.

The exact same input files provided below, run in PySCF 1.5.3 (compiled) and PySCF 1.6.2 (compiled), DO NOT have this issue. So it seems like something that could have been introduced around version 1.7?

The versions of PySCF I have found to be displaying this behavior:
PySCF 1.7.6 (conda)
PySCF 2.0.0a (compiled)
PySCF 2.0.1 (conda)
PySCF 2.1.1 (conda)
PySCF 2.1.1 (compiled)
PySCF 2.1.1 (compiled w/ OMP_NUM_THREADS=1)

Please let me know if you need any additional info from me! Thanks in advance.

Best,
Ilia Mazin

[x2c_basis_set_test.zip](https://github.com/pyscf/pyscf/files/10818364/x2c_basis_set_test.zip)

diff --git a/pyscf/gto/mole.py b/pyscf/gto/mole.py
index 09253d2a59..6eff5b8a4f 100644
--- a/pyscf/gto/mole.py
+++ b/pyscf/gto/mole.py
@@ -916,11 +916,15 @@ def make_atm_env(atom, ptr=0, nuclear_model=NUC_POINT, nucprop={}):
 def make_bas_env(basis_add, atom_id=0, ptr=0):
     '''Convert :attr:`Mole.basis` to the argument ``bas`` for ``libcint`` integrals
     '''
+    # First sort basis accroding to l. This is important for method
+    # decontract_basis, which assumes that basis functions with the same angular
+    # momentum are grouped together
+    basis_add = [b for b in basis_add if b]
+    basis_add = sorted(basis_add, key=lambda b: b[0])
+
     _bas = []
     _env = []
     for b in basis_add:
-        if not b:  # == []
-            continue
         angl = b[0]
         if angl > 14:
             sys.stderr.write('Warning: integral library does not support basis '

diff --git a/pyscf/gto/test/test_mole.py b/pyscf/gto/test/test_mole.py
index c42151c697..294233e168 100644
--- a/pyscf/gto/test/test_mole.py
+++ b/pyscf/gto/test/test_mole.py
@@ -1034,6 +1034,13 @@ def test_decontract_basis(self):
         s = ctr_coeff.T.dot(pmol.intor('int1e_ovlp')).dot(ctr_coeff)
         self.assertAlmostEqual(abs(s - mol.intor('int1e_ovlp')).max(), 0, 12)

+        mol = gto.M(atom='He',
+                    basis=('ccpvdz', [[0, [5, 1]], [1, [3, 1]]]))
+        pmol, contr_coeff = mol.decontract_basis()
+        contr_coeff = scipy.linalg.block_diag(*contr_coeff)
+        s = contr_coeff.T.dot(pmol.intor('int1e_ovlp')).dot(contr_coeff)
+        self.assertAlmostEqual(abs(s - mol.intor('int1e_ovlp')).max(), 0, 12)
+
     def test_ao_rotation_matrix(self):
         mol = gto.M(atom='O 0 0 0.2; H1 0 -.8 -.5; H2 0 .8 -.5', basis='ccpvdz')
         numpy.random.seed(1)


DIIS instability due to numerical noises (fix issue #1524) (pyscf/pyscf#1638)

DIIS fails to converge triplet O in cc-pVDZ

The oxygen 3P wave function cannot be converged even in cc-pVDZ with DIIS. Without DIIS the calculation works.
```
import pyscf

mol = pyscf.M(
    atom = 'O',
    spin = 2,
    basis = 'cc-pVDZ',
    symmetry = True,
    verbose = 4)

# This fails
mf = mol.ROHF()
mf.init_guess = '1e'
mf.irrep_nelec = {'s+0' : (2,2), 'p-1' : (1,0), 'p+0' : (1,0), 'p+1' : (1,1) }
mf.kernel()

# This works
mf = mol.ROHF()
mf.init_guess = '1e'
mf.irrep_nelec = {'s+0' : (2,2), 'p-1' : (1,0), 'p+0' : (1,0), 'p+1' : (1,1) }
mf.diis = None
mf.kernel()
```

diff --git a/pyscf/scf/diis.py b/pyscf/scf/diis.py
index 21dba408b6..27e6e8cc0a 100644
--- a/pyscf/scf/diis.py
+++ b/pyscf/scf/diis.py
@@ -43,6 +43,10 @@ def __init__(self, mf=None, filename=None, Corth=None):
         self.rollback = 0
         self.space = 8
         self.Corth = Corth
+        #?self._scf = mf
+        #?if hasattr(self._scf, 'get_orbsym'): # Symmetry adapted SCF objects
+        #?    self.orbsym = mf.get_orbsym(Corth)
+        #?    sym_forbid = self.orbsym[:,None] != self.orbsym

     def update(self, s, d, f, *args, **kwargs):
         errvec = get_err_vec(s, d, f, self.Corth)
@@ -83,16 +87,24 @@ def get_err_vec_orig(s, d, f):

 def get_err_vec_orth(s, d, f, Corth):
     '''error vector in orthonormal basis = C.T.conj() (SDF - FDS) C'''
+    # Symmetry information to reduce numerical error in DIIS (issue #1524)
+    orbsym = getattr(Corth, 'orbsym', None)
+    if orbsym is not None:
+        sym_forbid = orbsym[:,None] != orbsym
+
     if isinstance(f, numpy.ndarray) and f.ndim == 2:
-        sdf = reduce(numpy.dot, (s,d,f))
-        errvec = Corth.conj().T.dot(sdf.conj().T - sdf).dot(Corth).ravel()
+        sdf = reduce(numpy.dot, (Corth.conj().T, s, d, f, Corth))
+        if orbsym is not None:
+            sdf[sym_forbid] = 0
+        errvec = (sdf.conj().T - sdf).ravel()

     elif isinstance(f, numpy.ndarray) and f.ndim == 3 and s.ndim == 3:
         errvec = []
         for i in range(f.shape[0]):
-            sdf = reduce(numpy.dot, (s[i], d[i], f[i]))
-            errvec.append(
-                Corth[i].conj().T.dot(sdf.conj().T - sdf).dot(Corth[i]).ravel())
+            sdf = reduce(numpy.dot, (Corth[i].conj().T, s[i], d[i], f[i], Corth[i]))
+            if orbsym is not None:
+                sdf[sym_forbid] = 0
+            errvec.append((sdf.conj().T - sdf).ravel())
         errvec = numpy.vstack(errvec).ravel()

     elif f.ndim == s.ndim+1 and f.shape[0] == 2:  # for UHF
diff --git a/pyscf/scf/ghf_symm.py b/pyscf/scf/ghf_symm.py
index d0a5709e4b..8ddc0a6c11 100644
--- a/pyscf/scf/ghf_symm.py
+++ b/pyscf/scf/ghf_symm.py
@@ -39,9 +39,6 @@
 def analyze(mf, verbose=logger.DEBUG, with_meta_lowdin=WITH_META_LOWDIN,
             **kwargs):
     mol = mf.mol
-    if not mol.symmetry:
-        return ghf.analyze(mf, verbose, **kwargs)
-
     mo_energy = mf.mo_energy
     mo_occ = mf.mo_occ
     mo_coeff = mf.mo_coeff
@@ -83,7 +80,7 @@ def canonicalize(mf, mo_coeff, mo_occ, fock=None):
     '''
     mol = mf.mol
     if not mol.symmetry:
-        return ghf.canonicalize(mf, mo_coeff, mo_occ, fock)
+        raise RuntimeError('mol.symmetry not enabled')

     if getattr(mo_coeff, 'orbsym', None) is not None:
         return hf_symm.canonicalize(mf, mo_coeff, mo_occ, fock)
@@ -113,42 +110,42 @@ def dump_flags(self, verbose=None):

     def build(self, mol=None):
         if mol is None: mol = self.mol
-        if mol.symmetry:
-            for irname in self.irrep_nelec:
-                if irname not in mol.irrep_name:
-                    logger.warn(self, 'Molecule does not have irrep %s', irname)
-
-            nelec_fix = self.irrep_nelec.values()
-            if any(isinstance(x, (tuple, list)) for x in nelec_fix):
-                msg =('Number of alpha/beta electrons cannot be assigned '
-                      'separately in GHF.  irrep_nelec = %s' % self.irrep_nelec)
-                raise ValueError(msg)
-            nelec_fix = sum(nelec_fix)
-            float_irname = set(mol.irrep_name) - set(self.irrep_nelec)
-            if nelec_fix > mol.nelectron:
-                msg =('More electrons defined by irrep_nelec than total num electrons. '
-                      'mol.nelectron = %d  irrep_nelec = %s' %
-                      (mol.nelectron, self.irrep_nelec))
-                raise ValueError(msg)
-            else:
-                logger.info(mol, 'Freeze %d electrons in irreps %s',
-                            nelec_fix, self.irrep_nelec.keys())
-
-            if len(float_irname) == 0 and nelec_fix != mol.nelectron:
-                msg =('Num electrons defined by irrep_nelec != total num electrons. '
-                      'mol.nelectron = %d  irrep_nelec = %s' %
-                      (mol.nelectron, self.irrep_nelec))
-                raise ValueError(msg)
-            else:
-                logger.info(mol, '    %d free electrons in irreps %s',
-                            mol.nelectron-nelec_fix, ' '.join(float_irname))
+        if not mol.symmetry:
+            raise RuntimeError('mol.symmetry not enabled')
+
+        for irname in self.irrep_nelec:
+            if irname not in mol.irrep_name:
+                logger.warn(self, 'Molecule does not have irrep %s', irname)
+
+        nelec_fix = self.irrep_nelec.values()
+        if any(isinstance(x, (tuple, list)) for x in nelec_fix):
+            msg =('Number of alpha/beta electrons cannot be assigned '
+                  'separately in GHF.  irrep_nelec = %s' % self.irrep_nelec)
+            raise ValueError(msg)
+        nelec_fix = sum(nelec_fix)
+        float_irname = set(mol.irrep_name) - set(self.irrep_nelec)
+        if nelec_fix > mol.nelectron:
+            msg =('More electrons defined by irrep_nelec than total num electrons. '
+                  'mol.nelectron = %d  irrep_nelec = %s' %
+                  (mol.nelectron, self.irrep_nelec))
+            raise ValueError(msg)
+        else:
+            logger.info(mol, 'Freeze %d electrons in irreps %s',
+                        nelec_fix, self.irrep_nelec.keys())
+
+        if len(float_irname) == 0 and nelec_fix != mol.nelectron:
+            msg =('Num electrons defined by irrep_nelec != total num electrons. '
+                  'mol.nelectron = %d  irrep_nelec = %s' %
+                  (mol.nelectron, self.irrep_nelec))
+            raise ValueError(msg)
+        else:
+            logger.info(mol, '    %d free electrons in irreps %s',
+                        mol.nelectron-nelec_fix, ' '.join(float_irname))
         return ghf.GHF.build(self, mol)

     def eig(self, h, s, symm_orb=None, irrep_id=None):
         if symm_orb is None or irrep_id is None:
             mol = self.mol
-            if not mol.symmetry:
-                return self._eigh(h, s)
             symm_orb = mol.symm_orb
             irrep_id = mol.irrep_id

@@ -171,12 +168,11 @@ def eig(self, h, s, symm_orb=None, irrep_id=None):

     def get_grad(self, mo_coeff, mo_occ, fock=None):
         g = ghf.GHF.get_grad(self, mo_coeff, mo_occ, fock)
-        if self.mol.symmetry:
-            occidx = mo_occ > 0
-            viridx = ~occidx
-            orbsym = self.get_orbsym(mo_coeff, self.get_ovlp())
-            sym_forbid = orbsym[viridx].reshape(-1,1) != orbsym[occidx]
-            g[sym_forbid.ravel()] = 0
+        occidx = mo_occ > 0
+        viridx = ~occidx
+        orbsym = self.get_orbsym(mo_coeff)
+        sym_forbid = orbsym[viridx].reshape(-1,1) != orbsym[occidx]
+        g[sym_forbid.ravel()] = 0
         return g

     def get_occ(self, mo_energy=None, mo_coeff=None):
@@ -186,9 +182,9 @@ def get_occ(self, mo_energy=None, mo_coeff=None):
         if mo_energy is None: mo_energy = self.mo_energy
         mol = self.mol
         if not mol.symmetry:
-            return ghf.GHF.get_occ(self, mo_energy, mo_coeff)
+            raise RuntimeError('mol.symmetry not enabled')

-        orbsym = self.get_orbsym(mo_coeff, self.get_ovlp())
+        orbsym = self.get_orbsym(mo_coeff)
         mo_occ = numpy.zeros_like(mo_energy)
         rest_idx = numpy.ones(mo_occ.size, dtype=bool)
         nelec_fix = 0
@@ -243,7 +239,7 @@ def _finalize(self):
         # ordering of the symmetry labels when two orbitals are degenerated.
         o_sort = numpy.argsort(self.mo_energy[self.mo_occ> 0].round(9), kind='mergesort')
         v_sort = numpy.argsort(self.mo_energy[self.mo_occ==0].round(9), kind='mergesort')
-        orbsym = self.get_orbsym(self.mo_coeff, self.get_ovlp())
+        orbsym = self.get_orbsym(self.mo_coeff)
         self.mo_energy = numpy.hstack((self.mo_energy[self.mo_occ> 0][o_sort],
                                        self.mo_energy[self.mo_occ==0][v_sort]))
         self.mo_coeff = numpy.hstack((self.mo_coeff[:,self.mo_occ> 0].take(o_sort, axis=1),
@@ -276,6 +272,8 @@ def get_irrep_nelec(self, mol=None, mo_coeff=None, mo_occ=None, s=None):
     def get_orbsym(self, mo_coeff=None, s=None):
         if mo_coeff is None:
             mo_coeff = self.mo_coeff
+        if getattr(mo_coeff, 'orbsym', None) is not None:
+            return mo_coeff.orbsym
         if s is None:
             s = self.get_ovlp()
         return numpy.asarray(get_orbsym(self.mol, mo_coeff, s))
diff --git a/pyscf/scf/hf_symm.py b/pyscf/scf/hf_symm.py
index 2e5d577bd5..40eab05874 100644
--- a/pyscf/scf/hf_symm.py
+++ b/pyscf/scf/hf_symm.py
@@ -53,9 +53,6 @@ def analyze(mf, verbose=logger.DEBUG, with_meta_lowdin=WITH_META_LOWDIN,
     from pyscf.lo import orth
     from pyscf.tools import dump_mat
     mol = mf.mol
-    if not mol.symmetry:
-        return hf.analyze(mf, verbose, with_meta_lowdin, **kwargs)
-
     mo_energy = mf.mo_energy
     mo_occ = mf.mo_occ
     mo_coeff = mf.mo_coeff
@@ -153,8 +150,7 @@ def canonicalize(mf, mo_coeff, mo_occ, fock=None):
     '''
     mol = mf.mol
     if not mol.symmetry:
-        return hf.canonicalize(mf, mo_coeff, mo_occ, fock)
-
+        raise RuntimeError('mol.symmetry not enabled')
     if fock is None:
         dm = mf.make_rdm1(mo_coeff, mo_occ)
         fock = mf.get_hcore() + mf.get_veff(mf.mol, dm)
@@ -305,7 +301,7 @@ def eig(mf, h, s, symm_orb=None, irrep_id=None):
     mol = mf.mol
     if symm_orb is None or irrep_id is None:
         if not mol.symmetry:
-            return mf._eigh(h, s)
+            raise RuntimeError('mol.symmetry not enabled')
         symm_orb = mol.symm_orb
         irrep_id = mol.irrep_id

@@ -344,14 +340,15 @@ def eig(mf, h, s, symm_orb=None, irrep_id=None):
     return e, c

 def get_orbsym(mol, mo_coeff, s=None, check=False, symm_orb=None, irrep_id=None):
+    if getattr(mo_coeff, 'orbsym', None) is not None:
+        return mo_coeff.orbsym
+
     if symm_orb is None or irrep_id is None:
         symm_orb = mol.symm_orb
         irrep_id = mol.irrep_id
     if mo_coeff is None:
         orbsym = numpy.hstack([[ir] * symm_orb[i].shape[1]
                                for i, ir in enumerate(irrep_id)])
-    elif getattr(mo_coeff, 'orbsym', None) is not None:
-        orbsym = mo_coeff.orbsym
     else:
         orbsym = symm.label_orb_symm(mol, irrep_id, symm_orb,
                                      mo_coeff, s, check)
@@ -445,33 +442,32 @@ def __init__(self, mol):

     def build(self, mol=None):
         if mol is None: mol = self.mol
-        if mol.symmetry:
-            check_irrep_nelec(mol, self.irrep_nelec, self.mol.nelectron)
+        if not mol.symmetry:
+            raise RuntimeError('mol.symmetry not enabled')
+        check_irrep_nelec(mol, self.irrep_nelec, self.mol.nelectron)
         return hf.RHF.build(self, mol)

     eig = eig

     def get_grad(self, mo_coeff, mo_occ, fock=None):
         g = hf.RHF.get_grad(self, mo_coeff, mo_occ, fock)
-        if self.mol.symmetry:
-            occidx = mo_occ > 0
-            viridx = ~occidx
-            orbsym = self.get_orbsym(mo_coeff, self.get_ovlp())
-            sym_forbid = orbsym[viridx].reshape(-1,1) != orbsym[occidx]
-            g[sym_forbid.ravel()] = 0
+        occidx = mo_occ > 0
+        viridx = ~occidx
+        orbsym = self.get_orbsym(mo_coeff)
+        sym_forbid = orbsym[viridx].reshape(-1,1) != orbsym[occidx]
+        g[sym_forbid.ravel()] = 0
         return g

     def get_occ(self, mo_energy=None, mo_coeff=None):
         ''' We assumed mo_energy are grouped by symmetry irreps, (see function
         self.eig). The orbitals are sorted after SCF.
         '''
-
         if mo_energy is None: mo_energy = self.mo_energy
         mol = self.mol
         if not mol.symmetry:
-            return hf.RHF.get_occ(self, mo_energy, mo_coeff)
+            raise RuntimeError('mol.symmetry not enabled')

-        orbsym = self.get_orbsym(mo_coeff, self.get_ovlp())
+        orbsym = self.get_orbsym(mo_coeff)
         mo_occ = numpy.zeros_like(mo_energy)
         rest_idx = numpy.ones(mo_occ.size, dtype=bool)
         nelec_fix = 0
@@ -528,7 +524,7 @@ def _finalize(self):
                             idx[self.mo_occ==0][v_sort]))
         self.mo_energy = self.mo_energy[idx]
         self.mo_occ = self.mo_occ[idx]
-        orbsym = self.get_orbsym(self.mo_coeff, self.get_ovlp())
+        orbsym = self.get_orbsym(self.mo_coeff)
         orbsym = orbsym[idx]
         degen_mapping = None
         if self.mol.groupname in ('Dooh', 'Coov'):
@@ -563,6 +559,8 @@ def get_irrep_nelec(self, mol=None, mo_coeff=None, mo_occ=None, s=None):
     def get_orbsym(self, mo_coeff=None, s=None):
         if mo_coeff is None:
             mo_coeff = self.mo_coeff
+        if getattr(mo_coeff, 'orbsym', None) is not None:
+            return mo_coeff.orbsym
         if s is None:
             s = self.get_ovlp()
         return numpy.asarray(get_orbsym(self.mol, mo_coeff, s))
@@ -615,21 +613,22 @@ def dump_flags(self, verbose=None):

     def build(self, mol=None):
         if mol is None: mol = self.mol
-        if mol.symmetry:
-            fix_na, fix_nb = check_irrep_nelec(mol, self.irrep_nelec, self.nelec)[:2]
-            alpha_open = beta_open = False
-            for ne in self.irrep_nelec.values():
-                if not isinstance(ne, (int, numpy.integer)):
-                    alpha_open |= ne[0] > ne[1]
-                    beta_open  |= ne[0] < ne[1]
-
-            frozen_spin = fix_na - fix_nb
-            if ((alpha_open and beta_open) or
-                (0 < mol.spin < frozen_spin) or (frozen_spin < 0 < mol.spin) or
-                (frozen_spin < mol.spin < 0) or (mol.spin < 0 < frozen_spin)):
-                raise ValueError('Low-spin configuration was found in '
-                                 'the irrep_nelec input. ROHF does not '
-                                 'support low-spin configuration.')
+        if not mol.symmetry:
+            raise RuntimeError('mol.symmetry not enabled')
+        fix_na, fix_nb = check_irrep_nelec(mol, self.irrep_nelec, self.nelec)[:2]
+        alpha_open = beta_open = False
+        for ne in self.irrep_nelec.values():
+            if not isinstance(ne, (int, numpy.integer)):
+                alpha_open |= ne[0] > ne[1]
+                beta_open  |= ne[0] < ne[1]
+
+        frozen_spin = fix_na - fix_nb
+        if ((alpha_open and beta_open) or
+            (0 < mol.spin < frozen_spin) or (frozen_spin < 0 < mol.spin) or
+            (frozen_spin < mol.spin < 0) or (mol.spin < 0 < frozen_spin)):
+            raise ValueError('Low-spin configuration was found in '
+                             'the irrep_nelec input. ROHF does not '
+                             'support low-spin configuration.')
         return hf.RHF.build(self, mol)

     @lib.with_doc(eig.__doc__)
@@ -643,25 +642,24 @@ def eig(self, fock, s):

     def get_grad(self, mo_coeff, mo_occ, fock=None):
         g = rohf.ROHF.get_grad(self, mo_coeff, mo_occ, fock)
-        if self.mol.symmetry:
-            occidxa = mo_occ > 0
-            occidxb = mo_occ == 2
-            viridxa = ~occidxa
-            viridxb = ~occidxb
-            uniq_var_a = viridxa.reshape(-1,1) & occidxa
-            uniq_var_b = viridxb.reshape(-1,1) & occidxb
-
-            orbsym = self.get_orbsym(mo_coeff, self.get_ovlp())
-            sym_forbid = orbsym.reshape(-1,1) != orbsym
-            sym_forbid = sym_forbid[uniq_var_a | uniq_var_b]
-            g[sym_forbid.ravel()] = 0
+        occidxa = mo_occ > 0
+        occidxb = mo_occ == 2
+        viridxa = ~occidxa
+        viridxb = ~occidxb
+        uniq_var_a = viridxa.reshape(-1,1) & occidxa
+        uniq_var_b = viridxb.reshape(-1,1) & occidxb
+
+        orbsym = self.get_orbsym(mo_coeff)
+        sym_forbid = orbsym.reshape(-1,1) != orbsym
+        sym_forbid = sym_forbid[uniq_var_a | uniq_var_b]
+        g[sym_forbid.ravel()] = 0
         return g

     def get_occ(self, mo_energy=None, mo_coeff=None):
         if mo_energy is None: mo_energy = self.mo_energy
         mol = self.mol
         if not mol.symmetry:
-            return rohf.ROHF.get_occ(self, mo_energy, mo_coeff)
+            raise RuntimeError('mol.symmetry not enabled')

         if getattr(mo_energy, 'mo_ea', None) is not None:
             mo_ea = mo_energy.mo_ea
@@ -670,8 +668,7 @@ def get_occ(self, mo_energy=None, mo_coeff=None):
             mo_ea = mo_eb = mo_energy
         nmo = mo_ea.size
         mo_occ = numpy.zeros(nmo)
-
-        orbsym = self.get_orbsym(mo_coeff, self.get_ovlp())
+        orbsym = self.get_orbsym(mo_coeff)

         rest_idx = numpy.ones(mo_occ.size, dtype=bool)
         neleca_fix = 0
@@ -790,7 +787,7 @@ def _finalize(self):
         else:
             self.mo_energy = self.mo_energy[idx]
         self.mo_occ = self.mo_occ[idx]
-        orbsym = self.get_orbsym(self.mo_coeff, self.get_ovlp())
+        orbsym = self.get_orbsym(self.mo_coeff)
         orbsym = orbsym[idx]
         degen_mapping = None
         if self.mol.groupname in ('Dooh', 'Coov'):
@@ -813,9 +810,6 @@ def analyze(self, verbose=None, with_meta_lowdin=WITH_META_LOWDIN,
         if verbose is None: verbose = self.verbose
         from pyscf.lo import orth
         from pyscf.tools import dump_mat
-        if not self.mol.symmetry:
-            return rohf.ROHF.analyze(self, verbose, with_meta_lowdin, **kwargs)
-
         mol = self.mol
         mo_energy = self.mo_energy
         mo_occ = self.mo_occ
@@ -826,7 +820,7 @@ def analyze(self, verbose=None, with_meta_lowdin=WITH_META_LOWDIN,
             self.dump_scf_summary(log)

             nirrep = len(mol.irrep_id)
-            orbsym = self.get_orbsym(mo_coeff, self.get_ovlp())
+            orbsym = self.get_orbsym(mo_coeff)
             irreps = numpy.asarray(mol.irrep_id)
             ndoccs = numpy.count_nonzero(irreps[:,None] == orbsym[mo_occ==2], axis=1)
             nsoccs = numpy.count_nonzero(irreps[:,None] == orbsym[mo_occ==1], axis=1)
@@ -926,12 +920,7 @@ def canonicalize(self, mo_coeff, mo_occ, fock=None):
         mo_e = lib.tag_array(mo_e, mo_ea=mo_ea, mo_eb=mo_eb)
         return mo_e, mo_coeff

-    def get_orbsym(self, mo_coeff=None, s=None):
-        if mo_coeff is None:
-            mo_coeff = self.mo_coeff
-        if s is None:
-            s = self.get_ovlp()
-        return numpy.asarray(get_orbsym(self.mol, mo_coeff, s))
+    get_orbsym = SymAdaptedRHF.get_orbsym
     orbsym = property(get_orbsym)

     get_wfnsym = get_wfnsym
diff --git a/pyscf/scf/uhf_symm.py b/pyscf/scf/uhf_symm.py
index 447457d371..5acbe23d62 100644
--- a/pyscf/scf/uhf_symm.py
+++ b/pyscf/scf/uhf_symm.py
@@ -41,9 +41,6 @@ def analyze(mf, verbose=logger.DEBUG, with_meta_lowdin=WITH_META_LOWDIN,
     from pyscf.lo import orth
     from pyscf.tools import dump_mat
     mol = mf.mol
-    if not mol.symmetry:
-        return uhf.analyze(mf, verbose, with_meta_lowdin, **kwargs)
-
     mo_energy = mf.mo_energy
     mo_occ = mf.mo_occ
     mo_coeff = mf.mo_coeff
@@ -193,8 +190,7 @@ def canonicalize(mf, mo_coeff, mo_occ, fock=None):
     '''
     mol = mf.mol
     if not mol.symmetry:
-        return uhf.canonicalize(mf, mo_coeff, mo_occ, fock)
-
+        raise RuntimeError('mol.symmetry not enabled')
     mo_occ = numpy.asarray(mo_occ)
     assert (mo_occ.ndim == 2)
     if fock is None:
@@ -326,15 +322,13 @@ def dump_flags(self, verbose=None):

     def build(self, mol=None):
         if mol is None: mol = self.mol
-        if mol.symmetry:
-            hf_symm.check_irrep_nelec(mol, self.irrep_nelec, self.nelec)
+        if not mol.symmetry:
+            raise RuntimeError('mol.symmetry not enabled')
+        hf_symm.check_irrep_nelec(mol, self.irrep_nelec, self.nelec)
         return uhf.UHF.build(self, mol)

     def eig(self, h, s):
         mol = self.mol
-        if not mol.symmetry:
-            return self._eigh(h, s)
-
         nirrep = mol.symm_orb.__len__()
         s = symm.symmetrize_matrix(s, mol.symm_orb)
         ha = symm.symmetrize_matrix(h[0], mol.symm_orb)
@@ -391,17 +385,16 @@ def eig(self, h, s):

     def get_grad(self, mo_coeff, mo_occ, fock=None):
         g = uhf.UHF.get_grad(self, mo_coeff, mo_occ, fock)
-        if self.mol.symmetry:
-            occidxa = mo_occ[0] > 0
-            occidxb = mo_occ[1] > 0
-            viridxa = ~occidxa
-            viridxb = ~occidxb
-            orbsyma, orbsymb = self.get_orbsym(mo_coeff, self.get_ovlp())
-            sym_forbida = orbsyma[viridxa].reshape(-1,1) != orbsyma[occidxa]
-            sym_forbidb = orbsymb[viridxb].reshape(-1,1) != orbsymb[occidxb]
-            sym_forbid = numpy.hstack((sym_forbida.ravel(),
-                                       sym_forbidb.ravel()))
-            g[sym_forbid] = 0
+        occidxa = mo_occ[0] > 0
+        occidxb = mo_occ[1] > 0
+        viridxa = ~occidxa
+        viridxb = ~occidxb
+        orbsyma, orbsymb = self.get_orbsym(mo_coeff)
+        sym_forbida = orbsyma[viridxa].reshape(-1,1) != orbsyma[occidxa]
+        sym_forbidb = orbsymb[viridxb].reshape(-1,1) != orbsymb[occidxb]
+        sym_forbid = numpy.hstack((sym_forbida.ravel(),
+                                   sym_forbidb.ravel()))
+        g[sym_forbid] = 0
         return g

     def get_occ(self, mo_energy=None, mo_coeff=None):
@@ -411,9 +404,9 @@ def get_occ(self, mo_energy=None, mo_coeff=None):
         if mo_energy is None: mo_energy = self.mo_energy
         mol = self.mol
         if not mol.symmetry:
-            return uhf.UHF.get_occ(self, mo_energy, mo_coeff)
+            raise RuntimeError('mol.symmetry not enabled')

-        orbsyma, orbsymb = self.get_orbsym(mo_coeff, self.get_ovlp())
+        orbsyma, orbsymb = self.get_orbsym(mo_coeff)
         mo_occ = numpy.zeros_like(mo_energy)
         idx_ea_left = []
         idx_eb_left = []
@@ -516,7 +509,7 @@ def _finalize(self):
         idxb = numpy.hstack((idxb[self.mo_occ[1]> 0][ob_sort],
                              idxb[self.mo_occ[1]==0][vb_sort]))
         self.mo_energy = (ea[idxa], eb[idxb])
-        orbsyma, orbsymb = self.get_orbsym(self.mo_coeff, self.get_ovlp())
+        orbsyma, orbsymb = self.get_orbsym(self.mo_coeff)
         orbsyma = orbsyma[idxa]
         orbsymb = orbsymb[idxb]
         degen_a = degen_b = None
@@ -558,6 +551,8 @@ def get_irrep_nelec(self, mol=None, mo_coeff=None, mo_occ=None, s=None):
     def get_orbsym(self, mo_coeff=None, s=None):
         if mo_coeff is None:
             mo_coeff = self.mo_coeff
+        if getattr(mo_coeff, 'orbsym', None) is not None:
+            return mo_coeff.orbsym
         if s is None:
             s = self.get_ovlp()
         return get_orbsym(self.mol, mo_coeff, s)

diff --git a/pyscf/scf/test/test_diis.py b/pyscf/scf/test/test_diis.py
index 442c5a716a..afa53d989b 100644
--- a/pyscf/scf/test/test_diis.py
+++ b/pyscf/scf/test/test_diis.py
@@ -94,6 +94,17 @@ def test_diis_restart(self):
         e = mf.kernel()
         self.assertAlmostEqual(e, eref, 9)

+    # issue 1524
+    def test_diis_for_symmetry_adapted_scf(self):
+        mol = gto.M(atom='O', spin=2, basis='ccpvdz', symmetry=True)
+        mf = mol.ROHF()
+        mf.init_guess = '1e'
+        mf.irrep_nelec = {'s+0' : (2,2), 'p-1' : (1,0), 'p+0' : (1,0), 'p+1' : (1,1) }
+        mf.kernel()
+        self.assertTrue(mf.converged)
+        self.assertAlmostEqual(mf.e_tot, -74.7874921601008, 9)
+

 if __name__ == "__main__":
     print("Full Tests for DIIS")
diff --git a/pyscf/scf/test/test_rhf.py b/pyscf/scf/test/test_rhf.py
index 1109b7ff7e..ffd4876fef 100644
--- a/pyscf/scf/test/test_rhf.py
+++ b/pyscf/scf/test/test_rhf.py
@@ -590,8 +590,7 @@ def test_rohf_symm_get_occ(self):
                 [0, 2, 0, 0, 0, 1, 2, 0, 1, 2, 0, 0, 2, 0, 2, 0, 0, 0, 0, 2]))

         mf1 = scf.RHF(mol).set(verbose=0).view(scf.hf_symm.ROHF)
-        self.assertTrue(numpy.allclose(mf1.get_occ(energy, mo_coeff),
-                [0 ,2 ,0 ,0 ,0 ,0 ,2 ,0 ,0 ,0 ,0 ,0 ,2 ,0 ,2 ,0 ,0 ,0 ,0 ,2]))
+        self.assertRaises(RuntimeError, mf1.get_occ, energy, mo_coeff)

     def test_get_occ_extreme_case(self):
         mol = gto.M(atom='He', verbose=7, output='/dev/null')


Fix molecule orientation bug for C2v symmetry (pyscf/pyscf#1643)

Inconsistent symmetries under trivial transformation

I have just upgraded to pyscf 2.1.1 and encountered some unexpected behaviour in orbital symmetries. I have a C2v CH2 molecule, and the orbital symmetries are different when I have a coordinate in the molecule which is zero for all atoms:
```
  from pyscf import gto, scf, symm
  basis = "sto3g"

  geom = [
      ["C", [0.0000000, 0.0000000, 0.1238210]],
      ["H", [0.0000000, 0.9620540, -0.3714630]],
      ["H", [0.0000000, -0.9620540, -0.3714630]],
  ]
  mol = gto.Mole(
      atom=geom,
      basis=basis,
      spin=2,
      symmetry=True,
  )
  mol.build()
  mf = scf.ROHF(mol)
  mf.kernel()
  osym = symm.addons.label_orb_symm(
      mol,
      mol.irrep_name,
      mol.symm_orb,
      mf.mo_coeff
  )

  # Shifting everything along x...
  geom_shift = [
      ["C", [1.0000000, 0.0000000, 0.1238210]],
      ["H", [1.0000000, 0.9620540, -0.3714630]],
      ["H", [1.0000000, -0.9620540, -0.3714630]],
  ]
  mol_shift = gto.Mole(
      atom=geom_shift,
      basis=basis,
      spin=2,
      symmetry=True,
  )
  mf_shift = scf.ROHF(mol_shift)
  mf_shift.kernel()
  osym_shift = symm.addons.label_orb_symm(
      mol_shift,
      mol_shift.irrep_name,
      mol_shift.symm_orb,
      mf_shift.mo_coeff
  )

  print(osym)
  print(osym_shift)
```
this outputs:
```
['A1' 'A1' 'B2' 'A1' 'B1' 'A1' 'B2']
['A1' 'A1' 'B1' 'A1' 'B2' 'A1' 'B1']
```
Both calculations arrive at the same energy (-38.431653993852), and the latter calculation (with the shifted coords) is consistent with the unshifted molecule in pyscf 2.0, and with an equivalent calculation in nwchem.

diff --git a/pyscf/symm/geom.py b/pyscf/symm/geom.py
index 1915e84048..89cf7e480e 100644
--- a/pyscf/symm/geom.py
+++ b/pyscf/symm/geom.py
@@ -118,34 +118,32 @@ def alias_axes(axes, ref):
         new_axes = axes[[y_id,x_id,z_id]]
     return new_axes

-def _adjust_planar_c2v(atom_coords, center, axes):
+def _adjust_planar_c2v(atom_coords, axes):
     '''Adjust axes for planar molecules'''
     # Following http://iopenshell.usc.edu/resources/howto/symmetry/
     # See also dicussions in issue #1201
     # * planar C2v molecules should be oriented such that the X axis is perpendicular
     # to the plane of the molecule, and the Z axis is the axis of symmetry;
-    r = atom_coords - center
     natm = len(atom_coords)
     tol = TOLERANCE / numpy.sqrt(1+natm)
-    atoms_on_xz = abs(r.dot(axes[1])) < tol
+    atoms_on_xz = abs(atom_coords.dot(axes[1])) < tol
     if all(atoms_on_xz):
         # rotate xy
         axes = numpy.array([-axes[1], axes[0], axes[2]])
     return axes

-def _adjust_planar_d2h(atom_coords, center, axes):
+def _adjust_planar_d2h(atom_coords, axes):
     '''Adjust axes for planar molecules'''
     # Following http://iopenshell.usc.edu/resources/howto/symmetry/
     # See also dicussions in issue #1201
     # * planar D2h molecules should be oriented such that the X axis is
     # perpendicular to the plane of the molecule, and the Z axis passes through
     # the greatest number of atoms.
-    r = atom_coords - center
     natm = len(atom_coords)
     tol = TOLERANCE / numpy.sqrt(1+natm)
-    natm_with_x = numpy.count_nonzero(abs(r.dot(axes[0])) > tol)
-    natm_with_y = numpy.count_nonzero(abs(r.dot(axes[1])) > tol)
-    natm_with_z = numpy.count_nonzero(abs(r.dot(axes[2])) > tol)
+    natm_with_x = numpy.count_nonzero(abs(atom_coords.dot(axes[0])) > tol)
+    natm_with_y = numpy.count_nonzero(abs(atom_coords.dot(axes[1])) > tol)
+    natm_with_z = numpy.count_nonzero(abs(atom_coords.dot(axes[2])) > tol)
     if natm_with_z == 0:  # atoms on xy plane
         if natm_with_x >= natm_with_y:  # atoms-on-y >= atoms-on-x
             # rotate xz
@@ -292,7 +290,7 @@ def detect_symm(atoms, basis=None, verbose=logger.WARN):
                 if rawsys.has_icenter():
                     gpname = 'D2h'
                     # _adjust_planar_d2h is unlikely to be called
-                    axes = _adjust_planar_d2h(rawsys.atom_coords, charge_center, axes)
+                    axes = _adjust_planar_d2h(rawsys.atom_coords, axes)
                 else:
                     gpname = 'D2'
                 axes = alias_axes(axes, numpy.eye(3))
@@ -305,7 +303,7 @@ def detect_symm(atoms, basis=None, verbose=logger.WARN):
                     gpname = 'C2h'
                 elif rawsys.has_mirror(axes[0]):
                     gpname = 'C2v'
-                    axes = _adjust_planar_c2v(rawsys.atom_coords, charge_center, axes)
+                    axes = _adjust_planar_c2v(rawsys.atom_coords, axes)
                 else:
                     gpname = 'C2'
             else:

diff --git a/pyscf/symm/test/test_geom.py b/pyscf/symm/test/test_geom.py
index dd8c8475e4..4fd7c8fadd 100644
--- a/pyscf/symm/test/test_geom.py
+++ b/pyscf/symm/test/test_geom.py
@@ -876,6 +876,16 @@ def test_sort_coords(self):
         idx = symm.argsort_coords(c)
         self.assertAlmostEqual(abs(c[idx] - c0).max(), 0, 9)

+    def test_c2v_shifted(self):
+        atoms = [
+            ["C", [1.0000000, 0.0000000, 0.1238210]],
+            ["H", [1.0000000, 0.9620540, -0.3714630]],
+            ["H", [1.0000000, -0.9620540, -0.3714630]],
+        ]
+        l, orig, axes = geom.detect_symm(atoms)
+        self.assertEqual(l, 'C2v')
+        self.assertAlmostEqual(abs(axes - numpy.diag(axes.diagonal())).max(), 0, 9)
+

 def ring(n, start=0):
     r = 1. / numpy.sin(numpy.pi/n)


fix supercell with symmetry (pyscf/pyscf#1773)

Error when calculating the IAOs of a supercell with space_group_symmetry=True.

Hello!
I've come across an error when trying to obtain the IAOs of a supercell generated from a cell initialised with space group symmetry.
A minimal example reproducing the issue is
```
import pyscf.lo
import pyscf.pbc
import numpy as np

cell = pyscf.pbc.gto.Cell()
cell.atom = ['He 0.0 0.0 0.0']
cell.a = 1.4 * np.eye(3)
cell.basis = 'def2-svp'
cell.output = 'pyscf.out'
cell.space_group_symmetry = True
cell.symmorphic = True
cell.build()

scell = pyscf.pbc.tools.super_cell(cell, (2,2,2))
pyscf.lo.iao.reference_mol(scell, minao="sto-3g")
```

which gives traceback

```
Traceback (most recent call last):
 File "testscript.py", line 16, in <module>
    pyscf.lo.iao.reference_mol(scell, minao="sto-3g")
 File "python3.10/site-packages/pyscf/lo/iao.py", line 135, in reference_mol
    pmol.build(False, False, basis=minao)
 File "python3.10/site-packages/pyscf/pbc/gto/cell.py", line 1516, in build
    _lattice_symm.build(space_group_symmetry=True,
 File "python3.10/site-packages/pyscf/pbc/symm/symmetry.py", line 176, in build
    self.spacegroup = space_group.SpaceGroup(cell).build(dump_info=False)
 File "python3.10/site-packages/pyscf/pbc/symm/space_group.py", line 302, in build
    self.ops = geom.search_space_group_ops(self.cell, tol=self.symprec)
 File "python3.10/site-packages/pyscf/pbc/symm/geom.py", line 81, in search_space_group_ops
    atmgrp = mole.atom_types(cell._atom, magmom=cell.magmom)
 File "python3.10/site-packages/pyscf/gto/mole.py", line 308, in atom_types
    uniq_mag = np.unique(magmom[idx])
IndexError: index 1 is out of bounds for axis 0 with size 1
```

This can be avoided by disabling space group symmetry initially or in the supercell object after generation, but I don't know whether that should be necessary. Any advice as to whether this is an intended incompatibility would be greatly appreciated.

diff --git a/pyscf/gto/mole.py b/pyscf/gto/mole.py
index 97e3c650ba..1d832b2b43 100644
--- a/pyscf/gto/mole.py
+++ b/pyscf/gto/mole.py
@@ -304,7 +304,7 @@ def atom_types(atoms, basis=None, magmom=None):

     if magmom is not None:
         atmgroup_new = {}
-        suffix = {1.0:'u', -1.0:'d', 0.0:'o'}
+        suffix = {1:'u', -1:'d', 0:'o'}
         magmom = np.asarray(magmom)
         for elem, idx in atmgroup.items():
             uniq_mag = np.unique(magmom[idx])
@@ -312,7 +312,7 @@ def atom_types(atoms, basis=None, magmom=None):
                 for i, mag in enumerate(uniq_mag):
                     subgrp = np.asarray(idx)[np.where(magmom[idx] == mag)[0]]
                     if mag not in suffix:
-                        raise RuntimeError("Magmom should be chosen from [-1., 0., 1.], but %s is given" % mag)
+                        raise RuntimeError("Magmom should be chosen from [-1, 0, 1], but %s is given" % mag)
                     atmgroup_new[elem+'_'+suffix[mag]] = subgrp.tolist()
             else:
                 atmgroup_new[elem] = idx
@@ -2160,7 +2160,7 @@ class Mole(lib.StreamObject):
         cart : boolean
             Using Cartesian GTO basis and integrals (6d,10f,15g)
         magmom : list
-            Collinear spin of each atom. Default is [0.0,]*natm
+            Collinear spin of each atom. Default is [0,]*natm

         ** Following attributes are generated by :func:`Mole.build` **

@@ -2259,7 +2259,7 @@ def __init__(self, **kwargs):
         self.ecp = {}
 # Nuclear property. self.nucprop = {atom_symbol: {key: value}}
         self.nucprop = {}
-# Collinear spin of each atom. self.magmom = [0., ...]
+# Collinear spin of each atom. self.magmom = [0, ...]
         self.magmom = []
 ##################################################
 # don't modify the following private variables, they are not input options
@@ -2470,7 +2470,7 @@ def build(self, dump_input=True, parse_arg=ARGPARSE,
                 Whether to use symmetry.  If given a string of point group
                 name, the given point group symmetry will be used.
             magmom : list
-                Collinear spin of each atom. Default is [0.0,]*natm
+                Collinear spin of each atom. Default is [0,]*natm

         '''
         if not DISABLE_GC:
@@ -2563,9 +2563,9 @@ def build(self, dump_input=True, parse_arg=ARGPARSE,
             # number of electrons are consistent.
             self.nelec

-        if not self.magmom:
-            self.magmom = [0.,]*self.natm
-        if self.spin == 0 and abs(numpy.sum(numpy.asarray(self.magmom)) - self.spin) > 1e-6:
+        if self.magmom is None or len(self.magmom) != self.natm:
+            self.magmom = [0,] * self.natm
+        if self.spin == 0 and abs(numpy.sum(self.magmom) - self.spin) > 1e-6:
             #don't check for unrestricted calcs.
             raise ValueError("mol.magmom is set incorrectly.")

diff --git a/pyscf/pbc/gto/cell.py b/pyscf/pbc/gto/cell.py
index 075a69e8eb..8ae280772a 100644
--- a/pyscf/pbc/gto/cell.py
+++ b/pyscf/pbc/gto/cell.py
@@ -276,12 +276,7 @@ def byteify(inp):
     # Symmetry class cannot be serialized by dumps function.
     # Recreate it manually
     if cell.natm > 0 and cell.space_group_symmetry:
-        from pyscf.pbc.symm import Symmetry
-        _lattice_symm = Symmetry(cell)
-        _lattice_symm.build(space_group_symmetry=True,
-                            symmorphic=cell.symmorphic,
-                            check_mesh_symmetry=False)
-        cell.lattice_symmetry = _lattice_symm
+        cell.build_lattice_symmetry()

     return cell

@@ -1268,6 +1263,32 @@ def symmetrize_mesh(self, mesh=None):
             logger.debug(self, 'mesh %s is symmetrized as %s', mesh, mesh1)
         return mesh1

+    def build_lattice_symmetry(self, check_mesh_symmetry=True):
+        '''Build cell.lattice_symmetry object.
+
+        Kwargs:
+            check_mesh_symmetry : bool
+                For nonsymmorphic symmetry groups, `cell.mesh` may have
+                lower symmetry than the lattice. In this case, if
+                `check_mesh_symmetry` is `True`, the lower symmetry group will
+                be used. Otherwise, if `check_mesh_symmetry` is `False`,
+                the mesh grid will be modified to satisfy the higher symmetry.
+                Default value is `True`.
+
+        Note:
+            This function modifies the attributes of `cell`.
+        '''
+        from pyscf.pbc.symm import Symmetry
+        self.lattice_symmetry = Symmetry(self).build(
+                                    space_group_symmetry=True,
+                                    symmorphic=self.symmorphic,
+                                    check_mesh_symmetry=check_mesh_symmetry)
+        if not check_mesh_symmetry:
+            _mesh_from_build = self._mesh_from_build
+            self.mesh = self.symmetrize_mesh()
+            self._mesh_from_build = _mesh_from_build
+        return self
+
 #Note: Exculde dump_input, parse_arg, basis from kwargs to avoid parsing twice
     def build(self, dump_input=True, parse_arg=mole.ARGPARSE,
               a=None, mesh=None, ke_cutoff=None, precision=None, nimgs=None,
@@ -1484,17 +1505,8 @@ def build(self, dump_input=True, parse_arg=mole.ARGPARSE,
             self._mesh_from_build = True

         if self.space_group_symmetry:
-            from pyscf.pbc.symm import Symmetry
-            _check_mesh_symm = False
-            if not self._mesh_from_build:
-                _check_mesh_symm = True
-            _lattice_symm = Symmetry(self)
-            _lattice_symm.build(space_group_symmetry=True,
-                                symmorphic=self.symmorphic,
-                                check_mesh_symmetry=_check_mesh_symm)
-            self.lattice_symmetry = _lattice_symm
-            if self._mesh_from_build:
-                self._mesh = self.symmetrize_mesh()
+            _check_mesh_symm = not self._mesh_from_build
+            self.build_lattice_symmetry(check_mesh_symmetry=_check_mesh_symm)

         if dump_input and not _built and self.verbose > logger.NOTE:
             self.dump_input()
diff --git a/pyscf/pbc/symm/geom.py b/pyscf/pbc/symm/geom.py
index 32b40c44bd..74119a4483 100644
--- a/pyscf/pbc/symm/geom.py
+++ b/pyscf/pbc/symm/geom.py
@@ -224,7 +224,7 @@ def get_crystal_class(cell, ops=None, tol=SYMPREC):
     """
     cell.a = [[4.0, 0., 0.], [0., 4.0, 0.], [0., 0., 16.0]]
     cell.dimension = 2
-    cell.magmom = [1., 1., -1., -1., 1., -1., 1., 1., -1., -1., 1., -1.]
+    cell.magmom = [1, 1, -1, -1, 1, -1, 1, 1, -1, -1, 1, -1]
     cell.build()

     ops = search_space_group_ops(cell)
diff --git a/pyscf/pbc/tools/pbc.py b/pyscf/pbc/tools/pbc.py
index 9945c59f7f..985c0f2550 100644
--- a/pyscf/pbc/tools/pbc.py
+++ b/pyscf/pbc/tools/pbc.py
@@ -589,6 +589,8 @@ def super_cell(cell, ncopy, wrap_around=False):
     supcell.a = np.einsum('i,ij->ij', ncopy, a)
     mesh = np.asarray(ncopy) * np.asarray(cell.mesh)
     supcell.mesh = (mesh // 2) * 2 + 1
+    supcell.magmom = np.repeat(np.asarray(cell.magmom).reshape(1,-1),
+                               np.prod(ncopy), axis=0).ravel()
     return _build_supcell_(supcell, cell, Ls)

@@ -621,6 +623,7 @@ def _build_supcell_(supcell, cell, Ls):
     Construct supcell ._env directly without calling supcell.build() method.
     This reserves the basis contraction coefficients defined in cell
     '''
+    from pyscf.pbc import gto as pbcgto
     nimgs = len(Ls)
     symbs = [atom[0] for atom in cell._atom] * nimgs
     coords = Ls.reshape(-1,1,3) + cell.atom_coords()
@@ -646,6 +649,9 @@ def _build_supcell_(supcell, cell, Ls):
     supcell._atm = np.asarray(_atm, dtype=np.int32)
     supcell._bas = np.asarray(_bas.reshape(-1, BAS_SLOTS), dtype=np.int32)
     supcell._env = _env
+
+    if isinstance(supcell, pbcgto.Cell) and supcell.space_group_symmetry:
+        supcell.build_lattice_symmetry(not cell._mesh_from_build)
     return supcell

diff --git a/pyscf/pbc/mp/test/test_ksym.py b/pyscf/pbc/mp/test/test_ksym.py
index faeb6188b7..42d7e0c39b 100644
--- a/pyscf/pbc/mp/test/test_ksym.py
+++ b/pyscf/pbc/mp/test/test_ksym.py
@@ -29,6 +29,7 @@ def setUpModule():
     He.atom =[['He' , ( L/2+0., L/2+0., L/2+0.)],]
     He.basis = {'He': [[0, (4.0, 1.0)], [0, (1.0, 1.0)]]}
     He.space_group_symmetry=True
+    He.output = '/dev/null'
     He.build()

     nk = [2,2,2]
@@ -45,6 +46,7 @@ def setUpModule():

 def tearDownModule():
     global He, nk, kpts0, kpts, kmf0, kmf, kmp2ref
+    He.stdout.close()
     del He, nk, kpts0, kpts, kmf0, kmf, kmp2ref

 class KnownValues(unittest.TestCase):
diff --git a/pyscf/pbc/symm/test/test_spg.py b/pyscf/pbc/symm/test/test_spg.py
index 4abc4a5948..02d8ab64f7 100644
--- a/pyscf/pbc/symm/test/test_spg.py
+++ b/pyscf/pbc/symm/test/test_spg.py
@@ -55,7 +55,7 @@ class KnownValues(unittest.TestCase):
     @unittest.skipIf(not has_spglib, "spglib not found")
     def test_D4h_vs_spglib(self):
         dim = 3
-        magmom = [1., 1., -1., -1., 1., -1., 1., 1., -1., -1., 1., -1.]
+        magmom = [1, 1, -1, -1, 1, -1, 1, 1, -1, -1, 1, -1]
         cell = make_cell_D4h(dim, magmom)
         sg = spg.SpaceGroup(cell)

@@ -88,7 +88,7 @@ def test_D4h_2d(self):
                 rot[0,2] == 0 and rot[1,2] == 0 and
                 rot[2,2] != -1):
                 ops2.append(op)
-        ops2.sort
+        ops2.sort()

         dim = 2
         cell = make_cell_D4h(dim)
diff --git a/pyscf/pbc/tools/test/test_pbc.py b/pyscf/pbc/tools/test/test_pbc.py
index ee37c5bd84..3a8774f78e 100644
--- a/pyscf/pbc/tools/test/test_pbc.py
+++ b/pyscf/pbc/tools/test/test_pbc.py
@@ -148,6 +148,16 @@ def test_super_cell(self):
         self.assertAlmostEqual(lib.fp(cl2.atom_coords()), -18.946080642714836, 9)
         self.assertAlmostEqual(lib.fp(cl2._bas[:,gto.ATOM_OF]), 16.515144238434807, 9)

+    def test_super_cell_with_symm(self):
+        cl1 = pbcgto.M(a = 1.4 * numpy.eye(3),
+                       atom ='''He .0 .0 .0''',
+                       basis = 'ccpvdz',
+                       space_group_symmetry=True,
+                       symmorphic=False)
+        self.assertEqual(cl1.lattice_symmetry.nop, 48)
+        cl2 = tools.super_cell(cl1, [2,2,2])
+        self.assertEqual(cl2.lattice_symmetry.nop, 48*8)
+
     def test_cell_plus_imgs(self):
         numpy.random.seed(2)
         cl1 = pbcgto.M(a = numpy.random.random((3,3))*3,


Bug in NAO symmetry-averaged density matrix (issue #1755) (pyscf/pyscf#1803)

Problem with lo.nao.nao function :  non-symmetric results for benzene

Hi,
I'm trying to get natural atomic orbital populations using both lo.orth_ao or lo.nao.nao functions and it appears there is an issue because I don't get symmetric results for a d6h benzene geometry at RHF level. When calling lo.orth_ao with Lowdin or meta_lowdin I do get symmetric enough results.
Just to confirm I was  operating correctly with the matrices, I've just tried your example at
[https://github.com/pyscf/pyscf/blob/master/examples/local_orb/01-pop_with_nao.py](url)
but using the following geometry
mol.atom='''
 6       -0.000000   -1.399560    0.000000
 6       -1.212054   -0.699779   -0.000000
 6        1.212054   -0.699779   -0.000000
 1       -2.156901   -1.245293    0.000000
 1        2.156901   -1.245293    0.000000
 6       -1.212054    0.699779    0.000000
 6        1.212054    0.699779    0.000000
 1       -2.156901    1.245293   -0.000000
 1        2.156901    1.245293   -0.000000
 6       -0.000000    1.399560   -0.000000
 1        0.000000    2.490576    0.000000
 1        0.000000   -2.490576   -0.000000
'''

The results printed are as follows:
 ** Mulliken pop  **
pop of  0 C 1s        2.00000
pop of  0 C 2s        0.95998
pop of  0 C 3s        0.00097
pop of  0 C 2px       1.04921
pop of  0 C 2py       1.17158
pop of  0 C 2pz       0.99624
pop of  0 C 3px       0.00483
pop of  0 C 3py       0.00663
pop of  0 C 3pz       0.00130
pop of  0 C 3dxy      0.00736
pop of  0 C 3dyz      0.00134
pop of  0 C 3dz^2     0.00571
pop of  0 C 3dxz      0.00114
pop of  0 C 3dx2-y2    0.00332
pop of  1 C 1s        2.00000
pop of  1 C 2s        0.96229
pop of  1 C 3s        0.00096
pop of  1 C 2px       1.14762
pop of  1 C 2py       1.08598
pop of  1 C 2pz       1.00342
pop of  1 C 3px       0.00618
pop of  1 C 3py       0.00683
pop of  1 C 3pz       0.00086
pop of  1 C 3dxy      0.00434
pop of  1 C 3dyz      0.00119
pop of  1 C 3dz^2     0.00569
pop of  1 C 3dxz      0.00129
pop of  1 C 3dx2-y2    0.00630
pop of  2 C 1s        2.00000
pop of  2 C 2s        0.95762
pop of  2 C 3s        0.00099
pop of  2 C 2px       1.13327
pop of  2 C 2py       1.07281
pop of  2 C 2pz       0.98791
pop of  2 C 3px       0.00719
pop of  2 C 3py       0.00449
pop of  2 C 3pz       0.00265
pop of  2 C 3dxy      0.00432
pop of  2 C 3dyz      0.00120
pop of  2 C 3dz^2     0.00573
pop of  2 C 3dxz      0.00129
pop of  2 C 3dx2-y2    0.00640
pop of  3 H 1s        0.77763
pop of  3 H 2s        0.00206
pop of  3 H 2px       0.00411
pop of  3 H 2py       0.00208
pop of  3 H 2pz       0.00006
pop of  4 H 1s        0.78690
pop of  4 H 2s        0.00214
pop of  4 H 2px       0.00402
pop of  4 H 2py       0.00205
pop of  4 H 2pz       0.00006
pop of  5 C 1s        2.00000
pop of  5 C 2s        0.95762
pop of  5 C 3s        0.00099
pop of  5 C 2px       1.13327
pop of  5 C 2py       1.07281
pop of  5 C 2pz       0.98791
pop of  5 C 3px       0.00719
pop of  5 C 3py       0.00449
pop of  5 C 3pz       0.00265
pop of  5 C 3dxy      0.00432
pop of  5 C 3dyz      0.00120
pop of  5 C 3dz^2     0.00573
pop of  5 C 3dxz      0.00129
pop of  5 C 3dx2-y2    0.00640
pop of  6 C 1s        2.00000
pop of  6 C 2s        0.96229
pop of  6 C 3s        0.00096
pop of  6 C 2px       1.14762
pop of  6 C 2py       1.08598
pop of  6 C 2pz       1.00342
pop of  6 C 3px       0.00618
pop of  6 C 3py       0.00683
pop of  6 C 3pz       0.00086
pop of  6 C 3dxy      0.00434
pop of  6 C 3dyz      0.00119
pop of  6 C 3dz^2     0.00569
pop of  6 C 3dxz      0.00129
pop of  6 C 3dx2-y2    0.00630
pop of  7 H 1s        0.78690
pop of  7 H 2s        0.00214
pop of  7 H 2px       0.00402
pop of  7 H 2py       0.00205
pop of  7 H 2pz       0.00006
pop of  8 H 1s        0.77763
pop of  8 H 2s        0.00206
pop of  8 H 2px       0.00411
pop of  8 H 2py       0.00208
pop of  8 H 2pz       0.00006
pop of  9 C 1s        2.00000
pop of  9 C 2s        0.95998
pop of  9 C 3s        0.00097
pop of  9 C 2px       1.04921
pop of  9 C 2py       1.17158
pop of  9 C 2pz       0.99624
pop of  9 C 3px       0.00483
pop of  9 C 3py       0.00663
pop of  9 C 3pz       0.00130
pop of  9 C 3dxy      0.00736
pop of  9 C 3dyz      0.00134
pop of  9 C 3dz^2     0.00571
pop of  9 C 3dxz      0.00114
pop of  9 C 3dx2-y2    0.00332
pop of  10 H 1s        0.78221
pop of  10 H 2s        0.00210
pop of  10 H 2px       0.00106
pop of  10 H 2py       0.00507
pop of  10 H 2pz       0.00006
pop of  11 H 1s        0.78221
pop of  11 H 2s        0.00210
pop of  11 H 2px       0.00106
pop of  11 H 2py       0.00507
pop of  11 H 2pz       0.00006
 ** Mulliken atomic charges  **
charge of  0C =     -0.20959
charge of  1C =     -0.23295
charge of  2C =     -0.18585
charge of  3H =      0.21407
charge of  4H =      0.20482
charge of  5C =     -0.18585
charge of  6C =     -0.23295
charge of  7H =      0.20482
charge of  8H =      0.21407
charge of  9C =     -0.20959
charge of  10H =      0.20950
charge of  11H =      0.20950

The asymmetry of the C atoms is way too much.  It is already seen in the contribution of nao's 2s orbitals. Curiously, the C atoms in para position are equivalent to each other.

Thank you very much for your help.

Pedro

diff --git a/pyscf/lo/nao.py b/pyscf/lo/nao.py
index cc5ba2c48d..3cf505cb30 100644
--- a/pyscf/lo/nao.py
+++ b/pyscf/lo/nao.py
@@ -23,7 +23,7 @@
 '''

 import sys
-from functools import reduce
+from functools import reduce, lru_cache
 import numpy
 import scipy.linalg
 from pyscf import lib
@@ -72,6 +72,11 @@ def nao(mol, mf, s=None, restore=True):
     pre_occ, pre_nao = _prenao_sub(mol, p, s)
     cnao = _nao_sub(mol, pre_occ, pre_nao)
     if restore:
+        if mol.cart:
+            # The atomic natural character for Cartesian basis is not clearly
+            # defined. Skip restore.
+            return cnao
+
         # restore natural character
         p_nao = reduce(numpy.dot, (cnao.T, p, cnao))
         s_nao = numpy.eye(p_nao.shape[0])
@@ -98,10 +103,12 @@ def _prenao_sub(mol, p, s):

             if mol.cart:
                 degen = (l + 1) * (l + 2) // 2
+                p_frag = _cart_average_mat(p, l, idx)
+                s_frag = _cart_average_mat(s, l, idx)
             else:
                 degen = l * 2 + 1
-            p_frag = _spheric_average_mat(p, l, idx, degen)
-            s_frag = _spheric_average_mat(s, l, idx, degen)
+                p_frag = _sph_average_mat(p, l, idx)
+                s_frag = _sph_average_mat(s, l, idx)
             e, v = scipy.linalg.eigh(p_frag, s_frag)
             e = e[::-1]
             v = v[:,::-1]
@@ -187,12 +194,24 @@ def _core_val_ryd_list(mol):
         count[ia,l] += nc
     return core_lst, val_lst, rydbg_lst

-def _spheric_average_mat(mat, l, lst, degen=None):
-    if degen is None:
-        degen = l * 2 + 1
-    nd = len(lst) // degen
-    mat_frag = mat[lst][:,lst].reshape(nd,degen,nd,degen)
-    return numpy.einsum('imjn->ij', mat_frag) / degen
+@lru_cache(10)
+def _cart_averge_wt(l):
+    '''Weight matrix for spherical symmetry averaging in Cartensian GTOs'''
+    c = mole.cart2sph(l, normalized='sp')
+    return numpy.einsum('pi,qi->pq', c, c)
+
+def _cart_average_mat(mat, l, lst):
+    degen = (l+1) * (l+2) // 2
+    nshl = len(lst) // degen
+    submat = mat[lst[:,None],lst].reshape(nshl,degen,nshl,degen)
+    wt = _cart_averge_wt(l)
+    return numpy.einsum('imjn,mn->ij', submat, wt) / (2*l+1)
+
+def _sph_average_mat(mat, l, lst):
+    degen = 2 * l + 1
+    nshl = len(lst) // degen
+    submat = mat[lst[:,None],lst].reshape(nshl,degen,nshl,degen)
+    return numpy.einsum('imjm->ij', submat) / (2*l+1)

 def set_atom_conf(element, description):
     '''Change the default atomic core and valence configuration to the one
diff --git a/pyscf/lo/orth.py b/pyscf/lo/orth.py
index 70e5c894a6..c463038fe1 100644
--- a/pyscf/lo/orth.py
+++ b/pyscf/lo/orth.py
@@ -331,28 +331,3 @@ def orth_ao(mf_or_mol, method=ORTH_METHOD, pre_orth_ao=REF_BASIS, s=None):
     return c_orth

 del (ORTH_METHOD)
-
-
-if __name__ == '__main__':
-    from pyscf import scf
-    from pyscf.lo import nao
-    mol = gto.Mole()
-    mol.verbose = 1
-    mol.output = 'out_orth'
-    mol.atom.extend([
-        ['O' , (0. , 0.     , 0.)],
-        [1   , (0. , -0.757 , 0.587)],
-        [1   , (0. , 0.757  , 0.587)] ])
-    mol.basis = {'H': '6-31g',
-                 'O': '6-31g',}
-    mol.build()
-
-    mf = scf.RHF(mol)
-    mf.scf()
-
-    c0 = nao.prenao(mol, mf.make_rdm1())
-    c = orth_ao(mol, 'meta_lowdin', c0)
-
-    s = mol.intor_symmetric('int1e_ovlp_sph')
-    p = reduce(numpy.dot, (s, mf.make_rdm1(), s))
-    print(reduce(numpy.dot, (c.T, p, c)).diagonal())


Fix pbc empty cell error (pyscf/pyscf#1821)

example-pbc: 40-customize_kpts_hamiltonian.py

Hi all,
I tried to run 40-customize_kpts_hamiltonian.py, but got the error:

'''
Traceback (most recent call last):
  File "/home/jtolle/Software/pyscf_master/pyscf/examples/pbc/40-customize_kpts_hamiltonian.py", line 33, in <module>
    cell = pbc.gto.M(
  File "/home/jtolle/Software/pyscf_master/pyscf/pyscf/pbc/gto/cell.py", line 73, in M
    cell.build(**kwargs)
  File "/home/jtolle/Software/pyscf_master/pyscf/pyscf/pbc/gto/cell.py", line 1522, in build
    atom_boundary_max = scaled_atom_coords[:,:self.dimension].max(axis=0)
  File "/home/jtolle/anaconda3/lib/python3.9/site-packages/numpy/core/_methods.py", line 41, in _amax
    return umr_maximum(a, axis, None, out, keepdims, initial, where)
ValueError: zero-size array to reduction operation maximum which has no identity
'''
(using the current master)

Using this commit: 71391d862c3db83db28553bbb1518acee466662c it works.
Would be great if someone can help to resolve this issue.
Best regards,
Johannes

diff --git a/pyscf/pbc/gto/cell.py b/pyscf/pbc/gto/cell.py
index 8ae280772a..8fc38d5e61 100644
--- a/pyscf/pbc/gto/cell.py
+++ b/pyscf/pbc/gto/cell.py
@@ -1517,7 +1517,7 @@ def build(self, dump_input=True, parse_arg=mole.ARGPARSE,
             logger.info(self, 'low_dim_ft_type = %s', self.low_dim_ft_type)
             logger.info(self, 'Cell volume = %g', self.vol)
             # Check atoms coordinates
-            if self.dimension > 0:
+            if self.dimension > 0 and self.natm > 0:
                 scaled_atom_coords = np.linalg.solve(_a.T, self.atom_coords().T).T
                 atom_boundary_max = scaled_atom_coords[:,:self.dimension].max(axis=0)
                 atom_boundary_min = scaled_atom_coords[:,:self.dimension].min(axis=0)
diff --git a/pyscf/pbc/tools/pbc.py b/pyscf/pbc/tools/pbc.py
index 985c0f2550..447f752590 100644
--- a/pyscf/pbc/tools/pbc.py
+++ b/pyscf/pbc/tools/pbc.py
@@ -503,7 +503,7 @@ def get_lattice_Ls(cell, nimgs=None, rcut=None, dimension=None, discard=True):
     if rcut is None:
         rcut = cell.rcut

-    if dimension == 0 or rcut <= 0:
+    if dimension == 0 or rcut <= 0 or cell.natm == 0:
         return np.zeros((1, 3))

     a = cell.lattice_vectors()

diff --git a/pyscf/pbc/gto/test/test_cell.py b/pyscf/pbc/gto/test/test_cell.py
index e7aeafe577..61dc9f6e72 100644
--- a/pyscf/pbc/gto/test/test_cell.py
+++ b/pyscf/pbc/gto/test/test_cell.py
@@ -478,6 +478,11 @@ def test_eval_gto(self):
         ao_value = cell.pbc_eval_gto("GTOval_ip_cart", coords, kpts=cell.make_kpts([3]*3))
         self.assertAlmostEqual(lib.fp(ao_value), (0.38051517609460028+0.062526488684770759j), 9)

+    def test_empty_cell(self):
+        cell = pgto.M(a=np.eye(3)*4)
+        Ls = pbctools.get_lattice_Ls(cell)
+        self.assertEqual(abs(Ls-np.zeros([1,3])).max(), 0)
+
 if __name__ == '__main__':
     print("Full Tests for pbc.gto.cell")
     unittest.main()


Rewrite _break_dm_spin_symm (pyscf/pyscf#1841)

Rewrite _break_dm_spin_symm to form a doublet state density instead of zeroing out interatomic blocks of density matrix.

Closes #1839

strange behavior of init_guess_breaksym

Hi,

I'm wondering if the breaking symmetry option

https://github.com/pyscf/pyscf/blob/master/pyscf/scf/uhf.py#L29
https://github.com/pyscf/pyscf/blob/master/pyscf/scf/uhf.py#L773

works as it is supposed to do.
I would propose not to turn on the option by default
for the initial density matrix methods.
For augmented basis sets, e.g., aug-pc-n (n=1,2..),
the number of electrons in the beta channel explodes
using MINAO with `init_guess_breaksym=True`.
The problem is reproducible with all 2.X.Y PySCF versions.
For benzene, this leads already to significant convergence issues.

Please see the attached script to check to problem yourself.

**Tab. 1 initial_guess_dm=True, PySCF version: 2.3.0**
| basis| N$`_{elec}`$(from initial guess) = (N$`_{\alpha}`$, N$`_{\beta}`$)  |
| :--: | :-----------------: |
| pc-0 | (20.951768099569655, 20.91592508353395) |
| pc-1 | (20.992586040795963, 22.024225871340562) |
| pc-2 | (20.99925998574021, 20.614775142071956) |
| aug-pc-0 | (20.964939403069046, 24.754095109846062) |
| aug-pc-1 | (20.996679459866204, 39.278482611398196) |
| aug-pc-2 | (20.99947479330699, 352.8876628321081)|

**Tab. 2 initial_guess_dm=False, PySCF version: 2.3.0**
| basis| N$`_{elec}`$(from initial guess) = (N$`_{\alpha}`$, N$`_{\beta}`$)  |
| :--: | :-----------------: |
| pc-0 | (20.951768099569655, 20.951768099569655)|
| pc-1 | (20.992586040795963, 20.992586040795963) |
| pc-2 | (20.99925998574021, 20.99925998574021) |
| aug-pc-0 | (20.964939403069046, 20.964939403069046)|
| aug-pc-1 | (20.996679459866204, 20.996679459866204)|
| aug-pc-2 | (20.99947479330699, 20.99947479330699) |

```python=
import pyscf
from pyscf import gto, scf

def dft(basis="pc-0",init_guess_breaksym=True):
    mol = gto.M(atom='''
C       0.000000000000000      1.391976000000000      0.000000000000000
C       1.205486000000000      0.695988000000000      0.000000000000000
C       1.205486000000000     -0.695988000000000      0.000000000000000
C       0.000000000000000     -1.391976000000000      0.000000000000000
C      -1.205486000000000     -0.695988000000000      0.000000000000000
C      -1.205486000000000      0.695988000000000      0.000000000000000
H       0.000000000000000      2.472230000000000      0.000000000000000
H       2.141014000000000      1.236115000000000      0.000000000000000
H       2.141014000000000     -1.236115000000000      0.000000000000000
H       0.000000000000000     -2.472230000000000      0.000000000000000
H      -2.141014000000000     -1.236115000000000      0.000000000000000
H      -2.141014000000000      1.236115000000000      0.000000000000000
''')

    mol.verbose = 9
    mol.basis = basis
    mol.output = "dft.log"
    mol.build()
    mf = scf.UKS(mol)
    mf.init_guess_breaksym = init_guess_breaksym
    mf.max_cycle = 0
    mf.xc = "spw92"
    mf.conv_tol = 1e-8
    mf.grids.level = 7
    mf.grids.prune = None
    mf.grids.build()
    # mf.init_guess = "vsap"
    edft = mf.kernel()

def main():
    print(f"PySCF version: {pyscf.__version__}")
    dft(basis="aug-pc-1",init_guess_breaksym=True)

if __name__ == '__main__':
    main()

```

Best,
Wanja, Kai, and Sebastian

### Minimal API referenced by tests (typed signatures)
_Added only when tests call brand‑new symbols introduced by the fix; types reflect annotations present in the repo or `Unknown` if not annotated._

- Function in `pyscf/scf/uhf.py`: `def init_guess_by_minao(mol, breaksym) -> numpy.ndarray: ...`
  **Inputs:** mol: gto.Mole, breaksym: bool
  **Output:** numpy.ndarray

- Function in `pyscf/scf/uhf.py`: `def init_guess_by_1e(mol, breaksym) -> numpy.ndarray: ...`
  **Inputs:** mol: gto.Mole, breaksym: bool
  **Output:** numpy.ndarray

```python
def init_guess_by_minao(mol, breaksym) -> numpy.ndarray: ...
def init_guess_by_1e(mol, breaksym) -> numpy.ndarray: ...
```

diff --git a/pyscf/pbc/scf/kuhf.py b/pyscf/pbc/scf/kuhf.py
index ff911afac4..8827cbad6e 100644
--- a/pyscf/pbc/scf/kuhf.py
+++ b/pyscf/pbc/scf/kuhf.py
@@ -369,6 +369,7 @@ class KUHF(khf.KSCF):
     '''UHF class with k-point sampling.
     '''
     conv_tol_grad = getattr(__config__, 'pbc_scf_KSCF_conv_tol_grad', None)
+    init_guess_breaksym = getattr(__config__, 'scf_uhf_init_guess_breaksym', 1)

     _keys = {"init_guess_breaksym"}

@@ -390,7 +391,6 @@ def __init__(self, cell, kpts=np.zeros((1,3)),
                  exxdiv=getattr(__config__, 'pbc_scf_SCF_exxdiv', 'ewald')):
         khf.KSCF.__init__(self, cell, kpts, exxdiv)
         self.nelec = None
-        self.init_guess_breaksym = None

     @property
     def nelec(self):
diff --git a/pyscf/pbc/scf/uhf.py b/pyscf/pbc/scf/uhf.py
index 3dfd53866b..ea48c75efb 100644
--- a/pyscf/pbc/scf/uhf.py
+++ b/pyscf/pbc/scf/uhf.py
@@ -107,6 +107,8 @@ def dip_moment(cell, dm, unit='Debye', verbose=logger.NOTE,
 class UHF(pbchf.SCF):
     '''UHF class for PBCs.
     '''
+    init_guess_breaksym = getattr(__config__, 'scf_uhf_init_guess_breaksym', 1)
+
     _keys = {"init_guess_breaksym"}

     init_guess_by_minao  = mol_uhf.UHF.init_guess_by_minao
@@ -134,7 +136,6 @@ def __init__(self, cell, kpt=np.zeros(3),
                  exxdiv=getattr(__config__, 'pbc_scf_SCF_exxdiv', 'ewald')):
         pbchf.SCF.__init__(self, cell, kpt, exxdiv)
         self.nelec = None
-        self.init_guess_breaksym = None

     @property
     def nelec(self):
diff --git a/pyscf/scf/uhf.py b/pyscf/scf/uhf.py
index 4797dbe5d9..c4c1660ceb 100644
--- a/pyscf/scf/uhf.py
+++ b/pyscf/scf/uhf.py
@@ -26,48 +26,34 @@

 WITH_META_LOWDIN = getattr(__config__, 'scf_analyze_with_meta_lowdin', True)
 PRE_ORTH_METHOD = getattr(__config__, 'scf_analyze_pre_orth_method', 'ANO')
-BREAKSYM = getattr(__config__, 'scf_uhf_init_guess_breaksym', True)
 MO_BASE = getattr(__config__, 'MO_BASE', 1)

-def init_guess_by_minao(mol, breaksym=BREAKSYM):
+def init_guess_by_minao(mol, breaksym=None):
     '''Generate initial guess density matrix based on ANO basis, then project
     the density matrix to the basis set defined by ``mol``

     Returns:
         Density matrices, a list of 2D ndarrays for alpha and beta spins
     '''
-    dm = hf.init_guess_by_minao(mol)
-    dma = dmb = dm*.5
-    if breaksym:
-        dma, dmb = _break_dm_spin_symm(mol, (dma, dmb))
-    return numpy.array((dma,dmb))
+    return UHF(mol).init_guess_by_minao(mol, breaksym)

-def init_guess_by_1e(mol, breaksym=BREAKSYM):
+def init_guess_by_1e(mol, breaksym=None):
     return UHF(mol).init_guess_by_1e(mol, breaksym)

-def init_guess_by_atom(mol, breaksym=BREAKSYM):
-    dm = hf.init_guess_by_atom(mol)
-    dma = dmb = dm*.5
-    if mol.spin == 0 and breaksym:
-        #Add off-diagonal part for alpha DM
-        dma = mol.intor_symmetric('int1e_ovlp') * 1e-2
-        for b0, b1, p0, p1 in mol.aoslice_by_atom():
-            dma[p0:p1,p0:p1] = dmb[p0:p1,p0:p1]
-    return numpy.array((dma,dmb))
-
-def init_guess_by_huckel(mol, breaksym=BREAKSYM):
+def init_guess_by_atom(mol, breaksym=None):
+    return UHF(mol).init_guess_by_atom(mol, breaksym)
+
+def init_guess_by_huckel(mol, breaksym=None):
     return UHF(mol).init_guess_by_huckel(mol, breaksym)

-def init_guess_by_mod_huckel(mol, breaksym=BREAKSYM):
+def init_guess_by_mod_huckel(mol, breaksym=None):
     return UHF(mol).init_guess_by_mod_huckel(mol, breaksym)

-def init_guess_by_sap(mol, sap_basis, breaksym=BREAKSYM, **kwargs):
-    dm = hf.init_guess_by_sap(mol, sap_basis)
-    dma = dmb = dm*.5
-    if breaksym:
-        dma, dmb = _break_dm_spin_symm(mol, (dma, dmb))
-    return numpy.array((dma,dmb))
+def init_guess_by_sap(mol, sap_basis, breaksym=None, **kwargs):
+    mf = UHF(mol)
+    mf.sap_basis = sap_basis
+    return mf.init_guess_by_sap(mol, breaksym)

 def init_guess_by_chkfile(mol, chkfile_name, project=None):
     '''Read SCF chkfile and make the density matrix for UHF initial guess.
@@ -127,14 +113,24 @@ def fproj(mo):
         dm = make_rdm1([fproj(mo[0]),fproj(mo[1])], mo_occ)
     return dm

-def _break_dm_spin_symm(mol, dm):
+def _break_dm_spin_symm(mol, dm, breaksym=1):
     dma, dmb = dm
     # For spin polarized system, no need to manually break spin symmetry
-    if mol.spin == 0 and abs(dma - dmb).max() < 1e-2:
-        #remove off-diagonal part of beta DM
-        dmb = numpy.zeros_like(dma)
-        for b0, b1, p0, p1 in mol.aoslice_by_atom():
-            dmb[...,p0:p1,p0:p1] = dma[...,p0:p1,p0:p1]
+    if breaksym and mol.spin == 0 and abs(dma - dmb).max() < 1e-2:
+        if breaksym == 1:
+            #remove off-diagonal part of beta DM
+            dmb = numpy.zeros_like(dma)
+            for b0, b1, p0, p1 in mol.aoslice_by_atom():
+                dmb[...,p0:p1,p0:p1] = dma[...,p0:p1,p0:p1]
+        else:
+            # Adjust num. electrons for density matrices (issue #1839)
+            # Get overlap matrix
+            s1e = mol.intor_symmetric('int1e_ovlp')
+            # Compute norm of density matrices
+            nelec_half = numpy.einsum('ij,ji->', dma, s1e)
+            # Scale density matrices to form doublet state
+            dma = dma * (nelec_half+1) / nelec_half
+            dmb = dmb * (nelec_half-1) / nelec_half
     return dma, dmb

 def get_init_guess(mol, key='minao', **kwargs):
@@ -757,8 +753,12 @@ class UHF(hf.SCF):
             If given, freeze the number of (alpha,beta) electrons to the given value.
         level_shift : number or two-element list
             level shift (in Eh) for alpha and beta Fock if two-element list is given.
-        init_guess_breaksym : logical
-            If given, overwrite BREAKSYM.
+        init_guess_breaksym : int
+             This configuration controls the algorithm used to break the spin
+             symmetry of the initial guess:
+             - 0 to disable symmetry breaking in the initial guess.
+             - 1 to use the default algorithm introduced in pyscf-1.7.
+             - 2 to adjust the num. electrons for spin-up and spin-down density matrices (issue #1839).

     Examples:

@@ -770,7 +770,7 @@ class UHF(hf.SCF):
     S^2 = 0.7570150, 2S+1 = 2.0070027
     '''

-    init_guess_breaksym = None
+    init_guess_breaksym = getattr(__config__, 'scf_uhf_init_guess_breaksym', 1)

     _keys = {"init_guess_breaksym"}

@@ -847,29 +847,34 @@ def get_init_guess(self, mol=None, key='minao', **kwargs):
             logger.debug1(self, 'Nelec from initial guess = %s', nelec)
         return dm

-    def init_guess_by_minao(self, mol=None, breaksym=BREAKSYM):
+    def init_guess_by_minao(self, mol=None, breaksym=None):
         '''Initial guess in terms of the overlap to minimal basis.'''
         if mol is None: mol = self.mol
-        user_set_breaksym = getattr(self, "init_guess_breaksym", None)
-        if user_set_breaksym is not None:
-            breaksym = user_set_breaksym
+        if breaksym is None: breaksym = self.init_guess_breaksym
         # For spin polarized system, no need to manually break spin symmetry
-        if mol.spin != 0:
-            breaksym = False
-        return init_guess_by_minao(mol, breaksym)
+        dm = hf.init_guess_by_minao(mol)
+        dma = dmb = dm*.5
+        dma, dmb = _break_dm_spin_symm(mol, (dma, dmb), breaksym)
+        return numpy.array((dma, dmb))

-    def init_guess_by_atom(self, mol=None, breaksym=BREAKSYM):
+    def init_guess_by_atom(self, mol=None, breaksym=None):
         if mol is None: mol = self.mol
-        user_set_breaksym = getattr(self, "init_guess_breaksym", None)
-        if user_set_breaksym is not None:
-            breaksym = user_set_breaksym
-        return init_guess_by_atom(mol, breaksym)
+        if breaksym is None: breaksym = self.init_guess_breaksym
+        dm = hf.init_guess_by_atom(mol)
+        dma = dmb = dm*.5
+        if mol.spin == 0 and breaksym:
+            if breaksym == 1:
+                #Add off-diagonal part for alpha DM
+                dma = mol.intor_symmetric('int1e_ovlp') * 1e-2
+                for b0, b1, p0, p1 in mol.aoslice_by_atom():
+                    dma[p0:p1,p0:p1] = dmb[p0:p1,p0:p1]
+            else:
+                dma, dmb = _break_dm_spin_symm(mol, (dma, dmb), breaksym)
+        return numpy.array((dma,dmb))

-    def init_guess_by_huckel(self, mol=None, breaksym=BREAKSYM):
+    def init_guess_by_huckel(self, mol=None, breaksym=None):
         if mol is None: mol = self.mol
-        user_set_breaksym = getattr(self, "init_guess_breaksym", None)
-        if user_set_breaksym is not None:
-            breaksym = user_set_breaksym
+        if breaksym is None: breaksym = self.init_guess_breaksym
         logger.info(self, 'Initial guess from on-the-fly Huckel, doi:10.1021/acs.jctc.8b01089.')
         mo_energy, mo_coeff = hf._init_guess_huckel_orbitals(mol, updated_rule = False)
         mo_energy = (mo_energy, mo_energy)
@@ -880,11 +885,9 @@ def init_guess_by_huckel(self, mol=None, breaksym=BREAKSYM):
             dma, dmb = _break_dm_spin_symm(mol, (dma, dmb))
         return numpy.array((dma,dmb))

-    def init_guess_by_mod_huckel(self, mol=None, breaksym=BREAKSYM):
+    def init_guess_by_mod_huckel(self, mol=None, breaksym=None):
         if mol is None: mol = self.mol
-        user_set_breaksym = getattr(self, "init_guess_breaksym", None)
-        if user_set_breaksym is not None:
-            breaksym = user_set_breaksym
+        if breaksym is None: breaksym = self.init_guess_breaksym
         logger.info(self, '''Initial guess from on-the-fly Huckel, doi:10.1021/acs.jctc.8b01089,
 employing the updated GWH rule from doi:10.1021/ja00480a005.''')
         mo_energy, mo_coeff = hf._init_guess_huckel_orbitals(mol, updated_rule = True)
@@ -893,14 +896,12 @@ def init_guess_by_mod_huckel(self, mol=None, breaksym=BREAKSYM):
         mo_occ = self.get_occ(mo_energy, mo_coeff)
         dma, dmb = self.make_rdm1(mo_coeff, mo_occ)
         if breaksym:
-            dma, dmb = _break_dm_spin_symm(mol, (dma, dmb))
+            dma, dmb = _break_dm_spin_symm(mol, (dma, dmb), breaksym)
         return numpy.array((dma,dmb))

-    def init_guess_by_1e(self, mol=None, breaksym=BREAKSYM):
+    def init_guess_by_1e(self, mol=None, breaksym=None):
         if mol is None: mol = self.mol
-        user_set_breaksym = getattr(self, "init_guess_breaksym", None)
-        if user_set_breaksym is not None:
-            breaksym = user_set_breaksym
+        if breaksym is None: breaksym = self.init_guess_breaksym
         logger.info(self, 'Initial guess from hcore.')
         h1e = self.get_hcore(mol)
         s1e = self.get_ovlp(mol)
@@ -911,12 +912,13 @@ def init_guess_by_1e(self, mol=None, breaksym=BREAKSYM):
         dma, dmb = self.make_rdm1(mo_coeff, mo_occ)
         natm = getattr(mol, 'natm', 0)  # handle custom Hamiltonian
         if natm > 0 and breaksym:
-            dma, dmb = _break_dm_spin_symm(mol, (dma, dmb))
+            dma, dmb = _break_dm_spin_symm(mol, (dma, dmb), breaksym)
         return numpy.array((dma,dmb))

-    def init_guess_by_sap(self, mol=None, breaksym=BREAKSYM, **kwargs):
+    def init_guess_by_sap(self, mol=None, breaksym=None, **kwargs):
         from pyscf.gto.basis import load
         if mol is None: mol = self.mol
+        if breaksym is None: breaksym = self.init_guess_breaksym
         sap_basis = self.sap_basis
         logger.info(self, '''Initial guess from superposition of atomic potentials (doi:10.1021/acs.jctc.8b01089)
 This is the Gaussian fit version as described in doi:10.1063/5.0004046.''')
@@ -935,11 +937,11 @@ def init_guess_by_sap(self, mol=None, breaksym=BREAKSYM, **kwargs):
             for key in sap_basis:
                 sapbas[key] = numpy.asarray(sap_basis[key][0][1:], dtype=float)
         else:
-            logger.error(self, 'sap_basis is of an unexpected datatype.')
-        user_set_breaksym = getattr(self, "init_guess_breaksym", None)
-        if user_set_breaksym is not None:
-            breaksym = user_set_breaksym
-        return init_guess_by_sap(mol, sap_basis=sapbas, breaksym=breaksym)
+            raise RuntimeError('sap_basis is of an unexpected datatype.')
+        dm = hf.init_guess_by_sap(mol, sapbas)
+        dma = dmb = dm*.5
+        dma, dmb = _break_dm_spin_symm(mol, (dma, dmb), breaksym)
+        return numpy.array((dma,dmb))

     def init_guess_by_chkfile(self, chkfile=None, project=None):
         if chkfile is None: chkfile = self.chkfile
@@ -1124,4 +1126,4 @@ class HF1e(UHF):
     def spin_square(self, mo_coeff=None, s=None):
         return .75, 2

-del (WITH_META_LOWDIN, PRE_ORTH_METHOD, BREAKSYM)
+del (WITH_META_LOWDIN, PRE_ORTH_METHOD)

diff --git a/pyscf/scf/test/test_uhf.py b/pyscf/scf/test/test_uhf.py
index addba13ea1..a60501eb16 100644
--- a/pyscf/scf/test/test_uhf.py
+++ b/pyscf/scf/test/test_uhf.py
@@ -70,10 +70,14 @@ def tearDownModule():

 class KnownValues(unittest.TestCase):
     def test_init_guess_minao(self):
+        mf = scf.UHF(mol)
         dm1 = mf.init_guess_by_minao(mol, breaksym=False)
         self.assertAlmostEqual(abs(dm1).sum(), 13.649710173723337, 9)
         dm2 = scf.uhf.get_init_guess(mol, key='minao')
         self.assertAlmostEqual(abs(dm2).sum(), 12.913908927027279, 9)
+        mf.init_guess_breaksym = 2
+        dm1 = mf.init_guess_by_minao(mol)
+        self.assertAlmostEqual(abs(dm1).sum(), 13.649710173723337, 9)

     def test_init_guess_1e(self):
         dm1 = scf.uhf.init_guess_by_1e(mol, breaksym=False)


Fix the edge case CAS(2,2) for Selected CI (issue #1808) (pyscf/pyscf#1845)

Selected CI gives wrong results for a CAS of (2,2)

Dear developers,

I was trying to use selected CI as my CASCI solver by looking at the following example: https://github.com/pyscf/pyscf.github.io/blob/master/examples/mcscf/50-casscf_with_selected_ci.py

However, the selected CI results were wrong when I choose a CAS of (2,2):
```
from pyscf import gto, scf, mcscf, fci

b = 1.2
mol = gto.M(
    atom = 'N 0 0 0; N 0 0 %f'%b,
    basis = 'cc-pvdz',
)
mf = scf.RHF(mol).run()
mc = mcscf.CASCI(mf, 2, 2).run()

#
# Use SCI (Selected CI) to replace fcisolver
#
mc = mcscf.CASCI(mf, 2, 2)
mc.fcisolver = fci.SCI(mol)
mc.kernel()
```

Here are the results. I am using pyscf version `2.1.1`
```
converged SCF energy = -108.914051975052

CASCI E = -108.94356411985  E(CI) = -1.71486588690826  S^2 = 0.0000000

CASCI E = -108.914051975052  E(CI) = -1.68535374211038  S^2 = 0.0000000
```
The selected CI seems to have only the doubly occupied determinant and misses the other doubly excited determinant. I know this is an edge case but thought of bringing this to your attention nevertheless. Thanks!

diff --git a/pyscf/fci/selected_ci.py b/pyscf/fci/selected_ci.py
index c51b8a811a..14aae7861c 100644
--- a/pyscf/fci/selected_ci.py
+++ b/pyscf/fci/selected_ci.py
@@ -115,7 +115,7 @@ def select_strs(myci, eri, eri_pq_max, civec_max, strs, norb, nelec):
     strs = numpy.asarray(strs, dtype=numpy.int64)
     nstrs = len(strs)
     nvir = norb - nelec
-    strs_add = numpy.empty((nstrs*(nelec*nvir)**2//4), dtype=numpy.int64)
+    strs_add = numpy.empty((nstrs*((nelec+1)*(nvir+1))**2//4), dtype=numpy.int64)
     libfci.SCIselect_strs.restype = ctypes.c_int
     nadd = libfci.SCIselect_strs(strs_add.ctypes.data_as(ctypes.c_void_p),
                                  strs.ctypes.data_as(ctypes.c_void_p),

diff --git a/pyscf/fci/test/test_selected_ci.py b/pyscf/fci/test/test_selected_ci.py
index b138f8468d..1300ea63cf 100644
--- a/pyscf/fci/test/test_selected_ci.py
+++ b/pyscf/fci/test/test_selected_ci.py
@@ -495,6 +495,16 @@ def test_guess_wfnsym(self):
         ci0[:] = 0
         self.assertRaises(RuntimeError, cis.guess_wfnsym, norb, nelec, ci0, wfnsym=1)

+    # The edge case CAS(2, 2). issue #1808
+    def test_cas_2_2(self):
+        mol = gto.M(atom='H 0 0 0; H 0 0 1.')
+        mc = mol.RHF().run().CASCI(2, 2)
+        mc.fcisolver = fci.SCI(mol)
+        mc.run()
+        self.assertAlmostEqual(mc.e_tot, -1.1011503302326, 9)
+        self.assertAlmostEqual(abs(mc.ci[0,0]), .984513596, 5)
+        self.assertAlmostEqual(abs(mc.ci[1,1]), .175308242, 5)
+

 def gen_des_linkstr(strs, norb, nelec):
     '''Given intermediates, the link table to generate input strs
@@ -691,4 +701,3 @@ def select_strs(myci, eri, eri_pq_max, civec_max, strs, norb, nelec):
 if __name__ == "__main__":
     print("Full Tests for selected_ci")
     unittest.main()
-