def _clean_site_offsets(site_offsets, atoms_coord, basis_vectors): """Check and convert `site_offsets` init argument.""" if atoms_coord is not None and site_offsets is not None: raise ValueError( "atoms_coord is deprecated and replaced by site_offsets, " "so both cannot be specified at the same time.") if atoms_coord is not None: warnings.warn( "atoms_coord is deprecated and may be removed in future versions, " "please use site_offsets instead", FutureWarning, ) site_offsets = atoms_coord if site_offsets is None: site_offsets = _np.zeros(basis_vectors.shape[0])[None, :] site_offsets = _np.asarray(site_offsets) fractional_coords = site_offsets @ _np.linalg.inv(basis_vectors) fractional_coords_int = comparable_periodic(fractional_coords) # Check for duplicates (also across unit cells) uniques, idx = _np.unique(fractional_coords_int, axis=0, return_index=True) if len(site_offsets) != len(uniques): site_offsets = site_offsets[idx] fractional_coords = fractional_coords[idx] fractional_coords_int = fractional_coords_int[idx] warnings.warn( "Some atom positions are not unique. Duplicates were dropped, and " f"now atom positions are {site_offsets}", UserWarning, ) # Check if any site is outside primitive cell (may cause KDTree to malfunction) if _np.any(fractional_coords_int < comparable(0.0)) or _np.any( fractional_coords_int > comparable(1.0)): warnings.warn( "Some sites were specified outside the primitive unit cell. This may" "cause errors in automatic edge finding.", UserWarning, ) return site_offsets, fractional_coords
def get_naive_edges(positions, cutoff, order): """ Given an array of spatial `positions`, returns a list `es`, so that `es[k]` contains all pairs of (k + 1)-nearest neighbors up to `order`. Only edges up to distance `cutoff` are considered. """ kdtree = cKDTree(positions) dist_matrix = kdtree.sparse_distance_matrix(kdtree, cutoff) row, col, dst = find(triu(dist_matrix)) dst = comparable(dst) _, ii = np.unique(dst, return_inverse=True) return [sorted(list(zip(row[ii == k], col[ii == k]))) for k in range(order)]
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 _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
def __eq__(self, other): if isinstance(other, PGSymmetry): return HashableArray(comparable(self._affine)) == HashableArray( comparable(other._affine)) else: return False
def __hash__(self): return hash(HashableArray(comparable(self._affine)))