def character_table_by_class(self) -> Array: r""" Calculates the character table using Burnside's algorithm. Each row of the output lists the characters of one irrep in the order the conjugacy classes are listed in `self.conjugacy_classes`. Assumes that `Identity() == self[0]`, if not, the sign of some characters may be flipped. The irreps are sorted by dimension. """ classes, _, _ = self.conjugacy_classes class_sizes = classes.sum(axis=1) # Construct a random linear combination of the class matrices c_S # (c_S)_{RT} = #{r,s: r \in R, s \in S: rs = t} # for conjugacy classes R,S,T, and a fixed t \in T. # # From our oblique times table it is easier to calculate # (d_S)_{RT} = #{r,t: r \in R, t \in T: rs = t} # for a fixed s \in S. This is just `product_table == s`, aggregrated # over conjugacy classes. c_S and d_S are related by # c_{RST} = |S| d_{RST} / |T|; # since we only want a random linear combination, we forget about the # constant |S| and only divide each column through with the appropriate |T| class_matrix = (classes @ np.random.uniform( size=len(self))[self.product_table] @ classes.T) class_matrix /= class_sizes # The vectors |R|\chi(r) are (column) eigenvectors of all class matrices # the random linear combination ensures (with probability 1) that # none of them are degenerate _, table = np.linalg.eig(class_matrix) table = table.T / class_sizes # Normalise the eigenvectors by orthogonality: \sum_g |\chi(g)|^2 = |G| norm = np.sum(np.abs(table)**2 * class_sizes, axis=1, keepdims=True)**0.5 table /= norm table /= _cplx_sign(table[:, 0])[:, np.newaxis] # ensure correct sign table *= len(self)**0.5 # Sort lexicographically, ascending by first column, descending by others sorting_table = np.column_stack((table.real, table.imag)) sorting_table[:, 1:] *= -1 sorting_table = comparable(sorting_table) _, indices = np.unique(sorting_table, axis=0, return_index=True) table = table[indices] # Get rid of annoying nearly-zero entries table = prune_zeros(table) return table
def space_group_irreps(self, *k: Array) -> Array: """ Returns the portion of the character table of the full space group corresponding to the star of the wave vector *k*. Arguments: k: the wave vector in Cartesian axes Returns: An array `CT` listing the characters for a number of irreps of the space group. `CT[i]` for each `i` gives a distinct irrep, each corresponding to `self.little_group(k).character_table[i]. `CT[i,j]` gives the character of `self.space_group[j]` in the same. """ k = _ensure_iterable(k) # Wave vectors big_star_Cart = np.tensordot(self.point_group_.matrices(), k, axes=1) big_star = self.lattice.to_reciprocal_lattice(big_star_Cart) * ( 2 * pi / self.lattice.extent) # Little-group-irrep factors # Conjugacy_table[g,p] lists p^{-1}gp, so point_group_factors[i,:,p] # of irrep #i for the little group of p(k) is the equivalent # Phase factor for non-symmorphic symmetries is exp(-i w_g . p(k)) point_group_factors = self._little_group_irreps( k, divide=True )[:, self.point_group_.conjugacy_table] * np.exp(-1j * np.tensordot( self.point_group_.translations(), big_star_Cart, axes=(-1, -1))) # Translational factors trans_factors = [] for axis in range(self.lattice.ndim): n_trans = self.lattice.extent[axis] if self.lattice.pbc[axis] else 1 factors = np.exp(-1j * np.outer(np.arange(n_trans), big_star[:, axis])) shape = ([1] * axis + [n_trans] + [1] * (self.lattice.ndim - 1 - axis) + [len(self.point_group_)]) trans_factors.append(factors.reshape(shape)) trans_factors = reduce(np.multiply, trans_factors).reshape(-1, len(self.point_group_)) # Multiply the factors together and sum over the "p" PGSymmetry axis # Translations are more major than point group operations result = np.einsum("igp, tp -> itg", point_group_factors, trans_factors).reshape(point_group_factors.shape[0], -1) return prune_zeros(result)
def _irrep_matrices(self) -> PyTree: random = np.random.standard_normal # Generate the eigensystem of a random linear combination of matrices # in the group's regular representation (namely, product_table == g) # The regular representation contains d copies of each d-dimensional irrep # all of which return the same eigenvalues with eigenvectors that # correspond to one another in the bases of the several irrep copies e, vs = np.linalg.eig(random(len(self))[self.product_table]) e = np.stack((e.real, e.imag)) # we only need one eigenvector per eigenvalue _, idx = np.unique(comparable(e), return_index=True, axis=1) vs = vs[:, idx] # We will nedd the true product table as well true_product_table = self.product_table[self.inverse] irreps = [] for chi in self.character_table(): # Check which eigenvectors belong to this irrep # the last argument of einsum is the regular projector on this irrep proj = np.einsum("gi,hi,gh->i", vs.conj(), vs, chi.conj()[self.product_table]) proj = np.logical_not(np.isclose(proj, 0.0)) # Pick the first eigenvector in this irrep idx = np.arange(vs.shape[1], dtype=int)[proj][0] v = vs[:, idx] # Generate an orthonormal basis for the irrep spanned by A_reg(g).v # for all matrices in the regular representation # A basis follows (with probability 1) as d random linear # combinations of these vectors; make them orthonormal using QR # NB v[product_table] generates a matrix whose columns are A_reg(h).v dim = int(np.rint(chi[0].real)) w, _ = np.linalg.qr(v[true_product_table] @ random( (len(self), dim))) # Project the regular representation on this basis irreps.append( prune_zeros( np.einsum("gi,ghj ->hij", w.conj(), w[true_product_table, :]))) return irreps