Skip to content

Commit 330cfe1

Browse files
committed
simplify hkl counting
1 parent 8d3c85d commit 330cfe1

File tree

2 files changed

+171
-7
lines changed

2 files changed

+171
-7
lines changed

pyxtal/symmetry.py

Lines changed: 145 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -793,6 +793,28 @@ def generate_possible_hkls(self, h_max, k_max=None, l_max=None, max_square=12):
793793
# Generate all possible hkls and filter by extinction rules
794794
possible_hkls = []
795795
canonical_seen = set() # Track canonical forms to avoid duplicates
796+
symmetry_seen = set() # Track symmetry-equivalent hkls
797+
798+
# Build reciprocal-space rotation operators from the general Wyckoff position
799+
reciprocal_ops = []
800+
op_seen = set()
801+
if len(self.wyckoffs) > 0 and len(self.wyckoffs[0]) > 0:
802+
for op in self.wyckoffs[0]:
803+
try:
804+
matrix = np.rint(np.linalg.inv(op.rotation_matrix).T).astype(int)
805+
except np.linalg.LinAlgError:
806+
continue
807+
key = tuple(matrix.flatten().tolist())
808+
if key not in op_seen:
809+
op_seen.add(key)
810+
reciprocal_ops.append(matrix)
811+
if len(reciprocal_ops) == 0:
812+
reciprocal_ops = [np.eye(3, dtype=int)]
813+
814+
def get_symmetry_key(hkl):
815+
vec = np.array(hkl, dtype=int)
816+
orbit = [tuple((matrix @ vec).tolist()) for matrix in reciprocal_ops]
817+
return max(orbit)
796818

797819
for h in range(0, h_max + 1):
798820
# add permutation
@@ -821,7 +843,11 @@ def generate_possible_hkls(self, h_max, k_max=None, l_max=None, max_square=12):
821843
#print('AAAAAAAAAAAAAAAAAAA', h, k, l, sh, sk, sl)
822844
if valid_hkls:
823845
canonical_seen.add(canonical)
824-
possible_hkls.extend(valid_hkls)
846+
for hkl in valid_hkls:
847+
symmetry_key = get_symmetry_key(hkl)
848+
if symmetry_key not in symmetry_seen:
849+
symmetry_seen.add(symmetry_key)
850+
possible_hkls.append(hkl)
825851

826852
# Sort by h²+k²+l² in ascending order
827853
possible_hkls.sort(key=lambda hkl: hkl[0]**2 + hkl[1]**2 + hkl[2]**2)
@@ -961,14 +987,14 @@ def reduce_hkl_guesses(self, hkls):
961987
# must follow the ordering constraints
962988
mask1 = np.all(hkls[:,0,:] >= hkls[:,1,:], axis=1) # (h1, k1, l1) >= (h2, k2, l2)
963989
hkls = hkls[~mask1]
964-
print("Reducing order", len(hkls), "hkl guesses for space group", self.number)
990+
#print("Reducing order", len(hkls), "hkl guesses for space group", self.number)
965991

966992
# must be non-coplanar
967993
B = np.zeros([len(hkls), 2, 2])
968994
B[:,:,0] = hkls[:,:,0] ** 2 + hkls[:,:,1] ** 2
969995
B[:,:,1] = hkls[:,:,2] ** 2
970996
hkls = hkls[np.linalg.det(B) != 0]
971-
print("Reducing colinear", len(hkls), "hkl guesses for space group", self.number)
997+
# print("Reducing colinear", len(hkls), "hkl guesses for space group", self.number)
972998

