Skip to content

Commit b1c3fd4

Browse files
authored
Symmetry detection bug due to np.round tolerance (pyscf#2715)
* Fix point-group symmetry verficication due to the np.round issue * indexing bug * Fix symmetry detection tests * Fix argsort_coords * Fix bug in sort_coords * Update comments
1 parent 672d940 commit b1c3fd4

File tree

3 files changed

+73
-8
lines changed

3 files changed

+73
-8
lines changed

pyscf/symm/geom.py

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,18 +56,31 @@ def parallel_vectors(v1, v2, tol=TOLERANCE):
5656
cos = numpy.dot(_normalize(v1), _normalize(v2))
5757
return (abs(cos-1) < TOLERANCE) | (abs(cos+1) < TOLERANCE)
5858

59-
def argsort_coords(coords, decimals=None):
59+
def argsort_coords(coords, decimals=None, tol=0.05):
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.12, 0.13]
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.
68+
# * For normal molecules, tol~=0.1 in coordinates is enough to distinguish
69+
# atoms in molecule. A very tight threshold is not appropriate here. With
70+
# tight threshold, small differences in coordinates may lead to different
71+
# arg orders.
6072
if decimals is None:
61-
decimals = int(-numpy.log10(TOLERANCE)) - 1
62-
coords = numpy.around(coords, decimals=decimals)
73+
fac = 2**int(-numpy.log2(tol))
74+
else:
75+
fac = 2**int(3.3219281 * decimals)
76+
# +.5 for rounding to the nearest integer
77+
coords = (coords*fac + .5).astype(int)
6378
idx = numpy.lexsort((coords[:,2], coords[:,1], coords[:,0]))
6479
return idx
6580

66-
def sort_coords(coords, decimals=None):
67-
if decimals is None:
68-
decimals = int(-numpy.log10(TOLERANCE)) - 1
81+
def sort_coords(coords, decimals=None, tol=0.05):
6982
coords = numpy.asarray(coords)
70-
idx = argsort_coords(coords, decimals=decimals)
83+
idx = argsort_coords(coords, tol=tol)
7184
return coords[idx]
7285

7386
# ref. http://en.wikipedia.org/wiki/Rotation_matrix
@@ -482,7 +495,11 @@ def symm_identical_atoms(gpname, atoms):
482495
newc = numpy.dot(coords, op)
483496
idx = argsort_coords(newc)
484497
if not numpy.allclose(coords0, newc[idx], atol=TOLERANCE):
485-
raise PointGroupSymmetryError('Symmetry identical atoms not found')
498+
raise PointGroupSymmetryError(
499+
'Symmetry identical atoms not found. This may be due to '
500+
'the strict setting of the threshold symm.geom.TOLERANCE. '
501+
'Consider adjusting the tolerance.')
502+
486503
dup_atom_ids.append(idx)
487504

488505
dup_atom_ids = numpy.sort(dup_atom_ids, axis=0).T
@@ -524,6 +541,14 @@ def check_symm(gpname, atoms, basis=None):
524541
opdic = symm_ops(gpname)
525542
ops = [opdic[op] for op in OPERATOR_TABLE[gpname]]
526543
rawsys = SymmSys(atoms, basis)
544+
545+
# A fast check using Casimir tensors
546+
coords = rawsys.atoms[:,1:]
547+
weights = rawsys.atoms[:,0]
548+
for op in ops:
549+
if not is_identical_geometry(coords, coords.dot(op), weights):
550+
return False
551+
527552
for lst in rawsys.atomtypes.values():
528553
coords = rawsys.atoms[lst,1:]
529554
idx = argsort_coords(coords)
@@ -543,6 +568,27 @@ def shift_atom(atoms, orig, axis):
543568
c = numpy.dot(c - orig, numpy.array(axis).T)
544569
return [[atoms[i][0], c[i]] for i in range(len(atoms))]
545570

571+
def is_identical_geometry(coords1, coords2, weights):
572+
'''A fast check to compare the geometry of two molecules using Casimir tensors'''
573+
if coords1.shape != coords2.shape:
574+
return False
575+
for order in range(1, 4):
576+
if abs(casimir_tensors(coords1, weights, order) -
577+
casimir_tensors(coords2, weights, order)).max() > TOLERANCE:
578+
return False
579+
return True
580+
581+
def casimir_tensors(r, q, order=1):
582+
if order == 1:
583+
return q.dot(r)
584+
elif order == 2:
585+
return numpy.einsum('i,ix,iy->xy', q, r, r)
586+
elif order == 3:
587+
return numpy.einsum('i,ix,iy,iz->xyz', q, r, r, r)
588+
else:
589+
raise NotImplementedError
590+
591+
546592
class RotationAxisNotFound(PointGroupSymmetryError):
547593
pass
548594

pyscf/symm/test/test_basis.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,11 @@ def test_symm_orb_c1(self):
141141
self.assertEqual(get_so(atoms,basis)[0], 220)
142142

143143
def test_symm_orb_c3v_as_cs(self):
144+
# This molecule has an approximate C3v symmetry for TOLERANCE>1e-5
145+
# When using its subgroup cs, a mirror perpedicular to [-.5, -.866, 0]
146+
# will be adopted as the mirror. This mirror leads to more errors in
147+
# atomic coordinates than the yz-mirror. In this case, one can adjust
148+
# TOLERANCE to pass the check_symm or symm_identical_atoms functions.
144149
atoms = [['Fe', ( 0.000000 , 0.000000 , 0.015198 )],
145150
['C', ( 0.000000 , 0.000000 , -1.938396)],
146151
['C', ( 0.000000 , -1.394127 , -1.614155)],
@@ -158,6 +163,10 @@ def test_symm_orb_c3v_as_cs(self):
158163
['O', ( 0.000000 , 2.572496 , 1.441607 )],
159164
['O', ( 2.227847 , -1.286248 , 1.441607 )],
160165
['O', ( -2.227847 , -1.286248 , 1.441607 )],]
166+
numpy.random.seed(2)
167+
u = numpy.linalg.svd(numpy.random.rand(3,3))[0]
168+
r = numpy.array([a[1] for a in atoms])
169+
atoms = [[a[0], x] for a, x in zip(atoms, r.dot(u))]
161170
basis = {'Fe':gto.basis.load('def2svp', 'C'),
162171
'C': gto.basis.load('def2svp', 'C'),
163172
'H': gto.basis.load('def2svp', 'C'),

pyscf/symm/test/test_geom.py

Lines changed: 10 additions & 0 deletions
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)