Skip to content

Commit baced81

Browse files
committed
Skip unnecessary QR decompositions in make_unique_kpoint_irreps
1 parent 5a845c6 commit baced81

2 files changed

Lines changed: 163 additions & 68 deletions

File tree

python/libcasm/configuration/_ReciprocalSupercell.py

Lines changed: 162 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
get_all_subgroup_orbits,
88
get_cyclic_subgroup_orbits,
99
)
10-
from libcasm.irreps import IrrepDecomposition, IrrepInfo, make_invariant_subspace
10+
from libcasm.irreps import IrrepDecomposition, IrrepInfo
1111
from libcasm.xtal import (
1212
UnitCellIndexConverter,
1313
min_periodic_displacement,
@@ -222,6 +222,14 @@ def __init__(self, supercell: Supercell, tol: float = 1e-5):
222222
K-points are ordered as generated by :py:attr:`index_converter`.
223223
"""
224224

225+
self.neg_kpoint_index = []
226+
"""list[int | None]: For each k-point index i, the index of −k in
227+
`self.coordinates` (mod primitive reciprocal lattice), or None if not found.
228+
229+
For TRIM points where k ≡ −k (mod primitive reciprocal lattice),
230+
``neg_kpoint_index[i] == i``.
231+
"""
232+
225233
self.orbits = []
226234
"""list[list[int]]: Symmetry orbits of k-point indices under the supercell
227235
factor group.
@@ -232,8 +240,14 @@ def __init__(self, supercell: Supercell, tol: float = 1e-5):
232240
"""
233241

234242
self.equivalence_map = []
235-
"""list[list[libcasm.xtal.SymOp]: List of symmetry operations mapping the first
236-
k-point in each orbit to the other k-points in the orbit.
243+
"""list[list[libcasm.configuration.SupercellSymOp]]: List of SupercellSymOp
244+
operations mapping the first k-point in each orbit to the other k-points in
245+
the orbit.
246+
247+
Each element ``equivalence_map[i_orbit][j]`` is a factor group
248+
SupercellSymOp (``translation_index() == 0``) that maps
249+
``coordinates[:, orbits[i_orbit][0]]`` to
250+
``coordinates[:, orbits[i_orbit][j]]``.
237251
"""
238252

239253
self.little_groups = []
@@ -244,6 +258,16 @@ def __init__(self, supercell: Supercell, tol: float = 1e-5):
244258
supercell factor group that leave the k-point invariant.
245259
"""
246260

261+
self.orbit_independent_indices = []
262+
"""list[list[int]]: For each orbit, the positions j within the orbit whose
263+
real-valued plane wave subspace is independent of all earlier members.
264+
265+
Position 0 (the prototype) is always included. A later position j is excluded
266+
when ``coordinates[:, orbits[i_orbit][j]]`` is the negative (mod primitive
267+
reciprocal lattice) of an already-independent k-point in the same orbit,
268+
because the real-valued plane wave basis for k and −k spans the same subspace.
269+
"""
270+
247271
# build data
248272
self._build_kpoints()
249273
self._build_kpoint_orbits()
@@ -272,6 +296,15 @@ def _build_kpoints(self):
272296

273297
# Compute k-points by applying S_recip to the integer index columns
274298
self.coordinates = pretty(S_recip @ self.indices)
299+
300+
# For each k-point, find the index of its negative (mod primitive reciprocal
301+
# lattice), or None if not found
302+
n_kpts = self.coordinates.shape[1]
303+
neg_kpoint_index = []
304+
for i in range(n_kpts):
305+
idx = self._get_kpoint_index(-self.coordinates[:, i])
306+
neg_kpoint_index.append(idx if idx != -1 else None)
307+
self.neg_kpoint_index = neg_kpoint_index
275308
return
276309

277310
def _get_kpoint_index(self, kpt: np.array) -> int:
@@ -293,14 +326,12 @@ def _build_kpoint_orbits(self):
293326

294327
found = [False] * self.coordinates.shape[1]
295328
kpoint_orbits = []
296-
kpoint_equivalence_map = []
297329
elements = self.supercell.factor_group.elements
298330
for i in range(self.coordinates.shape[1]):
299331
if found[i]:
300332
continue
301333
kpt = self.coordinates[:, i]
302334
orbit = [i]
303-
equivalence_map = [elements[0].copy()]
304335
found[i] = True
305336
for op in elements:
306337
matrix = op.matrix()
@@ -312,46 +343,88 @@ def _build_kpoint_orbits(self):
312343
)
313344
if not found[kpt_index]:
314345
orbit.append(kpt_index)
315-
equivalence_map.append(op.copy())
316346
found[kpt_index] = True
317-
# Sort orbit and equivalence_map together by kpoint index
318-
sorted_pairs = sorted(zip(orbit, equivalence_map), key=lambda p: p[0])
319-
orbit, equivalence_map = map(list, zip(*sorted_pairs))
347+
orbit.sort()
320348
kpoint_orbits.append(orbit)
321-
kpoint_equivalence_map.append(equivalence_map)
322349
self.orbits = kpoint_orbits
323-
self.equivalence_map = kpoint_equivalence_map
324350
return
325351

326352
def _build_little_groups(self):
327-
"""Populate self.little_groups using symmetry operations of the supercell."""
353+
"""Populate self.little_groups and self.equivalence_map using symmetry
354+
operations of the supercell."""
328355
if self.coordinates is None:
329356
raise RuntimeError("coordinates must be built before computing orbits")
330357

331-
little_groups = []
332-
for i in range(self.coordinates.shape[1]):
333-
little_group = []
334-
335-
kpt = self.coordinates[:, i]
336-
337-
it = SupercellSymOp.begin(self.supercell)
338-
end = SupercellSymOp.end(self.supercell)
339-
while it != end:
340-
op = it.copy()
341-
matrix = op.to_symop().matrix()
342-
kpt_transformed = matrix @ kpt
343-
kpt_index = self._get_kpoint_index(kpt_transformed)
344-
345-
if kpt_index == -1:
358+
n_kpts = self.coordinates.shape[1]
359+
360+
# Build lookup tables from k-point index to orbit position
361+
orbit_index_of = {}
362+
position_in_orbit = {}
363+
for i_orbit, orbit in enumerate(self.orbits):
364+
for j, kpt_idx in enumerate(orbit):
365+
orbit_index_of[kpt_idx] = i_orbit
366+
position_in_orbit[kpt_idx] = j
367+
368+
prototype_indices = [orbit[0] for orbit in self.orbits]
369+
370+
little_groups = [[] for _ in range(n_kpts)]
371+
equivalence_map = [[None] * len(orbit) for orbit in self.orbits]
372+
373+
it = SupercellSymOp.begin(self.supercell)
374+
end = SupercellSymOp.end(self.supercell)
375+
while it != end:
376+
op = it.copy()
377+
matrix = op.to_symop().matrix()
378+
is_factor_group_op = op.translation_index() == 0
379+
380+
# Compute transformed k-point index for each initial k-point index
381+
kpt_transformed_indices = []
382+
for kpt_index_init in range(n_kpts):
383+
kpt = self.coordinates[:, kpt_index_init]
384+
kpt_index_final = self._get_kpoint_index(matrix @ kpt)
385+
if kpt_index_final == -1:
346386
raise ValueError(
347387
"Transformed k-point not found in list of k-points"
348388
)
349-
if kpt_index == i:
350-
little_group.append(op)
351-
it.next()
389+
kpt_transformed_indices.append(kpt_index_final)
390+
if kpt_index_final == kpt_index_init:
391+
little_groups[kpt_index_init].append(op)
392+
393+
# Use factor group ops to build equivalence_map (maps prototype k-point
394+
# in each orbit to each other k-point in the orbit)
395+
if is_factor_group_op:
396+
for i_orbit, proto_idx in enumerate(prototype_indices):
397+
dest_idx = kpt_transformed_indices[proto_idx]
398+
j_dest = position_in_orbit.get(dest_idx)
399+
if j_dest is not None and equivalence_map[i_orbit][j_dest] is None:
400+
equivalence_map[i_orbit][j_dest] = op
401+
402+
it.next()
352403

353-
little_groups.append(little_group)
354404
self.little_groups = little_groups
405+
self.equivalence_map = equivalence_map
406+
407+
# For each orbit, determine which positions j contribute an independent
408+
# real-valued plane wave subspace. Position j is dependent if k_j is the
409+
# negative of an already-independent k-point, because the real-valued cos/sin
410+
# basis for k and −k spans the same subspace.
411+
orbit_independent_indices = []
412+
for orbit in self.orbits:
413+
independent = [0]
414+
covered = {orbit[0]}
415+
neg = self.neg_kpoint_index[orbit[0]]
416+
if neg is not None:
417+
covered.add(neg)
418+
for j in range(1, len(orbit)):
419+
if orbit[j] in covered:
420+
continue
421+
independent.append(j)
422+
covered.add(orbit[j])
423+
neg = self.neg_kpoint_index[orbit[j]]
424+
if neg is not None:
425+
covered.add(neg)
426+
orbit_independent_indices.append(independent)
427+
self.orbit_independent_indices = orbit_independent_indices
355428

356429

357430
class SupercellDoF:
@@ -1141,14 +1214,10 @@ def make_unique_kpoint_irreps(
11411214
in the supercell, and combines the symmetry-adapted bases from each irrep
11421215
decomposition into a full symmetry-adapted DoF space for the supercell.
11431216
1144-
Note that an irrep subspaces is constructed for a single k-point in each orbit
1145-
and operations from the equivalence map are used to transform the subspace to the
1146-
subspaces for the other k-points in the orbit. Therefore, the axes of the combined
1147-
symmetry-adapted basis are not completely symmetrized as is obtained by
1148-
an irrep decomposition for DoFSpace.
1149-
1150-
these subspaces are not
1151-
fully symmetrized with respect to the symmetry of the full supercell.
1217+
Note that this method results in symmetry-adapted modes that are restricted to the
1218+
plane wave subspace for each k-point and do not mix with each other. This may
1219+
result in axes that are less symmetrized than if the symmetry-adapted modes were
1220+
constructed using the full supercell symmetry from the start.
11521221
11531222
Parameters
11541223
----------
@@ -1177,36 +1246,23 @@ def make_unique_kpoint_irreps(
11771246
# Get the full DoF space for the supercell
11781247
full_dof_space = supercell_dof.dof_space
11791248

1180-
# Make matrix representation of supercell factor group on full DoF space
1181-
factor_group = []
1182-
it = SupercellSymOp.begin(supercell_kpoints.supercell)
1183-
end = SupercellSymOp.end(supercell_kpoints.supercell)
1184-
while it != end:
1185-
if it.translation_index() == 0:
1186-
factor_group.append(it.copy())
1187-
it.next()
1188-
matrix_rep = make_dof_space_rep(
1189-
group=factor_group,
1190-
dof_space=full_dof_space,
1191-
)
1192-
11931249
## Iterate over k-point orbits to build irreps and symmetry-adapted bases ##
11941250

11951251
# Irreps generated from a single k-point in each orbit
11961252
kpoint_irreps: list[IrrepInfo] = []
11971253

1198-
# Bases generated by applying the supercell factor group to expand the symmetry
1199-
# adapted basis from a single k-point in each orbit to the full orbit. This
1200-
# will be combined at the end to form the full symmetry-adapted DoF space.
1254+
# Bases generated by applying the equivalence map to expand the symmetry-adapted
1255+
# basis from the prototype k-point in each orbit to the other k-points in the
1256+
# orbit. Combined at the end to form the full symmetry-adapted DoF space.
12011257
orbit_adapted_bases: list[np.ndarray] = []
12021258

12031259
# Information about each axis in the final symmetry-adapted basis
12041260
axis_irrep_info = []
12051261
next_irrep_char_index = 0
12061262

12071263
# For each k-point orbit, build irreps for the plane wave basis from the first
1208-
# k-point in the orbit, then apply the supercell factor group to extend the
1209-
# symmetry-adapted basis to the space for the full orbit.
1264+
# k-point in the orbit, then use the equivalence map to extend the
1265+
# symmetry-adapted basis to the other k-points in the orbit.
12101266
for i_orbit, orbit in enumerate(supercell_kpoints.orbits):
12111267
# Get irreps for first k-point in orbit
12121268
kpoint_index = orbit[0]
@@ -1220,19 +1276,49 @@ def make_unique_kpoint_irreps(
12201276
B = irrep_decomp.symmetry_adapted_subspace
12211277
B = pretty(B)
12221278

1279+
# Matrix reps for independent orbit members (skipping −k partners)
1280+
independent_indices = supercell_kpoints.orbit_independent_indices[i_orbit]
1281+
independent_ops = [
1282+
supercell_kpoints.equivalence_map[i_orbit][j] for j in independent_indices
1283+
]
1284+
equivalence_map_reps = make_dof_space_rep(
1285+
group=independent_ops,
1286+
dof_space=full_dof_space,
1287+
)
1288+
12231289
# For each irrep in the decomposition, extend basis to full orbit and
12241290
# store axis irrep info
12251291
i_axis = 0
12261292
for i_irrep, irrep in enumerate(irrep_decomp.irreps):
12271293

12281294
d = irrep.irrep_dim
12291295

1230-
# Apply supercell factor group to adapt basis to full symmetry
1296+
# Apply equivalence map ops for independent orbit members to generate
1297+
# orthogonal subspaces. −k partners are skipped because the real-valued
1298+
# plane wave basis for k and −k spans the same subspace.
12311299
kpoint_adapted_basis = B[:, i_axis : i_axis + d]
1232-
orbit_adapted_basis = make_invariant_subspace(
1233-
matrix_rep=matrix_rep,
1234-
init_subspace=kpoint_adapted_basis,
1235-
)
1300+
# Sanity check: columns of kpoint_adapted_basis should be orthonormal
1301+
inner = kpoint_adapted_basis.T @ kpoint_adapted_basis
1302+
if not np.allclose(inner, np.eye(d), atol=1e-8):
1303+
raise ValueError(
1304+
"Error in make_unique_kpoint_irreps: kpoint_adapted_basis "
1305+
"columns are not orthonormal."
1306+
)
1307+
orbit_cols = [kpoint_adapted_basis]
1308+
for D in equivalence_map_reps[1:]:
1309+
transformed = D @ kpoint_adapted_basis
1310+
# Sanity check: transformed subspace should be orthogonal to prototype
1311+
overlap = kpoint_adapted_basis.T @ transformed
1312+
if not np.allclose(overlap, np.zeros_like(overlap), atol=1e-8):
1313+
raise ValueError(
1314+
"Error in make_unique_kpoint_irreps: equivalence map "
1315+
"transformed subspace is not orthogonal to prototype subspace."
1316+
)
1317+
orbit_cols.append(transformed)
1318+
orbit_adapted_basis = np.hstack(orbit_cols)
1319+
# q, r = np.linalg.qr(orbit_adapted_basis)
1320+
# rank = np.linalg.matrix_rank(r)
1321+
# orbit_adapted_basis = pretty(q[:, :rank])
12361322

12371323
orbit_adapted_bases.append(orbit_adapted_basis)
12381324

@@ -1258,11 +1344,20 @@ def make_unique_kpoint_irreps(
12581344
kpoint_irreps.append(irrep)
12591345
i_axis += d
12601346

1261-
# Combine all symmetry-adapted bases from each irrep decomposition
1262-
symmetry_adapted_basis = np.hstack(orbit_adapted_bases)
1263-
q, r = np.linalg.qr(symmetry_adapted_basis)
1264-
rank = np.linalg.matrix_rank(r)
1265-
symmetry_adapted_basis = pretty(q[:, :rank])
1347+
# Combine all symmetry-adapted bases from each irrep decomposition. Each
1348+
# orbit-adapted basis should be orthogonal to all previously accumulated bases.
1349+
symmetry_adapted_basis = orbit_adapted_bases[0]
1350+
for basis in orbit_adapted_bases[1:]:
1351+
overlap = symmetry_adapted_basis.T @ basis
1352+
if not np.allclose(overlap, np.zeros_like(overlap), atol=1e-8):
1353+
raise ValueError(
1354+
"Error in make_unique_kpoint_irreps: orbit-adapted basis is not "
1355+
"orthogonal to previously accumulated symmetry-adapted basis."
1356+
)
1357+
symmetry_adapted_basis = np.hstack([symmetry_adapted_basis, basis])
1358+
# q, r = np.linalg.qr(symmetry_adapted_basis)
1359+
# rank = np.linalg.matrix_rank(r)
1360+
# symmetry_adapted_basis = pretty(q[:, :rank])
12661361

12671362
dof_space = DoFSpace(
12681363
dof_key=supercell_dof.dof_key,

python/tests/configuration/test_SupercellKpoints.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ def test_make_unique_kpoint_irreps(self, sc_kpts, sc_dof):
146146

147147
def test_axis_irrep_info(self, sc_kpts, sc_dof):
148148
"""Each axis_irrep_info entry has valid orbit_index and kpoint_irreps_index."""
149-
_, kpoint_irreps, axis_irrep_info = casmconfig.make_unique_kpoint_irreps(
149+
kpoint_irreps, _, axis_irrep_info = casmconfig.make_unique_kpoint_irreps(
150150
supercell_kpoints=sc_kpts,
151151
supercell_dof=sc_dof,
152152
)

0 commit comments

Comments
 (0)