Repository: pyscf/pyscf — pull request #1643 (python)
Title: Fix molecule orientation bug for C2v symmetry

Fix issue #1464 

Resolved issues:
title: Inconsistent symmetries under trivial transformation
body: 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.

Hints:

Fix patch:
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:


Test patch:
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)