973999
elif 15 < self.number < 75:
9741000
# must follow the ordering constraints
@@ -5017,13 +5043,33 @@ def get_canonical_hkl(h, k, l, spg):
50175043
"""
50185044
hkl = [abs(h), abs(k), abs(l)] # Take absolute values first
50195045

5046+
def canonical_hex_hkl(h, k, l):
5047+
"""Canonicalize hkl for hexagonal systems using 6-fold in-plane symmetry."""
5048+
candidates = [
5049+
(h, k, l),
5050+
(-k, h + k, l),
5051+
(-h - k, h, l),
5052+
(-h, -k, l),
5053+
(k, -h - k, l),
5054+
(h + k, -h, l),
5055+
]
5056+
candidates = [tuple(abs(x) for x in c) for c in candidates]
5057+
return max(candidates)
5058+
50205059
if spg >= 195: # cubic
50215060
# For cubic: sort in descending order
50225061
# (2,2,0), (2,0,2), (0,2,2) all become (2,2,0)
50235062
hkl.sort(reverse=True)
50245063
return tuple(hkl)
50255064

5026-
elif spg >= 75: # tetragonal/hexagonal
5065+
elif spg >= 168: # hexagonal
5066+
return canonical_hex_hkl(h, k, abs(l))
5067+
5068+
elif spg >= 143: # trigonal
5069+
h_sorted = sorted([hkl[0], hkl[1]], reverse=True)
5070+
return tuple([h_sorted[0], h_sorted[1], hkl[2]])
5071+
5072+
elif spg >= 75: # tetragonal
50275073
# For tetragonal: h and k are equivalent, l is unique
50285074
# (2,1,3) and (1,2,3) are equivalent -> (2,1,3)
50295075
h_sorted = sorted([hkl[0], hkl[1]], reverse=True)
@@ -5048,6 +5094,19 @@ def get_canonical_hkl_series(hkl_series, spg):
50485094
"""
50495095
from itertools import permutations
50505096

5097+
def canonical_hex_hkl(h, k, l):
5098+
"""Canonicalize hkl for hexagonal systems using 6-fold in-plane symmetry."""
5099+
candidates = [
5100+
(h, k, l),
5101+
(-k, h + k, l),
5102+
(-h - k, h, l),
5103+
(-h, -k, l),
5104+
(k, -h - k, l),
5105+
(h + k, -h, l),
5106+
]
5107+
candidates = [tuple(abs(x) for x in c) for c in candidates]
5108+
return max(candidates)
5109+
50515110
def apply_permutation_to_series(hkl_series, perm):
50525111
"""Apply the same permutation to all hkls in the series"""
50535112
return [tuple(abs(hkl[i]) for i in perm) for hkl in hkl_series]
@@ -5074,7 +5133,38 @@ def apply_permutation_to_series(hkl_series, perm):
50745133

50755134
return tuple(best_canonical)
50765135

5077-
elif spg >= 75: # tetragonal/hexagonal - h and k equivalent, l unique
5136+
elif spg >= 168: # hexagonal - 6-fold in-plane symmetry, l unique
5137+
canonical_series = [canonical_hex_hkl(h, k, l) for h, k, l in hkl_series]
5138+
return tuple(canonical_series)
5139+
5140+
elif spg >= 143: # trigonal - h and k equivalent, l unique
5141+
best_canonical = None
5142+
best_score = None
5143+
5144+
# Try permutations that maintain crystallographic meaning
5145+
perms_to_try = [(0, 1, 2), (1, 0, 2)] # Keep l in position, swap h,k
5146+
5147+
for perm in perms_to_try:
5148+
# Apply the same permutation to the entire series
5149+
canonical_series = apply_permutation_to_series(hkl_series, perm)
5150+
5151+
# For trigonal: sort h,k but keep l separate
5152+
canonical_series = [
5153+
tuple([*sorted([hkl[0], hkl[1]], reverse=True), hkl[2]])
5154+
for hkl in canonical_series
5155+
]
5156+
5157+
# Score the result
5158+
score = sum(sum(h * (10**(3-i)) for i, h in enumerate(hkl))
5159+
for hkl in canonical_series)
5160+
5161+
if best_score is None or score > best_score:
5162+
best_score = score
5163+
best_canonical = canonical_series
5164+
5165+
return tuple(best_canonical)
5166+
5167+
elif spg >= 75: # tetragonal - h and k equivalent, l unique
50785168
best_canonical = None
50795169
best_score = None
50805170

@@ -5296,7 +5386,56 @@ def generate_possible_hkls(bravais, h_max=50, k_max=50, l_max=50):
52965386
hkls_with_signs = np.column_stack([sh, sk, sl])
52975387
all_hkls.append(hkls_with_signs)
52985388

