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
1111from 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
357430class 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 ,
0 commit comments