Skip to content

Commit 3056e91

Browse files
committed
Fix point-group symmetry verficication due to the np.round issue
1 parent 9253c88 commit 3056e91

File tree

2 files changed

+56
-3
lines changed

2 files changed

+56
-3
lines changed

pyscf/symm/geom.py

+46-3
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,19 @@ def parallel_vectors(v1, v2, tol=TOLERANCE):
5757
return (abs(cos-1) < TOLERANCE) | (abs(cos+1) < TOLERANCE)
5858

5959
def argsort_coords(coords, decimals=None):
60+
# * np.round for decimal places can lead to more errors than the actual
61+
# difference between two numbers. For example,
62+
# np.round([0.1249999999,0.1250000001], 2) => [0.124, 0.125]
63+
# np.round([0.1249999999,0.1250000001], 3) => [0.125, 0.125]
64+
# When loosen tolerance is used, compared to the more strict tolerance,
65+
# the coordinates might look more different.
66+
# * Using the power of two as the factor can reduce such errors, although not
67+
# faithfully rounding to the required decimals.
6068
if decimals is None:
61-
decimals = int(-numpy.log10(TOLERANCE)) - 1
62-
coords = numpy.around(coords, decimals=decimals)
69+
fac = 2**int(-numpy.log2(TOLERANCE)+.5)
70+
else:
71+
fac = 2**int(3.3219281 * decimals + .5)
72+
coords = numpy.around(coords*fac)
6373
idx = numpy.lexsort((coords[:,2], coords[:,1], coords[:,0]))
6474
return idx
6575

@@ -482,7 +492,11 @@ def symm_identical_atoms(gpname, atoms):
482492
newc = numpy.dot(coords, op)
483493
idx = argsort_coords(newc)
484494
if not numpy.allclose(coords0, newc[idx], atol=TOLERANCE):
485-
raise PointGroupSymmetryError('Symmetry identical atoms not found')
495+
raise PointGroupSymmetryError(
496+
'Symmetry identical atoms not found. This may be due to '
497+
'the strict setting of the threshold symm.geom.TOLERANCE. '
498+
'Consider adjusting the tolerance.')
499+
486500
dup_atom_ids.append(idx)
487501

488502
dup_atom_ids = numpy.sort(dup_atom_ids, axis=0).T
@@ -521,6 +535,13 @@ def check_symm(gpname, atoms, basis=None):
521535
opdic = symm_ops(gpname)
522536
ops = [opdic[op] for op in OPERATOR_TABLE[gpname]]
523537
rawsys = SymmSys(atoms, basis)
538+
539+
# A fast check using Casimir tensors
540+
coords = rawsys.atoms[:,1:]
541+
for op in ops:
542+
if not is_identical_geometry(coords, coords.dot(op), rawsys.weights):
543+
return False
544+
524545
for lst in rawsys.atomtypes.values():
525546
coords = rawsys.atoms[lst,1:]
526547
idx = argsort_coords(coords)
@@ -540,6 +561,27 @@ def shift_atom(atoms, orig, axis):
540561
c = numpy.dot(c - orig, numpy.array(axis).T)
541562
return [[atoms[i][0], c[i]] for i in range(len(atoms))]
542563

564+
def is_identical_geometry(coords1, coords2, weights):
565+
'''A fast check to compare the geometry of two molecules using Casimir tensors'''
566+
if coords1.shape != coords2.shape:
567+
return False
568+
for order in range(1, 4):
569+
if abs(casimir_tensors(coords1[lst], weights, order) -
570+
casimir_tensors(coords2[lst], weights, order)).max() > TOLERANCE:
571+
return False
572+
return True
573+
574+
def casimir_tensors(r, q, order=1):
575+
if order == 1:
576+
return q.dot(r)
577+
elif order == 2:
578+
return numpy.einsum('i,ix,iy->xy', q, r, r)
579+
elif order == 3:
580+
return numpy.einsum('i,ix,iy,iz->xyz', q, r, r, r)
581+
else:
582+
raise NotImplementedError
583+
584+
543585
class RotationAxisNotFound(PointGroupSymmetryError):
544586
pass
545587

@@ -572,6 +614,7 @@ def __init__(self, atoms, basis=None):
572614
fake_chgs.append([mole.charge(ksymb)] * len(lst))
573615
coords = numpy.array(numpy.vstack(coords), dtype=float)
574616
fake_chgs = numpy.hstack(fake_chgs)
617+
self.weights = fake_chgs
575618
self.charge_center = numpy.einsum('i,ij->j', fake_chgs, coords)/fake_chgs.sum()
576619
coords = coords - self.charge_center
577620

pyscf/symm/test/test_geom.py

+10
Original file line numberDiff line numberDiff line change
@@ -886,6 +886,16 @@ def test_c2v_shifted(self):
886886
self.assertEqual(l, 'C2v')
887887
self.assertAlmostEqual(abs(axes - numpy.diag(axes.diagonal())).max(), 0, 9)
888888

889+
def test_geometry_small_discrepancy(self):
890+
# issue 2713
891+
mol = gto.M(
892+
atom='''
893+
O 0.000000 0.000000 0.900000
894+
H 0.000000 0.000000 0.000000
895+
H 0.914864 0.000000 1.249646
896+
H -0.498887 0.766869 1.249646''',
897+
charge=1, symmetry=True)
898+
self.assertEqual(mol.groupname, 'Cs')
889899

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

0 commit comments

Comments
 (0)