5299-
return np.vstack(all_hkls)
5389+
all_hkls = np.vstack(all_hkls)
5390+
5391+
# Remove symmetry-equivalent hkls using representative space groups
5392+
bravais_to_spg = {
5393+
1: 1, # triclinic P
5394+
2: 3, # monoclinic P
5395+
3: 5, # monoclinic C
5396+
4: 16, # orthorhombic P
5397+
5: 16, # orthorhombic A
5398+
6: 16, # orthorhombic C
5399+
7: 16, # orthorhombic I
5400+
8: 16, # orthorhombic F
5401+
9: 75, # tetragonal P
5402+
10: 79, # tetragonal I
5403+
11: 168, # hexagonal P
5404+
12: 143, # rhombohedral/trigonal R
5405+
13: 195, # cubic P
5406+
14: 197, # cubic I
5407+
15: 196, # cubic F
5408+
}
5409+
spg = bravais_to_spg[bravais]
5410+
5411+
# Build reciprocal-space rotation operators from a representative space group
5412+
reciprocal_ops = []
5413+
op_seen = set()
5414+
group = Group(spg)
5415+
if len(group.wyckoffs) > 0 and len(group.wyckoffs[0]) > 0:
5416+
for op in group.wyckoffs[0]:
5417+
try:
5418+
matrix = np.rint(np.linalg.inv(op.rotation_matrix).T).astype(int)
5419+
except np.linalg.LinAlgError:
5420+
continue
5421+
key = tuple(matrix.flatten().tolist())
5422+
if key not in op_seen:
5423+
op_seen.add(key)
5424+
reciprocal_ops.append(matrix)
5425+
if len(reciprocal_ops) == 0:
5426+
reciprocal_ops = [np.eye(3, dtype=int)]
5427+
5428+
unique_hkls = []
5429+
seen = set()
5430+
for h, k, l in all_hkls:
5431+
vec = np.array([int(h), int(k), int(l)], dtype=int)
5432+
orbit = [tuple((matrix @ vec).tolist()) for matrix in reciprocal_ops]
5433+
symmetry_key = max(orbit)
5434+
if symmetry_key not in seen:
5435+
seen.add(symmetry_key)
5436+
unique_hkls.append(tuple(vec.tolist()))
5437+
5438+
return np.array(unique_hkls, dtype=int)
53005439

53015440

53025441
if __name__ == "__main__":

tests/test_symmetry.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import unittest
55

66
from pyxtal.lattice import Lattice
7-
from pyxtal.symmetry import Group, Wyckoff_position
7+
from pyxtal.symmetry import Group, Wyckoff_position, get_canonical_hkl, get_canonical_hkl_series
88

99
def resource_filename(package_name, resource_path):
1010
package_path = importlib.util.find_spec(package_name).submodule_search_locations[0]
@@ -18,6 +18,31 @@ def resource_filename(package_name, resource_path):
1818

1919

2020
class TestSymmetry(unittest.TestCase):
21+
def test_hexagonal_canonical_hkl_equivalence(self):
22+
# Hexagonal space groups: 168-194
23+
for spg in [168, 194]:
24+
# (100), (010), and (1-10) are symmetry-equivalent in hexagonal
25+
c1 = get_canonical_hkl(1, 0, 0, spg)
26+
c2 = get_canonical_hkl(0, 1, 0, spg)
27+
c3 = get_canonical_hkl(1, -1, 0, spg)
28+
assert c1 == c2 == c3
29+
30+
# General hkl pair related by a 60-degree in-plane rotation
31+
c4 = get_canonical_hkl(2, 1, 3, spg)
32+
c5 = get_canonical_hkl(-1, 3, 3, spg)
33+
assert c4 == c5
34+
35+
def test_hexagonal_canonical_hkl_series_equivalence(self):
36+
spg = 194
37+
38+
# Two equivalent series under hexagonal in-plane rotations
39+
s1 = [(1, 0, 0), (2, 1, 3)]
40+
s2 = [(0, 1, 0), (-1, 3, 3)]
41+
42+
c1 = get_canonical_hkl_series(s1, spg)
43+
c2 = get_canonical_hkl_series(s2, spg)
44+
assert c1 == c2
45+
2146
def test_from_symops_wo_grou(self):
2247
data = [
2348
(["x, y, z", "-x, y+1/2, -z"], 4, 6),

0 commit comments

Comments
 (0)